diff --git a/docs/_partials/custom-flows/future-api-callout.mdx b/docs/_partials/custom-flows/future-api-callout.mdx new file mode 100644 index 0000000000..f241bba853 --- /dev/null +++ b/docs/_partials/custom-flows/future-api-callout.mdx @@ -0,0 +1,2 @@ +> [!IMPORTANT] +> The APIs described here are stable, and will become the default in the next major version of `clerk-js`. diff --git a/docs/guides/development/custom-flows/authentication/application-invitations.mdx b/docs/guides/development/custom-flows/authentication/application-invitations.mdx index 130aa4aa78..1055bcd5ee 100644 --- a/docs/guides/development/custom-flows/authentication/application-invitations.mdx +++ b/docs/guides/development/custom-flows/authentication/application-invitations.mdx @@ -1,6 +1,7 @@ --- title: Sign-up with application invitations description: Learn how to use the Clerk API to build a custom flow for handling application invitations. +sdk: nextjs, react, expo, react-router, tanstack-react-start --- @@ -19,112 +20,69 @@ Once the user visits the invitation link and is redirected to the specified URL, For example, if the redirect URL was `https://www.example.com/accept-invitation`, the URL that the user would be redirected to would be `https://www.example.com/accept-invitation?__clerk_ticket=.....`. -To create a sign-up flow using the invitation token, you need to extract the token from the URL and pass it to the [`signUp.create()`](/docs/reference/javascript/sign-up#create) method, as shown in the following example. The following example also demonstrates how to collect additional user information for the sign-up; you can either remove these fields or adjust them to fit your application. +To create a sign-up flow using the invitation token, you need to call the [`signUp.ticket()`](/docs/reference/javascript/sign-up-future#ticket) method, as shown in the following example. The following example also demonstrates how to collect additional user information for the sign-up; you can either remove these fields or adjust them to fit your application. - + ```tsx {{ filename: 'app/accept-invitation/page.tsx', collapsible: true }} 'use client' import * as React from 'react' import { useSignUp, useUser } from '@clerk/nextjs' - import { useSearchParams, useRouter } from 'next/navigation' + import { useRouter } from 'next/navigation' export default function Page() { const { isSignedIn, user } = useUser() + const { signUp, errors, fetchStatus } = useSignUp() const router = useRouter() - const { isLoaded, signUp, setActive } = useSignUp() - const [firstName, setFirstName] = React.useState('') - const [lastName, setLastName] = React.useState('') - const [password, setPassword] = React.useState('') - - // Handle signed-in users visiting this page - // This will also redirect the user once they finish the sign-up process - React.useEffect(() => { - if (isSignedIn) { - router.push('/') - } - }, [isSignedIn]) - - // Get the token from the query params - const token = useSearchParams().get('__clerk_ticket') - // If there is no invitation token, restrict access to this page - if (!token) { - return

No invitation token found.

- } + const handleSubmit = async (formData: FormData) => { + const firstName = formData.get('firstName') as string + const lastName = formData.get('lastName') as string + const password = formData.get('password') as string - // Handle submission of the sign-up form - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - - if (!isLoaded) return - - try { - if (!token) return null - - // Create a new sign-up with the supplied invitation token. - // Make sure you're also passing the ticket strategy. - // After the below call, the user's email address will be - // automatically verified because of the invitation token. - const signUpAttempt = await signUp.create({ - strategy: 'ticket', - ticket: token, - firstName, - lastName, - password, + await signUp.ticket({ + firstName, + lastName, + password, + }) + if (signUp.status === 'complete') { + await signUp.finalize({ + navigate: () => { + router.push('/') + }, }) - - // If the sign-up was completed, set the session to active - if (signUpAttempt.status === 'complete') { - await setActive({ session: signUpAttempt.createdSessionId }) - } else { - // If the sign-up status is not complete, check why. User may need to - // complete further steps. - console.error(JSON.stringify(signUpAttempt, null, 2)) - } - } catch (err) { - console.error(JSON.stringify(err, null, 2)) } } + if (signUp.status === 'complete' || isSignedIn) { + return null + } + return ( <>

Sign up

-
+
- setFirstName(e.target.value)} - /> + + {errors.fields.firstName &&

{errors.fields.firstName.message}

}
- setLastName(e.target.value)} - /> + + {errors.fields.lastName &&

{errors.fields.lastName.message}

}
- setPassword(e.target.value)} - /> + + {errors.fields.password &&

{errors.fields.password.message}

}
- +
@@ -132,123 +90,4 @@ To create a sign-up flow using the invitation token, you need to extract the tok } ``` - - - - ```html {{ filename: 'index.html', collapsible: true }} - - - - - - Clerk + JavaScript App - - -
- -
- -
-

Sign up

-
- - - - - - - -
-
- - - - - ``` - - ```js {{ filename: 'main.js', collapsible: true }} - import { Clerk } from '@clerk/clerk-js' - - const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY - - const clerk = new Clerk(pubKey) - await clerk.load() - - if (clerk.isSignedIn) { - // Mount user button component - document.getElementById('signed-in').innerHTML = ` -
- ` - - const userbuttonDiv = document.getElementById('user-button') - - clerk.mountUserButton(userbuttonDiv) - } else if (clerk.session.currentTask) { - // Check for pending tasks and display custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - switch (clerk.session.currentTask.key) { - case 'choose-organization': { - document.getElementById('app').innerHTML = ` -
- ` - - const taskDiv = document.getElementById('task') - - clerk.mountTaskChooseOrganization(taskDiv) - } - } - } else { - // Get the token from the query parameter - const param = '__clerk_ticket' - const token = new URL(window.location.href).searchParams.get(param) - - // Handle the sign-up form - document.getElementById('sign-up-form').addEventListener('submit', async (e) => { - e.preventDefault() - - const formData = new FormData(e.target) - const firstName = formData.get('firstName') - const lastName = formData.get('lastName') - const password = formData.get('password') - - try { - // Start the sign-up process using the ticket method - const signUpAttempt = await clerk.client.signUp.create({ - strategy: 'ticket', - ticket: token, - firstName, - lastName, - password, - }) - - // If sign-up was successful, set the session to active - if (signUpAttempt.status === 'complete') { - await clerk.setActive({ - session: signUpAttempt.createdSessionId, - navigate: async ({ session }) => { - if (session?.currentTask) { - // Check for tasks and navigate to custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - console.log(session?.currentTask) - return - } - - await router.push('/') - }, - }) - } else { - // If the status is not complete, check why. User may need to - // complete further steps. - console.error(JSON.stringify(signUpAttempt, null, 2)) - } - } catch (err) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(JSON.stringify(err, null, 2)) - } - }) - } - ``` -
-
diff --git a/docs/guides/development/custom-flows/authentication/bot-sign-up-protection.mdx b/docs/guides/development/custom-flows/authentication/bot-sign-up-protection.mdx index d9863c9b42..e27b4bbb54 100644 --- a/docs/guides/development/custom-flows/authentication/bot-sign-up-protection.mdx +++ b/docs/guides/development/custom-flows/authentication/bot-sign-up-protection.mdx @@ -1,6 +1,7 @@ --- title: Add bot protection to your custom sign-up flow description: Learn how to add Clerk's bot protection to your custom sign-up flow. +sdk: nextjs, react, expo, js-frontend, react-router, tanstack-react-start --- diff --git a/docs/guides/development/custom-flows/authentication/email-password-mfa.mdx b/docs/guides/development/custom-flows/authentication/email-password-mfa.mdx index 51d2933608..1d1761f8da 100644 --- a/docs/guides/development/custom-flows/authentication/email-password-mfa.mdx +++ b/docs/guides/development/custom-flows/authentication/email-password-mfa.mdx @@ -1,10 +1,24 @@ --- title: Build a custom sign-in flow with multi-factor authentication description: Learn how to build a custom email/password sign-in flow that requires multi-factor authentication (MFA). +sdk: nextjs, react, expo, js-frontend, react-router, tanstack-react-start, ios, android --- + + > [!IMPORTANT] + > This guide applies to the following Clerk SDKs: + > + > - `@clerk/react` v6 or higher + > - `@clerk/nextjs` v7 or higher + > - `@clerk/expo` v3 or higher + > - `@clerk/react-router` v3 or higher + > - `@clerk/tanstack-react-start` v0.26.0 or higher + > + > If you're using an older version of one of these SDKs, or are using the legacy API, refer to the [legacy API documentation](/docs/guides/development/custom-flows/authentication/legacy/email-password). + + [Multi-factor verification (MFA)](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) is an added layer of security that requires users to provide a second verification factor to access an account. Clerk supports second factor verification through **SMS verification code**, **Authenticator application**, and **Backup codes**. @@ -36,534 +50,259 @@ This guide will walk you through how to build a custom email/password sign-in fl To authenticate a user using their email and password, you need to: - 1. Initiate the sign-in process by collecting the user's email address and password. - 1. Prepare the first factor verification. - 1. Attempt to complete the first factor verification. - 1. Prepare the second factor verification. (This is where MFA comes into play.) - 1. Attempt to complete the second factor verification. - 1. If the verification is successful, set the newly created session as the active session. - - > [!TIP] - > For this example to work, the user must have MFA enabled on their account. You need to add the ability for your users to manage their MFA settings. See the [manage SMS-based MFA](/docs/guides/development/custom-flows/account-updates/manage-sms-based-mfa) or the [manage TOTP-based MFA](/docs/guides/development/custom-flows/account-updates/manage-totp-based-mfa) guide, depending on your needs. - - - - ```tsx {{ filename: 'app/sign-in/[[...sign-in]]/page.tsx', collapsible: true }} - 'use client' - - import * as React from 'react' - import { useSignIn } from '@clerk/nextjs' - import { useRouter } from 'next/navigation' - - export default function SignInForm() { - const { isLoaded, signIn, setActive } = useSignIn() - const [email, setEmail] = React.useState('') - const [password, setPassword] = React.useState('') - const [code, setCode] = React.useState('') - const [useBackupCode, setUseBackupCode] = React.useState(false) - const [displayTOTP, setDisplayTOTP] = React.useState(false) - const router = useRouter() - - // Handle user submitting email and pass and swapping to TOTP form - const handleFirstStage = (e: React.FormEvent) => { - e.preventDefault() - setDisplayTOTP(true) - } + + 1. Initiate the sign-up process by collecting the user's email address and password with the [`signIn.password()`](/docs/reference/javascript/sign-in-future#password) method. + 1. Collect the TOTP code and verify it with the [`signIn.mfa.verifyTOTP()`](/docs/reference/javascript/sign-in-future#mfa-verify-totp) method. + 1. If the TOTP verification is successful, finalize the sign-in with the [`signIn.finalize()`](/docs/reference/javascript/sign-in-future#finalize) method to set the newly created session as the active session. + + > [!TIP] + > For this example to work, the user must have MFA enabled on their account. You need to add the ability for your users to manage their MFA settings. See the [manage SMS-based MFA](/docs/guides/development/custom-flows/account-updates/manage-sms-based-mfa) or the [manage TOTP-based MFA](/docs/guides/development/custom-flows/account-updates/manage-totp-based-mfa) guide, depending on your needs. + + + + ```tsx {{ filename: 'app/sign-in/page.tsx', collapsible: true }} + 'use client' - // Handle the submission of the TOTP of Backup Code submission - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() + import * as React from 'react' + import { useSignIn } from '@clerk/nextjs' + import { useRouter } from 'next/navigation' - if (!isLoaded) return + export default function SignInForm() { + const { signIn, errors, fetchStatus } = useSignIn() + const router = useRouter() - // Start the sign-in process using the email and password provided - try { - await signIn.create({ - identifier: email, + const handleSubmit = async (formData: FormData) => { + const emailAddress = formData.get('email') as string + const password = formData.get('password') as string + + await signIn.password({ + emailAddress, password, }) + } - // Attempt the TOTP or backup code verification - const signInAttempt = await signIn.attemptSecondFactor({ - strategy: useBackupCode ? 'backup_code' : 'totp', - code: code, - }) + const handleSubmitTOTP = async (formData: FormData) => { + const code = formData.get('code') as string + const useBackupCode = formData.get('useBackupCode') === 'on' - // If verification was completed, set the session to active - // and redirect the user - if (signInAttempt.status === 'complete') { - await setActive({ - session: signInAttempt.createdSessionId, - navigate: async ({ session }) => { - if (session?.currentTask) { - // Check for tasks and navigate to custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - console.log(session?.currentTask) - return - } - - await router.push('/') + if (useBackupCode) { + await signIn.mfa.verifyBackupCode({ code }) + } else { + await signIn.mfa.verifyTOTP({ code }) + } + + if (signIn.status === 'complete') { + await signIn.finalize({ + navigate: () => { + router.push('/') }, }) - } else { - // If the status is not complete, check why. User may need to - // complete further steps. - console.log(signInAttempt) } - } catch (err) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error('Error:', JSON.stringify(err, null, 2)) } - } - if (displayTOTP) { + if (signIn.status === 'needs_second_factor') { + return ( +
+

Verify your account

+
+
+ + + {errors.fields.code &&

{errors.fields.code.message}

} +
+
+ +
+ +
+
+ ) + } + return ( -
-

Verify your account

-
handleSubmit(e)}> + <> +

Sign in

+
- - setCode(e.target.value)} - id="code" - name="code" - type="text" - value={code} - /> + + + {errors.fields.emailAddress &&

{errors.fields.emailAddress.message}

}
- - setUseBackupCode((prev) => !prev)} - id="backupcode" - name="backupcode" - type="checkbox" - checked={useBackupCode} - /> + + + {errors.fields.password &&

{errors.fields.password.message}

}
- +
-
+ ) } - - return ( - <> -

Sign in

-
handleFirstStage(e)}> -
- - setEmail(e.target.value)} - id="email" - name="email" - type="email" - value={email} - /> -
-
- - setPassword(e.target.value)} - id="password" - name="password" - type="password" - value={password} - /> -
- -
- - ) - } - ``` -
- - - - ```html {{ filename: 'index.html', collapsible: true }} - - - - - - Clerk + JavaScript App - - -
- -
- -
-

Sign in

-
- - - - - -
-
- - - - - - ``` - - ```js {{ filename: 'main.js', collapsible: true }} - import { Clerk } from '@clerk/clerk-js' - - const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY - - const clerk = new Clerk(pubKey) - await clerk.load() - - if (clerk.isSignedIn) { - // Mount user button component - document.getElementById('signed-in').innerHTML = ` -
- ` - - const userbuttonDiv = document.getElementById('user-button') - - clerk.mountUserButton(userbuttonDiv) - } else if (clerk.session?.currentTask) { - // Check for pending tasks and display custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - switch (clerk.session.currentTask.key) { - case 'choose-organization': { - document.getElementById('app').innerHTML = ` -
- ` - - const taskDiv = document.getElementById('task') - - clerk.mountTaskChooseOrganization(taskDiv) - } +
+
+
+ + + 1. Initiate the sign-in process by collecting the user's email address and password. + 1. Prepare the first factor verification. + 1. Attempt to complete the first factor verification. + 1. Prepare the second factor verification. (This is where MFA comes into play.) + 1. Attempt to complete the second factor verification. + 1. If the verification is successful, set the newly created session as the active session. + + > [!TIP] + > For this example to work, the user must have MFA enabled on their account. You need to add the ability for your users to manage their MFA settings. See the [manage SMS-based MFA](/docs/guides/development/custom-flows/account-updates/manage-sms-based-mfa) or the [manage TOTP-based MFA](/docs/guides/development/custom-flows/account-updates/manage-totp-based-mfa) guide, depending on your needs. + + + + ```tsx {{ filename: 'app/sign-in/page.tsx', collapsible: true }} + 'use client' + + import * as React from 'react' + import { useSignIn } from '@clerk/nextjs' + import { useRouter } from 'next/navigation' + + export default function SignInForm() { + const { signIn, errors, fetchStatus } = useSignIn() + const router = useRouter() + + const handleSubmit = async (formData: FormData) => { + const emailAddress = formData.get('email') as string + const password = formData.get('password') as string + + await signIn.password({ + emailAddress, + password, + }) } - } else { - // Handle the sign-in form - document.getElementById('sign-in-form').addEventListener('submit', async (e) => { - e.preventDefault() - - const formData = new FormData(e.target) - const emailAddress = formData.get('email') - const password = formData.get('password') - - try { - // Start the sign-in process - await clerk.client.signIn.create({ - identifier: emailAddress, - password, - }) - // Hide sign-in form - document.getElementById('sign-in').setAttribute('hidden', '') - // Show verification form - document.getElementById('verifying').removeAttribute('hidden') - } catch (error) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(error) - } - }) - - // Handle the verification form - document.getElementById('verifying').addEventListener('submit', async (e) => { - const formData = new FormData(e.target) - const totp = formData.get('totp') - const backupCode = formData.get('backupCode') - - try { - const useBackupCode = backupCode ? true : false - const code = backupCode ? backupCode : totp - - // Attempt the TOTP or backup code verification - const signInAttempt = await clerk.client.signIn.attemptSecondFactor({ - strategy: useBackupCode ? 'backup_code' : 'totp', - code: code, - }) - - // If verification was completed, set the session to active - // and redirect the user - if (signInAttempt.status === 'complete') { - await clerk.setActive({ session: signInAttempt.createdSessionId }) + const handleSubmitTOTP = async (formData: FormData) => { + const code = formData.get('code') as string + const useBackupCode = formData.get('useBackupCode') === 'on' - location.reload() - } else { - // If the status is not complete, check why. User may need to - // complete further steps. - console.error(signInAttempt) - } - } catch (error) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(error) + if (useBackupCode) { + await signIn.mfa.verifyBackupCode({ code }) + } else { + await signIn.mfa.verifyTOTP({ code }) } - }) - } - ``` - - - - - ### Before you start - - Install `expo-checkbox` for the UI. - - - ```bash {{ filename: 'terminal' }} - npm install expo-checkbox - ``` - - ```bash {{ filename: 'terminal' }} - yarn add expo-checkbox - ``` - - ```bash {{ filename: 'terminal' }} - pnpm add expo-checkbox - ``` - - ```bash {{ filename: 'terminal' }} - bun add expo-checkbox - ``` - - - ### Build the flow - - 1. Create the `(auth)` route group. This groups your sign-up and sign-in pages. - 1. In the `(auth)` group, create a `_layout.tsx` file with the following code. The [`useAuth()`](/docs/reference/hooks/use-auth) hook is used to access the user's authentication state. If the user's already signed in, they'll be redirected to the home page. - - ```tsx {{ filename: 'app/(auth)/_layout.tsx' }} - import { Redirect, Stack } from 'expo-router' - import { useAuth } from '@clerk/clerk-expo' - export default function AuthenticatedLayout() { - const { isSignedIn } = useAuth() - - if (isSignedIn) { - return - } - - return - } - ``` - - In the `(auth)` group, create a `sign-in.tsx` file with the following code. The [`useSignIn()`](/docs/reference/hooks/use-sign-in) hook is used to create a sign-in flow. The user can sign in using their email and password and will be prompted to verify their account with a code from their authenticator app or with a backup code. - - ```tsx {{ filename: 'app/(auth)/sign-in.tsx', collapsible: true }} - import React from 'react' - import { useSignIn } from '@clerk/clerk-expo' - import { useRouter } from 'expo-router' - import { Text, TextInput, Button, View } from 'react-native' - import Checkbox from 'expo-checkbox' - - export default function Page() { - const { signIn, setActive, isLoaded } = useSignIn() - - const [email, setEmail] = React.useState('') - const [password, setPassword] = React.useState('') - const [code, setCode] = React.useState('') - const [useBackupCode, setUseBackupCode] = React.useState(false) - const [displayTOTP, setDisplayTOTP] = React.useState(false) - const router = useRouter() - - // Handle user submitting email and pass and swapping to TOTP form - const handleFirstStage = async () => { - if (!isLoaded) return - - // Attempt to sign in using the email and password provided - try { - const attemptFirstFactor = await signIn.create({ - identifier: email, - password, - }) - - // If the sign-in was successful, set the session to active - // and redirect the user - if (attemptFirstFactor.status === 'complete') { - await setActive({ - session: attemptFirstFactor.createdSessionId, - navigate: async ({ session }) => { - if (session?.currentTask) { - // Check for tasks and navigate to custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - console.log(session?.currentTask) - return - } - - await router.push('/') + if (signIn.status === 'complete') { + await signIn.finalize({ + navigate: () => { + router.push('/') }, }) - } else if (attemptFirstFactor.status === 'needs_second_factor') { - // If the sign-in requires a second factor, display the TOTP form - setDisplayTOTP(true) - } else { - // If the sign-in failed, check why. User might need to - // complete further steps. - console.error(JSON.stringify(attemptFirstFactor, null, 2)) } - } catch (err) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(JSON.stringify(err, null, 2)) } - } - // Handle the submission of the TOTP or backup code - const onPressTOTP = React.useCallback(async () => { - if (!isLoaded) return - - try { - // Attempt the TOTP or backup code verification - const attemptSecondFactor = await signIn.attemptSecondFactor({ - strategy: useBackupCode ? 'backup_code' : 'totp', - code: code, - }) - - // If verification was completed, set the session to active - // and redirect the user - if (attemptSecondFactor.status === 'complete') { - await setActive({ - session: attemptSecondFactor.createdSessionId, - navigate: async ({ session }) => { - if (session?.currentTask) { - // Check for tasks and navigate to custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - console.log(session?.currentTask) - return - } - - await router.push('/') - }, - }) - } else { - // If the status is not complete, check why. User may need to - // complete further steps. - console.error(JSON.stringify(attemptSecondFactor, null, 2)) - } - } catch (err) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(JSON.stringify(err, null, 2)) + if (signIn.status === 'needs_second_factor') { + return ( +
+

Verify your account

+
+
+ + + {errors.fields.code &&

{errors.fields.code.message}

} +
+
+ +
+ +
+
+ ) } - }, [isLoaded, email, password, code, useBackupCode]) - if (displayTOTP) { return ( - - Verify your account - - - setCode(c)} - /> - - - Check if this code is a backup code - setUseBackupCode((prev) => !prev)} /> - - + + ) } + ``` +
+ + + ```swift {{ filename: 'MFASignInView.swift', collapsible: true }} + import SwiftUI + import Clerk - return ( - - Sign in - - setEmail(email)} - /> - - - - setPassword(password)} - /> - - - - - - ) - } + if (signUp.status === 'complete' || isSignedIn) { + return null + } - // Display the initial sign-up form to capture the email and password - return ( - <> -

