ultimate-jekyll-manager 0.0.273 → 0.0.274

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
@@ -18,13 +18,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
18
18
  ## [Unreleased]
19
19
  ### Added
20
20
  - Quick boot mode (`UJ_QUICK=true`) for faster dev server startup (~5s vs ~20s) by skipping clean, slow setup operations, and deferring webpack/sass compilation until after Jekyll's first build
21
+ - Dev-only warning in FormManager for form fields missing `name` attributes (skipped by validation and `getData()`)
21
22
 
22
23
  ### Changed
24
+ - 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`)
25
+ - Use WM bindings (`data-wm-bind`) for billing plan heading, action button visibility, and cancel trigger instead of manual JS DOM manipulation
26
+ - Standardize cancel, delete, and data-request forms to use FormManager built-in `required` validation instead of manual disabled toggle and checkbox throws
27
+ - Test subscriptions now deep-merge into real user data instead of full replacement, preserving actual product/payment info
23
28
  - Add `onsubmit="return false"` to all JS-managed forms as a safety net against native submission before FormManager loads
24
29
  - Checkout payment method buttons start hidden and are revealed via `data-wm-bind` when payment methods load
25
30
  - Remove development-only guard from click prevention logging in body.html
26
31
 
27
32
  ### Fixed
33
+ - Fix form checkboxes missing `name` attributes causing FormManager to silently skip validation (cancel, delete forms)
34
+ - Fix admin forms (notifications, users) and blog/status forms missing `novalidate`, `onsubmit`, `name` attributes, and `.button-text` spans
35
+ - Fix profile premium badge using removed `trialing` status and `access` field
28
36
  - Add dev-only artificial pre-delay support to checkout page for testing form protection timing
29
37
 
30
38
  ---
@@ -94,7 +94,8 @@
94
94
 
95
95
  // Shared styles for data-request and delete sections
96
96
  #data-request-section,
97
- #delete-section {
97
+ #delete-section,
98
+ #refund-section {
98
99
  .card-form-border {
99
100
  border-width: 2px;
100
101
  }
@@ -413,6 +413,14 @@ export class FormManager {
413
413
  setError(name, 'This field is required');
414
414
  return;
415
415
  }
416
+ if (type === 'radio') {
417
+ // Radio groups: check if any radio in the group is checked
418
+ const $checked = this.$form.querySelector(`input[name="${name}"]:checked`);
419
+ if (!$checked) {
420
+ setError(name, 'This field is required');
421
+ }
422
+ return;
423
+ }
416
424
  if (!value || !value.trim()) {
417
425
  setError(name, 'This field is required');
418
426
  return;
@@ -523,6 +531,24 @@ export class FormManager {
523
531
  return;
524
532
  }
525
533
 
534
+ // Radio groups: show error text under the last radio without highlighting
535
+ if ($field.type === 'radio') {
536
+ const $radios = this.$form.querySelectorAll(`[name="${fieldName}"]`);
537
+ const $last = $radios[$radios.length - 1];
538
+
539
+ const $parent = $last.closest('.form-check') || $last.parentElement;
540
+ let $feedback = $parent.querySelector('.invalid-feedback');
541
+ if (!$feedback) {
542
+ $feedback = document.createElement('div');
543
+ $feedback.className = 'invalid-feedback';
544
+ $parent.appendChild($feedback);
545
+ }
546
+
547
+ $feedback.textContent = message;
548
+ $feedback.style.display = 'block';
549
+ return;
550
+ }
551
+
526
552
  // Add invalid class to field
527
553
  $field.classList.add('is-invalid');
528
554
 
@@ -533,7 +559,7 @@ export class FormManager {
533
559
  $feedback.className = 'invalid-feedback';
534
560
 
535
561
  // Insert after the field (or after the label for checkboxes)
536
- if ($field.type === 'checkbox' || $field.type === 'radio') {
562
+ if ($field.type === 'checkbox') {
537
563
  const $parent = $field.closest('.form-check') || $field.parentElement;
538
564
  $parent.appendChild($feedback);
539
565
  } else {
@@ -556,6 +582,19 @@ export class FormManager {
556
582
  return;
557
583
  }
558
584
 
585
+ // Radio groups: clear the error text under the last radio
586
+ if ($field.type === 'radio') {
587
+ const $radios = this.$form.querySelectorAll(`[name="${fieldName}"]`);
588
+ const $last = $radios[$radios.length - 1];
589
+
590
+ const $parent = $last.closest('.form-check') || $last.parentElement;
591
+ const $feedback = $parent.querySelector('.invalid-feedback');
592
+ if ($feedback) {
593
+ $feedback.style.display = 'none';
594
+ }
595
+ return;
596
+ }
597
+
559
598
  $field.classList.remove('is-invalid');
560
599
 
561
600
  const $feedback = $field.parentElement.querySelector('.invalid-feedback');
@@ -10,6 +10,7 @@ import * as apiKeysSection from './sections/api-keys.js';
10
10
  import * as deleteSection from './sections/delete.js';
11
11
  import * as dataRequestSection from './sections/data-request.js';
12
12
  import * as connectionsSection from './sections/connections.js';
13
+ import * as refundSection from './sections/refund.js';
13
14
  let webManager = null;
14
15
 
15
16
  // Module
@@ -49,7 +50,8 @@ const sectionModules = {
49
50
  'api-keys': apiKeysSection,
50
51
  delete: deleteSection,
51
52
  'data-request': dataRequestSection,
52
- connections: connectionsSection
53
+ connections: connectionsSection,
54
+ refund: refundSection,
53
55
  };
54
56
 
55
57
  // Main initialization
@@ -71,6 +73,9 @@ async function initializeAccount() {
71
73
  if (window.location.hash === '#data-request') {
72
74
  showDataRequestOption();
73
75
  }
76
+ if (window.location.hash === '#refund') {
77
+ showRefundOption();
78
+ }
74
79
 
75
80
  // Initialize all section modules
76
81
  Object.values(sectionModules).forEach(module => {
@@ -198,6 +203,10 @@ function loadAllSectionData(authState) {
198
203
  if (sectionModules.connections.loadData) {
199
204
  sectionModules.connections.loadData(account, appData);
200
205
  }
206
+
207
+ if (sectionModules.refund.loadData) {
208
+ sectionModules.refund.loadData(account);
209
+ }
201
210
  }
202
211
 
203
212
  // Setup navigation between sections
@@ -241,6 +250,9 @@ function handleHashChange() {
241
250
  if (hash === 'data-request') {
242
251
  showDataRequestOption();
243
252
  }
253
+ if (hash === 'refund') {
254
+ showRefundOption();
255
+ }
244
256
 
245
257
  if (hash) {
246
258
  // Check if the section exists
@@ -303,6 +315,27 @@ function showDataRequestOption() {
303
315
  }
304
316
  }
305
317
 
318
+ // Show refund option in navigation
319
+ function showRefundOption() {
320
+ // Show desktop nav item
321
+ const $refundNavItem = document.getElementById('refund-nav-item');
322
+ if ($refundNavItem) {
323
+ $refundNavItem.classList.remove('d-none');
324
+ }
325
+
326
+ // Add mobile dropdown option if not exists
327
+ const $mobileNavSelect = document.getElementById('mobile-nav-select');
328
+ if ($mobileNavSelect) {
329
+ const refundOption = $mobileNavSelect.querySelector('option[value="refund"]');
330
+ if (!refundOption) {
331
+ const option = document.createElement('option');
332
+ option.value = 'refund';
333
+ option.textContent = 'Refund';
334
+ $mobileNavSelect.appendChild(option);
335
+ }
336
+ }
337
+ }
338
+
306
339
  // Hide all sections except loading
307
340
  function hideAllSectionsExceptLoading() {
308
341
  $sections.forEach(section => {
@@ -263,14 +263,14 @@ function setupActionButtons() {
263
263
  if ($manageBtn) {
264
264
  $manageBtn.addEventListener('click', () => {
265
265
  trackBilling('manage_billing_click');
266
- openStripePortal();
266
+ openBillingPortal();
267
267
  });
268
268
  }
269
269
  }
270
270
 
271
- // ─── Stripe Portal ───────────────────────────────────────────
271
+ // ─── Billing Portal ─────────────────────────────────────────
272
272
 
273
- async function openStripePortal() {
273
+ async function openBillingPortal() {
274
274
  const $manageBtn = document.getElementById('manage-billing-btn');
275
275
  const $btnText = $manageBtn?.querySelector('.button-text');
276
276
  const originalText = $btnText?.textContent;
@@ -280,7 +280,7 @@ async function openStripePortal() {
280
280
  if ($manageBtn) $manageBtn.disabled = true;
281
281
  if ($btnText) $btnText.textContent = 'Opening...';
282
282
 
283
- const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/user/subscription/portal`, {
283
+ const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/payments/portal`, {
284
284
  method: 'POST',
285
285
  timeout: 15000,
286
286
  response: 'json',
@@ -295,8 +295,8 @@ async function openStripePortal() {
295
295
  throw new Error('No portal URL returned');
296
296
  }
297
297
  } catch (error) {
298
- console.warn('Failed to open Stripe portal, falling back to pricing page:', error);
299
- window.location.href = '/pricing';
298
+ console.error('Failed to open billing portal:', error);
299
+ webManager.utilities().showNotification(error.message || 'Failed to open billing portal. Please try again later.', 'danger');
300
300
  } finally {
301
301
  if ($manageBtn) $manageBtn.disabled = false;
302
302
  if ($btnText) $btnText.textContent = originalText;
@@ -328,7 +328,7 @@ function setupCancellationForm() {
328
328
 
329
329
  trackBilling('cancel_submit');
330
330
 
331
- const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/user/subscription/cancel`, {
331
+ const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/payments/cancel`, {
332
332
  method: 'POST',
333
333
  timeout: 30000,
334
334
  response: 'json',
@@ -378,7 +378,9 @@ function populateCancelReasons() {
378
378
  return;
379
379
  }
380
380
 
381
- const shuffled = shuffleArray([...CANCEL_REASONS]);
381
+ const reasons = [...CANCEL_REASONS];
382
+ const other = reasons.pop(); // Remove 'Other' from the end
383
+ const shuffled = [...shuffleArray(reasons), other]; // Shuffle rest, append 'Other' last
382
384
 
383
385
  $container.innerHTML = shuffled.map((reason, i) => `
384
386
  <div class="form-check mb-2">
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Refund Section JavaScript
3
+ */
4
+
5
+ // Libraries
6
+ import { FormManager } from '__main_assets__/js/libs/form-manager.js';
7
+ import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
8
+
9
+ // Refund reasons (will be shuffled on each render)
10
+ const REFUND_REASONS = [
11
+ 'Charged by mistake',
12
+ 'Not satisfied with the service',
13
+ 'Too expensive',
14
+ 'Found a better alternative',
15
+ 'Technical issues or bugs',
16
+ 'Not using it enough',
17
+ 'Billing or payment issue',
18
+ 'Other',
19
+ ];
20
+
21
+ let webManager = null;
22
+ let formManager = null;
23
+ let currentAccount = null;
24
+
25
+ // Initialize refund section
26
+ export async function init(wm) {
27
+ webManager = wm;
28
+ populateRefundReasons();
29
+ setupRefundForm();
30
+ }
31
+
32
+ // Load refund section data
33
+ export async function loadData(account) {
34
+ currentAccount = account;
35
+ updateRefundEligibility(account);
36
+ }
37
+
38
+ // Called when section is shown
39
+ export function onShow() {
40
+ if (currentAccount) {
41
+ updateRefundEligibility(currentAccount);
42
+ }
43
+ }
44
+
45
+ // ─── Eligibility ────────────────────────────────────────────
46
+
47
+ function updateRefundEligibility(account) {
48
+ const subscription = account?.subscription || {};
49
+ const productId = subscription.product?.id || 'basic';
50
+ const isPaid = productId !== 'basic';
51
+ const isCancelled = subscription.status === 'cancelled';
52
+ const isPendingCancel = subscription.cancellation?.pending === true;
53
+ const isEligible = isPaid && (isCancelled || isPendingCancel);
54
+
55
+ const $eligible = document.getElementById('refund-eligible');
56
+ const $ineligible = document.getElementById('refund-ineligible');
57
+
58
+ if ($eligible) {
59
+ $eligible.classList.toggle('d-none', !isEligible);
60
+ }
61
+ if ($ineligible) {
62
+ $ineligible.classList.toggle('d-none', isEligible);
63
+ }
64
+ }
65
+
66
+ // ─── Refund Form ────────────────────────────────────────────
67
+
68
+ function setupRefundForm() {
69
+ const $form = document.getElementById('refund-form');
70
+ if (!$form) {
71
+ return;
72
+ }
73
+
74
+ formManager = new FormManager('#refund-form', {
75
+ allowResubmit: false,
76
+ warnOnUnsavedChanges: false,
77
+ submittingText: 'Processing refund...',
78
+ submittedText: 'Refund Processed',
79
+ });
80
+
81
+ formManager.on('submit', async ({ data }) => {
82
+ // Get selected reason from radio buttons
83
+ const $selectedReason = document.querySelector('input[name="refund_reason"]:checked');
84
+ const reason = $selectedReason?.value || '';
85
+
86
+ if (!reason) {
87
+ throw new Error('Please select a reason for your refund request.');
88
+ }
89
+
90
+ trackRefund('submit');
91
+
92
+ const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/payments/refund`, {
93
+ method: 'POST',
94
+ timeout: 30000,
95
+ response: 'json',
96
+ body: {
97
+ confirmed: true,
98
+ reason: reason,
99
+ feedback: data.feedback || '',
100
+ },
101
+ });
102
+
103
+ if (response.error) {
104
+ throw new Error(response.message || 'Failed to process refund.');
105
+ }
106
+
107
+ // Build success message with refund details
108
+ const refund = response.refund || {};
109
+ const amount = refund.amount
110
+ ? new Intl.NumberFormat('en-US', { style: 'currency', currency: (refund.currency || 'usd').toUpperCase() }).format(refund.amount)
111
+ : '';
112
+ const typeLabel = refund.full ? 'full' : 'prorated';
113
+ const message = amount
114
+ ? `Your ${typeLabel} refund of ${amount} has been processed. Your subscription has been cancelled.`
115
+ : 'Your refund has been processed. Your subscription has been cancelled.';
116
+
117
+ formManager.showSuccess(message);
118
+ });
119
+ }
120
+
121
+ // ─── Reasons ────────────────────────────────────────────────
122
+
123
+ function populateRefundReasons() {
124
+ const $container = document.getElementById('refund-reasons-container');
125
+ if (!$container) {
126
+ return;
127
+ }
128
+
129
+ const reasons = [...REFUND_REASONS];
130
+ const other = reasons.pop(); // Remove 'Other' from the end
131
+ const shuffled = [...shuffleArray(reasons), other]; // Shuffle rest, append 'Other' last
132
+
133
+ $container.innerHTML = shuffled.map((reason, i) => `
134
+ <div class="form-check mb-2">
135
+ <input class="form-check-input" type="radio" name="refund_reason" id="refund-reason-${i}" value="${reason}" required>
136
+ <label class="form-check-label" for="refund-reason-${i}">${reason}</label>
137
+ </div>
138
+ `).join('');
139
+ }
140
+
141
+ function shuffleArray(arr) {
142
+ for (let i = arr.length - 1; i > 0; i--) {
143
+ const j = Math.floor(Math.random() * (i + 1));
144
+ [arr[i], arr[j]] = [arr[j], arr[i]];
145
+ }
146
+ return arr;
147
+ }
148
+
149
+ // ─── Tracking ───────────────────────────────────────────────
150
+
151
+ function trackRefund(action) {
152
+ gtag('event', 'refund_action', {
153
+ action: action,
154
+ });
155
+ fbq('trackCustom', 'RefundAction', {
156
+ action: action,
157
+ });
158
+ ttq.track('ViewContent', {
159
+ content_id: `refund-${action}`,
160
+ content_type: 'product',
161
+ content_name: `Refund ${action}`,
162
+ });
163
+ }
@@ -29,6 +29,7 @@ prerender_icons:
29
29
  - name: "arrow-up-right-from-square"
30
30
  - name: "rotate-right"
31
31
  - name: "ban"
32
+ - name: "circle-info"
32
33
 
33
34
  ### PAGE CONFIG ###
34
35
  sections:
@@ -62,6 +63,9 @@ sections:
62
63
  - id: "delete"
63
64
  name: "Delete Account"
64
65
  icon: "trash"
66
+ - id: "refund"
67
+ name: "Refund"
68
+ icon: "rotate-left"
65
69
 
66
70
  connections:
67
71
  - id: "google"
@@ -138,6 +142,8 @@ badges:
138
142
  bg_color: "#ffc107"
139
143
  ---
140
144
 
145
+ {% assign hidden_sections = "delete,data-request,refund" | split: "," %}
146
+
141
147
  <!-- Fixed Header -->
142
148
  <header class="position-fixed top-0 start-0 w-100 bg-body-secondary border-bottom shadow-sm" style="z-index: 1030;">
143
149
  <div class="container-fluid px-4">
@@ -164,13 +170,13 @@ badges:
164
170
  <div class="d-flex gap-2">
165
171
  <select class="form-select flex-grow-1" id="mobile-nav-select" aria-label="Select account section">
166
172
  {% for section in page.resolved.sections %}
167
- {% unless section.id == 'delete' or section.id == 'data-request' %}
173
+ {% unless hidden_sections contains section.id %}
168
174
  <option value="{{ section.id }}" {% if forloop.first %}selected{% endif %}>
169
175
  {{ section.name }}
170
176
  </option>
171
177
  {% endunless %}
172
178
  {% endfor %}
173
- <!-- Delete and Data Request options will be added dynamically by JavaScript when needed -->
179
+ <!-- Hidden section options are added dynamically by JavaScript when needed -->
174
180
  </select>
175
181
  <button class="btn btn-danger auth-signout-btn">
176
182
  {% uj_icon "arrow-right-from-bracket", "fa-sm" %}
@@ -192,7 +198,7 @@ badges:
192
198
  <div class="card-body p-3">
193
199
  <ul class="nav nav-pills flex-column" id="account-nav" role="tablist" aria-label="Account sections">
194
200
  {% for section in page.resolved.sections %}
195
- <li class="nav-item {% if section.id == 'delete' or section.id == 'data-request' %}d-none{% endif %}" id="{{ section.id }}-nav-item">
201
+ <li class="nav-item {% if hidden_sections contains section.id %}d-none{% endif %}" id="{{ section.id }}-nav-item">
196
202
  <a class="nav-link d-flex align-items-center {% if forloop.first %}active{% endif %}"
197
203
  href="#{{ section.id }}"
198
204
  data-section="{{ section.id }}"
@@ -1013,10 +1019,10 @@ badges:
1013
1019
  </div>
1014
1020
 
1015
1021
  <div class="collapse" id="cancel-subscription-accordion">
1016
- <div class="card card-form-border border-warning mt-4">
1022
+ <div class="card card-form-border border-danger mt-4">
1017
1023
  <div class="card-body">
1018
- <h5 class="card-title text-warning">
1019
- {% uj_icon "triangle-exclamation", "fa-lg me-2" %}
1024
+ <h5 class="card-title text-danger">
1025
+ {% uj_icon "ban", "fa-lg me-2" %}
1020
1026
  Cancel your subscription
1021
1027
  </h5>
1022
1028
 
@@ -1053,7 +1059,7 @@ badges:
1053
1059
  <!-- Buttons -->
1054
1060
  <div class="row g-2">
1055
1061
  <div class="col-12 col-md-6 order-md-2">
1056
- <button type="submit" class="btn btn-outline-danger w-100" id="cancel-subscription-btn">
1062
+ <button type="submit" class="btn btn-outline-adaptive w-100" id="cancel-subscription-btn">
1057
1063
  {% uj_icon "ban", "fa-sm me-2" %}
1058
1064
  <span class="button-text">Cancel Subscription</span>
1059
1065
  </button>
@@ -1220,8 +1226,8 @@ badges:
1220
1226
  <div class="card mb-4">
1221
1227
  <div class="card-body">
1222
1228
  <h5 class="card-title text-primary">
1223
- {% uj_icon "file-shield", "fa-lg me-2" %}
1224
- About Data Subject Access Requests
1229
+ {% uj_icon "file-export", "fa-lg me-2" %}
1230
+ Data request policy
1225
1231
  </h5>
1226
1232
  <p class="card-text small text-muted">
1227
1233
  Under applicable data protection regulations including the General Data Protection Regulation (GDPR), the California Consumer Privacy Act (CCPA), and other similar legislative frameworks enacted by governmental bodies around the world, you may have the right to request a copy of the personal data we hold about you. This process is commonly referred to as a Subject Access Request (SAR) or a Data Portability Request. It is important that you understand the full scope, limitations, and procedural requirements of this request before proceeding. Please read the following information carefully and in its entirety before submitting a request.
@@ -1295,7 +1301,10 @@ badges:
1295
1301
  <div class="collapse" id="data-request-form-accordion">
1296
1302
  <div class="card card-form-border border-primary mt-4">
1297
1303
  <div class="card-body">
1298
- <h5 class="card-title text-primary">Confirm data request</h5>
1304
+ <h5 class="card-title text-primary">
1305
+ {% uj_icon "file-export", "fa-lg me-2" %}
1306
+ Confirm data request
1307
+ </h5>
1299
1308
 
1300
1309
  <form id="data-request-form" novalidate onsubmit="return false">
1301
1310
  <div class="mb-3">
@@ -1351,8 +1360,8 @@ badges:
1351
1360
  <div class="card mb-4">
1352
1361
  <div class="card-body">
1353
1362
  <h5 class="card-title text-danger">
1354
- {% uj_icon "triangle-exclamation", "fa-lg me-2" %}
1355
- About Account Deletion
1363
+ {% uj_icon "trash", "fa-lg me-2" %}
1364
+ Account deletion policy
1356
1365
  </h5>
1357
1366
  <p class="card-text small text-muted">
1358
1367
  Under applicable data protection regulations including the General Data Protection Regulation (GDPR) and the California Consumer Privacy Act (CCPA), you have the right to request the deletion of the personal data we hold about you. Account deletion is a permanent, irreversible action that cannot be undone under any circumstances. Please read the following important information carefully before proceeding.
@@ -1361,7 +1370,7 @@ badges:
1361
1370
  <strong>Permanent and irreversible:</strong> Once your account is deleted, all personal data associated with your account will be permanently removed from our systems. This includes, but is not limited to, your profile information, subscription and billing history, activity logs, API keys, OAuth2 connections, referral data, saved preferences, and all other stored information. There is no mechanism to recover any data after deletion. Our support team cannot restore deleted accounts or retrieve any associated data, regardless of the circumstances.
1362
1371
  </p>
1363
1372
  <p class="card-text small text-muted">
1364
- <strong>Active subscriptions:</strong> You must cancel any active paid subscriptions before deleting your account. Accounts with active or suspended paid subscriptions cannot be deleted until the subscription is cancelled or expires. If you have a subscription that is currently in a billing cycle, you will need to wait for the cycle to complete or cancel the subscription first. No refunds will be issued for unused portions of cancelled subscriptions in connection with account deletion.
1373
+ <strong>Active subscriptions:</strong> You must cancel any active paid subscriptions before deleting your account. Accounts with active or suspended paid subscriptions cannot be deleted until the subscription is cancelled or expires. If you have a subscription that is currently in a billing cycle, you will need to wait for the cycle to complete or cancel the subscription first. No refunds will be issued for unused portions of cancelled subscriptions in connection with account deletion. <strong>Deleting your account permanently forfeits your right to request a refund</strong>, as all payment records and subscription history will be irreversibly removed from our systems and we will no longer be able to verify or process any refund requests. If you wish to request a refund, you must do so <strong>before</strong> deleting your account.
1365
1374
  </p>
1366
1375
  <p class="card-text small text-muted">
1367
1376
  <strong>Pending data requests:</strong> If you have a pending data request (Subject Access Request), it will be permanently cancelled upon account deletion. We will not be able to fulfill data requests after your account has been deleted. If you wish to obtain a copy of your data, you must complete the data request and download process <strong>before</strong> initiating account deletion. You can request a copy of your data from the <a href="#data-request">data request section</a>.
@@ -1382,6 +1391,7 @@ badges:
1382
1391
  <li>All API keys, OAuth2 connections, and integrations will be revoked</li>
1383
1392
  <li>Access to all services, features, and platforms will be terminated immediately</li>
1384
1393
  <li>Any pending data requests will be cancelled and cannot be fulfilled</li>
1394
+ <li>Your right to request a refund will be permanently forfeited</li>
1385
1395
  <li>This action cannot be reversed, appealed, or undone by our support team</li>
1386
1396
  </ul>
1387
1397
 
@@ -1394,9 +1404,17 @@ badges:
1394
1404
  <div class="collapse" id="delete-form-accordion">
1395
1405
  <div class="card card-form-border border-danger mt-4">
1396
1406
  <div class="card-body">
1397
- <h5 class="card-title text-danger">Confirm account deletion</h5>
1407
+ <h5 class="card-title text-danger">
1408
+ {% uj_icon "trash", "fa-lg me-2" %}
1409
+ Confirm account deletion
1410
+ </h5>
1398
1411
 
1399
1412
  <form id="delete-account-form" novalidate onsubmit="return false">
1413
+ <div class="mb-3">
1414
+ <label for="delete-reason" class="form-label">Reason for leaving</label>
1415
+ <textarea class="form-control" id="delete-reason" name="reason" rows="3" placeholder="Help us improve by sharing why you're leaving..."></textarea>
1416
+ </div>
1417
+
1400
1418
  <div class="mb-3">
1401
1419
  <div class="form-check">
1402
1420
  <input class="form-check-input" type="checkbox" id="delete-confirm-checkbox" name="confirm_deletion" required>
@@ -1417,11 +1435,6 @@ badges:
1417
1435
  </div>
1418
1436
  </div>
1419
1437
 
1420
- <div class="mb-3">
1421
- <label for="delete-reason" class="form-label">Reason for leaving</label>
1422
- <textarea class="form-control" id="delete-reason" name="reason" rows="3" placeholder="Help us improve by sharing why you're leaving..."></textarea>
1423
- </div>
1424
-
1425
1438
  <div class="row g-2">
1426
1439
  <div class="col-12 col-md-6 order-md-2">
1427
1440
  <button type="submit" class="btn btn-outline-adaptive w-100" id="delete-account-btn">
@@ -1442,6 +1455,136 @@ badges:
1442
1455
  </div>
1443
1456
  </section>
1444
1457
 
1458
+ <!-- Refund Section (Hidden by default) -->
1459
+ <section id="refund-section" class="account-section d-none">
1460
+ <h2 class="h3 mb-4">Request a refund</h2>
1461
+
1462
+ <!-- Refund policy info card -->
1463
+ <div class="card mb-4">
1464
+ <div class="card-body">
1465
+ <h5 class="card-title text-primary">
1466
+ {% uj_icon "rotate-left", "fa-lg me-2" %}
1467
+ Refund policy
1468
+ </h5>
1469
+ <p class="card-text small text-muted">
1470
+ We understand that circumstances may change after making a purchase, and we want to ensure that our refund process is as transparent and fair as possible. Before submitting a refund request, please carefully read the following information in its entirety. By proceeding with a refund request, you acknowledge that you have read, understood, and agree to the terms and conditions outlined below. Our refund policy is governed by and subject to our <a href="/legal/terms" target="_blank">Terms of Service</a> and <a href="/legal/privacy" target="_blank">Privacy Policy</a>.
1471
+ </p>
1472
+ <p class="card-text small text-muted">
1473
+ <strong>Eligibility requirements:</strong> To be eligible for a refund, your subscription must first be cancelled or have a pending cancellation. Refunds cannot be issued for active subscriptions that have not been cancelled. If you have not yet cancelled your subscription, you must do so before initiating a refund request. You can cancel your subscription from the <a href="#billing">billing section</a> of your account. We require cancellation prior to refund processing to ensure that the subscription lifecycle is properly terminated and that no further charges are incurred during the refund processing window.
1474
+ </p>
1475
+ <p class="card-text small text-muted">
1476
+ <strong>Refund amount calculation:</strong> The amount of your refund is determined by how recently your most recent payment was processed. If your refund request is submitted within <strong>7 days</strong> of your last payment, you will receive a full refund of the entire payment amount. If your refund request is submitted after 7 days from the date of your last payment, you will receive a prorated refund calculated based on the remaining unused portion of your current billing period. The prorated amount is calculated proportionally based on the number of days remaining in your billing cycle relative to the total number of days in the cycle. <strong>Payments older than 6 months are not eligible for refunds under any circumstances.</strong> If your most recent payment was processed more than 6 months ago, your refund request will be automatically denied and cannot be overridden by our support team. All refund calculations are performed automatically by our payment processing system and are final.
1477
+ </p>
1478
+ <p class="card-text small text-muted">
1479
+ <strong>Processing and timeline:</strong> Refunds are processed through your original payment method, which is the same payment instrument (credit card, debit card, or other payment method) that was used to make the original purchase. Once a refund has been approved and initiated on our end, the actual processing time depends on your financial institution and payment provider. In most cases, refunds will appear on your statement within <strong>5&ndash;10 business days</strong>, although some financial institutions may take longer. We have no control over the processing timelines of your bank or payment provider. If your refund has not appeared after 10 business days, we recommend contacting your financial institution directly for further information.
1480
+ </p>
1481
+ <p class="card-text small text-muted">
1482
+ <strong>Immediate subscription cancellation:</strong> Upon successful processing of your refund, your subscription will be <strong>immediately and permanently cancelled</strong>. This means that you will instantly lose access to all premium features, content, elevated rate limits, priority support, and any other benefits associated with your paid subscription plan. Your account will be reverted to the free plan with its corresponding limitations. This action takes effect immediately upon refund approval and cannot be reversed, deferred, or scheduled for a later date.
1483
+ </p>
1484
+ <p class="card-text small text-muted">
1485
+ <strong>One refund per subscription period:</strong> Only one refund may be issued per subscription billing period. If you have already received a refund for the current billing period, you will not be eligible for an additional refund until a new billing period begins with a new payment. Additionally, refunds are only applicable to the most recent payment on your account. Payments from previous billing cycles are not eligible for refund through this self-service process.
1486
+ </p>
1487
+ <p class="card-text small text-muted">
1488
+ <strong>Account deletion and refunds:</strong> If you choose to delete your account, all personal data associated with your account will be permanently and irreversibly removed from our systems, including all payment and subscription records. Once your account has been deleted, we will no longer be able to look up your payment information, verify previous transactions, or process any refund requests. <strong>Account deletion permanently forfeits your right to request a refund</strong>, as we will have no means to verify or process such a request. If you are considering both requesting a refund and deleting your account, you must complete the refund process in full <strong>before</strong> initiating account deletion. For more information about account deletion, please visit the <a href="#delete">account deletion section</a>.
1489
+ </p>
1490
+ <p class="card-text small text-muted">
1491
+ <strong>Fraud and abuse prevention:</strong> We reserve the right to deny refund requests that we determine, in our sole discretion, to be fraudulent, abusive, or inconsistent with normal usage patterns. This includes, but is not limited to, repeated subscription and refund cycles, chargebacks filed in conjunction with refund requests, and any other activity that violates our <a href="/legal/terms" target="_blank">Terms of Service</a>. Abuse of the refund system may result in suspension or termination of your account.
1492
+ </p>
1493
+ <p class="card-text small text-muted">
1494
+ <strong>Contact and support:</strong> If you have questions about this refund policy, need assistance with your refund request, or believe there has been an error in your refund calculation, please contact our support team through our <a href="/contact" target="_blank">contact page</a> before submitting a refund request. Our support team can help clarify your eligibility, explain the expected refund amount, and address any other concerns you may have. Please note that our support team is unable to override the automated refund calculation system or issue refunds outside the parameters of this policy.
1495
+ </p>
1496
+
1497
+ <hr>
1498
+
1499
+ <h6 class="text-muted">Summary of key points:</h6>
1500
+ <ul class="small text-muted">
1501
+ <li>Your subscription must be cancelled or pending cancellation before requesting a refund</li>
1502
+ <li>Full refund if requested within <strong>7 days</strong> of your last payment</li>
1503
+ <li>Prorated refund based on remaining billing period if requested after 7 days</li>
1504
+ <li><strong>Payments older than 6 months are not eligible</strong> for refunds under any circumstances</li>
1505
+ <li>Refunds are processed to your original payment method within <strong>5&ndash;10 business days</strong></li>
1506
+ <li>Your subscription will be <strong>immediately cancelled</strong> upon refund</li>
1507
+ <li>Account deletion permanently forfeits your right to a refund</li>
1508
+ <li>Only one refund per billing period; only the most recent payment is eligible</li>
1509
+ </ul>
1510
+
1511
+ <!-- Ineligible message (shown when not eligible) -->
1512
+ <div id="refund-ineligible" class="d-none">
1513
+ <hr>
1514
+ <div class="d-flex align-items-start text-warning">
1515
+ <span class="me-2 flex-shrink-0">{% uj_icon "triangle-exclamation", "fa-md" %}</span>
1516
+ <div>
1517
+ <strong>Not eligible for refund</strong>
1518
+ <p class="mb-0 small text-muted">To request a refund, you must first cancel your subscription. Refunds are only available for cancelled or pending-cancellation subscriptions. You can cancel your subscription from the <a href="#billing">billing section</a> of your account.</p>
1519
+ </div>
1520
+ </div>
1521
+ </div>
1522
+
1523
+ <!-- Eligible: show refund form trigger -->
1524
+ <div id="refund-eligible" class="d-none">
1525
+ <p class="card-text small text-muted mb-0">
1526
+ If you have read and understood all of the information above and still wish to proceed with requesting a refund, you may proceed to <button type="button" class="accordion-trigger small text-muted text-decoration-underline" data-bs-toggle="collapse" data-bs-target="#refund-form-accordion" aria-expanded="false" aria-controls="refund-form-accordion">submit your refund request &rarr;</button>.
1527
+ </p>
1528
+ </div>
1529
+ </div>
1530
+ </div>
1531
+
1532
+ <div class="collapse" id="refund-form-accordion">
1533
+ <div class="card card-form-border border-primary mt-4">
1534
+ <div class="card-body">
1535
+ <h5 class="card-title text-primary">
1536
+ {% uj_icon "rotate-left", "fa-lg me-2" %}
1537
+ Confirm refund request
1538
+ </h5>
1539
+
1540
+ <form id="refund-form" novalidate onsubmit="return false">
1541
+ <!-- Refund reasons (randomized by JS) -->
1542
+ <div class="mb-3">
1543
+ <label class="form-label">Why are you requesting a refund? <span class="text-danger">*</span></label>
1544
+ <div id="refund-reasons-container">
1545
+ <!-- Radio buttons injected by JS in random order -->
1546
+ </div>
1547
+ </div>
1548
+
1549
+ <!-- Additional feedback -->
1550
+ <div class="mb-3">
1551
+ <label for="refund-feedback" class="form-label">Anything else you'd like us to know? <span class="text-danger">*</span></label>
1552
+ <textarea class="form-control" id="refund-feedback" name="feedback" rows="3" placeholder="Help us understand what we could do better..." required></textarea>
1553
+ <div class="invalid-feedback">Please provide feedback before requesting a refund.</div>
1554
+ </div>
1555
+
1556
+ <!-- Required confirmation -->
1557
+ <div class="mb-3">
1558
+ <div class="form-check">
1559
+ <input class="form-check-input" type="checkbox" id="refund-confirm-checkbox" name="confirm_refund" required>
1560
+ <label class="form-check-label" for="refund-confirm-checkbox">
1561
+ I understand that my subscription will be immediately cancelled and I will lose access to premium features. The refund amount will be determined by my refund eligibility and may be prorated.
1562
+ </label>
1563
+ <div class="invalid-feedback">You must confirm before requesting a refund.</div>
1564
+ </div>
1565
+ </div>
1566
+
1567
+ <!-- Buttons -->
1568
+ <div class="row g-2">
1569
+ <div class="col-12 col-md-6 order-md-2">
1570
+ <button type="submit" class="btn btn-outline-adaptive w-100" id="refund-submit-btn">
1571
+ {% uj_icon "rotate-left", "fa-sm me-2" %}
1572
+ <span class="button-text">Request Refund</span>
1573
+ </button>
1574
+ </div>
1575
+ <div class="col-12 col-md-6 order-md-1">
1576
+ <a href="#billing" class="btn btn-primary w-100">
1577
+ {% uj_icon "arrow-left", "fa-sm me-2" %}
1578
+ <span>Return to Billing</span>
1579
+ </a>
1580
+ </div>
1581
+ </div>
1582
+ </form>
1583
+ </div>
1584
+ </div>
1585
+ </div>
1586
+ </section>
1587
+
1445
1588
  </div>
1446
1589
  </div>
1447
1590
  </div>
@@ -335,12 +335,12 @@ web_manager:
335
335
  <div class="d-flex align-items-center wm-binding-skeleton" data-wm-bind="@show checkout">
336
336
  <div class="position-relative me-3">
337
337
  <div class="product-thumbnail bg-light rounded-3 d-flex align-items-center justify-content-center">
338
- <img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" class="img-fluid p-3" alt="{{ site.brand.name }} Plan Icon"/>
338
+ <img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" class="img-fluid p-2" alt="{{ site.brand.name }} Plan Icon"/>
339
339
  </div>
340
- <span class="position-absolute badge rounded-pill bg-primary" style="top: -0.375rem; right: -0.375rem;">
340
+ <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-primary">
341
341
  1
342
342
  <span class="visually-hidden">quantity</span>
343
- </span>
343
+ </>
344
344
  </div>
345
345
  <div class="flex-grow-1 text-start wm-binding-skeleton" data-wm-bind="@show checkout">
346
346
  <h6 class="mb-1">{{ site.brand.name }} <span data-wm-bind="@text checkout.product.name">Loading...</span></h6>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "0.0.273",
3
+ "version": "0.0.274",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {