Skip to main content
Docs

Build a custom email/password authentication flow

Warning

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

This guide will walk you through how to build a custom email and 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. Enable Sign-up with email.
    • 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 email links custom flow.
  2. Enable Sign in with email.
  3. Select the Password tab and enable Sign-up with password.

Sign-up flow

This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.

  1. Initiate the sign-up process by passing the user's email address and password to the SignUp.create() method.
  2. To verify the user's email address, send a one-time code to the provided email address with the SignUp.prepareEmailAddressVerification() method.
  3. Collect the user's one-time code and verify it with the SignUp.attemptEmailAddressVerification() method.
  4. If the email address verification is successful, the SignUp.status will be complete, and you can finish the sign-up flow by calling the setActive() method, which will set the newly created session as the active session. You may need to check for that are required for the user to complete after signing up.
app/sign-up/[[...sign-up]]/page.tsx
'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 <div>Loading...</div>

    // 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 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 <div>Loading...</div>

    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) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/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('Sign-up attempt not complete:', signUpAttempt)
        console.error('Sign-up attempt status:', signUpAttempt.status)
      }
    } 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))
    }
  }

  // Display the verification form to capture the code
  if (verifying) {
    return (
      <>
        <h1>Verify your email</h1>
        <form onSubmit={handleVerify}>
          <label id="code">Enter your verification code</label>
          <input value={code} id="code" name="code" onChange={(e) => setCode(e.target.value)} />
          <button type="submit">Verify</button>
        </form>
      </>
    )
  }

  // Display the initial sign-up form to capture the email and password
  return (
    <>
      <h1>Sign up</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Enter email address</label>
          <input
            id="email"
            type="email"
            name="email"
            value={emailAddress}
            onChange={(e) => setEmailAddress(e.target.value)}
          />
        </div>

        <div>
          <label htmlFor="password">Enter password</label>
          <input
            id="password"
            type="password"
            name="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>

        {/* Required for sign-up flows
        Clerk's bot sign-up protection is enabled by default */}
        <div id="clerk-captcha" />

        <div>
          <button type="submit">Continue</button>
        </div>
      </form>
    </>
  )
}
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App</title>
  </head>
  <body>
    <div id="signed-in"></div>

    <div id="sign-up">
      <h2>Sign up</h2>
      <form id="sign-up-form">
        <label for="email">Enter email address</label>
        <input type="email" name="email" id="sign-up-email" />
        <label for="password">Enter password</label>
        <input type="password" name="password" id="sign-up-password" />
        <button type="submit">Continue</button>
      </form>
    </div>

    <form id="verifying" hidden>
      <h2>Verify your email</h2>
      <label for="code">Enter your verification code</label>
      <input id="code" name="code" />
      <button type="submit" id="verify-button">Verify</button>
    </form>

    <script type="module" src="/src/main.js" async crossorigin="anonymous"></script>
  </body>
</html>
main.js
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 = `
    <div id="user-button"></div>
  `

  const userbuttonDiv = document.getElementById('user-button')

  clerk.mountUserButton(userbuttonDiv)
} else {
  // Handle the sign-up form
  document.getElementById('sign-up-form').addEventListener('submit', async (e) => {
    e.preventDefault()

    const formData = new FormData(e.target)
    const emailAddress = formData.get('email')
    const password = formData.get('password')

    try {
      // Start the sign-up process using the email and password provided
      await clerk.client.signUp.create({ emailAddress, password })
      await clerk.client.signUp.prepareEmailAddressVerification()
      // Hide sign-up form
      document.getElementById('sign-up').setAttribute('hidden', '')
      // Show verification form
      document.getElementById('verifying').removeAttribute('hidden')
    } catch (error) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error(error)
    }
  })

  // Handle the verification form
  document.getElementById('verifying').addEventListener('submit', async (e) => {
    const formData = new FormData(e.target)
    const code = formData.get('code')

    try {
      // Use the code the user provided to attempt verification
      const signUpAttempt = await clerk.client.signUp.attemptEmailAddressVerification({
        code,
      })

      // Now that the user is created, set the session to active.
      await clerk.setActive({ session: signUpAttempt.createdSessionId })
    } catch (error) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error(error)
    }
  })
}

In the (auth) group, create a sign-up.tsx file with the following code. The useSignUp() hook is used to create a sign-up flow. The user can sign up using their email and password and will receive an email verification code to confirm their email.