Sign up

-
-
- - setEmailAddress(e.target.value)} - /> -
-
- - setPassword(e.target.value)} - /> -
-
- -
-
- - ) - } - ``` -
- - - - ```html {{ filename: 'index.html', collapsible: true }} - - - - - - Clerk + JavaScript App - - -
- -
-

Sign up

-
- - - - - + if ( + signUp.status === 'missing_requirements' && + signUp.unverifiedFields.includes('email_address') + ) { + return ( + + +
-
+ ) + } - - - - - + ) + } ``` - - ```js {{ filename: 'main.js', collapsible: true }} - import { Clerk } from '@clerk/clerk-js' - - const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY - - const clerk = new Clerk(pubKey) - await clerk.load() - - if (clerk.isSignedIn) { - // Mount user button component - document.getElementById('signed-in').innerHTML = ` -
- ` - - const userbuttonDiv = document.getElementById('user-button') - - clerk.mountUserButton(userbuttonDiv) - } else { - // Handle the sign-up form - document.getElementById('sign-up-form').addEventListener('submit', async (e) => { - e.preventDefault() - - const formData = new FormData(e.target) - const emailAddress = formData.get('email') - const password = formData.get('password') - - try { - // Start the sign-up process using the email and password provided - await clerk.client.signUp.create({ emailAddress, password }) - await clerk.client.signUp.prepareEmailAddressVerification() - // Hide sign-up form - document.getElementById('sign-up').setAttribute('hidden', '') - // Show verification form - document.getElementById('verifying').removeAttribute('hidden') - } catch (error) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(error) +
+
+
+ + + 1. Initiate the sign-up process by collecting the user's email address and password. + 1. Prepare the email address verification, which sends a one-time code to the given address. + 1. Collect the one-time code and attempt to complete the email address verification with it. + 1. If the email address verification is successful, set the newly created session as the active session. + + + + ```swift {{ filename: 'EmailPasswordSignUpView.swift', collapsible: true }} + import SwiftUI + import Clerk + + struct EmailPasswordSignUpView: View { + @State private var email = "" + @State private var password = "" + @State private var code = "" + @State private var isVerifying = false + + var body: some View { + if isVerifying { + // Display the verification form to capture the OTP code + TextField("Enter your verification code", text: $code) + Button("Verify") { + Task { await verify(code: code) } + } + } else { + // Display the initial sign-up form to capture the email and password + TextField("Enter email address", text: $email) + SecureField("Enter password", text: $password) + Button("Next") { + Task { await submit(email: email, password: password) } + } } - }) - - // Handle the verification form - document.getElementById('verifying').addEventListener('submit', async (e) => { - const formData = new FormData(e.target) - const code = formData.get('code') - - try { - // Use the code the user provided to attempt verification - const signUpAttempt = await clerk.client.signUp.attemptEmailAddressVerification({ - code, - }) - - // Now that the user is created, set the session to active. - await clerk.setActive({ session: signUpAttempt.createdSessionId }) - } catch (error) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(error) } - }) - } - ``` - - - - - 1. Create the `(auth)` route group. This groups your sign-up and sign-in pages. - 1. In the `(auth)` group, create a `_layout.tsx` file with the following code. The [`useAuth()`](/docs/reference/hooks/use-auth) hook is used to access the user's authentication state. If the user's already signed in, they'll be redirected to the home page. - - ```tsx {{ filename: 'app/(auth)/_layout.tsx' }} - import { Redirect, Stack } from 'expo-router' - import { useAuth } from '@clerk/clerk-expo' - - export default function GuestLayout() { - const { isSignedIn } = useAuth() - - if (isSignedIn) { - return - } - - return - } - ``` - - In the `(auth)` group, create a `sign-up.tsx` file with the following code. The [`useSignUp()`](/docs/reference/hooks/use-sign-up) hook is used to create a sign-up flow. The user can sign up using their email and password and will receive an email verification code to confirm their email. - - ```tsx {{ filename: 'app/(auth)/sign-up.tsx', collapsible: true }} - import * as React from 'react' - import { Text, TextInput, Button, View } from 'react-native' - import { useSignUp } from '@clerk/clerk-expo' - import { Link, useRouter } from 'expo-router' - - export default function Page() { - const { isLoaded, signUp, setActive } = useSignUp() - const router = useRouter() - - const [emailAddress, setEmailAddress] = React.useState('') - const [password, setPassword] = React.useState('') - const [pendingVerification, setPendingVerification] = React.useState(false) - const [code, setCode] = React.useState('') - - // Handle submission of sign-up form - const onSignUpPress = async () => { - if (!isLoaded) return - - // Start sign-up process using email and password provided - try { - await signUp.create({ - emailAddress, - password, - }) - - // Send user an email with verification code - await signUp.prepareEmailAddressVerification({ strategy: 'email_code' }) - - // Set 'pendingVerification' to true to display second form - // and capture OTP code - setPendingVerification(true) - } catch (err) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(JSON.stringify(err, null, 2)) - } } - // Handle submission of verification form - const onVerifyPress = async () => { - if (!isLoaded) return + extension EmailPasswordSignUpView { - try { - // Use the code the user provided to attempt verification - const signUpAttempt = await signUp.attemptEmailAddressVerification({ - code, - }) - - // If verification was completed, set the session to active - // and redirect the user - if (signUpAttempt.status === 'complete') { - await setActive({ - session: signUpAttempt.createdSessionId, - navigate: async ({ session }) => { - if (session?.currentTask) { - // Check for tasks and navigate to custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - console.log(session?.currentTask) - return - } - - router.replace('/') - }, - }) - } else { - // If the status is not complete, check why. User may need to - // complete further steps. - console.error(JSON.stringify(signUpAttempt, null, 2)) - } - } catch (err) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(JSON.stringify(err, null, 2)) - } - } + func submit(email: String, password: String) async { + do { + // Start the sign-up process using the email and password provided + let signUp = try await SignUp.create(strategy: .standard(emailAddress: email, password: password)) - if (pendingVerification) { - return ( - <> - Verify your email - setCode(code)} - /> - - - ) - } - ``` - - - - - ```html {{ filename: 'index.html', collapsible: true }} - - - - - - Clerk + JavaScript App - - -
- -
-

Sign in

-
- - - - - -
-
- - - - + ) + } ``` +
+
+
- ```js {{ filename: 'main.js', collapsible: true }} - import { Clerk } from '@clerk/clerk-js' - - const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY - - const clerk = new Clerk(pubKey) - await clerk.load() - - if (clerk.isSignedIn) { - // Mount user button component - document.getElementById('signed-in').innerHTML = ` -
- ` + + 1. Initiate the sign-in process by creating a `SignIn` using the email address and password provided. + 1. If the attempt is successful, set the newly created session as the active session. - const userbuttonDiv = document.getElementById('user-button') + + + ```swift {{ filename: 'EmailPasswordSignInView.swift', collapsible: true }} + import SwiftUI + import Clerk - clerk.mountUserButton(userbuttonDiv) - } else if (clerk.session?.currentTask) { - // Check for pending tasks and display custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - switch (clerk.session.currentTask.key) { - case 'choose-organization': { - document.getElementById('app').innerHTML = ` -
- ` + struct EmailPasswordSignInView: View { + @State private var email = "" + @State private var password = "" - const taskDiv = document.getElementById('task') - - clerk.mountTaskChooseOrganization(taskDiv) + var body: some View { + TextField("Enter email address", text: $email) + SecureField("Enter password", text: $password) + Button("Sign In") { + Task { await submit(email: email, password: password) } } - } - } else { - // Handle the sign-in form - document.getElementById('sign-in-form').addEventListener('submit', async (e) => { - e.preventDefault() - - const formData = new FormData(e.target) - const emailAddress = formData.get('email') - const password = formData.get('password') - - try { - // Start the sign-in process - const signInAttempt = await clerk.client.signIn.create({ - identifier: emailAddress, - password, - }) - - // If the sign-in is complete, set the user as active - if (signInAttempt.status === 'complete') { - await clerk.setActive({ session: signInAttempt.createdSessionId }) - - location.reload() - } else { - // If the status is not complete, check why. User may need to - // complete further steps. - console.error(JSON.stringify(signInAttempt, null, 2)) - } - } catch (error) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(error) } - }) } - ``` - -
- - - In the `(auth)` group, create a `sign-in.tsx` file with the following code. The [`useSignIn()`](/docs/reference/hooks/use-sign-in) hook is used to create a sign-in flow. The user can sign in using email address and password, or navigate to the sign-up page. - - ```tsx {{ filename: 'app/(auth)/sign-in.tsx', collapsible: true }} - import { useSignIn } from '@clerk/clerk-expo' - import { Link, useRouter } from 'expo-router' - import { Text, TextInput, Button, View } from 'react-native' - import React from 'react' - - export default function Page() { - const { signIn, setActive, isLoaded } = useSignIn() - const router = useRouter() - - const [emailAddress, setEmailAddress] = React.useState('') - const [password, setPassword] = React.useState('') - // Handle the submission of the sign-in form - const onSignInPress = React.useCallback(async () => { - if (!isLoaded) return + extension EmailPasswordSignInView { - // Start the sign-in process using the email and password provided - try { - const signInAttempt = await signIn.create({ - identifier: emailAddress, - password, - }) + func submit(email: String, password: String) async { + do { + // Start the sign-in process using the email and password provided + let signIn = try await SignIn.create(strategy: .identifier(email, password: password)) - // If sign-in process is complete, set the created session as active - // and redirect the user - if (signInAttempt.status === 'complete') { - await setActive({ - session: signInAttempt.createdSessionId, - navigate: async ({ session }) => { - if (session?.currentTask) { - // Check for tasks and navigate to custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - console.log(session?.currentTask) - return - } - - router.replace('/') - }, - }) - } else { - // If the status is not complete, check why. User may need to - // complete further steps. - console.error(JSON.stringify(signInAttempt, null, 2)) + switch signIn.status { + case .complete: + // If sign-in process is complete, navigate the user as needed. + dump(Clerk.shared.session) + default: + // If the status is not complete, check why. User may need to + // complete further steps. + dump(signIn.status) + } + } catch { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + dump(error) } - } catch (err) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(JSON.stringify(err, null, 2)) - } - }, [isLoaded, emailAddress, password]) - - return ( - - Sign in - setEmailAddress(emailAddress)} - /> - setPassword(password)} - /> - - - - ) - } + if (signUp.status === 'complete' || isSignedIn) { + return null + } - return ( - <> -

Sign up

-
- - setPhone(e.target.value)} - /> - -
- - ) - } - ``` -
- - - - ```html {{ filename: 'index.html', collapsible: true }} - - - - - - Clerk + JavaScript App - - -
- -
- -
-

Sign up

-
- - - + if ( + signUp.status === 'missing_requirements' && + signUp.unverifiedFields.includes('phone_number') + ) { + return ( + + +
-
+ ) + } - - - - - + ) + } ``` +
+
- ```js {{ filename: 'main.js', collapsible: true }} - import { Clerk } from '@clerk/clerk-js' - - const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY - - const clerk = new Clerk(pubKey) - await clerk.load() - - if (clerk.isSignedIn) { - // Mount user button component - document.getElementById('signed-in').innerHTML = ` -
- ` - - const userbuttonDiv = document.getElementById('user-button') - - clerk.mountUserButton(userbuttonDiv) - } else if (clerk.session?.currentTask) { - // Check for pending tasks and display custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - switch (clerk.session.currentTask.key) { - case 'choose-organization': { - document.getElementById('app').innerHTML = ` -
- ` - - const taskDiv = document.getElementById('task') + To create a sign-up flow for email OTP, use the [`signUp.emailCode.sendCode()`](/docs/reference/javascript/sign-up-future) and [`signUp.emailCode.verifyCode()`](/docs/reference/javascript/sign-up-future) methods. These methods work the same way as their phone number counterparts do in the previous example. You can find all available methods in the [`SignUpFuture`](/docs/reference/javascript/sign-up-future) object documentation. +
- clerk.mountTaskChooseOrganization(taskDiv) - } - } - } else { - // Handle the sign-up form - document.getElementById('sign-up-form').addEventListener('submit', async (e) => { - e.preventDefault() - - const formData = new FormData(e.target) - const phoneNumber = formData.get('phone') - - try { - // Start the sign-up process using the phone number method - await clerk.client.signUp.create({ phoneNumber }) - await clerk.client.signUp.preparePhoneNumberVerification() - // Hide sign-up form - document.getElementById('sign-up').setAttribute('hidden', '') - // Show verification form - document.getElementById('verifying').removeAttribute('hidden') - } catch (error) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(error) - } - }) + + 1. Initiate the sign-up process by collecting the user's identifier, which for this example is a phone number. + 1. Prepare the verification, which sends a one-time code to the given identifier. + 1. Attempt to complete the verification with the code the user provides. + 1. If the verification is successful, set the newly created session as the active session. - // Handle the verification form - document.getElementById('verifying').addEventListener('submit', async (e) => { - const formData = new FormData(e.target) - const code = formData.get('code') + + + ```swift {{ filename: 'SMSOTPSignUpView.swift', collapsible: true }} + import SwiftUI + import Clerk - try { - // Verify the phone number - const verify = await clerk.client.signUp.attemptPhoneNumberVerification({ - code, - }) - - // Now that the user is created, set the session to active. - await clerk.setActive({ session: verify.createdSessionId }) - } catch (error) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(error) - } - }) - } - ``` - - - - - ```swift {{ filename: 'SMSOTPSignUpView.swift', collapsible: true }} - import SwiftUI - import Clerk - - struct SMSOTPSignUpView: View { + struct SMSOTPSignUpView: View { @State private var phoneNumber = "" @State private var code = "" @State private var isVerifying = false var body: some View { - if isVerifying { + if isVerifying { TextField("Enter your verification code", text: $code) Button("Verify") { - Task { await verify(code: code) } + Task { await verify(code: code) } } - } else { + } else { TextField("Enter phone number", text: $phoneNumber) Button("Continue") { - Task { await submit(phoneNumber: phoneNumber) } + Task { await submit(phoneNumber: phoneNumber) } } - } + } + } } - } - extension SMSOTPSignUpView { + extension SMSOTPSignUpView { func submit(phoneNumber: String) async { - do { + do { // Start the sign-up process using the phone number method. let signUp = try await SignUp.create(strategy: .standard(phoneNumber: phoneNumber)) @@ -294,15 +156,15 @@ This guide will walk you through how to build a custom SMS OTP sign-up and sign- // Set isVerifying to true to display second form and capture the OTP code. isVerifying = true - } catch { + } catch { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling dump(error) - } + } } func verify(code: String) async { - do { + do { // Access the in progress sign up stored on the client object. guard let inProgressSignUp = Clerk.shared.client?.signUp else { return } @@ -311,89 +173,89 @@ This guide will walk you through how to build a custom SMS OTP sign-up and sign- switch signUp.status { case .complete: - // If verification was completed, navigate the user as needed. - dump(Clerk.shared.session) + // If verification was completed, navigate the user as needed. + dump(Clerk.shared.session) default: - // If the status is not complete, check why. User may need to - // complete further steps. - dump(signUp.status) + // If the status is not complete, check why. User may need to + // complete further steps. + dump(signUp.status) } - } catch { + } catch { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling dump(error) - } + } + } } - } - ``` - - - - ```kotlin {{ filename: 'SMSOTPSignUpViewModel.kt', collapsible: true }} - import androidx.lifecycle.ViewModel - import androidx.lifecycle.viewModelScope - import com.clerk.api.Clerk - import com.clerk.api.network.serialization.flatMap - import com.clerk.api.network.serialization.onFailure - import com.clerk.api.network.serialization.onSuccess - import com.clerk.api.signup.SignUp - import com.clerk.api.signup.attemptVerification - import com.clerk.api.signup.prepareVerification - import kotlinx.coroutines.flow.MutableStateFlow - import kotlinx.coroutines.flow.asStateFlow - import kotlinx.coroutines.flow.combine - import kotlinx.coroutines.flow.launchIn - import kotlinx.coroutines.launch - - class SMSOTPSignUpViewModel : ViewModel() { - - private val _uiState = MutableStateFlow(UiState.Unverified) - val uiState = _uiState.asStateFlow() - - init { + ``` + + + + ```kotlin {{ filename: 'SMSOTPSignUpViewModel.kt', collapsible: true }} + import androidx.lifecycle.ViewModel + import androidx.lifecycle.viewModelScope + import com.clerk.api.Clerk + import com.clerk.api.network.serialization.flatMap + import com.clerk.api.network.serialization.onFailure + import com.clerk.api.network.serialization.onSuccess + import com.clerk.api.signup.SignUp + import com.clerk.api.signup.attemptVerification + import com.clerk.api.signup.prepareVerification + import kotlinx.coroutines.flow.MutableStateFlow + import kotlinx.coroutines.flow.asStateFlow + import kotlinx.coroutines.flow.combine + import kotlinx.coroutines.flow.launchIn + import kotlinx.coroutines.launch + + class SMSOTPSignUpViewModel : ViewModel() { + + private val _uiState = MutableStateFlow(UiState.Unverified) + val uiState = _uiState.asStateFlow() + + init { combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user -> - _uiState.value = + _uiState.value = when { - !isInitialized -> UiState.Loading - user == null -> UiState.Unverified - else -> UiState.Verified + !isInitialized -> UiState.Loading + user == null -> UiState.Unverified + else -> UiState.Verified } } .launchIn(viewModelScope) - } + } - fun submit(phoneNumber: String) { + fun submit(phoneNumber: String) { viewModelScope.launch { SignUp.create(SignUp.CreateParams.Standard(phoneNumber = phoneNumber)) - .flatMap { it.prepareVerification(SignUp.PrepareVerificationParams.Strategy.PhoneCode()) } - .onSuccess { _uiState.value = UiState.Verifying } - .onFailure { + .flatMap { it.prepareVerification(SignUp.PrepareVerificationParams.Strategy.PhoneCode()) } + .onSuccess { _uiState.value = UiState.Verifying } + .onFailure { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling - } + } + } } - } - fun verify(code: String) { + fun verify(code: String) { val inProgressSignUp = Clerk.signUp ?: return viewModelScope.launch { inProgressSignUp - .attemptVerification(SignUp.AttemptVerificationParams.PhoneCode(code)) - .onSuccess { + .attemptVerification(SignUp.AttemptVerificationParams.PhoneCode(code)) + .onSuccess { if (it.status == SignUp.Status.COMPLETE) { - _uiState.value = UiState.Verified + _uiState.value = UiState.Verified } else { - // The user may need to complete further steps + // The user may need to complete further steps } - } - .onFailure { + } + .onFailure { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling - } + } + } } - } - sealed interface UiState { + sealed interface UiState { data object Loading : UiState data object Unverified : UiState @@ -402,378 +264,211 @@ This guide will walk you through how to build a custom SMS OTP sign-up and sign- data object Verified : UiState } - } - ``` - - ```kotlin {{ filename: 'SMSOTPSignUpActivity.kt', collapsible: true }} - import android.os.Bundle - import androidx.activity.ComponentActivity - import androidx.activity.compose.setContent - import androidx.activity.viewModels - import androidx.compose.foundation.layout.Arrangement - import androidx.compose.foundation.layout.Box - import androidx.compose.foundation.layout.Column - import androidx.compose.foundation.layout.fillMaxSize - import androidx.compose.material3.Button - import androidx.compose.material3.CircularProgressIndicator - import androidx.compose.material3.Text - import androidx.compose.material3.TextField - import androidx.compose.runtime.Composable - import androidx.compose.runtime.getValue - import androidx.compose.runtime.mutableStateOf - import androidx.compose.runtime.remember - import androidx.compose.runtime.setValue - import androidx.compose.ui.Alignment - import androidx.compose.ui.Modifier - import androidx.compose.ui.unit.dp - import androidx.lifecycle.compose.collectAsStateWithLifecycle - - class SMSOTPSignUpActivity : ComponentActivity() { - val viewModel: SMSOTPSignUpViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - val state by viewModel.uiState.collectAsStateWithLifecycle() - SMSOTPSignUpView(state, viewModel::submit, viewModel::verify) - } } - } - - @Composable - fun SMSOTPSignUpView( - state: SMSOTPSignUpViewModel.UiState, - onSubmit: (String) -> Unit, - onVerify: (String) -> Unit, - ) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - when (state) { - SMSOTPSignUpViewModel.UiState.Unverified -> { - InputContent( - placeholder = "Enter your phone number", - buttonText = "Continue", - onClick = onSubmit, - ) - } - SMSOTPSignUpViewModel.UiState.Verified -> { - Text("Verified") - } - SMSOTPSignUpViewModel.UiState.Verifying -> { - InputContent( - placeholder = "Enter your verification code", - buttonText = "Verify", - onClick = onVerify, - ) - } + ``` - SMSOTPSignUpViewModel.UiState.Loading -> { - CircularProgressIndicator() + ```kotlin {{ filename: 'SMSOTPSignUpActivity.kt', collapsible: true }} + import android.os.Bundle + import androidx.activity.ComponentActivity + import androidx.activity.compose.setContent + import androidx.activity.viewModels + import androidx.compose.foundation.layout.Arrangement + import androidx.compose.foundation.layout.Box + import androidx.compose.foundation.layout.Column + import androidx.compose.foundation.layout.fillMaxSize + import androidx.compose.material3.Button + import androidx.compose.material3.CircularProgressIndicator + import androidx.compose.material3.Text + import androidx.compose.material3.TextField + import androidx.compose.runtime.Composable + import androidx.compose.runtime.getValue + import androidx.compose.runtime.mutableStateOf + import androidx.compose.runtime.remember + import androidx.compose.runtime.setValue + import androidx.compose.ui.Alignment + import androidx.compose.ui.Modifier + import androidx.compose.ui.unit.dp + import androidx.lifecycle.compose.collectAsStateWithLifecycle + + class SMSOTPSignUpActivity : ComponentActivity() { + val viewModel: SMSOTPSignUpViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + val state by viewModel.uiState.collectAsStateWithLifecycle() + SMSOTPSignUpView(state, viewModel::submit, viewModel::verify) } } } - } - - @Composable - fun InputContent(placeholder: String, buttonText: String, onClick: (String) -> Unit) { - var value by remember { mutableStateOf("") } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + + @Composable + fun SMSOTPSignUpView( + state: SMSOTPSignUpViewModel.UiState, + onSubmit: (String) -> Unit, + onVerify: (String) -> Unit, ) { - TextField(placeholder = { Text(placeholder) }, value = value, onValueChange = { value = it }) - Button(onClick = { onClick(value) }) { Text(buttonText) } + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + when (state) { + SMSOTPSignUpViewModel.UiState.Unverified -> { + InputContent( + placeholder = "Enter your phone number", + buttonText = "Continue", + onClick = onSubmit, + ) + } + SMSOTPSignUpViewModel.UiState.Verified -> { + Text("Verified") + } + SMSOTPSignUpViewModel.UiState.Verifying -> { + InputContent( + placeholder = "Enter your verification code", + buttonText = "Verify", + onClick = onVerify, + ) + } + + SMSOTPSignUpViewModel.UiState.Loading -> { + CircularProgressIndicator() + } + } + } } - } - ``` - - - To create a sign-up flow for email OTP, use the [`prepareEmailAddressVerification`](/docs/reference/javascript/sign-up#prepare-email-address-verification) and [`attemptEmailAddressVerification`](/docs/reference/javascript/sign-up#attempt-email-address-verification). These helpers work the same way as their phone number counterparts do in the previous example. You can find all available methods in the [`SignUp`](/docs/reference/javascript/sign-in) object documentation. + @Composable + fun InputContent(placeholder: String, buttonText: String, onClick: (String) -> Unit) { + var value by remember { mutableStateOf("") } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + ) { + TextField(placeholder = { Text(placeholder) }, value = value, onValueChange = { value = it }) + Button(onClick = { onClick(value) }) { Text(buttonText) } + } + } + ``` +
+
+ + To create a sign-up flow for email OTP, use the [`prepareEmailAddressVerification`](/docs/reference/javascript/sign-up#prepare-email-address-verification) and [`attemptEmailAddressVerification`](/docs/reference/javascript/sign-up#attempt-email-address-verification). These helpers work the same way as their phone number counterparts do in the previous example. You can find all available methods in the [`SignUp`](/docs/reference/javascript/sign-in) object documentation. + ## Sign-in flow To authenticate a user with an OTP, you must: - 1. Initiate the sign-in process by creating a `SignIn` using the identifier provided, which for this example is a phone number. - 1. Prepare the first factor verification. - 1. Attempt verification with the code the user provides. - 1. If the attempt is successful, set the newly created session as the active session. - - - - This example is written for Next.js App Router but it can be adapted to any React-based framework. - - ```tsx {{ filename: 'app/sign-in/[[...sign-in]]/page.tsx', collapsible: true }} - 'use client' + + 1. Initiate the sign-in process by calling the [`signIn.phoneCode.sendCode()`](/docs/reference/javascript/sign-in-future#phone-code-send-code) method using the identifier provided, which for this example is a phone number. + 1. Verify the phone code provided by the user with the [`signIn.phoneCode.verifyCode()`](/docs/reference/javascript/sign-in-future#phone-code-verify-code) method. + 1. If the attempt is successful, finalize the sign-in with the [`signIn.finalize()`](/docs/reference/javascript/sign-in-future#finalize) method to set the newly created session as the active session. - import * as React from 'react' - import { useSignIn } from '@clerk/nextjs' - import { PhoneCodeFactor, SignInFirstFactor } from '@clerk/types' - import { useRouter } from 'next/navigation' + + + This example is written for Next.js App Router but it can be adapted to any React-based framework. - export default function Page() { - const { isLoaded, signIn, setActive } = useSignIn() - const [verifying, setVerifying] = React.useState(false) - const [phone, setPhone] = React.useState('') - const [code, setCode] = React.useState('') - const router = useRouter() + ```tsx {{ filename: 'app/sign-in/page.tsx', collapsible: true }} + 'use client' - async function handleSubmit(e: React.FormEvent) { - e.preventDefault() + import * as React from 'react' + import { useSignIn } from '@clerk/nextjs' + import { useRouter } from 'next/navigation' - if (!isLoaded && !signIn) return null + export default function Page() { + const { signIn, errors, fetchStatus } = useSignIn() + const router = useRouter() - try { - // Start the sign-in process using the phone number method - const { supportedFirstFactors } = await signIn.create({ - identifier: phone, - }) - - // Filter the returned array to find the 'phone_code' entry - const isPhoneCodeFactor = (factor: SignInFirstFactor): factor is PhoneCodeFactor => { - return factor.strategy === 'phone_code' - } - const phoneCodeFactor = supportedFirstFactors?.find(isPhoneCodeFactor) - - if (phoneCodeFactor) { - // Grab the phoneNumberId - const { phoneNumberId } = phoneCodeFactor - - // Send the OTP code to the user - await signIn.prepareFirstFactor({ - strategy: 'phone_code', - phoneNumberId, - }) + async function handleSubmit(formData: FormData) { + const phoneNumber = formData.get('phoneNumber') as string - // Set verifying to true to display second form - // and capture the OTP code - setVerifying(true) - } - } catch (err) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error('Error:', JSON.stringify(err, null, 2)) + await signIn.phoneCode.sendCode({ phoneNumber }) } - } - async function handleVerification(e: React.FormEvent) { - e.preventDefault() - - if (!isLoaded && !signIn) return null - - try { - // Use the code provided by the user and attempt verification - const signInAttempt = await signIn.attemptFirstFactor({ - strategy: 'phone_code', - code, - }) - - // If verification was completed, set the session to active - // and redirect the user - if (signInAttempt.status === 'complete') { - await setActive({ - session: signInAttempt.createdSessionId, - navigate: async ({ session }) => { - if (session?.currentTask) { - // Check for tasks and navigate to custom UI to help users resolve them - // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks - console.log(session?.currentTask) - return - } + async function handleVerification(formData: FormData) { + const code = formData.get('code') as string + await signIn.phoneCode.verifyCode({ code }) + if (signIn.status === 'complete') { + await signIn.finalize({ + navigate: () => { router.push('/') }, }) - } else { - // If the status is not complete, check why. User may need to - // complete further steps. - console.error(signInAttempt) } - } catch (err) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error('Error:', JSON.stringify(err, null, 2)) } - } - if (verifying) { + if (signIn.status === 'needs_second_factor') { + return ( + <> +

Verify your phone number

+
+ + + {errors.fields.code &&

{errors.fields.code.message}

} + +
+ + ) + } + return ( <> -

Verify your phone number

-
- - setCode(e.target.value)} /> - +

Sign in

+ + + + {errors.fields.phoneNumber &&

{errors.fields.phoneNumber.message}

} +
) } - - return ( - <> -

Sign in

-
- - setPhone(e.target.value)} - /> - -
- - ) - } - ``` -
- - - - ```html {{ filename: 'index.html', collapsible: true }} - - - - - - Clerk + JavaScript App - - -
- -
-

