ultimate-jekyll-manager 0.0.290 → 0.0.293
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 +13 -0
- package/TODO-NEW.md +21 -0
- package/dist/assets/css/core/_utilities.scss +22 -0
- package/dist/assets/js/pages/payment/checkout/index.js +38 -22
- 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/recaptcha.js +14 -0
- package/dist/assets/js/pages/payment/checkout/modules/state.js +10 -16
- package/dist/defaults/dist/_includes/admin/sections/sidebar.json +2 -1
- package/dist/defaults/dist/_includes/themes/classy/backend/sections/sidebar.html +22 -4
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/checkout.html +8 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
17
17
|
---
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
### Added
|
|
20
|
+
- Abandoned cart tracking on checkout page: creates a Firestore document in `payments-carts/{uid}` when authenticated users begin checkout, with a 15-minute first reminder delay
|
|
21
|
+
- Backend sidebar auto-expands collapsible dropdown sections containing the currently active page link (desktop and mobile)
|
|
20
22
|
- Email preferences page (`/portal/account/email-preferences`) for unsubscribe/resubscribe from marketing emails
|
|
21
23
|
- Email masking on preferences page to prevent forwarded-email abuse (e.g., `ia***b@gm***.com`)
|
|
22
24
|
- HMAC signature verification for unsubscribe links to prevent forged requests
|
|
@@ -27,6 +29,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
27
29
|
- Dev-only warning in FormManager for form fields missing `name` attributes (skipped by validation and `getData()`)
|
|
28
30
|
|
|
29
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
|
|
30
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`)
|
|
31
39
|
- Use WM bindings (`data-wm-bind`) for billing plan heading, action button visibility, and cancel trigger instead of manual JS DOM manipulation
|
|
32
40
|
- Standardize cancel, delete, and data-request forms to use FormManager built-in `required` validation instead of manual disabled toggle and checkbox throws
|
|
@@ -35,6 +43,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
35
43
|
- Checkout payment method buttons start hidden and are revealed via `data-wm-bind` when payment methods load
|
|
36
44
|
- Remove development-only guard from click prevention logging in body.html
|
|
37
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
|
+
|
|
38
51
|
### Fixed
|
|
39
52
|
- Fix broken `</>` tag in checkout HTML causing page rendering to break
|
|
40
53
|
- Fix checkout price display for APIs returning plain numbers instead of `{amount: N}` objects
|
package/TODO-NEW.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
ALSO!!!
|
|
2
|
+
I THINK THE SIGNUP EVENT IS NOT FIRING (the manual send that comes from the user to the server)
|
|
3
|
+
|
|
4
|
+
also, singup takes a while to go thru make go fstrrrr
|
|
5
|
+
|
|
6
|
+
on BEM, we need to store the brand data somewhere else besides ITW because thats stupid and takes too long to load
|
|
7
|
+
|
|
8
|
+
also,, need to handle the auth error for too many signups that we manually throw via beforeCreate
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
FOR ATTRIBUTION FIXES LOOK AT
|
|
12
|
+
/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/core/auth.js
|
|
13
|
+
/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/core/query-strings.js
|
|
14
|
+
/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/core/exit-popup.js
|
|
15
|
+
/Users/ian/Developer/Repositories/ITW-Creative-Works/ultimate-jekyll-manager/src/assets/js/pages/payment/checkout/modules/tracking.js
|
|
16
|
+
and anywhere else with attribution.!!!!
|
|
17
|
+
|
|
18
|
+
FOR TRACKING FIXES (JSUTSEARCH FOR TRACKING EVENTS)
|
|
19
|
+
|
|
20
|
+
CHECK OUT LEGACY!!!
|
|
21
|
+
/Users/ian/Developer/Repositories/_Legacy/ultimate-jekyll-legacy/.github/workflows/build.yml
|
|
@@ -78,3 +78,25 @@ button *, a * {
|
|
|
78
78
|
opacity: $i * 0.1 !important;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
// ============================================
|
|
83
|
+
// Sidebar
|
|
84
|
+
// ============================================
|
|
85
|
+
.sidebar {
|
|
86
|
+
width: 280px;
|
|
87
|
+
min-width: 280px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.sidebar-logo {
|
|
91
|
+
min-height: 57px;
|
|
92
|
+
|
|
93
|
+
a {
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
span {
|
|
98
|
+
white-space: nowrap;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
text-overflow: ellipsis;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -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
|
|
|
@@ -30,24 +30,38 @@ function showError(message) {
|
|
|
30
30
|
updateUI();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
// Create/reset abandoned cart tracker in Firestore (fire-and-forget)
|
|
34
|
+
function trackAbandonedCart(webManager, product, state) {
|
|
35
|
+
const user = webManager.auth().getUser();
|
|
36
|
+
if (!user) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
38
39
|
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
40
|
+
const uid = user.uid;
|
|
41
|
+
const now = Math.floor(Date.now() / 1000);
|
|
42
|
+
const nowISO = new Date().toISOString();
|
|
43
|
+
const FIRST_REMINDER_DELAY = 900; // 15 minutes
|
|
44
|
+
|
|
45
|
+
webManager.firestore().doc(`payments-carts/${uid}`).set({
|
|
46
|
+
id: uid,
|
|
47
|
+
owner: uid,
|
|
48
|
+
status: 'pending',
|
|
49
|
+
productId: product.id,
|
|
50
|
+
type: product.type || 'subscription',
|
|
51
|
+
frequency: state.frequency || null,
|
|
52
|
+
reminderIndex: 0,
|
|
53
|
+
nextReminderAt: now + FIRST_REMINDER_DELAY,
|
|
54
|
+
metadata: {
|
|
55
|
+
created: { timestamp: nowISO, timestampUNIX: now },
|
|
56
|
+
updated: { timestamp: nowISO, timestampUNIX: now },
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
.catch((e) => console.warn('Failed to track abandoned cart:', e));
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
// Initialize checkout
|
|
46
63
|
async function initializeCheckout() {
|
|
47
64
|
try {
|
|
48
|
-
// Generate session ID
|
|
49
|
-
state.checkoutId = generateCheckoutId();
|
|
50
|
-
|
|
51
65
|
// Parse URL params
|
|
52
66
|
const urlParams = new URLSearchParams(window.location.search);
|
|
53
67
|
const productId = urlParams.get('product');
|
|
@@ -150,8 +164,8 @@ async function initializeCheckout() {
|
|
|
150
164
|
// Track begin_checkout
|
|
151
165
|
trackBeginCheckout(state);
|
|
152
166
|
|
|
153
|
-
//
|
|
154
|
-
|
|
167
|
+
// Create/reset abandoned cart tracker (fire-and-forget, authenticated only)
|
|
168
|
+
trackAbandonedCart(webManager, product, state);
|
|
155
169
|
|
|
156
170
|
} catch (error) {
|
|
157
171
|
console.error('Checkout initialization failed:', error);
|
|
@@ -179,13 +193,12 @@ function setupForm() {
|
|
|
179
193
|
|
|
180
194
|
// Form submission (payment)
|
|
181
195
|
formManager.on('submit', async ({ $submitButton }) => {
|
|
182
|
-
if
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
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');
|
|
187
200
|
if (!paymentMethod) {
|
|
188
|
-
throw new Error('
|
|
201
|
+
throw new Error('Please choose a payment method.');
|
|
189
202
|
}
|
|
190
203
|
|
|
191
204
|
// Track payment info
|
|
@@ -202,6 +215,9 @@ function setupForm() {
|
|
|
202
215
|
formData: formManager.getData(),
|
|
203
216
|
});
|
|
204
217
|
|
|
218
|
+
// Clear dirty state so FormManager doesn't trigger "leave site" prompt
|
|
219
|
+
formManager.setDirty(false);
|
|
220
|
+
|
|
205
221
|
// Redirect to processor checkout
|
|
206
222
|
window.location.href = response.url;
|
|
207
223
|
|
|
@@ -214,7 +230,7 @@ function setupForm() {
|
|
|
214
230
|
if ($applyDiscountBtn) {
|
|
215
231
|
$applyDiscountBtn.addEventListener('click', () => {
|
|
216
232
|
const data = formManager.getData();
|
|
217
|
-
applyDiscountCode(data.discount, updateUI);
|
|
233
|
+
applyDiscountCode(data.discount, updateUI, webManager);
|
|
218
234
|
});
|
|
219
235
|
}
|
|
220
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
|
-
}
|
|
@@ -56,6 +56,20 @@ export async function initializeRecaptcha(siteKey, webManager) {
|
|
|
56
56
|
|
|
57
57
|
// Get reCAPTCHA token
|
|
58
58
|
export async function getRecaptchaToken(action = 'checkout') {
|
|
59
|
+
/* @dev-only:start */
|
|
60
|
+
{
|
|
61
|
+
const devRecaptcha = new URLSearchParams(window.location.search).get('_dev_recaptcha');
|
|
62
|
+
if (devRecaptcha === 'invalid') {
|
|
63
|
+
console.warn('[Checkout Dev] Sending invalid reCAPTCHA token');
|
|
64
|
+
return 'invalid-dev-token';
|
|
65
|
+
}
|
|
66
|
+
if (devRecaptcha === 'empty') {
|
|
67
|
+
console.warn('[Checkout Dev] Sending empty reCAPTCHA token');
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/* @dev-only:end */
|
|
72
|
+
|
|
59
73
|
if (!recaptchaReady || !recaptchaSiteKey) {
|
|
60
74
|
console.warn('reCAPTCHA not initialized');
|
|
61
75
|
return null;
|
|
@@ -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
|
}
|
|
@@ -37,8 +37,17 @@
|
|
|
37
37
|
{% elsif link.dropdown %}
|
|
38
38
|
<!-- Collapsible Dropdown -->
|
|
39
39
|
{% capture link_id %}sidebar-collapse-{{ forloop.index }}{% endcapture %}
|
|
40
|
+
{% assign dropdown_has_active = false %}
|
|
41
|
+
{% for child in link.dropdown %}
|
|
42
|
+
{% unless child.divider %}
|
|
43
|
+
{% capture _check_active %}{% urlmatches child.href, "active" %}{% endcapture %}
|
|
44
|
+
{% if _check_active == "active" %}
|
|
45
|
+
{% assign dropdown_has_active = true %}
|
|
46
|
+
{% endif %}
|
|
47
|
+
{% endunless %}
|
|
48
|
+
{% endfor %}
|
|
40
49
|
<li class="mb-1">
|
|
41
|
-
<button class="btn btn-toggle d-inline-flex align-items-center justify-content-between rounded py-2 px-3 border-0 collapsed" data-bs-toggle="collapse" data-bs-target="#{{ link_id }}" aria-expanded="
|
|
50
|
+
<button class="btn btn-toggle d-inline-flex align-items-center justify-content-between rounded py-2 px-3 border-0 {% unless dropdown_has_active %}collapsed{% endunless %}" data-bs-toggle="collapse" data-bs-target="#{{ link_id }}" aria-expanded="{{ dropdown_has_active }}">
|
|
42
51
|
<span class="d-inline-flex align-items-center">
|
|
43
52
|
{% if link.icon %}
|
|
44
53
|
{% uj_icon link.icon, "fa-xl me-3" %}
|
|
@@ -46,7 +55,7 @@
|
|
|
46
55
|
{{ link.label }}
|
|
47
56
|
</span>
|
|
48
57
|
</button>
|
|
49
|
-
<div class="collapse" id="{{ link_id }}">
|
|
58
|
+
<div class="collapse {% if dropdown_has_active %}show{% endif %}" id="{{ link_id }}">
|
|
50
59
|
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
|
51
60
|
{% for child in link.dropdown %}
|
|
52
61
|
{% if child.divider %}
|
|
@@ -157,8 +166,17 @@
|
|
|
157
166
|
</li>
|
|
158
167
|
{% elsif link.dropdown %}
|
|
159
168
|
{% capture link_id %}mobile-collapse-{{ forloop.index }}{% endcapture %}
|
|
169
|
+
{% assign dropdown_has_active = false %}
|
|
170
|
+
{% for child in link.dropdown %}
|
|
171
|
+
{% unless child.divider %}
|
|
172
|
+
{% capture _check_active %}{% urlmatches child.href, "active" %}{% endcapture %}
|
|
173
|
+
{% if _check_active == "active" %}
|
|
174
|
+
{% assign dropdown_has_active = true %}
|
|
175
|
+
{% endif %}
|
|
176
|
+
{% endunless %}
|
|
177
|
+
{% endfor %}
|
|
160
178
|
<li class="mb-1">
|
|
161
|
-
<button class="btn btn-toggle d-inline-flex align-items-center justify-content-between rounded py-2 px-3 border-0 collapsed" data-bs-toggle="collapse" data-bs-target="#{{ link_id }}" aria-expanded="
|
|
179
|
+
<button class="btn btn-toggle d-inline-flex align-items-center justify-content-between rounded py-2 px-3 border-0 {% unless dropdown_has_active %}collapsed{% endunless %}" data-bs-toggle="collapse" data-bs-target="#{{ link_id }}" aria-expanded="{{ dropdown_has_active }}">
|
|
162
180
|
<span class="d-inline-flex align-items-center">
|
|
163
181
|
{% if link.icon %}
|
|
164
182
|
{% uj_icon link.icon, "fa-xl me-3" %}
|
|
@@ -166,7 +184,7 @@
|
|
|
166
184
|
{{ link.label }}
|
|
167
185
|
</span>
|
|
168
186
|
</button>
|
|
169
|
-
<div class="collapse" id="{{ link_id }}">
|
|
187
|
+
<div class="collapse {% if dropdown_has_active %}show{% endif %}" id="{{ link_id }}">
|
|
170
188
|
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
|
171
189
|
{% for child in link.dropdown %}
|
|
172
190
|
{% if child.divider %}
|
|
@@ -139,6 +139,14 @@ web_manager:
|
|
|
139
139
|
<option value="chargebee">Chargebee</option>
|
|
140
140
|
</select>
|
|
141
141
|
</div>
|
|
142
|
+
<div class="mb-2">
|
|
143
|
+
<label class="form-label mb-1 text-body-secondary">reCAPTCHA</label>
|
|
144
|
+
<select data-dev-param="_dev_recaptcha" class="form-select form-select-sm">
|
|
145
|
+
<option value="">(normal)</option>
|
|
146
|
+
<option value="invalid">Send invalid token</option>
|
|
147
|
+
<option value="empty">Send empty token</option>
|
|
148
|
+
</select>
|
|
149
|
+
</div>
|
|
142
150
|
<button id="checkout-dev-apply" class="btn btn-primary btn-sm w-100 fw-bold">Apply & Reload</button>
|
|
143
151
|
</div>
|
|
144
152
|
</div>
|