本文へ移動

ウォレットの設定と認証

Seamsプロバイダーを設定し、パスキーでウォレットを登録・アンロックするか、GoogleとメールOTPで認証します。

ウォレットの設定と認証 ​

ホスト環境での登録とサインインの準備 ​

登録画面を開いたら、SeamsWebProvider内にHostedRegistrationを表示してください。 その画面が開いている間に、ウォレットとチャレンジを準備します。結果はアプリ側で処理してください。

SignInIntentButtonは、ポインター操作、フォーカス、タッチを契機にサインイン用コードをダウンロードします。 認証メニューを表示するために、onOpenを指定してください。 ダウンロードだけでは、ウォレットの予約やパスキーのチャレンジの準備は行いません。 registerWallet()を直接呼ぶ場合は、その呼び出しの中で準備を開始します。 ホスト環境への導入を参照してください。

tsx
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>
  );
}

新しいアプリにSeamsを追加する際は、この実装例を使ってください。 プロバイダーを一度設定し、ウォレットを登録します。署名前にウォレットセッションを作成してください。

前提条件 ​

@seams/walletをインストールし、設定したウォレットオリジンを配信してください。 設定例にある登録用とリレーヤー用の環境変数を指定してください。

プロバイダーを設定する ​

useSeamsを呼ぶコンポーネントより上に、SeamsWebProviderを配置します。

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には、ウォレットオリジン、リレーヤーURL、公開可能キーが必要です。 ウォレットサービスのパス、SDKのベースパス、リレーヤーアカウント、チェーンのRPC・エクスプローラーURLには既定値を設定します。 この実装例は、アプリ環境からVITE_WALLET_ORIGIN、VITE_RELAYER_URL、VITE_SEAMS_PUBLISHABLE_KEYを読み取ります。 デプロイ先ごとに、自社の値を指定してください。テストネット以外のネットワークにはdefineSeamsConfigを使います。

パスキーで登録する ​

CreateWalletButtonを表示するか、独自の登録画面からseams.registration.registerWalletを呼び出します。

tsx
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は型付きのユニオンです。 登録が成功すると、すぐに利用可能な状態か、NEARのプロビジョニング待ちの状態を返します。 チェーン固有の機能を参照する前に、結果の分岐を確認してください。 待機中の分岐では、seams.registration.waitForNearAccount({ walletId })を待ってください。 getNearAccountStateを独自にポーリングする必要はありません。

既存のウォレットをアンロックする ​

アプリのアカウントレコードにあるウォレットIDを、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;
}

成功すると、再利用可能な署名セッションへのアクセスを得られます。 エクスポートには、操作ごとの新たな承認が必要です。 両方の鍵方式に対応するアプリでは、near_wallet_unlockedとecdsa_wallet_unlockedの両分岐を処理してください。

GoogleとメールOTPで認証する ​

IDプロバイダーのGoogle IDトークンを使い、独自UIでOTPを入力してもらいます。

ts
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には、既存ウォレットのログインフローが必要です。 アカウントの登録が必要な場合、このヘルパーはログインフローをキャンセルし、その状態を通知します。 アプリは、先に登録へ案内できます。

期待する結果 ​

登録は、ウォレットIDと、利用可能または準備待ちの機能を返します。 アンロックは、利用可能なチェーン識別情報を含むウォレットセッションの結果を返します。 メールOTPは、コードの受理後に認証済みログイン結果を返します。

対処可能な失敗 ​

  • パスキーやOTPの入力をキャンセルすると、現在の試行は終了します。同じ画面から再開できるようにしてください。
  • 登録やアンロックの失敗結果には、エラー文字列が含まれます。簡潔なメッセージを表示し、結果にウォレットIDがある場合は保持してください。
  • Googleのログインフローが登録モードの場合は、登録画面へ進めてください。同じウォレット状態でログインを繰り返さないでください。
  • セッションが期限切れか利用枠を使い切った場合は、署名やエクスポートの前に、再度アンロックしてください。

次に、署名、ウォレットの高度な操作、結果とエラーを参照してください。