diff options
| author | Adam Soltys <[email protected]> | 2026-08-04 07:08:34 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-04 07:08:34 -0700 |
| commit | d04882474a98e3429d7d4b71a56213c6240c2816 (patch) | |
| tree | 7daec1ed2568313fc2d605a5b7167ceee2f077c5 /src | |
| parent | a07ea35d32d21a5f1044ebdc17d53a79fc7c184a (diff) | |
| download | coinosv3-d04882474a98e3429d7d4b71a56213c6240c2816.tar.gz coinosv3-d04882474a98e3429d7d4b71a56213c6240c2816.zip | |
One More-options link replaces the BOLT 12 link on Receive
Behind it: a one-time BOLT 11 invoice (minted by the ASP), the reusable
BOLT 12 offer flow, the wallet's raw ark address, and the CLINK offer
code — each on its own pane so only one scannable code shows at a time.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015QwAtBSpKvECqp1rHLHjJk
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/names.js | 136 | ||||
| -rw-r--r-- | src/i18n.js | 17 |
2 files changed, 137 insertions, 16 deletions
diff --git a/src/features/names.js b/src/features/names.js index 862511f..47bf0c6 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -311,9 +311,9 @@ export function namesFeature(ctx) { claimForm(false)); } const addr = `${st.name}@${st.domain || DOMAIN}`; - // The offer section takes over the card when open: two payment codes on - // screen at once is a good way to have someone scan the wrong one. - if (ui.namesOfferOpen) return h('div', { class: 'card col', style: 'align-items:center;gap:14px' }, seg, offerSection()); + // The more-options section takes over the card when open: two payment + // codes on screen at once is a good way to have someone scan the wrong one. + if (ui.namesMore) return h('div', { class: 'card col', style: 'align-items:center;gap:14px' }, seg, moreSection()); return h('div', { class: 'card col', style: 'align-items:center;gap:14px' }, seg, h('div', { html: qrSvg(addr) }), @@ -331,7 +331,7 @@ export function namesFeature(ctx) { onClick: () => copy(addr), html: '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>', }))), - offerSection()); + moreSection()); } // ---- BOLT 12 offers with a memo ---------------------------------------- @@ -368,18 +368,9 @@ export function namesFeature(ctx) { ui.namesOfferBusy = false; render(); } - function offerSection() { - if (!ui.namesOfferOpen) { - return h('button', { - class: 'linklike small', - onClick: () => { ui.namesOfferOpen = true; loadOffers(); render(); }, - }, t('namesOfferOpen')); - } + function offerPane() { const offers = Array.isArray(ui.namesOffers) ? ui.namesOffers : []; return h('div', { class: 'col', style: 'gap:10px;width:100%' }, - h('div', { class: 'row between', style: 'align-items:baseline' }, - h('strong', {}, t('namesOfferTitle')), - h('button', { class: 'linklike small', onClick: () => { ui.namesOfferOpen = false; render(); } }, t('back'))), h('div', { class: 'row gap6' }, h('input', { type: 'text', class: 'grow', maxlength: '120', @@ -403,6 +394,123 @@ export function namesFeature(ctx) { : null))); } + // ---- one-time BOLT 11 invoice (minted by the ASP against this wallet) --- + + async function createInvoice() { + const inv = ui.namesInv || (ui.namesInv = {}); + const sat = Math.floor(Number(inv.amount)); + if (!sat || sat < 1) { inv.error = t('namesInvBadAmount'); render(); return; } + inv.busy = true; inv.error = null; render(); + try { + const a = await hook('arkMakeInvoice', sat, (inv.memo || '').trim().slice(0, 100)); + inv.invoice = a.invoice; + } catch (e) { inv.error = e.message; } + inv.busy = false; render(); + } + + function invoicePane() { + const inv = ui.namesInv || (ui.namesInv = {}); + if (inv.invoice) + return h('div', { class: 'col', style: 'gap:10px;width:100%;align-items:center' }, + h('div', { html: qrSvg(inv.invoice.toUpperCase(), { ec: 'L', mode: 'Alphanumeric' }) }), + h('div', { class: 'addr-box break', style: 'width:100%;font-size:11px' }, inv.invoice), + h('div', { class: 'row gap6' }, + copyBtn(inv.invoice, t('copy')), + h('button', { class: 'btn-sm', onClick: () => { ui.namesInv = {}; render(); } }, t('namesInvNew')))); + return h('div', { class: 'col', style: 'gap:10px;width:100%' }, + h('input', { + type: 'number', inputmode: 'numeric', min: '1', + placeholder: t('namesInvAmountPh'), + value: inv.amount || '', + onInput: (e) => { inv.amount = e.target.value; }, + }), + h('input', { + type: 'text', maxlength: '100', + placeholder: t('namesInvMemoPh'), + value: inv.memo || '', + onInput: (e) => { inv.memo = e.target.value; }, + }), + inv.error ? h('div', { class: 'notice err small' }, inv.error) : null, + h('button', { class: 'btn-primary btn-block', disabled: !!inv.busy, onClick: createInvoice }, + inv.busy ? h('span', { class: 'spinner sm' }) : t('namesInvCreate'))); + } + + // ---- the raw ark address & the CLINK offer code ------------------------- + + function arkPane() { + if (!ui.namesArkAddr) { + ui.namesArkAddr = 'loading'; + Promise.resolve(hook('arkStaticAddress')) + .then((a) => { ui.namesArkAddr = a ? { address: a } : { error: t('namesNeedArk') }; }) + .catch((e) => { ui.namesArkAddr = { error: e.message }; }) + .then(render); + } + const st = ui.namesArkAddr; + if (st === 'loading') return h('span', { class: 'spinner', style: 'align-self:center' }); + if (st.error) return h('div', { class: 'notice err small', style: 'width:100%' }, st.error); + return h('div', { class: 'col', style: 'gap:10px;width:100%;align-items:center' }, + h('div', { html: qrSvg(st.address) }), + h('div', { class: 'addr-box break', style: 'width:100%;font-size:11px' }, st.address), + copyBtn(st.address, t('copy'))); + } + + function clinkPane() { + const code = hook('nwcOfferString'); + if (!code) return h('div', { class: 'notice err small', style: 'width:100%' }, t('namesNeedArk')); + return h('div', { class: 'col', style: 'gap:10px;width:100%;align-items:center' }, + h('div', { html: qrSvg(code) }), + h('div', { class: 'addr-box break', style: 'width:100%;font-size:10px' }, code), + copyBtn(code, t('copy')), + h('p', { class: 'small muted', style: 'margin:0' }, t('namesZapCodeHow'))); + } + + // ---- more options ------------------------------------------------------- + // The address covers almost everyone; behind one link live the other codes + // a payer might insist on. + + function menuPane() { + const opt = (id, label, how, onOpen) => h('div', { + class: 'item col clickable', style: 'gap:2px;align-items:flex-start', + onClick: () => { ui.namesMore = id; if (onOpen) onOpen(); render(); }, + }, + h('strong', { class: 'small' }, label), + h('span', { class: 'small muted' }, how)); + return h('div', { class: 'list', style: 'width:100%' }, + opt('bolt11', t('namesMoreBolt11'), t('namesMoreBolt11How')), + opt('bolt12', t('namesMoreBolt12'), t('namesMoreBolt12How'), loadOffers), + opt('ark', t('namesMoreArk'), t('namesMoreArkHow')), + opt('clink', t('namesMoreClink'), t('namesMoreClinkHow'))); + } + + function moreSection() { + if (!ui.namesMore) { + return h('button', { + class: 'linklike small', + onClick: () => { ui.namesMore = 'menu'; render(); }, + }, t('namesMoreOpen')); + } + const titles = { + menu: t('namesMoreTitle'), + bolt11: t('namesMoreBolt11'), + bolt12: t('namesMoreBolt12'), + ark: t('namesMoreArk'), + clink: t('namesMoreClink'), + }; + const body = ui.namesMore === 'bolt11' ? invoicePane() + : ui.namesMore === 'bolt12' ? offerPane() + : ui.namesMore === 'ark' ? arkPane() + : ui.namesMore === 'clink' ? clinkPane() + : menuPane(); + return h('div', { class: 'col', style: 'gap:10px;width:100%' }, + h('div', { class: 'row between', style: 'align-items:baseline' }, + h('strong', {}, titles[ui.namesMore] || titles.menu), + h('button', { + class: 'linklike small', + onClick: () => { ui.namesMore = ui.namesMore === 'menu' ? null : 'menu'; render(); }, + }, t('back'))), + body); + } + return { id: 'names', init() { checked = false; lastError = null; refresh(); }, diff --git a/src/i18n.js b/src/i18n.js index 3e82eca..fdb5ccc 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -597,8 +597,21 @@ const STR = { namesClaim: 'Claim', namesClaimed: 'You are now {name}', namesCopy: 'Copy address', - namesOfferOpen: 'Need a Lightning offer (BOLT 12)?', - namesOfferTitle: 'Lightning offer', + namesMoreOpen: 'More options', + namesMoreTitle: 'More ways to get paid', + namesMoreBolt11: 'Lightning invoice (BOLT 11)', + namesMoreBolt11How: 'A one-time invoice for a specific amount', + namesMoreBolt12: 'Lightning offer (BOLT 12)', + namesMoreBolt12How: 'A reusable offer — mining pools like Ocean pay to one', + namesMoreArk: 'Ark address', + namesMoreArkHow: 'Get paid directly from any Ark wallet', + namesMoreClink: 'Nostr offer (CLINK)', + namesMoreClinkHow: 'A static code Nostr wallets can pay', + namesInvAmountPh: 'Amount (sats)', + namesInvMemoPh: 'Description (optional)', + namesInvCreate: 'Create invoice', + namesInvNew: 'New invoice', + namesInvBadAmount: 'Enter an amount in sats', namesOfferMemoPh: 'Label (e.g. Ocean payout)', namesOfferCreate: 'Create', namesOfferNoMemo: 'No label', |
