PageSourceSearch

https://site.fr/_static/8c8af5c529149f32/scripts/features/common/auth/email-confirmation.js

js site.fr collected 2026-09-24 18:20:29 UTC 5,777 bytes, 150 lines download raw bytes

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.