aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-04 07:53:03 -0700
committerAdam Soltys <[email protected]>2026-07-04 07:53:03 -0700
commit2da22a3dc82bc8c0beb456097c2749ebc02d8017 (patch)
tree8560c3a3bb161045e57031bce0e796544653f6be
parentd8da74e50a374d59629b8ec8731e8cb38bc6b42e (diff)
downloadcoinosv3-2da22a3dc82bc8c0beb456097c2749ebc02d8017.tar.gz
coinosv3-2da22a3dc82bc8c0beb456097c2749ebc02d8017.zip
Gift claim: make the wallet chooser read as a choice
The existing-wallet buttons looked like inert inputs. Now: a "Receive into one of your wallets:" label, a chevron on each wallet row so it reads as tappable, and an "or" divider before the new-wallet button. (tools/ark/gift-shot.js: screenshot harness that mints a real regtest gift and opens its link.) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QFAcD4V284DvYZZHP7Rxxy
-rw-r--r--src/app.js10
-rw-r--r--src/i18n.js2
-rw-r--r--tools/ark/gift-shot.js82
3 files changed, 91 insertions, 3 deletions
diff --git a/src/app.js b/src/app.js
index e024e06..53cb35b 100644
--- a/src/app.js
+++ b/src/app.js
@@ -2302,11 +2302,15 @@ function claimChooseView() {
h('p', { class: 'muted', style: 'margin:0' }, t('claimIntoPrompt'))
),
h('div', { class: 'card col gap6' },
+ h('div', { class: 'small muted' }, t('claimIntoExisting')),
...accounts.map((a) =>
- h('button', { class: 'btn-block', style: 'text-align:left', onClick: () => claimIntoAccount(a, code) },
- a.label,
- a.type === 'watch' ? h('span', { class: 'small faint', style: 'margin-left:6px' }, '(' + t('watchOnly') + ')') : null)
+ h('button', { class: 'btn-block', style: 'display:flex;justify-content:space-between;align-items:center;text-align:left', onClick: () => claimIntoAccount(a, code) },
+ h('span', {},
+ a.label,
+ a.type === 'watch' ? h('span', { class: 'small faint', style: 'margin-left:6px' }, '(' + t('watchOnly') + ')') : null),
+ h('span', { class: 'muted', style: 'font-size:18px;line-height:1' }, '\u203a'))
),
+ h('div', { class: 'small faint', style: 'text-align:center' }, t('claimOr')),
h('button', { class: 'btn-primary btn-block', onClick: () => { ui.claimChoose = null; enterWallet(newMnemonic(), '', { gift: code }); } }, t('claimNewWallet'))
)
);
diff --git a/src/i18n.js b/src/i18n.js
index 9cbc09d..552795d 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -290,6 +290,8 @@ const STR = {
claimBody: 'Claim it into a brand-new wallet that only you control.',
claimIntoPrompt: 'Choose where to receive it.',
claimNewWallet: 'Receive into a new wallet',
+ claimIntoExisting: 'Receive into one of your wallets:',
+ claimOr: 'or',
goToWallet: 'Go to wallet',
giftClaimedToast: 'Gift claimed',
claimBtn: 'Claim it',
diff --git a/tools/ark/gift-shot.js b/tools/ark/gift-shot.js
new file mode 100644
index 0000000..9874e8c
--- /dev/null
+++ b/tools/ark/gift-shot.js
@@ -0,0 +1,82 @@
+// Screenshot the gift-claim wallet chooser: mint a real gift from a funded
+// wallet, open its link in the same browser (wallet 1 still in session),
+// capture the "choose where to receive" screen.
+import { spawn, execSync } from 'node:child_process';
+import puppeteer from 'puppeteer-core';
+import { generateMnemonic } from '@scure/bip39';
+import { wordlist } from '@scure/bip39/wordlists/english';
+
+const PORT = 5199;
+const APP = `http://localhost:${PORT}`;
+const BCLI = `bitcoin-cli -regtest -datadir=${process.env.HOME}/ark-regtest/bitcoind -rpcport=18543 -rpcwallet=miner`;
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+const sh = (cmd) => execSync(cmd, { shell: '/bin/bash' }).toString().trim();
+
+const dev = spawn('bun', ['run', 'dev'], { cwd: process.env.HOME + '/halwallet', env: { ...process.env, PORT: String(PORT) }, stdio: 'ignore' });
+for (let i = 0; i < 30; i++) { try { if ((await fetch(APP)).ok) break; } catch {} await sleep(500); }
+
+const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] });
+const page = await browser.newPage();
+await page.setViewport({ width: 420, height: 860, deviceScaleFactor: 2 });
+const clickText = (sel, text) => page.evaluate((sel, text) => {
+ const el = [...document.querySelectorAll(sel)].find((e) => e.textContent.trim().toLowerCase().includes(text.toLowerCase()));
+ if (el) { el.click(); return true; } return false;
+}, sel, text);
+const waitText = async (text, ms = 20000) => {
+ for (let i = 0; i < ms / 250; i++) {
+ if (await page.evaluate((t) => document.body.innerText.toLowerCase().includes(t.toLowerCase()), text)) return true;
+ await sleep(250);
+ }
+ return false;
+};
+
+await page.goto(APP, { waitUntil: 'domcontentloaded' });
+await page.evaluate(() => {
+ localStorage.setItem('btc-wallet-network', 'regtest');
+ localStorage.setItem('btc-wallet-source:regtest', JSON.stringify({ id: 'custom', url: 'http://localhost:30002' }));
+ localStorage.setItem('btc-wallet-ark-provider:regtest', 'local');
+});
+await page.reload({ waitUntil: 'domcontentloaded' });
+await sleep(500);
+await clickText('button', 'Import existing');
+await sleep(200);
+await page.type('textarea', generateMnemonic(wordlist));
+await clickText('button', 'Open wallet');
+await waitText('receive');
+
+// fund on-chain, confirmed
+const addr = await page.evaluate(() => document.querySelector('.addr-box')?.textContent.trim());
+sh(`${BCLI} sendtoaddress ${addr} 0.0005`);
+sh(`${BCLI} generatetoaddress 1 $(${BCLI} getnewaddress) >/dev/null`);
+await waitText('50,000', 30000);
+if (await page.evaluate(() => document.body.innerText.includes('Payment received'))) {
+ await clickText('.card', 'Payment received');
+ await sleep(300);
+}
+
+// make a 2000-sat gift
+await clickText('.tabs button', 'Send');
+await sleep(300);
+await clickText('button', 'Gift some Bitcoin');
+await sleep(300);
+const amtInput = await page.evaluateHandle(() => [...document.querySelectorAll('input[type="number"]')][0]);
+await amtInput.asElement().type('2000');
+await clickText('button', 'Create gift link');
+await sleep(500);
+if (await page.evaluate(() => document.body.innerText.includes('Lock the whole coin'))) {
+ await clickText('button', 'Lock the whole coin');
+}
+await waitText('/g/', 20000);
+const link = await page.evaluate(() =>
+ [...document.querySelectorAll('.addr-box')].map((e) => e.textContent.trim()).find((x) => x.includes('/g/')));
+console.log('gift link:', (link || '').slice(0, 50) + '…');
+
+// open the gift link — wallet 1 exists, so the chooser should appear
+await page.goto(link, { waitUntil: 'domcontentloaded' });
+const shown = await waitText('Choose where', 15000);
+console.log('chooser shown:', shown);
+await sleep(400);
+await page.screenshot({ path: '/tmp/gift-choose.png' });
+console.log('done');
+await browser.close();
+dev.kill();