aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/gifts.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/gifts.js')
-rw-r--r--src/features/gifts.js45
1 files changed, 38 insertions, 7 deletions
diff --git a/src/features/gifts.js b/src/features/gifts.js
index 0ed3dc1..bcc9b3f 100644
--- a/src/features/gifts.js
+++ b/src/features/gifts.js
@@ -32,6 +32,7 @@ export function giftsFeature(ctx) {
try {
const am = location.pathname.match(/^\/ag\/([A-Za-z0-9]+)\/?$/i); // ark gift
if (am) {
+ ui.claimNote = readNoteFrag();
history.replaceState(null, '', '/');
return arkGiftOf(am[1]) ? am[1] : null;
}
@@ -43,6 +44,7 @@ export function giftsFeature(ctx) {
if (hm) code = hm[1];
}
if (!code) return null;
+ if (pm) ui.claimNote = readNoteFrag();
history.replaceState(null, '', '/');
return previewGift(code) ? code : null;
} catch {
@@ -57,6 +59,7 @@ export function giftsFeature(ctx) {
try {
const m = location.pathname.match(/^\/lg\/([A-Za-z0-9_-]+)\/?$/);
if (!m) return null;
+ ui.claimNote = null;
history.replaceState(null, '', '/');
return previewLockedGift(m[1]);
} catch { return null; }
@@ -120,6 +123,25 @@ export function giftsFeature(ctx) {
if (!acceptLockedPayload(payload)) { ui.claimError = t('claimCodeWrong'); render(); }
}
+ // A gift's note rides its link after the # (#m=<text>): the fragment never
+ // leaves the browser, so no server sees it, and it is only as private as the
+ // link itself (which already IS the money).
+ const GIFT_NOTE_MAX = 280;
+ const noteFrag = (note) => (note ? '#m=' + encodeURIComponent(note) : '');
+ function readNoteFrag() {
+ try {
+ const m = new URLSearchParams(location.hash.slice(1)).get('m');
+ return m ? m.trim().slice(0, GIFT_NOTE_MAX) || null : null;
+ } catch { return null; }
+ }
+ // the sender's words, quoted above the amount wherever the gift is opened
+ const giftNoteView = () => (ui.claimNote
+ ? h('blockquote', { class: 'gift-note' }, ui.claimNote)
+ : null);
+ // with a note the link has lower case in it: a byte-mode QR (the bare
+ // link is upper-cased into the denser alphanumeric mode)
+ const giftQr = (url, hasNote) => qrSvg(hasNote ? url : url.toUpperCase(), { ec: 'L', mode: hasNote ? undefined : 'Alphanumeric' });
+
// Entry point for claiming a gift code. If the recipient already has wallet(s),
// offer to claim into one or make a new one; otherwise go straight to a fresh
// wallet (the original first-timer flow).
@@ -171,6 +193,7 @@ export function giftsFeature(ctx) {
h('div', { class: 'card col', style: 'align-items:center;text-align:center;gap:12px' },
h('div', { class: 'check-badge', style: 'background:var(--accent)' }, '🎁'),
h('h2', { style: 'margin:0' }, t('giftWelcome')),
+ giftNoteView(),
pv ? h('div', { class: 'amt', style: 'font-size:30px' }, h('span', { class: 'amount-pos' }, fmtAmount(pv.room)), ' ', unitTag('unit')) : null,
h('div', { class: 'small faint' }, claimFeeNote(code)),
h('p', { class: 'muted', style: 'margin:0' }, t('claimIntoPrompt'))
@@ -311,6 +334,7 @@ export function giftsFeature(ctx) {
h('div', { class: 'card col', style: 'align-items:center;text-align:center;gap:12px' },
h('div', { class: 'check-badge', style: 'background:var(--accent)' }, '🎁'),
h('h2', { style: 'margin:0' }, t('giftWelcome')),
+ giftNoteView(),
h('div', { class: 'amt', style: 'font-size:30px' },
h('span', { class: 'amount-pos' }, fmtAmount(total)), ' ', unitTag('unit')
),
@@ -534,13 +558,14 @@ export function giftsFeature(ctx) {
// the sender can re-share an unclaimed gift.
function viewGiftView() {
const g = ui.viewGift; // { code, locked, amount, claimCode }
- const url = `${location.origin}/g/${g.code}`;
+ const url = `${location.origin}/g/${g.code}${noteFrag(g.note)}`;
let svg = null;
- try { svg = qrSvg(url.toUpperCase(), { ec: 'L', mode: 'Alphanumeric' }); } catch {}
+ try { svg = giftQr(url, !!g.note); } catch {}
return h('div', { class: 'col', style: 'gap:16px;padding:16px;max-width:460px;margin:0 auto;width:100%' },
h('div', { class: 'card col', style: 'gap:12px;align-items:center' },
h('h3', { style: 'margin:0' }, t('giftView')),
g.amount != null ? h('div', { class: 'amt' }, fmtAmount(g.amount), ' ', unitTag()) : null,
+ g.note ? h('blockquote', { class: 'gift-note', style: 'width:100%' }, g.note) : null,
svg ? h('div', { html: svg }) : h('div', { class: 'small faint', style: 'text-align:center;padding:8px' }, t('giftQrTooLong')),
h('div', { class: 'addr-box break', style: 'width:100%;font-size:12px' }, url),
copyBtn(url, t('copyLink')),
@@ -575,7 +600,7 @@ export function giftsFeature(ctx) {
}
ui.busy = true; ui.giftError = ''; render();
try {
- const g = await hook('arkGiftCreate', amt);
+ const g = await hook('arkGiftCreate', amt, giftNoteNow());
ui.giftIsArk = true;
finishGift(g, amt);
} catch (e) {
@@ -623,8 +648,10 @@ export function giftsFeature(ctx) {
function finishGift(g, fallbackAmt) {
const amount = g.amount || fallbackAmt || 0;
ui.giftCode = g.code;
- wallet.recordGift({ code: g.code, locked: false, amount, outpoints: g.reserved });
+ ui.giftNoteMade = giftNoteNow(); // the link says what was typed when it was made
+ wallet.recordGift({ code: g.code, locked: false, amount, outpoints: g.reserved, note: ui.giftNoteMade });
}
+ const giftNoteNow = () => String(ui.giftNote || '').trim().slice(0, GIFT_NOTE_MAX) || null;
function giftCard() {
const active = wallet.outstandingGifts();
@@ -639,13 +666,14 @@ export function giftsFeature(ctx) {
// (many-input) gift can exceed QR capacity — then fall back to the link.
(() => {
let svg = null;
- try { svg = qrSvg(giftUrl().toUpperCase(), { ec: 'L', mode: 'Alphanumeric' }); } catch {}
+ try { svg = giftQr(giftUrl(), !!ui.giftNoteMade); } catch {}
return svg ? h('div', { html: svg }) : h('div', { class: 'small faint', style: 'text-align:center;padding:8px' }, t('giftQrTooLong'));
})(),
+ ui.giftNoteMade ? h('blockquote', { class: 'gift-note', style: 'width:100%' }, ui.giftNoteMade) : null,
h('div', { class: 'addr-box break', style: 'width:100%;font-size:12px' }, giftUrl()),
h('div', { class: 'row gap6 wrap' },
copyBtn(giftUrl(), t('copyLink')),
- h('button', { class: 'btn-sm grow', onClick: () => { ui.giftCode = null; ui.giftAmount = ''; ui.giftMax = false; ui.giftIsArk = false; render(); } }, t('giftAnother'))
+ h('button', { class: 'btn-sm grow', onClick: () => { ui.giftCode = null; ui.giftAmount = ''; ui.giftNote = ''; ui.giftNoteMade = null; ui.giftMax = false; ui.giftIsArk = false; render(); } }, t('giftAnother'))
)
)
: ui.giftSplitOffer
@@ -700,6 +728,9 @@ export function giftsFeature(ctx) {
ui.giftSource === 'chain'
? h('div', { class: 'notice warn small' }, t('giftChainWarn'))
: null,
+ // an optional note, shown to whoever opens the gift
+ h('textarea', { class: 'gift-note-input', rows: 2, maxlength: GIFT_NOTE_MAX, placeholder: t('giftNotePlaceholder'),
+ value: ui.giftNote || '', onInput: (e) => { ui.giftNote = e.target.value; } }),
ui.giftError && h('div', { class: 'notice err' }, ui.giftError),
ui.busy
? h('button', { class: 'btn-block', disabled: true }, h('span', { class: 'spinner sm' }))
@@ -872,7 +903,7 @@ export function giftsFeature(ctx) {
// Gift link: presign a chosen amount as a #gift= PSBT that whoever opens claims
// into a fresh wallet only they control. The coin is reserved until claimed.
function giftUrl() {
- return `${location.origin}/${ui.giftIsArk ? 'ag' : 'g'}/${ui.giftCode}`;
+ return `${location.origin}/${ui.giftIsArk ? 'ag' : 'g'}/${ui.giftCode}${noteFrag(ui.giftNoteMade)}`;
}
function giftRate() {