From 42a8b75254cd81fd7c5f8d9ab80bb7b46b0f8d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20H=C3=B6glund?= Date: Thu, 29 Oct 2026 13:03:59 +0100 Subject: [PATCH 01/13] Turn on the preserve-manual-memoization rule --- eslint.config.mjs | 1 - .../mosaic/src/primitives/hooks/use-focus-target.ts | 1 + packages/mosaic/src/primitives/select/select-root.tsx | 2 +- packages/mosaic/src/primitives/toast/toast-root.tsx | 3 ++- .../ConfigureDirectorySyncContext.tsx | 10 +++++----- packages/ui/src/contexts/components/Plans.tsx | 8 +++++--- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index d1dc74af297..d6e1c3a190d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -589,7 +589,6 @@ export default tseslint.config([ // Turned off while upgrading eslint-plugin-react-hooks to v7, which added these to the recommended preset. // They are not meant to stay off; they will be enabled in follow-up changes. 'react-hooks/immutability': 'off', - 'react-hooks/preserve-manual-memoization': 'off', 'react-hooks/refs': 'off', 'react-hooks/set-state-in-effect': 'off', }, diff --git a/packages/mosaic/src/primitives/hooks/use-focus-target.ts b/packages/mosaic/src/primitives/hooks/use-focus-target.ts index f294d220f18..4468c955018 100644 --- a/packages/mosaic/src/primitives/hooks/use-focus-target.ts +++ b/packages/mosaic/src/primitives/hooks/use-focus-target.ts @@ -34,6 +34,7 @@ export function useInitialFocus( floatingContext: FloatingContext, ): ResolvedInitialFocus { const elementRef = React.useRef(null); + // eslint-disable-next-line react-hooks/preserve-manual-memoization -- this flags a real bug: openEvent is only updated through floatingContext.onOpenChange, so a controlled open after a controlled close passes initialFocus the previous interaction type return React.useMemo(() => { if (!open || initialFocus === undefined || initialFocus === true) { return 0; diff --git a/packages/mosaic/src/primitives/select/select-root.tsx b/packages/mosaic/src/primitives/select/select-root.tsx index 07462834642..60ce17039ea 100644 --- a/packages/mosaic/src/primitives/select/select-root.tsx +++ b/packages/mosaic/src/primitives/select/select-root.tsx @@ -132,7 +132,7 @@ function SelectInner(props: SelectProps) { } setOpen(next); }, - [setOpen], + [setOpen, setTouchOpen], ); const { diff --git a/packages/mosaic/src/primitives/toast/toast-root.tsx b/packages/mosaic/src/primitives/toast/toast-root.tsx index 93203713021..f13bcab8843 100644 --- a/packages/mosaic/src/primitives/toast/toast-root.tsx +++ b/packages/mosaic/src/primitives/toast/toast-root.tsx @@ -150,7 +150,8 @@ export const ToastRoot = React.forwardRef(functi const index = peers.indexOf(toast); const offsetY = peers.slice(0, Math.max(index, 0)).reduce((sum, t) => sum + (t.limited ? 0 : (t.height ?? 0)), 0); - const behind = frontmost !== undefined && peers.includes(frontmost) && index > peers.indexOf(frontmost); + const frontmostIndex = frontmost === undefined ? -1 : peers.indexOf(frontmost); + const behind = frontmostIndex !== -1 && index > frontmostIndex; const contextValue = useMemo( () => ({ toast, behind, rootId, titleId, descriptionId, setTitleId, setDescriptionId }), diff --git a/packages/ui/src/components/ConfigureDirectorySync/ConfigureDirectorySyncContext.tsx b/packages/ui/src/components/ConfigureDirectorySync/ConfigureDirectorySyncContext.tsx index b26438bf192..322cb581c03 100644 --- a/packages/ui/src/components/ConfigureDirectorySync/ConfigureDirectorySyncContext.tsx +++ b/packages/ui/src/components/ConfigureDirectorySync/ConfigureDirectorySyncContext.tsx @@ -131,16 +131,16 @@ export const ConfigureDirectorySyncProvider = ({ return rotated; }, [rotateDirectorySyncToken]); - const setDirectoryEnabled = React.useCallback( - (enabled: boolean) => updateDirectorySync({ enabled }), - [updateDirectorySync], - ); - const provider = directory?.provider ?? (connection ? directorySyncProviderForConnection(connection.provider) : undefined); const roleMapping = useRoleMapping(directory, updateDirectorySync); + const setDirectoryEnabled = React.useCallback( + (enabled: boolean) => updateDirectorySync({ enabled }), + [updateDirectorySync], + ); + const value: ConfigureDirectorySyncData = { isLoading: isLoadingConnections || (Boolean(enterpriseConnectionId) && isLoadingDirectory), connection, diff --git a/packages/ui/src/contexts/components/Plans.tsx b/packages/ui/src/contexts/components/Plans.tsx index 4d17c38455f..b6ede6fcbf3 100644 --- a/packages/ui/src/contexts/components/Plans.tsx +++ b/packages/ui/src/contexts/components/Plans.tsx @@ -341,6 +341,8 @@ export const usePlansContext = () => { [clerk, subscriberType, revalidateAll], ); + const sessionId = session?.id; + // handle the selection of a plan, either by opening the subscription details or checkout const handleSelectPlan = useCallback( ({ @@ -369,8 +371,8 @@ export const usePlansContext = () => { onSubscriptionComplete?.(); }, onClose: () => { - if (session?.id) { - void clerk.setActive({ session: session.id }); + if (sessionId) { + void clerk.setActive({ session: sessionId }); } }, appearance, @@ -378,7 +380,7 @@ export const usePlansContext = () => { newSubscriptionRedirectUrl, }); }, - [clerk, revalidateAll, subscriberType, session?.id], + [clerk, revalidateAll, subscriberType, sessionId], ); const defaultFreePlan = useMemo(() => { From 28a9bd3fb410865750fe2e6d9933dff86a29817b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20H=C3=B6glund?= Date: Thu, 29 Oct 2026 13:35:24 +0100 Subject: [PATCH 02/13] Fix the simple immutability eslint rule violations --- .../src/primitives/combobox/combobox-root.tsx | 3 +- .../src/primitives/table/table-root.tsx | 2 +- packages/nextjs/src/pages/ClerkProvider.tsx | 2 +- .../src/client/ReactRouterClerkProvider.tsx | 3 +- .../shared/src/react/query/useBaseQuery.ts | 11 +- .../SignIn/SignInFactorOneEmailLinkCard.tsx | 8 +- .../SignIn/SignInFactorTwoCodeForm.tsx | 16 +- .../SignIn/SignInFactorTwoEmailLinkCard.tsx | 8 +- .../ui/src/components/SignIn/SignInStart.tsx | 232 +++++++++--------- .../components/SignUp/SignUpEmailLinkCard.tsx | 8 +- .../VerifyWithEnterpriseConnection.tsx | 8 +- .../UserVerification/UVFactorOneCodeForm.tsx | 12 +- .../UserVerification/UVFactorTwoCodeForm.tsx | 16 +- packages/ui/src/elements/TimerButton.tsx | 12 +- .../ui/src/hooks/usePasswordComplexity.ts | 10 +- 15 files changed, 176 insertions(+), 175 deletions(-) diff --git a/packages/mosaic/src/primitives/combobox/combobox-root.tsx b/packages/mosaic/src/primitives/combobox/combobox-root.tsx index e41883e3653..39d6fd6a3ac 100644 --- a/packages/mosaic/src/primitives/combobox/combobox-root.tsx +++ b/packages/mosaic/src/primitives/combobox/combobox-root.tsx @@ -73,6 +73,8 @@ function ComboboxInner(props: ComboboxProps) { ), ); + const highlightFirstRef = useRef(false); + const setOpen = useCallback( (nextOpen: boolean) => { if (!nextOpen) { @@ -107,7 +109,6 @@ function ComboboxInner(props: ComboboxProps) { const popupRef = useRef(null); const triggerRef = useRef(null); const valuesByIndexRef = useRef>(new Map()); - const highlightFirstRef = useRef(false); const registerOption = useCallback((index: number) => { if (index === 0 && highlightFirstRef.current) { highlightFirstRef.current = false; diff --git a/packages/mosaic/src/primitives/table/table-root.tsx b/packages/mosaic/src/primitives/table/table-root.tsx index 6775870a6c9..11a9935de90 100644 --- a/packages/mosaic/src/primitives/table/table-root.tsx +++ b/packages/mosaic/src/primitives/table/table-root.tsx @@ -64,7 +64,7 @@ export const TableRoot = React.forwardRef(func const [columnCount, registerColumn] = useCounter(); const [rowHeaderColumns, registerRowHeaderColumn] = useRegistry(); const anchorRef = React.useRef(null); - const excluded = React.useMemo(() => new WeakSet(), []); + const [excluded] = React.useState(() => new WeakSet()); const { tree } = bodyRows; const selectable = React.useMemo( diff --git a/packages/nextjs/src/pages/ClerkProvider.tsx b/packages/nextjs/src/pages/ClerkProvider.tsx index 6d47886a470..13527e51075 100644 --- a/packages/nextjs/src/pages/ClerkProvider.tsx +++ b/packages/nextjs/src/pages/ClerkProvider.tsx @@ -18,11 +18,11 @@ import { ClerkScripts } from './ClerkScripts'; setErrorThrowerOptions({ packageName: PACKAGE_NAME }); setClerkJSLoadingErrorPackageName(PACKAGE_NAME); +ReactClerkProvider.displayName = 'ReactClerkProvider'; export function ClerkProvider({ children, ...props }: NextClerkProviderProps): JSX.Element { const { __internal_invokeMiddlewareOnAuthStateChange = true } = props; const { push, replace } = useRouter(); - ReactClerkProvider.displayName = 'ReactClerkProvider'; useSafeLayoutEffect(() => { window.__internal_onBeforeSetActive = invalidateNextRouterCache; diff --git a/packages/react-router/src/client/ReactRouterClerkProvider.tsx b/packages/react-router/src/client/ReactRouterClerkProvider.tsx index 3883b95ea55..b51704551b3 100644 --- a/packages/react-router/src/client/ReactRouterClerkProvider.tsx +++ b/packages/react-router/src/client/ReactRouterClerkProvider.tsx @@ -17,6 +17,8 @@ const SDK_METADATA = { version: PACKAGE_VERSION, }; +ReactClerkProvider.displayName = 'ReactClerkProvider'; + /** * React Router hydration errors should not stop Clerk navigation from working, as the components mount only after * hydration is done (in the case of a hydration error, the components will simply mount after client-side hydration). @@ -40,7 +42,6 @@ function ClerkProviderBase({ children, ...rest }: ClerkProv }, [awaitableNavigate]); const { clerkState, ...restProps } = rest; - ReactClerkProvider.displayName = 'ReactClerkProvider'; if (typeof isSpaMode !== 'undefined' && !isSpaMode) { assertValidClerkState(clerkState); diff --git a/packages/shared/src/react/query/useBaseQuery.ts b/packages/shared/src/react/query/useBaseQuery.ts index 2c044590b1d..a179f1e9265 100644 --- a/packages/shared/src/react/query/useBaseQuery.ts +++ b/packages/shared/src/react/query/useBaseQuery.ts @@ -25,12 +25,13 @@ export function useBaseQuery, CommonQueryResult> { const [client, isQueryClientLoaded] = useClerkQueryClient(); - const defaultedOptions = isQueryClientLoaded - ? client.defaultQueryOptions(options) - : (options as DefaultedQueryObserverOptions); - // Make sure results are optimistically set in fetching state before subscribing or updating options - defaultedOptions._optimisticResults = 'optimistic'; + const defaultedOptions: DefaultedQueryObserverOptions = { + ...(isQueryClientLoaded + ? client.defaultQueryOptions(options) + : (options as DefaultedQueryObserverOptions)), + _optimisticResults: 'optimistic', + }; const observer = React.useMemo(() => { return new Observer(client, defaultedOptions); diff --git a/packages/ui/src/components/SignIn/SignInFactorOneEmailLinkCard.tsx b/packages/ui/src/components/SignIn/SignInFactorOneEmailLinkCard.tsx index 382b86207dd..192084d36fc 100644 --- a/packages/ui/src/components/SignIn/SignInFactorOneEmailLinkCard.tsx +++ b/packages/ui/src/components/SignIn/SignInFactorOneEmailLinkCard.tsx @@ -35,10 +35,6 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard const [switchTabStatus, setSwitchTabStatus] = React.useState<'verified_switch_tab' | 'transferable' | null>(null); const clerk = useClerk(); - React.useEffect(() => { - void startEmailLinkVerification(); - }, []); - const restartVerification = () => { cancelEmailLinkFlow(); void startEmailLinkVerification(); @@ -118,6 +114,10 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard ); } + React.useEffect(() => { + void startEmailLinkVerification(); + }, []); + return ( return signIn.clientTrustState === 'new' && !anyAttributeUsedForSecondFactor; }, [signIn.clientTrustState, env.userSettings.attributes]); - React.useEffect(() => { - if (props.factorAlreadyPrepared) { - return; - } - - void prepare?.(); - }, []); - const prepare = props.prepare ? () => { return props @@ -113,6 +105,14 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => }); }; + React.useEffect(() => { + if (props.factorAlreadyPrepared) { + return; + } + + void prepare?.(); + }, []); + return ( { - void startEmailLinkVerification(); - }, []); - const restartVerification = () => { cancelEmailLinkFlow(); void startEmailLinkVerification(); @@ -93,6 +89,10 @@ export const SignInFactorTwoEmailLinkCard = (props: SignInFactorTwoEmailLinkCard ); } + React.useEffect(() => { + void startEmailLinkVerification(); + }, []); + return ( { - if (!organizationTicket) { - return; - } - - if (clerkStatus === 'sign_up') { - const paramsToForward = new URLSearchParams(); - if (organizationTicket) { - paramsToForward.set('__clerk_ticket', organizationTicket); - } - // We explicitly navigate to 'create' in the combined flow to trigger a client-side navigation. Navigating to - // signUpUrl triggers a full page reload when used with the hash router. - void navigate(isCombinedFlow ? `create` : signUpUrl, { searchParams: paramsToForward }); - return; - } - - status.setLoading(); - card.setLoading(); - signIn - .create({ - strategy: 'ticket', - ticket: organizationTicket, - }) - .then(res => { - if (navigateOnSignInProtectGate(res, navigate, 'protect-check')) { - return; - } - switch (res.status) { - case 'needs_first_factor': { - if (!shouldHandOffToEnterpriseConnection(res)) { - return navigate('factor-one'); - } - - return authenticateWithEnterpriseSSO(); - } - case 'needs_second_factor': - return navigate('factor-two'); - case 'needs_client_trust': - return navigate('client-trust'); - case 'complete': - removeClerkQueryParam('__clerk_ticket'); - return clerk.setActive({ - session: res.createdSessionId, - navigate: async ({ session, decorateUrl }) => { - await navigateOnSetActive({ session, redirectUrl: afterSignInUrl, decorateUrl }); - }, - }); - default: { - console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); - return; - } - } - }) - .catch(err => { - return attemptToRecoverFromSignInError(err); - }) - .finally(() => { - // Keep the card in loading state during SSO redirect to prevent UI flicker - // This is necessary because there's a brief delay between initiating the SSO flow - // and the actual redirect to the external Identity Provider - const isRedirectingToSSOProvider = !!hasOnlyEnterpriseSSOFirstFactors(signIn); - if (isRedirectingToSSOProvider) { - return; - } - - status.setIdle(); - card.setIdle(); - }); - }, []); - - useEffect(() => { - async function handleOauthError() { - const defaultErrorHandler = () => { - // Error from server may be too much information for the end user, so set a generic error - card.setError('Unable to complete action at this time. If the problem persists please contact support.'); - }; - - const error = signIn?.firstFactorVerification?.error; - if (error) { - switch (error.code) { - case ERROR_CODES.NOT_ALLOWED_TO_SIGN_UP: - case ERROR_CODES.OAUTH_ACCESS_DENIED: - case ERROR_CODES.NOT_ALLOWED_ACCESS: - case ERROR_CODES.SAML_USER_ATTRIBUTE_MISSING: - case ERROR_CODES.OAUTH_EMAIL_DOMAIN_RESERVED_BY_SAML: - case ERROR_CODES.USER_LOCKED: - case ERROR_CODES.EXTERNAL_ACCOUNT_NOT_FOUND: - case ERROR_CODES.SIGN_UP_MODE_RESTRICTED: - case ERROR_CODES.SIGN_UP_MODE_RESTRICTED_WAITLIST: - case ERROR_CODES.ENTERPRISE_SSO_USER_ATTRIBUTE_MISSING: - case ERROR_CODES.ENTERPRISE_SSO_EMAIL_ADDRESS_DOMAIN_MISMATCH: - case ERROR_CODES.ENTERPRISE_SSO_HOSTED_DOMAIN_MISMATCH: - case ERROR_CODES.SAML_EMAIL_ADDRESS_DOMAIN_MISMATCH: - case ERROR_CODES.ORGANIZATION_MEMBERSHIP_QUOTA_EXCEEDED_FOR_SSO: - case ERROR_CODES.CAPTCHA_INVALID: - case ERROR_CODES.FRAUD_DEVICE_BLOCKED: - case ERROR_CODES.FRAUD_ACTION_BLOCKED: - case ERROR_CODES.SIGNUP_RATE_LIMIT_EXCEEDED: - case ERROR_CODES.USER_BANNED: - case ERROR_CODES.USER_DEACTIVATED: - card.setError(error); - break; - default: - defaultErrorHandler(); - } - - // TODO: This is a workaround in order to reset the sign in attempt - // so that the oauth error does not persist on full page reloads. - void (await signIn.create({})); - } - } - - void handleOauthError(); - }, []); - const buildSignInParams = (fields: Array>): SignInCreateParams => { const hasPassword = fields.some(f => f.name === 'password' && !!f.value); @@ -532,8 +417,6 @@ function SignInStartInternal(): JSX.Element { return navigate(waitlistUrl); } - clerk.client.signUp[attribute] = identifierField.value; - const redirectUrl = ctx.ssoCallbackUrl; const redirectUrlComplete = ctx.afterSignUpUrl || '/'; @@ -565,6 +448,121 @@ function SignInStartInternal(): JSX.Element { } }; + useEffect(() => { + if (!organizationTicket) { + return; + } + + if (clerkStatus === 'sign_up') { + const paramsToForward = new URLSearchParams(); + if (organizationTicket) { + paramsToForward.set('__clerk_ticket', organizationTicket); + } + // We explicitly navigate to 'create' in the combined flow to trigger a client-side navigation. Navigating to + // signUpUrl triggers a full page reload when used with the hash router. + void navigate(isCombinedFlow ? `create` : signUpUrl, { searchParams: paramsToForward }); + return; + } + + status.setLoading(); + card.setLoading(); + signIn + .create({ + strategy: 'ticket', + ticket: organizationTicket, + }) + .then(res => { + if (navigateOnSignInProtectGate(res, navigate, 'protect-check')) { + return; + } + switch (res.status) { + case 'needs_first_factor': { + if (!shouldHandOffToEnterpriseConnection(res)) { + return navigate('factor-one'); + } + + return authenticateWithEnterpriseSSO(); + } + case 'needs_second_factor': + return navigate('factor-two'); + case 'needs_client_trust': + return navigate('client-trust'); + case 'complete': + removeClerkQueryParam('__clerk_ticket'); + return clerk.setActive({ + session: res.createdSessionId, + navigate: async ({ session, decorateUrl }) => { + await navigateOnSetActive({ session, redirectUrl: afterSignInUrl, decorateUrl }); + }, + }); + default: { + console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); + return; + } + } + }) + .catch(err => { + return attemptToRecoverFromSignInError(err); + }) + .finally(() => { + // Keep the card in loading state during SSO redirect to prevent UI flicker + // This is necessary because there's a brief delay between initiating the SSO flow + // and the actual redirect to the external Identity Provider + const isRedirectingToSSOProvider = !!hasOnlyEnterpriseSSOFirstFactors(signIn); + if (isRedirectingToSSOProvider) { + return; + } + + status.setIdle(); + card.setIdle(); + }); + }, []); + + useEffect(() => { + async function handleOauthError() { + const defaultErrorHandler = () => { + // Error from server may be too much information for the end user, so set a generic error + card.setError('Unable to complete action at this time. If the problem persists please contact support.'); + }; + + const error = signIn?.firstFactorVerification?.error; + if (error) { + switch (error.code) { + case ERROR_CODES.NOT_ALLOWED_TO_SIGN_UP: + case ERROR_CODES.OAUTH_ACCESS_DENIED: + case ERROR_CODES.NOT_ALLOWED_ACCESS: + case ERROR_CODES.SAML_USER_ATTRIBUTE_MISSING: + case ERROR_CODES.OAUTH_EMAIL_DOMAIN_RESERVED_BY_SAML: + case ERROR_CODES.USER_LOCKED: + case ERROR_CODES.EXTERNAL_ACCOUNT_NOT_FOUND: + case ERROR_CODES.SIGN_UP_MODE_RESTRICTED: + case ERROR_CODES.SIGN_UP_MODE_RESTRICTED_WAITLIST: + case ERROR_CODES.ENTERPRISE_SSO_USER_ATTRIBUTE_MISSING: + case ERROR_CODES.ENTERPRISE_SSO_EMAIL_ADDRESS_DOMAIN_MISMATCH: + case ERROR_CODES.ENTERPRISE_SSO_HOSTED_DOMAIN_MISMATCH: + case ERROR_CODES.SAML_EMAIL_ADDRESS_DOMAIN_MISMATCH: + case ERROR_CODES.ORGANIZATION_MEMBERSHIP_QUOTA_EXCEEDED_FOR_SSO: + case ERROR_CODES.CAPTCHA_INVALID: + case ERROR_CODES.FRAUD_DEVICE_BLOCKED: + case ERROR_CODES.FRAUD_ACTION_BLOCKED: + case ERROR_CODES.SIGNUP_RATE_LIMIT_EXCEEDED: + case ERROR_CODES.USER_BANNED: + case ERROR_CODES.USER_DEACTIVATED: + card.setError(error); + break; + default: + defaultErrorHandler(); + } + + // TODO: This is a workaround in order to reset the sign in attempt + // so that the oauth error does not persist on full page reloads. + void (await signIn.create({})); + } + } + + void handleOauthError(); + }, []); + const handleFirstPartySubmit = async (e: React.FormEvent) => { e.preventDefault(); return signInWithFields([identifierField, instantPasswordField]); diff --git a/packages/ui/src/components/SignUp/SignUpEmailLinkCard.tsx b/packages/ui/src/components/SignUp/SignUpEmailLinkCard.tsx index 34a9767cf40..a53ec35d296 100644 --- a/packages/ui/src/components/SignUp/SignUpEmailLinkCard.tsx +++ b/packages/ui/src/components/SignUp/SignUpEmailLinkCard.tsx @@ -21,10 +21,6 @@ export const SignUpEmailLinkCard = () => { const { startEmailLinkFlow, cancelEmailLinkFlow } = useEmailLink(signUp); - React.useEffect(() => { - void startEmailLinkVerification(); - }, []); - const restartVerification = () => { cancelEmailLinkFlow(); void startEmailLinkVerification(); @@ -67,6 +63,10 @@ export const SignUpEmailLinkCard = () => { ); } + React.useEffect(() => { + void startEmailLinkVerification(); + }, []); + return ( { - startVerification(); - }, []); - function startVerification() { const { mode, componentName } = profileContext; @@ -43,6 +39,10 @@ export const VerifyWithEnterpriseConnection = (props: VerifyWithEnterpriseConnec await navigate(email.verification.externalVerificationRedirectURL?.href || ''); } + React.useEffect(() => { + startVerification(); + }, []); + return ( <> diff --git a/packages/ui/src/components/UserVerification/UVFactorOneCodeForm.tsx b/packages/ui/src/components/UserVerification/UVFactorOneCodeForm.tsx index 3f18d9fb364..1f4e21b0a4f 100644 --- a/packages/ui/src/components/UserVerification/UVFactorOneCodeForm.tsx +++ b/packages/ui/src/components/UserVerification/UVFactorOneCodeForm.tsx @@ -33,12 +33,6 @@ export const UVFactorOneCodeForm = (props: UVFactorOneCodeFormProps) => { const { handleVerificationResponse } = useAfterVerification(); - React.useEffect(() => { - if (!props.factorAlreadyPrepared) { - prepare(); - } - }, []); - const prepare = () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion void session! @@ -58,6 +52,12 @@ export const UVFactorOneCodeForm = (props: UVFactorOneCodeFormProps) => { .catch(reject); }; + React.useEffect(() => { + if (!props.factorAlreadyPrepared) { + prepare(); + } + }, []); + return ( { const { session } = useSession(); const { handleVerificationResponse } = useAfterVerification(); - React.useEffect(() => { - if (props.factorAlreadyPrepared) { - return; - } - - void prepare?.(); - }, []); - const prepare = props.prepare ? () => props @@ -58,6 +50,14 @@ export const UVFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => { .catch(reject); }; + React.useEffect(() => { + if (props.factorAlreadyPrepared) { + return; + } + + void prepare?.(); + }, []); + return ( { const [remainingSeconds, setRemainingSeconds] = React.useState(0); const intervalIdRef = React.useRef(undefined); - useSafeLayoutEffect(() => { - if (startDisabled) { - disable(); - } - }, []); - useEffect(() => { return () => clearInterval(intervalIdRef.current); }, []); @@ -54,6 +48,12 @@ export const TimerButton = (props: TimerButtonProps) => { disable(); }; + useSafeLayoutEffect(() => { + if (startDisabled) { + disable(); + } + }, []); + return (