education verify redirection

This commit is contained in:
JzoNg 2025-03-15 14:32:50 +08:00
parent 9f673dab0d
commit 05e25096ba
11 changed files with 103 additions and 35 deletions

View File

@ -14,6 +14,7 @@ import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { updateUserProfile } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import { IS_CE_EDITION } from '@/config'
@ -33,6 +34,7 @@ export default function AccountPage() {
const { t } = useTranslation()
const { systemFeatures } = useAppContext()
const { mutateUserProfile, userProfile, apps } = useAppContext()
const { isEducationAccount } = useProviderContext()
const { notify } = useContext(ToastContext)
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
const [editName, setEditName] = useState('')
@ -140,10 +142,12 @@ export default function AccountPage() {
<div className='ml-4'>
<p className='system-xl-semibold text-text-primary'>
{userProfile.name}
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='w-3 h-3 mr-1' />
<span className='system-2xs-medium'>EDU</span>
</PremiumBadge>
{isEducationAccount && (
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='w-3 h-3 mr-1' />
<span className='system-2xs-medium'>EDU</span>
</PremiumBadge>
)}
</p>
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
</div>

View File

@ -9,6 +9,7 @@ import { Menu, Transition } from '@headlessui/react'
import Avatar from '@/app/components/base/avatar'
import { logout } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
import PremiumBadge from '@/app/components/base/premium-badge'
@ -20,6 +21,7 @@ export default function AppSelector() {
const router = useRouter()
const { t } = useTranslation()
const { userProfile } = useAppContext()
const { isEducationAccount } = useProviderContext()
const handleLogout = async () => {
await logout({
@ -74,10 +76,12 @@ export default function AppSelector() {
<div className='grow'>
<div className='system-md-medium text-text-primary break-all'>
{userProfile.name}
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='w-3 h-3 mr-1' />
<span className='system-2xs-medium'>EDU</span>
</PremiumBadge>
{isEducationAccount && (
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='w-3 h-3 mr-1' />
<span className='system-2xs-medium'>EDU</span>
</PremiumBadge>
)}
</div>
<div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
</div>

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import {
RiBook2Line,
RiBox3Line,
@ -29,8 +30,9 @@ const PlanComp: FC<Props> = ({
loc,
}) => {
const { t } = useTranslation()
const router = useRouter()
const { userProfile } = useAppContext()
const { plan } = useProviderContext()
const { plan, enableEducationPlan, isEducationAccount } = useProviderContext()
const {
type,
} = plan
@ -41,6 +43,12 @@ const PlanComp: FC<Props> = ({
} = plan
const [showModal, setShowModal] = React.useState(false)
const handleVerify = () => {
if (userProfile.email.endsWith('.edu'))
router.push('/education-apply')
else
setShowModal(true)
}
return (
<div className='bg-background-section-burn rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off'>
<div className='p-6 pb-2'>
@ -65,12 +73,12 @@ const PlanComp: FC<Props> = ({
<div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div>
</div>
<div className='shrink-0 flex items-center gap-1'>
{/* {(plan.type === Plan.sandbox || plan.type === Plan.professional) && ( */}
<Button variant='ghost' onClick={() => setShowModal(true)}>
<RiGraduationCapLine className='w-4 h-4 mr-1'/>
{t('education.toVerified')}
</Button>
{/* )} */}
{enableEducationPlan && !isEducationAccount && (
<Button variant='ghost' onClick={handleVerify}>
<RiGraduationCapLine className='w-4 h-4 mr-1'/>
{t('education.toVerified')}
</Button>
)}
{(plan.type as any) !== SelfHostedPlan.enterprise && (
<UpgradeBtn
className='shrink-0'

View File

@ -86,6 +86,10 @@ export type CurrentPlanInfoBackend = {
can_replace_logo: boolean
model_load_balancing_enabled: boolean
dataset_operator_enabled: boolean
education: {
enabled: boolean
activated: boolean
}
}
export type SubscriptionItem = {

View File

@ -27,6 +27,7 @@ import I18n from '@/context/i18n'
import Avatar from '@/app/components/base/avatar'
import { logout } from '@/service/common'
import AppContext, { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import { LanguagesSupported } from '@/i18n/language'
import { LicenseStatus } from '@/types/feature'
@ -45,6 +46,7 @@ export default function AppSelector() {
const { locale } = useContext(I18n)
const { t } = useTranslation()
const { userProfile, langeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
const { isEducationAccount } = useProviderContext()
const { setShowAccountSettingModal } = useModalContext()
const handleLogout = async () => {
@ -90,10 +92,12 @@ export default function AppSelector() {
<div className='grow'>
<div className='system-md-medium text-text-primary break-all'>
{userProfile.name}
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='w-3 h-3 mr-1' />
<span className='system-2xs-medium'>EDU</span>
</PremiumBadge>
{isEducationAccount && (
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='w-3 h-3 mr-1' />
<span className='system-2xs-medium'>EDU</span>
</PremiumBadge>
)}
</div>
<div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
</div>

View File

@ -68,7 +68,7 @@ const Header = () => {
<WorkspaceProvider>
<WorkplaceSelector />
</WorkspaceProvider>
{enableBilling ? <PlanBadge size='s' allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
{enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
</div>
</div>
}
@ -79,7 +79,7 @@ const Header = () => {
<LogoSite />
</Link>
<div className='font-light text-divider-deep'>/</div>
{enableBilling ? <PlanBadge size='s' allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
{enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}
</div >
)}
{

View File

@ -1,9 +1,9 @@
import { useProviderContext } from '@/context/provider-context'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
// import {
// RiGraduationCapFill,
// } from '@remixicon/react'
import {
RiGraduationCapFill,
} from '@remixicon/react'
import { SparklesSoft } from '../../base/icons/src/public/common'
import PremiumBadge from '../../base/premium-badge'
import { Plan } from '../../billing/type'
@ -16,7 +16,7 @@ type PlanBadgeProps = {
}
const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, sandboxAsUpgrade = false, onClick }) => {
const { isFetchedPlan } = useProviderContext()
const { isFetchedPlan, isEducationWorkspace } = useProviderContext()
const { t } = useTranslation()
if (!isFetchedPlan) return null
@ -42,7 +42,8 @@ const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, sandboxAsUpgrade = fa
if (plan === Plan.professional) {
return <PremiumBadge className='select-none' size='s' color='blue' allowHover={allowHover} onClick={onClick}>
<div className='system-2xs-medium-uppercase'>
<span className='p-1'>
<span className='p-1 inline-flex items-center gap-1'>
{isEducationWorkspace && <RiGraduationCapFill className='w-3 h-3' />}
pro
</span>
</div>

View File

@ -13,7 +13,9 @@ import Checkbox from '@/app/components/base/checkbox'
import {
useEducationAdd,
useEducationVerify,
useInvalidateEducationStatus,
} from '@/service/use-education'
import { useProviderContext } from '@/context/provider-context'
const EducationApplyAge = () => {
const { t } = useTranslation()
@ -25,8 +27,16 @@ const EducationApplyAge = () => {
isPending,
mutateAsync: educationAdd,
} = useEducationAdd({ onSuccess: () => {} })
const [modalShow, setShowModal] = useState<undefined | { title: string; desc: string }>(undefined)
const [modalShow, setShowModal] = useState<undefined | { title: string; desc: string; onConfirm?: () => void }>(undefined)
const { data } = useEducationVerify()
const { onPlanInfoChanged } = useProviderContext()
const updateEducationStatus = useInvalidateEducationStatus()
const handleModalConfirm = () => {
setShowModal(undefined)
onPlanInfoChanged()
updateEducationStatus()
}
const handleSubmit = () => {
educationAdd({
@ -38,6 +48,7 @@ const EducationApplyAge = () => {
setShowModal({
title: t('education.successTitle'),
desc: t('education.successContent'),
onConfirm: handleModalConfirm,
})
}
else {
@ -139,7 +150,7 @@ const EducationApplyAge = () => {
isShow={!!modalShow}
title={modalShow?.title || ''}
content={modalShow?.desc}
onConfirm={() => setShowModal(undefined)}
onConfirm={modalShow?.onConfirm || (() => {})}
onCancel={() => setShowModal(undefined)}
/>
</div>

View File

@ -6,16 +6,14 @@ import {
} from 'react'
import { useRouter } from 'next/navigation'
import EducationApplyAge from './components/education-apply-page'
import { IS_CE_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { LicenseStatus } from '@/types/feature'
import { useProviderContext } from '@/context/provider-context'
export default function EducationApply() {
const router = useRouter()
const { systemFeatures } = useAppContext()
const { enableEducationPlan, isEducationAccount } = useProviderContext()
const hiddenEducationApply = useMemo(() => {
return IS_CE_EDITION || (systemFeatures.license.status !== LicenseStatus.NONE)
}, [systemFeatures.license.status])
return enableEducationPlan && isEducationAccount
}, [enableEducationPlan, isEducationAccount])
useEffect(() => {
if (hiddenEducationApply)

View File

@ -22,6 +22,9 @@ import { fetchCurrentPlanInfo } from '@/service/billing'
import { parseCurrentPlan } from '@/app/components/billing/utils'
import { defaultPlan } from '@/app/components/billing/config'
import Toast from '@/app/components/base/toast'
import {
useEducationStatus,
} from '@/service/use-education'
type ProviderContextState = {
modelProviders: ModelProvider[]
@ -40,6 +43,9 @@ type ProviderContextState = {
enableReplaceWebAppLogo: boolean
modelLoadBalancingEnabled: boolean
datasetOperatorEnabled: boolean
enableEducationPlan: boolean
isEducationWorkspace: boolean
isEducationAccount: boolean
}
const ProviderContext = createContext<ProviderContextState>({
modelProviders: [],
@ -70,6 +76,9 @@ const ProviderContext = createContext<ProviderContextState>({
enableReplaceWebAppLogo: false,
modelLoadBalancingEnabled: false,
datasetOperatorEnabled: false,
enableEducationPlan: false,
isEducationWorkspace: false,
isEducationAccount: false,
})
export const useProviderContext = () => useContext(ProviderContext)
@ -97,13 +106,19 @@ export const ProviderContextProvider = ({
const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false)
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
const { data: isEducationAccount } = useEducationStatus(!enableEducationPlan)
const fetchPlan = async () => {
const data = await fetchCurrentPlanInfo()
const enabled = data.billing.enabled
setEnableBilling(enabled)
setEnableEducationPlan(data.education.enabled)
setIsEducationWorkspace(data.education.activated)
setEnableReplaceWebAppLogo(data.can_replace_logo)
if (enabled) {
setPlan(parseCurrentPlan(data))
setPlan(parseCurrentPlan(data) as any)
setIsFetchedPlan(true)
}
if (data.model_load_balancing_enabled)
@ -155,6 +170,9 @@ export const ProviderContextProvider = ({
enableReplaceWebAppLogo,
modelLoadBalancingEnabled,
datasetOperatorEnabled,
enableEducationPlan,
isEducationWorkspace,
isEducationAccount: isEducationAccount?.result || false,
}}>
{children}
</ProviderContext.Provider>

View File

@ -3,6 +3,7 @@ import {
useMutation,
useQuery,
} from '@tanstack/react-query'
import { useInvalid } from './use-base'
import type { EducationAddParams } from '@/app/education-apply/components/types'
const NAME_SPACE = 'education'
@ -50,3 +51,18 @@ export const useEducationAutocomplete = () => {
},
})
}
export const useEducationStatus = (disable?: boolean) => {
return useQuery({
enabled: !disable,
queryKey: [NAME_SPACE, 'education-status'],
queryFn: () => {
return get<{ result: boolean }>('/account/education')
},
retry: false,
})
}
export const useInvalidateEducationStatus = () => {
return useInvalid([NAME_SPACE, 'education-status'])
}