wiki-plugin-shoppe 0.0.20 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,458 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <script src="https://js.stripe.com/v3/"></script>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
7
+ <meta name="twitter:title" content="{{title}}">
8
+ <meta name="description" content="{{description}}">
9
+ <meta name="og:title" content="{{title}}">
10
+ <meta name="og:description" content="{{description}}">
11
+ <title>Book: {{title}}</title>
12
+ <style>
13
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
14
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f12; color: white; min-height: 100vh; }
15
+
16
+ .layout { display: flex; min-height: 100vh; }
17
+ .product-panel { flex: 0 0 380px; background: #18181c; border-right: 1px solid #333; padding: 40px 32px; display: flex; flex-direction: column; gap: 20px; }
18
+ .booking-panel { flex: 1; padding: 40px 40px; overflow-y: auto; }
19
+
20
+ .product-img { width: 100%; border-radius: 12px; object-fit: cover; max-height: 260px; display: block; }
21
+ .product-title { font-size: 22px; font-weight: 700; }
22
+ .product-desc { font-size: 14px; color: #aaa; line-height: 1.5; }
23
+ .product-meta { display: flex; gap: 16px; flex-wrap: wrap; }
24
+ .meta-chip { background: #2a2a2e; border-radius: 20px; padding: 6px 14px; font-size: 13px; color: #ccc; }
25
+ .meta-chip span { color: #7ec8e3; font-weight: 600; }
26
+
27
+ h2 { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #ddd; }
28
+
29
+ /* Date strip */
30
+ .date-strip { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 8px; margin-bottom: 24px; scrollbar-width: thin; }
31
+ .date-strip::-webkit-scrollbar { height: 4px; }
32
+ .date-strip::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }
33
+ .date-card { flex: 0 0 72px; background: #2a2a2e; border: 2px solid #444; border-radius: 12px; padding: 10px 6px; text-align: center; cursor: pointer; transition: border-color 0.15s, background 0.15s; }
34
+ .date-card:hover { border-color: #7ec8e3; }
35
+ .date-card.active { border-color: #7ec8e3; background: #1a3040; }
36
+ .date-card .dow { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
37
+ .date-card .dom { font-size: 22px; font-weight: 700; margin: 2px 0; }
38
+ .date-card .mon { font-size: 11px; color: #aaa; }
39
+
40
+ /* Slot grid */
41
+ .slot-section { display: none; margin-bottom: 28px; }
42
+ .slot-section.visible { display: block; }
43
+ .slot-grid { display: flex; flex-wrap: wrap; gap: 8px; }
44
+ .slot-btn { background: #2a2a2e; border: 2px solid #444; border-radius: 8px; padding: 8px 16px; font-size: 14px; color: #ddd; cursor: pointer; transition: border-color 0.15s, background 0.15s; }
45
+ .slot-btn:hover { border-color: #7ec8e3; }
46
+ .slot-btn.active { border-color: #7ec8e3; background: #1a3040; color: white; }
47
+
48
+ /* Contact + recovery form */
49
+ .booking-form { display: none; background: #1e1e22; border: 1px solid #333; border-radius: 16px; padding: 28px; margin-bottom: 24px; }
50
+ .booking-form.visible { display: block; }
51
+ .selected-slot-display { background: #1a3040; border: 1px solid #7ec8e3; border-radius: 8px; padding: 10px 16px; font-size: 14px; color: #7ec8e3; margin-bottom: 20px; }
52
+ .field-group { margin-bottom: 16px; }
53
+ .field-group label { display: block; font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
54
+ .field-group input { width: 100%; background: #2a2a2e; border: 1px solid #444; border-radius: 8px; padding: 10px 14px; color: white; font-size: 14px; outline: none; transition: border-color 0.15s; }
55
+ .field-group input:focus { border-color: #7ec8e3; }
56
+ .field-group .hint { font-size: 11px; color: #666; margin-top: 4px; }
57
+ .recovery-note { background: #2a2a1a; border: 1px solid #665; border-radius: 8px; padding: 12px 16px; font-size: 13px; color: #cc9; margin-bottom: 20px; line-height: 1.4; }
58
+
59
+ /* Payment section */
60
+ .payment-section { display: none; }
61
+ .payment-section.visible { display: block; }
62
+ #payment-element { margin-bottom: 16px; }
63
+
64
+ .btn-primary { width: 100%; padding: 14px; background: linear-gradient(90deg, #0066cc, #7ec8e3); color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; transition: opacity 0.15s; }
65
+ .btn-primary:hover { opacity: 0.9; }
66
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
67
+ .btn-outline { width: 100%; padding: 12px; background: transparent; color: #7ec8e3; border: 1px solid #7ec8e3; border-radius: 10px; font-size: 14px; cursor: pointer; margin-bottom: 12px; }
68
+ .btn-outline:hover { background: #1a3040; }
69
+
70
+ .error-msg { color: #ff6b6b; font-size: 13px; margin-top: 8px; display: none; }
71
+ .loading-msg { color: #aaa; font-size: 13px; margin-top: 8px; display: none; }
72
+ .no-slots { color: #888; font-size: 14px; padding: 20px 0; }
73
+
74
+ /* Confirmation */
75
+ .confirmation { display: none; text-align: center; padding: 40px 20px; }
76
+ .confirmation.visible { display: block; }
77
+ .confirmation .icon { font-size: 64px; margin-bottom: 16px; }
78
+ .confirmation h2 { font-size: 24px; margin-bottom: 8px; }
79
+ .confirmation p { color: #aaa; margin-bottom: 6px; }
80
+ .confirmation .slot-confirmed { font-size: 18px; color: #7ec8e3; font-weight: 600; margin: 16px 0; }
81
+
82
+ @media (max-width: 700px) {
83
+ .layout { flex-direction: column; }
84
+ .product-panel { flex: none; border-right: none; border-bottom: 1px solid #333; padding: 24px 20px; }
85
+ .booking-panel { padding: 24px 20px; }
86
+ }
87
+ </style>
88
+ </head>
89
+ <body>
90
+ <div class="layout">
91
+ <!-- Left: product info -->
92
+ <div class="product-panel">
93
+ <script>
94
+ (function() {
95
+ const src = {{image}};
96
+ if (src) {
97
+ document.write('<img class="product-img" src="' + src + '" alt="">');
98
+ }
99
+ })();
100
+ </script>
101
+ <div class="product-title">{{title}}</div>
102
+ <div class="product-desc">{{description}}</div>
103
+ <div class="product-meta">
104
+ <div class="meta-chip">💰 <span>${{formattedAmount}}</span> / session</div>
105
+ <div class="meta-chip">⏱ <span>{{duration}} min</span></div>
106
+ <div class="meta-chip">🕐 <span id="tz-display">{{timezone}}</span></div>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Right: booking flow -->
111
+ <div class="booking-panel">
112
+
113
+ <!-- Date selection -->
114
+ <div id="step-dates">
115
+ <h2>Choose a date</h2>
116
+ <div id="date-strip" class="date-strip"></div>
117
+ <div id="loading-slots" class="loading-msg" style="display:block;">Loading availability…</div>
118
+ <div id="no-availability" class="no-slots" style="display:none;">No upcoming availability found.</div>
119
+ </div>
120
+
121
+ <!-- Time slot selection -->
122
+ <div id="step-slots" class="slot-section">
123
+ <h2 id="slot-heading">Available times</h2>
124
+ <div id="slot-grid" class="slot-grid"></div>
125
+ </div>
126
+
127
+ <!-- Contact info + recovery key form -->
128
+ <div id="booking-form" class="booking-form">
129
+ <div id="selected-slot-display" class="selected-slot-display"></div>
130
+
131
+ <div class="recovery-note">
132
+ 🔑 Choose a recovery key — a word or phrase only you know. You'll use it to look up your booking confirmation later.
133
+ </div>
134
+
135
+ <div class="field-group">
136
+ <label>Recovery Key *</label>
137
+ <input type="text" id="recovery-key" placeholder="e.g. sunflower-2026" autocomplete="off">
138
+ </div>
139
+ <div class="field-group">
140
+ <label>Your Name *</label>
141
+ <input type="text" id="contact-name" placeholder="Full name">
142
+ </div>
143
+ <div class="field-group">
144
+ <label>Email *</label>
145
+ <input type="email" id="contact-email" placeholder="For booking confirmation">
146
+ <div class="hint">Shared with the service provider only.</div>
147
+ </div>
148
+ <div class="field-group">
149
+ <label>Phone (optional)</label>
150
+ <input type="tel" id="contact-phone" placeholder="For appointment reminders">
151
+ <div class="hint">Optional. Shared with the service provider only.</div>
152
+ </div>
153
+ <button class="btn-outline" onclick="backToSlots()">← Change time</button>
154
+ <button class="btn-primary" id="proceed-to-pay" onclick="proceedToPay()">{{proceedLabel}}</button>
155
+ <div id="form-error" class="error-msg"></div>
156
+ </div>
157
+
158
+ <!-- Stripe payment -->
159
+ <div id="payment-section" class="payment-section">
160
+ <h2>Complete payment</h2>
161
+ <div id="selected-slot-payment" class="selected-slot-display" style="margin-bottom:16px;"></div>
162
+ <div id="payment-element"></div>
163
+ <button class="btn-primary" id="pay-btn" onclick="confirmPayment()">Pay ${{formattedAmount}}</button>
164
+ <div id="pay-error" class="error-msg"></div>
165
+ <div id="pay-loading" class="loading-msg"></div>
166
+ </div>
167
+
168
+ <!-- Confirmation -->
169
+ <div id="confirmation" class="confirmation">
170
+ <div class="icon">✅</div>
171
+ <h2>You're booked!</h2>
172
+ <div id="confirmation-slot" class="slot-confirmed"></div>
173
+ <p>{{title}}</p>
174
+ <p style="margin-top:16px;">A confirmation has been sent to the service provider.</p>
175
+ <p style="margin-top:8px; font-size:13px; color:#666;">Keep your recovery key safe — you can use it to look up this booking.</p>
176
+ <button class="btn-outline" style="margin-top:24px; max-width:200px;" onclick="window.location.href='{{shoppeUrl}}'">← Back to Shoppe</button>
177
+ </div>
178
+
179
+ </div>
180
+ </div>
181
+
182
+ <script>
183
+ (function() {
184
+ 'use strict';
185
+
186
+ const SHOPPE_URL = '{{shoppeUrl}}';
187
+ const PRODUCT_ID = '{{productId}}';
188
+ const TITLE = '{{title}}';
189
+ const AMOUNT = {{amount}};
190
+ const TIMEZONE = '{{timezone}}';
191
+
192
+ let availableDates = []; // [{ date, dayLabel, slots }]
193
+ let selectedDate = null;
194
+ let selectedSlot = null;
195
+ let stripe = null;
196
+ let elements = null;
197
+
198
+ // ── Slot loading ──────────────────────────────────────────────────────────
199
+
200
+ async function loadSlots() {
201
+ try {
202
+ const resp = await fetch(`${SHOPPE_URL}/book/${encodeURIComponent(TITLE)}/slots`);
203
+ const data = await resp.json();
204
+ document.getElementById('loading-slots').style.display = 'none';
205
+
206
+ if (!data.available || data.available.length === 0) {
207
+ document.getElementById('no-availability').style.display = 'block';
208
+ return;
209
+ }
210
+
211
+ availableDates = data.available;
212
+ renderDateStrip();
213
+ selectDate(availableDates[0]);
214
+ } catch (err) {
215
+ document.getElementById('loading-slots').textContent = 'Could not load availability.';
216
+ }
217
+ }
218
+
219
+ // ── Date strip ────────────────────────────────────────────────────────────
220
+
221
+ function renderDateStrip() {
222
+ const strip = document.getElementById('date-strip');
223
+ strip.innerHTML = '';
224
+ availableDates.forEach((d, i) => {
225
+ const parts = d.date.split('-');
226
+ const dateObj = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
227
+ const dow = dateObj.toLocaleDateString('en-US', { weekday: 'short' });
228
+ const mon = dateObj.toLocaleDateString('en-US', { month: 'short' });
229
+ const dom = dateObj.getDate();
230
+
231
+ const card = document.createElement('div');
232
+ card.className = 'date-card';
233
+ card.innerHTML = `<div class="dow">${dow}</div><div class="dom">${dom}</div><div class="mon">${mon}</div>`;
234
+ card.addEventListener('click', () => selectDate(d));
235
+ strip.appendChild(card);
236
+ });
237
+ }
238
+
239
+ function selectDate(dateData) {
240
+ selectedDate = dateData;
241
+ selectedSlot = null;
242
+
243
+ // Highlight active date card
244
+ document.querySelectorAll('.date-card').forEach((c, i) => {
245
+ c.classList.toggle('active', availableDates[i] === dateData);
246
+ });
247
+
248
+ // Show slot section
249
+ const slotSection = document.getElementById('step-slots');
250
+ slotSection.classList.add('visible');
251
+ document.getElementById('slot-heading').textContent = `Times on ${dateData.dayLabel}`;
252
+ renderSlotGrid(dateData.slots);
253
+
254
+ // Hide booking form and payment
255
+ document.getElementById('booking-form').classList.remove('visible');
256
+ document.getElementById('payment-section').classList.remove('visible');
257
+ }
258
+
259
+ // ── Slot grid ─────────────────────────────────────────────────────────────
260
+
261
+ function renderSlotGrid(slots) {
262
+ const grid = document.getElementById('slot-grid');
263
+ grid.innerHTML = '';
264
+ slots.forEach(slotStr => {
265
+ const [, time] = slotStr.split('T');
266
+ const [h, m] = time.split(':').map(Number);
267
+ const ampm = h >= 12 ? 'PM' : 'AM';
268
+ const h12 = h % 12 || 12;
269
+ const label = `${h12}:${m.toString().padStart(2,'0')} ${ampm}`;
270
+
271
+ const btn = document.createElement('button');
272
+ btn.className = 'slot-btn';
273
+ btn.textContent = label;
274
+ btn.addEventListener('click', () => selectSlot(slotStr, label));
275
+ grid.appendChild(btn);
276
+ });
277
+ }
278
+
279
+ function selectSlot(slotStr, label) {
280
+ selectedSlot = slotStr;
281
+ document.querySelectorAll('.slot-btn').forEach(b => b.classList.remove('active'));
282
+ event.currentTarget.classList.add('active');
283
+
284
+ const display = formatSlotDisplay(slotStr);
285
+ document.getElementById('selected-slot-display').textContent = `📅 ${display}`;
286
+ document.getElementById('booking-form').classList.add('visible');
287
+ document.getElementById('booking-form').scrollIntoView({ behavior: 'smooth', block: 'start' });
288
+ }
289
+
290
+ function formatSlotDisplay(slotStr) {
291
+ const [datePart, timePart] = slotStr.split('T');
292
+ const [y, mo, d] = datePart.split('-').map(Number);
293
+ const [h, m] = timePart.split(':').map(Number);
294
+ const dateObj = new Date(y, mo - 1, d);
295
+ const dateLabel = dateObj.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
296
+ const ampm = h >= 12 ? 'PM' : 'AM';
297
+ const h12 = h % 12 || 12;
298
+ const timeLabel = `${h12}:${m.toString().padStart(2,'0')} ${ampm} ${TIMEZONE}`;
299
+ return `${dateLabel} at ${timeLabel}`;
300
+ }
301
+
302
+ // ── Navigation ────────────────────────────────────────────────────────────
303
+
304
+ window.backToSlots = function() {
305
+ document.getElementById('booking-form').classList.remove('visible');
306
+ document.getElementById('payment-section').classList.remove('visible');
307
+ selectedSlot = null;
308
+ document.querySelectorAll('.slot-btn').forEach(b => b.classList.remove('active'));
309
+ };
310
+
311
+ // ── Proceed to payment ────────────────────────────────────────────────────
312
+
313
+ window.proceedToPay = async function() {
314
+ const recoveryKey = document.getElementById('recovery-key').value.trim();
315
+ const name = document.getElementById('contact-name').value.trim();
316
+ const email = document.getElementById('contact-email').value.trim();
317
+ const errorEl = document.getElementById('form-error');
318
+
319
+ if (!recoveryKey) { showFormError('Recovery key is required.'); return; }
320
+ if (!name) { showFormError('Name is required.'); return; }
321
+ if (!email) { showFormError('Email is required.'); return; }
322
+ if (!selectedSlot){ showFormError('Please select a time slot.'); return; }
323
+ errorEl.style.display = 'none';
324
+
325
+ document.getElementById('proceed-to-pay').disabled = true;
326
+
327
+ try {
328
+ const resp = await fetch(`${SHOPPE_URL}/purchase/intent`, {
329
+ method: 'POST',
330
+ headers: { 'Content-Type': 'application/json' },
331
+ body: JSON.stringify({ recoveryKey, productId: PRODUCT_ID, title: TITLE, slotDatetime: selectedSlot })
332
+ });
333
+ const data = await resp.json();
334
+
335
+ if (data.error) {
336
+ showFormError(data.error);
337
+ document.getElementById('proceed-to-pay').disabled = false;
338
+ return;
339
+ }
340
+
341
+ if (data.free) {
342
+ // Free booking — skip Stripe and record directly
343
+ const contactInfo = {
344
+ name: document.getElementById('contact-name').value.trim(),
345
+ email: document.getElementById('contact-email').value.trim(),
346
+ phone: document.getElementById('contact-phone').value.trim()
347
+ };
348
+ await fetch(`${SHOPPE_URL}/purchase/complete`, {
349
+ method: 'POST',
350
+ headers: { 'Content-Type': 'application/json' },
351
+ body: JSON.stringify({ recoveryKey, productId: PRODUCT_ID, title: TITLE, slotDatetime: selectedSlot, contactInfo })
352
+ });
353
+ document.getElementById('booking-form').classList.remove('visible');
354
+ showConfirmation();
355
+ return;
356
+ }
357
+
358
+ // Show Stripe payment section
359
+ document.getElementById('booking-form').classList.remove('visible');
360
+ const paySection = document.getElementById('payment-section');
361
+ paySection.classList.add('visible');
362
+ document.getElementById('selected-slot-payment').textContent = `📅 ${formatSlotDisplay(selectedSlot)}`;
363
+
364
+ stripe = Stripe(data.publishableKey);
365
+ elements = stripe.elements({ clientSecret: data.clientSecret });
366
+ elements.create('payment').mount('#payment-element');
367
+
368
+ paySection.scrollIntoView({ behavior: 'smooth', block: 'start' });
369
+ } catch (err) {
370
+ showFormError('Could not start checkout. Please try again.');
371
+ document.getElementById('proceed-to-pay').disabled = false;
372
+ }
373
+ };
374
+
375
+ // ── Confirm payment ───────────────────────────────────────────────────────
376
+
377
+ window.confirmPayment = async function() {
378
+ const payBtn = document.getElementById('pay-btn');
379
+ const payError = document.getElementById('pay-error');
380
+ const payLoading = document.getElementById('pay-loading');
381
+
382
+ payBtn.disabled = true;
383
+ payLoading.style.display = 'block';
384
+ payLoading.textContent = 'Processing…';
385
+ payError.style.display = 'none';
386
+
387
+ try {
388
+ const { error } = await stripe.confirmPayment({
389
+ elements,
390
+ confirmParams: { return_url: SHOPPE_URL },
391
+ redirect: 'if_required'
392
+ });
393
+
394
+ if (error) {
395
+ payError.textContent = error.message;
396
+ payError.style.display = 'block';
397
+ payBtn.disabled = false;
398
+ payLoading.style.display = 'none';
399
+ return;
400
+ }
401
+
402
+ // Payment succeeded — record booking server-side (contact info never goes direct to Sanora)
403
+ const recoveryKey = document.getElementById('recovery-key').value.trim();
404
+ const contactInfo = {
405
+ name: document.getElementById('contact-name').value.trim(),
406
+ email: document.getElementById('contact-email').value.trim(),
407
+ phone: document.getElementById('contact-phone').value.trim()
408
+ };
409
+
410
+ await fetch(`${SHOPPE_URL}/purchase/complete`, {
411
+ method: 'POST',
412
+ headers: { 'Content-Type': 'application/json' },
413
+ body: JSON.stringify({
414
+ recoveryKey,
415
+ productId: PRODUCT_ID,
416
+ title: TITLE,
417
+ slotDatetime: selectedSlot,
418
+ contactInfo
419
+ })
420
+ });
421
+
422
+ showConfirmation();
423
+ } catch (err) {
424
+ payError.textContent = 'An unexpected error occurred.';
425
+ payError.style.display = 'block';
426
+ payBtn.disabled = false;
427
+ payLoading.style.display = 'none';
428
+ console.warn('payment error:', err);
429
+ }
430
+ };
431
+
432
+ // ── Confirmation screen ───────────────────────────────────────────────────
433
+
434
+ function showConfirmation() {
435
+ document.getElementById('payment-section').classList.remove('visible');
436
+ document.getElementById('step-dates').style.display = 'none';
437
+ document.getElementById('step-slots').classList.remove('visible');
438
+
439
+ const conf = document.getElementById('confirmation');
440
+ conf.classList.add('visible');
441
+ document.getElementById('confirmation-slot').textContent = formatSlotDisplay(selectedSlot);
442
+ conf.scrollIntoView({ behavior: 'smooth' });
443
+ }
444
+
445
+ // ── Helpers ───────────────────────────────────────────────────────────────
446
+
447
+ function showFormError(msg) {
448
+ const el = document.getElementById('form-error');
449
+ el.textContent = msg;
450
+ el.style.display = 'block';
451
+ }
452
+
453
+ // Boot
454
+ loadSlots();
455
+ })();
456
+ </script>
457
+ </body>
458
+ </html>
@@ -454,11 +454,16 @@
454
454
  };
455
455
 
456
456
  function handleSubmit(formData) {
457
- console.log('Form submitted:', formData);
458
- // Do whatever you want with the form data
459
- window.formData = formData;
460
- document.getElementById('payment-form').style.display = 'block';
461
- window.addPaymentForm();
457
+ const address = {
458
+ name: formData['Name'],
459
+ line1: formData['Address 1'],
460
+ line2: formData['Address 2'] || '',
461
+ city: formData['City'],
462
+ state: formData['State'],
463
+ zip: formData['Zip Code']
464
+ };
465
+ window.pendingAddress = address;
466
+ window.addPaymentForm(address);
462
467
  }
463
468
 
464
469
  const form = getForm(formConfig, handleSubmit);
@@ -468,83 +473,79 @@
468
473
  <script type="text/javascript">
469
474
  let stripe;
470
475
  let elements;
471
- let response;
476
+ let orderRef;
472
477
 
473
478
  const paymentForm = document.getElementById('payment-form');
474
479
  const submitButton = document.getElementById('submit-button');
475
480
  const errorMessage = document.getElementById('error-message');
476
481
  const loadingMessage = document.getElementById('loading');
477
482
 
478
- async function getPaymentIntentWithoutSplits(amount, currency) {
483
+ // Request a Stripe payment intent from the shoppe server.
484
+ // Address is held in memory here and only sent to the server after payment succeeds.
485
+ async function requestPaymentIntent(address) {
479
486
  try {
480
- const payload = {
481
- timestamp: new Date().getTime() + '',
482
- amount: {{amount}},
483
- currency: 'USD',
484
- payees: {{payees}}
485
- };
486
-
487
- const res = await fetch(`{{allyabaseOrigin}}/plugin/allyabase/addie/processor/stripe/intent`, {
488
- method: 'put',
489
- body: JSON.stringify(payload),
490
- headers: {'Content-Type': 'application/json'}
487
+ const res = await fetch('{{shoppeUrl}}/purchase/intent', {
488
+ method: 'POST',
489
+ headers: {'Content-Type': 'application/json'},
490
+ body: JSON.stringify({
491
+ productId: '{{productId}}',
492
+ title: '{{title}}'
493
+ })
491
494
  });
492
495
 
493
- const response = await res.json();
494
- console.log('got intent response', response);
496
+ const data = await res.json();
497
+ if (data.error) { showError(data.error); return; }
495
498
 
496
- stripe = Stripe(response.publishableKey);
497
- elements = stripe.elements({
498
- clientSecret: response.paymentIntent
499
- });
499
+ orderRef = data.orderRef;
500
500
 
501
+ stripe = Stripe(data.publishableKey);
502
+ elements = stripe.elements({ clientSecret: data.clientSecret });
501
503
  const paymentElement = elements.create('payment');
502
504
  paymentElement.mount('#payment-element');
505
+ document.getElementById('payment-form').style.display = 'block';
503
506
  } catch(err) {
504
- console.warn(err);
505
- }
506
- };
507
+ showError('Could not start checkout. Please try again.');
508
+ console.warn(err);
509
+ }
510
+ }
507
511
 
512
+ // Called after Stripe confirms payment. Sends address to the shoppe server which
513
+ // records the order in Sanora — address never goes directly from browser to Sanora.
508
514
  window.confirmPayment = async () => {
509
- const order = {
510
- title: "{{title}}",
511
- productId: "{{productId}}",
512
- formData
513
- };
514
- await fetch('{{sanoraUrl}}/user/orders', {
515
- method: 'PUT',
516
- headers: {'Content-Type': 'application/json'},
517
- body: JSON.stringify({timestamp: new Date().getTime() + '', order})
518
- });
519
515
  try {
520
516
  const { error } = await stripe.confirmPayment({
521
- elements,
522
- confirmParams: {
523
- return_url: '{{shoppeUrl}}',
524
- },
517
+ elements,
518
+ confirmParams: { return_url: '{{shoppeUrl}}' },
519
+ redirect: 'if_required'
525
520
  });
526
521
 
527
- if(error) {
528
- showError(error.message);
529
- }
522
+ if (error) { showError(error.message); return; }
523
+
524
+ // Payment succeeded — now record the order server-side with the shipping address
525
+ await fetch('{{shoppeUrl}}/purchase/complete', {
526
+ method: 'POST',
527
+ headers: {'Content-Type': 'application/json'},
528
+ body: JSON.stringify({
529
+ orderRef,
530
+ productId: '{{productId}}',
531
+ title: '{{title}}',
532
+ amount: {{amount}},
533
+ address: window.pendingAddress
534
+ })
535
+ });
536
+
537
+ window.location.href = '{{shoppeUrl}}';
530
538
  } catch(err) {
531
539
  showError('An unexpected error occurred.');
532
- console.warn('payment error: ', err);
540
+ console.warn('payment error: ', err);
533
541
  }
534
542
  };
535
543
 
536
544
  paymentForm.addEventListener('submit', async (event) => {
537
545
  event.preventDefault();
538
-
539
- if (!stripe || !elements) {
540
- return;
541
- }
542
-
543
- // Disable payment form submission while processing
546
+ if (!stripe || !elements) return;
544
547
  setLoading(true);
545
-
546
548
  await window.confirmPayment();
547
-
548
549
  setLoading(false);
549
550
  });
550
551
 
@@ -562,11 +563,7 @@ console.warn('payment error: ', err);
562
563
  loadingMessage.style.display = isLoading ? 'block' : 'none';
563
564
  };
564
565
 
565
- const start = () => {
566
- getPaymentIntentWithoutSplits({{amount}}, 'USD');
567
- };
568
-
569
- window.addPaymentForm = start;
566
+ window.addPaymentForm = requestPaymentIntent;
570
567
  </script>
571
568
  </body>
572
569
  </html>