Skip to content

Headless React Setup ​

TIP

This page describes how to use @privy-io/react-auth in a headless manner. This gives your app complete control over the UIs for authentication flows, embedded wallet usage, and more.

Authentication ​

Login with email or SMS ​

To authenticate your users with a one-time passcode (OTP) sent to their email address, use the useLoginWithEmail hook. To authenticate with an OTP sent to their phone number, use the useLoginWithSms hook.

Both the useLoginWithEmail and useLoginWithSms hooks return two methods necessary for the authentication flow:

  • sendCode: sends an OTP to the user's email address or phone number
  • loginWithCode: authenticates the user with the OTP sent to their email address or phone number

Sending the OTP ​

To authenticate your users with email or SMS, first, prompt your user for their email address/phone number, and send an OTP to this address using the sendCode method. As a required parameter to sendCode, pass a JSON object with a variable containing the user's provided email address or phone number as a string. When invoked, this method will return a Promise that:

  • resolves to void if the OTP was successfully sent
  • rejects if there was an error

Formatting the phone number ​

The sendCode method from the useLoginWithSms hook will require a phoneNumber string param that must follow the formatting conventions below:

  • By default, the implicit phone number country code is +1/US. Any phone number inputted is read as a US phone number.
  • Explicitly prepending a (+)1 to the phone number will do nothing, and the phone number will still be read and parsed as a US phone number.
  • If you are trying to send a message to a non (+1) phone number, you must append a +${countryCode} to the beginning of the input value.
  • The phone number will ignore all non-numerical values in the string except for when the first character is a + to denote a custom country code. This numerical string must be a valid phone number in the country code specified.

Authenticating with the OTP ​

Once the OTP has been sent via sendCode, prompt your user for the OTP sent to their email address/phone number, and use the loginWithCode method to authenticate them. As a required parameter to loginWithCode, pass a JSON object with a variable code containing the OTP provided by the user as a string. When invoked, this method will return a Promise that:

  • resolves to void if the entered OTP was correct, and the user has successfully authenticated
  • rejects if there was an error, such as an invalid code entry

To handle cases where a user accidentally enters the wrong OTP in your prompt, for a given OTP delivered to the user's email/phone number with sendCode, you may call loginWithCode up to a maximum of 5 times, to allow the user to retry if the code they entered is invalid. After 5 attempts, the OTP is no longer valid, and you must request a new code for the user via sendCode.

Example usage ​

As an example, you might set up a LoginWithEmail or LoginWithSms component for your app that uses these methods, like below:

INFO

The useLoginWithEmail and useLoginWithSms hooks do not automatically create embedded wallets for users when they login.


Instead, after the user is authenticated, once the user needs a wallet, you can first check if they already have one (!user.wallet), and if not, call Privy's createWallet method to create their wallet.

Login with OAuth ​

INFO

Make sure any OAuth provider you wish to use is enabled in the Privy dashboard.

How does OAuth login work?

At a high-level, the login with OAuth flow works as follows:

  1. 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.).
  2. 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.
  3. 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.

To authenticate your users with a social (OAuth) provider, use the useLoginWithOAuth hook.

Use the initOAuth function returned from the useLoginWithOAuth hook to begin an OAuth flow. As an argument, it takes an object with a provider key, which can be one of: 'google', 'discord', 'twitter', 'github', 'spotify', 'instagram', 'tiktok', 'linkedin', 'apple'.

As an example, you might set up a LoginWithSocial component for your app that uses these methods, like below:

tsx
import {useEffect, useState} from 'react';

import {useLoginWithOAuth, usePrivy} from '@privy-io/react-auth';

export default function LoginWithSocial() {
  const {user, authenticated} = usePrivy();
  const {
    // When the OAuth provider redirects back to your app, the `loading`
    // value can be used to show an intermediate state while login completes.
    loading,
    initOAuth,
  } = useLoginWithOAuth();

  const handleInitOAuth = async () => {
    try {
      // The user will be redirected to OAuth provider's login page.
      // If the login is successful, they will then be redirected back to your app.
      await initOAuth({provider: 'google'});
    } catch (err) {
      // Handle errors due to network availability, captcha failure, or input validation here
    }
  };

  return <button onClick={handleInitOAuth}>Log in with Google</button>;
}

TIP

Note that initOAuth returns a Promise<void> and can be awaited, but any values then updated in useState will be lost on redirect.

Enabling CAPTCHA ​

INFO

Currently only Cloudflare's Turnstile is supported as a Captcha provider.

  1. First reach out to the Privy team to request the captcha integration for your app.
  2. Next, import the Captcha component and place it as a peer to your login form: (When this component mounts, it will execute the invisible Captcha.)
tsx
import {Captcha, useLoginWithEmail} from '@privy-io/react-auth';

const MyLoginForm = () => {
  const [email, setEmail] = useState('');
  const {sendCode, loginWithCode} = useLoginWithEmail();

  const handleSendCode = async () => {
    try {
      await sendCode(email);
    } catch (err) {
      // Captcha failures due to timeout or otherwise will show up here
      // in addition to possible network errors from the sendCode request
      //
      // The `sendCode` method from `useLoginWithSms` and `initOAuth` method
      // from `useLoginWithOAuth` work exactly the same way.
    }
  };

  return (
    <>
      <input type="text" onChange={(e) => setEmail(e.target.value)} />
      <button onClick={handleSendCode}>Send Code</button>
      <Captcha />
    </>
  );
};

That's it! Whenever a user tries to log into your app, Privy will pre-validate the attempt with an invisible captcha. 🎉 ​

Embedded Wallets ​

WARNING

When using headless authentication methods the embeddedWallets.createOnLogin setting is not honored. Instead, you will need to call createWallet from usePrivy() after a user is logged in successfully.

Configuring headless signatures ​

*To configure headless use of embedded wallets, set Add confirmation modals to "off" in your app's Embedded Wallet page in the Dashboard.

TIP

You can override the server configuration for confirmation modals on a per-client basis by setting the embeddedWallets.showWalletUIs config in the Privy SDK. This configuration option will take precedence over the server configuration.

You can then prompt the user for signatures and transactions with the embedded wallet using your own UIs, instead of displaying Privy's default UIs.

Awaiting transaction confirmation ​

By default, Privy will return from transactions flows once the transaction has been confirmed by the network.

Instead, If you'd like Privy to return immediately once a hash is available for the pending transaction, set the config.embeddedWallets.waitForTransactionConfirmation prop of the PrivyProvider to false:

tsx
<PrivyProvider
  config={{
    ...yourConfig,
    embeddedWallets: {
      waitForTransactionConfirmation: false, // Defaults to true
    },
  }}
>
  <Component {...pageProps} />
</PrivyProvider>

INFO

waitForTransactionConfirmation may only be set to false if wallet confirmation modals are disabled in the developer dashboard.