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 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, 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
 
@@ -30,24 +30,38 @@ function showError(message) {
30
30
  updateUI();
31
31
  }
32
32
 
33
- // Generate unique checkout session ID
34
- function generateCheckoutId() {
35
- const urlParams = new URLSearchParams(window.location.search);
36
- const existing = urlParams.get('checkoutId');
37
- if (existing) return existing;
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 timestamp = Date.now().toString(36);
40
- const random1 = Math.random().toString(36).substring(2, 8);
41
- const random2 = Math.random().toString(36).substring(2, 8);
42
- return `CHK-${timestamp}-${random1}-${random2}`.toUpperCase();
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
- // Auto-apply welcome coupon
154
- autoApplyWelcomeCoupon(formManager, updateUI);
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 (!$submitButton) {
183
- throw new Error('Please choose a payment method.');
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('Invalid payment method selected.');
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
- // 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
- }
@@ -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
- 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
  }
@@ -1,6 +1,7 @@
1
1
  {
2
2
  logo: {
3
- href: '/'
3
+ href: '/',
4
+ class: 'filter-adaptive',
4
5
  },
5
6
  links: [
6
7
  {
@@ -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="false">
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="false">
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 &amp; Reload</button>
143
151
  </div>
144
152
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "0.0.290",
3
+ "version": "0.0.293",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {