Appearance
Wallet setup and authentication
Use this path when adding Seams to a new app. Configure the provider once, register a wallet, and 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.
tsx
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 takes the three values a wallet cannot start without and defaults the rest — 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, and defineSeamsConfig when you are not on testnet.
Register with a passkey
Render CreateWalletButton or call createPasskeyWallet from your own registration screen.
tsx
import type { RegistrationFlowEvent } from '@seams/wallet';
import { useSeams } from '@seams/wallet/react';
export function CreateWalletButton() {
const { registerPasskey, seams } = useSeams();
const onCreateWallet = async (): Promise<void> => {
const result = await registerPasskey({
onEvent: (event: RegistrationFlowEvent) =>
console.log(event.phase, event.status, event.message),
});
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})`);
// A mixed registration returns before NEAR provisioning finishes, so the
// result carries no NEAR account id yet. Wait for one before signing NEAR.
if (result.kind === 'ecdsa_wallet_registered_near_pending') {
const near = await seams.registration.awaitNearReady({ walletId: result.walletId });
console.log('NEAR provisioning finished:', near.kind);
}
};
return <button onClick={() => void onCreateWallet()}>Create wallet</button>;
}RegistrationResult is a typed union. A successful registration can be ready immediately or can report pending NEAR provisioning; keep the branch handling before reading a chain-specific capability. For the pending branch, await seams.registration.awaitNearReady({ walletId }) rather than polling getNearProvisioningState yourself.
Unlock an existing wallet
Pass the wallet id from your app's account record to unlockWallet.
ts
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 the wallet session used by signing and export flows. 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.
ts
import type {
GoogleEmailOtpWalletAuthLoginFlow,
GoogleEmailOtpWalletAuthSubmitSuccess,
SeamsWeb,
} from '@seams/wallet';
export async function startGoogleEmailOtpLogin(
seams: SeamsWeb,
googleIdToken: string,
): Promise<GoogleEmailOtpWalletAuthLoginFlow> {
const started = await seams.auth.beginGoogleEmailOtpWalletAuth({
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.