Expo Quickstart
The Privy Expo SDK is a react-native library client for Privy that allows you to add secure authentication, non-custodial embedded wallets, and powerful user management into your application.
The Privy Expo SDK is under continued development. We are rapidly shipping features and improvements to our developer interface.
Feature Support Matrix
Feature | Supported? |
---|---|
Sign In w/ Email | โ |
Sign In w/ SMS | โ |
Sign In w/ Social Providers | โ |
Sign In w/ Wallets (SIWE) | ๐ |
Embedded Wallet Creation | โ |
Embedded Wallet Creation with Passwords | โ |
Embedded Wallet Recovery | โ |
Embedded Wallet Signatures & Transactions** | โ |
Setting password on existing embedded wallet | ๐ |
** The Privy Expo SDK ships an EIP-1193Provider
interface to request signatures/transactions from the embedded wallet.
Starter Projectsโ
For examples of the Privy Expo SDK in action, see our starter repos on Github.
Project type | repo |
---|---|
Expo | privy-io/expo-starter |
Bare expo | privy-io/expo-bare-starter |
Setupโ
Installationโ
Install the Privy Expo SDK along with its peer dependencies using npm
:
npx expo install expo-application expo-constants expo-linking expo-secure-store react-native-webview @privy-io/expo
Install polyfills which account for APIs required by the Privy Expo SDK that are not available in the React Native environment.
npm i --save react-native-get-random-values @ethersproject/shims
These must be installed and imported as early as possible in your application.
import 'react-native-get-random-values'
import '@ethersproject/shims'
// Your root component
Install babel plugins (required for upstream dependencies)
npm i --save-dev @babel/plugin-transform-class-properties @babel/plugin-transform-private-methods @babel/plugin-transform-flow-strip-types
Add to your babel config (babel.config.js
or .babelrc
)
{
// babel config...
"plugins": [
["@babel/plugin-transform-class-properties", { "loose": true }],
["@babel/plugin-transform-private-methods", { "loose": true }],
"@babel/plugin-transform-flow-strip-types"
]
}
Initializationโ
Initialize the Privy client with an object containing the following fields:
appId
: your Privy App ID from the Privy console
// Import required polyfils first
import 'react-native-get-random-values';
import '@ethersproject/shims';
import React from 'react';
// Import the PrivyProvider
import {PrivyProvider} from '@privy-io/expo';
// Your components
import {HomeScreen} from './Homescreen';
export default function App() {
return (
// Render the PrivyProvider with your app ID
<PrivyProvider appId={'insert-your-privy-app-id'}>
<HomeScreen />
</PrivyProvider>
);
}
That's it! You can now use the Privy Expo SDK to securely authenticate and provision embedded wallets for your users. ๐
App Configurationโ
Allowed application IDsโ
A Privy application can be configured to restrict which mobile apps can use it's client-side App ID (similar to allowed domains for web apps). For this, we'll use the unique value that identifies your app in the Apple App Store or Google Play Store.
Copy your ID from
app.config.js
orapp.json
.{
// other config
"ios": {
"bundleIdentifier": "com.myorg.app" // <โ
}, // โ
// โโ your app ID
"android": { // โ
"package": "com.myorg.app" // <โโโโโโโโโโ
}
}Add your ID in the privy console in the
Configuration
section.
- Multiple app IDs can be added
- An empty list will mean all requests from mobile apps are denied.
- For development only, if you are using expo go, enter
host.exp.Exponent
to allow requests. (You can also add a wildcard character (*
) to accept all requests, but this is not recommended)
Allowed url schemesโ
When a url is used to launch your app at the end of an OAuth login flow, it will contain a custom scheme like myapp://oauth-callback
. This scheme is specified in your app config, and should be added in the Privy Console for increased security.
Copy your app's url scheme from
app.json
orapp.config.ts
.Add your url scheme in the Privy Console.
- Multiple schemes can be added
- An empty list will mean all url schemes (other than
https
andhttp
) will be rejected. - For development only if you are using Expo Go, enter
exp
which is the custom scheme for the Expo Go app. (You can also add a wildcard character (*
) to accept all schemes, but this is not recommended)
Authenticationโ
The Privy Expo SDK allows you to log users in either via:
- one-time passcode (OTP) sent to their email address or phone number.
- a variety of OAuth providers (apple, google, etc..)
Follow the steps below for your desired login flow.
- Login with email
- Login with SMS
- Login with OAuth
Send an OTP by passing the user's email address to the
sendCode
method returned fromuseLoginWithEmail
import {useLoginWithEmail} from '@privy-io/expo';
export function LoginScreen() {
const [email, setEmail] = useState('');
const {sendCode} = useLoginWithEmail();
return (
<View style={styles.container}>
<Text>Login</Text>
<TextInput value={email} onChangeText={setEmail} placeholder="Email" inputMode="email" />
<Button onPress={() => sendCode({ email })}>
Send Code
</Button>
</View>
);
}The user will receive an email with a 6-digit OTP. Prompt for this OTP within your application, then authenticate by passing it to
loginWithCode
method returned fromuseLoginWithEmail
:import {useLoginWithEmail} from '@privy-io/expo';
export function LoginScreen() {
const [code, setCode] = useState('');
const {loginWithCode} = useLoginWithEmail();
return (
<View style={styles.container}>
<Text>Login</Text>
<TextInput value={code} onChangeText={setCode} placeholder="Code" inputMode="numeric" />
<Button onPress={() => loginWithCode({ code })}>
Login
</Button>
</View>
);
}If the OTP is correct,
loginWithCode
will return an object representing the user session, and theuser
object fromusePrivy
will be updated, triggering re-renders wherever it is used ๐.
After a user is logged in, the same workflow may be followed to link an email to their account using the useLinkWithEmail
hook instead.
Send an OTP by passing the user's phone number to the
sendCode
method returned fromuseLoginWithSMS
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={setEmail} placeholder="Email" inputMode="email" />
<Button onPress={() => sendCode({ phone })}>
Send Code
</Button>
</View>
);
}The user will receive an SMS message with a 6-digit OTP. Prompt for this OTP within your application, then authenticate by passing it to
loginWithCode
method returned fromuseLoginWithSMS
: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 })}>
Login
</Button>
</View>
);
}If the OTP is correct,
loginWithCode
will return an object representing the user session, and theuser
object fromusePrivy
will be updated, triggering re-renders wherever it is used ๐.
After a user is logged in, the same workflow may be followed to link an SMS number to their account using the useLinkWithSMS
hook instead.
How does OAuth login work?
At a high-level, the login with OAuth flow works as follows:
- First, your app generates an OAuth login URL and redirects the user to this URL. This URL must be newly generated for each login attempt, and is specific to each OAuth provider (Google, Twitter, Apple, etc.).
- Once the user has been redirected to the OAuth login URL, the user completes the login flow with the corresponding OAuth provider. Upon successfully completing the flow, the user will be redirected back to your app.
- When the user is redirected back to your app, the OAuth provider will include an authorization code in the redirect URL's query parameters. Your app should pass this code to Privy to authenticate your user.
Make sure your app specifies a custom
scheme
inapp.json
orapp.config.js
. This is to allow it to be re-launched with a redirect URL after a successful OAuth flow.Use the
start
function fromuseOAuthFlow
hook to kick off an OAuth authentication by passing your desired provider. (Valid provider arguments are typed asOAuthProviderType
which is exported from@privy-io/js-sdk-core
).import {useOAuthFlow} from '@privy-io/expo';
export function LoginScreen() {
const {start} = useOAuthFlow();
return (
<View style={styles.container}>
<Button onPress={() => start({ provider: 'google' })}>
Login with Google
</Button>
</View>
);
}This will open a browser to complete the provider's authentication requirements, after which they will be redirected back to your application fully authenticated ๐.
How do I render a loading state in my app while it's backgrounded?
Since your app will be running in the background while the OAuth flow is completed, you can use the state.status
value returned from useOAuthFlow
to reactively update your UI in case the user returns before finishing.
export function LoginScreen() {
const {start, state} = useOAuthFlow();
return (
<View style={styles.container}>
<Button onPress={() => start({ provider: 'google' })}>
{state.status === 'loading' ? 'Loading...' : 'Login with Google'}
</Button>
</View>
);
}
User Dataโ
To get an object representation of your user's identity data, including their Privy user ID, linked accounts, and embedded wallets, use the Privy client's usePrivy
hook:
const {user} = usePrivy();
You may also use this method to determine whether a user is authenticated in your app. If the user
returned from usePrivy
is truthy, the user is authenticated; otherwise, they are unauthenticated.
Persistenceโ
By default, the Privy Expo SDK makes use of expo-secure-store
package to persist sessions after your app is closed.
If you'd rather persist sessions in a different way, you can easily build an adapter and provide it as an optional prop to the <PrivyProvider />
import StorageProvider from 'my-storage-provider'
import type {Storage} from '@privy-io/js-sdk-core'
import {PrivyProvider} from '@privy-io/expo'
import AppContent from './AppContent'
const storage: Storage = {
get: (key) => StorageProvider.getItemAsync(key),
put: (key, val) => StorageProvider.setItemAsync(key, val),
del: (key) => StorageProvider.deleteItemAsync(key),
getKeys: () => StorageProvider.getAllKeys(),
};
export function App() {
return (
<PrivyProvider appId={'my-app-id'} storage={storage}>
<AppContent />
</PrivyProvider>
)
}
Embedded Walletsโ
Waiting for secure contextโ
Privy uses a secure context for embedded wallet communication that needs to be loaded before any read or write operations can be completed.
This is easy to ensure in your application by waiting for the value of isReady
which is returned by the usePrivy
hook to be true.
import {usePrivy} from '@privy-io/expo'
export function App() {
const {isReady} = usePrivy()
if (!isReady) return <Spinner />
return <AppContent />
}
Creating the walletโ
To create an embedded wallet for your user, use the Privy Expo SDK's useEmbeddedWallet
hook.
As an optional parameter to create
, you may include a password
set by the user as a string. Specifically:
- If you do include a password, the recovery share of the embedded wallet will be secured by the user's password. This is called password-based recovery.
- If you do not include a password, the recovery share will be secured by Privy. This is called automatic recovery.
- Creating a wallet without a password
- Creating a wallet with a password
import {useEmbeddedWallet, isNotCreated} from '@privy-io/expo';
const CreateWalletButton = () => {
const wallet = useEmbeddedWallet();
if (isNotCreated(wallet)) {
return <Button onPress={() => wallet.create()}>Create Wallet</Button>;
}
return null;
};
import {useEmbeddedWallet, isNotCreated} from '@privy-io/expo';
const CreateWalletButton = () => {
const wallet = useEmbeddedWallet();
const [password, setPassword] = useState('');
if (isNotCreated(wallet)) {
return (
<View>
{/* Make sure to use appropriate input to handle sensitive information */}
<TextInput value={password} onChangeText={setPassword} />
<Button onPress={() => wallet.create(password)}>Create Wallet</Button>;
</View>
);
}
return null;
};
How can my app know if a user already has an embedded wallet?
To determine if the current user already has an embedded wallet, you can either:
- obtain an object representation of the user from the Privy SDK's
usePrivy
hook, and check if it includes a wallet with awalletClientType
of'privy'
, or - use the Privy client's
useEmbeddedWallet
hook, like below:
import {useEmbeddedWallet, isConnected, needsRecovery} from '@privy-io/expo';
const Component = () => {
const wallet = useEmbeddedWallet();
const [password, setPassword] = useState('');
if (isConnected(wallet)) {
/* The user's embedded wallet exists and is ready to be used! */
return <View>Wallet Exists</View>;
}
if (needsRecovery(wallet)) {
/*
The user's embedded wallet exists but has never been loaded on this device.
They will need to go through the password recovery flow to use it.
*/
return (
<View>
The user's embedded wallet exists but has never been loaded on this device. They will need
to go through the password recovery flow to use it.
</View>
);
}
return null;
};
Why is wallet.create
undefined or throwing a type error?
wallet.create
will be undefined if the wallet has already been created- If your application uses Typescript,
isNotCreated
refines the type ofwallet
. Sowallet.create
will cause a type error outside a check that eitherisNotCreated
istrue
orwallet.status === 'not-created'
Getting an EIP-1193 providerโ
To enable your app to request signatures and transactions from the embedded wallet, Privy embedded wallets export an EIP-1193 provider. This allows you request signatures and transactions from the wallet via a familiar JSON-RPC API (e.g. personal_sign
).
To get an EIP-1193 provider for the embedded wallet, use the Privy Expo SDK's useEmbeddedWallet
hook.
import {useEmbeddedWallet, isConnected, needsRecovery} from '@privy-io/expo';
const Component = () => {
const wallet = useEmbeddedWallet();
const [password, setPassword] = useState('');
const signMessage = (provider: EIP1193Provider) => {
// Get the wallet address
const accounts = await provider.request({
method: 'eth_requestAccounts'
});
// Sign message
const message = 'I hereby vote for foobar';
const signature = await provider.request({
method: 'personal_sign',
params: [message, accounts[0]]
});
}
if (isConnected(wallet)) {
/* The user's embedded wallet exists and is ready to be used! */
return <Button onPress={() => { signMessage(wallet.provider) }}>Sign a message</Button>
}
if (needsRecovery(wallet)) {
/*
The user's embedded wallet exists but has never been loaded on this device.
They will need to go through the password recovery flow to use it.
*/
return (
<View>
The user's embedded wallet exists but has never been loaded on this device. They will need
to go through the password recovery flow to use it.
</View>
);
}
return null;
};
Users are only required to enter the password for their embedded wallet if both of the following are true:
- their wallet is secured by password-based recovery (instead of automatic recovery)
- this is the user's first time using the wallet on a given device or browser
The useEmbeddedWallet
hook's status
property helps inform your app whether these conditions are true for the current user's wallet, allowing you to prompt the user for their password only when necessary.
Requesting signatures and transactionsโ
Once you have used isConnected(wallet)
or wallet.status === 'connected'
to make sure the wallet is connected and get it's EIP-1193 provider, you can use the provider.request
method to send JSON-RPC requests that request signatures and transactions from the wallet.
The request
method accepts an object with the fields:
method
(required): the name of the JSON-RPC method as a string, e.g.personal_sign
params
(optional): an array of arguments for the JSON-RPC method specified bymethod
- Example signature with the embedded wallet
- Example transaction with the embedded wallet
// Get address
const accounts = await wallet.provider.request({
method: 'eth_requestAccounts'
});
// Sign message
const message = 'I hereby vote for foobar';
const signature = await wallet.provider.request({
method: 'personal_sign',
params: [message, accounts[0]]
});
// Get address
const accounts = await wallet.provider.request({
method: 'eth_requestAccounts'
});
// Send transaction (will be signed and populated)
const response = await wallet.provider.request({
method: 'eth_sendTransaction',
params: [
{
from: accounts[0],
to: '0x0000000000000000000000000000000000000000',
value: '1'
}
]
});
Switching networksโ
By default, embedded wallets are connected to the Ethereum mainnet.
To switch the embedded wallet to a different network, send an wallet_switchEthereumChain
JSON-RPC request to the wallet's EIP-1193 provider. In the request's params
, specify your target chainId
as a hexadecimal string.
await wallet.provider.request({
method: 'wallet_switchEthereumChain',
// Replace '0x5' with the chainId of your target network
params: [{chainId: '0x5'}]
});
The Privy Expo SDK currently only supports the networks listed here. We are actively adding support for additional networks; please reach out if you need one urgently prioritized!
Integrating with third-party librariesโ
Using the wallet's EIP-1193 provider, you can easily integrate Privy alongside a third-party library like viem
or ethers
to interface with the embedded wallet.
Third-party libraries may require additional shims to be used in a React Native environment.
- Viem
- Ethers
First, import the necessary methods, objects, and networks from viem
:
import {createWalletClient, custom} from 'viem';
// Replace 'mainnet' with your desired network
import {mainnet} from 'viem/chains';
Next, get an EIP-1193 provider for the user's embedded wallet, and switch its network to your desired network:
await wallet.provider.request({
method: 'wallet_switchEthereumChain',
// Replace '0x1' with the chain ID of your desired network
params: [{chainId: '0x1'}]
});
Lastly, initialize a viem Wallet Client from the EIP-1193 provider:
const walletClient = createWalletClient({
// Replace this with your desired network that you imported from viem
chain: mainnet,
transport: custom(wallet.provider)
});
You can now use methods implemented by viem's Wallet Client, including signMessage
, signTypedData
, and sendTransaction
!
First, import ethers
:
import {ethers} from 'ethers';
Next, get an EIP-1193 provider for the user's embedded wallet, and switch its network to your desired network:
await wallet.provider.request({
method: 'wallet_switchEthereumChain',
// Replace '0x1' with the chain ID of your desired network
params: [{chainId: '0x1'}]
});
Lastly, initialize an ethers provider and signer from this EIP-1193 provider:
const ethersProvider = new ethers.providers.Web3Provider(wallet.provider);
const ethersSigner = ethersProvider.getSigner();
You can then use methods implemented by ethers' providers and signers, including signMessage
and sendTransaction
.
Setting a password for an existing walletโ
Coming Soon