Sign in

-
- - - -
-
- - - - - - ``` +
+
- ```js {{ filename: 'main.js', collapsible: true }} - import { Clerk } from '@clerk/clerk-js' - - const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY - - const clerk = new Clerk(pubKey) - await clerk.load() - - if (clerk.isSignedIn) { - // Mount user button component - document.getElementById('signed-in').innerHTML = ` -
- ` - - const userbuttonDiv = document.getElementById('user-button') - - clerk.mountUserButton(userbuttonDiv) - } else { - // Handle the sign-in form - document.getElementById('sign-in-form').addEventListener('submit', async (e) => { - e.preventDefault() - - const formData = new FormData(e.target) - const phone = formData.get('phone') - - try { - // Start the sign-in process using the user's identifier. - // In this case, it's their phone number. - const { supportedFirstFactors } = await clerk.client.signIn.create({ - identifier: phone, - }) - - // Find the phoneNumberId from all the available first factors for the current sign-in - const firstPhoneFactor = supportedFirstFactors.find((factor) => { - return factor.strategy === 'phone_code' - }) - - const { phoneNumberId } = firstPhoneFactor - - // Prepare first factor verification, specifying - // the phone code strategy. - await clerk.client.signIn.prepareFirstFactor({ - strategy: 'phone_code', - phoneNumberId, - }) - - // Hide sign-in form - document.getElementById('sign-in').setAttribute('hidden', '') - // Show verification form - document.getElementById('verifying').removeAttribute('hidden') - } catch (error) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(error) - } - }) - - // Handle the verification form - document.getElementById('verifying').addEventListener('submit', async (e) => { - const formData = new FormData(e.target) - const code = formData.get('code') - - try { - // Verify the phone number - const verify = await clerk.client.signIn.attemptFirstFactor({ - strategy: 'phone_code', - code, - }) + To create a sign-in flow for email OTP, use the [`signIn.emailCode.sendCode()`](/docs/reference/javascript/sign-in-future#email-code-send-code) and [`signIn.emailCode.verifyCode()`](/docs/reference/javascript/sign-in-future#email-code-verify-code) methods. These methods work the same way as their phone number counterparts do in the previous example. You can find all available methods in the [`SignInFuture`](/docs/reference/javascript/sign-in-future) object documentation. +
- // Now that the user is created, set the session to active. - await clerk.setActive({ session: verify.createdSessionId }) - } catch (error) { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling - console.error(error) - } - }) - } - ``` - -
+ + 1. Initiate the sign-in process by creating a `SignIn` using the identifier provided, which for this example is a phone number. + 1. Prepare the first factor verification. + 1. Attempt verification with the code the user provides. + 1. If the attempt is successful, set the newly created session as the active session. - - ```swift {{ filename: 'SMSOTPSignInView.swift', collapsible: true }} - import SwiftUI - import Clerk + + + ```swift {{ filename: 'SMSOTPSignInView.swift', collapsible: true }} + import SwiftUI + import Clerk - struct SMSOTPSignInView: View { + struct SMSOTPSignInView: View { @State private var phoneNumber = "" @State private var code = "" @State private var isVerifying = false var body: some View { - if isVerifying { + if isVerifying { TextField("Enter your verification code", text: $code) Button("Verify") { - Task { await verify(code: code) } + Task { await verify(code: code) } } - } else { + } else { TextField("Enter phone number", text: $phoneNumber) Button("Continue") { - Task { await submit(phoneNumber: phoneNumber) } + Task { await submit(phoneNumber: phoneNumber) } } - } + } + } } - } - extension SMSOTPSignInView { + extension SMSOTPSignInView { func submit(phoneNumber: String) async { - do { + do { // Start the sign-in process using the phone number method. let signIn = try await SignIn.create(strategy: .identifier(phoneNumber)) @@ -783,15 +478,15 @@ This guide will walk you through how to build a custom SMS OTP sign-up and sign- // Set isVerifying to true to display second form // and capture the OTP code. isVerifying = true - } catch { + } catch { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling dump(error) - } + } } func verify(code: String) async { - do { + do { // Access the in progress sign in stored on the client object. guard let inProgressSignIn = Clerk.shared.client?.signIn else { return } @@ -800,183 +495,184 @@ This guide will walk you through how to build a custom SMS OTP sign-up and sign- switch signIn.status { case .complete: - // If verification was completed, navigate the user as needed. - dump(Clerk.shared.session) + // If verification was completed, navigate the user as needed. + dump(Clerk.shared.session) default: - // If the status is not complete, check why. User may need to - // complete further steps. - dump(signIn.status) + // If the status is not complete, check why. User may need to + // complete further steps. + dump(signIn.status) } - } catch { + } catch { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling dump(error) - } + } + } } - } - ``` - - - - ```kotlin {{ filename: 'SMSOTPSignInViewModel.kt', collapsible: true }} - import androidx.lifecycle.ViewModel - import androidx.lifecycle.viewModelScope - import com.clerk.api.Clerk - import com.clerk.api.network.serialization.flatMap - import com.clerk.api.network.serialization.onFailure - import com.clerk.api.network.serialization.onSuccess - import com.clerk.api.signin.SignIn - import com.clerk.api.signin.attemptFirstFactor - import com.clerk.api.signin.prepareFirstFactor - import kotlinx.coroutines.flow.MutableStateFlow - import kotlinx.coroutines.flow.asStateFlow - import kotlinx.coroutines.flow.combine - import kotlinx.coroutines.flow.launchIn - import kotlinx.coroutines.launch - - class SMSOTPSignInViewModel : ViewModel() { + ``` + + + + ```kotlin {{ filename: 'SMSOTPSignInViewModel.kt', collapsible: true }} + import androidx.lifecycle.ViewModel + import androidx.lifecycle.viewModelScope + import com.clerk.api.Clerk + import com.clerk.api.network.serialization.flatMap + import com.clerk.api.network.serialization.onFailure + import com.clerk.api.network.serialization.onSuccess + import com.clerk.api.signin.SignIn + import com.clerk.api.signin.attemptFirstFactor + import com.clerk.api.signin.prepareFirstFactor + import kotlinx.coroutines.flow.MutableStateFlow + import kotlinx.coroutines.flow.asStateFlow + import kotlinx.coroutines.flow.combine + import kotlinx.coroutines.flow.launchIn + import kotlinx.coroutines.launch + + class SMSOTPSignInViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState.Unverified) val uiState = _uiState.asStateFlow() init { - combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user -> - _uiState.value = + combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user -> + _uiState.value = when { - !isInitialized -> UiState.Loading - user == null -> UiState.Unverified - else -> UiState.Verified + !isInitialized -> UiState.Loading + user == null -> UiState.Unverified + else -> UiState.Verified } } .launchIn(viewModelScope) } fun submit(phoneNumber: String) { - viewModelScope.launch { + viewModelScope.launch { SignIn.create(SignIn.CreateParams.Strategy.PhoneCode(phoneNumber)).flatMap { - it + it .prepareFirstFactor(SignIn.PrepareFirstFactorParams.PhoneCode()) .onSuccess { _uiState.value = UiState.Verifying } .onFailure { - // See https://clerk.com/docs/guides/development/custom-flows/error-handling - // for more info on error handling + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling } } - } + } } fun verify(code: String) { - val inProgressSignIn = Clerk.signIn ?: return - viewModelScope.launch { + val inProgressSignIn = Clerk.signIn ?: return + viewModelScope.launch { inProgressSignIn - .attemptFirstFactor(SignIn.AttemptFirstFactorParams.PhoneCode(code)) - .onSuccess { + .attemptFirstFactor(SignIn.AttemptFirstFactorParams.PhoneCode(code)) + .onSuccess { if (it.status == SignIn.Status.COMPLETE) { - _uiState.value = UiState.Verified + _uiState.value = UiState.Verified } else { - // The user may need to complete further steps + // The user may need to complete further steps } - } - .onFailure { + } + .onFailure { // See https://clerk.com/docs/guides/development/custom-flows/error-handling // for more info on error handling - } - } + } + } } sealed interface UiState { - data object Loading : UiState + data object Loading : UiState - data object Unverified : UiState + data object Unverified : UiState - data object Verifying : UiState + data object Verifying : UiState - data object Verified : UiState + data object Verified : UiState } - } - ``` - - ```kotlin {{ filename: 'SMSOTPSignInActivity.kt', collapsible: true }} - import android.os.Bundle - import androidx.activity.ComponentActivity - import androidx.activity.compose.setContent - import androidx.activity.viewModels - import androidx.compose.foundation.layout.Arrangement - import androidx.compose.foundation.layout.Box - import androidx.compose.foundation.layout.Column - import androidx.compose.foundation.layout.fillMaxSize - import androidx.compose.material3.Button - import androidx.compose.material3.CircularProgressIndicator - import androidx.compose.material3.Text - import androidx.compose.material3.TextField - import androidx.compose.runtime.Composable - import androidx.compose.runtime.getValue - import androidx.compose.runtime.mutableStateOf - import androidx.compose.runtime.remember - import androidx.compose.runtime.setValue - import androidx.compose.ui.Alignment - import androidx.compose.ui.Modifier - import androidx.compose.ui.unit.dp - import androidx.lifecycle.compose.collectAsStateWithLifecycle - - class SMSOTPSignInActivity : ComponentActivity() { - val viewModel: SMSOTPSignInViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - val state by viewModel.uiState.collectAsStateWithLifecycle() - SMSOTPSignInView(state, viewModel::submit, viewModel::verify) - } } - } - - @Composable - fun SMSOTPSignInView( - state: SMSOTPSignInViewModel.UiState, - onSubmit: (String) -> Unit, - onVerify: (String) -> Unit, - ) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - when (state) { - SMSOTPSignInViewModel.UiState.Unverified -> { - InputContent( - placeholder = "Enter your phone number", - buttonText = "Continue", - onClick = onSubmit, - ) - } - SMSOTPSignInViewModel.UiState.Verified -> { - Text("Verified") - } - SMSOTPSignInViewModel.UiState.Verifying -> { - InputContent( - placeholder = "Enter your verification code", - buttonText = "Verify", - onClick = onVerify, - ) - } + ``` - SMSOTPSignInViewModel.UiState.Loading -> { - CircularProgressIndicator() + ```kotlin {{ filename: 'SMSOTPSignInActivity.kt', collapsible: true }} + import android.os.Bundle + import androidx.activity.ComponentActivity + import androidx.activity.compose.setContent + import androidx.activity.viewModels + import androidx.compose.foundation.layout.Arrangement + import androidx.compose.foundation.layout.Box + import androidx.compose.foundation.layout.Column + import androidx.compose.foundation.layout.fillMaxSize + import androidx.compose.material3.Button + import androidx.compose.material3.CircularProgressIndicator + import androidx.compose.material3.Text + import androidx.compose.material3.TextField + import androidx.compose.runtime.Composable + import androidx.compose.runtime.getValue + import androidx.compose.runtime.mutableStateOf + import androidx.compose.runtime.remember + import androidx.compose.runtime.setValue + import androidx.compose.ui.Alignment + import androidx.compose.ui.Modifier + import androidx.compose.ui.unit.dp + import androidx.lifecycle.compose.collectAsStateWithLifecycle + + class SMSOTPSignInActivity : ComponentActivity() { + val viewModel: SMSOTPSignInViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + val state by viewModel.uiState.collectAsStateWithLifecycle() + SMSOTPSignInView(state, viewModel::submit, viewModel::verify) } } } - } - @Composable - fun InputContent(placeholder: String, buttonText: String, onClick: (String) -> Unit) { - var value by remember { mutableStateOf("") } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + @Composable + fun SMSOTPSignInView( + state: SMSOTPSignInViewModel.UiState, + onSubmit: (String) -> Unit, + onVerify: (String) -> Unit, ) { - TextField(placeholder = { Text(placeholder) }, value = value, onValueChange = { value = it }) - Button(onClick = { onClick(value) }) { Text(buttonText) } + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + when (state) { + SMSOTPSignInViewModel.UiState.Unverified -> { + InputContent( + placeholder = "Enter your phone number", + buttonText = "Continue", + onClick = onSubmit, + ) + } + SMSOTPSignInViewModel.UiState.Verified -> { + Text("Verified") + } + SMSOTPSignInViewModel.UiState.Verifying -> { + InputContent( + placeholder = "Enter your verification code", + buttonText = "Verify", + onClick = onVerify, + ) + } + + SMSOTPSignInViewModel.UiState.Loading -> { + CircularProgressIndicator() + } + } + } } - } - ``` - - - To create a sign-in flow for email OTP, pass the value `email_code` as the first factor strategy. You can find all available methods in the [`SignIn`](/docs/reference/javascript/sign-in) object documentation. + @Composable + fun InputContent(placeholder: String, buttonText: String, onClick: (String) -> Unit) { + var value by remember { mutableStateOf("") } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + ) { + TextField(placeholder = { Text(placeholder) }, value = value, onValueChange = { value = it }) + Button(onClick = { onClick(value) }) { Text(buttonText) } + } + } + ``` + +
+ + To create a sign-in flow for email OTP, pass the value `email_code` as the first factor strategy. You can find all available methods in the [`SignIn`](/docs/reference/javascript/sign-in) object documentation. + diff --git a/docs/guides/development/custom-flows/authentication/google-one-tap.mdx b/docs/guides/development/custom-flows/authentication/google-one-tap.mdx index 68b68c93e8..51c267f80e 100644 --- a/docs/guides/development/custom-flows/authentication/google-one-tap.mdx +++ b/docs/guides/development/custom-flows/authentication/google-one-tap.mdx @@ -1,6 +1,7 @@ --- title: Build a custom Google One Tap authentication flow description: Learn how to build a custom Google One Tap authentication flow using the Clerk API. +sdk: nextjs, react, react-router, tanstack-react-start --- diff --git a/docs/guides/development/custom-flows/authentication/legacy/application-invitations.mdx b/docs/guides/development/custom-flows/authentication/legacy/application-invitations.mdx new file mode 100644 index 0000000000..130aa4aa78 --- /dev/null +++ b/docs/guides/development/custom-flows/authentication/legacy/application-invitations.mdx @@ -0,0 +1,254 @@ +--- +title: Sign-up with application invitations +description: Learn how to use the Clerk API to build a custom flow for handling application invitations. +--- + + + +When a user visits an [invitation](/docs/guides/users/inviting) link, Clerk first checks whether a custom redirect URL was provided. + +**If no redirect URL is specified**, the user will be redirected to the appropriate Account Portal page (either [sign-up](/docs/guides/customizing-clerk/account-portal#sign-up) or [sign-in](/docs/guides/customizing-clerk/account-portal#sign-in)), or to the custom sign-up/sign-in pages that you've configured for your application. + +**If you specified [a redirect URL when creating the invitation](/docs/guides/users/inviting#redirect-url)**, you must handle the authentication flows in your code for that page. You can either embed the [``](/docs/reference/components/authentication/sign-in) component on that page, or if the prebuilt component doesn't meet your specific needs or if you require more control over the logic, you can rebuild the existing Clerk flows using the Clerk API. + +This guide demonstrates how to use Clerk's API to build a custom flow for accepting application invitations. + +## Build the custom flow + +Once the user visits the invitation link and is redirected to the specified URL, the query parameter `__clerk_ticket` will be appended to the URL. This query parameter contains the invitation token. + +For example, if the redirect URL was `https://www.example.com/accept-invitation`, the URL that the user would be redirected to would be `https://www.example.com/accept-invitation?__clerk_ticket=.....`. + +To create a sign-up flow using the invitation token, you need to extract the token from the URL and pass it to the [`signUp.create()`](/docs/reference/javascript/sign-up#create) method, as shown in the following example. The following example also demonstrates how to collect additional user information for the sign-up; you can either remove these fields or adjust them to fit your application. + + + + ```tsx {{ filename: 'app/accept-invitation/page.tsx', collapsible: true }} + 'use client' + + import * as React from 'react' + import { useSignUp, useUser } from '@clerk/nextjs' + import { useSearchParams, useRouter } from 'next/navigation' + + export default function Page() { + const { isSignedIn, user } = useUser() + const router = useRouter() + const { isLoaded, signUp, setActive } = useSignUp() + const [firstName, setFirstName] = React.useState('') + const [lastName, setLastName] = React.useState('') + const [password, setPassword] = React.useState('') + + // Handle signed-in users visiting this page + // This will also redirect the user once they finish the sign-up process + React.useEffect(() => { + if (isSignedIn) { + router.push('/') + } + }, [isSignedIn]) + + // Get the token from the query params + const token = useSearchParams().get('__clerk_ticket') + + // If there is no invitation token, restrict access to this page + if (!token) { + return

No invitation token found.

+ } + + // Handle submission of the sign-up form + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!isLoaded) return + + try { + if (!token) return null + + // Create a new sign-up with the supplied invitation token. + // Make sure you're also passing the ticket strategy. + // After the below call, the user's email address will be + // automatically verified because of the invitation token. + const signUpAttempt = await signUp.create({ + strategy: 'ticket', + ticket: token, + firstName, + lastName, + password, + }) + + // If the sign-up was completed, set the session to active + if (signUpAttempt.status === 'complete') { + await setActive({ session: signUpAttempt.createdSessionId }) + } else { + // If the sign-up status is not complete, check why. User may need to + // complete further steps. + console.error(JSON.stringify(signUpAttempt, null, 2)) + } + } catch (err) { + console.error(JSON.stringify(err, null, 2)) + } + } + + return ( + <> +

Sign up

+
+
+ + setFirstName(e.target.value)} + /> +
+
+ + setLastName(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
+
+ +
+ + + ) + } + ``` + + + + + ```html {{ filename: 'index.html', collapsible: true }} + + + + + + Clerk + JavaScript App + + +
+ +
+ +
+

Sign up

+
+ + + + + + + +
+
+ + + + + ``` + + ```js {{ filename: 'main.js', collapsible: true }} + import { Clerk } from '@clerk/clerk-js' + + const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY + + const clerk = new Clerk(pubKey) + await clerk.load() + + if (clerk.isSignedIn) { + // Mount user button component + document.getElementById('signed-in').innerHTML = ` +
+ ` + + const userbuttonDiv = document.getElementById('user-button') + + clerk.mountUserButton(userbuttonDiv) + } else if (clerk.session.currentTask) { + // Check for pending tasks and display custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + switch (clerk.session.currentTask.key) { + case 'choose-organization': { + document.getElementById('app').innerHTML = ` +
+ ` + + const taskDiv = document.getElementById('task') + + clerk.mountTaskChooseOrganization(taskDiv) + } + } + } else { + // Get the token from the query parameter + const param = '__clerk_ticket' + const token = new URL(window.location.href).searchParams.get(param) + + // Handle the sign-up form + document.getElementById('sign-up-form').addEventListener('submit', async (e) => { + e.preventDefault() + + const formData = new FormData(e.target) + const firstName = formData.get('firstName') + const lastName = formData.get('lastName') + const password = formData.get('password') + + try { + // Start the sign-up process using the ticket method + const signUpAttempt = await clerk.client.signUp.create({ + strategy: 'ticket', + ticket: token, + firstName, + lastName, + password, + }) + + // If sign-up was successful, set the session to active + if (signUpAttempt.status === 'complete') { + await clerk.setActive({ + session: signUpAttempt.createdSessionId, + navigate: async ({ session }) => { + if (session?.currentTask) { + // Check for tasks and navigate to custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + console.log(session?.currentTask) + return + } + + await router.push('/') + }, + }) + } else { + // If the status is not complete, check why. User may need to + // complete further steps. + console.error(JSON.stringify(signUpAttempt, null, 2)) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + } + }) + } + ``` +
+
+ diff --git a/docs/guides/development/custom-flows/authentication/legacy/email-links.mdx b/docs/guides/development/custom-flows/authentication/legacy/email-links.mdx new file mode 100644 index 0000000000..3169935987 --- /dev/null +++ b/docs/guides/development/custom-flows/authentication/legacy/email-links.mdx @@ -0,0 +1,515 @@ +--- +title: Build a custom flow for handling email links +description: Learn how to build a custom flow using Clerk's API to handle email links for sign-up, sign-in, and email address verification. +--- + + + + + > [!WARNING] + > Expo does not support email links. You can request this feature on [Clerk's roadmap](https://feedback.clerk.com/). + + +[Email links](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) can be used to sign up new users, sign in existing users, or allow existing users to verify newly added email addresses to their user profiles. + +The email link flow works as follows: + +1. The user enters their email address and asks for an email link. +1. Clerk sends an email to the user, containing a link to the verification URL. +1. The user visits the email link, either on the same device where they entered their email address or on a different device, depending on the settings in the Clerk Dashboard. +1. Clerk verifies the user's identity and advances any sign-up or sign-in attempt that might be in progress. +1. If the verification is successful, the user is authenticated or their email address is verified, depending on the reason for the email link. + +This guide demonstrates how to use Clerk's API to build a custom flow for handling email links. It covers the following scenarios: + +- [Sign up](#sign-up-flow) +- [Sign in](#sign-in-flow) +- [Verify a new email address](#add-new-email-flow) + + + ## Enable email link authentication + + To allow your users to sign up or sign in using email links, you must first configure the appropriate settings in the Clerk Dashboard. + + 1. In the Clerk Dashboard, navigate to the [**User & authentication**](https://dashboard.clerk.com/last-active?path=user-authentication/user-and-authentication) page. + 1. Enable **Verify at sign-up**, and under **Verification methods**, enable **Email verification link**. + 1. Enable **Sign-in with email**. Because this guide focuses on email links, disable **Email verification code** and enable **Email verification link**. By default, **Require the same device and browser** is enabled, which means that email links are required to be verified from the same device and browser on which the sign-up or sign-in was initiated. For this guide, leave this setting enabled. + + ## Sign-up flow + + 1. The [`useSignUp()`](/docs/reference/hooks/use-sign-up) hook is used to get the [`SignUp`](/docs/reference/javascript/sign-up) object. + 1. The `SignUp` object is used to access the [`createEmailLinkFlow()`](/docs/reference/javascript/types/email-address#create-email-link-flow) method. + 1. The `createEmailLinkFlow()` method is used to access the `startEmailLinkFlow()` method. + 1. The `startEmailLinkFlow()` method is called with the `redirectUrl` parameter set to `/sign-up/verify`. It sends an email with a verification link to the user. When the user visits the link, they are redirected to the URL that was provided. + 1. On the `/sign-up/verify` page, the [`useClerk()`](/docs/reference/hooks/use-clerk) hook is used to get the [`handleEmailLinkVerification()`](/docs/reference/javascript/clerk#handle-email-link-verification) method. + 1. The `handleEmailLinkVerification()` method is called to verify the email address. Error handling is included to handle any errors that occur during the verification process. + + + + + ```tsx {{ filename: 'app/sign-up/page.tsx', collapsible: true }} + 'use client' + + import * as React from 'react' + import { useSignUp } from '@clerk/nextjs' + + export default function SignInPage() { + const [emailAddress, setEmailAddress] = React.useState('') + const [verified, setVerified] = React.useState(false) + const [verifying, setVerifying] = React.useState(false) + const [error, setError] = React.useState('') + const { signUp, isLoaded } = useSignUp() + + if (!isLoaded) return null + + const { startEmailLinkFlow } = signUp.createEmailLinkFlow() + + async function submit(e: React.FormEvent) { + e.preventDefault() + // Reset states in case user resubmits form mid sign-up + setVerified(false) + setError('') + + setVerifying(true) + + if (!isLoaded && !signUp) return null + + // Start the sign-up process using the email provided + try { + await signUp.create({ + emailAddress, + }) + + // Dynamically set the host domain for dev and prod + // You could instead use an environment variable or other source for the host domain + const protocol = window.location.protocol + const host = window.location.host + + // Send the user an email with the email link + const signUpAttempt = await startEmailLinkFlow({ + // URL to navigate to after the user visits the link in their email + redirectUrl: `${protocol}//${host}/sign-up/verify`, + }) + + // Check the verification result + const verification = signUpAttempt.verifications.emailAddress + + // Handle if user visited the link and completed sign-up from /sign-up/verify + if (verification.verifiedFromTheSameClient()) { + setVerifying(false) + setVerified(true) + } + } catch (err: any) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + + if (err.errors?.[0]?.longMessage) { + console.log('Clerk error:', err.errors[0].longMessage) + setError(err.errors[0].longMessage) + } else { + setError('An error occurred.') + } + } + } + + async function reset(e: React.FormEvent) { + e.preventDefault() + setVerifying(false) + } + + if (error) { + return ( +
+

Error: {error}

+ +
+ ) + } + + if (verifying) { + return ( +
+

Check your email and visit the link that was sent to you.

+
+ +
+
+ ) + } + + if (verified) { + return
Signed up successfully!
+ } + + return ( +
+

Sign up

+
+ setEmailAddress(e.target.value)} + /> + +
+
+ ) + } + ``` + + ```tsx {{ filename: 'app/sign-up/verify/page.tsx', collapsible: true }} + 'use client' + + import * as React from 'react' + import { useClerk } from '@clerk/nextjs' + import { EmailLinkErrorCodeStatus, isEmailLinkError } from '@clerk/nextjs/errors' + import Link from 'next/link' + + export default function VerifyEmailLink() { + const [verificationStatus, setVerificationStatus] = React.useState('loading') + + const { handleEmailLinkVerification, loaded } = useClerk() + + async function verify() { + try { + // Dynamically set the host domain for dev and prod + // You could instead use an environment variable or other source for the host domain + const protocol = window.location.protocol + const host = window.location.host + + await handleEmailLinkVerification({ + // URL to navigate to if sign-up flow needs more requirements, such as MFA + redirectUrl: `${protocol}//${host}/sign-up`, + }) + + // If not redirected at this point, + // the flow has completed + setVerificationStatus('verified') + } catch (err: any) { + let status = 'failed' + + if (isEmailLinkError(err)) { + // If link expired, set status to expired + if (err.code === EmailLinkErrorCodeStatus.Expired) { + status = 'expired' + } else if (err.code === EmailLinkErrorCodeStatus.ClientMismatch) { + // OPTIONAL: This check is only required if you have + // the 'Require the same device and browser' setting + // enabled in the Clerk Dashboard + status = 'client_mismatch' + } + } + + setVerificationStatus(status) + } + } + + React.useEffect(() => { + if (!loaded) return + + verify() + }, [handleEmailLinkVerification, loaded]) + + if (verificationStatus === 'loading') { + return
Loading...
+ } + + if (verificationStatus === 'failed') { + return ( +
+

Verify your email

+

The email link verification failed.

+ Sign up +
+ ) + } + + if (verificationStatus === 'expired') { + return ( +
+

Verify your email

+

The email link has expired.

+ Sign up +
+ ) + } + + // OPTIONAL: This check is only required if you have + // the 'Require the same device and browser' setting + // enabled in the Clerk Dashboard + if (verificationStatus === 'client_mismatch') { + return ( +
+

Verify your email

+

+ You must complete the email link sign-up on the same device and browser that you started + it on. +

+ Sign up +
+ ) + } + + return ( +
+

Verify your email

+

Successfully signed up. Return to the original tab to continue.

+
+ ) + } + ``` +
+
+
+ + ## Sign-in flow + + 1. The [`useSignIn()`](/docs/reference/hooks/use-sign-in) hook is used to get the [`SignIn`](/docs/reference/javascript/sign-in) object. + 1. The `SignIn` object is used to access the [`createEmailLinkFlow()`](/docs/reference/javascript/types/email-address#create-email-link-flow) method. + 1. The `createEmailLinkFlow()` method is used to access the `startEmailLinkFlow()` method. + 1. The `startEmailLinkFlow()` method is called with the `redirectUrl` parameter set to `/sign-in/verify`. It sends an email with a verification link to the user. When the user visits the link, they are redirected to the URL that was provided. + 1. On the `/sign-in/verify` page, the [`useClerk()`](/docs/reference/hooks/use-clerk) hook is used to get the [`handleEmailLinkVerification()`](/docs/reference/javascript/clerk#handle-email-link-verification) method. + 1. The `handleEmailLinkVerification()` method is called to verify the email address. Error handling is included to handle any errors that occur during the verification process. + + + + + ```tsx {{ filename: 'app/sign-in/page.tsx', collapsible: true }} + 'use client' + + import * as React from 'react' + import { useSignIn } from '@clerk/nextjs' + import { EmailLinkFactor, SignInFirstFactor } from '@clerk/types' + + export default function SignInPage() { + const [emailAddress, setEmailAddress] = React.useState('') + const [verified, setVerified] = React.useState(false) + const [verifying, setVerifying] = React.useState(false) + const [error, setError] = React.useState('') + const { signIn, isLoaded } = useSignIn() + + if (!isLoaded) return null + + const { startEmailLinkFlow } = signIn.createEmailLinkFlow() + + async function submit(e: React.FormEvent) { + e.preventDefault() + // Reset states in case user resubmits form mid sign-in + setVerified(false) + setError('') + + if (!isLoaded && !signIn) return null + + // Start the sign-in process using the email provided + try { + const { supportedFirstFactors } = await signIn.create({ + identifier: emailAddress, + }) + + setVerifying(true) + + // Filter the returned array to find the 'email_link' entry + const isEmailLinkFactor = (factor: SignInFirstFactor): factor is EmailLinkFactor => { + return factor.strategy === 'email_link' + } + const emailLinkFactor = supportedFirstFactors?.find(isEmailLinkFactor) + + if (!emailLinkFactor) { + setError('Email link factor not found') + return + } + + const { emailAddressId } = emailLinkFactor + + // Dynamically set the host domain for dev and prod + // You could instead use an environment variable or other source for the host domain + const protocol = window.location.protocol + const host = window.location.host + + // Send the user an email with the email link + const signInAttempt = await startEmailLinkFlow({ + emailAddressId, + redirectUrl: `${protocol}//${host}/sign-in/verify`, + }) + + // Check the verification result + const verification = signInAttempt.firstFactorVerification + + // Handle if verification expired + if (verification.status === 'expired') { + setError('The email link has expired.') + } + + // Handle if user visited the link and completed sign-in from /sign-in/verify + if (verification.verifiedFromTheSameClient()) { + setVerifying(false) + setVerified(true) + } + } catch (err: any) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + setError('An error occurred.') + } + } + + async function reset(e: React.FormEvent) { + e.preventDefault() + setVerifying(false) + } + + if (error) { + return ( +
+

Error: {error}

+ +
+ ) + } + + if (verifying) { + return ( +
+

Check your email and visit the link that was sent to you.

+
+ +
+
+ ) + } + + if (verified) { + return
Signed in successfully!
+ } + + return ( +
+

Sign in

+
+ setEmailAddress(e.target.value)} + /> + +
+
+ ) + } + ``` + + ```tsx {{ filename: 'app/sign-in/verify/page.tsx', collapsible: true }} + 'use client' + + import * as React from 'react' + import { useClerk } from '@clerk/nextjs' + import { EmailLinkErrorCodeStatus, isEmailLinkError } from '@clerk/nextjs/errors' + import Link from 'next/link' + + export default function VerifyEmailLink() { + const [verificationStatus, setVerificationStatus] = React.useState('loading') + + const { handleEmailLinkVerification, loaded } = useClerk() + + async function verify() { + try { + // Dynamically set the host domain for dev and prod + // You could instead use an environment variable or other source for the host domain + const protocol = window.location.protocol + const host = window.location.host + + await handleEmailLinkVerification({ + // URL to navigate to if sign-in flow needs more requirements, such as MFA + redirectUrl: `${protocol}//${host}/sign-in`, + }) + + // If not redirected at this point, + // the flow has completed + setVerificationStatus('verified') + } catch (err: any) { + let status = 'failed' + + if (isEmailLinkError(err)) { + // If link expired, set status to expired + if (err.code === EmailLinkErrorCodeStatus.Expired) { + status = 'expired' + } else if (err.code === EmailLinkErrorCodeStatus.ClientMismatch) { + // OPTIONAL: This check is only required if you have + // the 'Require the same device and browser' setting + // enabled in the Clerk Dashboard + status = 'client_mismatch' + } + } + + setVerificationStatus(status) + return + } + } + + React.useEffect(() => { + if (!loaded) return + + verify() + }, [handleEmailLinkVerification, loaded]) + + if (verificationStatus === 'loading') { + return
Loading...
+ } + + if (verificationStatus === 'failed') { + return ( +
+

Verify your email

+

The email link verification failed.

+ Sign in +
+ ) + } + + if (verificationStatus === 'expired') { + return ( +
+

Verify your email

+

The email link has expired.

+ Sign in +
+ ) + } + + // OPTIONAL: This check is only required if you have + // the 'Require the same device and browser' setting + // enabled in the Clerk Dashboard + if (verificationStatus === 'client_mismatch') { + return ( +
+

Verify your email

+

+ You must complete the email link sign-in on the same device and browser as you started it + on. +

+ Sign in +
+ ) + } + + return ( +
+

Verify your email

+

Successfully signed in. Return to the original tab to continue.

+
+ ) + } + ``` +
+
+
+ + ## Add new email flow + + When a user adds an email address to their account, you can use email links to verify the email address. + + +
diff --git a/docs/guides/development/custom-flows/authentication/legacy/email-password-mfa.mdx b/docs/guides/development/custom-flows/authentication/legacy/email-password-mfa.mdx new file mode 100644 index 0000000000..51d2933608 --- /dev/null +++ b/docs/guides/development/custom-flows/authentication/legacy/email-password-mfa.mdx @@ -0,0 +1,734 @@ +--- +title: Build a custom sign-in flow with multi-factor authentication +description: Learn how to build a custom email/password sign-in flow that requires multi-factor authentication (MFA). +--- + + + +[Multi-factor verification (MFA)](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) is an added layer of security that requires users to provide a second verification factor to access an account. + +Clerk supports second factor verification through **SMS verification code**, **Authenticator application**, and **Backup codes**. + +This guide will walk you through how to build a custom email/password sign-in flow that supports **Authenticator application** and **Backup codes** as the second factor. + + + ## Enable email and password + + This guide uses email and password to sign in, however, you can modify this approach according to the needs of your application. + + To follow this guide, you first need to ensure email and password are enabled for your application. + + 1. In the Clerk Dashboard, navigate to the [**User & authentication**](https://dashboard.clerk.com/last-active?path=user-authentication/user-and-authentication) page. + 1. Enable **Sign-in with email**. + 1. Select the **Password** tab and enable **Sign-up with password**. Leave **Require a password at sign-up** enabled. + + ## Enable multi-factor authentication + + For your users to be able to enable MFA for their account, you need to enable MFA for your application. + + 1. In the Clerk Dashboard, navigate to the [**Multi-factor**](https://dashboard.clerk.com/last-active?path=user-authentication/multi-factor) page. + 1. For the purpose of this guide, toggle on both the **Authenticator application** and **Backup codes** strategies. + 1. Select **Save**. + + ## Sign-in flow + + Signing in to an MFA-enabled account is identical to the regular sign-in process. However, in the case of an MFA-enabled account, a sign-in won't convert until both first factor and second factor verifications are completed. + + To authenticate a user using their email and password, you need to: + + 1. Initiate the sign-in process by collecting the user's email address and password. + 1. Prepare the first factor verification. + 1. Attempt to complete the first factor verification. + 1. Prepare the second factor verification. (This is where MFA comes into play.) + 1. Attempt to complete the second factor verification. + 1. If the verification is successful, set the newly created session as the active session. + + > [!TIP] + > For this example to work, the user must have MFA enabled on their account. You need to add the ability for your users to manage their MFA settings. See the [manage SMS-based MFA](/docs/guides/development/custom-flows/account-updates/manage-sms-based-mfa) or the [manage TOTP-based MFA](/docs/guides/development/custom-flows/account-updates/manage-totp-based-mfa) guide, depending on your needs. + + + + ```tsx {{ filename: 'app/sign-in/[[...sign-in]]/page.tsx', collapsible: true }} + 'use client' + + import * as React from 'react' + import { useSignIn } from '@clerk/nextjs' + import { useRouter } from 'next/navigation' + + export default function SignInForm() { + const { isLoaded, signIn, setActive } = useSignIn() + const [email, setEmail] = React.useState('') + const [password, setPassword] = React.useState('') + const [code, setCode] = React.useState('') + const [useBackupCode, setUseBackupCode] = React.useState(false) + const [displayTOTP, setDisplayTOTP] = React.useState(false) + const router = useRouter() + + // Handle user submitting email and pass and swapping to TOTP form + const handleFirstStage = (e: React.FormEvent) => { + e.preventDefault() + setDisplayTOTP(true) + } + + // Handle the submission of the TOTP of Backup Code submission + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!isLoaded) return + + // Start the sign-in process using the email and password provided + try { + await signIn.create({ + identifier: email, + password, + }) + + // Attempt the TOTP or backup code verification + const signInAttempt = await signIn.attemptSecondFactor({ + strategy: useBackupCode ? 'backup_code' : 'totp', + code: code, + }) + + // If verification was completed, set the session to active + // and redirect the user + if (signInAttempt.status === 'complete') { + await setActive({ + session: signInAttempt.createdSessionId, + navigate: async ({ session }) => { + if (session?.currentTask) { + // Check for tasks and navigate to custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + console.log(session?.currentTask) + return + } + + await router.push('/') + }, + }) + } else { + // If the status is not complete, check why. User may need to + // complete further steps. + console.log(signInAttempt) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error('Error:', JSON.stringify(err, null, 2)) + } + } + + if (displayTOTP) { + return ( +
+

Verify your account

+
handleSubmit(e)}> +
+ + setCode(e.target.value)} + id="code" + name="code" + type="text" + value={code} + /> +
+
+ + setUseBackupCode((prev) => !prev)} + id="backupcode" + name="backupcode" + type="checkbox" + checked={useBackupCode} + /> +
+ +
+
+ ) + } + + return ( + <> +

Sign in

+
handleFirstStage(e)}> +
+ + setEmail(e.target.value)} + id="email" + name="email" + type="email" + value={email} + /> +
+
+ + setPassword(e.target.value)} + id="password" + name="password" + type="password" + value={password} + /> +
+ +
+ + ) + } + ``` +
+ + + + ```html {{ filename: 'index.html', collapsible: true }} + + + + + + Clerk + JavaScript App + + +
+ +
+ +
+

