Appearance
Login with an external wallet
To authenticate a user via an ethereum wallet (SIWE), use the Expo SDK's useLoginWithSiwe
hook
INFO
In order to use Privy's login with wallet flow, users must actively have an EVM wallet connected to your app from which you can request signatures.
tsx
import {useLoginWithSiwe} from '@privy-io/expo';
...
const {generateSiweMessage, loginWithSiwe} = useLoginWithSiwe();
Once the user has a wallet connected to your app, you can use the returned methods generateSiweMessage
and loginWithSiwe
to authenticate your user per the instructions below.
TIP
After a user has already been authenticated, you can link their external wallet to an existing account by following the same flow with the useLinkWithSiwe
hook instead.
Generate a SIWE message
As an argument to the generateSiweMessage
function returned from useLoginWithSiwe
, pass a JSON object with the following fields
wallet
- object containing EIP-55 compliant walletaddress
andchainId
in CAIP-2 format.from
- object containing your app'sdomain
anduri
.
This will generate a unique message for the user to sign in order to prove ownership of the wallet:
tsx
import {useLoginWithSiwe} from '@privy-io/expo';
export function LoginScreen() {
const [address, setAddress] = useState('');
const [message, setMessage] = useState('');
const {generateSiweMessage} = useLoginWithSiwe();
const handleGenerate = async () => {
const message = await generateSiweMessage({
from: {
domain: 'my-domain.com',
uri: 'https://my-domain.com',
},
wallet: {
// sepolia chainId with CAIP-2 prefix
chainId: `eip155:11155111`,
address,
},
});
setMessage(message);
};
return (
<View>
<TextInput
value={address}
onChangeText={setAddress}
placeholder="0x..."
inputMode="ascii-capable"
/>
<Button onPress={handleGenerate}>Generate Message</Button>
{Boolean(message) && <Text>{message}</Text>}
</View>
);
}
If generateSiweMessage
succeeds, it will return a unique SIWE message as a string, (the onGenerateMessage
callback will also be called with this value).
If it fails due to an invalid address, a network issue, or otherwise, undefined
will be returned, and the resulting error can be handled with the onError
callback detailed below.
Sign the SIWE message
Then, request an EIP-191 personal_sign
signature for the message
returned by generateSiweMessage
, from a connected wallet.
TIP
See Connecting wallets for suggestions on how to connect to a wallet.
Login with signature
Finally, authenticate the user by passing their SIWE signature to the loginWithSiwe
method returned from the useLoginWithSiwe
hook:
tsx
import {useLoginWithSiwe, usePrivy} from '@privy-io/expo';
export function LoginScreen() {
const [signature, setSignature] = useState('');
const {user} = usePrivy();
const {loginWithSiwe} = useLoginWithSiwe();
if (user) {
return (
<>
<Text>Logged In</Text>
<Text>{JSON.stringify(user, null, 2)}</Text>
</>
);
}
return (
<View>
<TextInput
value={signature}
onChangeText={setSignature}
placeholder="0x..."
inputMode="ascii-capable"
/>
<Button onPress={() => loginWithSiwe({signature})}>Login</Button>
</View>
);
}
If loginWithSiwe
succeeds, it will return a PrivyUser
object with details about the authenticated user.
Reasons loginWithSiwe
might fail include:
- the network request fails
- the login attempt is made after the user is already logged in
- the SIWE message has a different
timestamp
ornonce
than the one most recently generated bygenerateSiweMessage
for the givenaddress
.
To handle these failures, use the onError
callback as described below.
Callbacks
Optional callbacks can be passed to useLoginWithSiwe
to handle events that occur during the authentication or linking flows.
tsx
const {state, generateSiweMessage, loginWithSiwe} = useLoginWithSiwe({
onSuccess: console.log,
onError: console.log,
onGenerateMessage: console.log,
});
onGenerateMessage
Pass an onGenerateMessage
function to useLoginWithSiwe
to run custom logic after an SIWE message has been generated. Within this callback you can access message.
You can use this callback with both the useLoginWithSiwe
and useLinkWithSiwe
hooks.
tsx
import {useLoginWithSiwe} from '@privy-io/expo';
export function LoginScreen() {
const {generateSiweMessage, loginWithSiwe} = useLoginWithSiwe({
onGenerateMessage(message) {
// show a toast, send analytics event, etc...
},
});
// ...
}
onSuccess
Pass an onSuccess
function to useLoginWithSiwe
to run custom logic after a successful login. Within this callback you can access the PrivyUser
returned by loginWithSiwe
, as well as an isNewUser
boolean indicating if this is the user's first login to your app.
You can use this callback with both the useLoginWithSiwe
and useLinkWithSiwe
hooks.
tsx
import {useLoginWithSiwe} from '@privy-io/expo';
export function LoginScreen() {
const {generateSiweMessage, loginWithSiwe} = useLoginWithSiwe({
onSuccess(user, isNewUser) {
// show a toast, send analytics event, etc...
},
});
// ...
}
onError
Pass an onError
function to useLoginWithSiwe
to declaratively handle errors occur during the flow.
tsx
import {useLoginWithSiwe} from '@privy-io/expo';
export function LoginScreen() {
const {generateSiweMessage, loginWithSiwe} = useLoginWithSiwe({
onError(error) {
// show a toast, update form errors, etc...
},
});
// ...
}
Tracking login flow state
The state
variable returned from useLoginWithSiwe
will always be one of the following values.
ts
type SiweFlowState =
| {status: 'initial'}
| {status: 'error'; error: Error | null}
| {status: 'generating-message'}
| {status: 'awaiting-signature'}
| {status: 'submitting-signature'}
| {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 * as Clipboard from 'expo-clipboard';
import {View, Text, TextInput, Button} from 'react-native';
import {useLoginWithSiwe} from '@privy-io/expo';
export function LoginScreen() {
const [address, setAddress] = useState('');
const [message, setMessage] = useState('');
const [signature, setSignature] = useState('');
const {state, generateSiweMessage, loginWithSiwe} = useLoginWithSiwe();
return (
<View>
{state.status === 'error' && <Text>{state.error?.message}</Text>}
<View>
<TextInput
value={address}
onChangeText={setAddress}
placeholder="Address (0x...)"
keyboardType="ascii-capable"
/>
{state.status === 'awaiting-signature' && (
<TextInput
value={signature}
onChangeText={setSignature}
placeholder="Signature (0x...)"
keyboardType="ascii-capable"
/>
)}
</View>
<View>
<Button
disabled={
state.status === 'generating-message' || state.status === 'submitting-signature'
}
onPress={async () => {
const message = await generateSiweMessage({
wallet: {address, chainId: 'eip155:1'},
from: {domain: 'my-domain.com', uri: 'https://my-domain.com'},
});
setMessage(message);
}}
>
Generate Message
</Button>
{state.status === 'awaiting-signature' && (
<Button onPress={() => loginWithSiwe({signature})}>Login</Button>
)}
</View>
{message && (
<Button onPress={() => Clipboard.setStringAsync(message)}>
<Text>Click to copy message</Text>
</Button>
)}
</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 {useLoginWithSiwe, hasError} from '@privy-io/expo';
export function LoginScreen() {
const {state, generateSiweMessage, loginWithSiwe} = useLoginWithSiwe();
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>
);
}
Connecting wallets
There are many ways to connect a wallet to a mobile app, a few good options are:
Unlinking Wallets
Once a user has linked a wallet to their profile, you may also want to give them the option to unlink them.
To do this, use the useUnlinkWallet
hook
tsx
import {useUnlinkWallet} from '@privy-io/expo';
export function WalletScreen() {
const {unlinkWallet} = useUnlinkWallet();
return (
<View>
<Text>Wallet: {wallet.address}</Text>
<Button onPress={() => unlinkWallet({address: wallet.address})}>Unlink Wallet</Button>
</View>
);
}
You can use the returned method unlinkWallet
to unlink a wallet given its address
.
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={() =>
loginWithSiwe({
signature,
})
}
>
<text>login</text>
</button>