Skip to content

Commit c71cf42

Browse files
committed
feat(clerk-js): Introduce 'passwordUntrusted' flow
1 parent c9efd21 commit c71cf42

File tree

8 files changed

+67
-13
lines changed

8 files changed

+67
-13
lines changed

packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { SignInSocialButtons } from './SignInSocialButtons';
1818
import { useResetPasswordFactor } from './useResetPasswordFactor';
1919
import { withHavingTrouble } from './withHavingTrouble';
2020

21-
type AlternativeMethodsMode = 'forgot' | 'pwned' | 'default';
21+
export type AlternativeMethodsMode = 'forgot' | 'pwned' | 'passwordUntrusted' | 'default';
2222

2323
export type AlternativeMethodsProps = {
2424
onBackLinkClick: React.MouseEventHandler | undefined;
@@ -55,7 +55,9 @@ const AlternativeMethodsList = (props: AlternativeMethodListProps) => {
5555
<Card.Content>
5656
<Header.Root showLogo>
5757
<Header.Title localizationKey={cardTitleKey} />
58-
{!isReset && <Header.Subtitle localizationKey={localizationKeys('signIn.alternativeMethods.subtitle')} />}
58+
{!isReset && mode !== 'passwordUntrusted' && (
59+
<Header.Subtitle localizationKey={localizationKeys('signIn.alternativeMethods.subtitle')} />
60+
)}
5961
</Header.Root>
6062
<Card.Alert>{card.error}</Card.Alert>
6163
{/*TODO: extract main in its own component */}
@@ -183,6 +185,8 @@ function determineFlowPart(mode: AlternativeMethodsMode) {
183185
return 'forgotPasswordMethods';
184186
case 'pwned':
185187
return 'passwordPwnedMethods';
188+
case 'passwordUntrusted':
189+
return 'passwordUntrustedMethods';
186190
default:
187191
return 'alternativeMethods';
188192
}
@@ -194,6 +198,8 @@ function determineTitle(mode: AlternativeMethodsMode): LocalizationKey {
194198
return localizationKeys('signIn.forgotPasswordAlternativeMethods.title');
195199
case 'pwned':
196200
return localizationKeys('signIn.passwordPwned.title');
201+
case 'passwordUntrusted':
202+
return localizationKeys('signIn.passwordPwned.title');
197203
default:
198204
return localizationKeys('signIn.alternativeMethods.title');
199205
}
@@ -204,6 +210,8 @@ function determineIsReset(mode: AlternativeMethodsMode): boolean {
204210
case 'forgot':
205211
case 'pwned':
206212
return true;
213+
case 'passwordUntrusted':
214+
return false;
207215
default:
208216
return false;
209217
}

packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useCoreSignIn, useEnvironment } from '../../contexts';
1111
import { useAlternativeStrategies } from '../../hooks/useAlternativeStrategies';
1212
import { localizationKeys } from '../../localization';
1313
import { useRouter } from '../../router';
14+
import type { AlternativeMethodsMode } from './AlternativeMethods';
1415
import { AlternativeMethods } from './AlternativeMethods';
1516
import { hasMultipleEnterpriseConnections } from './shared';
1617
import { SignInFactorOneAlternativePhoneCodeCard } from './SignInFactorOneAlternativePhoneCodeCard';
@@ -41,6 +42,25 @@ const factorKey = (factor: SignInFactor | null | undefined) => {
4142
return key;
4243
};
4344

45+
function determineAlternativeMethodsMode(
46+
showForgotPasswordStrategies: boolean,
47+
untrustedPasswordErrorCode: string | null,
48+
): AlternativeMethodsMode {
49+
if (!showForgotPasswordStrategies) {
50+
return 'default';
51+
}
52+
53+
if (untrustedPasswordErrorCode === 'form_password_pwned__sign_in') {
54+
return 'pwned';
55+
}
56+
57+
if (untrustedPasswordErrorCode === 'form_password_untrusted__sign_in') {
58+
return 'passwordUntrusted';
59+
}
60+
61+
return 'forgot';
62+
}
63+
4464
function SignInFactorOneInternal(): JSX.Element {
4565
const { __internal_setActiveInProgress } = useClerk();
4666
const signIn = useCoreSignIn();
@@ -84,7 +104,7 @@ function SignInFactorOneInternal(): JSX.Element {
84104

85105
const [showForgotPasswordStrategies, setShowForgotPasswordStrategies] = React.useState(false);
86106

87-
const [isPasswordPwned, setIsPasswordPwned] = React.useState(false);
107+
const [untrustedPasswordErrorCode, setUntrustedPasswordErrorCode] = React.useState<string | null>(null);
88108

89109
React.useEffect(() => {
90110
if (__internal_setActiveInProgress) {
@@ -139,11 +159,11 @@ function SignInFactorOneInternal(): JSX.Element {
139159
const toggle = showAllStrategies ? toggleAllStrategies : toggleForgotPasswordStrategies;
140160
const backHandler = () => {
141161
card.setError(undefined);
142-
setIsPasswordPwned(false);
162+
setUntrustedPasswordErrorCode(null);
143163
toggle?.();
144164
};
145165

146-
const mode = showForgotPasswordStrategies ? (isPasswordPwned ? 'pwned' : 'forgot') : 'default';
166+
const mode = determineAlternativeMethodsMode(showForgotPasswordStrategies, untrustedPasswordErrorCode);
147167

148168
return (
149169
<AlternativeMethods
@@ -175,8 +195,8 @@ function SignInFactorOneInternal(): JSX.Element {
175195
<SignInFactorOnePasswordCard
176196
onForgotPasswordMethodClick={resetPasswordFactor ? toggleForgotPasswordStrategies : toggleAllStrategies}
177197
onShowAlternativeMethodsClick={toggleAllStrategies}
178-
onPasswordPwned={() => {
179-
setIsPasswordPwned(true);
198+
onUntrustedPassword={errorCode => {
199+
setUntrustedPasswordErrorCode(errorCode);
180200
toggleForgotPasswordStrategies();
181201
}}
182202
/>

packages/clerk-js/src/ui/components/SignIn/SignInFactorOnePasswordCard.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isPasswordPwnedError, isUserLockedError } from '@clerk/shared/error';
1+
import { isPasswordPwnedError, isPasswordUntrustedError, isUserLockedError } from '@clerk/shared/error';
22
import { useClerk } from '@clerk/shared/react';
33
import React from 'react';
44

@@ -21,7 +21,7 @@ import { useResetPasswordFactor } from './useResetPasswordFactor';
2121
type SignInFactorOnePasswordProps = {
2222
onForgotPasswordMethodClick: React.MouseEventHandler | undefined;
2323
onShowAlternativeMethodsClick: React.MouseEventHandler | undefined;
24-
onPasswordPwned?: () => void;
24+
onUntrustedPassword?: (errorCode: string) => void;
2525
};
2626

2727
const usePasswordControl = (props: SignInFactorOnePasswordProps) => {
@@ -50,7 +50,7 @@ const usePasswordControl = (props: SignInFactorOnePasswordProps) => {
5050
};
5151

5252
export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps) => {
53-
const { onShowAlternativeMethodsClick, onPasswordPwned } = props;
53+
const { onShowAlternativeMethodsClick, onUntrustedPassword } = props;
5454
const passwordInputRef = React.useRef<HTMLInputElement>(null);
5555
const card = useCardState();
5656
const { setActive } = useClerk();
@@ -92,9 +92,18 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps)
9292
return clerk.__internal_navigateWithError('..', err.errors[0]);
9393
}
9494

95-
if (isPasswordPwnedError(err) && onPasswordPwned) {
96-
card.setError({ ...err.errors[0], code: 'form_password_pwned__sign_in' });
97-
onPasswordPwned();
95+
if (onUntrustedPassword) {
96+
// TODO(vaggelis): those will eventually be unified into a single error code
97+
if (isPasswordPwnedError(err)) {
98+
card.setError({ ...err.errors[0], code: 'form_password_pwned__sign_in' });
99+
onUntrustedPassword('form_password_pwned__sign_in');
100+
return;
101+
}
102+
if (isPasswordUntrustedError(err)) {
103+
card.setError({ ...err.errors[0], code: 'form_password_untrusted__sign_in' });
104+
onUntrustedPassword('form_password_untrusted__sign_in');
105+
return;
106+
}
98107
return;
99108
}
100109

packages/clerk-js/src/ui/elements/contexts/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export type FlowMetadata = {
120120
| 'alternativeMethods'
121121
| 'forgotPasswordMethods'
122122
| 'passwordPwnedMethods'
123+
| 'passwordUntrustedMethods'
123124
| 'havingTrouble'
124125
| 'ssoCallback'
125126
| 'popupCallback'

packages/localizations/src/en-GB.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,9 @@ export const enGB: LocalizationResource = {
697697
passwordPwned: {
698698
title: 'Password compromised',
699699
},
700+
passwordUntrusted: {
701+
title: 'Password compromised',
702+
},
700703
phoneCode: {
701704
formTitle: 'Verification code',
702705
resendButton: "Didn't receive a code? Resend",

packages/shared/src/error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export {
2424
isMetamaskError,
2525
isNetworkError,
2626
isPasswordPwnedError,
27+
isPasswordUntrustedError,
2728
isReverificationCancelledError,
2829
isUnauthorizedError,
2930
isUserLockedError,

packages/shared/src/errors/helpers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ export function isPasswordPwnedError(err: any) {
120120
return isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'form_password_pwned';
121121
}
122122

123+
/**
124+
* Checks if the provided error is a clerk api response error indicating a password was pwned.
125+
*
126+
* @internal
127+
*/
128+
export function isPasswordUntrustedError(err: any) {
129+
return isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'form_password_untrusted';
130+
}
131+
123132
/**
124133
* Checks if the provided error is an EmailLinkError.
125134
*

packages/shared/src/types/localization.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,9 @@ export type __internal_LocalizationResource = {
400400
passwordPwned: {
401401
title: LocalizationValue;
402402
};
403+
passwordUntrusted: {
404+
title: LocalizationValue;
405+
};
403406
passkey: {
404407
title: LocalizationValue;
405408
subtitle: LocalizationValue;

0 commit comments

Comments
 (0)