//! Payment page HTML renderer for the L402 402-challenge response. //! //! Separated from `lib.rs` to keep the core module focused on Nginx plumbing. //! This module owns all HTML, CSS, and JavaScript that is sent to the browser //! when a protected resource requires payment. /// Render the full 402 payment page as an HTML string. /// /// # Arguments /// * `invoice` - BOLT-11 invoice string (used for QR + copy) /// * `amount_msat` - Amount in millisatoshis (displayed as sats) /// * `macaroon_b64` - Base64-encoded macaroon token /// * `auto_detect` - Whether to poll for automatic payment detection /// * `cashu_enabled` - Whether to show the Cashu/eCash tab /// * `cashu_payment_request` - Optional P2PK Cashu payment request string pub fn render_payment_page( invoice: &str, amount_msat: i64, macaroon_b64: &str, auto_detect: bool, cashu_enabled: bool, cashu_payment_request: Option<&str>, ) -> String { // ── QR Code ───────────────────────────────────────────────────────────── // Generate at 280 px and inject centering styles into the SVG root so the // image always fills its white card wrapper without cropping. let raw_qr = qrcode_generator::to_svg_to_string( invoice.to_uppercase(), qrcode_generator::QrCodeEcc::Medium, 280, None::<&str>, ) .unwrap_or_else(|_| { "\ \ QR Error\ " .to_string() }); let qr_svg = raw_qr.replacen( " 40 { format!("{}\u{2026}{}", &invoice[..20], &invoice[invoice.len() - 10..]) } else { invoice.to_string() }; // ── Cashu tab ──────────────────────────────────────────────────────────── let cashu_tab_html = if cashu_enabled { let payment_req_hint = cashu_payment_request .map(|r| { let preview = &r[..r.len().min(60)]; format!( "
\ Payment Request\ {preview}\u{2026}\
", preview = preview, ) }) .unwrap_or_default(); format!( "
\
\
\ 🥜\
\
Pay with Cashu eCash
\
Paste a Cashu token to instantly unlock access
\
\
\ {payment_req_hint}\
\ \ \
\ \
\
\
", payment_req_hint = payment_req_hint, ) } else { String::new() }; let ecash_tab_btn = if cashu_enabled { "" } else { "" }; // ── Auto-detect polling JS ──────────────────────────────────────────────── let auto_detect_js = if auto_detect { format!( r#" let pollAttempts = 0; const MAX_POLL = 100; function startPolling() {{ if (pollAttempts++ > MAX_POLL) {{ document.getElementById('auto-status').classList.add('hidden'); document.getElementById('preimage-section').classList.remove('hidden'); return; }} fetch(window.location.href, {{ headers: {{'Authorization': 'L402 {mac}'}}, redirect: 'follow', credentials: 'same-origin' }}).then(r => {{ if (r.ok || r.status === 200) {{ document.getElementById('auto-status').innerHTML = '✓ Payment confirmed! Redirecting…'; setTimeout(() => window.location.reload(), 800); }} else {{ setTimeout(startPolling, 3000); }} }}).catch(() => setTimeout(startPolling, 3000)); }} startPolling(); "#, mac = macaroon_b64 ) } else { String::new() }; let auto_detect_section = if auto_detect { "
\
\ Waiting for payment confirmation\u{2026}\ \
" } else { "" }; let preimage_hidden_class = if auto_detect { "hidden" } else { "" }; // ── Full HTML page ──────────────────────────────────────────────────────── format!( r#" 402 Payment Required — L402
⚡ 402 Payment Required

Lightning Payment

Access to this resource requires a payment

{amount_sats} sats ({amount_msat} msats)
{ecash_tab_btn}
{qr_svg}
{invoice_short}
Copied to clipboard!

{auto_detect_section}
{cashu_tab_html}
"#, amount_sats = amount_sats, amount_msat = amount_msat, ecash_tab_btn = ecash_tab_btn, qr_svg = qr_svg, invoice_short = invoice_short, auto_detect_section = auto_detect_section, preimage_hidden_class = preimage_hidden_class, cashu_tab_html = cashu_tab_html, invoice_json = serde_json::to_string(invoice).unwrap_or_else(|_| "\"\"".to_string()), macaroon_json = serde_json::to_string(macaroon_b64).unwrap_or_else(|_| "\"\"".to_string()), auto_detect_js = auto_detect_js, ) }