mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:13:59 +08:00
refactor(web): migrate education verifying storage to useLocalStorage (#36934)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
f591da7865
commit
ca31762e26
@ -1867,11 +1867,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/billing/plan/index.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/billing/pricing/assets/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 12
|
||||
@ -2403,11 +2398,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/education-verify-action-recorder.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/explore/app-list/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@ -5100,11 +5090,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/context/modal-context-provider.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/context/provider-context-provider.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@ -22,6 +22,7 @@ const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockRouterPush = vi.fn()
|
||||
const mockMutateAsync = vi.fn()
|
||||
const mockSetEducationVerifying = vi.hoisted(() => vi.fn())
|
||||
|
||||
// ─── Context mocks ───────────────────────────────────────────────────────────
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
@ -76,6 +77,10 @@ vi.mock('@/hooks/use-async-window-open', () => ({
|
||||
useAsyncWindowOpen: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-local-storage', () => ({
|
||||
useSetLocalStorage: () => mockSetEducationVerifying,
|
||||
}))
|
||||
|
||||
// ─── External component mocks ───────────────────────────────────────────────
|
||||
vi.mock('@/app/education-apply/verify-state-modal', () => ({
|
||||
default: ({ isShow, title, content, email, showLink }: {
|
||||
@ -206,7 +211,7 @@ describe('Education Verification Flow', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove education verifying flag from localStorage on success', async () => {
|
||||
it('should clear education verifying flag on success', async () => {
|
||||
mockMutateAsync.mockResolvedValue({ token: 'token-xyz' })
|
||||
setupContexts({}, { enableEducationPlan: true, isEducationAccount: false })
|
||||
const user = userEvent.setup()
|
||||
@ -216,7 +221,7 @@ describe('Education Verification Flow', () => {
|
||||
await user.click(screen.getByText(/toVerified/i))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorage.removeItem).toHaveBeenCalledWith('educationVerifying')
|
||||
expect(mockSetEducationVerifying).toHaveBeenCalledWith(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,15 +2,20 @@ import { render, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
} from '@/app/education-apply/constants'
|
||||
import { useSearchParams } from '@/next/navigation'
|
||||
import { EducationVerifyActionRecorder } from '../education-verify-action-recorder'
|
||||
|
||||
const setEducationVerifyingMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useSearchParams: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-local-storage', () => ({
|
||||
useSetLocalStorage: () => setEducationVerifyingMock,
|
||||
}))
|
||||
|
||||
const mockUseSearchParams = vi.mocked(useSearchParams)
|
||||
|
||||
describe('EducationVerifyActionRecorder', () => {
|
||||
@ -28,13 +33,13 @@ describe('EducationVerifyActionRecorder', () => {
|
||||
render(<EducationVerifyActionRecorder />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)).toBe('yes')
|
||||
expect(setEducationVerifyingMock).toHaveBeenCalledWith('yes')
|
||||
})
|
||||
})
|
||||
|
||||
it('should leave localStorage unchanged for unrelated routes', () => {
|
||||
render(<EducationVerifyActionRecorder />)
|
||||
|
||||
expect(window.localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)).toBeNull()
|
||||
expect(setEducationVerifyingMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/constants'
|
||||
import { fetchSubscriptionUrls } from '@/service/billing'
|
||||
import { Plan, SelfHostedPlan } from '../../type'
|
||||
import PlanComp from '../index'
|
||||
|
||||
const setEducationVerifyingMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
let currentPath = '/billing'
|
||||
|
||||
const push = vi.fn()
|
||||
@ -35,6 +36,10 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-local-storage', () => ({
|
||||
useSetLocalStorage: () => setEducationVerifyingMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/billing', () => ({
|
||||
fetchSubscriptionUrls: vi.fn(),
|
||||
}))
|
||||
@ -143,7 +148,7 @@ describe('PlanComp', () => {
|
||||
|
||||
await waitFor(() => expect(mutateAsyncMock).toHaveBeenCalled())
|
||||
await waitFor(() => expect(push).toHaveBeenCalledWith('/education-apply?token=token'))
|
||||
expect(localStorage.removeItem).toHaveBeenCalledWith(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
expect(setEducationVerifyingMock).toHaveBeenCalledWith(null)
|
||||
})
|
||||
|
||||
it('shows modal when education verify fails', async () => {
|
||||
|
||||
@ -18,6 +18,7 @@ import VerifyStateModal from '@/app/education-apply/verify-state-modal'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { useEducationVerify } from '@/service/use-education'
|
||||
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
||||
@ -70,12 +71,13 @@ const PlanComp: FC<Props> = ({
|
||||
const { handleEducationDiscount, isEducationDiscountLoading } = useEducationDiscount()
|
||||
const { mutateAsync, isPending } = useEducationVerify()
|
||||
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||
const setEducationVerifying = useSetLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true })
|
||||
const unmountedRef = useUnmountedRef()
|
||||
const handleVerify = () => {
|
||||
if (isPending)
|
||||
return
|
||||
mutateAsync().then((res) => {
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
setEducationVerifying(null)
|
||||
if (unmountedRef.current)
|
||||
return
|
||||
router.push(`/education-apply?token=${res.token}`)
|
||||
|
||||
@ -5,15 +5,17 @@ import {
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
} from '@/app/education-apply/constants'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import { useSearchParams } from '@/next/navigation'
|
||||
|
||||
export function EducationVerifyActionRecorder() {
|
||||
const searchParams = useSearchParams()
|
||||
const setEducationVerifying = useSetLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true })
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('action') === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
||||
}, [searchParams])
|
||||
setEducationVerifying('yes')
|
||||
}, [searchParams, setEducationVerifying])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
} from '@/app/education-apply/constants'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||
import {
|
||||
useAccountSettingModal,
|
||||
usePricingModal,
|
||||
@ -99,14 +100,11 @@ export const ModalContextProvider = ({
|
||||
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
|
||||
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
|
||||
const { currentWorkspace } = useAppContext()
|
||||
const setEducationVerifying = useSetLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true })
|
||||
|
||||
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
||||
const handleCancelAccountSettingModal = () => {
|
||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
|
||||
if (educationVerifying === 'yes')
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
|
||||
setEducationVerifying(educationVerifying => educationVerifying === 'yes' ? null : educationVerifying)
|
||||
accountSettingCallbacksRef.current?.onCancelCallback?.()
|
||||
accountSettingCallbacksRef.current = null
|
||||
setUrlAccountModalState(null)
|
||||
|
||||
@ -3,9 +3,13 @@ import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { ModalContextProvider } from '@/context/modal-context-provider'
|
||||
import { renderWithNuqs } from '@/test/nuqs-testing'
|
||||
|
||||
const mockSetEducationVerifying = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return {
|
||||
@ -22,6 +26,16 @@ vi.mock('@/app/components/billing/pricing', () => ({
|
||||
default: () => <div>billing.plansCommon.mostPopular</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting', () => ({
|
||||
default: ({ onCancelAction }: { onCancelAction: () => void }) => (
|
||||
<button type="button" onClick={onCancelAction}>cancel account setting</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-local-storage', () => ({
|
||||
useSetLocalStorage: () => mockSetEducationVerifying,
|
||||
}))
|
||||
|
||||
const mockUseProviderContext = vi.fn()
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => mockUseProviderContext(),
|
||||
@ -61,16 +75,30 @@ const createPlan = (overrides: PlanOverrides = {}): PlanShape => ({
|
||||
},
|
||||
})
|
||||
|
||||
const renderProvider = () => renderWithNuqs(
|
||||
const renderProvider = (children: React.ReactNode = <div data-testid="modal-context-test-child" />) => renderWithNuqs(
|
||||
<ModalContextProvider>
|
||||
<div data-testid="modal-context-test-child" />
|
||||
{children}
|
||||
</ModalContextProvider>,
|
||||
)
|
||||
|
||||
const AccountSettingOpener = () => {
|
||||
const setShowAccountSettingModal = useModalContextSelector(state => state.setShowAccountSettingModal)
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })}
|
||||
>
|
||||
open account setting
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
describe('ModalContextProvider trigger events limit modal', () => {
|
||||
beforeEach(() => {
|
||||
mockUseAppContext.mockReset()
|
||||
mockUseProviderContext.mockReset()
|
||||
mockSetEducationVerifying.mockReset()
|
||||
window.localStorage.clear()
|
||||
mockUseAppContext.mockReturnValue({
|
||||
currentWorkspace: {
|
||||
@ -115,6 +143,24 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
expect(value).toBe('1')
|
||||
})
|
||||
|
||||
it('clears the education verifying flag when account settings are canceled', async () => {
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan: createPlan(),
|
||||
isFetchedPlan: true,
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderProvider(<AccountSettingOpener />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'open account setting' }))
|
||||
await user.click(await screen.findByRole('button', { name: 'cancel account setting' }))
|
||||
|
||||
expect(mockSetEducationVerifying).toHaveBeenCalledWith(expect.any(Function))
|
||||
const updater = mockSetEducationVerifying.mock.calls[0]?.[0] as (educationVerifying: string) => string | null
|
||||
expect(updater('yes')).toBeNull()
|
||||
expect(updater('no')).toBe('no')
|
||||
})
|
||||
|
||||
it('relies on the in-memory guard when localStorage reads throw', async () => {
|
||||
const plan = createPlan({
|
||||
type: Plan.professional,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user