feat: notice of the expire of education verify (#24210)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Joel
2025-08-20 15:37:46 +08:00
committed by GitHub
parent 870e3daa95
commit ddf05ca059
20 changed files with 400 additions and 11 deletions

View File

@@ -13,12 +13,12 @@ import { useProviderContext } from '@/context/provider-context'
export default function EducationApply() {
const router = useRouter()
const { enableEducationPlan, isEducationAccount } = useProviderContext()
const { enableEducationPlan } = useProviderContext()
const searchParams = useSearchParams()
const token = searchParams.get('token')
const showEducationApplyPage = useMemo(() => {
return enableEducationPlan && !isEducationAccount && token
}, [enableEducationPlan, isEducationAccount, token])
return enableEducationPlan && token
}, [enableEducationPlan, token])
useEffect(() => {
if (!showEducationApplyPage)

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M12.5674 1.56341C12.5532 1.43246 12.4469 1.33346 12.3203 1.33333C12.1938 1.3332 12.0873 1.43196 12.0728 1.56288C12.0053 2.1724 11.8316 2.59056 11.5593 2.87418C11.287 3.1578 10.8856 3.33881 10.3004 3.40911C10.1747 3.42421 10.08 3.53514 10.0801 3.66693C10.0802 3.79872 10.1752 3.90944 10.3009 3.92427C10.8762 3.99215 11.2868 4.17312 11.566 4.45869C11.8437 4.74271 12.0207 5.16027 12.0721 5.76368C12.0836 5.89756 12.1913 6.00015 12.3203 6C12.4494 5.99984 12.5569 5.897 12.568 5.7631C12.6174 5.16988 12.7943 4.74291 13.0737 4.45176C13.3533 4.1606 13.7632 3.9763 14.3326 3.92496C14.4612 3.91336 14.56 3.80136 14.5601 3.66696C14.5602 3.53255 14.4617 3.42032 14.3332 3.40842C13.7539 3.35482 13.3531 3.17038 13.0804 2.88113C12.8063 2.5903 12.6325 2.16262 12.5674 1.56341Z" fill="#155AEF"/>
<path d="M8.15567 3.25831C8.11906 2.92157 7.84578 2.66702 7.52041 2.66667C7.19509 2.66633 6.92124 2.92029 6.88399 3.25695C6.71042 4.8243 6.2636 5.89953 5.56346 6.62885C4.86332 7.35814 3.83109 7.82361 2.32643 8.00441C2.00323 8.04321 1.75943 8.32847 1.75977 8.66734C1.7601 9.00627 2.00446 9.29094 2.32773 9.32907C3.80694 9.50361 4.86268 9.96901 5.58062 10.7033C6.29465 11.4337 6.74997 12.5073 6.88226 14.059C6.91164 14.4033 7.18869 14.6671 7.52047 14.6667C7.85231 14.6663 8.12879 14.4018 8.1574 14.0575C8.28412 12.5321 8.73909 11.4342 9.45781 10.6855C10.1766 9.93681 11.2305 9.46287 12.6949 9.33087C13.0255 9.30107 13.2794 9.01307 13.2798 8.66741C13.2801 8.32181 13.0269 8.03321 12.6964 8.00261C11.2068 7.86481 10.1761 7.39054 9.47497 6.64673C8.77001 5.89887 8.32322 4.79915 8.15567 3.25831Z" fill="#155AEF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,36 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"opacity": "0.5",
"d": "M12.5674 1.56341C12.5532 1.43246 12.4469 1.33346 12.3203 1.33333C12.1938 1.3332 12.0873 1.43196 12.0728 1.56288C12.0053 2.1724 11.8316 2.59056 11.5593 2.87418C11.287 3.1578 10.8856 3.33881 10.3004 3.40911C10.1747 3.42421 10.08 3.53514 10.0801 3.66693C10.0802 3.79872 10.1752 3.90944 10.3009 3.92427C10.8762 3.99215 11.2868 4.17312 11.566 4.45869C11.8437 4.74271 12.0207 5.16027 12.0721 5.76368C12.0836 5.89756 12.1913 6.00015 12.3203 6C12.4494 5.99984 12.5569 5.897 12.568 5.7631C12.6174 5.16988 12.7943 4.74291 13.0737 4.45176C13.3533 4.1606 13.7632 3.9763 14.3326 3.92496C14.4612 3.91336 14.56 3.80136 14.5601 3.66696C14.5602 3.53255 14.4617 3.42032 14.3332 3.40842C13.7539 3.35482 13.3531 3.17038 13.0804 2.88113C12.8063 2.5903 12.6325 2.16262 12.5674 1.56341Z",
"fill": "#155AEF"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M8.15567 3.25831C8.11906 2.92157 7.84578 2.66702 7.52041 2.66667C7.19509 2.66633 6.92124 2.92029 6.88399 3.25695C6.71042 4.8243 6.2636 5.89953 5.56346 6.62885C4.86332 7.35814 3.83109 7.82361 2.32643 8.00441C2.00323 8.04321 1.75943 8.32847 1.75977 8.66734C1.7601 9.00627 2.00446 9.29094 2.32773 9.32907C3.80694 9.50361 4.86268 9.96901 5.58062 10.7033C6.29465 11.4337 6.74997 12.5073 6.88226 14.059C6.91164 14.4033 7.18869 14.6671 7.52047 14.6667C7.85231 14.6663 8.12879 14.4018 8.1574 14.0575C8.28412 12.5321 8.73909 11.4342 9.45781 10.6855C10.1766 9.93681 11.2305 9.46287 12.6949 9.33087C13.0255 9.30107 13.2794 9.01307 13.2798 8.66741C13.2801 8.32181 13.0269 8.03321 12.6964 8.00261C11.2068 7.86481 10.1761 7.39054 9.47497 6.64673C8.77001 5.89887 8.32322 4.79915 8.15567 3.25831Z",
"fill": "#155AEF"
},
"children": []
}
]
},
"name": "SparklesSoftAccent"
}

