diff options
| author | Adam Soltys <[email protected]> | 2026-10-22 23:07:20 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-22 23:07:20 -0700 |
| commit | a6c30c044a31dd1adc6a7ccf5cffff74ca2d6258 (patch) | |
| tree | 9379d6be9f978cfeab6de9805ad9982a419de013 | |
| parent | 9e86e14955e716f1b9ea92d264dbc39fe9a86f82 (diff) | |
| download | coinosv3-a6c30c044a31dd1adc6a7ccf5cffff74ca2d6258.tar.gz coinosv3-a6c30c044a31dd1adc6a7ccf5cffff74ca2d6258.zip | |
Spending settings: the coins first, then the custody strategy as a drop-down that describes only the chosen one
Adam, 2026-10-23: three descriptions at once were a wall of text. The card now sits between the coins table and Renewal, is a select of the three strategies with their prices, and shows one paragraph — the chosen strategy's. The intro is two sentences shorter.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
| -rw-r--r-- | src/features/ark.js | 21 | ||||
| -rw-r--r-- | src/i18n.js | 2 | ||||
| -rw-r--r-- | tools/ark/custody-ui-test.js | 25 |
3 files changed, 24 insertions, 24 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index df33e0f..bc5bd10 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -2604,6 +2604,7 @@ export function arkFeature(ctx) { h('td', { class: 'num' }, '—'), h('td', { class: 'num' }, h('span', { class: 'tag pending' }, t('arkCoinsRenewing'))))))))), renewingCard, + embedded ? custodyCard() : null, h('div', { class: 'card col', style: 'gap:8px' }, h('h4', { style: 'margin:0' }, t('arkCoinsRenewTitle')), // the round cadence comes from the server (it has been an hour and @@ -2678,18 +2679,16 @@ export function arkFeature(ctx) { { id: 'self', label: t('arkCustodySelf'), desc: t('arkCustodySelfDesc', { days: lifeDays }), cost: t('arkCustodyCostSelf', { base: baseCost }) }, { id: 'exit', label: t('arkCustodyExit'), desc: t('arkCustodyExitDesc', { hops }), cost: t('arkCustodyCostExit', { pct: earlyPct, base: baseCost }) }, ].filter((x) => honors || x.id !== 'custodial'); + const chosen = tiers.find((x) => x.id === cur) || tiers[0]; + // A drop-down, and only the chosen strategy's description under it: three + // paragraphs at once read as a wall, and two of them are about choices + // the reader has not made. return h('div', { class: 'card col ark-custody', style: 'gap:10px' }, - h('h3', { style: 'margin:0' }, t('arkCustodyTitle')), + h('h4', { style: 'margin:0' }, t('arkCustodyTitle')), h('p', { class: 'small muted', style: 'margin:0' }, t('arkCustodyDesc', { days: lifeDays })), - h('div', { class: 'col', style: 'gap:8px' }, ...tiers.map((x) => h('label', { - class: 'row gap6 ark-custody-opt' + (x.id === cur ? ' active' : ''), style: 'align-items:flex-start;cursor:pointer', - 'data-tier': x.id, - }, - h('input', { type: 'radio', name: 'arkCustody', value: x.id, checked: x.id === cur, disabled: !honors, style: 'flex:0 0 auto;margin:3px 0 0', - onChange: () => applyCustody(x.id) }), - h('div', { class: 'col', style: 'gap:2px;flex:1' }, - h('div', { class: 'row between gap6', style: 'align-items:baseline' }, h('span', { style: 'white-space:nowrap' }, x.label), h('span', { class: 'small' + (x.id === 'custodial' ? ' faint' : ''), style: 'text-align:right' }, x.cost)), - h('div', { class: 'small muted' }, x.desc))))), + h('select', { id: 'ark-custody-select', disabled: !honors, onChange: (e) => applyCustody(e.target.value) }, + ...tiers.map((x) => h('option', { value: x.id, selected: x.id === cur }, x.label + ' · ' + x.cost))), + h('p', { class: 'small muted ark-custody-desc', style: 'margin:0' }, chosen.desc), honors ? null : h('p', { class: 'small faint', style: 'margin:0' }, t('arkCustodyNotHere'))); } @@ -3809,7 +3808,7 @@ export function arkFeature(ctx) { // the Spending face's settings page: the custody choice, then its coins spendingSettings() { if (wallet.watchOnly) return null; // a locked wallet can't renew or exit - return h('div', { class: 'col', style: 'gap:16px' }, custodyCard(), arkCoinsPage({ embedded: true })); + return arkCoinsPage({ embedded: true }); // the custody strategy sits inside, after the coins }, // the wallet's own pages keep the top nav: these are pages, not takeovers screenView() { diff --git a/src/i18n.js b/src/i18n.js index 16b93d4..aa66f3a 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -783,7 +783,7 @@ const STR = { arkCoopSelectedAmt: '{n} selected coins · {amount}', arkCoopSelectedAmtOne: '1 selected coin · {amount}', arkCustodyTitle: 'Custody', - arkCustodyDesc: 'Each coin expires about {days} days after it was made. Until then you can bring it on-chain yourself, without Coinos. Renewing a coin before it expires keeps that right for another {days} days and costs a small fee. Coinos keeps honoring expired coins, so renewing is a choice:', + arkCustodyDesc: 'Each coin expires about {days} days after it was made. Until then you can bring it on-chain yourself, without Coinos; after that, Coinos keeps honoring it. Renewing before expiry keeps your own exit, for a fee, so it is a choice:', arkCustodyCustodial: 'Allow coins to expire', arkCustodyCustodialDesc: 'Nothing renews and no renewal fees are paid. A coin keeps its own exit until it expires. After that, paying still works, but bringing it on-chain needs Coinos’s cooperation.', arkCustodySelf: 'Minimize renewal cost', diff --git a/tools/ark/custody-ui-test.js b/tools/ark/custody-ui-test.js index 7ebc2ea..419ed21 100644 --- a/tools/ark/custody-ui-test.js +++ b/tools/ark/custody-ui-test.js @@ -77,20 +77,20 @@ try { let tx = await text(); assert.match(tx, /Custody/); - assert.match(tx, /Allow coins to expire[\s\S]*no fees/); - assert.match(tx, /keeps its own exit until it expires/); - assert.doesNotMatch(tx, /timelock|Spending coin carries|old Coinos/, 'no jargon on the card'); - assert.match(tx, /Minimize renewal cost[\s\S]*300 sats per renewal/); - assert.match(tx, /Minimize exit cost[\s\S]*300 sats near expiry, 0\.2% \+ 300 sats when early/); - assert.match(tx, /4 hops deep/); - const checked = await page.$$eval('.ark-custody input[type=radio]', (els) => els.filter((e) => e.checked).map((e) => e.value)); - assert.deepEqual(checked, ['custodial'], 'coinos holds the coins by default'); + const opts = await page.$$eval('.ark-custody option', (es) => es.map((e) => e.value + '=' + e.textContent)); + assert.deepEqual(opts, ['custodial=Allow coins to expire · no fees', 'self=Minimize renewal cost · 300 sats per renewal', 'exit=Minimize exit cost · 300 sats near expiry, 0.2% + 300 sats when early']); + assert.equal(await page.$eval('#ark-custody-select', (e) => e.value), 'custodial', 'coins expire by default'); assert.equal(await page.evaluate(() => coopFeature.getCustody()), 'custodial'); + assert.match(await page.$eval('.ark-custody-desc', (e) => e.textContent), /keeps its own exit until it expires/); + assert.doesNotMatch(tx, /Coins renew in the last hours|Every transfer adds a hop/, 'only the chosen strategy is described'); + assert.doesNotMatch(tx, /timelock|Spending coin carries|old Coinos/, 'no jargon on the card'); + const order = await page.$$eval('.card h4, .card h3', (es) => es.map((e) => e.textContent).filter((x) => /Spending coins|Custody|Renewal/.test(x))); + assert.deepEqual(order, ['Spending coins', 'Custody', 'Renewal'], 'coins first, then the strategy, then renewal'); assert.match(tx, /held by Coinos/, 'the coin past its timelock is held, not "expired"'); assert.doesNotMatch(tx, /5,000 sats\s+expired/, 'the row never says "expired"'); assert.match(tx, /With “Allow coins to expire”, nothing renews on its own/); if (process.env.SHOT) await page.screenshot({ path: process.env.SHOT, fullPage: true }); - console.log('✓ the custody card: three tiers with their prices, coinos holding by default, a past-timelock coin shown as held'); + console.log('✓ the custody card: a drop-down of three strategies with their prices, coins expire by default, one description, a past-timelock coin shown as held'); // the unilateral exit only counts the coin still inside its timelock assert.match(tx, /Exit now/); @@ -103,13 +103,14 @@ try { console.log('✓ a unilateral exit counts only the coin still inside its timelock'); // opt into keeping the exit: persisted, the page reprices, renewal starts covering - await page.click('.ark-custody-opt[data-tier=self] input'); await sleep(400); + await page.select('#ark-custody-select', 'self'); await sleep(400); assert.equal(await page.evaluate(() => coopFeature.getCustody()), 'self'); assert.equal(await page.evaluate(() => coopApp.wallet.loadFeatureState('arkCustody')), 'self', 'saved with the wallet'); tx = await text(); assert.match(tx, /Custody choice saved/); assert.doesNotMatch(tx, /nothing renews on its own/); assert.match(tx, /renews coins automatically once they are close to expiring/); + assert.match(await page.$eval('.ark-custody-desc', (e) => e.textContent), /Coins renew in the last hours/); const calls = await page.evaluate(() => coopFeature.autoRefreshCalls); assert.deepEqual(calls[0], ['ensure', 6, 8, 1], 'backstop orders registered at once (regtest margin 6)'); assert.deepEqual(calls[1], ['refresh', ['coin-1', 'coin-2']], 'the coin past its timelock is renewed now with the small change; the deep healthy one is left alone'); @@ -117,12 +118,12 @@ try { // the exit tier also flattens the deep coin — but not the deep 800-sat one on its own merits await page.evaluate(() => { coopFeature.autoRefreshCalls.length = 0; }); - await page.click('.ark-custody-opt[data-tier=exit] input'); await sleep(400); + await page.select('#ark-custody-select', 'exit'); await sleep(400); assert.equal(await page.evaluate(() => coopFeature.getCustody()), 'exit'); const calls2 = await page.evaluate(() => coopFeature.autoRefreshCalls); assert.deepEqual(calls2[1], ['refresh', ['coin-1', 'coin-0', 'coin-2']], 'expired + deep (worth it) + small change ride one round'); console.log('✓ "Keep my exit cheap" renews the deep coin too, when the fee is under a tenth of it'); - await page.click('.ark-custody-opt[data-tier=custodial] input'); await sleep(300); + await page.select('#ark-custody-select', 'custodial'); await sleep(300); assert.equal(await page.evaluate(() => coopFeature.getCustody()), 'custodial'); assert.match(await text(), /nothing renews on its own/); console.log('✓ the other tiers switch the same way, and back'); |
