From 58b8c53b89ca6445ea0b05cb80df272c77616cf7 Mon Sep 17 00:00:00 2001 From: Mark Pitsilos Date: Fri, 3 Nov 2023 23:48:04 +0200 Subject: [PATCH] feat(clerk-js,shared): Add a navigateWithError utility for SignIn --- .changeset/spotty-apples-march.md | 6 ++++++ packages/clerk-js/src/core/clerk.ts | 18 +++++++++++++++++ packages/clerk-js/src/core/resources/Error.ts | 1 + .../SignIn/SignInFactorOneCodeForm.tsx | 11 +++++++++- .../SignIn/SignInFactorOneEmailLinkCard.tsx | 11 +++++++++- .../SignIn/SignInFactorOnePasswordCard.tsx | 11 +++++++++- .../SignIn/SignInFactorTwoBackupCodeCard.tsx | 11 +++++++++- .../SignIn/SignInFactorTwoCodeForm.tsx | 20 +++++++++++++++++-- .../ui/elements/contexts/CardStateContext.tsx | 4 +++- packages/shared/src/error.ts | 9 +++++++++ 10 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 .changeset/spotty-apples-march.md diff --git a/.changeset/spotty-apples-march.md b/.changeset/spotty-apples-march.md new file mode 100644 index 00000000000..c8f1e1be1c0 --- /dev/null +++ b/.changeset/spotty-apples-march.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +--- + +Add a private \_\_navigateWithError util function to clerk for use in User Lockout scenarios diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 1f7e1d81cc3..c72eefc93b0 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -20,6 +20,7 @@ import type { BeforeEmitCallback, BuildUrlWithAuthParams, Clerk as ClerkInterface, + ClerkAPIError, ClerkOptions, ClientResource, CreateOrganizationParams, @@ -161,6 +162,8 @@ export default class Clerk implements ClerkInterface { public readonly frontendApi: string; public readonly publishableKey?: string; + protected internal_last_error: ClerkAPIError | null = null; + #domain: DomainOrProxyUrl['domain']; #proxyUrl: DomainOrProxyUrl['proxyUrl']; #authService: SessionCookieService | null = null; @@ -1187,6 +1190,16 @@ export default class Clerk implements ClerkInterface { } }; + get __internal_last_error(): ClerkAPIError | null { + const value = this.internal_last_error; + this.internal_last_error = null; + return value; + } + + set __internal_last_error(value: ClerkAPIError | null) { + this.internal_last_error = value; + } + updateClient = (newClient: ClientResource): void => { if (!this.client) { // This is the first time client is being @@ -1260,6 +1273,11 @@ export default class Clerk implements ClerkInterface { return this.#componentControls?.ensureMounted().then(controls => controls.updateProps(props)); }; + __internal_navigateWithError(to: string, err: ClerkAPIError) { + this.__internal_last_error = err; + return this.navigate(to); + } + #hasJustSynced = () => getClerkQueryParam(CLERK_SYNCED) === 'true'; #clearJustSynced = () => removeClerkQueryParam(CLERK_SYNCED); diff --git a/packages/clerk-js/src/core/resources/Error.ts b/packages/clerk-js/src/core/resources/Error.ts index a8baf8742af..80ff047b8a5 100644 --- a/packages/clerk-js/src/core/resources/Error.ts +++ b/packages/clerk-js/src/core/resources/Error.ts @@ -9,6 +9,7 @@ export { isKnownError, isMagicLinkError, isMetamaskError, + isUserLockedError, MagicLinkError, MagicLinkErrorCode, parseError, diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx index d6b0a9aa59c..0df94eeff54 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneCodeForm.tsx @@ -1,3 +1,4 @@ +import { isUserLockedError } from '@clerk/shared'; import type { EmailCodeFactor, PhoneCodeFactor, ResetPasswordCodeFactor } from '@clerk/types'; import React from 'react'; @@ -34,6 +35,7 @@ export const SignInFactorOneCodeForm = (props: SignInFactorOneCodeFormProps) => const { navigateAfterSignIn } = useSignInContext(); const { setActive } = useCoreClerk(); const supportEmail = useSupportEmail(); + const clerk = useCoreClerk(); const goBack = () => { return navigate('../'); @@ -69,7 +71,14 @@ export const SignInFactorOneCodeForm = (props: SignInFactorOneCodeFormProps) => return console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); } }) - .catch(err => reject(err)); + .catch(err => { + if (isUserLockedError(err)) { + // @ts-expect-error -- private method for the time being + return clerk.__internal_navigateWithError('..', err.errors[0]); + } + + reject(err); + }); }; return ( diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx index 44acdc7086d..dea2ef6134a 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx @@ -1,3 +1,4 @@ +import { isUserLockedError } from '@clerk/shared/error'; import type { EmailLinkFactor, SignInResource } from '@clerk/types'; import React from 'react'; @@ -29,6 +30,7 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard const { setActive } = useCoreClerk(); const { startEmailLinkFlow, cancelEmailLinkFlow } = useEmailLink(signIn); const [showVerifyModal, setShowVerifyModal] = React.useState(false); + const clerk = useCoreClerk(); React.useEffect(() => { void startEmailLinkVerification(); @@ -45,7 +47,14 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard redirectUrl: buildEmailLinkRedirectUrl(signInContext, signInUrl), }) .then(res => handleVerificationResult(res)) - .catch(err => handleError(err, [], card.setError)); + .catch(err => { + if (isUserLockedError(err)) { + // @ts-expect-error -- private method for the time being + return clerk.__internal_navigateWithError('..', err.errors[0]); + } + + handleError(err, [], card.setError); + }); }; const handleVerificationResult = async (si: SignInResource) => { diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx index a6cb43b79a9..37e697949f9 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx @@ -1,3 +1,4 @@ +import { isUserLockedError } from '@clerk/shared/error'; import type { ResetPasswordCodeFactor } from '@clerk/types'; import React from 'react'; @@ -53,6 +54,7 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) const { navigate } = useRouter(); const [showHavingTrouble, setShowHavingTrouble] = React.useState(false); const toggleHavingTrouble = React.useCallback(() => setShowHavingTrouble(s => !s), [setShowHavingTrouble]); + const clerk = useCoreClerk(); const goBack = () => { return navigate('../'); @@ -72,7 +74,14 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) return console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); } }) - .catch(err => handleError(err, [passwordControl], card.setError)); + .catch(err => { + if (isUserLockedError(err)) { + // @ts-expect-error -- private method for the time being + return clerk.__internal_navigateWithError('..', err.errors[0]); + } + + handleError(err, [passwordControl], card.setError); + }); }; if (showHavingTrouble) { diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx index 8eebbb8c36c..e368b6288eb 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoBackupCodeCard.tsx @@ -1,3 +1,4 @@ +import { isUserLockedError } from '@clerk/shared/error'; import type { SignInResource } from '@clerk/types'; import React from 'react'; @@ -28,6 +29,7 @@ export const SignInFactorTwoBackupCodeCard = (props: SignInFactorTwoBackupCodeCa label: localizationKeys('formFieldLabel__backupCode'), isRequired: true, }); + const clerk = useCoreClerk(); const isResettingPassword = (resource: SignInResource) => isResetPasswordStrategy(resource.firstFactorVerification?.strategy) && @@ -50,7 +52,14 @@ export const SignInFactorTwoBackupCodeCard = (props: SignInFactorTwoBackupCodeCa return console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); } }) - .catch(err => handleError(err, [codeControl], card.setError)); + .catch(err => { + if (isUserLockedError(err)) { + // @ts-expect-error -- private method for the time being + return clerk.__internal_navigateWithError('..', err.errors[0]); + } + + handleError(err, [codeControl], card.setError); + }); }; return ( diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoCodeForm.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoCodeForm.tsx index 52ea37786b7..bc9aa8ba951 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoCodeForm.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorTwoCodeForm.tsx @@ -1,3 +1,4 @@ +import { isUserLockedError } from '@clerk/shared/error'; import type { PhoneCodeFactor, SignInResource, TOTPFactor } from '@clerk/types'; import React from 'react'; @@ -34,6 +35,7 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => const { setActive } = useCoreClerk(); const { navigate } = useRouter(); const supportEmail = useSupportEmail(); + const clerk = useCoreClerk(); React.useEffect(() => { if (props.factorAlreadyPrepared) { @@ -48,7 +50,14 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => return props .prepare?.() .then(() => props.onFactorPrepare()) - .catch(err => handleError(err, [], card.setError)); + .catch(err => { + if (isUserLockedError(err)) { + // @ts-expect-error -- private method for the time being + return clerk.__internal_navigateWithError('..', err.errors[0]); + } + + handleError(err, [], card.setError); + }); } : undefined; @@ -73,7 +82,14 @@ export const SignInFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => return console.error(clerkInvalidFAPIResponse(res.status, supportEmail)); } }) - .catch(err => reject(err)); + .catch(err => { + if (isUserLockedError(err)) { + // @ts-expect-error -- private method for the time being + return clerk.__internal_navigateWithError('..', err.errors[0]); + } + + reject(err); + }); }; return ( diff --git a/packages/clerk-js/src/ui/elements/contexts/CardStateContext.tsx b/packages/clerk-js/src/ui/elements/contexts/CardStateContext.tsx index 744c080e539..d974c12402c 100644 --- a/packages/clerk-js/src/ui/elements/contexts/CardStateContext.tsx +++ b/packages/clerk-js/src/ui/elements/contexts/CardStateContext.tsx @@ -16,10 +16,12 @@ type CardStateCtxValue = { const [CardStateCtx, _useCardState] = createContextAndHook('CardState'); const CardStateProvider = (props: React.PropsWithChildren) => { + const { translateError } = useLocalizations(); + const [state, setState] = useSafeState({ status: 'idle', metadata: undefined, - error: undefined, + error: translateError(window?.Clerk?.__internal_last_error || undefined), }); const value = React.useMemo(() => ({ value: { state, setState } }), [state, setState]); diff --git a/packages/shared/src/error.ts b/packages/shared/src/error.ts index 38cb1927d33..9ac224afe7b 100644 --- a/packages/shared/src/error.ts +++ b/packages/shared/src/error.ts @@ -65,6 +65,10 @@ export function isMetamaskError(err: any): err is MetamaskError { return 'code' in err && [4001, 32602, 32603].includes(err.code) && 'message' in err; } +export function isUserLockedError(err: any) { + return isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'user_locked'; +} + export function parseErrors(data: ClerkAPIErrorJSON[] = []): ClerkAPIError[] { return data.length > 0 ? data.map(parseError) : []; } @@ -242,10 +246,15 @@ export type ErrorThrowerOptions = { export interface ErrorThrower { setPackageName(options: ErrorThrowerOptions): ErrorThrower; + setMessages(options: ErrorThrowerOptions): ErrorThrower; + throwInvalidPublishableKeyError(params: { key?: string }): never; + throwInvalidFrontendApiError(params: { key?: string }): never; + throwInvalidProxyUrl(params: { url?: string }): never; + throwMissingPublishableKeyError(): never; }