ultimate-jekyll-manager 1.0.22 → 1.1.0

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 CHANGED
@@ -15,6 +15,25 @@ 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.0] - 2026-04-06
19
+ ### Added
20
+ - `payment-config.js` shared library for reading payment data from build-time config
21
+ - Pricing layout resolves prices and feature limits from `_config.yml` when not set in frontmatter
22
+ - `oauth2` config injected into client-side Configuration object via `foot.html`
23
+ - Pricing page shows "Switch to This Plan" on other paid plans when user has active subscription
24
+
25
+ ### Changed
26
+ - Move `payment` under `web_manager` in default `_config.yml` so it serializes into client-side config
27
+ - Checkout page uses `payment-config.js` instead of fetching `/backend-manager/brand`
28
+ - Account billing section uses config for products/limits/currency instead of brand API
29
+ - Account connections section reads `oauth2` from config instead of brand API
30
+ - Admin dashboard uses config for product list in MRR calculations
31
+ - Remove `/backend-manager/brand` fetch from account page entirely
32
+ - "Everything in [plan]" now uses dynamic previous plan name instead of hardcoded index
33
+
34
+ ### Fixed
35
+ - Liquid 4.x compatibility: use loop-based hash lookup instead of bracket notation for config limits
36
+
18
37
  ## [1.0.22] - 2026-04-05
19
38
  ### Changed
20
39
  - Bump web-manager from ^4.1.36 to ^4.1.37
