From ae85dc0afba70f40b333cf03c64abbbf4c8dfea0 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 5 Sep 2026 19:38:10 -0700 Subject: Point of sale: a coinos-pos terminal works against a name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registrar: the owner mints a bearer POS token from Settings (NIP-98 POST /pos/token, DELETE revokes; /admin/pos/token for operators). With it the terminal gets /pos/me, rings up sales at /pos/invoice (fiat via coinos.io rates, or sats), reads /pos/payments, and holds a websocket at /pos/ws that gets a payment message when the sale settles. Sales are always minted on our node so the settle loop sees them and the ordinary forward path delivers the sats to the name's ark address. The name's lnurlp pins the newest unpaid sale under five minutes old — coinos.io's /p/ rule — so an NFC tap or a scan pays exactly that invoice. Wallet: a "Point of sale" section on the names card creates, copies and revokes tokens. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y --- src/features/names.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'src/features/names.js') diff --git a/src/features/names.js b/src/features/names.js index bd4ab4d..dc3426e 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -436,6 +436,10 @@ export function namesFeature(ctx) { h('div', { class: 'addr-box break', style: 'font-size:10px' }, code), copyBtn(code, t('namesZapCodeCopy'))) : null; })(), + h('details', { class: 'small faint' }, + h('summary', {}, t('namesPos')), + h('p', { style: 'margin:4px 0' }, t('namesPosHow')), + posSection(st)), h('details', { class: 'small faint' }, h('summary', {}, t('namesOwnNode')), h('p', { style: 'margin:4px 0' }, t('namesOwnNodeMenuHow')), @@ -452,6 +456,27 @@ export function namesFeature(ctx) { claimForm(false)); } + // A coinos POS terminal (coinos-pos) takes payments to this name with a + // bearer token the registrar mints for the owner: the terminal can ring up + // sales and see them settle, never spend. Shown once, never stored — the + // token goes straight into the terminal's config at coinos.io/pos. + function posSection(st) { + const tok = ui.posToken; + const body = () => ({ name: st.name, domain: st.domain || DOMAIN() }); + return h('div', { class: 'col', style: 'gap:6px' }, + tok ? h('div', { class: 'addr-box break', style: 'font-size:10px' }, tok) : null, + h('div', { class: 'row gap6', style: 'flex-wrap:wrap' }, + h('button', { class: 'btn-ghost btn-sm', onClick: async () => { + try { ui.posToken = (await post('/pos/token', 'POST', body())).token; render(); } + catch (e) { toast(e.message); } + } }, t('namesPosCreate')), + tok ? copyBtn(tok, t('namesPosCopy')) : null, + h('button', { class: 'btn-ghost btn-sm', onClick: async () => { + try { await post('/pos/token', 'DELETE', body()); ui.posToken = null; toast(t('namesPosRevoked')); render(); } + catch (e) { toast(e.message); } + } }, t('namesPosRevoke')))); + } + // The Receive tab's default pane: your name, big and scannable. function namePane(seg) { const st = load(); -- cgit v1.2.3