// Generated, do not edit. sha256:5dea810da6d84ed187a1154955f4194498f812771e3ddaa6b6dc7c547a305b8d import { t, localeTag } from '../i18n/index.js'; import { CONFIG } from '../../shared/config.js'; import { esc } from '../ui/escape.js'; import { licenseSnapshot, loadLicense, markLicenseSeen } from '../license.js'; import { installLicense } from '../data/license-install.js'; import { refreshAttention } from '../ui/attention.js'; import { npubFromHex, shortKey, secretFromText, publicKeyOf, nsecFromSecret, newSecret } from '../nostr/keys.js'; import { keySigner, extensionSigner, nip98Header } from '../nostr/nip98.js'; import { qrSvg, invoiceQrText } from '../ui/qr.js'; import { CHECK, CHEVRON } from '../ui/icons.js'; import { splashMarkHtml } from '../ui/splash.js'; import { expanders } from '../ui/expand.js'; import { supportInfo, supportMessages, sendSupport, supportAttachment } from '../data/support.js'; import { licenseShop, buyerLicenses, saleInfo, placeOrder, orderStatus } from '../data/license-buyer.js'; const BROKEN = new Set(['invalid', 'unreadable']); function buyerOf(s) { const npub = npubFromHex(s.buyer); return { text: shortKey(npub ?? s.buyer, 10), title: npub ?? s.buyer ?? '' }; } function lines(s) { const out = []; const line = (text, title = '') => out.push({ text, title }); if (s.why === 'develop' || s.why === 'forced-develop') line(t('license.cardDevelop')); line(s.level === 'full' ? t('license.cardFull') : t('license.cardFree')); if (s.installed) { const buyer = buyerOf(s); line(t('license.cardFacts', { buyer: buyer.text, issued: s.issued ?? '', from: s.installed.majorFrom, to: s.installed.majorTo === 'unlimited' ? t('license.unlimited') : s.installed.majorTo, }), buyer.title); if (!s.installedCovers) line(t('license.cardOutOfRange', { version: s.version })); } if (BROKEN.has(s.why)) line(`${t('license.cardProblem')} ${t(`license.why_${s.why}`)}.`); if (s.level !== 'full') line(t('license.cardBuy')); return out; } async function installAndReload(text, message) { message.className = 't-body-sm'; message.textContent = ''; try { const result = await installLicense(text); message.classList.add('t-pos'); message.textContent = result.covers ? t('license.installOk') : t('license.installOkFuture'); setTimeout(() => location.reload(), 1200); } catch (err) { message.classList.add('t-neg'); message.textContent = err.message; } } const INFO = ['Nostr', 'Why', 'SignIn', 'Zap', 'Privacy']; const link = (url, html) => `${html}`; const payHtml = (expand) => `
${say('license.payTitle')}

${say('license.payIntro')}

` + ['easy', 'other'].map(group => `
${say(`license.payGroup_${group}`)}
` + CONFIG.LICENSE.PAY_APPS.filter(a => a.group === group) .map(a => `

${link(a.url, `${esc(a.name)}`)} ${say(`license.payApp_${a.id}`)}

`).join('') + '
').join('') + `

${say('license.payNote')}

${say('license.nostrMore')}` + INFO.map(k => `

${say(`license.info${k}Head`)} ${say(`license.info${k}`)}

`).join('') + CONFIG.LICENSE.INFO_LINKS.map(l => `

${link(l.url, say(`license.link_${l.id}`))}

`).join('') + '
'; let signer = null; let newKey = null; const npubText = (hex) => { const npub = npubFromHex(hex) ?? hex; return `${esc(shortKey(npub, 10))}`; }; const say = (key, params) => esc(t(key, params)); async function drawSignIn(box, s) { let shop = { serviceUrl: null, buyer: null }; try { shop = await licenseShop(); } catch { } serviceUrl = shop.serviceUrl; drawBuy(); drawSupport(); const head = `
${say('license.signInTitle')}
`; if (signer) { const who = t(signer.kind === 'extension' ? 'license.signedInExtension' : 'license.signedIn', { npub: '§' }); const [before, after] = who.split('§'); const other = s.installed && s.buyer && s.buyer !== signer.pubkey; box.innerHTML = `${head}

