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