Sign in

+
+ + + + + +
+
+ + + + + + + ``` + + ```js {{ filename: 'main.js', collapsible: true }} + import { Clerk } from '@clerk/clerk-js' + + const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY + + const clerk = new Clerk(pubKey) + await clerk.load() + + if (clerk.isSignedIn) { + // Mount user button component + document.getElementById('signed-in').innerHTML = ` +
+ ` + + const userbuttonDiv = document.getElementById('user-button') + + clerk.mountUserButton(userbuttonDiv) + } else if (clerk.session?.currentTask) { + // Check for pending tasks and display custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + switch (clerk.session.currentTask.key) { + case 'choose-organization': { + document.getElementById('app').innerHTML = ` +
+ ` + + const taskDiv = document.getElementById('task') + + clerk.mountTaskChooseOrganization(taskDiv) + } + } + } else { + // Handle the sign-in form + document.getElementById('sign-in-form').addEventListener('submit', async (e) => { + e.preventDefault() + + const formData = new FormData(e.target) + const emailAddress = formData.get('email') + const password = formData.get('password') + + try { + // Start the sign-in process + await clerk.client.signIn.create({ + identifier: emailAddress, + password, + }) + + // Hide sign-in form + document.getElementById('sign-in').setAttribute('hidden', '') + // Show verification form + document.getElementById('verifying').removeAttribute('hidden') + } catch (error) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(error) + } + }) + + // Handle the verification form + document.getElementById('verifying').addEventListener('submit', async (e) => { + const formData = new FormData(e.target) + const totp = formData.get('totp') + const backupCode = formData.get('backupCode') + + try { + const useBackupCode = backupCode ? true : false + const code = backupCode ? backupCode : totp + + // Attempt the TOTP or backup code verification + const signInAttempt = await clerk.client.signIn.attemptSecondFactor({ + strategy: useBackupCode ? 'backup_code' : 'totp', + code: code, + }) + + // If verification was completed, set the session to active + // and redirect the user + if (signInAttempt.status === 'complete') { + await clerk.setActive({ session: signInAttempt.createdSessionId }) + + location.reload() + } else { + // If the status is not complete, check why. User may need to + // complete further steps. + console.error(signInAttempt) + } + } catch (error) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(error) + } + }) + } + ``` +
+
+ + + ### Before you start + + Install `expo-checkbox` for the UI. + + + ```bash {{ filename: 'terminal' }} + npm install expo-checkbox + ``` + + ```bash {{ filename: 'terminal' }} + yarn add expo-checkbox + ``` + + ```bash {{ filename: 'terminal' }} + pnpm add expo-checkbox + ``` + + ```bash {{ filename: 'terminal' }} + bun add expo-checkbox + ``` + + + ### Build the flow + + 1. Create the `(auth)` route group. This groups your sign-up and sign-in pages. + 1. In the `(auth)` group, create a `_layout.tsx` file with the following code. The [`useAuth()`](/docs/reference/hooks/use-auth) hook is used to access the user's authentication state. If the user's already signed in, they'll be redirected to the home page. + + ```tsx {{ filename: 'app/(auth)/_layout.tsx' }} + import { Redirect, Stack } from 'expo-router' + import { useAuth } from '@clerk/clerk-expo' + + export default function AuthenticatedLayout() { + const { isSignedIn } = useAuth() + + if (isSignedIn) { + return + } + + return + } + ``` + + In the `(auth)` group, create a `sign-in.tsx` file with the following code. The [`useSignIn()`](/docs/reference/hooks/use-sign-in) hook is used to create a sign-in flow. The user can sign in using their email and password and will be prompted to verify their account with a code from their authenticator app or with a backup code. + + ```tsx {{ filename: 'app/(auth)/sign-in.tsx', collapsible: true }} + import React from 'react' + import { useSignIn } from '@clerk/clerk-expo' + import { useRouter } from 'expo-router' + import { Text, TextInput, Button, View } from 'react-native' + import Checkbox from 'expo-checkbox' + + export default function Page() { + const { signIn, setActive, isLoaded } = useSignIn() + + const [email, setEmail] = React.useState('') + const [password, setPassword] = React.useState('') + const [code, setCode] = React.useState('') + const [useBackupCode, setUseBackupCode] = React.useState(false) + const [displayTOTP, setDisplayTOTP] = React.useState(false) + const router = useRouter() + + // Handle user submitting email and pass and swapping to TOTP form + const handleFirstStage = async () => { + if (!isLoaded) return + + // Attempt to sign in using the email and password provided + try { + const attemptFirstFactor = await signIn.create({ + identifier: email, + password, + }) + + // If the sign-in was successful, set the session to active + // and redirect the user + if (attemptFirstFactor.status === 'complete') { + await setActive({ + session: attemptFirstFactor.createdSessionId, + navigate: async ({ session }) => { + if (session?.currentTask) { + // Check for tasks and navigate to custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + console.log(session?.currentTask) + return + } + + await router.push('/') + }, + }) + } else if (attemptFirstFactor.status === 'needs_second_factor') { + // If the sign-in requires a second factor, display the TOTP form + setDisplayTOTP(true) + } else { + // If the sign-in failed, check why. User might need to + // complete further steps. + console.error(JSON.stringify(attemptFirstFactor, null, 2)) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + } + } + + // Handle the submission of the TOTP or backup code + const onPressTOTP = React.useCallback(async () => { + if (!isLoaded) return + + try { + // Attempt the TOTP or backup code verification + const attemptSecondFactor = await signIn.attemptSecondFactor({ + strategy: useBackupCode ? 'backup_code' : 'totp', + code: code, + }) + + // If verification was completed, set the session to active + // and redirect the user + if (attemptSecondFactor.status === 'complete') { + await setActive({ + session: attemptSecondFactor.createdSessionId, + navigate: async ({ session }) => { + if (session?.currentTask) { + // Check for tasks and navigate to custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + console.log(session?.currentTask) + return + } + + await router.push('/') + }, + }) + } else { + // If the status is not complete, check why. User may need to + // complete further steps. + console.error(JSON.stringify(attemptSecondFactor, null, 2)) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + } + }, [isLoaded, email, password, code, useBackupCode]) + + if (displayTOTP) { + return ( + + Verify your account + + + setCode(c)} + /> + + + Check if this code is a backup code + setUseBackupCode((prev) => !prev)} /> + + + + + ) + } + + // Display the initial sign-up form to capture the email and password + return ( + <> +

Sign up

+
+
+ + setEmailAddress(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
+ +
+
+ + ) + } + ``` +
+ + + + ```html {{ filename: 'index.html', collapsible: true }} + + + + + + Clerk + JavaScript App + + +
+ +
+

