Skip to content

Commit fd8a5d9

Browse files
Add support for sending recovery email for webauthn
1 parent 52e6685 commit fd8a5d9

File tree

7 files changed

+130
-19
lines changed

7 files changed

+130
-19
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved.
2+
*
3+
* This software is licensed under the Apache License, Version 2.0 (the
4+
* "License") as published by the Apache Software Foundation.
5+
*
6+
* You may not use this file except in compliance with the License. You may
7+
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
* License for the specific language governing permissions and limitations
13+
* under the License.
14+
*/
15+
16+
import { withOverride } from "../../../../../components/componentOverride/withOverride";
17+
import { useTranslation } from "../../../../../translation/translationContext";
18+
19+
import type { EmailSentProps } from "../../../types";
20+
21+
export const PasskeyEmailSent = withOverride("PasskeyEmailSent", (props: EmailSentProps): JSX.Element => {
22+
const t = useTranslation();
23+
return (
24+
<div data-supertokens="passkeyEmailSentContainer">
25+
<div data-supertokens="headerTitle">{t("WEBAUTHN_EMAIL_SENT_LABEL")}</div>
26+
<div data-supertokens="emailSentDescription">
27+
{t("WEBAUTHN_EMAIL_SENT_PRE_LABEL")}
28+
{props.email}
29+
{t("WEBAUTHN_EMAIL_SENT_POST_LABEL")}
30+
<a
31+
onClick={props.onEmailChangeClick}
32+
data-supertokens="link linkButton formLabelLinkBtn changeEmailBtn">
33+
{t("WEBAUTHN_RESEND_OR_CHANGE_EMAIL_LABEL")}
34+
</a>
35+
</div>
36+
</div>
37+
);
38+
});

