wiki-plugin-shoppe 0.0.14 → 0.0.15

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.14",
3
+ "version": "0.0.15",
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
@@ -10,8 +10,9 @@ const sessionless = require('sessionless-node');
10
10
  const SHOPPE_BASE_EMOJI = process.env.SHOPPE_BASE_EMOJI || '🛍️🎨🎁';
11
11
 
12
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');
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
+ const EBOOK_DOWNLOAD_TMPL = fs.readFileSync(path.join(TEMPLATES_DIR, 'ebook-download.html'), 'utf8');
15
16
 
16
17
  function getAllyabaseOrigin() {
17
18
  try { return new URL(getSanoraUrl()).origin; } catch { return getSanoraUrl(); }
@@ -959,7 +960,7 @@ async function startServer(params) {
959
960
  if (!product) return res.status(404).send('<h1>Product not found</h1>');
960
961
 
961
962
  const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
962
- const ebookUrl = `${sanoraUrl}/products/${tenant.uuid}/${encodeURIComponent(title)}/ebook-download`;
963
+ const ebookUrl = `${req.protocol}://${req.get('host')}/plugin/shoppe/${tenant.uuid}/download/${encodeURIComponent(title)}`;
963
964
  const shoppeUrl = `${req.protocol}://${req.get('host')}/plugin/shoppe/${tenant.uuid}`;
964
965
 
965
966
  const html = fillTemplate(templateHtml, {
@@ -993,6 +994,49 @@ async function startServer(params) {
993
994
  app.get('/plugin/shoppe/:identifier/buy/:title/address', (req, res) =>
994
995
  renderPurchasePage(req, res, ADDRESS_STRIPE_TMPL));
995
996
 
997
+ // Ebook download page (reached after successful payment + hash creation)
998
+ app.get('/plugin/shoppe/:identifier/download/:title', async (req, res) => {
999
+ try {
1000
+ const tenant = getTenantByIdentifier(req.params.identifier);
1001
+ if (!tenant) return res.status(404).send('<h1>Shoppe not found</h1>');
1002
+
1003
+ const title = decodeURIComponent(req.params.title);
1004
+ const sanoraUrl = getSanoraUrl();
1005
+ const productsResp = await fetch(`${sanoraUrl}/products/${tenant.uuid}`);
1006
+ const products = await productsResp.json();
1007
+ const product = products[title] || Object.values(products).find(p => p.title === title);
1008
+ if (!product) return res.status(404).send('<h1>Book not found</h1>');
1009
+
1010
+ const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
1011
+
1012
+ // Map artifact UUIDs to download paths by extension
1013
+ let epubPath = '', pdfPath = '', mobiPath = '';
1014
+ (product.artifacts || []).forEach(artifact => {
1015
+ if (artifact.includes('epub')) epubPath = `${sanoraUrl}/artifacts/${artifact}`;
1016
+ if (artifact.includes('pdf')) pdfPath = `${sanoraUrl}/artifacts/${artifact}`;
1017
+ if (artifact.includes('mobi')) mobiPath = `${sanoraUrl}/artifacts/${artifact}`;
1018
+ });
1019
+
1020
+ const html = fillTemplate(EBOOK_DOWNLOAD_TMPL, {
1021
+ title: product.title || title,
1022
+ description: product.description || '',
1023
+ image: imageUrl,
1024
+ productId: product.productId || '',
1025
+ pubKey: '',
1026
+ signature: '',
1027
+ epubPath,
1028
+ pdfPath,
1029
+ mobiPath
1030
+ });
1031
+
1032
+ res.set('Content-Type', 'text/html');
1033
+ res.send(html);
1034
+ } catch (err) {
1035
+ console.error('[shoppe] download page error:', err);
1036
+ res.status(500).send(`<h1>Error</h1><p>${err.message}</p>`);
1037
+ }
1038
+ });
1039
+
996
1040
  // Post reader — fetches markdown from Sanora and renders it as HTML
997
1041
  app.get('/plugin/shoppe/:identifier/post/:title', async (req, res) => {
998
1042
  try {
@@ -0,0 +1,331 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.00, maximum-scale=1.00, minimum-scale=1.00">
6
+
7
+ <!-- Web preview meta tags -->
8
+ <meta name="twitter:title" content="{{title}}">
9
+ <meta name="description" content="{{description}}">
10
+ <meta name="twitter:description" content="{{description}}">
11
+ <meta name="twitter:image" content="{{image}}">
12
+ <meta name="og:title" content="{{title}}">
13
+ <meta name="og:description" content="{{description}}">
14
+ <meta name="og:image" content="{{image}}">
15
+
16
+ <title>{{title}} - Download</title>
17
+
18
+ <style>
19
+ * {
20
+ margin: 0;
21
+ padding: 0;
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ body {
26
+ font-family: Arial, sans-serif;
27
+ height: 100vh;
28
+ overflow: hidden;
29
+ background: linear-gradient(135deg, #0f0f12 0%, #1a1a1e 100%);
30
+ color: white;
31
+ }
32
+
33
+ .main-container {
34
+ width: 100vw;
35
+ height: 100vh;
36
+ display: flex;
37
+ overflow: hidden;
38
+ }
39
+
40
+ .product-section {
41
+ flex: 1;
42
+ display: flex;
43
+ flex-direction: column;
44
+ justify-content: center;
45
+ align-items: center;
46
+ padding: 20px;
47
+ min-height: 0;
48
+ }
49
+
50
+ .download-section {
51
+ flex: 1;
52
+ display: flex;
53
+ flex-direction: column;
54
+ justify-content: center;
55
+ align-items: center;
56
+ padding: 20px;
57
+ min-height: 0;
58
+ background: rgba(42, 42, 46, 0.3);
59
+ border-left: 1px solid #444;
60
+ }
61
+
62
+ .product-content {
63
+ width: 100%;
64
+ max-width: 500px;
65
+ text-align: center;
66
+ }
67
+
68
+ .product-image {
69
+ width: 100%;
70
+ height: auto;
71
+ max-height: 40vh;
72
+ object-fit: contain;
73
+ border-radius: 12px;
74
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
75
+ margin-bottom: 20px;
76
+ }
77
+
78
+ .product-title {
79
+ font-size: clamp(1.2rem, 4vw, 2rem);
80
+ font-weight: bold;
81
+ margin-bottom: 10px;
82
+ background: linear-gradient(90deg, #3eda82, #a855f7);
83
+ -webkit-background-clip: text;
84
+ -webkit-text-fill-color: transparent;
85
+ background-clip: text;
86
+ }
87
+
88
+ .product-description {
89
+ font-size: clamp(0.9rem, 2.5vw, 1.1rem);
90
+ opacity: 0.8;
91
+ font-weight: normal;
92
+ line-height: 1.4;
93
+ }
94
+
95
+ .download-container {
96
+ width: 100%;
97
+ max-width: 400px;
98
+ }
99
+
100
+ .download-header {
101
+ text-align: center;
102
+ margin-bottom: 30px;
103
+ }
104
+
105
+ .download-title {
106
+ font-size: 1.8rem;
107
+ font-weight: bold;
108
+ margin-bottom: 10px;
109
+ color: #ffffff;
110
+ }
111
+
112
+ .download-subtitle {
113
+ font-size: 1rem;
114
+ opacity: 0.7;
115
+ color: #bbbbbb;
116
+ }
117
+
118
+ .format-grid {
119
+ display: grid;
120
+ gap: 15px;
121
+ margin-bottom: 20px;
122
+ }
123
+
124
+ .format-button {
125
+ background: linear-gradient(135deg, #2a2a2e 0%, #323236 100%);
126
+ border: 2px solid #444;
127
+ border-radius: 12px;
128
+ padding: 20px;
129
+ cursor: pointer;
130
+ transition: all 0.3s ease;
131
+ text-decoration: none;
132
+ color: white;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: space-between;
136
+ position: relative;
137
+ overflow: hidden;
138
+ }
139
+
140
+ .format-button:hover {
141
+ border-color: #3eda82;
142
+ transform: translateY(-2px);
143
+ box-shadow: 0 8px 25px rgba(62, 218, 130, 0.2);
144
+ }
145
+
146
+ .format-button:active {
147
+ transform: translateY(0);
148
+ }
149
+
150
+ .format-button::before {
151
+ content: '';
152
+ position: absolute;
153
+ top: 0;
154
+ left: -100%;
155
+ width: 100%;
156
+ height: 100%;
157
+ background: linear-gradient(90deg, transparent, rgba(62, 218, 130, 0.1), transparent);
158
+ transition: left 0.5s ease;
159
+ }
160
+
161
+ .format-button:hover::before {
162
+ left: 100%;
163
+ }
164
+
165
+ .format-info {
166
+ display: flex;
167
+ flex-direction: column;
168
+ align-items: flex-start;
169
+ }
170
+
171
+ .format-name {
172
+ font-size: 1.1rem;
173
+ font-weight: bold;
174
+ margin-bottom: 5px;
175
+ }
176
+
177
+ .format-description {
178
+ font-size: 0.85rem;
179
+ opacity: 0.7;
180
+ }
181
+
182
+ .download-icon {
183
+ font-size: 1.5rem;
184
+ opacity: 0.8;
185
+ }
186
+
187
+ .bulk-download {
188
+ margin-top: 20px;
189
+ padding-top: 20px;
190
+ border-top: 1px solid #444;
191
+ }
192
+
193
+ .bulk-button {
194
+ background: linear-gradient(90deg, #3eda82, #a855f7);
195
+ border: none;
196
+ border-radius: 12px;
197
+ padding: 15px 25px;
198
+ color: white;
199
+ font-size: 1rem;
200
+ font-weight: bold;
201
+ cursor: pointer;
202
+ width: 100%;
203
+ transition: all 0.3s ease;
204
+ text-decoration: none;
205
+ display: inline-block;
206
+ text-align: center;
207
+ }
208
+
209
+ .bulk-button:hover {
210
+ transform: translateY(-2px);
211
+ box-shadow: 0 8px 25px rgba(62, 218, 130, 0.3);
212
+ }
213
+
214
+ @media (max-width: 768px) {
215
+ .main-container {
216
+ flex-direction: column;
217
+ }
218
+
219
+ .download-section {
220
+ border-left: none;
221
+ border-top: 1px solid #444;
222
+ }
223
+
224
+ .product-image {
225
+ max-height: 30vh;
226
+ }
227
+ }
228
+ </style>
229
+ </head>
230
+ <body>
231
+ <div class="main-container">
232
+ <!-- Product Section -->
233
+ <div class="product-section">
234
+ <div class="product-content">
235
+ <img id="product-image" class="product-image" src="{{image}}" alt="{{title}}">
236
+ <h1 class="product-title">{{title}}</h1>
237
+ <p class="product-description">{{description}}</p>
238
+ </div>
239
+ </div>
240
+
241
+ <!-- Download Section -->
242
+ <div class="download-section">
243
+ <div class="download-container">
244
+ <div class="download-header">
245
+ <h2 class="download-title">Download Your Book</h2>
246
+ <p class="download-subtitle">Choose your preferred format</p>
247
+ </div>
248
+
249
+ <div class="format-grid" id="format-grid">
250
+ <!-- Format buttons will be inserted here -->
251
+ </div>
252
+
253
+ <div class="bulk-download">
254
+ <a href="#" class="bulk-button" id="bulk-download">
255
+ 📦 Download All Formats
256
+ </a>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ </div>
261
+
262
+ <script>
263
+ // Configuration - replace with your actual file paths
264
+ const downloadConfig = Object.fromEntries(Object.entries({
265
+ epub: { path: "{{epubPath}}", name: "EPUB", description: "Best for e-readers & mobile devices", icon: "📱" },
266
+ pdf: { path: "{{pdfPath}}", name: "PDF", description: "Universal format, preserves layout", icon: "📄" },
267
+ mobi: { path: "{{mobiPath}}", name: "MOBI", description: "Optimized for Kindle devices", icon: "📚" }
268
+ }).filter(([, c]) => c.path));
269
+
270
+ function createFormatButton(format, config) {
271
+ const button = document.createElement('a');
272
+ button.className = 'format-button';
273
+ button.href = config.path;
274
+ button.download = `{{title}}.${format}`;
275
+
276
+ button.innerHTML = `
277
+ <div class="format-info">
278
+ <div class="format-name">${config.icon} ${config.name}</div>
279
+ <div class="format-description">${config.description}</div>
280
+ </div>
281
+ <div class="download-icon">⬇️</div>
282
+ `;
283
+
284
+ // Add download tracking
285
+ button.addEventListener('click', (e) => {
286
+ console.log(`Downloading ${format.toUpperCase()} format`);
287
+ // You can add analytics or tracking here
288
+ });
289
+
290
+ return button;
291
+ }
292
+
293
+ function setupBulkDownload() {
294
+ const bulkButton = document.getElementById('bulk-download');
295
+ bulkButton.addEventListener('click', (e) => {
296
+ e.preventDefault();
297
+ console.log('Starting bulk download...');
298
+
299
+ // Download each format with a small delay
300
+ Object.entries(downloadConfig).forEach(([format, config], index) => {
301
+ setTimeout(() => {
302
+ const link = document.createElement('a');
303
+ link.href = config.path;
304
+ link.download = `{{title}}.${format}`;
305
+ link.style.display = 'none';
306
+ document.body.appendChild(link);
307
+ link.click();
308
+ document.body.removeChild(link);
309
+ }, index * 500); // 500ms delay between downloads
310
+ });
311
+ });
312
+ }
313
+
314
+ function initializeDownloads() {
315
+ const formatGrid = document.getElementById('format-grid');
316
+
317
+ // Create format buttons
318
+ Object.entries(downloadConfig).forEach(([format, config]) => {
319
+ const button = createFormatButton(format, config);
320
+ formatGrid.appendChild(button);
321
+ });
322
+
323
+ // Setup bulk download
324
+ setupBulkDownload();
325
+ }
326
+
327
+ // Initialize when page loads
328
+ document.addEventListener('DOMContentLoaded', initializeDownloads);
329
+ </script>
330
+ </body>
331
+ </html>
@@ -484,7 +484,7 @@
484
484
  payees: []
485
485
  };
486
486
 
487
- const res = await fetch(`{{allyabaseOrigin}}/processor/stripe/intent`, {
487
+ const res = await fetch(`{{allyabaseOrigin}}/plugin/allyabase/addie/processor/stripe/intent`, {
488
488
  method: 'put',
489
489
  body: JSON.stringify(payload),
490
490
  headers: {'Content-Type': 'application/json'}
@@ -624,7 +624,7 @@
624
624
  payees: []
625
625
  };
626
626
 
627
- const res = await fetch(`{{allyabaseOrigin}}/processor/stripe/intent`, {
627
+ const res = await fetch(`{{allyabaseOrigin}}/plugin/allyabase/addie/processor/stripe/intent`, {
628
628
  method: 'put',
629
629
  body: JSON.stringify(payload),
630
630
  headers: {'Content-Type': 'application/json'}