app/(auth)/sign-up.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignUp } from '@clerk/clerk-expo'
import { Link, useRouter } from 'expo-router'
import * as React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'

export default function Page() {
  const { isLoaded, signUp, setActive } = useSignUp()
  const router = useRouter()

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

  // Handle submission of sign-up form
  const onSignUpPress = async () => {
    if (!isLoaded) return

    // Start sign-up process using email and password provided
    try {
      await signUp.create({
        emailAddress,
        password,
      })

      // Send user an email with verification code
      await signUp.prepareEmailAddressVerification({ strategy: 'email_code' })

      // Set 'pendingVerification' to true to display second form
      // and capture code
      setPendingVerification(true)
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }

  // Handle submission of verification form
  const onVerifyPress = async () => {
    if (!isLoaded) return

    try {
      // Use the code the user provided to attempt verification
      const signUpAttempt = await signUp.attemptEmailAddressVerification({
        code,
      })

      // If verification was completed, set the session to active
      // and redirect the user
      if (signUpAttempt.status === 'complete') {
        await setActive({
          session: signUpAttempt.createdSessionId,
          navigate: async ({ session }) => {
            if (session?.currentTask) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            router.replace('/')
          },
        })
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(signUpAttempt, null, 2))
      }
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }

  if (pendingVerification) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Verify your email
        </ThemedText>
        <ThemedText style={styles.description}>
          A verification code has been sent to your email.
        </ThemedText>
        <TextInput
          style={styles.input}
          value={code}
          placeholder="Enter your verification code"
          placeholderTextColor="#666666"
          onChangeText={(code) => setCode(code)}
          keyboardType="numeric"
        />
        <Pressable
          style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
          onPress={onVerifyPress}
        >
          <ThemedText style={styles.buttonText}>Verify</ThemedText>
        </Pressable>
      </ThemedView>
    )
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Sign up
      </ThemedText>
      <ThemedText style={styles.label}>Email address</ThemedText>
      <TextInput
        style={styles.input}
        autoCapitalize="none"
        value={emailAddress}
        placeholder="Enter email"
        placeholderTextColor="#666666"
        onChangeText={(email) => setEmailAddress(email)}
        keyboardType="email-address"
      />
      <ThemedText style={styles.label}>Password</ThemedText>
      <TextInput
        style={styles.input}
        value={password}
        placeholder="Enter password"
        placeholderTextColor="#666666"
        secureTextEntry={true}
        onChangeText={(password) => setPassword(password)}
      />
      <Pressable
        style={({ pressed }) => [
          styles.button,
          (!emailAddress || !password) && styles.buttonDisabled,
          pressed && styles.buttonPressed,
        ]}
        onPress={onSignUpPress}
        disabled={!emailAddress || !password}
      >
        <ThemedText style={styles.buttonText}>Continue</ThemedText>
      </Pressable>
      <View style={styles.linkContainer}>
        <ThemedText>Have an account? </ThemedText>
        <Link href="/sign-in">
          <ThemedText type="link">Sign in</ThemedText>
        </Link>
      </View>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 12,
  },
  title: {
    marginBottom: 8,
  },
  description: {
    fontSize: 14,
    marginBottom: 16,
    opacity: 0.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',
  },
  linkContainer: {
    flexDirection: 'row',
    gap: 4,
    marginTop: 12,
    alignItems: 'center',
  },
})
  1. Initiate the sign-up process by passing the user's email address and password to the auth.signUp()iOS Icon method.
  2. To verify the user's email address, send a one-time code to the provided email address with the signUp.sendEmailCode() method.
  3. Collect the user's one-time code and verify it with the signUp.verifyEmailCode() method.
  4. If the email address verification is successful, the SignUp.status will be complete. You may need to check for that are required for the user to complete after signing up.
EmailPasswordSignUpView.swift
  import SwiftUI
  import ClerkKit

  struct EmailPasswordSignUpView: View {
    @Environment(Clerk.self) private var clerk
    @State private var email = ""
    @State private var password = ""
    @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 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 sign-up with email/password
        var signUp = try await clerk.auth.signUp(
          emailAddress: email,
          password: password
        )

        // Send the email verification code
        signUp = try await signUp.sendEmailCode()

        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 {
        // Verify the email code
        guard var signUp = clerk.auth.currentSignUp else { return }

        signUp = try await signUp.verifyEmailCode(code)

        switch signUp.status {
        case .complete:
          dump(clerk.session)
        default:
          dump(signUp.status)
        }
      } catch {
        // See https://clerk.com/docs/guides/development/custom-flows/error-handling
        // for more info on error handling
        dump(error)
      }
    }
  }
