ultimate-jekyll-manager 1.3.7 → 1.3.10
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 +33 -0
- package/dist/assets/css/pages/account/index.scss +0 -10
- package/dist/assets/js/core/auth.js +10 -0
- package/dist/assets/js/libs/auth.js +15 -0
- package/dist/assets/js/pages/account/sections/notifications.js +52 -54
- package/dist/assets/js/pages/account/sections/security.js +91 -0
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +81 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,39 @@ 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.10] - 2026-05-24
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Generate signin link** — advanced-user feature on `/account#security` that creates a temporary `/signin?authCustomToken=<token>` URL via `POST /backend-manager/user/token`. Centered text-link trigger sits under the Active sessions card (same Bootstrap utility classes as Cancel subscription: `btn btn-link btn-sm text-muted text-decoration-underline opacity-50`). Opens a danger-bordered modal with a typed-phrase gate (`I will not share this link`) that enables the red Generate button. On success, the warning view swaps in-place for the link inside a readonly monospaced input + Copy button, with a prominent 1-hour expiry warning. Token never persists outside the input — `show.bs.modal` resets state every open. Backend route already existed (`functions/routes/user/token/post.js`) and the existing `handleCustomTokenSignin` flow in `src/assets/js/libs/auth.js` consumes the resulting URL.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **Account-page marketing toggle now requires an explicit "Save preferences" button.** v1.3.9 made the toggle auto-submit on change. Cleaner UX: user flips the toggle, sees the new state visually, then clicks Save when they actually want to commit. Markup adds a `<button type="submit" class="btn btn-primary">Save preferences</button>` to the `#marketing-emails-form` (classy account template). JS drops the `addEventListener('change', () => formManager.submit())` line. On failure, the toggle stays where the user left it (no auto-revert) — they see the error message via FormManager and can hit Save again.
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- **`.cancel-trigger-link` SCSS class.** Replaced with Bootstrap utilities on both the existing Cancel subscription button and the new Generate signin link button (`text-muted opacity-50` instead of the custom `opacity: 0.7` + hover transition + `0.8125rem` font size). Avoids inventing project-specific classes when Bootstrap utilities cover the same styling.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
## [1.3.9] - 2026-05-24
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Account-page marketing toggle showed "Failed to update email preferences" even on successful unsubscribe.** Root cause: `pages/account/sections/notifications.js` was checking `response.data?.success !== true`, but `authorizedFetch` returns the JSON body directly (no `data` wrapper), and BEM's `assistant.respond({ success: true })` writes the object at the response root via `res.json(response)`. So `response.success === true` and `response.data?.success === undefined` — the check fired even though the backend had successfully written `consent.marketing.status = 'revoked'` AND removed the contact from SendGrid + Beehiiv. Frontend UX showed a danger toast and reverted the toggle, but server state was already correct, putting the UI out of sync with reality. Fix: check `response.success` at the root.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **`pages/account/sections/notifications.js` refactored to use FormManager**, matching the project rule that all user-driven API forms use form-manager for in-flight/success/error UX. The toggle is now wrapped in `<form id="marketing-emails-form">` (classy account template, `src/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html`), and the change event triggers `formManager.submit()`. Success notification uses `formManager.showSuccess()`; failure throws so FormManager surfaces the error toast and the JS reverts the toggle to its last-known-good state. Replaces the ad-hoc `addEventListener('change')` + raw `authorizedFetch` + manual `webManager.utilities().showNotification()` pattern.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
## [1.3.8] - 2026-05-24
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **Reverse-signup now keeps the user on `/signin` so they actually see the inline error.** v1.3.7 fixed `isNewUser` detection, but a follow-on race appeared: when Firebase's `getRedirectResult()` returns a fresh-signup user, the auth-state-change listener in `core/auth.js` fires `state.user = <about-to-be-deleted>` BEFORE `reverseAccidentalSignup`'s `await newUser.delete() → signOut()` chain completes. The listener's `policy === 'unauthenticated'` branch then redirects to `/account` (or `authReturnUrl`), and by the time the inline `showError()` call fires, the user is already off the page. Fixed with a `window.__UJM_REVERSING_SIGNUP` flag set synchronously before the delete + cleared after signOut's followup state-change. The listener checks the flag at the top and short-circuits the entire callback — no redirect, no metadata POST, no consent guard, nothing — until the reversal completes and the user lands on `user = null` with the inline error visible on `/signin`.
|
|
49
|
+
|
|
17
50
|
---
|
|
18
51
|
## [1.3.7] - 2026-05-24
|
|
19
52
|
|
|
@@ -78,16 +78,6 @@
|
|
|
78
78
|
background-color: var(--bs-tertiary-bg);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
.cancel-trigger-link {
|
|
82
|
-
font-size: 0.8125rem;
|
|
83
|
-
opacity: 0.7;
|
|
84
|
-
transition: opacity 0.2s;
|
|
85
|
-
|
|
86
|
-
&:hover {
|
|
87
|
-
opacity: 1;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
81
|
#cancel-subscription-btn {
|
|
92
82
|
&:disabled {
|
|
93
83
|
opacity: 0.5;
|
|
@@ -51,6 +51,16 @@ export default function () {
|
|
|
51
51
|
// Log
|
|
52
52
|
console.log('[Auth] state changed:', state);
|
|
53
53
|
|
|
54
|
+
// Short-circuit if a reverse-signup is in progress (libs/auth.js#reverseAccidentalSignup
|
|
55
|
+
// sets this synchronously before .delete() + signOut()). Without this, the brief
|
|
56
|
+
// window where Firebase shows user=<about-to-be-deleted-account> would trigger the
|
|
57
|
+
// policy-based redirect to /account (or authReturnUrl) BEFORE the user sees the
|
|
58
|
+
// inline error on /signin. Flag is cleared at the end of reverseAccidentalSignup.
|
|
59
|
+
if (window.__UJM_REVERSING_SIGNUP) {
|
|
60
|
+
console.warn('[Auth] Skipping state-change processing — reverse-signup in progress');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
// Set user ID for analytics tracking
|
|
55
65
|
setAnalyticsUserId(user);
|
|
56
66
|
|
|
@@ -125,6 +125,16 @@ export default function () {
|
|
|
125
125
|
async function reverseAccidentalSignup(newUser) {
|
|
126
126
|
console.warn('[Auth] Reversing accidental signup from /signin (new Google account created with no consent on record)');
|
|
127
127
|
|
|
128
|
+
// SYNCHRONOUSLY flag the reversal so the auth-state-change listener in
|
|
129
|
+
// core/auth.js short-circuits its policy-based redirect for this user.
|
|
130
|
+
// Without this, Firebase's redirect-result-success path triggers an auth
|
|
131
|
+
// state change with user=<the-about-to-be-deleted-account> BEFORE we
|
|
132
|
+
// finish .delete() + signOut(), and the listener redirects to /account
|
|
133
|
+
// (or authReturnUrl) before the user ever sees the inline error.
|
|
134
|
+
// Cleared in the finally block after signOut() has fired the followup
|
|
135
|
+
// auth-state-change with user=null.
|
|
136
|
+
window.__UJM_REVERSING_SIGNUP = true;
|
|
137
|
+
|
|
128
138
|
try {
|
|
129
139
|
await newUser.delete();
|
|
130
140
|
} catch (e) {
|
|
@@ -152,6 +162,11 @@ export default function () {
|
|
|
152
162
|
formManager.showError(`This account doesn't exist. Try signing up first or use a different account.`);
|
|
153
163
|
formManager.ready();
|
|
154
164
|
}
|
|
165
|
+
|
|
166
|
+
// Clear the flag now that signOut() has fired its auth-state-change
|
|
167
|
+
// with user=null. Future state changes (e.g. user re-clicks Continue
|
|
168
|
+
// with Google after seeing the error) get normal listener processing.
|
|
169
|
+
window.__UJM_REVERSING_SIGNUP = false;
|
|
155
170
|
}
|
|
156
171
|
|
|
157
172
|
// Validate that the user has agreed to the legal terms. Instead of highlighting the
|
|
@@ -2,25 +2,71 @@
|
|
|
2
2
|
* Notifications section — marketing email consent toggle.
|
|
3
3
|
*
|
|
4
4
|
* Reads consent.marketing.status from the user doc for the toggle's initial state.
|
|
5
|
-
*
|
|
5
|
+
* User flips the toggle then clicks Save; on submit, POSTs to
|
|
6
|
+
* /backend-manager/marketing/email-preferences with subscribe|unsubscribe.
|
|
6
7
|
* The server writes consent.marketing to the user doc + syncs SendGrid + Beehiiv.
|
|
8
|
+
*
|
|
9
|
+
* Uses FormManager for standard in-flight/success/error UX. On failure, the
|
|
10
|
+
* error message is shown via FormManager and the toggle stays where the user
|
|
11
|
+
* left it — they can try Save again without re-flipping.
|
|
7
12
|
*/
|
|
8
13
|
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
14
|
+
import { FormManager } from '__main_assets__/js/libs/form-manager.js';
|
|
9
15
|
import webManager from 'web-manager';
|
|
10
16
|
|
|
17
|
+
const FORM_ID = 'marketing-emails-form';
|
|
11
18
|
const TOGGLE_ID = 'marketing-emails';
|
|
12
19
|
const GRANT_DATE_ID = 'marketing-emails-grant-date';
|
|
13
20
|
|
|
21
|
+
let formManager = null;
|
|
22
|
+
|
|
14
23
|
export function init() {
|
|
15
|
-
const $
|
|
16
|
-
if (!$
|
|
24
|
+
const $form = document.getElementById(FORM_ID);
|
|
25
|
+
if (!$form) {
|
|
17
26
|
return;
|
|
18
27
|
}
|
|
19
|
-
|
|
28
|
+
|
|
29
|
+
formManager = new FormManager(`#${FORM_ID}`, {
|
|
30
|
+
autoReady: false, // Wait for loadData() to populate the toggle
|
|
31
|
+
allowResubmit: true, // Save → flip again → Save again is normal flow
|
|
32
|
+
warnOnUnsavedChanges: false, // Toggle changes are explicit-Save, not draft
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
formManager.on('submit', async ({ data }) => {
|
|
36
|
+
const action = data.enabled ? 'subscribe' : 'unsubscribe';
|
|
37
|
+
|
|
38
|
+
const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/marketing/email-preferences`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
timeout: 60000,
|
|
41
|
+
response: 'json',
|
|
42
|
+
tries: 2,
|
|
43
|
+
body: { action },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response?.success) {
|
|
47
|
+
throw new Error(response?.message || 'Failed to update email preferences. Please try again.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
formManager.showSuccess(
|
|
51
|
+
data.enabled
|
|
52
|
+
? 'Subscribed to email updates.'
|
|
53
|
+
: 'Unsubscribed from email updates.'
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Hide the grant-date line on unsubscribe (the displayed date was the OLD grant;
|
|
57
|
+
// informational only). loadData() will repopulate it on the next page load if
|
|
58
|
+
// the user re-subscribes.
|
|
59
|
+
if (!data.enabled) {
|
|
60
|
+
const $date = document.getElementById(GRANT_DATE_ID);
|
|
61
|
+
if ($date) {
|
|
62
|
+
$date.classList.add('d-none');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
20
66
|
}
|
|
21
67
|
|
|
22
68
|
export function loadData(account) {
|
|
23
|
-
if (!account) {
|
|
69
|
+
if (!account || !formManager) {
|
|
24
70
|
return;
|
|
25
71
|
}
|
|
26
72
|
|
|
@@ -42,54 +88,6 @@ export function loadData(account) {
|
|
|
42
88
|
$date.classList.remove('d-none');
|
|
43
89
|
}
|
|
44
90
|
}
|
|
45
|
-
}
|
|
46
|
-
|
|
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';
|
|
52
|
-
|
|
53
|
-
// Disable while in-flight so rapid clicks don't fire multiple requests
|
|
54
|
-
$toggle.disabled = true;
|
|
55
|
-
|
|
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
|
-
});
|
|
64
|
-
|
|
65
|
-
if (response.error || response.data?.success !== true) {
|
|
66
|
-
throw new Error(response.message || response.error || 'Failed to update email preferences.');
|
|
67
|
-
}
|
|
68
91
|
|
|
69
|
-
|
|
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);
|
|
84
|
-
|
|
85
|
-
// Revert toggle on failure so UI matches server state
|
|
86
|
-
$toggle.checked = wasChecked;
|
|
87
|
-
|
|
88
|
-
webManager.utilities().showNotification(
|
|
89
|
-
'Failed to update email preferences. Please try again.',
|
|
90
|
-
{ type: 'danger' }
|
|
91
|
-
);
|
|
92
|
-
} finally {
|
|
93
|
-
$toggle.disabled = false;
|
|
94
|
-
}
|
|
92
|
+
formManager.ready();
|
|
95
93
|
}
|
|
@@ -20,6 +20,7 @@ const useAuthPopup = url.searchParams.get('authPopup') === 'true' || window !==
|
|
|
20
20
|
export function init() {
|
|
21
21
|
initializeSigninMethods();
|
|
22
22
|
initializeSignoutAllForm();
|
|
23
|
+
initializeSigninLinkGenerator();
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
// Load security data
|
|
@@ -401,6 +402,96 @@ function initializeSignoutAllForm() {
|
|
|
401
402
|
}
|
|
402
403
|
}
|
|
403
404
|
|
|
405
|
+
// Initialize signin link generator (advanced feature).
|
|
406
|
+
// Creates a temporary signin URL using a Firebase custom token. The link grants
|
|
407
|
+
// full account access to anyone who holds it, so we gate it behind a typed
|
|
408
|
+
// confirmation phrase before hitting BEM's /user/token route.
|
|
409
|
+
function initializeSigninLinkGenerator() {
|
|
410
|
+
const $modal = document.getElementById('generate-signin-link-modal');
|
|
411
|
+
if (!$modal) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const $phrase = document.getElementById('signin-link-confirm-phrase');
|
|
416
|
+
const $input = document.getElementById('signin-link-confirm-input');
|
|
417
|
+
const $generateBtn = document.getElementById('signin-link-generate-btn');
|
|
418
|
+
const $warningView = document.getElementById('generate-signin-link-warning');
|
|
419
|
+
const $resultView = document.getElementById('generate-signin-link-result');
|
|
420
|
+
const $output = document.getElementById('signin-link-output');
|
|
421
|
+
const $copyBtn = document.getElementById('signin-link-copy-btn');
|
|
422
|
+
|
|
423
|
+
const expectedPhrase = $phrase.textContent.trim();
|
|
424
|
+
|
|
425
|
+
// Reset modal state every time it opens. The custom token is never persisted
|
|
426
|
+
// outside the input — closing the modal must drop it from the DOM.
|
|
427
|
+
$modal.addEventListener('show.bs.modal', () => {
|
|
428
|
+
$input.value = '';
|
|
429
|
+
$output.value = '';
|
|
430
|
+
$generateBtn.disabled = true;
|
|
431
|
+
$warningView.classList.remove('d-none');
|
|
432
|
+
$resultView.classList.add('d-none');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Enable Generate only when the typed phrase matches exactly.
|
|
436
|
+
$input.addEventListener('input', () => {
|
|
437
|
+
$generateBtn.disabled = $input.value.trim() !== expectedPhrase;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
$generateBtn.addEventListener('click', async () => {
|
|
441
|
+
// Defensive: the button is disabled until the phrase matches, but re-check
|
|
442
|
+
// in case anything bypassed the input handler.
|
|
443
|
+
if ($input.value.trim() !== expectedPhrase) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const originalText = $generateBtn.querySelector('.button-text').textContent;
|
|
448
|
+
$generateBtn.disabled = true;
|
|
449
|
+
$generateBtn.querySelector('.button-text').textContent = 'Generating...';
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const tokenURL = `${webManager.getApiUrl()}/backend-manager/user/token`;
|
|
453
|
+
const data = await authorizedFetch(tokenURL, {
|
|
454
|
+
method: 'POST',
|
|
455
|
+
timeout: 60000,
|
|
456
|
+
response: 'json',
|
|
457
|
+
tries: 2,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const token = data?.token;
|
|
461
|
+
if (!token) {
|
|
462
|
+
throw new Error('No token returned from server');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const signinURL = new URL('/signin', window.location.origin);
|
|
466
|
+
signinURL.searchParams.set('authCustomToken', token);
|
|
467
|
+
|
|
468
|
+
$output.value = signinURL.toString();
|
|
469
|
+
$warningView.classList.add('d-none');
|
|
470
|
+
$resultView.classList.remove('d-none');
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error('[Security] Failed to generate signin link:', error);
|
|
473
|
+
webManager.utilities().showNotification(
|
|
474
|
+
`Failed to generate signin link: ${error.message || 'Unknown error'}`,
|
|
475
|
+
{ type: 'danger', timeout: 8000 }
|
|
476
|
+
);
|
|
477
|
+
$generateBtn.disabled = false;
|
|
478
|
+
} finally {
|
|
479
|
+
$generateBtn.querySelector('.button-text').textContent = originalText;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
$copyBtn.addEventListener('click', async () => {
|
|
484
|
+
try {
|
|
485
|
+
await navigator.clipboard.writeText($output.value);
|
|
486
|
+
webManager.utilities().showNotification('Signin link copied to clipboard', 'success');
|
|
487
|
+
} catch (error) {
|
|
488
|
+
$output.select();
|
|
489
|
+
document.execCommand('copy');
|
|
490
|
+
webManager.utilities().showNotification('Signin link copied to clipboard', 'success');
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
404
495
|
// Connect Google provider
|
|
405
496
|
async function connectGoogleProvider() {
|
|
406
497
|
// Dynamic import of Firebase auth methods
|
|
@@ -863,6 +863,73 @@ badges:
|
|
|
863
863
|
</div>
|
|
864
864
|
</div>
|
|
865
865
|
</div>
|
|
866
|
+
|
|
867
|
+
<!-- Generate Signin Link (advanced) -->
|
|
868
|
+
<div id="generate-signin-link-trigger" class="text-center">
|
|
869
|
+
<button type="button" class="btn btn-link btn-sm text-muted text-decoration-underline opacity-50" data-bs-toggle="modal" data-bs-target="#generate-signin-link-modal">
|
|
870
|
+
Generate signin link
|
|
871
|
+
</button>
|
|
872
|
+
</div>
|
|
873
|
+
|
|
874
|
+
<!-- Generate Signin Link Modal -->
|
|
875
|
+
<div class="modal fade" id="generate-signin-link-modal" tabindex="-1" aria-labelledby="generate-signin-link-title" aria-hidden="true">
|
|
876
|
+
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
|
877
|
+
<div class="modal-content border border-danger">
|
|
878
|
+
<div class="modal-header">
|
|
879
|
+
<h5 class="modal-title text-danger" id="generate-signin-link-title">
|
|
880
|
+
{% uj_icon "triangle-exclamation", "fa-lg me-2" %}
|
|
881
|
+
Generate signin link
|
|
882
|
+
</h5>
|
|
883
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
884
|
+
</div>
|
|
885
|
+
<div class="modal-body">
|
|
886
|
+
<!-- Warning view -->
|
|
887
|
+
<div id="generate-signin-link-warning">
|
|
888
|
+
<div class="alert alert-danger">
|
|
889
|
+
<strong>This is a dangerous feature for advanced users.</strong>
|
|
890
|
+
<p class="mb-2 mt-2 small">
|
|
891
|
+
Generating a signin link creates a single URL that lets <strong>anyone who has it</strong> sign in to your account — no password, no 2FA, no email verification.
|
|
892
|
+
</p>
|
|
893
|
+
<ul class="small mb-0">
|
|
894
|
+
<li>Use it only to sign in to your own account on another browser or device.</li>
|
|
895
|
+
<li><strong>Never share, message, email, or paste this link anywhere.</strong> Anyone with the link gets full access to your account.</li>
|
|
896
|
+
<li>The link expires in 1 hour, but treat it as a live key until then.</li>
|
|
897
|
+
<li>If you suspect the link was exposed, sign out of all sessions immediately.</li>
|
|
898
|
+
</ul>
|
|
899
|
+
</div>
|
|
900
|
+
|
|
901
|
+
<p class="mb-2">Type the following phrase exactly to confirm you understand the risks:</p>
|
|
902
|
+
<p class="mb-2"><code id="signin-link-confirm-phrase">I will not share this link</code></p>
|
|
903
|
+
|
|
904
|
+
<input type="text" class="form-control" id="signin-link-confirm-input" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="Type the phrase above">
|
|
905
|
+
</div>
|
|
906
|
+
|
|
907
|
+
<!-- Result view (hidden until generated) -->
|
|
908
|
+
<div id="generate-signin-link-result" class="d-none">
|
|
909
|
+
<div class="alert alert-warning small mb-3">
|
|
910
|
+
{% uj_icon "clock", "me-1" %}
|
|
911
|
+
This link expires in <strong>1 hour</strong>. Use it immediately and do not share it.
|
|
912
|
+
</div>
|
|
913
|
+
<label for="signin-link-output" class="form-label small text-muted mb-1">Your signin link:</label>
|
|
914
|
+
<div class="input-group">
|
|
915
|
+
<input type="text" class="form-control font-monospace small" id="signin-link-output" readonly>
|
|
916
|
+
<button type="button" class="btn btn-outline-primary" id="signin-link-copy-btn">
|
|
917
|
+
{% uj_icon "copy", "me-1" %}
|
|
918
|
+
<span class="button-text">Copy</span>
|
|
919
|
+
</button>
|
|
920
|
+
</div>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
<div class="modal-footer">
|
|
924
|
+
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
925
|
+
<button type="button" class="btn btn-danger" id="signin-link-generate-btn" disabled>
|
|
926
|
+
{% uj_icon "key", "me-1" %}
|
|
927
|
+
<span class="button-text">Generate link</span>
|
|
928
|
+
</button>
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
866
933
|
</section>
|
|
867
934
|
|
|
868
935
|
<!-- Connections Section -->
|
|
@@ -1026,7 +1093,7 @@ badges:
|
|
|
1026
1093
|
|
|
1027
1094
|
<!-- Cancel Subscription (only visible for paid users) -->
|
|
1028
1095
|
<div id="cancel-subscription-trigger" class="text-center" data-wm-bind="@show billing.buttons.cancel" hidden>
|
|
1029
|
-
<button type="button" class="btn btn-link btn-sm text-muted text-decoration-underline
|
|
1096
|
+
<button type="button" class="btn btn-link btn-sm text-muted text-decoration-underline opacity-50" data-bs-toggle="collapse" data-bs-target="#cancel-subscription-accordion" aria-expanded="false" aria-controls="cancel-subscription-accordion">
|
|
1030
1097
|
Cancel subscription
|
|
1031
1098
|
</button>
|
|
1032
1099
|
</div>
|
|
@@ -1195,14 +1262,19 @@ badges:
|
|
|
1195
1262
|
<div class="card-body">
|
|
1196
1263
|
<h5 class="card-title">Email preferences</h5>
|
|
1197
1264
|
|
|
1198
|
-
<
|
|
1199
|
-
<
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1265
|
+
<form id="marketing-emails-form" novalidate>
|
|
1266
|
+
<div class="form-check form-switch mb-3">
|
|
1267
|
+
<input class="form-check-input" type="checkbox" id="marketing-emails" name="enabled">
|
|
1268
|
+
<label class="form-check-label" for="marketing-emails">
|
|
1269
|
+
Product updates, newsletters, and marketing communications
|
|
1270
|
+
<small class="d-block text-muted">You can withdraw consent at any time.</small>
|
|
1271
|
+
<small id="marketing-emails-grant-date" class="d-block text-muted d-none"></small>
|
|
1272
|
+
</label>
|
|
1273
|
+
</div>
|
|
1274
|
+
<button type="submit" class="btn btn-primary">
|
|
1275
|
+
<span class="button-text">Save preferences</span>
|
|
1276
|
+
</button>
|
|
1277
|
+
</form>
|
|
1206
1278
|
</div>
|
|
1207
1279
|
</div>
|
|
1208
1280
|
</section>
|