diff --git a/.changeset/flat-bugs-visit.md b/.changeset/flat-bugs-visit.md new file mode 100644 index 00000000000..eda2e695baa --- /dev/null +++ b/.changeset/flat-bugs-visit.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Bug fix: Avoid triggering prepare verification twice. (Affects only dev mode) diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpEmailCodeCard.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpEmailCodeCard.tsx index b52226d286b..355e76b1d3d 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpEmailCodeCard.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpEmailCodeCard.tsx @@ -1,26 +1,34 @@ -import React from 'react'; - import { useCoreSignUp } from '../../contexts'; import { Flow, localizationKeys } from '../../customizables'; +import { useFetch } from '../../hooks'; import { SignUpVerificationCodeForm } from './SignUpVerificationCodeForm'; export const SignUpEmailCodeCard = () => { const signUp = useCoreSignUp(); - React.useEffect(() => { - // TODO: This prepare method is not idempotent. - // We need to make sure that R18 won't trigger this twice - void prepare(); - }, []); + const emailVerificationStatus = signUp.verifications.emailAddress.status; + const shouldAvoidPrepare = !signUp.status || emailVerificationStatus === 'verified'; const prepare = () => { - const emailVerificationStatus = signUp.verifications.emailAddress.status; - if (!signUp.status || emailVerificationStatus === 'verified') { + if (shouldAvoidPrepare) { return; } return signUp.prepareEmailAddressVerification({ strategy: 'email_code' }); }; + // TODO: Introduce a useMutation to handle mutating requests + useFetch( + shouldAvoidPrepare ? undefined : () => signUp.prepareEmailAddressVerification({ strategy: 'email_code' }), + { + name: 'prepare', + strategy: 'email_code', + number: signUp.emailAddress, + }, + { + staleTime: 100, + }, + ); + const attempt = (code: string) => signUp.attemptEmailAddressVerification({ code }); return ( diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpPhoneCodeCard.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpPhoneCodeCard.tsx index 46407f4bee8..8cbaf3ee9b2 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpPhoneCodeCard.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpPhoneCodeCard.tsx @@ -1,27 +1,34 @@ -import React from 'react'; - import { useCoreSignUp } from '../../contexts'; import { Flow, localizationKeys } from '../../customizables'; import { withCardStateProvider } from '../../elements'; +import { useFetch } from '../../hooks'; import { SignUpVerificationCodeForm } from './SignUpVerificationCodeForm'; export const SignUpPhoneCodeCard = withCardStateProvider(() => { const signUp = useCoreSignUp(); - React.useEffect(() => { - // TODO: This prepare method is not idempotent. - // We need to make sure that R18 won't trigger this twice - void prepare(); - }, []); - + const phoneVerificationStatus = signUp.verifications.phoneNumber.status; + const shouldAvoidPrepare = !signUp.status || phoneVerificationStatus === 'verified'; const prepare = () => { - const phoneVerificationStatus = signUp.verifications.phoneNumber.status; - if (!signUp.status || phoneVerificationStatus === 'verified') { + if (shouldAvoidPrepare) { return; } return signUp.preparePhoneNumberVerification({ strategy: 'phone_code' }); }; + // TODO: Introduce a useMutation to handle mutating requests + useFetch( + shouldAvoidPrepare ? undefined : () => signUp.preparePhoneNumberVerification({ strategy: 'phone_code' }), + { + name: 'signUp.preparePhoneNumberVerification', + strategy: 'phone_code', + number: signUp.phoneNumber, + }, + { + staleTime: 100, + }, + ); + const attempt = (code: string) => signUp.attemptPhoneNumberVerification({ code }); return ( diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyEmail.test.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyEmail.test.tsx index 9cd0189dc5c..3a17f562c60 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyEmail.test.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyEmail.test.tsx @@ -14,10 +14,11 @@ describe('SignUpVerifyEmail', () => { }); it('shows the email associated with the sign up', async () => { - const { wrapper } = await createFixtures(f => { + const { wrapper, fixtures } = await createFixtures(f => { f.withEmailAddress({ required: true }); f.startSignUpWithEmailAddress({ emailAddress: 'test@clerk.com' }); }); + fixtures.signUp.prepareEmailAddressVerification.mockRejectedValue(null); render(, { wrapper }); screen.getByText('test@clerk.com'); }); @@ -44,13 +45,8 @@ describe('SignUpVerifyEmail', () => { f.withEmailAddress({ required: true, verifications: ['email_code'] }); f.startSignUpWithEmailAddress({ emailAddress: 'test@clerk.com' }); }); - fixtures.signUp.createEmailLinkFlow.mockImplementation( - () => - ({ - startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), - } as any), - ); + + fixtures.signUp.prepareEmailAddressVerification.mockRejectedValue(null); render(, { wrapper }); screen.getByText(/Verify your email/i); @@ -62,6 +58,8 @@ describe('SignUpVerifyEmail', () => { f.withEmailAddress({ required: true }); f.startSignUpWithEmailAddress({ emailAddress: 'test@clerk.com' }); }); + fixtures.signUp.prepareEmailAddressVerification.mockRejectedValue(null); + const { userEvent } = render(, { wrapper }); await userEvent.click( screen.getByRole('button', { @@ -94,13 +92,8 @@ describe('SignUpVerifyEmail', () => { f.withEmailAddress({ required: true, verifications: ['email_code'] }); f.startSignUpWithEmailAddress({ emailAddress: 'test@clerk.com' }); }); - fixtures.signUp.createEmailLinkFlow.mockImplementation( - () => - ({ - startEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), - cancelEmailLinkFlow: jest.fn(() => new Promise(() => ({}))), - } as any), - ); + + fixtures.signUp.prepareEmailAddressVerification.mockRejectedValue(null); render(, { wrapper }); const resendButton = screen.getByText(/Resend/i); diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyPhone.test.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyPhone.test.tsx index bf5d6ddfe7e..2d6b440dfa9 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyPhone.test.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpVerifyPhone.test.tsx @@ -14,19 +14,21 @@ describe('SignUpVerifyPhone', () => { }); it('shows the phone number associated with the sign up', async () => { - const { wrapper } = await createFixtures(f => { + const { wrapper, fixtures } = await createFixtures(f => { f.withPhoneNumber({ required: true }); f.startSignUpWithPhoneNumber({ phoneNumber: '+306911111111' }); }); + fixtures.signUp.preparePhoneNumberVerification.mockRejectedValue(null); render(, { wrapper }); screen.getByText('+30 691 1111111'); }); it('shows the verify with code message', async () => { - const { wrapper } = await createFixtures(f => { + const { wrapper, fixtures } = await createFixtures(f => { f.withPhoneNumber({ required: true }); f.startSignUpWithPhoneNumber(); }); + fixtures.signUp.preparePhoneNumberVerification.mockRejectedValue(null); render(, { wrapper }); screen.getByText(/Verify your phone/i); screen.getByText(/Enter the verification code sent to your phone/i); @@ -37,6 +39,7 @@ describe('SignUpVerifyPhone', () => { f.withPhoneNumber({ required: true }); f.startSignUpWithPhoneNumber(); }); + fixtures.signUp.preparePhoneNumberVerification.mockRejectedValue(null); const { userEvent } = render(, { wrapper }); await userEvent.click( screen.getByRole('button', { @@ -47,10 +50,11 @@ describe('SignUpVerifyPhone', () => { }); it('Resend code button exists', async () => { - const { wrapper } = await createFixtures(f => { + const { wrapper, fixtures } = await createFixtures(f => { f.withPhoneNumber({ required: true }); f.startSignUpWithEmailAddress({ emailAddress: 'test@clerk.com' }); }); + fixtures.signUp.preparePhoneNumberVerification.mockRejectedValue(null); render(, { wrapper }); const resendButton = screen.getByText(/Resend/i); expect(resendButton.tagName.toUpperCase()).toBe('BUTTON'); diff --git a/packages/clerk-js/src/ui/hooks/useFetch.ts b/packages/clerk-js/src/ui/hooks/useFetch.ts index 99e3b23028b..8e717b5aec9 100644 --- a/packages/clerk-js/src/ui/hooks/useFetch.ts +++ b/packages/clerk-js/src/ui/hooks/useFetch.ts @@ -37,7 +37,7 @@ const useCache = ( key: K, ): { getCache: () => State | undefined; - setCache: (state: State) => void; + setCache: (state: State) => void; subscribeCache: (callback: () => void) => () => void; } => { const serializedKey = serialize(key); @@ -64,18 +64,21 @@ const useCache = ( export const useFetch = ( fetcher: ((...args: any) => Promise) | undefined, params: K, - callbacks?: { + options?: { onSuccess?: (data: T) => void; + staleTime?: number; }, ) => { const { subscribeCache, getCache, setCache } = useCache(params); + + const staleTime = options?.staleTime || 1000 * 60 * 2; //cache for 2 minutes by default const fetcherRef = useRef(fetcher); const cached = useSyncExternalStore(subscribeCache, getCache); useEffect(() => { const fetcherMissing = !fetcherRef.current; - const isCacheStale = Date.now() - (getCache()?.cachedAt || 0) < 1000 * 60 * 2; //cache for 2 minutes; + const isCacheStale = Date.now() - (getCache()?.cachedAt || 0) < staleTime; const isRequestOnGoing = getCache()?.isValidating; if (fetcherMissing || isCacheStale || isRequestOnGoing) { @@ -99,7 +102,7 @@ export const useFetch = ( error: null, cachedAt: Date.now(), }); - callbacks?.onSuccess?.(data); + options?.onSuccess?.(data); } }) .catch(() => {