wiki-plugin-shoppe 0.0.35 → 0.0.37

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.
@@ -4,383 +4,422 @@
4
4
  <script src="https://js.stripe.com/v3/"></script>
5
5
  <meta charset="utf-8">
6
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-->
7
+ <!-- Shoppere App Clip — triggers on iOS Safari when user visits this buy page -->
8
+ <meta name="apple-itunes-app"
9
+ content="app-id=6760954663, app-clip-bundle-id=app.foures.shoppere.shoppere, app-clip-display=card, app-clip-url={{clipUrl}}">
9
10
  <meta name="twitter:title" content="{{title}}">
10
11
  <meta name="description" content="{{description}}">
11
12
  <meta name="keywords" content="{{keywords}}">
12
13
  <meta name="twitter:description" content="{{description}}">
13
14
  <meta name="twitter:image" content={{image}}>
14
-
15
15
  <meta name="og:title" content="{{title}}">
16
16
  <meta name="og:description" content="{{description}}">
17
17
  <meta name="og:image" content={{image}}>
18
-
19
18
  <title>{{title}}</title>
19
+ <style>
20
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
21
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f12; color: white; }
22
+ #main-container { min-height: 100vh; display: flex; flex-direction: column; }
23
+
24
+ /* ── Product hero ── */
25
+ #product-section {
26
+ display: flex; flex-direction: column; justify-content: center; align-items: center;
27
+ padding: 40px 20px; box-sizing: border-box; min-height: 100vh;
28
+ }
29
+ #product-section .inner { width: 100%; max-width: 600px; text-align: center; }
30
+ #product-image { width: 100%; height: auto; max-height: 50vh; object-fit: contain; border-radius: 12px; margin-bottom: 30px; }
31
+ #product-section h1 { margin: 0 0 20px; font-size: clamp(1.8rem, 5vw, 3rem); font-weight: bold; }
32
+ #product-section p { margin: 0 0 40px; opacity: 0.9; font-size: clamp(1rem, 3vw, 1.3rem); line-height: 1.5; }
33
+ .buy-btn {
34
+ width: 100%; max-width: 400px; padding: 20px 40px;
35
+ background: linear-gradient(135deg, #8b5cf6, #10b981);
36
+ color: white; border: none; border-radius: 50px;
37
+ font-size: clamp(1.2rem, 4vw, 1.5rem); font-weight: bold; cursor: pointer;
38
+ transition: all 0.3s ease; box-shadow: 0 8px 32px rgba(139,92,246,0.3);
39
+ }
40
+ .buy-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 40px rgba(139,92,246,0.4); }
41
+
42
+ /* ── Shoppere path ── */
43
+ #shoppere-section {
44
+ display: none; padding: 40px 20px; box-sizing: border-box;
45
+ background: rgba(20,20,24,0.8); backdrop-filter: blur(10px);
46
+ }
47
+ .shoppere-card {
48
+ max-width: 520px; margin: 0 auto;
49
+ background: #18181c; border: 1px solid #333; border-radius: 16px; padding: 32px;
50
+ text-align: center;
51
+ }
52
+ .shoppere-card .icon { font-size: 48px; margin-bottom: 16px; }
53
+ .shoppere-card h2 { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
54
+ .shoppere-card .sub { font-size: 14px; color: #aaa; margin-bottom: 24px; line-height: 1.5; }
55
+ .shoppere-card .price-line { font-size: 28px; font-weight: 700; color: #7ec8e3; margin-bottom: 24px; }
56
+ #shoppere-stripe-wrap { margin-bottom: 20px; }
57
+ #shoppere-pay-btn {
58
+ width: 100%; padding: 16px;
59
+ background: linear-gradient(90deg, #10b981, #8b5cf6);
60
+ color: white; border: none; border-radius: 12px;
61
+ font-size: 16px; font-weight: 700; cursor: pointer; transition: opacity 0.15s;
62
+ }
63
+ #shoppere-pay-btn:disabled { opacity: 0.45; cursor: not-allowed; }
64
+ #shoppere-pay-btn:hover:not(:disabled) { opacity: 0.88; }
65
+ #shoppere-error { color: #ff6b6b; font-size: 13px; margin-top: 10px; display: none; }
66
+ #shoppere-loading { color: #aaa; font-size: 13px; margin-top: 10px; display: none; }
67
+
68
+ /* ── Shoppere success ── */
69
+ #shoppere-success { display: none; }
70
+ .success-icon { font-size: 64px; margin-bottom: 16px; }
71
+ .success-title { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
72
+ .success-sub { font-size: 14px; color: #aaa; margin-bottom: 24px; line-height: 1.5; }
73
+ .success-back { color: #7ec8e3; font-size: 14px; text-decoration: none; }
74
+ .success-back:hover { text-decoration: underline; }
75
+
76
+ /* ── Legacy recovery key path ── */
77
+ #forms-section {
78
+ display: none; padding: 40px 20px; box-sizing: border-box;
79
+ background: rgba(20,20,24,0.8); backdrop-filter: blur(10px);
80
+ }
81
+ #forms-section h2 { text-align: center; margin-bottom: 40px; font-size: clamp(1.5rem, 4vw, 2rem); }
82
+ #forms-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: start; }
83
+ #payment-section { display: none; }
84
+ #payment-section h3 { margin-bottom: 20px; color: #10b981; }
85
+ #payment-form {
86
+ background: #2a2a2e; padding: 30px; border-radius: 15px; border: 1px solid #444;
87
+ }
88
+ .amount-display { text-align: center; margin-bottom: 20px; padding: 15px; background: rgba(255,255,255,0.05); border-radius: 8px; }
89
+ .amount-display .label { color: #bbb; font-size: 14px; margin-bottom: 5px; }
90
+ .amount-display .value { color: #3eda82; font-size: 24px; font-weight: bold; }
91
+ #payment-element { margin-bottom: 20px; }
92
+ #submit-button {
93
+ width: 100%; padding: 15px; background: #666; color: #999;
94
+ border: none; border-radius: 22.5px; font-size: 16px; font-weight: bold; cursor: not-allowed;
95
+ }
96
+ #submit-button.ready { background: linear-gradient(90deg, #10b981, #8b5cf6); color: white; cursor: pointer; }
97
+ #error-message { display: none; color: #ff6b6b; margin-top: 10px; text-align: center; }
98
+ #loading { display: none; color: white; margin-top: 10px; text-align: center; }
99
+
100
+ @media (max-width: 768px) {
101
+ #forms-grid { grid-template-columns: 1fr !important; gap: 30px !important; }
102
+ #product-section { min-height: 80vh !important; padding: 20px !important; }
103
+ }
104
+ </style>
20
105
  </head>
21
- <body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background: #0f0f12; color: white;">
22
- <div id="main-container" style="
23
- min-height: 100vh;
24
- display: flex;
25
- flex-direction: column;
26
- ">
27
- <!-- Product Section -->
28
- <div id="product-section" style="
29
- display: flex;
30
- flex-direction: column;
31
- justify-content: center;
32
- align-items: center;
33
- padding: 40px 20px;
34
- box-sizing: border-box;
35
- min-height: 100vh;
36
- ">
37
- <teleport pubKey="{{pubKey}}" signature="{{signature}}" message="{{title}}{{amount}}" spell="sanora-club" amount="{{amount}}">
38
- <div style="width: 100%; max-width: 600px; text-align: center;">
39
- <img id="product-image" style="width: 100%; height: auto; max-height: 50vh; object-fit: contain; border-radius: 12px; margin-bottom: 30px;"></img>
40
- <script type="text/javascript">
41
- document.getElementById('product-image').src = {{image}};
42
- </script>
43
- <h1 style="margin: 0 0 20px 0; font-size: clamp(1.8rem, 5vw, 3rem); font-weight: bold;">{{title}}</h1>
44
- <p style="margin: 0 0 40px 0; opacity: 0.9; font-size: clamp(1rem, 3vw, 1.3rem); line-height: 1.5;">{{description}}</p>
45
-
46
- <!-- Big Buy Button -->
47
- <button id="buy-button" style="
48
- width: 100%;
49
- max-width: 400px;
50
- padding: 20px 40px;
51
- background: linear-gradient(135deg, #8b5cf6, #10b981);
52
- color: white;
53
- border: none;
54
- border-radius: 50px;
55
- font-size: clamp(1.2rem, 4vw, 1.5rem);
56
- font-weight: bold;
57
- cursor: pointer;
58
- transition: all 0.3s ease;
59
- box-shadow: 0 8px 32px rgba(139, 92, 246, 0.3);
60
- " onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 12px 40px rgba(139, 92, 246, 0.4)';"
61
- onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 8px 32px rgba(139, 92, 246, 0.3)';">
62
- BUY NOW - ${{formattedAmount}}
63
- </button>
106
+ <body>
107
+ <div id="main-container">
108
+
109
+ <!-- ── Product hero ─────────────────────────────────────────────────────── -->
110
+ <div id="product-section">
111
+ <div class="inner">
112
+ <img id="product-image" alt="{{title}}">
113
+ <script>document.getElementById('product-image').src = {{image}};</script>
114
+ <h1>{{title}}</h1>
115
+ <p>{{description}}</p>
116
+ <button class="buy-btn" id="buy-button">BUY NOW — ${{formattedAmount}}</button>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- ── Shoppere (pubKey) path ────────────────────────────────────────────── -->
121
+ <div id="shoppere-section">
122
+ <div class="shoppere-card">
123
+ <!-- Purchase panel -->
124
+ <div id="shoppere-purchase">
125
+ <div class="icon">🛍️</div>
126
+ <h2>{{title}}</h2>
127
+ <div class="sub">Completing your purchase with Shoppere.</div>
128
+ <div class="price-line" id="shoppere-price">${{formattedAmount}}</div>
129
+ <div id="shoppere-stripe-wrap"></div>
130
+ <button id="shoppere-pay-btn" disabled>Pay ${{formattedAmount}}</button>
131
+ <div id="shoppere-error"></div>
132
+ <div id="shoppere-loading">Processing…</div>
133
+ </div>
134
+ <!-- Success panel -->
135
+ <div id="shoppere-success">
136
+ <div class="success-icon">✅</div>
137
+ <div class="success-title">Purchase complete!</div>
138
+ <div class="success-sub">
139
+ Open <strong>Shoppere</strong> to access <em>{{title}}</em>.<br>
140
+ Your purchase is saved to your shop credential.
141
+ </div>
142
+ <a class="success-back" href="{{shoppeUrl}}">← Back to shoppe</a>
143
+ </div>
64
144
  </div>
65
- </teleport>
66
145
  </div>
67
146
 
68
- <!-- Forms Section -->
69
- <div id="forms-section" style="
70
- display: none;
71
- padding: 40px 20px;
72
- box-sizing: border-box;
73
- background: rgba(20, 20, 24, 0.8);
74
- backdrop-filter: blur(10px);
75
- ">
147
+ <!-- ── Legacy recovery key path ─────────────────────────────────────────── -->
148
+ <div id="forms-section">
76
149
  <div style="max-width: 1200px; margin: 0 auto;">
77
- <h2 style="text-align: center; margin-bottom: 40px; font-size: clamp(1.5rem, 4vw, 2rem);">Complete Your Purchase</h2>
78
-
79
- <div id="forms-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: start;">
80
- <!-- Recovery Form -->
150
+ <h2>Complete Your Purchase</h2>
151
+ <div id="forms-grid">
81
152
  <div>
82
153
  <h3 style="margin-bottom: 20px; color: #a855f7;">Recovery Information</h3>
83
154
  <div id="address-form"></div>
84
155
  </div>
85
-
86
- <!-- Payment Form -->
87
- <div id="payment-section" style="display:none">
88
- <h3 style="margin-bottom: 20px; color: #10b981;">Payment Details</h3>
89
- <form id="payment-form" class="payment-form" style="
90
- background: #2a2a2e;
91
- padding: 30px;
92
- border-radius: 15px;
93
- border: 1px solid #444;
94
- ">
95
- <div style="
96
- text-align: center;
97
- margin-bottom: 20px;
98
- padding: 15px;
99
- background: rgba(255,255,255,0.05);
100
- border-radius: 8px;
101
- ">
102
- <div style="color: #bbbbbb; font-size: 14px; margin-bottom: 5px;">Total</div>
103
- <div style="color: #3eda82; font-size: 24px; font-weight: bold;">${{formattedAmount}}</div>
156
+ <div id="payment-section">
157
+ <h3>Payment Details</h3>
158
+ <form id="payment-form">
159
+ <div class="amount-display">
160
+ <div class="label">Total</div>
161
+ <div class="value">${{formattedAmount}}</div>
104
162
  </div>
105
- <div id="payment-element" style="margin-bottom: 20px;"></div>
106
- <button id="submit-button" style="
107
- width: 100%;
108
- padding: 15px;
109
- background: #666666;
110
- color: #999999;
111
- border: none;
112
- border-radius: 22.5px;
113
- font-size: 16px;
114
- font-weight: bold;
115
- cursor: not-allowed;
116
- " disabled>Complete Purchase</button>
117
- <div id="error-message" class="error-message" style="display: none; color: #ff6b6b; margin-top: 10px; text-align: center;"></div>
118
- <div id="loading" class="loading" style="display: none; color: white; margin-top: 10px; text-align: center;">Processing payment...</div>
163
+ <div id="payment-element"></div>
164
+ <button id="submit-button" disabled>Complete Purchase</button>
165
+ <div id="error-message"></div>
166
+ <div id="loading">Processing payment…</div>
119
167
  </form>
120
168
  </div>
121
169
  </div>
122
170
  </div>
123
171
  </div>
124
- </div>
125
172
 
126
- <style>
127
- @media (max-width: 768px) {
128
- #forms-grid {
129
- grid-template-columns: 1fr !important;
130
- gap: 30px !important;
173
+ </div><!-- #main-container -->
174
+
175
+ <!-- ── Shared payees param parser ─────────────────────────────────────────── -->
176
+ <script>
177
+ function parseUrlPayees() {
178
+ const raw = new URLSearchParams(window.location.search).get('payees');
179
+ if (!raw) return null;
180
+ return raw.split('|').map(t => {
181
+ const [pubKey, amount, percent, addieUrl] = t.split(':');
182
+ const p = {};
183
+ if (pubKey) p.pubKey = pubKey;
184
+ if (amount) p.amount = Number(amount);
185
+ if (percent) p.percent = Number(percent);
186
+ if (addieUrl) p.addieUrl = addieUrl;
187
+ return p;
188
+ }).filter(p => p.pubKey);
131
189
  }
132
-
133
- #product-section {
134
- min-height: 80vh !important;
135
- padding: 20px !important;
190
+ const URL_PAYEES = parseUrlPayees();
191
+ </script>
192
+
193
+ <!-- ── Shoppere (pubKey) path ────────────────────────────────────────────── -->
194
+ <script>
195
+ // Credentials injected by the server from req.query, or read from URL params
196
+ // (the App Clip appends them when redirecting back to the buy page).
197
+ const _buyerPubKey = '{{buyerPubKey}}' || new URLSearchParams(window.location.search).get('pubKey') || '';
198
+ const _buyerTimestamp = '{{buyerTimestamp}}' || new URLSearchParams(window.location.search).get('timestamp') || '';
199
+ const _buyerSignature = '{{buyerSignature}}' || new URLSearchParams(window.location.search).get('signature') || '';
200
+ const IS_SHOPPERE = !!(_buyerPubKey && _buyerTimestamp && _buyerSignature);
201
+
202
+ let _shoppereStripe, _shoppereElements, _shoppereClientSecret;
203
+
204
+ async function initShopperePath() {
205
+ document.getElementById('product-section').style.display = 'none';
206
+ document.getElementById('shoppere-section').style.display = 'block';
207
+
208
+ const payBtn = document.getElementById('shoppere-pay-btn');
209
+ const errEl = document.getElementById('shoppere-error');
210
+ const loadEl = document.getElementById('shoppere-loading');
211
+
212
+ // Call purchase/intent immediately — credentials are already verified
213
+ try {
214
+ const resp = await fetch('/plugin/shoppe/{{tenantUuid}}/purchase/intent', {
215
+ method: 'POST',
216
+ headers: { 'Content-Type': 'application/json' },
217
+ body: JSON.stringify({
218
+ pubKey: _buyerPubKey,
219
+ timestamp: _buyerTimestamp,
220
+ signature: _buyerSignature,
221
+ productId: '{{productId}}',
222
+ title: '{{title}}',
223
+ ...(URL_PAYEES && { payees: URL_PAYEES })
224
+ })
225
+ });
226
+ const json = await resp.json();
227
+
228
+ if (json.error) { showShoppereError(json.error); return; }
229
+
230
+ if (json.purchased) {
231
+ // Already purchased — skip payment, show success immediately
232
+ showShoppereSuccess();
233
+ return;
234
+ }
235
+
236
+ if (json.free) {
237
+ // Free item — record without Stripe
238
+ await completeShopperePurchase(null);
239
+ return;
240
+ }
241
+
242
+ // Mount Stripe payment element
243
+ _shoppereClientSecret = json.clientSecret;
244
+ _shoppereStripe = Stripe(json.publishableKey);
245
+ _shoppereElements = _shoppereStripe.elements({ clientSecret: json.clientSecret });
246
+ const payEl = _shoppereElements.create('payment');
247
+ payEl.mount('#shoppere-stripe-wrap');
248
+ payEl.on('ready', () => { payBtn.disabled = false; });
249
+
250
+ payBtn.addEventListener('click', async () => {
251
+ payBtn.disabled = true;
252
+ loadEl.style.display = 'block';
253
+ errEl.style.display = 'none';
254
+
255
+ const { error } = await _shoppereStripe.confirmPayment({
256
+ elements: _shoppereElements,
257
+ confirmParams: { return_url: window.location.href },
258
+ redirect: 'if_required'
259
+ });
260
+
261
+ if (error) {
262
+ showShoppereError(error.message);
263
+ payBtn.disabled = false;
264
+ loadEl.style.display = 'none';
265
+ return;
266
+ }
267
+
268
+ const paymentIntentId = _shoppereClientSecret.split('_secret_')[0];
269
+ await completeShopperePurchase(paymentIntentId);
270
+ loadEl.style.display = 'none';
271
+ });
272
+
273
+ } catch (err) {
274
+ showShoppereError('Unexpected error: ' + err.message);
275
+ }
136
276
  }
137
- }
138
- </style>
139
-
140
- <script type="text/javascript">
141
- // Buy button — just reveal the forms section (payment init happens after recovery key submitted)
142
- document.getElementById('buy-button').addEventListener('click', function() {
143
- const formsSection = document.getElementById('forms-section');
144
- formsSection.style.display = 'block';
145
- formsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
146
- });
147
277
 
148
- // Enable submit once Stripe payment element is ready
149
- function validateAllForms() {
150
- const paymentValid = stripe && elements;
151
- const submitButton = document.getElementById('submit-button');
152
- if (paymentValid) {
153
- submitButton.style.background = 'linear-gradient(90deg, #10b981, #8b5cf6)';
154
- submitButton.style.color = 'white';
155
- submitButton.style.cursor = 'pointer';
156
- submitButton.disabled = false;
157
- submitButton.textContent = 'Complete Purchase';
158
- } else {
159
- submitButton.style.background = '#666666';
160
- submitButton.style.color = '#999999';
161
- submitButton.style.cursor = 'not-allowed';
162
- submitButton.disabled = true;
278
+ async function completeShopperePurchase(paymentIntentId) {
279
+ try {
280
+ const resp = await fetch('/plugin/shoppe/{{tenantUuid}}/purchase/complete', {
281
+ method: 'POST',
282
+ headers: { 'Content-Type': 'application/json' },
283
+ body: JSON.stringify({
284
+ pubKey: _buyerPubKey,
285
+ timestamp: _buyerTimestamp,
286
+ signature: _buyerSignature,
287
+ productId: '{{productId}}',
288
+ title: '{{title}}',
289
+ ...(paymentIntentId && { paymentIntentId })
290
+ })
291
+ });
292
+ const json = await resp.json();
293
+ if (json.success) {
294
+ showShoppereSuccess();
295
+ } else {
296
+ showShoppereError(json.error || 'Purchase recording failed — contact support');
297
+ }
298
+ } catch (err) {
299
+ showShoppereError('Unexpected error: ' + err.message);
163
300
  }
164
301
  }
302
+
303
+ function showShoppereSuccess() {
304
+ document.getElementById('shoppere-purchase').style.display = 'none';
305
+ document.getElementById('shoppere-success').style.display = 'block';
306
+ }
307
+
308
+ function showShoppereError(msg) {
309
+ const el = document.getElementById('shoppere-error');
310
+ el.textContent = msg;
311
+ el.style.display = 'block';
312
+ }
313
+
314
+ // Route on load
315
+ if (IS_SHOPPERE) {
316
+ document.addEventListener('DOMContentLoaded', initShopperePath);
317
+ }
165
318
  </script>
166
319
 
167
- <script type="text/javascript">
320
+ <!-- ── Legacy recovery key path ─────────────────────────────────────────── -->
321
+ <script>
322
+ // The SVG form system (unchanged from original)
168
323
  (function(window) {
169
324
  'use strict';
170
325
 
171
326
  function calculateTextBlockHeight(text, width = 340, fontSize = 14, lineHeight = 1.4) {
172
- // Create a temporary element to measure text height
173
327
  const temp = document.createElement('div');
174
- temp.style.position = 'absolute';
175
- temp.style.visibility = 'hidden';
176
- temp.style.width = `${width}px`;
177
- temp.style.fontSize = `${fontSize}px`;
178
- temp.style.lineHeight = `${lineHeight}`;
179
- temp.style.fontFamily = 'Arial, sans-serif';
180
- temp.style.padding = '12px';
328
+ temp.style.cssText = `position:absolute;visibility:hidden;width:${width}px;font-size:${fontSize}px;line-height:${lineHeight};font-family:Arial,sans-serif;padding:12px`;
181
329
  temp.innerHTML = text;
182
-
183
330
  document.body.appendChild(temp);
184
331
  const height = temp.offsetHeight;
185
332
  document.body.removeChild(temp);
186
-
187
333
  return height;
188
- };
334
+ }
189
335
 
190
336
  function calculateFormHeight(formConfig) {
191
- const fields = Object.keys(formConfig).filter(key => key !== 'form');
192
- const headerHeight = 85;
193
- const standardFieldHeight = 70;
194
- const submitButtonHeight = 45;
195
- const bottomPadding = 30;
196
-
197
- let totalHeight = headerHeight;
198
-
337
+ const fields = Object.keys(formConfig).filter(k => k !== 'form');
338
+ let total = 85;
199
339
  fields.forEach(key => {
200
- const fieldConfig = formConfig[key];
201
-
202
- if (fieldConfig.type === 'text-block') {
203
- // Calculate dynamic height for text blocks
204
- const textHeight = calculateTextBlockHeight(fieldConfig.content || fieldConfig.text);
205
- totalHeight += textHeight + 20; // Add some spacing
206
- } else {
207
- // Standard field height for inputs and selectors
208
- totalHeight += standardFieldHeight;
209
- }
340
+ const fc = formConfig[key];
341
+ total += fc.type === 'text-block'
342
+ ? calculateTextBlockHeight(fc.content || fc.text) + 20
343
+ : 70;
210
344
  });
211
-
212
- return totalHeight + submitButtonHeight + bottomPadding;
345
+ return total + 45 + 30; // submit + padding
213
346
  }
214
347
 
215
348
  function getBackgroundAndGradients(formConfig) {
216
- const dynamicHeight = calculateFormHeight(formConfig);
217
-
218
- const svg = `<rect width="500" height="${dynamicHeight}" fill="transparent"/>
219
-
220
- <!-- Form Container with Metallic Background -->
349
+ const h = calculateFormHeight(formConfig);
350
+ return `<rect width="500" height="${h}" fill="transparent"/>
221
351
  <linearGradient id="metallicBackground" x1="0%" y1="0%" x2="100%" y2="100%">
222
- <stop offset="0%" stop-color="#2a2a2e"/>
223
- <stop offset="50%" stop-color="#323236"/>
224
- <stop offset="100%" stop-color="#2a2a2e"/>
352
+ <stop offset="0%" stop-color="#2a2a2e"/>
353
+ <stop offset="50%" stop-color="#323236"/>
354
+ <stop offset="100%" stop-color="#2a2a2e"/>
225
355
  </linearGradient>
226
- <rect x="50" y="50" width="400" height="${dynamicHeight}" rx="15" fill="url(#metallicBackground)"
227
- stroke="#444" stroke-width="1"/>
228
-
229
- <!-- Subtle Metallic Highlight -->
356
+ <rect x="50" y="50" width="400" height="${h}" rx="15" fill="url(#metallicBackground)" stroke="#444" stroke-width="1"/>
230
357
  <line x1="51" y1="52" x2="449" y2="52" stroke="#555" stroke-width="1" opacity="0.5"/>
231
-
232
- <!-- Form Header -->
233
- <text x="250" y="85" font-family="Arial, sans-serif" font-size="24" font-weight="bold"
234
- fill="#ffffff" text-anchor="middle">RECOVERY KEY</text>
235
-
236
- <!-- Define the gradient for active input borders -->
358
+ <text x="250" y="85" font-family="Arial,sans-serif" font-size="24" font-weight="bold" fill="#ffffff" text-anchor="middle">RECOVERY KEY</text>
237
359
  <linearGradient id="inputGradient" x1="0%" y1="0%" x2="100%" y2="0%">
238
- <stop offset="0%" stop-color="purple"/>
239
- <stop offset="100%" stop-color="green"/>
360
+ <stop offset="0%" stop-color="purple"/>
361
+ <stop offset="100%" stop-color="green"/>
240
362
  </linearGradient>
241
-
242
- <!-- Button Gradient -->
243
363
  <linearGradient id="buttonGradient" x1="0%" y1="0%" x2="100%" y2="0%">
244
- <stop id="submitButtonGradientStart" offset="0%" stop-color="green"/>
245
- <stop id="submitButtonGradientEnd" offset="100%" stop-color="purple"/>
364
+ <stop id="submitButtonGradientStart" offset="0%" stop-color="green"/>
365
+ <stop id="submitButtonGradientEnd" offset="100%" stop-color="purple"/>
246
366
  </linearGradient>
247
-
248
367
  <linearGradient id="buttonPressedGradient" x1="0%" y1="0%" x2="100%" y2="0%">
249
- <stop offset="0%" stop-color="purple"/>
250
- <stop offset="100%" stop-color="green"/>
368
+ <stop offset="0%" stop-color="purple"/>
369
+ <stop offset="100%" stop-color="green"/>
251
370
  </linearGradient>`;
252
-
253
- return svg;
254
- }
255
-
256
- function getSegmentedSelector(x, y, text, options) {
257
- const borderId = `${text.replace(/\s+/g, '')}Border`;
258
- const selectorId = `${text.replace(/\s+/g, '')}Selector`;
259
-
260
- const segmentWidth = 340 / options.length;
261
-
262
- let segments = '';
263
- let segmentTexts = '';
264
-
265
- options.forEach((option, index) => {
266
- const segmentX = x + (index * segmentWidth);
267
- const segmentId = `${selectorId}_${index}`;
268
-
269
- 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"/>`;
270
-
271
- 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>`;
272
- });
273
-
274
- const svg = `<text x="${x}" y="${y}" font-family="Arial, sans-serif" font-size="14" fill="#bbbbbb">${text}</text>
275
- <!-- Segmented Container -->
276
- <rect id="${borderId}" x="${x}" y="${y + 10}" width="340" height="40" rx="8" fill="transparent" stroke="#444" stroke-width="2"/>
277
- ${segments}
278
- ${segmentTexts}
279
- <!-- Hidden input to store value -->
280
- <foreignObject x="${x}" y="${y + 55}" width="1" height="1" style="overflow: hidden;">
281
- <input xmlns="http://www.w3.org/1999/xhtml" id="${selectorId}" type="hidden" data-field="${text}"/>
282
- </foreignObject>`;
283
-
284
- return svg;
285
371
  }
286
372
 
287
373
  function getInput(x, y, text, inputType) {
288
- const borderId = `${text.replace(/\s+/g, '')}Border`;
289
- const inputId = `${text.replace(/\s+/g, '')}Input`;
290
-
291
- const svg = `<text x="${x}" y="${y}" font-family="Arial, sans-serif" font-size="14" fill="#bbbbbb">${text}</text>
292
- <!-- Field Background -->
293
- <rect id="${borderId}" x="${x}" y="${y + 10}" width="340" height="40" rx="8" fill="#1c1c20"
294
- stroke="#444" stroke-width="2" class="input-field"/>
295
- <!-- Inset shadow effect -->
296
- <rect x="${x + 2}" y="${y + 12}" width="336" height="36" rx="6" fill="none"
297
- stroke="#000" stroke-width="1" opacity="0.3"/>
298
- <!-- HTML Input Field -->
299
- <foreignObject x="${x + 5}" y="${y + 15}" width="330" height="30">
300
- <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;"/>
301
- </foreignObject>`;
302
-
303
- return svg;
374
+ const borderId = `${text.replace(/\s+/g,'')}Border`;
375
+ const inputId = `${text.replace(/\s+/g,'')}Input`;
376
+ return `<text x="${x}" y="${y}" font-family="Arial,sans-serif" font-size="14" fill="#bbbbbb">${text}</text>
377
+ <rect id="${borderId}" x="${x}" y="${y+10}" width="340" height="40" rx="8" fill="#1c1c20" stroke="#444" stroke-width="2"/>
378
+ <rect x="${x+2}" y="${y+12}" width="336" height="36" rx="6" fill="none" stroke="#000" stroke-width="1" opacity="0.3"/>
379
+ <foreignObject x="${x+5}" y="${y+15}" width="330" height="30">
380
+ <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:transparent;color:white;border:none;outline:none;padding:8px 12px;font-size:14px;font-family:Arial,sans-serif;border-radius:6px;"/>
381
+ </foreignObject>`;
304
382
  }
305
383
 
306
384
  function getTextBlock(x, y, text, content) {
307
385
  const textHeight = calculateTextBlockHeight(content);
308
-
309
- const svg = `<!-- Text Block Background -->
310
- <rect x="${x}" y="${y + 10}" width="340" height="${textHeight}" rx="8"
311
- fill="#1a1a1e" stroke="#333" stroke-width="1"/>
312
-
313
- <!-- Text Block Content -->
314
- <foreignObject x="${x + 12}" y="${y + 22}" width="316" height="${textHeight - 24}">
315
- <div xmlns="http://www.w3.org/1999/xhtml" style="
316
- font-family: Arial, sans-serif;
317
- font-size: 14px;
318
- line-height: 1.4;
319
- color: #cccccc;
320
- padding: 0;
321
- margin: 0;
322
- width: 100%;
323
- height: 100%;
324
- overflow: hidden;
325
- ">${content}</div>
326
- </foreignObject>`;
327
-
328
- return { svg, height: textHeight + 20 };
329
- };
386
+ return {
387
+ svg: `<rect x="${x}" y="${y+10}" width="340" height="${textHeight}" rx="8" fill="#1a1a1e" stroke="#333" stroke-width="1"/>
388
+ <foreignObject x="${x+12}" y="${y+22}" width="316" height="${textHeight-24}">
389
+ <div xmlns="http://www.w3.org/1999/xhtml" style="font-family:Arial,sans-serif;font-size:14px;line-height:1.4;color:#cccccc;">${content}</div>
390
+ </foreignObject>`,
391
+ height: textHeight + 20
392
+ };
393
+ }
330
394
 
331
395
  function getSubmitButton(x, y) {
332
- return `<rect id="submitButton" x="${x}" y="${y}" width="300" height="45" rx="22.5" fill="#666666" style="cursor: not-allowed;">
333
- </rect>
334
- <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>
335
- `;
396
+ return `<rect id="submitButton" x="${x}" y="${y}" width="300" height="45" rx="22.5" fill="#666666" style="cursor:not-allowed;"></rect>
397
+ <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>`;
336
398
  }
337
399
 
338
400
  function enableSubmitButton() {
339
- const submitButton = document.getElementById('submitButton');
340
- const submitButtonText = submitButton.nextElementSibling;
341
-
342
- submitButton.setAttribute('fill', 'url(#buttonGradient)');
343
- submitButton.setAttribute('style', 'cursor: pointer;');
344
- submitButtonText.setAttribute('fill', 'white');
401
+ const b = document.getElementById('submitButton');
402
+ b.setAttribute('fill', 'url(#buttonGradient)');
403
+ b.setAttribute('style', 'cursor:pointer;');
404
+ b.nextElementSibling.setAttribute('fill', 'white');
345
405
  }
346
406
 
347
407
  function disableSubmitButton() {
348
- const submitButton = document.getElementById('submitButton');
349
- const submitButtonText = submitButton.nextElementSibling;
350
-
351
- submitButton.setAttribute('fill', '#666666');
352
- submitButton.setAttribute('style', 'cursor: not-allowed;');
353
- submitButtonText.setAttribute('fill', '#999999');
408
+ const b = document.getElementById('submitButton');
409
+ b.setAttribute('fill', '#666666');
410
+ b.setAttribute('style', 'cursor:not-allowed;');
411
+ b.nextElementSibling.setAttribute('fill', '#999999');
354
412
  }
355
413
 
356
414
  function validateForm(formJSON) {
357
- const requiredFields = Object.keys(formJSON).filter(key =>
358
- key !== 'form' && formJSON[key].required
359
- );
360
-
361
- const allValid = requiredFields.every(key => {
362
- const fieldConfig = formJSON[key];
363
-
364
- if (fieldConfig.type === 'segmented') {
365
- const selectorId = `${key.replace(/\s+/g, '')}Selector`;
366
- const selector = document.getElementById(selectorId);
367
- return selector && selector.value && selector.value.trim() !== '';
368
- } else {
369
- const inputId = `${key.replace(/\s+/g, '')}Input`;
370
- const input = document.getElementById(inputId);
371
- return input && input.value && input.value.trim() !== '';
372
- }
373
- });
374
-
375
- if (allValid) {
376
- enableSubmitButton();
377
- } else {
378
- disableSubmitButton();
379
- }
380
-
381
- // Also trigger overall validation
415
+ const allValid = Object.keys(formJSON)
416
+ .filter(k => k !== 'form' && formJSON[k].required)
417
+ .every(k => {
418
+ const el = document.getElementById(`${k.replace(/\s+/g,'')}Input`);
419
+ return el && el.value.trim() !== '';
420
+ });
421
+ allValid ? enableSubmitButton() : disableSubmitButton();
382
422
  setTimeout(() => validateAllForms(), 100);
383
-
384
423
  return allValid;
385
424
  }
386
425
 
@@ -388,259 +427,138 @@
388
427
 
389
428
  function getForm(formJSON, onSubmit) {
390
429
  const keys = Object.keys(formJSON);
391
- let currentY = 130; // Initialize currentY
430
+ let currentY = 130;
392
431
  const inputs = [];
393
-
394
- keys.forEach((key, index) => {
395
- if (key === "form") return;
396
-
397
- const fieldConfig = formJSON[key];
398
- if (fieldConfig.type === 'text-block') {
399
- const textBlock = getTextBlock(80, currentY, key, fieldConfig.content || fieldConfig.text);
400
- inputs.push(textBlock.svg);
401
- currentY += textBlock.height;
402
- } else if (fieldConfig.type === 'segmented') {
403
- inputs.push(getSegmentedSelector(80, currentY, key, fieldConfig.options));
404
- currentY += 70; // Standard field height
405
- } else {
406
- inputs.push(getInput(80, currentY, key, fieldConfig.type));
407
- currentY += 70; // Standard field height
408
- }
432
+
433
+ keys.forEach(key => {
434
+ if (key === 'form') return;
435
+ const fc = formJSON[key];
436
+ if (fc.type === 'text-block') {
437
+ const tb = getTextBlock(80, currentY, key, fc.content || fc.text);
438
+ inputs.push(tb.svg);
439
+ currentY += tb.height;
440
+ } else {
441
+ inputs.push(getInput(80, currentY, key, fc.type));
442
+ currentY += 70;
443
+ }
409
444
  });
410
-
411
- // Add submit button at the final currentY position
412
445
  inputs.push(getSubmitButton(100, currentY + 20));
413
-
414
- const svg = getBackgroundAndGradients(formJSON) + inputs.join('');
415
- const dynamicHeight = calculateFormHeight(formJSON);
416
446
 
417
- const container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
418
- container.setAttribute('viewBox', `0 0 500 ${dynamicHeight + 100}`);
447
+ const h = calculateFormHeight(formJSON);
448
+ const container = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
449
+ container.setAttribute('viewBox', `0 0 500 ${h + 100}`);
419
450
  container.setAttribute('width', '100%');
420
451
  container.setAttribute('height', 'auto');
421
- container.innerHTML = svg;
452
+ container.innerHTML = getBackgroundAndGradients(formJSON) + inputs.join('');
422
453
 
423
- // Rest of your event handler code remains the same...
424
454
  setTimeout(() => {
425
- Object.keys(formJSON).forEach((key, index) => {
426
- if(key === 'form') {
427
- return;
428
- }
429
-
430
- const fieldConfig = formJSON[key];
431
- const borderId = `${key.replace(/\s+/g, '')}Border`;
432
-
433
- if (fieldConfig.type === 'segmented') {
434
- const selectorId = `${key.replace(/\s+/g, '')}Selector`;
435
- const selector = document.getElementById(selectorId);
436
-
437
- // Add click handlers for segments
438
- fieldConfig.options.forEach((option, optionIndex) => {
439
- const segmentId = `${selectorId}_${optionIndex}`;
440
- const segment = document.getElementById(segmentId);
441
-
442
- if (segment) {
443
- segment.addEventListener('click', () => {
444
- // Clear all segments
445
- fieldConfig.options.forEach((_, clearIndex) => {
446
- const clearSegmentId = `${selectorId}_${clearIndex}`;
447
- const clearSegment = document.getElementById(clearSegmentId);
448
- if (clearSegment) {
449
- clearSegment.setAttribute('fill', '#1c1c20');
450
- clearSegment.setAttribute('data-selected', 'false');
451
- }
452
- });
453
-
454
- // Select clicked segment
455
- segment.setAttribute('fill', '#3eda82');
456
- segment.setAttribute('data-selected', 'true');
457
-
458
- // Update hidden input value
459
- if (selector) {
460
- selector.value = option;
461
- }
462
-
463
- // Update border
464
- const borderElement = document.getElementById(borderId);
465
- if (borderElement) {
466
- borderElement.setAttribute('stroke', 'url(#inputGradient)');
467
- }
468
-
469
- // Validate form
470
- validateForm(formJSON);
471
- });
472
- }
473
- });
474
- } else {
475
- const inputId = `${key.replace(/\s+/g, '')}Input`;
476
- const inputElement = document.getElementById(inputId);
477
-
478
- if (inputElement) {
479
- inputElement.addEventListener('input', (evt) => {
480
- const borderElement = document.getElementById(borderId);
481
- if (borderElement) {
482
- borderElement.setAttribute('stroke', 'url(#inputGradient)');
483
- }
484
-
485
- // Validate form
486
- validateForm(formJSON);
487
- });
488
- }
489
- }
490
- });
491
-
492
- const submitButton = document.getElementById('submitButton');
493
- if (submitButton) {
494
- submitButton.addEventListener('click', () => {
495
- if (!validateForm(formJSON)) {
496
- console.log('Form validation failed');
497
- return;
498
- }
499
-
500
- console.log('Submit button clicked');
501
-
502
- // Create button animation
503
- const animation = document.createElementNS("http://www.w3.org/2000/svg", "animate");
504
- animation.setAttribute("attributeName", "offset");
505
- animation.setAttribute("values", "-0.5;2.5");
506
- animation.setAttribute("dur", "300ms");
507
- animation.setAttribute("repeatCount", "1");
508
-
509
- const secondAnimation = document.createElementNS("http://www.w3.org/2000/svg", "animate");
510
- secondAnimation.setAttribute("attributeName", "offset");
511
- secondAnimation.setAttribute("values", "0.5;3.5");
512
- secondAnimation.setAttribute("dur", "300ms");
513
- secondAnimation.setAttribute("repeatCount", "1");
514
-
515
- const startGradient = document.getElementById('submitButtonGradientStart');
516
- const endGradient = document.getElementById('submitButtonGradientEnd');
517
-
518
- if (startGradient && endGradient) {
519
- startGradient.appendChild(animation);
520
- endGradient.appendChild(secondAnimation);
521
- animation.beginElement();
522
- secondAnimation.beginElement();
523
- }
524
-
525
- // Collect form values
526
- const formValues = {};
527
- Object.keys(formJSON).forEach((key) => {
528
- if(key === 'form') {
529
- return;
530
- }
531
-
532
- const fieldConfig = formJSON[key];
533
-
534
- if (fieldConfig.type === 'segmented') {
535
- const selectorId = `${key.replace(/\s+/g, '')}Selector`;
536
- const selector = document.getElementById(selectorId);
537
- if (selector) {
538
- formValues[key] = selector.value;
539
- }
540
- } else {
541
- const inputId = `${key.replace(/\s+/g, '')}Input`;
542
- const inputElement = document.getElementById(inputId);
543
- if (inputElement) {
544
- formValues[key] = inputElement.value;
545
- }
546
- }
547
- });
548
-
549
- if (onSubmit) {
550
- onSubmit(formValues);
551
- }
552
- });
553
- }
554
-
555
- // Initial validation
556
- validateForm(formJSON);
455
+ keys.forEach(key => {
456
+ if (key === 'form') return;
457
+ const borderId = `${key.replace(/\s+/g,'')}Border`;
458
+ const inputId = `${key.replace(/\s+/g,'')}Input`;
459
+ const inputEl = document.getElementById(inputId);
460
+ if (inputEl) {
461
+ inputEl.addEventListener('input', () => {
462
+ document.getElementById(borderId)?.setAttribute('stroke', 'url(#inputGradient)');
463
+ validateForm(formJSON);
464
+ });
465
+ }
466
+ });
467
+
468
+ const submitBtn = document.getElementById('submitButton');
469
+ if (submitBtn) {
470
+ submitBtn.addEventListener('click', () => {
471
+ if (!validateForm(formJSON)) return;
472
+ const vals = {};
473
+ keys.forEach(key => {
474
+ if (key === 'form') return;
475
+ const el = document.getElementById(`${key.replace(/\s+/g,'')}Input`);
476
+ if (el) vals[key] = el.value;
477
+ });
478
+ if (onSubmit) onSubmit(vals);
479
+ });
480
+ }
481
+ validateForm(formJSON);
557
482
  }, 100);
558
483
 
559
484
  return container;
560
485
  }
561
486
 
562
487
  window.getForm = getForm;
563
-
564
488
  })(window);
565
489
  </script>
566
490
 
567
491
  <script>
568
- // Parse ?payees=pubKey:amount:percent:addieUrl|... from the page URL.
569
- // Returns an array of payee objects, or null if the param is absent.
570
- function parseUrlPayees() {
571
- const raw = new URLSearchParams(window.location.search).get('payees');
572
- if (!raw) return null;
573
- return raw.split('|').map(t => {
574
- const [pubKey, amount, percent, addieUrl] = t.split(':');
575
- const p = {};
576
- if (pubKey) p.pubKey = pubKey;
577
- if (amount) p.amount = Number(amount);
578
- if (percent) p.percent = Number(percent);
579
- if (addieUrl) p.addieUrl = addieUrl;
580
- return p;
581
- }).filter(p => p.pubKey);
492
+ // Legacy path: buy button reveals the recovery key form
493
+ function validateAllForms() {
494
+ const paymentValid = typeof stripe !== 'undefined' && typeof elements !== 'undefined';
495
+ const btn = document.getElementById('submit-button');
496
+ if (!btn) return;
497
+ if (paymentValid) {
498
+ btn.classList.add('ready');
499
+ btn.disabled = false;
500
+ btn.textContent = 'Complete Purchase';
501
+ } else {
502
+ btn.classList.remove('ready');
503
+ btn.disabled = true;
504
+ btn.textContent = 'Complete Purchase';
505
+ }
506
+ }
507
+
508
+ if (!IS_SHOPPERE) {
509
+ document.getElementById('buy-button').addEventListener('click', function() {
510
+ const sec = document.getElementById('forms-section');
511
+ sec.style.display = 'block';
512
+ sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
513
+ });
582
514
  }
583
- const URL_PAYEES = parseUrlPayees();
584
515
 
585
516
  const formConfig = {
586
- "Recovery": {type: "text", required: true},
587
- "Explanation": {type: "text-block", content: "Put whatever you want here, but make sure you remember it. This is how you'll be able to download this again."}
517
+ 'Recovery': { type: 'text', required: true },
518
+ 'Explanation': { type: 'text-block', content: 'Put whatever you want here, but make sure you remember it. This is how you\'ll be able to download this again.' }
588
519
  };
589
520
 
590
521
  async function handleSubmit(formData) {
591
- window.formData = formData;
592
- const recoveryKey = formData.Recovery;
593
-
594
- const submitBtn = document.querySelector('[type=submit]');
595
- if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Checking…'; }
596
-
597
- try {
598
- const resp = await fetch('/plugin/shoppe/{{tenantUuid}}/purchase/intent', {
599
- method: 'POST',
600
- headers: { 'Content-Type': 'application/json' },
601
- body: JSON.stringify({ recoveryKey, productId: '{{productId}}', title: '{{title}}',
602
- ...(URL_PAYEES && { payees: URL_PAYEES }) })
603
- });
604
- const json = await resp.json();
522
+ window.formData = formData;
523
+ const recoveryKey = formData.Recovery;
605
524
 
606
- if (json.purchased) {
607
- window.location.href = '{{ebookUrl}}';
608
- return;
609
- }
610
-
611
- if (json.error) {
612
- alert('Error: ' + json.error);
613
- return;
614
- }
525
+ try {
526
+ const resp = await fetch('/plugin/shoppe/{{tenantUuid}}/purchase/intent', {
527
+ method: 'POST',
528
+ headers: { 'Content-Type': 'application/json' },
529
+ body: JSON.stringify({
530
+ recoveryKey,
531
+ productId: '{{productId}}',
532
+ title: '{{title}}',
533
+ ...(URL_PAYEES && { payees: URL_PAYEES })
534
+ })
535
+ });
536
+ const json = await resp.json();
615
537
 
616
- // Show payment element
617
- window.clientSecret = json.clientSecret;
618
- document.getElementById('payment-section').style.display = 'block';
619
- stripe = Stripe(json.publishableKey);
620
- elements = stripe.elements({ clientSecret: json.clientSecret });
621
- const paymentElement = elements.create('payment');
622
- paymentElement.mount('#payment-element');
623
- paymentElement.on('ready', () => setTimeout(validateAllForms, 500));
624
- paymentElement.on('change', () => setTimeout(validateAllForms, 100));
625
- } catch (err) {
626
- alert('Unexpected error: ' + err.message);
627
- } finally {
628
- if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Check'; }
629
- }
538
+ if (json.purchased) { window.location.href = '{{ebookUrl}}'; return; }
539
+ if (json.error) { alert('Error: ' + json.error); return; }
540
+
541
+ window.clientSecret = json.clientSecret;
542
+ document.getElementById('payment-section').style.display = 'block';
543
+ stripe = Stripe(json.publishableKey);
544
+ elements = stripe.elements({ clientSecret: json.clientSecret });
545
+ const payEl = elements.create('payment');
546
+ payEl.mount('#payment-element');
547
+ payEl.on('ready', () => setTimeout(validateAllForms, 500));
548
+ payEl.on('change', () => setTimeout(validateAllForms, 100));
549
+ } catch (err) {
550
+ alert('Unexpected error: ' + err.message);
551
+ }
630
552
  }
631
553
 
632
- const form = getForm(formConfig, handleSubmit);
633
- document.getElementById('address-form').appendChild(form);
554
+ if (!IS_SHOPPERE) {
555
+ const form = getForm(formConfig, handleSubmit);
556
+ document.getElementById('address-form').appendChild(form);
557
+ }
634
558
  </script>
635
559
 
636
- <script type="text/javascript">
637
- let stripe;
638
- let elements;
639
-
640
- const paymentForm = document.getElementById('payment-form');
641
- const submitButton = document.getElementById('submit-button');
642
- const errorMessage = document.getElementById('error-message');
643
- const loadingMessage = document.getElementById('loading');
560
+ <script>
561
+ let stripe, elements;
644
562
 
645
563
  window.confirmPayment = async () => {
646
564
  try {
@@ -649,49 +567,45 @@
649
567
  confirmParams: { return_url: '{{shoppeUrl}}' },
650
568
  redirect: 'if_required'
651
569
  });
570
+ if (error) { showError(error.message); return; }
652
571
 
653
- if (error) return showError(error.message);
654
-
655
- const order = { title: '{{title}}', productId: '{{productId}}', formData: window.formData };
656
- const paymentIntentId = window.clientSecret ? window.clientSecret.split('_secret_')[0] : undefined;
572
+ const paymentIntentId = window.clientSecret?.split('_secret_')[0];
657
573
  const resp = await fetch('/plugin/shoppe/{{tenantUuid}}/purchase/complete', {
658
574
  method: 'POST',
659
575
  headers: { 'Content-Type': 'application/json' },
660
- body: JSON.stringify({ recoveryKey: window.formData.Recovery, productId: '{{productId}}', order, paymentIntentId })
576
+ body: JSON.stringify({
577
+ recoveryKey: window.formData?.Recovery,
578
+ productId: '{{productId}}',
579
+ paymentIntentId
580
+ })
661
581
  });
662
582
  const json = await resp.json();
663
-
664
583
  if (json.success) {
665
584
  window.location.href = '{{ebookUrl}}';
666
585
  } else {
667
- window.alert('Payment successful, but error recording purchase. Contact greetings@planetnine.app');
586
+ alert('Payment successful, but error recording purchase. Contact greetings@planetnine.app');
668
587
  }
669
588
  } catch (err) {
670
589
  showError('An unexpected error occurred.');
671
- console.warn('payment error:', err);
672
590
  }
673
591
  };
674
592
 
675
- paymentForm.addEventListener('submit', async (event) => {
676
- event.preventDefault();
593
+ document.getElementById('payment-form').addEventListener('submit', async (e) => {
594
+ e.preventDefault();
677
595
  if (!stripe || !elements) return;
678
- setLoading(true);
596
+ document.getElementById('loading').style.display = 'block';
597
+ document.getElementById('submit-button').disabled = true;
679
598
  await window.confirmPayment();
680
- setLoading(false);
599
+ document.getElementById('loading').style.display = 'none';
600
+ document.getElementById('submit-button').disabled = false;
681
601
  });
682
602
 
683
- const showError = (message) => {
684
- errorMessage.textContent = message;
685
- errorMessage.style.display = 'block';
686
- setTimeout(() => { errorMessage.style.display = 'none'; errorMessage.textContent = ''; }, 5000);
687
- };
688
-
689
- const setLoading = (isLoading) => {
690
- submitButton.disabled = isLoading;
691
- loadingMessage.style.display = isLoading ? 'block' : 'none';
692
- };
603
+ function showError(msg) {
604
+ const el = document.getElementById('error-message');
605
+ el.textContent = msg;
606
+ el.style.display = 'block';
607
+ setTimeout(() => { el.style.display = 'none'; }, 5000);
608
+ }
693
609
  </script>
694
610
  </body>
695
611
  </html>
696
-
697
-