Diff -- /var/www/vhosts/webstls.com/httpdocs/TEMPLATES/webstls_checkout.html
Diff

/var/www/vhosts/webstls.com/httpdocs/TEMPLATES/webstls_checkout.html

added on WebSTLs (webstls.com) at 2026-07-01 22:26:58

Added
+550
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 759aed903792
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <link rel="icon" type="image/svg+xml" href="/assets/images/favicon.svg">
7 <title>Checkout &middot; $store_name</title>
8 <link rel="stylesheet" href="/assets/css/site.css">
9 <style>:root { $theme_css_vars }</style>
10 <style>
11 body { background: var(--col-bg); color: var(--col-text); font-family: var(--font-ui, sans-serif); margin: 0; }
12 .co-page { max-width: 720px; margin: 0 auto; padding: 60px 24px 100px; }
13 .co-card {
14 background: var(--col-surface-1);
15 border: 1px solid var(--col-border-2);
16 border-radius: 16px;
17 padding: 32px;
18 }
19 .co-card h1 { margin: 0 0 14px; font-size: 26px; color: var(--col-text); }
20 .co-card p { color: var(--col-text-2); line-height: 1.7; }
21 .co-line { display:flex; justify-content:space-between; padding:10px 0; border-bottom:1px solid var(--col-border); font-size:14px; }
22 .co-line:last-child { border-bottom:0; }
23 .co-line .qty { color:var(--col-text-3); font-size:12px; margin-left:6px; }
24 .co-total-row { display:flex; justify-content:space-between; margin-top:16px; padding-top:14px; border-top:2px solid var(--col-border-2); font-weight:700; font-size:18px; color:var(--col-text); }
25 .co-field { margin-top:18px; }
26 .co-field label { display:block; font-size:12px; font-weight:600; color:var(--col-text-2); margin-bottom:6px; letter-spacing:0.4px; }
27 .co-field input { width:100%; box-sizing:border-box; padding:11px 13px; border-radius:9px; border:1px solid var(--col-border); background:var(--col-bg-2); color:var(--col-text); font-size:14px; }
28 #payment-element { margin-top:8px; padding:14px; background:var(--col-bg-2); border:1px solid var(--col-border); border-radius:9px; min-height:60px; }
29 .co-btn {
30 display:inline-block; padding:12px 22px; border-radius:10px;
31 background: linear-gradient(130deg, var(--col-accent), var(--col-accent-bright));
32 border:0; color:#fff; font-size:14px; font-weight:700; cursor:pointer;
33 width:100%; margin-top:20px;
34 }
35 .co-btn[disabled] { opacity:0.55; cursor:not-allowed; }
36 .co-secondary {
37 display:inline-block; padding:10px 18px; border-radius:9px;
38 background: var(--col-surface-2); border:1px solid var(--col-border);
39 color: var(--col-text); text-decoration:none;
40 font-size: 13px; font-weight: 600;
41 }
42 .co-err { margin-top:12px; padding:10px 12px; border-radius:8px; background:rgba(239,68,68,0.10); border:1px solid rgba(239,68,68,0.35); color:#fecaca; font-size:13px; display:none; }
43 .co-err.show { display:block; }
44 .co-coming {
45 font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase;
46 color: var(--col-accent-bright);
47 margin-bottom: 12px; font-weight: 700;
48 }
49 .co-foot { margin-top:18px; text-align:center; font-size:12px; color:var(--col-text-3); }
50 .co-foot svg { width:14px; height:14px; vertical-align:-2px; margin-right:4px; }
51 </style>
52</head>
53<body>
54$store_chrome_html
55<main class="co-page">
56
57 [if:$has_items]
58 <div class="co-card">
59
60 [if:$is_payable]
61 <h1>Checkout</h1>
62 <p style="margin-top:0">You're buying $item_count item(s) from <strong>$store_name</strong>. Files unlock the moment payment clears.</p>
63
64 <div style="margin-top:20px">
65 [loop:@items]
66 <div class="co-line"><span>$loop1.title<span class="qty">x$loop1.qty</span></span><span class="font-mono">$loop1.line_total</span></div>
67 [/loop]
68 <div class="co-line"><span>Subtotal</span><span class="font-mono" id="co-subtotal">$total_display</span></div>
69 [if:$needs_shipping]
70 <div class="co-line" id="co-ship-row"><span>Shipping</span><span class="font-mono" id="co-ship">\$0.00</span></div>
71 [/if]
72 <div class="co-line" id="co-tax-row" style="display:none"><span id="co-tax-label">Tax</span><span class="font-mono" id="co-tax">\$0.00</span></div>
73 <div class="co-total-row"><span>Total</span><span class="font-mono" id="co-total">$total_display</span></div>
74 </div>
75
76 <form id="payment-form">
77 <div class="co-field">
78 <label for="co-email">Email for receipt and downloads</label>
79 <input type="email" id="co-email" name="email" required value="$prefill_email" autocomplete="email">
80 </div>
81 <div class="co-field">
82 <label for="co-bill-country">Billing country (for tax)</label>
83 <select id="co-bill-country" name="bill_country" autocomplete="country">
84 <option value="">&mdash; Select country &mdash;</option>
85 <option value="US">United States</option>
86 <option value="CA">Canada</option>
87 <option value="GB">United Kingdom</option>
88 <option value="AT">Austria</option><option value="BE">Belgium</option><option value="BG">Bulgaria</option>
89 <option value="HR">Croatia</option><option value="CY">Cyprus</option><option value="CZ">Czechia</option>
90 <option value="DK">Denmark</option><option value="EE">Estonia</option><option value="FI">Finland</option>
91 <option value="FR">France</option><option value="DE">Germany</option><option value="GR">Greece</option>
92 <option value="HU">Hungary</option><option value="IE">Ireland</option><option value="IT">Italy</option>
93 <option value="LV">Latvia</option><option value="LT">Lithuania</option><option value="LU">Luxembourg</option>
94 <option value="MT">Malta</option><option value="NL">Netherlands</option><option value="PL">Poland</option>
95 <option value="PT">Portugal</option><option value="RO">Romania</option><option value="SK">Slovakia</option>
96 <option value="SI">Slovenia</option><option value="ES">Spain</option><option value="SE">Sweden</option>
97 <option value="AU">Australia</option><option value="NZ">New Zealand</option>
98 <option value="NO">Norway</option><option value="CH">Switzerland</option>
99 <option value="OTHER">Other / Not listed (no tax)</option>
100 </select>
101 </div>
102 <div class="co-field" id="co-region-wrap" style="display:none">
103 <label for="co-bill-region" id="co-region-label">State / Region</label>
104 <input type="text" id="co-bill-region" name="bill_region" maxlength="10" placeholder="CA" autocomplete="address-level1">
105 </div>
106 <div class="co-field" id="co-vat-wrap" style="display:none">
107 <label for="co-vat-id">VAT number (optional, B2B reverse charge)</label>
108 <input type="text" id="co-vat-id" name="vat_id" maxlength="20" placeholder="DE123456789">
109 <div id="co-vat-status" style="margin-top:6px;font-size:12px;color:var(--col-text-3)"></div>
110 </div>
111
112 <!-- Billing address (for the credit card). Always shown so we
113 can attach a full billing snapshot to the order even when
114 there's no physical line to ship. Pre-fills from the
115 billing country + region above; buyer fills in the rest. -->
116 <div style="margin-top:22px;padding:16px 18px;border:1px solid var(--col-border);border-radius:12px;background:var(--col-surface-1)">
117 <div style="font-size:13px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--col-text-2);margin-bottom:12px">Billing address</div>
118 <div class="co-field">
119 <label for="co-bill-name">Cardholder name</label>
120 <input type="text" id="co-bill-name" name="bill_name" required autocomplete="cc-name" maxlength="120">
121 </div>
122 <div class="co-field">
123 <label for="co-bill-addr1">Street address</label>
124 <input type="text" id="co-bill-addr1" name="bill_addr1" required autocomplete="billing address-line1" maxlength="200">
125 </div>
126 <div class="co-field">
127 <label for="co-bill-addr2">Apt / Suite <span style="color:var(--col-text-3);font-weight:400">(optional)</span></label>
128 <input type="text" id="co-bill-addr2" name="bill_addr2" autocomplete="billing address-line2" maxlength="200">
129 </div>
130 <div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
131 <div class="co-field">
132 <label for="co-bill-city">City</label>
133 <input type="text" id="co-bill-city" name="bill_city" required autocomplete="billing address-level2" maxlength="120">
134 </div>
135 <div class="co-field">
136 <label for="co-bill-postal">Postal code</label>
137 <input type="text" id="co-bill-postal" name="bill_postal" required autocomplete="billing postal-code" maxlength="20">
138 </div>
139 </div>
140 <div class="co-field">
141 <label for="co-bill-phone">Phone <span style="color:var(--col-text-3);font-weight:400">(optional)</span></label>
142 <input type="tel" id="co-bill-phone" name="bill_phone" autocomplete="billing tel" maxlength="40">
143 </div>
144 </div>
145
146 [if:$needs_shipping]
147 <!-- Shipping address + rate selector. Only rendered when at
148 least one cart line is a 3D-printed physical item. -->
149 <div style="margin-top:22px;padding:16px 18px;border:1px solid var(--col-border);border-radius:12px;background:var(--col-surface-1)">
150 <div style="font-size:13px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--col-text-2);margin-bottom:12px">Shipping address</div>
151 <label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--col-text-2);margin-bottom:14px;cursor:pointer">
152 <input type="checkbox" id="co-same-as-billing" checked>
153 <span>Same as billing</span>
154 </label>
155
156 <div class="co-field">
157 <label for="co-ship-name">Full name</label>
158 <input type="text" id="co-ship-name" name="ship_name" required autocomplete="name" maxlength="120">
159 </div>
160 <div class="co-field">
161 <label for="co-ship-addr1">Street address</label>
162 <input type="text" id="co-ship-addr1" name="ship_addr1" required autocomplete="address-line1" maxlength="200">
163 </div>
164 <div class="co-field">
165 <label for="co-ship-addr2">Apt / Suite / Unit <span style="color:var(--col-text-3);font-weight:400">(optional)</span></label>
166 <input type="text" id="co-ship-addr2" name="ship_addr2" autocomplete="address-line2" maxlength="200">
167 </div>
168 <div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
169 <div class="co-field">
170 <label for="co-ship-city">City</label>
171 <input type="text" id="co-ship-city" name="ship_city" required autocomplete="address-level2" maxlength="120">
172 </div>
173 <div class="co-field">
174 <label for="co-ship-region">State / Region</label>
175 <input type="text" id="co-ship-region" name="ship_region" autocomplete="address-level1" maxlength="80">
176 </div>
177 </div>
178 <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
179 <div class="co-field">
180 <label for="co-ship-postal">Postal code</label>
181 <input type="text" id="co-ship-postal" name="ship_postal" required autocomplete="postal-code" maxlength="20">
182 </div>
183 <div class="co-field">
184 <label for="co-ship-country">Country</label>
185 <input type="text" id="co-ship-country" name="ship_country" required autocomplete="country" maxlength="2" placeholder="US" style="text-transform:uppercase">
186 </div>
187 </div>
188 <div class="co-field">
189 <label for="co-ship-phone">Phone <span style="color:var(--col-text-3);font-weight:400">(optional, for delivery)</span></label>
190 <input type="tel" id="co-ship-phone" name="ship_phone" autocomplete="tel" maxlength="40">
191 </div>
192
193 <div style="font-size:13px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--col-text-2);margin:18px 0 10px">Shipping option</div>
194 <div id="co-ship-zones" style="display:flex;flex-direction:column;gap:8px">
195 [loop:@ship_options]
196 <label style="display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--col-border);border-radius:8px;cursor:pointer;background:var(--col-surface-2)">
197 <input type="radio" name="ship_zone" value="$loop1.slug" data-cents="$loop1.cents" $loop1.checked_attr>
198 <span style="flex:1;font-weight:600">$loop1.label</span>
199 <span class="font-mono" style="color:var(--col-accent-bright)">$loop1.cost_label</span>
200 </label>
201 [/loop]
202 </div>
203 </div>
204 [/if]
205
206 <div class="co-field" style="margin-top:22px">
207 <label>Card details</label>
208 <div id="payment-element">
209 <div style="color:var(--col-text-3);font-size:13px;text-align:center;padding:8px">Loading secure payment form...</div>
210 </div>
211 </div>
212 <div class="co-err" id="co-err"></div>
213 <button type="submit" id="co-submit" class="co-btn">Pay $total_display</button>
214 <div class="co-foot">
215 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
216 Secured by Stripe. WebSTLs never sees your card.
217 </div>
218 </form>
219
220 <script src="https://js.stripe.com/v3/"></script>
221 <script>
222 (function(){
223 var stripe = Stripe('$stripe_pk');
224 var elements = null;
225 var clientSecret = null;
226 var orderId = null;
227 var form = document.getElementById('payment-form');
228 var submit = document.getElementById('co-submit');
229 var emailInput = document.getElementById('co-email');
230 var errBox = document.getElementById('co-err');
231 var countrySel = document.getElementById('co-bill-country');
232 var regionInput = document.getElementById('co-bill-region');
233 var regionWrap = document.getElementById('co-region-wrap');
234 var regionLabel = document.getElementById('co-region-label');
235 var taxRow = document.getElementById('co-tax-row');
236 var taxOut = document.getElementById('co-tax');
237 var taxLabel = document.getElementById('co-tax-label');
238 var totalOut = document.getElementById('co-total');
239 var submitBtn = document.getElementById('co-submit');
240
241 function fmtCents(c){
242 var v = (c || 0) / 100;
243 return '$' + v.toFixed(2);
244 }
245 // Tax preview -- async refresh whenever country/region change.
246 // Cart subtotal is server-rendered; we only update the tax + total
247 // rows. PaymentIntent is still created on submit so a half-filled
248 // form doesn't burn intents.
249 var previewTimer = null;
250 // Subtotal in cents (parsed once from the server-rendered
251 // dollar string). Used to recompute the displayed total when
252 // tax + shipping change.
253 var subtotalCents = (function(){
254 var t = document.getElementById('co-subtotal').textContent || '';
255 var n = parseFloat(t.replace(/[^0-9.]/g, '')) || 0;
256 return Math.round(n * 100);
257 })();
258 function shipCents(){
259 var sel = document.querySelector('input[name="ship_zone"]:checked');
260 if (!sel) return 0;
261 return parseInt(sel.getAttribute('data-cents') || '0', 10) || 0;
262 }
263 function updateTotal(taxCents){
264 var ship = shipCents();
265 var ship_el = document.getElementById('co-ship');
266 if (ship_el) ship_el.textContent = fmtCents(ship);
267 var total = subtotalCents + ship + (taxCents || 0);
268 totalOut.textContent = fmtCents(total);
269 if (submitBtn) submitBtn.textContent = 'Pay ' + fmtCents(total);
270 }
271 document.querySelectorAll('input[name="ship_zone"]').forEach(function(r){
272 r.addEventListener('change', function(){ updateTotal(currentTaxCents); });
273 });
274
275 // "Same as billing" copies bill_* values into ship_* on toggle
276 // and on every billing-field change. Unchecking exposes the
277 // ship_* fields for independent entry.
278 var sameToggle = document.getElementById('co-same-as-billing');
279 var shipFields = ['name','addr1','addr2','city','region','postal','country','phone'];
280 var billPairs = { name:'name', addr1:'addr1', addr2:'addr2', city:'city',
281 region:null, postal:'postal', country:null, phone:'phone' };
282 function copyBillToShip() {
283 shipFields.forEach(function(suf){
284 var src = billPairs[suf];
285 if (!src) return;
286 var b = document.getElementById('co-bill-' + src);
287 var s = document.getElementById('co-ship-' + suf);
288 if (b && s) s.value = b.value;
289 });
290 // Pull ship region/country from the tax fields (which the
291 // billing tax flow already collects).
292 var sr = document.getElementById('co-ship-region');
293 if (sr && regionInput) sr.value = regionInput.value;
294 var sc = document.getElementById('co-ship-country');
295 if (sc && countrySel) sc.value = countrySel.value;
296 }
297 function applySameToggle() {
298 if (!sameToggle) return;
299 var same = sameToggle.checked;
300 shipFields.forEach(function(suf){
301 var el = document.getElementById('co-ship-' + suf);
302 if (el) el.disabled = same;
303 });
304 if (same) copyBillToShip();
305 }
306 if (sameToggle) {
307 sameToggle.addEventListener('change', applySameToggle);
308 ['name','addr1','addr2','city','postal','phone'].forEach(function(suf){
309 var el = document.getElementById('co-bill-' + suf);
310 if (el) el.addEventListener('input', function(){ if (sameToggle.checked) copyBillToShip(); });
311 });
312 if (regionInput) regionInput.addEventListener('input', function(){ if (sameToggle.checked) copyBillToShip(); });
313 if (countrySel) countrySel.addEventListener('change', function(){ if (sameToggle.checked) copyBillToShip(); });
314 applySameToggle();
315 }
316 var currentTaxCents = 0;
317 function refreshTax(){
318 var c = (countrySel.value || '').trim();
319 if (c === '' || c === 'OTHER') {
320 taxRow.style.display = 'none';
321 currentTaxCents = 0;
322 updateTotal(0);
323 return;
324 }
325 var r = (regionInput.value || '').trim().toUpperCase();
326 var v = (vatInput.value || '').trim().toUpperCase();
327 var url = '/_tax_preview.cgi?storefront_id=$store_id&bill_country=' + encodeURIComponent(c) + '&bill_region=' + encodeURIComponent(r) + '&vat_id=' + encodeURIComponent(v);
328 fetch(url, { credentials: 'same-origin' })
329 .then(function(rsp){ return rsp.json(); })
330 .then(function(j){
331 if (!j || !j.ok) return;
332 if (j.tax_cents > 0) {
333 var label = (j.lines && j.lines[0] && j.lines[0].label) ? j.lines[0].label : 'Tax';
334 taxLabel.textContent = label + ' (' + (j.lines[0].rate || '') + '%)';
335 taxOut.textContent = fmtCents(j.tax_cents);
336 taxRow.style.display = '';
337 } else {
338 taxRow.style.display = 'none';
339 }
340 currentTaxCents = j.tax_cents || 0;
341 updateTotal(currentTaxCents);
342 })
343 .catch(function(){});
344 }
345 function debouncedRefresh(){
346 if (previewTimer) clearTimeout(previewTimer);
347 previewTimer = setTimeout(refreshTax, 250);
348 }
349 var vatWrap = document.getElementById('co-vat-wrap');
350 var vatInput = document.getElementById('co-vat-id');
351 var vatStatus = document.getElementById('co-vat-status');
352 // EU member states + Northern Ireland VAT use VIES. UK proper
353 // (GBxxx) is also in VIES post-Brexit for NI numbers but most
354 // GB checks now go through HMRC -- the VIES API still answers,
355 // so we accept GB here too.
356 var VIES_COUNTRIES = {AT:1,BE:1,BG:1,HR:1,CY:1,CZ:1,DK:1,EE:1,FI:1,FR:1,DE:1,EL:1,GR:1,HU:1,IE:1,IT:1,LV:1,LT:1,LU:1,MT:1,NL:1,PL:1,PT:1,RO:1,SK:1,SI:1,ES:1,SE:1,GB:1};
357
358 countrySel.addEventListener('change', function(){
359 var c = countrySel.value;
360 if (c === 'US' || c === 'CA') {
361 regionWrap.style.display = '';
362 regionLabel.textContent = (c === 'US') ? 'State (e.g. CA, TX)' : 'Province (e.g. ON, BC)';
363 } else {
364 regionWrap.style.display = 'none';
365 regionInput.value = '';
366 }
367 // VAT-ID field only useful for VIES countries.
368 if (VIES_COUNTRIES[c]) {
369 vatWrap.style.display = '';
370 } else {
371 vatWrap.style.display = 'none';
372 vatInput.value = '';
373 vatStatus.textContent = '';
374 }
375 refreshTax();
376 });
377 regionInput.addEventListener('input', debouncedRefresh);
378
379 // VIES check fires on blur (avoid hammering the EU API on every keypress).
380 vatInput.addEventListener('blur', function(){
381 var v = (vatInput.value || '').replace(/[^A-Za-z0-9]/g,'').toUpperCase();
382 vatInput.value = v;
383 if (v.length < 4) { vatStatus.textContent = ''; return; }
384 vatStatus.textContent = 'Validating via VIES...';
385 var fd = new FormData();
386 fd.append('vat_id', v);
387 fd.append('email', (emailInput.value || '').trim());
388 fetch('/_vies_check.cgi', { method:'POST', body:fd, credentials:'same-origin' })
389 .then(function(r){ return r.json(); })
390 .then(function(j){
391 if (!j) { vatStatus.textContent = 'Check failed.'; return; }
392 if (j.valid) {
393 vatStatus.style.color = '#4ade80';
394 vatStatus.textContent = 'VAT valid' + (j.name && j.name !== '---' ? (' -- ' + j.name) : '') + '. Tax exempted under reverse-charge.';
395 refreshTax();
396 } else {
397 vatStatus.style.color = '#fca5a5';
398 vatStatus.textContent = 'Not valid in VIES' + (j.note ? (' (' + j.note + ')') : '') + '. Tax will be charged.';
399 refreshTax();
400 }
401 })
402 .catch(function(){ vatStatus.textContent = 'VIES check failed.'; });
403 });
404
405 function showErr(msg){
406 errBox.textContent = msg;
407 errBox.classList.add('show');
408 }
409 function hideErr(){
410 errBox.classList.remove('show');
411 }
412
413 // Step 1: ask the server for a PaymentIntent client_secret.
414 // We do this on first submit click rather than page load so
415 // abandoned-checkout intents don't pile up in Stripe.
416 function ensureIntent(email){
417 if (clientSecret) return Promise.resolve();
418 var fd = new FormData();
419 fd.append('storefront_id', '$store_id');
420 fd.append('email', email);
421 fd.append('bill_country', (countrySel.value || '').trim());
422 fd.append('bill_region', (regionInput.value || '').trim().toUpperCase());
423 fd.append('vat_id', (vatInput.value || '').trim().toUpperCase());
424 // Billing address (always collected for the card on file).
425 fd.append('bill_name', document.getElementById('co-bill-name').value || '');
426 fd.append('bill_addr1', document.getElementById('co-bill-addr1').value || '');
427 fd.append('bill_addr2', document.getElementById('co-bill-addr2').value || '');
428 fd.append('bill_city', document.getElementById('co-bill-city').value || '');
429 fd.append('bill_postal', document.getElementById('co-bill-postal').value || '');
430 fd.append('bill_phone', document.getElementById('co-bill-phone').value || '');
431 // Shipping fields: only sent when the address form is on the page.
432 var shipForm = document.getElementById('co-ship-name');
433 if (shipForm) {
434 var pick = document.querySelector('input[name="ship_zone"]:checked');
435 fd.append('ship_zone', pick ? pick.value : '');
436 fd.append('ship_cents', shipCents());
437 fd.append('ship_name', document.getElementById('co-ship-name').value || '');
438 fd.append('ship_addr1', document.getElementById('co-ship-addr1').value || '');
439 fd.append('ship_addr2', document.getElementById('co-ship-addr2').value || '');
440 fd.append('ship_city', document.getElementById('co-ship-city').value || '');
441 fd.append('ship_region', document.getElementById('co-ship-region').value || '');
442 fd.append('ship_postal', document.getElementById('co-ship-postal').value || '');
443 fd.append('ship_country', (document.getElementById('co-ship-country').value || '').toUpperCase());
444 fd.append('ship_phone', document.getElementById('co-ship-phone').value || '');
445 }
446 return fetch('/checkout_intent.cgi', { method: 'POST', body: fd })
447 .then(function(r){ return r.json(); })
448 .then(function(j){
449 if (!j.ok) throw new Error(j.error || 'Could not start checkout.');
450 // 100%-off promos (or zero-priced carts) come back with
451 // free=1 and a redirect URL. The order is already paid
452 // on the server -- skip Stripe Elements entirely and
453 // navigate straight to the completion page.
454 if (j.free) {
455 window.location = j.redirect || ('/checkout_complete.cgi?order=' + j.order_id);
456 // Resolve to a never-resolving promise so the rest of
457 // the submit pipeline (which calls stripe.confirmPayment)
458 // never runs after the redirect kicks off.
459 return new Promise(function(){});
460 }
461 clientSecret = j.client_secret;
462 orderId = j.order_id;
463 elements = stripe.elements({ clientSecret: clientSecret, appearance: { theme: 'night' } });
464 var pe = elements.create('payment');
465 pe.mount('#payment-element');
466 return new Promise(function(res){ setTimeout(res, 300); });
467 });
468 }
469
470 form.addEventListener('submit', function(e){
471 e.preventDefault();
472 hideErr();
473 var email = (emailInput.value || '').trim();
474 if (!email || email.indexOf('@') < 0) { showErr('Enter your email first.'); return; }
475 submit.disabled = true;
476 submit.textContent = 'Working...';
477
478 ensureIntent(email)
479 .then(function(){
480 return stripe.confirmPayment({
481 elements: elements,
482 confirmParams: {
483 return_url: window.location.origin + '/checkout_complete.cgi?order=' + orderId,
484 receipt_email: email
485 }
486 });
487 })
488 .then(function(result){
489 if (result && result.error) {
490 showErr(result.error.message || 'Payment failed.');
491 submit.disabled = false;
492 submit.textContent = 'Pay $total_display';
493 }
494 })
495 .catch(function(err){
496 showErr(err.message || String(err));
497 submit.disabled = false;
498 submit.textContent = 'Pay $total_display';
499 });
500 });
501 })();
502 </script>
503 [/if]
504
505 [if:$seller_not_ready]
506 <div class="co-coming">Checkout &middot; coming soon</div>
507 <h1>Almost there.</h1>
508 <p>
509 $store_name hasn't finished connecting payments yet. Your cart is saved -- as soon as $store_name
510 finishes onboarding with Stripe, you'll be able to complete this purchase right here.
511 </p>
512 <div style="margin-top:18px">
513 [loop:@items]
514 <div class="co-line"><span>$loop1.title<span class="qty">x$loop1.qty</span></span><span class="font-mono">$loop1.line_total</span></div>
515 [/loop]
516 <div class="co-total-row"><span>Total</span><span class="font-mono">$total_display</span></div>
517 </div>
518 <div style="margin-top:18px;display:flex;gap:10px;flex-wrap:wrap">
519 <a class="co-secondary" href="$cart_href">Review cart</a>
520 <a class="co-secondary" href="$back_href">Back to $store_name</a>
521 </div>
522 [/if]
523
524 [if:$platform_not_ready]
525 <div class="co-coming">Checkout &middot; offline</div>
526 <h1>Payments are temporarily unavailable.</h1>
527 <p>
528 WebSTLs is between deployments. Your cart is saved here -- come back in a few minutes and complete
529 your purchase. Sorry for the detour.
530 </p>
531 <div style="margin-top:18px;display:flex;gap:10px;flex-wrap:wrap">
532 <a class="co-secondary" href="$back_href">Back to $store_name</a>
533 </div>
534 [/if]
535 </div>
536 [/if]
537
538 [if:!$has_items]
539 <div class="co-card" style="text-align:center">
540 <h1>Your cart is empty</h1>
541 <p>Nothing to check out. Browse $store_name and add some designs first.</p>
542 <div style="margin-top:18px">
543 <a class="co-secondary" href="$back_href">Back to $store_name</a>
544 </div>
545 </div>
546 [/if]
547
548</main>
549</body>
550</html>
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help