package/CLAUDE.md CHANGED
@@ -1136,6 +1136,108 @@ webManager.auth().listen({ once: true }, async () => {
1136
1136
 
1137
1137
  **Reference:** `src/assets/js/libs/authorized-fetch.js`
1138
1138
 
1139
+ #### Payment Config Library
1140
+
1141
+ 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.
1142
+
1143
+ **Import:**
1144
+ ```javascript
1145
+ import { getPaymentConfig, getProcessors, getProducts, getProductById, getProductLimits, getCurrency } from '__main_assets__/js/libs/payment-config.js';
1146
+ ```
1147
+
1148
+ **Usage:**
1149
+ ```javascript
1150
+ // Get all products
1151
+ const products = getProducts();
1152
+
1153
+ // Find a specific product
1154
+ const product = getProductById('plus');
1155
+
1156
+ // Get product limits
1157
+ const limits = getProductLimits('plus'); // { credits: 500, agents: 3, ... }
1158
+
1159
+ // Get processors (stripe, paypal, etc.)
1160
+ const processors = getProcessors();
1161
+ ```
1162
+
1163
+ **Config location in `_config.yml`:**
1164
+ ```yaml
1165
+ web_manager:
1166
+ payment:
1167
+ processors:
1168
+ stripe:
1169
+ publishableKey: pk_live_...
1170
+ paypal:
1171
+ clientId: ...
1172
+ products:
1173
+ - id: basic
1174
+ name: Basic
1175
+ limits:
1176
+ credits: 100
1177
+ - id: plus
1178
+ name: Plus
1179
+ limits:
1180
+ credits: 500
1181
+ prices:
1182
+ monthly: 19
1183
+ annually: 190
1184
+ ```
1185
+
1186
+ **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.
1187
+
1188
+ **When to still use the brand API:**
1189
+ - `oauth2` provider configuration (used by the connections section on the account page)
1190
+ - Any data that is NOT in `_config.yml` and only exists server-side
1191
+
1192
+ **Reference:** `src/assets/js/libs/payment-config.js`
1193
+
1194
+ #### Pricing Page: Config-Resolved Values
1195
+
1196
+ 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.
1197
+
1198
+ **Resolution order (frontmatter wins):**
1199
+ 1. `plan.pricing.monthly` / `plan.pricing.annually` from page frontmatter
1200
+ 2. `site.web_manager.payment.products[matching_id].prices.monthly` / `.annually` from config
1201
+ 3. `0` (default)
1202
+
1203
+ **Feature value resolution:**
1204
+ 1. `feature.value` from page frontmatter
1205
+ 2. `site.web_manager.payment.products[matching_id].limits[feature.id]` from config (with `-1` → `"Unlimited"`)
1206
+
1207
+ **Example: Minimal pricing.md (prices/limits come from config):**
1208
+ ```yaml
1209
+ ---
1210
+ layout: blueprint/pricing
1211
+ permalink: /pricing
1212
+
1213
+ pricing:
1214
+ plans:
1215
+ - id: "basic"
1216
+ name: "Basic"
1217
+ tagline: "best for getting started"
1218
+ url: "/dashboard"
1219
+ features:
1220
+ - id: "credits"
1221
+ name: "Credits"
1222
+ icon: "sparkles"
1223
+ - id: "agents"
1224
+ name: "Agents"
1225
+ icon: "robot"
1226
+ - id: "plus"
1227
+ name: "Plus"
1228
+ tagline: "best for small websites"
1229
+ features:
1230
+ - id: "credits"
1231
+ name: "Credits"
1232
+ icon: "sparkles"
1233
+ - id: "agents"
1234
+ name: "Agents"
1235
+ icon: "robot"
1236
+ ---
1237
+ ```
1238
+
1239
+ In this example, `credits` value of 100 and price of $19/mo come from `_config.yml`'s `web_manager.payment.products` — no hardcoding needed.
1240
+
1139
1241
  #### FormManager Library
1140
1242
 
1141
1243
  Lightweight form state management library with built-in validation, state machine, and event system.
@@ -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
+ }
@@ -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
- let brandData = null;
37
- let fetchBrandDataPromise = null;
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, brandData);
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, brandData);
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 brandData = null;
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, sharedBrandData) {
44
+ export async function loadData(account, sharedPaymentConfig) {
45
45
  if (!account) {
46
46
  return;
47
47
  }
48
48
 
49
- brandData = sharedBrandData;
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 = brandData?.payment?.currency || 'USD';
107
+ const currency = paymentConfig?.currency || 'USD';
108
108
  const frequency = subscription.payment?.frequency;
109
109
  const hasValidBilling = nextBillingUnix && nextBillingUnix > 0 && amount;
110
110
 
@@ -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 = brandData?.payment?.products?.find(p => p.id === resolved.plan);
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 brandData product name
407
+ // Fall back to config product name
408
408
  const productId = subscription.product?.id || 'basic';
409
- const product = brandData?.payment?.products?.find(p => p.id === productId);
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 brandData = null;
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, sharedBrandData) {
27
+ export async function loadData(account, sharedOAuth2Config) {
28
28
  if (!account) {
29
29
  return;
30
30
  }
31
31
 
32
32
  accountData = account;
33
- brandData = sharedBrandData;
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 (!brandData) {
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 = brandData?.oauth2 || {};
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 = brandData?.oauth2?.[providerId];
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 = brandData?.oauth2?.[provider];
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 = brandData?.oauth2?.[providerId];
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.');
@@ -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 && brandData) {
325
+ if (accountData && oauth2Config) {
326
326
  displayConnections();
327
327
  }
328
328
  }
@@ -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
- // Fetch brand config to get product list and available frequencies
84
- const brandConfig = await fetch(`${webManager.getApiUrl()}/backend-manager/brand`, {
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 { fetchBrandConfig, fetchTrialEligibility, warmupServer, createPaymentIntent } from './modules/api.js';
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: brand config + trial eligibility + reCAPTCHA
81
- const [brandConfigResult, trialResult, recaptchaResult] = await Promise.allSettled([
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 = state.brandConfig?.payment?.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 API (stored once, never transformed)
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
- // Disable button for the user's current active plan
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
- const $button = document.querySelector(`button[data-plan-id="${resolved.plan}"]`);
332
-
333
- if (!$button) {
334
- return;
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
- $button.disabled = true;
338
- $button.textContent = 'Current Plan';
339
- $button.classList.remove('btn-primary', 'btn-gradient-rainbow', 'gradient-animated');
340
- $button.classList.add('btn-adaptive');
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
 
@@ -102,6 +102,7 @@
102
102
  {% endfor %}
103
103
  env: {{ page.resolved.web_manager.env | jsonify }},
104
104
  recaptcha: {{ page.resolved.recaptcha | jsonify }},
105
+ oauth2: {{ page.resolved.oauth2 | jsonify }},
105
106
  };
106
107
  </script>
107
108
 
@@ -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 plan.pricing.monthly == 0 %}
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="{{ plan.pricing.monthly }}" data-annually="{{ plan.pricing.annually | divided_by: 12 | round }}">{{ plan.pricing.annually | divided_by: 12 | round }}</span>
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 plan.pricing.monthly > 0 %}
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
- {% assign monthly_price_per_unit = plan.pricing.monthly | times: 1.0 | divided_by: feature.value | round: 2 %}
370
- {% assign annual_monthly_price = plan.pricing.annually | divided_by: 12.0 %}
371
- {% assign annual_price_per_unit = annual_monthly_price | divided_by: feature.value | round: 2 %}
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 plan.pricing.monthly == 0 %}btn-adaptive{% else %}{{ btn_style }}{% endif %} btn-md fw-semibold px-2 fs-5">
393
- {% if plan.pricing.monthly == 0 %}
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 plan.pricing.monthly == 0 %}
440
+ {% if _plan_monthly == 0 %}
410
441
  <span>No credit card required</span>
411
442
  {% else %}
412
- <span class="billing-info" data-monthly="Billed ${{ plan.pricing.monthly | uj_commaify }} monthly" data-annually="Billed ${{ plan.pricing.annually | uj_commaify }} annually">
413
- Billed ${{ plan.pricing.annually | uj_commaify }} annually
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 feature.value == "Unlimited" %}
477
+ {% if _feature_value == "Unlimited" %}
437
478
  Unlimited
438
479
  {% else %}
439
- {{ feature.value | uj_commaify }}
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 feature.value == "24/7" %}
481
- {{ feature.value }}
482
- {% elsif feature.value == "Included" or feature.value == "Available" or feature.value == "Full" %}
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
- {{ feature.value | uj_commaify }}
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 feature.value == "24/7" %}
542
- {{ feature.value }}
543
- {% elsif feature.value == "Included" or feature.value == "Available" or feature.value == "Full" %}
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
- {{ feature.value | uj_commaify }}
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 %}
@@ -134,18 +134,17 @@ web_manager:
134
134
  config:
135
135
  autoRequest: 1000 * 60
136
136
  validRedirectHosts: []
137
-
138
- # Payment
139
- payment:
140
- processors:
141
- stripe:
142
- publishableKey: null
143
- paypal:
144
- clientId: null
145
- chargebee:
146
- site: null
147
- coinbase:
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.0.22",
3
+ "version": "1.1.0",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {