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 +8 -0
- package/dist/assets/css/pages/account/index.scss +2 -1
- package/dist/assets/js/libs/form-manager.js +40 -1
- package/dist/assets/js/pages/account/index.js +34 -1
- package/dist/assets/js/pages/account/sections/billing.js +10 -8
- package/dist/assets/js/pages/account/sections/refund.js +163 -0
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +162 -19
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/checkout.html +3 -3
- package/package.json +1 -1
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
|
---
|
|
@@ -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'
|
|
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
|
-
|
|
266
|
+
openBillingPortal();
|
|
267
267
|
});
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
// ───
|
|
271
|
+
// ─── Billing Portal ─────────────────────────────────────────
|
|
272
272
|
|
|
273
|
-
async function
|
|
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/
|
|
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.
|
|
299
|
-
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
<!--
|
|
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
|
|
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-
|
|
1022
|
+
<div class="card card-form-border border-danger mt-4">
|
|
1017
1023
|
<div class="card-body">
|
|
1018
|
-
<h5 class="card-title text-
|
|
1019
|
-
{% uj_icon "
|
|
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-
|
|
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-
|
|
1224
|
-
|
|
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">
|
|
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 "
|
|
1355
|
-
|
|
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">
|
|
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–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–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 →</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-
|
|
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"
|
|
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
|
-
|
|
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>
|