wiki-plugin-shoppe 0.0.12 → 0.0.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-shoppe",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Multi-tenant digital goods shoppe for federated wiki, powered by Sanora",
5
5
  "keywords": [
6
6
  "wiki",
package/server/server.js CHANGED
@@ -9,6 +9,19 @@ const sessionless = require('sessionless-node');
9
9
 
10
10
  const SHOPPE_BASE_EMOJI = process.env.SHOPPE_BASE_EMOJI || '🛍️🎨🎁';
11
11
 
12
+ const TEMPLATES_DIR = path.join(__dirname, 'templates');
13
+ const RECOVER_STRIPE_TMPL = fs.readFileSync(path.join(TEMPLATES_DIR, 'generic-recover-stripe.html'), 'utf8');
14
+ const ADDRESS_STRIPE_TMPL = fs.readFileSync(path.join(TEMPLATES_DIR, 'generic-address-stripe.html'), 'utf8');
15
+
16
+ function getAllyabaseOrigin() {
17
+ try { return new URL(getSanoraUrl()).origin; } catch { return getSanoraUrl(); }
18
+ }
19
+
20
+ function fillTemplate(tmpl, vars) {
21
+ return Object.entries(vars).reduce((html, [k, v]) =>
22
+ html.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), v), tmpl);
23
+ }
24
+
12
25
  const DATA_DIR = path.join(process.env.HOME || '/root', '.shoppe');
13
26
  const TENANTS_FILE = path.join(DATA_DIR, 'tenants.json');
14
27
  const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
@@ -681,7 +694,13 @@ async function getShoppeGoods(tenant) {
681
694
  image: product.image ? `${getSanoraUrl()}/images/${product.image}` : null,
682
695
  url: isPost
683
696
  ? `/plugin/shoppe/${tenant.uuid}/post/${encodeURIComponent(title)}`
684
- : `${getSanoraUrl()}/products/${tenant.uuid}/${encodeURIComponent(title)}`
697
+ : product.category === 'book'
698
+ ? `/plugin/shoppe/${tenant.uuid}/buy/${encodeURIComponent(title)}`
699
+ : product.category === 'product' && product.shipping > 0
700
+ ? `/plugin/shoppe/${tenant.uuid}/buy/${encodeURIComponent(title)}/address`
701
+ : product.category === 'product'
702
+ ? `/plugin/shoppe/${tenant.uuid}/buy/${encodeURIComponent(title)}`
703
+ : `${getSanoraUrl()}/products/${tenant.uuid}/${encodeURIComponent(title)}`
685
704
  };
686
705
  const CATEGORY_BUCKET = { book: 'books', music: 'music', post: 'posts', 'post-series': 'posts', album: 'albums', product: 'products' };
687
706
  const bucket = goods[CATEGORY_BUCKET[product.category]];
