wiki-plugin-shoppe 0.0.21 โ 0.0.22
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
package/server/server.js
CHANGED
|
@@ -48,6 +48,12 @@ function saveConfig(config) {
|
|
|
48
48
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Derive the public-facing protocol from the request, respecting reverse-proxy headers.
|
|
52
|
+
// Behind HTTPS proxies req.protocol is 'http'; X-Forwarded-Proto carries the real value.
|
|
53
|
+
function reqProto(req) {
|
|
54
|
+
return (req.get('x-forwarded-proto') || req.protocol || 'https').split(',')[0].trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
function getSanoraUrl() {
|
|
52
58
|
const config = loadConfig();
|
|
53
59
|
if (config.sanoraUrl) return config.sanoraUrl.replace(/\/$/, '');
|
|
@@ -898,7 +904,7 @@ async function getShoppeGoods(tenant) {
|
|
|
898
904
|
// Fetch and parse the schedule JSON artifact for an appointment product.
|
|
899
905
|
async function getAppointmentSchedule(tenant, product) {
|
|
900
906
|
const sanoraUrl = getSanoraUrl();
|
|
901
|
-
const scheduleArtifact = (product.artifacts || []).find(a => a.
|
|
907
|
+
const scheduleArtifact = (product.artifacts || []).find(a => a.endsWith('.json'));
|
|
902
908
|
if (!scheduleArtifact) return null;
|
|
903
909
|
const resp = await fetch(`${sanoraUrl}/artifacts/${scheduleArtifact}`);
|
|
904
910
|
if (!resp.ok) return null;
|
|
@@ -948,20 +954,15 @@ function generateAvailableSlots(schedule, bookedSlots) {
|
|
|
948
954
|
const dateStr = dateFmt.format(date);
|
|
949
955
|
const dayName = weekdayFmt.format(date).toLowerCase();
|
|
950
956
|
const rule = (schedule.availability || []).find(a => a.day.toLowerCase() === dayName);
|
|
951
|
-
if (!rule) continue;
|
|
952
|
-
|
|
953
|
-
const [startH, startM] = rule.start.split(':').map(Number);
|
|
954
|
-
const [endH, endM] = rule.end.split(':').map(Number);
|
|
955
|
-
const startMins = startH * 60 + startM;
|
|
956
|
-
const endMins = endH * 60 + endM;
|
|
957
|
+
if (!rule || !rule.slots || !rule.slots.length) continue;
|
|
957
958
|
|
|
958
959
|
const slots = [];
|
|
959
|
-
for (
|
|
960
|
+
for (const slotTime of rule.slots) {
|
|
961
|
+
const [h, m] = slotTime.split(':').map(Number);
|
|
962
|
+
const slotMins = h * 60 + m;
|
|
960
963
|
// For today, skip slots within the next hour
|
|
961
|
-
if (d === 0 &&
|
|
962
|
-
const
|
|
963
|
-
const min = (m % 60).toString().padStart(2, '0');
|
|
964
|
-
const slotStr = `${dateStr}T${h}:${min}`;
|
|
964
|
+
if (d === 0 && slotMins <= nowMins + 60) continue;
|
|
965
|
+
const slotStr = `${dateStr}T${slotTime}`;
|
|
965
966
|
if (!bookedSet.has(slotStr)) slots.push(slotStr);
|
|
966
967
|
}
|
|
967
968
|
|
|
@@ -979,7 +980,7 @@ function generateAvailableSlots(schedule, bookedSlots) {
|
|
|
979
980
|
// Fetch tier metadata (benefits list, renewalDays) from the tier-info artifact.
|
|
980
981
|
async function getTierInfo(tenant, product) {
|
|
981
982
|
const sanoraUrl = getSanoraUrl();
|
|
982
|
-
const tierArtifact = (product.artifacts || []).find(a => a.
|
|
983
|
+
const tierArtifact = (product.artifacts || []).find(a => a.endsWith('.json'));
|
|
983
984
|
if (!tierArtifact) return null;
|
|
984
985
|
const resp = await fetch(`${sanoraUrl}/artifacts/${tierArtifact}`);
|
|
985
986
|
if (!resp.ok) return null;
|
|
@@ -1049,7 +1050,7 @@ function generateShoppeHTML(tenant, goods) {
|
|
|
1049
1050
|
{ id: 'albums', label: '๐ผ๏ธ Albums', count: goods.albums.length },
|
|
1050
1051
|
{ id: 'products', label: '๐ฆ Products', count: goods.products.length },
|
|
1051
1052
|
{ id: 'appointments', label: '๐
Appointments', count: goods.appointments.length },
|
|
1052
|
-
{ id: 'subscriptions', label: '๐
|
|
1053
|
+
{ id: 'subscriptions', label: '๐ Infuse', count: goods.subscriptions.length }
|
|
1053
1054
|
]
|
|
1054
1055
|
.filter(t => t.always || t.count > 0)
|
|
1055
1056
|
.map((t, i) => `<div class="tab${i === 0 ? ' active' : ''}" onclick="show('${t.id}',this)">${t.label} <span class="badge">${t.count}</span></div>`)
|
|
@@ -1108,7 +1109,7 @@ function generateShoppeHTML(tenant, goods) {
|
|
|
1108
1109
|
<div id="appointments" class="section"><div class="grid">${renderCards(goods.appointments, 'appointment')}</div></div>
|
|
1109
1110
|
<div id="subscriptions" class="section"><div class="grid">${renderCards(goods.subscriptions, 'subscription')}</div></div>
|
|
1110
1111
|
<div style="text-align:center;padding:24px 0 8px;font-size:14px;color:#888;">
|
|
1111
|
-
Already
|
|
1112
|
+
Already infusing? <a href="/plugin/shoppe/${tenant.uuid}/membership" style="color:#0066cc;">Access your membership โ</a>
|
|
1112
1113
|
</div>
|
|
1113
1114
|
</main>
|
|
1114
1115
|
<script>
|
|
@@ -1265,7 +1266,7 @@ async function startServer(params) {
|
|
|
1265
1266
|
|
|
1266
1267
|
const title = decodeURIComponent(req.params.title);
|
|
1267
1268
|
const sanoraUrlInternal = getSanoraUrl();
|
|
1268
|
-
const wikiOrigin = `${req
|
|
1269
|
+
const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
|
|
1269
1270
|
const sanoraUrl = `${wikiOrigin}/plugin/allyabase/sanora`;
|
|
1270
1271
|
const productsResp = await fetch(`${sanoraUrlInternal}/products/${tenant.uuid}`);
|
|
1271
1272
|
const products = await productsResp.json();
|
|
@@ -1326,7 +1327,7 @@ async function startServer(params) {
|
|
|
1326
1327
|
if (!product) return res.status(404).send('<h1>Appointment not found</h1>');
|
|
1327
1328
|
|
|
1328
1329
|
const schedule = await getAppointmentSchedule(tenant, product);
|
|
1329
|
-
const wikiOrigin = `${req
|
|
1330
|
+
const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
|
|
1330
1331
|
const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
|
|
1331
1332
|
const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
|
|
1332
1333
|
|
|
@@ -1393,7 +1394,7 @@ async function startServer(params) {
|
|
|
1393
1394
|
if (!product) return res.status(404).send('<h1>Tier not found</h1>');
|
|
1394
1395
|
|
|
1395
1396
|
const tierInfo = await getTierInfo(tenant, product);
|
|
1396
|
-
const wikiOrigin = `${req
|
|
1397
|
+
const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
|
|
1397
1398
|
const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
|
|
1398
1399
|
const imageUrl = product.image ? `${sanoraUrl}/images/${product.image}` : '';
|
|
1399
1400
|
const benefits = tierInfo && tierInfo.benefits
|
|
@@ -1425,7 +1426,7 @@ async function startServer(params) {
|
|
|
1425
1426
|
app.get('/plugin/shoppe/:identifier/membership', (req, res) => {
|
|
1426
1427
|
const tenant = getTenantByIdentifier(req.params.identifier);
|
|
1427
1428
|
if (!tenant) return res.status(404).send('<h1>Shoppe not found</h1>');
|
|
1428
|
-
const wikiOrigin = `${req
|
|
1429
|
+
const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
|
|
1429
1430
|
const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
|
|
1430
1431
|
const html = fillTemplate(SUBSCRIPTION_MEMBERSHIP_TMPL, { shoppeUrl, tenantUuid: tenant.uuid });
|
|
1431
1432
|
res.set('Content-Type', 'text/html');
|
|
@@ -1444,7 +1445,7 @@ async function startServer(params) {
|
|
|
1444
1445
|
const sanoraUrl = getSanoraUrl();
|
|
1445
1446
|
const productsResp = await fetch(`${sanoraUrl}/products/${tenant.uuid}`);
|
|
1446
1447
|
const products = await productsResp.json();
|
|
1447
|
-
const wikiOrigin = `${req
|
|
1448
|
+
const wikiOrigin = `${reqProto(req)}://${req.get('host')}`;
|
|
1448
1449
|
const shoppeUrl = `${wikiOrigin}/plugin/shoppe/${tenant.uuid}`;
|
|
1449
1450
|
|
|
1450
1451
|
const subscriptions = [];
|
|
@@ -1459,7 +1460,7 @@ async function startServer(params) {
|
|
|
1459
1460
|
// Only expose exclusive artifact URLs to active subscribers
|
|
1460
1461
|
const exclusiveArtifacts = status.active
|
|
1461
1462
|
? (product.artifacts || [])
|
|
1462
|
-
.filter(a => !a.
|
|
1463
|
+
.filter(a => !a.endsWith('.json'))
|
|
1463
1464
|
.map(a => ({ name: a.split('-').slice(1).join('-'), url: `${sanoraUrl}/artifacts/${a}` }))
|
|
1464
1465
|
: [];
|
|
1465
1466
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>Infuse Portal</title>
|
|
7
7
|
<style>
|
|
8
8
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
9
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f12; color: white; min-height: 100vh; }
|
|
@@ -75,14 +75,14 @@
|
|
|
75
75
|
</head>
|
|
76
76
|
<body>
|
|
77
77
|
<header>
|
|
78
|
-
<h1>๐
|
|
79
|
-
<p>Enter your recovery key to access your
|
|
78
|
+
<h1>๐ Infuse Portal</h1>
|
|
79
|
+
<p>Enter your recovery key to access your infuser benefits</p>
|
|
80
80
|
</header>
|
|
81
81
|
|
|
82
82
|
<div class="container">
|
|
83
83
|
|
|
84
84
|
<div id="key-section">
|
|
85
|
-
<h2>Check your
|
|
85
|
+
<h2>Check your infusions</h2>
|
|
86
86
|
<p>Enter the recovery key you used when you subscribed.</p>
|
|
87
87
|
<div class="input-row">
|
|
88
88
|
<input type="text" id="recovery-key" placeholder="Your recovery keyโฆ" autocomplete="off" onkeydown="if(event.key==='Enter')checkMembership()">
|
|
@@ -161,8 +161,8 @@
|
|
|
161
161
|
|
|
162
162
|
const activeCount = subscriptions.filter(s => s.active).length;
|
|
163
163
|
heading.textContent = activeCount > 0
|
|
164
|
-
? `You have ${activeCount} active
|
|
165
|
-
: 'No active
|
|
164
|
+
? `You have ${activeCount} active infusion${activeCount !== 1 ? 's' : ''}`
|
|
165
|
+
: 'No active infusions found';
|
|
166
166
|
|
|
167
167
|
container.innerHTML = '';
|
|
168
168
|
|
|
@@ -278,7 +278,7 @@
|
|
|
278
278
|
const price = (sub.price / 100).toFixed(2);
|
|
279
279
|
cta.innerHTML = `
|
|
280
280
|
<a class="btn-subscribe" href="${sub.subscribeUrl}">
|
|
281
|
-
${sub.renewsAt ? 'Renew' : '
|
|
281
|
+
${sub.renewsAt ? 'Renew' : 'Infuse'} โ $${price}/month
|
|
282
282
|
</a>
|
|
283
283
|
`;
|
|
284
284
|
body.appendChild(cta);
|
|
@@ -4,11 +4,11 @@
|
|
|
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.0, maximum-scale=1.0">
|
|
7
|
-
<meta name="twitter:title" content="
|
|
7
|
+
<meta name="twitter:title" content="Infuse: {{title}}">
|
|
8
8
|
<meta name="description" content="{{description}}">
|
|
9
|
-
<meta name="og:title" content="
|
|
9
|
+
<meta name="og:title" content="Infuse: {{title}}">
|
|
10
10
|
<meta name="og:description" content="{{description}}">
|
|
11
|
-
<title>
|
|
11
|
+
<title>Infuse: {{title}}</title>
|
|
12
12
|
<style>
|
|
13
13
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
14
14
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f12; color: white; min-height: 100vh; }
|
|
@@ -97,13 +97,13 @@
|
|
|
97
97
|
<div class="pay-panel">
|
|
98
98
|
|
|
99
99
|
<div id="already-subscribed">
|
|
100
|
-
<h3>โ
You're already
|
|
100
|
+
<h3>โ
You're already infusing!</h3>
|
|
101
101
|
<p id="already-desc"></p>
|
|
102
102
|
<button class="btn-outline" onclick="window.location.href='{{shoppeUrl}}/membership'">View your membership โ</button>
|
|
103
103
|
</div>
|
|
104
104
|
|
|
105
105
|
<div id="subscribe-form">
|
|
106
|
-
<h2>Become
|
|
106
|
+
<h2>Become an infuser</h2>
|
|
107
107
|
<div class="subtitle">Use a recovery key to access your membership and exclusive content.</div>
|
|
108
108
|
|
|
109
109
|
<div class="recovery-note">
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
</div>
|
|
121
121
|
|
|
122
122
|
<div id="payment-section">
|
|
123
|
-
<h2>Complete your
|
|
123
|
+
<h2>Complete your infusion</h2>
|
|
124
124
|
<div class="subtitle">${{formattedAmount}}/month ยท renews every {{renewalDays}} days</div>
|
|
125
125
|
<div id="payment-element"></div>
|
|
126
126
|
<button class="btn-primary" id="pay-btn" onclick="confirmPayment()">Pay ${{formattedAmount}}</button>
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
|
|
131
131
|
<div id="confirmation">
|
|
132
132
|
<div class="icon">๐</div>
|
|
133
|
-
<h2>Thank you for
|
|
133
|
+
<h2>Thank you for infusing!</h2>
|
|
134
134
|
<div class="renews" id="renews-display"></div>
|
|
135
135
|
<p>Use your recovery key any time at the <a href="{{shoppeUrl}}/membership" style="color:#7ec8e3;">membership portal</a> to access exclusive content and check your subscription status.</p>
|
|
136
136
|
<p style="margin-top:12px;font-size:12px;color:#555;">Keep your recovery key safe โ it's the only way to access your membership benefits.</p>
|
package/test/test.js
CHANGED
|
@@ -293,8 +293,8 @@ describe('Sanora integration', function() {
|
|
|
293
293
|
const resp = await fetch(`${BASE_URL}/plugin/shoppe/${tenant.uuid}`);
|
|
294
294
|
resp.status.should.equal(200);
|
|
295
295
|
const html = await resp.text();
|
|
296
|
-
html.should.include('
|
|
297
|
-
html.should.include('tab
|
|
296
|
+
html.should.include('Test Shoppe');
|
|
297
|
+
html.should.include('class="tab"');
|
|
298
298
|
html.should.include('membership');
|
|
299
299
|
});
|
|
300
300
|
|
|
@@ -315,7 +315,7 @@ describe('Sanora integration', function() {
|
|
|
315
315
|
html.should.include('Test Session');
|
|
316
316
|
html.should.include('/slots');
|
|
317
317
|
html.should.include('purchase/intent');
|
|
318
|
-
html.should.include('
|
|
318
|
+
html.should.include('Continue to Payment'); // paid booking label (price=10000 in test archive)
|
|
319
319
|
});
|
|
320
320
|
|
|
321
321
|
it('should serve the subscription sign-up page with benefits', async () => {
|