From c284d0a0b4154fde1d00ad456e29e84be663b994 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Tue, 7 Oct 2025 15:06:50 -0500
Subject: [PATCH 01/18] Add initial docs for SignInFuture and SignUpFuture
---
.../email-password/sign-in-nextjs.mdx | 43 +
.../email-password/sign-up-nextjs.mdx | 65 +
.../custom-flows/future-api-callout.mdx | 2 +
.../authentication/email-password.mdx | 1399 +++++------------
docs/manifest.json | 8 +
docs/reference/javascript/sign-in-future.mdx | 596 +++++++
docs/reference/javascript/sign-up-future.mdx | 568 +++++++
7 files changed, 1669 insertions(+), 1012 deletions(-)
create mode 100644 docs/_partials/custom-flows/email-password/sign-in-nextjs.mdx
create mode 100644 docs/_partials/custom-flows/email-password/sign-up-nextjs.mdx
create mode 100644 docs/_partials/custom-flows/future-api-callout.mdx
create mode 100644 docs/reference/javascript/sign-in-future.mdx
create mode 100644 docs/reference/javascript/sign-up-future.mdx
diff --git a/docs/_partials/custom-flows/email-password/sign-in-nextjs.mdx b/docs/_partials/custom-flows/email-password/sign-in-nextjs.mdx
new file mode 100644
index 0000000000..fb79dff6fd
--- /dev/null
+++ b/docs/_partials/custom-flows/email-password/sign-in-nextjs.mdx
@@ -0,0 +1,43 @@
+This example is written for Next.js App Router but it can be adapted for any React-based framework.
+
+```tsx {{ filename: 'app/sign-in/page.tsx', collapsible: true }}
+'use client'
+
+import * as React from 'react'
+import { useAuth, useSignIn } from '@clerk/nextjs'
+import { useRouter } from 'next/navigation'
+
+export default function SignInPage() {
+ const { signIn, errors, fetchStatus } = useSignIn()
+ const { isSignedIn } = useAuth()
+
+ const handleSubmit = async (formData: FormData) => {
+ const email = formData.get('email') as string
+ const password = formData.get('password') as string
+
+ await signIn.password({
+ email,
+ password,
+ })
+ if (signIn.status === 'complete') {
+ await signIn.finalize({
+ navigate: () => {
+ router.push('/dashboard')
+ },
+ })
+ }
+ }
+
+ if (signIn.status === 'complete' || isSignedIn) {
+ return null
+ }
+
+ return (
+
+ )
+}
+```
diff --git a/docs/_partials/custom-flows/email-password/sign-up-nextjs.mdx b/docs/_partials/custom-flows/email-password/sign-up-nextjs.mdx
new file mode 100644
index 0000000000..f93cc57cab
--- /dev/null
+++ b/docs/_partials/custom-flows/email-password/sign-up-nextjs.mdx
@@ -0,0 +1,65 @@
+This example is written for Next.js App Router but it can be adapted for any React-based framework.
+
+```tsx {{ filename: 'app/sign-up/page.tsx', collapsible: true }}
+'use client'
+
+import * as React from 'react'
+import { useAuth, useSignUp } from '@clerk/nextjs'
+import { useRouter } from 'next/navigation'
+
+export default function SignUpPage() {
+ const { signUp, errors, fetchStatus } = useSignUp()
+ const { isSignedIn } = useAuth()
+
+ const handleSubmit = async (formData: FormData) => {
+ const email = formData.get('email') as string
+ const password = formData.get('password') as string
+
+ await signUp.password({
+ email,
+ password,
+ })
+
+ await signUp.verifications.sendEmailCode()
+ }
+
+ const handleVerify = async (formData: FormData) => {
+ const code = formData.get('code') as string
+
+ await signUp.verifications.verifyEmailCode({
+ code,
+ })
+ if (signUp.status === 'complete') {
+ await signUp.finalize({
+ navigate: () => {
+ router.push('/dashboard')
+ },
+ })
+ }
+ }
+
+ if (signUp.status === 'complete' || isSignedIn) {
+ return null
+ }
+
+ if (
+ signUp.status === 'missing_requirements' &&
+ signUp.unverifiedFields.includes('email_address')
+ ) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+```
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/email-password.mdx b/docs/guides/development/custom-flows/authentication/email-password.mdx
index d128f33444..3e0370bffb 100644
--- a/docs/guides/development/custom-flows/authentication/email-password.mdx
+++ b/docs/guides/development/custom-flows/authentication/email-password.mdx
@@ -23,1088 +23,463 @@ This guide will walk you through how to build a custom email/password sign-up an
To sign up a user using their email, password, and email verification code, you must:
- 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.
-
-
-
- This example is written for Next.js App Router but it can be adapted for any React-based framework.
-
- ```tsx {{ filename: 'app/sign-up/[[...sign-up]]/page.tsx', collapsible: true }}
- 'use client'
-
- import * as React from 'react'
- import { useSignUp } from '@clerk/nextjs'
- import { useRouter } from 'next/navigation'
-
- export default function Page() {
- const { isLoaded, signUp, setActive } = useSignUp()
- const [emailAddress, setEmailAddress] = React.useState('')
- const [password, setPassword] = React.useState('')
- const [verifying, setVerifying] = React.useState(false)
- const [code, setCode] = React.useState('')
- const router = useRouter()
-
- // Handle submission of the sign-up form
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
-
- if (!isLoaded) return
-
- // Start the sign-up process using the email and password provided
- try {
- await signUp.create({
- emailAddress,
- password,
- })
-
- // Send the user an email with the verification code
- await signUp.prepareEmailAddressVerification({
- strategy: 'email_code',
- })
-
- // Set 'verifying' true to display second form
- // and capture the OTP code
- setVerifying(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))
- }
- }
-
- // Handle the submission of the verification form
- const handleVerify = async (e: React.FormEvent) => {
- e.preventDefault()
-
- 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
- }
-
- await router.push('/')
- },
- })
+
+ 1. Initiate the sign-up process by collecting the user's email address and password with the [`signUp.password()`](/docs/reference/javascript/sign-up-future#password) method.
+ 1. Send a one-time code to the provided email address for verification with the [`signUp.verifications.sendEmailCode()`](/docs/reference/javascript/sign-up-future#verifications-send-email-code) method.
+ 1. Collect the one-time code and verify it with the [`signUp.verifications.verifyEmailCode()`](/docs/reference/javascript/sign-up-future#verifications-verify-email-code) method.
+ 1. If the email address verification is successful, finalize the sign-up with the [`signUp.finalize()`](/docs/reference/javascript/sign-up-future#finalize) method to create the user and set the newly created session as the active session.
+
+
+
+
+
+
+
+
+
+ 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 {
- // If the status is not complete, check why. User may need to
- // complete further steps.
- console.error(JSON.stringify(signUpAttempt, null, 2))
+ // 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) }
+ }
}
- } catch (err: any) {
- // 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))
}
}
- // Display the verification form to capture the OTP code
- if (verifying) {
- return (
- <>
-
Verify your email
-
- >
- )
- }
-
- // Display the initial sign-up form to capture the email and password
- return (
- <>
-
-
-
-
-
- ```
+
+ 1. Initiate the sign-in process by collecting the user's email address and password with the [`signIn.password()`](/docs/reference/javascript/sign-in-future#password) 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.
- ```js {{ filename: 'main.js', collapsible: true }}
- import { Clerk } from '@clerk/clerk-js'
+
+
+
+
+
+
- const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
+
+ 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 clerk = new Clerk(pubKey)
- await clerk.load()
+
+
+ ```swift {{ filename: 'EmailPasswordSignInView.swift', collapsible: true }}
+ import SwiftUI
+ import Clerk
- if (clerk.isSignedIn) {
- // Mount user button component
- document.getElementById('signed-in').innerHTML = `
-
- `
+ struct EmailPasswordSignInView: View {
+ @State private var email = ""
+ @State private var password = ""
- const userbuttonDiv = document.getElementById('user-button')
+ 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) }
+ }
+ }
+ }
- 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 = `
-
- `
+ extension EmailPasswordSignInView {
- const taskDiv = document.getElementById('task')
+ 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))
- 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 {
+ 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.
- console.error(JSON.stringify(signInAttempt, null, 2))
+ dump(signIn.status)
}
- } catch (error) {
+ } catch {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
- console.error(error)
+ dump(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)}
- />
-
-
- Don't have an account?
-
- Sign up
-
-
-
- )
- }
- ```
-
-
-
- ```swift {{ filename: 'EmailPasswordSignInView.swift', collapsible: true }}
- import SwiftUI
- import Clerk
-
- struct EmailPasswordSignInView: View {
- @State private var email = ""
- @State private var password = ""
-
- 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) }
- }
- }
- }
-
- extension EmailPasswordSignInView {
-
- 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))
-
- 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)
- }
- }
- }
- ```
-
-
-
- ```kotlin {{ filename: 'EmailPasswordSignInViewModel.kt', collapsible: true }}
- import androidx.lifecycle.ViewModel
- import androidx.lifecycle.viewModelScope
- import com.clerk.api.Clerk
- import com.clerk.api.network.serialization.onFailure
- import com.clerk.api.network.serialization.onSuccess
- import com.clerk.api.signin.SignIn
- 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 EmailPasswordSignInViewModel : ViewModel() {
- private val _uiState = MutableStateFlow(
- UiState.SignedOut
- )
- val uiState = _uiState.asStateFlow()
-
- init {
- combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
- _uiState.value = when {
- !isInitialized -> UiState.Loading
- user == null -> UiState.SignedOut
- else -> UiState.SignedIn
- }
- }.launchIn(viewModelScope)
- }
-
- fun submit(email: String, password: String) {
- viewModelScope.launch {
- SignIn.create(
- SignIn.CreateParams.Strategy.Password(
- identifier = email,
- password = password
- )
- ).onSuccess {
- _uiState.value = UiState.SignedIn
- }.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 SignedOut : UiState
-
- data object SignedIn : UiState
- }
}
- ```
+ ```
+
+
+
+ ```kotlin {{ filename: 'EmailPasswordSignInViewModel.kt', collapsible: true }}
+ import androidx.lifecycle.ViewModel
+ import androidx.lifecycle.viewModelScope
+ import com.clerk.api.Clerk
+ import com.clerk.api.network.serialization.onFailure
+ import com.clerk.api.network.serialization.onSuccess
+ import com.clerk.api.signin.SignIn
+ 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 EmailPasswordSignInViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(
+ UiState.SignedOut
+ )
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
+ _uiState.value = when {
+ !isInitialized -> UiState.Loading
+ user == null -> UiState.SignedOut
+ else -> UiState.SignedIn
+ }
+ }.launchIn(viewModelScope)
+ }
- ```kotlin {{ filename: 'EmailPasswordSignInActivity.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.*
- import androidx.compose.material3.*
- 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.*
- import androidx.compose.ui.text.input.PasswordVisualTransformation
- import androidx.compose.ui.unit.dp
- import androidx.lifecycle.compose.collectAsStateWithLifecycle
- import com.clerk.api.Clerk
+ fun submit(email: String, password: String) {
+ viewModelScope.launch {
+ SignIn.create(
+ SignIn.CreateParams.Strategy.Password(
+ identifier = email,
+ password = password
+ )
+ ).onSuccess {
+ _uiState.value = UiState.SignedIn
+ }.onFailure {
+ // See https://clerk.com/docs/guides/development/custom-flows/error-handling
+ // for more info on error handling
+ }
+ }
+ }
- class EmailPasswordSignInActivity : ComponentActivity() {
- val viewModel: EmailPasswordSignInViewModel by viewModels()
+ sealed interface UiState {
+ data object Loading : UiState
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- val state by viewModel.uiState.collectAsStateWithLifecycle()
- EmailPasswordSignInView(
- state = state,
- onSubmit = viewModel::submit
- )
- }
- }
- }
+ data object SignedOut : UiState
- @Composable
- fun EmailPasswordSignInView(
- state: EmailPasswordSignInViewModel.UiState,
- onSubmit: (String, String) -> Unit,
- ) {
- var email by remember { mutableStateOf("") }
- var password by remember { mutableStateOf("") }
-
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-
- when (state) {
-
- EmailPasswordSignInViewModel.UiState.SignedOut -> {
- Column(
- verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- TextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
- TextField(
- value = password,
- onValueChange = { password = it },
- visualTransformation = PasswordVisualTransformation(),
- label = { Text("Password") },
- )
- Button(onClick = { onSubmit(email, password) }) { Text("Sign in") }
- }
- }
+ data object SignedIn : UiState
+ }
+ }
+ ```
- EmailPasswordSignInViewModel.UiState.SignedIn -> {
- Text("Current session: ${Clerk.session?.id}")
- }
+ ```kotlin {{ filename: 'EmailPasswordSignInActivity.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.*
+ import androidx.compose.material3.*
+ 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.*
+ import androidx.compose.ui.text.input.PasswordVisualTransformation
+ import androidx.compose.ui.unit.dp
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
+ import com.clerk.api.Clerk
+
+ class EmailPasswordSignInActivity : ComponentActivity() {
+
+ val viewModel: EmailPasswordSignInViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ EmailPasswordSignInView(
+ state = state,
+ onSubmit = viewModel::submit
+ )
+ }
+ }
+ }
- EmailPasswordSignInViewModel.UiState.Loading ->
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator()
- }
- }
- }
- }
+ @Composable
+ fun EmailPasswordSignInView(
+ state: EmailPasswordSignInViewModel.UiState,
+ onSubmit: (String, String) -> Unit,
+ ) {
+ var email by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+
+ when (state) {
+
+ EmailPasswordSignInViewModel.UiState.SignedOut -> {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
+ TextField(
+ value = password,
+ onValueChange = { password = it },
+ visualTransformation = PasswordVisualTransformation(),
+ label = { Text("Password") },
+ )
+ Button(onClick = { onSubmit(email, password) }) { Text("Sign in") }
+ }
+ }
+
+ EmailPasswordSignInViewModel.UiState.SignedIn -> {
+ Text("Current session: ${Clerk.session?.id}")
+ }
+
+ EmailPasswordSignInViewModel.UiState.Loading ->
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ }
+ }
+ }
- ```
-
-
+ ```
+
+
+
diff --git a/docs/manifest.json b/docs/manifest.json
index a6259f5b79..d097e449a2 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -1602,10 +1602,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
+
From 6ed9df4c8bc88cc93c8a707a8af381484e7d068b Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 8 Oct 2025 10:56:45 -0500
Subject: [PATCH 02/18] Add legacy folder for auth custom flows
---
.../legacy/application-invitations.mdx | 254 ++++
.../legacy/bot-sign-up-protection.mdx | 76 ++
.../authentication/legacy/email-links.mdx | 515 ++++++++
.../legacy/email-password-mfa.mdx | 734 +++++++++++
.../authentication/legacy/email-password.mdx | 1110 +++++++++++++++++
.../authentication/legacy/email-sms-otp.mdx | 982 +++++++++++++++
.../legacy/embedded-email-links.mdx | 199 +++
.../legacy/enterprise-connections.mdx | 170 +++
.../authentication/legacy/google-one-tap.mdx | 128 ++
.../legacy/multi-session-applications.mdx | 173 +++
.../legacy/oauth-connections.mdx | 478 +++++++
.../authentication/legacy/passkeys.mdx | 197 +++
.../authentication/legacy/sign-out.mdx | 220 ++++
docs/manifest.json | 60 +
14 files changed, 5296 insertions(+)
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/application-invitations.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/bot-sign-up-protection.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/email-links.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/email-password-mfa.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/email-password.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/email-sms-otp.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/embedded-email-links.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/enterprise-connections.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/google-one-tap.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/multi-session-applications.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/oauth-connections.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/passkeys.mdx
create mode 100644 docs/guides/development/custom-flows/authentication/legacy/sign-out.mdx
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 (
+ <>
+
+
+
+
+
+ ```
+
+ ```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/bot-sign-up-protection.mdx b/docs/guides/development/custom-flows/authentication/legacy/bot-sign-up-protection.mdx
new file mode 100644
index 0000000000..d9863c9b42
--- /dev/null
+++ b/docs/guides/development/custom-flows/authentication/legacy/bot-sign-up-protection.mdx
@@ -0,0 +1,76 @@
+---
+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.
+---
+
+
+
+Clerk provides the ability to add a CAPTCHA widget to your sign-up flows to protect against bot sign-ups. The [``](/docs/reference/components/authentication/sign-up) component handles this flow out-of-the-box. However, if you're building a custom user interface, this guide will show you how to add the CAPTCHA widget to your custom sign-up flow.
+
+
+ ## Enable bot sign-up protection
+
+ 1. In the Clerk Dashboard, navigate to the [**Attack protection**](https://dashboard.clerk.com/last-active?path=user-authentication/attack-protection) page.
+ 1. Enable the **Bot sign-up protection** toggle.
+
+ > [!WARNING]
+ > If you currently have the **Invisible** CAPTCHA type selected, it's highly recommended to switch to the **Smart** option, as the **Invisible** option is deprecated and will be removed in a future update.
+
+ ## Add the CAPTCHA widget to your custom sign-up form
+
+ To render the CAPTCHA widget in your custom sign-up form, **you need to include the `` element by the time you call `signUp.create()`**. This element acts as a placeholder onto which the widget will be rendered.
+
+ If this element is not found, the SDK will transparently fall back to an invisible widget in order to avoid breaking your sign-up flow. If this happens, you should see a relevant error in your browser's console.
+
+ > [!TIP]
+ > The invisible widget fallback automatically blocks suspected bot traffic without offering users falsely detected as bots with an opportunity to prove otherwise. Therefore, it's strongly recommended that you ensure the `` element exists in your DOM.
+
+ The following example shows how to support the CAPTCHA widget:
+
+ ```tsx {{ mark: [[25, 26]] }}
+ <>
+
Sign up
+
+ >
+ ```
+
+ ## Customize the appearance of the CAPTCHA widget
+
+ You can customize the appearance of the CAPTCHA widget by passing data attributes to the `` element. The following attributes are supported:
+
+ - `data-cl-theme`: The CAPTCHA widget theme. Can take the following values: `'light'`, `'dark'`, `'auto'`. Defaults to `'auto'`.
+ - `data-cl-size`: The CAPTCHA widget size. Can take the following values: `'normal'`, `'flexible'`, `'compact'`. Defaults to `'normal'`.
+ - `data-cl-language`: The CAPTCHA widget language. Must be either `'auto'` (default) to use the language that the visitor has chosen, or language and country code (e.g. `'en-US'`). Some languages are [supported by Clerk](/docs/guides/customizing-clerk/localization) but not by Cloudflare Turnstile, which is used for the CAPTCHA widget. See [Cloudflare Turnstile's supported languages](https://developers.cloudflare.com/turnstile/reference/supported-languages).
+
+ For example, to set the theme to `'dark'`, the size to `'flexible'`, and the language to `'es-ES'`, you would add the following attributes to the `` element:
+
+ ```html
+
+ ```
+
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
+
+
+ )
+ }
+ ```
+
+ ```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
+ )
+ }
+
+ // 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
+
+
+ )
+ }
+ ```
+
+ ```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
+ )
+ }
+
+ // 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 (
+
+
+
+
+
+
+
+ ```
+
+ ```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)} />
+
+
+
+ )
+ }
+
+ return (
+
+ Sign in
+
+ setEmail(email)}
+ />
+
+
+
+ setPassword(password)}
+ />
+
+
+
+
+ )
+ }
+ ```
+
+
+
+ ```swift {{ filename: 'MFASignInView.swift', collapsible: true }}
+ import SwiftUI
+ import Clerk
+
+ struct MFASignInView: View {
+ @State private var email = ""
+ @State private var password = ""
+ @State private var code = ""
+ @State private var displayTOTP = false
+
+ var body: some View {
+ if displayTOTP {
+ TextField("Code", text: $code)
+ Button("Verify") {
+ Task { await verify(code: code) }
+ }
+ } else {
+ TextField("Email", text: $email)
+ SecureField("Password", text: $password)
+ Button("Next") {
+ Task { await submit(email: email, password: password) }
+ }
+ }
+ }
+ }
+
+ extension MFASignInView {
+
+ func submit(email: String, password: String) async {
+ do {
+ // Start the sign-in process.
+ let signIn = try await SignIn.create(strategy: .identifier(email, password: password))
+
+ switch signIn.status {
+ case .needsSecondFactor:
+ // Handle user submitting email and password and swapping to TOTP form.
+ displayTOTP = true
+ default:
+ // If the status is not needsSecondFactor, check why. User may need to
+ // complete different steps.
+ dump(signIn.status)
+ }
+ } 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 }
+
+ // Attempt the TOTP or backup code verification.
+ let signIn = try await inProgressSignIn.attemptSecondFactor(strategy: .totp(code: code))
+
+ 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)
+ }
+ }
+ }
+ ```
+
+
+
+ ```kotlin {{ filename: 'MFASignInViewModel.kt', collapsible: true }}
+ import androidx.lifecycle.ViewModel
+ import androidx.lifecycle.viewModelScope
+ import com.clerk.api.Clerk
+ 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.attemptSecondFactor
+ import kotlinx.coroutines.flow.MutableStateFlow
+ import kotlinx.coroutines.flow.asStateFlow
+ import kotlinx.coroutines.launch
+
+ class MFASignInViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(UiState.Unverified)
+ val uiState = _uiState.asStateFlow()
+
+ fun submit(email: String, password: String) {
+ viewModelScope.launch {
+ SignIn.create(SignIn.CreateParams.Strategy.Password(identifier = email, password = password))
+ .onSuccess {
+ if (it.status == SignIn.Status.NEEDS_SECOND_FACTOR) {
+ // Display TOTP Form
+ _uiState.value = UiState.NeedsSecondFactor
+ } else {
+ // If the status is not needsSecondFactor, check why. User may need to
+ // complete different steps.
+ }
+ }
+ .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
+ .attemptSecondFactor(SignIn.AttemptSecondFactorParams.TOTP(code))
+ .onSuccess {
+ if (it.status == SignIn.Status.COMPLETE) {
+ // User is now signed in and verified.
+ // You can navigate to the next screen or perform other actions.
+ _uiState.value = UiState.Verified
+ }
+ }
+ .onFailure {
+ // See https://clerk.com/docs/guides/development/custom-flows/error-handling
+ // for more info on error handling
+ }
+ }
+ }
+
+ sealed interface UiState {
+ data object Unverified : UiState
+ data object Verified : UiState
+ data object NeedsSecondFactor : UiState
+ }
+ }
+ ```
+
+ ```kotlin {{ filename: 'MFASignInActivity.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.Column
+ import androidx.compose.foundation.layout.fillMaxSize
+ import androidx.compose.material3.Button
+ 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.text.input.PasswordVisualTransformation
+ import androidx.compose.ui.unit.dp
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
+
+ class MFASignInActivity : ComponentActivity() {
+ val viewModel: MFASignInViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ MFASignInView(state = state, onSubmit = viewModel::submit, onVerify = viewModel::verify)
+ }
+ }
+ }
+
+ @Composable
+ fun MFASignInView(
+ state: MFASignInViewModel.UiState,
+ onSubmit: (String, String) -> Unit,
+ onVerify: (String) -> Unit,
+ ) {
+ var email by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+ var code by remember { mutableStateOf("") }
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ when (state) {
+ MFASignInViewModel.UiState.NeedsSecondFactor -> {
+ TextField(value = code, onValueChange = { code = it }, placeholder = { Text("Code") })
+ Button(onClick = { onVerify(code) }) { Text("Submit") }
+ }
+ MFASignInViewModel.UiState.Unverified -> {
+ TextField(value = email, onValueChange = { email = it }, placeholder = { Text("Email") })
+ TextField(
+ value = password,
+ onValueChange = { password = it },
+ placeholder = { Text("Password") },
+ visualTransformation = PasswordVisualTransformation(),
+ )
+ Button(onClick = { onSubmit(email, password) }) { Text("Next") }
+ }
+ MFASignInViewModel.UiState.Verified -> {
+ Text("Verified")
+ }
+ }
+ }
+ }
+ ```
+
+
+
+
+{/* TODO: Add logic for MFA for phone code */}
+
+## Next steps
+
+Now that users can sign in with MFA, you need to add the ability for your users to manage their MFA settings. Learn how to build a custom flow for [managing TOTP MFA](/docs/guides/development/custom-flows/account-updates/manage-totp-based-mfa) or for [managing SMS MFA](/docs/guides/development/custom-flows/account-updates/manage-sms-based-mfa).
diff --git a/docs/guides/development/custom-flows/authentication/legacy/email-password.mdx b/docs/guides/development/custom-flows/authentication/legacy/email-password.mdx
new file mode 100644
index 0000000000..d128f33444
--- /dev/null
+++ b/docs/guides/development/custom-flows/authentication/legacy/email-password.mdx
@@ -0,0 +1,1110 @@
+---
+title: Build a custom email/password authentication flow
+description: Learn how to build a custom email/password sign-up and sign-in flow using the Clerk API.
+---
+
+
+
+This guide will walk you through how to build a custom email/password sign-up and sign-in flow.
+
+
+ ## Enable email and password authentication
+
+ To use email and password authentication, you first need to ensure they 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-up with email** and **Sign-in with email**.
+ 1. Select the **Password** tab and enable **Sign-up with password**. Leave **Require a password at sign-up** enabled.
+
+ > [!NOTE]
+ > By default, **Email verification code** is enabled for both sign-up and sign-in. This means that when a user signs up using their email address, Clerk sends a one-time code to their email address. The user must then enter this code to verify their email and complete the sign-up process. When the user uses the email address to sign in, they are emailed a one-time code to sign in. If you'd like to use **Email verification link** instead, see the [custom flow for email links](/docs/guides/development/custom-flows/authentication/email-links).
+
+ ## Sign-up flow
+
+ To sign up a user using their email, password, and email verification code, you must:
+
+ 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.
+
+
+
+ This example is written for Next.js App Router but it can be adapted for any React-based framework.
+
+ ```tsx {{ filename: 'app/sign-up/[[...sign-up]]/page.tsx', collapsible: true }}
+ 'use client'
+
+ import * as React from 'react'
+ import { useSignUp } from '@clerk/nextjs'
+ import { useRouter } from 'next/navigation'
+
+ export default function Page() {
+ const { isLoaded, signUp, setActive } = useSignUp()
+ const [emailAddress, setEmailAddress] = React.useState('')
+ const [password, setPassword] = React.useState('')
+ const [verifying, setVerifying] = React.useState(false)
+ const [code, setCode] = React.useState('')
+ const router = useRouter()
+
+ // Handle submission of the sign-up form
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ if (!isLoaded) return
+
+ // Start the sign-up process using the email and password provided
+ try {
+ await signUp.create({
+ emailAddress,
+ password,
+ })
+
+ // Send the user an email with the verification code
+ await signUp.prepareEmailAddressVerification({
+ strategy: 'email_code',
+ })
+
+ // Set 'verifying' true to display second form
+ // and capture the OTP code
+ setVerifying(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))
+ }
+ }
+
+ // Handle the submission of the verification form
+ const handleVerify = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ 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
+ }
+
+ 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: any) {
+ // 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))
+ }
+ }
+
+ // Display the verification form to capture the OTP code
+ if (verifying) {
+ return (
+ <>
+
Verify your email
+
+ >
+ )
+ }
+
+ // Display the initial sign-up form to capture the email and password
+ 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-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 (
+
+ )
+ }
+ ```
+
+ ```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 (
+
+
+
+
+ )
+ }
+ ```
+
+
diff --git a/docs/guides/development/custom-flows/authentication/legacy/google-one-tap.mdx b/docs/guides/development/custom-flows/authentication/legacy/google-one-tap.mdx
new file mode 100644
index 0000000000..68b68c93e8
--- /dev/null
+++ b/docs/guides/development/custom-flows/authentication/legacy/google-one-tap.mdx
@@ -0,0 +1,128 @@
+---
+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.
+---
+
+
+
+[Google One Tap](https://developers.google.com/identity/gsi/web/guides/features) enables users to press a single button to authentication in your Clerk application with a Google account.
+
+This guide will walk you through how to build a custom Google One Tap authentication flow.
+
+
+ ## Enable Google as a social connection
+
+ To use Google One Tap with Clerk, follow the steps in the [dedicated guide](/docs/guides/configure/auth-strategies/social-connections/google#configure-for-your-production-instance) to configure Google as a social connection in the Clerk Dashboard using custom credentials.
+
+ ## Create the Google One Tap authentication flow
+
+ To authenticate users with Google One Tap, you must:
+
+ 1. Initialize a ["Sign In With Google"](https://developers.google.com/identity/gsi/web/reference/js-reference) client UI, passing in your Client ID.
+ 1. Use the response to authenticate the user in your Clerk app if the request was successful.
+ 1. Redirect the user back to the page they started the authentication flow from by default, or to another URL if necessary.
+
+ The following example creates a component that implements a custom Google One Tap authentication flow, which can be used in a sign-in or sign-up page.
+
+
+
+ ```tsx {{ filename: 'app/components/CustomGoogleOneTap.tsx', collapsible: true }}
+ 'use client'
+ import { useClerk } from '@clerk/nextjs'
+ import { useRouter } from 'next/navigation'
+ import Script from 'next/script'
+ import { useEffect } from 'react'
+
+ // Add clerk to Window to avoid type errors
+ declare global {
+ interface Window {
+ google: any
+ }
+ }
+
+ export function CustomGoogleOneTap({ children }: { children: React.ReactNode }) {
+ const clerk = useClerk()
+ const router = useRouter()
+
+ useEffect(() => {
+ // Will show the One Tap UI after two seconds
+ const timeout = setTimeout(() => oneTap(), 2000)
+ return () => {
+ clearTimeout(timeout)
+ }
+ }, [])
+
+ const oneTap = () => {
+ const { google } = window
+ if (google) {
+ google.accounts.id.initialize({
+ // Add your Google Client ID here.
+ client_id: 'xxx-xxx-xxx',
+ callback: async (response: any) => {
+ // Here we call our provider with the token provided by Google
+ call(response.credential)
+ },
+ })
+
+ // Uncomment below to show the One Tap UI without
+ // logging any notifications.
+ // return google.accounts.id.prompt() // without listening to notification
+
+ // Display the One Tap UI, and log any errors that occur.
+ return google.accounts.id.prompt((notification: any) => {
+ console.log('Notification ::', notification)
+ if (notification.isNotDisplayed()) {
+ console.log('getNotDisplayedReason ::', notification.getNotDisplayedReason())
+ } else if (notification.isSkippedMoment()) {
+ console.log('getSkippedReason ::', notification.getSkippedReason())
+ } else if (notification.isDismissedMoment()) {
+ console.log('getDismissedReason ::', notification.getDismissedReason())
+ }
+ })
+ }
+ }
+
+ const call = async (token: any) => {
+ try {
+ const res = await clerk.authenticateWithGoogleOneTap({
+ token,
+ })
+
+ await clerk.handleGoogleOneTapCallback(res, {
+ signInFallbackRedirectUrl: '/example-fallback-path',
+ })
+ } catch (error) {
+ router.push('/sign-in')
+ }
+ }
+
+ return (
+ <>
+
+ >
+ )
+ }
+ ```
+
+
+
+ You can then display this component on any page. The following example demonstrates a page that displays this component:
+
+
+
+ ```tsx {{ filename: 'app/google-sign-in-example/page.tsx' }}
+ import { CustomGoogleOneTap } from '@/app/components/CustomGoogleOneTap'
+
+ export default function CustomOneTapPage({ children }: { children: React.ReactNode }) {
+ return (
+
+
Google One Tap Example
+
+ )
+ }
+ ```
+
+
+
diff --git a/docs/guides/development/custom-flows/authentication/legacy/multi-session-applications.mdx b/docs/guides/development/custom-flows/authentication/legacy/multi-session-applications.mdx
new file mode 100644
index 0000000000..3a3beae7d4
--- /dev/null
+++ b/docs/guides/development/custom-flows/authentication/legacy/multi-session-applications.mdx
@@ -0,0 +1,173 @@
+---
+title: Build a custom multi-session flow
+description: Learn how to use the Clerk API to add multi-session handling to your application.
+---
+
+
+
+A multi-session application is an application that allows multiple accounts to be signed in from the same browser at the same time. The user can switch from one account to another seamlessly. Each account is independent from the rest and has access to different resources.
+
+This guide provides you with the necessary information to build a custom multi-session flow using the Clerk API.
+
+To implement the multi-session feature to your application, you need to handle the following scenarios:
+
+- [Switching between different accounts](#switch-between-sessions)
+- [Adding new accounts](#add-a-new-session)
+- [Signing out from one account, while remaining signed in to the rest](#sign-out-active-session)
+- [Signing out from all accounts](#sign-out-all-sessions)
+
+## Enable multi-session in your application
+
+To enable multi-session in your application, you need to configure it in the Clerk Dashboard.
+
+1. In the Clerk Dashboard, navigate to the [**Sessions**](https://dashboard.clerk.com/last-active?path=sessions) page.
+1. Toggle on **Multi-session handling**.
+1. Select **Save changes**.
+
+## Get the session and user
+
+
+ ```jsx
+ import { useClerk } from '@clerk/clerk-react'
+
+ // Get the session and user
+ const { session, user } = useClerk()
+ ```
+
+ ```js
+ // Get the session
+ const currentSession = window.Clerk.session
+
+ // Get the user
+ const currentUser = window.Clerk.user
+ ```
+
+ ```swift
+ // Get the current session
+ var currentSession: Session? { Clerk.shared.session }
+
+ // Get the current user
+ var currentUser: User? { Clerk.shared.user }
+ ```
+
+
+## Switch between sessions
+
+
+ ```jsx
+ import { useClerk } from '@clerk/clerk-react'
+
+ const { client, setActive } = useClerk()
+
+ // You can get all the available sessions through the client
+ const availableSessions = client.sessions
+ const currentSession = availableSessions[0].id
+
+ // Use setActive() to set the session as active
+ await setActive({
+ session: currentSession.id,
+ 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('/')
+ },
+ })
+ ```
+
+ ```js
+ // You can get all the available sessions through the client
+ const availableSessions = window.Clerk.client.sessions
+
+ // Use setActive() to set the session as active
+ await window.Clerk.setActive({
+ session: availableSessions[0].id,
+ 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('/')
+ },
+ })
+ ```
+
+ ```swift
+ // You can get all the available sessions through the client
+ var availableSessions: [Session] { Clerk.shared.client?.sessions ?? [] }
+
+ // Use setActive() to set the session as active
+ try await Clerk.shared.setActive(sessionId: session.id)
+ ```
+
+
+## Add a new session
+
+To add a new session, simply link to your existing sign-in flow. New sign-ins will automatically add to the list of available sessions on the client. To create a sign-in flow, see one of the following popular guides:
+
+- [Email and password](/docs/guides/development/custom-flows/authentication/email-password)
+- [Passwordless authentication](/docs/guides/development/custom-flows/authentication/email-sms-otp)
+- [Social sign-in (OAuth)](/docs/guides/configure/auth-strategies/social-connections/overview)
+
+For more information on how Clerk's sign-in flow works, see the [detailed sign-in guide](/docs/guides/development/custom-flows/overview#sign-in-flow).
+
+## Sign out all sessions
+
+Use [`signOut()`](/docs/reference/javascript/clerk#sign-out) to deactivate all sessions on the current client.
+
+
+ ```jsx
+ import { useClerk } from '@clerk/clerk-react'
+
+ const { signOut, session } = useClerk()
+
+ // Use signOut to sign-out all active sessions.
+ await signOut()
+ ```
+
+ ```js
+ // Use signOut to sign-out all active sessions.
+ await window.Clerk.signOut()
+ ```
+
+ ```swift
+ // Use signOut to sign-out all active sessions.
+ try await Clerk.shared.signOut()
+ ```
+
+
+## Sign out active session
+
+Use [`signOut()`](/docs/reference/javascript/clerk#sign-out) to deactivate a specific session by passing the session ID.
+
+
+ ```jsx
+ import { useClerk } from '@clerk/clerk-react'
+
+ // Get the signOut method and the active session
+ const { signOut, session } = useClerk()
+
+ // Use signOut to sign-out the active session
+ await signOut(session.id)
+ ```
+
+ ```js
+ // Get the current session
+ const currentSession = window.Clerk.session
+
+ // Use signOut to sign-out the active session
+ await window.Clerk.signOut(currentSession.id)
+ ```
+
+ ```swift
+ // Use signOut to sign-out a specific session
+ try await Clerk.shared.signOut(sessionId: session.id)
+ ```
+
diff --git a/docs/guides/development/custom-flows/authentication/legacy/oauth-connections.mdx b/docs/guides/development/custom-flows/authentication/legacy/oauth-connections.mdx
new file mode 100644
index 0000000000..4a722abfd6
--- /dev/null
+++ b/docs/guides/development/custom-flows/authentication/legacy/oauth-connections.mdx
@@ -0,0 +1,478 @@
+---
+title: Build a custom flow for authenticating with OAuth connections
+description: Learn how to use the Clerk API to build a custom sign-up and sign-in flow that supports OAuth connections.
+---
+
+
+
+## Before you start
+
+You must configure your application instance through the Clerk Dashboard for the social connection(s) that you want to use. Visit [the appropriate guide for your platform](/docs/guides/configure/auth-strategies/social-connections/all-providers) to learn how to configure your instance.
+
+## Create the sign-up and sign-in flow
+
+
+
+ First, in your `.env` file, set the `NEXT_PUBLIC_CLERK_SIGN_IN_URL` environment variable to tell Clerk where the sign-in page is being hosted. Otherwise, your app may default to using the [Account Portal sign-in page](/docs/guides/customizing-clerk/account-portal#sign-in) instead. This guide uses the `/sign-in` route.
+
+ ```env {{ filename: '.env' }}
+ NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
+ ```
+
+
+
+
+ ```tsx {{ filename: 'app/sign-in/page.tsx' }}
+ 'use client'
+
+ import * as React from 'react'
+ import { OAuthStrategy } from '@clerk/types'
+ import { useSignIn } from '@clerk/nextjs'
+
+ export default function Page() {
+ const { signIn } = useSignIn()
+
+ if (!signIn) return null
+
+ const signInWith = (strategy: OAuthStrategy) => {
+ return signIn
+ .authenticateWithRedirect({
+ strategy,
+ redirectUrl: '/sign-in/sso-callback',
+ redirectUrlComplete: '/sign-in/tasks', // Learn more about session tasks at https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks
+ })
+ .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)
+ })
+ }
+
+ // Render a button for each supported OAuth provider
+ // you want to add to your app. This example uses only Google.
+ 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 (
+
+
+
+ )
+ }
+ ```
+
+
+
+ ```swift {{ filename: 'OAuthView.swift', collapsible: true }}
+ import SwiftUI
+ import Clerk
+
+ struct OAuthView: View {
+ var body: some View {
+ // Render a button for each supported OAuth provider
+ // you want to add to your app. This example uses only Google.
+ Button("Sign In with Google") {
+ Task { await signInWithOAuth(provider: .google) }
+ }
+ }
+ }
+
+ extension OAuthView {
+
+ func signInWithOAuth(provider: OAuthProvider) async {
+ do {
+ // Start the sign-in process using the selected OAuth provider.
+ let result = try await SignIn.authenticateWithRedirect(strategy: .oauth(provider: provider))
+
+ // It is common for users who are authenticating with OAuth to use
+ // a sign-in button when they mean to sign-up, and vice versa.
+ // Clerk will handle this transfer for you if possible.
+ // Therefore, a TransferFlowResult can be either a SignIn or SignUp.
+
+ switch result {
+ case .signIn(let signIn):
+ 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)
+ }
+ case .signUp(let signUp):
+ switch signUp.status {
+ case .complete:
+ // If sign-up 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(signUp.status)
+ }
+ }
+ } catch {
+ // See https://clerk.com/docs/guides/development/custom-flows/error-handling
+ // for more info on error handling.
+ dump(error)
+ }
+ }
+ }
+ ```
+
+
+
+ ```kotlin {{ filename: 'OAuthViewModel.kt', collapsible: true }}
+ import android.util.Log
+ import androidx.lifecycle.ViewModel
+ import androidx.lifecycle.viewModelScope
+ import com.clerk.api.Clerk
+ import com.clerk.api.network.serialization.longErrorMessageOrNull
+ import com.clerk.api.network.serialization.onFailure
+ import com.clerk.api.network.serialization.onSuccess
+ import com.clerk.api.signin.SignIn
+ import com.clerk.api.signup.SignUp
+ import com.clerk.api.sso.OAuthProvider
+ import com.clerk.api.sso.ResultType
+ 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 OAuthViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(UiState.Loading)
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
+ _uiState.value = when {
+ !isInitialized -> UiState.Loading
+ user != null -> UiState.Authenticated
+ else -> UiState.SignedOut
+ }
+ }.launchIn(viewModelScope)
+ }
+
+ fun signInWithOAuth(provider: OAuthProvider) {
+ viewModelScope.launch {
+ SignIn.authenticateWithRedirect(SignIn.AuthenticateWithRedirectParams.OAuth(provider)).onSuccess {
+ when(it.resultType) {
+ ResultType.SIGN_IN -> {
+ // The OAuth flow resulted in a sign in
+ if (it.signIn?.status == SignIn.Status.COMPLETE) {
+ _uiState.value = UiState.Authenticated
+ } else {
+ // If the status is not complete, check why. User may need to
+ // complete further steps.
+ }
+ }
+ ResultType.SIGN_UP -> {
+ // The OAuth flow resulted in a sign up
+ if (it.signUp?.status == SignUp.Status.COMPLETE) {
+ _uiState.value = UiState.Authenticated
+ } else {
+ // If the status is not complete, check why. 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
+ Log.e("OAuthViewModel", it.longErrorMessageOrNull, it.throwable)
+ }
+ }
+ }
+
+ sealed interface UiState {
+ data object Loading : UiState
+
+ data object SignedOut : UiState
+
+ data object Authenticated : UiState
+ }
+ }
+ ```
+
+ ```kotlin {{ filename: 'OAuthActivity.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.Box
+ import androidx.compose.foundation.layout.fillMaxSize
+ import androidx.compose.material3.Button
+ import androidx.compose.material3.CircularProgressIndicator
+ import androidx.compose.material3.Text
+ import androidx.compose.runtime.getValue
+ import androidx.compose.ui.Alignment
+ import androidx.compose.ui.Modifier
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
+ import com.clerk.api.sso.OAuthProvider
+
+ class OAuthActivity : ComponentActivity() {
+ val viewModel: OAuthViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ when(state) {
+ OAuthViewModel.UiState.Authenticated -> Text("Authenticated")
+ OAuthViewModel.UiState.Loading -> CircularProgressIndicator()
+ OAuthViewModel.UiState.SignedOut -> {
+ val provider = OAuthProvider.GOOGLE // Or .GITHUB, .SLACK etc.
+ Button(onClick = {
+ viewModel.signInWithOAuth(provider)
+ }) {
+ Text("Sign in with ${provider.name}")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ```
+
+
+
+## Handle missing requirements
+
+Depending on your instance settings, users might need to provide extra information before their sign-up can be completed, such as when a username or accepting legal terms is required. In these cases, the `SignUp` object returns a status of `"missing_requirements"` along with a `missingFields` array. You can create a "Continue" page to collect these missing fields and complete the sign-up flow. Handling the missing requirements will depend on your instance settings. For example, if your instance settings require a phone number, you will need to [handle verifying the phone number](/docs/guides/development/custom-flows/authentication/email-sms-otp#sign-up-flow).
+
+With OAuth flows, it's common for users to try to _sign in_ with an OAuth provider, but they don't have a Clerk account for your app yet. Clerk automatically transfers the flow from the `SignIn` object to the `SignUp` object, which returns the `"missing_requirements"` status and `missingFields` array needed to handle the missing requirements flow. This is why the "Continue" page uses the [`useSignUp()`](/docs/reference/hooks/use-sign-up) hook and treats the missing requirements flow as a sign-up flow.
+
+
+
+
+ ```tsx {{ filename: 'app/sign-in/continue/page.tsx' }}
+ 'use client'
+
+ import { useState } from 'react'
+ import { useSignUp } from '@clerk/nextjs'
+ import { useRouter } from 'next/navigation'
+
+ export default function Page() {
+ const router = useRouter()
+ // Use `useSignUp()` hook to access the `SignUp` object
+ // `missing_requirements` and `missingFields` are only available on the `SignUp` object
+ const { isLoaded, signUp, setActive } = useSignUp()
+ const [formData, setFormData] = useState>({})
+
+ if (!isLoaded) return
Loading…
+
+ // Protect the page from users who are not in the sign-up flow
+ // such as users who visited this route directly
+ if (!signUp.id) router.push('/sign-in')
+
+ const status = signUp?.status
+ const missingFields = signUp?.missingFields ?? []
+
+ const handleChange = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }))
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ try {
+ // Update the `SignUp` object with the missing fields
+ // The logic that goes here will depend on your instance settings
+ // E.g. if your app requires a phone number, you will need to collect and verify it here
+ const res = await signUp?.update(formData)
+ if (res?.status === 'complete') {
+ await setActive({
+ session: res.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)
+ router.push('/sign-in/tasks')
+ return
+ }
+
+ router.push('/')
+ },
+ })
+ }
+ } 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 (status === 'missing_requirements') {
+ // For simplicity, all missing fields in this example are text inputs.
+ // In a real app, you might want to handle them differently:
+ // - legal_accepted: checkbox
+ // - username: text with validation
+ // - phone_number: phone input, etc.
+ return (
+
+
Continue sign-up
+
+
+ )
+ }
+
+ // 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 (
+ <>
+
+ >
+ )
+}
+```
+
+## 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/legacy/sign-out.mdx b/docs/guides/development/custom-flows/authentication/legacy/sign-out.mdx
new file mode 100644
index 0000000000..01ee0d2ba1
--- /dev/null
+++ b/docs/guides/development/custom-flows/authentication/legacy/sign-out.mdx
@@ -0,0 +1,220 @@
+---
+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.
+---
+
+
+
+Clerk's [``](/docs/reference/components/user/user-button) and [``](/docs/reference/components/unstyled/sign-out-button) components provide an out-of-the-box solution for signing out users. However, if you're building a custom solution, you can use the [`signOut()`](/docs/reference/javascript/clerk#sign-out) function to handle the sign-out process.
+
+The `signOut()` function signs a user out of all sessions in a [multi-session application](/docs/guides/secure/session-options#multi-session-applications), or only the current session in a single-session context. You can also specify a specific session to sign out by passing the `sessionId` parameter.
+
+> [!NOTE]
+> The sign-out flow deactivates only the current session. Other valid sessions associated with the same user (e.g., if the user is signed in on another device) will remain active.
+
+
+
+ The [`useClerk()`](/docs/reference/hooks/use-clerk) hook is used to access the `signOut()` function, which is called when the user clicks the sign-out button.
+
+ This example is written for Next.js App Router but can be adapted for any React-based framework.
+
+ ```jsx {{ filename: 'app/components/SignOutButton.tsx' }}
+ 'use client'
+
+ import { useClerk } from '@clerk/nextjs'
+
+ export const SignOutButton = () => {
+ const { signOut } = useClerk()
+
+ return (
+ // Clicking this button signs out a user
+ // and redirects them to the home page "/".
+
+ )
+ }
+ ```
+
+
+
+
+ ```html {{ filename: 'index.html', collapsible: true }}
+
+
+
+
+
+
+ Clerk + JavaScript App
+
+
+
+
+
+
+
+
+ ```
+
+ ```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) {
+ // Attach signOut function to the sign-out button
+ document.getElementById('sign-out').addEventListener('click', async () => {
+ await clerk.signOut()
+ // Optional: refresh page after sign-out
+ window.location.reload()
+ })
+ }
+ ```
+
+
+
+
+ The [`useClerk()`](/docs/reference/hooks/use-clerk) hook is used to access the `signOut()` function, which is called when the user clicks the "Sign out" button.
+
+
+
+
+
+ ```swift {{ filename: 'SignOutView.swift', collapsible: true }}
+ import SwiftUI
+ import Clerk
+
+ struct SignOutView: View {
+ @Environment(Clerk.self) private var clerk
+
+ var body: some View {
+ if let session = clerk.session {
+ Text("Active Session: \(session.id)")
+ Button("Sign out") {
+ Task { await signOut() }
+ }
+ } else {
+ Text("You are signed out")
+ }
+ }
+ }
+
+ extension SignOutView {
+
+ func signOut() async {
+ do {
+ try await clerk.signOut()
+ } catch {
+ // See https://clerk.com/docs/guides/development/custom-flows/error-handling
+ // for more info on error handling.
+ dump(error)
+ }
+ }
+ }
+ ```
+
+
+
+ ```kotlin {{ filename: 'MainViewModel.kt', collapsible: true }}
+ import androidx.lifecycle.ViewModel
+ import androidx.lifecycle.viewModelScope
+ import com.clerk.api.Clerk
+ import com.clerk.api.network.serialization.onFailure
+ import com.clerk.api.network.serialization.onSuccess
+ 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 MainViewModel : ViewModel() {
+
+ private val _uiState = MutableStateFlow(UiState.Loading)
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
+ _uiState.value =
+ when {
+ !isInitialized -> UiState.Loading
+ user != null -> UiState.SignedIn
+ else -> UiState.SignedOut
+ }
+ }
+ .launchIn(viewModelScope)
+ }
+
+ fun signOut() {
+ viewModelScope.launch {
+ Clerk.signOut()
+ .onSuccess { _uiState.value = UiState.SignedOut }
+ .onFailure {
+ // See https://clerk.com/docs/guides/development/custom-flows/error-handling
+ // for more info on error handling
+ }
+ }
+ }
+
+ sealed interface UiState {
+ data object SignedIn : UiState
+
+ data object SignedOut : UiState
+
+ data object Loading : UiState
+ }
+ }
+ ```
+
+ ```kotlin {{ filename: 'MainActivity.kt', collapsible: true }}
+ import android.os.Bundle
+ import androidx.activity.ComponentActivity
+ import androidx.activity.compose.setContent
+ import androidx.activity.enableEdgeToEdge
+ import androidx.activity.viewModels
+ import androidx.compose.foundation.layout.Box
+ import androidx.compose.foundation.layout.fillMaxSize
+ import androidx.compose.material3.Button
+ import androidx.compose.material3.CircularProgressIndicator
+ import androidx.compose.material3.Text
+ import androidx.compose.runtime.getValue
+ import androidx.compose.ui.Alignment
+ import androidx.compose.ui.Modifier
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
+
+ class MainActivity : ComponentActivity() {
+ val viewModel: MainViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ when (state) {
+ MainViewModel.UiState.Loading -> {
+ CircularProgressIndicator()
+ }
+
+ MainViewModel.UiState.SignedIn -> {
+ Button(onClick = { viewModel.signOut() }) {
+ Text("Sign out")
+ }
+ }
+
+ MainViewModel.UiState.SignedOut -> {
+ // Signed out content
+ }
+ }
+ }
+ }
+ }
+ }
+ ```
+
+
diff --git a/docs/manifest.json b/docs/manifest.json
index d097e449a2..200a7a2ac4 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -993,6 +993,66 @@
{
"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": "Google One Tap",
+ "href": "/docs/guides/development/custom-flows/authentication/legacy/google-one-tap"
+ },
+ {
+ "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 out",
+ "href": "/docs/guides/development/custom-flows/authentication/legacy/sign-out"
+ },
+ {
+ "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"
+ },
+ {
+ "title": "Multi-session applications",
+ "href": "/docs/guides/development/custom-flows/authentication/legacy/multi-session-applications"
+ },
+ {
+ "title": "Bot sign-up protection",
+ "href": "/docs/guides/development/custom-flows/authentication/legacy/bot-sign-up-protection"
+ }
+ ]
+ ]
}
]
]
From 8f9e5646e91859e990ab4949c15cb04559c01339 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 8 Oct 2025 11:03:54 -0500
Subject: [PATCH 03/18] Move email-password code samples to partials
---
.../email-password/sign-in-android.mdx | 131 ++++++
.../email-password/sign-in-ios.mdx | 41 ++
.../email-password/sign-up-android.mdx | 159 +++++++
.../email-password/sign-up-ios.mdx | 73 ++++
.../authentication/email-password.mdx | 409 +-----------------
5 files changed, 409 insertions(+), 404 deletions(-)
create mode 100644 docs/_partials/custom-flows/email-password/sign-in-android.mdx
create mode 100644 docs/_partials/custom-flows/email-password/sign-in-ios.mdx
create mode 100644 docs/_partials/custom-flows/email-password/sign-up-android.mdx
create mode 100644 docs/_partials/custom-flows/email-password/sign-up-ios.mdx
diff --git a/docs/_partials/custom-flows/email-password/sign-in-android.mdx b/docs/_partials/custom-flows/email-password/sign-in-android.mdx
new file mode 100644
index 0000000000..88471140d8
--- /dev/null
+++ b/docs/_partials/custom-flows/email-password/sign-in-android.mdx
@@ -0,0 +1,131 @@
+```kotlin {{ filename: 'EmailPasswordSignInViewModel.kt', collapsible: true }}
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.clerk.api.Clerk
+import com.clerk.api.network.serialization.onFailure
+import com.clerk.api.network.serialization.onSuccess
+import com.clerk.api.signin.SignIn
+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 EmailPasswordSignInViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(
+ UiState.SignedOut
+ )
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
+ _uiState.value = when {
+ !isInitialized -> UiState.Loading
+ user == null -> UiState.SignedOut
+ else -> UiState.SignedIn
+ }
+ }.launchIn(viewModelScope)
+ }
+
+ fun submit(email: String, password: String) {
+ viewModelScope.launch {
+ SignIn.create(
+ SignIn.CreateParams.Strategy.Password(
+ identifier = email,
+ password = password
+ )
+ ).onSuccess {
+ _uiState.value = UiState.SignedIn
+ }.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 SignedOut : UiState
+
+ data object SignedIn : UiState
+ }
+}
+```
+
+```kotlin {{ filename: 'EmailPasswordSignInActivity.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.*
+import androidx.compose.material3.*
+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.*
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.clerk.api.Clerk
+
+class EmailPasswordSignInActivity : ComponentActivity() {
+
+ val viewModel: EmailPasswordSignInViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ EmailPasswordSignInView(
+ state = state,
+ onSubmit = viewModel::submit
+ )
+ }
+ }
+}
+
+@Composable
+fun EmailPasswordSignInView(
+ state: EmailPasswordSignInViewModel.UiState,
+ onSubmit: (String, String) -> Unit,
+) {
+ var email by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+
+ when (state) {
+
+ EmailPasswordSignInViewModel.UiState.SignedOut -> {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
+ TextField(
+ value = password,
+ onValueChange = { password = it },
+ visualTransformation = PasswordVisualTransformation(),
+ label = { Text("Password") },
+ )
+ Button(onClick = { onSubmit(email, password) }) { Text("Sign in") }
+ }
+ }
+
+ EmailPasswordSignInViewModel.UiState.SignedIn -> {
+ Text("Current session: ${Clerk.session?.id}")
+ }
+
+ EmailPasswordSignInViewModel.UiState.Loading ->
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ }
+ }
+}
+
+```
diff --git a/docs/_partials/custom-flows/email-password/sign-in-ios.mdx b/docs/_partials/custom-flows/email-password/sign-in-ios.mdx
new file mode 100644
index 0000000000..b09140fb40
--- /dev/null
+++ b/docs/_partials/custom-flows/email-password/sign-in-ios.mdx
@@ -0,0 +1,41 @@
+```swift {{ filename: 'EmailPasswordSignInView.swift', collapsible: true }}
+import SwiftUI
+import Clerk
+
+struct EmailPasswordSignInView: View {
+ @State private var email = ""
+ @State private var password = ""
+
+ 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) }
+ }
+ }
+}
+
+extension EmailPasswordSignInView {
+
+ 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))
+
+ 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)
+ }
+ }
+}
+```
diff --git a/docs/_partials/custom-flows/email-password/sign-up-android.mdx b/docs/_partials/custom-flows/email-password/sign-up-android.mdx
new file mode 100644
index 0000000000..8c0467ab42
--- /dev/null
+++ b/docs/_partials/custom-flows/email-password/sign-up-android.mdx
@@ -0,0 +1,159 @@
+```kotlin {{ filename: 'EmailPasswordSignUpViewModel.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 EmailPasswordSignUpViewModel : ViewModel() {
+private val _uiState =
+ MutableStateFlow(UiState.Loading)
+val uiState = _uiState.asStateFlow()
+
+init {
+ combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
+ _uiState.value =
+ when {
+ !isInitialized -> UiState.Loading
+ user != null -> UiState.Verified
+ else -> UiState.Unverified
+ }
+ }
+ .launchIn(viewModelScope)
+}
+
+fun submit(email: String, password: String) {
+ viewModelScope.launch {
+ SignUp.create(SignUp.CreateParams.Standard(emailAddress = email, password = password))
+ .flatMap { it.prepareVerification(SignUp.PrepareVerificationParams.Strategy.EmailCode()) }
+ .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.EmailCode(code))
+ .onSuccess { _uiState.value = UiState.Verified }
+ .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: 'EmailPasswordSignUpActivity.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.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+
+class EmailPasswordSignUpActivity : ComponentActivity() {
+
+ val viewModel: EmailPasswordSignUpViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ EmailPasswordSignInView(
+ state = state,
+ onSubmit = viewModel::submit,
+ onVerify = viewModel::verify,
+ )
+ }
+ }
+}
+
+@Composable
+fun EmailPasswordSignInView(
+ state: EmailPasswordSignUpViewModel.UiState,
+ onSubmit: (String, String) -> Unit,
+ onVerify: (String) -> Unit,
+) {
+ var email by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+ var code by remember { mutableStateOf("") }
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ when (state) {
+ EmailPasswordSignUpViewModel.UiState.Unverified -> {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
+ TextField(
+ value = password,
+ onValueChange = { password = it },
+ visualTransformation = PasswordVisualTransformation(),
+ label = { Text("Password") },
+ )
+ Button(onClick = { onSubmit(email, password) }) { Text("Next") }
+ }
+ }
+ EmailPasswordSignUpViewModel.UiState.Verified -> {
+ Text("Verified!")
+ }
+ EmailPasswordSignUpViewModel.UiState.Verifying -> {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TextField(
+ value = code,
+ onValueChange = { code = it },
+ label = { Text("Enter your verification code") },
+ )
+ Button(onClick = { onVerify(code) }) { Text("Verify") }
+ }
+ }
+ EmailPasswordSignUpViewModel.UiState.Loading -> CircularProgressIndicator()
+ }
+ }
+}
+```
diff --git a/docs/_partials/custom-flows/email-password/sign-up-ios.mdx b/docs/_partials/custom-flows/email-password/sign-up-ios.mdx
new file mode 100644
index 0000000000..7b1045602a
--- /dev/null
+++ b/docs/_partials/custom-flows/email-password/sign-up-ios.mdx
@@ -0,0 +1,73 @@
+```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) }
+ }
+ }
+ }
+}
+
+extension EmailPasswordSignUpView {
+
+ 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))
+
+ // Send the user an email with the verification code
+ try await signUp.prepareVerification(strategy: .emailCode)
+
+ // Set 'isVerifying' 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
+ guard let inProgressSignUp = Clerk.shared.client?.signUp else { return }
+
+ // Use the code the user provided to attempt verification
+ let signUp = try await inProgressSignUp.attemptVerification(strategy: .emailCode(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)
+ }
+ }
+}
+```
diff --git a/docs/guides/development/custom-flows/authentication/email-password.mdx b/docs/guides/development/custom-flows/authentication/email-password.mdx
index 3e0370bffb..437558d951 100644
--- a/docs/guides/development/custom-flows/authentication/email-password.mdx
+++ b/docs/guides/development/custom-flows/authentication/email-password.mdx
@@ -1,6 +1,7 @@
---
title: Build a custom email/password authentication flow
description: Learn how to build a custom email/password sign-up and sign-in flow using the Clerk API.
+sdk: nextjs, react, expo, js-frontend, react-router, tanstack-react-start, ios, android
---
@@ -44,241 +45,11 @@ This guide will walk you through how to build a custom email/password sign-up an
- ```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) }
- }
- }
- }
- }
-
- extension EmailPasswordSignUpView {
-
- 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))
-
- // Send the user an email with the verification code
- try await signUp.prepareVerification(strategy: .emailCode)
-
- // Set 'isVerifying' 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
- guard let inProgressSignUp = Clerk.shared.client?.signUp else { return }
-
- // Use the code the user provided to attempt verification
- let signUp = try await inProgressSignUp.attemptVerification(strategy: .emailCode(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: 'EmailPasswordSignUpViewModel.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 EmailPasswordSignUpViewModel : ViewModel() {
- private val _uiState =
- MutableStateFlow(UiState.Loading)
- val uiState = _uiState.asStateFlow()
-
- init {
- combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
- _uiState.value =
- when {
- !isInitialized -> UiState.Loading
- user != null -> UiState.Verified
- else -> UiState.Unverified
- }
- }
- .launchIn(viewModelScope)
- }
-
- fun submit(email: String, password: String) {
- viewModelScope.launch {
- SignUp.create(SignUp.CreateParams.Standard(emailAddress = email, password = password))
- .flatMap { it.prepareVerification(SignUp.PrepareVerificationParams.Strategy.EmailCode()) }
- .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.EmailCode(code))
- .onSuccess { _uiState.value = UiState.Verified }
- .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: 'EmailPasswordSignUpActivity.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.text.input.PasswordVisualTransformation
- import androidx.compose.ui.unit.dp
- import androidx.lifecycle.compose.collectAsStateWithLifecycle
-
- class EmailPasswordSignUpActivity : ComponentActivity() {
-
- val viewModel: EmailPasswordSignUpViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- val state by viewModel.uiState.collectAsStateWithLifecycle()
- EmailPasswordSignInView(
- state = state,
- onSubmit = viewModel::submit,
- onVerify = viewModel::verify,
- )
- }
- }
- }
-
- @Composable
- fun EmailPasswordSignInView(
- state: EmailPasswordSignUpViewModel.UiState,
- onSubmit: (String, String) -> Unit,
- onVerify: (String) -> Unit,
- ) {
- var email by remember { mutableStateOf("") }
- var password by remember { mutableStateOf("") }
- var code by remember { mutableStateOf("") }
-
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- when (state) {
- EmailPasswordSignUpViewModel.UiState.Unverified -> {
- Column(
- verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- TextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
- TextField(
- value = password,
- onValueChange = { password = it },
- visualTransformation = PasswordVisualTransformation(),
- label = { Text("Password") },
- )
- Button(onClick = { onSubmit(email, password) }) { Text("Next") }
- }
- }
- EmailPasswordSignUpViewModel.UiState.Verified -> {
- Text("Verified!")
- }
- EmailPasswordSignUpViewModel.UiState.Verifying -> {
- Column(
- verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- TextField(
- value = code,
- onValueChange = { code = it },
- label = { Text("Enter your verification code") },
- )
- Button(onClick = { onVerify(code) }) { Text("Verify") }
- }
- }
- EmailPasswordSignUpViewModel.UiState.Loading -> CircularProgressIndicator()
- }
- }
- }
- ```
+
@@ -304,181 +75,11 @@ This guide will walk you through how to build a custom email/password sign-up an
- ```swift {{ filename: 'EmailPasswordSignInView.swift', collapsible: true }}
- import SwiftUI
- import Clerk
-
- struct EmailPasswordSignInView: View {
- @State private var email = ""
- @State private var password = ""
-
- 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) }
- }
- }
- }
-
- extension EmailPasswordSignInView {
-
- 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))
-
- 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)
- }
- }
- }
- ```
+
- ```kotlin {{ filename: 'EmailPasswordSignInViewModel.kt', collapsible: true }}
- import androidx.lifecycle.ViewModel
- import androidx.lifecycle.viewModelScope
- import com.clerk.api.Clerk
- import com.clerk.api.network.serialization.onFailure
- import com.clerk.api.network.serialization.onSuccess
- import com.clerk.api.signin.SignIn
- 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 EmailPasswordSignInViewModel : ViewModel() {
- private val _uiState = MutableStateFlow(
- UiState.SignedOut
- )
- val uiState = _uiState.asStateFlow()
-
- init {
- combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
- _uiState.value = when {
- !isInitialized -> UiState.Loading
- user == null -> UiState.SignedOut
- else -> UiState.SignedIn
- }
- }.launchIn(viewModelScope)
- }
-
- fun submit(email: String, password: String) {
- viewModelScope.launch {
- SignIn.create(
- SignIn.CreateParams.Strategy.Password(
- identifier = email,
- password = password
- )
- ).onSuccess {
- _uiState.value = UiState.SignedIn
- }.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 SignedOut : UiState
-
- data object SignedIn : UiState
- }
- }
- ```
-
- ```kotlin {{ filename: 'EmailPasswordSignInActivity.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.*
- import androidx.compose.material3.*
- 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.*
- import androidx.compose.ui.text.input.PasswordVisualTransformation
- import androidx.compose.ui.unit.dp
- import androidx.lifecycle.compose.collectAsStateWithLifecycle
- import com.clerk.api.Clerk
-
- class EmailPasswordSignInActivity : ComponentActivity() {
-
- val viewModel: EmailPasswordSignInViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- val state by viewModel.uiState.collectAsStateWithLifecycle()
- EmailPasswordSignInView(
- state = state,
- onSubmit = viewModel::submit
- )
- }
- }
- }
-
- @Composable
- fun EmailPasswordSignInView(
- state: EmailPasswordSignInViewModel.UiState,
- onSubmit: (String, String) -> Unit,
- ) {
- var email by remember { mutableStateOf("") }
- var password by remember { mutableStateOf("") }
-
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-
- when (state) {
-
- EmailPasswordSignInViewModel.UiState.SignedOut -> {
- Column(
- verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- TextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
- TextField(
- value = password,
- onValueChange = { password = it },
- visualTransformation = PasswordVisualTransformation(),
- label = { Text("Password") },
- )
- Button(onClick = { onSubmit(email, password) }) { Text("Sign in") }
- }
- }
-
- EmailPasswordSignInViewModel.UiState.SignedIn -> {
- Text("Current session: ${Clerk.session?.id}")
- }
-
- EmailPasswordSignInViewModel.UiState.Loading ->
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator()
- }
- }
- }
- }
-
- ```
+
From 0664f60e4f25fd4fd6bf9d4ce6a719dd2b16175f Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 8 Oct 2025 11:27:47 -0500
Subject: [PATCH 04/18] Ticket sign ups
---
.../sign-up-nextjs.mdx | 65 +++++
.../application-invitations.mdx | 233 +-----------------
.../authentication/bot-sign-up-protection.mdx | 1 +
3 files changed, 70 insertions(+), 229 deletions(-)
create mode 100644 docs/_partials/custom-flows/application-invitations/sign-up-nextjs.mdx
diff --git a/docs/_partials/custom-flows/application-invitations/sign-up-nextjs.mdx b/docs/_partials/custom-flows/application-invitations/sign-up-nextjs.mdx
new file mode 100644
index 0000000000..2745a3aa5f
--- /dev/null
+++ b/docs/_partials/custom-flows/application-invitations/sign-up-nextjs.mdx
@@ -0,0 +1,65 @@
+```tsx {{ filename: 'app/accept-invitation/page.tsx', collapsible: true }}
+'use client'
+
+import * as React from 'react'
+import { useSignUp, useUser } from '@clerk/nextjs'
+import { useRouter } from 'next/navigation'
+
+export default function Page() {
+ const { isSignedIn, user } = useUser()
+ const { signUp, errors, fetchStatus } = useSignUp()
+ const router = useRouter()
+
+ 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
+
+ await signUp.ticket({
+ firstName,
+ lastName,
+ password,
+ })
+ if (signUp.status === 'complete') {
+ await signUp.finalize({
+ navigate: () => {
+ router.push('/')
+ },
+ })
+ }
+ }
+
+ if (signUp.status === 'complete' || isSignedIn) {
+ return null
+ }
+
+ return (
+ <>
+
Sign up
+
+ >
+ )
+}
+```
diff --git a/docs/guides/development/custom-flows/authentication/application-invitations.mdx b/docs/guides/development/custom-flows/authentication/application-invitations.mdx
index 130aa4aa78..dba3507cc5 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,236 +20,10 @@ 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'
-
- 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 (
- <>
-
-
-
-
-
- ```
-
- ```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
---
From f91be3faf2c2a856668c303b139115001804e754 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 8 Oct 2025 11:49:00 -0500
Subject: [PATCH 05/18] Email password MFA
---
.../email-password-mfa/sign-in-android.mdx | 133 ++++
.../email-password-mfa/sign-in-ios.mdx | 74 ++
.../email-password-mfa/sign-in-nextjs.mdx | 86 +++
.../authentication/email-password-mfa.mdx | 730 +-----------------
4 files changed, 334 insertions(+), 689 deletions(-)
create mode 100644 docs/_partials/custom-flows/email-password-mfa/sign-in-android.mdx
create mode 100644 docs/_partials/custom-flows/email-password-mfa/sign-in-ios.mdx
create mode 100644 docs/_partials/custom-flows/email-password-mfa/sign-in-nextjs.mdx
diff --git a/docs/_partials/custom-flows/email-password-mfa/sign-in-android.mdx b/docs/_partials/custom-flows/email-password-mfa/sign-in-android.mdx
new file mode 100644
index 0000000000..cdba5aa8ed
--- /dev/null
+++ b/docs/_partials/custom-flows/email-password-mfa/sign-in-android.mdx
@@ -0,0 +1,133 @@
+```kotlin {{ filename: 'MFASignInViewModel.kt', collapsible: true }}
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.clerk.api.Clerk
+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.attemptSecondFactor
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+class MFASignInViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(UiState.Unverified)
+ val uiState = _uiState.asStateFlow()
+
+ fun submit(email: String, password: String) {
+ viewModelScope.launch {
+ SignIn.create(SignIn.CreateParams.Strategy.Password(identifier = email, password = password))
+ .onSuccess {
+ if (it.status == SignIn.Status.NEEDS_SECOND_FACTOR) {
+ // Display TOTP Form
+ _uiState.value = UiState.NeedsSecondFactor
+ } else {
+ // If the status is not needsSecondFactor, check why. User may need to
+ // complete different steps.
+ }
+ }
+ .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
+ .attemptSecondFactor(SignIn.AttemptSecondFactorParams.TOTP(code))
+ .onSuccess {
+ if (it.status == SignIn.Status.COMPLETE) {
+ // User is now signed in and verified.
+ // You can navigate to the next screen or perform other actions.
+ _uiState.value = UiState.Verified
+ }
+ }
+ .onFailure {
+ // See https://clerk.com/docs/guides/development/custom-flows/error-handling
+ // for more info on error handling
+ }
+ }
+ }
+
+ sealed interface UiState {
+ data object Unverified : UiState
+ data object Verified : UiState
+ data object NeedsSecondFactor : UiState
+ }
+}
+```
+
+```kotlin {{ filename: 'MFASignInActivity.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.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Button
+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.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+
+class MFASignInActivity : ComponentActivity() {
+ val viewModel: MFASignInViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ MFASignInView(state = state, onSubmit = viewModel::submit, onVerify = viewModel::verify)
+ }
+ }
+}
+
+@Composable
+fun MFASignInView(
+ state: MFASignInViewModel.UiState,
+ onSubmit: (String, String) -> Unit,
+ onVerify: (String) -> Unit,
+) {
+ var email by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+ var code by remember { mutableStateOf("") }
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ when (state) {
+ MFASignInViewModel.UiState.NeedsSecondFactor -> {
+ TextField(value = code, onValueChange = { code = it }, placeholder = { Text("Code") })
+ Button(onClick = { onVerify(code) }) { Text("Submit") }
+ }
+ MFASignInViewModel.UiState.Unverified -> {
+ TextField(value = email, onValueChange = { email = it }, placeholder = { Text("Email") })
+ TextField(
+ value = password,
+ onValueChange = { password = it },
+ placeholder = { Text("Password") },
+ visualTransformation = PasswordVisualTransformation(),
+ )
+ Button(onClick = { onSubmit(email, password) }) { Text("Next") }
+ }
+ MFASignInViewModel.UiState.Verified -> {
+ Text("Verified")
+ }
+ }
+ }
+}
+```
diff --git a/docs/_partials/custom-flows/email-password-mfa/sign-in-ios.mdx b/docs/_partials/custom-flows/email-password-mfa/sign-in-ios.mdx
new file mode 100644
index 0000000000..6bc8b60805
--- /dev/null
+++ b/docs/_partials/custom-flows/email-password-mfa/sign-in-ios.mdx
@@ -0,0 +1,74 @@
+```swift {{ filename: 'MFASignInView.swift', collapsible: true }}
+import SwiftUI
+import Clerk
+
+struct MFASignInView: View {
+@State private var email = ""
+@State private var password = ""
+@State private var code = ""
+@State private var displayTOTP = false
+
+var body: some View {
+ if displayTOTP {
+ TextField("Code", text: $code)
+ Button("Verify") {
+ Task { await verify(code: code) }
+ }
+ } else {
+ TextField("Email", text: $email)
+ SecureField("Password", text: $password)
+ Button("Next") {
+ Task { await submit(email: email, password: password) }
+ }
+ }
+}
+}
+
+extension MFASignInView {
+
+func submit(email: String, password: String) async {
+ do {
+ // Start the sign-in process.
+ let signIn = try await SignIn.create(strategy: .identifier(email, password: password))
+
+ switch signIn.status {
+ case .needsSecondFactor:
+ // Handle user submitting email and password and swapping to TOTP form.
+ displayTOTP = true
+ default:
+ // If the status is not needsSecondFactor, check why. User may need to
+ // complete different steps.
+ dump(signIn.status)
+ }
+ } 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 }
+
+ // Attempt the TOTP or backup code verification.
+ let signIn = try await inProgressSignIn.attemptSecondFactor(strategy: .totp(code: code))
+
+ 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)
+ }
+}
+}
+```
diff --git a/docs/_partials/custom-flows/email-password-mfa/sign-in-nextjs.mdx b/docs/_partials/custom-flows/email-password-mfa/sign-in-nextjs.mdx
new file mode 100644
index 0000000000..7221c5a257
--- /dev/null
+++ b/docs/_partials/custom-flows/email-password-mfa/sign-in-nextjs.mdx
@@ -0,0 +1,86 @@
+```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,
+ })
+ }
+
+ const handleSubmitTOTP = async (formData: FormData) => {
+ const code = formData.get('code') as string
+ const useBackupCode = formData.get('useBackupCode') === 'on'
+
+ if (useBackupCode) {
+ await signIn.mfa.verifyBackupCode({ code })
+ } else {
+ await signIn.mfa.verifyTOTP({ code })
+ }
+
+ if (signIn.status === 'complete') {
+ await signIn.finalize({
+ navigate: () => {
+ router.push('/')
+ },
+ })
+ }
+ }
+
+ if (signIn.status === 'needs_second_factor') {
+ return (
+
+
Verify your account
+
+
+ )
+ }
+
+ return (
+ <>
+
Sign in
+
+ >
+ )
+}
+```
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..3054daca2f 100644
--- a/docs/guides/development/custom-flows/authentication/email-password-mfa.mdx
+++ b/docs/guides/development/custom-flows/authentication/email-password-mfa.mdx
@@ -1,6 +1,7 @@
---
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
---
@@ -36,695 +37,46 @@ 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)
- }
-
- // 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 (
-
-
-
-
-
-
-
- ```
-
- ```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)} />
-
-
-
- )
- }
-
- return (
-
- Sign in
-
- setEmail(email)}
- />
-
-
-
- setPassword(password)}
- />
-
-
-
-
- )
- }
- ```
-
-
-
- ```swift {{ filename: 'MFASignInView.swift', collapsible: true }}
- import SwiftUI
- import Clerk
-
- struct MFASignInView: View {
- @State private var email = ""
- @State private var password = ""
- @State private var code = ""
- @State private var displayTOTP = false
-
- var body: some View {
- if displayTOTP {
- TextField("Code", text: $code)
- Button("Verify") {
- Task { await verify(code: code) }
- }
- } else {
- TextField("Email", text: $email)
- SecureField("Password", text: $password)
- Button("Next") {
- Task { await submit(email: email, password: password) }
- }
- }
- }
- }
-
- extension MFASignInView {
-
- func submit(email: String, password: String) async {
- do {
- // Start the sign-in process.
- let signIn = try await SignIn.create(strategy: .identifier(email, password: password))
-
- switch signIn.status {
- case .needsSecondFactor:
- // Handle user submitting email and password and swapping to TOTP form.
- displayTOTP = true
- default:
- // If the status is not needsSecondFactor, check why. User may need to
- // complete different steps.
- dump(signIn.status)
- }
- } 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 }
-
- // Attempt the TOTP or backup code verification.
- let signIn = try await inProgressSignIn.attemptSecondFactor(strategy: .totp(code: code))
-
- 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)
- }
- }
- }
- ```
-
-
-
- ```kotlin {{ filename: 'MFASignInViewModel.kt', collapsible: true }}
- import androidx.lifecycle.ViewModel
- import androidx.lifecycle.viewModelScope
- import com.clerk.api.Clerk
- 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.attemptSecondFactor
- import kotlinx.coroutines.flow.MutableStateFlow
- import kotlinx.coroutines.flow.asStateFlow
- import kotlinx.coroutines.launch
-
- class MFASignInViewModel : ViewModel() {
- private val _uiState = MutableStateFlow(UiState.Unverified)
- val uiState = _uiState.asStateFlow()
-
- fun submit(email: String, password: String) {
- viewModelScope.launch {
- SignIn.create(SignIn.CreateParams.Strategy.Password(identifier = email, password = password))
- .onSuccess {
- if (it.status == SignIn.Status.NEEDS_SECOND_FACTOR) {
- // Display TOTP Form
- _uiState.value = UiState.NeedsSecondFactor
- } else {
- // If the status is not needsSecondFactor, check why. User may need to
- // complete different steps.
- }
- }
- .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
- .attemptSecondFactor(SignIn.AttemptSecondFactorParams.TOTP(code))
- .onSuccess {
- if (it.status == SignIn.Status.COMPLETE) {
- // User is now signed in and verified.
- // You can navigate to the next screen or perform other actions.
- _uiState.value = UiState.Verified
- }
- }
- .onFailure {
- // See https://clerk.com/docs/guides/development/custom-flows/error-handling
- // for more info on error handling
- }
- }
- }
-
- sealed interface UiState {
- data object Unverified : UiState
- data object Verified : UiState
- data object NeedsSecondFactor : UiState
- }
- }
- ```
-
- ```kotlin {{ filename: 'MFASignInActivity.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.Column
- import androidx.compose.foundation.layout.fillMaxSize
- import androidx.compose.material3.Button
- 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.text.input.PasswordVisualTransformation
- import androidx.compose.ui.unit.dp
- import androidx.lifecycle.compose.collectAsStateWithLifecycle
-
- class MFASignInActivity : ComponentActivity() {
- val viewModel: MFASignInViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- val state by viewModel.uiState.collectAsStateWithLifecycle()
- MFASignInView(state = state, onSubmit = viewModel::submit, onVerify = viewModel::verify)
- }
- }
- }
-
- @Composable
- fun MFASignInView(
- state: MFASignInViewModel.UiState,
- onSubmit: (String, String) -> Unit,
- onVerify: (String) -> Unit,
- ) {
- var email by remember { mutableStateOf("") }
- var password by remember { mutableStateOf("") }
- var code by remember { mutableStateOf("") }
-
- Column(
- modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- when (state) {
- MFASignInViewModel.UiState.NeedsSecondFactor -> {
- TextField(value = code, onValueChange = { code = it }, placeholder = { Text("Code") })
- Button(onClick = { onVerify(code) }) { Text("Submit") }
- }
- MFASignInViewModel.UiState.Unverified -> {
- TextField(value = email, onValueChange = { email = it }, placeholder = { Text("Email") })
- TextField(
- value = password,
- onValueChange = { password = it },
- placeholder = { Text("Password") },
- visualTransformation = PasswordVisualTransformation(),
- )
- Button(onClick = { onSubmit(email, password) }) { Text("Next") }
- }
- MFASignInViewModel.UiState.Verified -> {
- Text("Verified")
- }
- }
- }
- }
- ```
-
-
+
+ 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.
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{/* TODO: Add logic for MFA for phone code */}
From 4f71b20c591b0852ffc552252650bd601c9d6e03 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 8 Oct 2025 12:49:39 -0500
Subject: [PATCH 06/18] Add sdks for google one tap
---
.../development/custom-flows/authentication/google-one-tap.mdx | 1 +
1 file changed, 1 insertion(+)
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
---
From fbb28c071ad3277ef33e37dd91ad5ed6180cde66 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 8 Oct 2025 12:49:52 -0500
Subject: [PATCH 07/18] Add version callout
---
.../authentication/email-password-mfa.mdx | 13 +++++++++++++
.../custom-flows/authentication/email-password.mdx | 13 +++++++++++++
2 files changed, 26 insertions(+)
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 3054daca2f..1cf829f54b 100644
--- a/docs/guides/development/custom-flows/authentication/email-password-mfa.mdx
+++ b/docs/guides/development/custom-flows/authentication/email-password-mfa.mdx
@@ -6,6 +6,19 @@ sdk: nextjs, react, expo, js-frontend, react-router, tanstack-react-start, ios,
+
+ > [!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**.
diff --git a/docs/guides/development/custom-flows/authentication/email-password.mdx b/docs/guides/development/custom-flows/authentication/email-password.mdx
index 437558d951..28ae11c358 100644
--- a/docs/guides/development/custom-flows/authentication/email-password.mdx
+++ b/docs/guides/development/custom-flows/authentication/email-password.mdx
@@ -6,6 +6,19 @@ sdk: nextjs, react, expo, js-frontend, react-router, tanstack-react-start, ios,
+
+ > [!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).
+
+
This guide will walk you through how to build a custom email/password sign-up and sign-in flow.
From db3dc143cbf0222dde932bd057e83f95b8e199db Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 8 Oct 2025 12:55:08 -0500
Subject: [PATCH 08/18] Remove docs that don't use legacy APIs
---
.../legacy/bot-sign-up-protection.mdx | 76 ------
.../authentication/legacy/google-one-tap.mdx | 128 ----------
.../legacy/multi-session-applications.mdx | 173 --------------
.../authentication/legacy/sign-out.mdx | 220 ------------------
4 files changed, 597 deletions(-)
delete mode 100644 docs/guides/development/custom-flows/authentication/legacy/bot-sign-up-protection.mdx
delete mode 100644 docs/guides/development/custom-flows/authentication/legacy/google-one-tap.mdx
delete mode 100644 docs/guides/development/custom-flows/authentication/legacy/multi-session-applications.mdx
delete mode 100644 docs/guides/development/custom-flows/authentication/legacy/sign-out.mdx
diff --git a/docs/guides/development/custom-flows/authentication/legacy/bot-sign-up-protection.mdx b/docs/guides/development/custom-flows/authentication/legacy/bot-sign-up-protection.mdx
deleted file mode 100644
index d9863c9b42..0000000000
--- a/docs/guides/development/custom-flows/authentication/legacy/bot-sign-up-protection.mdx
+++ /dev/null
@@ -1,76 +0,0 @@
----
-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.
----
-
-
-
-Clerk provides the ability to add a CAPTCHA widget to your sign-up flows to protect against bot sign-ups. The [``](/docs/reference/components/authentication/sign-up) component handles this flow out-of-the-box. However, if you're building a custom user interface, this guide will show you how to add the CAPTCHA widget to your custom sign-up flow.
-
-
- ## Enable bot sign-up protection
-
- 1. In the Clerk Dashboard, navigate to the [**Attack protection**](https://dashboard.clerk.com/last-active?path=user-authentication/attack-protection) page.
- 1. Enable the **Bot sign-up protection** toggle.
-
- > [!WARNING]
- > If you currently have the **Invisible** CAPTCHA type selected, it's highly recommended to switch to the **Smart** option, as the **Invisible** option is deprecated and will be removed in a future update.
-
- ## Add the CAPTCHA widget to your custom sign-up form
-
- To render the CAPTCHA widget in your custom sign-up form, **you need to include the `` element by the time you call `signUp.create()`**. This element acts as a placeholder onto which the widget will be rendered.
-
- If this element is not found, the SDK will transparently fall back to an invisible widget in order to avoid breaking your sign-up flow. If this happens, you should see a relevant error in your browser's console.
-
- > [!TIP]
- > The invisible widget fallback automatically blocks suspected bot traffic without offering users falsely detected as bots with an opportunity to prove otherwise. Therefore, it's strongly recommended that you ensure the `` element exists in your DOM.
-
- The following example shows how to support the CAPTCHA widget:
-
- ```tsx {{ mark: [[25, 26]] }}
- <>
-
Sign up
-
- >
- ```
-
- ## Customize the appearance of the CAPTCHA widget
-
- You can customize the appearance of the CAPTCHA widget by passing data attributes to the `` element. The following attributes are supported:
-
- - `data-cl-theme`: The CAPTCHA widget theme. Can take the following values: `'light'`, `'dark'`, `'auto'`. Defaults to `'auto'`.
- - `data-cl-size`: The CAPTCHA widget size. Can take the following values: `'normal'`, `'flexible'`, `'compact'`. Defaults to `'normal'`.
- - `data-cl-language`: The CAPTCHA widget language. Must be either `'auto'` (default) to use the language that the visitor has chosen, or language and country code (e.g. `'en-US'`). Some languages are [supported by Clerk](/docs/guides/customizing-clerk/localization) but not by Cloudflare Turnstile, which is used for the CAPTCHA widget. See [Cloudflare Turnstile's supported languages](https://developers.cloudflare.com/turnstile/reference/supported-languages).
-
- For example, to set the theme to `'dark'`, the size to `'flexible'`, and the language to `'es-ES'`, you would add the following attributes to the `` element:
-
- ```html
-
- ```
-
diff --git a/docs/guides/development/custom-flows/authentication/legacy/google-one-tap.mdx b/docs/guides/development/custom-flows/authentication/legacy/google-one-tap.mdx
deleted file mode 100644
index 68b68c93e8..0000000000
--- a/docs/guides/development/custom-flows/authentication/legacy/google-one-tap.mdx
+++ /dev/null
@@ -1,128 +0,0 @@
----
-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.
----
-
-
-
-[Google One Tap](https://developers.google.com/identity/gsi/web/guides/features) enables users to press a single button to authentication in your Clerk application with a Google account.
-
-This guide will walk you through how to build a custom Google One Tap authentication flow.
-
-
- ## Enable Google as a social connection
-
- To use Google One Tap with Clerk, follow the steps in the [dedicated guide](/docs/guides/configure/auth-strategies/social-connections/google#configure-for-your-production-instance) to configure Google as a social connection in the Clerk Dashboard using custom credentials.
-
- ## Create the Google One Tap authentication flow
-
- To authenticate users with Google One Tap, you must:
-
- 1. Initialize a ["Sign In With Google"](https://developers.google.com/identity/gsi/web/reference/js-reference) client UI, passing in your Client ID.
- 1. Use the response to authenticate the user in your Clerk app if the request was successful.
- 1. Redirect the user back to the page they started the authentication flow from by default, or to another URL if necessary.
-
- The following example creates a component that implements a custom Google One Tap authentication flow, which can be used in a sign-in or sign-up page.
-
-
-
- ```tsx {{ filename: 'app/components/CustomGoogleOneTap.tsx', collapsible: true }}
- 'use client'
- import { useClerk } from '@clerk/nextjs'
- import { useRouter } from 'next/navigation'
- import Script from 'next/script'
- import { useEffect } from 'react'
-
- // Add clerk to Window to avoid type errors
- declare global {
- interface Window {
- google: any
- }
- }
-
- export function CustomGoogleOneTap({ children }: { children: React.ReactNode }) {
- const clerk = useClerk()
- const router = useRouter()
-
- useEffect(() => {
- // Will show the One Tap UI after two seconds
- const timeout = setTimeout(() => oneTap(), 2000)
- return () => {
- clearTimeout(timeout)
- }
- }, [])
-
- const oneTap = () => {
- const { google } = window
- if (google) {
- google.accounts.id.initialize({
- // Add your Google Client ID here.
- client_id: 'xxx-xxx-xxx',
- callback: async (response: any) => {
- // Here we call our provider with the token provided by Google
- call(response.credential)
- },
- })
-
- // Uncomment below to show the One Tap UI without
- // logging any notifications.
- // return google.accounts.id.prompt() // without listening to notification
-
- // Display the One Tap UI, and log any errors that occur.
- return google.accounts.id.prompt((notification: any) => {
- console.log('Notification ::', notification)
- if (notification.isNotDisplayed()) {
- console.log('getNotDisplayedReason ::', notification.getNotDisplayedReason())
- } else if (notification.isSkippedMoment()) {
- console.log('getSkippedReason ::', notification.getSkippedReason())
- } else if (notification.isDismissedMoment()) {
- console.log('getDismissedReason ::', notification.getDismissedReason())
- }
- })
- }
- }
-
- const call = async (token: any) => {
- try {
- const res = await clerk.authenticateWithGoogleOneTap({
- token,
- })
-
- await clerk.handleGoogleOneTapCallback(res, {
- signInFallbackRedirectUrl: '/example-fallback-path',
- })
- } catch (error) {
- router.push('/sign-in')
- }
- }
-
- return (
- <>
-
- >
- )
- }
- ```
-
-
-
- You can then display this component on any page. The following example demonstrates a page that displays this component:
-
-
-
- ```tsx {{ filename: 'app/google-sign-in-example/page.tsx' }}
- import { CustomGoogleOneTap } from '@/app/components/CustomGoogleOneTap'
-
- export default function CustomOneTapPage({ children }: { children: React.ReactNode }) {
- return (
-
-
Google One Tap Example
-
- )
- }
- ```
-
-
-
diff --git a/docs/guides/development/custom-flows/authentication/legacy/multi-session-applications.mdx b/docs/guides/development/custom-flows/authentication/legacy/multi-session-applications.mdx
deleted file mode 100644
index 3a3beae7d4..0000000000
--- a/docs/guides/development/custom-flows/authentication/legacy/multi-session-applications.mdx
+++ /dev/null
@@ -1,173 +0,0 @@
----
-title: Build a custom multi-session flow
-description: Learn how to use the Clerk API to add multi-session handling to your application.
----
-
-
-
-A multi-session application is an application that allows multiple accounts to be signed in from the same browser at the same time. The user can switch from one account to another seamlessly. Each account is independent from the rest and has access to different resources.
-
-This guide provides you with the necessary information to build a custom multi-session flow using the Clerk API.
-
-To implement the multi-session feature to your application, you need to handle the following scenarios:
-
-- [Switching between different accounts](#switch-between-sessions)
-- [Adding new accounts](#add-a-new-session)
-- [Signing out from one account, while remaining signed in to the rest](#sign-out-active-session)
-- [Signing out from all accounts](#sign-out-all-sessions)
-
-## Enable multi-session in your application
-
-To enable multi-session in your application, you need to configure it in the Clerk Dashboard.
-
-1. In the Clerk Dashboard, navigate to the [**Sessions**](https://dashboard.clerk.com/last-active?path=sessions) page.
-1. Toggle on **Multi-session handling**.
-1. Select **Save changes**.
-
-## Get the session and user
-
-
- ```jsx
- import { useClerk } from '@clerk/clerk-react'
-
- // Get the session and user
- const { session, user } = useClerk()
- ```
-
- ```js
- // Get the session
- const currentSession = window.Clerk.session
-
- // Get the user
- const currentUser = window.Clerk.user
- ```
-
- ```swift
- // Get the current session
- var currentSession: Session? { Clerk.shared.session }
-
- // Get the current user
- var currentUser: User? { Clerk.shared.user }
- ```
-
-
-## Switch between sessions
-
-
- ```jsx
- import { useClerk } from '@clerk/clerk-react'
-
- const { client, setActive } = useClerk()
-
- // You can get all the available sessions through the client
- const availableSessions = client.sessions
- const currentSession = availableSessions[0].id
-
- // Use setActive() to set the session as active
- await setActive({
- session: currentSession.id,
- 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('/')
- },
- })
- ```
-
- ```js
- // You can get all the available sessions through the client
- const availableSessions = window.Clerk.client.sessions
-
- // Use setActive() to set the session as active
- await window.Clerk.setActive({
- session: availableSessions[0].id,
- 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('/')
- },
- })
- ```
-
- ```swift
- // You can get all the available sessions through the client
- var availableSessions: [Session] { Clerk.shared.client?.sessions ?? [] }
-
- // Use setActive() to set the session as active
- try await Clerk.shared.setActive(sessionId: session.id)
- ```
-
-
-## Add a new session
-
-To add a new session, simply link to your existing sign-in flow. New sign-ins will automatically add to the list of available sessions on the client. To create a sign-in flow, see one of the following popular guides:
-
-- [Email and password](/docs/guides/development/custom-flows/authentication/email-password)
-- [Passwordless authentication](/docs/guides/development/custom-flows/authentication/email-sms-otp)
-- [Social sign-in (OAuth)](/docs/guides/configure/auth-strategies/social-connections/overview)
-
-For more information on how Clerk's sign-in flow works, see the [detailed sign-in guide](/docs/guides/development/custom-flows/overview#sign-in-flow).
-
-## Sign out all sessions
-
-Use [`signOut()`](/docs/reference/javascript/clerk#sign-out) to deactivate all sessions on the current client.
-
-
- ```jsx
- import { useClerk } from '@clerk/clerk-react'
-
- const { signOut, session } = useClerk()
-
- // Use signOut to sign-out all active sessions.
- await signOut()
- ```
-
- ```js
- // Use signOut to sign-out all active sessions.
- await window.Clerk.signOut()
- ```
-
- ```swift
- // Use signOut to sign-out all active sessions.
- try await Clerk.shared.signOut()
- ```
-
-
-## Sign out active session
-
-Use [`signOut()`](/docs/reference/javascript/clerk#sign-out) to deactivate a specific session by passing the session ID.
-
-
- ```jsx
- import { useClerk } from '@clerk/clerk-react'
-
- // Get the signOut method and the active session
- const { signOut, session } = useClerk()
-
- // Use signOut to sign-out the active session
- await signOut(session.id)
- ```
-
- ```js
- // Get the current session
- const currentSession = window.Clerk.session
-
- // Use signOut to sign-out the active session
- await window.Clerk.signOut(currentSession.id)
- ```
-
- ```swift
- // Use signOut to sign-out a specific session
- try await Clerk.shared.signOut(sessionId: session.id)
- ```
-
diff --git a/docs/guides/development/custom-flows/authentication/legacy/sign-out.mdx b/docs/guides/development/custom-flows/authentication/legacy/sign-out.mdx
deleted file mode 100644
index 01ee0d2ba1..0000000000
--- a/docs/guides/development/custom-flows/authentication/legacy/sign-out.mdx
+++ /dev/null
@@ -1,220 +0,0 @@
----
-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.
----
-
-
-
-Clerk's [``](/docs/reference/components/user/user-button) and [``](/docs/reference/components/unstyled/sign-out-button) components provide an out-of-the-box solution for signing out users. However, if you're building a custom solution, you can use the [`signOut()`](/docs/reference/javascript/clerk#sign-out) function to handle the sign-out process.
-
-The `signOut()` function signs a user out of all sessions in a [multi-session application](/docs/guides/secure/session-options#multi-session-applications), or only the current session in a single-session context. You can also specify a specific session to sign out by passing the `sessionId` parameter.
-
-> [!NOTE]
-> The sign-out flow deactivates only the current session. Other valid sessions associated with the same user (e.g., if the user is signed in on another device) will remain active.
-
-
-
- The [`useClerk()`](/docs/reference/hooks/use-clerk) hook is used to access the `signOut()` function, which is called when the user clicks the sign-out button.
-
- This example is written for Next.js App Router but can be adapted for any React-based framework.
-
- ```jsx {{ filename: 'app/components/SignOutButton.tsx' }}
- 'use client'
-
- import { useClerk } from '@clerk/nextjs'
-
- export const SignOutButton = () => {
- const { signOut } = useClerk()
-
- return (
- // Clicking this button signs out a user
- // and redirects them to the home page "/".
-
- )
- }
- ```
-
-
-
-
- ```html {{ filename: 'index.html', collapsible: true }}
-
-
-
-
-
-
- Clerk + JavaScript App
-
-
-
-
-
-
-
-
- ```
-
- ```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) {
- // Attach signOut function to the sign-out button
- document.getElementById('sign-out').addEventListener('click', async () => {
- await clerk.signOut()
- // Optional: refresh page after sign-out
- window.location.reload()
- })
- }
- ```
-
-
-
-
- The [`useClerk()`](/docs/reference/hooks/use-clerk) hook is used to access the `signOut()` function, which is called when the user clicks the "Sign out" button.
-
-
-
-
-
- ```swift {{ filename: 'SignOutView.swift', collapsible: true }}
- import SwiftUI
- import Clerk
-
- struct SignOutView: View {
- @Environment(Clerk.self) private var clerk
-
- var body: some View {
- if let session = clerk.session {
- Text("Active Session: \(session.id)")
- Button("Sign out") {
- Task { await signOut() }
- }
- } else {
- Text("You are signed out")
- }
- }
- }
-
- extension SignOutView {
-
- func signOut() async {
- do {
- try await clerk.signOut()
- } catch {
- // See https://clerk.com/docs/guides/development/custom-flows/error-handling
- // for more info on error handling.
- dump(error)
- }
- }
- }
- ```
-
-
-
- ```kotlin {{ filename: 'MainViewModel.kt', collapsible: true }}
- import androidx.lifecycle.ViewModel
- import androidx.lifecycle.viewModelScope
- import com.clerk.api.Clerk
- import com.clerk.api.network.serialization.onFailure
- import com.clerk.api.network.serialization.onSuccess
- 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 MainViewModel : ViewModel() {
-
- private val _uiState = MutableStateFlow(UiState.Loading)
- val uiState = _uiState.asStateFlow()
-
- init {
- combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
- _uiState.value =
- when {
- !isInitialized -> UiState.Loading
- user != null -> UiState.SignedIn
- else -> UiState.SignedOut
- }
- }
- .launchIn(viewModelScope)
- }
-
- fun signOut() {
- viewModelScope.launch {
- Clerk.signOut()
- .onSuccess { _uiState.value = UiState.SignedOut }
- .onFailure {
- // See https://clerk.com/docs/guides/development/custom-flows/error-handling
- // for more info on error handling
- }
- }
- }
-
- sealed interface UiState {
- data object SignedIn : UiState
-
- data object SignedOut : UiState
-
- data object Loading : UiState
- }
- }
- ```
-
- ```kotlin {{ filename: 'MainActivity.kt', collapsible: true }}
- import android.os.Bundle
- import androidx.activity.ComponentActivity
- import androidx.activity.compose.setContent
- import androidx.activity.enableEdgeToEdge
- import androidx.activity.viewModels
- import androidx.compose.foundation.layout.Box
- import androidx.compose.foundation.layout.fillMaxSize
- import androidx.compose.material3.Button
- import androidx.compose.material3.CircularProgressIndicator
- import androidx.compose.material3.Text
- import androidx.compose.runtime.getValue
- import androidx.compose.ui.Alignment
- import androidx.compose.ui.Modifier
- import androidx.lifecycle.compose.collectAsStateWithLifecycle
-
- class MainActivity : ComponentActivity() {
- val viewModel: MainViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- setContent {
- val state by viewModel.uiState.collectAsStateWithLifecycle()
-
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- when (state) {
- MainViewModel.UiState.Loading -> {
- CircularProgressIndicator()
- }
-
- MainViewModel.UiState.SignedIn -> {
- Button(onClick = { viewModel.signOut() }) {
- Text("Sign out")
- }
- }
-
- MainViewModel.UiState.SignedOut -> {
- // Signed out content
- }
- }
- }
- }
- }
- }
- ```
-
-
From 8ddbd36751573c1f5a637b27709fb26385041679 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 8 Oct 2025 12:55:24 -0500
Subject: [PATCH 09/18] Add SDK filter to sign-out page
---
docs/guides/development/custom-flows/authentication/sign-out.mdx | 1 +
1 file changed, 1 insertion(+)
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
---
From 564424e4083db738f05cba2e09ebaf0bef259b24 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 8 Oct 2025 13:46:13 -0500
Subject: [PATCH 10/18] Add email/sms otp guide
---
.../email-sms-otp/sign-in-android.mdx | 157 +++
.../email-sms-otp/sign-in-ios.mdx | 69 ++
.../email-sms-otp/sign-in-nextjs.mdx | 63 +
.../email-sms-otp/sign-up-android.mdx | 156 +++
.../email-sms-otp/sign-up-ios.mdx | 69 ++
.../email-sms-otp/sign-up-nextjs.mdx | 60 +
.../authentication/email-sms-otp.mdx | 1018 ++---------------
7 files changed, 647 insertions(+), 945 deletions(-)
create mode 100644 docs/_partials/custom-flows/email-sms-otp/sign-in-android.mdx
create mode 100644 docs/_partials/custom-flows/email-sms-otp/sign-in-ios.mdx
create mode 100644 docs/_partials/custom-flows/email-sms-otp/sign-in-nextjs.mdx
create mode 100644 docs/_partials/custom-flows/email-sms-otp/sign-up-android.mdx
create mode 100644 docs/_partials/custom-flows/email-sms-otp/sign-up-ios.mdx
create mode 100644 docs/_partials/custom-flows/email-sms-otp/sign-up-nextjs.mdx
diff --git a/docs/_partials/custom-flows/email-sms-otp/sign-in-android.mdx b/docs/_partials/custom-flows/email-sms-otp/sign-in-android.mdx
new file mode 100644
index 0000000000..3456b608f5
--- /dev/null
+++ b/docs/_partials/custom-flows/email-sms-otp/sign-in-android.mdx
@@ -0,0 +1,157 @@
+```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) }
+ }
+}
+```
diff --git a/docs/_partials/custom-flows/email-sms-otp/sign-in-ios.mdx b/docs/_partials/custom-flows/email-sms-otp/sign-in-ios.mdx
new file mode 100644
index 0000000000..b3c531fb97
--- /dev/null
+++ b/docs/_partials/custom-flows/email-sms-otp/sign-in-ios.mdx
@@ -0,0 +1,69 @@
+```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)
+ }
+}
+}
+```
diff --git a/docs/_partials/custom-flows/email-sms-otp/sign-in-nextjs.mdx b/docs/_partials/custom-flows/email-sms-otp/sign-in-nextjs.mdx
new file mode 100644
index 0000000000..02e2b289f0
--- /dev/null
+++ b/docs/_partials/custom-flows/email-sms-otp/sign-in-nextjs.mdx
@@ -0,0 +1,63 @@
+This example is written for Next.js App Router but it can be adapted to any React-based framework.
+
+```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 Page() {
+ const { signIn, errors, fetchStatus } = useSignIn()
+ const router = useRouter()
+
+ async function handleSubmit(formData: FormData) {
+ const phoneNumber = formData.get('phoneNumber') as string
+
+ await signIn.phoneCode.sendCode({ phoneNumber })
+ }
+
+ 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('/')
+ },
+ })
+ }
+ }
+
+ if (signIn.status === 'needs_second_factor') {
+ return (
+ <>
+
Verify your phone number
+
+ >
+ )
+ }
+
+ return (
+ <>
+
Sign in
+
+ >
+ )
+}
+```
diff --git a/docs/_partials/custom-flows/email-sms-otp/sign-up-android.mdx b/docs/_partials/custom-flows/email-sms-otp/sign-up-android.mdx
new file mode 100644
index 0000000000..23a4faa3bd
--- /dev/null
+++ b/docs/_partials/custom-flows/email-sms-otp/sign-up-android.mdx
@@ -0,0 +1,156 @@
+```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) }
+ }
+}
+```
diff --git a/docs/_partials/custom-flows/email-sms-otp/sign-up-ios.mdx b/docs/_partials/custom-flows/email-sms-otp/sign-up-ios.mdx
new file mode 100644
index 0000000000..f13922b0c7
--- /dev/null
+++ b/docs/_partials/custom-flows/email-sms-otp/sign-up-ios.mdx
@@ -0,0 +1,69 @@
+```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)
+ }
+}
+}
+```
diff --git a/docs/_partials/custom-flows/email-sms-otp/sign-up-nextjs.mdx b/docs/_partials/custom-flows/email-sms-otp/sign-up-nextjs.mdx
new file mode 100644
index 0000000000..2df23bbebe
--- /dev/null
+++ b/docs/_partials/custom-flows/email-sms-otp/sign-up-nextjs.mdx
@@ -0,0 +1,60 @@
+This example is written for Next.js App Router but it can be adapted for any React-based framework.
+
+```tsx {{ filename: 'app/sign-up/page.tsx', collapsible: true }}
+'use client'
+
+import * as React from 'react'
+import { useAuth, useSignUp } from '@clerk/nextjs'
+import { useRouter } from 'next/navigation'
+
+export default function SignUpPage() {
+ const { signUp, errors, fetchStatus } = useSignUp()
+ const { isSignedIn } = useAuth()
+
+ const handleSubmit = async (formData: FormData) => {
+ const phoneNumber = formData.get('phoneNumber') as string
+
+ await signUp.create({ phoneNumber })
+
+ await signUp.verifications.sendPhoneCode()
+ }
+
+ const handleVerify = async (formData: FormData) => {
+ const code = formData.get('code') as string
+
+ await signUp.verifications.verifyPhoneCode({ code })
+ if (signUp.status === 'complete') {
+ await signUp.finalize({
+ navigate: () => {
+ router.push('/dashboard')
+ },
+ })
+ }
+ }
+
+ if (signUp.status === 'complete' || isSignedIn) {
+ return null
+ }
+
+ if (
+ signUp.status === 'missing_requirements' &&
+ signUp.unverifiedFields.includes('phone_number')
+ ) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+```
diff --git a/docs/guides/development/custom-flows/authentication/email-sms-otp.mdx b/docs/guides/development/custom-flows/authentication/email-sms-otp.mdx
index 32f4525723..189bbb7a9e 100644
--- a/docs/guides/development/custom-flows/authentication/email-sms-otp.mdx
+++ b/docs/guides/development/custom-flows/authentication/email-sms-otp.mdx
@@ -1,10 +1,24 @@
---
title: Build a custom email or SMS OTP authentication flow
description: Learn how build a custom email or SMS one time code (OTP) authentication flow using the Clerk API.
+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).
+
+
Clerk supports passwordless authentication, which lets users sign in and sign up without having to remember a password. Instead, users receive a one-time password (OTP), also known as a one-time code, via email or SMS, which they can use to authenticate themselves.
This guide will walk you through how to build a custom SMS OTP sign-up and sign-in flow. The process for using email OTP is similar, and the differences will be highlighted throughout.
@@ -23,960 +37,74 @@ This guide will walk you through how to build a custom SMS OTP sign-up and sign-
To sign up a user using an OTP, you must:
- 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.
-
-
-
- This example is written for Next.js App Router but it can be adapted to any React-based framework.
-
- ```tsx {{ filename: 'app/sign-up/[[...sign-up]]/page.tsx', collapsible: true }}
- 'use client'
-
- import * as React from 'react'
- import { useSignUp } from '@clerk/nextjs'
- import { useRouter } from 'next/navigation'
-
- export default function Page() {
- const { isLoaded, signUp, setActive } = useSignUp()
- 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 && !signUp) return null
-
- try {
- // Start the sign-up process using the phone number method
- await signUp.create({
- phoneNumber: phone,
- })
-
- // Start the verification - a SMS message will be sent to the
- // number with a one-time code
- await signUp.preparePhoneNumberVerification()
-
- // 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 && !signUp) return null
-
- try {
- // Use the code provided by the user and attempt verification
- const signUpAttempt = await signUp.attemptPhoneNumberVerification({
- 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
- }
-
- await router.push('/')
- },
- })
- } else {
- // If the status is not complete, check why. User may need to
- // complete further steps.
- console.error(signUpAttempt)
- }
- } 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 (
- <>
-
-
-
-
-
-
-
- ```
-
- ```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()
+
+ 1. Initiate the sign-up process by collecting the user's phone number with the [`signUp.create()`](/docs/reference/javascript/sign-up-future#create) method.
+ 1. Send a one-time code to the provided phone number for verification with the [`signUp.verifications.sendPhoneCode()`](/docs/reference/javascript/sign-up-future#verifications-send-phone-code) method.
+ 1. Collect the one-time code and verify it with the [`signUp.verifications.verifyPhoneCode()`](/docs/reference/javascript/sign-up-future#verifications-verify-phone-code) method.
+ 1. If the phone number verification is successful, finalize the sign-up with the [`signUp.finalize()`](/docs/reference/javascript/sign-up-future#finalize) method to create the user and set the newly created session as the active session.
- 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)
- }
- })
+ 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.
+
- // Handle the verification form
- document.getElementById('verifying').addEventListener('submit', async (e) => {
- const formData = new FormData(e.target)
- const code = formData.get('code')
+
+ 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.
- 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.
+ 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 (
- <>
-
+
+ >
+ )
+ }
+ ```
-
+ ```swift {{ filename: 'MFASignInView.swift', collapsible: true }}
+ import SwiftUI
+ import Clerk
+
+ struct MFASignInView: View {
+ @State private var email = ""
+ @State private var password = ""
+ @State private var code = ""
+ @State private var displayTOTP = false
+
+ var body: some View {
+ if displayTOTP {
+ TextField("Code", text: $code)
+ Button("Verify") {
+ Task { await verify(code: code) }
+ }
+ } else {
+ TextField("Email", text: $email)
+ SecureField("Password", text: $password)
+ Button("Next") {
+ Task { await submit(email: email, password: password) }
+ }
+ }
+ }
+ }
+
+ extension MFASignInView {
+
+ func submit(email: String, password: String) async {
+ do {
+ // Start the sign-in process.
+ let signIn = try await SignIn.create(strategy: .identifier(email, password: password))
+
+ switch signIn.status {
+ case .needsSecondFactor:
+ // Handle user submitting email and password and swapping to TOTP form.
+ displayTOTP = true
+ default:
+ // If the status is not needsSecondFactor, check why. User may need to
+ // complete different steps.
+ dump(signIn.status)
+ }
+ } 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 }
+
+ // Attempt the TOTP or backup code verification.
+ let signIn = try await inProgressSignIn.attemptSecondFactor(strategy: .totp(code: code))
+
+ 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)
+ }
+ }
+ }
+ ```
-
+ ```kotlin {{ filename: 'MFASignInViewModel.kt', collapsible: true }}
+ import androidx.lifecycle.ViewModel
+ import androidx.lifecycle.viewModelScope
+ import com.clerk.api.Clerk
+ 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.attemptSecondFactor
+ import kotlinx.coroutines.flow.MutableStateFlow
+ import kotlinx.coroutines.flow.asStateFlow
+ import kotlinx.coroutines.launch
+
+ class MFASignInViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(UiState.Unverified)
+ val uiState = _uiState.asStateFlow()
+
+ fun submit(email: String, password: String) {
+ viewModelScope.launch {
+ SignIn.create(SignIn.CreateParams.Strategy.Password(identifier = email, password = password))
+ .onSuccess {
+ if (it.status == SignIn.Status.NEEDS_SECOND_FACTOR) {
+ // Display TOTP Form
+ _uiState.value = UiState.NeedsSecondFactor
+ } else {
+ // If the status is not needsSecondFactor, check why. User may need to
+ // complete different steps.
+ }
+ }
+ .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
+ .attemptSecondFactor(SignIn.AttemptSecondFactorParams.TOTP(code))
+ .onSuccess {
+ if (it.status == SignIn.Status.COMPLETE) {
+ // User is now signed in and verified.
+ // You can navigate to the next screen or perform other actions.
+ _uiState.value = UiState.Verified
+ }
+ }
+ .onFailure {
+ // See https://clerk.com/docs/guides/development/custom-flows/error-handling
+ // for more info on error handling
+ }
+ }
+ }
+
+ sealed interface UiState {
+ data object Unverified : UiState
+ data object Verified : UiState
+ data object NeedsSecondFactor : UiState
+ }
+ }
+ ```
+
+ ```kotlin {{ filename: 'MFASignInActivity.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.Column
+ import androidx.compose.foundation.layout.fillMaxSize
+ import androidx.compose.material3.Button
+ 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.text.input.PasswordVisualTransformation
+ import androidx.compose.ui.unit.dp
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
+
+ class MFASignInActivity : ComponentActivity() {
+ val viewModel: MFASignInViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ MFASignInView(state = state, onSubmit = viewModel::submit, onVerify = viewModel::verify)
+ }
+ }
+ }
+
+ @Composable
+ fun MFASignInView(
+ state: MFASignInViewModel.UiState,
+ onSubmit: (String, String) -> Unit,
+ onVerify: (String) -> Unit,
+ ) {
+ var email by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+ var code by remember { mutableStateOf("") }
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ when (state) {
+ MFASignInViewModel.UiState.NeedsSecondFactor -> {
+ TextField(value = code, onValueChange = { code = it }, placeholder = { Text("Code") })
+ Button(onClick = { onVerify(code) }) { Text("Submit") }
+ }
+ MFASignInViewModel.UiState.Unverified -> {
+ TextField(value = email, onValueChange = { email = it }, placeholder = { Text("Email") })
+ TextField(
+ value = password,
+ onValueChange = { password = it },
+ placeholder = { Text("Password") },
+ visualTransformation = PasswordVisualTransformation(),
+ )
+ Button(onClick = { onSubmit(email, password) }) { Text("Next") }
+ }
+ MFASignInViewModel.UiState.Verified -> {
+ Text("Verified")
+ }
+ }
+ }
+ }
+ ```
diff --git a/docs/guides/development/custom-flows/authentication/email-password.mdx b/docs/guides/development/custom-flows/authentication/email-password.mdx
index 28ae11c358..8447c56651 100644
--- a/docs/guides/development/custom-flows/authentication/email-password.mdx
+++ b/docs/guides/development/custom-flows/authentication/email-password.mdx
@@ -45,7 +45,71 @@ This guide will walk you through how to build a custom email/password sign-up an
-
+ This example is written for Next.js App Router but it can be adapted for any React-based framework.
+
+ ```tsx {{ filename: 'app/sign-up/page.tsx', collapsible: true }}
+ 'use client'
+
+ import * as React from 'react'
+ import { useAuth, useSignUp } from '@clerk/nextjs'
+ import { useRouter } from 'next/navigation'
+
+ export default function SignUpPage() {
+ const { signUp, errors, fetchStatus } = useSignUp()
+ const { isSignedIn } = useAuth()
+
+ const handleSubmit = async (formData: FormData) => {
+ const email = formData.get('email') as string
+ const password = formData.get('password') as string
+
+ await signUp.password({
+ email,
+ password,
+ })
+
+ await signUp.verifications.sendEmailCode()
+ }
+
+ const handleVerify = async (formData: FormData) => {
+ const code = formData.get('code') as string
+
+ await signUp.verifications.verifyEmailCode({
+ code,
+ })
+ if (signUp.status === 'complete') {
+ await signUp.finalize({
+ navigate: () => {
+ router.push('/dashboard')
+ },
+ })
+ }
+ }
+
+ if (signUp.status === 'complete' || isSignedIn) {
+ return null
+ }
+
+ if (
+ signUp.status === 'missing_requirements' &&
+ signUp.unverifiedFields.includes('email_address')
+ ) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ }
+ ```
@@ -58,11 +122,241 @@ This guide will walk you through how to build a custom email/password sign-up an
-
+ ```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) }
+ }
+ }
+ }
+ }
+
+ extension EmailPasswordSignUpView {
+
+ 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))
+
+ // Send the user an email with the verification code
+ try await signUp.prepareVerification(strategy: .emailCode)
+
+ // Set 'isVerifying' 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
+ guard let inProgressSignUp = Clerk.shared.client?.signUp else { return }
+
+ // Use the code the user provided to attempt verification
+ let signUp = try await inProgressSignUp.attemptVerification(strategy: .emailCode(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: 'EmailPasswordSignUpViewModel.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 EmailPasswordSignUpViewModel : ViewModel() {
+ private val _uiState =
+ MutableStateFlow(UiState.Loading)
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
+ _uiState.value =
+ when {
+ !isInitialized -> UiState.Loading
+ user != null -> UiState.Verified
+ else -> UiState.Unverified
+ }
+ }
+ .launchIn(viewModelScope)
+ }
+
+ fun submit(email: String, password: String) {
+ viewModelScope.launch {
+ SignUp.create(SignUp.CreateParams.Standard(emailAddress = email, password = password))
+ .flatMap { it.prepareVerification(SignUp.PrepareVerificationParams.Strategy.EmailCode()) }
+ .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.EmailCode(code))
+ .onSuccess { _uiState.value = UiState.Verified }
+ .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: 'EmailPasswordSignUpActivity.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.text.input.PasswordVisualTransformation
+ import androidx.compose.ui.unit.dp
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
+
+ class EmailPasswordSignUpActivity : ComponentActivity() {
+
+ val viewModel: EmailPasswordSignUpViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ EmailPasswordSignInView(
+ state = state,
+ onSubmit = viewModel::submit,
+ onVerify = viewModel::verify,
+ )
+ }
+ }
+ }
+
+ @Composable
+ fun EmailPasswordSignInView(
+ state: EmailPasswordSignUpViewModel.UiState,
+ onSubmit: (String, String) -> Unit,
+ onVerify: (String) -> Unit,
+ ) {
+ var email by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+ var code by remember { mutableStateOf("") }
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ when (state) {
+ EmailPasswordSignUpViewModel.UiState.Unverified -> {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
+ TextField(
+ value = password,
+ onValueChange = { password = it },
+ visualTransformation = PasswordVisualTransformation(),
+ label = { Text("Password") },
+ )
+ Button(onClick = { onSubmit(email, password) }) { Text("Next") }
+ }
+ }
+ EmailPasswordSignUpViewModel.UiState.Verified -> {
+ Text("Verified!")
+ }
+ EmailPasswordSignUpViewModel.UiState.Verifying -> {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TextField(
+ value = code,
+ onValueChange = { code = it },
+ label = { Text("Enter your verification code") },
+ )
+ Button(onClick = { onVerify(code) }) { Text("Verify") }
+ }
+ }
+ EmailPasswordSignUpViewModel.UiState.Loading -> CircularProgressIndicator()
+ }
+ }
+ }
+ ```
@@ -77,7 +371,49 @@ This guide will walk you through how to build a custom email/password sign-up an
-
+ This example is written for Next.js App Router but it can be adapted for any React-based framework.
+
+ ```tsx {{ filename: 'app/sign-in/page.tsx', collapsible: true }}
+ 'use client'
+
+ import * as React from 'react'
+ import { useAuth, useSignIn } from '@clerk/nextjs'
+ import { useRouter } from 'next/navigation'
+
+ export default function SignInPage() {
+ const { signIn, errors, fetchStatus } = useSignIn()
+ const { isSignedIn } = useAuth()
+
+ const handleSubmit = async (formData: FormData) => {
+ const email = formData.get('email') as string
+ const password = formData.get('password') as string
+
+ await signIn.password({
+ email,
+ password,
+ })
+ if (signIn.status === 'complete') {
+ await signIn.finalize({
+ navigate: () => {
+ router.push('/dashboard')
+ },
+ })
+ }
+ }
+
+ if (signIn.status === 'complete' || isSignedIn) {
+ return null
+ }
+
+ return (
+
+ )
+ }
+ ```
@@ -88,11 +424,181 @@ This guide will walk you through how to build a custom email/password sign-up an
-
+ ```swift {{ filename: 'EmailPasswordSignInView.swift', collapsible: true }}
+ import SwiftUI
+ import Clerk
+
+ struct EmailPasswordSignInView: View {
+ @State private var email = ""
+ @State private var password = ""
+
+ 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) }
+ }
+ }
+ }
+
+ extension EmailPasswordSignInView {
+
+ 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))
+
+ 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)
+ }
+ }
+ }
+ ```
-
+ ```kotlin {{ filename: 'EmailPasswordSignInViewModel.kt', collapsible: true }}
+ import androidx.lifecycle.ViewModel
+ import androidx.lifecycle.viewModelScope
+ import com.clerk.api.Clerk
+ import com.clerk.api.network.serialization.onFailure
+ import com.clerk.api.network.serialization.onSuccess
+ import com.clerk.api.signin.SignIn
+ 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 EmailPasswordSignInViewModel : ViewModel() {
+ private val _uiState = MutableStateFlow(
+ UiState.SignedOut
+ )
+ val uiState = _uiState.asStateFlow()
+
+ init {
+ combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
+ _uiState.value = when {
+ !isInitialized -> UiState.Loading
+ user == null -> UiState.SignedOut
+ else -> UiState.SignedIn
+ }
+ }.launchIn(viewModelScope)
+ }
+
+ fun submit(email: String, password: String) {
+ viewModelScope.launch {
+ SignIn.create(
+ SignIn.CreateParams.Strategy.Password(
+ identifier = email,
+ password = password
+ )
+ ).onSuccess {
+ _uiState.value = UiState.SignedIn
+ }.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 SignedOut : UiState
+
+ data object SignedIn : UiState
+ }
+ }
+ ```
+
+ ```kotlin {{ filename: 'EmailPasswordSignInActivity.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.*
+ import androidx.compose.material3.*
+ 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.*
+ import androidx.compose.ui.text.input.PasswordVisualTransformation
+ import androidx.compose.ui.unit.dp
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
+ import com.clerk.api.Clerk
+
+ class EmailPasswordSignInActivity : ComponentActivity() {
+
+ val viewModel: EmailPasswordSignInViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ EmailPasswordSignInView(
+ state = state,
+ onSubmit = viewModel::submit
+ )
+ }
+ }
+ }
+
+ @Composable
+ fun EmailPasswordSignInView(
+ state: EmailPasswordSignInViewModel.UiState,
+ onSubmit: (String, String) -> Unit,
+ ) {
+ var email by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+
+ when (state) {
+
+ EmailPasswordSignInViewModel.UiState.SignedOut -> {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
+ TextField(
+ value = password,
+ onValueChange = { password = it },
+ visualTransformation = PasswordVisualTransformation(),
+ label = { Text("Password") },
+ )
+ Button(onClick = { onSubmit(email, password) }) { Text("Sign in") }
+ }
+ }
+
+ EmailPasswordSignInViewModel.UiState.SignedIn -> {
+ Text("Current session: ${Clerk.session?.id}")
+ }
+
+ EmailPasswordSignInViewModel.UiState.Loading ->
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ }
+ }
+ }
+
+ ```
diff --git a/docs/guides/development/custom-flows/authentication/email-sms-otp.mdx b/docs/guides/development/custom-flows/authentication/email-sms-otp.mdx
index 189bbb7a9e..3a1becd881 100644
--- a/docs/guides/development/custom-flows/authentication/email-sms-otp.mdx
+++ b/docs/guides/development/custom-flows/authentication/email-sms-otp.mdx
@@ -45,7 +45,66 @@ This guide will walk you through how to build a custom SMS OTP sign-up and sign-
-
+ This example is written for Next.js App Router but it can be adapted for any React-based framework.
+
+ ```tsx {{ filename: 'app/sign-up/page.tsx', collapsible: true }}
+ 'use client'
+
+ import * as React from 'react'
+ import { useAuth, useSignUp } from '@clerk/nextjs'
+ import { useRouter } from 'next/navigation'
+
+ export default function SignUpPage() {
+ const { signUp, errors, fetchStatus } = useSignUp()
+ const { isSignedIn } = useAuth()
+
+ const handleSubmit = async (formData: FormData) => {
+ const phoneNumber = formData.get('phoneNumber') as string
+
+ await signUp.create({ phoneNumber })
+
+ await signUp.verifications.sendPhoneCode()
+ }
+
+ const handleVerify = async (formData: FormData) => {
+ const code = formData.get('code') as string
+
+ await signUp.verifications.verifyPhoneCode({ code })
+ if (signUp.status === 'complete') {
+ await signUp.finalize({
+ navigate: () => {
+ router.push('/dashboard')
+ },
+ })
+ }
+ }
+
+ if (signUp.status === 'complete' || isSignedIn) {
+ return null
+ }
+
+ if (
+ signUp.status === 'missing_requirements' &&
+ signUp.unverifiedFields.includes('phone_number')
+ ) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ }
+ ```
@@ -60,11 +119,234 @@ This guide will walk you through how to build a custom SMS OTP sign-up and sign-
-
+ ```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) }
+ }
+ }
+ ```
@@ -82,7 +364,69 @@ This guide will walk you through how to build a custom SMS OTP sign-up and sign-
-
+ This example is written for Next.js App Router but it can be adapted to any React-based framework.
+
+ ```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 Page() {
+ const { signIn, errors, fetchStatus } = useSignIn()
+ const router = useRouter()
+
+ async function handleSubmit(formData: FormData) {
+ const phoneNumber = formData.get('phoneNumber') as string
+
+ await signIn.phoneCode.sendCode({ phoneNumber })
+ }
+
+ 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('/')
+ },
+ })
+ }
+ }
+
+ if (signIn.status === 'needs_second_factor') {
+ return (
+ <>
+
Verify your phone number
+
+ >
+ )
+ }
+
+ return (
+ <>
+
Sign in
+
+ >
+ )
+ }
+ ```
@@ -97,11 +441,235 @@ This guide will walk you through how to build a custom SMS OTP sign-up and sign-
-
+ ```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) }
+ }
+ }
+ ```
From 1ff6324bd87f501a3d70f857e6100f6615d02ffd Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Thu, 9 Oct 2025 14:56:31 -0500
Subject: [PATCH 13/18] Add SSO documentation
---
.../custom-flows/sso-connections-legacy.mdx | 12 +
.../custom-flows/sso-connections.mdx | 11 +-
.../authentication/enterprise-connections.mdx | 79 ++----
.../legacy/enterprise-connections.mdx | 2 +-
.../legacy/oauth-connections.mdx | 2 +-
.../authentication/oauth-connections.mdx | 262 +++++++-----------
6 files changed, 141 insertions(+), 227 deletions(-)
create mode 100644 docs/_partials/custom-flows/sso-connections-legacy.mdx
diff --git a/docs/_partials/custom-flows/sso-connections-legacy.mdx b/docs/_partials/custom-flows/sso-connections-legacy.mdx
new file mode 100644
index 0000000000..d338f58a35
--- /dev/null
+++ b/docs/_partials/custom-flows/sso-connections-legacy.mdx
@@ -0,0 +1,12 @@
+The following example **will both sign up _and_ sign in users**, eliminating the need for a separate sign-up page. However, if you want to have separate sign-up and sign-in pages, the sign-up and sign-in flows are equivalent, meaning that all you have to do is swap out the `SignIn` object for the `SignUp` object using the [`useSignUp()`](/docs/reference/hooks/use-sign-up) hook.
+
+The following example:
+
+1. Accesses the [`SignIn`](/docs/reference/javascript/sign-in) object using the [`useSignIn()`](/docs/reference/hooks/use-sign-in) hook.
+1. Starts the authentication process by calling [`SignIn.authenticateWithRedirect(params)`](/docs/reference/javascript/sign-in#authenticate-with-redirect). This method requires a `redirectUrl` param, which is the URL that the browser will be redirected to once the user authenticates with the identity provider.
+1. Creates a route at the URL that the `redirectUrl` param points to. The following example names this route `/sso-callback`. This route should either render the prebuilt [``](/docs/reference/components/control/authenticate-with-redirect-callback) component or call the [`Clerk.handleRedirectCallback()`](/docs/reference/javascript/clerk#handle-redirect-callback) method if you're not using the prebuilt component.
+
+The following example shows two files:
+
+1. The sign-in page where the user can start the authentication flow.
+1. The SSO callback page where the flow is completed.
diff --git a/docs/_partials/custom-flows/sso-connections.mdx b/docs/_partials/custom-flows/sso-connections.mdx
index d338f58a35..574b7e17b1 100644
--- a/docs/_partials/custom-flows/sso-connections.mdx
+++ b/docs/_partials/custom-flows/sso-connections.mdx
@@ -3,10 +3,7 @@ The following example **will both sign up _and_ sign in users**, eliminating the
The following example:
1. Accesses the [`SignIn`](/docs/reference/javascript/sign-in) object using the [`useSignIn()`](/docs/reference/hooks/use-sign-in) hook.
-1. Starts the authentication process by calling [`SignIn.authenticateWithRedirect(params)`](/docs/reference/javascript/sign-in#authenticate-with-redirect). This method requires a `redirectUrl` param, which is the URL that the browser will be redirected to once the user authenticates with the identity provider.
-1. Creates a route at the URL that the `redirectUrl` param points to. The following example names this route `/sso-callback`. This route should either render the prebuilt [``](/docs/reference/components/control/authenticate-with-redirect-callback) component or call the [`Clerk.handleRedirectCallback()`](/docs/reference/javascript/clerk#handle-redirect-callback) method if you're not using the prebuilt component.
-
-The following example shows two files:
-
-1. The sign-in page where the user can start the authentication flow.
-1. The SSO callback page where the flow is completed.
+1. Starts the authentication process by calling [`SignIn.sso(params)`](/docs/reference/javascript/sign-in-future#sso). This method requires the following params:
+ - `redirectUrl`: The URL that the browser will be redirected to once the user authenticates with the identity provider if no additional requirements are needed, and a session has been created
+ - `redirectCallbackUrl`: The URL that the browser will be redirected to once the user authenticates with the identity provider if additional requirements are needed
+1. Creates a route at the URL that the `redirectCallbackUrl` param points to. The following example re-uses the `/sign-in` route, which should be written to handle when a sign-in attempt is in a non-complete status such as `needs_second_factor`.
diff --git a/docs/guides/development/custom-flows/authentication/enterprise-connections.mdx b/docs/guides/development/custom-flows/authentication/enterprise-connections.mdx
index a63c2937fe..a0e773c35e 100644
--- a/docs/guides/development/custom-flows/authentication/enterprise-connections.mdx
+++ b/docs/guides/development/custom-flows/authentication/enterprise-connections.mdx
@@ -1,6 +1,7 @@
---
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.
+sdk: nextjs, react, expo, js-frontend, react-router, tanstack-react-start
---
@@ -15,61 +16,37 @@ You must configure your application instance through the Clerk Dashboard for the
-
- ```tsx {{ filename: 'app/sign-in/page.tsx' }}
- 'use client'
+ ```tsx {{ filename: 'app/sign-in/page.tsx' }}
+ 'use client'
- import * as React from 'react'
- import { useSignIn } from '@clerk/nextjs'
+ 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 (
-
- )
+ export default function Page() {
+ const { signIn, fetchStatus } = useSignIn()
+
+ const signInWithEnterpriseSSO = async (formData: FormData) => {
+ const email = formData.get('email') as string
+
+ await signIn.sso({
+ identifier: email,
+ strategy: 'enterprise_sso',
+ // The URL that the user will be redirected to if additional requirements are needed
+ redirectCallbackUrl: '/sign-in',
+ redirectUrl: '/',
+ })
}
- ```
- ```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
- }
- ```
-
+ return (
+
+ )
+ }
+ ```
diff --git a/docs/guides/development/custom-flows/authentication/legacy/enterprise-connections.mdx b/docs/guides/development/custom-flows/authentication/legacy/enterprise-connections.mdx
index a63c2937fe..d7c464ad09 100644
--- a/docs/guides/development/custom-flows/authentication/legacy/enterprise-connections.mdx
+++ b/docs/guides/development/custom-flows/authentication/legacy/enterprise-connections.mdx
@@ -13,7 +13,7 @@ You must configure your application instance through the Clerk Dashboard for the
-
+
```tsx {{ filename: 'app/sign-in/page.tsx' }}
diff --git a/docs/guides/development/custom-flows/authentication/legacy/oauth-connections.mdx b/docs/guides/development/custom-flows/authentication/legacy/oauth-connections.mdx
index 4a722abfd6..6d117a5e62 100644
--- a/docs/guides/development/custom-flows/authentication/legacy/oauth-connections.mdx
+++ b/docs/guides/development/custom-flows/authentication/legacy/oauth-connections.mdx
@@ -19,7 +19,7 @@ You must configure your application instance through the Clerk Dashboard for the
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
```
-
+
```tsx {{ filename: 'app/sign-in/page.tsx' }}
diff --git a/docs/guides/development/custom-flows/authentication/oauth-connections.mdx b/docs/guides/development/custom-flows/authentication/oauth-connections.mdx
index 4a722abfd6..c24a8bc025 100644
--- a/docs/guides/development/custom-flows/authentication/oauth-connections.mdx
+++ b/docs/guides/development/custom-flows/authentication/oauth-connections.mdx
@@ -1,6 +1,7 @@
---
title: Build a custom flow for authenticating with OAuth connections
description: Learn how to use the Clerk API to build a custom sign-up and sign-in flow that supports OAuth connections.
+sdk: nextjs, react, expo, js-frontend, react-router, tanstack-react-start
---
@@ -21,65 +22,33 @@ You must configure your application instance through the Clerk Dashboard for the
-
- ```tsx {{ filename: 'app/sign-in/page.tsx' }}
- 'use client'
+ ```tsx {{ filename: 'app/sign-in/page.tsx' }}
+ 'use client'
- import * as React from 'react'
- import { OAuthStrategy } from '@clerk/types'
- import { useSignIn } from '@clerk/nextjs'
+ import * as React from 'react'
+ import { OAuthStrategy } from '@clerk/types'
+ import { useSignIn } from '@clerk/nextjs'
- export default function Page() {
- const { signIn } = useSignIn()
-
- if (!signIn) return null
-
- const signInWith = (strategy: OAuthStrategy) => {
- return signIn
- .authenticateWithRedirect({
- strategy,
- redirectUrl: '/sign-in/sso-callback',
- redirectUrlComplete: '/sign-in/tasks', // Learn more about session tasks at https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks
- })
- .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)
- })
- }
-
- // Render a button for each supported OAuth provider
- // you want to add to your app. This example uses only Google.
- return (
-
-
-
- )
+ export default function Page() {
+ const { signIn } = useSignIn()
+
+ const signInWith = async (strategy: OAuthStrategy) => {
+ await signIn.sso({
+ strategy,
+ redirectCallbackUrl: '/sign-in',
+ redirectUrl: '/sign-in/tasks', // Learn more about session tasks at https://clerk.com/docs/guides/development/custom-flows/overview#session-tasks
+ })
}
- ```
-
- ```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 */}
-
- >
- )
- }
- ```
-
+ // Render a button for each supported OAuth provider
+ // you want to add to your app. This example uses only Google.
+ return (
+
+
+
+ )
+ }
+ ```
@@ -348,131 +317,90 @@ You must configure your application instance through the Clerk Dashboard for the
## Handle missing requirements
-Depending on your instance settings, users might need to provide extra information before their sign-up can be completed, such as when a username or accepting legal terms is required. In these cases, the `SignUp` object returns a status of `"missing_requirements"` along with a `missingFields` array. You can create a "Continue" page to collect these missing fields and complete the sign-up flow. Handling the missing requirements will depend on your instance settings. For example, if your instance settings require a phone number, you will need to [handle verifying the phone number](/docs/guides/development/custom-flows/authentication/email-sms-otp#sign-up-flow).
+Depending on your instance settings, users might need to provide extra information before their sign-up can be completed, such as when a username or accepting legal terms is required. In these cases, the `SignUp` object returns a status of `"missing_requirements"` along with a `missingFields` array. You can use your existing sign-up page to collect these missing fields and complete the sign-up flow. Handling the missing requirements will depend on your instance settings. For example, if your instance settings require a phone number, you will need to [handle verifying the phone number](/docs/guides/development/custom-flows/authentication/email-sms-otp#sign-up-flow).
With OAuth flows, it's common for users to try to _sign in_ with an OAuth provider, but they don't have a Clerk account for your app yet. Clerk automatically transfers the flow from the `SignIn` object to the `SignUp` object, which returns the `"missing_requirements"` status and `missingFields` array needed to handle the missing requirements flow. This is why the "Continue" page uses the [`useSignUp()`](/docs/reference/hooks/use-sign-up) hook and treats the missing requirements flow as a sign-up flow.
-
- ```tsx {{ filename: 'app/sign-in/continue/page.tsx' }}
- 'use client'
-
- import { useState } from 'react'
- import { useSignUp } from '@clerk/nextjs'
- import { useRouter } from 'next/navigation'
+ ```tsx {{ filename: 'app/sign-up/page.tsx' }}
+ 'use client'
- export default function Page() {
- const router = useRouter()
- // Use `useSignUp()` hook to access the `SignUp` object
- // `missing_requirements` and `missingFields` are only available on the `SignUp` object
- const { isLoaded, signUp, setActive } = useSignUp()
- const [formData, setFormData] = useState>({})
+ import { useState } from 'react'
+ import { useSignUp } from '@clerk/nextjs'
+ import { useRouter } from 'next/navigation'
- if (!isLoaded) return
Loading…
-
- // Protect the page from users who are not in the sign-up flow
- // such as users who visited this route directly
- if (!signUp.id) router.push('/sign-in')
-
- const status = signUp?.status
- const missingFields = signUp?.missingFields ?? []
-
- const handleChange = (field: string, value: string) => {
- setFormData((prev) => ({ ...prev, [field]: value }))
- }
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- try {
- // Update the `SignUp` object with the missing fields
- // The logic that goes here will depend on your instance settings
- // E.g. if your app requires a phone number, you will need to collect and verify it here
- const res = await signUp?.update(formData)
- if (res?.status === 'complete') {
- await setActive({
- session: res.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)
- router.push('/sign-in/tasks')
- return
- }
-
- router.push('/')
- },
- })
- }
- } 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))
- }
- }
+ function snakeToCamel(str: string | undefined): string {
+ return str ? str.replace(/([-_][a-z])/g, (match) => match.toUpperCase().replace(/-|_/, '')) : ''
+ }
- if (status === 'missing_requirements') {
- // For simplicity, all missing fields in this example are text inputs.
- // In a real app, you might want to handle them differently:
- // - legal_accepted: checkbox
- // - username: text with validation
- // - phone_number: phone input, etc.
- return (
-
-
Continue sign-up
-
-
- )
+ export default function Page() {
+ const router = useRouter()
+ // Use `useSignUp()` hook to access the `SignUp` object
+ // `missing_requirements` and `missingFields` are only available on the `SignUp` object
+ const { signUp } = useSignUp()
+
+ const handleSubmit = async (formData: FormData) => {
+ const params = Object.fromEntries(formData.entries()) as any
+ // Update the `SignUp` object with the missing fields
+ // The logic that goes here will depend on your instance settings
+ // E.g. if your app requires a phone number, you will need to collect and verify it here
+ await signUp.update(params)
+ if (signUp.status === 'complete') {
+ await signUp.finalize({
+ 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)
+ router.push('/sign-in/tasks')
+ return
+ }
+
+ router.push('/')
+ },
+ })
}
-
- // 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
+ if (signUp.status === 'missing_requirements') {
+ // For simplicity, all missing fields in this example are text inputs.
+ // In a real app, you might want to handle them differently:
+ // - legal_accepted: checkbox
+ // - username: text with validation
+ // - phone_number: phone input, etc.
return (
- <>
-
-
- {/* Required for sign-up flows
- Clerk's bot sign-up protection is enabled by default */}
-
- >
+
+
Continue sign-up
+
+
)
}
- ```
-
+
+ // Handle other statuses if needed
+ return (
+ <>
+ {/* Required for sign-up flows
+ Clerk's bot sign-up protection is enabled by default */}
+
+ >
+ )
+ }
+ ```
From 2a6da3e4c13e21c568798c9b42cb36a2c91e9b3a Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Tue, 14 Oct 2025 13:26:06 -0500
Subject: [PATCH 14/18] Update API docs for SignInFuture and SignUpFuture
---
docs/reference/javascript/sign-in-future.mdx | 444 +++++++-----------
docs/reference/javascript/sign-up-future.mdx | 458 +++++++++----------
2 files changed, 375 insertions(+), 527 deletions(-)
diff --git a/docs/reference/javascript/sign-in-future.mdx b/docs/reference/javascript/sign-in-future.mdx
index daff283292..92c13148be 100644
--- a/docs/reference/javascript/sign-in-future.mdx
+++ b/docs/reference/javascript/sign-in-future.mdx
@@ -6,196 +6,147 @@ 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
+The `SignInFuture` class holds the state of the current sign-in and provides helper methods to navigate and complete the sign-in process. It is used to manage the sign-in lifecycle, including the first and second factor verification, and the creation of a new session.
## Properties
- - `id`
- - `string | undefined`
+ - `createdSessionId`
+ - `null | string`
- The unique identifier for the current sign-in attempt.
+ The identifier of the session that was created upon completion of the current sign-in. The value of this property is `null` if the sign-in status is not `'complete'`.
---
- - `supportedFirstFactors`
- - [SignInFirstFactor](/docs/reference/javascript/types/sign-in-first-factor)\[]
+ - `existingSession?`
+ - `{ sessionId: string }`
- The list of first-factor strategies that are available for the current sign-in attempt.
+ TKTKTK
---
- - `supportedSecondFactors`
- - [SignInSecondFactor](/docs/reference/javascript/types/sign-in-second-factor)\[]
+ - `firstFactorVerification`
+ - `VerificationResource`
- The list of second-factor strategies that are available for the current sign-in attempt.
+ The state of the verification process for the selected first factor. Initially, this property contains an empty verification object, since there is no first factor selected.
---
- - `status`
- - `SignInStatus`
-
- The current status of the sign-in. `SignInStatus` supports the following values:
+ - `id?`
+ - `string`
- - `'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.
+ The unique identifier for the current sign-in attempt.
---
- - `isTransferable`
- - `boolean`
+ - `identifier`
+ - `null | string`
- 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.
+ The authentication identifier value for the current sign-in. `null` if the `strategy` is `'oauth_'` or `'enterprise_sso'`.
---
- - `existingSession`
- - `{ sessionId: string } | undefined`
+ - `isTransferable`
+ - `boolean`
- TKTKTK
+ 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.
---
- - `firstFactorVerification`
- - [`Verification`](/docs/reference/javascript/types/verification)
+ - `secondFactorVerification`
+ - `VerificationResource`
- TKTKTK
+ The state of the verification process for the selected second factor. Initially, this property contains an empty verification object, since there is no second factor selected.
---
- - `secondFactorVerification`
- - [`Verification`](/docs/reference/javascript/types/verification)
+ - `status`
+ - `SignInStatus`
- The second-factor verification for the current sign-in attempt.
+ The current status of the sign-in.
---
- - `identifier`
- - `string | null`
+ - `supportedFirstFactors`
+ - `SignInFirstFactor[]`
- The identifier for the current sign-in attempt.
+ Array of the first factors that are supported in the current sign-in. Each factor contains information about the verification strategy that can be used.
---
- - `createdSessionId`
- - `string | null`
+ - `supportedSecondFactors`
+ - `SignInSecondFactor[]`
- The created session ID for the current sign-in attempt.
+ Array of the second factors that are supported in the current sign-in. Each factor contains information about the verification strategy that can be used. his property is populated only when the first factor is verified.
---
- `userData`
- `UserData`
- The user data for the current sign-in attempt.
+ An object containing information about the user of the current sign-in. This property is populated only once an identifier is given to the `SignIn` object through `signIn.create()` or another method that populates the `identifier` property.
## 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`.
+Creates a new `SignIn` instance initialized with the provided parameters. The instance maintains the sign-in lifecycle state through its `status` property, which updates as the authentication flow progresses.
+
+What you must pass to `params` depends on which [sign-in options](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) you have enabled in your app's settings in the Clerk Dashboard.
+
+You can complete the sign-in process in one step if you supply the required fields to `create()`. Otherwise, Clerk's sign-in process provides great flexibility and allows users to easily create multi-step sign-in flows.
+
+> [!WARNING] > Once the sign-in process is complete, call the `signIn.finalize()` method to set the newly created session as > the active session.
```ts
function create(params: SignInFutureCreateParams): Promise<{ error: unknown }>
```
-#### SignInFutureCreateParams
+#### `SignInFutureCreateParams`
TKTKTK
- - `identifier?`
+ - `actionCompleteRedirectUrl?`
- `string`
- TKTKTK
+ The URL that the user will be redirected to, after successful authorization from the OAuth provider and Clerk sign-in.
---
- - `strategy?`
- - [OAuthStrategy](/docs/reference/javascript/types/sso#o-auth-strategy) | 'saml' | 'enterprise\_sso'
-
- TKTKTK
-
- ---
-
- - `redirectUrl?`
+ - `identifier?`
- `string`
- TKTKTK
+ The authentication identifier for the sign-in. This can be the value of the user's email address, phone number, username, or Web3 wallet address.
---
- - `actionCompleteRedirectUrl?`
+ - `redirectUrl?`
- `string`
- TKTKTK
+ The full URL or path that the OAuth provider should redirect to after successful authorization on their part.
---
- - `transfer?`
- - `boolean`
+ - `strategy?`
+ - `OAuthStrategy | "saml" | "enterprise_sso"`
- TKTKTK
+ The first factor verification strategy to use in the sign-in flow. Depends on the `identifier` value. Each authentication identifier supports different verification strategies.
---
- `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
+ The [ticket _or token_](/docs/guides/development/custom-flows/authentication/application-invitations) generated from the Backend API. **Required** if `strategy` is set to `'ticket'`.
---
- - `emailAddress?`
- - `string`
-
- TKTKTK
-
- ---
-
- - `phoneNumber?`
- - `string`
+ - `transfer?`
+ - `boolean`
- TKTKTK
+ When set to `true`, the `SignIn` will attempt to retrieve information from the active `SignUp` instance and use it to complete the sign-in process. This is useful when you want to seamlessly transition a user from a sign-up attempt to a sign-in attempt.
### `emailCode.sendCode()`
@@ -206,24 +157,22 @@ Used to send an email code to sign-in
function sendCode(params: SignInFutureEmailCodeSendParams): Promise<{ error: unknown }>
```
-#### SignInFutureEmailCodeSendParams
+#### `SignInFutureEmailCodeSendParams`
TKTKTK
-Provide either `emailAddress` or `emailAddressId`.
-
- `emailAddress?`
- `string`
- TKTKTK
+ The user's email address. Only supported if [Email address](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#email) is enabled.
---
- `emailAddressId?`
- `string`
- TKTKTK
+ The ID for the user's email address that will receive an email with the one-time authentication code.
### `emailCode.verifyCode()`
@@ -234,7 +183,7 @@ Used to verify a code sent via email to sign-in
function verifyCode(params: SignInFutureEmailCodeVerifyParams): Promise<{ error: unknown }>
```
-#### SignInFutureEmailCodeVerifyParams
+#### `SignInFutureEmailCodeVerifyParams`
TKTKTK
@@ -242,7 +191,7 @@ TKTKTK
- `code`
- `string`
- TKTKTK
+ The one-time code that was sent to the user.
### `emailLink.sendLink()`
@@ -253,31 +202,15 @@ Used to send an email link to sign-in
function sendLink(params: SignInFutureEmailLinkSendParams): Promise<{ error: unknown }>
```
-#### SignInFutureEmailLinkSendParams
+#### `SignInFutureEmailLinkSendParams`
TKTKTK
-Provide either `emailAddress` or `emailAddressId` along with `verificationUrl`.
-
- - `emailAddress?`
- - `string`
-
- TKTKTK
-
- ---
-
- - `emailAddressId?`
- - `string`
-
- TKTKTK
-
- ---
-
- `verificationUrl`
- `string`
- TKTKTK
+ The full URL that the user will be redirected to when they visit the email link.
### `emailLink.waitForVerification()`
@@ -288,50 +221,42 @@ Will wait for verification to complete or expire
function waitForVerification(): Promise<{ error: unknown }>
```
-### `phoneCode.sendCode()`
+### `finalize()`
-Used to send a phone code to sign-in
+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 sendCode(params: SignInFuturePhoneCodeSendParams): Promise<{ error: unknown }>
+function finalize(params?: SignInFutureFinalizeParams): Promise<{ error: unknown }>
```
-#### SignInFuturePhoneCodeSendParams
+#### `SignInFutureFinalizeParams`
TKTKTK
-Provide either `phoneNumber` or `phoneNumberId`. Optionally specify the `channel`.
-
- - `phoneNumber?`
- - `string`
-
- TKTKTK
-
- ---
-
- - `phoneNumberId?`
- - `string`
+ - `navigate?`
+ - `SetActiveNavigate`
TKTKTK
+
- ---
+### `mfa.sendPhoneCode()`
- - `channel?`
- - `PhoneCodeChannel`
+Used to send a phone code as a second factor to sign-in
- TKTKTK
-
+```ts
+function sendPhoneCode(): Promise<{ error: unknown }>
+```
-### `phoneCode.verifyCode()`
+### `mfa.verifyBackupCode()`
-Used to verify a code sent via phone to sign-in
+Used to verify a backup code as a second factor to sign-in
```ts
-function verifyCode(params: SignInFuturePhoneCodeVerifyParams): Promise<{ error: unknown }>
+function verifyBackupCode(params: SignInFutureBackupCodeVerifyParams): Promise<{ error: unknown }>
```
-#### SignInFuturePhoneCodeVerifyParams
+#### `SignInFutureBackupCodeVerifyParams`
TKTKTK
@@ -339,26 +264,37 @@ TKTKTK
- `code`
- `string`
- TKTKTK
+ The backup code that was provided to the user when they set up two-step authentication.
-### `resetPasswordEmailCode.sendCode()`
+### `mfa.verifyPhoneCode()`
-Used to send a password reset code to the first email address on the account
+Used to verify a phone code sent as a second factor to sign-in
```ts
-function sendCode(): Promise<{ error: unknown }>
+function verifyPhoneCode(params: SignInFutureMFAPhoneCodeVerifyParams): Promise<{ error: unknown }>
```
-### `resetPasswordEmailCode.verifyCode()`
+#### `SignInFutureMFAPhoneCodeVerifyParams`
-Used to verify a password reset code sent via email. Will cause `signIn.status` to become `'needs_new_password'`.
+TKTKTK
+
+
+ - `code`
+ - `string`
+
+ The one-time code that was sent to the user as part of the `signIn.mfa.sendPhoneCode()` method.
+
+
+### `mfa.verifyTOTP()`
+
+Used to verify a TOTP code as a second factor to sign-in
```ts
-function verifyCode(params: SignInFutureEmailCodeVerifyParams): Promise<{ error: unknown }>
+function verifyTOTP(params: SignInFutureTOTPVerifyParams): Promise<{ error: unknown }>
```
-#### SignInFutureEmailCodeVerifyParams
+#### `SignInFutureTOTPVerifyParams`
TKTKTK
@@ -366,18 +302,18 @@ TKTKTK
- `code`
- `string`
- TKTKTK
+ The TOTP generated by the user's authenticator app.
-### `resetPasswordEmailCode.submitPassword()`
+### `password()`
-Used to submit a new password, and move the `signIn.status` to `'complete'`.
+Used to submit a password to sign-in.
```ts
-function submitPassword(params: SignInFutureResetPasswordSubmitParams): Promise<{ error: unknown }>
+function password(params: SignInFuturePasswordParams): Promise<{ error: unknown }>
```
-#### SignInFutureResetPasswordSubmitParams
+#### `SignInFuturePasswordParams`
TKTKTK
@@ -385,92 +321,90 @@ TKTKTK
- `password`
- `string`
- TKTKTK
-
- ---
-
- - `signOutOfOtherSessions?`
- - `boolean`
-
- TKTKTK
+ The user's password. Only supported if [password](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#password) is enabled.
-### `sso()`
+### `phoneCode.sendCode()`
-Used to perform OAuth authentication.
+Used to send a phone code to sign-in
```ts
-function sso(params: SignInFutureSSOParams): Promise<{ error: unknown }>
+function sendCode(params: SignInFuturePhoneCodeSendParams): Promise<{ error: unknown }>
```
-#### SignInFutureSSOParams
+#### `SignInFuturePhoneCodeSendParams`
TKTKTK
- - `flow?`
- - `'auto' | 'modal'`
-
- TKTKTK
-
- ---
+ - `channel?`
+ - `PhoneCodeChannel`
- - `strategy`
- - [OAuthStrategy](/docs/reference/javascript/types/sso#o-auth-strategy) | 'saml' | 'enterprise\_sso'
+ The mechanism to use to send the code to the provided phone number. Defaults to `'sms'`.
+
- TKTKTK
+### `phoneCode.verifyCode()`
- ---
+Used to verify a code sent via phone to sign-in
- - `redirectUrl`
- - `string`
+```ts
+function verifyCode(params: SignInFuturePhoneCodeVerifyParams): Promise<{ error: unknown }>
+```
- The URL to redirect to after the user has completed the SSO flow.
+#### `SignInFuturePhoneCodeVerifyParams`
- ---
+TKTKTK
- - `redirectCallbackUrl`
+
+ - `code`
- `string`
- TODO @revamp-hooks: This should be handled by FAPI instead.
+ The one-time code that was sent to the user.
-### `mfa.sendPhoneCode()`
+### `resetPasswordEmailCode.sendCode()`
-Used to send a phone code as a second factor to sign-in
+Used to send a password reset code to the first email address on the account
```ts
-function sendPhoneCode(): Promise<{ error: unknown }>
+function sendCode(): Promise<{ error: unknown }>
```
-### `mfa.verifyPhoneCode()`
+### `resetPasswordEmailCode.submitPassword()`
-Used to verify a phone code sent as a second factor to sign-in
+Used to submit a new password, and move the `signIn.status` to `'complete'`.
```ts
-function verifyPhoneCode(params: SignInFutureMFAPhoneCodeVerifyParams): Promise<{ error: unknown }>
+function submitPassword(params: SignInFutureResetPasswordSubmitParams): Promise<{ error: unknown }>
```
-#### SignInFutureMFAPhoneCodeVerifyParams
+#### `SignInFutureResetPasswordSubmitParams`
TKTKTK
- - `code`
+ - `password`
- `string`
- TKTKTK
+ The new password for the user.
+
+ ---
+
+ - `signOutOfOtherSessions?`
+ - `boolean`
+
+ If `true`, signs the user out of all other authenticated sessions.
-### `mfa.verifyTOTP()`
+### `resetPasswordEmailCode.verifyCode()`
-Used to verify a TOTP code as a second factor to sign-in
+Used to verify a password reset code sent via email. Will cause `signIn.status` to become `'needs_new_password'`.
```ts
-function verifyTOTP(params: SignInFutureTOTPVerifyParams): Promise<{ error: unknown }>
+function verifyCode(params: SignInFutureEmailCodeVerifyParams): Promise<{ error: unknown }>
```
-#### SignInFutureTOTPVerifyParams
+#### `SignInFutureEmailCodeVerifyParams`
TKTKTK
@@ -478,25 +412,46 @@ TKTKTK
- `code`
- `string`
- TKTKTK
+ The one-time code that was sent to the user.
-### `mfa.verifyBackupCode()`
+### `sso()`
-Used to verify a backup code as a second factor to sign-in
+Used to perform OAuth authentication.
```ts
-function verifyBackupCode(params: SignInFutureBackupCodeVerifyParams): Promise<{ error: unknown }>
+function sso(params: SignInFutureSSOParams): Promise<{ error: unknown }>
```
-#### SignInFutureBackupCodeVerifyParams
+#### `SignInFutureSSOParams`
TKTKTK
- - `code`
+ - `flow?`
+ - `"auto" | "modal"`
+
+ TKTKTK
+
+ ---
+
+ - `redirectCallbackUrl`
- `string`
+ TODO @revamp-hooks: This should be handled by FAPI instead.
+
+ ---
+
+ - `redirectUrl`
+ - `string`
+
+ The URL to redirect to after the user has completed the SSO flow.
+
+ ---
+
+ - `strategy`
+ - `OAuthStrategy | "saml" | "enterprise_sso"`
+
TKTKTK
@@ -508,7 +463,7 @@ Used to perform a ticket-based sign-in.
function ticket(params?: SignInFutureTicketParams): Promise<{ error: unknown }>
```
-#### SignInFutureTicketParams
+#### `SignInFutureTicketParams`
TKTKTK
@@ -516,7 +471,7 @@ TKTKTK
- `ticket`
- `string`
- TKTKTK
+ The [ticket _or token_](/docs/guides/development/custom-flows/authentication/application-invitations) generated from the Backend API.
### `web3()`
@@ -527,70 +482,13 @@ Used to perform a Web3-based sign-in.
function web3(params: SignInFutureWeb3Params): Promise<{ error: unknown }>
```
-#### SignInFutureWeb3Params
+#### `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.
+ - `"web3_base_signature" | "web3_metamask_signature" | "web3_coinbase_wallet_signature" | "web3_okx_wallet_signature"`
-```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
+ The verification strategy to validate the user's sign-in request.
diff --git a/docs/reference/javascript/sign-up-future.mdx b/docs/reference/javascript/sign-up-future.mdx
index cd923d34e2..0c41cdff8c 100644
--- a/docs/reference/javascript/sign-up-future.mdx
+++ b/docs/reference/javascript/sign-up-future.mdx
@@ -8,165 +8,165 @@ 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`
+ - `abandonAt`
+ - `null | number`
- The unique identifier for the current sign-up attempt.
+ The epoch numerical time when the sign-up was abandoned by the user.
---
- - `status`
- - `SignUpStatus`
-
- The status of the current sign-up. The following values are possible:
+ - `createdSessionId`
+ - `null | string`
- - `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.
+ The identifier of the newly-created session. This attribute is populated only when the sign-up is complete.
---
- - `requiredFields`
- - `SignUpField[]`
+ - `createdUserId`
+ - `null | string`
- The list of required fields for the current sign-up attempt.
+ The identifier of the newly-created user. This attribute is populated only when the sign-up is complete.
---
- - `optionalFields`
- - `SignUpField[]`
+ - `emailAddress`
+ - `null | string`
- The list of optional fields for the current sign-up attempt.
+ The `emailAddress` supplied to the current sign-up. Only supported if [email address](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#email) is enabled in the instance settings.
---
- - `missingFields`
- - `SignUpField[]`
+ - `existingSession?`
+ - `{ sessionId: string }`
- The list of missing fields for the current sign-up attempt.
+ TKTKTK
---
- - `unverifiedFields`
- - `SignUpIdentificationField[]`
+ - `firstName`
+ - `null | string`
- An array of strings representing unverified fields such as `’email_address’`. Can be used to detect when verification is necessary.
+ The `firstName` supplied to the current sign-up. Only supported if [First and last name](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#user-model) is enabled in the instance settings.
---
- - `isTransferable`
+ - `hasPassword`
- `boolean`
- Indicates that there is a matching user for provided identifier, and that the sign-up can be transferred to a sign-in.
+ The value of this attribute is true if a password was supplied to the current sign-up. Only supported if [password](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#password) is enabled in the instance settings.
---
- - `existingSession`
- - `{ sessionId: string } | undefined`
+ - `id?`
+ - `string`
- TKTKTK
+ The unique identifier of the current sign-up.
---
- - `username`
- - `string | null`
+ - `isTransferable`
+ - `boolean`
- TKTKTK
+ Indicates that there is a matching user for provided identifier, and that the sign-up can be transferred to a sign-in.
---
- - `firstName`
- - `string | null`
+ - `lastName`
+ - `null | string`
- TKTKTK
+ The `lastName` supplied to the current sign-up. Only supported if [First and last name](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#user-model) is enabled in the instance settings.
---
- - `lastName`
- - `string | null`
+ - `legalAcceptedAt`
+ - `null | number`
- TKTKTK
+ The epoch numerical time when the user agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents.
---
- - `emailAddress`
- - `string | null`
+ - `missingFields`
+ - `SignUpField[]`
- TKTKTK
+ An array of all the fields whose values are not supplied yet but they are mandatory in order for a sign-up to be marked as complete.
---
- - `phoneNumber`
- - `string | null`
+ - `optionalFields`
+ - `SignUpField[]`
- TKTKTK
+ An array of all the fields that can be supplied to the sign-up, but their absence does not prevent the sign-up from being marked as complete.
---
- - `web3Wallet`
- - `string | null`
+ - `phoneNumber`
+ - `null | string`
- TKTKTK
+ The `phoneNumber` supplied to the current sign-up in E.164 format. Only supported if [phone number](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#phone) is enabled in the instance settings.
---
- - `hasPassword`
- - `boolean`
+ - `requiredFields`
+ - `SignUpField[]`
- TKTKTK
+ An array of all the required fields that need to be supplied and verified in order for this sign-up to be marked as complete and converted into a user.
---
- - `unsafeMetadata`
- - `SignUpUnsafeMetadata`
+ - `status`
+ - `SignUpStatus`
- TKTKTK
+ The status of the current sign-up.
---
- - `createdSessionId`
- - `string | null`
+ - `unsafeMetadata`
+ - `SignUpUnsafeMetadata`
- TKTKTK
+ Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created User object.
---
- - `createdUserId`
- - `string | null`
+ - `unverifiedFields`
+ - `SignUpIdentificationField[]`
- TKTKTK
+ An array of all the fields whose values have been supplied, but they need additional verification in order for them to be accepted. Examples of such fields are `email_address` and `phone_number`.
---
- - `abandonAt`
- - `number | null`
+ - `username`
+ - `null | string`
- TKTKTK
+ The `username` supplied to the current sign-up. Only supported if [username](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#username) is enabled in the instance settings.
---
- - `legalAcceptedAt`
- - `number | null`
+ - `web3Wallet`
+ - `null | string`
- TKTKTK
+ The Web3 wallet address supplied to the current sign-up, made up of 0x + 40 hexadecimal characters. Only supported if [Web3 authentication](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#web3-authentication) is enabled in the instance settings.
## Methods
### `create()`
-TKTKTK
+Creates a new `SignUp` instance initialized with the provided parameters. The instance maintains the sign-up lifecycle state through its `status` property, which updates as the authentication flow progresses. Will also deactivate any existing sign-up process the client may already have in progress.
+
+What you must pass to `params` depends on which [sign-up options](/docs/guides/configure/auth-strategies/sign-up-sign-in-options) you have enabled in your app's settings in the Clerk Dashboard.
+
+You can complete the sign-up process in one step if you supply the required fields to `create()`. Otherwise, Clerk's sign-up process provides great flexibility and allows users to easily create multi-step sign-up flows.
+
+> [!WARNING] > Once the sign-up process is complete, call the `signUp.finalize()` method to set the newly created session as > the active session.
```ts
function create(params: SignUpFutureCreateParams): Promise<{ error: unknown }>
```
-#### SignUpFutureCreateParams
+#### `SignUpFutureCreateParams`
TKTKTK
@@ -174,332 +174,300 @@ TKTKTK
- `emailAddress?`
- `string`
- TKTKTK
+ The user's email address. Only supported if [Email address](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#email) is enabled. Keep in mind that the email address requires an extra verification process.
---
- - `phoneNumber?`
+ - `firstName?`
- `string`
- TKTKTK
+ The user's first name. Only supported if [First and last name](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#user-model) is enabled in the instance settings.
---
- - `username?`
+ - `lastName?`
- `string`
- TKTKTK
+ The user's last name. Only supported if [First and last name](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#user-model) is enabled in the instance settings.
---
- - `transfer?`
+ - `legalAccepted?`
- `boolean`
- TKTKTK
+ A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents.
---
- - `ticket?`
+ - `phoneNumber?`
- `string`
- TKTKTK
+ The user's phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164). Only supported if [phone number](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#phone) is enabled. Keep in mind that the phone number requires an extra verification process.
---
- - `web3Wallet?`
+ - `ticket?`
- `string`
- TKTKTK
+ The [ticket _or token_](/docs/guides/development/custom-flows/authentication/application-invitations) generated from the Backend API. **Required** if `strategy` is set to `'ticket'`.
---
- - `firstName?`
- - `string`
+ - `transfer?`
+ - `boolean`
- TKTKTK
+ When set to `true`, the `SignUp` will attempt to retrieve information from the active `SignIn` instance and use it to complete the sign-up process. This is useful when you want to seamlessly transition a user from a sign-in attempt to a sign-up attempt.
---
- - `lastName?`
- - `string`
+ - `unsafeMetadata?`
+ - `SignUpUnsafeMetadata`
- TKTKTK
+ Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created User object.
---
- - `unsafeMetadata?`
- - `SignUpUnsafeMetadata`
+ - `username?`
+ - `string`
- TKTKTK
+ The user's username. Only supported if [username](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#username) is enabled in the instance settings.
---
- - `legalAccepted?`
- - `boolean`
+ - `web3Wallet?`
+ - `string`
- TKTKTK
+ The Web3 wallet address, made up of 0x + 40 hexadecimal characters. **Required** if [Web3 authentication](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#web3-authentication) is enabled.
-### `update()`
+### `finalize()`
-TKTKTK
+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 update(params: SignUpFutureUpdateParams): Promise<{ error: unknown }>
+function finalize(params?: SignUpFutureFinalizeParams): Promise<{ error: unknown }>
```
-#### SignUpFutureUpdateParams
+#### `SignUpFutureFinalizeParams`
TKTKTK
- - `firstName?`
- - `string`
-
- TKTKTK
-
- ---
-
- - `lastName?`
- - `string`
-
- TKTKTK
-
- ---
-
- - `unsafeMetadata?`
- - `SignUpUnsafeMetadata`
-
- TKTKTK
-
- ---
-
- - `legalAccepted?`
- - `boolean`
+ - `navigate?`
+ - `SetActiveNavigate`
TKTKTK
-### `verifications.sendEmailCode()`
-
-Used to send an email code to verify an email address.
-
-```ts
-function sendEmailCode(): Promise<{ error: unknown }>
-```
-
-### `verifications.verifyEmailCode()`
+### `password()`
-Used to verify a code sent via email.
+Used to sign up using an email address and password.
```ts
-function verifyEmailCode(params: SignUpFutureEmailCodeVerifyParams): Promise<{ error: unknown }>
+function password(params: SignUpFuturePasswordParams): Promise<{ error: unknown }>
```
-#### SignUpFutureEmailCodeVerifyParams
+#### `SignUpFuturePasswordParams`
TKTKTK
- - `code`
+ - `password`
- `string`
- TKTKTK
+ The user's password. Only supported if [password](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#password) is enabled.
-### `verifications.sendPhoneCode()`
+### `sso()`
-Used to send a phone code to verify a phone number.
+Used to create an account using an OAuth connection.
```ts
-function sendPhoneCode(params: SignUpFuturePhoneCodeSendParams): Promise<{ error: unknown }>
+function sso(params: SignUpFutureSSOParams): Promise<{ error: unknown }>
```
-#### SignUpFuturePhoneCodeSendParams
+#### `SignUpFutureSSOParams`
TKTKTK
- - `phoneNumber?`
+ - `redirectCallbackUrl`
- `string`
- TKTKTK
+ TODO @revamp-hooks: This should be handled by FAPI instead.
---
- - `channel?`
- - `PhoneCodeChannel`
-
- TKTKTK
-
-
-### `verifications.verifyPhoneCode()`
-
-Used to verify a code sent via phone.
-
-```ts
-function verifyPhoneCode(params: SignUpFuturePhoneCodeVerifyParams): Promise<{ error: unknown }>
-```
+ - `redirectUrl`
+ - `string`
-#### SignUpFuturePhoneCodeVerifyParams
+ The URL or path to navigate to after the OAuth or SAML flow completes. Can be provided as a relative URL (such as `/dashboard`), in which case it will be prefixed with the base URL of the current page.
-TKTKTK
+ ---
-
- - `code`
+ - `strategy`
- `string`
- TKTKTK
+ The strategy to use for authentication.
-### `password()`
+### `ticket()`
-Used to sign up using an email address and password.
+Used to perform a ticket-based sign-up.
```ts
-function password(params: SignUpFuturePasswordParams): Promise<{ error: unknown }>
+function ticket(params?: SignUpFutureTicketParams): Promise<{ error: unknown }>
```
-#### SignUpFuturePasswordParams
+#### `SignUpFutureTicketParams`
TKTKTK
-Must include `password` and exactly one of `emailAddress`, `phoneNumber`, or `username`. You can also provide additional optional fields.
-
- - `password`
+ - `firstName?`
- `string`
- TKTKTK
+ The user's first name. Only supported if [First and last name](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#user-model) is enabled in the instance settings.
---
- - `emailAddress?`
+ - `lastName?`
- `string`
- TKTKTK
+ The user's last name. Only supported if [First and last name](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#user-model) is enabled in the instance settings.
---
- - `phoneNumber?`
- - `string`
+ - `legalAccepted?`
+ - `boolean`
- TKTKTK
+ A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents.
---
- - `username?`
+ - `ticket`
- `string`
- TKTKTK
+ The [ticket _or token_](/docs/guides/development/custom-flows/authentication/application-invitations) generated from the Backend API. **Required** if `strategy` is set to `'ticket'`.
---
+ - `unsafeMetadata?`
+ - `SignUpUnsafeMetadata`
+
+ Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created User object.
+
+
+### `update()`
+
+Updates the current `SignUp`.
+
+```ts
+function update(params: SignUpFutureUpdateParams): Promise<{ error: unknown }>
+```
+
+#### `SignUpFutureUpdateParams`
+
+TKTKTK
+
+
- `firstName?`
- `string`
- TKTKTK
+ The user's first name. Only supported if [First and last name](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#user-model) is enabled in the instance settings.
---
- `lastName?`
- `string`
- TKTKTK
+ The user's last name. Only supported if [First and last name](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#user-model) is enabled in the instance settings.
---
- - `unsafeMetadata?`
- - `SignUpUnsafeMetadata`
+ - `legalAccepted?`
+ - `boolean`
- TKTKTK
+ A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents.
---
- - `legalAccepted?`
- - `boolean`
+ - `unsafeMetadata?`
+ - `SignUpUnsafeMetadata`
- TKTKTK
+ Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created User object.
-### `sso()`
+### `verifications.sendEmailCode()`
-Used to create an account using an OAuth connection.
+Used to send an email code to verify an email address.
```ts
-function sso(params: SignUpFutureSSOParams): Promise<{ error: unknown }>
+function sendEmailCode(): Promise<{ error: unknown }>
```
-#### SignUpFutureSSOParams
+### `verifications.sendPhoneCode()`
-TKTKTK
+Used to send a phone code to verify a phone number.
-
- - `strategy`
- - `string`
+```ts
+function sendPhoneCode(params: SignUpFuturePhoneCodeSendParams): Promise<{ error: unknown }>
+```
- TKTKTK
+#### `SignUpFuturePhoneCodeSendParams`
- ---
+TKTKTK
- - `redirectUrl`
- - `string`
+
+ - `channel?`
+ - `PhoneCodeChannel`
- The URL to redirect to after the user has completed the SSO flow.
+ The mechanism to use to send the code to the provided phone number. Defaults to `'sms'`.
---
- - `redirectCallbackUrl`
+ - `phoneNumber?`
- `string`
- TODO @revamp-hooks: This should be handled by FAPI instead.
+ The user's phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164). Only supported if [phone number](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#phone) is enabled. Keep in mind that the phone number requires an extra verification process.
-### `ticket()`
+### `verifications.verifyEmailCode()`
-Used to perform a ticket-based sign-up.
+Used to verify a code sent via email.
```ts
-function ticket(params?: SignUpFutureTicketParams): Promise<{ error: unknown }>
+function verifyEmailCode(params: SignUpFutureEmailCodeVerifyParams): Promise<{ error: unknown }>
```
-#### SignUpFutureTicketParams
+#### `SignUpFutureEmailCodeVerifyParams`
TKTKTK
- - `ticket`
- - `string`
-
- TKTKTK
-
- ---
-
- - `firstName?`
+ - `code`
- `string`
- TKTKTK
-
- ---
-
- - `lastName?`
- - `string`
+ The code that was sent to the user.
+
- TKTKTK
+### `verifications.verifyPhoneCode()`
- ---
+Used to verify a code sent via phone.
- - `unsafeMetadata?`
- - `SignUpUnsafeMetadata`
+```ts
+function verifyPhoneCode(params: SignUpFuturePhoneCodeVerifyParams): Promise<{ error: unknown }>
+```
- TKTKTK
+#### `SignUpFuturePhoneCodeVerifyParams`
- ---
+TKTKTK
- - `legalAccepted?`
- - `boolean`
+
+ - `code`
+ - `string`
- TKTKTK
+ The code that was sent to the user.
### `web3()`
@@ -510,59 +478,41 @@ Used to perform a Web3-based sign-up.
function web3(params: SignUpFutureWeb3Params): Promise<{ error: unknown }>
```
-#### SignUpFutureWeb3Params
+#### `SignUpFutureWeb3Params`
TKTKTK
- - `strategy`
- - `Web3Strategy`
+ - `firstName?`
+ - `string`
- TKTKTK
+ The user's first name. Only supported if [First and last name](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#user-model) is enabled in the instance settings.
---
- - `unsafeMetadata?`
- - `SignUpUnsafeMetadata`
+ - `lastName?`
+ - `string`
- TKTKTK
+ The user's last name. Only supported if [First and last name](/docs/guides/configure/auth-strategies/sign-up-sign-in-options#user-model) is enabled in the instance settings.
---
- `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`
+ A boolean indicating whether the user has agreed to the [legal compliance](/docs/guides/secure/legal-compliance) documents.
- TKTKTK
-
+ ---
-## Types
+ - `strategy`
+ - `"web3_base_signature" | "web3_metamask_signature" | "web3_coinbase_wallet_signature" | "web3_okx_wallet_signature"`
-### `ExistingSession`
+ The verification strategy to validate the user's sign-up request.
-The shape of `existingSession` when present.
+ ---
-
- - `sessionId`
- - `string`
+ - `unsafeMetadata?`
+ - `SignUpUnsafeMetadata`
- TKTKTK
+ Metadata that can be read and set from the frontend. Once the sign-up is complete, the value of this field will be automatically copied to the newly created user's unsafe metadata. One common use case for this attribute is to use it to implement custom fields that can be collected during sign-up and will automatically be attached to the created User object.
From f868d8a6cf69d58532b283892ea192476cea31db Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Tue, 14 Oct 2025 13:31:49 -0500
Subject: [PATCH 15/18] Fix line breaks
---
docs/reference/javascript/sign-in-future.mdx | 3 ++-
docs/reference/javascript/sign-up-future.mdx | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/docs/reference/javascript/sign-in-future.mdx b/docs/reference/javascript/sign-in-future.mdx
index 92c13148be..0e84bd44df 100644
--- a/docs/reference/javascript/sign-in-future.mdx
+++ b/docs/reference/javascript/sign-in-future.mdx
@@ -97,7 +97,8 @@ What you must pass to `params` depends on which [sign-in options](/docs/guides/c
You can complete the sign-in process in one step if you supply the required fields to `create()`. Otherwise, Clerk's sign-in process provides great flexibility and allows users to easily create multi-step sign-in flows.
-> [!WARNING] > Once the sign-in process is complete, call the `signIn.finalize()` method to set the newly created session as > the active session.
+> [!WARNING]
+> Once the sign-in process is complete, call the `signIn.finalize()` method to set the newly created session as the active session.
```ts
function create(params: SignInFutureCreateParams): Promise<{ error: unknown }>
diff --git a/docs/reference/javascript/sign-up-future.mdx b/docs/reference/javascript/sign-up-future.mdx
index 0c41cdff8c..8f2531099b 100644
--- a/docs/reference/javascript/sign-up-future.mdx
+++ b/docs/reference/javascript/sign-up-future.mdx
@@ -160,7 +160,8 @@ What you must pass to `params` depends on which [sign-up options](/docs/guides/c
You can complete the sign-up process in one step if you supply the required fields to `create()`. Otherwise, Clerk's sign-up process provides great flexibility and allows users to easily create multi-step sign-up flows.
-> [!WARNING] > Once the sign-up process is complete, call the `signUp.finalize()` method to set the newly created session as > the active session.
+> [!WARNING]
+> Once the sign-up process is complete, call the `signUp.finalize()` method to set the newly created session as the active session.
```ts
function create(params: SignUpFutureCreateParams): Promise<{ error: unknown }>
From 8cd114e615eeea9884900b4c0f127b5e69253e43 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 15 Oct 2025 11:27:23 -0500
Subject: [PATCH 16/18] Add hooks docs and legacy docs
---
docs/manifest.json | 16 ++++++++++++
docs/reference/hooks/legacy/use-sign-in.mdx | 7 ++++++
docs/reference/hooks/legacy/use-sign-up.mdx | 7 ++++++
docs/reference/hooks/use-sign-in.mdx | 27 ++++++++++++++++++++-
docs/reference/hooks/use-sign-up.mdx | 25 ++++++++++++++++++-
5 files changed, 80 insertions(+), 2 deletions(-)
create mode 100644 docs/reference/hooks/legacy/use-sign-in.mdx
create mode 100644 docs/reference/hooks/legacy/use-sign-up.mdx
diff --git a/docs/manifest.json b/docs/manifest.json
index 1cba89d6fd..5600524236 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -2999,6 +2999,22 @@
"title": "`useStatements()`",
"href": "/docs/reference/hooks/use-statements",
"tag": "(Beta)"
+ },
+ {
+ "title": "Legacy APIs",
+ "collapse": true,
+ "items": [
+ [
+ {
+ "title": "`useSignIn()`",
+ "href": "/docs/reference/hooks/legacy/use-sign-in"
+ },
+ {
+ "title": "`useSignUp()`",
+ "href": "/docs/reference/hooks/legacy/use-sign-up"
+ }
+ ]
+ ]
}
]
]
diff --git a/docs/reference/hooks/legacy/use-sign-in.mdx b/docs/reference/hooks/legacy/use-sign-in.mdx
new file mode 100644
index 0000000000..bddec03f46
--- /dev/null
+++ b/docs/reference/hooks/legacy/use-sign-in.mdx
@@ -0,0 +1,7 @@
+---
+title: useSignIn()
+description: Access and manage the current user's sign-in state in your React application with Clerk's useSignIn() hook.
+sdk: chrome-extension, expo, nextjs, react, react-router, remix, tanstack-react-start
+---
+
+
diff --git a/docs/reference/hooks/legacy/use-sign-up.mdx b/docs/reference/hooks/legacy/use-sign-up.mdx
new file mode 100644
index 0000000000..e9d38edece
--- /dev/null
+++ b/docs/reference/hooks/legacy/use-sign-up.mdx
@@ -0,0 +1,7 @@
+---
+title: useSignUp()
+description: Access and manage the current user's sign-up state in your React application with Clerk's useSignUp() hook.
+sdk: chrome-extension, expo, nextjs, react, react-router, remix, tanstack-react-start
+---
+
+
diff --git a/docs/reference/hooks/use-sign-in.mdx b/docs/reference/hooks/use-sign-in.mdx
index bddec03f46..156151617b 100644
--- a/docs/reference/hooks/use-sign-in.mdx
+++ b/docs/reference/hooks/use-sign-in.mdx
@@ -4,4 +4,29 @@ description: Access and manage the current user's sign-in state in your React ap
sdk: chrome-extension, expo, nextjs, react, react-router, remix, tanstack-react-start
---
-
+The `useSignIn()` hook provides access to the [`SignInFuture`](/docs/reference/javascript/sign-in-future) object, which allows you to check the current state of a sign-in attempt and manage the sign-in flow. You can use this to create a [custom sign-in flow](/docs/guides/development/custom-flows/overview#sign-in-flow).
+
+## Returns
+
+
+ - `signIn`
+ - [`SignInFuture`](/docs/reference/javascript/sign-in-future)
+
+ The current active `SignInFuture` instance, for use in custom flows.
+
+ ---
+
+ - `errors`
+ - [`Errors`](/docs/reference/javascript/errors)
+
+ The errors that occurred during the last API request.
+
+ ---
+
+ - `fetchStatus`
+ - `'idle' | 'fetching'`
+
+ The fetch status of the underlying `SignInFuture` resource.
+
+
+## Examples
diff --git a/docs/reference/hooks/use-sign-up.mdx b/docs/reference/hooks/use-sign-up.mdx
index e9d38edece..9961121b74 100644
--- a/docs/reference/hooks/use-sign-up.mdx
+++ b/docs/reference/hooks/use-sign-up.mdx
@@ -4,4 +4,27 @@ description: Access and manage the current user's sign-up state in your React ap
sdk: chrome-extension, expo, nextjs, react, react-router, remix, tanstack-react-start
---
-
+The `useSignUp()` hook provides access to the [`SignUpFuture`](/docs/reference/javascript/sign-up-future) object, which allows you to check the current state of a sign-up attempt and manage the sign-up flow. You can use this to create a [custom sign-up flow](/docs/guides/development/custom-flows/overview#sign-up-flow).
+
+## Returns
+
+
+ - `SignUp`
+ - [`SignUpFuture`](/docs/reference/javascript/sign-up-future)
+
+ The current active `SignUpFuture` instance, for use in custom flows.
+
+ ---
+
+ - `errors`
+ - [`Errors`](/docs/reference/javascript/errors)
+
+ The errors that occurred during the last API request.
+
+ ---
+
+ - `fetchStatus`
+ - `'idle' | 'fetching'`
+
+ The fetch status of the underlying `SignUpFuture` resource.
+
From 3832e90360f396227b3a072015697246b047d97d Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 15 Oct 2025 11:30:00 -0500
Subject: [PATCH 17/18] Remove link
---
docs/reference/hooks/use-sign-in.mdx | 2 +-
docs/reference/hooks/use-sign-up.mdx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/reference/hooks/use-sign-in.mdx b/docs/reference/hooks/use-sign-in.mdx
index 156151617b..cd120990be 100644
--- a/docs/reference/hooks/use-sign-in.mdx
+++ b/docs/reference/hooks/use-sign-in.mdx
@@ -17,7 +17,7 @@ The `useSignIn()` hook provides access to the [`SignInFuture`](/docs/reference/j
---
- `errors`
- - [`Errors`](/docs/reference/javascript/errors)
+ - `Errors`
The errors that occurred during the last API request.
diff --git a/docs/reference/hooks/use-sign-up.mdx b/docs/reference/hooks/use-sign-up.mdx
index 9961121b74..ede88cb98c 100644
--- a/docs/reference/hooks/use-sign-up.mdx
+++ b/docs/reference/hooks/use-sign-up.mdx
@@ -17,7 +17,7 @@ The `useSignUp()` hook provides access to the [`SignUpFuture`](/docs/reference/j
---
- `errors`
- - [`Errors`](/docs/reference/javascript/errors)
+ - `Errors`
The errors that occurred during the last API request.
From 6428b2f62c8ece8f8f20b03953c1d005272a44c1 Mon Sep 17 00:00:00 2001
From: Dylan Staley <88163+dstaley@users.noreply.github.com>
Date: Wed, 15 Oct 2025 11:35:05 -0500
Subject: [PATCH 18/18] Add legacy warning
---
docs/reference/hooks/legacy/use-sign-in.mdx | 3 +++
docs/reference/hooks/legacy/use-sign-up.mdx | 3 +++
2 files changed, 6 insertions(+)
diff --git a/docs/reference/hooks/legacy/use-sign-in.mdx b/docs/reference/hooks/legacy/use-sign-in.mdx
index bddec03f46..3d6c961c55 100644
--- a/docs/reference/hooks/legacy/use-sign-in.mdx
+++ b/docs/reference/hooks/legacy/use-sign-in.mdx
@@ -4,4 +4,7 @@ description: Access and manage the current user's sign-in state in your React ap
sdk: chrome-extension, expo, nextjs, react, react-router, remix, tanstack-react-start
---
+> [!WARNING]
+> This hook uses our legacy API, which will be removed in a future release. We recommend migrating to the new [`useSignIn()`](/docs/reference/hooks/use-sign-in) hook instead.
+
diff --git a/docs/reference/hooks/legacy/use-sign-up.mdx b/docs/reference/hooks/legacy/use-sign-up.mdx
index e9d38edece..042c814aa9 100644
--- a/docs/reference/hooks/legacy/use-sign-up.mdx
+++ b/docs/reference/hooks/legacy/use-sign-up.mdx
@@ -4,4 +4,7 @@ description: Access and manage the current user's sign-up state in your React ap
sdk: chrome-extension, expo, nextjs, react, react-router, remix, tanstack-react-start
---
+> [!WARNING]
+> This hook uses our legacy API, which will be removed in a future release. We recommend migrating to the new [`useSignUp()`](/docs/reference/hooks/use-sign-up) hook instead.
+