ultimate-jekyll-manager 1.0.22 → 1.1.1
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/CHANGELOG.md +35 -0
- package/CLAUDE.md +212 -1
- package/dist/assets/js/core/auth.js +2 -1
- package/dist/assets/js/libs/form-manager.js +1 -1
- package/dist/assets/js/libs/payment-config.js +44 -0
- package/dist/assets/js/libs/prerendered-icons.js +2 -1
- package/dist/assets/js/modules/redirect.js +15 -7
- package/dist/assets/js/modules/vert.js +5 -1
- package/dist/assets/js/pages/account/index.js +6 -35
- package/dist/assets/js/pages/account/sections/billing.js +9 -9
- package/dist/assets/js/pages/account/sections/connections.js +10 -10
- package/dist/assets/js/pages/account/sections/referrals.js +10 -18
- package/dist/assets/js/pages/account/sections/refund.js +2 -2
- package/dist/assets/js/pages/account/sections/security.js +1 -1
- package/dist/assets/js/pages/account/sections/team.js +1 -1
- package/dist/assets/js/pages/admin/calendar/campaign-preview.js +7 -1
- package/dist/assets/js/pages/admin/dashboard/index.js +3 -8
- package/dist/assets/js/pages/payment/checkout/index.js +15 -22
- package/dist/assets/js/pages/payment/checkout/modules/api.js +0 -11
- package/dist/assets/js/pages/payment/checkout/modules/state.js +1 -2
- package/dist/assets/js/pages/pricing/index.js +15 -9
- package/dist/defaults/dist/_includes/core/foot.html +1 -0
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html +123 -28
- package/dist/defaults/dist/redirects/misc/search-cse.html +1 -9
- package/dist/defaults/src/_config.yml +11 -12
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,41 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
17
|
---
|
|
18
|
+
## [1.1.1] - 2026-04-06
|
|
19
|
+
### Security
|
|
20
|
+
- Fix open redirect via `authReturnUrl` URL parameter in core/auth.js — now validated with `isValidRedirectUrl()`
|
|
21
|
+
- Fix cross-origin redirect via unvalidated postMessage in vert.js — added origin allowlist
|
|
22
|
+
- Replace `new Function()` code execution in redirect.js with safe named modifier lookup
|
|
23
|
+
- Sanitize markdown-it output with DOMPurify in campaign-preview.js (newsletter-safe tag allowlist)
|
|
24
|
+
- Validate OAuth redirect URL scheme in connections.js
|
|
25
|
+
- Escape `classes` parameter in prerendered-icons.js to prevent attribute breakout
|
|
26
|
+
- Defense-in-depth: escape `formatDate()` outputs in security.js, team.js, referrals.js
|
|
27
|
+
- Defense-in-depth: escape cancel/refund reason strings in billing.js, refund.js
|
|
28
|
+
- Defense-in-depth: escape `submittingText` in form-manager.js spinner
|
|
29
|
+
- Document redirect validation, postMessage origin checks, eval prohibition, and DOMPurify rules in CLAUDE.md
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- `dompurify` dependency for HTML sanitization
|
|
33
|
+
|
|
34
|
+
## [1.1.0] - 2026-04-06
|
|
35
|
+
### Added
|
|
36
|
+
- `payment-config.js` shared library for reading payment data from build-time config
|
|
37
|
+
- Pricing layout resolves prices and feature limits from `_config.yml` when not set in frontmatter
|
|
38
|
+
- `oauth2` config injected into client-side Configuration object via `foot.html`
|
|
39
|
+
- Pricing page shows "Switch to This Plan" on other paid plans when user has active subscription
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- Move `payment` under `web_manager` in default `_config.yml` so it serializes into client-side config
|
|
43
|
+
- Checkout page uses `payment-config.js` instead of fetching `/backend-manager/brand`
|
|
44
|
+
- Account billing section uses config for products/limits/currency instead of brand API
|
|
45
|
+
- Account connections section reads `oauth2` from config instead of brand API
|
|
46
|
+
- Admin dashboard uses config for product list in MRR calculations
|
|
47
|
+
- Remove `/backend-manager/brand` fetch from account page entirely
|
|
48
|
+
- "Everything in [plan]" now uses dynamic previous plan name instead of hardcoded index
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- Liquid 4.x compatibility: use loop-based hash lookup instead of bracket notation for config limits
|
|
52
|
+
|
|
18
53
|
## [1.0.22] - 2026-04-05
|
|
19
54
|
### Changed
|
|
20
55
|
- Bump web-manager from ^4.1.36 to ^4.1.37
|
package/CLAUDE.md
CHANGED
|
@@ -429,6 +429,69 @@ $el.textContent = data.message; // Safe — no escaping needed
|
|
|
429
429
|
|
|
430
430
|
Only use `innerHTML` when you need actual HTML structure (tags, classes, etc.), and escape every dynamic value in it.
|
|
431
431
|
|
|
432
|
+
### Even "Safe" Values Must Be Escaped
|
|
433
|
+
Even values that *seem* safe (like `Date.toLocaleDateString()` output, numeric calculations, or hardcoded config strings) MUST be escaped when inserted via `innerHTML`. This is defense-in-depth — if the data source ever changes, the escaping is already in place.
|
|
434
|
+
|
|
435
|
+
```javascript
|
|
436
|
+
// ✅ CORRECT — escape even "safe" values in innerHTML
|
|
437
|
+
$el.innerHTML = `<small>${webManager.utilities().escapeHTML(formatDate(timestamp))}</small>`;
|
|
438
|
+
$el.innerHTML = `<span>${webManager.utilities().escapeHTML(reason)}</span>`;
|
|
439
|
+
|
|
440
|
+
// ❌ WRONG — assuming the value is safe because it's from a date formatter
|
|
441
|
+
$el.innerHTML = `<small>${formatDate(timestamp)}</small>`;
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Redirects Must Be Validated
|
|
445
|
+
Never redirect to a URL from untrusted sources without validation:
|
|
446
|
+
|
|
447
|
+
```javascript
|
|
448
|
+
// ✅ CORRECT — validate before redirect
|
|
449
|
+
const url = urlParams.get('returnUrl');
|
|
450
|
+
if (url && webManager.isValidRedirectUrl(url)) {
|
|
451
|
+
window.location.href = url;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ✅ CORRECT — validate API response URLs have safe scheme
|
|
455
|
+
if (response.url && /^https?:\/\//i.test(response.url)) {
|
|
456
|
+
window.location.href = response.url;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ❌ WRONG — redirect to unvalidated input
|
|
460
|
+
window.location.href = urlParams.get('returnUrl');
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### postMessage Handlers Must Check Origin
|
|
464
|
+
Always validate `event.origin` when handling `window.addEventListener('message', ...)`:
|
|
465
|
+
|
|
466
|
+
```javascript
|
|
467
|
+
// ✅ CORRECT
|
|
468
|
+
window.addEventListener('message', (event) => {
|
|
469
|
+
if (event.origin !== window.location.origin && event.origin !== 'https://trusted-domain.com') {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// handle message
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ❌ WRONG — any origin can send messages
|
|
476
|
+
window.addEventListener('message', (event) => {
|
|
477
|
+
window.location.href = event.data.url; // attacker-controlled redirect
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Never Use eval() or new Function()
|
|
482
|
+
Do not use `eval()`, `new Function()`, `setTimeout(string)`, or `setInterval(string)`. These execute arbitrary code and violate CSP policies.
|
|
483
|
+
|
|
484
|
+
### Sanitize Markdown/Rich Text Output
|
|
485
|
+
When rendering user-authored markdown or rich text, use DOMPurify to sanitize the output:
|
|
486
|
+
|
|
487
|
+
```javascript
|
|
488
|
+
import DOMPurify from 'dompurify';
|
|
489
|
+
const safeHTML = DOMPurify.sanitize(md.render(userContent), {
|
|
490
|
+
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'p', 'br', 'a', 'b', 'strong', 'i', 'em', 'ul', 'ol', 'li', 'img', 'code', 'pre'],
|
|
491
|
+
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'target', 'rel'],
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
432
495
|
### Do NOT Escape Values Passed to textContent-Based APIs
|
|
433
496
|
`showNotification()`, `formManager.showSuccess()`, `formManager.showError()`, and `textContent` assignments use safe text insertion internally. Pre-escaping these causes double-encoding (e.g., `We'll` displays as `We'll`).
|
|
434
497
|
|
|
@@ -1108,12 +1171,24 @@ const response = await authorizedFetch(url, options);
|
|
|
1108
1171
|
- No need to manually call `webManager.auth().getIdToken()`
|
|
1109
1172
|
- Automatic token injection as Authorization Bearer header
|
|
1110
1173
|
- Centralized authentication handling
|
|
1174
|
+
- Automatic usage sync: extracts `bm-properties` header from every response and updates `webManager.bindings()` with fresh usage data under the `usage` key
|
|
1175
|
+
|
|
1176
|
+
**Options pass-through:** All `wonderful-fetch` options (`response`, `output`, `body`, `timeout`, etc.) are passed through untouched. Internally, `authorizedFetch` uses `output: 'complete'` to read response headers, then returns only the body by default. If the caller passes `output: 'complete'`, they get the full `{ status, headers, body }` response.
|
|
1177
|
+
|
|
1178
|
+
**Automatic Usage Binding Sync:**
|
|
1179
|
+
|
|
1180
|
+
After every successful response, `authorizedFetch` reads the `bm-properties` header and updates the `usage` bindings key:
|
|
1181
|
+
```javascript
|
|
1182
|
+
// After an API call, bindings are automatically updated:
|
|
1183
|
+
// usage.credits = { monthly: 5, daily: 2, limit: 100 }
|
|
1184
|
+
```
|
|
1185
|
+
This means any `data-wm-bind` elements bound to `usage.*` paths are automatically kept in sync without any manual work. See "Usage Bindings" below.
|
|
1111
1186
|
|
|
1112
1187
|
**⚠️ IMPORTANT: Auth State Requirement**
|
|
1113
1188
|
|
|
1114
1189
|
`authorizedFetch` requires Firebase Auth to have determined the current user's authentication state before being called. On fresh page loads (e.g., OAuth callback pages, deep links), Firebase Auth needs time to restore the session from IndexedDB/localStorage.
|
|
1115
1190
|
|
|
1116
|
-
**If called before auth state is determined, it will
|
|
1191
|
+
**If called before auth state is determined, it will warn: `"No authenticated user found"`**
|
|
1117
1192
|
|
|
1118
1193
|
**Solution:** Wait for auth state before calling `authorizedFetch`:
|
|
1119
1194
|
|
|
@@ -1136,6 +1211,142 @@ webManager.auth().listen({ once: true }, async () => {
|
|
|
1136
1211
|
|
|
1137
1212
|
**Reference:** `src/assets/js/libs/authorized-fetch.js`
|
|
1138
1213
|
|
|
1214
|
+
#### Usage Bindings
|
|
1215
|
+
|
|
1216
|
+
Usage data is available in the `usage` bindings key. It is populated from two sources:
|
|
1217
|
+
|
|
1218
|
+
1. **On page load (auth settle):** `web-manager` reads `account.usage` from Firestore and resolves plan limits from `config.payment.plans`, then sets `usage` bindings with the merged data.
|
|
1219
|
+
2. **After API calls:** `authorizedFetch` reads the `bm-properties` response header and merges fresh usage counters + limits into the existing `usage` bindings.
|
|
1220
|
+
|
|
1221
|
+
**Bindings structure:**
|
|
1222
|
+
```javascript
|
|
1223
|
+
// usage.credits = { monthly: 5, daily: 2, limit: 100 }
|
|
1224
|
+
// usage.requests = { monthly: 20, limit: 500 }
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
**HTML usage:**
|
|
1228
|
+
```html
|
|
1229
|
+
<!-- Show usage counter: "5/100" -->
|
|
1230
|
+
<span data-wm-bind="@show usage.credits">
|
|
1231
|
+
<span data-wm-bind="usage.credits.monthly">–</span>/<span data-wm-bind="usage.credits.limit">–</span>
|
|
1232
|
+
</span>
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
**Config requirement:** Plan limits must be defined in `_config.yml` under `web_manager.payment.plans`:
|
|
1236
|
+
```yaml
|
|
1237
|
+
web_manager:
|
|
1238
|
+
payment:
|
|
1239
|
+
plans:
|
|
1240
|
+
- id: basic
|
|
1241
|
+
limits:
|
|
1242
|
+
credits: 100
|
|
1243
|
+
- id: premium
|
|
1244
|
+
limits:
|
|
1245
|
+
credits: 500
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
#### Payment Config Library
|
|
1249
|
+
|
|
1250
|
+
Reads payment configuration (products, processors, prices, limits) from `webManager.config.payment` — populated from `_config.yml` at build time. **Do NOT fetch `/backend-manager/brand` to get payment data.** It's already available instantly via this library.
|
|
1251
|
+
|
|
1252
|
+
**Import:**
|
|
1253
|
+
```javascript
|
|
1254
|
+
import { getPaymentConfig, getProcessors, getProducts, getProductById, getProductLimits, getCurrency } from '__main_assets__/js/libs/payment-config.js';
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
**Usage:**
|
|
1258
|
+
```javascript
|
|
1259
|
+
// Get all products
|
|
1260
|
+
const products = getProducts();
|
|
1261
|
+
|
|
1262
|
+
// Find a specific product
|
|
1263
|
+
const product = getProductById('plus');
|
|
1264
|
+
|
|
1265
|
+
// Get product limits
|
|
1266
|
+
const limits = getProductLimits('plus'); // { credits: 500, agents: 3, ... }
|
|
1267
|
+
|
|
1268
|
+
// Get processors (stripe, paypal, etc.)
|
|
1269
|
+
const processors = getProcessors();
|
|
1270
|
+
```
|
|
1271
|
+
|
|
1272
|
+
**Config location in `_config.yml`:**
|
|
1273
|
+
```yaml
|
|
1274
|
+
web_manager:
|
|
1275
|
+
payment:
|
|
1276
|
+
processors:
|
|
1277
|
+
stripe:
|
|
1278
|
+
publishableKey: pk_live_...
|
|
1279
|
+
paypal:
|
|
1280
|
+
clientId: ...
|
|
1281
|
+
products:
|
|
1282
|
+
- id: basic
|
|
1283
|
+
name: Basic
|
|
1284
|
+
limits:
|
|
1285
|
+
credits: 100
|
|
1286
|
+
- id: plus
|
|
1287
|
+
name: Plus
|
|
1288
|
+
limits:
|
|
1289
|
+
credits: 500
|
|
1290
|
+
prices:
|
|
1291
|
+
monthly: 19
|
|
1292
|
+
annually: 190
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
**How it works:** The `foot.html` Configuration injection serializes all `web_manager` properties into `window.Configuration`, which `webManager.initialize()` stores in `webManager.config`. The payment config is available immediately — no API call needed.
|
|
1296
|
+
|
|
1297
|
+
**When to still use the brand API:**
|
|
1298
|
+
- `oauth2` provider configuration (used by the connections section on the account page)
|
|
1299
|
+
- Any data that is NOT in `_config.yml` and only exists server-side
|
|
1300
|
+
|
|
1301
|
+
**Reference:** `src/assets/js/libs/payment-config.js`
|
|
1302
|
+
|
|
1303
|
+
#### Pricing Page: Config-Resolved Values
|
|
1304
|
+
|
|
1305
|
+
The pricing layout automatically resolves prices and feature limits from `_config.yml` when not explicitly set in frontmatter. This means consuming projects can define ONLY display metadata (name, tagline, icon, features list) and let prices/limits come from the single source of truth.
|
|
1306
|
+
|
|
1307
|
+
**Resolution order (frontmatter wins):**
|
|
1308
|
+
1. `plan.pricing.monthly` / `plan.pricing.annually` from page frontmatter
|
|
1309
|
+
2. `site.web_manager.payment.products[matching_id].prices.monthly` / `.annually` from config
|
|
1310
|
+
3. `0` (default)
|
|
1311
|
+
|
|
1312
|
+
**Feature value resolution:**
|
|
1313
|
+
1. `feature.value` from page frontmatter
|
|
1314
|
+
2. `site.web_manager.payment.products[matching_id].limits[feature.id]` from config (with `-1` → `"Unlimited"`)
|
|
1315
|
+
|
|
1316
|
+
**Example: Minimal pricing.md (prices/limits come from config):**
|
|
1317
|
+
```yaml
|
|
1318
|
+
---
|
|
1319
|
+
layout: blueprint/pricing
|
|
1320
|
+
permalink: /pricing
|
|
1321
|
+
|
|
1322
|
+
pricing:
|
|
1323
|
+
plans:
|
|
1324
|
+
- id: "basic"
|
|
1325
|
+
name: "Basic"
|
|
1326
|
+
tagline: "best for getting started"
|
|
1327
|
+
url: "/dashboard"
|
|
1328
|
+
features:
|
|
1329
|
+
- id: "credits"
|
|
1330
|
+
name: "Credits"
|
|
1331
|
+
icon: "sparkles"
|
|
1332
|
+
- id: "agents"
|
|
1333
|
+
name: "Agents"
|
|
1334
|
+
icon: "robot"
|
|
1335
|
+
- id: "plus"
|
|
1336
|
+
name: "Plus"
|
|
1337
|
+
tagline: "best for small websites"
|
|
1338
|
+
features:
|
|
1339
|
+
- id: "credits"
|
|
1340
|
+
name: "Credits"
|
|
1341
|
+
icon: "sparkles"
|
|
1342
|
+
- id: "agents"
|
|
1343
|
+
name: "Agents"
|
|
1344
|
+
icon: "robot"
|
|
1345
|
+
---
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
In this example, `credits` value of 100 and price of $19/mo come from `_config.yml`'s `web_manager.payment.products` — no hardcoding needed.
|
|
1349
|
+
|
|
1139
1350
|
#### FormManager Library
|
|
1140
1351
|
|
|
1141
1352
|
Lightweight form state management library with built-in validation, state machine, and event system.
|
|
@@ -38,7 +38,8 @@ export default function () {
|
|
|
38
38
|
webManager.auth().listen({}, async (state) => {
|
|
39
39
|
const user = state.user;
|
|
40
40
|
const url = new URL(window.location.href);
|
|
41
|
-
const
|
|
41
|
+
const authReturnUrlRaw = url.searchParams.get('authReturnUrl');
|
|
42
|
+
const authReturnUrl = authReturnUrlRaw && webManager.isValidRedirectUrl(authReturnUrlRaw) ? authReturnUrlRaw : null;
|
|
42
43
|
const authSignout = url.searchParams.get('authSignout');
|
|
43
44
|
|
|
44
45
|
// Log
|
|
@@ -803,7 +803,7 @@ export class FormManager {
|
|
|
803
803
|
if (show) {
|
|
804
804
|
// Store original content
|
|
805
805
|
$btn._originalHTML = $btn.innerHTML;
|
|
806
|
-
$btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${this.config.submittingText}`;
|
|
806
|
+
$btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${webManager.utilities().escapeHTML(this.config.submittingText)}`;
|
|
807
807
|
} else if ($btn._originalHTML) {
|
|
808
808
|
$btn.innerHTML = $btn._originalHTML;
|
|
809
809
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Config Library
|
|
3
|
+
*
|
|
4
|
+
* Reads payment configuration (products, processors, prices, limits) from
|
|
5
|
+
* webManager.config.payment — which is populated from _config.yml at build time.
|
|
6
|
+
* This eliminates the need to fetch /backend-manager/brand at runtime.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import webManager from 'web-manager';
|
|
10
|
+
|
|
11
|
+
// Get the full payment config object
|
|
12
|
+
export function getPaymentConfig() {
|
|
13
|
+
return webManager.config?.payment || {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Get payment processors
|
|
17
|
+
export function getProcessors() {
|
|
18
|
+
return getPaymentConfig().processors || {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get all products
|
|
22
|
+
export function getProducts() {
|
|
23
|
+
return getPaymentConfig().products || [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Find a product by ID
|
|
27
|
+
export function getProductById(productId) {
|
|
28
|
+
return getProducts().find(p => p.id === productId) || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get a product's limits
|
|
32
|
+
export function getProductLimits(productId) {
|
|
33
|
+
return getProductById(productId)?.limits || {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get a product's prices
|
|
37
|
+
export function getProductPrices(productId) {
|
|
38
|
+
return getProductById(productId)?.prices || {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get payment currency
|
|
42
|
+
export function getCurrency() {
|
|
43
|
+
return getPaymentConfig().currency || 'USD';
|
|
44
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Prerendered Icons Library
|
|
3
3
|
* Retrieves pre-rendered icon HTML from the frontmatter icon system
|
|
4
4
|
*/
|
|
5
|
+
import webManager from 'web-manager';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Get pre-rendered icon by name from frontmatter icon system.
|
|
@@ -31,5 +32,5 @@ export function getPrerenderedIcon(iconName, classes) {
|
|
|
31
32
|
return $iconTemplate.innerHTML;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
return $iconTemplate.innerHTML.replace('class="fa"', `class="fa ${classes}"`);
|
|
35
|
+
return $iconTemplate.innerHTML.replace('class="fa"', `class="fa ${webManager.utilities().escapeHTML(classes)}"`);
|
|
35
36
|
}
|
|
@@ -34,15 +34,23 @@ const performRedirect = () => {
|
|
|
34
34
|
const currentUrl = new URL(window.location.href);
|
|
35
35
|
const siteUrl = new URL(config.siteUrl);
|
|
36
36
|
|
|
37
|
-
//
|
|
37
|
+
// Named modifier lookup (safe alternative to eval/new Function)
|
|
38
|
+
const MODIFIERS = {
|
|
39
|
+
'search-cse': (url) => {
|
|
40
|
+
const q = url.searchParams.get('q');
|
|
41
|
+
url.searchParams.set('q', 'site:' + window.location.origin + ' ' + q);
|
|
42
|
+
return url;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Resolve modifier by name
|
|
38
47
|
let modifierFunction = (url) => url;
|
|
39
48
|
if (config.modifier && config.modifier !== '""' && config.modifier !== '') {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
modifierFunction =
|
|
43
|
-
}
|
|
44
|
-
console.warn('[Redirect]
|
|
45
|
-
console.warn('[Redirect] Modifier string:', config.modifier);
|
|
49
|
+
const modifierName = config.modifier.trim();
|
|
50
|
+
if (MODIFIERS[modifierName]) {
|
|
51
|
+
modifierFunction = MODIFIERS[modifierName];
|
|
52
|
+
} else {
|
|
53
|
+
console.warn('[Redirect] Unknown modifier:', modifierName);
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
56
|
|
|
@@ -90,8 +90,12 @@ const setupMessageHandler = () => {
|
|
|
90
90
|
// Flag as set up
|
|
91
91
|
window.__ujVertMessageHandlerSetup = true;
|
|
92
92
|
|
|
93
|
-
// Listen for messages from iframes
|
|
93
|
+
// Listen for messages from vert iframes (validate origin)
|
|
94
94
|
window.addEventListener('message', (event) => {
|
|
95
|
+
if (event.origin !== window.location.origin && event.origin !== 'https://promo-server.itwcreativeworks.com') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
95
99
|
const message = event.data || {};
|
|
96
100
|
const command = message.command || '';
|
|
97
101
|
const payload = message.payload || {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Libraries
|
|
2
|
-
import fetch from 'wonderful-fetch';
|
|
3
2
|
import * as profileSection from './sections/profile.js';
|
|
4
3
|
import * as notificationsSection from './sections/notifications.js';
|
|
5
4
|
import * as securitySection from './sections/security.js';
|
|
@@ -12,6 +11,7 @@ import * as dataRequestSection from './sections/data-request.js';
|
|
|
12
11
|
import * as connectionsSection from './sections/connections.js';
|
|
13
12
|
import * as refundSection from './sections/refund.js';
|
|
14
13
|
import webManager from 'web-manager';
|
|
14
|
+
import { getPaymentConfig } from '__main_assets__/js/libs/payment-config.js';
|
|
15
15
|
|
|
16
16
|
// Module
|
|
17
17
|
export default () => {
|
|
@@ -33,8 +33,9 @@ export default () => {
|
|
|
33
33
|
// Global state
|
|
34
34
|
let $navLinks = null;
|
|
35
35
|
let $sections = null;
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
|
|
37
|
+
// Config from _config.yml (available instantly, no fetch needed)
|
|
38
|
+
const paymentConfig = getPaymentConfig();
|
|
38
39
|
|
|
39
40
|
// Section modules map
|
|
40
41
|
const sectionModules = {
|
|
@@ -85,10 +86,6 @@ async function initializeAccount() {
|
|
|
85
86
|
webManager.auth().listen({}, async (state) => {
|
|
86
87
|
console.log('Auth state with account data:', state);
|
|
87
88
|
|
|
88
|
-
// Load user data with the account information
|
|
89
|
-
// Wait for brand data to be fetched before loading section data
|
|
90
|
-
await fetchBrandData();
|
|
91
|
-
|
|
92
89
|
/* @dev-only:start */
|
|
93
90
|
{
|
|
94
91
|
// Check for test subscription parameter
|
|
@@ -128,32 +125,6 @@ async function initializeAccount() {
|
|
|
128
125
|
});
|
|
129
126
|
}
|
|
130
127
|
|
|
131
|
-
// Fetch brand data to get configuration and OAuth settings
|
|
132
|
-
async function fetchBrandData() {
|
|
133
|
-
if (fetchBrandDataPromise) return fetchBrandDataPromise;
|
|
134
|
-
|
|
135
|
-
fetchBrandDataPromise = (async () => {
|
|
136
|
-
try {
|
|
137
|
-
const serverApiURL = `${webManager.getApiUrl()}/backend-manager/brand`;
|
|
138
|
-
|
|
139
|
-
// Fetch brand data
|
|
140
|
-
const response = await fetch(serverApiURL, {
|
|
141
|
-
response: 'json',
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
console.log('Fetched brand data:', response);
|
|
145
|
-
brandData = response;
|
|
146
|
-
|
|
147
|
-
return response;
|
|
148
|
-
} catch (error) {
|
|
149
|
-
webManager.sentry().captureException(new Error('Failed to fetch brand data', { cause: error }));
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
})();
|
|
153
|
-
|
|
154
|
-
return fetchBrandDataPromise;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
128
|
// Load data for all sections
|
|
158
129
|
function loadAllSectionData(authState) {
|
|
159
130
|
const { user, account } = authState;
|
|
@@ -172,7 +143,7 @@ function loadAllSectionData(authState) {
|
|
|
172
143
|
}
|
|
173
144
|
|
|
174
145
|
if (sectionModules.billing.loadData) {
|
|
175
|
-
sectionModules.billing.loadData(account,
|
|
146
|
+
sectionModules.billing.loadData(account, paymentConfig);
|
|
176
147
|
}
|
|
177
148
|
|
|
178
149
|
if (sectionModules.team && sectionModules.team.loadData) {
|
|
@@ -196,7 +167,7 @@ function loadAllSectionData(authState) {
|
|
|
196
167
|
}
|
|
197
168
|
|
|
198
169
|
if (sectionModules.connections.loadData) {
|
|
199
|
-
sectionModules.connections.loadData(account,
|
|
170
|
+
sectionModules.connections.loadData(account, webManager.config?.oauth2 || {});
|
|
200
171
|
}
|
|
201
172
|
|
|
202
173
|
if (sectionModules.refund.loadData) {
|
|
@@ -7,7 +7,7 @@ import { FormManager } from '__main_assets__/js/libs/form-manager.js';
|
|
|
7
7
|
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
8
8
|
import webManager from 'web-manager';
|
|
9
9
|
|
|
10
|
-
let
|
|
10
|
+
let paymentConfig = null;
|
|
11
11
|
let cancelFormManager = null;
|
|
12
12
|
let currentAccount = null;
|
|
13
13
|
|
|
@@ -41,12 +41,12 @@ export async function init() {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Load billing data
|
|
44
|
-
export async function loadData(account,
|
|
44
|
+
export async function loadData(account, sharedPaymentConfig) {
|
|
45
45
|
if (!account) {
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
paymentConfig = sharedPaymentConfig;
|
|
50
50
|
currentAccount = account;
|
|
51
51
|
|
|
52
52
|
updateUI(account);
|
|
@@ -104,7 +104,7 @@ function buildBillingState(account) {
|
|
|
104
104
|
// Pre-format billing details
|
|
105
105
|
const nextBillingUnix = subscription.expires?.timestampUNIX;
|
|
106
106
|
const amount = subscription.payment?.price;
|
|
107
|
-
const currency =
|
|
107
|
+
const currency = paymentConfig?.currency || 'USD';
|
|
108
108
|
const frequency = subscription.payment?.frequency;
|
|
109
109
|
const hasValidBilling = nextBillingUnix && nextBillingUnix > 0 && amount;
|
|
110
110
|
|
|
@@ -320,8 +320,8 @@ function populateCancelReasons() {
|
|
|
320
320
|
|
|
321
321
|
$container.innerHTML = shuffled.map((reason, i) => `
|
|
322
322
|
<div class="form-check mb-2">
|
|
323
|
-
<input class="form-check-input" type="radio" name="cancel_reason" id="cancel-reason-${i}" value="${reason}">
|
|
324
|
-
<label class="form-check-label" for="cancel-reason-${i}">${reason}</label>
|
|
323
|
+
<input class="form-check-input" type="radio" name="cancel_reason" id="cancel-reason-${i}" value="${webManager.utilities().escapeHTML(reason)}">
|
|
324
|
+
<label class="form-check-label" for="cancel-reason-${i}">${webManager.utilities().escapeHTML(reason)}</label>
|
|
325
325
|
</div>
|
|
326
326
|
`).join('');
|
|
327
327
|
}
|
|
@@ -338,7 +338,7 @@ function updateUsageInfo(account) {
|
|
|
338
338
|
|
|
339
339
|
// Use the effective plan for usage limits (basic if cancelled/suspended)
|
|
340
340
|
const resolved = webManager.auth().resolveSubscription(account);
|
|
341
|
-
const product =
|
|
341
|
+
const product = paymentConfig?.products?.find(p => p.id === resolved.plan);
|
|
342
342
|
const limits = product?.limits || {};
|
|
343
343
|
|
|
344
344
|
// Clear container
|
|
@@ -404,9 +404,9 @@ function getDisplayName(subscription) {
|
|
|
404
404
|
return subscription.product.name;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
// Fall back to
|
|
407
|
+
// Fall back to config product name
|
|
408
408
|
const productId = subscription.product?.id || 'basic';
|
|
409
|
-
const product =
|
|
409
|
+
const product = paymentConfig?.products?.find(p => p.id === productId);
|
|
410
410
|
return product?.name || 'Free';
|
|
411
411
|
}
|
|
412
412
|
|
|
@@ -7,7 +7,7 @@ import { FormManager } from '__main_assets__/js/libs/form-manager.js';
|
|
|
7
7
|
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
8
8
|
import webManager from 'web-manager';
|
|
9
9
|
|
|
10
|
-
let
|
|
10
|
+
let oauth2Config = null;
|
|
11
11
|
let accountData = null;
|
|
12
12
|
let connectionForms = new Map();
|
|
13
13
|
|
|
@@ -24,13 +24,13 @@ export async function init() {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Load connections data
|
|
27
|
-
export async function loadData(account,
|
|
27
|
+
export async function loadData(account, sharedOAuth2Config) {
|
|
28
28
|
if (!account) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
accountData = account;
|
|
33
|
-
|
|
33
|
+
oauth2Config = sharedOAuth2Config;
|
|
34
34
|
|
|
35
35
|
displayConnections();
|
|
36
36
|
}
|
|
@@ -43,14 +43,14 @@ function displayConnections() {
|
|
|
43
43
|
$loading.classList.add('d-none');
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
if (!
|
|
46
|
+
if (!oauth2Config) {
|
|
47
47
|
if ($loading) {
|
|
48
48
|
$loading.classList.remove('d-none');
|
|
49
49
|
}
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const availableProviders =
|
|
53
|
+
const availableProviders = oauth2Config;
|
|
54
54
|
const userConnections = accountData?.oauth2 || {};
|
|
55
55
|
|
|
56
56
|
let hasEnabledProviders = false;
|
|
@@ -237,7 +237,7 @@ function initializeProviderForm(providerId) {
|
|
|
237
237
|
formManager.on('statechange', ({ state }) => {
|
|
238
238
|
if (state === 'ready') {
|
|
239
239
|
const userConnection = accountData?.oauth2?.[providerId];
|
|
240
|
-
const providerSettings =
|
|
240
|
+
const providerSettings = oauth2Config?.[providerId];
|
|
241
241
|
updateProviderStatus(providerId, userConnection, providerSettings);
|
|
242
242
|
}
|
|
243
243
|
});
|
|
@@ -252,7 +252,7 @@ function initializeProviderForm(providerId) {
|
|
|
252
252
|
const success = await handleDisconnect(provider);
|
|
253
253
|
|
|
254
254
|
if (success) {
|
|
255
|
-
const providerSettings =
|
|
255
|
+
const providerSettings = oauth2Config?.[provider];
|
|
256
256
|
updateProviderStatus(provider, null, providerSettings);
|
|
257
257
|
}
|
|
258
258
|
}
|
|
@@ -261,7 +261,7 @@ function initializeProviderForm(providerId) {
|
|
|
261
261
|
|
|
262
262
|
// Handle connect action
|
|
263
263
|
async function handleConnect(providerId) {
|
|
264
|
-
const provider =
|
|
264
|
+
const provider = oauth2Config?.[providerId];
|
|
265
265
|
|
|
266
266
|
if (!provider || provider.enabled === false) {
|
|
267
267
|
throw new Error('This connection service is not available.');
|
|
@@ -281,7 +281,7 @@ async function handleConnect(providerId) {
|
|
|
281
281
|
tries: 2,
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
-
if (response.url) {
|
|
284
|
+
if (response.url && /^https?:\/\//i.test(response.url)) {
|
|
285
285
|
window.location.href = response.url;
|
|
286
286
|
} else {
|
|
287
287
|
throw new Error(response.message || 'Failed to get authorization URL');
|
|
@@ -322,7 +322,7 @@ async function handleDisconnect(providerId) {
|
|
|
322
322
|
|
|
323
323
|
// Called when section is shown
|
|
324
324
|
export function onShow() {
|
|
325
|
-
if (accountData &&
|
|
325
|
+
if (accountData && oauth2Config) {
|
|
326
326
|
displayConnections();
|
|
327
327
|
}
|
|
328
328
|
}
|
|
@@ -110,12 +110,12 @@ function updateReferralsList(referrals) {
|
|
|
110
110
|
<span class="badge bg-secondary me-2">#${sortedReferrals.length - index}</span>
|
|
111
111
|
<div>
|
|
112
112
|
<strong class="font-monospace small">${webManager.utilities().escapeHTML(referral.uid || 'Unknown User')}</strong>
|
|
113
|
-
<div class="text-muted small">${dateStr}${timeStr ? ` at ${timeStr}` : ''}</div>
|
|
113
|
+
<div class="text-muted small">${webManager.utilities().escapeHTML(dateStr)}${timeStr ? ` at ${webManager.utilities().escapeHTML(timeStr)}` : ''}</div>
|
|
114
114
|
</div>
|
|
115
115
|
</div>
|
|
116
116
|
</div>
|
|
117
117
|
<div class="text-end">
|
|
118
|
-
|
|
118
|
+
<small class="text-muted">${webManager.utilities().escapeHTML(getTimeSince(timestamp))}</small>
|
|
119
119
|
</div>
|
|
120
120
|
</div>
|
|
121
121
|
</div>
|
|
@@ -146,48 +146,40 @@ function formatTime(date) {
|
|
|
146
146
|
|
|
147
147
|
// Get time since string
|
|
148
148
|
function getTimeSince(timestamp) {
|
|
149
|
-
if (!timestamp) return '
|
|
149
|
+
if (!timestamp) return 'Unknown';
|
|
150
150
|
|
|
151
151
|
const now = Date.now();
|
|
152
152
|
const diff = now - timestamp;
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
if (diff < 60000) {
|
|
156
|
-
return '<small class="text-success">Just now</small>';
|
|
157
|
-
}
|
|
154
|
+
if (diff < 60000) return 'Just now';
|
|
158
155
|
|
|
159
|
-
// Less than 1 hour
|
|
160
156
|
if (diff < 3600000) {
|
|
161
157
|
const minutes = Math.floor(diff / 60000);
|
|
162
|
-
return
|
|
158
|
+
return `${minutes} min${minutes > 1 ? 's' : ''} ago`;
|
|
163
159
|
}
|
|
164
160
|
|
|
165
|
-
// Less than 24 hours
|
|
166
161
|
if (diff < 86400000) {
|
|
167
162
|
const hours = Math.floor(diff / 3600000);
|
|
168
|
-
return
|
|
163
|
+
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
169
164
|
}
|
|
170
165
|
|
|
171
|
-
// Less than 7 days
|
|
172
166
|
if (diff < 604800000) {
|
|
173
167
|
const days = Math.floor(diff / 86400000);
|
|
174
|
-
return
|
|
168
|
+
return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
175
169
|
}
|
|
176
170
|
|
|
177
|
-
// Less than 30 days
|
|
178
171
|
if (diff < 2592000000) {
|
|
179
172
|
const weeks = Math.floor(diff / 604800000);
|
|
180
|
-
return
|
|
173
|
+
return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
|
|
181
174
|
}
|
|
182
175
|
|
|
183
|
-
// More than 30 days
|
|
184
176
|
const months = Math.floor(diff / 2592000000);
|
|
185
177
|
if (months < 12) {
|
|
186
|
-
return
|
|
178
|
+
return `${months} month${months > 1 ? 's' : ''} ago`;
|
|
187
179
|
}
|
|
188
180
|
|
|
189
181
|
const years = Math.floor(months / 12);
|
|
190
|
-
return
|
|
182
|
+
return `${years} year${years > 1 ? 's' : ''} ago`;
|
|
191
183
|
}
|
|
192
184
|
|
|
193
185
|
// Setup button handlers
|
|
@@ -128,8 +128,8 @@ function populateRefundReasons() {
|
|
|
128
128
|
|
|
129
129
|
$container.innerHTML = shuffled.map((reason, i) => `
|
|
130
130
|
<div class="form-check mb-2">
|
|
131
|
-
<input class="form-check-input" type="radio" name="refund_reason" id="refund-reason-${i}" value="${reason}" required>
|
|
132
|
-
<label class="form-check-label" for="refund-reason-${i}">${reason}</label>
|
|
131
|
+
<input class="form-check-input" type="radio" name="refund_reason" id="refund-reason-${i}" value="${webManager.utilities().escapeHTML(reason)}" required>
|
|
132
|
+
<label class="form-check-label" for="refund-reason-${i}">${webManager.utilities().escapeHTML(reason)}</label>
|
|
133
133
|
</div>
|
|
134
134
|
`).join('');
|
|
135
135
|
}
|
|
@@ -301,7 +301,7 @@ async function updateActiveSessions(account) {
|
|
|
301
301
|
</div>
|
|
302
302
|
</div>
|
|
303
303
|
<div class="text-end">
|
|
304
|
-
<small class="text-muted">${formatDate(session.timestamp || (session.timestampUNIX * 1000))}</small>
|
|
304
|
+
<small class="text-muted">${webManager.utilities().escapeHTML(formatDate(session.timestamp || (session.timestampUNIX * 1000)))}</small>
|
|
305
305
|
${session.isCurrent ? '<span class="badge bg-primary ms-2">Current</span>' : ''}
|
|
306
306
|
</div>
|
|
307
307
|
</div>
|
|
@@ -66,7 +66,7 @@ function updateInviteStatus(invites) {
|
|
|
66
66
|
<div class="d-flex justify-content-between align-items-center">
|
|
67
67
|
<div>
|
|
68
68
|
<strong>${webManager.utilities().escapeHTML(invite.email)}</strong>
|
|
69
|
-
<small class="text-muted d-block">Invited ${formatDate(invite.invitedAt)}</small>
|
|
69
|
+
<small class="text-muted d-block">Invited ${webManager.utilities().escapeHTML(formatDate(invite.invitedAt))}</small>
|
|
70
70
|
</div>
|
|
71
71
|
<div>
|
|
72
72
|
<button class="btn btn-sm btn-outline-danger" data-action="cancel-invite" data-invite-id="${webManager.utilities().escapeHTML(invite.id)}">
|
|
@@ -26,7 +26,13 @@ async function renderEmailPreview(formData) {
|
|
|
26
26
|
md = new MarkdownIt({ html: true, breaks: true, linkify: true });
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
const DOMPurify = (await import('dompurify')).default;
|
|
30
|
+
const renderedContent = content
|
|
31
|
+
? DOMPurify.sanitize(md.render(content), {
|
|
32
|
+
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'hr', 'ul', 'ol', 'li', 'a', 'b', 'strong', 'i', 'em', 'u', 's', 'del', 'blockquote', 'pre', 'code', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'span', 'sup', 'sub'],
|
|
33
|
+
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'width', 'height', 'class', 'style', 'target', 'rel'],
|
|
34
|
+
})
|
|
35
|
+
: '<p class="text-muted">No content yet</p>';
|
|
30
36
|
|
|
31
37
|
return `
|
|
32
38
|
<div class="email-preview">
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
// Libraries
|
|
6
6
|
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
|
|
7
|
-
import fetch from 'wonderful-fetch';
|
|
8
7
|
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
8
|
+
import { getProducts } from '__main_assets__/js/libs/payment-config.js';
|
|
9
9
|
import { formatTimeAgo, capitalize, setStatValue, setStatSubValue } from '__main_assets__/js/libs/admin-helpers.js';
|
|
10
10
|
import { Chart, DoughnutController, BarController, ArcElement, BarElement, CategoryScale, LinearScale, Tooltip, Legend } from 'chart.js';
|
|
11
11
|
import webManager from 'web-manager';
|
|
@@ -80,13 +80,8 @@ async function loadSubscriberData() {
|
|
|
80
80
|
const { collection, query, where, getCountFromServer } = await import('firebase/firestore');
|
|
81
81
|
const db = webManager.firebaseFirestore;
|
|
82
82
|
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
response: 'json',
|
|
86
|
-
tries: 2,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const products = (brandConfig?.payment?.products || []).filter((p) => p.id !== 'basic');
|
|
83
|
+
// Get product list from _config.yml (available instantly via webManager.config)
|
|
84
|
+
const products = getProducts().filter((p) => p.id !== 'basic');
|
|
90
85
|
const frequencyIds = [...new Set(products.flatMap((p) => Object.keys(p.prices || {})))];
|
|
91
86
|
|
|
92
87
|
// Run count queries for each product × frequency in parallel
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Payment Checkout Page
|
|
2
2
|
import { FormManager } from '__main_assets__/js/libs/form-manager.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getPaymentConfig, getProcessors, getProductById } from '__main_assets__/js/libs/payment-config.js';
|
|
4
|
+
import { fetchTrialEligibility, warmupServer, createPaymentIntent } from './modules/api.js';
|
|
4
5
|
import { state, buildBindingsState, resolveProcessor, FREQUENCIES, getAvailableFrequencies } from './modules/state.js';
|
|
5
6
|
import { applyDiscountCode } from './modules/discount.js';
|
|
6
7
|
import { initializeRecaptcha } from './modules/recaptcha.js';
|
|
@@ -71,15 +72,24 @@ async function initializeCheckout() {
|
|
|
71
72
|
throw new Error('Product ID is missing from URL.');
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
// Read payment config from _config.yml (available instantly via webManager.config)
|
|
76
|
+
state.processors = getProcessors();
|
|
77
|
+
|
|
78
|
+
// Find product
|
|
79
|
+
const product = getProductById(productId);
|
|
80
|
+
if (!product) {
|
|
81
|
+
throw new Error(`Product "${productId}" not found.`);
|
|
82
|
+
}
|
|
83
|
+
state.product = product;
|
|
84
|
+
|
|
74
85
|
// Wait for auth state to settle before any authorized calls
|
|
75
86
|
await new Promise((resolve) => webManager.auth().listen({ once: true }, resolve));
|
|
76
87
|
|
|
77
88
|
// Fire-and-forget server warmup
|
|
78
89
|
warmupServer();
|
|
79
90
|
|
|
80
|
-
// Parallel fetch:
|
|
81
|
-
const [
|
|
82
|
-
fetchBrandConfig(),
|
|
91
|
+
// Parallel fetch: trial eligibility + reCAPTCHA
|
|
92
|
+
const [trialResult, recaptchaResult] = await Promise.allSettled([
|
|
83
93
|
fetchTrialEligibility(),
|
|
84
94
|
initializeRecaptcha(webManager.config?.recaptcha?.['site-key']),
|
|
85
95
|
]);
|
|
@@ -96,23 +106,6 @@ async function initializeCheckout() {
|
|
|
96
106
|
}
|
|
97
107
|
/* @dev-only:end */
|
|
98
108
|
|
|
99
|
-
// Brand config is required
|
|
100
|
-
if (brandConfigResult.status === 'rejected') {
|
|
101
|
-
const reason = brandConfigResult.reason?.message || brandConfigResult.reason || 'Unknown error';
|
|
102
|
-
throw new Error(`Failed to load checkout brand config: ${reason}`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const brandConfig = brandConfigResult.value;
|
|
106
|
-
state.brandConfig = brandConfig;
|
|
107
|
-
state.processors = brandConfig.payment?.processors || {};
|
|
108
|
-
|
|
109
|
-
// Find product
|
|
110
|
-
const product = brandConfig.payment?.products?.find(p => p.id === productId);
|
|
111
|
-
if (!product) {
|
|
112
|
-
throw new Error(`Product "${productId}" not found.`);
|
|
113
|
-
}
|
|
114
|
-
state.product = product;
|
|
115
|
-
|
|
116
109
|
// Resolve frequency: URL param if valid, otherwise longest available term
|
|
117
110
|
const available = getAvailableFrequencies(product);
|
|
118
111
|
if (frequencyParam && FREQUENCIES.includes(frequencyParam) && available.includes(frequencyParam)) {
|
|
@@ -286,7 +279,7 @@ function initDevPanel() {
|
|
|
286
279
|
// Show the panel
|
|
287
280
|
$panel.hidden = false;
|
|
288
281
|
|
|
289
|
-
const products =
|
|
282
|
+
const products = getPaymentConfig().products || [];
|
|
290
283
|
const params = new URLSearchParams(window.location.search);
|
|
291
284
|
|
|
292
285
|
// Populate product dropdown
|
|
@@ -4,17 +4,6 @@ import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
|
4
4
|
import { getRecaptchaToken } from './recaptcha.js';
|
|
5
5
|
import webManager from 'web-manager';
|
|
6
6
|
|
|
7
|
-
// Fetch brand config (products + processors)
|
|
8
|
-
export async function fetchBrandConfig() {
|
|
9
|
-
const response = await fetch(`${webManager.getApiUrl()}/backend-manager/brand`, {
|
|
10
|
-
response: 'json',
|
|
11
|
-
tries: 2,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
console.log('Fetched brand config:', response);
|
|
15
|
-
return response;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
7
|
// Check trial eligibility via backend endpoint
|
|
19
8
|
export async function fetchTrialEligibility() {
|
|
20
9
|
try {
|
|
@@ -9,8 +9,7 @@ export const FREQUENCIES = ['daily', 'weekly', 'monthly', 'annually'];
|
|
|
9
9
|
|
|
10
10
|
// Minimal mutable state
|
|
11
11
|
export const state = {
|
|
12
|
-
// From
|
|
13
|
-
brandConfig: null,
|
|
12
|
+
// From config (stored once, never transformed)
|
|
14
13
|
product: null,
|
|
15
14
|
processors: null,
|
|
16
15
|
|
|
@@ -319,7 +319,7 @@ function setupPromoCountdown() {
|
|
|
319
319
|
adjustNavbarOffset();
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
//
|
|
322
|
+
// Update buttons based on the user's current active plan
|
|
323
323
|
function setupCurrentPlanIndicator() {
|
|
324
324
|
webManager.auth().listen({ once: true }, (state) => {
|
|
325
325
|
const resolved = webManager.auth().resolveSubscription(state.account);
|
|
@@ -328,16 +328,22 @@ function setupCurrentPlanIndicator() {
|
|
|
328
328
|
return;
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
331
|
+
// Mark current plan button
|
|
332
|
+
const $currentButton = document.querySelector(`button[data-plan-id="${resolved.plan}"]`);
|
|
333
|
+
if ($currentButton) {
|
|
334
|
+
$currentButton.disabled = true;
|
|
335
|
+
$currentButton.textContent = 'Current Plan';
|
|
336
|
+
$currentButton.classList.remove('btn-primary', 'btn-gradient-rainbow', 'gradient-animated');
|
|
337
|
+
$currentButton.classList.add('btn-adaptive');
|
|
335
338
|
}
|
|
336
339
|
|
|
337
|
-
|
|
338
|
-
$button
|
|
339
|
-
|
|
340
|
-
|
|
340
|
+
// Update other paid plan buttons to "Switch to this plan"
|
|
341
|
+
document.querySelectorAll('button[data-plan-id]').forEach(($button) => {
|
|
342
|
+
if ($button.dataset.planId === resolved.plan || $button.dataset.planId === 'basic' || $button.dataset.planId === 'enterprise') {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
$button.textContent = 'Switch to This Plan';
|
|
346
|
+
});
|
|
341
347
|
});
|
|
342
348
|
}
|
|
343
349
|
|
|
@@ -328,6 +328,32 @@ faqs:
|
|
|
328
328
|
|
|
329
329
|
<div class="row g-4 mb-5 justify-content-center">
|
|
330
330
|
{% for plan in page.resolved.pricing.plans %}
|
|
331
|
+
{% comment %} Look up matching product in _config.yml payment products {% endcomment %}
|
|
332
|
+
{% assign _config_product = nil %}
|
|
333
|
+
{% for p in site.web_manager.payment.products %}
|
|
334
|
+
{% if p.id == plan.id %}
|
|
335
|
+
{% assign _config_product = p %}
|
|
336
|
+
{% break %}
|
|
337
|
+
{% endif %}
|
|
338
|
+
{% endfor %}
|
|
339
|
+
|
|
340
|
+
{% comment %} Resolve prices: frontmatter pricing takes precedence, then config prices {% endcomment %}
|
|
341
|
+
{% if plan.pricing.monthly or plan.pricing.monthly == 0 %}
|
|
342
|
+
{% assign _plan_monthly = plan.pricing.monthly %}
|
|
343
|
+
{% elsif _config_product.prices.monthly or _config_product.prices.monthly == 0 %}
|
|
344
|
+
{% assign _plan_monthly = _config_product.prices.monthly %}
|
|
345
|
+
{% else %}
|
|
346
|
+
{% assign _plan_monthly = 0 %}
|
|
347
|
+
{% endif %}
|
|
348
|
+
|
|
349
|
+
{% if plan.pricing.annually or plan.pricing.annually == 0 %}
|
|
350
|
+
{% assign _plan_annually = plan.pricing.annually %}
|
|
351
|
+
{% elsif _config_product.prices.annually or _config_product.prices.annually == 0 %}
|
|
352
|
+
{% assign _plan_annually = _config_product.prices.annually %}
|
|
353
|
+
{% else %}
|
|
354
|
+
{% assign _plan_annually = 0 %}
|
|
355
|
+
{% endif %}
|
|
356
|
+
|
|
331
357
|
{% if plan.popular %}
|
|
332
358
|
{% assign border_classes = "border-gradient-rainbow border-3" %}
|
|
333
359
|
{% else %}
|
|
@@ -349,12 +375,12 @@ faqs:
|
|
|
349
375
|
<p class="text-muted small mb-3">{{ plan.tagline }}</p>
|
|
350
376
|
|
|
351
377
|
<!-- Price -->
|
|
352
|
-
{% if
|
|
378
|
+
{% if _plan_monthly == 0 %}
|
|
353
379
|
<p class="display-6 fw-bold mb-0">Free</p>
|
|
354
380
|
{% else %}
|
|
355
381
|
<p class="mb-0">
|
|
356
382
|
<span class="display-6 fw-bold">
|
|
357
|
-
<span class="fs-3">$</span><span class="amount" data-monthly="{{
|
|
383
|
+
<span class="fs-3">$</span><span class="amount" data-monthly="{{ _plan_monthly }}" data-annually="{{ _plan_annually | divided_by: 12 | round }}">{{ _plan_annually | divided_by: 12 | round }}</span>
|
|
358
384
|
</span>
|
|
359
385
|
<small class="text-muted">/month</small>
|
|
360
386
|
</p>
|
|
@@ -363,12 +389,17 @@ faqs:
|
|
|
363
389
|
<!-- Price per unit (only shown when enabled) -->
|
|
364
390
|
{% if page.resolved.pricing.price_per_unit.enabled %}
|
|
365
391
|
<p class="text-muted small mb-4">
|
|
366
|
-
{% if
|
|
392
|
+
{% if _plan_monthly > 0 %}
|
|
367
393
|
{% for feature in plan.features %}
|
|
368
394
|
{% if feature.id == page.resolved.pricing.price_per_unit.feature_id %}
|
|
369
|
-
{%
|
|
370
|
-
{% assign
|
|
371
|
-
{%
|
|
395
|
+
{% comment %} Resolve feature value from config limits if not set in frontmatter {% endcomment %}
|
|
396
|
+
{% assign _ppu_value = feature.value %}
|
|
397
|
+
{% if _config_product and _ppu_value == nil %}
|
|
398
|
+
{% for _lim in _config_product.limits %}{% if _lim[0] == feature.id %}{% assign _ppu_value = _lim[1] %}{% break %}{% endif %}{% endfor %}
|
|
399
|
+
{% endif %}
|
|
400
|
+
{% assign monthly_price_per_unit = _plan_monthly | times: 1.0 | divided_by: _ppu_value | round: 2 %}
|
|
401
|
+
{% assign annual_monthly_price = _plan_annually | divided_by: 12.0 %}
|
|
402
|
+
{% assign annual_price_per_unit = annual_monthly_price | divided_by: _ppu_value | round: 2 %}
|
|
372
403
|
<span class="price-per-unit" data-monthly="${{ monthly_price_per_unit }}" data-annually="${{ annual_price_per_unit }}">${{ annual_price_per_unit }}</span> per {{ page.resolved.pricing.price_per_unit.label }}
|
|
373
404
|
{% endif %}
|
|
374
405
|
{% endfor %}
|
|
@@ -389,8 +420,8 @@ faqs:
|
|
|
389
420
|
{% assign btn_style = "btn-primary" %}
|
|
390
421
|
|
|
391
422
|
{% iftruthy plan.url %}
|
|
392
|
-
<a href="{{ plan.url }}" class="btn {% if
|
|
393
|
-
{% if
|
|
423
|
+
<a href="{{ plan.url }}" class="btn {% if _plan_monthly == 0 %}btn-adaptive{% else %}{{ btn_style }}{% endif %} btn-md fw-semibold px-2 fs-5">
|
|
424
|
+
{% if _plan_monthly == 0 %}
|
|
394
425
|
Get Started
|
|
395
426
|
{% else %}
|
|
396
427
|
Get Free Trial
|
|
@@ -406,11 +437,11 @@ faqs:
|
|
|
406
437
|
|
|
407
438
|
<!-- Billing info -->
|
|
408
439
|
<p class="text-center text-muted small mb-3">
|
|
409
|
-
{% if
|
|
440
|
+
{% if _plan_monthly == 0 %}
|
|
410
441
|
<span>No credit card required</span>
|
|
411
442
|
{% else %}
|
|
412
|
-
<span class="billing-info" data-monthly="Billed ${{
|
|
413
|
-
Billed ${{
|
|
443
|
+
<span class="billing-info" data-monthly="Billed ${{ _plan_monthly | uj_commaify }} monthly" data-annually="Billed ${{ _plan_annually | uj_commaify }} annually">
|
|
444
|
+
Billed ${{ _plan_annually | uj_commaify }} annually
|
|
414
445
|
</span>
|
|
415
446
|
{% endif %}
|
|
416
447
|
</p>
|
|
@@ -423,6 +454,16 @@ faqs:
|
|
|
423
454
|
<ul class="list-unstyled mb-3">
|
|
424
455
|
{% for feature in plan.features %}
|
|
425
456
|
{% if common_feature_ids contains feature.id %}
|
|
457
|
+
{% comment %} Resolve feature value: frontmatter > config limits {% endcomment %}
|
|
458
|
+
{% assign _feature_value = feature.value %}
|
|
459
|
+
{% if _config_product and _feature_value == nil %}
|
|
460
|
+
{% assign _config_limit = nil %}{% for _lim in _config_product.limits %}{% if _lim[0] == feature.id %}{% assign _config_limit = _lim[1] %}{% break %}{% endif %}{% endfor %}
|
|
461
|
+
{% if _config_limit == -1 %}
|
|
462
|
+
{% assign _feature_value = "Unlimited" %}
|
|
463
|
+
{% elsif _config_limit %}
|
|
464
|
+
{% assign _feature_value = _config_limit %}
|
|
465
|
+
{% endif %}
|
|
466
|
+
{% endif %}
|
|
426
467
|
{% assign feature_definition = nil %}
|
|
427
468
|
{% for def in page.resolved.pricing.definitions %}
|
|
428
469
|
{% if def.id == feature.id %}
|
|
@@ -433,10 +474,10 @@ faqs:
|
|
|
433
474
|
<li class="d-flex align-items-start mb-2">
|
|
434
475
|
<span class="me-3">{% uj_icon feature.icon, "fa-md" %}</span>
|
|
435
476
|
<span>
|
|
436
|
-
{% if
|
|
477
|
+
{% if _feature_value == "Unlimited" %}
|
|
437
478
|
Unlimited
|
|
438
479
|
{% else %}
|
|
439
|
-
{{
|
|
480
|
+
{{ _feature_value | uj_commaify }}
|
|
440
481
|
{% endif %}
|
|
441
482
|
{% iftruthy feature_definition %}
|
|
442
483
|
<span class="text-decoration-underline text-decoration-dotted cursor-help" data-bs-toggle="tooltip" data-bs-title="{{ feature_definition }}">{{ feature.name }}</span>
|
|
@@ -467,6 +508,16 @@ faqs:
|
|
|
467
508
|
<ul class="list-unstyled mb-0">
|
|
468
509
|
{% for feature in plan.features %}
|
|
469
510
|
{% unless common_feature_ids contains feature.id %}
|
|
511
|
+
{% comment %} Resolve feature value: frontmatter > config limits {% endcomment %}
|
|
512
|
+
{% assign _feature_value = feature.value %}
|
|
513
|
+
{% if _config_product and _feature_value == nil %}
|
|
514
|
+
{% assign _config_limit = nil %}{% for _lim in _config_product.limits %}{% if _lim[0] == feature.id %}{% assign _config_limit = _lim[1] %}{% break %}{% endif %}{% endfor %}
|
|
515
|
+
{% if _config_limit == -1 %}
|
|
516
|
+
{% assign _feature_value = "Unlimited" %}
|
|
517
|
+
{% elsif _config_limit %}
|
|
518
|
+
{% assign _feature_value = _config_limit %}
|
|
519
|
+
{% endif %}
|
|
520
|
+
{% endif %}
|
|
470
521
|
{% assign feature_definition = nil %}
|
|
471
522
|
{% for def in page.resolved.pricing.definitions %}
|
|
472
523
|
{% if def.id == feature.id %}
|
|
@@ -477,12 +528,12 @@ faqs:
|
|
|
477
528
|
<li class="d-flex align-items-start mb-2 {% if forloop.last %}mb-0{% endif %}">
|
|
478
529
|
<span class="me-3">{% uj_icon feature.icon, "fa-md" %}</span>
|
|
479
530
|
<span>
|
|
480
|
-
{% if
|
|
481
|
-
{{
|
|
482
|
-
{% elsif
|
|
531
|
+
{% if _feature_value == "24/7" %}
|
|
532
|
+
{{ _feature_value }}
|
|
533
|
+
{% elsif _feature_value == "Included" or _feature_value == "Available" or _feature_value == "Full" %}
|
|
483
534
|
<!-- No prefix for these values -->
|
|
484
535
|
{% else %}
|
|
485
|
-
{{
|
|
536
|
+
{{ _feature_value | uj_commaify }}
|
|
486
537
|
{% endif %}
|
|
487
538
|
{% iftruthy feature_definition %}
|
|
488
539
|
<span class="text-decoration-underline text-decoration-dotted cursor-help" data-bs-toggle="tooltip" data-bs-title="{{ feature_definition }}">{{ feature.name }}</span>
|
|
@@ -509,16 +560,12 @@ faqs:
|
|
|
509
560
|
{% endunless %}
|
|
510
561
|
{% endfor %}
|
|
511
562
|
|
|
563
|
+
{% assign _prev_index = forloop.index0 | minus: 1 %}
|
|
564
|
+
{% assign _prev_plan = page.resolved.pricing.plans[_prev_index] %}
|
|
512
565
|
<div class="text-muted small mb-2">
|
|
513
566
|
<em>Everything in</em>
|
|
514
567
|
<em>
|
|
515
|
-
<strong>
|
|
516
|
-
{%- if forloop.index == 1 -%}Free{%- endif -%}
|
|
517
|
-
{%- if forloop.index == 2 -%}Basic{%- endif -%}
|
|
518
|
-
{%- if forloop.index == 3 -%}Starter{%- endif -%}
|
|
519
|
-
{%- if forloop.index == 4 -%}Pro{%- endif -%}
|
|
520
|
-
{%- if forloop.index == 5 -%}Max{% endif -%}
|
|
521
|
-
</strong>
|
|
568
|
+
<strong>{{ _prev_plan.name }}</strong>
|
|
522
569
|
{%- if has_additional -%}, plus:{%- endif -%}
|
|
523
570
|
</em>
|
|
524
571
|
</div>
|
|
@@ -528,6 +575,16 @@ faqs:
|
|
|
528
575
|
<ul class="list-unstyled mb-0">
|
|
529
576
|
{% for feature in plan.features %}
|
|
530
577
|
{% unless common_feature_ids contains feature.id %}
|
|
578
|
+
{% comment %} Resolve feature value: frontmatter > config limits {% endcomment %}
|
|
579
|
+
{% assign _feature_value = feature.value %}
|
|
580
|
+
{% if _config_product and _feature_value == nil %}
|
|
581
|
+
{% assign _config_limit = nil %}{% for _lim in _config_product.limits %}{% if _lim[0] == feature.id %}{% assign _config_limit = _lim[1] %}{% break %}{% endif %}{% endfor %}
|
|
582
|
+
{% if _config_limit == -1 %}
|
|
583
|
+
{% assign _feature_value = "Unlimited" %}
|
|
584
|
+
{% elsif _config_limit %}
|
|
585
|
+
{% assign _feature_value = _config_limit %}
|
|
586
|
+
{% endif %}
|
|
587
|
+
{% endif %}
|
|
531
588
|
{% assign feature_definition = nil %}
|
|
532
589
|
{% for def in page.resolved.pricing.definitions %}
|
|
533
590
|
{% if def.id == feature.id %}
|
|
@@ -538,12 +595,12 @@ faqs:
|
|
|
538
595
|
<li class="d-flex align-items-start mb-2 {% if forloop.last %}mb-0{% endif %}">
|
|
539
596
|
<span class="me-3">{% uj_icon feature.icon, "fa-md" %}</span>
|
|
540
597
|
<span>
|
|
541
|
-
{% if
|
|
542
|
-
{{
|
|
543
|
-
{% elsif
|
|
598
|
+
{% if _feature_value == "24/7" %}
|
|
599
|
+
{{ _feature_value }}
|
|
600
|
+
{% elsif _feature_value == "Included" or _feature_value == "Available" or _feature_value == "Full" %}
|
|
544
601
|
<!-- No prefix for these values -->
|
|
545
602
|
{% else %}
|
|
546
|
-
{{
|
|
603
|
+
{{ _feature_value | uj_commaify }}
|
|
547
604
|
{% endif %}
|
|
548
605
|
{% iftruthy feature_definition %}
|
|
549
606
|
<span class="text-decoration-underline text-decoration-dotted cursor-help" data-bs-toggle="tooltip" data-bs-title="{{ feature_definition }}">{{ feature.name }}</span>
|
|
@@ -664,6 +721,14 @@ faqs:
|
|
|
664
721
|
{% endiffalsy %}
|
|
665
722
|
</th>
|
|
666
723
|
{% for plan in page.resolved.pricing.plans %}
|
|
724
|
+
{% comment %} Look up config product for this plan {% endcomment %}
|
|
725
|
+
{% assign _tbl_config_product = nil %}
|
|
726
|
+
{% for p in site.web_manager.payment.products %}
|
|
727
|
+
{% if p.id == plan.id %}
|
|
728
|
+
{% assign _tbl_config_product = p %}
|
|
729
|
+
{% break %}
|
|
730
|
+
{% endif %}
|
|
731
|
+
{% endfor %}
|
|
667
732
|
<td>
|
|
668
733
|
{% assign has_feature = false %}
|
|
669
734
|
{% assign feature_value = nil %}
|
|
@@ -678,6 +743,16 @@ faqs:
|
|
|
678
743
|
{% endif %}
|
|
679
744
|
{% endfor %}
|
|
680
745
|
|
|
746
|
+
{% comment %} Resolve feature value from config limits if not set {% endcomment %}
|
|
747
|
+
{% if has_feature and feature_value == nil and _tbl_config_product %}
|
|
748
|
+
{% assign _tbl_limit = nil %}{% for _lim in _tbl_config_product.limits %}{% if _lim[0] == feature_def.id %}{% assign _tbl_limit = _lim[1] %}{% break %}{% endif %}{% endfor %}
|
|
749
|
+
{% if _tbl_limit == -1 %}
|
|
750
|
+
{% assign feature_value = "Unlimited" %}
|
|
751
|
+
{% elsif _tbl_limit %}
|
|
752
|
+
{% assign feature_value = _tbl_limit %}
|
|
753
|
+
{% endif %}
|
|
754
|
+
{% endif %}
|
|
755
|
+
|
|
681
756
|
{% unless has_feature %}
|
|
682
757
|
{% for check_plan in page.resolved.pricing.plans %}
|
|
683
758
|
{% if check_plan.id == plan.id %}
|
|
@@ -690,6 +765,26 @@ faqs:
|
|
|
690
765
|
{% break %}
|
|
691
766
|
{% endif %}
|
|
692
767
|
{% endfor %}
|
|
768
|
+
|
|
769
|
+
{% comment %} Resolve inherited value from config limits if not set {% endcomment %}
|
|
770
|
+
{% if inherited_feature and inherited_value == nil %}
|
|
771
|
+
{% assign _inh_config_product = nil %}
|
|
772
|
+
{% for p in site.web_manager.payment.products %}
|
|
773
|
+
{% if p.id == check_plan.id %}
|
|
774
|
+
{% assign _inh_config_product = p %}
|
|
775
|
+
{% break %}
|
|
776
|
+
{% endif %}
|
|
777
|
+
{% endfor %}
|
|
778
|
+
{% if _inh_config_product %}
|
|
779
|
+
{% assign _inh_limit = nil %}{% for _lim in _inh_config_product.limits %}{% if _lim[0] == feature_def.id %}{% assign _inh_limit = _lim[1] %}{% break %}{% endif %}{% endfor %}
|
|
780
|
+
{% if _inh_limit == -1 %}
|
|
781
|
+
{% assign inherited_value = "Unlimited" %}
|
|
782
|
+
{% elsif _inh_limit %}
|
|
783
|
+
{% assign inherited_value = _inh_limit %}
|
|
784
|
+
{% endif %}
|
|
785
|
+
{% endif %}
|
|
786
|
+
{% endif %}
|
|
787
|
+
|
|
693
788
|
{% if inherited_feature %}
|
|
694
789
|
{% break %}
|
|
695
790
|
{% endif %}
|
|
@@ -6,13 +6,5 @@ permalink: /search/cse
|
|
|
6
6
|
### REGULAR PAGES ###
|
|
7
7
|
redirect:
|
|
8
8
|
url: "https://cse.google.com/cse?cx={{ page.resolved.advertising.cse.site-id }}&ie=UTF-8"
|
|
9
|
-
modifier: "
|
|
10
|
-
function (url) {
|
|
11
|
-
var q = url.searchParams.get('q');
|
|
12
|
-
|
|
13
|
-
url.searchParams.set('q', 'site:' + window.location.origin + ' ' + q);
|
|
14
|
-
|
|
15
|
-
return url;
|
|
16
|
-
}
|
|
17
|
-
"
|
|
9
|
+
modifier: "search-cse"
|
|
18
10
|
---
|
|
@@ -134,18 +134,17 @@ web_manager:
|
|
|
134
134
|
config:
|
|
135
135
|
autoRequest: 1000 * 60
|
|
136
136
|
validRedirectHosts: []
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
enabled: false
|
|
137
|
+
payment:
|
|
138
|
+
processors:
|
|
139
|
+
stripe:
|
|
140
|
+
publishableKey: null
|
|
141
|
+
paypal:
|
|
142
|
+
clientId: null
|
|
143
|
+
chargebee:
|
|
144
|
+
site: null
|
|
145
|
+
coinbase:
|
|
146
|
+
enabled: false
|
|
147
|
+
products: []
|
|
149
148
|
|
|
150
149
|
# OAuth2
|
|
151
150
|
oauth2:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-jekyll-manager",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Ultimate Jekyll dependency manager",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"chart.js": "^4.5.1",
|
|
79
79
|
"cheerio": "^1.2.0",
|
|
80
80
|
"chrome-launcher": "^1.2.1",
|
|
81
|
+
"dompurify": "^3.3.3",
|
|
81
82
|
"dotenv": "^17.4.1",
|
|
82
83
|
"fast-xml-parser": "^5.5.10",
|
|
83
84
|
"fs-jetpack": "^5.1.0",
|