Skip to content

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 wallet address and chainId in CAIP-2 format.
  • from - object containing your app's domain and uri.

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 or nonce than the one most recently generated by generateSiweMessage for the given address.

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>