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;
}| Option | Type | Default | Description |
|---|---|---|---|
ssoBaseUrl | string | — | Base URL of the SSO service |
providerAddress | string | — | The platform provider address — Solana SSO is single-tenant; see the single-platform-provider note |
pollingInterval | number | 5000 | Polling interval in milliseconds |
credentialSignerProgramId | string | see core | Credential Signer program ID |
sessionRegistryProgramId | string | see core | Session Registry program ID |
sasProgramId | string | see core | SAS 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'] });generateDeeplink()
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| Option | Type | Default | Description |
|---|---|---|---|
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 viauseSolanaAuth().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 pattern | Purpose |
|---|---|
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:
- After successful attestation creation, the session address is cached
- The grace-period timestamp is stored in localStorage
- During the 60 seconds,
verifyAttestation()returns the cached value immediately and schedules a background re-verify (asetTimeout) for the moment the grace period expires - The background re-verify — and any
verifyAttestation()call after the 60 seconds — queries the server’s/solana/attestationendpoint again - 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
- Core API Reference — client methods, PDA utilities, and types
- React Integration — the guided setup, step by step
- Introduction — the wallet-linking model and attestation lifecycle
- Demo App — example implementation and source code