Appearance
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:
Field | Type | Description |
---|---|---|
code | string | OTP code inputted by the user in your app. |
phone | string | (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>