From 3f3b8aad6402df40ff52f6da0ac64eefe288fb9e Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Tue, 8 Sep 2026 02:08:55 +0530 Subject: [PATCH] docs(example): restructure example app into per-feature hooks and class demos --- example/README.md | 22 + example/package.json | 5 - example/src/App.tsx | 54 +- example/src/App.web.tsx | 2919 ++++++----------- example/src/api/auth0.ts | 18 - example/src/components/Button.tsx | 63 - example/src/components/Header.tsx | 31 - example/src/components/LabeledInput.tsx | 49 - example/src/components/Result.tsx | 61 - example/src/components/UserInfo.tsx | 70 - example/src/features/AdvancedTokensClass.tsx | 129 + example/src/features/AdvancedTokensHooks.tsx | 124 + example/src/features/CredentialsClass.tsx | 96 + example/src/features/CredentialsHooks.tsx | 90 + example/src/features/DirectAuthClass.tsx | 155 + example/src/features/DirectAuthHooks.tsx | 133 + example/src/features/MfaClass.tsx | 191 ++ example/src/features/MfaHooks.tsx | 183 ++ example/src/features/MyAccountClass.tsx | 356 ++ example/src/features/MyAccountHooks.tsx | 349 ++ example/src/features/PasskeysClass.tsx | 79 + example/src/features/PasskeysHooks.tsx | 79 + example/src/features/PasswordlessClass.tsx | 151 + example/src/features/PasswordlessHooks.tsx | 150 + example/src/features/WebAuthClass.tsx | 79 + example/src/features/WebAuthHooks.tsx | 79 + example/src/navigation/AuthStackNavigator.tsx | 29 - example/src/navigation/ClassDemoNavigator.tsx | 48 - example/src/navigation/HooksDemoNavigator.tsx | 111 - example/src/navigation/MainTabNavigator.tsx | 48 - example/src/navigation/RootNavigator.tsx | 78 - example/src/screens/SelectionScreen.tsx | 95 - .../src/screens/class-based/ClassApiTests.tsx | 312 -- .../src/screens/class-based/ClassLogin.tsx | 659 ---- .../src/screens/class-based/ClassProfile.tsx | 261 -- .../src/screens/hooks/CredentialsScreen.tsx | 246 -- example/src/screens/hooks/Home.tsx | 1222 ------- example/src/screens/hooks/More.tsx | 144 - example/src/screens/hooks/MyAccountScreen.tsx | 634 ---- example/src/screens/hooks/Profile.tsx | 157 - example/src/shared/ResultView.tsx | 37 + example/src/shared/Section.tsx | 17 + example/src/shared/api.ts | 16 + example/webpack.config.js | 3 - yarn.lock | 267 +- 45 files changed, 3587 insertions(+), 6512 deletions(-) delete mode 100644 example/src/api/auth0.ts delete mode 100644 example/src/components/Button.tsx delete mode 100644 example/src/components/Header.tsx delete mode 100644 example/src/components/LabeledInput.tsx delete mode 100644 example/src/components/Result.tsx delete mode 100644 example/src/components/UserInfo.tsx create mode 100644 example/src/features/AdvancedTokensClass.tsx create mode 100644 example/src/features/AdvancedTokensHooks.tsx create mode 100644 example/src/features/CredentialsClass.tsx create mode 100644 example/src/features/CredentialsHooks.tsx create mode 100644 example/src/features/DirectAuthClass.tsx create mode 100644 example/src/features/DirectAuthHooks.tsx create mode 100644 example/src/features/MfaClass.tsx create mode 100644 example/src/features/MfaHooks.tsx create mode 100644 example/src/features/MyAccountClass.tsx create mode 100644 example/src/features/MyAccountHooks.tsx create mode 100644 example/src/features/PasskeysClass.tsx create mode 100644 example/src/features/PasskeysHooks.tsx create mode 100644 example/src/features/PasswordlessClass.tsx create mode 100644 example/src/features/PasswordlessHooks.tsx create mode 100644 example/src/features/WebAuthClass.tsx create mode 100644 example/src/features/WebAuthHooks.tsx delete mode 100644 example/src/navigation/AuthStackNavigator.tsx delete mode 100644 example/src/navigation/ClassDemoNavigator.tsx delete mode 100644 example/src/navigation/HooksDemoNavigator.tsx delete mode 100644 example/src/navigation/MainTabNavigator.tsx delete mode 100644 example/src/navigation/RootNavigator.tsx delete mode 100644 example/src/screens/SelectionScreen.tsx delete mode 100644 example/src/screens/class-based/ClassApiTests.tsx delete mode 100644 example/src/screens/class-based/ClassLogin.tsx delete mode 100644 example/src/screens/class-based/ClassProfile.tsx delete mode 100644 example/src/screens/hooks/CredentialsScreen.tsx delete mode 100644 example/src/screens/hooks/Home.tsx delete mode 100644 example/src/screens/hooks/More.tsx delete mode 100644 example/src/screens/hooks/MyAccountScreen.tsx delete mode 100644 example/src/screens/hooks/Profile.tsx create mode 100644 example/src/shared/ResultView.tsx create mode 100644 example/src/shared/Section.tsx create mode 100644 example/src/shared/api.ts diff --git a/example/README.md b/example/README.md index ed75be153..fb98d9ff6 100644 --- a/example/README.md +++ b/example/README.md @@ -13,6 +13,28 @@ To run the example application inside the repository, follow these steps: The application will be built and launched on the specified platform, allowing you to interact with it. +To run the web example, run `yarn web` from the `example` directory and open the served URL. + +### Layout + +The app has no navigation. `App.tsx` (native) renders a single sectioned +`ScrollView`, one section per SDK feature group: Web Auth, Credentials Manager, +Direct Auth API (native only), Passwordless (native only), MFA, My Account, +Passkeys, and Advanced Tokens. `App.web.tsx` is a single file covering the +web-supported features only, with web-specific methods labelled inline. + +Each native feature has two files under `src/features/`: + +- `FeatureHooks.tsx` — the `useAuth0()` hooks approach. These are the files the + app actually imports and renders. +- `FeatureClass.tsx` — the same feature written against the `Auth0` class + instance (`src/shared/api.ts`), kept as unused side-by-side reference. Nothing + imports these; they exist to show the class API next to the hooks API. + +Platform-specific methods carry the platform in their label (e.g. +`resumeSession (Android)`, `cancelWebAuth (iOS)`, `saveCredentials (Native only)`). +There is no custom styling — only default React Native components with spacing. + ### To run on different Auth0 Application 1. Change the `clientId` and `domain` value in `example/src/auth0-configuration.js` diff --git a/example/package.json b/example/package.json index b5b840d91..c1d55e1d8 100644 --- a/example/package.json +++ b/example/package.json @@ -12,14 +12,9 @@ "build:ios": "react-native build-ios --mode Debug" }, "dependencies": { - "@react-navigation/bottom-tabs": "^7.18.15", - "@react-navigation/native": "^7.3.15", - "@react-navigation/stack": "^7.10.20", "react": "19.2.8", "react-native": "0.86.2", - "react-native-gesture-handler": "^3.0.2", "react-native-safe-area-context": "^5.8.1", - "react-native-screens": "^4.27.0", "react-native-web": "^0.21.2" }, "devDependencies": { diff --git a/example/src/App.tsx b/example/src/App.tsx index 03f3e1a72..74bfbdf1c 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,23 +1,49 @@ import React from 'react'; -import { StatusBar } from 'react-native'; -import { NavigationContainer } from '@react-navigation/native'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; -import RootNavigator from './navigation/RootNavigator'; +import { ScrollView, StatusBar, Text, View } from 'react-native'; +import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; +import { Auth0Provider } from 'react-native-auth0'; +import config from './auth0-configuration'; -/** - * The absolute root component of the example application. - * - * It sets up the main navigation container and renders the RootNavigator, - * which then decides which demo flow (Hooks or Class-based) to display. - * The Auth0Provider is now scoped within the Hooks demo flow itself. - */ +import WebAuthHooks from './features/WebAuthHooks'; +import CredentialsHooks from './features/CredentialsHooks'; +import DirectAuthHooks from './features/DirectAuthHooks'; +import PasswordlessHooks from './features/PasswordlessHooks'; +import MfaHooks from './features/MfaHooks'; +import MyAccountHooks from './features/MyAccountHooks'; +import PasskeysHooks from './features/PasskeysHooks'; +import AdvancedTokensHooks from './features/AdvancedTokensHooks'; + +// Native example app. Every feature is demonstrated with the hooks approach +// (wired up below). A matching *Class.tsx file sits next to each feature file +// showing the same feature with the Auth0 class instance as unused reference. +// +// Biometric-protected credentials can be enabled by passing +// localAuthenticationOptions to Auth0Provider, e.g.: +// function App(): React.JSX.Element { return ( - + - - + + + + react-native-auth0 example + + + + + + + + + + + + + ); } diff --git a/example/src/App.web.tsx b/example/src/App.web.tsx index 777b650f8..ce06c0081 100644 --- a/example/src/App.web.tsx +++ b/example/src/App.web.tsx @@ -1,19 +1,8 @@ import React, { useState } from 'react'; -import { - SafeAreaView, - ScrollView, - View, - Text, - StyleSheet, - ActivityIndicator, - TouchableOpacity, - Image, - Linking, -} from 'react-native'; +import { Button, ScrollView, Text, TextInput, View } from 'react-native'; import Auth0, { Auth0Provider, useAuth0, - User, MfaError, MfaErrorCodes, MfaFactorType, @@ -25,261 +14,205 @@ import type { MfaAuthenticator, MfaEnrollmentChallenge, MfaChallengeResult, + DPoPHeadersParameters, } from 'react-native-auth0'; import config from './auth0-configuration'; -import Button from './components/Button'; -import Header from './components/Header'; -import Result from './components/Result'; -import LabeledInput from './components/LabeledInput'; +import Section from './shared/Section'; +import ResultView from './shared/ResultView'; import { createWebPasskey } from './passkey/webPasskey'; -// My Account API is served from the `/me/` audience of the tenant. MRRT -// (Multi-Resource Refresh Tokens) lets a single web session mint an access -// token for this audience via `getApiCredentials` without a fresh redirect. const MY_ACCOUNT_AUDIENCE = `https://${config.domain}/me/`; const MY_ACCOUNT_SCOPE = - 'read:me:authentication_methods delete:me:authentication_methods update:me:authentication_methods read:me:factors create:me:authentication_methods'; + 'read:me:authentication_methods delete:me:authentication_methods ' + + 'update:me:authentication_methods read:me:factors create:me:authentication_methods'; type MfaStep = - | 'idle' - | 'list' - | 'enroll-select' - | 'enroll-details' - | 'verify' - | 'complete'; + 'idle' | 'list' | 'enroll-select' | 'enroll-details' | 'verify' | 'complete'; type EnrollType = MfaFactorType; -// ======================================================================== -// --- 1. HOOKS-BASED IMPLEMENTATION (Recommended) --- -// ======================================================================== +function makeRun( + setResult: (r: unknown) => void, + setError: (e: Error | null) => void +) { + return async (fn: () => Promise) => { + setResult(null); + setError(null); + try { + const res = await fn(); + setResult(res ?? { success: true }); + } catch (e) { + setError(e as Error); + } + }; +} -const HooksAuthContent = (): React.JSX.Element => { - const { - authorize, - clearSession, - user, - error, - isLoading, - getCredentials, - getApiCredentials, - createUser, - resetPassword, - loginWithPasswordRealm, - customTokenExchange, - mfa, - myAccount, - passkeySignupChallenge, - passkeyLoginChallenge, - getTokenByPasskey, - } = useAuth0(); +// ============================================================ +// Hooks — Web Auth +// ============================================================ - const [result, setResult] = useState(null); - const [apiError, setApiError] = useState(null); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [passkeyEmail, setPasskeyEmail] = useState(''); - const [passkeyLoading, setPasskeyLoading] = useState(false); +function WebAuthSection() { + const { authorize, clearSession, user } = useAuth0(); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const run = makeRun(setResult, setError); - // Custom Token Exchange (RFC 8693) state - const [subjectToken, setSubjectToken] = useState(''); - const [subjectTokenType, setSubjectTokenType] = useState( - 'urn:acme:external-idp-token' + return ( +
+ + {user ? ( + <> + Logged in as {user.name ?? user.email ?? user.sub} +
); - const [actorToken, setActorToken] = useState(''); - const [actorTokenType, setActorTokenType] = useState( - 'urn:ietf:params:oauth:token-type:id_token' +} + +// ============================================================ +// Hooks — Credentials Manager +// ============================================================ + +function CredentialsSection() { + const { getCredentials, hasValidCredentials, clearCredentials } = useAuth0(); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const run = makeRun(setResult, setError); + + return ( +
+ +
); +} - const fillActorTokenFromSession = async () => { - setApiError(null); - try { - const credentials = await getCredentials(); - if (credentials?.idToken) { - setActorToken(credentials.idToken); - } else { - setApiError(new Error('No ID token available in the current session.')); - } - } catch (e) { - setApiError(e as Error); - } - }; +// ============================================================ +// Hooks — API Credentials / MRRT +// ============================================================ + +function ApiCredentialsSection() { + const { getApiCredentials } = useAuth0(); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [audience, setAudience] = useState(MY_ACCOUNT_AUDIENCE); + const [scope, setScope] = useState(MY_ACCOUNT_SCOPE); + const run = makeRun(setResult, setError); + + return ( +
+ + + +
+ ); +} + +// ============================================================ +// Hooks — MFA Flexible Factors +// ============================================================ - // MFA wizard state +function MfaSection() { + const { mfa } = useAuth0(); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); const [mfaToken, setMfaToken] = useState(''); - const [mfaStep, setMfaStep] = useState('idle'); - const [mfaLoading, setMfaLoading] = useState(false); + const [step, setStep] = useState('idle'); + const [loading, setLoading] = useState(false); const [authenticators, setAuthenticators] = useState([]); - const [selectedAuthenticator, setSelectedAuthenticator] = - useState(null); const [enrollType, setEnrollType] = useState(null); - const [enrollPhoneNumber, setEnrollPhoneNumber] = useState(''); + const [enrollPhone, setEnrollPhone] = useState(''); const [enrollEmail, setEnrollEmail] = useState(''); const [enrollmentChallenge, setEnrollmentChallenge] = useState(null); const [challengeResult, setChallengeResult] = useState(null); const [verifyCode, setVerifyCode] = useState(''); - const [verifyBindingCode, setVerifyBindingCode] = useState(''); + const [verifyBinding, setVerifyBinding] = useState(''); const [verifyScope, setVerifyScope] = useState(''); const [verifyAudience, setVerifyAudience] = useState(''); - // My Account state - const [maResult, setMaResult] = useState(null); - const [maError, setMaError] = useState(null); - const [maLoading, setMaLoading] = useState(false); - const [maToken, setMaToken] = useState(null); - const [phoneNumber, setPhoneNumber] = useState(''); - const [emailAddress, setEmailAddress] = useState(''); - const [otpCode, setOtpCode] = useState(''); - const [methodId, setMethodId] = useState(''); - const [methodName, setMethodName] = useState(''); - const [enrollmentState, setEnrollmentState] = useState<{ - id: string; - authSession: string; - kind: 'phone' | 'email' | 'totp' | 'recovery'; - } | null>(null); - const [passkeyChallenge, setPasskeyChallenge] = useState<{ - authenticationMethodId: string; - authSession: string; - authParamsPublicKey: Record; - } | null>(null); - - const resetMfaWizard = () => { - setMfaStep('idle'); + const reset = () => { + setStep('idle'); setAuthenticators([]); - setSelectedAuthenticator(null); setEnrollType(null); - setEnrollPhoneNumber(''); + setEnrollPhone(''); setEnrollEmail(''); setEnrollmentChallenge(null); setChallengeResult(null); setVerifyCode(''); - setVerifyBindingCode(''); + setVerifyBinding(''); setVerifyScope(''); setVerifyAudience(''); - setMfaLoading(false); - }; - - const runDemo = async (action: () => Promise) => { - setResult(null); - setApiError(null); - try { - const response = await action(); - setResult(response ?? { success: true }); - } catch (e) { - if (e instanceof MfaError) { - setApiError(e); - return; - } - setApiError(e as Error); - } - }; - - const handleMfaError = (e: unknown, fallbackMsg: string) => { - if (e instanceof MfaError) { - if ( - e.type === MfaErrorCodes.EXPIRED_MFA_TOKEN || - e.type === MfaErrorCodes.INVALID_MFA_TOKEN - ) { - setMfaToken(''); - resetMfaWizard(); - } - setApiError(e); - } else { - setApiError(e as Error); - } - }; - - const handlePasskeyError = (e: unknown) => { - if (e instanceof PasskeyError) { - setApiError(e); - return; - } - setApiError(e as Error); - }; - - const onPasskeySignup = async () => { - setResult(null); - setApiError(null); - setPasskeyLoading(true); - try { - const challenge = await passkeySignupChallenge({ - email: passkeyEmail || undefined, - realm: 'Username-Password-Authentication', - }); - - // navigator.credentials isn't wrapped by the SDK — normalize a - // cancelled/failed WebAuthn ceremony into a PasskeyError ourselves - // so callers get the same PasskeyErrorCodes regardless of where the - // failure occurred. - let credential: PublicKeyCredential; - try { - credential = (await navigator.credentials.create({ - publicKey: - challenge.authParamsPublicKey as PublicKeyCredentialCreationOptions, - })) as PublicKeyCredential; - } catch (e) { - throw new PasskeyError(e as Error); - } - - const credentials = await getTokenByPasskey({ - authSession: challenge.authSession, - authResponse: credential, - realm: 'Username-Password-Authentication', - }); - - setResult({ - success: true, - accessToken: `${credentials.accessToken.substring(0, 30)}...`, - }); - } catch (e) { - handlePasskeyError(e); - } finally { - setPasskeyLoading(false); - } + setLoading(false); }; - const onPasskeyLogin = async () => { - setResult(null); - setApiError(null); - setPasskeyLoading(true); - try { - const challenge = await passkeyLoginChallenge({ - realm: 'Username-Password-Authentication', - }); - - let credential: PublicKeyCredential; - try { - credential = (await navigator.credentials.get({ - publicKey: - challenge.authParamsPublicKey as PublicKeyCredentialRequestOptions, - })) as PublicKeyCredential; - } catch (e) { - throw new PasskeyError(e as Error); - } - - const credentials = await getTokenByPasskey({ - authSession: challenge.authSession, - authResponse: credential, - realm: 'Username-Password-Authentication', - }); - - setResult({ - success: true, - accessToken: `${credentials.accessToken.substring(0, 30)}...`, - }); - } catch (e) { - handlePasskeyError(e); - } finally { - setPasskeyLoading(false); + const handleMfaError = (e: unknown) => { + if ( + e instanceof MfaError && + (e.type === MfaErrorCodes.EXPIRED_MFA_TOKEN || + e.type === MfaErrorCodes.INVALID_MFA_TOKEN) + ) { + setMfaToken(''); + reset(); } + setError(e as Error); }; - const onMfaStart = async () => { - setMfaLoading(true); - setApiError(null); + const onStart = async () => { + setLoading(true); + setError(null); try { const list = await mfa.getAuthenticators({ mfaToken, @@ -292,55 +225,45 @@ const HooksAuthContent = (): React.JSX.Element => { ], }); setAuthenticators(list); - setMfaStep('list'); + setStep('list'); } catch (e) { - handleMfaError(e, 'Failed to list authenticators.'); + handleMfaError(e); } finally { - setMfaLoading(false); + setLoading(false); } }; - const onMfaSelectAuthenticator = async (auth: MfaAuthenticator) => { - setSelectedAuthenticator(auth); - setMfaLoading(true); + const onChallenge = async (auth: MfaAuthenticator) => { + setLoading(true); try { const res = await mfa.challenge({ mfaToken, authenticatorId: auth.id }); setChallengeResult(res); - setMfaStep('verify'); + setStep('verify'); } catch (e) { - handleMfaError(e, 'Challenge failed.'); - setMfaStep('list'); + handleMfaError(e); + setStep('list'); } finally { - setMfaLoading(false); - } - }; - - const onMfaSelectEnrollType = (type: EnrollType) => { - setEnrollType(type); - if (type === MfaFactorType.OTP || type === MfaFactorType.PUSH) { - onMfaEnroll(type); - } else { - setMfaStep('enroll-details'); + setLoading(false); } }; - const onMfaEnroll = async (type?: EnrollType) => { - const factor = type || enrollType; + const onEnroll = async (type?: EnrollType) => { + const factor = type ?? enrollType; if (!factor) return; - setMfaLoading(true); + setLoading(true); try { let challenge: MfaEnrollmentChallenge; if (factor === MfaFactorType.SMS) { challenge = await mfa.enroll({ mfaToken, factorType: MfaFactorType.SMS, - phoneNumber: enrollPhoneNumber, + phoneNumber: enrollPhone, }); } else if (factor === MfaFactorType.VOICE) { challenge = await mfa.enroll({ mfaToken, factorType: MfaFactorType.VOICE, - phoneNumber: enrollPhoneNumber, + phoneNumber: enrollPhone, }); } else if (factor === MfaFactorType.EMAIL) { challenge = await mfa.enroll({ @@ -352,36 +275,33 @@ const HooksAuthContent = (): React.JSX.Element => { challenge = await mfa.enroll({ mfaToken, factorType: factor }); } setEnrollmentChallenge(challenge); - setMfaStep('verify'); + setStep('verify'); } catch (e) { - handleMfaError(e, 'Enrollment failed.'); + handleMfaError(e); } finally { - setMfaLoading(false); + setLoading(false); } }; - const onMfaVerify = async () => { - setMfaLoading(true); + const onVerify = async () => { + setLoading(true); try { - let credentials; - // scope/audience are optional: supply them to mint an access token for a - // specific API once MFA succeeds. const extra = { scope: verifyScope || undefined, audience: verifyAudience || undefined, }; const oobCode = - challengeResult?.oobCode || + challengeResult?.oobCode ?? (enrollmentChallenge?.type === 'oob' || enrollmentChallenge?.type === 'push' ? enrollmentChallenge.oobCode : undefined); - + let credentials; if (oobCode) { credentials = await mfa.verify({ mfaToken, oobCode, - bindingCode: verifyBindingCode || undefined, + bindingCode: verifyBinding || undefined, ...extra, }); } else if (enrollmentChallenge?.type === 'recovery-code') { @@ -397,1693 +317,914 @@ const HooksAuthContent = (): React.JSX.Element => { success: true, accessToken: credentials.accessToken.substring(0, 20) + '...', }); - setMfaStep('complete'); + setStep('complete'); } catch (e) { - handleMfaError(e, 'Verification failed.'); + handleMfaError(e); } finally { - setMfaLoading(false); + setLoading(false); } }; - // --- My Account helpers --- + return ( +
+ + {step === 'idle' && ( + <> + + Obtain an mfa_token from a password-login attempt with MFA + enabled, then paste it here. + + +
+ ); +} + +// ============================================================ +// Hooks — My Account API +// ============================================================ + +function MyAccountSection() { + const { getApiCredentials, myAccount } = useAuth0(); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [maToken, setMaToken] = useState(''); + const [phoneNumber, setPhoneNumber] = useState(''); + const [emailAddress, setEmailAddress] = useState(''); + const [otpCode, setOtpCode] = useState(''); + const [methodId, setMethodId] = useState(''); + const [methodName, setMethodName] = useState(''); + const [enrollmentState, setEnrollmentState] = useState<{ + id: string; + authSession: string; + kind: 'phone' | 'email' | 'totp' | 'recovery'; + } | null>(null); + const [passkeyChallenge, setPasskeyChallenge] = useState<{ + authenticationMethodId: string; + authSession: string; + authParamsPublicKey: Record; + } | null>(null); - const runMyAccount = async ( - action: (accessToken: string) => Promise - ) => { + const runMA = async (action: (token: string) => Promise) => { if (!maToken) { - setMaResult(null); - setMaError(new Error('Get the My Account token (MRRT) first.')); + setError(new Error('Fetch the My Account token first.')); return; } - setMaResult(null); - setMaError(null); - setMaLoading(true); + setResult(null); + setError(null); + setLoading(true); try { - const response = await action(maToken); - setMaResult(response ?? { success: true }); + const res = await action(maToken); + setResult(res ?? { success: true }); } catch (e) { - // MyAccountError / PasskeyError carry richer fields than a plain message. if (e instanceof MyAccountError) { - setMaError( + setError( new Error( - `[${e.statusCode ?? ''}] ${e.title ?? 'My Account Error'}: ${ - e.detail ?? e.message - }` + `[${e.statusCode ?? ''}] ${e.title ?? 'My Account Error'}: ${e.detail ?? e.message}` ) ); - } else if (e instanceof PasskeyError) { - setMaError(new Error(`[${e.type}] ${e.message}`)); } else { - setMaError(e as Error); + setError(e as Error); } } finally { - setMaLoading(false); + setLoading(false); } }; - const onEnrollPhone = () => - runMyAccount(async (accessToken) => { - const challenge = await myAccount.enrollPhone({ - accessToken, - phoneNumber: phoneNumber.trim(), - preferredAuthenticationMethod: PreferredAuthenticationMethods.SMS, - }); - setEnrollmentState({ ...challenge, kind: 'phone' }); - return { step: 'enrollPhone', ...challenge }; - }); + return ( +
+ + + Uses MRRT to mint a /me/ access token via getApiCredentials. Fetch it + before calling any method below. + + +
+ ); +} - const onEnrollEmail = () => - runMyAccount(async (accessToken) => { - const challenge = await myAccount.enrollEmail({ - accessToken, - emailAddress: emailAddress.trim(), - }); - setEnrollmentState({ ...challenge, kind: 'email' }); - return { step: 'enrollEmail', ...challenge }; - }); +// ============================================================ +// Hooks — Passkeys (browser WebAuthn) +// ============================================================ - const onEnrollTOTP = () => - runMyAccount(async (accessToken) => { - const challenge = await myAccount.enrollTOTP({ accessToken }); - setEnrollmentState({ - id: challenge.id, - authSession: challenge.authSession, - kind: 'totp', - }); - return { - step: 'enrollTOTP', - id: challenge.id, - barcodeUri: challenge.barcodeUri, - manualInputCode: challenge.manualInputCode, - }; - }); - - const onEnrollRecoveryCode = () => - runMyAccount(async (accessToken) => { - const challenge = await myAccount.enrollRecoveryCode({ accessToken }); - setEnrollmentState({ - id: challenge.id, - authSession: challenge.authSession, - kind: 'recovery', - }); - return { - step: 'enrollRecoveryCode', - id: challenge.id, - recoveryCode: challenge.recoveryCode, - }; - }); - - const onConfirmEnrollment = () => - runMyAccount(async (accessToken) => { - if (!enrollmentState) { - throw new Error('Start an enrollment first.'); - } - let method; - if (enrollmentState.kind === 'recovery') { - method = await myAccount.confirmRecoveryCodeEnrollment({ - accessToken, - id: enrollmentState.id, - authSession: enrollmentState.authSession, - }); - } else { - const confirmByKind = { - phone: myAccount.confirmPhoneEnrollment, - email: myAccount.confirmEmailEnrollment, - totp: myAccount.confirmTOTPEnrollment, - }; - method = await confirmByKind[enrollmentState.kind].call(myAccount, { - accessToken, - id: enrollmentState.id, - authSession: enrollmentState.authSession, - otpCode: otpCode.trim(), - }); - } - setEnrollmentState(null); - setOtpCode(''); - return { step: 'confirmEnrollment', ...method }; - }); - - const onPasskeyChallenge = () => - runMyAccount(async (accessToken) => { - const challenge = await myAccount.passkeyEnrollmentChallenge({ - accessToken, - }); - setPasskeyChallenge(challenge); - return { - step: 'passkeyEnrollmentChallenge', - authenticationMethodId: challenge.authenticationMethodId, - authSession: challenge.authSession, - }; - }); - - const onPasskeyVerify = () => - runMyAccount(async (accessToken) => { - if (!passkeyChallenge) { - throw new Error('Run the passkey challenge first.'); - } - const authResponse = await createWebPasskey( - passkeyChallenge.authParamsPublicKey - ); - const method = await myAccount.enrollPasskey({ - accessToken, - authenticationMethodId: passkeyChallenge.authenticationMethodId, - authSession: passkeyChallenge.authSession, - authResponse, - authParamsPublicKey: passkeyChallenge.authParamsPublicKey, - }); - setPasskeyChallenge(null); - return { step: 'enrollPasskey', ...method }; - }); - - const onGetFactors = () => - runMyAccount(async (accessToken) => { - const factors = await myAccount.getFactors({ accessToken }); - return { step: 'getFactors', factors }; - }); - - const onGetAuthenticationMethods = () => - runMyAccount(async (accessToken) => { - const methods = await myAccount.getAuthenticationMethods({ accessToken }); - return { - step: 'getAuthenticationMethods', - count: methods.length, - methods, - }; - }); - - const onUpdateMethod = () => - runMyAccount(async (accessToken) => { - const method = await myAccount.updateAuthenticationMethodById({ - accessToken, - id: methodId.trim(), - name: methodName.trim() || undefined, - }); - return { step: 'updateAuthenticationMethodById', ...method }; - }); +function PasskeysSection() { + const { passkeySignupChallenge, passkeyLoginChallenge, getTokenByPasskey } = + useAuth0(); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [passkeyEmail, setPasskeyEmail] = useState(''); - const onDeleteMethod = () => - runMyAccount(async (accessToken) => { - await myAccount.deleteAuthenticationMethodById({ - accessToken, - id: methodId.trim(), + const onSignup = async () => { + setResult(null); + setError(null); + setLoading(true); + try { + const challenge = await passkeySignupChallenge({ + email: passkeyEmail || undefined, + realm: 'Username-Password-Authentication', }); - setMethodId(''); - return { - step: 'deleteAuthenticationMethodById', - deleted: methodId.trim(), - }; - }); - - if (isLoading) { - return ( - - - - ); - } - - return ( - -
- {error && } - - {user ? ( - <> - - Welcome, {user.name}! - -