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 +11 -0
- package/dist/assets/js/pages/payment/checkout/index.js +10 -26
- package/dist/assets/js/pages/payment/checkout/modules/api.js +18 -18
- package/dist/assets/js/pages/payment/checkout/modules/discount.js +33 -17
- package/dist/assets/js/pages/payment/checkout/modules/state.js +10 -16
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/post.html +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
215
|
-
|
|
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('
|
|
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
|
-
//
|
|
47
|
-
const
|
|
56
|
+
// Discount code from form data (validated server-side)
|
|
57
|
+
const discountCode = state.discountCode || '';
|
|
48
58
|
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
33
|
+
try {
|
|
34
|
+
const result = await validateDiscountCode(_webManager, code);
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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
|
}
|