Skip to Content
View .md

Alien Solana SSO React API: AlienSolanaSsoProvider and useSolanaAuth

@alien-id/sso-solana-react wraps the core client with a React context provider, a hook, and ready-made sign-in components. For the guided setup, see the React Integration guide.

AlienSolanaSsoProvider

Context provider that wraps your application and exposes the wallet linking state. It must sit inside the Solana wallet adapter providers, because it reads the connected wallet from their context.

interface AlienSolanaSsoProviderProps { config: AlienSolanaSsoClientConfig; children: React.ReactNode; }
OptionTypeDefaultDescription
ssoBaseUrlstringBase URL of the SSO service
providerAddressstringThe platform provider address — Solana SSO is single-tenant; see the single-platform-provider note
pollingIntervalnumber5000Polling interval in milliseconds
credentialSignerProgramIdstringsee coreCredential Signer program ID
sessionRegistryProgramIdstringsee coreSession Registry program ID
sasProgramIdstringsee coreSAS program ID
import { AlienSolanaSsoProvider } from '@alien-id/sso-solana-react'; import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; function App() { return ( <ConnectionProvider endpoint="https://api.mainnet-beta.solana.com"> <WalletProvider wallets={[]} autoConnect> <AlienSolanaSsoProvider config={{ ssoBaseUrl: 'https://sso.alien-api.com', providerAddress: 'platform-provider-address', }} > <YourApp /> </AlienSolanaSsoProvider> </WalletProvider> </ConnectionProvider> ); }

useSolanaAuth

Hook that provides access to the wallet linking state and methods. Must be used within AlienSolanaSsoProvider and the Solana wallet adapter providers.

The hook returns the provider’s context value with the following shape. The context type itself is not exported by the package — SolanaWalletAdapter and SolanaConnectionAdapter are the only exported types:

