Wallet setup and authentication
Hosted registration and sign-in intent
Render HostedRegistration inside SeamsWebProvider when registration opens. It prepares the wallet and challenge while that view remains open. Handle its outcome in your application.
SignInIntentButton downloads sign-in code on pointer, focus, or touch. Supply onOpen to mount your auth menu. The download does not reserve a wallet or prepare a passkey challenge. Direct registerWallet() calls start setup inside the call. See hosted integration.
import {
HostedSeamsAuthMenu,
useSeams,
type HostedAuthMenuOutcome,
} from '@seams/wallet/react';
export function HostedRegistration({ onOutcome }: {
onOutcome: (outcome: HostedAuthMenuOutcome) => void;
}) {
return <HostedSeamsAuthMenu initialMode="register" onOutcome={onOutcome} />;
}
export function SignInIntentButton({ onOpen }: { onOpen: () => void }) {
const { seams } = useSeams();
return (
<button
onPointerEnter={seams.authMenu.prepare}
onFocus={seams.authMenu.prepare}
onTouchStart={seams.authMenu.prepare}
onClick={onOpen}
>
Sign in
</button>
);
}Use this example when adding Seams to a new app. Configure the provider once. Register a wallet. Create a wallet session before signing.
Prerequisites
Install @seams/wallet, serve the configured wallet origin, and provide the registration and relayer environment variables shown in the setup example.
Configure the provider
Place SeamsWebProvider above the components that call useSeams.
import { SeamsWebProvider, seamsTestnetConfig, useSeams } from '@seams/wallet/react';
// Everything else — wallet service path, SDK base path, relayer account, chain
// RPC and explorer URLs — comes from the SDK defaults.
const seamsConfig = seamsTestnetConfig({
walletOrigin: import.meta.env.VITE_WALLET_ORIGIN,
relayerUrl: import.meta.env.VITE_RELAYER_URL,
publishableKey: import.meta.env.VITE_SEAMS_PUBLISHABLE_KEY,
});
function WalletApp() {
const { loginState } = useSeams();
return <p>{loginState.isLoggedIn ? 'Wallet unlocked' : 'Wallet locked'}</p>;
}
export function App() {
return (
<SeamsWebProvider config={seamsConfig}>
<WalletApp />
</SeamsWebProvider>
);
}seamsTestnetConfig requires the wallet origin, relayer URL, and publishable key. It supplies defaults for the wallet service path, SDK base path, relayer account, and chain RPC and explorer URLs. The example reads VITE_WALLET_ORIGIN, VITE_RELAYER_URL, and VITE_SEAMS_PUBLISHABLE_KEY from the app environment. Use your own values in each deployment. Use defineSeamsConfig for networks other than testnet.
Register with a passkey
Render CreateWalletButton or call seams.registration.registerWallet from your own registration screen.
import type { RegistrationFlowEvent, SeamsWeb } from '@seams/wallet';
import { useSeams, type SeamsContextType } from '@seams/wallet/react';
function logRegistrationEvent(event: RegistrationFlowEvent): void {
console.log(event.phase, event.status, event.message);
}
function assertNever(value: never): never {
throw new Error(`Unexpected NEAR readiness result: ${JSON.stringify(value)}`);
}
async function createWallet(
registerWallet: SeamsContextType['registerWallet'],
seams: SeamsWeb,
registration: Parameters<SeamsContextType['registerWallet']>[0],
): Promise<void> {
try {
const result = await registerWallet({
...registration,
options: { ...registration.options, onEvent: logRegistrationEvent },
});
if (!result.success) {
console.error('Registration failed:', result.error);
return;
}
// `walletId` is the stable identifier for every later wallet operation.
console.log(`Wallet ${result.walletId} registered (${result.kind})`);
if (
result.kind === 'ecdsa_wallet_registered_near_pending' ||
result.kind === 'near_wallet_registered_pending'
) {
const near = await seams.registration.waitForNearAccount({ walletId: result.walletId });
switch (near.kind) {
case 'near_ready':
console.log('NEAR account ready:', near.nearAccountId);
break;
case 'near_failed_retryable':
console.warn('NEAR provisioning needs a retry:', near.reason);
break;
case 'timed_out':
console.warn('NEAR is still pending. Keep this wallet id and check readiness again.');
break;
default:
assertNever(near);
}
}
} catch (error) {
console.error('Wallet creation interrupted:', error);
}
}
export function CreateWalletButton({ registration }: {
registration: Parameters<SeamsContextType['registerWallet']>[0];
}) {
const { registerWallet, seams } = useSeams();
return (
<button onClick={createWallet.bind(null, registerWallet, seams, registration)}>
Create wallet
</button>
);
}RegistrationResult is a typed union. A successful registration can be ready immediately or report pending NEAR provisioning. Check the result branch before reading a chain-specific capability. For the pending branch, await seams.registration.waitForNearAccount({ walletId }) rather than polling getNearAccountState yourself.
Unlock an existing wallet
Pass the wallet id from your app's account record to unlockWallet.
import type { UnlockFlowEvent } from '@seams/wallet';
import type { SeamsContextType } from '@seams/wallet/react';
export async function unlockWallet(unlock: SeamsContextType['unlock'], walletId: string) {
const result = await unlock(walletId, {
onEvent: (event: UnlockFlowEvent) => console.log(event.phase, event.status, event.message),
});
if (!result.success) {
throw new Error(result.error);
}
// Read `nearAccountId` only from the NEAR branch.
if (result.kind === 'near_wallet_unlocked') {
console.log('NEAR account ready', result.nearAccountId);
} else {
console.log('EVM-family wallet ready', result.walletId);
}
return result;
}The successful result creates reusable signing-session access. Export requires fresh operation-specific authorization. Handle both near_wallet_unlocked and ecdsa_wallet_unlocked branches when the app supports both key families.
Authenticate with Google Email OTP
Use the Google ID token from your identity provider, then collect the OTP in your own UI.
import type {
GoogleEmailOtpWalletAuthLoginFlow,
GoogleEmailOtpWalletAuthSubmitSuccess,
SeamsWeb,
} from '@seams/wallet';
export async function startGoogleEmailOtpLogin(
seams: SeamsWeb,
googleIdToken: string,
): Promise<GoogleEmailOtpWalletAuthLoginFlow> {
const started = await seams.auth.startGoogleSignIn({
idToken: googleIdToken,
mode: 'login',
loginTarget: { kind: 'discoverable' },
});
if (!started.ok) {
throw new Error(started.error.message);
}
if (started.value.mode !== 'login') {
await started.value.cancel();
throw new Error('This Google account needs wallet registration');
}
return started.value;
}
export async function submitGoogleEmailOtp(
flow: GoogleEmailOtpWalletAuthLoginFlow,
otpCode: string,
): Promise<GoogleEmailOtpWalletAuthSubmitSuccess> {
const submitted = await flow.submit({ otpCode });
if (!submitted.ok) {
throw new Error(submitted.error.message);
}
return submitted.value;
}startGoogleEmailOtpLogin requires an existing wallet login flow. If the account needs registration, the helper cancels the login flow and reports that state so the app can send the person through registration first.
Expected result
Registration returns a wallet id and its ready or pending capabilities. Unlock returns a wallet session result with the chain identity that is ready to use. Email OTP returns the authenticated login result after the code is accepted.
Recoverable failures
- A cancelled passkey or OTP prompt ends the current attempt. Let the person start it again from the same screen.
- A failed registration or unlock result includes an error string. Display a concise message and keep the wallet id when the result includes one.
- A Google login flow in registration mode should continue through the registration screen instead of retrying login with the same wallet state.
- An expired or depleted session needs a fresh unlock before signing or export.
Read next: signing, advanced wallet operations, or results and errors.