We strongly recommend against this approach unless you have exhausted the configuration options available and will be planning to dedicate bandwidth to maintain this flow, as it is more time-intensive to build and maintain than the Para Modal.

Development support for Custom UI is only available starting in the Scale Tier.

Authentication States

There are three stages for an authenticating user and three corresponding AuthState types that are returned from various authentication methods:

StageMeaningApplicable Methods
'verify'The user has entered their email or phone number and been sent a confimation code via email or SMS. Alternatively, they have logged in via an external wallet and need to sign a message to verify their ownership of the wallet.signUpOrLogIn, loginExternalWallet
'signup'The user has verified their email, phone number, external wallet, or completed a third-party authentication and may now create a WebAuth passkey or password to secure their account.verifyNewAccount, verifyExternalWallet, verifyOAuth, verifyTelegram, verifyFarcaster
'login'The user has previously signed up and can now log in using their WebAuth passkey or password.signUpOrLogIn , loginExternalWallet , verifyOAuth , verifyTelegram, verifyFarcaster

Below are the type definitions for each AuthState subtype:

type AuthState = AuthStateVerify | AuthStateSignup | AuthStateLogin;

type AuthStateBase = {
  /**
   * The Para userId for the currently authenticating user.
   */
  userId: string;
  /**
   * The identity attestation for the current user, depending on their signup method:
   */
  auth:
    | { email: string }
    | { phone: `+${number}` }
    | { farcasterUsername: string }
    | { telegramUserId: string }
    | { externalWalletAddress: string }
  /**
   * For third-party authentication, additional useful metadata:
   */
  displayName?: string;
  pfpUrl?: string;
  username?: string;
  /**
   * For external wallet authentication, additional metadata:
   */
  externalWallet?: {
    address: string;
    type: 'EVM' | 'SOLANA' | 'COSMOS';
    provider?: string. // i.e. 'Metamask'
  };
}

type AuthStateVerify = AuthStateBase & {
  stage: "verify";
  /**
   * A unique string to be signed by the user's external wallet, if that is the current authentication method.
   */
  signatureVerificationMessage?: string;
};

type AuthStateSignup = AuthStateBase & {
  stage: "signup";
  /**
   * A Para Portal URL for creating a new WebAuth passkey. This URL is only present if you have enabled passkeys in your Developer Portal.
   * For compatibility and security, you should open this URL in a new window or tab.
   */
  passkeyUrl?: string;
  /**
   * The Para internal ID for the new passkey, if created. This is needed to complete the signup process for mobile devices.
   */
  passkeyId?: string;
  /**
   * A Para Portal URL for creating a new password. This URL is only present if you have enabled passwords in your Developer Portal.
   * You can open this URL in an iFrame or a new window or tab.
   */
  passwordUrl?: string;
};

type AuthStateLogin = AuthStateBase & {
  stage: "login";
  /**
   * A Para Portal URL for signing in with a WebAuth passkey. This URL is only present if the user has a previously created passkey.
   * For compatibility and security, you should open this URL in a new window or tab.
   */
  passkeyUrl?: string;
  /**
   * A Para Portal URL for creating a new password. This URL is only present if you have enabled passwords in your Developer Portal.
   * For compatibility and security, you should open this URL in a new window or tab.
   */
  passwordUrl?: string;
  /**
   * If the user has a previous passkey, an array of objects containing the associated `aaguid` and `useragent` fields.
   * You can format this to show a list of devices the user has previously logged in from.
   */
  biometricHints?: {
    aaguid: string;
    useragent: string;
  }[];
};

You will most likely want to track the AuthState within your app and update it with each method resolution. For example, you may want to store it in a dedicated context:

import React from 'react';
import { AuthState } from '@getpara/react-sdk@alpha';

const AuthStateContext = React.createContext<[
  AuthState | undefined,
  React.Dispatch<React.SetStateAction<AuthState | undefined>>
]>([undefined, () => {}]);

export function AuthStateProvider({ children }: React.PropsWithChildren) {
  const [authState, setAuthState] = React.useState<AuthState | undefined>();
  
	return {
		<AuthStateContext.Provider value={[authState, setAuthState]}>
		  {children}
		</AuthStateContext.Provider>
	};
}

export const useAuthState = () => React.useContext(AuthStateContext);

Phone or email accounts

Sign up or log in