${esc(before)}${npubText(signer.pubkey)}${esc(after)}

${other ? `

${say('license.otherBuyer')}

` : ''}

`; box.querySelector('[data-signout]').onclick = () => { signer = null; drawSignIn(box, s); }; await drawLicenses(box.querySelector('[data-licenses]'), box.querySelector('[data-signin-msg]'), shop); return; } if (newKey) { const nsec = nsecFromSecret(newKey.secret); box.innerHTML = `${head}

${say('license.newKeyIntro')}

${say('license.newKeyPublic', { npub: npubFromHex(publicKeyOf(newKey.secret)) })}

`; const go = box.querySelector('[data-go]'); const saved = box.querySelector('[data-saved]'); const allow = () => { go.disabled = !(saved.checked && newKey.copied); }; saved.onchange = allow; box.querySelector('[data-key-copy]').onclick = (e) => { newKey.copied = true; allow(); copyText(nsec, box.querySelector('[data-nsec]')).then((ok) => { if (ok) e.target.textContent = t('license.newKeyCopied'); }); }; box.querySelector('[data-download]').onclick = () => { download(t('license.newKeyFileName'), `${nsec}\n`); newKey.copied = true; allow(); }; go.onclick = () => { signer = keySigner(newKey.secret); newKey = null; drawSignIn(box, s); }; box.querySelector('[data-cancel]').onclick = () => { newKey = null; drawSignIn(box, s); }; return; } const hasExtension = !!globalThis.nostr; const known = s.installed && s.buyer ? { key: 'license.signInKnown', hex: s.buyer } : shop.buyer ? { key: 'license.signInLast', hex: shop.buyer } : null; box.innerHTML = `${head}

${say('license.signInIntro')}

${known ? `

${esc(t(known.key, { npub: '§' })).replace('§', npubText(known.hex))}

` : ''}
${hasExtension ? '' : `

${say('license.signInExtensionNone')}

`}

`; const msg = box.querySelector('[data-signin-msg]'); const fault = (text) => { msg.className = 't-body-sm mt-sm t-neg'; msg.textContent = text; }; box.querySelector('[data-ext]').onclick = async () => { try { signer = await extensionSigner(); drawSignIn(box, s); } catch (err) { fault(err.message); } }; const form = box.querySelector('[data-key-form]'); box.querySelector('[data-key]').onclick = () => { form.hidden = false; form.querySelector('input').focus(); }; form.onsubmit = (e) => { e.preventDefault(); const input = form.querySelector('input'); const { secret, error } = secretFromText(input.value); input.value = ''; if (error) { fault(t(error === 'npub' ? 'license.signInNpub' : 'license.signInBadKey')); return; } signer = keySigner(secret); drawSignIn(box, s); }; box.querySelector('[data-new]').onclick = () => { newKey = { secret: newSecret(), copied: false }; drawSignIn(box, s); }; } const BUY_POLL_MS = 3000; let buyBox = null; let serviceUrl = null; let purchase = null; let pollTimer = null; const sats = (n) => Number(n).toLocaleString(localeTag()); const clock = (ms) => new Date(ms).toLocaleTimeString(localeTag(), { hour: '2-digit', minute: '2-digit' }); const FINAL = { expired: 'license.buyExpired', canceled: 'license.buyCanceled', underpaid: 'license.buyUnderpaid' }; const AWAY = new Set(['marketService.unreachable', 'marketService.timeout', 'marketService.unavailable']); async function drawBuy() { const box = buyBox; if (!box) return; const head = `
${say('license.buyTitle')}
`; if (purchase) { const o = purchase.order; const final = FINAL[o.status]; const state = final ? say(final) : o.status === 'paid' ? say(o.waived ? 'license.buyWaived' : 'license.buyPaid') : say('license.buyWaiting'); box.innerHTML = `${head} ${o.status === 'open' && o.paymentRequest ? `

${say('license.buyInvoice', { time: clock(o.expiresAt) })}

${qrSvg(invoiceQrText(o.paymentRequest), { label: t('license.buyQr') })}
${say('license.buyOpenWallet')}
` : ''}

