ultimate-jekyll-manager 0.0.291 → 0.0.294

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
@@ -29,6 +29,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
29
29
  - Dev-only warning in FormManager for form fields missing `name` attributes (skipped by validation and `getData()`)
30
30
 
31
31
  ### Changed
32
+ - Replace hardcoded discount codes with server-side validation via `payments/discount` API endpoint
33
+ - Simplify payment intent payload: remove `auth`, `cancelUrl`, and `verification.status` fields; send `discountCode` from validated state
34
+ - Form submit falls back to first visible payment button when Enter is pressed instead of throwing
35
+ - Clear FormManager dirty state before redirect to avoid "leave site" prompt
36
+ - Use proper adjective forms in subscription terms text (e.g., "annual" instead of "annually")
37
+ - Add discount disclaimer to subscription terms when a discount code is applied
32
38
  - Align billing section to backend SSOT: consume unified subscription structure directly (3 statuses, `product.id` as object, `payment.price` in dollars, `cancellation.pending`, `trial.claimed` + `trial.expires`)
33
39
  - Use WM bindings (`data-wm-bind`) for billing plan heading, action button visibility, and cancel trigger instead of manual JS DOM manipulation
34
40
  - Standardize cancel, delete, and data-request forms to use FormManager built-in `required` validation instead of manual disabled toggle and checkbox throws
@@ -37,6 +43,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
37
43
  - Checkout payment method buttons start hidden and are revealed via `data-wm-bind` when payment methods load
38
44
  - Remove development-only guard from click prevention logging in body.html
39
45
 
46
+ ### Removed
47
+ - Remove hardcoded `DISCOUNT_CODES` map and `autoApplyWelcomeCoupon` (replaced by server-side validation)
48
+ - Remove `generateCheckoutId` and `state.checkoutId` from checkout session
49
+ - Unexport `resolvePrice` helper (internal-only usage)
50
+
40
51
  ### Fixed
41
52
  - Fix broken `</>` tag in checkout HTML causing page rendering to break
42
53
  - Fix checkout price display for APIs returning plain numbers instead of `{amount: N}` objects
@@ -2,7 +2,7 @@
2
2
  import { FormManager } from '__main_assets__/js/libs/form-manager.js';
3
3
  import { fetchAppConfig, fetchTrialEligibility, warmupServer, createPaymentIntent } from './modules/api.js';
4
4
  import { state, buildBindingsState, resolveProcessor, FREQUENCIES, getAvailableFrequencies } from './modules/state.js';
5
- import { applyDiscountCode, autoApplyWelcomeCoupon } from './modules/discount.js';
5
+ import { applyDiscountCode } from './modules/discount.js';
6
6
  import { initializeRecaptcha } from './modules/recaptcha.js';
7
7
  import { trackBeginCheckout, trackAddPaymentInfo } from './modules/tracking.js';
8
8
 
@@ -59,24 +59,9 @@ function trackAbandonedCart(webManager, product, state) {
59
59
  .catch((e) => console.warn('Failed to track abandoned cart:', e));
60
60
  }
61
61
 
62
- // Generate unique checkout session ID
63
- function generateCheckoutId() {
64
- const urlParams = new URLSearchParams(window.location.search);
65
- const existing = urlParams.get('checkoutId');
66
- if (existing) return existing;
67
-
68
- const timestamp = Date.now().toString(36);
69
- const random1 = Math.random().toString(36).substring(2, 8);
70
- const random2 = Math.random().toString(36).substring(2, 8);
71
- return `CHK-${timestamp}-${random1}-${random2}`.toUpperCase();
72
- }
73
-
74
62
  // Initialize checkout
