|
| 1 | +<!doctype html> |
| 2 | +<html lang="en"> |
| 3 | + <head> |
| 4 | + <meta charset="utf-8" /> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| 6 | + <title>Base Account Integration (HTML + JS)</title> |
| 7 | + <style> |
| 8 | + :root { |
| 9 | + --bg: #0b0f19; |
| 10 | + --panel: #111827; |
| 11 | + --text: #e5e7eb; |
| 12 | + --muted: #9ca3af; |
| 13 | + --border: #243042; |
| 14 | + --accent: #0052ff; |
| 15 | + --ok: #10b981; |
| 16 | + --err: #ef4444; |
| 17 | + } |
| 18 | + * { box-sizing: border-box; } |
| 19 | + body { |
| 20 | + margin: 0; |
| 21 | + font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, |
| 22 | + Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; |
| 23 | + background: radial-gradient(1200px 800px at 30% -10%, rgba(0,82,255,0.25), transparent 55%), |
| 24 | + var(--bg); |
| 25 | + color: var(--text); |
| 26 | + } |
| 27 | + header { |
| 28 | + padding: 28px 18px 8px; |
| 29 | + max-width: 980px; |
| 30 | + margin: 0 auto; |
| 31 | + } |
| 32 | + h1 { margin: 0 0 6px; font-size: 20px; font-weight: 700; } |
| 33 | + p { margin: 0; color: var(--muted); line-height: 1.5; } |
| 34 | + main { |
| 35 | + max-width: 980px; |
| 36 | + margin: 0 auto; |
| 37 | + padding: 18px; |
| 38 | + display: grid; |
| 39 | + grid-template-columns: 1fr; |
| 40 | + gap: 14px; |
| 41 | + } |
| 42 | + .card { |
| 43 | + background: color-mix(in srgb, var(--panel) 92%, transparent); |
| 44 | + border: 1px solid var(--border); |
| 45 | + border-radius: 14px; |
| 46 | + padding: 16px; |
| 47 | + box-shadow: 0 10px 28px rgba(0,0,0,0.25); |
| 48 | + } |
| 49 | + .row { |
| 50 | + display: flex; |
| 51 | + flex-wrap: wrap; |
| 52 | + gap: 10px; |
| 53 | + align-items: center; |
| 54 | + } |
| 55 | + label { color: var(--muted); font-size: 12px; display: block; margin-bottom: 6px; } |
| 56 | + input, select { |
| 57 | + width: 100%; |
| 58 | + padding: 10px 12px; |
| 59 | + border-radius: 10px; |
| 60 | + border: 1px solid var(--border); |
| 61 | + background: #0b1220; |
| 62 | + color: var(--text); |
| 63 | + outline: none; |
| 64 | + } |
| 65 | + input:focus, select:focus { border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); } |
| 66 | + .field { flex: 1 1 220px; min-width: 220px; } |
| 67 | + button { |
| 68 | + appearance: none; |
| 69 | + border: 1px solid var(--border); |
| 70 | + background: #0b1220; |
| 71 | + color: var(--text); |
| 72 | + padding: 10px 12px; |
| 73 | + border-radius: 10px; |
| 74 | + cursor: pointer; |
| 75 | + font-weight: 600; |
| 76 | + } |
| 77 | + button.primary { |
| 78 | + background: var(--accent); |
| 79 | + border-color: color-mix(in srgb, var(--accent) 80%, white); |
| 80 | + } |
| 81 | + button:disabled { opacity: 0.5; cursor: not-allowed; } |
| 82 | + .status { |
| 83 | + margin-top: 12px; |
| 84 | + padding: 10px 12px; |
| 85 | + border-radius: 10px; |
| 86 | + border: 1px solid var(--border); |
| 87 | + background: #0b1220; |
| 88 | + color: var(--text); |
| 89 | + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
| 90 | + white-space: pre-wrap; |
| 91 | + } |
| 92 | + .status.ok { border-color: color-mix(in srgb, var(--ok) 55%, var(--border)); } |
| 93 | + .status.err { border-color: color-mix(in srgb, var(--err) 55%, var(--border)); } |
| 94 | + .small { font-size: 12px; color: var(--muted); } |
| 95 | + code { color: #c7d2fe; } |
| 96 | + footer { max-width: 980px; margin: 0 auto; padding: 0 18px 28px; } |
| 97 | + </style> |
| 98 | + </head> |
| 99 | + <body> |
| 100 | + <header> |
| 101 | + <h1>Base Account: Sign in with Base + Base Pay (no framework)</h1> |
| 102 | + <p> |
| 103 | + Minimal integration using the Base Account SDK loaded from a CDN. |
| 104 | + <span class="small">Run this via a local server (not <code>file://</code>).</span> |
| 105 | + </p> |
| 106 | + </header> |
| 107 | + |
| 108 | + <main> |
| 109 | + <section class="card"> |
| 110 | + <div class="row"> |
| 111 | + <button id="btnSignIn" class="primary">Sign in with Base</button> |
| 112 | + <button id="btnDisconnect">Disconnect</button> |
| 113 | + <div class="small" id="walletInfo">Not connected</div> |
| 114 | + </div> |
| 115 | + <div class="status" id="authStatus">Auth status will appear here.</div> |
| 116 | + <p class="small" style="margin-top: 10px;"> |
| 117 | + SIWB uses <code>wallet_connect</code> with the <code>signInWithEthereum</code> capability. |
| 118 | + In production, send the returned <code>message</code> and <code>signature</code> to your backend for verification. |
| 119 | + </p> |
| 120 | + </section> |
| 121 | + |
| 122 | + <section class="card"> |
| 123 | + <h2 style="margin:0 0 10px; font-size: 16px;">Base Pay</h2> |
| 124 | + <div class="row"> |
| 125 | + <div class="field"> |
| 126 | + <label for="to">Recipient address</label> |
| 127 | + <input id="to" placeholder="0x..." value="0x2211d1D0020DAEA8039E46Cf1367962070d77DA9" /> |
| 128 | + </div> |
| 129 | + <div class="field"> |
| 130 | + <label for="amount">Amount (USD)</label> |
| 131 | + <input id="amount" inputmode="decimal" value="5.00" /> |
| 132 | + </div> |
| 133 | + <div class="field"> |
| 134 | + <label for="network">Network</label> |
| 135 | + <select id="network"> |
| 136 | + <option value="testnet" selected>Testnet (Base Sepolia)</option> |
| 137 | + <option value="mainnet">Mainnet (Base)</option> |
| 138 | + </select> |
| 139 | + </div> |
| 140 | + </div> |
| 141 | + |
| 142 | + <div class="row" style="margin-top: 10px;"> |
| 143 | + <button id="btnPay" class="primary">Pay with Base</button> |
| 144 | + <button id="btnCheck" disabled>Check payment status</button> |
| 145 | + <div class="small">Last payment id: <span id="paymentId">(none)</span></div> |
| 146 | + </div> |
| 147 | + |
| 148 | + <div class="status" id="payStatus">Payment status will appear here.</div> |
| 149 | + |
| 150 | + <p class="small" style="margin-top: 10px;"> |
| 151 | + Note: Base Pay works even if the user hasn’t signed in first. |
| 152 | + </p> |
| 153 | + </section> |
| 154 | + </main> |
| 155 | + |
| 156 | + <footer class="small"> |
| 157 | + Local dev: <code>npx serve .</code> or <code>python -m http.server</code> |
| 158 | + </footer> |
| 159 | + |
| 160 | + <!-- Base Account SDK via CDN (per Base docs) --> |
| 161 | + <script src="https://unpkg.com/@base-org/account/dist/base-account.min.js"></script> |
| 162 | + |
| 163 | + <script> |
| 164 | + // ---- Helpers ---- |
| 165 | + const $ = (id) => document.getElementById(id); |
| 166 | + |
| 167 | + function setStatus(el, msg, type) { |
| 168 | + el.textContent = msg; |
| 169 | + el.classList.remove('ok', 'err'); |
| 170 | + if (type === 'ok') el.classList.add('ok'); |
| 171 | + if (type === 'err') el.classList.add('err'); |
| 172 | + } |
| 173 | + |
| 174 | + function shortAddr(addr) { |
| 175 | + if (!addr || addr.length < 10) return addr || ''; |
| 176 | + return addr.slice(0, 6) + '...' + addr.slice(-4); |
| 177 | + } |
| 178 | + |
| 179 | + function generateNonce() { |
| 180 | + // Base docs use crypto.randomUUID().replace(/-/g, "") |
| 181 | + return window.crypto.randomUUID().replace(/-/g, ''); |
| 182 | + } |
| 183 | + |
| 184 | + // ---- SDK init ---- |
| 185 | + if (!window.createBaseAccountSDK) { |
| 186 | + // Script didn't load (blocked, offline, etc.) |
| 187 | + setStatus($('authStatus'), 'Base Account SDK failed to load. Check your network / CSP.', 'err'); |
| 188 | + } |
| 189 | + |
| 190 | + const sdk = window.createBaseAccountSDK({ |
| 191 | + appName: 'Base Account Demo', |
| 192 | + appLogoUrl: 'https://base.org/logo.png', |
| 193 | + }); |
| 194 | + const provider = sdk.getProvider(); |
| 195 | + |
| 196 | + // ---- State ---- |
| 197 | + let connectedAddress = null; |
| 198 | + let lastPaymentId = null; |
| 199 | + |
| 200 | + function refreshWalletInfo() { |
| 201 | + $('walletInfo').textContent = connectedAddress ? `Connected: ${shortAddr(connectedAddress)}` : 'Not connected'; |
| 202 | + } |
| 203 | + |
| 204 | + refreshWalletInfo(); |
| 205 | + |
| 206 | + // ---- Sign in (SIWB) ---- |
| 207 | + $('btnSignIn').onclick = async () => { |
| 208 | + try { |
| 209 | + setStatus($('authStatus'), 'Connecting to Base Account...', ''); |
| 210 | + const nonce = generateNonce(); |
| 211 | + |
| 212 | + const { accounts } = await provider.request({ |
| 213 | + method: 'wallet_connect', |
| 214 | + params: [ |
| 215 | + { |
| 216 | + version: '1', |
| 217 | + capabilities: { |
| 218 | + signInWithEthereum: { |
| 219 | + nonce, |
| 220 | + chainId: '0x2105', // Base mainnet (8453) as hex per docs |
| 221 | + }, |
| 222 | + }, |
| 223 | + }, |
| 224 | + ], |
| 225 | + }); |
| 226 | + |
| 227 | + const { address } = accounts[0]; |
| 228 | + connectedAddress = address; |
| 229 | + refreshWalletInfo(); |
| 230 | + |
| 231 | + const { message, signature } = accounts[0].capabilities.signInWithEthereum; |
| 232 | + |
| 233 | + setStatus( |
| 234 | + $('authStatus'), |
| 235 | + `Signed in!\nAddress: ${address}\n\nSend this to your backend to verify:\n- message (SIWE)\n- signature\n\n(Also see console for full object.)`, |
| 236 | + 'ok' |
| 237 | + ); |
| 238 | + |
| 239 | + console.log('SIWB auth payload:', { address, message, signature, nonce }); |
| 240 | + } catch (err) { |
| 241 | + console.error('Sign-in error:', err); |
| 242 | + setStatus($('authStatus'), `Sign-in failed: ${err?.message || String(err)}`, 'err'); |
| 243 | + } |
| 244 | + }; |
| 245 | + |
| 246 | + $('btnDisconnect').onclick = async () => { |
| 247 | + // The docs quickstart doesn’t specify a disconnect method. |
| 248 | + // We'll just clear local UI state. |
| 249 | + connectedAddress = null; |
| 250 | + refreshWalletInfo(); |
| 251 | + setStatus($('authStatus'), 'Disconnected (local UI reset).', ''); |
| 252 | + }; |
| 253 | + |
| 254 | + // ---- Base Pay ---- |
| 255 | + $('btnPay').onclick = async () => { |
| 256 | + try { |
| 257 | + if (!window.base?.pay || !window.base?.getPaymentStatus) { |
| 258 | + throw new Error('window.base.pay/getPaymentStatus not found. SDK may not be loaded.'); |
| 259 | + } |
| 260 | + |
| 261 | + const to = $('to').value.trim(); |
| 262 | + const amount = $('amount').value.trim(); |
| 263 | + const network = $('network').value; |
| 264 | + const testnet = network === 'testnet'; |
| 265 | + |
| 266 | + if (!/^0x[a-fA-F0-9]{40}$/.test(to)) { |
| 267 | + throw new Error('Invalid recipient address.'); |
| 268 | + } |
| 269 | + if (!amount || Number.isNaN(Number(amount)) || Number(amount) <= 0) { |
| 270 | + throw new Error('Invalid amount.'); |
| 271 | + } |
| 272 | + |
| 273 | + setStatus($('payStatus'), `Processing payment...\nTo: ${to}\nAmount (USD): ${amount}\nNetwork: ${testnet ? 'Base Sepolia (testnet)' : 'Base (mainnet)'}`, ''); |
| 274 | + |
| 275 | + const result = await window.base.pay({ |
| 276 | + amount, // USD – SDK quotes equivalent USDC |
| 277 | + to, |
| 278 | + testnet, |
| 279 | + }); |
| 280 | + |
| 281 | + lastPaymentId = result.id; |
| 282 | + $('paymentId').textContent = lastPaymentId; |
| 283 | + $('btnCheck').disabled = false; |
| 284 | + |
| 285 | + // Immediately query status (docs do this) |
| 286 | + const status = await window.base.getPaymentStatus({ id: result.id, testnet }); |
| 287 | + |
| 288 | + setStatus($('payStatus'), `Payment initiated/completed.\nPayment id: ${result.id}\nStatus: ${status.status}`, 'ok'); |
| 289 | + } catch (err) { |
| 290 | + console.error('Payment error:', err); |
| 291 | + setStatus($('payStatus'), `Payment failed: ${err?.message || String(err)}`, 'err'); |
| 292 | + } |
| 293 | + }; |
| 294 | + |
| 295 | + $('btnCheck').onclick = async () => { |
| 296 | + try { |
| 297 | + if (!lastPaymentId) throw new Error('No payment id yet.'); |
| 298 | + const testnet = $('network').value === 'testnet'; |
| 299 | + |
| 300 | + setStatus($('payStatus'), `Checking payment status...\nPayment id: ${lastPaymentId}`, ''); |
| 301 | + const status = await window.base.getPaymentStatus({ id: lastPaymentId, testnet }); |
| 302 | + setStatus($('payStatus'), `Payment id: ${lastPaymentId}\nStatus: ${status.status}`, 'ok'); |
| 303 | + } catch (err) { |
| 304 | + console.error('Check status error:', err); |
| 305 | + setStatus($('payStatus'), `Status check failed: ${err?.message || String(err)}`, 'err'); |
| 306 | + } |
| 307 | + }; |
| 308 | + </script> |
| 309 | + </body> |
| 310 | +</html> |
0 commit comments