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-walletsThe 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
endpointselects the Solana cluster — the demo app runs againstdevnet; 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
| Option | Type | Default | Description |
|---|---|---|---|
ssoBaseUrl | string | — | Base URL of the SSO service (required) |
providerAddress | string | — | The platform provider address (required) |
pollingInterval | number | 5000 | Polling interval in milliseconds |
allowInsecureSsoBaseUrl | boolean | false | Dev-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 returnsnullwithout a server call for any other address. To check an arbitrary address, useclient.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.AlienSolanaSsoProvideralready renders it. The component takes no props and is controlled entirely through context: open and close it only viauseSolanaAuth().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>;
}Verify the Link on Mount
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
- API Reference - React — hooks, components, and types
- Core Integration — framework-free flow for any JS/TS project
- Demo App — run the example app