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 uriBox = $('#uri');
const openWalletBtn = $('#openWallet');
const copyUriBtn = $('#copyUri');
const copyAddrBtn = $('#copyAddr');
const countdownEl = $('#countdown');
const fiatHint = $('#fiatHint');
const toast = $('#toast');
const shareLinkInput = $('#shareLink');
const copyShareLinkBtn = $('#copyShareLink');
const useShortLinkCheckbox = $('#useShortLink');
const newRequestBtn = $('#newRequest');
const homeLink = $('#homeLink');
let currentInvoiceHash = null;
// TX Proof DOM
const proofToggle = $('#proofToggle');
@@ -119,8 +122,12 @@
amountInput.addEventListener('input', updateFiatHint);
currencySelect.addEventListener('change', updateFiatHint);
generateBtn.addEventListener('click', generate);
copyUriBtn.addEventListener('click', () => copyToClipboard(uriBox.textContent));
copyAddrBtn.addEventListener('click', () => copyToClipboard(addrInput.value.trim()));
copyShareLinkBtn.addEventListener('click', () => copyToClipboard(shareLinkInput.value));
useShortLinkCheckbox.addEventListener('change', function () {
if (currentInvoiceHash) updateShareLink(currentInvoiceHash);
});
qrContainer.addEventListener('click', downloadQR);
newRequestBtn.addEventListener('click', resetForm);
homeLink.addEventListener('click', function (e) { e.preventDefault(); resetForm(); });
@@ -179,6 +186,8 @@
qrContainer.classList.remove('paid', 'confirming');
uriBox.textContent = '';
shareLinkInput.value = '';
useShortLinkCheckbox.checked = false;
currentInvoiceHash = null;
// Reset proof
invoiceCode = null;
stopConfirmationPolling();
@@ -326,7 +335,7 @@
buildSummary(xmrAmount, desc, timer);
updatePageTitle(xmrAmount, desc);
// Share link — keep existing short URL if present; otherwise shorten new hash
// Share link
var deadlineTs = null;
if (timer && timer > 0) {
if (!deadlineEndMs) {
@@ -335,14 +344,8 @@
deadlineTs = Math.floor(deadlineEndMs / 1000);
}
const hash = buildHash(addr, xmrAmount, desc, timer, deadlineTs);
if (invoiceCode) {
shareLinkInput.value = location.origin + '/s/' + invoiceCode;
} else {
shareLinkInput.value = location.origin + '/#' + hash;
shortenUrl(hash).then(function (shortUrl) {
if (shortUrl) shareLinkInput.value = shortUrl;
});
}
currentInvoiceHash = hash;
updateShareLink(hash);
// QR
qrContainer.innerHTML = '';
@@ -410,6 +413,7 @@
const code = params.get('c');
if (code) {
invoiceCode = code;
useShortLinkCheckbox.checked = true;
// Verify short URL integrity (detect tampering)
setTimeout(function () {
verifyShortUrlIntegrity(code, hash);
@@ -422,6 +426,26 @@
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.
function verifyShortUrlIntegrity(code, currentHash) {
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',
btn_generate: 'Create payment request',
btn_open_wallet: 'Open in wallet',
btn_copy_uri: 'Copy payment URI',
btn_copy_addr: 'Copy address',
btn_download_pdf: 'PDF Invoice',
pdf_title: 'Payment Request',
@@ -41,6 +42,8 @@ var I18n = (function () {
footer: footer,
aria_currency: 'Currency',
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',
toast_copied: 'Copied!',
countdown_expired: 'Payment deadline expired',
@@ -75,6 +78,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Tage',
btn_generate: 'Zahlungsanforderung erstellen',
btn_open_wallet: 'In Wallet öffnen',
btn_copy_uri: 'Zahlungs-URI kopieren',
btn_copy_addr: 'Adresse kopieren',
btn_download_pdf: 'PDF Rechnung',
pdf_title: 'Zahlungsanforderung',
@@ -90,6 +94,8 @@ var I18n = (function () {
footer: footer,
aria_currency: 'Währung',
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',
toast_copied: 'Kopiert!',
countdown_expired: 'Zahlungsfrist abgelaufen',
@@ -124,6 +130,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Jours',
btn_generate: 'Créer une demande de paiement',
btn_open_wallet: 'Ouvrir dans le wallet',
btn_copy_uri: 'Copier l\'URI de paiement',
btn_copy_addr: 'Copier l\'adresse',
btn_download_pdf: 'Facture PDF',
pdf_title: 'Demande de paiement',
@@ -139,6 +146,8 @@ var I18n = (function () {
footer: footer,
aria_currency: 'Devise',
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',
toast_copied: 'Copié !',
countdown_expired: 'Délai de paiement expiré',
@@ -173,6 +182,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Giorni',
btn_generate: 'Crea richiesta di pagamento',
btn_open_wallet: 'Apri nel wallet',
btn_copy_uri: 'Copia URI pagamento',
btn_copy_addr: 'Copia indirizzo',
btn_download_pdf: 'Fattura PDF',
pdf_title: 'Richiesta di pagamento',
@@ -188,6 +198,8 @@ var I18n = (function () {
footer: footer,
aria_currency: 'Valuta',
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',
toast_copied: 'Copiato!',
countdown_expired: 'Scadenza pagamento superata',
@@ -222,6 +234,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Días',
btn_generate: 'Crear solicitud de pago',
btn_open_wallet: 'Abrir en wallet',
btn_copy_uri: 'Copiar URI de pago',
btn_copy_addr: 'Copiar dirección',
btn_download_pdf: 'Factura PDF',
pdf_title: 'Solicitud de pago',
@@ -237,6 +250,8 @@ var I18n = (function () {
footer: footer,
aria_currency: 'Moneda',
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',
toast_copied: '¡Copiado!',
countdown_expired: 'Plazo de pago vencido',
@@ -271,6 +286,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Dias',
btn_generate: 'Criar pedido de pagamento',
btn_open_wallet: 'Abrir na wallet',
btn_copy_uri: 'Copiar URI de pagamento',
btn_copy_addr: 'Copiar endereço',
btn_download_pdf: 'Fatura PDF',
pdf_title: 'Pedido de pagamento',
@@ -286,6 +302,8 @@ var I18n = (function () {
footer: footer,
aria_currency: 'Moeda',
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',
toast_copied: 'Copiado!',
countdown_expired: 'Prazo de pagamento expirado',
@@ -320,6 +338,7 @@ var I18n = (function () {
placeholder_timer_custom: 'Дней',
btn_generate: 'Создать запрос на оплату',
btn_open_wallet: 'Открыть в кошельке',
btn_copy_uri: 'Копировать платежный URI',
btn_copy_addr: 'Копировать адрес',
btn_download_pdf: 'PDF счёт',
pdf_title: 'Запрос на оплату',
@@ -335,6 +354,8 @@ var I18n = (function () {
footer: footer,
aria_currency: 'Валюта',
label_share_link: 'Ссылка для отправки',
shortlink_toggle_label: 'Использовать короткую ссылку (нужно доверять серверу)',
shortlink_toggle_hint: 'Компромисс: короткие ссылки удобны, но скомпрометированный сервер может подменить данные счета при первом открытии.',
btn_new_request: 'Новый запрос на оплату',
toast_copied: 'Скопировано!',
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="share-link-box" id="shareLinkBox">
<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">
<input type="text" id="shareLink" readonly data-i18n-aria="label_share_link">
<button class="btn btn-secondary btn-icon" id="copyShareLink" title="Copy">
@@ -76,6 +83,7 @@
</div>
<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="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="downloadPdf" data-i18n="btn_download_pdf">PDF Invoice</button>
</div>

View File

@@ -459,6 +459,33 @@ textarea {
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 {
display: flex;
gap: 0.4rem;