${state}

${purchase.note ? esc(purchase.note) : ''}

${final ? `
` : ''}`; const copy = box.querySelector('[data-invoice-copy]'); if (copy) copy.onclick = (e) => copyText(o.paymentRequest, box.querySelector('[data-invoice]')).then((ok) => { if (ok) e.target.textContent = t('license.buyCopied'); }); const again = box.querySelector('[data-buy-again]'); if (again) again.onclick = () => { purchase = null; drawBuy(); }; return; } let sale = null; let problem = null; try { sale = await saleInfo(); } catch (err) { problem = AWAY.has(err.key) ? t('license.buyAway') : err.message; } const price = sale?.open ? say('license.buyPrice', { major: sale.major, sats: sats(sale.priceSat) }) : sale ? say('license.buyClosed') : esc(problem ?? ''); box.innerHTML = `${head}

${price}

${sale?.open && !signer ? `

${say('license.buySignInFirst')}

` : ''}

`; box.querySelector('[data-buy-go]').onclick = async (e) => { const msg = box.querySelector('[data-buy-msg]'); e.target.disabled = true; try { const body = '{}'; const header = await nip98Header(signer, { url: `${serviceUrl}/v1/orders`, method: 'POST', body }); const { order, readKey } = await placeOrder(header, body); purchase = { order, readKey, note: '' }; drawBuy(); schedulePoll(); } catch (err) { e.target.disabled = false; msg.className = 't-body-sm mt-sm t-neg'; msg.textContent = err.message; } }; } function schedulePoll() { clearTimeout(pollTimer); pollTimer = setTimeout(pollOnce, BUY_POLL_MS); } async function pollOnce() { if (!purchase?.readKey) return; try { const { order, license } = await orderStatus(purchase.order.id, purchase.readKey); purchase.order = order; purchase.note = ''; if (license) { drawBuy(); const msg = buyBox?.querySelector('[data-buy-msg]'); if (!license.valid) { purchase.note = t('license.buyNotValid'); drawBuy(); return; } if (msg && buyBox.isConnected) await installAndReload(license.file, msg); else await installLicense(license.file).then(() => location.reload()); return; } drawBuy(); if (FINAL[order.status]) return; } catch (err) { purchase.note = err.message; drawBuy(); } schedulePoll(); } const SUPPORT_POLL_MS = 30_000; let supportBox = null; let supportDraft = ''; let supportFile = null; let supportTimer = null; const shots = new Map(); const supportUrl = (info, path) => `${info.serviceUrl}/v1/support/${path}`; async function drawSupport() { const box = supportBox; if (!box) return; clearInterval(supportTimer); box.hidden = !signer; if (!signer) return; let info = null; try { info = await supportInfo(); } catch { info = null; } const head = `
${say('license.supportTitle')}
${info ? `` : ''}
`; const nostr = info?.nostr?.pubkey ? `

${esc(t('license.supportNostr', { name: '§' })).replace('§', `${esc(info.nostr.name)} (${npubText(info.nostr.pubkey)})`)}

` : ''; if (!info?.serviceUrl) { box.innerHTML = `${head}

${say('license.supportIntro')}

${nostr}`; return; } const mb = info.maxAttachmentBytes / 1024 / 1024; box.innerHTML = `${head}

${say('license.supportIntro')}

${say('license.supportAttachHint', { mb })}

