Skip to content

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/sdk, 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, useSeams, type SeamsConfigsInput } from '@seams/sdk/react';

const seamsConfig = {
  iframeWallet: {
    walletOrigin: import.meta.env.VITE_WALLET_ORIGIN,
    walletServicePath: '/wallet-service',
    sdkBasePath: '/sdk',
  },
  relayerAccount: 'w3a-relayer.testnet',
  relayer: {
    url: import.meta.env.VITE_RELAYER_URL,
  },
  registration: {
    mode: 'managed',
    projectEnvironmentId: import.meta.env.VITE_SEAMS_PROJECT_ENVIRONMENT_ID,
    publishableKey: import.meta.env.VITE_SEAMS_PUBLISHABLE_KEY,
  },
  chains: [
    {
      network: 'near-testnet',
      rpcUrl: 'https://rpc.testnet.near.org',
      explorerUrl: 'https://testnet.nearblocks.io',
    },
  ],
} satisfies SeamsConfigsInput;

function WalletApp() {
  const { loginState } = useSeams();
  return <p>{loginState.isLoggedIn ? 'Wallet unlocked' : 'Wallet locked'}</p>;
}

export function App() {
  return (
    <SeamsWebProvider config={seamsConfig}>
      <WalletApp />
    </SeamsWebProvider>
  );
}

The example reads VITE_WALLET_ORIGIN, VITE_RELAYER_URL, VITE_SEAMS_PROJECT_ENVIRONMENT_ID, and VITE_SEAMS_PUBLISHABLE_KEY from the app environment. Use your own values in each deployment.

Register with a passkey

Render CreateWalletButton or call createPasskeyWallet from your own registration screen.

tsx
import { useSeams } from '@seams/sdk/react';
import type {
  RegistrationHooksOptions,
  RegistrationResult,
  SeamsContextType,
} from '@seams/sdk/react';

type RegisterPasskey = SeamsContextType['registerPasskey'];

function assertNever(value: never): never {
  throw new Error(`Unhandled registration result: ${String(value)}`);
}

function logRegistrationEvent(
  event: Parameters<NonNullable<RegistrationHooksOptions['onEvent']>>[0],
): void {
  console.log(event.phase, event.status, event.message);
}

export function registrationSummary(result: RegistrationResult): string {
  if (!result.success) {
    return `Registration failed: ${result.error}`;
  }

  switch (result.kind) {
    case 'wallet_registered':
      return `Wallet ${result.walletId} registered with ${result.capabilities
        .map((capability) => capability.kind)
        .join(' and ')} capability`;
    case 'wallet_signer_added':
      return `Signer added to wallet ${result.walletId}: ${result.capabilities[0].kind}`;
    case 'ecdsa_wallet_registered_near_pending':
      return `Wallet ${result.walletId} is registered; NEAR provisioning is ${result.nearProvisioning.status}`;
    case 'near_wallet_registered_pending':
      return `Wallet ${result.walletId} is registered; NEAR provisioning is ${result.nearProvisioning.status}`;
    default:
      return assertNever(result);
  }
}

export async function createPasskeyWallet(
  registerPasskey: RegisterPasskey,
): Promise<RegistrationResult> {
  const result = await registerPasskey({ onEvent: logRegistrationEvent });
  if (!result.success) {
    throw new Error(result.error);
  }
  return result;
}

export function CreateWalletButton() {
  const { registerPasskey } = useSeams();

  const onCreateWallet = async (): Promise<void> => {
    const result = await createPasskeyWallet(registerPasskey);
    console.log(registrationSummary(result));
  };

  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.

Unlock an existing wallet

Pass the wallet id from your app's account record to unlockWallet.

ts
import type { LoginAndCreateSessionResult, UnlockFlowEvent } from '@seams/sdk';
import type { SeamsContextType } from '@seams/sdk/react';

type UnlockWallet = SeamsContextType['unlock'];

function assertNever(value: never): never {
  throw new Error(`Unhandled unlock result: ${String(value)}`);
}

function logUnlockEvent(event: UnlockFlowEvent): void {
  console.log(event.phase, event.status, event.message);
}

export async function unlockWallet(
  unlock: UnlockWallet,
  walletId: string,
): Promise<LoginAndCreateSessionResult> {
  const result = await unlock(walletId, { onEvent: logUnlockEvent });
  if (!result.success) {
    throw new Error(result.error);
  }

  switch (result.kind) {
    case 'near_wallet_unlocked':
      console.log('NEAR account ready', result.nearAccountId);
      return result;
    case 'ecdsa_wallet_unlocked':
      console.log('EVM-family wallet ready', result.walletId);
      return result;
    default:
      return assertNever(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 { SeamsWeb } from '@seams/sdk';

type StartResult = Awaited<ReturnType<SeamsWeb['auth']['beginGoogleEmailOtpWalletAuth']>>;
type AuthFlow = Extract<StartResult, { ok: true }>['value'];
type LoginFlow = Extract<AuthFlow, { mode: 'login' }>;
type SubmitResult = Awaited<ReturnType<LoginFlow['submit']>>;
type SubmitSuccess = Extract<SubmitResult, { ok: true }>['value'];

export async function startGoogleEmailOtpLogin(
  seams: SeamsWeb,
  googleIdToken: string,
): Promise<LoginFlow> {
  const started = await seams.auth.beginGoogleEmailOtpWalletAuth({
    idToken: googleIdToken,
    mode: 'login',
  });
  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: LoginFlow,
  otpCode: string,
): Promise<SubmitSuccess> {
  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.