diff --git a/CLAUDE.md b/CLAUDE.md index ef0ce48..9cf0cd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,3 +71,51 @@ Append `use context7` to your prompt when asking about Semaphore (e.g., "create - `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` — WalletConnect project ID - `NEXT_PUBLIC_ALCHEMY_API_KEY` — Alchemy API key (used for Base and Base Sepolia RPCs) - `E2E_PRIVATE_KEY` — Private key for e2e headless wallet (in .env.local, never committed) + +## Documentation standards + +These rules apply to all files in the `docs/` folder. + +### Writing + +- Use second-person voice ("you"). +- Assume the reader has a crypto wallet (MetaMask) and understands transactions, gas, and network switching. Don't explain these basics. +- List prerequisites at the start of procedural content. +- Add language tags on all fenced code blocks. +- Add alt text on all images. +- Use relative paths for internal links. +- Use sentence case for all headings (capitalize only the first word and proper nouns). +- Prefer active voice and direct language. +- Remove unnecessary words while maintaining clarity. +- Break complex instructions into clear numbered steps. +- Use broadly applicable examples rather than overly specific business cases. + +### Language and tone + +- No promotional language. +- Limit conjunction overuse (moreover, furthermore, additionally). +- No editorializing ("it's important to note", "in conclusion"). +- No undue emphasis on routine concepts. + +### Formatting + +- Use bold, italics, and other formatting only when it serves understanding, not visual appeal. +- Clean structure with no emoji or decorative elements. + +### Content strategy + +- Document just enough for user success. +- Prioritize accuracy and usability. +- Make content evergreen where possible. +- Search existing docs before adding new content to avoid duplication. +- Start with the smallest reasonable change. + +## Documentation roadmap + +Three integration guides in `docs/`: + +1. **Register a new app** (`guide-register-app.md`) — register via App Manager, view app settings +2. **Integrate BringID SDK** (planned) — add the `bringid` package to a frontend +3. **Integrate BringID smart contract** (planned) — call CredentialRegistry directly + +Registering an app is a prerequisite for SDK usage. The App Manager also handles app management (status, recovery timelock, admin transfer) and custom scorer configuration, covered in the existing `guide-set-custom-scores.md` and `guide-verify-integration.md`. diff --git a/README.md b/README.md index 9446c85..09303ea 100644 --- a/README.md +++ b/README.md @@ -56,5 +56,7 @@ The network switcher in the header allows switching between networks at any time ## Documentation -- [Guide: Creating an App & Setting a Custom Scorer](docs/guide-create-app-and-custom-scorer.md) +- [Guide: Register an app](docs/guide-register-app.md) +- [Guide: Set custom scores](docs/guide-set-custom-scores.md) +- [Guide: Verify your integration](docs/guide-verify-integration.md) - [Video Walkthrough](e2e/videos/walkthrough.mp4) diff --git a/docs/guide-create-app-and-custom-scorer.md b/docs/guide-create-app-and-custom-scorer.md deleted file mode 100644 index 00135b7..0000000 --- a/docs/guide-create-app-and-custom-scorer.md +++ /dev/null @@ -1,221 +0,0 @@ -# Guide: Creating an App & Setting a Custom Scorer - -This guide walks through registering a new app on the BringID CredentialRegistry and deploying a custom scorer with personalized credential group scores. - -> **Video walkthrough:** See the full UI flow in action — [walkthrough.mp4](../e2e/videos/walkthrough.mp4) - -## Prerequisites - -- A wallet (MetaMask, Coinbase Wallet, etc.) connected to **Base** (mainnet) or **Base Sepolia** (testnet) -- Some ETH on the target chain for gas fees -- The App Manager running at [https://manager.bringid.org](https://manager.bringid.org) (or `http://localhost:3000` for local development) - ---- - -## Step 1: Connect Your Wallet - -Navigate to the App Manager. You'll land on the **My Apps** page, which prompts you to connect your wallet. - -![My Apps - Disconnected](../e2e/screenshots/annotated/01-my-apps-disconnected.png) - -Click **Connect Wallet** in the top-right corner. Select your wallet provider from the modal (MetaMask, Coinbase Wallet, WalletConnect, etc.) and approve the connection. - -> Make sure your wallet is on the correct network (Base for production, Base Sepolia for testing). - ---- - -## Step 2: Register a New App - -Click **Register App** in the navigation bar to open the registration form. - -![Register App Form](../e2e/screenshots/annotated/02-register-app-form.png) - -### Configure the Recovery Timelock - -The **Recovery Timelock** determines how long admin recovery actions take. Choose a preset or enter a custom value in seconds: - -| Preset | Seconds | -|-----------|------------| -| 1 day | 86,400 | -| 1 week | 604,800 | -| 1 month | 2,592,000 | -| 3 months | 7,776,000 | -| 6 months | 15,552,000 | -| 1 year | 31,536,000 | -| Disabled | 0 | - -Select a timelock value. For this example, we choose **1 day** (86,400 seconds): - -![Timelock Selected](../e2e/screenshots/annotated/03-register-app-timelock-selected.png) - -### Submit the Transaction - -Click **Register App**. Your wallet will prompt you to confirm the transaction. The button will show: - -1. **"Confirm in wallet..."** — waiting for you to approve in your wallet -2. **"Confirming..."** — transaction submitted, waiting for on-chain confirmation -3. **"Confirmed!"** — transaction mined successfully - -### Success - -Once confirmed, a success banner appears showing your new **App ID**: - -> **App Registered!** -> Your App ID is **3** -> Save this ID — you'll need it to manage your app. - -You'll see two options: -- **Go to App Settings** — navigate to your app's management page -- **Register Another** — register an additional app - -Click **Go to App Settings** to continue. - ---- - -## Step 3: View App Settings - -The App Settings page (`/apps/{appId}`) shows your app's full configuration: - -![App Settings](../e2e/screenshots/annotated/04-app-settings.png) - -- **Status** — Active or Suspended, with a toggle button -- **Recovery Timelock** — current value with an option to update -- **Admin Transfer** — transfer admin rights to another address (irreversible) -- **Scorer Configuration** — which scorer your app uses - -By default, new apps use the **BringID Default Scorer**. To customize scoring, you need to deploy and set a custom scorer. - -From the Scorer Configuration section, click **"Set Custom Scores"**. - ---- - -## Step 4: Deploy a Custom Scorer - -The Deploy Custom Scorer page (`/apps/{appId}/scorer/deploy`) guides you through a **3-step wizard**: - -![Deploy Scorer Wizard](../e2e/screenshots/annotated/05-deploy-scorer.png) - -### Step 4a: Deploy Scorer Contract - -Click **Deploy New Scorer**. This calls the `ScorerFactory.create()` contract, deploying a new `DefaultScorer` instance owned by your wallet. - -Confirm the transaction in your wallet. Once mined, the wizard advances to Step 2 and displays your new scorer's contract address. - -> **Tip:** If you previously deployed a scorer, it will appear in the "You already have N deployed scorer(s)" section with a **Reuse** button, letting you skip this step. - -### Step 4b: Set Scorer on App - -The wizard shows: *"Scorer deployed at `0x47e5...7bf5`"* - -Click **Set App Scorer**. This calls `CredentialRegistry.setAppScorer(appId, scorerAddress)` to wire the new scorer to your app. - -Confirm the transaction. Once mined, the wizard advances to Step 3. - -### Step 4c: Done - -A yellow warning banner indicates: - -> **Almost done — set your scores** -> Your custom scorer is deployed but all scores are currently set to **0**. You need to set scores before your app can calculate humanity scores. - -Click **Set Scores** to configure your custom scores. - ---- - -## Step 5: Set Custom Scores - -The Manage Scores page (`/apps/{appId}/scorer/manage`) displays all 15 credential groups in an editable table: - -![Manage Custom Scores](../e2e/screenshots/annotated/06-manage-scores.png) - -### Understanding the Score Table - -| Column | Description | -|--------|-------------| -| **ID** | Credential group ID (1-15) | -| **Credential** | Human-readable name (e.g., Farcaster Low, GitHub High, zkPassport) | -| **Status** | Whether the credential group is Active | -| **Validity** | How long a credential proof remains valid (30d, 60d, 90d, 180d) | -| **Default Score** | The BringID default score for reference | -| **Custom Score** | Your custom score — editable input field. Header includes **Copy defaults** and **Reset** text links. | - -### Available Credential Groups - -| ID | Credential | Default Score | Validity | -|----|-----------|---------------|----------| -| 1 | Farcaster (Low) | 2 | 30d | -| 2 | Farcaster (Medium) | 5 | 60d | -| 3 | Farcaster (High) | 10 | 90d | -| 4 | GitHub (Low) | 2 | 30d | -| 5 | GitHub (Medium) | 5 | 60d | -| 6 | GitHub (High) | 10 | 90d | -| 7 | X / Twitter (Low) | 2 | 30d | -| 8 | X / Twitter (Medium) | 5 | 60d | -| 9 | X / Twitter (High) | 10 | 90d | -| 10 | zkPassport | 20 | 180d | -| 11 | Self | 20 | 180d | -| 12 | Uber Rides | 10 | 180d | -| 13 | Apple Subs | 10 | 180d | -| 14 | Binance KYC | 20 | 180d | -| 15 | OKX KYC | 20 | 180d | - -### Edit and Save Scores - -1. Click **Copy defaults** in the Custom Score column header to pre-fill all fields with BringID's default scores, or enter your desired scores manually. For example: - - Farcaster (Low): `5` - - Farcaster (Medium): `10` - - Farcaster (High): `20` - -2. The **Save** button updates to show how many scores you've changed: **"Save 3 Score(s)"** - -3. Click the Save button. This calls `DefaultScorer.setScores(ids[], scores[])` in a single batch transaction. - -4. Confirm in your wallet. Once mined, you'll see **"Transaction confirmed."** and the table refreshes with your new scores. - -> **Reset** — Click the Reset link in the Custom Score column header to discard all unsaved changes. - ---- - -## Step 6: Verify Your Integration - -After saving scores, click **Check Integration** at the bottom of the Manage Scores page. This opens the **SDK Demo** page pre-configured with your app. - -![SDK Demo](../e2e/screenshots/annotated/08-demo-page.png) - -The Demo page lets you test: - -- **verifyHumanity** — Start the BringID humanity verification flow (opens a modal) -- **verifyProofs** — Verify on-chain proofs and see the score breakdown by credential group - ---- - -## Score Explorer (Reference) - -The **Score Explorer** page (`/scores`) provides a read-only view of all credential groups and their default scores from the BringID DefaultScorer: - -![Score Explorer](../e2e/screenshots/annotated/07-score-explorer.png) - -Use this as a reference when deciding how to set your custom scores. - ---- - -## Summary - -| Step | Action | Contract Call | -|------|--------|---------------| -| 1 | Connect wallet | — | -| 2 | Register app with timelock | `CredentialRegistry.registerApp(timelock)` | -| 3 | View app settings | `CredentialRegistry.apps(appId)` (read) | -| 4a | Deploy custom scorer | `ScorerFactory.create()` | -| 4b | Wire scorer to app | `CredentialRegistry.setAppScorer(appId, scorer)` | -| 5 | Set custom scores | `DefaultScorer.setScores(ids[], scores[])` | -| 6 | Test integration | BringID SDK | - -### Contract Addresses (Base & Base Sepolia) - -| Contract | Address | -|----------|---------| -| Semaphore | `0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D` | -| CredentialRegistry | `0x17a22f130d4e1c4ba5C20a679a5a29F227083A62` | -| Default Scorer | `0x6791B588dAdeb4323bc1C3d987130bC13cBe3625` | -| Scorer Factory | `0x016bC46169533a8d3284c5D8DD590C91783C8C06` | diff --git a/docs/guide-register-app.md b/docs/guide-register-app.md new file mode 100644 index 0000000..98b7a51 --- /dev/null +++ b/docs/guide-register-app.md @@ -0,0 +1,41 @@ +# Register a new app + +Go to the [BringID App Manager](https://manager.bringid.org/apps/new) to register a new app and get the App ID required for integration. + +## Register app onchain + +![Registration form with recovery timelock fields](../e2e/screenshots/02-register-app-form.png) + +1. Connect a crypto wallet. Your connected wallet becomes the app admin. +2. Set the recovery timelock (see below). +3. Click **Create App** and confirm the transaction in your wallet. Apps are registered onchain in the BringID CredentialRegistry contract. + +### Recovery timelock + +Recovery lets users replace the key proving ownership of their credentials. If a user loses access to their wallet, they can re-authenticate through a verification flow to link a new key. The timelock sets a waiting period between initiating and finalizing recovery — during this window, the user cannot generate proofs with either key, which prevents double-spend. + +Choose a preset (1 day to 1 year) or enter a custom value in seconds. Setting the value to 0 disables key recovery entirely. + +The right value depends on how often users interact with your app: + +- Faucet dispensing tokens every 24 hours — set to 1 day +- Weekly promotion where unique users register within a week — set to 1 week +- One-time airdrop claimed by unique humans — disable recovery (set to 0) + +This setting can be changed later from the app settings page. + +## Get App ID + +After the transaction confirms, a success banner displays your new App ID. + +![Success banner showing the new App ID](../e2e/screenshots/03b-register-app-success.png) + +Copy and pass the App ID to the BringID SDK to integrate it with your app. + +## What's next + +Manage app settings, transfer admin to another wallet, or set custom scores on the settings page. + +--- + +Next: [Set custom scores](guide-set-custom-scores.md) diff --git a/docs/guide-set-custom-scores.md b/docs/guide-set-custom-scores.md new file mode 100644 index 0000000..8d3eaaf --- /dev/null +++ b/docs/guide-set-custom-scores.md @@ -0,0 +1,60 @@ +# Guide: Set custom scores + +Deploy a custom scorer and configure per-credential-group scores for your app. + +--- + +## Step 1: Deploy a custom scorer + +The Deploy Custom Scorer page (`/apps/{appId}/scorer/deploy`) guides you through a **3-step wizard**: + +![Deploy Scorer Wizard](../e2e/screenshots/annotated/05-deploy-scorer.png) + +### Step 1a: Deploy scorer contract + +Click **Deploy New Scorer**. This deploys a new `DefaultScorer` instance via `ScorerFactory.create()`. Once confirmed, the wizard advances and displays your scorer's contract address. + +> **Tip:** If you previously deployed a scorer, it will appear in the "You already have N deployed scorer(s)" section with a **Reuse** button, letting you skip this step. + +### Step 1b: Set scorer on app + +The wizard shows: *"Scorer deployed at `0x47e5...7bf5`"* + +Click **Set App Scorer** to call `CredentialRegistry.setAppScorer(appId, scorerAddress)`. Once confirmed, the wizard advances to Step 3. + +### Step 1c: Done + +A yellow warning banner indicates: + +> **Almost done — set your scores** +> Your custom scorer is deployed but all scores are currently set to **0**. You need to set scores before your app can calculate humanity scores. + +Click **Set Scores** to configure your custom scores. + +--- + +## Step 2: Set custom scores + +The Manage Scores page (`/apps/{appId}/scorer/manage`) displays all 15 credential groups in an editable table: + +![Manage Custom Scores](../e2e/screenshots/annotated/06-manage-scores.png) + + +### Edit and save scores + +1. Click **Copy defaults** in the Custom Score column header to pre-fill all fields with BringID's default scores, or enter your desired scores manually. For example: + - Farcaster (Low): `5` + - Farcaster (Medium): `10` + - Farcaster (High): `20` + +2. The **Save** button updates to show how many scores you've changed: **"Save 3 Score(s)"** + +3. Click the Save button. This calls `DefaultScorer.setScores(ids[], scores[])` in a single batch transaction. + +4. Confirm the transaction. The table refreshes with your new scores. + +> **Reset** — Click the Reset link in the Custom Score column header to discard all unsaved changes. + +--- + +Next: [Verify your integration](guide-verify-integration.md) diff --git a/docs/guide-verify-integration.md b/docs/guide-verify-integration.md new file mode 100644 index 0000000..8e7b7c8 --- /dev/null +++ b/docs/guide-verify-integration.md @@ -0,0 +1,10 @@ +# Guide: Verify your integration + +After saving scores, click **Check Integration** at the bottom of the Manage Scores page. This opens the **SDK Demo** page pre-configured with your app. + +![SDK Demo](../e2e/screenshots/annotated/08-demo-page.png) + +The Demo page lets you test: + +- **verifyHumanity** — Start the BringID humanity verification flow (opens a modal) +- **verifyProofs** — Verify on-chain proofs and see the score breakdown by credential group diff --git a/e2e/annotate-screenshots.mjs b/e2e/annotate-screenshots.mjs index fc85665..a36d2f3 100644 --- a/e2e/annotate-screenshots.mjs +++ b/e2e/annotate-screenshots.mjs @@ -67,12 +67,6 @@ await annotate("01-my-apps-disconnected.png", [ { box: [1133, 8, 1284, 56], r: 14, thick: true, label: 'Click "Connect Wallet"', lx: 1020, ly: 80 }, ]); -// 02 - Register App form: timelock presets row + (Register App btn not visible without wallet) -// 1 day btn at (489, 266), Disabled at (489+72, 298+24) → row spans ~489..780, 262..326 -await annotate("02-register-app-form.png", [ - { box: [485, 260, 780, 328], r: 10, label: "1. Pick a timelock preset", lx: 790, ly: 300, small: true }, -]); - // 03 - Timelock selected: same layout, "1 day" is active await annotate("03-register-app-timelock-selected.png", [ { box: [485, 260, 550, 294], r: 10, thick: true, label: '"1 day" selected — click "Register App"', lx: 560, ly: 284, small: true }, @@ -108,4 +102,5 @@ await annotate("08-demo-page.png", [ { box: [181, 400, 333, 444], r: 10, thick: true, label: 'Click "Verify Humanity"', lx: 343, ly: 430 }, ]); + console.log(`\n✓ All annotated screenshots in ${OUT}/`); diff --git a/e2e/mock-wallet-init.js b/e2e/mock-wallet-init.js new file mode 100644 index 0000000..a62de3f --- /dev/null +++ b/e2e/mock-wallet-init.js @@ -0,0 +1,96 @@ +// Mock EIP-1193 wallet provider for E2E tests. +// Globals __E2E_ADDRESS__ and __E2E_ALCHEMY_RPC__ are set via page.evaluate before navigation. +(function () { + if (!window.__E2E_ADDRESS__) return; + + var ADDRESS = window.__E2E_ADDRESS__; + var ALCHEMY_RPC = window.__E2E_ALCHEMY_RPC__; + var CHAIN_ID = "0x14a34"; // 84532 (Base Sepolia) + // Persist connection state across navigations via sessionStorage + var connected = false; + try { connected = sessionStorage.getItem("__e2e_connected") === "1"; } catch (e) {} + + var provider = { + _events: {}, + on: function (event, fn) { + if (!this._events[event]) this._events[event] = []; + this._events[event].push(fn); + return this; + }, + removeListener: function (event, fn) { + this._events[event] = (this._events[event] || []).filter(function (f) { + return f !== fn; + }); + return this; + }, + emit: function (event) { + var args = Array.prototype.slice.call(arguments, 1); + (this._events[event] || []).forEach(function (fn) { + fn.apply(null, args); + }); + }, + request: function (req) { + var method = req.method; + var params = req.params; + switch (method) { + case "eth_requestAccounts": + connected = true; + try { sessionStorage.setItem("__e2e_connected", "1"); } catch (e) {} + return Promise.resolve([ADDRESS]); + case "eth_accounts": + return Promise.resolve(connected ? [ADDRESS] : []); + case "eth_chainId": + return Promise.resolve(CHAIN_ID); + case "net_version": + return Promise.resolve("84532"); + case "wallet_switchEthereumChain": + case "wallet_addEthereumChain": + return Promise.resolve(null); + case "wallet_getPermissions": + return Promise.resolve([]); + case "wallet_requestPermissions": + connected = true; + try { sessionStorage.setItem("__e2e_connected", "1"); } catch (e) {} + return Promise.resolve(params && params[0] ? [params[0]] : []); + case "eth_sendTransaction": + return window.__e2e_sendTransaction(params[0]); + default: + return fetch(ALCHEMY_RPC, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: Date.now(), + method: method, + params: params || [], + }), + }) + .then(function (res) { + return res.json(); + }) + .then(function (json) { + if (json.error) throw new Error(json.error.message); + return json.result; + }); + } + }, + }; + + var info = Object.freeze({ + uuid: crypto.randomUUID(), + name: "E2E Wallet", + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==", + rdns: "dev.e2e.wallet", + }); + + var detail = Object.freeze({ info: info, provider: provider }); + + window.addEventListener("eip6963:requestProvider", function () { + window.dispatchEvent( + new CustomEvent("eip6963:announceProvider", { detail: detail }) + ); + }); + window.dispatchEvent( + new CustomEvent("eip6963:announceProvider", { detail: detail }) + ); +})(); diff --git a/e2e/screenshots/01-my-apps-disconnected.png b/e2e/screenshots/01-my-apps-disconnected.png index 5cd8e54..2267c10 100644 Binary files a/e2e/screenshots/01-my-apps-disconnected.png and b/e2e/screenshots/01-my-apps-disconnected.png differ diff --git a/e2e/screenshots/02-register-app-form.png b/e2e/screenshots/02-register-app-form.png index 7cd4619..af45607 100644 Binary files a/e2e/screenshots/02-register-app-form.png and b/e2e/screenshots/02-register-app-form.png differ diff --git a/e2e/screenshots/03-register-app-timelock-selected.png b/e2e/screenshots/03-register-app-timelock-selected.png index 141b3b5..d637db4 100644 Binary files a/e2e/screenshots/03-register-app-timelock-selected.png and b/e2e/screenshots/03-register-app-timelock-selected.png differ diff --git a/e2e/screenshots/03b-register-app-success.png b/e2e/screenshots/03b-register-app-success.png new file mode 100644 index 0000000..846c0a5 Binary files /dev/null and b/e2e/screenshots/03b-register-app-success.png differ diff --git a/e2e/screenshots/04b-app-settings-admin.png b/e2e/screenshots/04b-app-settings-admin.png new file mode 100644 index 0000000..99cf361 Binary files /dev/null and b/e2e/screenshots/04b-app-settings-admin.png differ diff --git a/e2e/screenshots/05-deploy-scorer.png b/e2e/screenshots/05-deploy-scorer.png index 16a2c5e..4e0ec3e 100644 Binary files a/e2e/screenshots/05-deploy-scorer.png and b/e2e/screenshots/05-deploy-scorer.png differ diff --git a/e2e/screenshots/06-manage-scores.png b/e2e/screenshots/06-manage-scores.png index 0b1b16d..843ab9c 100644 Binary files a/e2e/screenshots/06-manage-scores.png and b/e2e/screenshots/06-manage-scores.png differ diff --git a/e2e/screenshots/07-score-explorer.png b/e2e/screenshots/07-score-explorer.png index 152e37d..3fbc2cd 100644 Binary files a/e2e/screenshots/07-score-explorer.png and b/e2e/screenshots/07-score-explorer.png differ diff --git a/e2e/screenshots/08-demo-page.png b/e2e/screenshots/08-demo-page.png index d392c30..cde7549 100644 Binary files a/e2e/screenshots/08-demo-page.png and b/e2e/screenshots/08-demo-page.png differ diff --git a/e2e/screenshots/annotated/03b-register-app-success.png b/e2e/screenshots/annotated/03b-register-app-success.png new file mode 100644 index 0000000..f12ede5 Binary files /dev/null and b/e2e/screenshots/annotated/03b-register-app-success.png differ diff --git a/e2e/screenshots/annotated/04b-app-settings-admin.png b/e2e/screenshots/annotated/04b-app-settings-admin.png new file mode 100644 index 0000000..f5423d3 Binary files /dev/null and b/e2e/screenshots/annotated/04b-app-settings-admin.png differ diff --git a/e2e/screenshots/annotated/05-deploy-scorer.png b/e2e/screenshots/annotated/05-deploy-scorer.png index 78af196..1beb60d 100644 Binary files a/e2e/screenshots/annotated/05-deploy-scorer.png and b/e2e/screenshots/annotated/05-deploy-scorer.png differ diff --git a/e2e/take-screenshots.mjs b/e2e/take-screenshots.mjs index 15fac60..a56124e 100644 --- a/e2e/take-screenshots.mjs +++ b/e2e/take-screenshots.mjs @@ -4,6 +4,9 @@ */ import { chromium } from "@playwright/test"; import { readFileSync } from "fs"; +import { createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { baseSepolia } from "viem/chains"; const BASE_URL = "http://localhost:3000"; const SCREENSHOT_DIR = "e2e/screenshots"; @@ -16,10 +19,10 @@ const APP_ID = results.appId; const APP_ID_HEX = "0x" + BigInt(APP_ID).toString(16).padStart(64, "0"); const SCORER_ADDRESS = results.scorerAddress; -// Read E2E private key from .env.local for demo page wallet -const E2E_KEY = readFileSync(".env.local", "utf8") - .match(/E2E_PRIVATE_KEY=(.*)/)?.[1] - ?.trim(); +// Read secrets from .env.local +const envLocal = readFileSync(".env.local", "utf8"); +const E2E_KEY = envLocal.match(/E2E_PRIVATE_KEY=(.*)/)?.[1]?.trim(); +const ALCHEMY_KEY = envLocal.match(/NEXT_PUBLIC_ALCHEMY_API_KEY=(.*)/)?.[1]?.trim(); console.log(`App ID: ${APP_ID}, Scorer: ${SCORER_ADDRESS}\n`); @@ -31,15 +34,18 @@ const context = await browser.newContext({ const page = await context.newPage(); page.setDefaultNavigationTimeout(60000); -async function shot(name, description) { - await page.waitForTimeout(1500); // let animations settle +async function shot(name, description, targetPage = page) { + await targetPage.waitForTimeout(1500); // let animations settle const path = `${SCREENSHOT_DIR}/${name}.png`; - await page.screenshot({ path, fullPage: true }); + await targetPage.screenshot({ path, fullPage: true }); console.log(`📸 ${name}: ${description}`); } /** Wait for on-chain content to appear, retrying with page reload if needed. */ -async function waitForContent(locator, { retries = 3, timeout = 45000 } = {}) { +async function waitForContent( + locator, + { retries = 3, timeout = 45000, targetPage: tp = page } = {} +) { for (let i = 0; i < retries; i++) { try { await locator.waitFor({ timeout }); @@ -47,8 +53,8 @@ async function waitForContent(locator, { retries = 3, timeout = 45000 } = {}) { } catch { if (i < retries - 1) { console.log(` ↻ Retrying (on-chain data not loaded yet)...`); - await page.reload(); - await page.waitForLoadState("networkidle"); + await tp.reload(); + await tp.waitForLoadState("networkidle"); } else { throw new Error(`Content not found after ${retries} attempts`); } @@ -61,10 +67,24 @@ await page.goto(`${BASE_URL}/apps`); await page.waitForLoadState("networkidle"); await shot("01-my-apps-disconnected", "My Apps page before wallet connection"); -// ── 2. Register App page (form) ── +// ── 2. Register App page (form) — cropped to form card only ── await page.goto(`${BASE_URL}/apps/new`); await page.waitForLoadState("networkidle"); -await shot("02-register-app-form", "Register App form with timelock options"); +await page.waitForTimeout(1500); +{ + const box = await page.locator(".max-w-lg > div").boundingBox(); + const pad = 24; + await page.screenshot({ + path: `${SCREENSHOT_DIR}/02-register-app-form.png`, + clip: { + x: Math.max(0, box.x - pad), + y: Math.max(0, box.y - pad), + width: box.width + pad * 2, + height: box.height + pad * 2, + }, + }); +} +console.log("📸 02-register-app-form: Register App form with timelock options"); // ── 3. Register App - with 1 day timelock selected ── await page.click("text=1 day"); @@ -121,5 +141,127 @@ await page.waitForTimeout(8000); await shot("08-demo-page", "Demo page with BringID verification modal"); +// Close the first browser before starting wallet-connected screenshots +await context.close(); await browser.close(); + +// ── 9. Wallet-connected screenshots (register app + admin settings) ── +console.log("\n=== Wallet-connected screenshots ===\n"); + +const account = privateKeyToAccount(E2E_KEY); +const walletClient = createWalletClient({ + account, + chain: baseSepolia, + transport: http(`https://base-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}`), +}); +console.log(`E2E wallet: ${account.address}`); + +// Helper: set up a fresh page with mock wallet init scripts +const MOCK_WALLET_PATH = new URL("./mock-wallet-init.js", import.meta.url).pathname; + +async function setupWalletPage(ctx) { + const p = await ctx.newPage(); + p.setDefaultNavigationTimeout(60000); + p.on("pageerror", (err) => console.log(" PAGE ERROR:", err.message)); + + await p.exposeFunction("__e2e_sendTransaction", async (tx) => { + console.log(` → eth_sendTransaction: to=${tx.to} data=${tx.data?.slice(0, 10)}...`); + const hash = await walletClient.sendTransaction({ + to: tx.to, + data: tx.data, + value: tx.value ? BigInt(tx.value) : undefined, + }); + console.log(` → tx hash: ${hash}`); + return hash; + }); + + await p.addInitScript(` + window.__E2E_ADDRESS__ = "${account.address}"; + window.__E2E_ALCHEMY_RPC__ = "https://base-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}"; + `); + await p.addInitScript({ path: MOCK_WALLET_PATH }); + return p; +} + +// Use a fresh browser to avoid shared state from earlier screenshots +const walletBrowser = await chromium.launch({ headless: true }); +const walletContext = await walletBrowser.newContext({ + viewport: { width: 1440, height: 900 }, + colorScheme: "dark", +}); + +// Navigate and connect wallet (retry with fresh page on failure) +let walletPage; +for (let attempt = 1; attempt <= 3; attempt++) { + const p = await setupWalletPage(walletContext); + await p.goto(`${BASE_URL}/apps/new?chainId=84532`); + await p.waitForLoadState("networkidle"); + await p.waitForTimeout(2000); + + console.log(`Connecting wallet (attempt ${attempt})...`); + const connectBtn = p.locator('[data-testid="rk-connect-button"]'); + const visible = await connectBtn.isVisible({ timeout: 10000 }).catch(() => false); + if (!visible) { + console.log(" Connect button not found, retrying with fresh page..."); + await p.close(); + continue; + } + await connectBtn.click(); + await p.waitForTimeout(1000); + await p.locator('[data-testid="rk-wallet-option-dev.e2e.wallet"]').click(); + await p.waitForTimeout(3000); + console.log("Wallet connected"); + walletPage = p; + break; +} +if (!walletPage) throw new Error("Failed to connect wallet after 3 attempts"); + +// Select 1 day timelock +await walletPage.click("text=1 day"); +await walletPage.waitForTimeout(500); + +// Register the app +console.log("Registering app (on-chain transaction)..."); +// Use button selector to avoid clicking the nav link with the same text +await walletPage.locator('button:has-text("Create App")').click(); +await walletPage.locator("text=App Registered!").waitFor({ timeout: 120000 }); +console.log("App registered!"); + +// ── 03b: Registration success banner — cropped to banner only ── +await walletPage.waitForTimeout(1500); +{ + const box = await walletPage.locator(".max-w-lg > div").boundingBox(); + const pad = 24; + await walletPage.screenshot({ + path: `${SCREENSHOT_DIR}/03b-register-app-success.png`, + clip: { + x: Math.max(0, box.x - pad), + y: Math.max(0, box.y - pad), + width: box.width + pad * 2, + height: box.height + pad * 2, + }, + }); +} +console.log("📸 03b-register-app-success: Registration success banner with new App ID"); + +// Extract new app ID from "Go to App Settings" link +const settingsLink = walletPage.locator('a:has-text("Go to App Settings")'); +const href = await settingsLink.getAttribute("href"); +const newAppIdHex = href.match(/\/apps\/(0x[0-9a-f]+)/i)?.[1]; +console.log(`New app: ${newAppIdHex}`); + +// ── 04b: App settings page as admin ── +await walletPage.goto(`${BASE_URL}/apps/${newAppIdHex}?chainId=84532`); +await walletPage.waitForLoadState("networkidle"); +await waitForContent(walletPage.locator("text=/Active|Suspended/").first(), { + targetPage: walletPage, +}); +await shot( + "04b-app-settings-admin", + "App settings page with admin controls", + walletPage +); + +await walletContext.close(); +await walletBrowser.close(); console.log("\n✓ All screenshots saved to e2e/screenshots/");