EmailPasswordSignUpViewModel.kt
  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>(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
    }
  }
EmailPasswordSignUpActivity.kt
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()
    }
  }
}

Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.

This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.

  1. Initiate the sign-in process by passing the user's email address and password to the SignIn.create() method.
  2. Check if the sign-in requires a . Client Trust, which is enabled by default for new Clerk applications, may require users to verify their identity with . This example handles the email_code second factor, so send a one-time code to the provided email address with the SignIn.prepareSecondFactor() method.
  3. Collect the user's one-time code and verify it with the SignIn.attemptSecondFactor() method.
  4. If the second factor verification is successful, the SignIn.status will be complete, and you can finish the sign-in flow by calling the setActive() method, which will set the newly created session as the active session. You may need to check for that are required for the user to complete after signing in.
app/sign-in/[[...sign-in]]/page.tsx
'use client'

import * as React from 'react'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
import type { EmailCodeFactor } from '@clerk/types'

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 [showEmailCode, setShowEmailCode] = React.useState(false)
  const router = useRouter()

  // Handle the submission of the sign-in form
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()

    if (!isLoaded) return

    // Start the sign-in process using the email and password provided
    try {
      const signInAttempt = await signIn.create({
        identifier: email,
        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) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            router.push('/')
          },
        })
      } else if (signInAttempt.status === 'needs_second_factor') {
        // Check if email_code is a valid second factor
        // This is required when Client Trust is enabled and the user
        // is signing in from a new device.
        // See https://clerk.com/docs/guides/secure/client-trust
        const emailCodeFactor = signInAttempt.supportedSecondFactors?.find(
          (factor): factor is EmailCodeFactor => factor.strategy === 'email_code',
        )

        if (emailCodeFactor) {
          await signIn.prepareSecondFactor({
            strategy: 'email_code',
            emailAddressId: emailCodeFactor.emailAddressId,
          })
          setShowEmailCode(true)
        }
      } 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: 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 email verification code
  const handleEmailCode = async (e: React.FormEvent) => {
    e.preventDefault()

    if (!isLoaded) return

    try {
      const signInAttempt = await signIn.attemptSecondFactor({
        strategy: 'email_code',
        code,
      })

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

            router.push('/')
          },
        })
      } else {
        console.error(JSON.stringify(signInAttempt, null, 2))
      }
    } catch (err: any) {
      console.error(JSON.stringify(err, null, 2))
    }
  }

  // Display email code verification form
  if (showEmailCode) {
    return (
      <>
        <h1>Verify your email</h1>
        <p>A verification code has been sent to your email.</p>
        <form onSubmit={handleEmailCode}>
          <div>
            <label htmlFor="code">Enter verification code</label>
            <input
              onChange={(e) => setCode(e.target.value)}
              id="code"
              name="code"
              type="text"
              inputMode="numeric"
              value={code}
            />
          </div>
          <button type="submit">Verify</button>
        </form>
      </>
    )
  }

  // Display a form to capture the user's email and password
  return (
    <>
      <h1>Sign in</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Enter email address</label>
          <input
            onChange={(e) => setEmail(e.target.value)}
            id="email"
            name="email"
            type="email"
            value={email}
          />
        </div>
        <div>
          <label htmlFor="password">Enter password</label>
          <input
            onChange={(e) => setPassword(e.target.value)}
            id="password"
            name="password"
            type="password"
            value={password}
          />
        </div>
        <button type="submit">Sign in</button>
      </form>
    </>
  )
}
index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App</title>
  </head>
  <body>
    <div id="signed-in"></div>

    <div id="sign-in">
      <h2>Sign in</h2>
      <form id="sign-in-form">
        <label for="email">Enter email address</label>
        <input name="email" id="sign-in-email" />
        <label for="password">Enter password</label>
        <input name="password" id="sign-in-password" />
        <button type="submit">Continue</button>
      </form>
    </div>

    <form id="email-code-form" hidden>
      <h2>Verify your email</h2>
      <p>A verification code has been sent to your email.</p>
      <label for="code">Enter verification code</label>
      <input id="code" name="code" />
      <button type="submit">Verify</button>
    </form>

    <script type="module" src="/src/main.js" async crossorigin="anonymous"></script>
  </body>
