mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:32:01 +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
|
"count": 4
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/billing/plan/index.tsx": {
|
|
||||||
"no-restricted-globals": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/billing/pricing/assets/index.tsx": {
|
"web/app/components/billing/pricing/assets/index.tsx": {
|
||||||
"no-barrel-files/no-barrel-files": {
|
"no-barrel-files/no-barrel-files": {
|
||||||
"count": 12
|
"count": 12
|
||||||
@ -2403,11 +2398,6 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/education-verify-action-recorder.tsx": {
|
|
||||||
"no-restricted-globals": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/explore/app-list/index.tsx": {
|
"web/app/components/explore/app-list/index.tsx": {
|
||||||
"no-restricted-imports": {
|
"no-restricted-imports": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -5100,11 +5090,6 @@
|
|||||||
"count": 3
|
"count": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/context/modal-context-provider.tsx": {
|
|
||||||
"no-restricted-globals": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/context/provider-context-provider.tsx": {
|
"web/context/provider-context-provider.tsx": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const mockSetShowPricingModal = vi.fn()
|
|||||||
const mockSetShowAccountSettingModal = vi.fn()
|
const mockSetShowAccountSettingModal = vi.fn()
|
||||||
const mockRouterPush = vi.fn()
|
const mockRouterPush = vi.fn()
|
||||||
const mockMutateAsync = vi.fn()
|
const mockMutateAsync = vi.fn()
|
||||||
|
const mockSetEducationVerifying = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
// ─── Context mocks ───────────────────────────────────────────────────────────
|
// ─── Context mocks ───────────────────────────────────────────────────────────
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
@ -76,6 +77,10 @@ vi.mock('@/hooks/use-async-window-open', () => ({
|
|||||||
useAsyncWindowOpen: () => vi.fn(),
|
useAsyncWindowOpen: () => vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/hooks/use-local-storage', () => ({
|
||||||
|
useSetLocalStorage: () => mockSetEducationVerifying,
|
||||||
|
}))
|
||||||
|
|
||||||
// ─── External component mocks ───────────────────────────────────────────────
|
// ─── External component mocks ───────────────────────────────────────────────
|
||||||
vi.mock('@/app/education-apply/verify-state-modal', () => ({
|
vi.mock('@/app/education-apply/verify-state-modal', () => ({
|
||||||
default: ({ isShow, title, content, email, showLink }: {
|
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' })
|
mockMutateAsync.mockResolvedValue({ token: 'token-xyz' })
|
||||||
setupContexts({}, { enableEducationPlan: true, isEducationAccount: false })
|
setupContexts({}, { enableEducationPlan: true, isEducationAccount: false })
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
@ -216,7 +221,7 @@ describe('Education Verification Flow', () => {
|
|||||||
await user.click(screen.getByText(/toVerified/i))
|
await user.click(screen.getByText(/toVerified/i))
|
||||||
|
|
||||||
await waitFor(() => {
|
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 { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import {
|
import {
|
||||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
|
||||||
} from '@/app/education-apply/constants'
|
} from '@/app/education-apply/constants'
|
||||||
import { useSearchParams } from '@/next/navigation'
|
import { useSearchParams } from '@/next/navigation'
|
||||||
import { EducationVerifyActionRecorder } from '../education-verify-action-recorder'
|
import { EducationVerifyActionRecorder } from '../education-verify-action-recorder'
|
||||||
|
|
||||||
|
const setEducationVerifyingMock = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
vi.mock('@/next/navigation', () => ({
|
vi.mock('@/next/navigation', () => ({
|
||||||
useSearchParams: vi.fn(),
|
useSearchParams: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/hooks/use-local-storage', () => ({
|
||||||
|
useSetLocalStorage: () => setEducationVerifyingMock,
|
||||||
|
}))
|
||||||
|
|
||||||
const mockUseSearchParams = vi.mocked(useSearchParams)
|
const mockUseSearchParams = vi.mocked(useSearchParams)
|
||||||
|
|
||||||
describe('EducationVerifyActionRecorder', () => {
|
describe('EducationVerifyActionRecorder', () => {
|
||||||
@ -28,13 +33,13 @@ describe('EducationVerifyActionRecorder', () => {
|
|||||||
render(<EducationVerifyActionRecorder />)
|
render(<EducationVerifyActionRecorder />)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(window.localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)).toBe('yes')
|
expect(setEducationVerifyingMock).toHaveBeenCalledWith('yes')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should leave localStorage unchanged for unrelated routes', () => {
|
it('should leave localStorage unchanged for unrelated routes', () => {
|
||||||
render(<EducationVerifyActionRecorder />)
|
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 { 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 { fetchSubscriptionUrls } from '@/service/billing'
|
||||||
import { Plan, SelfHostedPlan } from '../../type'
|
import { Plan, SelfHostedPlan } from '../../type'
|
||||||
import PlanComp from '../index'
|
import PlanComp from '../index'
|
||||||
|
|
||||||
|
const setEducationVerifyingMock = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
let currentPath = '/billing'
|
let currentPath = '/billing'
|
||||||
|
|
||||||
const push = vi.fn()
|
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', () => ({
|
vi.mock('@/service/billing', () => ({
|
||||||
fetchSubscriptionUrls: vi.fn(),
|
fetchSubscriptionUrls: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@ -143,7 +148,7 @@ describe('PlanComp', () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(mutateAsyncMock).toHaveBeenCalled())
|
await waitFor(() => expect(mutateAsyncMock).toHaveBeenCalled())
|
||||||
await waitFor(() => expect(push).toHaveBeenCalledWith('/education-apply?token=token'))
|
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 () => {
|
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 { useAppContext } from '@/context/app-context'
|
||||||
import { useModalContextSelector } from '@/context/modal-context'
|
import { useModalContextSelector } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||||
import { usePathname, useRouter } from '@/next/navigation'
|
import { usePathname, useRouter } from '@/next/navigation'
|
||||||
import { useEducationVerify } from '@/service/use-education'
|
import { useEducationVerify } from '@/service/use-education'
|
||||||
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
||||||
@ -70,12 +71,13 @@ const PlanComp: FC<Props> = ({
|
|||||||
const { handleEducationDiscount, isEducationDiscountLoading } = useEducationDiscount()
|
const { handleEducationDiscount, isEducationDiscountLoading } = useEducationDiscount()
|
||||||
const { mutateAsync, isPending } = useEducationVerify()
|
const { mutateAsync, isPending } = useEducationVerify()
|
||||||
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||||
|
const setEducationVerifying = useSetLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true })
|
||||||
const unmountedRef = useUnmountedRef()
|
const unmountedRef = useUnmountedRef()
|
||||||
const handleVerify = () => {
|
const handleVerify = () => {
|
||||||
if (isPending)
|
if (isPending)
|
||||||
return
|
return
|
||||||
mutateAsync().then((res) => {
|
mutateAsync().then((res) => {
|
||||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
setEducationVerifying(null)
|
||||||
if (unmountedRef.current)
|
if (unmountedRef.current)
|
||||||
return
|
return
|
||||||
router.push(`/education-apply?token=${res.token}`)
|
router.push(`/education-apply?token=${res.token}`)
|
||||||
|
|||||||
@ -5,15 +5,17 @@ import {
|
|||||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||||
} from '@/app/education-apply/constants'
|
} from '@/app/education-apply/constants'
|
||||||
|
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||||
import { useSearchParams } from '@/next/navigation'
|
import { useSearchParams } from '@/next/navigation'
|
||||||
|
|
||||||
export function EducationVerifyActionRecorder() {
|
export function EducationVerifyActionRecorder() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
const setEducationVerifying = useSetLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchParams.get('action') === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
if (searchParams.get('action') === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||||
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
setEducationVerifying('yes')
|
||||||
}, [searchParams])
|
}, [searchParams, setEducationVerifying])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
} from '@/app/education-apply/constants'
|
} from '@/app/education-apply/constants'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { useSetLocalStorage } from '@/hooks/use-local-storage'
|
||||||
import {
|
import {
|
||||||
useAccountSettingModal,
|
useAccountSettingModal,
|
||||||
usePricingModal,
|
usePricingModal,
|
||||||
@ -99,14 +100,11 @@ export const ModalContextProvider = ({
|
|||||||
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
|
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
|
||||||
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
|
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
|
||||||
const { currentWorkspace } = useAppContext()
|
const { currentWorkspace } = useAppContext()
|
||||||
|
const setEducationVerifying = useSetLocalStorage<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, { raw: true })
|
||||||
|
|
||||||
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
||||||
const handleCancelAccountSettingModal = () => {
|
const handleCancelAccountSettingModal = () => {
|
||||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
setEducationVerifying(educationVerifying => educationVerifying === 'yes' ? null : educationVerifying)
|
||||||
|
|
||||||
if (educationVerifying === 'yes')
|
|
||||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
|
||||||
|
|
||||||
accountSettingCallbacksRef.current?.onCancelCallback?.()
|
accountSettingCallbacksRef.current?.onCancelCallback?.()
|
||||||
accountSettingCallbacksRef.current = null
|
accountSettingCallbacksRef.current = null
|
||||||
setUrlAccountModalState(null)
|
setUrlAccountModalState(null)
|
||||||
|
|||||||
@ -3,9 +3,13 @@ import userEvent from '@testing-library/user-event'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { defaultPlan } from '@/app/components/billing/config'
|
import { defaultPlan } from '@/app/components/billing/config'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
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 { ModalContextProvider } from '@/context/modal-context-provider'
|
||||||
import { renderWithNuqs } from '@/test/nuqs-testing'
|
import { renderWithNuqs } from '@/test/nuqs-testing'
|
||||||
|
|
||||||
|
const mockSetEducationVerifying = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
vi.mock('@/config', async (importOriginal) => {
|
vi.mock('@/config', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('@/config')>()
|
const actual = await importOriginal<typeof import('@/config')>()
|
||||||
return {
|
return {
|
||||||
@ -22,6 +26,16 @@ vi.mock('@/app/components/billing/pricing', () => ({
|
|||||||
default: () => <div>billing.plansCommon.mostPopular</div>,
|
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()
|
const mockUseProviderContext = vi.fn()
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: () => mockUseProviderContext(),
|
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>
|
<ModalContextProvider>
|
||||||
<div data-testid="modal-context-test-child" />
|
{children}
|
||||||
</ModalContextProvider>,
|
</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', () => {
|
describe('ModalContextProvider trigger events limit modal', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockUseAppContext.mockReset()
|
mockUseAppContext.mockReset()
|
||||||
mockUseProviderContext.mockReset()
|
mockUseProviderContext.mockReset()
|
||||||
|
mockSetEducationVerifying.mockReset()
|
||||||
window.localStorage.clear()
|
window.localStorage.clear()
|
||||||
mockUseAppContext.mockReturnValue({
|
mockUseAppContext.mockReturnValue({
|
||||||
currentWorkspace: {
|
currentWorkspace: {
|
||||||
@ -115,6 +143,24 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
|||||||
expect(value).toBe('1')
|
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 () => {
|
it('relies on the in-memory guard when localStorage reads throw', async () => {
|
||||||
const plan = createPlan({
|
const plan = createPlan({
|
||||||
type: Plan.professional,
|
type: Plan.professional,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user