Skip to content

Login with email

To authenticate a user via their email address, use the Expo SDK's useLoginWithEmail hook

tsx
import {useLoginWithEmail} from '@privy-io/expo';
...
const {sendCode, loginWithCode} = useLoginWithEmail();

You can use the returned methods sendCode and loginWithCode to authenticate your user per the instructions below.

TIP

After a user has already been authenticated, you can link their email address to an existing account by following the same flow with the useLinkWithEmail hook instead.

Send an OTP

Send a one-time passcode (OTP) to the user's email by passing their email address to the sendCode method returned from useLoginWithEmail:

tsx
import {useLoginWithEmail} from '@privy-io/expo';

export function LoginScreen() {
  const [email, setEmail] = useState('');

  const {sendCode} = useLoginWithEmail();

  return (
    <View>
      <Text>Login</Text>

      <TextInput value={email} onChangeText={setEmail} placeholder="Email" inputMode="email" />

      <Button onPress={() => sendCode({email})}>Send Code</Button>
    </View>
  );
}

If sendCode succeeds, it will return {success: true}.

If it fails due to an invalid email address, a network issue, or otherwise, {success: false} will be returned, and the resulting error can be handled with the onError callback detailed below.

Authenticate with OTP

The user will then receive an email with a 6-digit OTP. Prompt for this OTP within your application, then authenticate the user with the loginWithCode method returned from the useLoginWithEmail hook. As a parameter to this method, pass an object with the following fields:

FieldTypeDescription
codestringOTP code inputted by the user in your app.
emailstring(Optional) The user's email address. Though this parameter is optional, it is highly recommended that you pass the user's email address explicitly.

Below is an example:

tsx
import {useLoginWithEmail} from '@privy-io/expo';

export function LoginScreen() {
  const [code, setCode] = useState('');
  const {loginWithCode} = useLoginWithEmail();

  return (
    <View>
      <Text>Login</Text>

      <TextInput value={code} onChangeText={setCode} placeholder="Code" inputMode="numeric" />
      <Button onPress={() => loginWithCode({code: code, email: 'user@email.com'})}>Login</Button>
    </View>
  );
}

If loginWithCode succeeds, it will return a PrivyUser object with details about the authenticated user.

Reasons loginWithCode might fail include:

  • the network request fails
  • the login attempt is made after the user is already logged in
  • the OTP code has not been either sent, or provided as optional param

To handle these failures, use the onError callback as described below.

Callbacks

onSendCodeSuccess

Pass an onSendCodeSuccess function to useLoginWithEmail to run custom logic after an OTP has been sent. Within this callback you can access the email the code was sent to.

You can use this callback with both the useLoginWithEmail and useLinkWithEmail hooks.

tsx
import {useLoginWithEmail} from '@privy-io/expo';

export function LoginScreen() {
  const {sendCode, loginWithCode} = useLoginWithEmail({
    onSendCodeSuccess({email}) {
      // show a toast, send analytics event, etc...
    },
  });

  // ...
}

onLoginSuccess

Pass an onLoginSuccess function to useLoginWithEmail to run custom logic after a successful login. Within this callback you can access the PrivyUser returned by loginWithCode, as well as an isNewUser boolean indicating if this is the user's first login to your app.

You may only use the onLoginSuccess callback with the useLoginWithEmail hook. For the useLinkWithEmail hook, you may use the identical onLinkSuccess callback instead.

tsx
import {useLoginWithEmail} from '@privy-io/expo';

export function LoginScreen() {
  const {sendCode, loginWithCode} = useLoginWithEmail({
    onLoginSuccess(user, isNewUser) {
      // show a toast, send analytics event, etc...
    },
  });

  // ...
}

onError

Pass an onError function to useLoginWithEmail to declaratively handle errors occur during the flow.

tsx
import {useLoginWithEmail} from '@privy-io/expo';

export function LoginScreen() {
  const {sendCode, loginWithCode} = useLoginWithEmail({
    onError(error) {
      // show a toast, update form errors, etc...
    },
  });

  // ...
}

Tracking login flow state

The state variable returned from useLoginWithEmail will always be one of the following values.

ts
type OtpFlowState =
  | {status: 'initial'}
  | {status: 'error'; error: Error | null}
  | {status: 'sending-code'}
  | {status: 'awaiting-code-input'}
  | {status: 'submitting-code'}
  | {status: 'done'};

Conditional rendering

You can use the state.status variable to conditionally render your UI based on the user's current state in the login flow.

tsx
import {View, Text, TextInput, Button} from 'react-native';

import {useLoginWithEmail} from '@privy-io/expo';

export function LoginScreen() {
  const [code, setCode] = useState('');
  const [email, setEmail] = useState('');
  const {state, sendCode, loginWithCode} = useLoginWithEmail();

  return (
    <View>
      <View>
        <TextInput onChangeText={setEmail} />
        <Button
          // Keeps button disabled while code is being sent
          disabled={state.status === 'sending-code'}
          onPress={() => sendCode({email})}
        >
          <Text>Send Code</Text>
        </Button>

        {state.status === 'sending-code' && (
          //  Shows only while the code is sending
          <Text>Sending Code...</Text>
        )}
      </View>

      <View>
        <TextInput onChangeText={setCode} />
        <Button
          // Keeps button disabled until the code has been sent
          disabled={state.status !== 'awaiting-code-input'}
          onPress={() => loginWithCode({code})}
        >
          <Text>Login</Text>
        </Button>
      </View>

      {state.status === 'submitting-code' && (
        // Shows only while the login is being attempted
        <Text>Logging in...</Text>
      )}
    </View>
  );
}

Error state

When state.status is equal to 'error', the error value is accessible as state.error which can be used to render inline hints in a login form.

tsx
import {useLoginWithEmail, hasError} from '@privy-io/expo';

export function LoginScreen() {
  const {state, sendCode, loginWithCode} = useLoginWithEmail();

  return (
    <View>
      {/* other ui... */}

      {state.status === 'error' && (
        <>
          <Text style={{color: 'red'}}>There was an error</Text>
          <Text style={{color: 'lightred'}}>{state.error.message}</Text>
        </>
      )}

      {hasError(state) && (
        // The `hasError` util is also provided as a convenience
        // (for typescript users, this provides the same type narrowing as above)
        <>
          <Text style={{color: 'red'}}>There was an error</Text>
          <Text style={{color: 'lightred'}}>{state.error.message}</Text>
        </>
      )}
    </View>
  );
}

Enforcing login vs. sign-up

Depending on how your app's authentication flow is set up, you may want to provide separate routes for signing up to your app (e.g. creating their account for the first time) and logging in as a returning user (e.g. logging into an existing account). You can distinguish login vs. sign-up flows by passing an optional disableSignup boolean to your login call like so:

tsx
<Button
  onPress={() =>
    loginWithCode({
      email,
      code,
    })
  }
>
  <Text>Login</Text>
</Button>