Skip to content

Commit

Permalink
feat(clerk-js,shared): Add a navigateWithError utility for SignIn
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark Pitsilos committed Nov 6, 2023
1 parent 791c498 commit 58b8c53
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 7 deletions.
6 changes: 6 additions & 0 deletions .changeset/spotty-apples-march.md
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
BeforeEmitCallback,
BuildUrlWithAuthParams,
Clerk as ClerkInterface,
ClerkAPIError,
ClerkOptions,
ClientResource,
CreateOrganizationParams,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions packages/clerk-js/src/core/resources/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
isKnownError,
isMagicLinkError,
isMetamaskError,
isUserLockedError,
MagicLinkError,
MagicLinkErrorCode,
parseError,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isUserLockedError } from '@clerk/shared';
import type { EmailCodeFactor, PhoneCodeFactor, ResetPasswordCodeFactor } from '@clerk/types';
import React from 'react';

Expand Down Expand Up @@ -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('../');
Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isUserLockedError } from '@clerk/shared/error';
import type { EmailLinkFactor, SignInResource } from '@clerk/types';
import React from 'react';

Expand Down Expand Up @@ -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();
Expand All @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isUserLockedError } from '@clerk/shared/error';
import type { ResetPasswordCodeFactor } from '@clerk/types';
import React from 'react';

Expand Down Expand Up @@ -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('../');
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isUserLockedError } from '@clerk/shared/error';
import type { SignInResource } from '@clerk/types';
import React from 'react';

Expand Down Expand Up @@ -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) &&
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isUserLockedError } from '@clerk/shared/error';
import type { PhoneCodeFactor, SignInResource, TOTPFactor } from '@clerk/types';
import React from 'react';

Expand Down Expand Up @@ -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) {
Expand All @@ -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;

Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ type CardStateCtxValue = {
const [CardStateCtx, _useCardState] = createContextAndHook<CardStateCtxValue>('CardState');

const CardStateProvider = (props: React.PropsWithChildren<any>) => {
const { translateError } = useLocalizations();

const [state, setState] = useSafeState<State>({
status: 'idle',
metadata: undefined,
error: undefined,
error: translateError(window?.Clerk?.__internal_last_error || undefined),
});

const value = React.useMemo(() => ({ value: { state, setState } }), [state, setState]);
Expand Down
9 changes: 9 additions & 0 deletions packages/shared/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) : [];
}
Expand Down Expand Up @@ -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;
}

Expand Down

0 comments on commit 58b8c53

Please sign in to comment.