aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-28 09:31:51 -0700
committerAdam Soltys <[email protected]>2026-06-28 09:31:51 -0700
commit0fbeb3c55413fb221d890b3bf3464ebd2aed9fa1 (patch)
treef8f034c2ba00d0f78a34e1df1972a46712487151 /src/app.js
parent96d9b7ee6914b157a17e078cfe597cb40f35c9e2 (diff)
downloadcoinosv3-0fbeb3c55413fb221d890b3bf3464ebd2aed9fa1.tar.gz
coinosv3-0fbeb3c55413fb221d890b3bf3464ebd2aed9fa1.zip
Locked gifts: let NIP-07 extension users decrypt in-place, skip the code
The gift payload is now encrypted two ways in the link: under the one-time code (the DM / manual path) AND to the recipient's nostr pubkey via an ephemeral key. When a NIP-07 browser extension (window.nostr) is present, the claim view offers 'Claim with nostr extension' — it checks the extension's pubkey matches the gift, decrypts ctKey via window.nostr.nip44.decrypt, and claims into a fresh wallet with no code. The pasted-code path remains as the fallback for everyone else. Verified: both decrypt paths recover the payload from the blob; a different extension account is rejected. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/app.js')
-rw-r--r--src/app.js26
1 files changed, 23 insertions, 3 deletions
diff --git a/src/app.js b/src/app.js
index 84cec51..803a82b 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1848,6 +1848,9 @@ function readLockedGiftHash() {
// then it's claimed into a fresh wallet exactly like a normal gift.
function lockedGiftClaimView() {
const lk = ui.claimLocked;
+ // NIP-07: if a nostr browser extension is present, the recipient can decrypt
+ // in-place — no code needed.
+ const hasExt = !!(typeof globalThis !== 'undefined' && globalThis.nostr && lk.eph && lk.ctKey);
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:14px;align-items:center' },
h('h3', { style: 'margin:0' }, t('giftForYou')),
@@ -1855,16 +1858,33 @@ function lockedGiftClaimView() {
h('div', { class: 'row gap6', style: 'align-items:center' },
h('span', { class: 'small muted' }, t('giftLockedTo')),
profileChip(lk.to, { size: 36 })),
- h('p', { class: 'small muted', style: 'text-align:center;margin:0' }, t('giftCodeHint')),
+ ui.claimError && h('div', { class: 'notice err' }, ui.claimError),
+ hasExt ? h('button', { class: 'btn-primary btn-block', disabled: ui.busy, onClick: claimViaExtension }, ui.busy ? h('span', { class: 'spinner' }) : t('claimWithExtension')) : null,
+ hasExt ? h('div', { class: 'small faint', style: 'text-align:center' }, t('orEnterCode')) : h('p', { class: 'small muted', style: 'text-align:center;margin:0' }, t('giftCodeHint')),
h('input', { type: 'text', class: 'mono-input', style: 'width:100%', placeholder: t('claimCodePlaceholder'),
autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: ui.claimCodeInput,
onInput: (e) => { ui.claimCodeInput = e.target.value; } }),
- ui.claimError && h('div', { class: 'notice err' }, ui.claimError),
- h('button', { class: 'btn-primary btn-block', onClick: submitLockedCode }, t('claimGift')),
+ h('button', { class: (hasExt ? '' : 'btn-primary ') + 'btn-block', onClick: submitLockedCode }, t('claimGift')),
h('button', { class: 'btn-ghost btn-block', onClick: () => { ui.claimLocked = null; ui.claimCodeInput = ''; ui.claimError = ''; render(); } }, t('dismiss'))
)
);
}
+async function claimViaExtension() {
+ const ext = typeof globalThis !== 'undefined' && globalThis.nostr;
+ if (!ext) return;
+ ui.busy = true; ui.claimError = ''; render();
+ try {
+ const pk = await ext.getPublicKey();
+ if (pk !== ui.claimLocked.to) { ui.claimError = t('extWrongAccount'); ui.busy = false; render(); return; }
+ if (!ext.nip44 || !ext.nip44.decrypt) { ui.claimError = t('extNoNip44'); ui.busy = false; render(); return; }
+ const payload = await ext.nip44.decrypt(ui.claimLocked.eph, ui.claimLocked.ctKey);
+ if (!payload || !previewGift(payload)) { ui.claimError = t('extDecryptFailed'); ui.busy = false; render(); return; }
+ ui.busy = false; ui.claimLocked = null; ui.claimCodeInput = ''; ui.claimError = '';
+ enterWallet(newMnemonic(), '', { gift: payload }); // fresh-wallet claim
+ } catch {
+ ui.busy = false; ui.claimError = t('extDecryptFailed'); render();
+ }
+}
function submitLockedCode() {
const code = (ui.claimCodeInput || '').trim();
if (!code) return;