To authenticate a user via email or phone number, use the useSignUpOrLogIn hook. This mutation will either fetch the user with the provided authentication method and return an AuthStateLogin object, or create a new user and return an AuthStateVerify object.

  • If the user already exists, you will need to open either the passkeyUrl or passwordUrl in a new window or tab, then invoke the useWaitForLogin mutation. This hook will wait until the user has completed the login process in the new window and then perform any needed setup.
  • If the user is new, you will then need to display a verification code input and later invoke the useVerifyNewAccount mutation.
import { useSignUpOrLogIn } from "@getpara/react-sdk@alpha";
import { useAuthState } from '@/hooks';

function AuthInput() {
  const { signUpOrLogIn, isLoading, isError } = useSignUpOrLogIn();
  const [authState, setAuthState] = useAuthState();
 
  const [authType, setAuthType] = useState<'email' | 'phone'>("email");
    // The determined authentication type from the input string

  const onSubmit = (identifier: string) => {
    signUpOrLogIn(
      {
        auth: authType === "email"
            ? { email: identifier }
            : { phone: identifier as `+${number}` },
      },
      {
        onSuccess: (authState) => {
          setAuthState(authState);
       
          switch (authState.stage) {
            case "verify":
              // Display verification code input
              break;
            case "login":
              const { passkeyUrl, passwordUrl } = authState;
              // Open a login URL in a new window or tab
              break;
          }
        },
        onError: (error) => {
          // Handle error
        },
      }
    );
  };

  // ...
}

Verify new account

While in the verify stage, you will need to display an input for a six-digit code and a callback that invokes the useVerifyNewAccount hook. This will validate the one-time code and, if successful, will return an AuthStateLogin object. (The email or phone number previously entered is now stored, and will not need to be resupplied.)

import { useVerifyNewAccount } from "@getpara/react-sdk@alpha";
import { useAuthState } from '@/hooks';

function VerifyOtp() {
  const { verifyNewAccount, isLoading, isError } = useVerifyNewAccount();
  const [_, setAuthState] = useAuthState();
  
  const [verificationCode, setVerificationCode] = useState('');
    // The six-digit code entered by the user

  const onSubmit = (verificationCode: string) => {
    verifyNewAccount(
      { verificationCode },
      {
        onSuccess: (authState) => {
          setAuthState(authState);
     
          const { passkeyUrl, passwordUrl } = authState;
         
          // Update your UI and prepare to log in the user
        },
        onError: (error) => {
          // Handle a mismatched code
        },
      }
    );
  };

  // ...
}

Resend verification code

You can present a button to resend the verification code to the user’s email or phone in case they need a second one.

import { useResendVerificationCode } from "@getpara/react-sdk@alpha";

function VerifyOtp() {
  const { resendVerificationCode, isLoading, isError } = useResendVerificationCode();
  const [isResent, setIsResent] = React.useState(false);
  const timeout = useRef();
  
  const onClickResend = () => {
    resendVerificationCode(
      undefined,
      {
        onSuccess: () => {
          setIsResent(true);
          
          timeout.current = setTimeout(() => {
            setIsResent(false);
          }, 3000)
        },
        onError: e => {
          console.error(e);
        }
      }
    );
  }
  
  // ...
}

Sign up a new user

After verification is complete, you will receive an AuthStateSignup object. Depending on your configuration, the AuthStateSignup will contain a Para URL for creating a WebAuth biometric passkey, a Para URL for creating a new password, or both. For compatibility and security, you will most likely want to open these URLs in a new popup window, and then immediately invoke the useWaitForWalletCreation hook. This will wait for the user to complete signup and then create a new wallet for each wallet type you have configured in the Para Developer Portal. If you would like more control over the signup process, you can also call the useWaitForSignup hook, which will resolve after signup but bypass automatic wallet provisioning. To cancel the process in response to UI events, you can pass the isCanceled callback.

import { useWaitForWalletCreation } from "@getpara/react-sdk@alpha";
import { useAuthState } from '@/hooks';

