@numueg/theme-sdk 0.12.0 → 0.13.0

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.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  export { resolveThemeSettings } from './chunk-XF2FGIVS.mjs';
2
- export { KNOWN_SETTING_TYPES, KNOWN_TEMPLATES, REQUIRED_TEMPLATES, SDK_VERSION, THEME_CONTRACT_VERSION, mergeResults, validateBuiltManifest, validateManifest, validateSectionSchema, validateSettingsAgainstSchema } from './chunk-ZECKSIDL.mjs';
2
+ export { KNOWN_SETTING_TYPES, KNOWN_TEMPLATES, REQUIRED_TEMPLATES, SDK_VERSION, THEME_CONTRACT_VERSION, mergeResults, validateBuiltManifest, validateManifest, validateSectionSchema, validateSettingsAgainstSchema } from './chunk-QPXMBK5U.mjs';
3
3
  import { ShopContext, ThemeSettingsContext, CurrentTemplateContext, PageContext, LocalizationContext, CurrencyContext, CartContext, CustomerContext, NavigationContext, CollectionContext, ProductContext, useShop, useLocalization, useCustomer, useThemeSettings } from './chunk-ZYZZG4JR.mjs';
4
4
  export { CartContext, CollectionContext, CurrencyContext, CustomerContext, LocalizationContext, NavigationContext, PageContext, ProductContext, ShopContext, ThemeSettingsContext, useCollections, useCustomer, useDirection, useFieldTranslation, useLocale, useLocalization, useNumberFormat, usePage, useProducts, useShop, useThemeSettings, useTranslation } from './chunk-ZYZZG4JR.mjs';
5
5
  import { createContext, forwardRef, useState, useImperativeHandle, useEffect, useCallback, useMemo, useRef, useContext, useSyncExternalStore, StrictMode, createElement, Component } from 'react';
@@ -1022,6 +1022,133 @@ function useRelatedProducts(productId, options = {}) {
1022
1022
  });
1023
1023
  return { items: data ?? EMPTY4, loading: isLoading, error: error ?? null };
1024
1024
  }