</html>
main.js
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 = `
    <div id="user-button"></div>
  `

  const userbuttonDiv = document.getElementById('user-button')

  clerk.mountUserButton(userbuttonDiv)
} else if (clerk.session?.currentTask) {
  // Handle pending session tasks
  // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
  switch (clerk.session.currentTask.key) {
    case 'choose-organization': {
      document.getElementById('app').innerHTML = `
            <div id="task"></div>
          `

      const taskDiv = document.getElementById('task')

      clerk.mountTaskChooseOrganization(taskDiv)
    }
  }
} else {
  // Handle the sign-in form
  document.getElementById('sign-in-form').addEventListener('submit', async (e) => {
    e.preventDefault()

    const formData = new FormData(e.target)
    const emailAddress = formData.get('email')
    const password = formData.get('password')

    try {
      // Start the sign-in process
      const signInAttempt = await clerk.client.signIn.create({
        identifier: emailAddress,
        password,
      })

      // If the sign-in is complete, set the user as active
      if (signInAttempt.status === 'complete') {
        await clerk.setActive({ session: signInAttempt.createdSessionId })

        location.reload()
      } else if (signInAttempt.status === 'needs_second_factor') {
        // Check if email_code is a valid second factor
        // This is required when Client Trust is enabled and the user
        // is signing in from a new device.
        // See https://clerk.com/docs/guides/secure/client-trust
        const emailCodeFactor = signInAttempt.supportedSecondFactors?.find(
          (factor) => factor.strategy === 'email_code',
        )

        if (emailCodeFactor) {
          await clerk.client.signIn.prepareSecondFactor({
            strategy: 'email_code',
            emailAddressId: emailCodeFactor.emailAddressId,
          })

          // Hide sign-in form and show email code form
          document.getElementById('sign-in').setAttribute('hidden', '')
          document.getElementById('email-code-form').removeAttribute('hidden')
        }
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(signInAttempt, null, 2))
      }
    } catch (error) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error(error)
    }
  })

  // Handle email code verification form
  document.getElementById('email-code-form').addEventListener('submit', async (e) => {
    e.preventDefault()

    const formData = new FormData(e.target)
    const code = formData.get('code')

    try {
      const signInAttempt = await clerk.client.signIn.attemptSecondFactor({
        strategy: 'email_code',
        code,
      })

      if (signInAttempt.status === 'complete') {
        await clerk.setActive({ session: signInAttempt.createdSessionId })

        location.reload()
      } else {
        console.error(JSON.stringify(signInAttempt, null, 2))
      }
    } catch (error) {
      console.error(error)
    }
  })
}

In the (auth) group, create a sign-in.tsx file with the following code. The useSignIn() 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.

app/(auth)/sign-in.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn } from '@clerk/clerk-expo'
import type { EmailCodeFactor } from '@clerk/types'
import { Link, useRouter } from 'expo-router'
import * as React from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'

