Sign-in-or-up custom flow
This guide demonstrates how to build a custom user interface that allows users to sign up or sign in within a single flow. There are two approaches:
- Standard flow: The sign-in attempt immediately tells you whether an account exists. If it doesn't exist yet, you start the sign-up flow. This is simple and gives users immediate feedback, but it reveals whether an account exists before any verification, making it susceptible to user enumeration attacks.
signUpIfMissingflow: The sign-in proceeds to verification regardless of whether an account exists. Only after verification does the backend reveal whether the account exists or needs to be created. This prevents user enumeration attacks.
Standard sign-in-or-up flow
Enable email and password authentication
This example uses the email and password sign-in custom flow as a base. However, you can modify this approach according to the settings you've configured for your application's instance in the Clerk Dashboard.
- In the Clerk Dashboard, navigate to the User & authentication page.
- Enable Sign-up with email.
- Require email address should be enabled.
- For Verify at sign-up, Email verification code is enabled by default, and is used for this guide. If you'd like to use Email verification link instead, see the dedicated custom flow.
- Enable Sign in with email.
- This guide supports password authentication. If you'd like to build a custom flow that allows users to sign in passwordlessly, see the email code custom flow or the email links custom flow.
- Select the Password tab and enable Sign-up with password.
- Client Trust is enabled by default. The sign-in example supports it using email verification codes because it's the default second factor strategy.
Build the flow
To blend a sign-up and sign-in flow into a single flow, you must treat it as a sign-in flow, but with the ability to sign up a new user if they don't have an account. You can do this by checking for the form_identifier_not_found error if the sign-in process fails, and then starting the sign-up process.
'use client'
import { useSignIn, useSignUp } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import React from 'react'
export default function Page() {
const { signIn, errors, fetchStatus } = useSignIn()
const { signUp } = useSignUp()
const router = useRouter()
const [emailAddress, setEmailAddress] = React.useState('')
const [password, setPassword] = React.useState('')
const [code, setCode] = React.useState('')
const [showEmailCode, setShowEmailCode] = React.useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const { error } = await signIn.password({
emailAddress,
password,
})
if (error) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(error, null, 2))
// If the identifier is not found, the user is not signed up yet
// So swap to the sign-up flow
if (error.errors[0].code === 'form_identifier_not_found') {
try {
const { error } = await signUp.password({
emailAddress,
password,
})
// Send the user an email with the verification code
if (!error) await signUp.verifications.sendEmailCode()
// Display second form to capture the verification code
if (
signUp.status === 'missing_requirements' &&
signUp.unverifiedFields.includes('email_address') &&
signUp.missingFields.length === 0
) {
setShowEmailCode(true)
return
}
} 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 (signIn.status === 'complete') {
await signIn.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url)
}
},
})
} else if (signIn.status === 'needs_second_factor') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
} else if (signIn.status === 'needs_client_trust') {
// For other second factor strategies,
// see https://clerk.com/docs/guides/development/custom-flows/authentication/client-trust
const emailCodeFactor = signIn.supportedSecondFactors.find(
(factor) => factor.strategy === 'email_code',
)
if (emailCodeFactor) {
await signIn.mfa.sendEmailCode()
}
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete:', signIn)
}
}
const handleVerify = async (e: React.FormEvent) => {
e.preventDefault()
// Flow for signing up a new user
if (showEmailCode) {
// Use the code the user provided to attempt verification
const { error } = await signUp.verifications.verifyEmailCode({
code,
})
if (error) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(error, null, 2))
return
}
// If verification was completed, set the session to active
// and redirect the user
if (signUp.status === 'complete') {
await signUp.finalize({
navigate: async ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url)
}
},
})
} else {
// Check why the status is not complete
console.error('Sign-up attempt not complete. Status:', signUp.status)
}
}
// Flow for signing in an existing user
const { error } = await signIn.mfa.verifyEmailCode({
code,
})
if (error) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'complete') {
await signIn.finalize({
navigate: async ({ session, decorateUrl }) => {
if (session?.currentTask) {
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url)
}
},
})
} else {
// Check why the status is not complete
console.error('Sign-in attempt not complete. Status:', signIn.status)
}
}
if (showEmailCode || signIn.status === 'needs_client_trust') {
return (
<>
<h1>Verify your account</h1>
<form onSubmit={handleVerify}>
<div>
<label htmlFor="code">Code</label>
<input
id="code"
name="code"
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
{errors.fields.code && <p>{errors.fields.code.message}</p>}
</div>
<button type="submit" disabled={fetchStatus === 'fetching'}>
Verify
</button>
</form>
<button onClick={() => signIn.mfa.sendEmailCode()}>I need a new code</button>
<button onClick={() => signIn.reset()}>Start over</button>
</>
)
}
return (
<>
<h1>Sign up/sign in</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Enter email address</label>
<input
id="email"
name="email"
type="email"
value={emailAddress}
onChange={(e) => setEmailAddress(e.target.value)}
/>
{errors.fields.identifier && <p>{errors.fields.identifier.message}</p>}
</div>
<div>
<label htmlFor="password">Enter password</label>
<input
id="password"
name="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{errors.fields.password && <p>{errors.fields.password.message}</p>}
</div>
<button type="submit" disabled={fetchStatus === 'fetching'}>
Continue
</button>
</form>
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <p>{JSON.stringify(errors, null, 2)}</p>}
{/* Required for sign-up flows. Clerk's bot sign-up protection is enabled by default */}
<div id="clerk-captcha" />
</>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn, useSignUp } from '@clerk/expo'
import { type Href, Link, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'
export default function Page() {
const { signIn, errors, fetchStatus } = useSignIn()
const { signUp } = useSignUp()
const router = useRouter()
const [emailAddress, setEmailAddress] = React.useState('')
const [password, setPassword] = React.useState('')
const [code, setCode] = React.useState('')
const [showEmailCode, setShowEmailCode] = React.useState(false)
const handleSubmit = async () => {
const { error } = await signIn.password({
emailAddress,
password,
})
if (error) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(error, null, 2))
// If the identifier is not found, the user is not signed up yet
// So swap to the sign-up flow
if (error.errors[0].code === 'form_identifier_not_found') {
try {
const { error: signUpError } = await signUp.password({
emailAddress,
password,
})
// Send the user an email with the verification code
if (!signUpError) await signUp.verifications.sendEmailCode()
// Display second form to capture the verification code
if (
signUp.status === 'missing_requirements' &&
signUp.unverifiedFields.includes('email_address') &&
signUp.missingFields.length === 0
) {
setShowEmailCode(true)
return
}
} catch (err: unknown) {
console.error(JSON.stringify(err, null, 2))
}
}
}
if (signIn.status === 'complete') {
await signIn.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
} else if (signIn.status === 'needs_second_factor') {
// Handle MFA if required
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
} else if (signIn.status === 'needs_client_trust') {
// Handle Client Trust
// See https://clerk.com/docs/guides/development/custom-flows/authentication/client-trust
const emailCodeFactor = signIn.supportedSecondFactors.find(
(factor) => factor.strategy === 'email_code',
)
if (emailCodeFactor) {
await signIn.mfa.sendEmailCode()
}
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete:', signIn)
}
}
const handleVerify = async () => {
// Flow for signing up a new user
if (showEmailCode) {
const { error } = await signUp.verifications.verifyEmailCode({
code,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signUp.status === 'complete') {
await signUp.finalize({
navigate: async ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
} else {
// Check why the sign-up is not complete
console.error('Sign-up attempt not complete. Status:', signUp.status)
}
}
// Flow for signing in an existing user
const { error } = await signIn.mfa.verifyEmailCode({
code,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'complete') {
await signIn.finalize({
navigate: async ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete. Status:', signIn.status)
}
}
if (showEmailCode || signIn.status === 'needs_client_trust') {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Verify your account
</ThemedText>
<ThemedText style={styles.label}>Code</ThemedText>
<TextInput
style={styles.input}
value={code}
placeholder="Enter verification code"
placeholderTextColor="#666666"
onChangeText={setCode}
keyboardType="number-pad"
autoCapitalize="none"
/>
{errors.fields.code && (
<ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
(!code || fetchStatus === 'fetching') && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleVerify}
disabled={!code || fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Verify</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
onPress={() => signIn.mfa.sendEmailCode()}
>
<ThemedText style={styles.secondaryButtonText}>I need a new code</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
onPress={() => signIn.reset()}
>
<ThemedText style={styles.secondaryButtonText}>Start over</ThemedText>
</Pressable>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Sign up / sign in
</ThemedText>
<ThemedText style={styles.label}>Email address</ThemedText>
<TextInput
style={styles.input}
autoCapitalize="none"
value={emailAddress}
placeholder="Enter email"
placeholderTextColor="#666666"
onChangeText={setEmailAddress}
keyboardType="email-address"
/>
{errors.fields.identifier && (
<ThemedText style={styles.error}>{errors.fields.identifier.message}</ThemedText>
)}
<ThemedText style={styles.label}>Password</ThemedText>
<TextInput
style={styles.input}
value={password}
placeholder="Enter password"
placeholderTextColor="#666666"
secureTextEntry
onChangeText={setPassword}
/>
{errors.fields.password && (
<ThemedText style={styles.error}>{errors.fields.password.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
(!emailAddress || !password || fetchStatus === 'fetching') && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleSubmit}
disabled={!emailAddress || !password || fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Continue</ThemedText>
</Pressable>
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}
{/* Required for sign-up flows. Clerk's bot sign-up protection is enabled by default */}
<View nativeID="clerk-captcha" style={styles.captchaContainer} />
<View style={styles.linkContainer}>
<ThemedText>{"Don't have an account? "}</ThemedText>
<Link href="/sign-up">
<ThemedText type="link">Sign up</ThemedText>
</Link>
</View>
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
secondaryButton: {
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
secondaryButtonText: {
color: '#0a7ea4',
fontWeight: '600',
},
linkContainer: {
flexDirection: 'row',
gap: 4,
marginTop: 12,
alignItems: 'center',
},
error: {
color: '#d32f2f',
fontSize: 12,
marginTop: -8,
},
debug: {
fontSize: 10,
opacity: 0.5,
marginTop: 8,
},
captchaContainer: {
minHeight: 1,
},
})Sign-in-or-up with signUpIfMissing
The signUpIfMissing option offers a privacy-preserving alternative to the standard sign-in-or-up flow. Unlike the standard flow, which discloses whether an account exists from the outset, this option proceeds to the verification step regardless of if the account exists. Only after the user has successfully completed the verification step does the flow reveal if an account already exists or if one needs to be created. Although it is recommended to pair the signUpIfMissing flow with strict user enumeration protections in the Clerk Dashboard for maximum security, this option doesn't require that setting.
How the flow works
- Start sign-in with
signIn.create({ identifier, signUpIfMissing: true }). - Prepare and complete verification (e.g.,
signIn.emailCode.sendCode()andsignIn.emailCode.verifyCode()). A verification code is sent whether or not the account exists. - After verification:
- If the account exists,
signIn.statusbecomes'complete'and you finalize the sign-in. - If the account does not exist,
verifyCode()returns an error with the codesign_up_if_missing_transfer. You then transfer to sign-up by callingsignUp.create({ transfer: true }).
- If the account exists,
- After transferring, the sign-up may complete immediately, or it may have
status === 'missing_requirements'if additional fields are needed (e.g., legal acceptance or first/last name depending on your application's settings in the Clerk Dashboard). In that case, collect the missing fields and callsignUp.update()to complete the sign-up.
Restrictions
- Password strategy is not supported. The flow requires a strategy with a separate prepare step (such as email code, phone code, or email link) because the flow needs to proceed to verification regardless of whether the account exists or not.
- Only email address, phone number, and Web3 wallet identifiers are supported. Username is not supported because there's no way to contact a user for verification using just a username. Social sign-in (OAuth) is already safe against user enumeration attacks regardless of the sign-in-or-up option chosen (standard or
signUpIfMissing). - Not available in restricted or waitlist sign-up modes. The instance must allow public sign-ups.
Enable email code authentication
This example uses the email OTP sign-in custom flow as a base. However, you can modify this approach for phone OTP sign-in or email link sign-in instead.
- In the Clerk Dashboard, navigate to the User & authentication page.
- Ensure Require email address is enabled.
- Ensure Verify at sign-up is enabled, with Email verification code selected.
- Ensure Sign-in with email is enabled, with Email verification code selected.
Build the flow
'use client'
import { useSignIn, useSignUp } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import React from 'react'
export default function Page() {
const { signIn, errors, fetchStatus } = useSignIn()
const { signUp } = useSignUp()
const router = useRouter()
const [emailAddress, setEmailAddress] = React.useState('')
const [code, setCode] = React.useState('')
const [verifying, setVerifying] = React.useState(false)
const [showMissingRequirements, setShowMissingRequirements] = React.useState(false)
// Helper to finalize sign-in and navigate
const finalizeSignIn = async () => {
await signIn.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url)
}
},
})
}
// Helper to finalize sign-up and navigate
const finalizeSignUp = async () => {
await signUp.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url)
}
},
})
}
// Step 1: Start sign-in with signUpIfMissing and send email code
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Create sign-in for the signUpIfMissing flow.
// The flow will proceed to verification regardless of whether an account exists or not.
const { error: createError } = await signIn.create({
identifier: emailAddress,
signUpIfMissing: true,
})
if (createError) {
console.error(JSON.stringify(createError, null, 2))
return
}
// Start the verification step
if (!createError) {
const { error: sendError } = await signIn.emailCode.sendCode()
if (sendError) {
console.error(JSON.stringify(sendError, null, 2))
return
}
setVerifying(true)
}
}
// Step 2: Verification step
const handleVerify = async (e: React.FormEvent) => {
e.preventDefault()
const { error } = await signIn.emailCode.verifyCode({ code })
// When the user doesn't exist, verifyCode returns an error with
// the code 'sign_up_if_missing_transfer'. Check for this error
// to determine if we need to transfer to sign-up.
if (error) {
if (error.errors[0]?.code === 'sign_up_if_missing_transfer') {
// The user doesn't exist - transfer to sign-up
await handleTransfer()
return
}
// Some other error occurred
console.error(JSON.stringify(error, null, 2))
return
}
// The user exists and verification succeeded
if (signIn.status === 'complete') {
await finalizeSignIn()
} else if (signIn.status === 'needs_second_factor') {
// Handle MFA if required
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
} else if (signIn.status === 'needs_client_trust') {
// Handle client trust if required
// See https://clerk.com/docs/guides/development/custom-flows/authentication/client-trust
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete:', signIn.status)
}
}
// Step 3: Transfer to sign-up
const handleTransfer = async () => {
// Create sign-up using transfer.
// This moves the verified identification from the sign-in to a new sign-up.
const { error } = await signUp.create({ transfer: true })
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signUp.status === 'complete') {
// No additional requirements - sign-up is complete
await finalizeSignUp()
} else if (signUp.status === 'missing_requirements') {
// Additional fields are required to complete sign-up.
// Common missing fields include legal_accepted, first_name, last_name, etc.
// Show a form to collect the missing fields.
setShowMissingRequirements(true)
} else {
console.error('Unexpected sign-up status:', signUp.status)
}
}
// Step 4: Submit missing requirements to complete sign-up
const handleMissingRequirements = async (e: React.FormEvent) => {
e.preventDefault()
// This example handles legal acceptance as an example.
// You can extend this to handle other missing fields like first_name, last_name, etc.
// by checking signUp.missingFields and collecting the appropriate values.
const { error } = await signUp.update({
legalAccepted: true,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signUp.status === 'complete') {
await finalizeSignUp()
} else if (signUp.status === 'missing_requirements') {
// Still missing other fields
console.error('Additional fields still required:', signUp.missingFields)
} else {
console.error('Unexpected sign-up status:', signUp.status)
}
}
// Step 4 UI: Show missing requirements form
if (showMissingRequirements) {
return (
<>
<h1>Complete your account</h1>
<p>Your email has been verified. Please complete the following to create your account.</p>
<form onSubmit={handleMissingRequirements}>
{signUp.missingFields.includes('legal_accepted') && (
<div>
<label>
<input type="checkbox" required />I agree to the Terms of Service and Privacy Policy
</label>
</div>
)}
<button type="submit" disabled={fetchStatus === 'fetching'}>
Create account
</button>
</form>
<button onClick={() => signIn.reset()}>Start over</button>
</>
)
}
// Step 2 UI: Show verification code form
if (verifying) {
return (
<>
<h1>Verify your email</h1>
<p>
We sent a verification code to <strong>{emailAddress}</strong>
</p>
<form onSubmit={handleVerify}>
<div>
<label htmlFor="code">Verification code</label>
<input
id="code"
name="code"
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
{errors.fields.code && <p>{errors.fields.code.message}</p>}
</div>
<button type="submit" disabled={fetchStatus === 'fetching'}>
Verify
</button>
</form>
<button onClick={() => signIn.emailCode.sendCode()}>Resend code</button>
<button onClick={() => signIn.reset()}>Start over</button>
</>
)
}
// Step 1 UI: Show email input form
return (
<>
<h1>Sign in or sign up</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Enter email address</label>
<input
id="email"
name="email"
type="email"
value={emailAddress}
onChange={(e) => setEmailAddress(e.target.value)}
/>
{errors.fields.identifier && <p>{errors.fields.identifier.message}</p>}
</div>
<button type="submit" disabled={fetchStatus === 'fetching'}>
Continue
</button>
</form>
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <p>{JSON.stringify(errors, null, 2)}</p>}
{/* Required for sign-up flows. Clerk's bot sign-up protection is enabled by default. */}
<div id="clerk-captcha" />
</>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn, useSignUp } from '@clerk/expo'
import { type Href, Link, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, Switch, TextInput, View } from 'react-native'
export default function Page() {
const { signIn, errors, fetchStatus } = useSignIn()
const { signUp } = useSignUp()
const router = useRouter()
const [emailAddress, setEmailAddress] = React.useState('')
const [code, setCode] = React.useState('')
const [verifying, setVerifying] = React.useState(false)
const [showMissingRequirements, setShowMissingRequirements] = React.useState(false)
const [legalAccepted, setLegalAccepted] = React.useState(false)
// Helper to finalize sign-in and navigate
const finalizeSignIn = async () => {
await signIn.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
}
// Helper to finalize sign-up and navigate
const finalizeSignUp = async () => {
await signUp.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
}
// Step 1: Start sign-in with signUpIfMissing and send email code
const handleSubmit = async () => {
// Create sign-in for the signUpIfMissing flow.
// The flow will proceed to verification regardless of whether an account exists or not.
const { error: createError } = await signIn.create({
identifier: emailAddress,
signUpIfMissing: true,
} as Parameters<typeof signIn.create>[0])
if (createError) {
console.error(JSON.stringify(createError, null, 2))
return
}
// Start the verification step
if (!createError) {
const { error: sendError } = await signIn.emailCode.sendCode()
if (sendError) {
console.error(JSON.stringify(sendError, null, 2))
return
}
setVerifying(true)
}
}
// Step 2: Verification step
const handleVerify = async () => {
const { error } = await signIn.emailCode.verifyCode({ code })
// When the user doesn't exist, verifyCode returns an error with
// the code 'sign_up_if_missing_transfer'. Check for this error
// to determine if we need to transfer to sign-up.
if (error) {
if (error.errors[0]?.code === 'sign_up_if_missing_transfer') {
// The user doesn't exist - transfer to sign-up
await handleTransfer()
return
}
// Some other error occurred
console.error(JSON.stringify(error, null, 2))
return
}
// The user exists and verification succeeded
if (signIn.status === 'complete') {
await finalizeSignIn()
} else if (signIn.status === 'needs_second_factor') {
// Handle MFA if required
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
} else if (signIn.status === 'needs_client_trust') {
// Handle client trust if required
// See https://clerk.com/docs/guides/development/custom-flows/authentication/client-trust
} else {
console.error('Sign-in attempt not complete:', signIn.status)
}
}
// Step 3: Transfer to sign-up
const handleTransfer = async () => {
// Create sign-up using transfer.
// This moves the verified identification from the sign-in to a new sign-up.
const { error } = await signUp.create({ transfer: true })
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signUp.status === 'complete') {
// No additional requirements - sign-up is complete
await finalizeSignUp()
} else if (signUp.status === 'missing_requirements') {
// Additional fields are required to complete sign-up.
// Common missing fields include legal_accepted, first_name, last_name, etc.
// Show a form to collect the missing fields.
setShowMissingRequirements(true)
} else {
// Check why the sign-up is not complete
console.error('Unexpected sign-up status:', signUp.status)
}
}
// Step 4: Submit missing requirements to complete sign-up
const handleMissingRequirements = async () => {
// This example handles legal acceptance as an example.
// You can extend this to handle other missing fields like first_name, last_name, etc.
// by checking signUp.missingFields and collecting the appropriate values.
const { error } = await signUp.update({
legalAccepted: true,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signUp.status === 'complete') {
await finalizeSignUp()
} else if (signUp.status === 'missing_requirements') {
// Still missing other fields
console.error('Additional fields still required:', signUp.missingFields)
} else {
console.error('Unexpected sign-up status:', signUp.status)
}
}
// Step 4 UI: Show missing requirements form
if (showMissingRequirements) {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Complete your account
</ThemedText>
<ThemedText style={styles.body}>
Your email has been verified. Please complete the following to create your account.
</ThemedText>
{signUp.missingFields.includes('legal_accepted') && (
<View style={styles.legalRow}>
<Switch value={legalAccepted} onValueChange={setLegalAccepted} />
<ThemedText style={styles.legalLabel}>
I agree to the Terms of Service and Privacy Policy
</ThemedText>
</View>
)}
<Pressable
style={({ pressed }) => [
styles.button,
(fetchStatus === 'fetching' ||
(signUp.missingFields.includes('legal_accepted') && !legalAccepted)) &&
styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleMissingRequirements}
disabled={
fetchStatus === 'fetching' ||
(signUp.missingFields.includes('legal_accepted') && !legalAccepted)
}
>
<ThemedText style={styles.buttonText}>Create account</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
onPress={() => signIn.reset()}
>
<ThemedText style={styles.secondaryButtonText}>Start over</ThemedText>
</Pressable>
</ThemedView>
)
}
// Step 2 UI: Show verification code form
if (verifying) {
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Verify your email
</ThemedText>
<View style={styles.verifyIntro}>
<ThemedText style={styles.body}>We sent a verification code to </ThemedText>
<ThemedText type="defaultSemiBold" style={styles.body}>
{emailAddress}
</ThemedText>
</View>
<ThemedText style={styles.label}>Verification code</ThemedText>
<TextInput
style={styles.input}
value={code}
placeholder="Enter code"
placeholderTextColor="#666666"
onChangeText={setCode}
keyboardType="number-pad"
autoCapitalize="none"
/>
{errors.fields.code && (
<ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
(!code || fetchStatus === 'fetching') && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleVerify}
disabled={!code || fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Verify</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
onPress={() => signIn.emailCode.sendCode()}
>
<ThemedText style={styles.secondaryButtonText}>Resend code</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
onPress={() => signIn.reset()}
>
<ThemedText style={styles.secondaryButtonText}>Start over</ThemedText>
</Pressable>
</ThemedView>
)
}
// Step 1 UI: Show email input form
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Sign in or sign up
</ThemedText>
<ThemedText style={styles.label}>Email address</ThemedText>
<TextInput
style={styles.input}
autoCapitalize="none"
value={emailAddress}
placeholder="Enter email"
placeholderTextColor="#666666"
onChangeText={setEmailAddress}
keyboardType="email-address"
/>
{errors.fields.identifier && (
<ThemedText style={styles.error}>{errors.fields.identifier.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
(!emailAddress || fetchStatus === 'fetching') && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleSubmit}
disabled={!emailAddress || fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Continue</ThemedText>
</Pressable>
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}
{/* Required for sign-up flows. Clerk's bot sign-up protection is enabled by default */}
<View nativeID="clerk-captcha" style={styles.captchaContainer} />
<View style={styles.linkContainer}>
<ThemedText>{"Don't have an account? "}</ThemedText>
<Link href="/sign-up">
<ThemedText type="link">Sign up</ThemedText>
</Link>
</View>
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
body: {
fontSize: 16,
lineHeight: 24,
},
verifyIntro: {
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'baseline',
marginBottom: 4,
},
label: {
fontWeight: '600',
fontSize: 14,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
secondaryButton: {
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
secondaryButtonText: {
color: '#0a7ea4',
fontWeight: '600',
},
linkContainer: {
flexDirection: 'row',
gap: 4,
marginTop: 12,
alignItems: 'center',
},
error: {
color: '#d32f2f',
fontSize: 12,
marginTop: -8,
},
debug: {
fontSize: 10,
opacity: 0.5,
marginTop: 8,
},
captchaContainer: {
minHeight: 1,
},
legalRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
marginTop: 8,
},
legalLabel: {
flex: 1,
fontSize: 14,
},
})Feedback
Last updated on