Login & sign-in

Getting a player signed in is the first thing your game does. IAMGame Wallet supports three sign-in methods, and you don’t have to build UI for any of them — drop in WalletLogin and you’re done. When you want your own look, the same three methods are exposed as async functions on useWalletAuth.

This page assumes the provider is already mounted — see Setup.

The three sign-in methods

MethodHow it worksWhere it shines
External wallet (SIWS)Player connects Phantom / Solflare / Backpack and signs a challenge message (Sign-In-With-Solana). No password.Crypto-native players on web.
Telegram Mini AppInside a Telegram Mini App the player is signed in automatically from Telegram's initData — zero taps.Games launched from a Telegram bot.
Email + OTPPlayer enters an email, gets a 6-digit code, and exchanges it for a session.Players with no wallet, on web.

Email and Telegram must be enabled in your app’s allowed auth methods in the portal, or those calls are rejected server-side. External-wallet sign-in works out of the box.

The auth status lifecycle

useWalletAuth().status is the single source of truth for “is someone signed in?”. It moves through three states:

  • “loading” — the provider is bootstrapping a stored session. Render a spinner; don’t show login or game UI yet.
  • “anonymous” — no session. Show your login UI.
  • “authenticated” — signed in. user and accessToken are now non-null; show the game.
Always branch on status
Don’t treat user === null as “logged out” — during loading it’s null too, and you’d flash the login screen on every reload. Gate on status.

Option A — drop in the prebuilt component

WalletLogin renders the whole sign-in card: the wallet picker, the Telegram flow when inside Telegram, and the email + code form. It renders nothing once the player is authenticated, so you can leave it mounted and just render your account UI alongside it.

TS
"use client";

import { WalletLogin, useWalletAuth } from "@iamgame/wallet-sdk";

export function SignInGate({ children }: { children: React.ReactNode }) {
  const { status } = useWalletAuth();

  if (status === "loading") return <Spinner />;
  if (status === "anonymous") {
    return <WalletLogin onSignIn={() => console.log("signed in")} />;
  }
  return <>{children}</>; // authenticated
}

Prefer an overlay? WalletLoginModal is the same content as a controlled modal — portal, backdrop, Esc-to-close, focus trap. You own the isOpen state:

TS
"use client";

import { useState } from "react";
import { WalletLoginModal, useWalletAuth } from "@iamgame/wallet-sdk";

export function ConnectButton() {
  const { status } = useWalletAuth();
  const [open, setOpen] = useState(false);
  if (status === "authenticated") return null;

  return (
    <>
      <button onClick={() => setOpen(true)}>Connect</button>
      <WalletLoginModal
        isOpen={open}
        onClose={() => setOpen(false)}
        onSignIn={() => setOpen(false)}   // auto-closes on success anyway
      />
    </>
  );
}

Theming & props

Both components take the same content props:

PropDefaultWhat it does
themeIAMGame defaultToken overrides: primary, background, foreground, muted, surface, border, radius, fontFamily.
onSignInCalled once a session is created.
title / subtitle"Sign in" / autoOverride the header copy.
showEmailtrueShow the email + code option (needs email enabled for the app).
autoTelegramtrueAuto-run the Telegram flow on mount when inside a Mini App.
TS
<WalletLogin
  title="Sign in to play"
  subtitle="Connect a wallet or use your email."
  theme={{ primary: "#7c3aed", radius: "0.75rem" }}
/>

Option B — build your own UI with useWalletAuth

Want your own buttons? useWalletAuth hands you the same three methods as async functions plus the current state. Use async/await and try/catch around each call — every method can throw if the player cancels or verification fails.

TS
const {
  status,          // "loading" | "anonymous" | "authenticated"
  user,            // IUser | null
  accessToken,     // string | null — hand to YOUR backend to verify
  connectExternal, // (adapter) => Promise<void>   SIWS
  connectTelegram, // () => Promise<void>          Telegram Mini App
  requestEmailOtp, // (email) => Promise<{ expiresAt }>
  connectEmail,    // (email, code) => Promise<void>
  logout,          // () => Promise<void>
} = useWalletAuth();

External wallet (SIWS)

Pass one of the exported adapters to connectExternal. The SDK requests a challenge, the adapter prompts the player to sign it, and a session is created on success.

TS
"use client";

import { useWalletAuth, phantomAdapter, solflareAdapter, backpackAdapter } from "@iamgame/wallet-sdk";

export function WalletButtons() {
  const { connectExternal } = useWalletAuth();

  const connect = async (adapter: () => ReturnType<typeof phantomAdapter>) => {
    try {
      await connectExternal(adapter());
    } catch (err) {
      // player rejected the signature, or the wallet isn't installed
      console.error(err);
    }
  };

  return (
    <div>
      <button onClick={() => connect(phantomAdapter)}>Phantom</button>
      <button onClick={() => connect(solflareAdapter)}>Solflare</button>
      <button onClick={() => connect(backpackAdapter)}>Backpack</button>
    </div>
  );
}

Prefer to render only wallets the player actually has installed? Use listSupportedWallets() to get descriptors with a detected flag and a buildAdapter() factory — that’s exactly what WalletLogin does internally.

Telegram Mini App

With the default provider setup, Telegram players are signed in automatically — you usually write no code. If you turned autoTelegram off, or want a manual retry button, call connectTelegram(). It reads window.Telegram.WebApp.initData and throws if you’re not inside a Mini App.

TS
import { useWalletAuth, useIsTelegram } from "@iamgame/wallet-sdk";

const inTelegram = useIsTelegram();
const { connectTelegram } = useWalletAuth();

if (inTelegram) {
  // only reachable inside a Telegram Mini App
  await connectTelegram();
}

Email + OTP

Two steps: request a code, then verify it. The email flow is a small state machine.

TS
"use client";

import { useState } from "react";
import { useWalletAuth } from "@iamgame/wallet-sdk";

export function EmailLogin() {
  const { requestEmailOtp, connectEmail } = useWalletAuth();
  const [email, setEmail] = useState("");
  const [code, setCode] = useState("");
  const [sent, setSent] = useState(false);

  const sendCode = async () => {
    try {
      await requestEmailOtp(email.trim());   // → { expiresAt }
      setSent(true);
    } catch (err) {
      console.error("couldn't send code", err);
    }
  };

  const verify = async () => {
    try {
      await connectEmail(email.trim(), code.trim());  // creates the session
    } catch (err) {
      console.error("bad or expired code", err);
    }
  };

  if (!sent) {
    return (
      <>
        <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@email.com" />
        <button onClick={sendCode}>Email me a code</button>
      </>
    );
  }
  return (
    <>
      <input value={code} onChange={(e) => setCode(e.target.value)} placeholder="123456" />
      <button onClick={verify}>Verify &amp; sign in</button>
    </>
  );
}

Logout

logout() clears the session server-side and locally; status returns to anonymous.

TS
const { logout } = useWalletAuth();

<button onClick={() => logout()}>Sign out</button>

After sign-in: trust it on your backend

A client session is not proof of identity on its own. Before you credit funds or trust a wallet address, forward accessToken to your backend and verify it server-to-server with your secret key. That handoff — and the full auth deep dive — lives in Authentication.

Where to go next