mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 01:18:05 +08:00
feat: notice of the expire of education verify (#24210)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@ -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'
|
||||
|
||||
96
web/app/education-apply/expire-notice-modal.tsx
Normal file
96
web/app/education-apply/expire-notice-modal.tsx
Normal 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)
|
||||
@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user