Sign up

+
+ + + + + +
+
+ + + + + + + ``` + + ```js {{ filename: 'main.js', collapsible: true }} + import { Clerk } from '@clerk/clerk-js' + + const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY + + const clerk = new Clerk(pubKey) + await clerk.load() + + if (clerk.isSignedIn) { + // Mount user button component + document.getElementById('signed-in').innerHTML = ` +
+ ` + + const userbuttonDiv = document.getElementById('user-button') + + clerk.mountUserButton(userbuttonDiv) + } else { + // Handle the sign-up form + document.getElementById('sign-up-form').addEventListener('submit', async (e) => { + e.preventDefault() + + const formData = new FormData(e.target) + const emailAddress = formData.get('email') + const password = formData.get('password') + + try { + // Start the sign-up process using the email and password provided + await clerk.client.signUp.create({ emailAddress, password }) + await clerk.client.signUp.prepareEmailAddressVerification() + // Hide sign-up form + document.getElementById('sign-up').setAttribute('hidden', '') + // Show verification form + document.getElementById('verifying').removeAttribute('hidden') + } catch (error) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(error) + } + }) + + // Handle the verification form + document.getElementById('verifying').addEventListener('submit', async (e) => { + const formData = new FormData(e.target) + const code = formData.get('code') + + try { + // Use the code the user provided to attempt verification + const signUpAttempt = await clerk.client.signUp.attemptEmailAddressVerification({ + code, + }) + + // Now that the user is created, set the session to active. + await clerk.setActive({ session: signUpAttempt.createdSessionId }) + } catch (error) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(error) + } + }) + } + ``` +
+
+ + + 1. Create the `(auth)` route group. This groups your sign-up and sign-in pages. + 1. In the `(auth)` group, create a `_layout.tsx` file with the following code. The [`useAuth()`](/docs/reference/hooks/use-auth) hook is used to access the user's authentication state. If the user's already signed in, they'll be redirected to the home page. + + ```tsx {{ filename: 'app/(auth)/_layout.tsx' }} + import { Redirect, Stack } from 'expo-router' + import { useAuth } from '@clerk/clerk-expo' + + export default function GuestLayout() { + const { isSignedIn } = useAuth() + + if (isSignedIn) { + return + } + + return + } + ``` + + In the `(auth)` group, create a `sign-up.tsx` file with the following code. The [`useSignUp()`](/docs/reference/hooks/use-sign-up) hook is used to create a sign-up flow. The user can sign up using their email and password and will receive an email verification code to confirm their email. + + ```tsx {{ filename: 'app/(auth)/sign-up.tsx', collapsible: true }} + import * as React from 'react' + import { Text, TextInput, Button, View } from 'react-native' + import { useSignUp } from '@clerk/clerk-expo' + import { Link, useRouter } from 'expo-router' + + export default function Page() { + const { isLoaded, signUp, setActive } = useSignUp() + const router = useRouter() + + const [emailAddress, setEmailAddress] = React.useState('') + const [password, setPassword] = React.useState('') + const [pendingVerification, setPendingVerification] = React.useState(false) + const [code, setCode] = React.useState('') + + // Handle submission of sign-up form + const onSignUpPress = async () => { + if (!isLoaded) return + + // Start sign-up process using email and password provided + try { + await signUp.create({ + emailAddress, + password, + }) + + // Send user an email with verification code + await signUp.prepareEmailAddressVerification({ strategy: 'email_code' }) + + // Set 'pendingVerification' to true to display second form + // and capture OTP code + setPendingVerification(true) + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + } + } + + // Handle submission of verification form + const onVerifyPress = async () => { + if (!isLoaded) return + + try { + // Use the code the user provided to attempt verification + const signUpAttempt = await signUp.attemptEmailAddressVerification({ + code, + }) + + // If verification was completed, set the session to active + // and redirect the user + if (signUpAttempt.status === 'complete') { + await setActive({ + session: signUpAttempt.createdSessionId, + navigate: async ({ session }) => { + if (session?.currentTask) { + // Check for tasks and navigate to custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + console.log(session?.currentTask) + return + } + + router.replace('/') + }, + }) + } else { + // If the status is not complete, check why. User may need to + // complete further steps. + console.error(JSON.stringify(signUpAttempt, null, 2)) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + } + } + + if (pendingVerification) { + return ( + <> + Verify your email + setCode(code)} + /> + + + + ) + } + ``` + + + + + ```html {{ filename: 'index.html', collapsible: true }} + + + + + + Clerk + JavaScript App + + +
+ +
+

Sign in

+
+ + + + + +
+
+ + + + + ``` + + ```js {{ filename: 'main.js', collapsible: true }} + import { Clerk } from '@clerk/clerk-js' + + const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY + + const clerk = new Clerk(pubKey) + await clerk.load() + + if (clerk.isSignedIn) { + // Mount user button component + document.getElementById('signed-in').innerHTML = ` +
+ ` + + const userbuttonDiv = document.getElementById('user-button') + + clerk.mountUserButton(userbuttonDiv) + } else if (clerk.session?.currentTask) { + // Check for pending tasks and display custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + switch (clerk.session.currentTask.key) { + case 'choose-organization': { + document.getElementById('app').innerHTML = ` +
+ ` + + const taskDiv = document.getElementById('task') + + clerk.mountTaskChooseOrganization(taskDiv) + } + } + } else { + // Handle the sign-in form + document.getElementById('sign-in-form').addEventListener('submit', async (e) => { + e.preventDefault() + + const formData = new FormData(e.target) + const emailAddress = formData.get('email') + const password = formData.get('password') + + try { + // Start the sign-in process + const signInAttempt = await clerk.client.signIn.create({ + identifier: emailAddress, + password, + }) + + // If the sign-in is complete, set the user as active + if (signInAttempt.status === 'complete') { + await clerk.setActive({ session: signInAttempt.createdSessionId }) + + location.reload() + } else { + // If the status is not complete, check why. User may need to + // complete further steps. + console.error(JSON.stringify(signInAttempt, null, 2)) + } + } catch (error) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(error) + } + }) + } + ``` +
+
+ + + In the `(auth)` group, create a `sign-in.tsx` file with the following code. The [`useSignIn()`](/docs/reference/hooks/use-sign-in) hook is used to create a sign-in flow. The user can sign in using email address and password, or navigate to the sign-up page. + + ```tsx {{ filename: 'app/(auth)/sign-in.tsx', collapsible: true }} + import { useSignIn } from '@clerk/clerk-expo' + import { Link, useRouter } from 'expo-router' + import { Text, TextInput, Button, View } from 'react-native' + import React from 'react' + + export default function Page() { + const { signIn, setActive, isLoaded } = useSignIn() + const router = useRouter() + + const [emailAddress, setEmailAddress] = React.useState('') + const [password, setPassword] = React.useState('') + + // Handle the submission of the sign-in form + const onSignInPress = React.useCallback(async () => { + if (!isLoaded) return + + // Start the sign-in process using the email and password provided + try { + const signInAttempt = await signIn.create({ + identifier: emailAddress, + password, + }) + + // If sign-in process is complete, set the created session as active + // and redirect the user + if (signInAttempt.status === 'complete') { + await setActive({ + session: signInAttempt.createdSessionId, + navigate: async ({ session }) => { + if (session?.currentTask) { + // Check for tasks and navigate to custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + console.log(session?.currentTask) + return + } + + router.replace('/') + }, + }) + } else { + // If the status is not complete, check why. User may need to + // complete further steps. + console.error(JSON.stringify(signInAttempt, null, 2)) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + } + }, [isLoaded, emailAddress, password]) + + return ( + + Sign in + setEmailAddress(emailAddress)} + /> + setPassword(password)} + /> + + + + ) + } + + return ( + <> +

Sign up

+
+ + setPhone(e.target.value)} + /> + +
+ + ) + } + ``` +
+ + + + ```html {{ filename: 'index.html', collapsible: true }} + + + + + + Clerk + JavaScript App + + +
+ +
+ +
+

Sign up

+
+ + + +
+
+ + + + + + + ``` + + ```js {{ filename: 'main.js', collapsible: true }} + import { Clerk } from '@clerk/clerk-js' + + const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY + + const clerk = new Clerk(pubKey) + await clerk.load() + + if (clerk.isSignedIn) { + // Mount user button component + document.getElementById('signed-in').innerHTML = ` +
+ ` + + const userbuttonDiv = document.getElementById('user-button') + + clerk.mountUserButton(userbuttonDiv) + } else if (clerk.session?.currentTask) { + // Check for pending tasks and display custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + switch (clerk.session.currentTask.key) { + case 'choose-organization': { + document.getElementById('app').innerHTML = ` +
+ ` + + const taskDiv = document.getElementById('task') + + clerk.mountTaskChooseOrganization(taskDiv) + } + } + } else { + // Handle the sign-up form + document.getElementById('sign-up-form').addEventListener('submit', async (e) => { + e.preventDefault() + + const formData = new FormData(e.target) + const phoneNumber = formData.get('phone') + + try { + // Start the sign-up process using the phone number method + await clerk.client.signUp.create({ phoneNumber }) + await clerk.client.signUp.preparePhoneNumberVerification() + // Hide sign-up form + document.getElementById('sign-up').setAttribute('hidden', '') + // Show verification form + document.getElementById('verifying').removeAttribute('hidden') + } catch (error) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(error) + } + }) + + // Handle the verification form + document.getElementById('verifying').addEventListener('submit', async (e) => { + const formData = new FormData(e.target) + const code = formData.get('code') + + try { + // Verify the phone number + const verify = await clerk.client.signUp.attemptPhoneNumberVerification({ + code, + }) + + // Now that the user is created, set the session to active. + await clerk.setActive({ session: verify.createdSessionId }) + } catch (error) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(error) + } + }) + } + ``` +
+
+ + + ```swift {{ filename: 'SMSOTPSignUpView.swift', collapsible: true }} + import SwiftUI + import Clerk + + struct SMSOTPSignUpView: View { + @State private var phoneNumber = "" + @State private var code = "" + @State private var isVerifying = false + + var body: some View { + if isVerifying { + TextField("Enter your verification code", text: $code) + Button("Verify") { + Task { await verify(code: code) } + } + } else { + TextField("Enter phone number", text: $phoneNumber) + Button("Continue") { + Task { await submit(phoneNumber: phoneNumber) } + } + } + } + } + + extension SMSOTPSignUpView { + + func submit(phoneNumber: String) async { + do { + // Start the sign-up process using the phone number method. + let signUp = try await SignUp.create(strategy: .standard(phoneNumber: phoneNumber)) + + // Start the verification - a SMS message will be sent to the + // number with a one-time code. + try await signUp.prepareVerification(strategy: .phoneCode) + + // Set isVerifying to true to display second form and capture the OTP code. + isVerifying = true + } catch { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + dump(error) + } + } + + func verify(code: String) async { + do { + // Access the in progress sign up stored on the client object. + guard let inProgressSignUp = Clerk.shared.client?.signUp else { return } + + // Use the code provided by the user and attempt verification. + let signUp = try await inProgressSignUp.attemptVerification(strategy: .phoneCode(code: code)) + + switch signUp.status { + case .complete: + // If verification was completed, navigate the user as needed. + dump(Clerk.shared.session) + default: + // If the status is not complete, check why. User may need to + // complete further steps. + dump(signUp.status) + } + } catch { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + dump(error) + } + } + } + ``` + + + + ```kotlin {{ filename: 'SMSOTPSignUpViewModel.kt', collapsible: true }} + import androidx.lifecycle.ViewModel + import androidx.lifecycle.viewModelScope + import com.clerk.api.Clerk + import com.clerk.api.network.serialization.flatMap + import com.clerk.api.network.serialization.onFailure + import com.clerk.api.network.serialization.onSuccess + import com.clerk.api.signup.SignUp + import com.clerk.api.signup.attemptVerification + import com.clerk.api.signup.prepareVerification + import kotlinx.coroutines.flow.MutableStateFlow + import kotlinx.coroutines.flow.asStateFlow + import kotlinx.coroutines.flow.combine + import kotlinx.coroutines.flow.launchIn + import kotlinx.coroutines.launch + + class SMSOTPSignUpViewModel : ViewModel() { + + private val _uiState = MutableStateFlow(UiState.Unverified) + val uiState = _uiState.asStateFlow() + + init { + combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user -> + _uiState.value = + when { + !isInitialized -> UiState.Loading + user == null -> UiState.Unverified + else -> UiState.Verified + } + } + .launchIn(viewModelScope) + } + + fun submit(phoneNumber: String) { + viewModelScope.launch { + SignUp.create(SignUp.CreateParams.Standard(phoneNumber = phoneNumber)) + .flatMap { it.prepareVerification(SignUp.PrepareVerificationParams.Strategy.PhoneCode()) } + .onSuccess { _uiState.value = UiState.Verifying } + .onFailure { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + } + } + } + + fun verify(code: String) { + val inProgressSignUp = Clerk.signUp ?: return + viewModelScope.launch { + inProgressSignUp + .attemptVerification(SignUp.AttemptVerificationParams.PhoneCode(code)) + .onSuccess { + if (it.status == SignUp.Status.COMPLETE) { + _uiState.value = UiState.Verified + } else { + // The user may need to complete further steps + } + } + .onFailure { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + } + } + } + + sealed interface UiState { + data object Loading : UiState + + data object Unverified : UiState + + data object Verifying : UiState + + data object Verified : UiState + } + } + ``` + + ```kotlin {{ filename: 'SMSOTPSignUpActivity.kt', collapsible: true }} + import android.os.Bundle + import androidx.activity.ComponentActivity + import androidx.activity.compose.setContent + import androidx.activity.viewModels + import androidx.compose.foundation.layout.Arrangement + import androidx.compose.foundation.layout.Box + import androidx.compose.foundation.layout.Column + import androidx.compose.foundation.layout.fillMaxSize + import androidx.compose.material3.Button + import androidx.compose.material3.CircularProgressIndicator + import androidx.compose.material3.Text + import androidx.compose.material3.TextField + import androidx.compose.runtime.Composable + import androidx.compose.runtime.getValue + import androidx.compose.runtime.mutableStateOf + import androidx.compose.runtime.remember + import androidx.compose.runtime.setValue + import androidx.compose.ui.Alignment + import androidx.compose.ui.Modifier + import androidx.compose.ui.unit.dp + import androidx.lifecycle.compose.collectAsStateWithLifecycle + + class SMSOTPSignUpActivity : ComponentActivity() { + val viewModel: SMSOTPSignUpViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + val state by viewModel.uiState.collectAsStateWithLifecycle() + SMSOTPSignUpView(state, viewModel::submit, viewModel::verify) + } + } + } + + @Composable + fun SMSOTPSignUpView( + state: SMSOTPSignUpViewModel.UiState, + onSubmit: (String) -> Unit, + onVerify: (String) -> Unit, + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + when (state) { + SMSOTPSignUpViewModel.UiState.Unverified -> { + InputContent( + placeholder = "Enter your phone number", + buttonText = "Continue", + onClick = onSubmit, + ) + } + SMSOTPSignUpViewModel.UiState.Verified -> { + Text("Verified") + } + SMSOTPSignUpViewModel.UiState.Verifying -> { + InputContent( + placeholder = "Enter your verification code", + buttonText = "Verify", + onClick = onVerify, + ) + } + + SMSOTPSignUpViewModel.UiState.Loading -> { + CircularProgressIndicator() + } + } + } + } + + @Composable + fun InputContent(placeholder: String, buttonText: String, onClick: (String) -> Unit) { + var value by remember { mutableStateOf("") } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + ) { + TextField(placeholder = { Text(placeholder) }, value = value, onValueChange = { value = it }) + Button(onClick = { onClick(value) }) { Text(buttonText) } + } + } + ``` + +
+ + To create a sign-up flow for email OTP, use the [`prepareEmailAddressVerification`](/docs/reference/javascript/sign-up#prepare-email-address-verification) and [`attemptEmailAddressVerification`](/docs/reference/javascript/sign-up#attempt-email-address-verification). These helpers work the same way as their phone number counterparts do in the previous example. You can find all available methods in the [`SignUp`](/docs/reference/javascript/sign-in) object documentation. + + ## Sign-in flow + + To authenticate a user with an OTP, you must: + + 1. Initiate the sign-in process by creating a `SignIn` using the identifier provided, which for this example is a phone number. + 1. Prepare the first factor verification. + 1. Attempt verification with the code the user provides. + 1. If the attempt is successful, set the newly created session as the active session. + + + + This example is written for Next.js App Router but it can be adapted to any React-based framework. + + ```tsx {{ filename: 'app/sign-in/[[...sign-in]]/page.tsx', collapsible: true }} + 'use client' + + import * as React from 'react' + import { useSignIn } from '@clerk/nextjs' + import { PhoneCodeFactor, SignInFirstFactor } from '@clerk/types' + import { useRouter } from 'next/navigation' + + export default function Page() { + const { isLoaded, signIn, setActive } = useSignIn() + const [verifying, setVerifying] = React.useState(false) + const [phone, setPhone] = React.useState('') + const [code, setCode] = React.useState('') + const router = useRouter() + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + + if (!isLoaded && !signIn) return null + + try { + // Start the sign-in process using the phone number method + const { supportedFirstFactors } = await signIn.create({ + identifier: phone, + }) + + // Filter the returned array to find the 'phone_code' entry + const isPhoneCodeFactor = (factor: SignInFirstFactor): factor is PhoneCodeFactor => { + return factor.strategy === 'phone_code' + } + const phoneCodeFactor = supportedFirstFactors?.find(isPhoneCodeFactor) + + if (phoneCodeFactor) { + // Grab the phoneNumberId + const { phoneNumberId } = phoneCodeFactor + + // Send the OTP code to the user + await signIn.prepareFirstFactor({ + strategy: 'phone_code', + phoneNumberId, + }) + + // Set verifying to true to display second form + // and capture the OTP code + setVerifying(true) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error('Error:', JSON.stringify(err, null, 2)) + } + } + + async function handleVerification(e: React.FormEvent) { + e.preventDefault() + + if (!isLoaded && !signIn) return null + + try { + // Use the code provided by the user and attempt verification + const signInAttempt = await signIn.attemptFirstFactor({ + strategy: 'phone_code', + code, + }) + + // If verification was completed, set the session to active + // and redirect the user + if (signInAttempt.status === 'complete') { + await setActive({ + session: signInAttempt.createdSessionId, + navigate: async ({ session }) => { + if (session?.currentTask) { + // Check for tasks and navigate to custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + console.log(session?.currentTask) + return + } + + router.push('/') + }, + }) + } else { + // If the status is not complete, check why. User may need to + // complete further steps. + console.error(signInAttempt) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error('Error:', JSON.stringify(err, null, 2)) + } + } + + if (verifying) { + return ( + <> +

Verify your phone number

+
+ + setCode(e.target.value)} /> + +
+ + ) + } + + return ( + <> +

Sign in

+
+ + setPhone(e.target.value)} + /> + +
+ + ) + } + ``` +
+ + + + ```html {{ filename: 'index.html', collapsible: true }} + + + + + + Clerk + JavaScript App + + +
+ +
+

