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:
y 2026-06-03 10:16:59 +08:00 committed by GitHub
parent f591da7865
commit ca31762e26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 80 additions and 32 deletions

View File

@ -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

View File

@ -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)
})
})
})

View File

@ -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()
})
})

View File

@ -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 () => {

View File

@ -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}`)

View File

@ -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
}

View File

@ -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)

View File

@ -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,