From e6fccfcb41e9a8d94878574a52003d9aa30f6b87 Mon Sep 17 00:00:00 2001 From: Abdullah Shahbaz Date: Tue, 28 Nov 2023 02:54:39 +0500 Subject: [PATCH] React: Package Translations (#62) * added translation file * add translation params * updated react example * refactored component structure * added german translation * resolved PR review --- .../PasskeyLoginWithEmailOTPFallbackFlow.tsx | 8 +- .../PasskeySignupWithEmailOTPFallbackFlow.tsx | 14 +- packages/react/src/i18n/index.ts | 41 ++++-- packages/react/src/i18n/locales/de.ts | 94 ++++++++++++ packages/react/src/i18n/locales/en.ts | 134 ++++++++++++------ packages/react/src/index.tsx | 15 +- packages/react/src/screens/PasskeyWelcome.tsx | 45 ------ packages/react/src/screens/login/EmailOTP.tsx | 102 +++++++++++++ .../src/screens/{ => login}/InitiateLogin.tsx | 24 ++-- .../react/src/screens/login/PasskeyError.tsx | 91 ++++++++++++ .../screens/{ => shared}/PasskeyAppend.tsx | 19 ++- .../src/screens/{ => signup}/EmailOTP.tsx | 35 ++--- .../screens/{ => signup}/InitiateSignup.tsx | 39 ++--- .../screens/{ => signup}/PasskeyBenefits.tsx | 19 ++- .../src/screens/{ => signup}/PasskeyError.tsx | 49 +++---- .../screens/{ => signup}/PasskeySignup.tsx | 28 ++-- .../src/screens/signup/PasskeyWelcome.tsx | 44 ++++++ .../react-example/src/pages/AuthPage.tsx | 14 +- .../react-example/src/translations/en.ts | 9 ++ .../react-example/src/translations/fr.ts | 92 ++++++++++++ 20 files changed, 678 insertions(+), 238 deletions(-) create mode 100644 packages/react/src/i18n/locales/de.ts delete mode 100644 packages/react/src/screens/PasskeyWelcome.tsx create mode 100644 packages/react/src/screens/login/EmailOTP.tsx rename packages/react/src/screens/{ => login}/InitiateLogin.tsx (83%) create mode 100644 packages/react/src/screens/login/PasskeyError.tsx rename packages/react/src/screens/{ => shared}/PasskeyAppend.tsx (71%) rename packages/react/src/screens/{ => signup}/EmailOTP.tsx (68%) rename packages/react/src/screens/{ => signup}/InitiateSignup.tsx (73%) rename packages/react/src/screens/{ => signup}/PasskeyBenefits.tsx (71%) rename packages/react/src/screens/{ => signup}/PasskeyError.tsx (55%) rename packages/react/src/screens/{ => signup}/PasskeySignup.tsx (67%) create mode 100644 packages/react/src/screens/signup/PasskeyWelcome.tsx create mode 100644 playground/react-example/src/translations/en.ts create mode 100644 playground/react-example/src/translations/fr.ts diff --git a/packages/react/src/flows/PasskeyLoginWithEmailOTPFallbackFlow.tsx b/packages/react/src/flows/PasskeyLoginWithEmailOTPFallbackFlow.tsx index c766dcca..fb1c6c80 100644 --- a/packages/react/src/flows/PasskeyLoginWithEmailOTPFallbackFlow.tsx +++ b/packages/react/src/flows/PasskeyLoginWithEmailOTPFallbackFlow.tsx @@ -1,9 +1,9 @@ import { PasskeyLoginWithEmailOtpFallbackScreens } from '@corbado/web-core'; -import { EmailOTP } from '../screens/EmailOTP'; -import { InitiateLogin } from '../screens/InitiateLogin'; -import { PasskeyAppend } from '../screens/PasskeyAppend'; -import { PasskeyError } from '../screens/PasskeyError'; +import { EmailOTP } from '../screens/login/EmailOTP'; +import { InitiateLogin } from '../screens/login/InitiateLogin'; +import { PasskeyError } from '../screens/login/PasskeyError'; +import { PasskeyAppend } from '../screens/shared/PasskeyAppend'; export const PasskeyLoginWithEmailOTPFallbackFlow = { [PasskeyLoginWithEmailOtpFallbackScreens.Start]: InitiateLogin, diff --git a/packages/react/src/flows/PasskeySignupWithEmailOTPFallbackFlow.tsx b/packages/react/src/flows/PasskeySignupWithEmailOTPFallbackFlow.tsx index fb0818df..c136f9b0 100644 --- a/packages/react/src/flows/PasskeySignupWithEmailOTPFallbackFlow.tsx +++ b/packages/react/src/flows/PasskeySignupWithEmailOTPFallbackFlow.tsx @@ -1,12 +1,12 @@ import { PasskeySignupWithEmailOtpFallbackScreens } from '@corbado/web-core'; -import { EmailOTP } from '../screens/EmailOTP'; -import { InitiateSignup } from '../screens/InitiateSignup'; -import { PasskeyAppend } from '../screens/PasskeyAppend'; -import { PasskeyBenefits } from '../screens/PasskeyBenefits'; -import { PasskeyError } from '../screens/PasskeyError'; -import { PasskeySignup } from '../screens/PasskeySignup'; -import { PasskeyWelcome } from '../screens/PasskeyWelcome'; +import { PasskeyAppend } from '../screens/shared/PasskeyAppend'; +import { EmailOTP } from '../screens/signup/EmailOTP'; +import { InitiateSignup } from '../screens/signup/InitiateSignup'; +import { PasskeyBenefits } from '../screens/signup/PasskeyBenefits'; +import { PasskeyError } from '../screens/signup/PasskeyError'; +import { PasskeySignup } from '../screens/signup/PasskeySignup'; +import { PasskeyWelcome } from '../screens/signup/PasskeyWelcome'; export const PasskeySignupWithEmailOTPFallbackFlow = { [PasskeySignupWithEmailOtpFallbackScreens.Start]: InitiateSignup, diff --git a/packages/react/src/i18n/index.ts b/packages/react/src/i18n/index.ts index 46369af5..e4cd6acd 100644 --- a/packages/react/src/i18n/index.ts +++ b/packages/react/src/i18n/index.ts @@ -2,9 +2,10 @@ import i18n from 'i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import { initReactI18next } from 'react-i18next'; +import TRANSLATIONS_DE from './locales/de'; import TRANSLATIONS_EN from './locales/en'; -const defaultLanguage = 'en'; +export const defaultLanguage = 'en'; void i18n .use(initReactI18next) @@ -12,6 +13,7 @@ void i18n .init({ resources: { en: { translation: TRANSLATIONS_EN }, + de: { translation: TRANSLATIONS_DE }, }, keySeparator: '.', fallbackLng: defaultLanguage, @@ -38,17 +40,40 @@ const setI18nLanguage = (lang: string): void => { /** * @function handleDynamicLocaleSetup An async function that handles setting the language of the user to the preferred locale. * - * @param {*} [locale=navigator.language] This is the locale a user wants to set. It deefaults to the locale of the user's browser if no value is padded. e.g `de-DE`, `en-GB` + * @param {boolean} [shouldAutoDetectLanguage=true] This is a boolean that determines if the language of the user should be auto detected. It defaults to true if no value is passed. + * @param {string} [defaultLang=defaultLanguage] This is the default language to be used if the language of the user cannot be auto detected. It defaults to `en` if no value is passed. + * @param {object} [customTranslations={}] This is an object containing custom translations. Each key should be a language code and each value should be an object containing the translations for that language. * @return {*} Promise */ -export const handleDynamicLocaleSetup = (locale = navigator.language) => { - const lang = getLanguage(locale.substring(0, 2)); +export const handleDynamicLocaleSetup = ( + shouldAutoDetectLanguage = true, + defaultLang = defaultLanguage, + customTranslations: Record | null = null, +) => { + const locale = window.navigator.language; + + // If the language of the user is the same as the default language and there are no custom translations, do nothing + if ( + shouldAutoDetectLanguage && + defaultLang === defaultLanguage && + locale === defaultLanguage && + !customTranslations + ) { + return; + } + + // Add custom translations + for (const [lang, translations] of Object.entries(customTranslations ?? {})) { + i18n.addResourceBundle(lang, 'translation', translations, true, true); + } + + const language = shouldAutoDetectLanguage ? getLanguage(locale) : defaultLang; try { - void i18n.changeLanguage(lang); - setI18nLanguage(lang); + void i18n.changeLanguage(language); + setI18nLanguage(language); } catch { - void i18n.changeLanguage(defaultLanguage); - setI18nLanguage(defaultLanguage); + void i18n.changeLanguage(defaultLang); + setI18nLanguage(defaultLang); } }; diff --git a/packages/react/src/i18n/locales/de.ts b/packages/react/src/i18n/locales/de.ts new file mode 100644 index 00000000..a7cdfc36 --- /dev/null +++ b/packages/react/src/i18n/locales/de.ts @@ -0,0 +1,94 @@ +const de = { + common: { + passkeyPrompt: { + header: 'Schneller anmelden mit', + button_showPasskeyBenefits: 'Passkeys', + button_start: 'Aktivieren', + button_skip: 'Vielleicht später', + }, + }, + signup: { + start: { + header: 'Erstellen Sie Ihr Konto', + subheader: 'Sie haben bereits ein Konto?', + button_login: 'Anmelden', + button_submit: 'Mit E-Mail fortfahren', + textField_name: 'Name', + textField_email: 'E-Mail-Adresse', + validationError_name: 'Bitte geben Sie einen Namen ein', + validationError_email: 'Bitte geben Sie eine gültige E-Mail ein', + }, + passkey: { + header: 'Lassen Sie uns einrichten mit', + body: 'Wir werden ein Konto für', + button_showPasskeyBenefits: 'Passkeys', + button_start: 'Konto erstellen', + button_emailOtp: 'Einmalcode per E-Mail senden', + button_back: 'Zurück', + }, + passkeyBenefits: { + header: 'Passkeys', + body_introduction: + 'Mit Passkeys müssen Sie sich keine komplexen Passwörter mehr merken. Melden Sie sich sicher an, indem Sie', + body_loginMethods: 'Face ID, Touch ID oder Sperrcode verwenden.', + button_start: 'Passkey erstellen', + button_skip: 'Vielleicht später', + }, + passkeySuccess: { + header: 'Willkommen!', + subheader: 'Passkey erstellt', + body_text1: 'Sie können Ihre Identität nun mit Ihrem', + body_text2: 'Passkey oder per E-Mail Einmalcode', + body_text3: 'bei der Anmeldung bestätigen.', + button: 'Fortsetzen', + }, + passkeyError: { + header: 'Lassen Sie uns einrichten', + body_errorMessage1: 'Das Erstellen Ihres Kontos war nicht möglich mit', + body_errorMessage2: 'Versuchen Sie es erneut oder melden Sie sich mit E-Mail Einmalcode an.', + button_showPasskeyBenefits: 'Passkeys', + button_retry: 'Erneut versuchen', + button_emailOtp: 'Einmalcode per E-Mail senden', + button_back: 'Zurück', + button_cancel: 'Abbrechen', + }, + emailOtp: { + header: 'Geben Sie den Code ein, um das Konto zu erstellen', + body_text1: 'Wir haben gerade einen Einmalcode gesendet an ', + body_text2: 'Der Code läuft in Kürze ab, bitte geben Sie ihn bald ein.', + validationError_otp: 'Der OTP ist falsch', + button_verify: 'Fortsetzen', + button_sendOtpAgain: 'Einmalcode erneut senden', + button_back: 'Abbrechen', + }, + }, + login: { + start: { + header: 'Willkommen zurück!', + subheader: 'Sie haben noch kein Konto?', + button_signup: 'Konto erstellen', + button_submit: 'Mit E-Mail fortfahren', + textField_email: 'E-Mail-Adresse', + validationError_email: 'Bitte geben Sie eine gültige E-Mail ein', + }, + passkeyError: { + header: 'Anmeldung mit Passkeys', + body: 'Anmeldung mit Passkeys nicht möglich. Versuchen Sie es erneut oder melden Sie sich mit E-Mail Einmalcode an.', + button_retry: 'Erneut versuchen', + button_emailOtp: 'Einmalcode per E-Mail senden', + button_back: 'Zurück', + button_cancel: 'Abbrechen', + }, + emailOtp: { + header: 'Geben Sie den Code ein, um sich anzumelden', + body_text1: 'Wir haben gerade einen Einmalcode gesendet an ', + body_text2: 'Der Code läuft in Kürze ab, bitte geben Sie ihn bald ein.', + validationError_otp: 'Der OTP ist falsch', + button_verify: 'Fortsetzen', + button_sendOtpAgain: 'Einmalcode erneut senden', + button_back: 'Abbrechen', + }, + }, +}; + +export default de; diff --git a/packages/react/src/i18n/locales/en.ts b/packages/react/src/i18n/locales/en.ts index e9d90d63..92108753 100644 --- a/packages/react/src/i18n/locales/en.ts +++ b/packages/react/src/i18n/locales/en.ts @@ -1,53 +1,93 @@ const en = { - signup: { - header: 'Create your account', - 'sub-header': 'You already have an account? <1>Log in', - continue_email: 'Continue with email', - }, - passkey_signup: { - header: 'Let’s get you set up with <1>Passkeys', - 'sub-header': 'We’ll create an account for <1>{{email_address}}.', - primary_btn: 'Create your account', - secondary_btn: 'Send email one time code', - tertiary_btn: 'Back', - }, - create_passkey: { - header: 'Passkeys', - body: 'With passkeys, you don’t need to remember complex passwords anymore. Log in securely to using <1>Face ID, Touch ID or screen lock code.', - primary_btn: 'Create passkey', - secondary_btn: 'Maybe later', - }, - create_passkey_error: { - header: 'Let’s get you set up', - body: 'Creating your account with <1>passkeys not possible. Try again or log in with email one time code.', - primary_btn: 'Try again', - secondary_btn: 'Send email one time code', - tertiary_btn: 'Back', + common: { + passkeyPrompt: { + header: 'Log in even faster with', + button_showPasskeyBenefits: 'Passkeys', + button_start: 'Activate', + button_skip: 'Maybe later', + }, }, - create_passkey_success: { - header: 'Welcome!', - secondary_header: 'Passkey created', - body: 'You can now confirm your identity using your <1>passkey or via email one time code when you log in.', - }, - activate_passkey: { - header: 'Log in even faster with <1>Passkeys', - primary_btn: 'Activate', - secondary_btn: 'Maybe later', - }, - email_link: { - header: 'Enter code to create account', - body: 'We just sent a one time code to <1>email adress. The code expires shortly, so please enter it soon.', - otp_required: 'Valid OTP required', - }, - generic: { - name: 'Name', - email: 'Email address', - continue: 'Continue', - cancel: 'Cancel', + signup: { + start: { + header: 'Create your account', + subheader: 'You already have an account?', + button_login: 'Log in', + button_submit: 'Continue with email', + textField_name: 'Name', + textField_email: 'Email address', + validationError_name: 'Please enter a name', + validationError_email: 'Please enter a valid email', + }, + passkey: { + header: "Let's get you set up with", + body: "We'll create an account for", + button_showPasskeyBenefits: 'Passkeys', + button_start: 'Create your account', + button_emailOtp: 'Send email one time code', + button_back: 'Back', + }, + passkeyBenefits: { + header: 'Passkeys', + body_introduction: + "With passkeys, you don't need to remember complex passwords anymore. Log in securely by using", + body_loginMethods: 'Face ID, Touch ID or screen lock code.', + button_start: 'Create passkey', + button_skip: 'Maybe later', + }, + passkeySuccess: { + header: 'Welcome!', + subheader: 'Passkey created', + body_text1: 'You can now confirm your identity using your', + body_text2: 'passkey or via email one time code', + body_text3: 'when you log in.', + button: 'Continue', + }, + passkeyError: { + header: "Let's get you set up", + body_errorMessage1: 'Creating your account was not possible with', + body_errorMessage2: 'Try again or sign up with email one time code.', + button_showPasskeyBenefits: 'passkeys', + button_retry: 'Try again', + button_emailOtp: 'Send email one time code', + button_back: 'Back', + button_cancel: 'Cancel', + }, + emailOtp: { + header: 'Enter code to create account', + body_text1: 'We just sent a one time code to ', + body_text2: 'The code expires shortly, so please enter it soon.', + validationError_otp: 'OTP is incorrect', + button_verify: 'Continue', + button_sendOtpAgain: 'Send one time code again', + button_back: 'Cancel', + }, }, - validation_errors: { - name: 'Please enter a name', - email: 'Please enter a valid email', + login: { + start: { + header: 'Welcome back!', + subheader: "Don't have and account yet?", + button_signup: 'Create account', + button_submit: 'Continue with email', + textField_email: 'Email address', + validationError_email: 'Please enter a valid email', + }, + passkeyError: { + header: 'Log in with Passkeys', + body: 'Login with passkeys not possible. Try again or log in with email one time code.', + button_retry: 'Try again', + button_emailOtp: 'Send email one time code', + button_back: 'Back', + button_cancel: 'Cancel', + }, + emailOtp: { + header: 'Enter code to log in', + body_text1: 'We just sent a one time code to ', + body_text2: 'The code expires shortly, so please enter it soon.', + validationError_otp: 'OTP is incorrect', + button_verify: 'Continue', + button_sendOtpAgain: 'Send one time code again', + button_back: 'Cancel', + }, }, }; diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index b302486a..cb41ae3e 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -6,13 +6,26 @@ import React from 'react'; import FlowHandlerProvider from './contexts/FlowHandlerProvider'; import UserDataProvider from './contexts/UserDataProvider'; +import { defaultLanguage as defaultAppLanguage, handleDynamicLocaleSetup } from './i18n'; import { ScreensFlow } from './screens/ScreenFlow'; interface Props { onLoggedIn: () => void; + defaultLanguage?: string; + autoDetectLanguage?: boolean; + customTranslations?: Record | null; } -const CorbadoAuthUI = ({ onLoggedIn }: Props) => { +const CorbadoAuthUI = ({ + onLoggedIn, + defaultLanguage = defaultAppLanguage, + autoDetectLanguage = true, + customTranslations = null, +}: Props) => { + React.useEffect(() => { + handleDynamicLocaleSetup(autoDetectLanguage, defaultLanguage, customTranslations); + }, []); + return (
diff --git a/packages/react/src/screens/PasskeyWelcome.tsx b/packages/react/src/screens/PasskeyWelcome.tsx deleted file mode 100644 index 3213c13d..00000000 --- a/packages/react/src/screens/PasskeyWelcome.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; - -import { PasskeyScreensWrapper } from '../components/PasskeyScreensWrapper'; -import useFlowHandler from '../hooks/useFlowHandler'; - -export const PasskeyWelcome = () => { - const { t } = useTranslation(); - const { navigateNext } = useFlowHandler(); - - const header = useMemo(() => t('create_passkey_success.header'), [t]); - const secondaryHeader = useMemo(() => t('create_passkey_success.secondary_header'), [t]); - const body = useMemo( - () => ( - - You can now confirm your identity using your passkey or via email one time code when you log - in. - - ), - [t], - ); - - const primaryButton = useMemo(() => t('generic.continue'), [t]); - - const handleClick = useCallback(() => { - void navigateNext(); - }, [navigateNext]); - - const props = useMemo( - () => ({ - header, - secondaryHeader, - body, - primaryButton, - onClick: handleClick, - }), - [header, secondaryHeader, body, primaryButton, handleClick], - ); - - return ( - <> - - - ); -}; diff --git a/packages/react/src/screens/login/EmailOTP.tsx b/packages/react/src/screens/login/EmailOTP.tsx new file mode 100644 index 00000000..0cb27411 --- /dev/null +++ b/packages/react/src/screens/login/EmailOTP.tsx @@ -0,0 +1,102 @@ +import { FlowHandlerEvents, useCorbado } from '@corbado/react-sdk'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Button, Gmail, Link, OTPInput, Outlook, Text, Yahoo } from '../../components'; +import useFlowHandler from '../../hooks/useFlowHandler'; +import useUserData from '../../hooks/useUserData'; + +export const EmailOTP = () => { + const { t } = useTranslation('translation', { keyPrefix: 'login.emailOtp' }); + const { navigateBack, navigateNext, currentFlow } = useFlowHandler(); + const { getUserAuthMethods, completeLoginWithEmailOTP } = useCorbado(); + const { email, sendEmail } = useUserData(); + + const [otp, setOTP] = React.useState([]); + const [error, setError] = React.useState(''); + const [loading, setLoading] = React.useState(false); + + React.useEffect(() => { + void sendEmail(currentFlow); + }, []); + + const handleCancel = () => navigateBack(); + + const handleOtpChange = (userOTP: string[]) => setOTP(userOTP); + + const handleOTPVerification = async (payload: string) => { + setLoading(true); + try { + await completeLoginWithEmailOTP(payload); + const authMethods = await getUserAuthMethods(email ?? ''); + const userHasPasskey = authMethods.selectedMethods.includes('webauthn'); + void navigateNext(FlowHandlerEvents.PasskeySuccess, { userHasPasskey }); + } catch (error) { + console.log({ error }); + setLoading(false); + } + }; + + const handleSubmit = () => { + setError(''); + const mergedChars = otp.join(''); + if (mergedChars.length < 6) { + setError(t('validationError_otp')); + return; + } + + void handleOTPVerification(mergedChars); + }; + + return ( + <> + {t('header')} + + + {t('body_text1')} {email}. {t('body_text2')} + + +
+ + Google + + + Yahoo + + + Outlook + +
+ + {error &&

{error}

} + + + + + ); +}; diff --git a/packages/react/src/screens/InitiateLogin.tsx b/packages/react/src/screens/login/InitiateLogin.tsx similarity index 83% rename from packages/react/src/screens/InitiateLogin.tsx rename to packages/react/src/screens/login/InitiateLogin.tsx index a4b33e3d..65bb80da 100644 --- a/packages/react/src/screens/InitiateLogin.tsx +++ b/packages/react/src/screens/login/InitiateLogin.tsx @@ -4,13 +4,13 @@ import { canUsePasskeys, FlowHandlerEvents, FlowType } from '@corbado/web-core'; import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, LabelledInput, Text } from '../components'; -import useFlowHandler from '../hooks/useFlowHandler'; -import useUserData from '../hooks/useUserData'; -import { emailRegex } from '../utils/validations'; +import { Button, LabelledInput, Text } from '../../components'; +import useFlowHandler from '../../hooks/useFlowHandler'; +import useUserData from '../../hooks/useUserData'; +import { emailRegex } from '../../utils/validations'; export const InitiateLogin = () => { - const { t } = useTranslation(); + const { t } = useTranslation('translation', { keyPrefix: 'login.start' }); const { setEmail } = useUserData(); const { navigateNext, changeFlow } = useFlowHandler(); const { initAutocompletedLoginWithPasskey, loginWithPasskey } = useCorbado(); @@ -73,7 +73,7 @@ export const InitiateLogin = () => { e.preventDefault(); if (!emailRegex.test(formEmail)) { - setErrorMessage(t('validation_errors.email')); + setErrorMessage(t('validationError_email')); return; } @@ -83,22 +83,22 @@ export const InitiateLogin = () => { return ( <> - Welcome back! + {t('header')} - Don't have an account yet?{' '} + {t('subheader')}{' '} changeFlow(FlowType.SignUp)} > - Create account + {t('button_signup')} {' '}
{ variant='primary' disabled={!formEmail} > - {t('signup.continue_email')} + {t('button_submit')}
diff --git a/packages/react/src/screens/login/PasskeyError.tsx b/packages/react/src/screens/login/PasskeyError.tsx new file mode 100644 index 00000000..f3cf359f --- /dev/null +++ b/packages/react/src/screens/login/PasskeyError.tsx @@ -0,0 +1,91 @@ +import { FlowHandlerEvents, useCorbado } from '@corbado/react-sdk'; +import React, { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import type { ButtonType } from '../../components/PasskeyScreensWrapper'; +import { PasskeyScreensWrapper } from '../../components/PasskeyScreensWrapper'; +import useFlowHandler from '../../hooks/useFlowHandler'; +import useUserData from '../../hooks/useUserData'; + +export const PasskeyError = () => { + const { t } = useTranslation('translation', { keyPrefix: 'login.passkeyError' }); + const { loginWithPasskey, shortSession } = useCorbado(); + const { navigateBack, navigateNext } = useFlowHandler(); + const { email } = useUserData(); + + const header = useMemo(() => t('header'), [t]); + + const body = useMemo(() => t('body'), [t]); + + const primaryButton = useMemo(() => t('button_retry'), [t]); + const secondaryButton = useMemo(() => { + if (shortSession) { + return ''; + } + + return t('button_emailOtp'); + }, [t]); + const tertiaryButton = useMemo(() => { + if (shortSession) { + return t('button_cancel'); + } + + return t('button_back'); + }, [t, shortSession]); + + const handleCreatePasskey = useCallback(async () => { + if (!email) { + navigateBack(); + return; + } + + await loginWithPasskey(email); + + void navigateNext(FlowHandlerEvents.PasskeySuccess); + }, [email, loginWithPasskey, navigateBack, navigateNext]); + + const handleSendOtp = useCallback(() => { + void navigateNext(FlowHandlerEvents.EmailOtp); + }, [navigateNext]); + + const handleBack = useCallback(() => { + if (shortSession) { + void navigateNext(FlowHandlerEvents.CancelPasskey); + return; + } + + navigateBack(); + }, [navigateBack, navigateNext, shortSession]); + + const handleClick = useCallback( + (btn: ButtonType) => { + switch (btn) { + case 'primary': + return handleCreatePasskey(); + case 'secondary': + return handleSendOtp(); + case 'tertiary': + return handleBack(); + } + }, + [handleBack, handleCreatePasskey, handleSendOtp], + ); + + const props = useMemo( + () => ({ + header, + body, + primaryButton, + secondaryButton, + tertiaryButton, + onClick: handleClick, + }), + [body, handleClick, header, primaryButton, secondaryButton, tertiaryButton], + ); + + return ( + <> + + + ); +}; diff --git a/packages/react/src/screens/PasskeyAppend.tsx b/packages/react/src/screens/shared/PasskeyAppend.tsx similarity index 71% rename from packages/react/src/screens/PasskeyAppend.tsx rename to packages/react/src/screens/shared/PasskeyAppend.tsx index c82653a3..01d4aed7 100644 --- a/packages/react/src/screens/PasskeyAppend.tsx +++ b/packages/react/src/screens/shared/PasskeyAppend.tsx @@ -3,35 +3,32 @@ import { FlowHandlerEvents } from '@corbado/web-core'; import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { ButtonType } from '../components/PasskeyScreensWrapper'; -import { PasskeyScreensWrapper } from '../components/PasskeyScreensWrapper'; -import useFlowHandler from '../hooks/useFlowHandler'; +import type { ButtonType } from '../../components/PasskeyScreensWrapper'; +import { PasskeyScreensWrapper } from '../../components/PasskeyScreensWrapper'; +import useFlowHandler from '../../hooks/useFlowHandler'; export const PasskeyAppend = () => { - const { t } = useTranslation(); + const { t } = useTranslation('translation', { keyPrefix: 'common.passkeyPrompt' }); const { navigateNext } = useFlowHandler(); const { appendPasskey } = useCorbado(); const header = useMemo( () => ( - // - // text x - // - Log in even faster with{' '} + {t('header')}{' '} void navigateNext(FlowHandlerEvents.ShowBenefits)} > - Passkeys + {t('button_showPasskeyBenefits')} ), [t], ); - const primaryButton = useMemo(() => t('activate_passkey.primary_btn'), [t]); - const secondaryButton = useMemo(() => t('activate_passkey.secondary_btn'), [t]); + const primaryButton = useMemo(() => t('button_start'), [t]); + const secondaryButton = useMemo(() => t('button_skip'), [t]); const handlePasskeyActivation = useCallback(async () => { try { diff --git a/packages/react/src/screens/EmailOTP.tsx b/packages/react/src/screens/signup/EmailOTP.tsx similarity index 68% rename from packages/react/src/screens/EmailOTP.tsx rename to packages/react/src/screens/signup/EmailOTP.tsx index 669734d0..0c40a679 100644 --- a/packages/react/src/screens/EmailOTP.tsx +++ b/packages/react/src/screens/signup/EmailOTP.tsx @@ -1,15 +1,15 @@ -import { FlowHandlerEvents, LoginFlowNames, useCorbado } from '@corbado/react-sdk'; +import { useCorbado } from '@corbado/react-sdk'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Gmail, Link, OTPInput, Outlook, Text, Yahoo } from '../components'; -import useFlowHandler from '../hooks/useFlowHandler'; -import useUserData from '../hooks/useUserData'; +import { Button, Gmail, Link, OTPInput, Outlook, Text, Yahoo } from '../../components'; +import useFlowHandler from '../../hooks/useFlowHandler'; +import useUserData from '../../hooks/useUserData'; export const EmailOTP = () => { - const { t } = useTranslation(); + const { t } = useTranslation('translation', { keyPrefix: 'signup.emailOtp' }); const { navigateBack, navigateNext, currentFlow } = useFlowHandler(); - const { completeSignUpWithEmailOTP, getUserAuthMethods } = useCorbado(); + const { completeSignUpWithEmailOTP } = useCorbado(); const { email, sendEmail } = useUserData(); const [otp, setOTP] = React.useState([]); @@ -28,14 +28,6 @@ export const EmailOTP = () => { setLoading(true); try { await completeSignUpWithEmailOTP(payload); - - if (currentFlow === LoginFlowNames.PasskeyLoginWithEmailOTPFallback) { - const authMethods = await getUserAuthMethods(email ?? ''); - const userHasPasskey = authMethods.selectedMethods.includes('webauthn'); - void navigateNext(FlowHandlerEvents.PasskeySuccess, { userHasPasskey }); - return; - } - void navigateNext(); } catch (error) { console.log({ error }); @@ -47,7 +39,7 @@ export const EmailOTP = () => { setError(''); const mergedChars = otp.join(''); if (mergedChars.length < 6) { - setError(t('email_link.otp_required')); + setError(t('validationError_otp')); return; } @@ -56,15 +48,10 @@ export const EmailOTP = () => { return ( <> - {t('email_link.header')} + {t('header')} - {/* "text" is a placeholder value for translations */} - {/* - text {email} text - */} - We just sent a one time code to {email}. The code expires shortly, - so please enter it soon. + {t('body_text1')} {email}. {t('body_text2')}
@@ -98,7 +85,7 @@ export const EmailOTP = () => { isLoading={loading} disabled={loading} > - {t('generic.continue')} + {t('button_verify')} ); diff --git a/packages/react/src/screens/InitiateSignup.tsx b/packages/react/src/screens/signup/InitiateSignup.tsx similarity index 73% rename from packages/react/src/screens/InitiateSignup.tsx rename to packages/react/src/screens/signup/InitiateSignup.tsx index da37736c..ed9cd099 100644 --- a/packages/react/src/screens/InitiateSignup.tsx +++ b/packages/react/src/screens/signup/InitiateSignup.tsx @@ -1,11 +1,11 @@ import { FlowType } from '@corbado/web-core'; import React, { useEffect } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; -import { Button, LabelledInput, Link, Text } from '../components'; -import useFlowHandler from '../hooks/useFlowHandler'; -import useUserData from '../hooks/useUserData'; -import { emailRegex } from '../utils/validations'; +import { Button, LabelledInput, Text } from '../../components'; +import useFlowHandler from '../../hooks/useFlowHandler'; +import useUserData from '../../hooks/useUserData'; +import { emailRegex } from '../../utils/validations'; interface SignupForm { name: string; @@ -23,7 +23,7 @@ const createFormTemplate = (email?: string, username?: string) => ({ }); export const InitiateSignup = () => { - const { t } = useTranslation(); + const { t } = useTranslation('translation', { keyPrefix: 'signup.start' }); const { navigateNext, changeFlow } = useFlowHandler(); const { setEmail, email, setUserName, userName } = useUserData(); @@ -62,10 +62,10 @@ export const InitiateSignup = () => { const errors: SignupForm = { ...defaultFormTemplate }; if (!signupData.name) { - errors.name = t('validation_errors.name'); + errors.name = t('validationError_name'); } if (!signupData.username || !emailRegex.test(signupData.username)) { - errors.username = t('validation_errors.email'); + errors.username = t('validationError_email'); } setErrorData(errors); @@ -81,25 +81,14 @@ export const InitiateSignup = () => { return ( <> - {t('signup.header')} + {t('header')} - {/* "text" is a placeholder value for translations */} - - text{' '} - changeFlow(FlowType.Login)} - > - text - {' '} - text - + {t('subheader')}{' '} changeFlow(FlowType.Login)} > - Log in + {t('button_login')} {' '}
@@ -107,14 +96,14 @@ export const InitiateSignup = () => {
{ variant='primary' isLoading={loading} > - {t('signup.continue_email')} + {t('button_submit')}
diff --git a/packages/react/src/screens/PasskeyBenefits.tsx b/packages/react/src/screens/signup/PasskeyBenefits.tsx similarity index 71% rename from packages/react/src/screens/PasskeyBenefits.tsx rename to packages/react/src/screens/signup/PasskeyBenefits.tsx index a88ff193..fb35514c 100644 --- a/packages/react/src/screens/PasskeyBenefits.tsx +++ b/packages/react/src/screens/signup/PasskeyBenefits.tsx @@ -2,30 +2,29 @@ import { FlowHandlerEvents, useCorbado } from '@corbado/react-sdk'; import React, { useCallback, useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import type { ButtonType } from '../components/PasskeyScreensWrapper'; -import { PasskeyScreensWrapper } from '../components/PasskeyScreensWrapper'; -import useFlowHandler from '../hooks/useFlowHandler'; -import useUserData from '../hooks/useUserData'; +import type { ButtonType } from '../../components/PasskeyScreensWrapper'; +import { PasskeyScreensWrapper } from '../../components/PasskeyScreensWrapper'; +import useFlowHandler from '../../hooks/useFlowHandler'; +import useUserData from '../../hooks/useUserData'; export const PasskeyBenefits = () => { - const { t } = useTranslation(); + const { t } = useTranslation('translation', { keyPrefix: 'signup.passkeyBenefits' }); const { email, userName } = useUserData(); const { signUpWithPasskey, shortSession, appendPasskey } = useCorbado(); const { navigateNext } = useFlowHandler(); - const header = useMemo(() => t('create_passkey.header'), [t]); + const header = useMemo(() => t('header'), [t]); const body = useMemo( () => ( - With passkeys, you don’t need to remember complex passwords anymore. Log in securely to using{' '} - Face ID, Touch ID or screen lock code. + {t('body_introduction')} {t('body_loginMethods')}. ), [t], ); - const primaryButton = useMemo(() => t('create_passkey.primary_btn'), [t]); - const secondaryButton = useMemo(() => t('create_passkey.secondary_btn'), [t]); + const primaryButton = useMemo(() => t('button_start'), [t]); + const secondaryButton = useMemo(() => t('button_skip'), [t]); const handleCreatePasskey = useCallback(async () => { try { diff --git a/packages/react/src/screens/PasskeyError.tsx b/packages/react/src/screens/signup/PasskeyError.tsx similarity index 55% rename from packages/react/src/screens/PasskeyError.tsx rename to packages/react/src/screens/signup/PasskeyError.tsx index 1d795c2b..42403a5d 100644 --- a/packages/react/src/screens/PasskeyError.tsx +++ b/packages/react/src/screens/signup/PasskeyError.tsx @@ -1,50 +1,51 @@ -import { FlowHandlerEvents, SignUpFlowNames, useCorbado } from '@corbado/react-sdk'; +import { FlowHandlerEvents, useCorbado } from '@corbado/react-sdk'; import React, { useCallback, useMemo } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; -import type { ButtonType } from '../components/PasskeyScreensWrapper'; -import { PasskeyScreensWrapper } from '../components/PasskeyScreensWrapper'; -import useFlowHandler from '../hooks/useFlowHandler'; -import useUserData from '../hooks/useUserData'; +import type { ButtonType } from '../../components/PasskeyScreensWrapper'; +import { PasskeyScreensWrapper } from '../../components/PasskeyScreensWrapper'; +import useFlowHandler from '../../hooks/useFlowHandler'; +import useUserData from '../../hooks/useUserData'; export const PasskeyError = () => { - const { t } = useTranslation(); - const { signUpWithPasskey, loginWithPasskey, shortSession } = useCorbado(); - const { currentFlow, navigateBack, navigateNext } = useFlowHandler(); + const { t } = useTranslation('translation', { keyPrefix: 'signup.passkeyError' }); + const { signUpWithPasskey, shortSession } = useCorbado(); + const { navigateBack, navigateNext } = useFlowHandler(); const { email, userName } = useUserData(); - const header = useMemo(() => t('create_passkey_error.header'), [t]); + const header = useMemo(() => t('header'), [t]); const body = useMemo( () => ( - - Creating your account with{' '} + + {t('body_errorMessage1')}{' '} void navigateNext(FlowHandlerEvents.ShowBenefits)} > - passkeys - {' '} - not possible. Try again or log in with email one time code. - + {t('button_showPasskeyBenefits')} + + {'. '} + {t('body_errorMessage2')} + ), [t], ); - const primaryButton = useMemo(() => t('create_passkey_error.primary_btn'), [t]); + const primaryButton = useMemo(() => t('button_retry'), [t]); const secondaryButton = useMemo(() => { if (shortSession) { return ''; } - return t('create_passkey_error.secondary_btn'); + return t('button_emailOtp'); }, [t]); const tertiaryButton = useMemo(() => { if (shortSession) { - return t('generic.cancel'); + return t('button_cancel'); } - return t('create_passkey_error.tertiary_btn'); + return t('button_back'); }, [t, shortSession]); const handleCreatePasskey = useCallback(async () => { @@ -53,14 +54,10 @@ export const PasskeyError = () => { return; } - if (currentFlow === SignUpFlowNames.PasskeySignupWithEmailOTPFallback) { - await signUpWithPasskey(email, userName ?? ''); - } else { - await loginWithPasskey(email); - } + await signUpWithPasskey(email, userName ?? ''); void navigateNext(FlowHandlerEvents.PasskeySuccess); - }, [currentFlow, email, loginWithPasskey, navigateBack, navigateNext, signUpWithPasskey, userName]); + }, [email, navigateBack, navigateNext, signUpWithPasskey, userName]); const handleSendOtp = useCallback(() => { void navigateNext(FlowHandlerEvents.EmailOtp); diff --git a/packages/react/src/screens/PasskeySignup.tsx b/packages/react/src/screens/signup/PasskeySignup.tsx similarity index 67% rename from packages/react/src/screens/PasskeySignup.tsx rename to packages/react/src/screens/signup/PasskeySignup.tsx index 19fe3832..ffd78388 100644 --- a/packages/react/src/screens/PasskeySignup.tsx +++ b/packages/react/src/screens/signup/PasskeySignup.tsx @@ -2,29 +2,26 @@ import { FlowHandlerEvents, useCorbado } from '@corbado/react-sdk'; import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { ButtonType } from '../components'; -import { PasskeyScreensWrapper } from '../components/PasskeyScreensWrapper'; -import useFlowHandler from '../hooks/useFlowHandler'; -import useUserData from '../hooks/useUserData'; +import type { ButtonType } from '../../components'; +import { PasskeyScreensWrapper } from '../../components/PasskeyScreensWrapper'; +import useFlowHandler from '../../hooks/useFlowHandler'; +import useUserData from '../../hooks/useUserData'; export const PasskeySignup = () => { - const { t } = useTranslation(); + const { t } = useTranslation('translation', { keyPrefix: 'signup.passkey' }); const { navigateNext, navigateBack } = useFlowHandler(); const { signUpWithPasskey } = useCorbado(); const { email, userName } = useUserData(); const header = useMemo( () => ( - // - // text x text - // - Let’s get you set up with{' '} + {t('header')}{' '} void navigateNext(FlowHandlerEvents.ShowBenefits)} > - Passkeys + {t('button_showPasskeyBenefits')} ), @@ -33,19 +30,16 @@ export const PasskeySignup = () => { const subHeader = useMemo( () => ( - // - // text {email} text - // - We’ll create an account for {email} + {t('body')} {email}. ), [t], ); - const primaryButton = useMemo(() => t('passkey_signup.primary_btn'), [t]); - const secondaryButton = useMemo(() => t('passkey_signup.secondary_btn'), [t]); - const tertiaryButton = useMemo(() => t('passkey_signup.tertiary_btn'), [t]); + const primaryButton = useMemo(() => t('button_start'), [t]); + const secondaryButton = useMemo(() => t('button_emailOtp'), [t]); + const tertiaryButton = useMemo(() => t('button_back'), [t]); const handleCreateAccount = useCallback(async () => { if (!email || !userName) { diff --git a/packages/react/src/screens/signup/PasskeyWelcome.tsx b/packages/react/src/screens/signup/PasskeyWelcome.tsx new file mode 100644 index 00000000..7ea53b2f --- /dev/null +++ b/packages/react/src/screens/signup/PasskeyWelcome.tsx @@ -0,0 +1,44 @@ +import React, { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { PasskeyScreensWrapper } from '../../components/PasskeyScreensWrapper'; +import useFlowHandler from '../../hooks/useFlowHandler'; + +export const PasskeyWelcome = () => { + const { t } = useTranslation('translation', { keyPrefix: 'signup.passkeySuccess' }); + const { navigateNext } = useFlowHandler(); + + const header = useMemo(() => t('header'), [t]); + const secondaryHeader = useMemo(() => t('subheader'), [t]); + const body = useMemo( + () => ( + + {t('body_text1')} {t('body_text2')} {t('body_text3')} + + ), + [t], + ); + + const primaryButton = useMemo(() => t('button'), [t]); + + const handleClick = useCallback(() => { + void navigateNext(); + }, [navigateNext]); + + const props = useMemo( + () => ({ + header, + secondaryHeader, + body, + primaryButton, + onClick: handleClick, + }), + [header, secondaryHeader, body, primaryButton, handleClick], + ); + + return ( + <> + + + ); +}; diff --git a/playground/react-example/src/pages/AuthPage.tsx b/playground/react-example/src/pages/AuthPage.tsx index 1bf0de70..6505bfe0 100644 --- a/playground/react-example/src/pages/AuthPage.tsx +++ b/playground/react-example/src/pages/AuthPage.tsx @@ -1,5 +1,7 @@ import { useNavigate } from 'react-router-dom'; import CorbadoAuthUI from '@corbado/react'; +import frenchTranslations from '../translations/fr'; +import englishTranslations from '../translations/en'; const AuthPage = () => { const navigate = useNavigate(); @@ -8,7 +10,17 @@ const AuthPage = () => { navigate('/'); }; - return ; + return ( + + ); }; export default AuthPage; diff --git a/playground/react-example/src/translations/en.ts b/playground/react-example/src/translations/en.ts new file mode 100644 index 00000000..2f5fc89f --- /dev/null +++ b/playground/react-example/src/translations/en.ts @@ -0,0 +1,9 @@ +const en = { + signup: { + start: { + header: 'This is new english header', + }, + }, +}; + +export default en; diff --git a/playground/react-example/src/translations/fr.ts b/playground/react-example/src/translations/fr.ts new file mode 100644 index 00000000..6aa2930a --- /dev/null +++ b/playground/react-example/src/translations/fr.ts @@ -0,0 +1,92 @@ +const fr = { + common: { + passkeyPrompt: { + header: 'Connectez-vous encore plus rapidement avec', + button_showPasskeyBenefits: 'Passkeys', + button_start: 'Activer', + button_skip: 'Peut-être plus tard', + }, + }, + signup: { + start: { + header: 'Créez votre compte', + subheader: 'Vous avez déjà un compte ?', + button_login: 'Se connecter', + button_submit: 'Continuer avec l’email', + textField_name: 'Nom', + textField_email: 'Adresse e-mail', + }, + passkey: { + header: 'Préparons votre compte avec', + body: 'Nous allons créer un compte pour', + button_showPasskeyBenefits: 'Passkeys', + button_start: 'Créer votre compte', + button_emailOtp: 'Envoyer un code par email', + button_back: 'Retour', + }, + passkeyBenefits: { + header: 'Passkeys', + body_introduction: + 'Avec les passkeys, plus besoin de se souvenir de mots de passe complexes. Connectez-vous en toute sécurité en utilisant', + body_loginMethods: 'Face ID, Touch ID ou un code de verrouillage d’écran.', + button_start: 'Créer un passkey', + button_skip: 'Peut-être plus tard', + }, + passkeySuccess: { + header: 'Bienvenue !', + subheader: 'Passkey créé', + body_text1: 'Vous pouvez maintenant confirmer votre identité en utilisant votre', + body_text2: 'passkey ou par code email à usage unique', + body_text3: 'lorsque vous vous connectez.', + button: 'Continuer', + }, + passkeyError: { + header: 'Configurons votre compte', + body_errorMessage1: 'Il n’a pas été possible de créer votre compte avec', + body_errorMessage2: 'Réessayez ou inscrivez-vous avec un code email à usage unique.', + button_showPasskeyBenefits: 'passkeys', + button_retry: 'Réessayer', + button_emailOtp: 'Envoyer un code par email', + button_back: 'Retour', + button_cancel: 'Annuler', + }, + emailOtp: { + header: 'Entrez le code pour créer le compte', + body_text1: 'Nous venons de vous envoyer un code à usage unique à ', + body_text2: 'Le code expire bientôt, veuillez le saisir rapidement.', + validationError_otp: 'Le code OTP est incorrect', + button_verify: 'Continuer', + button_sendOtpAgain: 'Envoyer le code à nouveau', + button_back: 'Annuler', + }, + }, + login: { + start: { + header: 'Bon retour !', + subheader: 'Vous n’avez pas encore de compte ?', + button_signup: 'Créer un compte', + button_submit: 'Continuer avec l’email', + textField_email: 'Adresse e-mail', + validationError_email: 'Veuillez saisir un email valide', + }, + passkeyError: { + header: 'Se connecter avec Passkeys', + body: 'La connexion avec passkeys n’est pas possible. Réessayez ou connectez-vous avec un code email à usage unique.', + button_retry: 'Réessayer', + button_emailOtp: 'Envoyer un code par email', + button_back: 'Retour', + button_cancel: 'Annuler', + }, + emailOtp: { + header: 'Entrez le code pour vous connecter', + body_text1: 'Nous venons de vous envoyer un code à usage unique à ', + body_text2: 'Le code expire bientôt, veuillez le saisir rapidement.', + validationError_otp: 'Le code OTP est incorrect', + button_verify: 'Continuer', + button_sendOtpAgain: 'Envoyer le code à nouveau', + button_back: 'Annuler', + }, + }, +}; + +export default fr;