Preserve absolute invoice deadline across reloads

This commit is contained in:
Alexander Schmidt
2026-03-26 13:43:30 +01:00
parent 69f173bc2f
commit 758b2f3589
3 changed files with 34 additions and 6 deletions

View File

@@ -45,6 +45,7 @@ $data = $urls[$code];
$hash = is_array($data) ? ($data['h'] ?? '') : $data; $hash = is_array($data) ? ($data['h'] ?? '') : $data;
$hash = is_string($hash) ? $hash : ''; $hash = is_string($hash) ? $hash : '';
$signature = is_array($data) ? $data['s'] : null; $signature = is_array($data) ? $data['s'] : null;
$expiryTs = is_array($data) ? intval($data['e'] ?? 0) : 0;
// Re-derive expected signature so client can verify // Re-derive expected signature so client can verify
$expected = $signature ? hash_hmac('sha256', $hash, get_hmac_secret()) : null; $expected = $signature ? hash_hmac('sha256', $hash, get_hmac_secret()) : null;
@@ -52,5 +53,6 @@ $expected = $signature ? hash_hmac('sha256', $hash, get_hmac_secret()) : null;
echo json_encode([ echo json_encode([
'code' => $code, 'code' => $code,
'hash' => $hash, 'hash' => $hash,
'signature' => $expected 'signature' => $expected,
'expiry_ts' => $expiryTs > 0 ? $expiryTs : null
]); ]);

34
app.js
View File

@@ -29,6 +29,7 @@
const timerCustom = $('#timerCustom'); const timerCustom = $('#timerCustom');
const deadlineBadges = $('#deadlineBadges'); const deadlineBadges = $('#deadlineBadges');
let selectedDays = 0; let selectedDays = 0;
let deadlineEndMs = null;
const generateBtn = $('#generate'); const generateBtn = $('#generate');
const resultSection = $('#result'); const resultSection = $('#result');
const qrContainer = $('#qr'); const qrContainer = $('#qr');
@@ -131,11 +132,13 @@
if (btn.classList.contains('active')) { if (btn.classList.contains('active')) {
btn.classList.remove('active'); btn.classList.remove('active');
selectedDays = 0; selectedDays = 0;
deadlineEndMs = null;
timerCustom.value = ''; timerCustom.value = '';
} else { } else {
deadlineBadges.querySelectorAll('.badge').forEach(function (b) { b.classList.remove('active'); }); deadlineBadges.querySelectorAll('.badge').forEach(function (b) { b.classList.remove('active'); });
btn.classList.add('active'); btn.classList.add('active');
selectedDays = days; selectedDays = days;
deadlineEndMs = null;
timerCustom.value = ''; timerCustom.value = '';
} }
}); });
@@ -143,6 +146,7 @@
timerCustom.addEventListener('input', function () { timerCustom.addEventListener('input', function () {
deadlineBadges.querySelectorAll('.badge').forEach(function (b) { b.classList.remove('active'); }); deadlineBadges.querySelectorAll('.badge').forEach(function (b) { b.classList.remove('active'); });
selectedDays = parseInt(timerCustom.value) || 0; selectedDays = parseInt(timerCustom.value) || 0;
deadlineEndMs = null;
}); });
// PDF // PDF
@@ -162,6 +166,7 @@
currencySelect.value = 'EUR'; currencySelect.value = 'EUR';
descInput.value = ''; descInput.value = '';
selectedDays = 0; selectedDays = 0;
deadlineEndMs = null;
timerCustom.value = ''; timerCustom.value = '';
deadlineBadges.querySelectorAll('.badge').forEach(function (b) { b.classList.remove('active'); }); deadlineBadges.querySelectorAll('.badge').forEach(function (b) { b.classList.remove('active'); });
fiatHint.textContent = ''; fiatHint.textContent = '';
@@ -271,12 +276,13 @@
return uri; return uri;
} }
function buildHash(addr, xmrAmount, desc, timer) { function buildHash(addr, xmrAmount, desc, timer, deadlineTs) {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('a', addr); params.set('a', addr);
if (xmrAmount) params.set('x', xmrAmount.toFixed(12)); if (xmrAmount) params.set('x', xmrAmount.toFixed(12));
if (desc) params.set('d', desc); if (desc) params.set('d', desc);
if (timer) params.set('t', timer); if (timer) params.set('t', timer);
if (deadlineTs) params.set('te', deadlineTs);
return params.toString(); return params.toString();
} }
@@ -321,7 +327,14 @@
updatePageTitle(xmrAmount, desc); updatePageTitle(xmrAmount, desc);
// Share link — keep existing short URL if present; otherwise shorten new hash // Share link — keep existing short URL if present; otherwise shorten new hash
const hash = buildHash(addr, xmrAmount, desc, timer); var deadlineTs = null;
if (timer && timer > 0) {
if (!deadlineEndMs) {
deadlineEndMs = Date.now() + timer * 86400000;
}
deadlineTs = Math.floor(deadlineEndMs / 1000);
}
const hash = buildHash(addr, xmrAmount, desc, timer, deadlineTs);
if (invoiceCode) { if (invoiceCode) {
shareLinkInput.value = location.origin + '/s/' + invoiceCode; shareLinkInput.value = location.origin + '/s/' + invoiceCode;
} else { } else {
@@ -388,6 +401,11 @@
} }
} }
const deadlineTs = parseInt(params.get('te') || '0');
if (deadlineTs > 0) {
deadlineEndMs = deadlineTs * 1000;
}
// Check for short URL code and load payment status // Check for short URL code and load payment status
const code = params.get('c'); const code = params.get('c');
if (code) { if (code) {
@@ -416,6 +434,13 @@
return; return;
} }
if (data.expiry_ts && parseInt(data.expiry_ts) > 0) {
deadlineEndMs = parseInt(data.expiry_ts) * 1000;
if (resultSection.classList.contains('visible')) {
startCountdown();
}
}
var params = new URLSearchParams(currentHash); var params = new URLSearchParams(currentHash);
params.delete('c'); params.delete('c');
var normalizedHash = params.toString(); var normalizedHash = params.toString();
@@ -462,9 +487,10 @@
countdownEl.textContent = ''; countdownEl.textContent = '';
countdownEl.className = 'countdown'; countdownEl.className = 'countdown';
if (!selectedDays || selectedDays <= 0) return; if ((!selectedDays || selectedDays <= 0) && !deadlineEndMs) return;
const end = Date.now() + selectedDays * 86400000; const end = deadlineEndMs || (Date.now() + selectedDays * 86400000);
deadlineEndMs = end;
countdownEl.classList.add('active'); countdownEl.classList.add('active');
function tick() { function tick() {

2
app.min.js vendored

File diff suppressed because one or more lines are too long