@@ -702,7 +721,7 @@ function renderCards(items, category) {
702
721
  ? `<div class="card-img"><img src="${item.image}" alt="" loading="lazy"></div>`
703
722
  : `<div class="card-img-placeholder">${CATEGORY_EMOJI[category] || '🎁'}</div>`;
704
723
  const priceHtml = (item.price > 0 || category === 'product')
705
- ? `<div class="price">$${item.price}${item.shipping ? ` <span class="shipping">+ $${item.shipping} shipping</span>` : ''}</div>`
724
+ ? `<div class="price">$${(item.price / 100).toFixed(2)}${item.shipping ? ` <span class="shipping">+ $${(item.shipping / 100).toFixed(2)} shipping</span>` : ''}</div>`
706
725
  : '';
707
726
  return `
708
727
  <div class="card" onclick="window.open('${item.url}','_blank')">
@@ -926,6 +945,54 @@ async function startServer(params) {
926
945
  res.json({ success: true });
927
946
  });
928
947
 
948
+ // Purchase pages — shoppe-hosted versions of the Sanora payment templates
949
+ async function renderPurchasePage(req, res, templateHtml) {
950
+ try {
951
+ const tenant = getTenantByIdentifier(req.params.identifier);
952
+ if (!tenant) return res.status(404).send('<h1>Shoppe not found</h1>');
953
+
954
+ const title = decodeURIComponent(req.params.title);
955
+ const sanoraUrl = getSanoraUrl();
956
+ const productsResp = await fetch(`${sanoraUrl}/products/${tenant.uuid}`);
957
+ const products = await productsResp.json();
958
+ const product = products[title] || Object.values(products).find(p => p.title === title);
959
+ if (!product) return res.status(404).send('<h1>Product not found</h1>');
960
+
961
+ const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
962
+ const ebookUrl = `${sanoraUrl}/products/${tenant.uuid}/${encodeURIComponent(title)}/ebook-download`;
963
+ const shoppeUrl = `${req.protocol}://${req.get('host')}/plugin/shoppe/${tenant.uuid}`;
964
+
965
+ const html = fillTemplate(templateHtml, {
966
+ title: product.title || title,
967
+ description: product.description || '',
968
+ image: `"${imageUrl}"`,
969
+ amount: String(product.price || 0),
970
+ formattedAmount: ((product.price || 0) / 100).toFixed(2),
971
+ productId: product.productId || '',
972
+ pubKey: '',
973
+ signature: '',
974
+ sanoraUrl,
975
+ allyabaseOrigin: getAllyabaseOrigin(),
976
+ ebookUrl,
977
+ shoppeUrl
978
+ });
979
+
980
+ res.set('Content-Type', 'text/html');
981
+ res.send(html);
982
+ } catch (err) {
983
+ console.error('[shoppe] purchase page error:', err);
984
+ res.status(500).send(`<h1>Error</h1><p>${err.message}</p>`);
985
+ }
986
+ }
987
+
988
+ // Books + no-shipping products → recovery key + stripe
989
+ app.get('/plugin/shoppe/:identifier/buy/:title', (req, res) =>
990
+ renderPurchasePage(req, res, RECOVER_STRIPE_TMPL));
991
+
992
+ // Physical products with shipping → address + stripe
993
+ app.get('/plugin/shoppe/:identifier/buy/:title/address', (req, res) =>
994
+ renderPurchasePage(req, res, ADDRESS_STRIPE_TMPL));
995
+
929
996
  // Post reader — fetches markdown from Sanora and renders it as HTML
