Skip to content

Login with phone

To authenticate a user via their phone number, use the Expo SDK's useLoginWithSMS hook

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

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 phone number to an existing account by following the same flow with the useLinkWithSMS hook instead.

Send an OTP

Send a one-time passcode (OTP) to the user's phone number with the sendCode method returned from useLoginWithSMS:

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

export function LoginScreen() {
  const [phone, setPhone] = useState('');

  const {sendCode} = useLoginWithSMS();

  return (
    <View style={styles.container}>
      <Text>Login</Text>

      <TextInput value={phone} onChangeText={setPhone} placeholder="Phone" inputMode="tel" />

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

If sendCode succeeds it will return {success: true}. If it fails due to an invalid phone number, 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 SMS message with a 6-digit OTP. Prompt for this OTP within your application, then authenticate the user with the loginWithCode method returned from useLoginWithSMS. As a parameter to this method, pass an object with the following fields:

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

Below is an example:

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

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

  const {loginWithCode} = useLoginWithSMS();

  return (
    <View style={styles.container}>
      <Text>Login</Text>

      <TextInput value={code} onChangeText={setCode} placeholder="Code" inputMode="numeric" />

      <Button onPress={() => loginWithCode({code: code, phone: '+1 555 555 5555'})}>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 useLoginWithSMS to run custom logic after an OTP has been sent. Within this callback you can access the phone number the code was sent to.

You can use this callback with both the useLoginWithSMS and useLinkWithSMS hooks.

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

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

  // ...
}

onLoginSuccess

Pass an onLoginSuccess function to useLoginWithSMS 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 useLoginWithSMS hook. For the useLinkWithSMS hook, you may use the identical onLinkSuccess callback instead.

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

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

  // ...
}

onError

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

You can use this callback with both the useLoginWithSMS and useLinkWithSMS hooks.

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

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

  // ...
}

Tracking login flow state

The state variable returned from useLoginWithSMS 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 {useLoginWithSMS} from '@privy-io/expo';

export function LoginScreen() {
  const [code, setCode] = useState('');
  const [phone, setPhone] = useState('');
  const {state, sendCode, loginWithCode} = useLoginWithSMS();

  return (
    <View>
      <View>
        <TextInput onChangeText={setPhone} />
        <Button
          // Keeps button disabled while code is being sent
          disabled={state.status === 'sending-code'}
          onPress={() => sendCode({phone})}
        >
          <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 {useLoginWithSMS, hasError} from '@privy-io/expo';

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

  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>