Skip to main content
Docs

Build a custom sign-in flow with client trust

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt components.

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt views.

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.

If you have Client Trust enabled for your application, when a user is signing in with a password on a new client (e.g. device), the sign-in attempt will return a status of needs_client_trust. Your custom sign-in flow needs to support handling either an email code or SMS code, depending on the settings you've enabled in the Clerk Dashboard.

If Client Trust and MFA are enabled, MFA will take precedence and the sign-in attempt will return a status of needs_second_factor. See the MFA custom flow guide for more information.

Configure application settings

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.

  1. In the Clerk Dashboard, navigate to the User & authentication page.
  2. Enable Sign-up with email.
    • Require email address should be enabled.
    • For Verify at sign-up, Email verification code is enabled by default. This is the option you'll want for this example.
  3. Enable Sign in with email.
  4. Select the Password tab and enable Sign-up with password.

Build the custom flow

Tip

Examples for this SDK aren't available yet. For now, try adapting the available example to fit your SDK.

app/sign-in/page.tsx
'use client'

import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'

export default function Page() {
  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,
    })

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          // Handle session tasks
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
          if (session?.currentTask) {
            console.log(session?.currentTask)
            return
          }

          // If no session tasks, navigate the signed-in user to the home page
          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') {
      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 (formData: FormData) => {
    const code = formData.get('code') as string

    await signIn.mfa.verifyEmailCode({ code })

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          // Handle session tasks
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
          if (session?.currentTask) {
            console.log(session?.currentTask)
            return
          }

          // If no session tasks, navigate the signed-in user to the home page
          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url)
          }
        },
      })
    }
  }

  if (signIn.status === 'needs_client_trust') {
    return (
      <>
        <h1>Verify your account</h1>
        <form action={handleVerify}>
          <div>
            <label htmlFor="code">Code</label>
            <input id="code" name="code" type="text" />
            {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>
        {/* 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>}
      </>
    )
  }

  return (
    <>
      <h1>Sign in</h1>
      <form action={handleSubmit}>
        <div>
          <label htmlFor="email">Enter email address</label>
          <input id="email" name="email" type="email" />
          {errors.fields.identifier && <p>{errors.fields.identifier.message}</p>}
        </div>
        <div>
          <label htmlFor="password">Enter password</label>
          <input id="password" name="password" type="password" />
          {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>}
    </>
  )
}
app/(auth)/sign-in.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn } 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 router = useRouter()

  const [emailAddress, setEmailAddress] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [code, setCode] = React.useState('')

  const handleSubmit = async () => {
    const { error } = await signIn.password({
      emailAddress,
      password,
    })
    if (error) {
      console.error(JSON.stringify(error, null, 2))
      return
    }

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          // Handle session tasks
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
          if (session?.currentTask) {
            console.log(session?.currentTask)
            return
          }

          // If no session tasks, navigate the signed-in user to the home page
          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url as Href)
          }
        },
      })
    } 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') {
      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 () => {
    await signIn.mfa.verifyEmailCode({ code })

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          // Handle session tasks
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
          if (session?.currentTask) {
            console.log(session?.currentTask)
            return
          }

          // If no session tasks, navigate the signed-in user to the home page
          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:', signIn)
    }
  }

  if (signIn.status === 'needs_client_trust') {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Verify your account
        </ThemedText>
        <TextInput
          style={styles.input}
          value={code}
          placeholder="Enter your verification code"
          placeholderTextColor="#666666"
          onChangeText={(code) => setCode(code)}
          keyboardType="numeric"
        />
        {errors.fields.code && (
          <ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
        )}
        <Pressable
          style={({ pressed }) => [
            styles.button,
            fetchStatus === 'fetching' && styles.buttonDisabled,
            pressed && styles.buttonPressed,
          ]}
          onPress={handleVerify}
          disabled={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>
      </ThemedView>
    )
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Sign in
      </ThemedText>
      <ThemedText style={styles.label}>Email address</ThemedText>
      <TextInput
        style={styles.input}
        autoCapitalize="none"
        value={emailAddress}
        placeholder="Enter email"
        placeholderTextColor="#666666"
        onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
        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={true}
        onChangeText={(password) => setPassword(password)}
      />
      {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>}

      <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,
  },
})
EmailPasswordSignInView.swift
import SwiftUI
import ClerkKit

struct EmailPasswordSignInView: View {
@Environment(Clerk.self) private var clerk
@State private var email = ""
@State private var password = ""
@State private var code = ""
@State private var showEmailCode = false

var body: some View {
   if showEmailCode {
     Text("Verify your email")
     Text("A verification code has been sent to your email.")
     TextField("Enter verification code", text: $code)
     Button("Verify") {
       Task { await verify(code: code) }
     }
  } else {
    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 sign-in with email/password
    var signIn = try await clerk.auth.signInWithPassword(
      identifier: email,
      password: password
    )

    switch signIn.status {
    case .complete:
      dump(clerk.session)
    case .needsSecondFactor:
      // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
      dump(signIn.status)
     case .needsClientTrust:
       signIn = try await signIn.sendMfaEmailCode()
       showEmailCode = true
    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)
}
}

 func verify(code: String) async {
   do {
     // Verify the email code
     guard var signIn = clerk.auth.currentSignIn else { return }

     signIn = try await signIn.verifyMfaCode(code, type: .emailCode)

      switch signIn.status {
      case .complete:
        dump(clerk.session)
      default:
        dump(signIn.status)
      }
  } catch {
    // See https://clerk.com/docs/guides/development/custom-flows/error-handling
    // for more info on error handling.
    dump(error)
  }
}
}
EmailPasswordSignInViewModel.kt
package com.clerk.customflows.emailpassword.signin

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.auth.types.MfaType
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<EmailPasswordSignInUiState>(EmailPasswordSignInUiState.Loading)
val uiState = _uiState.asStateFlow()

init {
combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
  Log.e("EmailPasswordSignInViewModel", "combine: $user, $isInitialized")
  _uiState.value =
    when {
      !isInitialized -> EmailPasswordSignInUiState.Loading
      user != null -> EmailPasswordSignInUiState.SignedIn
       _uiState.value == EmailPasswordSignInUiState.NeedsClientTrust ->
         EmailPasswordSignInUiState.NeedsClientTrust
      else -> EmailPasswordSignInUiState.SignedOut
    }
}
.launchIn(viewModelScope)
}

fun submit(email: String, password: String) {
viewModelScope.launch {
Clerk.auth
  .signInWithPassword {
    identifier = email
    this.password = password
  }
  .onSuccess { signIn ->
    when (signIn.status) {
      SignIn.Status.COMPLETE -> {
        _uiState.value = EmailPasswordSignInUiState.SignedIn
      }
      SignIn.Status.NEEDS_SECOND_FACTOR -> {
        // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
       }
       SignIn.Status.NEEDS_CLIENT_TRUST -> {
         val emailFactor =
           signIn.supportedSecondFactors.orEmpty().firstOrNull { it.strategy == "email_code" }
         if (emailFactor != null) {
           signIn
             .sendMfaEmailCode(emailAddressId = emailFactor.emailAddressId)
             .onSuccess { _uiState.value = EmailPasswordSignInUiState.NeedsClientTrust }
             .onFailure {
               // See https://clerk.com/docs/guides/development/custom-flows/error-handling
               // for more info on error handling
             }
        } else {
          Log.e(
            "EmailPasswordSignInViewModel",
            "Sign-in attempt not complete: ${signIn.status}",
          )
        }
      }
      else -> {
        Log.e(
          "EmailPasswordSignInViewModel",
          "Sign-in attempt not complete: ${signIn.status}",
        )
      }
    }
  }
  .onFailure {
    // See https://clerk.com/docs/guides/development/custom-flows/error-handling
    // for more info on error handling
  }
}
}

fun resendClientTrustCode() {
val signIn = Clerk.auth.currentSignIn ?: return
viewModelScope.launch {
  signIn
    .sendMfaEmailCode()
    .onFailure {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
    }
}
}

fun verifyClientTrust(code: String) {
val signIn = Clerk.auth.currentSignIn ?: return
viewModelScope.launch {
  signIn
    .verifyMfaCode(code, MfaType.EMAIL_CODE)
    .onSuccess { updated ->
      if (updated.status == SignIn.Status.COMPLETE) {
        _uiState.value = EmailPasswordSignInUiState.SignedIn
      } else {
        Log.e(
          "EmailPasswordSignInViewModel",
          "Sign-in attempt not complete: ${updated.status}",
        )
      }
    }
    .onFailure {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
    }
}
}

sealed interface EmailPasswordSignInUiState {
data object Loading : EmailPasswordSignInUiState

data object SignedOut : EmailPasswordSignInUiState

data object NeedsClientTrust : EmailPasswordSignInUiState

data object SignedIn : EmailPasswordSignInUiState
}
}
EmailPasswordSignInActivity.kt
package com.clerk.customflows.emailpassword.signin

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
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,
   onVerifyClientTrust = viewModel::verifyClientTrust,
   onResendClientTrustCode = viewModel::resendClientTrustCode,
)
}
}
}

@Composable
fun EmailPasswordSignInView(
state: EmailPasswordSignInViewModel.EmailPasswordSignInUiState,
onSubmit: (String, String) -> Unit,
onVerifyClientTrust: (String) -> Unit,
onResendClientTrustCode: () -> Unit,
) {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var clientTrustCode by remember { mutableStateOf("") }

Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
when (state) {
EmailPasswordSignInViewModel.EmailPasswordSignInUiState.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.EmailPasswordSignInUiState.NeedsClientTrust -> {
   Column(
     verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
     horizontalAlignment = Alignment.CenterHorizontally,
   ) {
     Text("Verify your account")
     TextField(
       value = clientTrustCode,
       onValueChange = { clientTrustCode = it },
       label = { Text("Code") },
     )
     Button(onClick = { onVerifyClientTrust(clientTrustCode) }) { Text("Verify") }
     Button(onClick = onResendClientTrustCode) { Text("I need a new code") }
   }
 }
EmailPasswordSignInViewModel.EmailPasswordSignInUiState.SignedIn -> {
  Text("Current session: ${Clerk.session?.id}")
}

EmailPasswordSignInViewModel.EmailPasswordSignInUiState.Loading ->
  Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    CircularProgressIndicator()
  }
}
}
}

Configure application settings

Client Trust will fallback to SMS verification code only if email is completely disabled for the application, or else Client Trust will fallback to either email code or email link, depending on your application's settings.

  1. In the Clerk Dashboard, navigate to the User & authentication page.
  2. Ensure all email options are disabled.
  3. Select the Phone tab. Ensure all the settings are enabled.
  4. Select the Password tab and enable Sign-up with password. Client Trust is enabled by default.

Build the custom flow

Tip

Examples for this SDK aren't available yet. For now, try adapting the available example to fit your SDK.

app/sign-in/page.tsx
'use client'

import { useSignIn } from '@clerk/nextjs'
import { PhoneCodeFactor } from '@clerk/nextjs/types'
import { useRouter } from 'next/navigation'

export default function Page() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const router = useRouter()

  const handleSubmit = async (formData: FormData) => {
    const phoneNumber = formData.get('phoneNumber') as string
    const password = formData.get('password') as string

    await signIn.password({
      phoneNumber,
      password,
    })

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          // Handle session tasks
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
          if (session?.currentTask) {
            console.log(session?.currentTask)
            return
          }

          // If no session tasks, navigate the signed-in user to the home page
          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') {
      const phoneCodeFactor = signIn.supportedSecondFactors.find(
        (factor): factor is PhoneCodeFactor => factor.strategy === 'phone_code',
      )

      if (phoneCodeFactor) {
        await signIn.mfa.sendPhoneCode()
      }
    } else {
      // Check why the sign-in is not complete
      console.error('Sign-in attempt not complete:', signIn)
    }
  }

  const handleVerify = async (formData: FormData) => {
    const code = formData.get('code') as string

    await signIn.mfa.verifyPhoneCode({ code })

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          // Handle session tasks
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
          if (session?.currentTask) {
            console.log(session?.currentTask)
            return
          }

          // If no session tasks, navigate the signed-in user to the home page
          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url)
          }
        },
      })
    }
  }

  if (signIn.status === 'needs_client_trust') {
    return (
      <>
        <h1>Verify your account</h1>
        <form action={handleVerify}>
          <div>
            <label htmlFor="code">Code</label>
            <input id="code" name="code" type="text" />
            {errors.fields.code && <p>{errors.fields.code.message}</p>}
          </div>
          <button type="submit" disabled={fetchStatus === 'fetching'}>
            Verify
          </button>
        </form>
        <button onClick={() => signIn.mfa.sendPhoneCode()}>I need a new code</button>
        <button onClick={() => signIn.reset()}>Start over</button>
        {/* 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>}
      </>
    )
  }

  return (
    <>
      <h1>Sign in</h1>
      <form action={handleSubmit}>
        <div>
          <label htmlFor="phoneNumber">Enter phone number</label>
          <input id="phoneNumber" name="phoneNumber" type="tel" />
          {errors.fields.identifier && <p>{errors.fields.identifier.message}</p>}
        </div>
        <div>
          <label htmlFor="password">Enter password</label>
          <input id="password" name="password" type="password" />
          {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>}
    </>
  )
}
app/(auth)/sign-in.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { Colors } from '@/constants/theme'
import { useColorScheme } from '@/hooks/use-color-scheme'
import { useSignIn } from '@clerk/expo'
import { type Href, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, TextInput } from 'react-native'

export default function Page() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const router = useRouter()
  const colorScheme = useColorScheme() ?? 'light'
  const themeColors = Colors[colorScheme]

  const [phoneNumber, setPhoneNumber] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [code, setCode] = React.useState('')

  const handleSubmit = async () => {
    await signIn.password({
      phoneNumber,
      password,
    })

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          // Handle session tasks
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
          if (session?.currentTask) {
            console.log(session?.currentTask)
            return
          }

          // If no session tasks, navigate the signed-in user to the home page
          const url = decorateUrl('/')
          if (url.startsWith('http')) {
            window.location.href = url
          } else {
            router.push(url as Href)
          }
        },
      })
    } 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') {
      const phoneCodeFactor = signIn.supportedSecondFactors.find(
        (factor): factor is PhoneCodeFactor => factor.strategy === 'phone_code',
      )

      if (phoneCodeFactor) {
        await signIn.mfa.sendPhoneCode()
      }
    } else {
      // Check why the sign-in is not complete
      console.error('Sign-in attempt not complete:', signIn)
    }
  }

  const handleVerify = async () => {
    await signIn.mfa.verifyPhoneCode({ code })

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          // Handle session tasks
          // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
          if (session?.currentTask) {
            console.log(session?.currentTask)
            return
          }

          // If no session tasks, navigate the signed-in user to the home page
          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:', signIn)
    }
  }

  if (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,
            { backgroundColor: themeColors.background, borderColor: themeColors.icon },
          ]}
          value={code}
          placeholder="Enter code"
          placeholderTextColor={themeColors.icon}
          onChangeText={(code) => setCode(code)}
          keyboardType="numeric"
        />
        {errors.fields.code && (
          <ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
        )}
        <Pressable
          style={({ pressed }) => [
            styles.button,
            fetchStatus === 'fetching' && styles.buttonDisabled,
            pressed && styles.buttonPressed,
          ]}
          onPress={handleVerify}
          disabled={fetchStatus === 'fetching'}
        >
          <ThemedText style={styles.buttonText}>Verify</ThemedText>
        </Pressable>
        <Pressable
          style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
          onPress={() => signIn.mfa.sendPhoneCode()}
        >
          <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>
        {errors && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}
      </ThemedView>
    )
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Sign in
      </ThemedText>
      <ThemedText style={styles.label}>Enter phone number</ThemedText>
      <TextInput
        style={[
          styles.input,
          { backgroundColor: themeColors.background, borderColor: themeColors.icon },
        ]}
        value={phoneNumber}
        placeholder="Enter phone number"
        placeholderTextColor={themeColors.icon}
        onChangeText={(phoneNumber) => setPhoneNumber(phoneNumber)}
        keyboardType="phone-pad"
      />
      {errors.fields.identifier && (
        <ThemedText style={styles.error}>{errors.fields.identifier.message}</ThemedText>
      )}
      <ThemedText style={styles.label}>Enter password</ThemedText>
      <TextInput
        style={[
          styles.input,
          { backgroundColor: themeColors.background, borderColor: themeColors.icon },
        ]}
        value={password}
        placeholder="Enter password"
        placeholderTextColor={themeColors.icon}
        secureTextEntry
        onChangeText={(password) => setPassword(password)}
      />
      {errors.fields.password && (
        <ThemedText style={styles.error}>{errors.fields.password.message}</ThemedText>
      )}
      <Pressable
        style={({ pressed }) => [
          styles.button,
          fetchStatus === 'fetching' && styles.buttonDisabled,
          pressed && styles.buttonPressed,
        ]}
        onPress={handleSubmit}
        disabled={fetchStatus === 'fetching'}
      >
        <ThemedText style={styles.buttonText}>Continue</ThemedText>
      </Pressable>
      {errors && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 12,
  },
  title: {
    marginBottom: 8,
  },
  label: {
    fontWeight: '600',
    fontSize: 14,
  },
  input: {
    borderWidth: 1,
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
  },
  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',
  },
  error: {
    color: '#d32f2f',
    fontSize: 12,
    marginTop: -8,
  },
  debug: {
    fontSize: 10,
    opacity: 0.5,
    marginTop: 8,
  },
})
PhonePasswordSignInView.swift
import SwiftUI
import ClerkKit

struct PhonePasswordSignInView: View {
@Environment(Clerk.self) private var clerk
@State private var phoneNumber = ""
@State private var password = ""
@State private var code = ""
@State private var showPhoneCode = false

var body: some View {
  if showPhoneCode {
    Text("Verify your account")
    Text("A verification code has been sent to your phone.")
    TextField("Enter verification code", text: $code)
    Button("Verify") {
      Task { await verify(code: code) }
    }
    Button("I need a new code") {
      Task { await resendPhoneCode() }
    }
  } else {
    TextField("Enter phone number", text: $phoneNumber)
    SecureField("Enter password", text: $password)
    Button("Continue") {
      Task { await submit(phoneNumber: phoneNumber, password: password) }
    }
  }
}
}

extension PhonePasswordSignInView {

func submit(phoneNumber: String, password: String) async {
  do {
    var signIn = try await clerk.auth.signInWithPassword(
      identifier: phoneNumber,
      password: password
    )

    switch signIn.status {
    case .complete:
      dump(clerk.session)
    case .needsSecondFactor:
      // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
      dump(signIn.status)
     case .needsClientTrust:
       signIn = try await signIn.sendMfaPhoneCode()
       showPhoneCode = true
    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)
  }
}

 func verify(code: String) async {
   do {
     guard var signIn = clerk.auth.currentSignIn else { return }

     signIn = try await signIn.verifyMfaCode(code, type: .phoneCode)

     switch signIn.status {
     case .complete:
       dump(clerk.session)
     default:
       dump(signIn.status)
     }
   } catch {
     // See https://clerk.com/docs/guides/development/custom-flows/error-handling
     // for more info on error handling.
     dump(error)
   }
 }

 func resendPhoneCode() async {
   do {
     guard var signIn = clerk.auth.currentSignIn else { return }
     signIn = try await signIn.sendMfaPhoneCode()
   } catch {
     // See https://clerk.com/docs/guides/development/custom-flows/error-handling
     // for more info on error handling.
     dump(error)
   }
 }
}
PhonePasswordSignInViewModel.kt
package com.clerk.customflows.phonepassword.signin

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.auth.types.MfaType
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 PhonePasswordSignInViewModel : ViewModel() {
private val _uiState =
MutableStateFlow<PhonePasswordSignInUiState>(PhonePasswordSignInUiState.Loading)
val uiState = _uiState.asStateFlow()

init {
combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
  Log.e("PhonePasswordSignInViewModel", "combine: $user, $isInitialized")
  _uiState.value =
    when {
      !isInitialized -> PhonePasswordSignInUiState.Loading
      user != null -> PhonePasswordSignInUiState.SignedIn
       _uiState.value == PhonePasswordSignInUiState.NeedsClientTrust ->
         PhonePasswordSignInUiState.NeedsClientTrust
      else -> PhonePasswordSignInUiState.SignedOut
    }
}
.launchIn(viewModelScope)
}

fun submit(phoneNumber: String, password: String) {
viewModelScope.launch {
Clerk.auth
  .signInWithPassword {
    identifier = phoneNumber
    this.password = password
  }
  .onSuccess { signIn ->
    when (signIn.status) {
      SignIn.Status.COMPLETE -> {
        _uiState.value = PhonePasswordSignInUiState.SignedIn
      }
      SignIn.Status.NEEDS_SECOND_FACTOR -> {
        // See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
      }
       SignIn.Status.NEEDS_CLIENT_TRUST -> {
         val phoneFactor =
           signIn.supportedSecondFactors.orEmpty().firstOrNull { it.strategy == "phone_code" }
         if (phoneFactor != null) {
           signIn
             .sendMfaPhoneCode(phoneNumberId = phoneFactor.phoneNumberId)
             .onSuccess { _uiState.value = PhonePasswordSignInUiState.NeedsClientTrust }
             .onFailure {
               // See https://clerk.com/docs/guides/development/custom-flows/error-handling
               // for more info on error handling
             }
        } else {
          Log.e(
            "PhonePasswordSignInViewModel",
            "Sign-in attempt not complete: ${signIn.status}",
          )
        }
      }
      else -> {
        Log.e(
          "PhonePasswordSignInViewModel",
          "Sign-in attempt not complete: ${signIn.status}",
        )
      }
    }
  }
  .onFailure {
    // See https://clerk.com/docs/guides/development/custom-flows/error-handling
    // for more info on error handling
  }
}
}

fun resendClientTrustCode() {
val signIn = Clerk.auth.currentSignIn ?: return
viewModelScope.launch {
  signIn
    .sendMfaPhoneCode()
    .onFailure {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
    }
}
}

fun verifyClientTrust(code: String) {
val signIn = Clerk.auth.currentSignIn ?: return
viewModelScope.launch {
  signIn
    .verifyMfaCode(code, MfaType.PHONE_CODE)
    .onSuccess { updated ->
      if (updated.status == SignIn.Status.COMPLETE) {
        _uiState.value = PhonePasswordSignInUiState.SignedIn
      } else {
        Log.e(
          "PhonePasswordSignInViewModel",
          "Sign-in attempt not complete: ${updated.status}",
        )
      }
    }
    .onFailure {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
    }
}
}

sealed interface PhonePasswordSignInUiState {
data object Loading : PhonePasswordSignInUiState

data object SignedOut : PhonePasswordSignInUiState

data object NeedsClientTrust : PhonePasswordSignInUiState

data object SignedIn : PhonePasswordSignInUiState
}
}
PhonePasswordSignInActivity.kt
package com.clerk.customflows.phonepassword.signin

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
import com.clerk.api.Clerk

class PhonePasswordSignInActivity : ComponentActivity() {

val viewModel: PhonePasswordSignInViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val state by viewModel.uiState.collectAsStateWithLifecycle()
PhonePasswordSignInView(
  state = state,
  onSubmit = viewModel::submit,
   onVerifyClientTrust = viewModel::verifyClientTrust,
   onResendClientTrustCode = viewModel::resendClientTrustCode,
)
}
}
}

@Composable
fun PhonePasswordSignInView(
state: PhonePasswordSignInViewModel.PhonePasswordSignInUiState,
onSubmit: (String, String) -> Unit,
onVerifyClientTrust: (String) -> Unit,
onResendClientTrustCode: () -> Unit,
) {
var phoneNumber by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var clientTrustCode by remember { mutableStateOf("") }

Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
when (state) {
PhonePasswordSignInViewModel.PhonePasswordSignInUiState.SignedOut -> {
  Column(
    verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
    horizontalAlignment = Alignment.CenterHorizontally,
  ) {
    TextField(
      value = phoneNumber,
      onValueChange = { phoneNumber = it },
      label = { Text("Phone number") },
    )
    TextField(
      value = password,
      onValueChange = { password = it },
      visualTransformation = PasswordVisualTransformation(),
      label = { Text("Password") },
    )
    Button(onClick = { onSubmit(phoneNumber, password) }) { Text("Continue") }
  }
}
 PhonePasswordSignInViewModel.PhonePasswordSignInUiState.NeedsClientTrust -> {
   Column(
     verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
     horizontalAlignment = Alignment.CenterHorizontally,
   ) {
     Text("Verify your account")
     Text("A verification code has been sent to your phone.")
     TextField(
       value = clientTrustCode,
       onValueChange = { clientTrustCode = it },
       label = { Text("Code") },
     )
     Button(onClick = { onVerifyClientTrust(clientTrustCode) }) { Text("Verify") }
     Button(onClick = onResendClientTrustCode) { Text("I need a new code") }
   }
 }
PhonePasswordSignInViewModel.PhonePasswordSignInUiState.SignedIn -> {
  Text("Current session: ${Clerk.session?.id}")
}

PhonePasswordSignInViewModel.PhonePasswordSignInUiState.Loading ->
  Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    CircularProgressIndicator()
  }
}
}
}

Feedback

What did you think of this content?

Last updated on