|
| 1 | +export type ReceiptOptions = { |
| 2 | + apiUrl?: string; |
| 3 | +}; |
| 4 | + |
| 5 | +function moneyFmt(value: any, currency = 'USD') { |
| 6 | + try { return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format((value && Number(value)) / 100 || value); } catch (e) { return String(value); } |
| 7 | +} |
| 8 | + |
| 9 | +async function fetchFromSession(endpoint: string, sessionId: string) { |
| 10 | + try { |
| 11 | + const res = await fetch(endpoint, { |
| 12 | + method: 'POST', |
| 13 | + headers: { 'Content-Type': 'application/json' }, |
| 14 | + body: JSON.stringify({ action: 'stripe.receipt', session_id: sessionId }), |
| 15 | + }); |
| 16 | + if (!res.ok) { |
| 17 | + console.error('receipt.client: server returned', res.status); |
| 18 | + return null; |
| 19 | + } |
| 20 | + const json = await res.json().catch(() => null); |
| 21 | + return json?.data?.link?.response || json?.data || null; |
| 22 | + } catch (e) { |
| 23 | + console.error('receipt.client: failed to fetch receipt from session', e); |
| 24 | + return null; |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +function parseReceiptFromQuery() { |
| 29 | + try { |
| 30 | + const qs = new URLSearchParams(location.search); |
| 31 | + const raw = qs.get('receipt') || qs.get('data') || qs.get('r') || qs.get('payload'); |
| 32 | + if (!raw) return null; |
| 33 | + try { return JSON.parse(decodeURIComponent(raw)); } catch (e) {} |
| 34 | + try { return JSON.parse(atob(raw)); } catch (e) {} |
| 35 | + return null; |
| 36 | + } catch (e) { return null; } |
| 37 | +} |
| 38 | + |
| 39 | +export default async function initReceipt(opts: ReceiptOptions = {}) { |
| 40 | + // if apiUrl not provided, try to read server-rendered DOM config or window global |
| 41 | + let apiUrl = opts.apiUrl; |
| 42 | + if (!apiUrl) { |
| 43 | + try { |
| 44 | + const cfg = document.getElementById('receipt-config'); |
| 45 | + if (cfg) apiUrl = cfg.getAttribute('data-api-url') || undefined; |
| 46 | + } catch (e) { /* ignore */ } |
| 47 | + } |
| 48 | + if (!apiUrl && typeof window !== 'undefined') { |
| 49 | + // @ts-ignore window global may be set by legacy pages |
| 50 | + apiUrl = (window as any).__RECEIPT_API_URL || apiUrl; |
| 51 | + } |
| 52 | + |
| 53 | + const apiBase = (apiUrl || '').replace(/\/+$/, ''); |
| 54 | + const endpoint = apiBase ? apiBase + '/api/markket' : '/api/markket'; |
| 55 | + |
| 56 | + const qs = new URLSearchParams(location.search); |
| 57 | + const sessionIdParam = qs.get('session_id') || qs.get('session') || qs.get('sid'); |
| 58 | + |
| 59 | + let data = null; |
| 60 | + if (sessionIdParam) { |
| 61 | + data = await fetchFromSession(endpoint, sessionIdParam); |
| 62 | + } else { |
| 63 | + data = parseReceiptFromQuery(); |
| 64 | + } |
| 65 | + |
| 66 | + // page uses specific IDs rather than data-output attributes |
| 67 | + const outputs = { |
| 68 | + total: document.getElementById('order-total'), |
| 69 | + subtotal: document.getElementById('order-subtotal'), |
| 70 | + email: document.getElementById('order-email'), |
| 71 | + number: document.getElementById('order-number'), |
| 72 | + date: document.getElementById('order-date'), |
| 73 | + }; |
| 74 | + |
| 75 | + if (!data) { |
| 76 | + console.info('Receipt page: no data found (session_id or receipt payload)'); |
| 77 | + return; |
| 78 | + } |
| 79 | + |
| 80 | + // total and subtotal |
| 81 | + const currency = (data.currency || data.currency_code || (data?.link && data.link.response && data.link.response.currency) || 'USD').toUpperCase(); |
| 82 | + const totalVal = data.amount_total ?? data.amountTotal ?? data.total ?? data.amount ?? data.payment_intent_amount ?? 0; |
| 83 | + const subtotalVal = data.amount_subtotal ?? data.amountSubtotal ?? data.subtotal ?? 0; |
| 84 | + |
| 85 | + if (outputs.total) outputs.total.textContent = moneyFmt(totalVal, currency); |
| 86 | + if (outputs.subtotal) outputs.subtotal.textContent = moneyFmt(subtotalVal, currency); |
| 87 | + |
| 88 | + // customer email / name |
| 89 | + const email = data.customer_details?.email || data.customer_email || data.email || data.receipt_email || ''; |
| 90 | + const name = data.customer_details?.name || data.customer || ''; |
| 91 | + if (outputs.email) { |
| 92 | + const safe = name ? `${name}${email ? ' <' + email + '>' : ''}` : (email || '—'); |
| 93 | + outputs.email.textContent = safe; |
| 94 | + } |
| 95 | + |
| 96 | + // order number / session id |
| 97 | + const orderId = data.id || data.session_id || data.transaction || data.payment_intent || ''; |
| 98 | + if (outputs.number) outputs.number.textContent = orderId || '—'; |
| 99 | + |
| 100 | + // created date (timestamp in seconds) |
| 101 | + const createdTs = data.created ?? data.created_at ?? null; |
| 102 | + try { |
| 103 | + if (outputs.date && createdTs) { |
| 104 | + const d = typeof createdTs === 'number' && String(createdTs).length === 10 ? new Date(createdTs * 1000) : new Date(createdTs); |
| 105 | + outputs.date.textContent = d.toLocaleString(); |
| 106 | + } |
| 107 | + } catch (e) { /* ignore */ } |
| 108 | + |
| 109 | + // render items if present |
| 110 | + const itemsList = document.getElementById('order-items'); |
| 111 | + const itemsArr = data.items || data.line_items || data.line_items?.data || []; |
| 112 | + if (itemsList && Array.isArray(itemsArr)) { |
| 113 | + itemsList.innerHTML = ''; |
| 114 | + const arr = itemsArr; |
| 115 | + for (const it of arr) { |
| 116 | + const li = document.createElement('li'); |
| 117 | + li.className = 'flex justify-between items-center'; |
| 118 | + const title = document.createElement('div'); |
| 119 | + title.innerHTML = `<div class="font-medium">${it.name || it.description || 'Item'}</div><div class="text-sm">qty: ${it.qty ?? it.quantity ?? 1}</div>`; |
| 120 | + const price = document.createElement('div'); |
| 121 | + price.className = 'font-medium'; |
| 122 | + const p = Number(it.unit_price ?? it.price ?? it.amount ?? 0); |
| 123 | + try { |
| 124 | + const currency = data?.currency || data?.currency_code || 'USD'; |
| 125 | + price.textContent = new Intl.NumberFormat(undefined, { style: 'currency', currency }).format((isNaN(p) ? 0 : p) / 100); |
| 126 | + } catch (e) { |
| 127 | + price.textContent = String((isNaN(p) ? 0 : p) / 100); |
| 128 | + } |
| 129 | + li.appendChild(title); |
| 130 | + li.appendChild(price); |
| 131 | + itemsList.appendChild(li); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + // reveal receipt section if hidden |
| 136 | + const emptyEl = document.getElementById('receipt-empty'); |
| 137 | + const receiptEl = document.getElementById('receipt'); |
| 138 | + if (emptyEl) emptyEl.classList.add('hidden'); |
| 139 | + if (receiptEl) receiptEl.classList.remove('hidden'); |
| 140 | + |
| 141 | + // shipping details |
| 142 | + try { |
| 143 | + const shipping = data.shipping_details || data.shipping || data.collected_information?.shipping_details || data.link?.response?.shipping_details || null; |
| 144 | + const shipEl = document.getElementById('order-shipping'); |
| 145 | + if (shipEl) { |
| 146 | + if (shipping && (shipping.address || shipping.name)) { |
| 147 | + let lines = []; |
| 148 | + if (shipping.name) lines.push(String(shipping.name)); |
| 149 | + const addr = shipping.address || shipping; |
| 150 | + if (addr?.line1) lines.push(String(addr.line1)); |
| 151 | + if (addr?.line2) lines.push(String(addr.line2)); |
| 152 | + const cityParts = [addr?.city, addr?.state, addr?.postal_code].filter(Boolean).join(', '); |
| 153 | + if (cityParts) lines.push(cityParts); |
| 154 | + if (addr?.country) lines.push(String(addr.country)); |
| 155 | + shipEl.textContent = lines.join('\n'); |
| 156 | + } else { |
| 157 | + // no shipping -> hide or keep placeholder |
| 158 | + // leave the existing placeholder |
| 159 | + } |
| 160 | + } |
| 161 | + } catch (e) { /* ignore shipping render errors */ } |
| 162 | +} |
0 commit comments