Sign in

+
+ + + +
+
+ + + + + + + ``` + + ```js {{ filename: 'main.js', collapsible: true }} + import { Clerk } from '@clerk/clerk-js' + + const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY + + const clerk = new Clerk(pubKey) + await clerk.load() + + if (clerk.isSignedIn) { + // Mount user button component + document.getElementById('signed-in').innerHTML = ` +
+ ` + + const userbuttonDiv = document.getElementById('user-button') + + clerk.mountUserButton(userbuttonDiv) + } else { + // Handle the sign-in form + document.getElementById('sign-in-form').addEventListener('submit', async (e) => { + e.preventDefault() + + const formData = new FormData(e.target) + const phone = formData.get('phone') + + try { + // Start the sign-in process using the user's identifier. + // In this case, it's their phone number. + const { supportedFirstFactors } = await clerk.client.signIn.create({ + identifier: phone, + }) + + // Find the phoneNumberId from all the available first factors for the current sign-in + const firstPhoneFactor = supportedFirstFactors.find((factor) => { + return factor.strategy === 'phone_code' + }) + + const { phoneNumberId } = firstPhoneFactor + + // Prepare first factor verification, specifying + // the phone code strategy. + await clerk.client.signIn.prepareFirstFactor({ + strategy: 'phone_code', + phoneNumberId, + }) + + // Hide sign-in form + document.getElementById('sign-in').setAttribute('hidden', '') + // Show verification form + document.getElementById('verifying').removeAttribute('hidden') + } catch (error) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(error) + } + }) + + // Handle the verification form + document.getElementById('verifying').addEventListener('submit', async (e) => { + const formData = new FormData(e.target) + const code = formData.get('code') + + try { + // Verify the phone number + const verify = await clerk.client.signIn.attemptFirstFactor({ + strategy: 'phone_code', + code, + }) + + // Now that the user is created, set the session to active. + await clerk.setActive({ session: verify.createdSessionId }) + } catch (error) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(error) + } + }) + } + ``` +
+
+ + + ```swift {{ filename: 'SMSOTPSignInView.swift', collapsible: true }} + import SwiftUI + import Clerk + + struct SMSOTPSignInView: View { + @State private var phoneNumber = "" + @State private var code = "" + @State private var isVerifying = false + + var body: some View { + if isVerifying { + TextField("Enter your verification code", text: $code) + Button("Verify") { + Task { await verify(code: code) } + } + } else { + TextField("Enter phone number", text: $phoneNumber) + Button("Continue") { + Task { await submit(phoneNumber: phoneNumber) } + } + } + } + } + + extension SMSOTPSignInView { + + func submit(phoneNumber: String) async { + do { + // Start the sign-in process using the phone number method. + let signIn = try await SignIn.create(strategy: .identifier(phoneNumber)) + + // Send the OTP code to the user. + try await signIn.prepareFirstFactor(strategy: .phoneCode()) + + // Set isVerifying to true to display second form + // and capture the OTP code. + isVerifying = true + } catch { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + dump(error) + } + } + + func verify(code: String) async { + do { + // Access the in progress sign in stored on the client object. + guard let inProgressSignIn = Clerk.shared.client?.signIn else { return } + + // Use the code provided by the user and attempt verification. + let signIn = try await inProgressSignIn.attemptFirstFactor(strategy: .phoneCode(code: code)) + + switch signIn.status { + case .complete: + // If verification was completed, navigate the user as needed. + dump(Clerk.shared.session) + default: + // If the status is not complete, check why. User may need to + // complete further steps. + dump(signIn.status) + } + } catch { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + dump(error) + } + } + } + ``` + + + + ```kotlin {{ filename: 'SMSOTPSignInViewModel.kt', collapsible: true }} + import androidx.lifecycle.ViewModel + import androidx.lifecycle.viewModelScope + import com.clerk.api.Clerk + import com.clerk.api.network.serialization.flatMap + import com.clerk.api.network.serialization.onFailure + import com.clerk.api.network.serialization.onSuccess + import com.clerk.api.signin.SignIn + import com.clerk.api.signin.attemptFirstFactor + import com.clerk.api.signin.prepareFirstFactor + import kotlinx.coroutines.flow.MutableStateFlow + import kotlinx.coroutines.flow.asStateFlow + import kotlinx.coroutines.flow.combine + import kotlinx.coroutines.flow.launchIn + import kotlinx.coroutines.launch + + class SMSOTPSignInViewModel : ViewModel() { + private val _uiState = MutableStateFlow(UiState.Unverified) + val uiState = _uiState.asStateFlow() + + init { + combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user -> + _uiState.value = + when { + !isInitialized -> UiState.Loading + user == null -> UiState.Unverified + else -> UiState.Verified + } + } + .launchIn(viewModelScope) + } + + fun submit(phoneNumber: String) { + viewModelScope.launch { + SignIn.create(SignIn.CreateParams.Strategy.PhoneCode(phoneNumber)).flatMap { + it + .prepareFirstFactor(SignIn.PrepareFirstFactorParams.PhoneCode()) + .onSuccess { _uiState.value = UiState.Verifying } + .onFailure { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + } + } + } + } + + fun verify(code: String) { + val inProgressSignIn = Clerk.signIn ?: return + viewModelScope.launch { + inProgressSignIn + .attemptFirstFactor(SignIn.AttemptFirstFactorParams.PhoneCode(code)) + .onSuccess { + if (it.status == SignIn.Status.COMPLETE) { + _uiState.value = UiState.Verified + } else { + // The user may need to complete further steps + } + } + .onFailure { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + } + } + } + + sealed interface UiState { + data object Loading : UiState + + data object Unverified : UiState + + data object Verifying : UiState + + data object Verified : UiState + } + } + ``` + + ```kotlin {{ filename: 'SMSOTPSignInActivity.kt', collapsible: true }} + import android.os.Bundle + import androidx.activity.ComponentActivity + import androidx.activity.compose.setContent + import androidx.activity.viewModels + import androidx.compose.foundation.layout.Arrangement + import androidx.compose.foundation.layout.Box + import androidx.compose.foundation.layout.Column + import androidx.compose.foundation.layout.fillMaxSize + import androidx.compose.material3.Button + import androidx.compose.material3.CircularProgressIndicator + import androidx.compose.material3.Text + import androidx.compose.material3.TextField + import androidx.compose.runtime.Composable + import androidx.compose.runtime.getValue + import androidx.compose.runtime.mutableStateOf + import androidx.compose.runtime.remember + import androidx.compose.runtime.setValue + import androidx.compose.ui.Alignment + import androidx.compose.ui.Modifier + import androidx.compose.ui.unit.dp + import androidx.lifecycle.compose.collectAsStateWithLifecycle + + class SMSOTPSignInActivity : ComponentActivity() { + val viewModel: SMSOTPSignInViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + val state by viewModel.uiState.collectAsStateWithLifecycle() + SMSOTPSignInView(state, viewModel::submit, viewModel::verify) + } + } + } + + @Composable + fun SMSOTPSignInView( + state: SMSOTPSignInViewModel.UiState, + onSubmit: (String) -> Unit, + onVerify: (String) -> Unit, + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + when (state) { + SMSOTPSignInViewModel.UiState.Unverified -> { + InputContent( + placeholder = "Enter your phone number", + buttonText = "Continue", + onClick = onSubmit, + ) + } + SMSOTPSignInViewModel.UiState.Verified -> { + Text("Verified") + } + SMSOTPSignInViewModel.UiState.Verifying -> { + InputContent( + placeholder = "Enter your verification code", + buttonText = "Verify", + onClick = onVerify, + ) + } + + SMSOTPSignInViewModel.UiState.Loading -> { + CircularProgressIndicator() + } + } + } + } + + @Composable + fun InputContent(placeholder: String, buttonText: String, onClick: (String) -> Unit) { + var value by remember { mutableStateOf("") } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + ) { + TextField(placeholder = { Text(placeholder) }, value = value, onValueChange = { value = it }) + Button(onClick = { onClick(value) }) { Text(buttonText) } + } + } + ``` + +
+ + To create a sign-in flow for email OTP, pass the value `email_code` as the first factor strategy. You can find all available methods in the [`SignIn`](/docs/reference/javascript/sign-in) object documentation. +
diff --git a/docs/guides/development/custom-flows/authentication/legacy/embedded-email-links.mdx b/docs/guides/development/custom-flows/authentication/legacy/embedded-email-links.mdx new file mode 100644 index 0000000000..b3117bf16d --- /dev/null +++ b/docs/guides/development/custom-flows/authentication/legacy/embedded-email-links.mdx @@ -0,0 +1,199 @@ +--- +title: Embeddable email links with sign-in tokens +description: Learn how to build custom embeddable email link sign-in flows to increase user engagement and reduce drop off in transactional emails, SMS's, and more. +--- + + + > [!WARNING] + > Expo does not support email links. You can request this feature on [Clerk's roadmap](https://feedback.clerk.com/). + + +An "email link" is a link that, when visited, will automatically authenticate your user so that they can perform some action on your site with less friction than if they had to sign in manually. You can create email links with Clerk by generating a sign-in token. + +Common use cases include: + +- Welcome emails when users are added off a waitlist +- Promotional emails for users +- Recovering abandoned carts +- Surveys or questionnaires + +This guide will demonstrate how to generate a sign-in token and use it to sign in a user. + + + ## Generate a sign-in token + + [Sign-in tokens](/docs/reference/backend-api/tag/sign-in-tokens/post/sign_in_tokens){{ target: '_blank' }} are JWTs that can be used to sign in to an application without specifying any credentials. A sign-in token can be used **once**, and can be consumed from the Frontend API using the [`ticket`](/docs/reference/javascript/sign-in#sign-in-create-params) strategy, which is demonstrated in the following example. + + > [!NOTE] + > By default, sign-in tokens expire in 30 days. You can optionally specify a different duration in seconds using the `expires_in_seconds` property. + + The following example demonstrates a cURL request that creates a valid sign-in token: + + ```bash + curl 'https://api.clerk.com/v1/sign_in_tokens' \ + -X POST \ + -H 'Authorization: Bearer {{secret}}' \ + -H 'Content-Type: application/json' \ + -d '{ "user_id": "user_123" }' + ``` + + This will return a `url` property, which can be used as your email link. Keep in mind that this link will use the [Account Portal sign-in page](/docs/guides/customizing-clerk/account-portal#sign-in) to sign in the user. + + If you would rather use your own sign-in page, you can use the `token` property that is returned. Add the `token` as a query param in any link, such as the following example: + + `https://your-site.com/accept-token?token=` + + Then, you can embed this link anywhere, such as an email. + + ## Build a custom flow for signing in with a sign-in token + + To handle email links with sign-in tokens, you must set up a page in your frontend that detects the token, signs the user in, and performs any additional actions you need. + + The following example demonstrates basic code that detects a token in the URL query params and uses it to initiate a sign-in with Clerk: + + + + + ```tsx {{ filename: 'app/accept-token/page.tsx' }} + 'use client' + import { useUser, useSignIn } from '@clerk/nextjs' + import { useEffect, useState } from 'react' + import { useSearchParams } from 'next/navigation' + + export default function Page() { + const [loading, setLoading] = useState(false) + const { signIn, setActive } = useSignIn() + const { isSignedIn, user } = useUser() + + // Get the token from the query params + const signInToken = useSearchParams().get('token') + + useEffect(() => { + if (!signIn || !setActive || !signInToken || user || loading) { + return + } + + const createSignIn = async () => { + setLoading(true) + try { + // Create the `SignIn` with the token + const signInAttempt = await signIn.create({ + strategy: 'ticket', + ticket: signInToken as string, + }) + + // If the sign-in was successful, set the session to active + if (signInAttempt.status === 'complete') { + setActive({ + session: signInAttempt.createdSessionId, + }) + } else { + // If the sign-in attempt is not complete, check why. + // User may need to complete further steps. + console.error(JSON.stringify(signInAttempt, null, 2)) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error('Error:', JSON.stringify(err, null, 2)) + } finally { + setLoading(false) + } + } + + createSignIn() + }, [signIn, setActive, signInToken, user, loading]) + + if (!signInToken) { + return
No token provided.
+ } + + if (!isSignedIn) { + // Handle signed out state + return null + } + + if (loading) { + return
Signing you in...
+ } + + return
Signed in as {user.id}
+ } + ``` + + ```tsx {{ filename: 'pages/accept-token.tsx' }} + import { InferGetServerSidePropsType, GetServerSideProps } from 'next' + import { useUser, useSignIn } from '@clerk/nextjs' + import { useEffect, useState } from 'react' + + // Get the token from the query param server-side, and pass through props + export const getServerSideProps: GetServerSideProps = async (context) => { + return { + props: { signInToken: context.query.token ? context.query.token : null }, + } + } + + export default function AcceptTokenPage({ + signInToken, + }: InferGetServerSidePropsType) { + const [loading, setLoading] = useState(false) + const { signIn, setActive } = useSignIn() + const { isSignedIn, user } = useUser() + + useEffect(() => { + if (!signIn || !setActive || !signInToken || user || loading) { + return + } + + const createSignIn = async () => { + setLoading(true) + try { + // Create the `SignIn` with the token + const signInAttempt = await signIn.create({ + strategy: 'ticket', + ticket: signInToken as string, + }) + + // If the sign-in was successful, set the session to active + if (signInAttempt.status === 'complete') { + setActive({ + session: signInAttempt.createdSessionId, + }) + } else { + // If the sign-in attempt is not complete, check why. + // User may need to complete further steps. + console.error(JSON.stringify(signInAttempt, null, 2)) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error('Error:', JSON.stringify(err, null, 2)) + setLoading(true) + } finally { + setLoading(false) + } + } + + createSignIn() + }, [signIn, setActive, signInToken, user, loading]) + + if (!signInToken) { + return
No token provided.
+ } + + if (loading) { + return
Loading...
+ } + + if (!isSignedIn) { + // Handle signed out state + return null + } + + return
Signed in as {user.id}
+ } + ``` +
+
+
+
diff --git a/docs/guides/development/custom-flows/authentication/legacy/enterprise-connections.mdx b/docs/guides/development/custom-flows/authentication/legacy/enterprise-connections.mdx new file mode 100644 index 0000000000..a63c2937fe --- /dev/null +++ b/docs/guides/development/custom-flows/authentication/legacy/enterprise-connections.mdx @@ -0,0 +1,170 @@ +--- +title: Build a custom flow for authenticating with enterprise connections +description: Learn how to use the Clerk API to build a custom sign-up and sign-in flow that supports enterprise connections. +--- + + + +## Before you start + +You must configure your application instance through the Clerk Dashboard for the enterprise connection(s) that you want to use. Visit [the appropriate guide for your platform](/docs/guides/configure/auth-strategies/enterprise-connections/overview) to learn how to configure your instance. + +## Create the sign-up and sign-in flow + + + + + + + ```tsx {{ filename: 'app/sign-in/page.tsx' }} + 'use client' + + import * as React from 'react' + import { useSignIn } from '@clerk/nextjs' + + export default function Page() { + const { signIn, isLoaded } = useSignIn() + + const signInWithEnterpriseSSO = (e: React.FormEvent) => { + e.preventDefault() + + if (!isLoaded) return null + + const email = (e.target as HTMLFormElement).email.value + + signIn + .authenticateWithRedirect({ + identifier: email, + strategy: 'enterprise_sso', + redirectUrl: '/sign-in/sso-callback', + redirectUrlComplete: '/', + }) + .then((res) => { + console.log(res) + }) + .catch((err: any) => { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.log(err.errors) + console.error(err, null, 2) + }) + } + + return ( +
signInWithEnterpriseSSO(e)}> + + +
+ ) + } + ``` + + ```jsx {{ filename: 'app/sign-in/sso-callback/page.tsx' }} + import { AuthenticateWithRedirectCallback } from '@clerk/nextjs' + + export default function Page() { + // Handle the redirect flow by calling the Clerk.handleRedirectCallback() method + // or rendering the prebuilt component. + // This is the final step in the custom Enterprise SSO flow. + return + } + ``` +
+
+ + + The following example **will both sign up _and_ sign in users**, eliminating the need for a separate sign-up page. + + The following example: + + 1. Uses the [`useSSO()`](/docs/reference/expo/use-sso) hook to access the `startSSOFlow()` method. + 1. Calls the `startSSOFlow()` method with the `strategy` param set to `enterprise_sso` and the `identifier` param set to the user's email address that they provided. The optional `redirect_url` param is also set in order to redirect the user once they finish the authentication flow. + - If authentication is successful, the `setActive()` method is called to set the active session with the new `createdSessionId`. + - If authentication is not successful, you can handle the missing requirements, such as MFA, using the [`signIn`](/docs/reference/javascript/sign-in) or [`signUp`](/docs/reference/javascript/sign-up) object returned from `startSSOFlow()`, depending on if the user is signing in or signing up. These objects include properties, like `status`, that can be used to determine the next steps. See the respective linked references for more information. + + ```tsx {{ filename: 'app/(auth)/sign-in.tsx', collapsible: true }} + import React, { useEffect, useState } from 'react' + import * as WebBrowser from 'expo-web-browser' + import * as AuthSession from 'expo-auth-session' + import { useSSO } from '@clerk/clerk-expo' + import { View, Button, TextInput, Platform } from 'react-native' + + export const useWarmUpBrowser = () => { + useEffect(() => { + // Preloads the browser for Android devices to reduce authentication load time + // See: https://docs.expo.dev/guides/authentication/#improving-user-experience + if (Platform.OS !== 'android') return + void WebBrowser.warmUpAsync() + return () => { + // Cleanup: closes browser when component unmounts + void WebBrowser.coolDownAsync() + } + }, []) + } + + // Handle any pending authentication sessions + WebBrowser.maybeCompleteAuthSession() + + export default function Page() { + useWarmUpBrowser() + + const [email, setEmail] = useState('') + + // Use the `useSSO()` hook to access the `startSSOFlow()` method + const { startSSOFlow } = useSSO() + + const onPress = async () => { + try { + // Start the authentication process by calling `startSSOFlow()` + const { createdSessionId, setActive, signIn, signUp } = await startSSOFlow({ + strategy: 'enterprise_sso', + identifier: email, + // For web, defaults to current path + // For native, you must pass a scheme, like AuthSession.makeRedirectUri({ scheme, path }) + // For more info, see https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturioptions + redirectUrl: AuthSession.makeRedirectUri(), + }) + + // If sign in was successful, set the active session + if (createdSessionId) { + setActive!({ + session: createdSessionId, + navigate: async ({ session }) => { + if (session?.currentTask) { + // Check for tasks and navigate to custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + console.log(session?.currentTask) + return + } + + router.push('/') + }, + }) + } else { + // If there is no `createdSessionId`, + // there are missing requirements, such as MFA + // Use the `signIn` or `signUp` returned from `startSSOFlow` + // to handle next steps + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + } + } + + return ( + + + +
+ ) + } + ``` + + ```tsx {{ filename: 'app/sign-in/sso-callback/page.tsx' }} + import { AuthenticateWithRedirectCallback } from '@clerk/nextjs' + + export default function Page() { + // Handle the redirect flow by calling the Clerk.handleRedirectCallback() method + // or rendering the prebuilt component. + return ( + <> + + + {/* Required for sign-up flows + Clerk's bot sign-up protection is enabled by default */} +
+ + ) + } + ``` + + + + + The following example **will both sign up _and_ sign in users**, eliminating the need for a separate sign-up page. + + The following example: + + 1. Uses the [`useSSO()`](/docs/reference/expo/use-sso) hook to access the `startSSOFlow()` method. + 1. Calls the `startSSOFlow()` method with the `strategy` param set to `oauth_google`, but you can use any of the [supported OAuth strategies](/docs/reference/javascript/types/sso#o-auth-strategy). The optional `redirect_url` param is also set in order to redirect the user once they finish the authentication flow. + - If authentication is successful, the `setActive()` method is called to set the active session with the new `createdSessionId`. + - If authentication is not successful, you can [handle the missing requirements](#handle-missing-requirements), such as MFA, using the [`signIn`](/docs/reference/javascript/sign-in) or [`signUp`](/docs/reference/javascript/sign-up) object returned from `startSSOFlow()`, depending on if the user is signing in or signing up. These objects include properties, like `status`, that can be used to determine the next steps. See the respective linked references for more information. + + ```tsx {{ filename: 'app/(auth)/sign-in.tsx', collapsible: true }} + import React, { useCallback, useEffect } from 'react' + import * as WebBrowser from 'expo-web-browser' + import * as AuthSession from 'expo-auth-session' + import { useSSO } from '@clerk/clerk-expo' + import { View, Button, Platform } from 'react-native' + + // Preloads the browser for Android devices to reduce authentication load time + // See: https://docs.expo.dev/guides/authentication/#improving-user-experience + export const useWarmUpBrowser = () => { + useEffect(() => { + if (Platform.OS !== 'android') return + void WebBrowser.warmUpAsync() + return () => { + // Cleanup: closes browser when component unmounts + void WebBrowser.coolDownAsync() + } + }, []) + } + + // Handle any pending authentication sessions + WebBrowser.maybeCompleteAuthSession() + + export default function Page() { + useWarmUpBrowser() + + // Use the `useSSO()` hook to access the `startSSOFlow()` method + const { startSSOFlow } = useSSO() + + const onPress = useCallback(async () => { + try { + // Start the authentication process by calling `startSSOFlow()` + const { createdSessionId, setActive, signIn, signUp } = await startSSOFlow({ + strategy: 'oauth_google', + // For web, defaults to current path + // For native, you must pass a scheme, like AuthSession.makeRedirectUri({ scheme, path }) + // For more info, see https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturioptions + redirectUrl: AuthSession.makeRedirectUri(), + }) + + // If sign in was successful, set the active session + if (createdSessionId) { + setActive!({ + session: createdSessionId, + // Check for session tasks and navigate to custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + navigate: async ({ session }) => { + if (session?.currentTask) { + console.log(session?.currentTask) + router.push('/sign-in/tasks') + return + } + + router.push('/') + }, + }) + } else { + // If there is no `createdSessionId`, + // there are missing requirements, such as MFA + // See https://clerk.com/docs/guides/development/custom-flows/authentication/oauth-connections#handle-missing-requirements + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error(JSON.stringify(err, null, 2)) + } + }, []) + + return ( + + + +
+ ) + } + + // Handle other statuses if needed + return ( + <> + {/* Required for sign-up flows + Clerk's bot sign-up protection is enabled by default */} +
+ + ) + } + ``` + + ```tsx {{ filename: 'app/sign-in/sso-callback/page.tsx' }} + import { AuthenticateWithRedirectCallback } from '@clerk/nextjs' + + export default function Page() { + // Set the `continueSignUpUrl` to the route of your "Continue" page + // Once a user authenticates with the OAuth provider, they will be redirected to that route + return ( + <> + + + {/* Required for sign-up flows + Clerk's bot sign-up protection is enabled by default */} +
+ + ) + } + ``` + + + diff --git a/docs/guides/development/custom-flows/authentication/legacy/passkeys.mdx b/docs/guides/development/custom-flows/authentication/legacy/passkeys.mdx new file mode 100644 index 0000000000..5d40dda194 --- /dev/null +++ b/docs/guides/development/custom-flows/authentication/legacy/passkeys.mdx @@ -0,0 +1,197 @@ +--- +title: Build a custom authentication flow using passkeys +description: Learn how to use the Clerk API to build a custom authentication flow using passkeys. +--- + + + +Clerk supports passwordless authentication via [passkeys](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#passkeys), enabling users to sign in without having to remember a password. Instead, users select a passkey associated with their device, which they can use to authenticate themselves. + +This guide demonstrates how to use the Clerk API to build a custom user interface for creating, signing users in with, and managing passkeys. + +## Enable passkeys + +To use passkeys, you must first enable it for your application. + +1. In the Clerk Dashboard, navigate to the [**User & authentication**](https://dashboard.clerk.com/last-active?path=user-authentication/user-and-authentication) page. +1. Select the **Passkeys** tab and enable **Sign-in with passkey**. + +### Domain restrictions for passkeys + + + +## Create user passkeys + +To create a passkey for a user, you must call [`User.createPasskey()`](/docs/reference/javascript/user#create-passkey), as shown in the following example: + +```tsx {{ filename: 'app/components/CustomCreatePasskeysButton.tsx' }} +export function CreatePasskeyButton() { + const { isSignedIn, user } = useUser() + + const createClerkPasskey = async () => { + if (!isSignedIn) { + // Handle signed out state + return + } + + try { + await user?.createPasskey() + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error('Error:', JSON.stringify(err, null, 2)) + } + } + + return +} +``` + +## Sign a user in with a passkey + +To sign a user into your Clerk app with a passkey, you must call [`SignIn.authenticateWithPasskey()`](/docs/reference/javascript/sign-in#authenticate-with-passkey). This method allows users to choose from their discoverable passkeys, such as hardware keys or passkeys in password managers. + +```tsx {{ filename: 'components/SignInWithPasskeyButton.tsx' }} +export function SignInWithPasskeyButton() { + const { signIn } = useSignIn() + const router = useRouter() + + const signInWithPasskey = async () => { + // 'discoverable' lets the user choose a passkey + // without auto-filling any of the options + try { + const signInAttempt = await signIn?.authenticateWithPasskey({ + flow: 'discoverable', + }) + + if (signInAttempt?.status === 'complete') { + await setActive({ + session: signInAttempt.createdSessionId, + redirectUrl: '/', + navigate: async ({ session }) => { + if (session?.currentTask) { + // Check for tasks and navigate to custom UI to help users resolve them + // See https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks + console.log(session?.currentTask) + return + } + + router.push('/') + }, + }) + } else { + // If the status is not complete, check why. User may need to + // complete further steps. + console.error(JSON.stringify(signInAttempt, null, 2)) + } + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error('Error:', JSON.stringify(err, null, 2)) + } + } + + return +} +``` + +## Rename user passkeys + +Clerk generates a name based on the device associated with the passkey when it's created. Sometimes users may want to rename a passkey to make it easier to identify. + +To rename a user's passkey in your Clerk app, you must call the [`update()`](/docs/reference/javascript/types/passkey-resource#update) method of the passkey object, as shown in the following example: + +```tsx {{ filename: 'components/RenamePasskeyUI.tsx' }} +export function RenamePasskeyUI() { + const { user } = useUser() + const { passkeys } = user + + const passkeyToUpdateId = useRef(null) + const newPasskeyName = useRef(null) + const [success, setSuccess] = useState(false) + + const renamePasskey = async () => { + try { + const passkeyToUpdate = passkeys?.find( + (pk: PasskeyResource) => pk.id === passkeyToUpdateId.current?.value, + ) + + await passkeyToUpdate?.update({ + name: newPasskeyName.current?.value, + }) + + setSuccess(true) + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error('Error:', JSON.stringify(err, null, 2)) + setSuccess(false) + } + } + + return ( + <> +

Passkeys:

+
    + {passkeys?.map((pk: PasskeyResource) => { + return ( +
  • + Name: {pk.name} | ID: {pk.id} +
  • + ) + })} +
+ + + +

Passkey updated: {success ? 'Yes' : 'No'}

+ + ) +} +``` + +## Delete user passkeys + +To delete a user's passkey from your Clerk app, you must call the [`delete()`](/docs/reference/javascript/types/passkey-resource#delete) method of the passkey object, as shown in the following example: + +```tsx {{ filename: 'components/DeletePasskeyUI.tsx' }} +export function DeletePasskeyUI() { + const { user } = useUser() + const { passkeys } = user + + const passkeyToDeleteId = useRef(null) + const [success, setSuccess] = useState(false) + + const deletePasskey = async () => { + const passkeyToDelete = passkeys?.find((pk: any) => pk.id === passkeyToDeleteId.current?.value) + try { + await passkeyToDelete?.delete() + + setSuccess(true) + } catch (err) { + // See https://clerk.com/docs/guides/development/custom-flows/error-handling + // for more info on error handling + console.error('Error:', JSON.stringify(err, null, 2)) + setSuccess(false) + } + } + + return ( + <> +

Passkeys:

+
    + {passkeys?.map((pk: any) => { + return ( +
  • + Name: {pk.name} | ID: {pk.id} +
  • + ) + })} +
+ + +

Passkey deleted: {success ? 'Yes' : 'No'}

+ + ) +} +``` diff --git a/docs/guides/development/custom-flows/authentication/sign-out.mdx b/docs/guides/development/custom-flows/authentication/sign-out.mdx index 01ee0d2ba1..4f0843eea5 100644 --- a/docs/guides/development/custom-flows/authentication/sign-out.mdx +++ b/docs/guides/development/custom-flows/authentication/sign-out.mdx @@ -1,6 +1,7 @@ --- title: Build a custom sign-out flow description: Learn how to use the Clerk API to build a custom sign-out flow using Clerk's signOut() function. +sdk: nextjs, react, expo, js-frontend, react-router, tanstack-react-start, ios, android --- diff --git a/docs/manifest.json b/docs/manifest.json index a6259f5b79..1cba89d6fd 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -993,6 +993,50 @@ { "title": "Bot sign-up protection", "href": "/docs/guides/development/custom-flows/authentication/bot-sign-up-protection" + }, + { + "title": "Legacy APIs", + "collapse": true, + "items": [ + [ + { + "title": "Email & password", + "href": "/docs/guides/development/custom-flows/authentication/legacy/email-password" + }, + { + "title": "Email / SMS OTP", + "href": "/docs/guides/development/custom-flows/authentication/legacy/email-sms-otp" + }, + { + "title": "Email links", + "href": "/docs/guides/development/custom-flows/authentication/legacy/email-links" + }, + { + "title": "Email & password + MFA", + "href": "/docs/guides/development/custom-flows/authentication/legacy/email-password-mfa" + }, + { + "title": "Passkeys", + "href": "/docs/guides/development/custom-flows/authentication/legacy/passkeys" + }, + { + "title": "OAuth connections", + "href": "/docs/guides/development/custom-flows/authentication/legacy/oauth-connections" + }, + { + "title": "Enterprise connections", + "href": "/docs/guides/development/custom-flows/authentication/legacy/enterprise-connections" + }, + { + "title": "Sign-up with application invitations", + "href": "/docs/guides/development/custom-flows/authentication/legacy/application-invitations" + }, + { + "title": "Embedded email links", + "href": "/docs/guides/development/custom-flows/authentication/legacy/embedded-email-links" + } + ] + ] } ] ] @@ -1602,10 +1646,18 @@ "title": "`SignIn`", "href": "/docs/reference/javascript/sign-in" }, + { + "title": "`SignInFuture`", + "href": "/docs/reference/javascript/sign-in-future" + }, { "title": "`SignUp`", "href": "/docs/reference/javascript/sign-up" }, + { + "title": "`SignUpFuture`", + "href": "/docs/reference/javascript/sign-up-future" + }, { "title": "`Organization`", "href": "/docs/reference/javascript/organization" diff --git a/docs/reference/javascript/sign-in-future.mdx b/docs/reference/javascript/sign-in-future.mdx new file mode 100644 index 0000000000..daff283292 --- /dev/null +++ b/docs/reference/javascript/sign-in-future.mdx @@ -0,0 +1,596 @@ +--- +title: '`SignInFuture`' +description: The current active `SignIn` instance, for use in custom flows. +sdk: js-frontend +--- + + + +The `SignInFuture` object holds the state of the current sign-in attempt and provides methods to drive custom sign-in flows, including first- and second-factor verifications, SSO, ticket-based, and Web3-based authentication. + +TKTKTK + +## Properties + + + - `id` + - `string | undefined` + + The unique identifier for the current sign-in attempt. + + --- + + - `supportedFirstFactors` + - [SignInFirstFactor](/docs/reference/javascript/types/sign-in-first-factor)\[] + + The list of first-factor strategies that are available for the current sign-in attempt. + + --- + + - `supportedSecondFactors` + - [SignInSecondFactor](/docs/reference/javascript/types/sign-in-second-factor)\[] + + The list of second-factor strategies that are available for the current sign-in attempt. + + --- + + - `status` + - `SignInStatus` + + The current status of the sign-in. `SignInStatus` supports the following values: + + - `'complete'`: The user is signed in and the custom flow can proceed to `signIn.finalize()` to create a session. + - `'needs_identifier'`: The user's identifier (e.g., email address, phone number, username) hasn't been provided. + - `'needs_first_factor'`: One of the following [first factor verification strategies](/docs/reference/javascript/sign-in) is missing: `'email_link'`, `'email_code'`, `'phone_code'`, `'web3_base_signature'`, `'web3_metamask_signature'`, `'web3_coinbase_wallet_signature'` or `'oauth_provider'`. + - `'needs_second_factor'`: One of the following [second factor verification strategies](/docs/reference/javascript/sign-in) is missing: `'phone_code'` or `'totp'`. + - `'needs_new_password'`: The user needs to set a new password. + + --- + + - `isTransferable` + - `boolean` + + Indicates that there is not a matching user for the first-factor verification used, and that the sign-in can be transferred to a sign-up. + + --- + + - `existingSession` + - `{ sessionId: string } | undefined` + + TKTKTK + + --- + + - `firstFactorVerification` + - [`Verification`](/docs/reference/javascript/types/verification) + + TKTKTK + + --- + + - `secondFactorVerification` + - [`Verification`](/docs/reference/javascript/types/verification) + + The second-factor verification for the current sign-in attempt. + + --- + + - `identifier` + - `string | null` + + The identifier for the current sign-in attempt. + + --- + + - `createdSessionId` + - `string | null` + + The created session ID for the current sign-in attempt. + + --- + + - `userData` + - `UserData` + + The user data for the current sign-in attempt. + + +## Methods + +### `create()` + +Used to supply an identifier for the sign-in attempt. Calling this method will populate data on the sign-in attempt, such as `signIn.resource.supportedFirstFactors`. + +```ts +function create(params: SignInFutureCreateParams): Promise<{ error: unknown }> +``` + +#### SignInFutureCreateParams + +TKTKTK + + + - `identifier?` + - `string` + + TKTKTK + + --- + + - `strategy?` + - [OAuthStrategy](/docs/reference/javascript/types/sso#o-auth-strategy) | 'saml' | 'enterprise\_sso' + + TKTKTK + + --- + + - `redirectUrl?` + - `string` + + TKTKTK + + --- + + - `actionCompleteRedirectUrl?` + - `string` + + TKTKTK + + --- + + - `transfer?` + - `boolean` + + TKTKTK + + --- + + - `ticket?` + - `string` + + TKTKTK + + +### `password()` + +Used to submit a password to sign-in. + +```ts +function password(params: SignInFuturePasswordParams): Promise<{ error: unknown }> +``` + +#### SignInFuturePasswordParams + +TKTKTK + +One of the following shapes is supported (exactly one identifier field may be provided): + +- `{ password: string; identifier: string }` +- `{ password: string; emailAddress: string }` +- `{ password: string; phoneNumber: string }` +- `{ password: string }` + + + * `password` + * `string` + + TKTKTK + + --- + + - `identifier?` + - `string` + + TKTKTK + + --- + + - `emailAddress?` + - `string` + + TKTKTK + + --- + + - `phoneNumber?` + - `string` + + TKTKTK + + +### `emailCode.sendCode()` + +Used to send an email code to sign-in + +```ts +function sendCode(params: SignInFutureEmailCodeSendParams): Promise<{ error: unknown }> +``` + +#### SignInFutureEmailCodeSendParams + +TKTKTK + +Provide either `emailAddress` or `emailAddressId`. + + + - `emailAddress?` + - `string` + + TKTKTK + + --- + + - `emailAddressId?` + - `string` + + TKTKTK + + +### `emailCode.verifyCode()` + +Used to verify a code sent via email to sign-in + +```ts +function verifyCode(params: SignInFutureEmailCodeVerifyParams): Promise<{ error: unknown }> +``` + +#### SignInFutureEmailCodeVerifyParams + +TKTKTK + + + - `code` + - `string` + + TKTKTK + + +### `emailLink.sendLink()` + +Used to send an email link to sign-in + +```ts +function sendLink(params: SignInFutureEmailLinkSendParams): Promise<{ error: unknown }> +``` + +#### SignInFutureEmailLinkSendParams + +TKTKTK + +Provide either `emailAddress` or `emailAddressId` along with `verificationUrl`. + + + - `emailAddress?` + - `string` + + TKTKTK + + --- + + - `emailAddressId?` + - `string` + + TKTKTK + + --- + + - `verificationUrl` + - `string` + + TKTKTK + + +### `emailLink.waitForVerification()` + +Will wait for verification to complete or expire + +```ts +function waitForVerification(): Promise<{ error: unknown }> +``` + +### `phoneCode.sendCode()` + +Used to send a phone code to sign-in + +```ts +function sendCode(params: SignInFuturePhoneCodeSendParams): Promise<{ error: unknown }> +``` + +#### SignInFuturePhoneCodeSendParams + +TKTKTK + +Provide either `phoneNumber` or `phoneNumberId`. Optionally specify the `channel`. + + + - `phoneNumber?` + - `string` + + TKTKTK + + --- + + - `phoneNumberId?` + - `string` + + TKTKTK + + --- + + - `channel?` + - `PhoneCodeChannel` + + TKTKTK + + +### `phoneCode.verifyCode()` + +Used to verify a code sent via phone to sign-in + +```ts +function verifyCode(params: SignInFuturePhoneCodeVerifyParams): Promise<{ error: unknown }> +``` + +#### SignInFuturePhoneCodeVerifyParams + +TKTKTK + + + - `code` + - `string` + + TKTKTK + + +### `resetPasswordEmailCode.sendCode()` + +Used to send a password reset code to the first email address on the account + +```ts +function sendCode(): Promise<{ error: unknown }> +``` + +### `resetPasswordEmailCode.verifyCode()` + +Used to verify a password reset code sent via email. Will cause `signIn.status` to become `'needs_new_password'`. + +```ts +function verifyCode(params: SignInFutureEmailCodeVerifyParams): Promise<{ error: unknown }> +``` + +#### SignInFutureEmailCodeVerifyParams + +TKTKTK + + + - `code` + - `string` + + TKTKTK + + +### `resetPasswordEmailCode.submitPassword()` + +Used to submit a new password, and move the `signIn.status` to `'complete'`. + +```ts +function submitPassword(params: SignInFutureResetPasswordSubmitParams): Promise<{ error: unknown }> +``` + +#### SignInFutureResetPasswordSubmitParams + +TKTKTK + + + - `password` + - `string` + + TKTKTK + + --- + + - `signOutOfOtherSessions?` + - `boolean` + + TKTKTK + + +### `sso()` + +Used to perform OAuth authentication. + +```ts +function sso(params: SignInFutureSSOParams): Promise<{ error: unknown }> +``` + +#### SignInFutureSSOParams + +TKTKTK + + + - `flow?` + - `'auto' | 'modal'` + + TKTKTK + + --- + + - `strategy` + - [OAuthStrategy](/docs/reference/javascript/types/sso#o-auth-strategy) | 'saml' | 'enterprise\_sso' + + TKTKTK + + --- + + - `redirectUrl` + - `string` + + The URL to redirect to after the user has completed the SSO flow. + + --- + + - `redirectCallbackUrl` + - `string` + + TODO @revamp-hooks: This should be handled by FAPI instead. + + +### `mfa.sendPhoneCode()` + +Used to send a phone code as a second factor to sign-in + +```ts +function sendPhoneCode(): Promise<{ error: unknown }> +``` + +### `mfa.verifyPhoneCode()` + +Used to verify a phone code sent as a second factor to sign-in + +```ts +function verifyPhoneCode(params: SignInFutureMFAPhoneCodeVerifyParams): Promise<{ error: unknown }> +``` + +#### SignInFutureMFAPhoneCodeVerifyParams + +TKTKTK + + + - `code` + - `string` + + TKTKTK + + +### `mfa.verifyTOTP()` + +Used to verify a TOTP code as a second factor to sign-in + +```ts +function verifyTOTP(params: SignInFutureTOTPVerifyParams): Promise<{ error: unknown }> +``` + +#### SignInFutureTOTPVerifyParams + +TKTKTK + + + - `code` + - `string` + + TKTKTK + + +### `mfa.verifyBackupCode()` + +Used to verify a backup code as a second factor to sign-in + +```ts +function verifyBackupCode(params: SignInFutureBackupCodeVerifyParams): Promise<{ error: unknown }> +``` + +#### SignInFutureBackupCodeVerifyParams + +TKTKTK + + + - `code` + - `string` + + TKTKTK + + +### `ticket()` + +Used to perform a ticket-based sign-in. + +```ts +function ticket(params?: SignInFutureTicketParams): Promise<{ error: unknown }> +``` + +#### SignInFutureTicketParams + +TKTKTK + + + - `ticket` + - `string` + + TKTKTK + + +### `web3()` + +Used to perform a Web3-based sign-in. + +```ts +function web3(params: SignInFutureWeb3Params): Promise<{ error: unknown }> +``` + +#### SignInFutureWeb3Params + +TKTKTK + + + - `strategy` + - `Web3Strategy` + + TKTKTK + + +### `finalize()` + +Used to convert a sign-in with `status === 'complete'` into an active session. Will cause anything observing the session state (such as the `useUser()` hook) to update automatically. + +```ts +function finalize(params?: SignInFutureFinalizeParams): Promise<{ error: unknown }> +``` + +#### SignInFutureFinalizeParams + +TKTKTK + + + - `navigate?` + - `SetActiveNavigate` + + TKTKTK + + +## Types + +### `Email Link Verification` + +The shape of `emailLink.verification` when present. + + + - `status` + - `'verified' | 'expired' | 'failed' | 'client_mismatch'` + + The verification status + + --- + + - `createdSessionId` + - `string` + + The created session ID + + --- + + - `verifiedFromTheSameClient` + - `boolean` + + Whether the verification was from the same client + + +### `ExistingSession` + +The shape of `existingSession` when present. + + + - `sessionId` + - `string` + + TKTKTK + diff --git a/docs/reference/javascript/sign-up-future.mdx b/docs/reference/javascript/sign-up-future.mdx new file mode 100644 index 0000000000..cd923d34e2 --- /dev/null +++ b/docs/reference/javascript/sign-up-future.mdx @@ -0,0 +1,568 @@ +--- +title: '`SignUpFuture`' +description: The current active `SignUp` instance, for use in custom flows. +sdk: js-frontend +--- + + + +The `SignUpFuture` object holds the state of the current sign-up attempt and provides methods to drive custom sign-up flows, including email/phone verification, password, SSO, ticket-based, and Web3-based account creation. + +TKTKTK + +## Properties + + + - `id` + - `string | undefined` + + The unique identifier for the current sign-up attempt. + + --- + + - `status` + - `SignUpStatus` + + The status of the current sign-up. The following values are possible: + + - `complete:` The user has been created and the custom flow can proceed to `signUp.finalize()` to create session. + - `missing_requirements:` A requirement is unverified or missing from the [**User & authentication**](https://dashboard.clerk.com/last-active?path=user-authentication/user-and-authentication) settings. For example, in the Clerk Dashboard, the **Password** setting is required but a password wasn't provided in the custom flow. + - `abandoned:` The sign-up has been inactive for over 24 hours. + + --- + + - `requiredFields` + - `SignUpField[]` + + The list of required fields for the current sign-up attempt. + + --- + + - `optionalFields` + - `SignUpField[]` + + The list of optional fields for the current sign-up attempt. + + --- + + - `missingFields` + - `SignUpField[]` + + The list of missing fields for the current sign-up attempt. + + --- + + - `unverifiedFields` + - `SignUpIdentificationField[]` + + An array of strings representing unverified fields such as `’email_address’`. Can be used to detect when verification is necessary. + + --- + + - `isTransferable` + - `boolean` + + Indicates that there is a matching user for provided identifier, and that the sign-up can be transferred to a sign-in. + + --- + + - `existingSession` + - `{ sessionId: string } | undefined` + + TKTKTK + + --- + + - `username` + - `string | null` + + TKTKTK + + --- + + - `firstName` + - `string | null` + + TKTKTK + + --- + + - `lastName` + - `string | null` + + TKTKTK + + --- + + - `emailAddress` + - `string | null` + + TKTKTK + + --- + + - `phoneNumber` + - `string | null` + + TKTKTK + + --- + + - `web3Wallet` + - `string | null` + + TKTKTK + + --- + + - `hasPassword` + - `boolean` + + TKTKTK + + --- + + - `unsafeMetadata` + - `SignUpUnsafeMetadata` + + TKTKTK + + --- + + - `createdSessionId` + - `string | null` + + TKTKTK + + --- + + - `createdUserId` + - `string | null` + + TKTKTK + + --- + + - `abandonAt` + - `number | null` + + TKTKTK + + --- + + - `legalAcceptedAt` + - `number | null` + + TKTKTK + + +## Methods + +### `create()` + +TKTKTK + +```ts +function create(params: SignUpFutureCreateParams): Promise<{ error: unknown }> +``` + +#### SignUpFutureCreateParams + +TKTKTK + + + - `emailAddress?` + - `string` + + TKTKTK + + --- + + - `phoneNumber?` + - `string` + + TKTKTK + + --- + + - `username?` + - `string` + + TKTKTK + + --- + + - `transfer?` + - `boolean` + + TKTKTK + + --- + + - `ticket?` + - `string` + + TKTKTK + + --- + + - `web3Wallet?` + - `string` + + TKTKTK + + --- + + - `firstName?` + - `string` + + TKTKTK + + --- + + - `lastName?` + - `string` + + TKTKTK + + --- + + - `unsafeMetadata?` + - `SignUpUnsafeMetadata` + + TKTKTK + + --- + + - `legalAccepted?` + - `boolean` + + TKTKTK + + +### `update()` + +TKTKTK + +```ts +function update(params: SignUpFutureUpdateParams): Promise<{ error: unknown }> +``` + +#### SignUpFutureUpdateParams + +TKTKTK + + + - `firstName?` + - `string` + + TKTKTK + + --- + + - `lastName?` + - `string` + + TKTKTK + + --- + + - `unsafeMetadata?` + - `SignUpUnsafeMetadata` + + TKTKTK + + --- + + - `legalAccepted?` + - `boolean` + + TKTKTK + + +### `verifications.sendEmailCode()` + +Used to send an email code to verify an email address. + +```ts +function sendEmailCode(): Promise<{ error: unknown }> +``` + +### `verifications.verifyEmailCode()` + +Used to verify a code sent via email. + +```ts +function verifyEmailCode(params: SignUpFutureEmailCodeVerifyParams): Promise<{ error: unknown }> +``` + +#### SignUpFutureEmailCodeVerifyParams + +TKTKTK + + + - `code` + - `string` + + TKTKTK + + +### `verifications.sendPhoneCode()` + +Used to send a phone code to verify a phone number. + +```ts +function sendPhoneCode(params: SignUpFuturePhoneCodeSendParams): Promise<{ error: unknown }> +``` + +#### SignUpFuturePhoneCodeSendParams + +TKTKTK + + + - `phoneNumber?` + - `string` + + TKTKTK + + --- + + - `channel?` + - `PhoneCodeChannel` + + TKTKTK + + +### `verifications.verifyPhoneCode()` + +Used to verify a code sent via phone. + +```ts +function verifyPhoneCode(params: SignUpFuturePhoneCodeVerifyParams): Promise<{ error: unknown }> +``` + +#### SignUpFuturePhoneCodeVerifyParams + +TKTKTK + + + - `code` + - `string` + + TKTKTK + + +### `password()` + +Used to sign up using an email address and password. + +```ts +function password(params: SignUpFuturePasswordParams): Promise<{ error: unknown }> +``` + +#### SignUpFuturePasswordParams + +TKTKTK + +Must include `password` and exactly one of `emailAddress`, `phoneNumber`, or `username`. You can also provide additional optional fields. + + + - `password` + - `string` + + TKTKTK + + --- + + - `emailAddress?` + - `string` + + TKTKTK + + --- + + - `phoneNumber?` + - `string` + + TKTKTK + + --- + + - `username?` + - `string` + + TKTKTK + + --- + + - `firstName?` + - `string` + + TKTKTK + + --- + + - `lastName?` + - `string` + + TKTKTK + + --- + + - `unsafeMetadata?` + - `SignUpUnsafeMetadata` + + TKTKTK + + --- + + - `legalAccepted?` + - `boolean` + + TKTKTK + + +### `sso()` + +Used to create an account using an OAuth connection. + +```ts +function sso(params: SignUpFutureSSOParams): Promise<{ error: unknown }> +``` + +#### SignUpFutureSSOParams + +TKTKTK + + + - `strategy` + - `string` + + TKTKTK + + --- + + - `redirectUrl` + - `string` + + The URL to redirect to after the user has completed the SSO flow. + + --- + + - `redirectCallbackUrl` + - `string` + + TODO @revamp-hooks: This should be handled by FAPI instead. + + +### `ticket()` + +Used to perform a ticket-based sign-up. + +```ts +function ticket(params?: SignUpFutureTicketParams): Promise<{ error: unknown }> +``` + +#### SignUpFutureTicketParams + +TKTKTK + + + - `ticket` + - `string` + + TKTKTK + + --- + + - `firstName?` + - `string` + + TKTKTK + + --- + + - `lastName?` + - `string` + + TKTKTK + + --- + + - `unsafeMetadata?` + - `SignUpUnsafeMetadata` + + TKTKTK + + --- + + - `legalAccepted?` + - `boolean` + + TKTKTK + + +### `web3()` + +Used to perform a Web3-based sign-up. + +```ts +function web3(params: SignUpFutureWeb3Params): Promise<{ error: unknown }> +``` + +#### SignUpFutureWeb3Params + +TKTKTK + + + - `strategy` + - `Web3Strategy` + + TKTKTK + + --- + + - `unsafeMetadata?` + - `SignUpUnsafeMetadata` + + TKTKTK + + --- + + - `legalAccepted?` + - `boolean` + + TKTKTK + + +### `finalize()` + +Used to convert a sign-up with `status === 'complete'` into an active session. Will cause anything observing the session state (such as the `useUser()` hook) to update automatically. + +```ts +function finalize(params?: SignUpFutureFinalizeParams): Promise<{ error: unknown }> +``` + +#### SignUpFutureFinalizeParams + +TKTKTK + + + - `navigate?` + - `SetActiveNavigate` + + TKTKTK + + +## Types + +### `ExistingSession` + +The shape of `existingSession` when present. + + + - `sessionId` + - `string` + + TKTKTK +