From f17de3a56e18860568f2343cbe4f01b22c5ecd7d Mon Sep 17 00:00:00 2001 From: Mihaly Lengyel Date: Mon, 30 Oct 2023 14:31:19 +0100 Subject: [PATCH] feat: finishing totp initial impl --- .../components/themes/mfa/blockedScreen.d.ts | 11 +- .../components/themes/mfa/retryButton.d.ts | 5 + .../mfa/totpCodeVerificationFooter.d.ts | 6 +- .../mfa/totpCodeVerificationHeader.d.ts | 7 +- .../themes/mfa/totpDeviceSetupFooter.d.ts | 6 +- .../themes/mfa/totpDeviceSetupHeader.d.ts | 7 +- .../totp/components/themes/translations.d.ts | 7 +- lib/build/recipe/totp/types.d.ts | 3 + lib/build/totpprebuiltui.js | 365 +++++++++++++----- .../totp/components/features/mfa/index.tsx | 27 +- .../components/themes/mfa/blockedScreen.tsx | 9 +- .../totp/components/themes/mfa/index.tsx | 23 +- .../components/themes/mfa/retryButton.tsx | 84 ++++ .../components/themes/mfa/totpCodeForm.tsx | 7 +- .../themes/mfa/totpCodeVerificationFooter.tsx | 26 +- .../themes/mfa/totpCodeVerificationHeader.tsx | 20 +- .../themes/mfa/totpDeviceSetupFooter.tsx | 11 +- .../themes/mfa/totpDeviceSetupHeader.tsx | 20 +- .../recipe/totp/components/themes/styles.css | 14 + .../totp/components/themes/translations.ts | 10 +- lib/ts/recipe/totp/types.ts | 3 + 21 files changed, 510 insertions(+), 161 deletions(-) create mode 100644 lib/build/recipe/totp/components/themes/mfa/retryButton.d.ts create mode 100644 lib/ts/recipe/totp/components/themes/mfa/retryButton.tsx diff --git a/lib/build/recipe/totp/components/themes/mfa/blockedScreen.d.ts b/lib/build/recipe/totp/components/themes/mfa/blockedScreen.d.ts index 813dacf0d..feb3680e8 100644 --- a/lib/build/recipe/totp/components/themes/mfa/blockedScreen.d.ts +++ b/lib/build/recipe/totp/components/themes/mfa/blockedScreen.d.ts @@ -1,4 +1,9 @@ /// -export declare const BlockedScreen: import("react").ComponentType<{ - children?: import("react").ReactNode; -}>; +export declare const BlockedScreen: import("react").ComponentType< + { + nextRetryAt: number; + onRetry: () => void; + } & { + children?: import("react").ReactNode; + } +>; diff --git a/lib/build/recipe/totp/components/themes/mfa/retryButton.d.ts b/lib/build/recipe/totp/components/themes/mfa/retryButton.d.ts new file mode 100644 index 000000000..1c4898840 --- /dev/null +++ b/lib/build/recipe/totp/components/themes/mfa/retryButton.d.ts @@ -0,0 +1,5 @@ +import React from "react"; +export declare const RetryButton: React.ComponentType<{ + nextRetryAt: number; + onClick: () => void; +}>; diff --git a/lib/build/recipe/totp/components/themes/mfa/totpCodeVerificationFooter.d.ts b/lib/build/recipe/totp/components/themes/mfa/totpCodeVerificationFooter.d.ts index ae8020891..1727e61b3 100644 --- a/lib/build/recipe/totp/components/themes/mfa/totpCodeVerificationFooter.d.ts +++ b/lib/build/recipe/totp/components/themes/mfa/totpCodeVerificationFooter.d.ts @@ -1,3 +1,7 @@ /// import type { TOTPMFACommonProps } from "../../../types"; -export declare const CodeVerificationFooter: import("react").ComponentType; +export declare const CodeVerificationFooter: import("react").ComponentType< + TOTPMFACommonProps & { + onSignOutClicked: () => void; + } +>; diff --git a/lib/build/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.d.ts b/lib/build/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.d.ts index ca45901cf..9552df0fa 100644 --- a/lib/build/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.d.ts +++ b/lib/build/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.d.ts @@ -1,3 +1,8 @@ /// import type { TOTPMFACommonProps } from "../../../types"; -export declare const CodeVerificationHeader: import("react").ComponentType; +export declare const CodeVerificationHeader: import("react").ComponentType< + TOTPMFACommonProps & { + showBackButton: boolean; + onBackButtonClicked: () => void; + } +>; diff --git a/lib/build/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.d.ts b/lib/build/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.d.ts index 12061c5f1..7894c4852 100644 --- a/lib/build/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.d.ts +++ b/lib/build/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.d.ts @@ -1,3 +1,7 @@ /// import type { TOTPMFACommonProps } from "../../../types"; -export declare const DeviceSetupFooter: import("react").ComponentType; +export declare const DeviceSetupFooter: import("react").ComponentType< + TOTPMFACommonProps & { + onSignOutClicked: () => void; + } +>; diff --git a/lib/build/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.d.ts b/lib/build/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.d.ts index c2b3dc020..52772d758 100644 --- a/lib/build/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.d.ts +++ b/lib/build/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.d.ts @@ -1,3 +1,8 @@ /// import type { TOTPMFACommonProps } from "../../../types"; -export declare const DeviceSetupHeader: import("react").ComponentType; +export declare const DeviceSetupHeader: import("react").ComponentType< + TOTPMFACommonProps & { + showBackButton: boolean; + onBackButtonClicked: () => void; + } +>; diff --git a/lib/build/recipe/totp/components/themes/translations.d.ts b/lib/build/recipe/totp/components/themes/translations.d.ts index cb35b598d..31176da77 100644 --- a/lib/build/recipe/totp/components/themes/translations.d.ts +++ b/lib/build/recipe/totp/components/themes/translations.d.ts @@ -7,12 +7,15 @@ export declare const defaultTranslationsTOTP: { TOTP_CODE_VERIFICATION_HEADER_SUBTITLE: string; TOTP_DEVICE_SETUP_HEADER_TITLE: string; TOTP_DEVICE_SETUP_HEADER_SUBTITLE: string; - TOTP_DEVICE_SETUP_FOOTER: string; TOTP_CODE_INPUT_LABEL: string; TOTP_CODE_CONTINUE_BUTTON: string; - TOTP_REMOVE_DEVICE_LINK: string; TOTP_BLOCKED_TITLE: string; TOTP_BLOCKED_SUBTITLE: string; + TOTP_MFA_BLOCKED_TIMER_START: string; + TOTP_MFA_BLOCKED_TIMER_END: string; + TOTP_MFA_BLOCKED_RETRY: string; + TOTP_MFA_LOGOUT: string; + INVALID_TOTP_ERROR: string; BRANDING_POWERED_BY_START: string; BRANDING_POWERED_BY_END: string; SOMETHING_WENT_WRONG_ERROR: string; diff --git a/lib/build/recipe/totp/types.d.ts b/lib/build/recipe/totp/types.d.ts index 8f7c2f471..82a012ecc 100644 --- a/lib/build/recipe/totp/types.d.ts +++ b/lib/build/recipe/totp/types.d.ts @@ -20,6 +20,7 @@ export declare type TOTPDeviceInfo = { export declare type TOTPMFAAction = | { type: "load"; + showBackButton: boolean; deviceInfo: TOTPDeviceInfo | undefined; error: string | undefined; } @@ -51,6 +52,7 @@ export declare type TOTPMFAState = { showSecret: boolean; nextRetryAt?: number; isBlocked: boolean; + showBackButton: boolean; loaded: boolean; error: string | undefined; }; @@ -67,6 +69,7 @@ export declare type TOTPMFAProps = { onShowSecretClick: () => void; onBackButtonClicked: () => void; onRetryClicked: () => void; + onSignOutClicked: () => void; dispatch: Dispatch; featureState: TOTPMFAState; userContext?: any; diff --git a/lib/build/totpprebuiltui.js b/lib/build/totpprebuiltui.js index 6ba41e30a..45602170e 100644 --- a/lib/build/totpprebuiltui.js +++ b/lib/build/totpprebuiltui.js @@ -8,16 +8,18 @@ var session = require("./session-shared3.js"); var recipe$2 = require("./totp-shared.js"); var React = require("react"); var windowHandler = require("supertokens-web-js/utils/windowHandler"); +var multifactorauth = require("./multifactorauth-shared2.js"); var recipe = require("./multifactorauth-shared.js"); var recipe$1 = require("./session-shared2.js"); var SuperTokensBranding = require("./SuperTokensBranding.js"); var translations = require("./translations.js"); var generalError = require("./emailpassword-shared.js"); var translationContext = require("./translationContext.js"); -var STGeneralError = require("supertokens-web-js/utils/error"); var formBase = require("./emailpassword-shared9.js"); +var STGeneralError = require("supertokens-web-js/utils/error"); var validators = require("./passwordless-shared3.js"); var arrowLeftIcon = require("./arrowLeftIcon.js"); +var backButton = require("./emailpassword-shared8.js"); require("supertokens-web-js"); require("supertokens-web-js/utils/cookieHandler"); require("supertokens-web-js/utils/postSuperTokensInitCallbacks"); @@ -71,7 +73,7 @@ var React__namespace = /*#__PURE__*/ _interopNamespace(React); var STGeneralError__default = /*#__PURE__*/ _interopDefault(STGeneralError); var styles = - '[data-supertokens~="container"] {\n --palette-background: 255, 255, 255;\n --palette-inputBackground: 250, 250, 250;\n --palette-inputBorder: 224, 224, 224;\n --palette-primary: 255, 155, 51;\n --palette-primaryBorder: 238, 141, 35;\n --palette-success: 65, 167, 0;\n --palette-successBackground: 217, 255, 191;\n --palette-error: 255, 23, 23;\n --palette-errorBackground: 255, 241, 235;\n --palette-textTitle: 34, 34, 34;\n --palette-textLabel: 34, 34, 34;\n --palette-textInput: 34, 34, 34;\n --palette-textPrimary: 101, 101, 101;\n --palette-textLink: 0, 118, 255;\n --palette-buttonText: 255, 255, 255;\n --palette-textGray: 128, 128, 128;\n --palette-superTokensBrandingBackground: 242, 245, 246;\n --palette-superTokensBrandingText: 173, 189, 196;\n\n --font-size-0: 12px;\n --font-size-1: 14px;\n --font-size-2: 16px;\n --font-size-3: 19px;\n --font-size-4: 24px;\n}\n/*\n * Default styles.\n */\n@-webkit-keyframes slideTop {\n 0% {\n -webkit-transform: translateY(-5px);\n transform: translateY(-5px);\n }\n 100% {\n -webkit-transform: translateY(0px);\n transform: translateY(0px);\n }\n}\n@keyframes slideTop {\n 0% {\n -webkit-transform: translateY(-5px);\n transform: translateY(-5px);\n }\n 100% {\n -webkit-transform: translateY(0px);\n transform: translateY(0px);\n }\n}\n@-webkit-keyframes swing-in-top-fwd {\n 0% {\n -webkit-transform: rotateX(-100deg);\n transform: rotateX(-100deg);\n -webkit-transform-origin: top;\n transform-origin: top;\n opacity: 0;\n }\n 100% {\n -webkit-transform: rotateX(0deg);\n transform: rotateX(0deg);\n -webkit-transform-origin: top;\n transform-origin: top;\n opacity: 1;\n }\n}\n@keyframes swing-in-top-fwd {\n 0% {\n -webkit-transform: rotateX(-100deg);\n transform: rotateX(-100deg);\n -webkit-transform-origin: top;\n transform-origin: top;\n opacity: 0;\n }\n 100% {\n -webkit-transform: rotateX(0deg);\n transform: rotateX(0deg);\n -webkit-transform-origin: top;\n transform-origin: top;\n opacity: 1;\n }\n}\n[data-supertokens~="container"] {\n font-family: "Rubik", sans-serif;\n margin: 12px auto;\n margin-top: 26px;\n margin-bottom: 26px;\n width: 420px;\n text-align: center;\n border-radius: 8px;\n box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.16);\n background-color: rgb(var(--palette-background));\n}\n@media (max-width: 440px) {\n [data-supertokens~="container"] {\n width: 95vw;\n }\n}\n[data-supertokens~="row"] {\n margin: 0 auto;\n width: 76%;\n padding-top: 30px;\n padding-bottom: 10px;\n}\n[data-supertokens~="superTokensBranding"] {\n display: block;\n margin: 0 auto;\n background: rgb(var(--palette-superTokensBrandingBackground));\n color: rgb(var(--palette-superTokensBrandingText));\n text-decoration: none;\n width: -webkit-fit-content;\n width: -moz-fit-content;\n width: fit-content;\n border-radius: 6px 6px 0 0;\n padding: 4px 9px;\n font-weight: 300;\n font-size: var(--font-size-0);\n letter-spacing: 0.4px;\n}\n[data-supertokens~="generalError"] {\n background: rgb(var(--palette-errorBackground));\n padding-top: 10px;\n padding-bottom: 10px;\n margin-bottom: 15px;\n padding-left: 18px;\n padding-right: 18px;\n letter-spacing: 0.2px;\n font-size: var(--font-size-1);\n border-radius: 8px;\n color: rgb(var(--palette-error));\n -webkit-animation: swing-in-top-fwd 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;\n animation: swing-in-top-fwd 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;\n word-wrap: break-word;\n}\n[data-supertokens~="headerTitle"] {\n font-size: var(--font-size-4);\n line-height: 40px;\n letter-spacing: 0.58px;\n font-weight: 800;\n margin-bottom: 2px;\n color: rgb(var(--palette-textTitle));\n}\n[data-supertokens~="headerSubtitle"] {\n margin-bottom: 21px;\n}\n[data-supertokens~="privacyPolicyAndTermsAndConditions"] {\n max-width: 300px;\n margin-top: 10px;\n}\n[data-supertokens~="privacyPolicyAndTermsAndConditions"] a {\n line-height: 21px;\n}\n/* TODO: split the link style into separate things*/\n/* We add this before primary and secondary text, because if they are applied to the same element the other ones take priority */\n[data-supertokens~="link"] {\n padding-left: 3px;\n padding-right: 3px;\n color: rgb(var(--palette-textLink));\n font-size: var(--font-size-1);\n cursor: pointer;\n letter-spacing: 0.16px;\n line-height: 26px;\n}\n[data-supertokens~="primaryText"] {\n font-size: var(--font-size-1);\n font-weight: 500;\n letter-spacing: 0.4px;\n line-height: 21px;\n color: rgb(var(--palette-textLabel));\n}\n[data-supertokens~="secondaryText"] {\n font-size: var(--font-size-1);\n font-weight: 300;\n letter-spacing: 0.4px;\n color: rgb(var(--palette-textPrimary));\n}\n[data-supertokens~="divider"] {\n margin-top: 1em;\n margin-bottom: 1em;\n border-bottom: 0.3px solid #dddddd;\n align-items: center;\n padding-bottom: 5px;\n}\n[data-supertokens~="headerTinyTitle"] {\n margin-top: 13px;\n font-size: var(--font-size-3);\n letter-spacing: 1.1px;\n font-weight: 500;\n line-height: 28px;\n}\n[data-supertokens~="secondaryLinkWithArrow"] {\n margin-top: 10px;\n margin-bottom: 30px;\n cursor: pointer;\n}\n[data-supertokens~="secondaryLinkWithArrow"]:hover {\n position: relative;\n left: 2px;\n word-spacing: 4px;\n}\n[data-supertokens~="generalSuccess"] {\n color: rgb(var(--palette-success));\n font-size: var(--font-size-1);\n background: rgb(var(--palette-successBackground));\n -webkit-animation: swing-in-top-fwd 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;\n animation: swing-in-top-fwd 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;\n padding: 9px 15px 9px 15px;\n border-radius: 6px;\n display: inline-block;\n}\n[data-supertokens~="spinner"] {\n width: 80px;\n height: auto;\n padding-top: 20px;\n padding-bottom: 40px;\n margin: 0 auto;\n}\n[data-supertokens~="error"] {\n color: rgb(var(--palette-error));\n}\n[data-supertokens~="linkButton"] {\n background-color: transparent;\n border: 0;\n}\n[data-supertokens~="secondaryLinkWithLeftArrow"] {\n margin-top: 10px;\n margin-bottom: 40px;\n cursor: pointer;\n}\n[data-supertokens~="secondaryLinkWithLeftArrow"] svg {\n margin-right: 0.3em;\n}\n[data-supertokens~="secondaryLinkWithLeftArrow"]:hover svg {\n position: relative;\n left: -4px;\n}\n[data-supertokens~="button"] {\n background-color: rgb(var(--palette-primary));\n color: rgb(var(--palette-buttonText));\n width: 100%;\n height: 34px;\n font-weight: 700;\n border-width: 1px;\n border-style: solid;\n border-radius: 6px;\n border-color: rgb(var(--palette-primaryBorder));\n background-position: center;\n transition: all 0.4s;\n background-size: 12000%;\n cursor: pointer;\n}\n[data-supertokens~="button"]:disabled {\n border: none;\n cursor: no-drop;\n}\n[data-supertokens~="button"]:active {\n outline: none;\n transition: all 0s;\n background-size: 100%;\n -webkit-filter: brightness(0.85);\n filter: brightness(0.85);\n}\n[data-supertokens~="button"]:focus {\n outline: none;\n}\n[data-supertokens~="backButtonCommon"] {\n width: 16px;\n height: 13px;\n}\n[data-supertokens~="backButton"] {\n cursor: pointer;\n border: none;\n background-color: transparent;\n padding: 0px;\n}\n[data-supertokens~="backButtonPlaceholder"] {\n display: block;\n}\n[data-supertokens~="delayedRender"] {\n -webkit-animation-duration: 0.1s;\n animation-duration: 0.1s;\n -webkit-animation-name: animate-fade;\n animation-name: animate-fade;\n -webkit-animation-delay: 0.2s;\n animation-delay: 0.2s;\n -webkit-animation-fill-mode: backwards;\n animation-fill-mode: backwards;\n}\n@-webkit-keyframes animate-fade {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n}\n@keyframes animate-fade {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n}\n/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.\n *\n * This software is licensed under the Apache License, Version 2.0 (the\n * "License") as published by the Apache Software Foundation.\n *\n * You may not use this file except in compliance with the License. You may\n * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n */\n[data-supertokens~="inputContainer"] {\n margin-top: 6px;\n}\n[data-supertokens~="inputWrapper"] {\n box-sizing: border-box;\n width: 100%;\n display: flex;\n align-items: center;\n background-color: rgb(var(--palette-inputBackground));\n height: 34px;\n border-radius: 6px;\n border: 1px solid rgb(var(--palette-inputBorder));\n}\n[data-supertokens~="inputWrapper"][focus-within] {\n background-color: rgba(var(--palette-inputBackground), 0.25);\n border: 1px solid rgb(var(--palette-primary));\n box-shadow: 0 0 0 0.2rem rgba(var(--palette-primary), 0.25);\n outline: none;\n}\n[data-supertokens~="inputWrapper"]:focus-within {\n background-color: rgba(var(--palette-inputBackground), 0.25);\n border: 1px solid rgb(var(--palette-primary));\n box-shadow: 0 0 0 0.2rem rgba(var(--palette-primary), 0.25);\n outline: none;\n}\n[data-supertokens~="inputError"] {\n border: 1px solid rgb(var(--palette-error));\n box-shadow: 0 0 0 0.2rem rgba(var(--palette-error), 0.25);\n outline: none;\n}\n[data-supertokens~="inputError"][focus-within] {\n border: 1px solid rgb(var(--palette-error));\n box-shadow: 0 0 0 0.2rem rgba(var(--palette-error), 0.25);\n outline: none;\n}\n[data-supertokens~="inputError"]:focus-within {\n border: 1px solid rgb(var(--palette-error));\n box-shadow: 0 0 0 0.2rem rgba(var(--palette-error), 0.25);\n outline: none;\n}\n[data-supertokens~="input"] {\n box-sizing: border-box;\n padding-left: 15px;\n -webkit-filter: none;\n filter: none;\n color: rgb(var(--palette-textInput));\n background-color: transparent;\n border-radius: 6px;\n font-size: var(--font-size-1);\n border: none;\n padding-right: 25px;\n letter-spacing: 1.2px;\n flex: 9 1 75%;\n width: 75%;\n height: 32px;\n}\n[data-supertokens~="input"]:focus {\n border: none;\n outline: none;\n}\n[data-supertokens~="input"]:-webkit-autofill,\n[data-supertokens~="input"]:-webkit-autofill:hover,\n[data-supertokens~="input"]:-webkit-autofill:focus,\n[data-supertokens~="input"]:-webkit-autofill:active {\n -webkit-text-fill-color: rgb(var(--palette-textInput));\n box-shadow: 0 0 0 30px rgb(var(--palette-inputBackground)) inset;\n}\n[data-supertokens~="inputAdornment"] {\n justify-content: center;\n margin-right: 5px;\n}\n[data-supertokens~="showPassword"] {\n cursor: pointer;\n}\n[data-supertokens~="forgotPasswordLink"] {\n margin-top: 10px;\n}\n[data-supertokens~="enterEmailSuccessMessage"] {\n margin-top: 15px;\n margin-bottom: 15px;\n word-break: break-word;\n}\n[data-supertokens~="submitNewPasswordSuccessMessage"] {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n[data-supertokens~="inputErrorMessage"] {\n padding-top: 5px;\n padding-bottom: 5px;\n color: rgb(var(--palette-error));\n line-height: 24px;\n font-weight: 400;\n font-size: var(--font-size-1);\n text-align: left;\n -webkit-animation: slideTop 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;\n animation: slideTop 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;\n max-width: 330px;\n}\n@media (max-width: 440px) {\n [data-supertokens~="inputErrorMessage"] {\n max-width: 250px;\n }\n}\n[data-supertokens~="inputErrorSymbol"] {\n margin-right: 5px;\n top: 1px;\n position: relative;\n left: 2px;\n}\n[data-supertokens~="label"] {\n text-align: left;\n font-weight: 600;\n font-size: var(--font-size-1);\n line-height: 24px;\n color: rgb(var(--palette-textLabel));\n}\n[data-supertokens~="formRow"] {\n display: flex;\n flex-direction: column;\n padding-top: 0px;\n padding-bottom: 34px;\n}\n[data-supertokens~="formRow"][data-supertokens~="hasError"] {\n padding-bottom: 0;\n}\n[data-supertokens~="sendVerifyEmailIcon"] {\n margin-top: 11px;\n}\n[data-supertokens~="headerTinyTitle"] {\n margin-top: 13px;\n font-size: var(--font-size-3);\n letter-spacing: 1.1px;\n font-weight: 500;\n line-height: 28px;\n}\n[data-supertokens~="sendVerifyEmailText"] {\n line-height: 21px;\n font-size: var(--font-size-1);\n text-align: center;\n font-weight: 300;\n letter-spacing: 0.8px;\n}\n[data-supertokens~="secondaryLinkWithArrow"] {\n margin-top: 10px;\n margin-bottom: 30px;\n cursor: pointer;\n}\n[data-supertokens~="secondaryLinkWithArrow"]:hover {\n position: relative;\n left: 2px;\n word-spacing: 4px;\n}\n[data-supertokens~="sendVerifyEmailResend"] {\n margin-top: 13px;\n font-weight: 300;\n}\n[data-supertokens~="sendVerifyEmailResend"]:hover {\n text-decoration: underline;\n}\n[data-supertokens~="noFormRow"] {\n padding-bottom: 25px;\n}\n[data-supertokens~="emailVerificationButtonWrapper"] {\n padding-top: 25px;\n max-width: 96px;\n margin: 0 auto;\n}\n[data-supertokens~="withBackButton"] {\n position: relative;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n[data-supertokens~="resendEmailLink"] {\n display: inline-block;\n}\n[data-supertokens~="container"] {\n padding-top: 24px;\n}\n[data-supertokens~="divider"] {\n margin-top: 32px;\n margin-bottom: 32px;\n}\n[data-supertokens~="row"] {\n padding-top: 16px;\n padding-bottom: 8px;\n width: auto;\n margin: 0 50px;\n}\n[data-supertokens~="totpDeviceQR"] {\n border-radius: 12px;\n border: 1px solid rgb(var(--palette-inputBorder));\n padding: 16px;\n}\n[data-supertokens~="showTOTPSecret"] {\n display: block;\n color: rgb(var(--palette-textSecondary));\n font-size: var(--font-size-0);\n margin: 4px;\n}\n[data-supertokens~="totpSecret"] {\n display: block;\n border-radius: 6px;\n background: rgba(var(--palette-textLink), 0.08);\n}\n'; + '[data-supertokens~="container"] {\n --palette-background: 255, 255, 255;\n --palette-inputBackground: 250, 250, 250;\n --palette-inputBorder: 224, 224, 224;\n --palette-primary: 255, 155, 51;\n --palette-primaryBorder: 238, 141, 35;\n --palette-success: 65, 167, 0;\n --palette-successBackground: 217, 255, 191;\n --palette-error: 255, 23, 23;\n --palette-errorBackground: 255, 241, 235;\n --palette-textTitle: 34, 34, 34;\n --palette-textLabel: 34, 34, 34;\n --palette-textInput: 34, 34, 34;\n --palette-textPrimary: 101, 101, 101;\n --palette-textLink: 0, 118, 255;\n --palette-buttonText: 255, 255, 255;\n --palette-textGray: 128, 128, 128;\n --palette-superTokensBrandingBackground: 242, 245, 246;\n --palette-superTokensBrandingText: 173, 189, 196;\n\n --font-size-0: 12px;\n --font-size-1: 14px;\n --font-size-2: 16px;\n --font-size-3: 19px;\n --font-size-4: 24px;\n}\n/*\n * Default styles.\n */\n@-webkit-keyframes slideTop {\n 0% {\n -webkit-transform: translateY(-5px);\n transform: translateY(-5px);\n }\n 100% {\n -webkit-transform: translateY(0px);\n transform: translateY(0px);\n }\n}\n@keyframes slideTop {\n 0% {\n -webkit-transform: translateY(-5px);\n transform: translateY(-5px);\n }\n 100% {\n -webkit-transform: translateY(0px);\n transform: translateY(0px);\n }\n}\n@-webkit-keyframes swing-in-top-fwd {\n 0% {\n -webkit-transform: rotateX(-100deg);\n transform: rotateX(-100deg);\n -webkit-transform-origin: top;\n transform-origin: top;\n opacity: 0;\n }\n 100% {\n -webkit-transform: rotateX(0deg);\n transform: rotateX(0deg);\n -webkit-transform-origin: top;\n transform-origin: top;\n opacity: 1;\n }\n}\n@keyframes swing-in-top-fwd {\n 0% {\n -webkit-transform: rotateX(-100deg);\n transform: rotateX(-100deg);\n -webkit-transform-origin: top;\n transform-origin: top;\n opacity: 0;\n }\n 100% {\n -webkit-transform: rotateX(0deg);\n transform: rotateX(0deg);\n -webkit-transform-origin: top;\n transform-origin: top;\n opacity: 1;\n }\n}\n[data-supertokens~="container"] {\n font-family: "Rubik", sans-serif;\n margin: 12px auto;\n margin-top: 26px;\n margin-bottom: 26px;\n width: 420px;\n text-align: center;\n border-radius: 8px;\n box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.16);\n background-color: rgb(var(--palette-background));\n}\n@media (max-width: 440px) {\n [data-supertokens~="container"] {\n width: 95vw;\n }\n}\n[data-supertokens~="row"] {\n margin: 0 auto;\n width: 76%;\n padding-top: 30px;\n padding-bottom: 10px;\n}\n[data-supertokens~="superTokensBranding"] {\n display: block;\n margin: 0 auto;\n background: rgb(var(--palette-superTokensBrandingBackground));\n color: rgb(var(--palette-superTokensBrandingText));\n text-decoration: none;\n width: -webkit-fit-content;\n width: -moz-fit-content;\n width: fit-content;\n border-radius: 6px 6px 0 0;\n padding: 4px 9px;\n font-weight: 300;\n font-size: var(--font-size-0);\n letter-spacing: 0.4px;\n}\n[data-supertokens~="generalError"] {\n background: rgb(var(--palette-errorBackground));\n padding-top: 10px;\n padding-bottom: 10px;\n margin-bottom: 15px;\n padding-left: 18px;\n padding-right: 18px;\n letter-spacing: 0.2px;\n font-size: var(--font-size-1);\n border-radius: 8px;\n color: rgb(var(--palette-error));\n -webkit-animation: swing-in-top-fwd 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;\n animation: swing-in-top-fwd 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;\n word-wrap: break-word;\n}\n[data-supertokens~="headerTitle"] {\n font-size: var(--font-size-4);\n line-height: 40px;\n letter-spacing: 0.58px;\n font-weight: 800;\n margin-bottom: 2px;\n color: rgb(var(--palette-textTitle));\n}\n[data-supertokens~="headerSubtitle"] {\n margin-bottom: 21px;\n}\n[data-supertokens~="privacyPolicyAndTermsAndConditions"] {\n max-width: 300px;\n margin-top: 10px;\n}\n[data-supertokens~="privacyPolicyAndTermsAndConditions"] a {\n line-height: 21px;\n}\n/* TODO: split the link style into separate things*/\n/* We add this before primary and secondary text, because if they are applied to the same element the other ones take priority */\n[data-supertokens~="link"] {\n padding-left: 3px;\n padding-right: 3px;\n color: rgb(var(--palette-textLink));\n font-size: var(--font-size-1);\n cursor: pointer;\n letter-spacing: 0.16px;\n line-height: 26px;\n}\n[data-supertokens~="primaryText"] {\n font-size: var(--font-size-1);\n font-weight: 500;\n letter-spacing: 0.4px;\n line-height: 21px;\n color: rgb(var(--palette-textLabel));\n}\n[data-supertokens~="secondaryText"] {\n font-size: var(--font-size-1);\n font-weight: 300;\n letter-spacing: 0.4px;\n color: rgb(var(--palette-textPrimary));\n}\n[data-supertokens~="divider"] {\n margin-top: 1em;\n margin-bottom: 1em;\n border-bottom: 0.3px solid #dddddd;\n align-items: center;\n padding-bottom: 5px;\n}\n[data-supertokens~="headerTinyTitle"] {\n margin-top: 13px;\n font-size: var(--font-size-3);\n letter-spacing: 1.1px;\n font-weight: 500;\n line-height: 28px;\n}\n[data-supertokens~="secondaryLinkWithArrow"] {\n margin-top: 10px;\n margin-bottom: 30px;\n cursor: pointer;\n}\n[data-supertokens~="secondaryLinkWithArrow"]:hover {\n position: relative;\n left: 2px;\n word-spacing: 4px;\n}\n[data-supertokens~="generalSuccess"] {\n color: rgb(var(--palette-success));\n font-size: var(--font-size-1);\n background: rgb(var(--palette-successBackground));\n -webkit-animation: swing-in-top-fwd 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;\n animation: swing-in-top-fwd 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;\n padding: 9px 15px 9px 15px;\n border-radius: 6px;\n display: inline-block;\n}\n[data-supertokens~="spinner"] {\n width: 80px;\n height: auto;\n padding-top: 20px;\n padding-bottom: 40px;\n margin: 0 auto;\n}\n[data-supertokens~="error"] {\n color: rgb(var(--palette-error));\n}\n[data-supertokens~="linkButton"] {\n background-color: transparent;\n border: 0;\n}\n[data-supertokens~="secondaryLinkWithLeftArrow"] {\n margin-top: 10px;\n margin-bottom: 40px;\n cursor: pointer;\n}\n[data-supertokens~="secondaryLinkWithLeftArrow"] svg {\n margin-right: 0.3em;\n}\n[data-supertokens~="secondaryLinkWithLeftArrow"]:hover svg {\n position: relative;\n left: -4px;\n}\n[data-supertokens~="button"] {\n background-color: rgb(var(--palette-primary));\n color: rgb(var(--palette-buttonText));\n width: 100%;\n height: 34px;\n font-weight: 700;\n border-width: 1px;\n border-style: solid;\n border-radius: 6px;\n border-color: rgb(var(--palette-primaryBorder));\n background-position: center;\n transition: all 0.4s;\n background-size: 12000%;\n cursor: pointer;\n}\n[data-supertokens~="button"]:disabled {\n border: none;\n cursor: no-drop;\n}\n[data-supertokens~="button"]:active {\n outline: none;\n transition: all 0s;\n background-size: 100%;\n -webkit-filter: brightness(0.85);\n filter: brightness(0.85);\n}\n[data-supertokens~="button"]:focus {\n outline: none;\n}\n[data-supertokens~="backButtonCommon"] {\n width: 16px;\n height: 13px;\n}\n[data-supertokens~="backButton"] {\n cursor: pointer;\n border: none;\n background-color: transparent;\n padding: 0px;\n}\n[data-supertokens~="backButtonPlaceholder"] {\n display: block;\n}\n[data-supertokens~="delayedRender"] {\n -webkit-animation-duration: 0.1s;\n animation-duration: 0.1s;\n -webkit-animation-name: animate-fade;\n animation-name: animate-fade;\n -webkit-animation-delay: 0.2s;\n animation-delay: 0.2s;\n -webkit-animation-fill-mode: backwards;\n animation-fill-mode: backwards;\n}\n@-webkit-keyframes animate-fade {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n}\n@keyframes animate-fade {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n}\n/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.\n *\n * This software is licensed under the Apache License, Version 2.0 (the\n * "License") as published by the Apache Software Foundation.\n *\n * You may not use this file except in compliance with the License. You may\n * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\n * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n * License for the specific language governing permissions and limitations\n * under the License.\n */\n[data-supertokens~="inputContainer"] {\n margin-top: 6px;\n}\n[data-supertokens~="inputWrapper"] {\n box-sizing: border-box;\n width: 100%;\n display: flex;\n align-items: center;\n background-color: rgb(var(--palette-inputBackground));\n height: 34px;\n border-radius: 6px;\n border: 1px solid rgb(var(--palette-inputBorder));\n}\n[data-supertokens~="inputWrapper"][focus-within] {\n background-color: rgba(var(--palette-inputBackground), 0.25);\n border: 1px solid rgb(var(--palette-primary));\n box-shadow: 0 0 0 0.2rem rgba(var(--palette-primary), 0.25);\n outline: none;\n}\n[data-supertokens~="inputWrapper"]:focus-within {\n background-color: rgba(var(--palette-inputBackground), 0.25);\n border: 1px solid rgb(var(--palette-primary));\n box-shadow: 0 0 0 0.2rem rgba(var(--palette-primary), 0.25);\n outline: none;\n}\n[data-supertokens~="inputError"] {\n border: 1px solid rgb(var(--palette-error));\n box-shadow: 0 0 0 0.2rem rgba(var(--palette-error), 0.25);\n outline: none;\n}\n[data-supertokens~="inputError"][focus-within] {\n border: 1px solid rgb(var(--palette-error));\n box-shadow: 0 0 0 0.2rem rgba(var(--palette-error), 0.25);\n outline: none;\n}\n[data-supertokens~="inputError"]:focus-within {\n border: 1px solid rgb(var(--palette-error));\n box-shadow: 0 0 0 0.2rem rgba(var(--palette-error), 0.25);\n outline: none;\n}\n[data-supertokens~="input"] {\n box-sizing: border-box;\n padding-left: 15px;\n -webkit-filter: none;\n filter: none;\n color: rgb(var(--palette-textInput));\n background-color: transparent;\n border-radius: 6px;\n font-size: var(--font-size-1);\n border: none;\n padding-right: 25px;\n letter-spacing: 1.2px;\n flex: 9 1 75%;\n width: 75%;\n height: 32px;\n}\n[data-supertokens~="input"]:focus {\n border: none;\n outline: none;\n}\n[data-supertokens~="input"]:-webkit-autofill,\n[data-supertokens~="input"]:-webkit-autofill:hover,\n[data-supertokens~="input"]:-webkit-autofill:focus,\n[data-supertokens~="input"]:-webkit-autofill:active {\n -webkit-text-fill-color: rgb(var(--palette-textInput));\n box-shadow: 0 0 0 30px rgb(var(--palette-inputBackground)) inset;\n}\n[data-supertokens~="inputAdornment"] {\n justify-content: center;\n margin-right: 5px;\n}\n[data-supertokens~="showPassword"] {\n cursor: pointer;\n}\n[data-supertokens~="forgotPasswordLink"] {\n margin-top: 10px;\n}\n[data-supertokens~="enterEmailSuccessMessage"] {\n margin-top: 15px;\n margin-bottom: 15px;\n word-break: break-word;\n}\n[data-supertokens~="submitNewPasswordSuccessMessage"] {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n[data-supertokens~="inputErrorMessage"] {\n padding-top: 5px;\n padding-bottom: 5px;\n color: rgb(var(--palette-error));\n line-height: 24px;\n font-weight: 400;\n font-size: var(--font-size-1);\n text-align: left;\n -webkit-animation: slideTop 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;\n animation: slideTop 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;\n max-width: 330px;\n}\n@media (max-width: 440px) {\n [data-supertokens~="inputErrorMessage"] {\n max-width: 250px;\n }\n}\n[data-supertokens~="inputErrorSymbol"] {\n margin-right: 5px;\n top: 1px;\n position: relative;\n left: 2px;\n}\n[data-supertokens~="label"] {\n text-align: left;\n font-weight: 600;\n font-size: var(--font-size-1);\n line-height: 24px;\n color: rgb(var(--palette-textLabel));\n}\n[data-supertokens~="formRow"] {\n display: flex;\n flex-direction: column;\n padding-top: 0px;\n padding-bottom: 34px;\n}\n[data-supertokens~="formRow"][data-supertokens~="hasError"] {\n padding-bottom: 0;\n}\n[data-supertokens~="sendVerifyEmailIcon"] {\n margin-top: 11px;\n}\n[data-supertokens~="headerTinyTitle"] {\n margin-top: 13px;\n font-size: var(--font-size-3);\n letter-spacing: 1.1px;\n font-weight: 500;\n line-height: 28px;\n}\n[data-supertokens~="sendVerifyEmailText"] {\n line-height: 21px;\n font-size: var(--font-size-1);\n text-align: center;\n font-weight: 300;\n letter-spacing: 0.8px;\n}\n[data-supertokens~="secondaryLinkWithArrow"] {\n margin-top: 10px;\n margin-bottom: 30px;\n cursor: pointer;\n}\n[data-supertokens~="secondaryLinkWithArrow"]:hover {\n position: relative;\n left: 2px;\n word-spacing: 4px;\n}\n[data-supertokens~="sendVerifyEmailResend"] {\n margin-top: 13px;\n font-weight: 300;\n}\n[data-supertokens~="sendVerifyEmailResend"]:hover {\n text-decoration: underline;\n}\n[data-supertokens~="noFormRow"] {\n padding-bottom: 25px;\n}\n[data-supertokens~="emailVerificationButtonWrapper"] {\n padding-top: 25px;\n max-width: 96px;\n margin: 0 auto;\n}\n[data-supertokens~="withBackButton"] {\n position: relative;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n[data-supertokens~="resendEmailLink"] {\n display: inline-block;\n}\n[data-supertokens~="container"] {\n padding-top: 24px;\n}\n[data-supertokens~="divider"] {\n margin-top: 32px;\n margin-bottom: 32px;\n}\n[data-supertokens~="row"] {\n padding-top: 16px;\n padding-bottom: 8px;\n width: auto;\n margin: 0 50px;\n}\n[data-supertokens~="totpDeviceQR"] {\n border-radius: 12px;\n border: 1px solid rgb(var(--palette-inputBorder));\n padding: 16px;\n max-width: 100px;\n max-height: 100px;\n}\n[data-supertokens~="showTOTPSecret"] {\n display: block;\n color: rgb(var(--palette-textSecondary));\n font-size: var(--font-size-0);\n margin: 4px;\n}\n[data-supertokens~="totpSecret"] {\n display: block;\n border-radius: 6px;\n padding: 12px;\n color: rgb(var(--palette-textLink));\n font-size: var(--font-size-1);\n font-weight: 600;\n letter-spacing: 3.36px;\n background: rgba(var(--palette-textLink), 0.08);\n}\n[data-supertokens~="retryCodeBtn"]:disabled {\n border: 0;\n border-radius: 6px;\n color: rgb(var(--palette-error));\n background: rgb(var(--palette-errorBackground));\n}\n'; var ThemeBase = function (_a) { var children = _a.children, @@ -113,7 +115,71 @@ var BlockedIcon = function () { ); }; -var TOTPBlockedScreen = function () { +var RetryButton = uiEntry.withOverride("TOTPRetryButton", function TOTPRetryButton(_a) { + var nextRetryAt = _a.nextRetryAt, + onClick = _a.onClick; + var t = translationContext.useTranslation(); + var getTimeLeft = React.useCallback( + function () { + var timeLeft = nextRetryAt - Date.now(); + return timeLeft < 0 ? undefined : Math.ceil(timeLeft / 1000); + }, + [nextRetryAt] + ); + var _b = React.useState(getTimeLeft()), + secsUntilRetry = _b[0], + setSecsUntilRetry = _b[1]; + React.useEffect( + function () { + // This runs every time the loginAttemptInfo updates, so after every resend + var interval = setInterval(function () { + var timeLeft = getTimeLeft(); + if (timeLeft === undefined) { + clearInterval(interval); + } + setSecsUntilRetry(timeLeft); + }, 500); + return function () { + // This can safely run twice + clearInterval(interval); + }; + }, + [getTimeLeft, setSecsUntilRetry] + ); + return jsxRuntime.jsx( + "button", + genericComponentOverrideContext.__assign( + { + type: "button", + disabled: secsUntilRetry !== undefined, + onClick: onClick, + "data-supertokens": "button retryCodeBtn", + }, + { + children: + secsUntilRetry !== undefined + ? jsxRuntime.jsxs(React__namespace.default.Fragment, { + children: [ + t("TOTP_MFA_BLOCKED_TIMER_START"), + jsxRuntime.jsxs("strong", { + children: [ + Math.floor(secsUntilRetry / 60) + .toString() + .padStart(2, "0"), + ":", + (secsUntilRetry % 60).toString().padStart(2, "0"), + ], + }), + t("TOTP_MFA_BLOCKED_TIMER_END"), + ], + }) + : t("TOTP_MFA_BLOCKED_RETRY"), + } + ) + ); +}); + +var TOTPBlockedScreen = function (props) { var t = translationContext.useTranslation(); return jsxRuntime.jsx( "div", @@ -134,7 +200,6 @@ var TOTPBlockedScreen = function () { { children: t("TOTP_BLOCKED_TITLE") } ) ), - jsxRuntime.jsx("div", { "data-supertokens": "divider" }), jsxRuntime.jsx( "div", genericComponentOverrideContext.__assign( @@ -143,6 +208,16 @@ var TOTPBlockedScreen = function () { ) ), jsxRuntime.jsx("div", { "data-supertokens": "divider" }), + jsxRuntime.jsx( + formBase.FormRow, + { + children: jsxRuntime.jsx(RetryButton, { + nextRetryAt: props.nextRetryAt, + onClick: props.onRetry, + }), + }, + "form-button" + ), ], } ) @@ -241,7 +316,7 @@ var CodeForm = uiEntry.withOverride("TOTPCodeForm", function TOTPCodeForm(props) response = _b.sent(); _b.label = 4; case 4: - // We can redirect these statuses, since they all cause a redirection + // We can return these statuses, since they all cause a redirection // and we don't really want to show anything if ( response.status === "OK" || @@ -251,7 +326,13 @@ var CodeForm = uiEntry.withOverride("TOTPCodeForm", function TOTPCodeForm(props) return [2 /*return*/, response]; } if (response.status === "INVALID_TOTP_ERROR") { - throw new STGeneralError__default.default("GENERAL_ERROR_OTP_INVALID"); + return [ + 2 /*return*/, + { + status: "FIELD_ERROR", + formFields: [{ id: "totp", error: "INVALID_TOTP_ERROR" }], + }, + ]; } throw new STGeneralError__default.default("SOMETHING_WENT_WRONG_ERROR"); } @@ -268,48 +349,46 @@ var CodeForm = uiEntry.withOverride("TOTPCodeForm", function TOTPCodeForm(props) var CodeVerificationFooter = uiEntry.withOverride( "TOTPCodeVerificationFooter", function TOTPCodeVerificationFooter(_a) { - var featureState = _a.featureState, - recipeImplementation = _a.recipeImplementation; + var onSignOutClicked = _a.onSignOutClicked; var t = translationContext.useTranslation(); - var userContext = uiEntry.useUserContext(); - return jsxRuntime.jsx(React.Fragment, { - children: jsxRuntime.jsxs( - "div", - genericComponentOverrideContext.__assign( - { - "data-supertokens": "secondaryText secondaryLinkWithLeftArrow", - onClick: function () { - // TODO: onChooseAnotherFactor - return recipeImplementation.removeDevice({ - deviceName: featureState.deviceInfo.deviceName, - userContext: userContext, - }); - }, - }, - { - children: [ - jsxRuntime.jsx(arrowLeftIcon.ArrowLeftIcon, { color: "rgb(var(--palette-textPrimary))" }), - t("TOTP_REMOVE_DEVICE_LINK"), - ], - } - ) - ), - }); + return jsxRuntime.jsxs( + "div", + genericComponentOverrideContext.__assign( + { "data-supertokens": "secondaryText secondaryLinkWithLeftArrow", onClick: onSignOutClicked }, + { + children: [ + jsxRuntime.jsx(arrowLeftIcon.ArrowLeftIcon, { color: "rgb(var(--palette-textPrimary))" }), + t("TOTP_MFA_LOGOUT"), + ], + } + ) + ); } ); var CodeVerificationHeader = uiEntry.withOverride( "TOTPCodeVerificationHeader", - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function TOTPCodeVerificationHeader(_props) { + function TOTPCodeVerificationHeader(props) { var t = translationContext.useTranslation(); return jsxRuntime.jsxs(React.Fragment, { children: [ - jsxRuntime.jsx( + jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "headerTitle" }, - { children: t("TOTP_CODE_VERIFICATION_HEADER_TITLE") } + { "data-supertokens": "headerTitle withBackButton" }, + { + children: [ + props.showBackButton + ? jsxRuntime.jsx(backButton.BackButton, { onClick: props.onBackButtonClicked }) + : jsxRuntime.jsx("span", { + "data-supertokens": "backButtonPlaceholder backButtonCommon", + }), + t("TOTP_CODE_VERIFICATION_HEADER_TITLE"), + jsxRuntime.jsx("span", { + "data-supertokens": "backButtonPlaceholder backButtonCommon", + }), + ], + } ) ), jsxRuntime.jsx( @@ -3304,47 +3383,55 @@ var DeviceInfoSection = uiEntry.withOverride("TOTPDeviceInfoSection", function T }); }); -var DeviceSetupFooter = uiEntry.withOverride( - "TOTPDeviceSetupFooter", - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function TOTPDeviceSetupFooter(_props) { - var t = translationContext.useTranslation(); - return jsxRuntime.jsx( - "div", - genericComponentOverrideContext.__assign( - { "data-supertokens": "secondaryText privacyPolicyAndTermsAndConditions" }, - { children: t("TOTP_DEVICE_SETUP_FOOTER") } - ) - ); - } -); +var DeviceSetupFooter = uiEntry.withOverride("TOTPDeviceSetupFooter", function TOTPDeviceSetupFooter(_a) { + var onSignOutClicked = _a.onSignOutClicked; + var t = translationContext.useTranslation(); + return jsxRuntime.jsxs( + "div", + genericComponentOverrideContext.__assign( + { "data-supertokens": "secondaryText secondaryLinkWithLeftArrow", onClick: onSignOutClicked }, + { + children: [ + jsxRuntime.jsx(arrowLeftIcon.ArrowLeftIcon, { color: "rgb(var(--palette-textPrimary))" }), + t("TOTP_MFA_LOGOUT"), + ], + } + ) + ); +}); -var DeviceSetupHeader = uiEntry.withOverride( - "TOTPDeviceSetupHeader", - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function TOTPDeviceSetupHeader(_props) { - var t = translationContext.useTranslation(); - return jsxRuntime.jsxs(React.Fragment, { - children: [ - jsxRuntime.jsx( - "div", - genericComponentOverrideContext.__assign( - { "data-supertokens": "headerTitle" }, - { children: t("TOTP_DEVICE_SETUP_HEADER_TITLE") } - ) - ), - jsxRuntime.jsx( - "div", - genericComponentOverrideContext.__assign( - { "data-supertokens": "headerSubtitle secondaryText" }, - { children: t("TOTP_DEVICE_SETUP_HEADER_SUBTITLE") } - ) - ), - jsxRuntime.jsx("div", { "data-supertokens": "divider" }), - ], - }); - } -); +var DeviceSetupHeader = uiEntry.withOverride("TOTPDeviceSetupHeader", function TOTPDeviceSetupHeader(props) { + var t = translationContext.useTranslation(); + return jsxRuntime.jsxs(React.Fragment, { + children: [ + jsxRuntime.jsxs( + "div", + genericComponentOverrideContext.__assign( + { "data-supertokens": "headerTitle withBackButton" }, + { + children: [ + props.showBackButton + ? jsxRuntime.jsx(backButton.BackButton, { onClick: props.onBackButtonClicked }) + : jsxRuntime.jsx("span", { + "data-supertokens": "backButtonPlaceholder backButtonCommon", + }), + t("TOTP_DEVICE_SETUP_HEADER_TITLE"), + jsxRuntime.jsx("span", { "data-supertokens": "backButtonPlaceholder backButtonCommon" }), + ], + } + ) + ), + jsxRuntime.jsx( + "div", + genericComponentOverrideContext.__assign( + { "data-supertokens": "headerSubtitle secondaryText" }, + { children: t("TOTP_DEVICE_SETUP_HEADER_SUBTITLE") } + ) + ), + jsxRuntime.jsx("div", { "data-supertokens": "divider" }), + ], + }); +}); var TOTPMFAScreens; (function (TOTPMFAScreens) { @@ -3372,7 +3459,7 @@ var SignInUpTheme = function (_a) { }, }; return activeScreen === TOTPMFAScreens.Blocked - ? jsxRuntime.jsx(BlockedScreen, {}) + ? jsxRuntime.jsx(BlockedScreen, { nextRetryAt: featureState.nextRetryAt, onRetry: props.onRetryClicked }) : activeScreen === TOTPMFAScreens.Loading ? jsxRuntime.jsx(LoadingScreen, {}) : jsxRuntime.jsxs( @@ -3393,16 +3480,18 @@ var SignInUpTheme = function (_a) { activeScreen === TOTPMFAScreens.DeviceSetup ? jsxRuntime.jsx( DeviceSetupHeader, - genericComponentOverrideContext.__assign({}, commonProps) + genericComponentOverrideContext.__assign({}, commonProps, { + showBackButton: featureState.showBackButton, + onBackButtonClicked: props.onBackButtonClicked, + }) ) : jsxRuntime.jsx( CodeVerificationHeader, - genericComponentOverrideContext.__assign({}, commonProps) + genericComponentOverrideContext.__assign({}, commonProps, { + showBackButton: featureState.showBackButton, + onBackButtonClicked: props.onBackButtonClicked, + }) ), - featureState.error !== undefined && - jsxRuntime.jsx(generalError.GeneralError, { - error: featureState.error, - }), activeScreen === TOTPMFAScreens.DeviceSetup && jsxRuntime.jsx( DeviceInfoSection, @@ -3412,6 +3501,10 @@ var SignInUpTheme = function (_a) { onShowSecretClick: props.onShowSecretClick, }) ), + featureState.error !== undefined && + jsxRuntime.jsx(generalError.GeneralError, { + error: featureState.error, + }), jsxRuntime.jsx( CodeForm, genericComponentOverrideContext.__assign({}, commonProps, { @@ -3422,14 +3515,16 @@ var SignInUpTheme = function (_a) { DeviceSetupFooter, genericComponentOverrideContext.__assign( {}, - commonProps + commonProps, + { onSignOutClicked: props.onSignOutClicked } ) ) : jsxRuntime.jsx( CodeVerificationFooter, genericComponentOverrideContext.__assign( {}, - commonProps + commonProps, + { onSignOutClicked: props.onSignOutClicked } ) ), }) @@ -3501,15 +3596,18 @@ var defaultTranslationsTOTP = { TOTP_CODE_VERIFICATION_HEADER_TITLE: "Enter TOTP", TOTP_CODE_VERIFICATION_HEADER_SUBTITLE: "Open the two-factor authenticator (TOTP) app on your mobile device to view your authentication code", - TOTP_DEVICE_SETUP_HEADER_TITLE: "Setup an authenticator app", + TOTP_DEVICE_SETUP_HEADER_TITLE: "Enable TOTP", TOTP_DEVICE_SETUP_HEADER_SUBTITLE: "Please scan the given QR code from a phone app like Google Authenticator or Authy.", - TOTP_DEVICE_SETUP_FOOTER: "", TOTP_CODE_INPUT_LABEL: "Please enter TOTP from the app", TOTP_CODE_CONTINUE_BUTTON: "Continue", - TOTP_REMOVE_DEVICE_LINK: "Choose another factor", TOTP_BLOCKED_TITLE: "Account locked", TOTP_BLOCKED_SUBTITLE: "Account locked due to multiple failed login attempts.", + TOTP_MFA_BLOCKED_TIMER_START: "", + TOTP_MFA_BLOCKED_TIMER_END: "", + TOTP_MFA_BLOCKED_RETRY: "Try again", + TOTP_MFA_LOGOUT: "Logout", + INVALID_TOTP_ERROR: "Invalid TOTP.", } ), }; @@ -3523,13 +3621,14 @@ var useFeatureReducer = function () { loaded: true, error: action.error, deviceInfo: action.deviceInfo, + showBackButton: action.showBackButton, isBlocked: false, showSecret: false, }; case "setBlocked": return genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, oldState), - { error: action.error, deviceInfo: undefined } + { isBlocked: true, nextRetryAt: action.nextRetryAt, error: action.error } ); case "setError": return genericComponentOverrideContext.__assign( @@ -3567,6 +3666,7 @@ var useFeatureReducer = function () { deviceInfo: undefined, showSecret: false, isBlocked: false, + showBackButton: false, }, function (initArg) { var error = undefined; @@ -3621,9 +3721,13 @@ function useOnLoad(recipeImpl, dispatch, userContext) { isAllowedToSetup, isAlreadySetup, deviceInfo, - createResp; - return genericComponentOverrideContext.__generator(this, function (_a) { - switch (_a.label) { + createResp, + mfaClaim, + nextLength, + showBackButton; + var _a; + return genericComponentOverrideContext.__generator(this, function (_b) { + switch (_b.label) { case 0: error = undefined; errorQueryParam = genericComponentOverrideContext.getQueryParams("error"); @@ -3646,22 +3750,43 @@ function useOnLoad(recipeImpl, dispatch, userContext) { return [2 /*return*/]; } if (doSetup && !isAllowedToSetup) { + // TODO: redirect to access denied dispatch({ type: "setError", error: "Setup not allowed" }); return [2 /*return*/]; } if (!(isAllowedToSetup && (doSetup || !isAlreadySetup))) return [3 /*break*/, 2]; return [4 /*yield*/, recipeImpl.createDevice({ userContext: userContext })]; case 1: - createResp = _a.sent(); + createResp = _b.sent(); if ((createResp === null || createResp === void 0 ? void 0 : createResp.status) !== "OK") { throw new Error("TOTP device creation failed with duplicate name; should never happen"); } deviceInfo = genericComponentOverrideContext.__assign({}, createResp); delete deviceInfo.status; - _a.label = 2; + _b.label = 2; case 2: + return [ + 4 /*yield*/, + recipe$1.Session.getInstanceOrThrow().getClaimValue({ + claim: multifactorauth.MultiFactorAuthClaim, + userContext: userContext, + }), + ]; + case 3: + mfaClaim = _b.sent(); + nextLength = + (_a = mfaClaim === null || mfaClaim === void 0 ? void 0 : mfaClaim.n.length) !== null && + _a !== void 0 + ? _a + : 0; + showBackButton = nextLength !== 1; // No need to check if the component is unmounting, since this has no effect then. - dispatch({ type: "load", deviceInfo: deviceInfo, error: error }); + dispatch({ + type: "load", + deviceInfo: deviceInfo, + error: error, + showBackButton: showBackButton, + }); return [2 /*return*/]; } }); @@ -3718,6 +3843,40 @@ function useChildProps(recipe, recipeImplementation, state, dispatch, userContex onRetryClicked: function () { dispatch({ type: "restartFlow", error: undefined }); }, + onSignOutClicked: function () { + return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () { + return genericComponentOverrideContext.__generator(this, function (_a) { + switch (_a.label) { + case 0: + return [ + 4 /*yield*/, + recipe$1.Session.getInstanceOrThrow().signOut({ userContext: userContext }), + ]; + case 1: + _a.sent(); + if (!state.deviceInfo) return [3 /*break*/, 3]; + return [ + 4 /*yield*/, + recipeImplementation.removeDevice({ + deviceName: state.deviceInfo.deviceName, + userContext: userContext, + }), + ]; + case 2: + _a.sent(); + _a.label = 3; + case 3: + return [ + 4 /*yield*/, + uiEntry.redirectToAuth({ redirectBack: false, history: history }), + ]; + case 4: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }, onSuccess: function () { var redirectToPath = genericComponentOverrideContext.getRedirectToPathFromURL(); var redirectInfo = @@ -3876,19 +4035,15 @@ function getModifiedRecipeImplementation(originalImpl, dispatch) { error: "ERROR_TOTP_MFA_VERIFY_DEVICE_BLOCKED", nextRetryAt: Date.now() + res.retryAfterMs, }); - return [3 /*break*/, 5]; + return [3 /*break*/, 4]; case 2: - if (!(res.status === "INVALID_TOTP_ERROR")) return [3 /*break*/, 3]; - dispatch({ type: "setError", error: "ERROR_TOTP_MFA_VERIFY_DEVICE_INVALID_TOTP" }); - return [3 /*break*/, 5]; - case 3: - if (!(res.status === "UNKNOWN_DEVICE_ERROR")) return [3 /*break*/, 5]; + if (!(res.status === "UNKNOWN_DEVICE_ERROR")) return [3 /*break*/, 4]; return [4 /*yield*/, originalImpl.clearDeviceInfo({ userContext: input.userContext })]; - case 4: + case 3: _a.sent(); dispatch({ type: "restartFlow", error: "ERROR_TOTP_MFA_VERIFY_DEVICE_UNKNOWN_DEVICE" }); - _a.label = 5; - case 5: + _a.label = 4; + case 4: return [2 /*return*/, res]; } }); diff --git a/lib/ts/recipe/totp/components/features/mfa/index.tsx b/lib/ts/recipe/totp/components/features/mfa/index.tsx index edcb9e344..2411b555d 100644 --- a/lib/ts/recipe/totp/components/features/mfa/index.tsx +++ b/lib/ts/recipe/totp/components/features/mfa/index.tsx @@ -20,10 +20,12 @@ import { Fragment } from "react"; import { useMemo } from "react"; import { WindowHandlerReference } from "supertokens-web-js/utils/windowHandler"; +import { redirectToAuth } from "../../../../.."; import { ComponentOverrideContext } from "../../../../../components/componentOverride/componentOverrideContext"; import FeatureWrapper from "../../../../../components/featureWrapper"; import { useUserContext } from "../../../../../usercontext"; import { getQueryParams, getRedirectToPathFromURL, useOnMountAPICall } from "../../../../../utils"; +import { MultiFactorAuthClaim } from "../../../../multifactorauth"; import MultiFactorAuth from "../../../../multifactorauth/recipe"; import SessionRecipe from "../../../../session/recipe"; import MFATOTPThemeWrapper from "../../themes/mfa"; @@ -50,14 +52,16 @@ export const useFeatureReducer = (): [TOTPMFAState, React.Dispatch { let error: string | undefined = undefined; @@ -152,6 +157,7 @@ function useOnLoad(recipeImpl: RecipeInterface, dispatch: React.Dispatch { dispatch({ type: "restartFlow", error: undefined }); }, + onSignOutClicked: async () => { + await SessionRecipe.getInstanceOrThrow().signOut({ userContext }); + if (state.deviceInfo) { + await recipeImplementation.removeDevice({ deviceName: state.deviceInfo.deviceName, userContext }); + } + await redirectToAuth({ redirectBack: false, history: history }); + }, onSuccess: () => { const redirectToPath = getRedirectToPathFromURL(); const redirectInfo = @@ -335,8 +354,6 @@ function getModifiedRecipeImplementation( error: "ERROR_TOTP_MFA_VERIFY_DEVICE_BLOCKED", nextRetryAt: Date.now() + res.retryAfterMs, }); - } else if (res.status === "INVALID_TOTP_ERROR") { - dispatch({ type: "setError", error: "ERROR_TOTP_MFA_VERIFY_DEVICE_INVALID_TOTP" }); } else if (res.status === "UNKNOWN_DEVICE_ERROR") { await originalImpl.clearDeviceInfo({ userContext: input.userContext }); dispatch({ type: "restartFlow", error: "ERROR_TOTP_MFA_VERIFY_DEVICE_UNKNOWN_DEVICE" }); diff --git a/lib/ts/recipe/totp/components/themes/mfa/blockedScreen.tsx b/lib/ts/recipe/totp/components/themes/mfa/blockedScreen.tsx index 7570be1dd..86ffab026 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/blockedScreen.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/blockedScreen.tsx @@ -15,8 +15,11 @@ import { BlockedIcon } from "../../../../../components/assets/blockedIcon"; import { withOverride } from "../../../../../components/componentOverride/withOverride"; import { useTranslation } from "../../../../../translation/translationContext"; +import { FormRow } from "../../../../emailpassword/components/library"; -const TOTPBlockedScreen: React.FC = () => { +import { RetryButton } from "./retryButton"; + +const TOTPBlockedScreen: React.FC<{ nextRetryAt: number; onRetry: () => void }> = (props) => { const t = useTranslation(); return ( @@ -24,9 +27,11 @@ const TOTPBlockedScreen: React.FC = () => {
{t("TOTP_BLOCKED_TITLE")}
-
{t("TOTP_BLOCKED_SUBTITLE")}
+ + +
); diff --git a/lib/ts/recipe/totp/components/themes/mfa/index.tsx b/lib/ts/recipe/totp/components/themes/mfa/index.tsx index a8268aa30..bafe73729 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/index.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/index.tsx @@ -58,7 +58,7 @@ const SignInUpTheme: React.FC = }; return activeScreen === TOTPMFAScreens.Blocked ? ( - + ) : activeScreen === TOTPMFAScreens.Loading ? ( ) : ( @@ -67,11 +67,18 @@ const SignInUpTheme: React.FC = {featureState.loaded && ( {activeScreen === TOTPMFAScreens.DeviceSetup ? ( - + ) : ( - + )} - {featureState.error !== undefined && } {activeScreen === TOTPMFAScreens.DeviceSetup && ( = onShowSecretClick={props.onShowSecretClick} /> )} + {featureState.error !== undefined && } + ) : ( - + ) } /> diff --git a/lib/ts/recipe/totp/components/themes/mfa/retryButton.tsx b/lib/ts/recipe/totp/components/themes/mfa/retryButton.tsx new file mode 100644 index 000000000..74286dccf --- /dev/null +++ b/lib/ts/recipe/totp/components/themes/mfa/retryButton.tsx @@ -0,0 +1,84 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * Imports. + */ + +import React, { useCallback, useEffect, useState } from "react"; + +import { withOverride } from "../../../../../components/componentOverride/withOverride"; +import { useTranslation } from "../../../../../translation/translationContext"; + +export const RetryButton = withOverride( + "TOTPRetryButton", + function TOTPRetryButton({ + nextRetryAt, + onClick, + }: { + nextRetryAt: number; + onClick: () => void; + }): JSX.Element | null { + const t = useTranslation(); + + const getTimeLeft = useCallback(() => { + const timeLeft = nextRetryAt - Date.now(); + return timeLeft < 0 ? undefined : Math.ceil(timeLeft / 1000); + }, [nextRetryAt]); + + const [secsUntilRetry, setSecsUntilRetry] = useState(getTimeLeft()); + + useEffect(() => { + // This runs every time the loginAttemptInfo updates, so after every resend + const interval = setInterval(() => { + const timeLeft = getTimeLeft(); + + if (timeLeft === undefined) { + clearInterval(interval); + } + + setSecsUntilRetry(timeLeft); + }, 500); + + return () => { + // This can safely run twice + clearInterval(interval); + }; + }, [getTimeLeft, setSecsUntilRetry]); + + return ( + + ); + } +); diff --git a/lib/ts/recipe/totp/components/themes/mfa/totpCodeForm.tsx b/lib/ts/recipe/totp/components/themes/mfa/totpCodeForm.tsx index f590f4e46..df23b7bef 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/totpCodeForm.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/totpCodeForm.tsx @@ -76,7 +76,7 @@ export const CodeForm = withOverride( }); } - // We can redirect these statuses, since they all cause a redirection + // We can return these statuses, since they all cause a redirection // and we don't really want to show anything if ( response.status === "OK" || @@ -87,7 +87,10 @@ export const CodeForm = withOverride( } if (response.status === "INVALID_TOTP_ERROR") { - throw new STGeneralError("GENERAL_ERROR_OTP_INVALID"); + return { + status: "FIELD_ERROR", + formFields: [{ id: "totp", error: "INVALID_TOTP_ERROR" }], + }; } throw new STGeneralError("SOMETHING_WENT_WRONG_ERROR"); diff --git a/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationFooter.tsx b/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationFooter.tsx index c2958c8fd..0fca18208 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationFooter.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationFooter.tsx @@ -12,36 +12,24 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { Fragment } from "react"; - import ArrowLeftIcon from "../../../../../components/assets/arrowLeftIcon"; import { withOverride } from "../../../../../components/componentOverride/withOverride"; import { useTranslation } from "../../../../../translation/translationContext"; -import { useUserContext } from "../../../../../usercontext"; import type { TOTPMFACommonProps } from "../../../types"; export const CodeVerificationFooter = withOverride( "TOTPCodeVerificationFooter", - function TOTPCodeVerificationFooter({ featureState, recipeImplementation }: TOTPMFACommonProps): JSX.Element { + function TOTPCodeVerificationFooter({ + onSignOutClicked, + }: TOTPMFACommonProps & { onSignOutClicked: () => void }): JSX.Element { const t = useTranslation(); - const userContext = useUserContext(); return ( - -
- // TODO: onChooseAnotherFactor - recipeImplementation.removeDevice({ - deviceName: featureState.deviceInfo!.deviceName, - userContext, - }) - }> - - {t("TOTP_REMOVE_DEVICE_LINK")} -
-
+
+ + {t("TOTP_MFA_LOGOUT")} +
); } ); diff --git a/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.tsx b/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.tsx index 98969624f..8e0e8fc27 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.tsx @@ -16,18 +16,32 @@ import { Fragment } from "react"; import { withOverride } from "../../../../../components/componentOverride/withOverride"; import { useTranslation } from "../../../../../translation/translationContext"; +import BackButton from "../../../../emailpassword/components/library/backButton"; import type { TOTPMFACommonProps } from "../../../types"; export const CodeVerificationHeader = withOverride( "TOTPCodeVerificationHeader", - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function TOTPCodeVerificationHeader(_props: TOTPMFACommonProps): JSX.Element { + function TOTPCodeVerificationHeader( + props: TOTPMFACommonProps & { showBackButton: boolean; onBackButtonClicked: () => void } + ): JSX.Element { const t = useTranslation(); return ( -
{t("TOTP_CODE_VERIFICATION_HEADER_TITLE")}
+
+ {props.showBackButton ? ( + + ) : ( + + {/* empty span for spacing the back button */} + + )} + {t("TOTP_CODE_VERIFICATION_HEADER_TITLE")} + + {/* empty span for spacing the back button */} + +
{t("TOTP_CODE_VERIFICATION_HEADER_SUBTITLE")}
diff --git a/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.tsx b/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.tsx index a61248a70..7fd6d01ef 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.tsx @@ -13,6 +13,7 @@ * under the License. */ +import ArrowLeftIcon from "../../../../../components/assets/arrowLeftIcon"; import { withOverride } from "../../../../../components/componentOverride/withOverride"; import { useTranslation } from "../../../../../translation/translationContext"; @@ -20,13 +21,15 @@ import type { TOTPMFACommonProps } from "../../../types"; export const DeviceSetupFooter = withOverride( "TOTPDeviceSetupFooter", - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function TOTPDeviceSetupFooter(_props: TOTPMFACommonProps): JSX.Element | null { + function TOTPDeviceSetupFooter({ + onSignOutClicked, + }: TOTPMFACommonProps & { onSignOutClicked: () => void }): JSX.Element | null { const t = useTranslation(); return ( -
- {t("TOTP_DEVICE_SETUP_FOOTER")} +
+ + {t("TOTP_MFA_LOGOUT")}
); } diff --git a/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.tsx b/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.tsx index aad3767f1..6854c5963 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.tsx @@ -16,18 +16,32 @@ import { Fragment } from "react"; import { withOverride } from "../../../../../components/componentOverride/withOverride"; import { useTranslation } from "../../../../../translation/translationContext"; +import BackButton from "../../../../emailpassword/components/library/backButton"; import type { TOTPMFACommonProps } from "../../../types"; export const DeviceSetupHeader = withOverride( "TOTPDeviceSetupHeader", - // eslint-disable-next-line @typescript-eslint/no-unused-vars - function TOTPDeviceSetupHeader(_props: TOTPMFACommonProps): JSX.Element { + function TOTPDeviceSetupHeader( + props: TOTPMFACommonProps & { showBackButton: boolean; onBackButtonClicked: () => void } + ): JSX.Element { const t = useTranslation(); return ( -
{t("TOTP_DEVICE_SETUP_HEADER_TITLE")}
+
+ {props.showBackButton ? ( + + ) : ( + + {/* empty span for spacing the back button */} + + )} + {t("TOTP_DEVICE_SETUP_HEADER_TITLE")} + + {/* empty span for spacing the back button */} + +
{t("TOTP_DEVICE_SETUP_HEADER_SUBTITLE")}
diff --git a/lib/ts/recipe/totp/components/themes/styles.css b/lib/ts/recipe/totp/components/themes/styles.css index b07f06efe..f8d98cb77 100644 --- a/lib/ts/recipe/totp/components/themes/styles.css +++ b/lib/ts/recipe/totp/components/themes/styles.css @@ -21,6 +21,8 @@ border-radius: 12px; border: 1px solid rgb(var(--palette-inputBorder)); padding: 16px; + max-width: 100px; + max-height: 100px; } [data-supertokens~="showTOTPSecret"] { @@ -33,5 +35,17 @@ [data-supertokens~="totpSecret"] { display: block; border-radius: 6px; + padding: 12px; + color: rgb(var(--palette-textLink)); + font-size: var(--font-size-1); + font-weight: 600; + letter-spacing: 3.36px; background: rgba(var(--palette-textLink), 0.08); } + +[data-supertokens~="retryCodeBtn"]:disabled { + border: 0; + border-radius: 6px; + color: rgb(var(--palette-error)); + background: rgb(var(--palette-errorBackground)); +} diff --git a/lib/ts/recipe/totp/components/themes/translations.ts b/lib/ts/recipe/totp/components/themes/translations.ts index 8c0a3b14e..d6ba413f8 100644 --- a/lib/ts/recipe/totp/components/themes/translations.ts +++ b/lib/ts/recipe/totp/components/themes/translations.ts @@ -9,14 +9,18 @@ export const defaultTranslationsTOTP = { TOTP_CODE_VERIFICATION_HEADER_TITLE: "Enter TOTP", TOTP_CODE_VERIFICATION_HEADER_SUBTITLE: "Open the two-factor authenticator (TOTP) app on your mobile device to view your authentication code", - TOTP_DEVICE_SETUP_HEADER_TITLE: "Setup an authenticator app", + TOTP_DEVICE_SETUP_HEADER_TITLE: "Enable TOTP", TOTP_DEVICE_SETUP_HEADER_SUBTITLE: "Please scan the given QR code from a phone app like Google Authenticator or Authy.", - TOTP_DEVICE_SETUP_FOOTER: "", TOTP_CODE_INPUT_LABEL: "Please enter TOTP from the app", TOTP_CODE_CONTINUE_BUTTON: "Continue", - TOTP_REMOVE_DEVICE_LINK: "Choose another factor", TOTP_BLOCKED_TITLE: "Account locked", TOTP_BLOCKED_SUBTITLE: "Account locked due to multiple failed login attempts.", + TOTP_MFA_BLOCKED_TIMER_START: "", + TOTP_MFA_BLOCKED_TIMER_END: "", + TOTP_MFA_BLOCKED_RETRY: "Try again", + TOTP_MFA_LOGOUT: "Logout", + + INVALID_TOTP_ERROR: "Invalid TOTP.", }, }; diff --git a/lib/ts/recipe/totp/types.ts b/lib/ts/recipe/totp/types.ts index 3c65e2a78..9cbd6ce28 100644 --- a/lib/ts/recipe/totp/types.ts +++ b/lib/ts/recipe/totp/types.ts @@ -38,6 +38,7 @@ export type TOTPDeviceInfo = { export type TOTPMFAAction = | { type: "load"; + showBackButton: boolean; deviceInfo: TOTPDeviceInfo | undefined; error: string | undefined; } @@ -70,6 +71,7 @@ export type TOTPMFAState = { showSecret: boolean; nextRetryAt?: number; isBlocked: boolean; + showBackButton: boolean; loaded: boolean; error: string | undefined; }; @@ -88,6 +90,7 @@ export type TOTPMFAProps = { onShowSecretClick: () => void; onBackButtonClicked: () => void; onRetryClicked: () => void; + onSignOutClicked: () => void; dispatch: Dispatch; featureState: TOTPMFAState; userContext?: any;