1025
+ function useActivePromotions(page = "/", locale = "ar") {
1026
+ const key = `numu:promotions:${page}:${locale}`;
1027
+ const fetcher = useCallback(
1028
+ async (signal) => {
1029
+ const qs = new URLSearchParams({ page, locale });
1030
+ const res = await fetch(`/api/storefront/promotions?${qs.toString()}`, {
1031
+ credentials: "include",
1032
+ cache: "no-store",
1033
+ signal
1034
+ });
1035
+ if (!res.ok) return null;
1036
+ const json = await res.json();
1037
+ if (!json) return null;
1038
+ return (json.data ?? json) || null;
1039
+ },
1040
+ [page, locale]
1041
+ );
1042
+ const { data } = useCachedResource(
1043
+ key,
1044
+ fetcher,
1045
+ { initialData: null }
1046
+ );
1047
+ return data ?? null;
1048
+ }
1049
+
1050
+ // src/utils/money.ts
1051
+ function resolveLocale(locale) {
1052
+ return locale === "ar" ? "ar-EG" : "en-EG";
1053
+ }
1054
+ function formatMoney(cents, options = {}) {
1055
+ const { currency, locale, fractionDigits = 0 } = options;
1056
+ const safe = Number.isFinite(cents) ? cents : 0;
1057
+ return new Intl.NumberFormat(resolveLocale(locale), {
1058
+ style: "currency",
1059
+ currency: currency || "EGP",
1060
+ maximumFractionDigits: fractionDigits
1061
+ }).format(safe / 100);
1062
+ }
1063
+ function formatMoneyMajor(amount, options = {}) {
1064
+ return formatMoney(
1065
+ (Number.isFinite(amount) ? amount : 0) * 100,
1066
+ options
1067
+ );
1068
+ }
1069
+ function centsToMajor(cents) {
1070
+ return (Number.isFinite(cents) ? cents : 0) / 100;
1071
+ }
1072
+ function majorToCents(amount) {
1073
+ return Math.round((Number.isFinite(amount) ? amount : 0) * 100);
1074
+ }
1075
+
1076
+ // src/lib/promotions.ts
1077
+ function multibuyOffers(promotions) {
1078
+ if (!promotions) return [];
1079
+ const list = Array.isArray(promotions) ? promotions : promotions.auto_discounts ?? [];
1080
+ const offers = [];
1081
+ for (const promo of list) {
1082
+ const rule = promo?.discount_rule;
1083
+ if (!rule || rule.kind !== "multibuy") continue;
1084
+ const quantity = rule.multibuy_quantity;
1085
+ const groupPriceCents = rule.multibuy_price_cents;
1086
+ if (typeof quantity !== "number" || quantity < 2) continue;
1087
+ if (typeof groupPriceCents !== "number" || groupPriceCents <= 0) continue;
1088
+ const eligibleProductIds = promo.eligible_product_ids ?? [];
1089
+ const eligibleCategoryIds = promo.eligible_category_ids ?? [];
1090
+ offers.push({
1091
+ promotionId: promo.promotion_id,
1092
+ quantity,
1093
+ groupPriceCents,
1094
+ groupPriceMajor: centsToMajor(groupPriceCents),
1095
+ headline: promo.translated_content?.headline,
1096
+ eligibleProductIds,
1097
+ eligibleCategoryIds,
1098
+ // No scoping rows at all ⇒ every product qualifies. Note an older
1099
+ // backend that doesn't send these fields also lands here, which is the
1100
+ // right default: counting everything is what themes did before.
1101
+ isStoreWide: eligibleProductIds.length === 0 && eligibleCategoryIds.length === 0,
1102
+ raw: promo
1103
+ });
1104
+ }
1105
+ return offers;
1106
+ }
1107
+ function offerIncludesProduct(offer, product) {
1108
+ if (!offer) return false;
1109
+ if (offer.isStoreWide) return true;
1110
+ const id = product?.product_id ?? product?.id;
1111
+ if (id && offer.eligibleProductIds.includes(id)) return true;
1112
+ const category = product?.category_id;
1113
+ return Boolean(category && offer.eligibleCategoryIds.includes(category));
1114
+ }
1115
+ function eligibleUnitsInCart(offer, cart) {
1116
+ if (!offer) return 0;
1117
+ return (cart?.items ?? []).reduce((sum, item) => {
1118
+ if (!item) return sum;
1119
+ if (!offerIncludesProduct(offer, item)) return sum;
1120
+ return sum + (item.quantity || 0);
1121
+ }, 0);
1122
+ }
1123
+ function offerProgress(offer, cart, eligibleUnits) {
1124
+ const empty = {
1125
+ unitsInCart: 0,
1126
+ unitsNeeded: 0,
1127
+ groupsUnlocked: 0,
1128
+ savingMajor: 0
1129
+ };
1130
+ if (!offer) return empty;
1131
+ const unitsInCart = typeof eligibleUnits === "number" ? Math.max(0, eligibleUnits) : eligibleUnitsInCart(offer, cart);
1132
+ const groupsUnlocked = Math.floor(unitsInCart / offer.quantity);
1133
+ const remainder = unitsInCart % offer.quantity;
1134
+ const unitsNeeded = remainder === 0 ? 0 : offer.quantity - remainder;
1135
+ const applied = (cart?.applied_promotions ?? []).find(
1136
+ (p) => p && p.id === offer.promotionId
1137
+ );
1138
+ return {
1139
+ unitsInCart,
1140
+ unitsNeeded,
1141
+ groupsUnlocked,
1142
+ savingMajor: applied ? applied.amount || 0 : 0
1143
+ };
1144
+ }
1145
+ function offerBeatsRegularPrice(offer, unitPriceMajor) {
1146
+ if (!offer || typeof unitPriceMajor !== "number" || unitPriceMajor <= 0) {
1147
+ return false;
1148
+ }
1149
+ const unitCents = Math.round(unitPriceMajor * 100);
1150
+ return unitCents * offer.quantity > offer.groupPriceCents;
1151
+ }
1025
1152
  function useProductSizeChart(productOverride) {
1026
1153
  const ctxProduct = useProductOptional();
1027
1154
  const product = productOverride ?? ctxProduct;
@@ -1495,11 +1622,27 @@ var EMPTY_CART = {
1495
1622
  };
1496
1623
  function normalizeCartFromServer(cart) {
1497
1624
  const toMajor = (n) => typeof n === "number" ? n / 100 : 0;
1625
+ const { automatic_discount_cents: wireAutomaticCents, ...rest } = cart;
1498
1626
  return {
1499
- ...cart,
1627
+ ...rest,
1500
1628
  subtotal: toMajor(cart.subtotal),
1501
1629
  total: toMajor(cart.total),
1502
1630
  ...cart.discount_amount != null ? { discount_amount: toMajor(cart.discount_amount) } : {},
1631
+ // Offers-v2 automatic promotions. These MUST be converted here too —
1632
+ // they arrive in cents next to already-major totals, so forwarding them
1633
+ // raw makes every theme render a saving 100x too large. This function is
1634
+ // the one and only cents→major boundary for the cart.
1635
+ //
1636
+ // The backend field is `automatic_discount_cents`; themes get
1637
+ // `automatic_discount` (major), because a `_cents` name holding pounds
1638
+ // invites exactly the double-divide this conversion exists to prevent.
1639
+ ...wireAutomaticCents != null ? { automatic_discount: toMajor(wireAutomaticCents) } : {},
1640
+ ...Array.isArray(cart.applied_promotions) ? {
1641
+ applied_promotions: cart.applied_promotions.map((p) => ({
1642
+ ...p,
1643
+ amount: toMajor(p.amount)
1644
+ }))
1645
+ } : {},
1503
1646
  items: Array.isArray(cart.items) ? cart.items.map((it) => {
1504
1647
  const raw = it;
1505
1648
  return {
@@ -3850,32 +3993,6 @@ function assetUrl(name) {
3850
3993
  return `${cleanBase}${filename}`;
3851
3994
  }
3852
3995
 
3853
- // src/utils/money.ts
3854
- function resolveLocale(locale) {
3855
- return locale === "ar" ? "ar-EG" : "en-EG";
3856
- }
3857
- function formatMoney(cents, options = {}) {
3858
- const { currency, locale, fractionDigits = 0 } = options;
3859
- const safe = Number.isFinite(cents) ? cents : 0;
3860
- return new Intl.NumberFormat(resolveLocale(locale), {
3861
- style: "currency",
3862
- currency: currency || "EGP",
3863
- maximumFractionDigits: fractionDigits
3864
- }).format(safe / 100);
3865
- }
3866
- function formatMoneyMajor(amount, options = {}) {
3867
- return formatMoney(
3868
- (Number.isFinite(amount) ? amount : 0) * 100,
3869
- options
3870
- );
3871
- }
3872
- function centsToMajor(cents) {
3873
- return (Number.isFinite(cents) ? cents : 0) / 100;
3874
- }
3875
- function majorToCents(amount) {
3876
- return Math.round((Number.isFinite(amount) ? amount : 0) * 100);
3877
- }
3878
-
3879
3996
  // src/utils/templates.ts
3880
3997
  function resolveSections(group) {
3881
3998
  if (!group) return [];
@@ -3964,6 +4081,6 @@ function buildLocaleBundle(modules) {
3964
4081
  return bundle;
3965
4082
  }
3966
4083
 
3967
- export { AddToCartButton, Block, CollectionCard, CollectionProvider, CurrencySwitcher, EditableImage, EditableText, Form, HeroMedia, ICON_NAMES, Icon, IconMap, Image, LOGO_SHAPE_OPTIONS, LOGO_SIZE_OPTIONS, Link, LocaleSwitcher, Logo, MAX_BLOCK_DEPTH, Money, NAVIGATE_EVENT, NuMuProvider, ProductCard, ProductProvider, RichText, Section, SectionContext, applyGlobalStyleTokens, applyImageTransform, asImageTransform, assetUrl, availableValues, buildLocaleBundle, buildThemeElement, centsToMajor, clearSdkSingleton, collectBlocks, collectSections, collectionHref, computeGlobalStyleTokens, defaultVariant, defineBlock, defineSection, defineThemeEntry, dynamicSource, findVariantByOptions, flattenMessages, focalSrc, formatMoney, formatMoneyMajor, getReactSingleton, getSdkSingleton, isDefinedBlock, isDefinedSection, isDynamicSource, isSdkAvailable, logoImgStyle, logoStyleTokens, majorToCents, mountTheme, pickTranslations, productHref, publishVariantSelection, readVariantSelection, registerReactSingleton, registerSdkSingleton, requestNavigate, resolveDynamicValue, resolveFontStack, resolveSections, resolveSettingsMap, resolveSizeChart, resolveSourcePath, sanitizeHtml, selectChromeSections, selectTemplateSections, useAnalytics, useApp, useArticle, useArticles, useBlog, useBlogs, useCachedResource, useCart, useCheckout, useCollection, useCollectionOptional, useCurrency, useCurrentTemplate, useCustomerActions, useCustomerAddresses, useGiftCardBalance, useImage, useListingHeading, useMetafield, useMetafields, useMoney, useNavigation, useOrder, useOrders, useProduct, useProductOptional, useProductSizeChart, useRelatedProducts, useReorder, useResolvedSettings, useSearch, useSection, useSectionGroup, useSectionOptional, useShippingRates, useVariantSelection, useWishlist };
4084
+ export { AddToCartButton, Block, CollectionCard, CollectionProvider, CurrencySwitcher, EditableImage, EditableText, Form, HeroMedia, ICON_NAMES, Icon, IconMap, Image, LOGO_SHAPE_OPTIONS, LOGO_SIZE_OPTIONS, Link, LocaleSwitcher, Logo, MAX_BLOCK_DEPTH, Money, NAVIGATE_EVENT, NuMuProvider, ProductCard, ProductProvider, RichText, Section, SectionContext, applyGlobalStyleTokens, applyImageTransform, asImageTransform, assetUrl, availableValues, buildLocaleBundle, buildThemeElement, centsToMajor, clearSdkSingleton, collectBlocks, collectSections, collectionHref, computeGlobalStyleTokens, defaultVariant, defineBlock, defineSection, defineThemeEntry, dynamicSource, eligibleUnitsInCart, findVariantByOptions, flattenMessages, focalSrc, formatMoney, formatMoneyMajor, getReactSingleton, getSdkSingleton, isDefinedBlock, isDefinedSection, isDynamicSource, isSdkAvailable, logoImgStyle, logoStyleTokens, majorToCents, mountTheme, multibuyOffers, offerBeatsRegularPrice, offerIncludesProduct, offerProgress, pickTranslations, productHref, publishVariantSelection, readVariantSelection, registerReactSingleton, registerSdkSingleton, requestNavigate, resolveDynamicValue, resolveFontStack, resolveSections, resolveSettingsMap, resolveSizeChart, resolveSourcePath, sanitizeHtml, selectChromeSections, selectTemplateSections, useActivePromotions, useAnalytics, useApp, useArticle, useArticles, useBlog, useBlogs, useCachedResource, useCart, useCheckout, useCollection, useCollectionOptional, useCurrency, useCurrentTemplate, useCustomerActions, useCustomerAddresses, useGiftCardBalance, useImage, useListingHeading, useMetafield, useMetafields, useMoney, useNavigation, useOrder, useOrders, useProduct, useProductOptional, useProductSizeChart, useRelatedProducts, useReorder, useResolvedSettings, useSearch, useSection, useSectionGroup, useSectionOptional, useShippingRates, useVariantSelection, useWishlist };
3968
4085
  //# sourceMappingURL=index.mjs.map
3969
4086
  //# sourceMappingURL=index.mjs.map