{ client: AlienSolanaSsoClient; // from @alien-id/sso-solana auth: { sessionAddress?: string | null }; wallet: SolanaWalletAdapter; connectionAdapter: SolanaConnectionAdapter; queryClient: QueryClient; generateDeeplink: (solanaAddress: string) => Promise<SolanaLinkResponse>; pollAuth: (pollingCode: string) => Promise<SolanaPollResponse>; verifyAttestation: (solanaAddress: string) => Promise<string | null>; logout: () => void; openModal: () => void; closeModal: () => void; isModalOpen: boolean; }

client

Direct access to the underlying AlienSolanaSsoClient instance — the escape hatch when you need a core method the hook doesn’t re-export.

const { client } = useSolanaAuth(); const sessionAddress = await client.getAttestation(walletAddress);

auth

Current wallet linking state. sessionAddress is set when the connected wallet has a linking attestation.

const { auth } = useSolanaAuth(); if (auth.sessionAddress) { console.log('Wallet is linked. Session:', auth.sessionAddress); }

wallet

The Solana wallet adapter from context (from @solana/wallet-adapter-react).

const { wallet } = useSolanaAuth(); if (wallet.publicKey) { console.log('Wallet connected:', wallet.publicKey.toBase58()); } // Sign a transaction — `signTransaction` is optional on the adapter, // so guard before calling it const signedTx = await wallet.signTransaction?.(transaction);

connectionAdapter

The Solana connection adapter from context (from @solana/wallet-adapter-react).

const { connectionAdapter } = useSolanaAuth(); const balance = await connectionAdapter.connection.getBalance(wallet.publicKey);

queryClient

The React Query client instance, for advanced cache control.

const { queryClient } = useSolanaAuth(); queryClient.invalidateQueries({ queryKey: ['some-key'] });

Generates an attestation creation deep link and polling code for a Solana address. Use it when the wallet doesn’t have an attestation yet. Returns a SolanaLinkResponse.

async generateDeeplink(solanaAddress: string): Promise<SolanaLinkResponse>
const { wallet, generateDeeplink } = useSolanaAuth(); const handleSignIn = async () => { if (!wallet.publicKey) return; const { deep_link, polling_code } = await generateDeeplink( wallet.publicKey.toBase58() ); console.log('Deep link:', deep_link); };

pollAuth()

Polls the linking status for a polling code from generateDeeplink(). Returns a SolanaPollResponse.

async pollAuth(pollingCode: string): Promise<SolanaPollResponse>
const { pollAuth } = useSolanaAuth(); const response = await pollAuth(polling_code); if (response.status === 'authorized') { console.log('Session address:', response.session_address); }

verifyAttestation()

Re-verifies the linking attestation for wallets previously linked in this browser. The hook first checks the cached authed address in localStorage: if the queried address was never linked from this browser, it returns null immediately, without calling the server. For a previously linked address it returns the cached session address within the 60-second grace period after attestation creation; outside the grace period it re-queries the server and clears the linking state when the attestation is gone. To check an arbitrary wallet address regardless of this browser’s history, use client.getAttestation().

This is not proof of authentication. A non-null result only means a linking record exists and was created from this browser — and with client.getAttestation(), anyone who knows a public wallet address gets the same information. Neither proves the current user controls the wallet. To authenticate a user, require a fresh wallet signature over a server-issued challenge and verify it server-side; this SDK does not provide that primitive.

async verifyAttestation(solanaAddress: string): Promise<string | null>
const { wallet, verifyAttestation } = useSolanaAuth(); const sessionAddress = await verifyAttestation(wallet.publicKey.toBase58()); if (sessionAddress) { // Wallet linked in this browser — skip the creation flow console.log('Wallet is already linked:', sessionAddress); } else { // Not linked, or never linked from this browser — // start the creation flow console.log('No attestation found, need to create one'); }

logout()

Clears the linking state: removes the session address from auth, deletes the session data from localStorage, and clears the grace-period cache.

const { logout } = useSolanaAuth(); <button onClick={logout}>Logout</button>

openModal() / closeModal()

Open and close the built-in sign-in modal. This is the only supported way to control it.

const { openModal, closeModal, isModalOpen } = useSolanaAuth(); <button onClick={openModal}>Sign In</button> {isModalOpen && <button onClick={closeModal}>Close</button>}

isModalOpen

Boolean indicating whether the sign-in modal is currently open.

const { isModalOpen } = useSolanaAuth(); console.log('Modal open:', isModalOpen);

Components

SolanaSignInButton

Pre-styled button that opens the Solana sign-in modal.

interface SolanaSignInButtonProps { variant?: 'default' | 'short'; color?: 'dark' | 'light'; } // exported as a React.FC<SolanaSignInButtonProps> function SolanaSignInButton(props: SolanaSignInButtonProps): JSX.Element
OptionTypeDefaultDescription
variant'default' | 'short''default''default' shows the icon plus “Sign in with Alien ID” text; 'short' shows the icon only
color'dark' | 'light''light'Color scheme
import { SolanaSignInButton } from '@alien-id/sso-solana-react'; import { useWallet } from '@solana/wallet-adapter-react'; import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; function LoginPage() { const { connected } = useWallet(); if (!connected) { return ( <div> <h1>Connect Your Wallet</h1> <WalletMultiButton /> </div> ); } return ( <div> <h1>Sign In with Alien</h1> <SolanaSignInButton /> </div> ); }

The button ships with default styles via CSS modules, so its class names are hashed — there is no stable class to target. Theme it through the CSS custom properties it reads:

:root { --alien-brand-button-color: /* text color */; --alien-brand-button-background-color-light: /* light background */; --alien-brand-button-border-color-light: /* light border */; --alien-brand-button-hover-background-color-light: /* light hover */; --alien-brand-button-background-color-dark: /* dark background */; --alien-brand-button-border-color-dark: /* dark border */; --alien-brand-button-hover-background-color-dark: /* dark hover */; }

SolanaSignInModal

Modal component for the attestation creation flow. It is rendered automatically by AlienSolanaSsoProvider, takes no props, and is controlled entirely through the useSolanaAuth() context.

Do not render <SolanaSignInModal /> yourself. The provider already renders one instance. Mounting a second instance shares the same query keys with the provider’s copy and the two fight over internal state, producing broken UI (e.g. a stuck QR spinner). Open and close the modal only via useSolanaAuth().openModal() / closeModal().

The modal handles the whole flow on its own: it checks whether a linking attestation already exists (and restores the linked session immediately if so), displays the QR code with the deep link, polls automatically, builds the attestation transaction, prompts the user to sign it, creates the on-chain attestation, and shows loading and error states along the way.

const { openModal, closeModal } = useSolanaAuth(); // Open the modal openModal(); // Close the modal closeModal();

Types

@alien-id/sso-solana-react exports two types of its own — SolanaWalletAdapter and SolanaConnectionAdapter — plus the three storage-key helper functions. The shared types come from @alien-id/sso-solana — import them from there:

import type { AlienSolanaSsoClientConfig, SolanaLinkResponse, SolanaPollResponse, } from '@alien-id/sso-solana';

The canonical definitions live in the core reference: AlienSolanaSsoClientConfig, SolanaLinkResponse, SolanaPollResponse.

The auth value returned by useSolanaAuth() has this shape (the type itself is internal to the package and not exported):

{ sessionAddress?: string | null; }

Storage Keys

The React package persists linking state in localStorage. Every key carries a short digest suffix derived from (providerAddress, ssoBaseUrl), so two providers or environments mounted on the same origin can’t read each other’s state. Build the exact key names with the exported helpers:

import { getSolanaAuthedAddressKey, getSessionAddressKey, getAttestationCreatedAtKey, } from '@alien-id/sso-solana-react'; const key = getSolanaAuthedAddressKey( 'platform-provider-address', 'https://sso.alien-api.com' ); // 'alien-sso_solana_authed_address_<digest>'
Key patternPurpose
alien-sso_solana_authed_address_<digest>Which wallet address is linked (restores state on page reload)
alien-sso_session_address_<digest>Cached session address
alien-sso_attestation_created_at_<digest>Attestation creation timestamp, for the 60-second grace period

Grace Period Mechanism

The provider implements a 60-second grace period after attestation creation to handle RPC indexing delays:

  1. After successful attestation creation, the session address is cached
  2. The grace-period timestamp is stored in localStorage
  3. During the 60 seconds, verifyAttestation() returns the cached value immediately and schedules a background re-verify (a setTimeout) for the moment the grace period expires
  4. The background re-verify — and any verifyAttestation() call after the 60 seconds — queries the server’s /solana/attestation endpoint again
  5. If that verification fails, the linking state is cleared
// Right after attestation creation: // returns the cached value within 60 seconds const sessionAddress = await verifyAttestation(walletAddress); // After 60 seconds: // queries the attestation again const sessionAddressLater = await verifyAttestation(walletAddress);

Usage Examples

Basic Wallet-Linking Flow

import { AlienSolanaSsoProvider, useSolanaAuth, SolanaSignInButton } from '@alien-id/sso-solana-react'; import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { useWallet } from '@solana/wallet-adapter-react'; import { clusterApiUrl } from '@solana/web3.js'; function App() { const endpoint = clusterApiUrl('mainnet-beta'); return ( <ConnectionProvider endpoint={endpoint}> <WalletProvider wallets={[]} autoConnect> <AlienSolanaSsoProvider config={{ ssoBaseUrl: 'https://sso.alien-api.com', providerAddress: 'platform-provider-address', }} > <Dashboard /> </AlienSolanaSsoProvider> </WalletProvider> </ConnectionProvider> ); } function Dashboard() { const { connected } = useWallet(); const { auth, logout } = useSolanaAuth(); if (!connected) { return ( <div> <h1>Connect Wallet</h1> <WalletMultiButton /> </div> ); } if (!auth.sessionAddress) { return ( <div> <h1>Sign In</h1> <SolanaSignInButton /> </div> ); } return ( <div> <h1>Dashboard</h1> <p>Session: {auth.sessionAddress}</p> <button onClick={logout}>Logout</button> </div> ); }

Protected Route

import { useSolanaAuth } from '@alien-id/sso-solana-react'; import { useWallet } from '@solana/wallet-adapter-react'; import { Navigate } from 'react-router-dom'; function ProtectedRoute({ children }: { children: React.ReactNode }) { const { connected } = useWallet(); const { auth } = useSolanaAuth(); if (!connected) { return <Navigate to="/connect-wallet" replace />; } if (!auth.sessionAddress) { return <Navigate to="/sign-in" replace />; } return <>{children}</>; }

Verify Attestation on Mount

import { useSolanaAuth } from '@alien-id/sso-solana-react'; import { useWallet } from '@solana/wallet-adapter-react'; import { useEffect, useState } from 'react'; function App() { const { publicKey } = useWallet(); const { verifyAttestation, auth } = useSolanaAuth(); const [isChecking, setIsChecking] = useState(true); useEffect(() => { const verify = async () => { if (publicKey) { await verifyAttestation(publicKey.toBase58()); } setIsChecking(false); }; verify(); }, [publicKey]); if (isChecking) { return <div>Checking wallet link...</div>; } return auth.sessionAddress ? <Dashboard /> : <LoginPage />; }

Error Handling

All async methods can throw — wrap them in try/catch:

const { wallet, generateDeeplink, verifyAttestation } = useSolanaAuth(); try { const { deep_link, polling_code } = await generateDeeplink( wallet.publicKey.toBase58() ); } catch (error) { console.error('Failed to generate deep link:', error); } try { const sessionAddress = await verifyAttestation( wallet.publicKey.toBase58() ); } catch (error) { console.error('Attestation verification failed:', error); }

Next Steps

Last updated on