// app/page.tsx
"use client";
import { useEffect, useRef, useState } from "react";
import { setCookie, getCookie } from "cookies-next";
import { SocialLoginProvider } from "@circle-fin/w3s-pw-web-sdk/dist/src/types";
import type { W3SSdk } from "@circle-fin/w3s-pw-web-sdk";
const appId = process.env.NEXT_PUBLIC_CIRCLE_APP_ID as string;
const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID as string;
type LoginResult = {
userToken: string;
encryptionKey: string;
// other fields (refreshToken, oAuthInfo, etc.) are ignored in this quickstart
};
type Wallet = {
id: string;
address: string;
blockchain: string;
[key: string]: unknown;
};
export default function HomePage() {
const sdkRef = useRef<W3SSdk | null>(null);
const [sdkReady, setSdkReady] = useState(false);
const [deviceId, setDeviceId] = useState<string>("");
const [deviceIdLoading, setDeviceIdLoading] = useState(false);
const [deviceToken, setDeviceToken] = useState<string>("");
const [deviceEncryptionKey, setDeviceEncryptionKey] = useState<string>("");
const [loginResult, setLoginResult] = useState<LoginResult | null>(null);
const [loginError, setLoginError] = useState<string | null>(null);
const [challengeId, setChallengeId] = useState<string | null>(null);
const [wallets, setWallets] = useState<Wallet[]>([]);
const [usdcBalance, setUsdcBalance] = useState<string | null>(null);
const [status, setStatus] = useState<string>("Ready");
// Initialize SDK on mount, using cookies to restore config after redirect
useEffect(() => {
let cancelled = false;
const initSdk = async () => {
try {
const { W3SSdk } = await import("@circle-fin/w3s-pw-web-sdk");
const onLoginComplete = (error: unknown, result: any) => {
if (cancelled) return;
if (error) {
const err = error as any;
console.log("Login failed:", err);
setLoginError(err.message || "Login failed");
setLoginResult(null);
setStatus("Login failed");
return;
}
setLoginResult({
userToken: result.userToken,
encryptionKey: result.encryptionKey,
});
setLoginError(null);
setStatus("Login successful. Credentials received from Google.");
};
const restoredAppId = (getCookie("appId") as string) || appId || "";
const restoredGoogleClientId =
(getCookie("google.clientId") as string) || googleClientId || "";
const restoredDeviceToken = (getCookie("deviceToken") as string) || "";
const restoredDeviceEncryptionKey =
(getCookie("deviceEncryptionKey") as string) || "";
const initialConfig = {
appSettings: { appId: restoredAppId },
loginConfigs: {
deviceToken: restoredDeviceToken,
deviceEncryptionKey: restoredDeviceEncryptionKey,
google: {
clientId: restoredGoogleClientId,
redirectUri:
typeof window !== "undefined" ? window.location.origin : "",
selectAccountPrompt: true,
},
},
};
const sdk = new W3SSdk(initialConfig, onLoginComplete);
sdkRef.current = sdk;
if (!cancelled) {
setSdkReady(true);
setStatus("SDK initialized. Ready to create device token.");
}
} catch (err) {
console.log("Failed to initialize Web SDK:", err);
if (!cancelled) {
setStatus("Failed to initialize Web SDK");
}
}
};
void initSdk();
return () => {
cancelled = true;
};
}, []);
// Get / cache deviceId
useEffect(() => {
const fetchDeviceId = async () => {
if (!sdkRef.current) return;
try {
const cached =
typeof window !== "undefined"
? window.localStorage.getItem("deviceId")
: null;
if (cached) {
setDeviceId(cached);
return;
}
setDeviceIdLoading(true);
const id = await sdkRef.current.getDeviceId();
setDeviceId(id);
if (typeof window !== "undefined") {
window.localStorage.setItem("deviceId", id);
}
} catch (error) {
console.log("Failed to get deviceId:", error);
setStatus("Failed to get deviceId");
} finally {
setDeviceIdLoading(false);
}
};
if (sdkReady) {
void fetchDeviceId();
}
}, [sdkReady]);
// Helper to load USDC balance for a wallet
async function loadUsdcBalance(userToken: string, walletId: string) {
try {
const response = await fetch("/api/endpoints", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "getTokenBalance",
userToken,
walletId,
}),
});
const data = await response.json();
if (!response.ok) {
console.log("Failed to load USDC balance:", data);
setStatus("Failed to load USDC balance");
return null;
}
const balances = (data.tokenBalances as any[]) || [];
const usdcEntry =
balances.find((t) => {
const symbol = t.token?.symbol || "";
const name = t.token?.name || "";
return symbol.startsWith("USDC") || name.includes("USDC");
}) ?? null;
const amount = usdcEntry?.amount ?? "0";
setUsdcBalance(amount);
return amount;
} catch (err) {
console.log("Failed to load USDC balance:", err);
setStatus("Failed to load USDC balance");
return null;
}
}
// Helper to load wallets for the current user
const loadWallets = async (
userToken: string,
options?: { source?: "afterCreate" | "alreadyInitialized" },
) => {
try {
setStatus("Loading wallet details...");
setUsdcBalance(null);
const response = await fetch("/api/endpoints", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "listWallets",
userToken,
}),
});
const data = await response.json();
if (!response.ok) {
console.log("List wallets failed:", data);
setStatus("Failed to load wallet details");
return;
}
const wallets = (data.wallets as Wallet[]) || [];
setWallets(wallets);
if (wallets.length > 0) {
// Load USDC balance for the primary wallet
await loadUsdcBalance(userToken, wallets[0].id);
if (options?.source === "afterCreate") {
setStatus(
"Wallet created successfully! 🎉 Wallet details and USDC balance loaded.",
);
} else if (options?.source === "alreadyInitialized") {
setStatus(
"User already initialized. Wallet details and USDC balance loaded.",
);
} else {
setStatus("Wallet details and USDC balance loaded.");
}
} else {
setStatus("No wallets found for this user.");
}
} catch (err) {
console.log("Failed to load wallet details:", err);
setStatus("Failed to load wallet details");
}
};
const handleCreateDeviceToken = async () => {
if (!deviceId) {
setStatus("Missing deviceId");
return;
}
try {
setStatus("Creating device token...");
const response = await fetch("/api/endpoints", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "createDeviceToken",
deviceId,
}),
});
const data = await response.json();
if (!response.ok) {
console.log("Create device token failed:", data);
setStatus("Failed to create device token");
return;
}
setDeviceToken(data.deviceToken);
setDeviceEncryptionKey(data.deviceEncryptionKey);
setCookie("deviceToken", data.deviceToken);
setCookie("deviceEncryptionKey", data.deviceEncryptionKey);
setStatus("Device token created");
} catch (err) {
console.log("Error creating device token:", err);
setStatus("Failed to create device token");
}
};
const handleLoginWithGoogle = () => {
const sdk = sdkRef.current;
if (!sdk) {
setStatus("SDK not ready");
return;
}
if (!deviceToken || !deviceEncryptionKey) {
setStatus("Missing deviceToken or deviceEncryptionKey");
return;
}
// Persist configs so SDK can rehydrate after redirect
setCookie("appId", appId);
setCookie("google.clientId", googleClientId);
setCookie("deviceToken", deviceToken);
setCookie("deviceEncryptionKey", deviceEncryptionKey);
sdk.updateConfigs({
appSettings: {
appId,
},
loginConfigs: {
deviceToken,
deviceEncryptionKey,
google: {
clientId: googleClientId,
redirectUri: window.location.origin,
selectAccountPrompt: true,
},
},
});
setStatus("Redirecting to Google...");
sdk.performLogin(SocialLoginProvider.GOOGLE);
};
const handleInitializeUser = async () => {
if (!loginResult?.userToken) {
setStatus("Missing userToken. Please login with Google first.");
return;
}
try {
setStatus("Initializing user...");
const response = await fetch("/api/endpoints", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "initializeUser",
userToken: loginResult.userToken,
}),
});
const data = await response.json();
if (!response.ok) {
// 155106 = user already initialized
if (data.code === 155106) {
// User already initialized; load wallet details instead of trying to create again
await loadWallets(loginResult.userToken, {
source: "alreadyInitialized",
});
// No challenge to execute when wallet already exists
setChallengeId(null);
return;
}
const errorMsg = data.code
? `[${data.code}] ${data.error || data.message}`
: data.error || data.message;
setStatus("Failed to initialize user: " + errorMsg);
return;
}
// Successful initialization → get challengeId
setChallengeId(data.challengeId);
setStatus(`User initialized. challengeId: ${data.challengeId}`);
} catch (err) {
const error = err as any;
if (error?.code === 155106 && loginResult?.userToken) {
await loadWallets(loginResult.userToken, {
source: "alreadyInitialized",
});
setChallengeId(null);
return;
}
const errorMsg = error?.code
? `[${error.code}] ${error.message}`
: error?.message || "Unknown error";
setStatus("Failed to initialize user: " + errorMsg);
}
};
const handleExecuteChallenge = () => {
const sdk = sdkRef.current;
if (!sdk) {
setStatus("SDK not ready");
return;
}
if (!challengeId) {
setStatus("Missing challengeId. Initialize user first.");
return;
}
if (!loginResult?.userToken || !loginResult?.encryptionKey) {
setStatus("Missing login credentials. Please login again.");
return;
}
sdk.setAuthentication({
userToken: loginResult.userToken,
encryptionKey: loginResult.encryptionKey,
});
setStatus("Executing challenge...");
sdk.execute(challengeId, (error) => {
const err = (error || {}) as any;
if (error) {
console.log("Execute challenge failed:", err);
setStatus(
"Failed to execute challenge: " + (err?.message ?? "Unknown error"),
);
return;
}
setStatus("Challenge executed. Loading wallet details...");
void (async () => {
// small delay to give Circle time to index the wallet
await new Promise((resolve) => setTimeout(resolve, 2000));
// Challenge consumed; clear it and load wallet details (and balance)
setChallengeId(null);
await loadWallets(loginResult.userToken, { source: "afterCreate" });
})().catch((e) => {
console.log("Post-execute follow-up failed:", e);
setStatus("Wallet created, but failed to load wallet details.");
});
});
};
const primaryWallet = wallets[0];
return (
<main>
<div style={{ width: "50%", margin: "0 auto" }}>
<h1>Create a user wallet with Google social login</h1>
<p>Follow the buttons below to complete the flow:</p>
<div>
<button
onClick={handleCreateDeviceToken}
style={{ margin: "6px" }}
disabled={!sdkReady || !deviceId || deviceIdLoading}
>
1. Create device token
</button>
<br />
<button
onClick={handleLoginWithGoogle}
style={{ margin: "6px" }}
disabled={!deviceToken || !deviceEncryptionKey}
>
2. Login with Google
</button>
<br />
<button
onClick={handleInitializeUser}
style={{ margin: "6px" }}
disabled={!loginResult || wallets.length > 0}
>
3. Initialize user (get challenge)
</button>
<br />
<button
onClick={handleExecuteChallenge}
style={{ margin: "6px" }}
disabled={!challengeId || wallets.length > 0}
>
4. Create wallet (execute challenge)
</button>
</div>
<p>
<strong>Status:</strong> {status}
</p>
{loginError && (
<p style={{ color: "red" }}>
<strong>Error:</strong> {loginError}
</p>
)}
{primaryWallet && (
<div style={{ marginTop: "12px" }}>
<h2>Wallet details</h2>
<p>
<strong>Address:</strong> {primaryWallet.address}
</p>
<p>
<strong>Blockchain:</strong> {primaryWallet.blockchain}
</p>
{usdcBalance !== null && (
<p>
<strong>USDC balance:</strong> {usdcBalance}
</p>
)}
</div>
)}
<pre
style={{
whiteSpace: "pre-wrap",
wordBreak: "break-all",
lineHeight: "1.8",
marginTop: "16px",
}}
>
{JSON.stringify(
{
deviceId,
deviceToken,
deviceEncryptionKey,
userToken: loginResult?.userToken,
encryptionKey: loginResult?.encryptionKey,
challengeId,
wallets,
usdcBalance,
},
null,
2,
)}
</pre>
</div>
</main>
);
}
Prerequisites
Before you begin this tutorial, ensure you’ve:Console → Keys → Create a key → API key → Standard Key.
Step 1. Configure the Google Console
Set up Google OAuth so users can sign in to your app with their Google account.
Click Create after agreeing to the policies.App name: for example, “Social Login App”User support email: select your emailAudience: select ExternalContact email addresses: type your email againApplication type: select Web applicationClient name: for example, “Web client 1”Authorized redirect URIs: typehttp://localhost:3000 Step 2. Configure the Circle Console
Connect your Google OAuth client to your Circle Wallets configuration so users can sign in through your app, and copy the App ID that identifies your user-controlled wallets configuration.Paste your Google OAuth Client ID (from Step 1) into the Client ID (Web) field.
Step 3. Create the web application
Build a Next.js app that authenticates users through Google OAuth and creates a wallet for each authenticated user. 3.1. Create the Next.js project
In your terminal: 3.2. Install dependencies
Install the user-controlled wallets Web SDK and supporting packages: 3.3. Add environment variables
Create a.env.localfile in the project directory:.env.localfile and add the following:YOUR_CIRCLE_API_KEYis your Circle Developer API key.YOUR_GOOGLE_WEB_CLIENT_IDis the Google OAuth Client ID created in Step 1.YOUR_CIRCLE_APP_IDis the Circle Wallet App ID obtained in Step 2. 3.4. Simplify the default layout
Replace the contents ofapp/layout.tsxwith the minimal layout below:app/layout.tsxfile, but the default one created bycreate-next-appincludes fonts and styling that can cause build errors in some environments. 3.5. Add unified backend route
Create a file namedapp/api/endpoints/route.tsand add the code below:createDeviceTokeninitializeUserchallengeIdrequired for wallet creation.listWalletsgetTokenBalancelistWalletsandgetTokenBalancedirectly for simplicity. In production, apps typically store wallet and balance data in a backend database and keep it in sync using Circle webhooks for scalability. 3.6. Add UI and frontend code
Replace the contents ofapp/page.tsxwith the code below:deviceId, which identifies the user’s browser. Your backend exchanges thedeviceIdfor temporary verification tokens (deviceToken,deviceEncryptionKey) used by the Web SDK to allow Google authentication.userTokenandencryptionKey, which together represent an authenticated Circle user session.userToken. If the user hasn’t created a wallet yet, Circle returns achallengeIdto create one. If the user is already initialized, the app loads the existing wallet instead.challengeId. The user approves the action, and Circle creates the wallet.Wallets → User Controlled → Users.
Step 5. Fund the wallet
Fund the new wallet manually through the Circle Faucet and confirm the updated balance in the app.0x...) from the web app UI.Note: Use the same Google account to show the same wallet.
Prerequisites
Before you begin this tutorial, ensure you’ve:Console → Keys → Create a key → API key → Standard Key.
Step 1. Get Mailtrap SMTP credentials
Copy SMTP credentials from Mailtrap so Circle can send OTP codes to users.- Host
- Port
- Username
- Password
You need them for the next step. Step 2. Configure the Circle Console
Add your Mailtrap SMTP credentials to the Circle Console so Circle can send OTP emails, and copy the App ID that identifies your user-controlled wallets configuration.no-reply@example.com). Step 3. Create the web application
Build a Next.js app that authenticates users with an OTP sent to their email and creates a wallet for each authenticated user. 3.1. Create the Next.js project
In your terminal: 3.2. Install dependencies
Install the user-controlled wallets Web SDK: 3.3. Add environment variables
Create a.env.localfile in your project directory:.env.localfile and add the following:YOUR_CIRCLE_API_KEYis your Circle Developer API key.YOUR_CIRCLE_APP_IDis the Circle Wallet App ID obtained in Step 2. 3.4. Simplify the default layout
Replace the contents ofapp/layout.tsxwith the minimal layout below:app/layout.tsxfile, but the default one created bycreate-next-appincludes fonts and styling that can cause build errors in some environments. 3.5. Add unified backend route
Create a file namedapp/api/endpoints/route.tsand add the code below:requestEmailOtpinitializeUserchallengeIdrequired for wallet creation.listWalletsgetTokenBalancelistWalletsandgetTokenBalancedirectly for simplicity. In production, apps typically store wallet and balance data in a backend database and keep it in sync using Circle webhooks for scalability. 3.6. Add UI and frontend code
Replace the contents ofapp/page.tsxwith the code below:deviceId, which identifies the user’s browser. Your backend sends thedeviceIdand email address to Circle, which emails an OTP code and returns temporary verification tokens (deviceToken,deviceEncryptionKey,otpToken) used by the Web SDK to verify the OTP.userTokenandencryptionKey. Together, they enable an authenticated Circle user session.userToken. If the user hasn’t created a wallet yet, Circle returns achallengeIdto create one. If the user is already initialized, the app loads the existing wallet instead.challengeId. The user approves the action, and Circle creates the wallet.Wallets → User Controlled → Users.
Step 5. Fund the wallet
Fund the new wallet manually through the Circle Faucet and confirm the updated balance in the app.0x...) from the web app UI.Note: Use the same email address to show the same wallet.
Prerequisites
Before you begin this tutorial, ensure you’ve:Console → Keys → Create a key → API key → Standard Key.
Step 1. Get your Circle App ID
Copy the App ID that identifies your user-controlled wallet configuration in the Circle Developer Console, so it can manage the User IDs created by your app. Step 2. Create the web application
Build a Next.js app that lets users set an authorization PIN and use it to create a user-controlled wallet. 2.1. Create the Next.js project
In your terminal: 2.2. Install dependencies
Install the user-controlled wallets Web SDK: 2.3. Add environment variables
Create a.env.localfile in your project directory:.env.localfile and add the following:YOUR_CIRCLE_API_KEYis your Circle Developer API key.YOUR_CIRCLE_APP_IDis the Circle Wallet App ID obtained in Step 1. 2.4. Simplify the default layout
Replace the contents ofapp/layout.tsxwith the minimal layout below:app/layout.tsxfile, but the default one created bycreate-next-appincludes fonts and styling that can cause build errors in some environments. 2.5. Add unified backend route
Create a file namedapp/api/endpoints/route.tsand add the code below:createUseruserId.getUserTokenuserTokenandencryptionKeyrequired by the Web SDK to run PIN and wallet challenges.initializeUserchallengeIdrequired for wallet creation.listWalletsgetTokenBalancelistWalletsandgetTokenBalancedirectly for simplicity. In production, apps typically store wallet and balance data in a backend database and keep it in sync using Circle webhooks for scalability. 2.6. Add UI and frontend code
Replace the contents ofapp/page.tsxwith the code below:userId, retrieves auserToken, and initializes the user’s wallet on a specified blockchain. The returnedchallengeIdis executed using the SDK, prompting the user to authorize the action with a PIN. After wallet creation, the app loads the wallet details and USDC balance.userId. Circle associates it with your Console account and user-controlled wallet configuration based on your API key.userTokenandencryptionKey. The Web SDK uses these credentials to authenticate the user session with Circle and submit subsequent user-scoped challenges.userToken. If the user hasn’t created a wallet yet, Circle returns achallengeIdrequired to create one. If the user is already initialized, the app loads the existing wallet instead.challengeId. Because wallet creation is a sensitive action, Circle opens a hosted UI where the user sets their authorization PIN and security questions. After the user approves the request, Circle creates the wallet.Wallets → User Controlled → Users.
Step 4. Fund the wallet
Fund the new wallet manually through the Circle Faucet and confirm the updated balance in the app.0x...) from the web app UI.Note: Use the same email address to show the same wallet.