Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_checkout.html
Diff

/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_checkout.html

added on local at 2026-07-01 13:48:01

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