${nostr}`; const field = box.querySelector('#lic-support'); const msg = box.querySelector('[data-support-msg]'); const thread = box.querySelector('[data-support-thread]'); const picker = box.querySelector('[data-support-input]'); const show = (cls, line) => { msg.className = `t-body-sm mt-sm ${cls}`; msg.textContent = line; }; field.oninput = () => { supportDraft = field.value; }; const showFile = () => { const area = box.querySelector('[data-support-file]'); area.innerHTML = supportFile ? `
${esc(t('license.supportScreenshot'))}
` : ''; area.querySelector('[data-support-unpick]')?.addEventListener('click', () => { supportFile = null; showFile(); }); }; const take = (file) => { if (!file) return; if (!info.attachmentTypes.includes(file.type)) { show('t-neg', t('license.supportAttachType')); return; } if (file.size > info.maxAttachmentBytes) { show('t-neg', t('license.supportAttachTooLarge', { mb })); return; } const reader = new FileReader(); reader.onload = () => { const url = String(reader.result); supportFile = { type: file.type, data: url.slice(url.indexOf(',') + 1), url }; show('', ''); showFile(); }; reader.readAsDataURL(file); }; showFile(); box.querySelector('[data-support-pick]').onclick = () => picker.click(); picker.onchange = () => { take(picker.files?.[0]); picker.value = ''; }; field.addEventListener('paste', (e) => { const item = [...(e.clipboardData?.items ?? [])].find(i => i.kind === 'file' && i.type.startsWith('image/')); if (item) { e.preventDefault(); take(item.getAsFile()); } }); box.querySelector('[data-support-refresh]').onclick = () => drawThread(thread, info); drawThread(thread, info); if (signer.kind === 'key') { supportTimer = setInterval(() => { if (!thread.isConnected) { clearInterval(supportTimer); return; } drawThread(thread, info, { quiet: true }); }, SUPPORT_POLL_MS); } box.querySelector('[data-support-send]').onclick = async (e) => { const text = field.value.trim(); if (!text && !supportFile) { show('t-neg', t('license.supportEmpty')); return; } if (text.length > info.maxText) { show('t-neg', t('license.supportTooLong', { max: info.maxText })); return; } e.target.disabled = true; e.target.textContent = t('license.supportSending'); try { const body = JSON.stringify({ text, attachment: supportFile ? { type: supportFile.type, data: supportFile.data } : null }); const header = await nip98Header(signer, { url: supportUrl(info, 'messages'), method: 'POST', body }); const sent = await sendSupport(header, body); if (supportFile && sent.attachments?.[0]) shots.set(sent.attachments[0].id, supportFile.url); supportDraft = ''; supportFile = null; field.value = ''; showFile(); show('', ''); await drawThread(thread, info); } catch (err) { show('t-neg', AWAY.has(err.key) ? t('license.supportAway') : err.message); } finally { e.target.disabled = false; e.target.textContent = t('license.supportSend'); } }; } async function drawThread(area, info, { quiet = false } = {}) { if (!quiet) area.innerHTML = `

${say('license.supportThreadLoading')}

`; let list; try { const header = await nip98Header(signer, { url: supportUrl(info, 'messages'), method: 'GET' }); list = await supportMessages(header); } catch (err) { if (quiet) return; area.innerHTML = `

${esc(AWAY.has(err.key) ? t('license.supportAway') : err.message)}

`; return; } if (!list.length) { area.innerHTML = `

${say('license.supportThreadEmpty')}