export default function Page() {
  const { signIn, setActive, isLoaded } = useSignIn()
  const router = useRouter()

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

  // 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) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            router.replace('/')
          },
        })
      } else if (signInAttempt.status === 'needs_second_factor') {
        // Check if email_code is a valid second factor
        // This is required when Client Trust is enabled and the user
        // is signing in from a new device.
        // See https://clerk.com/docs/guides/secure/client-trust
        const emailCodeFactor = signInAttempt.supportedSecondFactors?.find(
          (factor): factor is EmailCodeFactor => factor.strategy === 'email_code',
        )

        if (emailCodeFactor) {
          await signIn.prepareSecondFactor({
            strategy: 'email_code',
            emailAddressId: emailCodeFactor.emailAddressId,
          })
          setShowEmailCode(true)
        }
      } 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, signIn, setActive, router, emailAddress, password])

  // Handle the submission of the email verification code
  const onVerifyPress = React.useCallback(async () => {
    if (!isLoaded) return

    try {
      const signInAttempt = await signIn.attemptSecondFactor({
        strategy: 'email_code',
        code,
      })

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

            router.replace('/')
          },
        })
      } else {
        console.error(JSON.stringify(signInAttempt, null, 2))
      }
    } catch (err) {
      console.error(JSON.stringify(err, null, 2))
    }
  }, [isLoaded, signIn, setActive, router, code])

  // Display email code verification form
  if (showEmailCode) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Verify your email
        </ThemedText>
        <ThemedText style={styles.description}>
          A verification code has been sent to your email.
        </ThemedText>
        <TextInput
          style={styles.input}
          value={code}
          placeholder="Enter verification code"
          placeholderTextColor="#666666"
          onChangeText={(code) => setCode(code)}
          keyboardType="numeric"
        />
        <Pressable
          style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
          onPress={onVerifyPress}
        >
          <ThemedText style={styles.buttonText}>Verify</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"
      />
      <ThemedText style={styles.label}>Password</ThemedText>
      <TextInput
        style={styles.input}
        value={password}
        placeholder="Enter password"
        placeholderTextColor="#666666"
        secureTextEntry={true}
        onChangeText={(password) => setPassword(password)}
      />
      <Pressable
        style={({ pressed }) => [
          styles.button,
          (!emailAddress || !password) && styles.buttonDisabled,
          pressed && styles.buttonPressed,
        ]}
        onPress={onSignInPress}
        disabled={!emailAddress || !password}
      >
        <ThemedText style={styles.buttonText}>Sign in</ThemedText>
      </Pressable>
      <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,
  },
  description: {
    fontSize: 14,
    marginBottom: 16,
    opacity: 0.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',
  },
  linkContainer: {
    flexDirection: 'row',
    gap: 4,
    marginTop: 12,
    alignItems: 'center',
  },
})
  1. Initiate the sign-in process by passing the user's email address and password to the auth.signInWithPassword()iOS Icon method.
  2. Check if the sign-in requires a . Client Trust, which is enabled by default for new Clerk applications, may require users to verify their identity with . This example handles the email_code second factor, so send a one-time code to the provided email address with the signIn.sendMfaEmailCode() method.
  3. Collect the user's one-time code and verify it with the signIn.verifyMfaCode()) method.
  4. If the second factor verification is successful, the SignIn.status will be complete. You may need to check for that are required for the user to complete after signing in.
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:
          // This is required when Client Trust is enabled and the user
          // is signing in from a new device
          // See https://clerk.com/docs/guides/secure/client-trust
          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
  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.prepareSecondFactor
  import com.clerk.api.signin.attemptSecondFactor
  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>(
          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 { signIn ->
                  when (signIn.status) {
                      SignIn.Status.COMPLETE -> {
                          _uiState.value = UiState.SignedIn
                      }
                      SignIn.Status.NEEDS_SECOND_FACTOR -> {
                          // Check if email_code is a valid second factor
                          // This is required when Client Trust is enabled and the user
                          // is signing in from a new device.
                          // See https://clerk.com/docs/guides/secure/client-trust
                          val hasEmailCode = signIn.supportedSecondFactors?.any {
                              it.strategy == "email_code"
                          } == true
                          if (hasEmailCode) {
                              signIn.prepareSecondFactor(
                                  SignIn.PrepareSecondFactorParams.EmailCode()
                              ).onSuccess {
                                  _uiState.value = UiState.NeedsEmailCode
                              }
                          }
                      }
                      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
              }
          }
      }

      fun verify(code: String) {
          val inProgressSignIn = Clerk.signIn ?: return
          viewModelScope.launch {
              inProgressSignIn
                  .attemptSecondFactor(SignIn.AttemptSecondFactorParams.EmailCode(code))
                  .onSuccess {
                      if (it.status == SignIn.Status.COMPLETE) {
                          _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 NeedsEmailCode : UiState

          data object SignedIn : UiState
      }
  }
EmailPasswordSignInActivity.kt
  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,
                  onVerify = viewModel::verify
              )
          }
      }
  }

  @Composable
  fun EmailPasswordSignInView(
      state: EmailPasswordSignInViewModel.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) {

              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.NeedsEmailCode -> {
                  Column(
                      verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
                      horizontalAlignment = Alignment.CenterHorizontally,
                  ) {
                      Text("Verify your email")
                      Text("A verification code has been sent to your email.")
                      TextField(
                          value = code,
                          onValueChange = { code = it },
                          label = { Text("Verification code") },
                      )
                      Button(onClick = { onVerify(code) }) { Text("Verify") }
                  }
              }

              EmailPasswordSignInViewModel.UiState.SignedIn -> {
                  Text("Current session: ${Clerk.activeSession?.id}")
              }

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

Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.

Feedback

What did you think of this content?

Last updated on