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
| Method | How it works | Where 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 App | Inside a Telegram Mini App the player is signed in automatically from Telegram's initData — zero taps. | Games launched from a Telegram bot. |
| Email + OTP | Player 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.userandaccessTokenare now non-null; show the game.
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.
"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:
"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:
| Prop | Default | What it does |
|---|---|---|
theme | IAMGame default | Token overrides: primary, background, foreground, muted, surface, border, radius, fontFamily. |
onSignIn | — | Called once a session is created. |
title / subtitle | "Sign in" / auto | Override the header copy. |
showEmail | true | Show the email + code option (needs email enabled for the app). |
autoTelegram | true | Auto-run the Telegram flow on mount when inside a Mini App. |
<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.
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.
"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.
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.
"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 & sign in</button>
</>
);
}Logout
logout() clears the session server-side and locally; status returns to anonymous.
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
- Authentication — verify the session on your backend and mint your own app JWT.
- Address & balances — read the signed-in player’s wallet and funds.