`; return; } const when = (ms) => new Date(ms).toLocaleString(localeTag(), { dateStyle: 'short', timeStyle: 'short' }); const who = (m) => t(m.from === 'buyer' ? 'license.supportYou' : m.ai ? 'license.supportAi' : 'license.supportSeller'); const shot = (a) => (shots.has(a.id) ? `${esc(t('license.supportScreenshot'))}` : ``); area.innerHTML = list.map(m => `
${m.text ? `

${esc(m.text)}

` : ''}${m.attachments.map(shot).join('')}
${esc(who(m))} · ${esc(when(m.at))}
`).join(''); const toEnd = () => { area.scrollTop = area.scrollHeight; }; toEnd(); area.querySelectorAll('img').forEach(img => img.addEventListener('load', toEnd, { once: true })); const load = async (button) => { const id = button.dataset.shot; try { const header = await nip98Header(signer, { url: supportUrl(info, `attachments/${id}`), method: 'GET' }); const { mime, data } = await supportAttachment(header, id); if (!info.attachmentTypes.includes(mime) || !/^[A-Za-z0-9+/=]+$/.test(data)) return; shots.set(id, `data:${mime};base64,${data}`); const img = document.createElement('img'); img.className = 'lic-shot'; img.alt = t('license.supportScreenshot'); img.addEventListener('load', toEnd, { once: true }); img.src = shots.get(id); button.replaceWith(img); } catch (err) { button.textContent = err.message; } }; area.querySelectorAll('[data-shot]').forEach(button => { if (signer.kind === 'key') load(button); else button.onclick = () => load(button); }); } async function drawLicenses(area, msg, shop) { area.innerHTML = `

${say('license.licensesLoading')}

`; let list; try { const header = await nip98Header(signer, { url: `${shop.serviceUrl}/v1/license`, method: 'GET' }); list = await buyerLicenses(header); } catch (err) { area.innerHTML = ''; msg.className = 't-body-sm t-neg'; msg.textContent = err.message; return; } if (!list.length) { area.innerHTML = `

${say('license.licensesNone')}

`; return; } area.innerHTML = list.map((l, i) => { if (l.unreadable) return `

${say('license.licenseNotValid')}