function Signup() {
  const popupWindow = React.useRef<Window | null>(null);
  const { waitForWalletCreation, isLoading, isError } = useWaitForWalletCreation();
  const [authState, setAuthState] = useAuthState();
  
  const onSelectSignupMethod = (chosenMethod: 'passkey' | 'password') => {
    const popupUrl = chosenMethod === 'passkey'
      ? authState.passkeyUrl!
      : authState.passwordUrl!
    
    popupWindow.current = window.open(popupUrl, `ParaSignup_${chosenMethod}`);
    
    waitForWalletCreation(
      {
        isCanceled: () => popupWindow.current?.closed,
      },
      {
        onSuccess: () => {
          // Handle successful signup and wallet provisioning
        },
        onError: (error) => {
          // Handle a canceled signup
        },
      }
    );
  };

  // ...
}

Log in an existing user

Depending on your configuration, the AuthStateLogin will contain a Para URL for creating a WebAuth biometric passkey, a Para URL for creating a new password, or both. For compatibility and security, you will most likely want to open these URLs in a new popup window, and then immediately invoke the useWaitForLogin hook. This will wait for the user to complete the login process and resolve when it is finished. To cancel the process in response to UI events, you can pass the isCanceled callback.

import { useWaitForLogin } from "@getpara/react-sdk@alpha";
import { useAuthState } from '@/hooks';

function Login() {
  const popupWindow = React.useRef<Window | null>(null);
  const { waitForLogin, isLoading, isError } = useWaitForLogin();
  const [authState, setAuthState] = useAuthState();
  
  const onSelectLoginMethod = (chosenMethod: 'passkey' | 'password') => {
    const popupUrl = chosenMethod === 'passkey'
      ? authState.passkeyUrl!
      : authState.passwordUrl!;
    
    popupWindow.current = window.open(popupUrl, 'ParaLogin');
    
    waitForLogin(
      {
        isCanceled: () => popupWindow.current?.closed,
      },
      {
        onSuccess: (result) => {
          const { needsWallet } = result;
          
          if (needsWallet) {
            // Create wallet(s) for the user if needed
          } else {
            // Set up signed-in UI
          }
                 },
        onError: (error) => {
          // Handle a canceled login
        },
      }
    );
  };

  // ...
}

Third-party authentication

For third-party authentication, the OTP verification step is bypassed. A successful authentication will advance your application to either the login or signup stage immediately.

OAuth

Para supports OAuth 2.0 sign-ins via Google, Apple, Facebook, Discord, and X, provided the linked account has an email address set. Once a valid email account is fetched, the process is identical to that for email authentication, simply bypassing the one-time code verification step. To implement OAuth flow, use the useVerifyOAuth hook.

import { type TOAuthMethod, useVerifyOAuth } from "@getpara/react-sdk@alpha";
import { useAuthState } from '@/hooks';

function OAuthLogin() {
  const popupWindow = React.useRef<Window | null>(null);
  const { verifyOAuth, isLoading, isError } = useVerifyOAuth();
  const [authState, setAuthState] = useAuthState();
  
  const onOAuthLogin = (method: TOAuthMethod) => {
    verifyOAuth(
      {
        method,
        // Mandatory callback invoked when the OAuth URL is available.
        // You should open this URL in a new window or tab.
        onOAuthUrl: () => {
	        popupWindow.current = window.open(popupUrl, 'ParaOAuth');
        },
        isCanceled: () => popupWindow.current?.closed,
      },
      {
        onSuccess: (authState) => {
          setAuthState(authState);
          
          switch (authState.stage) {
            case 'signup':
              // New user: refer to 'Sign up a new user'
              break;
            case 'login':
              // Returning user: refer to 'Log in an existing user'
              break;
          };
        },
        onError: (error) => {
          // Handle a canceled OAuth verification
        },
      }
    );
  };

  // ...
}

Telegram

To implement your own Telegram authentication flow, please refer to the official documentation. Para uses the following bots to handle authentication requests:

EnvironmentUsernameBot ID
BETA@para_oauth_beta_bot7788006052
PROD@para_oauth_bot7643995807

Once a Telegram authentication response is received, you can invoke the useVerifyTelegram hook to sign up or log in a user associated with the returned Telegram user ID. Users created via Telegram will not have an associated email address or phone number.

import { useVerifyTelegram } from "@getpara/react-sdk@alpha";
import { useAuthState } from '@/hooks';

type TelegramAuthObject = {
  auth_date: number;
  first_name?: string;
  hash: string;
  id: number;
  last_name?: string;
  photo_url?: string;
  username?: string;
};