75
63
  async function initializeCheckout() {
76
64
  try {
77
- // Generate session ID
78
- state.checkoutId = generateCheckoutId();
79
-
80
65
  // Parse URL params
81
66
  const urlParams = new URLSearchParams(window.location.search);
82
67
  const productId = urlParams.get('product');
@@ -182,9 +167,6 @@ async function initializeCheckout() {
182
167
  // Create/reset abandoned cart tracker (fire-and-forget, authenticated only)
183
168
  trackAbandonedCart(webManager, product, state);
184
169
 
185
- // Auto-apply welcome coupon
186
- autoApplyWelcomeCoupon(formManager, updateUI);
187
-
188
170
  } catch (error) {
189
171
  console.error('Checkout initialization failed:', error);
190
172
  showError(error.message || 'Failed to load checkout. Please refresh the page and try again.');
@@ -211,13 +193,12 @@ function setupForm() {
211
193
 
212
194
  // Form submission (payment)
213
195
  formManager.on('submit', async ({ $submitButton }) => {
214
- if (!$submitButton) {
215
- throw new Error('Please choose a payment method.');
216
- }
217
-
218
- const paymentMethod = $submitButton.getAttribute('data-payment-method');
196
+ // Fall back to first visible payment button if Enter was pressed without clicking one
197
+ const $btn = $submitButton
198
+ || document.querySelector('#checkout-form button[data-payment-method]:not([hidden])');
199
+ const paymentMethod = $btn?.getAttribute('data-payment-method');
219
200
  if (!paymentMethod) {
220
- throw new Error('Invalid payment method selected.');
201
+ throw new Error('Please choose a payment method.');
221
202
  }
222
203
 
223
204
  // Track payment info
@@ -234,6 +215,9 @@ function setupForm() {
234
215
  formData: formManager.getData(),
235
216
  });
236
217
 
218
+ // Clear dirty state so FormManager doesn't trigger "leave site" prompt
219
+ formManager.setDirty(false);
220
+
237
221
  // Redirect to processor checkout
238
222
  window.location.href = response.url;
239
223
 
@@ -246,7 +230,7 @@ function setupForm() {
246
230
  if ($applyDiscountBtn) {
247
231
  $applyDiscountBtn.addEventListener('click', () => {
248
232
  const data = formManager.getData();
249
- applyDiscountCode(data.discount, updateUI);
233
+ applyDiscountCode(data.discount, updateUI, webManager);
250
234
  });
251
235
  }
252
236
 
@@ -30,6 +30,16 @@ export async function fetchTrialEligibility(webManager) {
30
30
  }
31
31
  }
32
32
 
33
+ // Validate a discount code via backend
34
+ export async function validateDiscountCode(webManager, code) {
35
+ const response = await fetch(`${webManager.getApiUrl()}/backend-manager/payments/discount`, {
36
+ response: 'json',
37
+ query: { code },
38
+ });
39
+
40
+ return response;
41
+ }
42
+
33
43
  // Fire-and-forget server warmup
34
44
  export function warmupServer(webManager) {
35
45
  fetch(`${webManager.getApiUrl()}/backend-manager/payments/intent`, {
@@ -43,28 +53,22 @@ export async function createPaymentIntent({ webManager, state, processor, formDa
43
53
  // Get reCAPTCHA token
44
54
  const recaptchaToken = await getRecaptchaToken('payment_intent');
45
55
 
46
- // User info
47
- const user = webManager.auth().getUser();
56
+ // Discount code from form data (validated server-side)
57
+ const discountCode = state.discountCode || '';
48
58
 
49
- // Discount code from form data
50
- const discountCode = state.discountPercent > 0
51
- ? (formData.discount || '').trim().toUpperCase()
52
- : undefined;
59
+ // Supplemental form data (everything except fields we handle explicitly)
60
+ const supplemental = { ...formData };
61
+ delete supplemental.frequency;
62
+ delete supplemental.discount;
53
63
 
54
- // Build payload (flat fields to match backend schema)
64
+ // Build payload
55
65
  const payload = {
56
66
  processor,
57
67
  productId: state.product.id,
58
68
  frequency: state.frequency,
59
69
  trial: state.trialEligible,
60
- auth: {
61
- uid: user?.uid || '',
62
- email: user?.email || '',
63
- },
64
70
  attribution: webManager.storage().get('attribution', {}),
65
- cancelUrl: window.location.href,
66
71
  verification: {
67
- status: 'pending',
68
72
  'g-recaptcha-response': recaptchaToken || '',
69
73
  },
70
74
  };
@@ -74,11 +78,7 @@ export async function createPaymentIntent({ webManager, state, processor, formDa
74
78
  payload.discount = discountCode;
75
79
  }
76
80
 
77
- if (formData) {
78
- // Clean form data -- remove fields we handle explicitly
79
- const supplemental = { ...formData };
80
- delete supplemental.frequency;
81
- delete supplemental.discount;
81
+ if (Object.keys(supplemental).length > 0) {
82
82
  payload.supplemental = supplemental;
83
83
  }
84
84
 
@@ -1,12 +1,26 @@
1
1
  // Discount code logic for checkout
2
- import { state, DISCOUNT_CODES } from './state.js';
2
+ import { state } from './state.js';
3
+ import { validateDiscountCode } from './api.js';
4
+
5
+ // Cached webManager reference (set on first call)
6
+ let _webManager = null;
7
+
8
+ /**
9
+ * Apply a discount code via server-side validation
10
+ * @param {string} code - Discount code to validate
11
+ * @param {Function} updateUI - Callback to refresh bindings
12
+ * @param {object} webManager - WebManager instance (required on first call)
13
+ */
14
+ export async function applyDiscountCode(code, updateUI, webManager) {
15
+ if (webManager) {
16
+ _webManager = webManager;
17
+ }
3
18
 
4
- // Apply a discount code
5
- // updateUI callback decouples this from the bindings system
6
- export async function applyDiscountCode(code, updateUI) {
7
19
  code = (code || '').trim().toUpperCase();
8
20
 
9
21
  if (!code) {
22
+ state.discountCode = null;
23
+ state.discountPercent = 0;
10
24
  state.discountUI = { loading: false, success: false, error: true, message: 'Please enter a discount code' };
11
25
  updateUI();
12
26
  return;
@@ -16,22 +30,24 @@ export async function applyDiscountCode(code, updateUI) {
16
30
  state.discountUI = { loading: true, success: false, error: false, message: '' };
17
31
  updateUI();
18
32
 
19
- // Simulate API delay (TODO: replace with real API call)
20
- await new Promise(resolve => setTimeout(resolve, 800));
33
+ try {
34
+ const result = await validateDiscountCode(_webManager, code);
21
35
 
22
- if (DISCOUNT_CODES[code]) {
23
- state.discountPercent = DISCOUNT_CODES[code];
24
- state.discountUI = { loading: false, success: true, error: false, message: `Discount applied: ${state.discountPercent}% off` };
25
- } else {
36
+ if (result.valid) {
37
+ state.discountCode = result.code;
38
+ state.discountPercent = result.percent;
39
+ state.discountUI = { loading: false, success: true, error: false, message: `Discount applied: ${result.percent}% off` };
40
+ } else {
41
+ state.discountCode = null;
42
+ state.discountPercent = 0;
43
+ state.discountUI = { loading: false, success: false, error: true, message: 'Invalid discount code' };
44
+ }
45
+ } catch (e) {
46
+ console.warn('Discount validation failed:', e);
47
+ state.discountCode = null;
26
48
  state.discountPercent = 0;
27
- state.discountUI = { loading: false, success: false, error: true, message: 'Invalid discount code' };
49
+ state.discountUI = { loading: false, success: false, error: true, message: 'Unable to validate discount code. Please try again.' };
28
50
  }
29
51
 
30
52
  updateUI();
31
53
  }
32
-
33
- // Auto-apply welcome coupon
34
- export function autoApplyWelcomeCoupon(formManager, updateUI) {
35
- formManager.setData({ discount: 'WELCOME15' });
36
- applyDiscountCode('WELCOME15', updateUI);
37
- }
@@ -6,13 +6,6 @@ import { calculatePrices } from './pricing.js';
6
6
  // All supported billing frequencies
7
7
  export const FREQUENCIES = ['daily', 'weekly', 'monthly', 'annually'];
8
8
 
9
- // Hardcoded discount codes (TODO: move to API)
10
- export const DISCOUNT_CODES = {
11
- 'FLASH20': 20,
12
- 'SAVE10': 10,
13
- 'WELCOME15': 15,
14
- };
15
-
16
9
  // Minimal mutable state
17
10
  export const state = {
18
11
  // From API (stored once, never transformed)
@@ -22,15 +15,13 @@ export const state = {
22
15
 
23
16
  // User selections
24
17
  frequency: 'annually',
18
+ discountCode: null,
25
19
  discountPercent: 0,
26
20
  trialEligible: false,
27
21
 
28
22
  // UI state
29
23
  discountUI: { loading: false, success: false, error: false, message: '' },
30
24
  error: { show: false, message: '' },
31
-
32
- // Session
33
- checkoutId: null,
34
25
  };
35
26
 
36
27
  // Resolve which processor handles a payment method
@@ -52,7 +43,7 @@ export function resolveProcessor(paymentMethod) {
52
43
  }
53
44
 
54
45
  // Resolve price for a frequency (handles both `{ amount: N }` and plain `N` formats)
55
- export function resolvePrice(product, frequency) {
46
+ function resolvePrice(product, frequency) {
56
47
  const entry = product?.prices?.[frequency];
57
48
  if (entry == null) return 0;
58
49
  return typeof entry === 'object' ? (entry.amount || 0) : Number(entry) || 0;
@@ -122,7 +113,7 @@ export function buildBindingsState(webManager) {
122
113
  recurringAmount: formatCurrency(prices.recurring),
123
114
  recurringPeriod: frequencyLabels[cycle] || cycle,
124
115
  showTerms: isSubscription,
125
- termsText: buildTermsText(product, cycle, hasFreeTrial, prices),
116
+ termsText: buildTermsText(product, cycle, hasFreeTrial, prices, state.discountPercent > 0),
126
117
  },
127
118
  trial: {
128
119
  show: hasFreeTrial,
@@ -170,10 +161,11 @@ function formatCurrency(amount) {
170
161
  const FREQUENCY_DAYS = { daily: 1, weekly: 7, monthly: 30, annually: 365 };
171
162
 
172
163
  // Build subscription terms text
173
- function buildTermsText(product, cycle, hasFreeTrial, prices) {
164
+ function buildTermsText(product, cycle, hasFreeTrial, prices, hasDiscount) {
174
165
  if (!product || product.type !== 'subscription') return '';
175
166
 
176
- const periodText = cycle;
167
+ const periodAdjectiveMap = { daily: 'daily', weekly: 'weekly', monthly: 'monthly', annually: 'annual' };
168
+ const periodText = periodAdjectiveMap[cycle] || cycle;
177
169
  const renewalDate = new Date();
178
170
  const daysToAdd = hasFreeTrial
179
171
  ? (product.trial?.days || 7)
@@ -186,9 +178,11 @@ function buildTermsText(product, cycle, hasFreeTrial, prices) {
186
178
  year: 'numeric',
187
179
  });
188
180
 
181
+ const discountNote = hasDiscount ? ' Discount code applies to first payment only and is not available with PayPal.' : '';
182
+
189
183
  if (hasFreeTrial) {
190
- return `You won't be charged for your free trial. On ${formatted}, your ${periodText} subscription will start and you'll be charged ${formatCurrency(prices.recurring)} plus applicable tax. Cancel anytime before then.`;
184
+ return `You won't be charged for your free trial. On ${formatted}, your ${periodText} subscription will start and you'll be charged ${formatCurrency(prices.recurring)} plus applicable tax. Cancel anytime before then.${discountNote}`;
191
185
  }
192
186
 
193
- return `Your ${periodText} subscription will start today and renew on ${formatted} for ${formatCurrency(prices.recurring)} plus applicable tax. Cancel anytime.`;
187
+ return `Your ${periodText} subscription will start today and renew on ${formatted} for ${formatCurrency(prices.recurring)} plus applicable tax. Cancel anytime.${discountNote}`;
194
188
  }
@@ -37,7 +37,7 @@ newsletter:
37
37
  ---
38
38
 
39
39
  <!-- Article Header & Content -->
40
- <article class="pt-8">
40
+ <article class="pt-10">
41
41
  <div class="container">
42
42
  <div class="row justify-content-center mb-3">
43
43
  <div class="col-xl-8 col-lg-8 col-md-12 col-12">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "0.0.291",
3
+ "version": "0.0.294",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {