From d8dc02e6c661e29ff632061a0ce392621c43b771 Mon Sep 17 00:00:00 2001 From: Quentin Aupetit Date: Tue, 19 Nov 2024 13:48:52 -0400 Subject: [PATCH 1/2] replace custom multi-view component with an implementation using react-router --- package-lock.json | 39 +++++++++++++++++++ package.json | 1 + src/components/miscComponent.tsx | 6 +-- src/components/widget/widget.tsx | 34 ++++++++++++++++ .../accountRecovery/accountRecoveryWidget.tsx | 24 ++++++------ src/widgets/auth/authWidget.tsx | 6 +-- .../views/accountRecoveryViewComponent.tsx | 6 +-- .../views/forgotPasswordViewComponent.tsx | 6 +-- src/widgets/auth/views/loginViewComponent.tsx | 14 +++---- .../views/loginWithPasswordViewComponent.tsx | 16 ++++---- .../views/loginWithWebAuthnViewComponent.tsx | 13 +++---- .../auth/views/signupViewComponent.tsx | 8 ++-- src/widgets/emailEditor/emailEditorWidget.tsx | 12 +++--- src/widgets/mfa/MfaCredentialsWidget.tsx | 37 +++++++++--------- .../passwordReset/passwordResetWidget.tsx | 14 +++---- .../passwordless/passwordlessWidget.tsx | 18 ++++----- .../phoneNumberEditorWidget.tsx | 16 ++++---- .../socialAccounts/socialAccountsWidget.tsx | 22 +++++------ src/widgets/stepUp/mfaStepUpWidget.tsx | 20 +++++----- types/identity-ui.d.ts | 4 +- 20 files changed, 194 insertions(+), 122 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9aaf68d..730d5cce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "react": "^16.9.24", "react-dom": "^16.9.24", "react-phone-number-input": "^3.4.3", + "react-router-dom": "^6.28.0", "react-transition-group": "4.4.2", "remarkable": "2.0.1", "validator": "^13.11.0" @@ -3661,6 +3662,14 @@ "integrity": "sha512-Oq32s1NHGFhHKPyXfLPVhJ57zcWk6NFgLnHmOIwt8TRQ6sQoYFm5sALNxgzpLXQk6HeQHCciXoPzVLGF7/D7yA==", "license": "MIT" }, + "node_modules/@remix-run/router": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", + "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "25.0.8", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", @@ -12619,6 +12628,36 @@ "react-dom": ">=16.8" } }, + "node_modules/react-router": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", + "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==", + "dependencies": { + "@remix-run/router": "1.21.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz", + "integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==", + "dependencies": { + "@remix-run/router": "1.21.0", + "react-router": "6.28.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-test-renderer": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", diff --git a/package.json b/package.json index 9adae5f1..dd0d6812 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "react": "^16.9.24", "react-dom": "^16.9.24", "react-phone-number-input": "^3.4.3", + "react-router-dom": "^6.28.0", "react-transition-group": "4.4.2", "remarkable": "2.0.1", "validator": "^13.11.0" diff --git a/src/components/miscComponent.tsx b/src/components/miscComponent.tsx index dbb2acd1..7ac73cfc 100644 --- a/src/components/miscComponent.tsx +++ b/src/components/miscComponent.tsx @@ -2,7 +2,7 @@ import React, { AnchorHTMLAttributes, ComponentType, HTMLAttributes, MouseEvent import { Remarkable } from 'remarkable'; import styled from 'styled-components'; -import { useRouting } from '../contexts/routing' +import { useNavigate } from 'react-router-dom'; export const Heading = styled.div` margin-bottom: ${props => props.theme.spacing * 1.5}px; @@ -74,12 +74,12 @@ export const Alternative = styled.div` `; export const Link = styled(({ target, href = '#', children, className, controller }: {controller?: AbortController} & AnchorHTMLAttributes) => { - const { goTo } = useRouting() + const navigate = useNavigate() const onClick = target ? ((e: MouseEvent) => { controller?.abort(`Going to ${target}`) e.preventDefault(); - goTo(target); + navigate(target); }) : (() => { }); return ( diff --git a/src/components/widget/widget.tsx b/src/components/widget/widget.tsx index 3d639056..29693f0c 100644 --- a/src/components/widget/widget.tsx +++ b/src/components/widget/widget.tsx @@ -1,6 +1,7 @@ import React, { ComponentType } from 'react'; import { ThemeProvider } from 'styled-components' import type { SessionInfo, Client as CoreClient } from '@reachfive/identity-core' +import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; import type { Config, Prettify } from '../../types' import type { I18nMessages } from '../../core/i18n'; @@ -113,3 +114,36 @@ function multiViewWidget({ initialView, views, initialState = {} as MultiW } } } + +export interface RouterWidgetProps { + fallbackElement?: React.ReactNode + initialView: ((props: Omit) => string) | string + prepare?: PrepareFn + routes: Record>> +} + +export function createRouterWidget({ fallbackElement, initialView, prepare, routes }: RouterWidgetProps) { + console.log(window.location) + return createWidget({ + component: (props: Omit) => { + const initialRoute = typeof initialView === 'function' ? initialView(props) : initialView + const router = createBrowserRouter([ + ...Object.entries(routes).map(([path, RouteComponent]) => ({ + path, + element: , + index: true, + })), + { path: '*', element: } + ]) + console.log(router) + return ( + + ) + }, + prepare: prepare, + noIntro: true + }); +} diff --git a/src/widgets/accountRecovery/accountRecoveryWidget.tsx b/src/widgets/accountRecovery/accountRecoveryWidget.tsx index b392e5a2..692badb3 100644 --- a/src/widgets/accountRecovery/accountRecoveryWidget.tsx +++ b/src/widgets/accountRecovery/accountRecoveryWidget.tsx @@ -1,16 +1,16 @@ import React from 'react'; +import styled from 'styled-components' +import { useNavigate } from 'react-router-dom'; import { parseQueryString } from '../../helpers/queryString' import { Alternative, Heading, Info, Link, Intro, Separator } from '../../components/miscComponent'; -import { createMultiViewWidget } from '../../components/widget/widget'; +import { createRouterWidget } from '../../components/widget/widget'; import { useReachfive } from '../../contexts/reachfive'; -import { useRouting } from '../../contexts/routing'; import { useI18n } from '../../contexts/i18n'; import { createForm } from '../../components/form/formComponent' import { PasswordEditorForm, PasswordEditorFormData } from '../passwordEditor/passwordEditorWidget.tsx' import { ReactComponent as Passkeys } from '../../icons/passkeys.svg' -import styled from 'styled-components' interface MainViewProps { @@ -79,7 +79,7 @@ const NewPasskey = ({ }: PropsWithAuthentication) => { const coreClient = useReachfive() const i18n = useI18n() - const {goTo} = useRouting() + const navigate = useNavigate() const handleSubmit = () => { return coreClient.resetPasskeys({ @@ -91,7 +91,7 @@ const NewPasskey = ({ const handleSuccess = () => { onSuccess(); - goTo('passkey-success'); + navigate('/passkey-success'); }; return ( @@ -108,7 +108,7 @@ const NewPasskey = ({ {allowCreatePassword && - Create a new password + Create a new password } @@ -157,7 +157,7 @@ export const NewPasswordView = ({ }: PropsWithAuthentication) => { const coreClient = useReachfive() const i18n = useI18n() - const { goTo } = useRouting() + const navigate = useNavigate() const handleSubmit = ({ password }: PasswordEditorFormData) => { return coreClient.updatePassword({ @@ -168,7 +168,7 @@ export const NewPasswordView = ({ const handleSuccess = () => { onSuccess(); - goTo('password-success'); + navigate('/password-success'); }; return ( @@ -181,7 +181,7 @@ export const NewPasswordView = ({ onSuccess={handleSuccess} onError={onError} /> - Back + Back ) @@ -189,7 +189,7 @@ export const NewPasswordView = ({ const resolveCode = () => { const qs = (window.location.search && window.location.search.length) - ? window.location.search.substr(1) + ? window.location.search.substring(1) : ''; const {verificationCode, email, clientId} = parseQueryString(qs) return {authentication: { verificationCode, email, clientId } as Authentication}; @@ -201,9 +201,9 @@ type PropsWithAuthentication

= P & { authentication: Authentication } export interface AccountRecoveryWidgetProps extends MainViewProps, SuccessViewProps { } -export default createMultiViewWidget>({ +export default createRouterWidget>({ initialView: 'new-passkey', - views: { + routes: { 'new-passkey': NewPasskey, 'new-password': NewPasswordView, 'passkey-success': PasskeySuccessView, diff --git a/src/widgets/auth/authWidget.tsx b/src/widgets/auth/authWidget.tsx index 6afe5a00..34aae445 100644 --- a/src/widgets/auth/authWidget.tsx +++ b/src/widgets/auth/authWidget.tsx @@ -1,7 +1,7 @@ import { type SessionInfo } from '@reachfive/identity-core' import { UserError } from '../../helpers/errors'; -import { createMultiViewWidget } from '../../components/widget/widget'; +import { createRouterWidget } from '../../components/widget/widget'; import LoginView, { type LoginViewProps } from './views/loginViewComponent' import LoginWithWebAuthnView, { type LoginWithWebAuthnViewProps } from './views/loginWithWebAuthnViewComponent' @@ -57,7 +57,7 @@ export function selectLogin(initialScreen?: InitialScreen, allowWebAuthnLogin?: return !allowWebAuthnLogin ? 'login' : 'login-with-web-authn' } -export default createMultiViewWidget>({ +export default createRouterWidget>({ initialView({ initialScreen, allowLogin = true, @@ -80,7 +80,7 @@ export default createMultiViewWidget({ @@ -80,7 +80,7 @@ export const AccountRecoveryView = ({ returnToAfterAccountRecovery, }: AccountRecoveryViewProps) => { const coreClient = useReachfive() - const { goTo } = useRouting() + const navigate = useNavigate() const i18n = useI18n() const callback = useCallback((data: RequestAccountRecoveryParams) => @@ -104,7 +104,7 @@ export const AccountRecoveryView = ({ goTo('account-recovery-success')} + onSuccess={() => navigate('/account-recovery-success')} skipError={displaySafeErrorMessage && skipError} /> {allowLogin && {i18n('accountRecovery.backToLoginLink')} diff --git a/src/widgets/auth/views/forgotPasswordViewComponent.tsx b/src/widgets/auth/views/forgotPasswordViewComponent.tsx index 46a2b396..d33ee587 100644 --- a/src/widgets/auth/views/forgotPasswordViewComponent.tsx +++ b/src/widgets/auth/views/forgotPasswordViewComponent.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useLayoutEffect } from 'react'; import { RequestPasswordResetParams } from '@reachfive/identity-core/es/main/profileClient'; +import { useNavigate } from 'react-router-dom'; import { AppError } from '../../../helpers/errors' @@ -11,7 +12,6 @@ import { simpleField } from '../../../components/form/fields/simpleField'; import ReCaptcha, {importGoogleRecaptchaScript} from '../../../components/reCaptcha'; import { useI18n } from '../../../contexts/i18n' -import { useRouting } from '../../../contexts/routing'; import { useReachfive } from '../../../contexts/reachfive'; import { selectLogin } from '../authWidget.tsx'; import { InitialScreen } from '../../../../constants.ts'; @@ -92,7 +92,7 @@ export const ForgotPasswordView = ({ returnToAfterPasswordReset, }: ForgotPasswordViewProps) => { const coreClient = useReachfive() - const { goTo } = useRouting() + const navigate = useNavigate() const i18n = useI18n() const callback = useCallback((data: RequestPasswordResetParams) => @@ -116,7 +116,7 @@ export const ForgotPasswordView = ({ goTo('forgot-password-success')} + onSuccess={() => navigate('/forgot-password-success')} skipError={displaySafeErrorMessage && skipError} /> {allowLogin && {i18n('forgotPassword.backToLoginLink')} diff --git a/src/widgets/auth/views/loginViewComponent.tsx b/src/widgets/auth/views/loginViewComponent.tsx index db59e38f..cb8724cb 100644 --- a/src/widgets/auth/views/loginViewComponent.tsx +++ b/src/widgets/auth/views/loginViewComponent.tsx @@ -1,6 +1,7 @@ import React, { useLayoutEffect } from 'react'; import { AuthOptions, LoginWithPasswordParams } from '@reachfive/identity-core' import styled from 'styled-components'; +import { useNavigate } from 'react-router-dom'; import { Heading, Link, Alternative, Separator } from '../../../components/miscComponent'; @@ -12,11 +13,8 @@ import identifierField from '../../../components/form/fields/identifierField'; import ReCaptcha, { importGoogleRecaptchaScript, type WithCaptchaToken } from '../../../components/reCaptcha' import { simpleField } from '../../../components/form/fields/simpleField'; -import { FaSelectionViewState } from '../../stepUp/mfaStepUpWidget' - import { useI18n } from '../../../contexts/i18n'; import { useReachfive } from '../../../contexts/reachfive'; -import { useRouting } from '../../../contexts/routing'; import { useSession } from '../../../contexts/session'; import { specializeIdentifierData } from '../../../helpers/utils'; @@ -87,14 +85,14 @@ export const LoginForm = createForm({ showForgotPassword && !showAccountRecovery && { staticContent: ( - {i18n('login.forgotPasswordLink')} + {i18n('login.forgotPasswordLink')} ) }, showAccountRecovery && { staticContent: ( - {i18n('accountRecovery.title')} + {i18n('accountRecovery.title')} ) }, @@ -208,7 +206,7 @@ export const LoginView = ({ }: LoginViewProps) => { const i18n = useI18n() const coreClient = useReachfive() - const { goTo } = useRouting() + const navigate = useNavigate() const session = useSession() useLayoutEffect(() => { @@ -238,7 +236,7 @@ export const LoginView = ({ ...auth, }, }) - .then(res => res?.stepUpToken ? goTo('fa-selection', {token: res.stepUpToken, amr: res.amr ?? []}) : res) + .then(res => res?.stepUpToken ? navigate('/fa-selection', { state: { token: res.stepUpToken, amr: res.amr ?? [] } }) : res) } const defaultIdentifier = session?.lastLoginType === 'password' ? session.email : undefined; @@ -267,7 +265,7 @@ export const LoginView = ({ {i18n('login.signupLinkPrefix')}   - {i18n('login.signupLink')} + {i18n('login.signupLink')} } diff --git a/src/widgets/auth/views/loginWithPasswordViewComponent.tsx b/src/widgets/auth/views/loginWithPasswordViewComponent.tsx index 347e733c..100f990a 100644 --- a/src/widgets/auth/views/loginWithPasswordViewComponent.tsx +++ b/src/widgets/auth/views/loginWithPasswordViewComponent.tsx @@ -1,6 +1,7 @@ import React, { useLayoutEffect } from 'react'; import { type AuthOptions } from '@reachfive/identity-core' import { LoginWithPasswordParams } from '@reachfive/identity-core/es/main/oAuthClient' +import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -14,10 +15,8 @@ import ReCaptcha, {importGoogleRecaptchaScript} from '../../../components/reCapt import { useI18n } from '../../../contexts/i18n'; import { useReachfive } from '../../../contexts/reachfive'; -import { useRouting } from '../../../contexts/routing'; import { specializeIdentifierData } from '../../../helpers/utils'; -import { FaSelectionViewState } from '../../stepUp/mfaStepUpWidget'; const ResetCredentialWrapper = styled.div<{ floating?: boolean }>` margin-bottom: ${props => props.theme.spacing}px; @@ -71,14 +70,14 @@ export const LoginWithPasswordForm = createForm - {i18n('login.forgotPasswordLink')} + {i18n('login.forgotPasswordLink')} ) }, showAccountRecovery && { staticContent: ( - {i18n('accountRecovery.title')} + {i18n('accountRecovery.title')} ) }, @@ -119,8 +118,9 @@ export const LoginWithPasswordView = ({ }: LoginWithPasswordViewProps) => { const i18n = useI18n() const coreClient = useReachfive() - const { goTo, params } = useRouting() - const { username } = params as LoginWithPasswordViewState + const navigate = useNavigate() + const location = useLocation() + const { username } = location.state as LoginWithPasswordViewState useLayoutEffect(() => { importGoogleRecaptchaScript(recaptcha_site_key) @@ -136,7 +136,7 @@ export const LoginWithPasswordView = ({ ...auth, }, }) - .then(res => res?.stepUpToken ? goTo('fa-selection', {token: res.stepUpToken, amr: res.amr ?? []}) : res) + .then(res => res?.stepUpToken ? navigate('/fa-selection', { state: { token: res.stepUpToken, amr: res.amr ?? [] }}) : res) } return ( @@ -152,7 +152,7 @@ export const LoginWithPasswordView = ({ handler={(data: LoginWithPasswordFormData) => ReCaptcha.handle(data, { recaptcha_enabled, recaptcha_site_key }, callback, "login")} /> - {i18n('login.password.userAnotherIdentifier')} + {i18n('login.password.userAnotherIdentifier')} ); diff --git a/src/widgets/auth/views/loginWithWebAuthnViewComponent.tsx b/src/widgets/auth/views/loginWithWebAuthnViewComponent.tsx index 8277c94f..119e41e9 100644 --- a/src/widgets/auth/views/loginWithWebAuthnViewComponent.tsx +++ b/src/widgets/auth/views/loginWithWebAuthnViewComponent.tsx @@ -1,7 +1,8 @@ import React, { useCallback } from 'react'; import type { AuthOptions, LoginWithWebAuthnParams } from '@reachfive/identity-core' +import styled from 'styled-components' +import { useNavigate } from 'react-router-dom'; -import { LoginWithPasswordViewState } from './loginWithPasswordViewComponent'; import { Alternative, Heading, Link, Separator } from '../../../components/miscComponent'; import { SocialButtons } from '../../../components/form/socialButtonsComponent'; import { @@ -13,11 +14,9 @@ import identifierField from '../../../components/form/fields/identifierField'; import { useI18n } from '../../../contexts/i18n'; import { useReachfive } from '../../../contexts/reachfive'; -import { useRouting } from '../../../contexts/routing'; import { useSession } from '../../../contexts/session'; import { isCustomIdentifier, specializeIdentifierData } from '../../../helpers/utils'; -import styled from 'styled-components' type LoginWithWebAuthnFormData = { identifier: string } | { email: string } @@ -49,7 +48,7 @@ export const LoginWithWebAuthnForm = createForm - {i18n('accountRecovery.title')} + {i18n('accountRecovery.title')} ) } @@ -90,7 +89,7 @@ export interface LoginWithWebAuthnViewProps { export const LoginWithWebAuthnView = ({ acceptTos, allowSignup = true, auth, showLabels = false, socialProviders, allowAccountRecovery }: LoginWithWebAuthnViewProps) => { const coreClient = useReachfive() - const { goTo } = useRouting() + const navigate = useNavigate() const i18n = useI18n() const session = useSession() @@ -131,9 +130,9 @@ export const LoginWithWebAuthnView = ({ acceptTos, allowSignup = true, auth, sho const redirectToPasswordLoginView = useCallback( (data: LoginWithWebAuthnFormData) => { const username = 'identifier' in data ? data.identifier : 'email' in data ? data.email : '' - goTo('login-with-password', { username }) + navigate('/login-with-password', { state: { username }}) }, - [goTo] + [navigate] ) const defaultIdentifier = session?.lastLoginType === 'password' ? session.email : undefined; diff --git a/src/widgets/auth/views/signupViewComponent.tsx b/src/widgets/auth/views/signupViewComponent.tsx index bb7cfa9a..081b8098 100644 --- a/src/widgets/auth/views/signupViewComponent.tsx +++ b/src/widgets/auth/views/signupViewComponent.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useNavigate } from 'react-router-dom'; import { Heading, Link, Alternative, Separator } from '../../../components/miscComponent'; import { SocialButtons } from '../../../components/form/socialButtonsComponent'; @@ -6,7 +7,6 @@ import PasswordSignupForm from '../../../components/form/passwordSignupFormCompo import { WebAuthnSignupViewButtons } from '../../../components/form/webAuthAndPasswordButtonsComponent'; import { type PhoneNumberOptions } from '../../../components/form/fields/phoneNumberField'; import { useI18n } from '../../../contexts/i18n'; -import { useRouting } from '../../../contexts/routing'; import type { SignupWithPasswordViewProps } from './signupWithPasswordViewComponent' import type { SignupWithWebAuthnViewProps } from './signupWithWebAuthnViewComponent' @@ -53,7 +53,7 @@ export const SignupView = ({ ...props }: SignupViewProps) => { const i18n = useI18n() - const { goTo } = useRouting() + const navigate = useNavigate() return (

@@ -65,8 +65,8 @@ export const SignupView = ({ {allowWebAuthnSignup ? goTo('signup-with-password')} - onBiometricClick={() => goTo('signup-with-web-authn')} /> + onPasswordClick={() => navigate('/signup-with-password')} + onBiometricClick={() => navigate('/signup-with-web-authn')} /> : } diff --git a/src/widgets/emailEditor/emailEditorWidget.tsx b/src/widgets/emailEditor/emailEditorWidget.tsx index 32d2e67a..7a88fd16 100644 --- a/src/widgets/emailEditor/emailEditorWidget.tsx +++ b/src/widgets/emailEditor/emailEditorWidget.tsx @@ -1,15 +1,15 @@ import React, { useLayoutEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; import { email } from '../../core/validation'; -import { createMultiViewWidget } from '../../components/widget/widget'; +import { createRouterWidget } from '../../components/widget/widget'; import { Info, Intro } from '../../components/miscComponent'; import { createForm } from '../../components/form/formComponent'; import { simpleField } from '../../components/form/fields/simpleField'; import ReCaptcha, { importGoogleRecaptchaScript, type WithCaptchaToken } from '../../components/reCaptcha' import { useI18n } from '../../contexts/i18n'; import { useReachfive } from '../../contexts/reachfive'; -import { useRouting } from '../../contexts/routing'; type EmailFormData = { email: string } @@ -61,7 +61,7 @@ const MainView = ({ }: MainViewProps) => { const coreClient = useReachfive() const i18n = useI18n() - const { goTo } = useRouting() + const navigate = useNavigate() useLayoutEffect(() => { importGoogleRecaptchaScript(recaptcha_site_key) @@ -71,7 +71,7 @@ const MainView = ({ return coreClient.updateEmail({ ...data, accessToken, redirectUrl }); } - const handleSuccess = () => goTo('success'); + const handleSuccess = () => navigate('/success'); return (
@@ -91,9 +91,9 @@ const SuccessView = () => { export interface EmailEditorWidgetProps extends MainViewProps {} -export default createMultiViewWidget({ +export default createRouterWidget({ initialView: 'main', - views: { + routes: { main: MainView, success: SuccessView } diff --git a/src/widgets/mfa/MfaCredentialsWidget.tsx b/src/widgets/mfa/MfaCredentialsWidget.tsx index 659db654..e34d1f6d 100644 --- a/src/widgets/mfa/MfaCredentialsWidget.tsx +++ b/src/widgets/mfa/MfaCredentialsWidget.tsx @@ -2,10 +2,11 @@ import React from 'react'; import styled from 'styled-components'; import { MFA } from '@reachfive/identity-core'; import type { StartMfaEmailRegistrationResponse, StartMfaPhoneNumberRegistrationResponse } from '@reachfive/identity-core/es/main/mfaClient'; +import { useLocation, useNavigate } from 'react-router-dom'; import type { Config, Prettify } from '../../types'; -import { createMultiViewWidget } from '../../components/widget/widget'; +import { createRouterWidget } from '../../components/widget/widget'; import { simpleField } from '../../components/form/fields/simpleField'; import { Info, Intro, Separator } from '../../components/miscComponent'; @@ -16,7 +17,6 @@ import { UserError } from '../../helpers/errors'; import { useI18n } from '../../contexts/i18n'; import { useReachfive } from '../../contexts/reachfive'; -import { useRouting } from '../../contexts/routing'; import { useConfig } from '../../contexts/config'; @@ -107,7 +107,7 @@ const MainView = ({ const coreClient = useReachfive() const config = useConfig() const i18n = useI18n() - const { goTo } = useRouting() + const navigate = useNavigate() const onEmailRegistering = () => { return coreClient.startMfaEmailRegistration({ @@ -150,7 +150,7 @@ const MainView = ({ {showIntro && {requireMfaRegistration ? i18n('mfa.email.explain.required') :i18n('mfa.email.explain')}} >) => goTo('verification-code', {...data, registrationType: 'email'})} + onSuccess={(data: Awaited>) => navigate('/verification-code', { state: { ...data, registrationType: 'email' }})} />
} @@ -162,7 +162,7 @@ const MainView = ({ {showIntro && {i18n('mfa.phoneNumber.explain')}} >) => goTo('verification-code', {...data, registrationType: 'sms'})} + onSuccess={(data: Awaited>) => navigate('/verification-code', { state: { ...data, registrationType: 'sms' }})} sharedProps={{ ...phoneNumberOptions }} @@ -178,7 +178,7 @@ const MainView = ({ {showIntro && {i18n('mfa.email.remove.explain')}} goTo('credential-removed', { credentialType: 'email' })} + onSuccess={() => navigate('/credential-removed', { state: { credentialType: 'email' }})} />
} @@ -196,7 +196,7 @@ const MainView = ({ {showIntro && {i18n('mfa.phoneNumber.remove.explain')}} onPhoneNumberRemoval({ ...phoneNumberCredentialRegistered })} - onSuccess={() => goTo('credential-removed', {credentialType: 'sms'})} + onSuccess={() => navigate('/credential-removed', { state: { credentialType: 'sms' }})} /> } @@ -223,8 +223,9 @@ type VerificationCodeViewState = const VerificationCodeView = ({ accessToken, showIntro = true }: VerificationCodeViewProps) => { const coreClient = useReachfive() const i18n = useI18n() - const { goTo, params } = useRouting() - const { registrationType, status } = params as VerificationCodeViewState + const navigate = useNavigate() + const location = useLocation() + const { registrationType, status } = location.state as VerificationCodeViewState const onEmailCodeVerification = (data: VerificationCodeFormData) => { return coreClient.verifyMfaEmailRegistration({ @@ -241,7 +242,7 @@ const VerificationCodeView = ({ accessToken, showIntro = true }: VerificationCod } if (showIntro && status === 'enabled') { - goTo('credential-registered', { registrationType }) + navigate('/credential-registered', { state: { registrationType }}) return null } @@ -251,7 +252,7 @@ const VerificationCodeView = ({ accessToken, showIntro = true }: VerificationCod {status === 'email_sent' && goTo('credential-registered', { registrationType })} + onSuccess={() => navigate('/credential-registered', { state: { registrationType }})} /> } @@ -259,7 +260,7 @@ const VerificationCodeView = ({ accessToken, showIntro = true }: VerificationCod {status === 'sms_sent' && goTo('credential-registered', { registrationType })} + onSuccess={() => navigate('/credential-registered', { state: { registrationType }})} /> } @@ -274,8 +275,8 @@ type CredentialRegisteredViewState = { const CredentialRegisteredView = () => { const i18n = useI18n() - const { params } = useRouting() - const { registrationType } = params as CredentialRegisteredViewState + const location = useLocation() + const { registrationType } = location.state as CredentialRegisteredViewState return (
{registrationType === 'email' && {i18n('mfa.email.registered')}} @@ -292,8 +293,8 @@ type CredentialRemovedViewState = { const CredentialRemovedView = () => { const i18n = useI18n() - const { params } = useRouting() - const { credentialType } = params as CredentialRemovedViewState + const location = useLocation() + const { credentialType } = location.state as CredentialRemovedViewState return (
{credentialType === 'email' && {i18n('mfa.email.removed')}} @@ -306,9 +307,9 @@ type MfaCredentialsProps = Prettify> -export default createMultiViewWidget({ +export default createRouterWidget({ initialView: 'main', - views: { + routes: { 'main': MainView, 'credential-registered': CredentialRegisteredView, 'verification-code': VerificationCodeView, diff --git a/src/widgets/passwordReset/passwordResetWidget.tsx b/src/widgets/passwordReset/passwordResetWidget.tsx index b93e409c..c7aeaab5 100644 --- a/src/widgets/passwordReset/passwordResetWidget.tsx +++ b/src/widgets/passwordReset/passwordResetWidget.tsx @@ -1,12 +1,12 @@ import React from 'react'; +import { useNavigate } from 'react-router-dom'; import { parseQueryString } from '../../helpers/queryString' import { Heading, Info, Link } from '../../components/miscComponent'; -import { createMultiViewWidget } from '../../components/widget/widget'; +import { createRouterWidget } from '../../components/widget/widget'; import { PasswordEditorForm, PasswordEditorFormData } from '../passwordEditor/passwordEditorWidget' import { useReachfive } from '../../contexts/reachfive'; -import { useRouting } from '../../contexts/routing'; import { useI18n } from '../../contexts/i18n'; interface MainViewProps { @@ -40,7 +40,7 @@ const MainView = ({ }: PropsWithAuthentication) => { const coreClient = useReachfive() const i18n = useI18n() - const { goTo } = useRouting() + const navigate = useNavigate() const handleSubmit = ({ password }: PasswordEditorFormData) => { return coreClient.updatePassword({ @@ -51,7 +51,7 @@ const MainView = ({ const handleSuccess = () => { onSuccess(); - goTo('success'); + navigate('/success'); }; return ( @@ -89,7 +89,7 @@ const SuccessView = ({ loginLink }: SuccessViewProps) => { const resolveCode = () => { const qs = (window.location.search && window.location.search.length) - ? window.location.search.substr(1) + ? window.location.search.substring(1) : ''; const { verificationCode, email } = parseQueryString(qs) return { authentication: { verificationCode, email } as Authentication }; @@ -100,9 +100,9 @@ type PropsWithAuthentication

= P & { authentication?: Authentication } export interface PasswordResetWidgetProps extends MainViewProps, SuccessViewProps {} -export default createMultiViewWidget>({ +export default createRouterWidget>({ initialView: 'main', - views: { + routes: { main: MainView, success: SuccessView }, diff --git a/src/widgets/passwordless/passwordlessWidget.tsx b/src/widgets/passwordless/passwordlessWidget.tsx index c4f9cad9..be6e4162 100644 --- a/src/widgets/passwordless/passwordlessWidget.tsx +++ b/src/widgets/passwordless/passwordlessWidget.tsx @@ -1,11 +1,12 @@ import React, { useLayoutEffect } from 'react'; import { AuthOptions, SingleFactorPasswordlessParams } from '@reachfive/identity-core'; +import { useLocation, useNavigate } from 'react-router-dom'; import type { Config, Prettify } from '../../types'; import { email } from '../../core/validation'; -import { createMultiViewWidget } from '../../components/widget/widget'; +import { createRouterWidget } from '../../components/widget/widget'; import { Info, Intro, Separator } from '../../components/miscComponent'; import { createForm } from '../../components/form/formComponent'; @@ -15,7 +16,6 @@ import { SocialButtons } from '../../components/form/socialButtonsComponent'; import ReCaptcha, { importGoogleRecaptchaScript, type WithCaptchaToken } from '../../components/reCaptcha'; import { useReachfive } from '../../contexts/reachfive'; -import { useRouting } from '../../contexts/routing'; import { useI18n } from '../../contexts/i18n'; import { useConfig } from '../../contexts/config'; @@ -107,7 +107,7 @@ const MainView = ({ const coreClient = useReachfive() const config = useConfig() const i18n = useI18n() - const { goTo } = useRouting() + const navigate = useNavigate() useLayoutEffect(() => { importGoogleRecaptchaScript(recaptcha_site_key) @@ -122,8 +122,8 @@ const MainView = ({ const handleSuccess = (data: EmailFormData | PhoneNumberFormFata) => 'email' in data - ? goTo('emailSent') - : goTo('verificationCode', data) + ? navigate('/emailSent') + : navigate('/verificationCode', { state: data }) const isEmail = authType === 'magic_link'; const PhoneNumberInputForm = phoneNumberInputForm(config); @@ -181,8 +181,8 @@ const VerificationCodeView = ({ }: VerificationCodeViewProps) => { const coreClient = useReachfive() const i18n = useI18n() - const { params } = useRouting() - const { phoneNumber } = params as VerificationCodeViewState + const location = useLocation() + const { phoneNumber } = location.state as VerificationCodeViewState const handleSubmit = (data: WithCaptchaToken) => { return coreClient.verifyPasswordless({ @@ -209,9 +209,9 @@ const EmailSentView = () => { export type PasswordlessWidgetProps = Prettify -export default createMultiViewWidget({ +export default createRouterWidget({ initialView: 'main', - views: { + routes: { main: MainView, emailSent: EmailSentView, verificationCode: VerificationCodeView diff --git a/src/widgets/phoneNumberEditor/phoneNumberEditorWidget.tsx b/src/widgets/phoneNumberEditor/phoneNumberEditorWidget.tsx index 04c1c384..bc48751c 100644 --- a/src/widgets/phoneNumberEditor/phoneNumberEditorWidget.tsx +++ b/src/widgets/phoneNumberEditor/phoneNumberEditorWidget.tsx @@ -1,8 +1,9 @@ import React, { useMemo } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; import type { Config, Prettify } from '../../types'; -import { createMultiViewWidget } from '../../components/widget/widget'; +import { createRouterWidget } from '../../components/widget/widget'; import { Info, Intro } from '../../components/miscComponent'; import { createForm } from '../../components/form/formComponent'; import { simpleField } from '../../components/form/fields/simpleField'; @@ -10,7 +11,6 @@ import phoneNumberField, { type PhoneNumberOptions } from '../../components/form import { useReachfive } from '../../contexts/reachfive'; import { useI18n } from '../../contexts/i18n'; -import { useRouting } from '../../contexts/routing'; import { useConfig } from '../../contexts/config'; type PhoneNumberFormData = { phoneNumber: string } @@ -54,7 +54,7 @@ const MainView = ({ accessToken, showLabels = false, phoneNumberOptions }: MainV const coreClient = useReachfive() const config = useConfig() const i18n = useI18n() - const { goTo } = useRouting() + const navigate = useNavigate() const handleSubmit = (data: PhoneNumberFormData) => { return coreClient.updatePhoneNumber({ @@ -63,7 +63,7 @@ const MainView = ({ accessToken, showLabels = false, phoneNumberOptions }: MainV }).then(() => data); }; - const handleSuccess = (data: PhoneNumberFormData) => goTo('verificationCode', data); + const handleSuccess = (data: PhoneNumberFormData) => navigate('/verificationCode', { state: data }); const PhoneNumberInputForm = useMemo(() => phoneNumberInputForm(config), [config]); @@ -112,8 +112,8 @@ const VerificationCodeView = ({ }: VerificationCodeViewProps) => { const coreClient = useReachfive() const i18n = useI18n() - const { params } = useRouting() - const { phoneNumber } = params as VerificationCodeViewState + const location = useLocation() + const { phoneNumber } = location.state as VerificationCodeViewState const handleSubmit = (data: VerificationCodeFormData) => { return coreClient.verifyPhoneNumber({ ...data, phoneNumber, accessToken }); @@ -133,9 +133,9 @@ const VerificationCodeView = ({ export type PhoneNumberEditorWidgetProps = Prettify -export default createMultiViewWidget({ +export default createRouterWidget({ initialView: 'main', - views: { + routes: { main: MainView, verificationCode: VerificationCodeView, } diff --git a/src/widgets/socialAccounts/socialAccountsWidget.tsx b/src/widgets/socialAccounts/socialAccountsWidget.tsx index 12249be2..f2fff522 100644 --- a/src/widgets/socialAccounts/socialAccountsWidget.tsx +++ b/src/widgets/socialAccounts/socialAccountsWidget.tsx @@ -1,6 +1,7 @@ import React, { Fragment, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { AuthOptions, Identity as CoreIdentity, Profile } from '@reachfive/identity-core'; +import { useNavigate } from 'react-router-dom'; import { UserError } from '../../helpers/errors'; @@ -8,12 +9,11 @@ import { ProviderId, providers as socialProviders } from '../../providers/provid import { useI18n } from '../../contexts/i18n'; import { useReachfive } from '../../contexts/reachfive'; -import { useRouting } from '../../contexts/routing'; import { ErrorMessage } from '../../components/error'; import { Card, CloseIcon } from '../../components/form/cardComponent'; import { Link, Info, Alternative, MutedText } from '../../components/miscComponent'; -import { createMultiViewWidget } from '../../components/widget/widget'; +import { createRouterWidget } from '../../components/widget/widget'; import { SocialButtons } from '../../components/form/socialButtonsComponent'; import { DefaultButton } from '../../components/form/buttonComponent'; @@ -41,8 +41,8 @@ const withIdentities = ( const displayName = WrappedComponent.displayName || WrappedComponent.name || "Component"; const ComponentWithIdentities = (props: Omit) => { + const navigate = useNavigate() const coreClient = useReachfive() - const { goTo } = useRouting() const [identities, setIdentities] = useState([]) const refresh = useCallback(() => { @@ -70,8 +70,8 @@ const withIdentities = ( const handleAuthenticated = useCallback(() => { refresh() - goTo('links') - }, [goTo, refresh]) + navigate('/links') + }, [navigate, refresh]) useEffect(() => { if (props.auth?.popupMode) { @@ -159,14 +159,14 @@ interface SocialAccountsProps { const SocialAccounts = withIdentities(({ identities = [], providers, unlink }: SocialAccountsProps) => { const i18n = useI18n() - const { goTo } = useRouting() + const navigate = useNavigate() const availableProviders = findAvailableProviders(providers, identities) return ( {availableProviders.length > 0 && ( - goTo('link-account')}> + navigate('/link-account')}> {i18n('socialAccounts.linkNewAccount')} @@ -190,7 +190,7 @@ const LinkAccount = withIdentities(({ auth, accessToken, identities = [], provid - {i18n('back')} + {i18n('back')} ) @@ -218,14 +218,14 @@ interface SocialAccountsWidgetPropsPrepared extends Omit {} -export default createMultiViewWidget({ +export default createRouterWidget({ initialView: 'links', - views: { + routes: { 'links': SocialAccounts, 'link-account': LinkAccount }, prepare: (options, { config }) => ({ - providers: options.providers || (config.socialProviders as string[]), + providers: options.providers || config.socialProviders, ...options, }), }); diff --git a/src/widgets/stepUp/mfaStepUpWidget.tsx b/src/widgets/stepUp/mfaStepUpWidget.tsx index 88f369a1..f7f0efa3 100644 --- a/src/widgets/stepUp/mfaStepUpWidget.tsx +++ b/src/widgets/stepUp/mfaStepUpWidget.tsx @@ -1,10 +1,11 @@ import React, { useCallback, useEffect, useState } from 'react'; import { AuthOptions, MFA, PasswordlessResponse } from '@reachfive/identity-core'; import { PasswordlessParams } from '@reachfive/identity-core/es/main/oAuthClient'; +import { useLocation, useNavigate } from 'react-router-dom'; import { Prettify, RequiredProperty } from '../../types' -import {createMultiViewWidget} from '../../components/widget/widget'; +import {createRouterWidget} from '../../components/widget/widget'; import {createForm} from '../../components/form/formComponent'; import radioboxField from '../../components/form/fields/radioboxField'; import {Info, Intro} from '../../components/miscComponent'; @@ -13,7 +14,6 @@ import {simpleField} from '../../components/form/fields/simpleField'; import { toQueryString } from '../../helpers/queryString'; import { useReachfive } from '../../contexts/reachfive'; -import { useRouting } from '../../contexts/routing'; import { useI18n } from '../../contexts/i18n'; const StartStepUpMfaButton = createForm({ @@ -85,7 +85,7 @@ export interface MainViewProps { export const MainView = ({ accessToken, auth, showIntro = true, showStepUpStart = true }: MainViewProps) => { const coreClient = useReachfive() - const { goTo } = useRouting() + const navigate = useNavigate() const [response, setResponse] = useState() @@ -112,7 +112,7 @@ export const MainView = ({ accessToken, auth, showIntro = true, showStepUpStart return ( goTo('fa-selection', { ...data })} + onSuccess={(data: MFA.StepUpResponse) => navigate('/fa-selection', { state: { ...data } })} /> ) } @@ -139,8 +139,8 @@ type StepUpHandlerResponse = StepUpResponse & StartPasswordlessFormData export const FaSelectionView = (props: FaSelectionViewProps) => { const coreClient = useReachfive() const i18n = useI18n() - const { params } = useRouting() - const state = params as FaSelectionViewState + const location = useLocation() + const state = location.state as FaSelectionViewState const { amr, showIntro = true, token } = { ...props, ...state } @@ -192,8 +192,8 @@ export type VerificationCodeViewProps = Prettify export const VerificationCodeView = (props: VerificationCodeViewProps) => { const coreClient = useReachfive() const i18n = useI18n() - const { params } = useRouting() - const state = params as VerificationCodeViewState + const location = useLocation() + const state = location.state as VerificationCodeViewState const { auth, authType, challengeId } = { ...props, ...state } @@ -216,9 +216,9 @@ export type MfaStepUpProps = MainViewProps & FaSelectionViewProps & Verification export type MfaStepUpWidgetProps = MfaStepUpProps -export default createMultiViewWidget({ +export default createRouterWidget({ initialView: 'main', - views: { + routes: { 'main': MainView, 'fa-selection': FaSelectionView, 'verification-code': VerificationCodeView diff --git a/types/identity-ui.d.ts b/types/identity-ui.d.ts index fe17d662..7067431b 100644 --- a/types/identity-ui.d.ts +++ b/types/identity-ui.d.ts @@ -1,6 +1,6 @@ /** - * @reachfive/identity-ui - v1.28.0 - * Compiled Fri, 08 Nov 2024 09:24:35 UTC + * @reachfive/identity-ui - v1.29.0 + * Compiled Mon, 18 Nov 2024 19:38:54 UTC * * Copyright (c) ReachFive. * From 1e7b9bd7117d74264a6b42831fdff222af9e5833 Mon Sep 17 00:00:00 2001 From: Quentin Aupetit Date: Fri, 22 Nov 2024 10:16:19 -0400 Subject: [PATCH 2/2] use memory router to avoid url path change --- src/components/widget/widget.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/widget/widget.tsx b/src/components/widget/widget.tsx index 6332145c..fcf19b7b 100644 --- a/src/components/widget/widget.tsx +++ b/src/components/widget/widget.tsx @@ -1,7 +1,7 @@ import React, { ComponentType } from 'react'; import { ThemeProvider } from 'styled-components' import type { SessionInfo, Client as CoreClient } from '@reachfive/identity-core' -import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; +import { createMemoryRouter, Navigate, RouterProvider } from 'react-router-dom'; import type { Config, Prettify } from '../../types' import type { I18nMessages } from '../../core/i18n'; @@ -127,7 +127,7 @@ export function createRouterWidget({ fallbackElement, initialView, pre return createWidget({ component: (props: Omit) => { const initialRoute = typeof initialView === 'function' ? initialView(props) : initialView - const router = createBrowserRouter([ + const router = createMemoryRouter([ ...Object.entries(routes).map(([path, RouteComponent]) => ({ path, element: ,