View File

@@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './SparklesSoftAccent.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'SparklesSoftAccent'
export default Icon

View File

@@ -12,4 +12,5 @@ export { default as MultiPathRetrieval } from './MultiPathRetrieval'
export { default as NTo1Retrieval } from './NTo1Retrieval'
export { default as Notion } from './Notion'
export { default as Soc2 } from './Soc2'
export { default as SparklesSoftAccent } from './SparklesSoftAccent'
export { default as SparklesSoft } from './SparklesSoft'

View File

@@ -35,7 +35,8 @@ const PlanComp: FC<Props> = ({
const { t } = useTranslation()
const router = useRouter()
const { userProfile } = useAppContext()
const { plan, enableEducationPlan, isEducationAccount } = useProviderContext()
const { plan, enableEducationPlan, allowRefreshEducationVerify, isEducationAccount } = useProviderContext()
const isAboutToExpire = allowRefreshEducationVerify
const {
type,
} = plan
@@ -81,7 +82,7 @@ const PlanComp: FC<Props> = ({
<div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div>
</div>
<div className='flex shrink-0 items-center gap-1'>
{enableEducationPlan && !isEducationAccount && (
{enableEducationPlan && (!isEducationAccount || isAboutToExpire) && (
<Button variant='ghost' onClick={handleVerify}>
<RiGraduationCapLine className='mr-1 h-4 w-4'/>
{t('education.toVerified')}

View File

@@ -59,6 +59,11 @@ export default function AppSelector() {
localStorage.removeItem('console_token')
localStorage.removeItem('refresh_token')
// To avoid use other account's education notice info
localStorage.removeItem('education-reverify-prev-expire-at')
localStorage.removeItem('education-reverify-has-noticed')
localStorage.removeItem('education-expired-has-noticed')
router.push('/signin')
}

View File

@@ -1,2 +1,4 @@
export const EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION = 'getEducationVerify'
export const EDUCATION_VERIFYING_LOCALSTORAGE_ITEM = 'educationVerifying'
export const EDUCATION_PRICING_SHOW_ACTION = 'educationPricing'
export const EDUCATION_RE_VERIFY_ACTION = 'educationReVerify'

View File

@@ -0,0 +1,96 @@
'use client'
import React from 'react'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { useDocLink } from '@/context/i18n'
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import { RiExternalLinkLine } from '@remixicon/react'
import { SparklesSoftAccent } from '../components/base/icons/src/public/common'
import useTimestamp from '@/hooks/use-timestamp'
import { useModalContextSelector } from '@/context/modal-context'
import { useEducationVerify } from '@/service/use-education'
import { useRouter } from 'next/navigation'
export type ExpireNoticeModalPayloadProps = {
expireAt: number
expired: boolean
}
export type Props = {
onClose: () => void
} & ExpireNoticeModalPayloadProps
const i18nPrefix = 'education.notice'
const ExpireNoticeModal: React.FC<Props> = ({ expireAt, expired, onClose }) => {
const { t } = useTranslation()
const docLink = useDocLink()
const eduDocLink = docLink('/getting-started/dify-for-education')
const { formatTime } = useTimestamp()
const setShowPricingModal = useModalContextSelector(s => s.setShowPricingModal)
const { mutateAsync } = useEducationVerify()
const router = useRouter()
const handleVerify = async () => {
const { token } = await mutateAsync()
if (token)
router.push(`/education-apply?token=${token}`)
}
const handleConfirm = async () => {
await handleVerify()
onClose()
}
return (
<Modal
isShow
onClose={onClose}
title={expired ? t(`${i18nPrefix}.expired.title`) : t(`${i18nPrefix}.isAboutToExpire.title`, { date: formatTime(expireAt, t(`${i18nPrefix}.dateFormat`) as string), interpolation: { escapeValue: false } })}
closable
className='max-w-[600px]'
>
<div className='body-md-regular mt-5 space-y-5 text-text-secondary'>
<div>
{expired ? (<>
<div>{t(`${i18nPrefix}.expired.summary.line1`)}</div>
<div>{t(`${i18nPrefix}.expired.summary.line2`)}</div>
</>
) : t(`${i18nPrefix}.isAboutToExpire.summary`)}
</div>
<div>
<strong className='title-md-semi-bold block'>{t(`${i18nPrefix}.stillInEducation.title`)}</strong>
{t(`${i18nPrefix}.stillInEducation.${expired ? 'expired' : 'isAboutToExpire'}`)}
</div>
<div>
<strong className='title-md-semi-bold block'>{t(`${i18nPrefix}.alreadyGraduated.title`)}</strong>
{t(`${i18nPrefix}.alreadyGraduated.${expired ? 'expired' : 'isAboutToExpire'}`)}
</div>
</div>
<div className="mt-7 flex items-center justify-between space-x-2">
<Link className='system-xs-regular flex items-center space-x-1 text-text-accent' href={eduDocLink} target="_blank" rel="noopener noreferrer">
<div>{t('education.learn')}</div>
<RiExternalLinkLine className='size-3' />
</Link>
<div className='flex space-x-2'>
{expired ? (
<Button onClick={() => {
onClose()
setShowPricingModal()
}} className='flex items-center space-x-1'>
<SparklesSoftAccent className='size-4' />
<div className='text-components-button-secondary-accent-text'>{t(`${i18nPrefix}.action.upgrade`)}</div>
</Button>
) : (
<Button onClick={onClose}>
{t(`${i18nPrefix}.action.dismiss`)}
</Button>
)}
<Button variant='primary' onClick={handleConfirm}>
{t(`${i18nPrefix}.action.reVerify`)}
</Button>
</div>
</div>
</Modal>
)
}
export default React.memo(ExpireNoticeModal)

View File

@@ -3,16 +3,26 @@ import {
useEffect,
useState,
} from 'react'
import { useDebounceFn } from 'ahooks'
import { useDebounceFn, useLocalStorageState } from 'ahooks'
import { useSearchParams } from 'next/navigation'
import type { SearchParams } from './types'
import {
EDUCATION_PRICING_SHOW_ACTION,
EDUCATION_RE_VERIFY_ACTION,
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
} from './constants'
import { useEducationAutocomplete } from '@/service/use-education'
import { useEducationAutocomplete, useEducationVerify } from '@/service/use-education'
import { useModalContextSelector } from '@/context/modal-context'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
import { useAppContext } from '@/context/app-context'
import { useRouter } from 'next/navigation'
import { useProviderContext } from '@/context/provider-context'
dayjs.extend(utc)
dayjs.extend(timezone)
export const useEducation = () => {
const {
mutateAsync,
@@ -50,12 +60,99 @@ export const useEducation = () => {
}
}
type useEducationReverifyNoticeParams = {
onNotice: ({
expireAt,
expired,
}: {
expireAt: number
expired: boolean
}) => void
}
const isExpired = (expireAt?: number, timezone?: string) => {
if (!expireAt || !timezone)
return false
const today = dayjs().tz(timezone).startOf('day')
const expiredDay = dayjs.unix(expireAt).tz(timezone).startOf('day')
return today.isSame(expiredDay) || today.isAfter(expiredDay)
}
const useEducationReverifyNotice = ({
onNotice,
}: useEducationReverifyNoticeParams) => {
const { userProfile: { timezone } } = useAppContext()
// const [educationInfo, setEducationInfo] = useState<{ is_student: boolean, allow_refresh: boolean, expire_at: number | null } | null>(null)
// const isLoading = !educationInfo
const { educationAccountExpireAt, allowRefreshEducationVerify, isLoadingEducationAccountInfo: isLoading } = useProviderContext()
const [prevExpireAt, setPrevExpireAt] = useLocalStorageState<number | undefined>('education-reverify-prev-expire-at', {
defaultValue: 0,
})
const [reverifyHasNoticed, setReverifyHasNoticed] = useLocalStorageState<boolean | undefined>('education-reverify-has-noticed', {
defaultValue: false,
})
const [expiredHasNoticed, setExpiredHasNoticed] = useLocalStorageState<boolean | undefined>('education-expired-has-noticed', {
defaultValue: false,
})
useEffect(() => {
if (isLoading || !timezone)
return
if (allowRefreshEducationVerify) {
const expired = isExpired(educationAccountExpireAt!, timezone)
const isExpireAtChanged = prevExpireAt !== educationAccountExpireAt
if (isExpireAtChanged) {
setPrevExpireAt(educationAccountExpireAt!)
setReverifyHasNoticed(false)
setExpiredHasNoticed(false)
}
const shouldNotice = (() => {
if (isExpireAtChanged)
return true
return expired ? !expiredHasNoticed : !reverifyHasNoticed
})()
if (shouldNotice) {
onNotice({
expireAt: educationAccountExpireAt!,
expired,
})
if (expired)
setExpiredHasNoticed(true)
else
setReverifyHasNoticed(true)
}
}
}, [allowRefreshEducationVerify, timezone])
return {
isLoading,
expireAt: educationAccountExpireAt!,
expired: isExpired(educationAccountExpireAt!, timezone),
}
}
export const useEducationInit = () => {
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
const setShowPricingModal = useModalContextSelector(s => s.setShowPricingModal)
const setShowEducationExpireNoticeModal = useModalContextSelector(s => s.setShowEducationExpireNoticeModal)
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
const searchParams = useSearchParams()
const educationVerifyAction = searchParams.get('action')
useEducationReverifyNotice({
onNotice: (payload) => {
setShowEducationExpireNoticeModal({ payload })
},
})
const router = useRouter()
const { mutateAsync } = useEducationVerify()
const handleVerify = async () => {
const { token } = await mutateAsync()
if (token)
router.push(`/education-apply?token=${token}`)
}
useEffect(() => {
if (educationVerifying === 'yes' || educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION) {
setShowAccountSettingModal({ payload: 'billing' })
@@ -63,5 +160,9 @@ export const useEducationInit = () => {
if (educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
}
if (educationVerifyAction === EDUCATION_PRICING_SHOW_ACTION)
setShowPricingModal()
if (educationVerifyAction === EDUCATION_RE_VERIFY_ACTION)
handleVerify()
}, [setShowAccountSettingModal, educationVerifying, educationVerifyAction])
}

View File

@@ -26,6 +26,7 @@ import type { UpdatePluginPayload } from '@/app/components/plugins/types'
import { removeSpecificQueryParam } from '@/utils'
import { noop } from 'lodash-es'
import dynamic from 'next/dynamic'
import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal'
const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
ssr: false,
@@ -64,6 +65,10 @@ const UpdatePlugin = dynamic(() => import('@/app/components/plugins/update-plugi
ssr: false,
})
const ExpireNoticeModal = dynamic(() => import('@/app/education-apply/expire-notice-modal'), {
ssr: false,
})
export type ModalState<T> = {
payload: T
onCancelCallback?: () => void
@@ -102,6 +107,7 @@ export type ModalContextState = {
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
}> | null>>
setShowUpdatePluginModal: Dispatch<SetStateAction<ModalState<UpdatePluginPayload> | null>>
setShowEducationExpireNoticeModal: Dispatch<SetStateAction<ModalState<ExpireNoticeModalPayloadProps> | null>>
}
const ModalContext = createContext<ModalContextState>({
setShowAccountSettingModal: noop,
@@ -116,6 +122,7 @@ const ModalContext = createContext<ModalContextState>({
setShowModelLoadBalancingEntryModal: noop,
setShowOpeningModal: noop,
setShowUpdatePluginModal: noop,
setShowEducationExpireNoticeModal: noop,
})
export const useModalContext = () => useContext(ModalContext)
@@ -145,6 +152,7 @@ export const ModalContextProvider = ({
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
}> | null>(null)
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
const searchParams = useSearchParams()
const router = useRouter()
@@ -272,6 +280,7 @@ export const ModalContextProvider = ({
setShowModelLoadBalancingEntryModal,
setShowOpeningModal,
setShowUpdatePluginModal,
setShowEducationExpireNoticeModal,
}}>
<>
{children}
@@ -318,7 +327,7 @@ export const ModalContextProvider = ({
<Pricing onCancel={() => {
if (searchParams.get('show-pricing') === '1')
router.push(location.pathname, { forceOptimisticNavigation: true } as any)
removeSpecificQueryParam('action')
setShowPricingModal(false)
}} />
)
@@ -398,6 +407,13 @@ export const ModalContextProvider = ({
/>
)
}
{
!!showEducationExpireNoticeModal && (
<ExpireNoticeModal
{...showEducationExpireNoticeModal.payload}
onClose={() => setShowEducationExpireNoticeModal(null)}
/>
)}
</>
</ModalContext.Provider>
)

View File

@@ -48,6 +48,10 @@ type ProviderContextState = {
enableEducationPlan: boolean
isEducationWorkspace: boolean
isEducationAccount: boolean
allowRefreshEducationVerify: boolean
educationAccountExpireAt: number | null
isLoadingEducationAccountInfo: boolean
isFetchingEducationAccountInfo: boolean
webappCopyrightEnabled: boolean
licenseLimit: {
workspace_members: {
@@ -90,6 +94,10 @@ const ProviderContext = createContext<ProviderContextState>({
enableEducationPlan: false,
isEducationWorkspace: false,
isEducationAccount: false,
allowRefreshEducationVerify: false,
educationAccountExpireAt: null,
isLoadingEducationAccountInfo: false,
isFetchingEducationAccountInfo: false,
webappCopyrightEnabled: false,
licenseLimit: {
workspace_members: {
@@ -135,7 +143,7 @@ export const ProviderContextProvider = ({
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
const { data: isEducationAccount } = useEducationStatus(!enableEducationPlan)
const { data: educationAccountInfo, isLoading: isLoadingEducationAccountInfo, isFetching: isFetchingEducationAccountInfo } = useEducationStatus(!enableEducationPlan)
const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false)
const fetchPlan = async () => {
@@ -223,7 +231,11 @@ export const ProviderContextProvider = ({
datasetOperatorEnabled,
enableEducationPlan,
isEducationWorkspace,
isEducationAccount: isEducationAccount?.result || false,
isEducationAccount: educationAccountInfo?.is_student || false,
allowRefreshEducationVerify: educationAccountInfo?.allow_refresh || false,
educationAccountExpireAt: educationAccountInfo?.expire_at || null,
isLoadingEducationAccountInfo,
isFetchingEducationAccountInfo,
webappCopyrightEnabled,
licenseLimit,
refreshLicenseLimit: fetchPlan,

View File

@@ -2,6 +2,7 @@ const translation = {
title: 'Protokolle',
description: 'Die Protokolle zeichnen den Betriebsstatus der Anwendung auf, einschließlich Benutzereingaben und KI-Antworten.',
dateTimeFormat: 'MM/DD/YYYY hh:mm A',
dateFormat: 'MM/DD/YYYY',
table: {
header: {
updatedTime: 'Aktualisierungszeit',

View File

@@ -2,6 +2,7 @@ const translation = {
title: 'Logs',
description: 'The logs record the running status of the application, including user inputs and AI replies.',
dateTimeFormat: 'MM/DD/YYYY hh:mm A',
dateFormat: 'MM/DD/YYYY',
table: {
header: {
updatedTime: 'Updated time',

View File

@@ -42,6 +42,36 @@ const translation = {
rejectTitle: 'Your Dify Educational Verification Has Been Rejected',
rejectContent: 'Unfortunately, you are not eligible for Education Verified status and therefore cannot receive the exclusive 100% coupon for the Dify Professional Plan if you use this email address.',
emailLabel: 'Your current email',
notice: {
dateFormat: 'MM/DD/YYYY',
expired: {
title: 'Your education status has expired',
summary: {
line1: 'You can still access and use Dify. ',
line2: 'However, you\'re no longer eligible for new education discount coupons.',
},
},
isAboutToExpire: {
title: 'Your education status will expire on {{date}}',
summary: 'Don\'t worry — this won\'t affect your current subscription, but you won\'t get the education discount when it renews unless you verify your status again.',
},
stillInEducation: {
title: 'Still in education?',
expired: 'Re-verify now to get a new coupon for the upcoming academic year. We\'ll add it to your account and you can use it for the next upgrade.',
isAboutToExpire: 'Re-verify now to get a new coupon for the upcoming academic year. It\'ll be saved to your account and ready to use at your next renewal.',
},
alreadyGraduated: {
title: 'Already graduated?',
expired: 'Feel free to upgrade anytime to get full access to paid features.',
isAboutToExpire: 'Your current subscription will still remain active. When it ends, you\'ll be moved to the Sandbox plan, or you can upgrade anytime to restore full access to paid features.',
},
action: {
dismiss: 'Dismiss',
upgrade: 'Upgrade',
reVerify: 'Re-verify',
},
},
}
export default translation

View File

@@ -2,6 +2,7 @@ const translation = {
title: 'ログ',
description: 'ログは、アプリケーションの実行状態を記録します。ユーザーの入力や AI の応答などが含まれます。',
dateTimeFormat: 'YYYY/MM/DD hh:mm A',
dateFormat: 'YYYY/MM/DD',
table: {
header: {
updatedTime: '更新時間',

View File

@@ -42,6 +42,36 @@ const translation = {
rejectTitle: 'Dify 教育認証が拒否されました',
rejectContent: '申し訳ございませんが、このメールアドレスでは 教育認証 の資格を取得できず、Dify プロフェッショナルプランの 100割引クーポン を受け取ることはできません。',
emailLabel: '現在のメールアドレス',
notice: {
dateFormat: 'YYYY/MM/DD',
expired: {
title: 'あなたの教育認証は失効しました',
summary: {
line1: 'Dify は引き続きご利用いただけますが、新しい教育割引クーポンの対象外となります。',
line2: '',
},
},
isAboutToExpire: {
title: 'あなたの教育認証は {{date}} に有効期限を迎えます',
summary: 'ご安心ください。現在のサブスクリプションには影響ありません。ただし、再認証を行わない場合、次回の更新時に教育割引を受けることができません。',
},
stillInEducation: {
title: 'まだ在学中ですか?',
expired: '今すぐ再認証して、次の学年度向けの教育クーポンを取得してください。クーポンはあなたのアカウントに追加され、次回のアップグレード時にご利用いただけます。',
isAboutToExpire: '今すぐ再認証して、次の学年度向けの教育クーポンを取得してください。クーポンは個人のアカウントに保存され、次回の更新時に使用できます。',
},
alreadyGraduated: {
title: 'すでに卒業しましたか?',
expired: 'いつでもアップグレードして、すべての有料機能にアクセスすることができます。',
isAboutToExpire: '今すぐ再認証して、次の学年度向けの教育クーポンを取得してください。クーポンはあなたのアカウントに追加され、次回のアップグレード時にご利用いただけます。',
},
action: {
dismiss: '無視',
upgrade: 'アップグレード',
reVerify: '再認証する',
},
},
}
export default translation

View File

@@ -2,6 +2,7 @@ const translation = {
title: '日志',
description: '日志记录了应用的运行情况,包括用户的输入和 AI 的回复。',
dateTimeFormat: 'YYYY-MM-DD HH:mm',
dateFormat: 'YYYY-MM-DD',
table: {
header: {
updatedTime: '更新时间',

View File

@@ -42,6 +42,36 @@ const translation = {
rejectTitle: '您的 Dify 教育版认证已被拒绝',
rejectContent: '非常遗憾,您无法使用此电子邮件以获得教育版认证资格,也无法领取 Dify Professional 版的 100% 独家优惠券。',
emailLabel: '您当前的邮箱',
notice: {
dateFormat: 'YYYY/MM/DD',
expired: {
title: '您的教育认证已过期',
summary: {
line1: '您仍可继续使用 Dify但将无法再领取新的教育优惠券。',
line2: '',
},
},
isAboutToExpire: {
title: '您的教育认证将于 {{date}} 过期',
summary: '别担心,这不会影响您当前的订阅。但续订时您将无法继续享受教育优惠,除非重新完成身份验证。',
},
stillInEducation: {
title: '仍在就读?',
expired: '立即重新认证,获取新学年的教育优惠券。优惠券将发放至您的账户,并可在下次升级时使用。',
isAboutToExpire: '立即重新验证,获取新学年的教育优惠券。优惠券将发放至您的账户,并可在下次续订时使用。',
},
alreadyGraduated: {
title: '已毕业?',
expired: '您可以随时升级以获得所有付费功能。',
isAboutToExpire: '您的当前订阅仍将保持有效。订阅结束后,空间将切换为 Sandbox 套餐,您也可以随时升级,恢复全部付费功能的使用。',
},
action: {
dismiss: '忽略',
upgrade: '升级套餐',
reVerify: '重新认证',
},
},
}
export default translation

View File

@@ -56,9 +56,10 @@ export const useEducationStatus = (disable?: boolean) => {
enabled: !disable,
queryKey: [NAME_SPACE, 'education-status'],
queryFn: () => {
return get<{ result: boolean }>('/account/education')
return get<{ is_student: boolean, allow_refresh: boolean, expire_at: number | null }>('/account/education')
},
retry: false,
gcTime: 0, // No cache. Prevent switch account caused stale data
})
}