Skip to Content
GuideReact Integration
View .md

Alien Solana SSO React Integration: AlienSolanaSsoProvider Guide

Add Alien wallet linking to a React app with @alien-id/sso-solana-react — a provider, one hook, and a pre-built sign-in button.

Single-platform provider. Solana SSO is single-tenant: the SSO service only serves the configured platform provider. Read how wallet linking works before integrating.

Quick Start

1. Install

npm install @alien-id/sso-solana-react @solana/web3.js @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets

The React SDK declares React 19.1.1+, react-dom, @solana/wallet-adapter-react, and @solana/web3.js as peer dependencies. @solana/wallet-adapter-react-ui and @solana/wallet-adapter-wallets are companion packages used by the setup below — not declared peers, but required for the wallet-adapter flow. The wallet adapter UI also needs its stylesheet — imported in the next step.

2. Wrap your app

Wrap your app with the wallet adapter providers and AlienSolanaSsoProvider:

import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; import { AlienSolanaSsoProvider } from '@alien-id/sso-solana-react'; import { clusterApiUrl } from '@solana/web3.js'; import { useMemo } from 'react'; import '@solana/wallet-adapter-react-ui/styles.css'; function App() { const endpoint = useMemo(() => clusterApiUrl('mainnet-beta'), []); return ( <ConnectionProvider endpoint={endpoint}> <WalletProvider wallets={[]} autoConnect> <WalletModalProvider> <AlienSolanaSsoProvider config={{ ssoBaseUrl: 'https://sso.alien-api.com', providerAddress: 'platform-provider-address', }} > <YourApp /> </AlienSolanaSsoProvider> </WalletModalProvider> </WalletProvider> </ConnectionProvider> ); } export default App;

The provider address comes from the Developer Portal. The endpoint selects the Solana cluster — the demo app runs against devnet; confirm the cluster and RPC endpoint with the platform operator. To complete a link you need the Alien App on a phone with an Alien ID.

3. Add the sign-in button

Gate on a connected wallet, then render SolanaSignInButton:

import { useWallet } from '@solana/wallet-adapter-react'; import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { SolanaSignInButton } from '@alien-id/sso-solana-react'; 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> ); }

Run it — after connecting a wallet you should see the Sign in with Alien button; clicking it opens the linking modal with a QR code.

4. Read the linking state

import { useSolanaAuth } from '@alien-id/sso-solana-react'; function Dashboard() { const { auth, wallet, logout } = useSolanaAuth(); if (!auth.sessionAddress) { return <div>Wallet not linked</div>; } return ( <div> <p>Wallet: {wallet.publicKey?.toBase58()}</p> <p>Session: {auth.sessionAddress}</p> <button onClick={logout}>Logout</button> </div> ); }

The wallet is now linked to a verified Alien ID. A non-null auth.sessionAddress is the linked session identifier.

Configuration

OptionTypeDefaultDescription
ssoBaseUrlstringBase URL of the SSO service (required)
providerAddressstringThe platform provider address (required)
pollingIntervalnumber5000Polling interval in milliseconds
allowInsecureSsoBaseUrlbooleanfalseDev-only escape hatch allowing a non-localhost http:// ssoBaseUrl; otherwise https:// is enforced

The on-chain program IDs (credentialSignerProgramId, sessionRegistryProgramId, sasProgramId) can also be overridden — see the core API reference for the full config shape.

Linking State and Methods

The useSolanaAuth() hook exposes the wallet-linking state and the flow methods:

