1import { bindLocaleRerender, getCurrentUser, tText } from '../../../client-utils.js'; 2import { createModal, createModalShell } from '../../../ui/modal.js'; 3import { confirmEmail, resendConfirmationEmail } from './auth-api.js'; 4import { getAuthReturnTarget } from './return-target.js'; 5import { setVisibility } from './ui-state.js'; 6import { isValidEmail } from './validators.js'; 7 8const COPY_BASE = 'backend.frontend.other.auth.userConfirm'; 9const GENERIC_ERROR_KEY = 'backend.frontend.other.auth.forgotPassword.custom'; 10 11const getEmailConfirmationRequest = (locationRef = window.location) => { 12 const url = new URL(locationRef.href); 13 if (!url.searchParams.has('confirm')) return null; 14 // The backend's legacy email links leave plus-address aliases unescaped. 15 const emailParams = new URLSearchParams(url.search.replace(/\+/g, '%2B')); 16 return { 17 email: String(emailParams.get('email') || '').trim(), 18 confirmationKey: String(url.searchParams.get('confirm') || '').trim(), 19 }; 20}; 21 22const clearConfirmationQuery = () => { 23 const url = new URL(window.location.href); 24 url.searchParams.delete('confirm'); 25 url.searchParams.delete('email'); 26 window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`); 27}; 28 29const createElement = (tag, className, attributes = {}) => { 30 const element = document.createElement(tag); 31 element.className = className; 32 Object.entries(attributes).forEach(([name, value]) => element.setAttribute(name, value)); 33 return element; 34}; 35 36// Returns true for malformed links too, so session/cart redirects cannot hide the error. 37const initEmailConfirmation = () => { 38 const request = getEmailConfirmationRequest(); 39 if (!request) return false; 40 41 const shell = createModalShell({ 42 size: 'md', 43 panelClassName: 'text-center', 44 labelledBy: 'auth-confirmation-title', 45 describedBy: 'auth-confirmation-description', 46 rootAttributes: { 'data-auth-confirmation-modal': '' }, 47 }); 48 if (!shell) return true; 49 50 const { root, panel, overlay, content } = shell; 51 const title = createElement('h2', 'm-0 text-2xl font-extrabold', { id: 'auth-confirmation-title' }); 52 const description = createElement('p', 'my-5 leading-7', { id: 'auth-confirmation-description' }); 53 const error = createElement('p', 'mb-5 text-error', { role: 'alert', 'data-auth-confirmation-error': '' }); 54 const action = createElement( 55 'button', 56 'shadow-button inline-flex min-w-25 items-center justify-center rounded-md border border-yellow bg-yellow px-4 py-2.5 text-xl font-extrabold text-foreground shadow-yellow/20 hover:bg-yellow-light focus:outline-hidden focus-visible:ring-4 focus-visible:ring-yellow/20', 57 { type: 'button', 'data-auth-confirmation-action': '' }, 58 ); 59 const closeButton = createElement( 60 'button', 61 'absolute top-2.5 right-2.5 rounded-full p-2 text-orange hover:text-foreground focus:outline-hidden focus-visible:ring-2 focus-visible:ring-orange/30', 62 { type: 'button', 'data-auth-confirmation-close': '', 'data-modal-close': '' }, 63 ); 64 closeButton.append(createElement('span', 'icon-cross text-2xl', { 'aria-hidden': 'true' })); 65 content.setAttribute('aria-live', 'polite'); 66 content.append(title, description, error, action); 67 panel.append(closeButton); 68 69 const validEmail = isValidEmail(request.email); 70 let phase = validEmail && request.confirmationKey ? 'loading' : 'failed'; 71 let busy = phase === 'loading'; 72 let errorKey = ''; 73 let locale; 74 let unsubscribeLocale = () => {}; 75 76 const render = (nextLocale) => { 77 locale = nextLocale || locale; 78 const text = (key) => tText(key, { locale }); 79 const loading = phase === 'loading'; 80 root.dataset.confirmationState = phase; 81 panel.setAttribute('aria-busy', String(busy)); 82 title.textContent = text(loading ? 'common.loading' : `${COPY_BASE}.${phase}.title`); 83 description.textContent = loading ? '' : text(!validEmail ? GENERIC_ERROR_KEY : `${COPY_BASE}.${phase}.text`); 84 action.textContent = text(busy ? 'common.loading' : `${COPY_BASE}.${phase}.button`); 85 action.disabled = busy; 86 setVisibility(action, !loading && validEmail); 87 error.textContent = errorKey ? text(errorKey) : '';
88 setVisibility(error, Boolean(errorKey)); 89 closeButton.setAttribute('aria-label', text('common.close')); 90 closeButton.disabled = busy; 91 }; 92 93 const modal = createModal({ 94 root, 95 panel, 96 overlay, 97 canClose: () => !busy, 98 initialFocus: () => (busy ? panel : action.hidden ? closeButton : action), 99 onClose: () => { 100 unsubscribeLocale(); 101 }, 102 }); 103 104 const finish = (nextPhase) => { 105 phase = nextPhase; 106 busy = false; 107 render(); 108 if (modal.isOpen()) (action.hidden ? closeButton : action).focus(); 109 }; 110 111 action.addEventListener('click', async () => { 112 if (busy) return; 113 if (phase === 'success') { 114 window.location.assign(getAuthReturnTarget()); 115 return; 116 } 117 busy = true; 118 errorKey = ''; 119 render(); 120 try { 121 await resendConfirmationEmail(request.email); 122 finish('resend'); 123 } catch { 124 errorKey = GENERIC_ERROR_KEY; 125 finish('failed'); 126 } 127 }); 128 129 // Keep credentials out of later navigation/referrers while retaining the safe return destination. 130 clearConfirmationQuery(); 131 render(); 132 modal.open(); 133 unsubscribeLocale = bindLocaleRerender({ render, hasServerMarkup: () => true }); 134 135 if (busy) { 136 void (async () => { 137 try { 138 const result = await confirmEmail(request); 139 if (result !== 'user.email.confirmed') throw new Error('Email confirmation did not complete'); 140 finish('success'); 141 void getCurrentUser({ force: true }); 142 } catch { 143 finish('failed'); 144 } 145 })(); 146 } 147 return true; 148}; 149 150export { getEmailConfirmationRequest, initEmailConfirmation };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.