From 58f7de1729bac5db2ade612ed1fd159b22ef587e Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 25 Oct 2026 14:52:14 -0700 Subject: Gifts carry a note: an optional "Add a note" when making one, quoted above the amount for whoever opens it. It rides the link after the # (#m=…), so no server sees it; the gift's record keeps it, so Copy link again and View include it; with a note the QR is byte mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QjVvE4YAcK3AGwV7kZXeN4 --- src/features/ark.js | 8 +++---- src/features/gifts-wallet.js | 4 ++-- src/features/gifts.js | 45 +++++++++++++++++++++++++++++++++------- src/i18n.js | 1 + src/locales/ar.json | 1 + src/locales/bn.json | 1 + src/locales/de.json | 1 + src/locales/es.json | 1 + src/locales/fr.json | 1 + src/locales/hi.json | 1 + src/locales/id.json | 1 + src/locales/it.json | 1 + src/locales/ja.json | 1 + src/locales/ko.json | 1 + src/locales/mr.json | 1 + src/locales/pt.json | 1 + src/locales/ru.json | 1 + src/locales/sw.json | 1 + src/locales/ta.json | 1 + src/locales/te.json | 1 + src/locales/tr.json | 1 + src/locales/ur.json | 1 + src/locales/vi.json | 1 + src/locales/zh.json | 1 + src/style.css | 9 ++++++++ tools/gift-profile-setup-test.js | 11 +++++++--- 26 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/features/ark.js b/src/features/ark.js index 5ed9a9d..f17acff 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1719,7 +1719,7 @@ export function arkFeature(ctx) { const g = giftForMovement(m); const open = g && !g.claimed && !g.revoked; const chips = [ - open ? copyBtn(`${location.origin}/g/${encodeArkGiftCode(getNetwork(), g.amountSat, hex.decode(g.secretHex))}`, t('giftCopyLinkAgain')) : null, + open ? copyBtn(`${location.origin}/g/${encodeArkGiftCode(getNetwork(), g.amountSat, hex.decode(g.secretHex))}${g.note ? '#m=' + encodeURIComponent(g.note) : ''}`, t('giftCopyLinkAgain')) : null, m.preimage ? copyBtn(m.preimage, t('copyPreimage')) : null, m.txid ? copyBtn(m.txid, t('copyTxid')) : null, m.txid ? h('a', { class: 'btn btn-sm', href: url, target: '_blank', rel: 'noopener', onClick: (e) => { e.preventDefault(); openExternal(url); } }, t('viewOnMempool')) : null, @@ -3168,7 +3168,7 @@ export function arkFeature(ctx) { // Fund a fresh bearer gift identity. Shared by the gifts feature's hook and // the zap flow's locked-gift fallback; `lockedTo` records who a locked gift // was for (the sender can still revoke either kind while unclaimed). - async function createArkGift(amountSat, lockedTo = null) { + async function createArkGift(amountSat, lockedTo = null, note = null) { const mgr = await connectArk(); const secret = crypto.getRandomValues(new Uint8Array(32)); const secretHex = hex.encode(secret); @@ -3178,7 +3178,7 @@ export function arkFeature(ctx) { const action = mgr.state.actions.find((a) => a.id === actionId); if (!action || action.step === 'failed') throw new Error(action?.error || t('claimFailed')); const vtxoId = decodeVtxo(hex.decode((action.destBytesList || [action.destBytes])[0])).id; - arkGiftRecords().push({ id: vtxoId, amountSat, secretHex, created: Date.now(), revoked: false, claimed: false, ...(lockedTo ? { lockedTo } : {}) }); + arkGiftRecords().push({ id: vtxoId, amountSat, secretHex, created: Date.now(), revoked: false, claimed: false, ...(lockedTo ? { lockedTo } : {}), ...(note ? { note } : {}) }); mgr._save(); return { code: encodeArkGiftCode(getNetwork(), amountSat, secret), amount: amountSat, address }; } @@ -4490,7 +4490,7 @@ export function arkFeature(ctx) { setNetwork(g.net); return true; }, - arkGiftCreate(amountSat) { return createArkGift(amountSat); }, + arkGiftCreate(amountSat, note) { return createArkGift(amountSat, null, note); }, // Claim-side status: claimable (with the live amount), taken, wrongnet, or // unknown (not visible here — wrong server or not yet delivered). async arkGiftStatus(code) { diff --git a/src/features/gifts-wallet.js b/src/features/gifts-wallet.js index 824ddc0..7004b12 100644 --- a/src/features/gifts-wallet.js +++ b/src/features/gifts-wallet.js @@ -274,9 +274,9 @@ export function installGiftWallet(wallet) { // gift's link/QR can be re-viewed later and a claimed one identified in history. _giftRecordsKey() { return this._cacheKey() + ':giftrec'; }, - recordGift({ code, locked, amount, claimCode, outpoints }) { + recordGift({ code, locked, amount, claimCode, outpoints, note }) { if (!outpoints || !outpoints.length) return; - this.giftRecords()[outpoints[0]] = { id: outpoints[0], code, locked: !!locked, amount, claimCode: claimCode || null, outpoints, created: Date.now(), revoked: false }; + this.giftRecords()[outpoints[0]] = { id: outpoints[0], code, locked: !!locked, amount, claimCode: claimCode || null, outpoints, created: Date.now(), revoked: false, ...(note ? { note } : {}) }; this._saveGiftRecords(); try { this.saveCache(); } catch {} // publish the new gift to cross-device sync }, 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=): 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() { diff --git a/src/i18n.js b/src/i18n.js index ebd8d31..b542dfd 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1059,6 +1059,7 @@ const STR = { giftWelcome: 'You’ve been sent Bitcoin', giftBackedUp: 'I’ve written it down', giftLink: 'Gift some Bitcoin', + giftNotePlaceholder: 'Add a note (optional)', giftLinkDesc: 'Presign an amount as a link. Whoever opens it claims the Bitcoin into a brand-new wallet that only they control.', giftLinkWarn: '⚠ Anyone who opens this link can claim the amount, so share it only with the person you are gifting. Keep amounts small.', giftLinkReveal: 'Create gift link', diff --git a/src/locales/ar.json b/src/locales/ar.json index d6b5a54..f7cda76 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -364,6 +364,7 @@ "giftWelcome": "لقد أُرسلت إليك عملة Bitcoin", "giftBackedUp": "لقد دوّنتها", "giftLink": "أهدِ بعض عملات Bitcoin", + "giftNotePlaceholder": "أضف ملاحظة (اختياري)", "giftLinkDesc": "وقّع مبلغاً مسبقاً كرابط. من يفتحه يطالب بعملات Bitcoin في محفظة جديدة تماماً يتحكم بها وحده.", "giftLinkWarn": "⚠ أي شخص يفتح هذا الرابط يمكنه المطالبة بالمبلغ، لذا شاركه فقط مع الشخص الذي تهديه إياه. أبقِ المبالغ صغيرة.", "giftLinkReveal": "إنشاء رابط الإهداء", diff --git a/src/locales/bn.json b/src/locales/bn.json index f36ba9e..367ce7e 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -364,6 +364,7 @@ "giftWelcome": "আপনাকে Bitcoin পাঠানো হয়েছে", "giftBackedUp": "আমি এটি লিখে রেখেছি", "giftLink": "কিছু Bitcoin উপহার দিন", + "giftNotePlaceholder": "একটি নোট যোগ করুন (ঐচ্ছিক)", "giftLinkDesc": "একটি পরিমাণ লিঙ্ক হিসেবে আগে থেকে সই করুন। যে এটি খুলবে সে Bitcoin-টি একটি একদম নতুন ওয়ালেটে দাবি করে, যা কেবল সে-ই নিয়ন্ত্রণ করে।", "giftLinkWarn": "⚠ যে কেউ এই লিঙ্ক খুললে পরিমাণটি দাবি করতে পারে, তাই কেবল যাকে উপহার দিচ্ছেন তার সাথেই এটি শেয়ার করুন। পরিমাণ কম রাখুন।", "giftLinkReveal": "লিংক নির্মাণ করুন", diff --git a/src/locales/de.json b/src/locales/de.json index 18951fb..da4a616 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -364,6 +364,7 @@ "giftWelcome": "Dir wurde Bitcoin gesendet", "giftBackedUp": "Ich habe sie aufgeschrieben", "giftLink": "Bitcoin verschenken", + "giftNotePlaceholder": "Notiz hinzufügen (optional)", "giftLinkDesc": "Signiere einen Betrag im Voraus als Link. Wer ihn öffnet, beansprucht das Bitcoin in einer brandneuen Wallet, die nur er selbst kontrolliert.", "giftLinkWarn": "⚠ Wer diesen Link öffnet, kann den Betrag beanspruchen. Teile ihn daher nur mit der Person, die du beschenkst. Halte die Beträge klein.", "giftLinkReveal": "Geschenk-Link erstellen", diff --git a/src/locales/es.json b/src/locales/es.json index ca5f2e3..2ee5478 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -364,6 +364,7 @@ "giftWelcome": "Te han enviado Bitcoin", "giftBackedUp": "Ya la anoté", "giftLink": "Regala algo de Bitcoin", + "giftNotePlaceholder": "Añade una nota (opcional)", "giftLinkDesc": "Prefirma un importe como enlace. Quien lo abra reclama el Bitcoin en una billetera totalmente nueva que solo esa persona controla.", "giftLinkWarn": "⚠ Cualquiera que abra este enlace puede reclamar el importe, así que compártelo solo con la persona a quien se lo regalas. Mantén los importes pequeños.", "giftLinkReveal": "Crear enlace de regalo", diff --git a/src/locales/fr.json b/src/locales/fr.json index 33d4cca..36578b7 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -364,6 +364,7 @@ "giftWelcome": "Vous avez reçu des Bitcoin", "giftBackedUp": "Je l’ai notée", "giftLink": "Offrir des Bitcoin", + "giftNotePlaceholder": "Ajouter un mot (facultatif)", "giftLinkDesc": "Présignez un montant sous forme de lien. Celui qui l’ouvre récupère les Bitcoin dans un tout nouveau portefeuille qu’il est seul à contrôler.", "giftLinkWarn": "⚠ Quiconque ouvre ce lien peut récupérer le montant, alors ne le partagez qu’avec la personne à qui vous l’offrez. Gardez les montants faibles.", "giftLinkReveal": "Créer le lien cadeau", diff --git a/src/locales/hi.json b/src/locales/hi.json index 5857818..4e05804 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -364,6 +364,7 @@ "giftWelcome": "आपको Bitcoin भेजा गया है", "giftBackedUp": "मैंने इसे लिख लिया है", "giftLink": "कुछ Bitcoin उपहार में दें", + "giftNotePlaceholder": "एक नोट जोड़ें (वैकल्पिक)", "giftLinkDesc": "एक राशि को लिंक के रूप में पहले से साइन करें। जो भी इसे खोलेगा, वह Bitcoin को एक बिल्कुल नए वॉलेट में प्राप्त कर लेगा जिसे केवल वही नियंत्रित करता है।", "giftLinkWarn": "⚠ जो भी इस लिंक को खोलेगा वह राशि प्राप्त कर सकता है, इसलिए इसे केवल उसी व्यक्ति के साथ साझा करें जिसे आप उपहार दे रहे हैं। राशि कम रखें।", "giftLinkReveal": "उपहार लिंक बनाएँ", diff --git a/src/locales/id.json b/src/locales/id.json index 28e7970..dfedcb8 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -137,6 +137,7 @@ "giftSkip": "Lewati dulu", "giftBackedUp": "Saya sudah mencatatnya", "giftLink": "Hadiahkan Bitcoin", + "giftNotePlaceholder": "Tambahkan catatan (opsional)", "giftLinkDesc": "Tandatangani sejumlah dana sebagai tautan. Siapa pun yang membukanya mengklaim Bitcoin itu ke dompet baru yang hanya dia kendalikan.", "giftLinkWarn": "⚠ Siapa pun yang membuka tautan ini bisa mengklaim jumlahnya, jadi bagikan hanya kepada orang yang Anda beri hadiah. Jaga jumlahnya tetap kecil.", "giftAmountLabel": "Jumlah yang dihadiahkan", diff --git a/src/locales/it.json b/src/locales/it.json index 9660d81..0c83ad7 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -827,6 +827,7 @@ "giftWelcome": "Ti hanno inviato dei Bitcoin", "giftBackedUp": "L'ho scritta", "giftLink": "Regala dei Bitcoin", + "giftNotePlaceholder": "Aggiungi un messaggio (facoltativo)", "giftLinkDesc": "Prefirma un importo sotto forma di link. Chi lo apre riscuote i Bitcoin in un portafoglio nuovo di zecca che controlla solo lui.", "giftLinkWarn": "⚠ Chiunque apra questo link può riscuotere l'importo, quindi condividilo solo con la persona a cui fai il regalo. Mantieni importi piccoli.", "giftLinkReveal": "Crea link regalo", diff --git a/src/locales/ja.json b/src/locales/ja.json index ce46732..779ab0e 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -364,6 +364,7 @@ "giftWelcome": "Bitcoin が届きました", "giftBackedUp": "書き留めました", "giftLink": "Bitcoin を贈る", + "giftNotePlaceholder": "メッセージを添える(任意)", "giftLinkDesc": "金額をリンクとして事前署名します。リンクを開いた人が、その人だけが管理できる真新しいウォレットに Bitcoin を受け取ります。", "giftLinkWarn": "⚠ このリンクを開いた人は誰でもその金額を受け取れるので、贈る相手とだけ共有してください。金額は少なめにしましょう。", "giftLinkReveal": "ギフトリンクを作成", diff --git a/src/locales/ko.json b/src/locales/ko.json index ad2d340..97dfe5f 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -363,6 +363,7 @@ "giftWelcome": "Bitcoin을 받았습니다", "giftBackedUp": "적어 두었습니다", "giftLink": "Bitcoin 선물하기", + "giftNotePlaceholder": "메시지 추가 (선택)", "giftLinkDesc": "금액을 링크로 미리 서명하세요. 링크를 여는 사람은 그 Bitcoin을 자신만 통제하는 완전히 새로운 지갑으로 받게 됩니다.", "giftLinkWarn": "⚠ 이 링크를 여는 사람은 누구나 금액을 받을 수 있으니, 선물할 상대에게만 공유하세요. 금액은 적게 유지하세요.", "giftLinkReveal": "선물 링크 만들기", diff --git a/src/locales/mr.json b/src/locales/mr.json index c72ed32..ecfc799 100644 --- a/src/locales/mr.json +++ b/src/locales/mr.json @@ -137,6 +137,7 @@ "giftSkip": "आत्ता वगळा", "giftBackedUp": "मी ते लिहून ठेवले आहे", "giftLink": "थोडे Bitcoin भेट द्या", + "giftNotePlaceholder": "एक नोंद जोडा (पर्यायी)", "giftLinkDesc": "एखादी रक्कम लिंक म्हणून आधीच स्वाक्षरित करा. ती उघडणारा कोणीही ते Bitcoin फक्त त्याच्याच नियंत्रणातील एका नव्या कोऱ्या वॉलेटमध्ये घेतो.", "giftLinkWarn": "⚠ ही लिंक उघडणारा कोणीही ती रक्कम घेऊ शकतो, म्हणून ज्याला भेट देत आहात फक्त त्याच्यासोबतच ती शेअर करा. रक्कम कमी ठेवा.", "giftAmountLabel": "भेट देण्याची रक्कम", diff --git a/src/locales/pt.json b/src/locales/pt.json index f0a4e82..259bb52 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -364,6 +364,7 @@ "giftWelcome": "Você recebeu Bitcoin", "giftBackedUp": "Já anotei", "giftLink": "Presentear Bitcoin", + "giftNotePlaceholder": "Adicione um recado (opcional)", "giftLinkDesc": "Pré-assine um valor como um link. Quem o abrir resgata os Bitcoin para uma carteira novinha que só essa pessoa controla.", "giftLinkWarn": "⚠ Qualquer pessoa que abrir este link pode resgatar o valor, então compartilhe-o apenas com a pessoa que você está presenteando. Mantenha os valores baixos.", "giftLinkReveal": "Criar link de presente", diff --git a/src/locales/ru.json b/src/locales/ru.json index bd69688..8ae9fa7 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -364,6 +364,7 @@ "giftWelcome": "Вам отправили Bitcoin", "giftBackedUp": "Я записал(а)", "giftLink": "Подарить немного Bitcoin", + "giftNotePlaceholder": "Добавьте записку (необязательно)", "giftLinkDesc": "Заранее подпишите сумму в виде ссылки. Тот, кто откроет её, получит Bitcoin в совершенно новый кошелёк, которым управляет только он.", "giftLinkWarn": "⚠ Любой, кто откроет эту ссылку, может получить эту сумму, поэтому делитесь ею только с тем, кому дарите. Держите суммы небольшими.", "giftLinkReveal": "Создать ссылку-подарок", diff --git a/src/locales/sw.json b/src/locales/sw.json index 182668b..ddf1ca1 100644 --- a/src/locales/sw.json +++ b/src/locales/sw.json @@ -137,6 +137,7 @@ "giftSkip": "Ruka kwa sasa", "giftBackedUp": "Nimeshakiandika", "giftLink": "Toa Bitcoin kama zawadi", + "giftNotePlaceholder": "Ongeza ujumbe (si lazima)", "giftLinkDesc": "Tia saini kiasi mapema kama kiungo. Yeyote anayekifungua hudai Bitcoin kwenye pochi mpya kabisa anayoidhibiti yeye pekee.", "giftLinkWarn": "⚠ Yeyote anayefungua kiungo hiki anaweza kudai kiasi hicho, kwa hivyo kishiriki tu na mtu unayempa zawadi. Weka kiasi kidogo.", "giftAmountLabel": "Kiasi cha kutoa zawadi", diff --git a/src/locales/ta.json b/src/locales/ta.json index fd3825d..ef5371a 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -137,6 +137,7 @@ "giftSkip": "இப்போதைக்குத் தவிர்", "giftBackedUp": "நான் எழுதி வைத்துவிட்டேன்", "giftLink": "கொஞ்சம் Bitcoin பரிசளி", + "giftNotePlaceholder": "ஒரு குறிப்பைச் சேர்க்கவும் (விருப்பம்)", "giftLinkDesc": "ஒரு தொகையை இணைப்பாக முன்கூட்டியே கையொப்பமிடுங்கள். இதைத் திறப்பவர், அவர்கள் மட்டுமே கட்டுப்படுத்தும் புத்தம்புதிய வாலட்டிற்குள் Bitcoin-ஐ உரிமை கொள்வார்கள்.", "giftLinkWarn": "⚠ இந்த இணைப்பைத் திறக்கும் எவரும் தொகையை உரிமை கொள்ளலாம், எனவே நீங்கள் பரிசளிக்கும் நபருடன் மட்டுமே இதைப் பகிரவும். தொகையைக் குறைவாக வைத்திருங்கள்.", "giftAmountLabel": "பரிசளிக்கும் தொகை", diff --git a/src/locales/te.json b/src/locales/te.json index 87b750e..f7a6d34 100644 --- a/src/locales/te.json +++ b/src/locales/te.json @@ -137,6 +137,7 @@ "giftSkip": "ప్రస్తుతానికి దాటవేయి", "giftBackedUp": "నేను దాన్ని రాసుకున్నాను", "giftLink": "కొంత Bitcoin బహుమతిగా ఇవ్వండి", + "giftNotePlaceholder": "ఒక సందేశం జోడించండి (ఐచ్ఛికం)", "giftLinkDesc": "ఒక మొత్తాన్ని ముందుగా సంతకం చేసి లింక్‌గా ఇవ్వండి. దాన్ని తెరిచినవారు ఆ Bitcoinను తాము మాత్రమే నియంత్రించే సరికొత్త వాలెట్‌లోకి క్లెయిమ్ చేసుకుంటారు.", "giftLinkWarn": "⚠ ఈ లింక్ తెరిచిన ఎవరైనా ఆ మొత్తాన్ని క్లెయిమ్ చేయగలరు, కాబట్టి మీరు బహుమతి ఇస్తున్న వ్యక్తికి మాత్రమే దీన్ని పంచుకోండి. మొత్తాలను తక్కువగా ఉంచండి.", "giftAmountLabel": "బహుమతిగా ఇచ్చే మొత్తం", diff --git a/src/locales/tr.json b/src/locales/tr.json index b1530c1..6605c6c 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -363,6 +363,7 @@ "giftWelcome": "Size Bitcoin gönderildi", "giftBackedUp": "Yazdım", "giftLink": "Bitcoin hediye et", + "giftNotePlaceholder": "Not ekle (isteğe bağlı)", "giftLinkDesc": "Bir tutarı önceden imzalayıp bağlantı olarak verin. Bağlantıyı açan, Bitcoin’i yalnızca kendi kontrolündeki yepyeni bir cüzdana alır.", "giftLinkWarn": "⚠ Bu bağlantıyı açan herkes tutarı talep edebilir, bu yüzden yalnızca hediye ettiğiniz kişiyle paylaşın. Tutarları küçük tutun.", "giftLinkReveal": "Hediye bağlantısı oluştur", diff --git a/src/locales/ur.json b/src/locales/ur.json index 21f0071..258d1a3 100644 --- a/src/locales/ur.json +++ b/src/locales/ur.json @@ -137,6 +137,7 @@ "giftSkip": "ابھی چھوڑیں", "giftBackedUp": "میں نے اسے لکھ لیا ہے", "giftLink": "کچھ Bitcoin تحفے میں دیں", + "giftNotePlaceholder": "ایک نوٹ شامل کریں (اختیاری)", "giftLinkDesc": "ایک رقم کو بطور لنک پہلے سے دستخط کریں۔ جو بھی اسے کھولے وہ Bitcoin کو ایک بالکل نئے والیٹ میں وصول کر لیتا ہے جو صرف اسی کے کنٹرول میں ہوتا ہے۔", "giftLinkWarn": "⚠ جو بھی یہ لنک کھولے وہ اس رقم کا دعویٰ کر سکتا ہے، اس لیے اسے صرف اسی شخص کے ساتھ شیئر کریں جسے آپ تحفہ دے رہے ہیں۔ رقم کم رکھیں۔", "giftLinkReveal": "تحفہ لنک بنائیں", diff --git a/src/locales/vi.json b/src/locales/vi.json index cb87b30..4c2793a 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -137,6 +137,7 @@ "giftSkip": "Bỏ qua lúc này", "giftBackedUp": "Tôi đã ghi lại rồi", "giftLink": "Tặng một ít Bitcoin", + "giftNotePlaceholder": "Thêm lời nhắn (không bắt buộc)", "giftLinkDesc": "Ký trước một số tiền thành một liên kết. Ai mở liên kết sẽ nhận số Bitcoin đó vào một ví hoàn toàn mới mà chỉ riêng họ kiểm soát.", "giftLinkWarn": "⚠ Bất kỳ ai mở liên kết này đều có thể nhận số tiền, vì vậy chỉ chia sẻ với người bạn muốn tặng. Hãy giữ số tiền nhỏ.", "giftAmountLabel": "Số tiền muốn tặng", diff --git a/src/locales/zh.json b/src/locales/zh.json index 54169eb..a9cfef7 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -364,6 +364,7 @@ "giftWelcome": "有人给你转了 Bitcoin", "giftBackedUp": "我已经抄写下来了", "giftLink": "赠送一些 Bitcoin", + "giftNotePlaceholder": "添加留言(可选)", "giftLinkDesc": "将一笔金额预签为一个链接。打开链接的人可将这笔 Bitcoin 领取到一个仅由他们自己掌控的全新钱包中。", "giftLinkWarn": "⚠ 任何打开此链接的人都能领取这笔金额,所以请仅分享给你要赠送的对象。请保持金额较小。", "giftLinkReveal": "创建赠送链接", diff --git a/src/style.css b/src/style.css index 0fb8e9a..31231fe 100644 --- a/src/style.css +++ b/src/style.css @@ -2057,3 +2057,12 @@ html:has(.call-overlay:not(.call-min)) { overflow: hidden; scrollbar-gutter: aut .live-watching.has-who { cursor: pointer; text-decoration: underline dotted; text-underline-offset: 3px; } .live-who { display: flex; flex-wrap: wrap; gap: 6px; } .live-who-p { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px 3px 3px; border-radius: 999px; background: var(--bg2, rgba(127,127,127,.12)); border: 0; color: inherit; cursor: pointer; } + +/* a gift's note: the sender's words above the amount, and the field for them */ +.gift-note { + margin: 0; padding: 8px 12px; max-width: 100%; + border-left: 3px solid var(--accent); border-radius: 4px; + background: color-mix(in srgb, var(--accent) 8%, transparent); + font-style: italic; text-align: left; white-space: pre-wrap; overflow-wrap: anywhere; +} +.gift-note-input { width: 100%; min-height: 0; font-family: var(--sans); } diff --git a/tools/gift-profile-setup-test.js b/tools/gift-profile-setup-test.js index 67da261..9304d1a 100644 --- a/tools/gift-profile-setup-test.js +++ b/tools/gift-profile-setup-test.js @@ -33,11 +33,16 @@ const waitText = async (page, re, ms = 30000) => { for (let t = 0; t < ms; t += 250) { if (re.test(await text(page))) return true; await sleep(250); } return false; }; -async function toSeed(page) { +async function toSeed(page, note) { await page.goto(server.url.href); await page.evaluate(() => { localStorage.clear(); localStorage.setItem('btc-wallet-network', 'mutinynet'); }); - await page.goto(server.url.href + 'ag/' + giftCode()); + await page.goto(server.url.href + 'ag/' + giftCode() + (note ? '#m=' + encodeURIComponent(note) : '')); check('the gift opens: the server sees no coin behind it', await waitText(page, /aren.t visible/i)); + if (note) { + const q = await page.evaluate(() => document.querySelector('.gift-note')?.textContent || null); + check('the sender\'s note shows on the gift', q === note, String(q)); + check('...and leaves the address bar with the code', await page.evaluate(() => location.pathname === '/' && !location.hash)); + } await click(page, 'claim'); check('...so the claim finds it taken', await waitText(page, /create a wallet anyway/i)); check('...create a wallet anyway shows the seed', await click(page, 'create a wallet anyway') && await waitText(page, /recovery phrase/i, 10000)); @@ -47,7 +52,7 @@ try { const errors = []; page.on('pageerror', (e) => errors.push(String(e))); await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 2 }); - await toSeed(page); + await toSeed(page, 'Happy birthday! 🎂 not bold'); await click(page, 'skip verification'); check('the seed done: the wizard asks for a username', await waitText(page, /now pick a username/i, 10000), (await text(page)).slice(0, 200)); check('...with the claim form (or its wait) and Not now', await page.evaluate(() => !!document.querySelector('.onb')) && /not now/i.test(await text(page))); -- cgit v1.2.3