`; const text = say('license.licenseLine', { level: t(`license.level_${l.level}`), from: l.majorFrom, to: l.majorTo === 'unlimited' ? t('license.unlimited') : l.majorTo, issued: l.issued, }); const state = !l.valid ? say('license.licenseNotValid') : l.installed ? `${say('license.licenseInstalled')}` : `` + (l.covers ? '' : ` ${say('license.licenseNotCovered')}`); return `
${text}${state}
`; }).join(''); area.querySelectorAll('[data-install]').forEach(b => { b.onclick = () => installAndReload(list[Number(b.dataset.install)].file, msg); }); } async function copyText(text, field) { try { field.select(); if (document.execCommand('copy')) return true; } catch { } try { await navigator.clipboard.writeText(text); return true; } catch { return false; } } function download(name, text) { const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([text], { type: 'text/plain' })); a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); } const mark = (v) => { if (v === true) return `${CHECK}`; if (v === false) return `-`; if (v === 'partial') return `${esc(t('license.featPartial'))}`; return `?`; }; const inFree = f => (f.has ? CONFIG.LICENSE.FREE.HAS[f.has] : f.free); function groupFree(items) { const v = items.map(inFree); if (v.every(x => x === true)) return true; if (v.every(x => x === false)) return false; return v.some(x => x === true) ? 'partial' : null; } const groupKey = g => `feat_${g}`; function featuresHtml(expand) { const groups = CONFIG.LICENSE.FEATURE_GROUPS.map((g) => { const items = CONFIG.LICENSE.FEATURES.filter(f => f.group === g); const sub = items.map(f => `
  • ${esc(t(`license.feat_${f.id}`))}${mark(true)}${mark(inFree(f))}
  • `).join(''); return `
    ${CHEVRON}${esc(t(`license.featGroup_${g}`))}${mark(true)}${mark(groupFree(items))}
    `; }).join(''); const open = CONFIG.LICENSE.FEATURES.some(f => inFree(f) === null); return `
    ${splashMarkHtml()}
    ${esc(t('license.featTitle'))}
    ${esc(t('license.featFull'))}${esc(t('license.featFree'))}
    ${groups}
    ${open ? `

    ${esc(t('license.featFreeOpen'))}

    ` : ''}`; } function placeShot(shot, row) { const gap = 8, pad = 8; const r = row.getBoundingClientRect(); const w = shot.offsetWidth, h = shot.offsetHeight; let top = r.top - gap - h; if (top < pad) top = r.bottom + gap; const left = Math.min(Math.max(pad, r.left + 24), window.innerWidth - pad - w); shot.style.left = `${left}px`; shot.style.top = `${Math.min(Math.max(pad, top), window.innerHeight - pad - h)}px`; } function bindFeatures(root) { document.querySelector('body > .lic-feat-shot')?.remove(); const shot = document.createElement('div'); shot.className = 'lic-feat-shot'; shot.hidden = true; document.body.appendChild(shot); let current = null; let on = null; const show = (row) => { if (row === on && !shot.hidden) return; on = row; root.querySelectorAll('[data-feature]').forEach(r => r.classList.toggle('is-on', r === row)); shot.hidden = false; placeShot(shot, row); const id = row.dataset.feature; if (id === current) return; current = id; shot.innerHTML = ''; const img = new Image(); img.className = 'lic-feat-img'; img.alt = t(`license.feat_${id}`); img.onload = () => { if (current === id) shot.replaceChildren(img); }; img.onerror = () => { if (current === id) shot.innerHTML = `

    ${esc(t('license.featNoShot'))}

    `; }; img.src = `${CONFIG.LICENSE.FEATURE_IMAGES}/${id}.webp`; }; const hide = () => { on = null; shot.hidden = true; root.querySelectorAll('.is-on').forEach(r => r.classList.remove('is-on')); }; const over = (e) => { if (!root.isConnected) { stop(); return; } const row = e.target.closest?.('[data-feature]'); if (row && root.contains(row)) show(row); else hide(); }; const stop = () => { shot.remove(); document.removeEventListener('pointerover', over); window.removeEventListener('scroll', hide, true); window.removeEventListener('hashchange', stop); }; document.addEventListener('pointerover', over); window.addEventListener('scroll', hide, { capture: true, passive: true }); window.addEventListener('hashchange', stop); root.addEventListener('focusin', (e) => { const row = e.target.closest('[data-feature]'); if (row) show(row); }); root.addEventListener('focusout', hide); root.querySelectorAll('.lic-group').forEach(d => d.addEventListener('toggle', hide)); } export async function render(el) { const s = licenseSnapshot() ?? await loadLicense(); const expand = await expanders('license', { nostr: false, ...Object.fromEntries(CONFIG.LICENSE.FEATURE_GROUPS.map(g => [groupKey(g), false])), }); el.innerHTML = `
    ${esc(t('license.cardTitle'))}
    ${esc(t('license.cardVersion', { version: s.version }))}
    ${lines(s).map(r => `

    ${esc(r.text)}

    `).join('')}
    ${esc(t('license.dropHere'))} ${esc(t('license.dropOr'))}
    ${payHtml(expand)}
    ${featuresHtml(expand)}
    `; expand.bind(el); bindFeatures(el.querySelector('[data-features]')); buyBox = el.querySelector('[data-buy]'); supportBox = el.querySelector('[data-support]'); drawSignIn(el.querySelector('[data-signin]'), s); markLicenseSeen().then(() => refreshAttention()).catch(() => { }); const message = el.querySelector('#lic-msg'); const install = (text) => installAndReload(text, message); const MAX = 64 * 1024; const zone = el.querySelector('#lic-drop'); const picker = el.querySelector('#lic-file'); async function fromFile(file) { if (!file) return; if (file.size > MAX) { message.className = 't-body-sm t-neg'; message.textContent = t('license.dropTooBig'); return; } await install(await file.text()); } zone.onclick = () => picker.click(); zone.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); picker.click(); } }; picker.onchange = () => fromFile(picker.files?.[0]); zone.ondragover = (e) => { e.preventDefault(); zone.classList.add('is-over'); }; zone.ondragleave = () => zone.classList.remove('is-over'); zone.ondrop = (e) => { e.preventDefault(); zone.classList.remove('is-over'); fromFile(e.dataTransfer?.files?.[0]); }; }