const { client, // Direct access to AlienSolanaSsoClient instance auth, // Linking state: { sessionAddress: string | null } wallet, // Solana wallet adapter connectionAdapter, // Solana connection from context queryClient, // React Query client instance generateDeeplink, // Generate a linking deep link pollAuth, // Poll for linking status verifyAttestation, // Re-verify a wallet previously linked in this browser logout, // Clear linking state openModal, // Open the built-in sign-in modal closeModal, // Close the sign-in modal isModalOpen // Modal open state } = useSolanaAuth();

Linking is not authentication. verifyAttestation() only re-checks wallets that were previously linked in this browser — it returns null without a server call for any other address. To check an arbitrary address, use client.getAttestation() — but note that anyone who knows a public address gets the same answer, so neither call proves the current visitor controls the wallet. See the introduction.

Control the Sign-In Modal

The modal is rendered automatically by AlienSolanaSsoProvider and handles QR code display, polling for approval, transaction building and signing, and on-chain attestation creation.

Warning: do not render <SolanaSignInModal /> manually. AlienSolanaSsoProvider already renders it. The component takes no props and is controlled entirely through context: open and close it only via useSolanaAuth().openModal() / closeModal(). Rendering a second instance makes the copies fight over shared query state and breaks the sign-in flow.

Open it from your own UI:

function CustomButton() { const { openModal } = useSolanaAuth(); return <button onClick={openModal}>Sign In</button>; }

Re-check the link when your app loads. verifyAttestation() only re-verifies wallets previously linked in this browser — for a wallet with no local linking record it returns null immediately, without calling the server (use client.getAttestation() to check any address):

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

After attestation creation the provider caches the link for a 60-second grace period to absorb RPC indexing delays, so verifyAttestation() returns immediately right after linking. See Grace Period Mechanism for the full behavior and the storage keys involved.

Build a Custom Linking UI

If you want your own UI instead of the built-in modal, drive the flow with generateDeeplink and pollAuth — and finish the link yourself. The modal builds, signs, and sends the attestation transaction only while it is open, so a custom flow must do those steps explicitly:

import { useState, useEffect } from 'react'; import { PublicKey } from '@solana/web3.js'; import { Buffer } from 'buffer'; import { QRCodeSVG } from 'qrcode.react'; function CustomLinkFlow() { const { client, wallet, connectionAdapter, generateDeeplink, pollAuth } = useSolanaAuth(); const [deepLink, setDeepLink] = useState<string | null>(null); const [pollingCode, setPollingCode] = useState<string | null>(null); const handleSignIn = async () => { if (!wallet.publicKey) return; const response = await generateDeeplink(wallet.publicKey.toBase58()); setDeepLink(response.deep_link); setPollingCode(response.polling_code); }; useEffect(() => { if (!pollingCode) return; const interval = setInterval(async () => { const response = await pollAuth(pollingCode); if (response.status === 'authorized') { clearInterval(interval); // Build the attestation transaction locally const connection = connectionAdapter.connection; const transaction = await client.buildCreateAttestationTransaction({ connection, payerPublicKey: wallet.publicKey!, sessionAddress: response.session_address!, oracleSignature: Uint8Array.from(Buffer.from(response.oracle_signature!, 'hex')), oraclePublicKey: new PublicKey(Buffer.from(response.oracle_public_key!, 'hex')), timestamp: response.timestamp!, expiry: 0, }); // Set the blockhash and fee payer, sign, and send promptly const { blockhash } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = wallet.publicKey!; const signedTransaction = await wallet.signTransaction!(transaction); const signature = await connection.sendRawTransaction( signedTransaction.serialize() ); await connection.confirmTransaction(signature); setDeepLink(null); setPollingCode(null); } else if (response.status === 'rejected' || response.status === 'expired') { clearInterval(interval); alert(`Linking ${response.status}`); setDeepLink(null); setPollingCode(null); } }, 5000); return () => clearInterval(interval); }, [pollingCode, pollAuth, client, connectionAdapter, wallet]); if (deepLink) { return ( <div> <h2>Scan QR Code</h2> <QRCodeSVG value={deepLink} /> </div> ); } return <button onClick={handleSignIn}>Sign In with Alien</button>; }

Protect Routes

Gate routes on both a connected wallet and a linked session:

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" />; } if (!auth.sessionAddress) { return <Navigate to="/sign-in" />; } return <>{children}</>; } // Usage <Route path="/dashboard" element={ <ProtectedRoute> <Dashboard /> </ProtectedRoute> } />

Complete Example

import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { AlienSolanaSsoProvider, useSolanaAuth, SolanaSignInButton } from '@alien-id/sso-solana-react'; import { useWallet } from '@solana/wallet-adapter-react'; import { clusterApiUrl } from '@solana/web3.js'; import { useMemo } from 'react'; import '@solana/wallet-adapter-react-ui/styles.css'; function App() { const endpoint = useMemo(() => clusterApiUrl('mainnet-beta'), []); return ( <ConnectionProvider endpoint={endpoint}> <WalletProvider wallets={[]} autoConnect> <WalletModalProvider> <AlienSolanaSsoProvider config={{ ssoBaseUrl: 'https://sso.alien-api.com', providerAddress: 'platform-provider-address', }} > <Dashboard /> </AlienSolanaSsoProvider> </WalletModalProvider> </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> ); } export default App;

TypeScript

The React SDK is fully typed:

import type { AlienSolanaSsoClient, SolanaPollResponse } from '@alien-id/sso-solana'; import type { SolanaWalletAdapter, SolanaConnectionAdapter } from '@alien-id/sso-solana-react'; const client: AlienSolanaSsoClient = useSolanaAuth().client; const wallet: SolanaWalletAdapter = useSolanaAuth().wallet;

Client and response types (AlienSolanaSsoClient, SolanaPollResponse) are exported from @alien-id/sso-solana; the React package exports the SolanaWalletAdapter and SolanaConnectionAdapter adapter interfaces. The auth state and hook return types are inferred from useSolanaAuth() and are not exported.

Next Steps

Last updated on