function TelegramLogin() {
  const popupWindow = React.useRef<Window | null>(null);
  const { verifyTelegram, isLoading, isError } = useVerifyTelegram();
  const [authState, setAuthState] = useAuthState();
  
  const onTelegramResponse = (response: TelegramAuthObject) => {
    waitForLogin(
      { telegramAuthObject },
      {
        onSuccess: (authState) => {
          setAuthState(authState);
          
          switch (authState.stage) {
            case 'signup':
              // New user: refer to 'Sign up a new user'
              break;
            case 'login':
              // Returning user: refer to 'Log in an existing user'
              break;
          };
        },
        onError: (error) => {
          // Handle a failed Telegram verification
        },
      }
    );
  };

  // ...
}

Farcaster

Refer to the official documentation for information on Farcaster’s SIWF (Sign In with Farcaster) feature.

To use this authentication method, use the useVerifyFarcaster hook. The hook will supply a Farcaster Connect URI, which should be displayed to your users as a QR code. Like with Telegram, users created via Farcaster will not have an associated email address or phone number.

import { useVerifyFarcaster } from "@getpara/react-sdk@alpha";
import { useAuthState } from '@/hooks';

function FarcasterLogin() {
  const { verifyTelegram, isLoading, isError } = useVerifyFarcaster();
  const [authState, setAuthState] = useAuthState();
  
  const [farcasterConnectUri, setFarcasterConnectUri] = useState<string | null>(null);
  const isCanceled = React.useRef(false);
  
  useEffect(() => {
    isCanceled.current = !farcasterConnectUri;
  }, [farcasterConnectUri]);
  
  const onClickCancelButton = () => {
    setFarcasterConnectUri(null);
  }
  
  const onClickFarcasterLoginButton = () => {
    verifyFarcaster(
      {
        // Mandatory callback invoked when the OAuth URL is available.
        // You should display the URI as a QR code.
        onConnectUri: connectUri => {
	        setFarcasterConnectUri(connectUri);
	      },
	      // Cancel the login process if the URI is unset	
	      isCanceled: () => isCanceled.current,
	    },
      {
        onSuccess: (authState) => {
          setAuthState(authState);
          
          switch (authState.stage) {
            case 'signup':
              // New user: refer to 'Sign up a new user'
              break;
            case 'login':
              // Returning user: refer to 'Log in an existing user'
              break;
          };
        },
        onError: (error) => {
          // Handle a failed Telegram verification
        },
      }
    );
  };

  // ...
}

Two-factor authentication

Para supports two-factor authentication via one-time codes in Google Authenticator or similar apps. To check a user’s current 2FA status, use the useSetup2fa hook:

  • If the user has already set up two-factor authentication, the setup2fa mutation will return { isSetup: true }.
  • If not, the mutation will return { isSetup: false, uri: string }, where uri is a URI that can be scanned as a QR code by Authenticator or a similar app.
  • When the user has entered the code from their authenticator app, you can use the useEnable2fa hook to activate 2FA for their account.
  • Subsequently, you can use the useVerify2fa hook to verify the user’s account by their one-time code.
import { useSetup2fa } from '@getpara/react-sdk@alpha';

function Setup2fa() {
  const { setup2fa, isPending } = useSetup2fa();
  const [twoFAUri, setTwoFAUri] = useState<string | null>(null);
  
  const onClickSetup2fa = () => {
    setup2fa(
      undefined,
      {
        onSuccess: (result) => {
          if (result.isSetup) {
            // User has already set up 2FA
          } else {
            // Display QR code with result.uri
            setTwoFAUri(result.uri);
          }
        },
        onError: (error) => {
          // Handle error
        },
      }
    );
  };
  
  // ...
}

Logout

To sign out the current user, use the useLogout hook. By default, signing out will preserve any pre-generated wallets (including guest login wallets) present in the device storage, some of which may not belong to the signed-in user. To clear all wallets, set the clearPregenWallets parameter to true.

import { useLogout } from '@getpara/web-sdk@alpha';

function Account() {
	const { logout, isPending } = useLogout();

  const onClickLogout = () => {
	  logout(
	    undefined,
      {
        onSuccess: () => {
          // Update UI to non-authenticated state
        },
      }
	  );
  };
  
  const onClickLogoutAndClearWallets = () => {
    logout(
	    { clearPregenWallets: true },
      {
        onSuccess: () => {
          // Update UI to non-authenticated state
        },
      }
	  );
  };
  
  // ...
}