PageSourceSearch

https://site.fr/_static/8c8af5c529149f32/scripts/features/search/search-plan-cart-sync.js

js site.fr collected 2026-09-24 18:21:32 UTC 10,326 bytes, 339 lines download raw bytes

1import {
2  CART_REFRESH_EVENT,
3  addPlanToCart,
4  buildCartSnapshotWithProducts,
5  createEmptyCartSnapshot,
6  emitCartRefresh,
7  fetchCurrentCart,
8  getCartFromRefreshEvent,
9  getCartItemsCount,
10  getCartProducts,
11  savePlanCartPeriod,
12} from '../common/cart-client.js';
13import { getCurrencyFromRoot, getRootData } from '../../client-utils.js';
14import { showNotificationModal } from '../common/notification-modal.js';
15import { addToCartEvent } from '../common/data-layer-events.js';
16import { normalizePlanKey } from '../domain/plans-shared.js';
17import { getSearchPlanCatalog } from './search-plan-catalog.js';
18import {
19  buildSelectedPlanCartState,
20  buildStateFromCartProduct,
21  findSelectedPlanCartProduct,
22  readStoredSelectedPlanCart,
23  writeStoredSelectedPlanCart,
24} from './search-plan-cart-state.js';
25import {
26  getSelectedSearchPlanId,
27  getSelectedSearchPlanPeriod,
28  setSelectedSearchPlanPeriod,
29  subscribeSelectedSearchPlanId,
30  subscribeSelectedSearchPlanPeriod,
31} from './search-plan-selection.js';
32
33const cartSnapsh
33otSubscribers = new Set();
34
35let searchPlanCartSnapshot = null;
36let currentSelectedPlanCart = null;
37let hydratePromise = null;
38let refreshEventSuppressed = false;
39let selectedPlanPeriodSyncInProgress = false;
40let cartSyncInitialized = false;
41
42const toPositiveNumber = (value) => {
43  const numeric = Number(value);
44  return Number.isFinite(numeric) && numeric > 0 ? numeric : null;
45};
46
47const coerceCartSnapshot = (cart) => (cart && typeof cart === 'object' ? cart : createEmptyCartSnapshot());
48
49const notifyCartSnapshotSubscribers = () => {
50  cartSnapshotSubscribers.forEach((listener) => {
51    listener(searchPlanCartSnapshot);
52  });
53};
54
55const setCurrentSelectedPlanCart = (value) => {
56  currentSelectedPlanCart = value || null;
57  writeStoredSelectedPlanCart(currentSelectedPlanCart);
58  return currentSelectedPlanCart;
59};
60
61const getSelectedSearchPlanCart = () => currentSelectedPlanCart;
62
63const getSearchPlanCartSnapshot = () => searchPlanCartSnapshot;
64
65const subscribeSearchPlanCartSnapshot = (listener, { emitCurrent = false } = {}) => {
66  if (typeof listener !== 'function') {
67    return () => {};
68  }
69
70  cartSnapshotSubscribers.add(listener);
71  if (emitCurrent) {
72    listener(searchPlanCartSnapshot);
73  }
74
75  return () => {
76    cartSnapshotSubscribers.delete(listener);
77  };
78};
79
80const emitSharedCartRefresh = () => {
81  refreshEventSuppressed = true;
82  try {
83    emitCartRefresh({
84      cart: searchPlanCartSnapshot,
85      count: getCartItemsCount(searchPlanCartSnapshot),
86      item: currentSelectedPlanCart,
87    });
88  } finally {
89    refreshEventSuppressed = false;
90  }
91};
92
93const resolveSelectedPlanCartFromSnapshot = ({ preferredProductId = null, syncSelectedPeriod = false } = {}) => {
94  const selectedPlanId = getSelectedSearchPlanId();
95  if (!selectedPlanId) {
96    setCurrentSelectedPlanCart(null);
97    return null;
98  }
99
100  const storedState = readStoredSelectedPlanCart();
101  const { planKeysById } = getSearchPlanCatalog();
102  const selectedPlanName = planKeysById.get(String(selectedPlanId)) || '';
103  const matchedProduct = findSelectedPlanCartProduct(getCartProducts(searchPlanCartSnapshot), selectedPlanId, {
104    preferredProductId: preferredProductId ?? storedState?.id ?? null,
105    planName: selectedPlanName,
106  });
107  const nextState = matchedProduct ? buildStateFromCartProduct(matchedProduct, selectedPlanId) : null;
108
109  setCurrentSelectedPlanCart(nextState);
110
111  if (syncSelectedPeriod && nextState?.orderPeriod && getSelectedSearchPlanPeriod() !== Number(nextState.orderPeriod)) {
112    setSelectedSearchPlanPeriod(nextState.orderPeriod);
113  }
114
115  return currentSelectedPlanCart;
116};
117
118const commitSearchPlanCartSnapshot = (
119  nextCart,
120  { emitRefresh = true, preferredProductId = null, syncSelectedPeriod = false } = {},
121) => {
122  searchPlanCartSnapshot = coerceCartSnapshot(nextCart);
123  resolveSelectedPlanCartFromSnapshot({
124    preferredProductId,
125    syncSelectedPeriod,
126  });
127  notifyCartSnapshotSubscribers();
128
129  if (emitRefresh) {
130    emitSharedCartRefresh();
131  }
132
133  return searchPlanCartSnapshot;
134};
135
136const findCartProductById = (productId) => {
137  const safeProductId = toPositiveNumber(productId);
138  if (!safeProductId) {
139    return null;
140  }
141  return getCartProducts(searchPlanCartSnapshot).find((product) => Number(product?.id) === safeProductId) || null;
142};
143
144const mergeCartProductIntoSnapshot = (product, { appendIfMissing = true } = {}) => {
145  if (!product || typeof product !== 'object') {
146    return getCartProducts(searchPlanCartSnapshot);
147  }
148
149  const nextProductId = Number(product?.id);
150  const nextProducts = getCartProducts(searchPlanCartSnapshot).slice();
151  const existingIndex = nextProducts.findIndex((entry) => Number(entry?.id) === nextProductId);
152
153  if (existingIndex >= 0) {
154    nextProducts[existingIndex] = product;
155    return nextProducts;
156  }
157
158  if (appendIfMissing) {
159    nextProducts.push(product);
160  }
161
162  return nextProducts;
163};
164
165const syncSelectedPlanPeriodToCart = async () => {
166  if (selectedPlanPeriodSyncInProgress) {
167    return;
168  }
169
170  selectedPlanPeriodSyncInProgress = true;
171
172  try {
173    await whenSearchPlanCartReady();
174    while (true) {
175      const selectedPlanId = getSelectedSearchPlanId();
176      const desiredOrderPeriod = getSelectedSearchPlanPeriod();
177      const selectedPlanCart = getSelectedSearchPlanCart();
178
179      if (!selectedPlanId || !selectedPlanCart || selectedPlanCart.planId !== selectedPlanId) {
180        break;
181      }
182
183      const currentOrderPeriod = Number(selectedPlanCart.orderPeriod) || 1;
184      if (desiredOrderPeriod === currentOrderPeriod) {
185        break;
186      }
187
188      const selectedCartProductId = toPositiveNumber(selectedPlanCart.id);
189      const selectedCartConfigId = toPositiveNumber(selectedPlanCart?.shopping_cart_product_configuration?.id);
190      if (!selectedCartProductId || !selectedCartConfigId || !findCartProductById(selectedCartProductId)) {
191        resolveSelectedPlanCartFromSnapshot();
192        break;
193      }
194
195      try {
196        const cart = await savePlanCartPeriod(selectedCartConfigId, desiredOrderPeriod);
197        commitSearchPlanCartSnapshot(cart, { preferredProductId: selectedCartProductId });
198        const confirmedProduct = getCartProducts(cart).find(
199          (product) => Number(product?.shopping_cart_product_configuration?.id) === selectedCartConfigId,
200        );
201        if (
202          confirmedProduct &&
203          Number(confirmedProduct.shopping_cart_product_configuration.product_configuration?.order_period) !==
204            desiredOrderPeriod
205        ) {
206          throw new Error('The selected plan period was not confirmed by the cart');
207        }
208      } catch (error) {
209        console.error('Failed to update selected plan period:', error);
210        // Keep the last confirmed period and stop. A new user selection can retry.
211        resolveSelectedPlanCartFromSnapshot({ syncSelectedPeriod: true });
212        showNotificationModal({ mode: 'error' });
213        break;
214      }
215    }
216  } finally {
217    selectedPlanPeriodSyncInProgress = false;
218  }
219};
220
221const addSelectedSearchPlanToCart = async (planId, { orderPeriod = getSelectedSearchPlanPeriod() } = {}) => {
222  await whenSearchPlanCartReady();
223
224  const normalizedPlanId = String(planId || '').trim();
225  const safeOrderPeriod = Number.isFinite(Number(orderPeriod)) && Number(orderPeriod) > 0 ? Number(orderPeriod) : 1;
226  if (!normalizedPlanId) {
227    return null;
228  }
229
230  const { plansById } = getSearchPlanCatalog();
231  const nextPlanEntry = plansById.get(normalizedPlanId);
232  const nextPlanName = normalizePlanKey(nextPlanEntry?.plan?.name);
233  if (!nextPlanEntry || !nextPlanName) {
234    return null;
235  }
236
237  const cartIndex = getCartItemsCount(searchPlanCartSnapshot);
238  const response = await addPlanToCart(nextPlanName, safeOrderPeriod);
239  addToCartEvent({
240    price: response?.price,
241    name: nextPlanName,
242    index: cartIndex,
243    currency: getCurrencyFromRoot(getRootData()) || 'EUR',
244  });
245  const confirmedState =
246    buildSelectedPlanCartState({
247      response,
248      planId: normalizedPlanId,
249      planName: nextPlanName,
250      orderPeriod: safeOrderPeriod,
251      price: response?.price,
252      priceWithoutDiscount: response?.price_without_discount,
253    }) || buildStateFromCartProduct(response, normalizedPlanId);
254
255  commitSearchPlanCartSnapshot(
256    buildCartSnapshotWithProducts(mergeCartProductIntoSnapshot(response), searchPlanCartSnapshot),
257    {
258      preferredProductId: getSelectedSearchPlanId() === normalizedPlanId ? (confirmedState?.id ?? null) : null,
259    },
260  );
261
262  if (getSelectedSearchPlanId() === normalizedPlanId && getSelectedSearchPlanPeriod() !== safeOrderPeriod) {
263    setSelectedSearchPlanPeriod(safeOrderPeriod);
264  }
265
266  return confirmedState;
267};
268
269const whenSearchPlanCartReady = async () => {
270  if (!hydratePromise) {
271    return searchPlanCartSnapshot;
272  }
273
274  await hydratePromise;
275  return searchPlanCartSnapshot;
276};
277
278const initSearchPlanCartSync = () => {
279  if (cartSyncInitialized || typeof document === 'undefined') {
280    return hydratePromise;
281  }
282  cartSyncInitialized = true;
283
284  document.addEventListener(CART_REFRESH_EVENT, (event) => {
285    if (refreshEventSuppressed) {
286      return;
287    }
288
289    const cart = getCartFromRefreshEvent(event);
290    if (!cart) {
291      return;
292    }
293
294    commitSearchPlanCartSnapshot(cart, {
295      emitRefresh: false,
296      syncSelectedPeriod: !selectedPlanPeriodSyncInProgress,
297    });
298  });
299
300  subscribeSelectedSearchPlanId(() => {
301    resolveSelectedPlanCartFromSnapshot({
302      syncSelectedPeriod: true,
303    });
304  });
305
306  subscribeSelectedSearchPlanPeriod(() => {
307    void syncSelectedPlanPeriodToCart();
308  });
309
310  hydratePromise = fetchCurrentCart()
311    .then((cart) => {
312      commitSearchPlanCartSnapshot(cart, {
313        syncSelectedPeriod: true,
314      });
315      return searchPlanCartSnapshot;
316    })
317    .catch((error) => {
318      console.error('Failed to hydrate selected plan cart state:', error);
319      commitSearchPlanCartSnapshot(createEmptyCartSnapshot(), {
320        syncSelectedPeriod: false,
321      });
322      return searchPlanCartSnapshot;
323    });
324
325  return hydratePromise;
326};
327
328if (typeof document !== 'undefined') {
329  initSearchPlanCartSync();
330}
331
332export {
333  initSearchPlanCartSync,
334  whenSearchPlanCartReady,
335  getSearchPlanCartSnapshot,
336  subscribeSearchPlanCartSnapshot,
337  getSelectedSearchPlanCart,
338  addSelectedSearchPlanToCart,
339};

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.