930
997
  app.get('/plugin/shoppe/:identifier/post/:title', async (req, res) => {
931
998
  try {
@@ -0,0 +1,572 @@
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.00, maximum-scale=1.00, minimum-scale=1.00">
7
+
8
+ <!--these are the tags that create the web preview card. You can use og:, or twitter: tags or both-->
9
+ <meta name="twitter:title" content="{{title}}">
10
+ <meta name="description" content="{{description}}">
11
+ <meta name="twitter:description" content="{{description}}">
12
+ <meta name="twitter:image" content={{image}}>
13
+
14
+ <meta name="og:title" content="{{title}}">
15
+ <meta name="og:description" content="{{description}}">
16
+ <meta name="og:image" content={{image}}>
17
+
18
+ <title>{{title}}</title>
19
+ </head>
20
+ <body style="margin: 0; padding: 0; font-family: Arial, sans-serif; height: 100vh; overflow: hidden; background: #0f0f12;">
21
+ <div id="main-container" style="
22
+ width: 100vw;
23
+ height: 100vh;
24
+ display: flex;
25
+ overflow: hidden;
26
+ flex-direction: row;
27
+ ">
28
+ <!-- Product Section -->
29
+ <div id="product-section" style="
30
+ flex: 1;
31
+ display: flex;
32
+ flex-direction: column;
33
+ justify-content: center;
34
+ align-items: center;
35
+ padding: 20px;
36
+ box-sizing: border-box;
37
+ min-height: 0;
38
+ overflow: hidden;
39
+ ">
40
+ <teleport pubKey="{{pubKey}}" signature="{{signature}}" message="{{title}}{{description}}{{amount}}" spell="sanora-club" amount="{{amount}}">
41
+ <div style="width: 100%; max-width: 500px; text-align: center; color: white;">
42
+ <img id="product-image" style="width: 100%; height: auto; max-height: 40vh; object-fit: contain; border-radius: 8px;"></img>
43
+ <script type="text/javascript">
44
+ document.getElementById('product-image').src = {{image}};
45
+ </script>
46
+ <h2 style="margin: 20px 0 10px 0; font-size: clamp(1.2rem, 4vw, 2rem);">{{title}}</h2>
47
+ <h4 style="margin: 10px 0; opacity: 0.8; font-weight: normal; font-size: clamp(0.9rem, 2.5vw, 1.1rem);">{{description}}</h4>
48
+ </div>
49
+ </teleport>
50
+ </div>
51
+
52
+ <!-- Forms Section -->
53
+ <div id="forms-section" style="
54
+ flex: 1;
55
+ display: flex;
56
+ flex-direction: column;
57
+ justify-content: center;
58
+ align-items: center;
59
+ padding: 20px;
60
+ box-sizing: border-box;
61
+ min-height: 0;
62
+ overflow: auto;
63
+ ">
64
+ <div id="address-form" style="width: 100%; max-width: 500px; margin-bottom: 20px;"></div>
65
+
66
+ <form id="payment-form" class="payment-form" style="
67
+ width: 100%;
68
+ max-width: 500px;
69
+ background: #2a2a2e;
70
+ padding: 20px;
71
+ border-radius: 15px;
72
+ border: 1px solid #444;
73
+ display: none;
74
+ ">
75
+ <div id="payment-element" style="margin-bottom: 20px;"></div>
76
+ <button id="submit-button" style="
77
+ width: 100%;
78
+ padding: 15px;
79
+ background: linear-gradient(90deg, green, purple);
80
+ color: white;
81
+ border: none;
82
+ border-radius: 22.5px;
83
+ font-size: 16px;
84
+ font-weight: bold;
85
+ cursor: pointer;
86
+ ">Pay now</button>
87
+ <div id="error-message" class="error-message" style="display: none; color: #ff6b6b; margin-top: 10px; text-align: center;"></div>
88
+ <div id="loading" class="loading" style="display: none; color: white; margin-top: 10px; text-align: center;">Processing payment...</div>
89
+ </form>
90
+ </div>
91
+ </div>
92
+
93
+ <script type="text/javascript">
94
+ // Responsive layout logic
95
+ function updateLayout() {
96
+ const mainContainer = document.getElementById('main-container');
97
+ const productSection = document.getElementById('product-section');
98
+ const formsSection = document.getElementById('forms-section');
99
+
100
+ if (window.innerWidth > window.innerHeight) {
101
+ // Landscape: side by side
102
+ mainContainer.style.flexDirection = 'row';
103
+ productSection.style.flex = '1';
104
+ formsSection.style.flex = '1';
105
+ mainContainer.style.overflow = 'hidden';
106
+ } else {
107
+ // Portrait: vertical layout, always show 2 elements
108
+ mainContainer.style.flexDirection = 'column';
109
+ productSection.style.flex = '1';
110
+ formsSection.style.flex = '1';
111
+ mainContainer.style.overflow = 'hidden';
112
+ }
113
+ }
114
+
115
+ // Initial layout
116
+ updateLayout();
117
+
118
+ // Update on resize
119
+ window.addEventListener('resize', updateLayout);
120
+ </script>
121
+
122
+ <script type="text/javascript">
123
+ (function(window) {
124
+ 'use strict';
125
+
126
+ function calculateFormHeight(formConfig) {
127
+ const fieldCount = Object.keys(formConfig).filter(key => key !== 'form').length;
128
+ const headerHeight = 85;
129
+ const fieldHeight = 70;
130
+ const submitButtonHeight = 45;
131
+ const bottomPadding = 30;
132
+
133
+ return headerHeight + (fieldCount * fieldHeight) + submitButtonHeight + bottomPadding;
134
+ }
135
+
136
+ function getBackgroundAndGradients(formConfig) {
137
+ const dynamicHeight = calculateFormHeight(formConfig);
138
+
139
+ const svg = `<rect width="500" height="${dynamicHeight}" fill="transparent"/>
140
+
141
+ <!-- Form Container with Metallic Background -->
142
+ <linearGradient id="metallicBackground" x1="0%" y1="0%" x2="100%" y2="100%">
143
+ <stop offset="0%" stop-color="#2a2a2e"/>
144
+ <stop offset="50%" stop-color="#323236"/>
145
+ <stop offset="100%" stop-color="#2a2a2e"/>
146
+ </linearGradient>
147
+ <rect x="50" y="50" width="400" height="${dynamicHeight}" rx="15" fill="url(#metallicBackground)"
148
+ stroke="#444" stroke-width="1"/>
149
+
150
+ <!-- Subtle Metallic Highlight -->
151
+ <line x1="51" y1="52" x2="449" y2="52" stroke="#555" stroke-width="1" opacity="0.5"/>
152
+
153
+ <!-- Form Header -->
154
+ <text x="250" y="85" font-family="Arial, sans-serif" font-size="24" font-weight="bold"
155
+ fill="#ffffff" text-anchor="middle">US SHIPPING ADDRESS</text>
156
+
157
+ <!-- Define the gradient for active input borders -->
158
+ <linearGradient id="inputGradient" x1="0%" y1="0%" x2="100%" y2="0%">
159
+ <stop offset="0%" stop-color="purple"/>
160
+ <stop offset="100%" stop-color="green"/>
161
+ </linearGradient>
162
+
163
+ <!-- Button Gradient -->
164
+ <linearGradient id="buttonGradient" x1="0%" y1="0%" x2="100%" y2="0%">
165
+ <stop id="submitButtonGradientStart" offset="0%" stop-color="green"/>
166
+ <stop id="submitButtonGradientEnd" offset="100%" stop-color="purple"/>
167
+ </linearGradient>
168
+
169
+ <linearGradient id="buttonPressedGradient" x1="0%" y1="0%" x2="100%" y2="0%">
170
+ <stop offset="0%" stop-color="purple"/>
171
+ <stop offset="100%" stop-color="green"/>
172
+ </linearGradient>`;
173
+
174
+ return svg;
175
+ }
176
+
177
+ function getSegmentedSelector(x, y, text, options) {
178
+ const borderId = `${text.replace(/\s+/g, '')}Border`;
179
+ const selectorId = `${text.replace(/\s+/g, '')}Selector`;
180
+
181
+ const segmentWidth = 340 / options.length;
182
+
183
+ let segments = '';
184
+ let segmentTexts = '';
185
+
186
+ options.forEach((option, index) => {
187
+ const segmentX = x + (index * segmentWidth);
188
+ const segmentId = `${selectorId}_${index}`;
189
+
190
+ segments += `<rect id="${segmentId}" x="${segmentX}" y="${y + 10}" width="${segmentWidth}" height="40" rx="${index === 0 ? '8 0 0 8' : index === options.length - 1 ? '0 8 8 0' : '0'}" fill="#1c1c20" stroke="#444" stroke-width="1" style="cursor: pointer;" data-option="${option}" data-selected="false"/>`;
191
+
192
+ segmentTexts += `<text x="${segmentX + segmentWidth/2}" y="${y + 33}" font-family="Arial, sans-serif" font-size="14" fill="#bbbbbb" text-anchor="middle" style="pointer-events: none;">${option}</text>`;
193
+ });
194
+
195
+ const svg = `<text x="${x}" y="${y}" font-family="Arial, sans-serif" font-size="14" fill="#bbbbbb">${text}</text>
196
+ <!-- Segmented Container -->
197
+ <rect id="${borderId}" x="${x}" y="${y + 10}" width="340" height="40" rx="8" fill="transparent" stroke="#444" stroke-width="2"/>
198
+ ${segments}
199
+ ${segmentTexts}
200
+ <!-- Hidden input to store value -->
201
+ <foreignObject x="${x}" y="${y + 55}" width="1" height="1" style="overflow: hidden;">
202
+ <input xmlns="http://www.w3.org/1999/xhtml" id="${selectorId}" type="hidden" data-field="${text}"/>
203
+ </foreignObject>`;
204
+
205
+ return svg;
206
+ }
207
+
208
+ function getInput(x, y, text, inputType) {
209
+ const borderId = `${text.replace(/\s+/g, '')}Border`;
210
+ const inputId = `${text.replace(/\s+/g, '')}Input`;
211
+
212
+ const svg = `<text x="${x}" y="${y}" font-family="Arial, sans-serif" font-size="14" fill="#bbbbbb">${text}</text>
213
+ <!-- Field Background -->
214
+ <rect id="${borderId}" x="${x}" y="${y + 10}" width="340" height="40" rx="8" fill="#1c1c20"
215
+ stroke="#444" stroke-width="2" class="input-field"/>
216
+ <!-- Inset shadow effect -->
217
+ <rect x="${x + 2}" y="${y + 12}" width="336" height="36" rx="6" fill="none"
218
+ stroke="#000" stroke-width="1" opacity="0.3"/>
219
+ <!-- HTML Input Field -->
220
+ <foreignObject x="${x + 5}" y="${y + 15}" width="330" height="30">
221
+ <input xmlns="http://www.w3.org/1999/xhtml" id="${inputId}" type="text" placeholder="Enter ${text.toLowerCase()}" data-field="${text}" spellcheck="false" style="width:100%; height: 100%; background-color: transparent; color: white; border: none; outline: none; padding: 8px 12px; font-size: 14px; font-family: Arial, sans-serif; border-radius: 6px;"/>
222
+ </foreignObject>`;
223
+
224
+ return svg;
225
+ }
226
+
227
+ function getSubmitButton(x, y) {
228
+ return `<rect id="submitButton" x="${x}" y="${y}" width="300" height="45" rx="22.5" fill="#666666" style="cursor: not-allowed;">
229
+ </rect>
230
+ <text x="${x + 150}" y="${y + 28}" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#999999" text-anchor="middle" dominant-baseline="middle" style="pointer-events: none;">SUBMIT</text>
231
+ `;
232
+ }
233
+
234
+ function enableSubmitButton() {
235
+ const submitButton = document.getElementById('submitButton');
236
+ const submitButtonText = submitButton.nextElementSibling;
237
+
238
+ submitButton.setAttribute('fill', 'url(#buttonGradient)');
239
+ submitButton.setAttribute('style', 'cursor: pointer;');
240
+ submitButtonText.setAttribute('fill', 'white');
241
+ }
242
+
243
+ function disableSubmitButton() {
244
+ const submitButton = document.getElementById('submitButton');
245
+ const submitButtonText = submitButton.nextElementSibling;
246
+
247
+ submitButton.setAttribute('fill', '#666666');
248
+ submitButton.setAttribute('style', 'cursor: not-allowed;');
249
+ submitButtonText.setAttribute('fill', '#999999');
250
+ }
251
+
252
+ function validateForm(formJSON) {
253
+ const requiredFields = Object.keys(formJSON).filter(key =>
254
+ key !== 'form' && formJSON[key].required
255
+ );
256
+
257
+ const allValid = requiredFields.every(key => {
258
+ const fieldConfig = formJSON[key];
259
+
260
+ if (fieldConfig.type === 'segmented') {
261
+ const selectorId = `${key.replace(/\s+/g, '')}Selector`;
262
+ const selector = document.getElementById(selectorId);
263
+ return selector && selector.value && selector.value.trim() !== '';
264
+ } else {
265
+ const inputId = `${key.replace(/\s+/g, '')}Input`;
266
+ const input = document.getElementById(inputId);
267
+ return input && input.value && input.value.trim() !== '';
268
+ }
269
+ });
270
+
271
+ if (allValid) {
272
+ enableSubmitButton();
273
+ } else {
274
+ disableSubmitButton();
275
+ }
276
+
277
+ return allValid;
278
+ }
279
+
280
+ function getForm(formJSON, onSubmit) {
281
+ const keys = Object.keys(formJSON);
282
+ const inputs = keys.map((key, index) => {
283
+ if (key === "form") return '';
284
+
285
+ const fieldConfig = formJSON[key];
286
+ if (fieldConfig.type === 'segmented') {
287
+ return getSegmentedSelector(80, 70 * index + 130, key, fieldConfig.options);
288
+ } else {
289
+ return getInput(80, 70 * index + 130, key, fieldConfig.type);
290
+ }
291
+ });
292
+
293
+ const svg = getBackgroundAndGradients(formJSON) + inputs.join('') + getSubmitButton(100, 70 * (keys.length) + 130);
294
+
295
+ const dynamicHeight = calculateFormHeight(formJSON);
296
+
297
+ const container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
298
+ container.setAttribute('viewBox', `0 0 500 ${dynamicHeight + 100}`);
299
+ container.setAttribute('width', '100%');
300
+ container.setAttribute('height', 'auto');
301
+ container.innerHTML = svg;
302
+
303
+ setTimeout(() => {
304
+ Object.keys(formJSON).forEach((key, index) => {
305
+ if(key === 'form') {
306
+ return;
307
+ }
308
+
309
+ const fieldConfig = formJSON[key];
310
+ const borderId = `${key.replace(/\s+/g, '')}Border`;
311
+
312
+ if (fieldConfig.type === 'segmented') {
313
+ const selectorId = `${key.replace(/\s+/g, '')}Selector`;
314
+ const selector = document.getElementById(selectorId);
315
+
316
+ // Add click handlers for segments
317
+ fieldConfig.options.forEach((option, optionIndex) => {
318
+ const segmentId = `${selectorId}_${optionIndex}`;
319
+ const segment = document.getElementById(segmentId);
320
+
321
+ if (segment) {
322
+ segment.addEventListener('click', () => {
323
+ // Clear all segments
324
+ fieldConfig.options.forEach((_, clearIndex) => {
325
+ const clearSegmentId = `${selectorId}_${clearIndex}`;
326
+ const clearSegment = document.getElementById(clearSegmentId);
327
+ if (clearSegment) {
328
+ clearSegment.setAttribute('fill', '#1c1c20');
329
+ clearSegment.setAttribute('data-selected', 'false');
330
+ }
331
+ });
332
+
333
+ // Select clicked segment
334
+ segment.setAttribute('fill', '#3eda82');
335
+ segment.setAttribute('data-selected', 'true');
336
+
337
+ // Update hidden input value
338
+ if (selector) {
339
+ selector.value = option;
340
+ }
341
+
342
+ // Update border
343
+ const borderElement = document.getElementById(borderId);
344
+ if (borderElement) {
345
+ borderElement.setAttribute('stroke', 'url(#inputGradient)');
346
+ }
347
+
348
+ // Validate form
349
+ validateForm(formJSON);
350
+ });
351
+ }
352
+ });
353
+ } else {
354
+ const inputId = `${key.replace(/\s+/g, '')}Input`;
355
+ const inputElement = document.getElementById(inputId);
356
+
357
+ if (inputElement) {
358
+ inputElement.addEventListener('input', (evt) => {
359
+ const borderElement = document.getElementById(borderId);
360
+ if (borderElement) {
361
+ borderElement.setAttribute('stroke', 'url(#inputGradient)');
362
+ }
363
+
364
+ // Validate form
365
+ validateForm(formJSON);
366
+ });
367
+ }
368
+ }
369
+ });
370
+
371
+ const submitButton = document.getElementById('submitButton');
372
+ if (submitButton) {
373
+ submitButton.addEventListener('click', () => {
374
+ if (!validateForm(formJSON)) {
375
+ console.log('Form validation failed');
376
+ return;
377
+ }
378
+
379
+ console.log('Submit button clicked');
380
+
381
+ // Create button animation
382
+ const animation = document.createElementNS("http://www.w3.org/2000/svg", "animate");
383
+ animation.setAttribute("attributeName", "offset");
384
+ animation.setAttribute("values", "-0.5;2.5");
385
+ animation.setAttribute("dur", "300ms");
386
+ animation.setAttribute("repeatCount", "1");
387
+
388
+ const secondAnimation = document.createElementNS("http://www.w3.org/2000/svg", "animate");
389
+ secondAnimation.setAttribute("attributeName", "offset");
390
+ secondAnimation.setAttribute("values", "0.5;3.5");
391
+ secondAnimation.setAttribute("dur", "300ms");
392
+ secondAnimation.setAttribute("repeatCount", "1");
393
+
394
+ const startGradient = document.getElementById('submitButtonGradientStart');
395
+ const endGradient = document.getElementById('submitButtonGradientEnd');
396
+
397
+ if (startGradient && endGradient) {
398
+ startGradient.appendChild(animation);
399
+ endGradient.appendChild(secondAnimation);
400
+ animation.beginElement();
401
+ secondAnimation.beginElement();
402
+ }
403
+
404
+ // Collect form values
405
+ const formValues = {};
406
+ Object.keys(formJSON).forEach((key) => {
407
+ if(key === 'form') {
408
+ return;
409
+ }
410
+
411
+ const fieldConfig = formJSON[key];
412
+
413
+ if (fieldConfig.type === 'segmented') {
414
+ const selectorId = `${key.replace(/\s+/g, '')}Selector`;
415
+ const selector = document.getElementById(selectorId);
416
+ if (selector) {
417
+ formValues[key] = selector.value;
418
+ }
419
+ } else {
420
+ const inputId = `${key.replace(/\s+/g, '')}Input`;
421
+ const inputElement = document.getElementById(inputId);
422
+ if (inputElement) {
423
+ formValues[key] = inputElement.value;
424
+ }
425
+ }
426
+ });
427
+
428
+ if (onSubmit) {
429
+ onSubmit(formValues);
430
+ }
431
+ });
432
+ }
433
+
434
+ // Initial validation
435
+ validateForm(formJSON);
436
+ }, 100);
437
+
438
+ return container;
439
+ }
440
+
441
+ window.getForm = getForm;
442
+
443
+ })(window);
444
+ </script>
445
+
446
+ <script>
447
+ const formConfig = {
448
+ "Name": {type: "text", required: true},
449
+ "Address 1": {type: "text", required: true},
450
+ "Address 2": {type: "text", required: false},
451
+ "City": {type: "text", required: true},
452
+ "State": {type: "text", required: true},
453
+ "Zip Code": {type: "text", required: true}
454
+ };
455
+
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();
462
+ }
463
+
464
+ const form = getForm(formConfig, handleSubmit);
465
+ document.getElementById('address-form').appendChild(form);
466
+ </script>
467
+
468
+ <script type="text/javascript">
469
+ let stripe;
470
+ let elements;
471
+ let response;
472
+
473
+ const paymentForm = document.getElementById('payment-form');
474
+ const submitButton = document.getElementById('submit-button');
475
+ const errorMessage = document.getElementById('error-message');
476
+ const loadingMessage = document.getElementById('loading');
477
+
478
+ async function getPaymentIntentWithoutSplits(amount, currency) {
479
+ try {
480
+ const payload = {
481
+ timestamp: new Date().getTime() + '',
482
+ amount: {{amount}},
483
+ currency: 'USD',
484
+ payees: []
485
+ };
486
+
487
+ const res = await fetch(`{{allyabaseOrigin}}/processor/stripe/intent`, {
488
+ method: 'put',
489
+ body: JSON.stringify(payload),
490
+ headers: {'Content-Type': 'application/json'}
491
+ });
492
+
493
+ const response = await res.json();
494
+ console.log('got intent response', response);
495
+
496
+ stripe = Stripe(response.publishableKey);
497
+ elements = stripe.elements({
498
+ clientSecret: response.paymentIntent
499
+ });
500
+
501
+ const paymentElement = elements.create('payment');
502
+ paymentElement.mount('#payment-element');
503
+ } catch(err) {
504
+ console.warn(err);
505
+ }
506
+ };
507
+
508
+ 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
+ try {
520
+ const { error } = await stripe.confirmPayment({
521
+ elements,
522
+ confirmParams: {
523
+ return_url: '{{shoppeUrl}}',
524
+ },
525
+ });
526
+
527
+ if(error) {
528
+ showError(error.message);
529
+ }
530
+ } catch(err) {
531
+ showError('An unexpected error occurred.');
532
+ console.warn('payment error: ', err);
533
+ }
534
+ };
535
+
536
+ paymentForm.addEventListener('submit', async (event) => {
537
+ event.preventDefault();
538
+
539
+ if (!stripe || !elements) {
540
+ return;
541
+ }
542
+
543
+ // Disable payment form submission while processing
544
+ setLoading(true);
545
+
546
+ await window.confirmPayment();
547
+
548
+ setLoading(false);
549
+ });
550
+
551
+ const showError = (message) => {
552
+ errorMessage.textContent = message;
553
+ errorMessage.style.display = 'block';
554
+ setTimeout(() => {
555
+ errorMessage.style.display = 'none';
556
+ errorMessage.textContent = '';
557
+ }, 5000);
558
+ };
559
+
560
+ const setLoading = (isLoading) => {
561
+ submitButton.disabled = isLoading;
562
+ loadingMessage.style.display = isLoading ? 'block' : 'none';
563
+ };
564
+
565
+ const start = () => {
566
+ getPaymentIntentWithoutSplits({{amount}}, 'USD');
567
+ };
568
+
569
+ window.addPaymentForm = start;
570
+ </script>
571
+ </body>
572
+ </html>