ultimate-jekyll-manager 1.2.3 → 1.3.1
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 +28 -0
- package/dist/assets/js/core/auth.js +27 -0
- package/dist/assets/js/libs/auth.js +124 -0
- package/dist/assets/js/pages/account/sections/notifications.js +74 -124
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +8 -23
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/oauth2.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/reset.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signin.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signup.html +20 -6
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/token.html +1 -1
- package/dist/defaults/dist/_team/christina-hill.md +2 -2
- package/dist/defaults/dist/_team/james-oconnor.md +1 -1
- package/dist/defaults/dist/_team/marcus-johnson.md +3 -3
- package/dist/defaults/dist/_team/priya-sharma.md +2 -2
- package/dist/defaults/dist/_team/sarah-rodriguez.md +3 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
---
|
|
18
|
+
## [1.3.1] - 2026-05-21
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **`ENFORCE_CONSENT_GUARD` flipped to `true`** in `src/assets/js/core/auth.js`. The page-load consent guard now silently signs out any authenticated user whose doc has `consent.legal.status !== 'granted'`. Caveat: any pre-consent-system user doc (missing the field, or defaulted to `'revoked'`) will be signed out on page load — run the legacy-user migration first, or live-test against fresh signups.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
## [1.3.0] - 2026-05-21
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **Marketing consent capture on the signup form.** Frontend half of `backend-manager` v5.2.0's consent system. `src/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signup.html` replaces the legal-copy line with two real checkboxes (`consent-legal`, `consent-marketing`) wrapped in a `#consent-group` so validation can highlight the pair as a unit. `consent-legal` is required to submit.
|
|
30
|
+
- **`captureSignupConsent()` + `validateConsent()` in `src/assets/js/libs/auth.js`.** Pulls checkbox state + label text from the FormManager-collected data and writes it to `webManager.storage()` under key `consent` BEFORE Firebase auth fires — survives the post-signup redirect the same way `attribution` does. `validateConsent()` blocks submit via a phantom `__consent` field name and surfaces feedback via the wrapper outline + an inline error message instead of red-X-ing the single legal checkbox.
|
|
31
|
+
- **`reverseAccidentalSignup()` for the Google quirk.** Landing on `/signin` with an unknown Google account auto-creates the Firebase auth user; this reverses that path — deletes the user, signs out, strips `authReturnUrl`, and surfaces an inline form error. Best-effort delete; the page-load consent guard (below, currently OFF) is the backstop if delete fails.
|
|
32
|
+
- **`ENFORCE_CONSENT_GUARD` in `src/assets/js/core/auth.js`.** Page-load guard that silently signs out any authenticated user whose doc has `consent.legal.status !== 'granted'`. Default **FALSE** until the legacy-user migration runs (which sets all existing docs to `granted` + `source: 'imported'`); flipping it on before then would lock every existing user out.
|
|
33
|
+
- **`consent` field on the `sendUserSignupMetadata` payload.** Forwards the storage-survived consent blob to BEM's `/user/signup` route so it can write the canonical `consent.{legal,marketing}` sub-tree on the new user doc.
|
|
34
|
+
- **Marketing-emails toggle on the account page.** `src/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html` + `src/assets/js/pages/account/sections/notifications.js` reworked to read `account.consent.marketing.status` (not the old `preferences.notifications.marketing`) and POST to `/backend-manager/marketing/email-preferences` on change. Shows the original grant date below the toggle.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **`web-manager` bumped to `^4.2.0`** (was `file:../web-manager` from local dev). Locks in `DEFAULT_ACCOUNT.consent.{legal,marketing}` so `resolveAccount()` always returns a defined consent shape for legacy users.
|
|
39
|
+
- Minor template touchups on `oauth2.html`, `reset.html`, `signin.html`, `token.html`, `signup.html` — heading casing, `filter-adaptive` class on the brandmark logo so it inverts in dark mode.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **`_team` seed authors** — corrected LinkedIn/Twitter handles in `christina-hill.md`, `james-oconnor.md`, `marcus-johnson.md`, `priya-sharma.md`, `sarah-rodriguez.md` so the default scaffolded `/team/` page links don't 404.
|
|
44
|
+
|
|
17
45
|
---
|
|
18
46
|
## [1.2.3] - 2026-05-19
|
|
19
47
|
|
|
@@ -4,6 +4,12 @@ import webManager from 'web-manager';
|
|
|
4
4
|
// Constants
|
|
5
5
|
const SIGNUP_MAX_AGE = 5 * 60 * 1000;
|
|
6
6
|
|
|
7
|
+
// Enforce page-load consent guard. When true, any authenticated user whose doc has
|
|
8
|
+
// consent.legal.status !== 'granted' is silently signed out. Keep FALSE until the
|
|
9
|
+
// legacy user migration runs (sets all existing docs to status='granted',
|
|
10
|
+
// source='imported'). Otherwise every existing user gets locked out on signin.
|
|
11
|
+
const ENFORCE_CONSENT_GUARD = true;
|
|
12
|
+
|
|
7
13
|
// Auth Module
|
|
8
14
|
export default function () {
|
|
9
15
|
// Get auth policy
|
|
@@ -59,6 +65,25 @@ export default function () {
|
|
|
59
65
|
if (user) {
|
|
60
66
|
// User is authenticated
|
|
61
67
|
|
|
68
|
+
// Consent guard: if the user is authenticated but their account doc shows
|
|
69
|
+
// no legal consent on record, they're an orphan from a reversed Google signup
|
|
70
|
+
// that failed to delete cleanly. Sign them out and surface a toast so the
|
|
71
|
+
// user knows what happened (especially if they just clicked Google and saw
|
|
72
|
+
// a success message before this fired).
|
|
73
|
+
// Gated by ENFORCE_CONSENT_GUARD (off until the legacy-user migration runs).
|
|
74
|
+
if (ENFORCE_CONSENT_GUARD) {
|
|
75
|
+
const legalStatus = state.account?.consent?.legal?.status;
|
|
76
|
+
if (legalStatus && legalStatus !== 'granted') {
|
|
77
|
+
console.warn('[Auth] Signing out user with no legal consent on record');
|
|
78
|
+
await webManager.auth().signOut();
|
|
79
|
+
webManager.utilities().showNotification(
|
|
80
|
+
`This account hasn't completed setup. Please sign up first.`,
|
|
81
|
+
{ type: 'danger', timeout: 8000 }
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
// Send user signup metadata if account is new
|
|
63
88
|
await sendUserSignupMetadata(user);
|
|
64
89
|
|
|
@@ -241,12 +266,14 @@ async function sendUserSignupMetadata(user) {
|
|
|
241
266
|
|
|
242
267
|
// Get attribution data from storage
|
|
243
268
|
const attribution = webManager.storage().get('attribution', {});
|
|
269
|
+
const consent = webManager.storage().get('consent', {});
|
|
244
270
|
|
|
245
271
|
// Build the payload
|
|
246
272
|
const payload = {
|
|
247
273
|
// New structure
|
|
248
274
|
attribution: attribution,
|
|
249
275
|
context: webManager.utilities().getContext(),
|
|
276
|
+
consent: consent,
|
|
250
277
|
};
|
|
251
278
|
|
|
252
279
|
// Get server API URL
|
|
@@ -104,6 +104,13 @@ export default function () {
|
|
|
104
104
|
return async ({ data, $submitButton }) => {
|
|
105
105
|
const provider = $submitButton?.getAttribute('data-provider');
|
|
106
106
|
|
|
107
|
+
// Capture consent BEFORE any Firebase call. On signup pages the checkbox state
|
|
108
|
+
// must survive any post-auth redirect so BEM's /user/signup can write it to the doc.
|
|
109
|
+
// Read from FormManager-collected data on signup; ignored on signin (no checkboxes there).
|
|
110
|
+
if (action === 'signup') {
|
|
111
|
+
captureSignupConsent(data);
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
if (provider === 'email') {
|
|
108
115
|
await emailHandler(data);
|
|
109
116
|
} else if (provider) {
|
|
@@ -112,6 +119,101 @@ export default function () {
|
|
|
112
119
|
};
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
// Google's signInWithPopup/Redirect auto-creates accounts. If a user lands on /signin
|
|
123
|
+
// with a Google account that doesn't exist yet, Firebase creates one before we can stop it.
|
|
124
|
+
// This reverses that: delete the auth user, sign out, surface an inline error.
|
|
125
|
+
async function reverseAccidentalSignup(newUser) {
|
|
126
|
+
console.warn('[Auth] Reversing accidental signup from /signin (new Google account created with no consent on record)');
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await newUser.delete();
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// Best-effort. If delete fails (network/token issue), the page-load consent guard
|
|
132
|
+
// is the backstop — the orphan account will be signed out on every future visit.
|
|
133
|
+
console.error('[Auth] Failed to delete accidental account:', e);
|
|
134
|
+
webManager.sentry().captureException(new Error('Failed to reverse accidental signup', { cause: e }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const { getAuth, signOut } = await import('@firebase/auth');
|
|
139
|
+
await signOut(getAuth());
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error('[Auth] Failed to sign out after accidental signup:', e);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Strip authReturnUrl so the next attempt doesn't redirect them away from /signin
|
|
145
|
+
const url = new URL(window.location.href);
|
|
146
|
+
if (url.searchParams.has('authReturnUrl')) {
|
|
147
|
+
url.searchParams.delete('authReturnUrl');
|
|
148
|
+
window.history.replaceState({}, document.title, url.toString());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (formManager) {
|
|
152
|
+
formManager.showError(`This account doesn't exist. Try signing up first or use a different account.`);
|
|
153
|
+
formManager.ready();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Validate that the user has agreed to the legal terms. Instead of highlighting the
|
|
158
|
+
// single legal checkbox in red (which subtly frames it as "the one that matters"),
|
|
159
|
+
// we surround BOTH checkboxes with a red outline and surface a top-level banner.
|
|
160
|
+
// This frames consent as a unit the user is confirming, not a hurdle to clear.
|
|
161
|
+
function validateConsent({ data, setError }) {
|
|
162
|
+
if (data?.consentLegal === true) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Phantom field name — blocks submit (FormManager checks errorCount > 0) but
|
|
167
|
+
// skips rendering since no DOM field matches '__consent'. The visual treatment
|
|
168
|
+
// is the wrapper outline + inline error message below.
|
|
169
|
+
setError('__consent', 'Agreement to Terms required');
|
|
170
|
+
|
|
171
|
+
const $group = document.getElementById('consent-group');
|
|
172
|
+
if ($group) {
|
|
173
|
+
// Match the same border color the email/password fields show when invalid.
|
|
174
|
+
// The HTML baseline is 'border: 1px solid transparent' so we only swap the color.
|
|
175
|
+
$group.style.borderColor = 'var(--bs-form-invalid-border-color, var(--bs-danger, #dc3545))';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const $err = document.getElementById('consent-error');
|
|
179
|
+
if ($err) {
|
|
180
|
+
$err.textContent = `Please select "I agree" to the Terms of Service and Privacy Policy.`;
|
|
181
|
+
$err.classList.remove('d-none');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Clear the consent error styling once the user starts interacting with the boxes.
|
|
186
|
+
// Runs on every change to either checkbox. We only restore borderColor since the
|
|
187
|
+
// HTML baseline keeps 'border: 1px solid transparent' to reserve the layout space.
|
|
188
|
+
function clearConsentError() {
|
|
189
|
+
const $group = document.getElementById('consent-group');
|
|
190
|
+
if ($group) {
|
|
191
|
+
$group.style.borderColor = 'transparent';
|
|
192
|
+
}
|
|
193
|
+
const $err = document.getElementById('consent-error');
|
|
194
|
+
if ($err) {
|
|
195
|
+
$err.classList.add('d-none');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Read the consent checkboxes and stash to storage. Survives the post-signup redirect
|
|
200
|
+
// the same way attribution does. BEM's /user/signup route picks it up via sendUserSignupMetadata.
|
|
201
|
+
function captureSignupConsent(data) {
|
|
202
|
+
const legalLabel = document.querySelector('label[for="consent-legal"]')?.innerText?.trim() || null;
|
|
203
|
+
const marketingLabel = document.querySelector('label[for="consent-marketing"]')?.innerText?.trim() || null;
|
|
204
|
+
|
|
205
|
+
webManager.storage().set('consent', {
|
|
206
|
+
legal: {
|
|
207
|
+
granted: data?.consentLegal === true || data?.consentLegal === 'on',
|
|
208
|
+
text: legalLabel,
|
|
209
|
+
},
|
|
210
|
+
marketing: {
|
|
211
|
+
granted: data?.consentMarketing === true || data?.consentMarketing === 'on',
|
|
212
|
+
text: marketingLabel,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
115
217
|
// Initialize signin form
|
|
116
218
|
function initializeSigninForm() {
|
|
117
219
|
formManager = new FormManager('#auth-form', {
|
|
@@ -139,7 +241,12 @@ export default function () {
|
|
|
139
241
|
|
|
140
242
|
formManager.on('statechange', stateChangeHandler);
|
|
141
243
|
formManager.on('validation', validateEmailProvider);
|
|
244
|
+
formManager.on('validation', validateConsent);
|
|
142
245
|
formManager.on('submit', createAuthSubmitHandler('signup', handleEmailSignup));
|
|
246
|
+
|
|
247
|
+
// Clear consent error styling when either checkbox is toggled
|
|
248
|
+
document.getElementById('consent-legal')?.addEventListener('change', clearConsentError);
|
|
249
|
+
document.getElementById('consent-marketing')?.addEventListener('change', clearConsentError);
|
|
143
250
|
}
|
|
144
251
|
|
|
145
252
|
// Initialize reset form
|
|
@@ -183,6 +290,14 @@ export default function () {
|
|
|
183
290
|
const pagePath = document.documentElement.getAttribute('data-page-path');
|
|
184
291
|
const isSignupPage = pagePath === '/signup';
|
|
185
292
|
|
|
293
|
+
// Google quirk: if a new account was auto-created during a signin attempt
|
|
294
|
+
// (user came back from OAuth via the redirect path on /signin, not /signup),
|
|
295
|
+
// reverse it — they have no consent on record.
|
|
296
|
+
if (isNewUser && !isSignupPage) {
|
|
297
|
+
await reverseAccidentalSignup(result.user);
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
186
301
|
if (isNewUser || isSignupPage) {
|
|
187
302
|
trackSignup(providerId, result.user);
|
|
188
303
|
formManager.showSuccess('Account created successfully!');
|
|
@@ -525,6 +640,15 @@ export default function () {
|
|
|
525
640
|
|
|
526
641
|
// Track based on whether this is a new user
|
|
527
642
|
const isNewUser = result.additionalUserInfo?.isNewUser;
|
|
643
|
+
|
|
644
|
+
// Google quirk: signInWithPopup auto-creates accounts. If a brand-new visitor
|
|
645
|
+
// clicks "Sign in with Google" on the SIGNIN page (not signup), reverse the
|
|
646
|
+
// auto-creation — they have no consent on record and never asked to create one.
|
|
647
|
+
if (isNewUser && action === 'signin') {
|
|
648
|
+
await reverseAccidentalSignup(result.user);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
528
652
|
if (isNewUser || action === 'signup') {
|
|
529
653
|
trackSignup(providerName, result.user);
|
|
530
654
|
// Show success message
|
|
@@ -1,145 +1,95 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Notifications section — marketing email consent toggle.
|
|
3
|
+
*
|
|
4
|
+
* Reads consent.marketing.status from the user doc for the toggle's initial state.
|
|
5
|
+
* On toggle, POSTs to /backend-manager/marketing/email-preferences with subscribe|unsubscribe.
|
|
6
|
+
* The server writes consent.marketing to the user doc + syncs SendGrid + Beehiiv.
|
|
7
|
+
*/
|
|
8
|
+
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
2
9
|
import webManager from 'web-manager';
|
|
3
10
|
|
|
4
|
-
|
|
11
|
+
const TOGGLE_ID = 'marketing-emails';
|
|
12
|
+
const GRANT_DATE_ID = 'marketing-emails-grant-date';
|
|
13
|
+
|
|
5
14
|
export function init() {
|
|
6
|
-
|
|
15
|
+
const $toggle = document.getElementById(TOGGLE_ID);
|
|
16
|
+
if (!$toggle) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
$toggle.addEventListener('change', handleToggleChange);
|
|
7
20
|
}
|
|
8
21
|
|
|
9
|
-
// Load notifications data
|
|
10
22
|
export function loadData(account) {
|
|
11
|
-
if (!account)
|
|
12
|
-
|
|
13
|
-
// Load notification preferences from account
|
|
14
|
-
const preferences = account.preferences?.notifications || {};
|
|
15
|
-
|
|
16
|
-
// Set toggle states
|
|
17
|
-
setToggleState('marketing-emails', preferences.marketing !== false);
|
|
18
|
-
setToggleState('security-emails', preferences.security !== false);
|
|
19
|
-
setToggleState('product-emails', preferences.product === true);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Setup notification toggles
|
|
23
|
-
function setupNotificationToggles() {
|
|
24
|
-
const toggles = [
|
|
25
|
-
'marketing-emails',
|
|
26
|
-
'security-emails',
|
|
27
|
-
'product-emails'
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
toggles.forEach(toggleId => {
|
|
31
|
-
const $toggle = document.getElementById(toggleId);
|
|
32
|
-
if ($toggle) {
|
|
33
|
-
$toggle.addEventListener('change', handleToggleChange);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Handle toggle change
|
|
39
|
-
async function handleToggleChange(event) {
|
|
40
|
-
const toggleId = event.target.id;
|
|
41
|
-
const isEnabled = event.target.checked;
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
// Map toggle ID to preference key
|
|
45
|
-
const preferenceKey = toggleId.replace('-emails', '');
|
|
46
|
-
|
|
47
|
-
// Update preferences
|
|
48
|
-
await updateNotificationPreference(preferenceKey, isEnabled);
|
|
49
|
-
|
|
50
|
-
// Show feedback
|
|
51
|
-
const actionText = isEnabled ? 'enabled' : 'disabled';
|
|
52
|
-
showToast(`${getToggleLabel(toggleId)} ${actionText}`, 'success');
|
|
53
|
-
|
|
54
|
-
} catch (error) {
|
|
55
|
-
console.error('Failed to update notification preference:', error);
|
|
56
|
-
|
|
57
|
-
// Revert toggle state
|
|
58
|
-
event.target.checked = !isEnabled;
|
|
59
|
-
|
|
60
|
-
showToast('Failed to update notification preferences. Please try again.', 'danger');
|
|
23
|
+
if (!account) {
|
|
24
|
+
return;
|
|
61
25
|
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Update notification preference
|
|
65
|
-
async function updateNotificationPreference(key, value) {
|
|
66
|
-
// This would call the appropriate API endpoint
|
|
67
|
-
// For now, just update locally
|
|
68
|
-
const preferences = {
|
|
69
|
-
notifications: {
|
|
70
|
-
[key]: value
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
26
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Set toggle state
|
|
79
|
-
function setToggleState(toggleId, isEnabled) {
|
|
80
|
-
const $toggle = document.getElementById(toggleId);
|
|
81
|
-
if ($toggle) {
|
|
82
|
-
$toggle.checked = isEnabled;
|
|
27
|
+
const $toggle = document.getElementById(TOGGLE_ID);
|
|
28
|
+
if (!$toggle) {
|
|
29
|
+
return;
|
|
83
30
|
}
|
|
84
|
-
}
|
|
85
31
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
32
|
+
const isGranted = account.consent?.marketing?.status === 'granted';
|
|
33
|
+
$toggle.checked = isGranted;
|
|
34
|
+
|
|
35
|
+
// Show the original grant date if known — gives the user context on what they agreed to.
|
|
36
|
+
const grantTimestamp = account.consent?.marketing?.grantedAt?.timestamp;
|
|
37
|
+
if (isGranted && grantTimestamp) {
|
|
38
|
+
const $date = document.getElementById(GRANT_DATE_ID);
|
|
39
|
+
if ($date) {
|
|
40
|
+
const date = new Date(grantTimestamp);
|
|
41
|
+
$date.textContent = `Subscribed ${date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}.`;
|
|
42
|
+
$date.classList.remove('d-none');
|
|
43
|
+
}
|
|
97
44
|
}
|
|
98
45
|
}
|
|
99
46
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
$toast.setAttribute('role', 'alert');
|
|
106
|
-
$toast.setAttribute('aria-live', 'assertive');
|
|
107
|
-
$toast.setAttribute('aria-atomic', 'true');
|
|
108
|
-
$toast.style.zIndex = '9999';
|
|
109
|
-
|
|
110
|
-
const $inner = document.createElement('div');
|
|
111
|
-
$inner.className = 'd-flex';
|
|
47
|
+
async function handleToggleChange(event) {
|
|
48
|
+
const $toggle = event.target;
|
|
49
|
+
const wasChecked = !$toggle.checked; // checkbox already flipped at this point
|
|
50
|
+
const isEnabled = $toggle.checked;
|
|
51
|
+
const action = isEnabled ? 'subscribe' : 'unsubscribe';
|
|
112
52
|
|
|
113
|
-
|
|
114
|
-
$
|
|
115
|
-
$body.textContent = message;
|
|
53
|
+
// Disable while in-flight so rapid clicks don't fire multiple requests
|
|
54
|
+
$toggle.disabled = true;
|
|
116
55
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
56
|
+
try {
|
|
57
|
+
const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/marketing/email-preferences`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
timeout: 15000,
|
|
60
|
+
response: 'json',
|
|
61
|
+
tries: 2,
|
|
62
|
+
body: { action },
|
|
63
|
+
});
|
|
122
64
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
65
|
+
if (response.error || response.data?.success !== true) {
|
|
66
|
+
throw new Error(response.message || response.error || 'Failed to update email preferences.');
|
|
67
|
+
}
|
|
126
68
|
|
|
127
|
-
|
|
128
|
-
|
|
69
|
+
webManager.utilities().showNotification(
|
|
70
|
+
isEnabled ? 'Subscribed to email updates.' : 'Unsubscribed from email updates.',
|
|
71
|
+
{ type: 'success' }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Hide the grant-date line on unsubscribe (it was the OLD grant date — informational only).
|
|
75
|
+
// It'll get repopulated the next time loadData runs if the user re-subscribes.
|
|
76
|
+
if (!isEnabled) {
|
|
77
|
+
const $date = document.getElementById(GRANT_DATE_ID);
|
|
78
|
+
if ($date) {
|
|
79
|
+
$date.classList.add('d-none');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Failed to update marketing consent:', error);
|
|
129
84
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const toast = new window.bootstrap.Toast($toast);
|
|
133
|
-
toast.show();
|
|
85
|
+
// Revert toggle on failure so UI matches server state
|
|
86
|
+
$toggle.checked = wasChecked;
|
|
134
87
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
setTimeout(() => {
|
|
142
|
-
$toast.remove();
|
|
143
|
-
}, 3000);
|
|
88
|
+
webManager.utilities().showNotification(
|
|
89
|
+
'Failed to update email preferences. Please try again.',
|
|
90
|
+
{ type: 'danger' }
|
|
91
|
+
);
|
|
92
|
+
} finally {
|
|
93
|
+
$toggle.disabled = false;
|
|
144
94
|
}
|
|
145
95
|
}
|
|
@@ -36,8 +36,8 @@ sections:
|
|
|
36
36
|
- id: "profile"
|
|
37
37
|
name: "Profile"
|
|
38
38
|
icon: "user-circle"
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
- id: "notifications"
|
|
40
|
+
name: "Notifications"
|
|
41
41
|
icon: "bell"
|
|
42
42
|
- id: "security"
|
|
43
43
|
name: "Security"
|
|
@@ -777,29 +777,14 @@ badges:
|
|
|
777
777
|
|
|
778
778
|
<div class="card">
|
|
779
779
|
<div class="card-body">
|
|
780
|
-
<h5 class="card-title">Email
|
|
781
|
-
|
|
782
|
-
<div class="form-check form-switch mb-3">
|
|
783
|
-
<input class="form-check-input" type="checkbox" id="marketing-emails" checked>
|
|
784
|
-
<label class="form-check-label" for="marketing-emails">
|
|
785
|
-
Marketing emails
|
|
786
|
-
<small class="d-block text-muted">Receive emails about new features and updates</small>
|
|
787
|
-
</label>
|
|
788
|
-
</div>
|
|
789
|
-
|
|
790
|
-
<div class="form-check form-switch mb-3">
|
|
791
|
-
<input class="form-check-input" type="checkbox" id="security-emails" checked>
|
|
792
|
-
<label class="form-check-label" for="security-emails">
|
|
793
|
-
Security alerts
|
|
794
|
-
<small class="d-block text-muted">Get notified about important security updates</small>
|
|
795
|
-
</label>
|
|
796
|
-
</div>
|
|
780
|
+
<h5 class="card-title">Email preferences</h5>
|
|
797
781
|
|
|
798
782
|
<div class="form-check form-switch">
|
|
799
|
-
<input class="form-check-input" type="checkbox" id="
|
|
800
|
-
<label class="form-check-label" for="
|
|
801
|
-
Product updates
|
|
802
|
-
<small class="d-block text-muted">
|
|
783
|
+
<input class="form-check-input" type="checkbox" id="marketing-emails">
|
|
784
|
+
<label class="form-check-label" for="marketing-emails">
|
|
785
|
+
Product updates, newsletters, and marketing communications
|
|
786
|
+
<small class="d-block text-muted">You can withdraw consent at any time.</small>
|
|
787
|
+
<small id="marketing-emails-grant-date" class="d-block text-muted d-none"></small>
|
|
803
788
|
</label>
|
|
804
789
|
</div>
|
|
805
790
|
</div>
|
|
@@ -11,7 +11,7 @@ layout: themes/[ site.theme.id ]/frontend/core/cover
|
|
|
11
11
|
<!-- Logo -->
|
|
12
12
|
<div class="text-center mb-3">
|
|
13
13
|
<div class="avatar avatar-xl">
|
|
14
|
-
<img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" alt="{{ site.brand.name }} Logo"/>
|
|
14
|
+
<img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" class="filter-adaptive" alt="{{ site.brand.name }} Logo"/>
|
|
15
15
|
</div>
|
|
16
16
|
</div>
|
|
17
17
|
|
|
@@ -11,13 +11,13 @@ layout: themes/[ site.theme.id ]/frontend/core/cover
|
|
|
11
11
|
<!-- Logo -->
|
|
12
12
|
<div class="text-center mb-3">
|
|
13
13
|
<div class="avatar avatar-xl">
|
|
14
|
-
<img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" alt="{{ site.brand.name }} Logo"/>
|
|
14
|
+
<img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" class="filter-adaptive" alt="{{ site.brand.name }} Logo"/>
|
|
15
15
|
</div>
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
<!-- Header -->
|
|
19
19
|
<div class="text-center mb-4">
|
|
20
|
-
<h1 class="h3 mb-2">Reset
|
|
20
|
+
<h1 class="h3 mb-2">Reset password</h1>
|
|
21
21
|
<p class="text-muted">Enter your email address and we'll send you a link to reset your password.</p>
|
|
22
22
|
</div>
|
|
23
23
|
|
|
@@ -26,13 +26,13 @@ social_signin:
|
|
|
26
26
|
<!-- Logo -->
|
|
27
27
|
<div class="text-center mb-3">
|
|
28
28
|
<div class="avatar avatar-xl">
|
|
29
|
-
<img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" alt="{{ site.brand.name }} Logo"/>
|
|
29
|
+
<img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" class="filter-adaptive" alt="{{ site.brand.name }} Logo"/>
|
|
30
30
|
</div>
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
33
|
<!-- Header -->
|
|
34
34
|
<div class="text-center mb-4">
|
|
35
|
-
<h1 class="h3 mb-2">Welcome
|
|
35
|
+
<h1 class="h3 mb-2">Welcome back!</h1>
|
|
36
36
|
<p class="text-muted">Sign in to your {{ site.brand.name }} account</p>
|
|
37
37
|
</div>
|
|
38
38
|
|
|
@@ -26,13 +26,13 @@ social_signup:
|
|
|
26
26
|
<!-- Logo -->
|
|
27
27
|
<div class="text-center mb-3">
|
|
28
28
|
<div class="avatar avatar-xl">
|
|
29
|
-
<img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" alt="{{ site.brand.name }} Logo"/>
|
|
29
|
+
<img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" class="filter-adaptive" alt="{{ site.brand.name }} Logo"/>
|
|
30
30
|
</div>
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
33
|
<!-- Header -->
|
|
34
34
|
<div class="text-center mb-4">
|
|
35
|
-
<h1 class="h3 mb-2">Create
|
|
35
|
+
<h1 class="h3 mb-2">Create your account</h1>
|
|
36
36
|
<p class="text-muted">Get started with {{ site.brand.name }} in seconds</p>
|
|
37
37
|
</div>
|
|
38
38
|
|
|
@@ -94,10 +94,24 @@ social_signup:
|
|
|
94
94
|
<!-- <div class="form-text">Must be at least 8 characters with letters and numbers</div> -->
|
|
95
95
|
</div>
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
{% comment %} TODO: in regions that allow pre-checked consent (e.g. US), default both to checked. {% endcomment %}
|
|
98
|
+
<div id="consent-group" class="mb-3 text-start p-2 rounded" style="border: 1px solid transparent;">
|
|
99
|
+
<div class="form-check mb-2">
|
|
100
|
+
<input class="form-check-input" type="checkbox" id="consent-legal" name="consentLegal">
|
|
101
|
+
<label class="form-check-label small" for="consent-legal">
|
|
102
|
+
I agree to {{ site.brand.name }}'s
|
|
103
|
+
<a href="/terms" class="text-decoration-none" target="_blank">Terms of Service</a>
|
|
104
|
+
and
|
|
105
|
+
<a href="/privacy" class="text-decoration-none" target="_blank">Privacy Policy</a>.
|
|
106
|
+
</label>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="form-check">
|
|
109
|
+
<input class="form-check-input" type="checkbox" id="consent-marketing" name="consentMarketing">
|
|
110
|
+
<label class="form-check-label small" for="consent-marketing">
|
|
111
|
+
I agree to receive product updates, newsletters, and marketing communications from {{ site.brand.name }}. You can unsubscribe anytime.
|
|
112
|
+
</label>
|
|
113
|
+
</div>
|
|
114
|
+
<div id="consent-error" class="small mt-2 d-none" style="color: var(--bs-form-invalid-color, var(--bs-danger, #dc3545));"></div>
|
|
101
115
|
</div>
|
|
102
116
|
|
|
103
117
|
<button type="submit" class="btn btn-success _btn-lg w-100 mb-3" data-provider="email" disabled>
|
|
@@ -9,7 +9,7 @@ layout: themes/[ site.theme.id ]/frontend/core/cover
|
|
|
9
9
|
<!-- Logo -->
|
|
10
10
|
<div class="text-center mb-3">
|
|
11
11
|
<div class="avatar avatar-xl">
|
|
12
|
-
<img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" alt="{{ site.brand.name }} Logo"/>
|
|
12
|
+
<img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" class="filter-adaptive" alt="{{ site.brand.name }} Logo"/>
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
@@ -24,10 +24,10 @@ member:
|
|
|
24
24
|
links:
|
|
25
25
|
- id: "linkedin"
|
|
26
26
|
title: "LinkedIn"
|
|
27
|
-
url: "https://www.linkedin.com/in/christina-hill"
|
|
27
|
+
url: "https://www.linkedin.com/in/christina-hill-23487"
|
|
28
28
|
- id: "twitter"
|
|
29
29
|
title: "Twitter"
|
|
30
|
-
url: "https://twitter.com/christina-hill"
|
|
30
|
+
url: "https://twitter.com/christina-hill-23487"
|
|
31
31
|
---
|
|
32
32
|
|
|
33
33
|
Hey there, I'm Christina!
|
|
@@ -25,13 +25,13 @@ member:
|
|
|
25
25
|
links:
|
|
26
26
|
- id: "linkedin"
|
|
27
27
|
title: "LinkedIn"
|
|
28
|
-
url: "https://www.linkedin.com/in/marcus-johnson"
|
|
28
|
+
url: "https://www.linkedin.com/in/marcus-johnson-381958"
|
|
29
29
|
- id: "dribbble"
|
|
30
30
|
title: "Dribbble"
|
|
31
|
-
url: "https://dribbble.com/marcus-johnson"
|
|
31
|
+
url: "https://dribbble.com/marcus-johnson-381958"
|
|
32
32
|
- id: "instagram"
|
|
33
33
|
title: "Instagram"
|
|
34
|
-
url: "https://www.instagram.com/marcus-johnson"
|
|
34
|
+
url: "https://www.instagram.com/marcus-johnson-381958"
|
|
35
35
|
---
|
|
36
36
|
|
|
37
37
|
Hey there, I'm Marcus!
|
|
@@ -25,10 +25,10 @@ member:
|
|
|
25
25
|
links:
|
|
26
26
|
- id: "linkedin"
|
|
27
27
|
title: "LinkedIn"
|
|
28
|
-
url: "https://www.linkedin.com/in/priya-sharma"
|
|
28
|
+
url: "https://www.linkedin.com/in/priya-sharma-5829947"
|
|
29
29
|
- id: "twitter"
|
|
30
30
|
title: "Twitter"
|
|
31
|
-
url: "https://twitter.com/priya-sharma"
|
|
31
|
+
url: "https://twitter.com/priya-sharma-5829947"
|
|
32
32
|
---
|
|
33
33
|
|
|
34
34
|
Hi there, I'm Priya!
|
|
@@ -25,13 +25,13 @@ member:
|
|
|
25
25
|
links:
|
|
26
26
|
- id: "linkedin"
|
|
27
27
|
title: "LinkedIn"
|
|
28
|
-
url: "https://www.linkedin.com/in/sarah-rodriguez"
|
|
28
|
+
url: "https://www.linkedin.com/in/sarah-rodriguez-103863"
|
|
29
29
|
- id: "twitter"
|
|
30
30
|
title: "Twitter"
|
|
31
|
-
url: "https://twitter.com/sarah-rodriguez"
|
|
31
|
+
url: "https://twitter.com/sarah-rodriguez-103863"
|
|
32
32
|
- id: "instagram"
|
|
33
33
|
title: "Instagram"
|
|
34
|
-
url: "https://www.instagram.com/
|
|
34
|
+
url: "https://www.instagram.com/sarah-rodriguez-103863"
|
|
35
35
|
---
|
|
36
36
|
|
|
37
37
|
Hola! I'm Sarah.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-jekyll-manager",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Ultimate Jekyll dependency manager",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"prettier": "^3.8.3",
|
|
108
108
|
"sass": "^1.99.0",
|
|
109
109
|
"spellchecker": "^3.7.1",
|
|
110
|
-
"web-manager": "^4.
|
|
110
|
+
"web-manager": "^4.2.0",
|
|
111
111
|
"webpack": "^5.106.2",
|
|
112
112
|
"wonderful-fetch": "^2.0.5",
|
|
113
113
|
"wonderful-version": "^1.3.2",
|