lib/ts/recipe/webauthn/components/themes/signUp/index.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ function PasskeySignUpTheme(props: SignUpThemeProps): JSX.Element {
4949
props.factorIds.length > 1 ? "multiFactor" : "singleFactor"
5050
}`}>
5151
<div data-supertokens="row">
52-
{![SignUpScreen.Error, SignUpScreen.RecoverAccount].includes(activeScreen) && (
52+
{![SignUpScreen.Error, SignUpScreen.RecoverAccount, SignUpScreen.RecoverEmailSent].includes(
53+
activeScreen
54+
) && (
5355
<AuthPageHeader
5456
factorIds={props.factorIds}
5557
isSignUp={true}
@@ -73,13 +75,15 @@ function PasskeySignUpTheme(props: SignUpThemeProps): JSX.Element {
7375
activeScreen={activeScreen}
7476
setActiveScreen={setActiveScreen}
7577
/>
76-
<AuthPageFooter
77-
factorIds={props.factorIds}
78-
isSignUp={true}
79-
hasSeparateSignUpView={true}
80-
privacyPolicyLink={privacyPolicyLink}
81-
termsOfServiceLink={termsOfServiceLink}
82-
/>
78+
{activeScreen !== SignUpScreen.RecoverEmailSent && (
79+
<AuthPageFooter
80+
factorIds={props.factorIds}
81+
isSignUp={true}
82+
hasSeparateSignUpView={true}
83+
privacyPolicyLink={privacyPolicyLink}
84+
termsOfServiceLink={termsOfServiceLink}
85+
/>
86+
)}
8387
</div>
8488
<SuperTokensBranding />
8589
</div>

lib/ts/recipe/webauthn/components/themes/signUp/recoverAccountForm.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,29 @@ import STGeneralError from "supertokens-web-js/lib/build/error";
1818

1919
import { withOverride } from "../../../../../components/componentOverride/withOverride";
2020
import { useTranslation } from "../../../../../translation/translationContext";
21+
import { useUserContext } from "../../../../../usercontext";
2122
import { Label } from "../../../../emailpassword/components/library";
2223
import BackButton from "../../../../emailpassword/components/library/backButton";
2324
import FormBase from "../../../../emailpassword/components/library/formBase";
2425
import { defaultEmailValidator } from "../../../../emailpassword/validators";
26+
import { RecoverableError } from "../error/recoverableError";
2527

2628
import type { RecoverFromProps } from "../../../types";
2729

2830
export const PasskeyRecoverAccountFormInner = withOverride(
2931
"PasskeyRecoverAccountFormInner",
30-
(props: RecoverFromProps): JSX.Element => {
31-
const [, setError] = useState<string | undefined>(undefined);
32+
(
33+
props: RecoverFromProps & {
34+
setError: React.Dispatch<React.SetStateAction<string | undefined>>;
35+
}
36+
): JSX.Element => {
37+
const userContext = useUserContext();
3238

3339
return (
3440
<FormBase
35-
clearError={() => setError(undefined)}
36-
onFetchError={() => setError("Failed to fetch")}
37-
onError={(error) => setError(error)}
41+
clearError={() => props.setError(undefined)}
42+
onFetchError={() => props.setError("WEBAUTHN_ACCOUNT_RECOVERY_GENERAL_ERROR")}
43+
onError={() => props.setError("WEBAUTHN_ACCOUNT_RECOVERY_GENERAL_ERROR")}
3844
formFields={[
3945
{
4046
id: "email",
@@ -59,9 +65,18 @@ export const PasskeyRecoverAccountFormInner = withOverride(
5965
if (email === undefined) {
6066
throw new STGeneralError("GENERAL_ERROR_EMAIL_UNDEFINED");
6167
}
62-
// TODO: Define code to make the API call to send reset email.
68+
// Define code to make the API call to send reset email.
69+
const res = await props.recipeImplementation.generateRecoverAccountToken({
70+
email: email,
71+
userContext: userContext,
72+
});
73+
74+
if (res.status === "RECOVER_ACCOUNT_NOT_ALLOWED") {
75+
props.setError("WEBAUTHN_ACCOUNT_RECOVERY_NOT_ALLOWED_LABEL");
76+
}
77+
6378
return {
64-
status: "OK",
79+
...res,
6580
email,
6681
};
6782
}}
@@ -76,6 +91,7 @@ export const PasskeyRecoverAccountForm = withOverride(
7691
"PasskeyRecoverAccountForm",
7792
(props: RecoverFromProps): JSX.Element => {
7893
const t = useTranslation();
94+
const [errorLabel, setErrorLabel] = useState<string | undefined>(undefined);
7995

8096
return (
8197
<div data-supertokens="passkeyRecoverAccountFormContainer">
@@ -91,7 +107,12 @@ export const PasskeyRecoverAccountForm = withOverride(
91107
{t("WEBAUTHN_RECOVER_ACCOUNT_SUBHEADER_LABEL")}
92108
</div>
93109
</div>
94-
<PasskeyRecoverAccountFormInner {...props} />
110+
{errorLabel !== undefined && (
111+
<div data-supertokens="errorContainer">
112+
<RecoverableError errorMessageLabel={errorLabel} />
113+
</div>
114+
)}
115+
<PasskeyRecoverAccountFormInner {...props} setError={setErrorLabel} />
95116
</div>
96117
);
97118
}

lib/ts/recipe/webauthn/components/themes/signUp/signUpForm.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { defaultEmailValidator } from "../../../../emailpassword/validators";
2626

2727
import { PasskeyConfirmation } from "./confirmation";
2828
import { ContinueWithoutPasskey } from "./continueWithoutPasskey";
29+
import { PasskeyEmailSent } from "./emailSent";
2930
import { PasskeyRecoverAccountForm } from "./recoverAccountForm";
3031
import { SignUpSomethingWentWrong } from "./somethingWentWrong";
3132

@@ -122,6 +123,7 @@ export const SignUpForm = (
122123
const userContext = useUserContext();
123124
const [showPasskeyConfirmationError, setShowPasskeyConfirmationError] = useState(false);
124125
const [isLoading, setIsLoading] = useState(false);
126+
const [recoverAccountEmail, setRecoverAccountEmail] = useState<string>("");
125127

126128
const onContinueClickCallback = useCallback(
127129
(params: ContinueOnSuccessParams) => {
@@ -166,14 +168,19 @@ export const SignUpForm = (
166168
});
167169
}, [callAPI, props]);
168170

169-
const onRecoverAccountFormSuccess = () => {
171+
const onRecoverAccountFormSuccess = (result: { email: string }) => {
172+
setRecoverAccountEmail(result.email);
170173
props.setActiveScreen(SignUpScreen.RecoverEmailSent);
171174
};
172175

173176
const onRecoverAccountBackClick = () => {
174177
props.setActiveScreen(SignUpScreen.SignUpForm);
175178
};
176179

180+
const onEmailChangeClick = () => {
181+
props.setActiveScreen(SignUpScreen.RecoverAccount);
182+
};
183+
177184
return props.activeScreen === SignUpScreen.SignUpForm ? (
178185
<SignUpFormInner {...props} onContinueClick={onContinueClickCallback} />
179186
) : props.activeScreen === SignUpScreen.PasskeyConfirmation ? (
@@ -187,6 +194,12 @@ export const SignUpForm = (
187194
) : props.activeScreen === SignUpScreen.Error ? (
188195
<SignUpSomethingWentWrong onClick={() => props.setActiveScreen(SignUpScreen.SignUpForm)} />
189196
) : props.activeScreen === SignUpScreen.RecoverAccount ? (
190-
<PasskeyRecoverAccountForm onSuccess={onRecoverAccountFormSuccess} onBackClick={onRecoverAccountBackClick} />
197+
<PasskeyRecoverAccountForm
198+
onSuccess={onRecoverAccountFormSuccess}
199+
onBackClick={onRecoverAccountBackClick}
200+
recipeImplementation={props.recipeImplementation}
201+
/>
202+
) : props.activeScreen === SignUpScreen.RecoverEmailSent ? (
203+
<PasskeyEmailSent email={recoverAccountEmail} onEmailChangeClick={onEmailChangeClick} />
191204
) : null;
192205
};

lib/ts/recipe/webauthn/components/themes/styles.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,25 @@
192192
color: #808080;
193193
margin-bottom: 20px;
194194
}
195+
196+
[data-supertokens~="passkeyRecoverAccountFormContainer"] [data-supertokens~="errorContainer"] {
197+
margin-bottom: 15px;
198+
}
199+
200+
[data-supertokens~="passkeyEmailSentContainer"] [data-supertokens~="emailSentDescription"] {
201+
margin: 0 auto;
202+
font-family: Arial;
203+
font-size: 14px;
204+
font-weight: 400;
205+
line-height: 16.1px;
206+
text-align: center;
207+
text-underline-position: from-font;
208+
text-decoration-skip-ink: none;
209+
color: #808080;
210+
}
211+
212+
[data-supertokens~="passkeyEmailSentContainer"]
213+
[data-supertokens~="emailSentDescription"]
214+
[data-supertokens~="changeEmailBtn"] {
215+
line-height: 16.1px;
216+
}

lib/ts/recipe/webauthn/components/themes/translations.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,12 @@ export const defaultTranslationsWebauthn = {
2424
WEBAUTHN_ERROR_GO_BACK_BUTTON_LABEL: "Go back",
2525
WEBAUTHN_UNRECOVERABLE_ERROR: "Something went wrong",
2626
WEBAUTHN_UNRECOVERABLE_ERROR_DETAILS: "something went wrong with your current session. please try again.",
27+
WEBAUTHN_EMAIL_SENT_LABEL: "Email sent",
28+
WEBAUTHN_EMAIL_SENT_PRE_LABEL: "Account recovery email has been sent to ",
29+
WEBAUTHN_EMAIL_SENT_POST_LABEL: ", if it exists in our system.",
30+
WEBAUTHN_RESEND_OR_CHANGE_EMAIL_LABEL: "Resend or change email",
31+
WEBAUTHN_ACCOUNT_RECOVERY_NOT_ALLOWED_LABEL: "Account Recovery is not allowed, please contact support.",
32+
WEBAUTHN_ACCOUNT_RECOVERY_GENERAL_ERROR:
33+
"Something went wrong while trying to send recover account token, please try again.",
2734
},
2835
};

lib/ts/recipe/webauthn/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,14 @@ export type FeatureBlockDetailProps = {
176176
};
177177

178178
export type RecoverFromProps = {
179-
onSuccess: () => void;
179+
onSuccess: (result: any) => void;
180180
onBackClick: () => void;
181+
recipeImplementation: RecipeImplementation;
182+
};
183+
184+
export type EmailSentProps = {
185+
email: string;
186+
onEmailChangeClick: () => void;
181187
};
182188

183189
// Type to indicate what the `Continue with` button is being used for.

0 commit comments

Comments
 (0)