@shoppexio/storefront-react 0.1.5 → 0.2.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/dist/index.js CHANGED
@@ -5,19 +5,25 @@ import {
5
5
  ACCOUNT_STOREFRONT_ROUTES,
6
6
  ACCOUNT_TABS,
7
7
  DEFAULT_ACCOUNT_TAB,
8
+ FAVORITES_CHANGED_EVENT,
9
+ FavoritesProvider,
8
10
  IMPLEMENTED_ACCOUNT_TABS,
9
11
  SERVING_CONTEXT_HINT,
10
12
  accountOrderHref,
11
13
  accountTabHref,
12
14
  accountTabLabelKey,
13
15
  canonicalAccountPath,
16
+ createFavoritesStore,
14
17
  getBuyerCurrency,
15
18
  getShoppex,
16
19
  isAccountTab,
17
20
  isImplementedAccountTab,
21
+ notifyFavoritesChanged,
18
22
  parseAccountPath,
19
- resolveAccountView
20
- } from "./chunk-D6XKDVAV.js";
23
+ resolveAccountView,
24
+ sdkFavoritesTransport,
25
+ useFavorites
26
+ } from "./chunk-ZEFZPUAW.js";
21
27
 
22
28
  // src/account-route-messages.ts
23
29
  var ACCOUNT_ROUTE_MESSAGE_KEYS = [
@@ -283,6 +289,43 @@ function badgesForProductPage(badges) {
283
289
  return badges.filter((badge) => badge.showOnProductPage);
284
290
  }
285
291
 
292
+ // src/service-status.ts
293
+ var SERVICE_STATES = ["operational", "updating", "detected", "offline"];
294
+ var STATE_ORDER = { detected: 0, offline: 1, updating: 2, operational: 3 };
295
+ function parseServiceStatus(value, context) {
296
+ if (value === null || value === void 0) return null;
297
+ if (typeof value !== "object" || Array.isArray(value)) {
298
+ throw new Error(`Invalid Shoppex ${context}.service_status: expected an object or null.`);
299
+ }
300
+ const record = value;
301
+ const state = record.state;
302
+ if (typeof state !== "string" || !SERVICE_STATES.includes(state)) {
303
+ throw new Error(`Invalid Shoppex ${context}.service_status.state: expected one of ${SERVICE_STATES.join(", ")}.`);
304
+ }
305
+ const updatedAt = record.updated_at;
306
+ if (typeof updatedAt !== "string" || Number.isNaN(Date.parse(updatedAt))) {
307
+ throw new Error(`Invalid Shoppex ${context}.service_status.updated_at: expected an ISO timestamp.`);
308
+ }
309
+ const note = typeof record.note === "string" && record.note.trim() ? record.note.trim() : null;
310
+ return { state, note, updatedAt };
311
+ }
312
+ function buildStatusPageModel(products) {
313
+ const entries = [];
314
+ const seen = /* @__PURE__ */ new Set();
315
+ for (const product of products) {
316
+ if (product.serviceStatus === null || seen.has(product.id)) continue;
317
+ seen.add(product.id);
318
+ entries.push({ product, status: product.serviceStatus });
319
+ }
320
+ if (entries.length === 0) return null;
321
+ entries.sort((a, b) => STATE_ORDER[a.status.state] - STATE_ORDER[b.status.state]);
322
+ const counts = { operational: 0, updating: 0, detected: 0, offline: 0 };
323
+ for (const entry of entries) counts[entry.status.state] += 1;
324
+ const presentStates = [...SERVICE_STATES].sort((a, b) => STATE_ORDER[a] - STATE_ORDER[b]).filter((state) => counts[state] > 0);
325
+ const lastUpdatedAt = entries.map((entry) => entry.status.updatedAt).reduce((latest, value) => Date.parse(value) > Date.parse(latest) ? value : latest);
326
+ return { entries, counts, presentStates, lastUpdatedAt };
327
+ }
328
+
286
329
  // src/video-embed.ts
287
330
  var ALLOWED_VIDEO_EMBED_HOSTS = /* @__PURE__ */ new Set([
288
331
  "www.youtube-nocookie.com",
@@ -372,11 +415,30 @@ function resolveProductVideoEmbedSrc(value) {
372
415
  var PRODUCT_TYPES = ["PRODUCT", "SUBSCRIPTION", "INFO_CARD", "SERIALS", "PHYSICAL"];
373
416
  function resolveDeliveryPromise(product) {
374
417
  if (product.productType === "PHYSICAL") return { kind: "shipped" };
418
+ if (!product.deliveryTimeStated) return { kind: "unspecified" };
375
419
  if (product.deliveryTimeCount !== null) {
376
420
  return { kind: "timed", count: product.deliveryTimeCount, unit: product.deliveryTimeUnit };
377
421
  }
378
422
  return { kind: "instant" };
379
423
  }
424
+ var DELIVERABLES_BY_TYPE = {
425
+ SERIALS: "serials",
426
+ FILE: "file",
427
+ SERVICE: "service",
428
+ DYNAMIC: "dynamic",
429
+ INFO_CARD: "info_card",
430
+ SUBSCRIPTION: "subscription",
431
+ PHYSICAL: "physical"
432
+ };
433
+ function parseDeliverable(value) {
434
+ const normalized = typeof value === "string" ? value.trim().toUpperCase() : "";
435
+ return DELIVERABLES_BY_TYPE[normalized] ?? null;
436
+ }
437
+ function resolveYouReceive(product) {
438
+ if (product.deliverable === "serials") return { kind: "serials", unitLabel: product.unitLabelSingular };
439
+ if (product.deliverable === "file") return { kind: "download" };
440
+ return null;
441
+ }
380
442
  var SUBSCRIPTION_INTERVAL_UNITS = ["DAY", "WEEK", "MONTH", "YEAR"];
381
443
  var SUBSCRIPTION_INTERVAL_NOUNS = {
382
444
  DAY: "day",
@@ -405,6 +467,25 @@ function parseSubscription(product, productType) {
405
467
  trialDays: normalizePositiveDays(product.trial_period) ?? 0
406
468
  };
407
469
  }
470
+ var PAYMENT_METHOD_KINDS = ["card", "wallet", "crypto"];
471
+ function parsePaymentMethods(value) {
472
+ if (!Array.isArray(value)) {
473
+ throw new Error("Invalid Shoppex store: payment_methods must be an array.");
474
+ }
475
+ return value.map((entry, index) => {
476
+ const context = `store.payment_methods[${index}]`;
477
+ const record = requireUnknownRecord(entry, context);
478
+ const kind = record.kind;
479
+ if (typeof kind !== "string" || !PAYMENT_METHOD_KINDS.includes(kind)) {
480
+ throw new Error(`Invalid Shoppex ${context}: kind must be card, wallet or crypto.`);
481
+ }
482
+ return {
483
+ id: requireString(record, "id", context),
484
+ label: requireString(record, "label", context),
485
+ kind
486
+ };
487
+ });
488
+ }
408
489
  function hasInjectedStorefrontData() {
409
490
  return window.__SHOPPEX_INITIAL__ !== void 0;
410
491
  }
@@ -583,7 +664,7 @@ function getStorefrontSettings() {
583
664
  ]),
584
665
  defaultCurrency,
585
666
  activeCurrency,
586
- availableGateways: (Array.isArray(store.available_gateways) ? store.available_gateways : []).filter((entry) => typeof entry === "string" && entry.trim() !== ""),
667
+ paymentMethods: parsePaymentMethods(store.payment_methods),
587
668
  cartEnabled: optionalBoolean(store.cart_enabled, true, "store.cart_enabled"),
588
669
  hideOutOfStock: optionalBoolean(store.hide_out_of_stock, false, "store.hide_out_of_stock"),
589
670
  hideStockCounter: optionalBoolean(store.hide_stock_counter, false, "store.hide_stock_counter"),
@@ -608,6 +689,7 @@ function getStorefrontSettings() {
608
689
  // visitor only found out from the backend's refusal. A payload without them
609
690
  // is broken, so it fails here instead.
610
691
  contactFormEnabled: requireBoolean(store.contact_form_enabled, "store.contact_form_enabled"),
692
+ stockAlertsEnabled: requireBoolean(store.stock_alerts_enabled, "store.stock_alerts_enabled"),
611
693
  requireInvoiceForContact: requireBoolean(
612
694
  store.require_invoice_for_contact,
613
695
  "store.require_invoice_for_contact"
@@ -724,13 +806,19 @@ function parseProduct(value, index) {
724
806
  rating: rating?.average ?? null,
725
807
  ratingCount: rating?.count ?? 0,
726
808
  ratingTally: rating?.tally ?? null,
809
+ shortDescription: optionalString(product.short_description),
810
+ deliverable: parseDeliverable(product.type),
811
+ unitLabelSingular: optionalString(product.label_singular),
812
+ unitLabelPlural: optionalString(product.label_plural),
813
+ serviceStatus: parseServiceStatus(product.service_status, context),
727
814
  highlights: parseHighlights(product.product_highlights),
728
815
  metadata: parseMetadata(product.metadata),
729
816
  descriptionTabs: parseContentTabs(product.description_tabs),
730
817
  faqs: parseFaqs(product.faqs),
731
818
  license: parseProductLicense(product),
732
819
  licensingEnabled: product.licensing_enabled === true || product.licensing_enabled === 1,
733
- deliveryTimeCount: normalizePositiveDays(product.delivery_time),
820
+ deliveryTimeStated: Object.prototype.hasOwnProperty.call(product, "delivery_time") && product.delivery_time !== void 0,
821
+ deliveryTimeCount: normalizeDeliveryCount(product.delivery_time),
734
822
  deliveryTimeUnit: normalizeDeliveryTimeUnit(product.delivery_time_unit),
735
823
  subscription: parseSubscription(product, productType)
736
824
  };
@@ -796,6 +884,7 @@ function parsePriceVariants(value, productContext) {
796
884
  return {
797
885
  id: requireString(variant, "id", context),
798
886
  label: requireStringFromKeys(variant, ["label", "title"], context),
887
+ description: optionalString(variant.description),
799
888
  license: parseVariantLicense(variant),
800
889
  price: unitPrice,
801
890
  baseUnitPrice: unitPrice,
@@ -1105,6 +1194,11 @@ function normalizeDiscountPercent(value) {
1105
1194
  if (!Number.isFinite(percent) || percent <= 0) return 0;
1106
1195
  return Math.min(percent, 100);
1107
1196
  }
1197
+ function normalizeDeliveryCount(value) {
1198
+ if (value === null || value === void 0 || value === "") return null;
1199
+ const parsed = Number(value);
1200
+ return Number.isFinite(parsed) && parsed > 0 ? Math.ceil(parsed) : null;
1201
+ }
1108
1202
  function normalizePositiveDays(value) {
1109
1203
  if (value === null || value === void 0 || value === "") return null;
1110
1204
  const parsed = Number(value);
@@ -2743,6 +2837,53 @@ async function loadShopPage(slug) {
2743
2837
  }
2744
2838
  }
2745
2839
 
2840
+ // src/stock-alerts.ts
2841
+ function canRequestStockAlert(settings, product, soldOut) {
2842
+ return settings.stockAlertsEnabled && soldOut && !product.onHold && product.productType !== "INFO_CARD";
2843
+ }
2844
+ function bootstrapGlobal(name) {
2845
+ const value = window[name];
2846
+ if (typeof value !== "string" || !value.trim()) {
2847
+ throw new Error(
2848
+ `Shoppex bootstrap global "${name}" is missing. This storefront must be served by the Shoppex storefront (or preview) worker.`
2849
+ );
2850
+ }
2851
+ return value.trim().replace(/\/+$/, "");
2852
+ }
2853
+ function pageLocale() {
2854
+ const lang = typeof document === "undefined" ? "" : document.documentElement.lang.trim();
2855
+ return lang || null;
2856
+ }
2857
+ var GENERIC_FAILURE = "We could not save your request. Please try again.";
2858
+ async function requestStockAlert(request) {
2859
+ const email = request.email.trim();
2860
+ if (!email) return { ok: false, status: null, message: "Enter your email address." };
2861
+ let response;
2862
+ try {
2863
+ const shopSlug = bootstrapGlobal("shopSlug");
2864
+ const apiBaseUrl = bootstrapGlobal("apiBaseUrl");
2865
+ response = await fetch(
2866
+ `${apiBaseUrl}/v1/storefront/shops/name/${encodeURIComponent(shopSlug)}/products/${encodeURIComponent(request.productId)}/stock-alerts`,
2867
+ {
2868
+ method: "POST",
2869
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2870
+ body: JSON.stringify({
2871
+ email,
2872
+ variant_id: request.variantId,
2873
+ locale: pageLocale(),
2874
+ ...request.honeypot ? { website: request.honeypot } : {}
2875
+ })
2876
+ }
2877
+ );
2878
+ } catch {
2879
+ return { ok: false, status: null, message: GENERIC_FAILURE };
2880
+ }
2881
+ if (response.status === 202) return { ok: true };
2882
+ const payload = await response.json().catch(() => null);
2883
+ const serverMessage = typeof payload?.error === "string" && payload.error.trim() ? payload.error.trim() : typeof payload?.message === "string" && payload.message.trim() ? payload.message.trim() : null;
2884
+ return { ok: false, status: response.status, message: serverMessage ?? GENERIC_FAILURE };
2885
+ }
2886
+
2746
2887
  // src/storefront-context.tsx
2747
2888
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect4, useState as useState4 } from "react";
2748
2889
  import { jsx as jsx2 } from "react/jsx-runtime";
@@ -2789,10 +2930,13 @@ export {
2789
2930
  DEFAULT_ACCOUNT_TAB,
2790
2931
  DEFAULT_LOW_STOCK_THRESHOLD,
2791
2932
  DELIVERY_TIME_UNITS,
2933
+ FAVORITES_CHANGED_EVENT,
2934
+ FavoritesProvider,
2792
2935
  IMPLEMENTED_ACCOUNT_TABS,
2793
2936
  MAX_PAY_WHAT_YOU_WANT_PRICE,
2794
2937
  PRODUCT_BADGE_ICON_NAMES,
2795
2938
  QUOTE_UNAVAILABLE_CHECKOUT_ERROR,
2939
+ SERVICE_STATES,
2796
2940
  SERVING_CONTEXT_HINT,
2797
2941
  SHOP_REVIEW_PAGE_SIZE,
2798
2942
  STOREFRONT_SORT_KEYS,
@@ -2809,15 +2953,18 @@ export {
2809
2953
  buildCustomFieldPayload,
2810
2954
  buildInitialCheckoutFieldValues,
2811
2955
  buildProductDetail,
2956
+ buildStatusPageModel,
2812
2957
  buildStorefrontCatalog,
2813
2958
  calculateDisplayLineTotal,
2814
2959
  calculateLinePricing,
2960
+ canRequestStockAlert,
2815
2961
  canonicalAccountPath,
2816
2962
  checkoutFieldKey,
2817
2963
  checkoutFieldWriteRepriced,
2818
2964
  checkoutNeedsDetails,
2819
2965
  clampQuantity,
2820
2966
  clearRecoveredQuoteCheckoutError,
2967
+ createFavoritesStore,
2821
2968
  filterCatalogCards,
2822
2969
  findStorefrontProduct,
2823
2970
  firstOptionalString,
@@ -2851,6 +2998,7 @@ export {
2851
2998
  loadShopPage,
2852
2999
  loadShopReviewsPage,
2853
3000
  missingRequiredCheckoutValues,
3001
+ notifyFavoritesChanged,
2854
3002
  optionalBoolean,
2855
3003
  optionalString,
2856
3004
  optionalStringWithDefault,
@@ -2860,9 +3008,11 @@ export {
2860
3008
  parseEnum,
2861
3009
  parseFiniteNumber,
2862
3010
  parseProductBadges,
3011
+ parseServiceStatus,
2863
3012
  parseStorefrontSortKey,
2864
3013
  readCheckoutNeedsDetails,
2865
3014
  readStorefrontData,
3015
+ requestStockAlert,
2866
3016
  requireArray,
2867
3017
  requireBoolean,
2868
3018
  requireNumber,
@@ -2886,9 +3036,11 @@ export {
2886
3036
  resolveSupportChannels,
2887
3037
  resolveUnsellableLineIds,
2888
3038
  resolveVariantStock,
3039
+ resolveYouReceive,
2889
3040
  reviewsForProduct,
2890
3041
  roundCurrency,
2891
3042
  sanitizeMerchantHtml,
3043
+ sdkFavoritesTransport,
2892
3044
  sortCatalogCards,
2893
3045
  startCartMutation,
2894
3046
  stripMerchantHtmlImages,
@@ -2896,6 +3048,7 @@ export {
2896
3048
  toShopReviews,
2897
3049
  toStarCount,
2898
3050
  useCart,
3051
+ useFavorites,
2899
3052
  useNavigationLinks,
2900
3053
  useOnlineUserCount,
2901
3054
  usePagedShopReviews,