Add wallet URI copy and shortlink trust toggle

This commit is contained in:
Alexander Schmidt
2026-03-26 15:11:11 +01:00
parent 6f43f34d68
commit 3aa8277530
6 changed files with 91 additions and 11 deletions

42
app.js
View File

@@ -35,14 +35,17 @@
const qrContainer = $('#qr'); const qrContainer = $('#qr');
const uriBox = $('#uri'); const uriBox = $('#uri');
const openWalletBtn = $('#openWallet'); const openWalletBtn = $('#openWallet');
const copyUriBtn = $('#copyUri');
const copyAddrBtn = $('#copyAddr'); const copyAddrBtn = $('#copyAddr');
const countdownEl = $('#countdown'); const countdownEl = $('#countdown');
const fiatHint = $('#fiatHint'); const fiatHint = $('#fiatHint');
const toast = $('#toast'); const toast = $('#toast');
const shareLinkInput = $('#shareLink'); const shareLinkInput = $('#shareLink');
const copyShareLinkBtn = $('#copyShareLink'); const copyShareLinkBtn = $('#copyShareLink');
const useShortLinkCheckbox = $('#useShortLink');
const newRequestBtn = $('#newRequest'); const newRequestBtn = $('#newRequest');
const homeLink = $('#homeLink'); const homeLink = $('#homeLink');
let currentInvoiceHash = null;
// TX Proof DOM // TX Proof DOM
const proofToggle = $('#proofToggle'); const proofToggle = $('#proofToggle');
@@ -119,8 +122,12 @@
amountInput.addEventListener('input', updateFiatHint); amountInput.addEventListener('input', updateFiatHint);
currencySelect.addEventListener('change', updateFiatHint); currencySelect.addEventListener('change', updateFiatHint);
generateBtn.addEventListener('click', generate); generateBtn.addEventListener('click', generate);
copyUriBtn.addEventListener('click', () => copyToClipboard(uriBox.textContent));
copyAddrBtn.addEventListener('click', () => copyToClipboard(addrInput.value.trim())); copyAddrBtn.addEventListener('click', () => copyToClipboard(addrInput.value.trim()));
copyShareLinkBtn.addEventListener('click', () => copyToClipboard(shareLinkInput.value)); copyShareLinkBtn.addEventListener('click', () => copyToClipboard(shareLinkInput.value));
useShortLinkCheckbox.addEventListener('change', function () {
if (currentInvoiceHash) updateShareLink(currentInvoiceHash);
});
qrContainer.addEventListener('click', downloadQR); qrContainer.addEventListener('click', downloadQR);
newRequestBtn.addEventListener('click', resetForm); newRequestBtn.addEventListener('click', resetForm);
homeLink.addEventListener('click', function (e) { e.preventDefault(); resetForm(); }); homeLink.addEventListener('click', function (e) { e.preventDefault(); resetForm(); });
@@ -179,6 +186,8 @@
qrContainer.classList.remove('paid', 'confirming'); qrContainer.classList.remove('paid', 'confirming');
uriBox.textContent = ''; uriBox.textContent = '';
shareLinkInput.value = ''; shareLinkInput.value = '';
useShortLinkCheckbox.checked = false;
currentInvoiceHash = null;
// Reset proof // Reset proof
invoiceCode = null; invoiceCode = null;
stopConfirmationPolling(); stopConfirmationPolling();
@@ -326,7 +335,7 @@
buildSummary(xmrAmount, desc, timer); buildSummary(xmrAmount, desc, timer);
updatePageTitle(xmrAmount, desc); updatePageTitle(xmrAmount, desc);
// Share link — keep existing short URL if present; otherwise shorten new hash // Share link
var deadlineTs = null; var deadlineTs = null;
if (timer && timer > 0) { if (timer && timer > 0) {
if (!deadlineEndMs) { if (!deadlineEndMs) {
@@ -335,14 +344,8 @@
deadlineTs = Math.floor(deadlineEndMs / 1000); deadlineTs = Math.floor(deadlineEndMs / 1000);
} }
const hash = buildHash(addr, xmrAmount, desc, timer, deadlineTs); const hash = buildHash(addr, xmrAmount, desc, timer, deadlineTs);
if (invoiceCode) { currentInvoiceHash = hash;
shareLinkInput.value = location.origin + '/s/' + invoiceCode; updateShareLink(hash);
} else {
shareLinkInput.value = location.origin + '/#' + hash;
shortenUrl(hash).then(function (shortUrl) {
if (shortUrl) shareLinkInput.value = shortUrl;
});
}
// QR // QR
qrContainer.innerHTML = ''; qrContainer.innerHTML = '';
@@ -410,6 +413,7 @@
const code = params.get('c'); const code = params.get('c');
if (code) { if (code) {
invoiceCode = code; invoiceCode = code;
useShortLinkCheckbox.checked = true;
// Verify short URL integrity (detect tampering) // Verify short URL integrity (detect tampering)
setTimeout(function () { setTimeout(function () {
verifyShortUrlIntegrity(code, hash); verifyShortUrlIntegrity(code, hash);
@@ -422,6 +426,26 @@
return true; return true;
} }
function updateShareLink(hash) {
var longUrl = location.origin + '/#' + hash;
shareLinkInput.value = longUrl;
if (!useShortLinkCheckbox.checked) {
return;
}
if (invoiceCode) {
shareLinkInput.value = location.origin + '/s/' + invoiceCode;
return;
}
shortenUrl(hash).then(function (shortUrl) {
if (shortUrl && useShortLinkCheckbox.checked && currentInvoiceHash === hash) {
shareLinkInput.value = shortUrl;
}
});
}
// Verify that the redirected hash still matches the stored short URL mapping. // Verify that the redirected hash still matches the stored short URL mapping.
function verifyShortUrlIntegrity(code, currentHash) { function verifyShortUrlIntegrity(code, currentHash) {
fetch('/api/check-short.php?code=' + encodeURIComponent(code)) fetch('/api/check-short.php?code=' + encodeURIComponent(code))

2
app.min.js vendored

File diff suppressed because one or more lines are too long

21
i18n.js
View File

@@ -26,6 +26,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Days', placeholder_timer_custom: 'Days',
btn_generate: 'Create payment request', btn_generate: 'Create payment request',
btn_open_wallet: 'Open in wallet', btn_open_wallet: 'Open in wallet',
btn_copy_uri: 'Copy payment URI',
btn_copy_addr: 'Copy address', btn_copy_addr: 'Copy address',
btn_download_pdf: 'PDF Invoice', btn_download_pdf: 'PDF Invoice',
pdf_title: 'Payment Request', pdf_title: 'Payment Request',
@@ -41,6 +42,8 @@ var I18n = (function () {
footer: footer, footer: footer,
aria_currency: 'Currency', aria_currency: 'Currency',
label_share_link: 'Shareable link', label_share_link: 'Shareable link',
shortlink_toggle_label: 'Use short link (requires server trust)',
shortlink_toggle_hint: 'Trade-off: short links are convenient, but a compromised server could swap invoice data on first access.',
btn_new_request: 'New payment request', btn_new_request: 'New payment request',
toast_copied: 'Copied!', toast_copied: 'Copied!',
countdown_expired: 'Payment deadline expired', countdown_expired: 'Payment deadline expired',
@@ -75,6 +78,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Tage', placeholder_timer_custom: 'Tage',
btn_generate: 'Zahlungsanforderung erstellen', btn_generate: 'Zahlungsanforderung erstellen',
btn_open_wallet: 'In Wallet öffnen', btn_open_wallet: 'In Wallet öffnen',
btn_copy_uri: 'Zahlungs-URI kopieren',
btn_copy_addr: 'Adresse kopieren', btn_copy_addr: 'Adresse kopieren',
btn_download_pdf: 'PDF Rechnung', btn_download_pdf: 'PDF Rechnung',
pdf_title: 'Zahlungsanforderung', pdf_title: 'Zahlungsanforderung',
@@ -90,6 +94,8 @@ var I18n = (function () {
footer: footer, footer: footer,
aria_currency: 'Währung', aria_currency: 'Währung',
label_share_link: 'Teilbarer Link', label_share_link: 'Teilbarer Link',
shortlink_toggle_label: 'Kurzlink verwenden (Server-Vertrauen erforderlich)',
shortlink_toggle_hint: 'Trade-off: Kurzlinks sind bequem, aber ein kompromittierter Server könnte Rechnungsdaten beim ersten Aufruf austauschen.',
btn_new_request: 'Neue Zahlungsanforderung', btn_new_request: 'Neue Zahlungsanforderung',
toast_copied: 'Kopiert!', toast_copied: 'Kopiert!',
countdown_expired: 'Zahlungsfrist abgelaufen', countdown_expired: 'Zahlungsfrist abgelaufen',
@@ -124,6 +130,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Jours', placeholder_timer_custom: 'Jours',
btn_generate: 'Créer une demande de paiement', btn_generate: 'Créer une demande de paiement',
btn_open_wallet: 'Ouvrir dans le wallet', btn_open_wallet: 'Ouvrir dans le wallet',
btn_copy_uri: 'Copier l\'URI de paiement',
btn_copy_addr: 'Copier l\'adresse', btn_copy_addr: 'Copier l\'adresse',
btn_download_pdf: 'Facture PDF', btn_download_pdf: 'Facture PDF',
pdf_title: 'Demande de paiement', pdf_title: 'Demande de paiement',
@@ -139,6 +146,8 @@ var I18n = (function () {
footer: footer, footer: footer,
aria_currency: 'Devise', aria_currency: 'Devise',
label_share_link: 'Lien partageable', label_share_link: 'Lien partageable',
shortlink_toggle_label: 'Utiliser un lien court (confiance serveur requise)',
shortlink_toggle_hint: 'Compromis: les liens courts sont pratiques, mais un serveur compromis pourrait remplacer les donnees de facture au premier acces.',
btn_new_request: 'Nouvelle demande de paiement', btn_new_request: 'Nouvelle demande de paiement',
toast_copied: 'Copié !', toast_copied: 'Copié !',
countdown_expired: 'Délai de paiement expiré', countdown_expired: 'Délai de paiement expiré',
@@ -173,6 +182,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Giorni', placeholder_timer_custom: 'Giorni',
btn_generate: 'Crea richiesta di pagamento', btn_generate: 'Crea richiesta di pagamento',
btn_open_wallet: 'Apri nel wallet', btn_open_wallet: 'Apri nel wallet',
btn_copy_uri: 'Copia URI pagamento',
btn_copy_addr: 'Copia indirizzo', btn_copy_addr: 'Copia indirizzo',
btn_download_pdf: 'Fattura PDF', btn_download_pdf: 'Fattura PDF',
pdf_title: 'Richiesta di pagamento', pdf_title: 'Richiesta di pagamento',
@@ -188,6 +198,8 @@ var I18n = (function () {
footer: footer, footer: footer,
aria_currency: 'Valuta', aria_currency: 'Valuta',
label_share_link: 'Link condivisibile', label_share_link: 'Link condivisibile',
shortlink_toggle_label: 'Usa link breve (richiede fiducia nel server)',
shortlink_toggle_hint: 'Compromesso: i link brevi sono comodi, ma un server compromesso potrebbe sostituire i dati fattura al primo accesso.',
btn_new_request: 'Nuova richiesta di pagamento', btn_new_request: 'Nuova richiesta di pagamento',
toast_copied: 'Copiato!', toast_copied: 'Copiato!',
countdown_expired: 'Scadenza pagamento superata', countdown_expired: 'Scadenza pagamento superata',
@@ -222,6 +234,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Días', placeholder_timer_custom: 'Días',
btn_generate: 'Crear solicitud de pago', btn_generate: 'Crear solicitud de pago',
btn_open_wallet: 'Abrir en wallet', btn_open_wallet: 'Abrir en wallet',
btn_copy_uri: 'Copiar URI de pago',
btn_copy_addr: 'Copiar dirección', btn_copy_addr: 'Copiar dirección',
btn_download_pdf: 'Factura PDF', btn_download_pdf: 'Factura PDF',
pdf_title: 'Solicitud de pago', pdf_title: 'Solicitud de pago',
@@ -237,6 +250,8 @@ var I18n = (function () {
footer: footer, footer: footer,
aria_currency: 'Moneda', aria_currency: 'Moneda',
label_share_link: 'Enlace compartible', label_share_link: 'Enlace compartible',
shortlink_toggle_label: 'Usar enlace corto (requiere confiar en el servidor)',
shortlink_toggle_hint: 'Compromiso: los enlaces cortos son comodos, pero un servidor comprometido podria cambiar los datos de la factura en el primer acceso.',
btn_new_request: 'Nueva solicitud de pago', btn_new_request: 'Nueva solicitud de pago',
toast_copied: '¡Copiado!', toast_copied: '¡Copiado!',
countdown_expired: 'Plazo de pago vencido', countdown_expired: 'Plazo de pago vencido',
@@ -271,6 +286,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Dias', placeholder_timer_custom: 'Dias',
btn_generate: 'Criar pedido de pagamento', btn_generate: 'Criar pedido de pagamento',
btn_open_wallet: 'Abrir na wallet', btn_open_wallet: 'Abrir na wallet',
btn_copy_uri: 'Copiar URI de pagamento',
btn_copy_addr: 'Copiar endereço', btn_copy_addr: 'Copiar endereço',
btn_download_pdf: 'Fatura PDF', btn_download_pdf: 'Fatura PDF',
pdf_title: 'Pedido de pagamento', pdf_title: 'Pedido de pagamento',
@@ -286,6 +302,8 @@ var I18n = (function () {
footer: footer, footer: footer,
aria_currency: 'Moeda', aria_currency: 'Moeda',
label_share_link: 'Link partilhável', label_share_link: 'Link partilhável',
shortlink_toggle_label: 'Usar link curto (requer confianca no servidor)',
shortlink_toggle_hint: 'Compromisso: links curtos sao praticos, mas um servidor comprometido pode trocar os dados da fatura no primeiro acesso.',
btn_new_request: 'Novo pedido de pagamento', btn_new_request: 'Novo pedido de pagamento',
toast_copied: 'Copiado!', toast_copied: 'Copiado!',
countdown_expired: 'Prazo de pagamento expirado', countdown_expired: 'Prazo de pagamento expirado',
@@ -320,6 +338,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Дней', placeholder_timer_custom: 'Дней',
btn_generate: 'Создать запрос на оплату', btn_generate: 'Создать запрос на оплату',
btn_open_wallet: 'Открыть в кошельке', btn_open_wallet: 'Открыть в кошельке',
btn_copy_uri: 'Копировать платежный URI',
btn_copy_addr: 'Копировать адрес', btn_copy_addr: 'Копировать адрес',
btn_download_pdf: 'PDF счёт', btn_download_pdf: 'PDF счёт',
pdf_title: 'Запрос на оплату', pdf_title: 'Запрос на оплату',
@@ -335,6 +354,8 @@ var I18n = (function () {
footer: footer, footer: footer,
aria_currency: 'Валюта', aria_currency: 'Валюта',
label_share_link: 'Ссылка для отправки', label_share_link: 'Ссылка для отправки',
shortlink_toggle_label: 'Использовать короткую ссылку (нужно доверять серверу)',
shortlink_toggle_hint: 'Компромисс: короткие ссылки удобны, но скомпрометированный сервер может подменить данные счета при первом открытии.',
btn_new_request: 'Новый запрос на оплату', btn_new_request: 'Новый запрос на оплату',
toast_copied: 'Скопировано!', toast_copied: 'Скопировано!',
countdown_expired: 'Срок оплаты истёк', countdown_expired: 'Срок оплаты истёк',

2
i18n.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -67,6 +67,13 @@
<div class="uri-box" id="uri" style="display:none"></div> <div class="uri-box" id="uri" style="display:none"></div>
<div class="share-link-box" id="shareLinkBox"> <div class="share-link-box" id="shareLinkBox">
<label data-i18n="label_share_link">Shareable link</label> <label data-i18n="label_share_link">Shareable link</label>
<div class="shortlink-toggle-row">
<label class="shortlink-toggle-label">
<input type="checkbox" id="useShortLink">
<span data-i18n="shortlink_toggle_label">Use short link (requires server trust)</span>
</label>
<div class="shortlink-tradeoff" data-i18n="shortlink_toggle_hint">Trade-off: short links are convenient, but a compromised server could swap invoice data on first access.</div>
</div>
<div class="share-link-row"> <div class="share-link-row">
<input type="text" id="shareLink" readonly data-i18n-aria="label_share_link"> <input type="text" id="shareLink" readonly data-i18n-aria="label_share_link">
<button class="btn btn-secondary btn-icon" id="copyShareLink" title="Copy"> <button class="btn btn-secondary btn-icon" id="copyShareLink" title="Copy">
@@ -76,6 +83,7 @@
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-secondary" id="openWallet" data-i18n="btn_open_wallet">Open in wallet</button> <button class="btn btn-secondary" id="openWallet" data-i18n="btn_open_wallet">Open in wallet</button>
<button class="btn btn-secondary" id="copyUri" data-i18n="btn_copy_uri">Copy payment URI</button>
<button class="btn btn-secondary" id="copyAddr" data-i18n="btn_copy_addr">Copy address</button> <button class="btn btn-secondary" id="copyAddr" data-i18n="btn_copy_addr">Copy address</button>
<button class="btn btn-secondary" id="downloadPdf" data-i18n="btn_download_pdf">PDF Invoice</button> <button class="btn btn-secondary" id="downloadPdf" data-i18n="btn_download_pdf">PDF Invoice</button>
</div> </div>

View File

@@ -459,6 +459,33 @@ textarea {
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
} }
.shortlink-toggle-row {
margin-bottom: 0.45rem;
}
.shortlink-toggle-label {
display: flex;
align-items: center;
gap: 0.45rem;
margin: 0;
font-size: 0.78rem;
color: var(--text);
text-transform: none;
letter-spacing: 0;
}
.shortlink-toggle-label input {
width: auto;
accent-color: var(--accent);
}
.shortlink-tradeoff {
margin-top: 0.28rem;
font-size: 0.7rem;
color: #d08a61;
line-height: 1.35;
}
.share-link-row { .share-link-row {
display: flex; display: flex;
gap: 0.4rem; gap: 0.4rem;