diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/names.js | 79 | ||||
| -rw-r--r-- | src/i18n.js | 7 |
2 files changed, 83 insertions, 3 deletions
diff --git a/src/features/names.js b/src/features/names.js index a30038e..788c944 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -40,10 +40,11 @@ export function namesFeature(ctx) { async function post(path, method, payload, signer) { const url = `${REGISTRAR}${path}`; - const body = JSON.stringify(payload); + const hasBody = method !== 'GET'; + const body = hasBody ? JSON.stringify(payload) : ''; const auth = await withTimeout(nip98Header(signer || walletSigner(), url, method, body), 12000, 'signing'); const r = await withTimeout(fetch(url, { - method, headers: { 'content-type': 'application/json', authorization: auth }, body, + method, headers: { 'content-type': 'application/json', authorization: auth }, ...(hasBody ? { body } : {}), }), 12000, 'registrar'); const j = await r.json().catch(() => ({})); if (!r.ok || j.error) throw new Error(j.error || `registrar refused (${r.status})`); @@ -301,7 +302,79 @@ export function namesFeature(ctx) { seg, h('div', { html: qrSvg(addr) }), h('div', { class: 'addr-box', style: 'width:100%;text-align:center;font-size:16px' }, addr), - copyBtn(addr, t('namesCopy'))); + copyBtn(addr, t('namesCopy')), + offerSection()); + } + + // ---- BOLT 12 offers with a memo ---------------------------------------- + // A reusable offer some payers want instead of a lightning address (mining + // pools like Ocean pay to one, and the memo is how you label the payout). + // Settled offer payments ride the registrar's forwarder into Spending, the + // same path the address already uses. + async function loadOffers() { + if (ui.namesOffers) return; + ui.namesOffers = 'loading'; + try { + const j = await post('/offer', 'GET'); + ui.namesOffers = j.offers || []; + } catch (e) { + ui.namesOffers = []; + ui.namesOfferError = e.message; + } + render(); + } + + async function createOffer() { + const memo = (ui.namesOfferMemo || '').trim(); + ui.namesOfferBusy = true; ui.namesOfferError = null; render(); + try { + const j = await post('/offer', 'POST', { memo }); + ui.namesOffers = [ + ...(Array.isArray(ui.namesOffers) ? ui.namesOffers.filter((o) => o.offerId !== j.offerId) : []), + { offerId: j.offerId, memo: j.memo, bolt12: j.bolt12 }, + ]; + ui.namesOfferMemo = ''; + } catch (e) { + ui.namesOfferError = e.message; + } + ui.namesOfferBusy = false; render(); + } + + function offerSection() { + if (!ui.namesOfferOpen) { + return h('button', { + class: 'linklike small', + onClick: () => { ui.namesOfferOpen = true; loadOffers(); render(); }, + }, t('namesOfferOpen')); + } + const offers = Array.isArray(ui.namesOffers) ? ui.namesOffers : []; + return h('div', { class: 'col', style: 'gap:10px;width:100%' }, + h('div', { class: 'divider' }), + h('div', { class: 'row between', style: 'align-items:baseline' }, + h('strong', {}, t('namesOfferTitle')), + h('button', { class: 'linklike small', onClick: () => { ui.namesOfferOpen = false; render(); } }, t('hide'))), + h('p', { class: 'small muted', style: 'margin:0' }, t('namesOfferDesc')), + h('div', { class: 'row gap6' }, + h('input', { + type: 'text', class: 'grow', maxlength: '120', + placeholder: t('namesOfferMemoPh'), + value: ui.namesOfferMemo || '', + onInput: (e) => { ui.namesOfferMemo = e.target.value; }, + }), + h('button', { class: 'btn-sm', disabled: !!ui.namesOfferBusy, onClick: createOffer }, + ui.namesOfferBusy ? h('span', { class: 'spinner sm' }) : t('namesOfferCreate'))), + ui.namesOfferError ? h('div', { class: 'notice err small' }, ui.namesOfferError) : null, + ui.namesOffers === 'loading' ? h('span', { class: 'spinner sm' }) : null, + ...offers.map((o) => + h('div', { class: 'col', style: 'gap:6px;width:100%' }, + h('div', { class: 'small muted' }, o.memo || t('namesOfferNoMemo')), + h('div', { class: 'addr-box break', style: 'width:100%;font-size:11px' }, o.bolt12), + h('div', { class: 'row gap6' }, + copyBtn(o.bolt12, t('copy')), + h('button', { class: 'btn-sm', onClick: () => { ui.namesOfferQr = ui.namesOfferQr === o.offerId ? null : o.offerId; render(); } }, t('namesOfferQr'))), + ui.namesOfferQr === o.offerId + ? h('div', { style: 'align-self:center', html: qrSvg(o.bolt12.toUpperCase(), { ec: 'L', mode: 'Alphanumeric' }) }) + : null))); } return { diff --git a/src/i18n.js b/src/i18n.js index 3dd710a..b387285 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -548,6 +548,13 @@ const STR = { namesClaim: 'Claim', namesClaimed: 'You are now {name}', namesCopy: 'Copy address', + namesOfferOpen: 'Need a Lightning offer (BOLT 12)?', + namesOfferTitle: 'Lightning offer', + namesOfferDesc: 'A reusable BOLT 12 offer some payers want instead of an address — mining pools like Ocean pay to one. Payments land in Spending exactly like your address.', + namesOfferMemoPh: 'Label (e.g. Ocean payout)', + namesOfferCreate: 'Create', + namesOfferNoMemo: 'No label', + namesOfferQr: 'QR', namesRelease: 'Release', namesReleased: 'Name released', namesNeedArk: 'Your wallet is still connecting — try again in a moment', |
