@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.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkTBSNHHFH_cjs = require('./chunk-TBSNHHFH.cjs');
4
- var chunk7RZISLP2_cjs = require('./chunk-7RZISLP2.cjs');
4
+ var chunkH7RGDWSU_cjs = require('./chunk-H7RGDWSU.cjs');
5
5
  var chunkV3JDQXD3_cjs = require('./chunk-V3JDQXD3.cjs');
6
6
  var react = require('react');
7
7
  var client = require('react-dom/client');
@@ -1023,6 +1023,133 @@ function useRelatedProducts(productId, options = {}) {
1023
1023
  });
1024
1024
  return { items: data ?? EMPTY4, loading: isLoading, error: error ?? null };
1025
1025
  }
1026
+ function useActivePromotions(page = "/", locale = "ar") {
1027
+ const key = `numu:promotions:${page}:${locale}`;
1028
+ const fetcher = react.useCallback(
1029
+ async (signal) => {
1030
+ const qs = new URLSearchParams({ page, locale });
1031
+ const res = await fetch(`/api/storefront/promotions?${qs.toString()}`, {
1032
+ credentials: "include",
1033
+ cache: "no-store",
1034
+ signal
1035
+ });
1036
+ if (!res.ok) return null;
1037
+ const json = await res.json();
1038
+ if (!json) return null;
1039
+ return (json.data ?? json) || null;
1040
+ },
1041
+ [page, locale]
1042
+ );
1043
+ const { data } = useCachedResource(
1044
+ key,
1045
+ fetcher,
1046
+ { initialData: null }
1047
+ );
1048
+ return data ?? null;
1049
+ }
1050
+
1051
+ // src/utils/money.ts
1052
+ function resolveLocale(locale) {
1053
+ return locale === "ar" ? "ar-EG" : "en-EG";
1054
+ }
1055
+ function formatMoney(cents, options = {}) {
1056
+ const { currency, locale, fractionDigits = 0 } = options;
1057
+ const safe = Number.isFinite(cents) ? cents : 0;
1058
+ return new Intl.NumberFormat(resolveLocale(locale), {
1059
+ style: "currency",
1060
+ currency: currency || "EGP",
1061
+ maximumFractionDigits: fractionDigits
1062
+ }).format(safe / 100);
1063
+ }
1064
+ function formatMoneyMajor(amount, options = {}) {
1065
+ return formatMoney(
1066
+ (Number.isFinite(amount) ? amount : 0) * 100,
1067
+ options
1068
+ );
1069
+ }
1070
+ function centsToMajor(cents) {
1071
+ return (Number.isFinite(cents) ? cents : 0) / 100;
1072
+ }
1073
+ function majorToCents(amount) {
1074
+ return Math.round((Number.isFinite(amount) ? amount : 0) * 100);
1075
+ }
1076
+
1077
+ // src/lib/promotions.ts
1078
+ function multibuyOffers(promotions) {
1079
+ if (!promotions) return [];
1080
+ const list = Array.isArray(promotions) ? promotions : promotions.auto_discounts ?? [];
1081
+ const offers = [];
1082
+ for (const promo of list) {
1083
+ const rule = promo?.discount_rule;
1084
+ if (!rule || rule.kind !== "multibuy") continue;
1085
+ const quantity = rule.multibuy_quantity;
1086
+ const groupPriceCents = rule.multibuy_price_cents;
1087
+ if (typeof quantity !== "number" || quantity < 2) continue;
1088
+ if (typeof groupPriceCents !== "number" || groupPriceCents <= 0) continue;
1089
+ const eligibleProductIds = promo.eligible_product_ids ?? [];
1090
+ const eligibleCategoryIds = promo.eligible_category_ids ?? [];
1091
+ offers.push({
1092
+ promotionId: promo.promotion_id,
1093
+ quantity,
1094
+ groupPriceCents,
1095
+ groupPriceMajor: centsToMajor(groupPriceCents),
1096
+ headline: promo.translated_content?.headline,
1097
+ eligibleProductIds,
1098
+ eligibleCategoryIds,
1099
+ // No scoping rows at all ⇒ every product qualifies. Note an older
1100
+ // backend that doesn't send these fields also lands here, which is the
1101
+ // right default: counting everything is what themes did before.
1102
+ isStoreWide: eligibleProductIds.length === 0 && eligibleCategoryIds.length === 0,
1103
+ raw: promo
1104
+ });
1105
+ }
1106
+ return offers;
1107
+ }
1108
+ function offerIncludesProduct(offer, product) {
1109
+ if (!offer) return false;
1110
+ if (offer.isStoreWide) return true;
1111
+ const id = product?.product_id ?? product?.id;
1112
+ if (id && offer.eligibleProductIds.includes(id)) return true;
1113
+ const category = product?.category_id;
1114
+ return Boolean(category && offer.eligibleCategoryIds.includes(category));
1115
+ }
1116
+ function eligibleUnitsInCart(offer, cart) {
1117
+ if (!offer) return 0;
1118
+ return (cart?.items ?? []).reduce((sum, item) => {
1119
+ if (!item) return sum;
1120
+ if (!offerIncludesProduct(offer, item)) return sum;
1121
+ return sum + (item.quantity || 0);
1122
+ }, 0);
1123
+ }
1124
+ function offerProgress(offer, cart, eligibleUnits) {
1125
+ const empty = {
1126
+ unitsInCart: 0,
1127
+ unitsNeeded: 0,
1128
+ groupsUnlocked: 0,
1129
+ savingMajor: 0
1130
+ };
1131
+ if (!offer) return empty;
1132
+ const unitsInCart = typeof eligibleUnits === "number" ? Math.max(0, eligibleUnits) : eligibleUnitsInCart(offer, cart);
1133
+ const groupsUnlocked = Math.floor(unitsInCart / offer.quantity);
1134
+ const remainder = unitsInCart % offer.quantity;
1135
+ const unitsNeeded = remainder === 0 ? 0 : offer.quantity - remainder;
1136
+ const applied = (cart?.applied_promotions ?? []).find(
1137
+ (p) => p && p.id === offer.promotionId
1138
+ );
1139
+ return {
1140
+ unitsInCart,
1141
+ unitsNeeded,
1142
+ groupsUnlocked,
1143
+ savingMajor: applied ? applied.amount || 0 : 0
1144
+ };
1145
+ }
1146
+ function offerBeatsRegularPrice(offer, unitPriceMajor) {
1147
+ if (!offer || typeof unitPriceMajor !== "number" || unitPriceMajor <= 0) {
1148
+ return false;
1149
+ }
1150
+ const unitCents = Math.round(unitPriceMajor * 100);
1151
+ return unitCents * offer.quantity > offer.groupPriceCents;
1152
+ }
1026
1153
  function useProductSizeChart(productOverride) {
1027
1154
  const ctxProduct = useProductOptional();
1028
1155
  const product = productOverride ?? ctxProduct;
@@ -1496,11 +1623,27 @@ var EMPTY_CART = {
1496
1623
  };
1497
1624
  function normalizeCartFromServer(cart) {
1498
1625
  const toMajor = (n) => typeof n === "number" ? n / 100 : 0;
1626
+ const { automatic_discount_cents: wireAutomaticCents, ...rest } = cart;
1499
1627
  return {
1500
- ...cart,
1628
+ ...rest,
1501
1629
  subtotal: toMajor(cart.subtotal),
1502
1630
  total: toMajor(cart.total),
1503
1631
  ...cart.discount_amount != null ? { discount_amount: toMajor(cart.discount_amount) } : {},
1632
+ // Offers-v2 automatic promotions. These MUST be converted here too —
1633
+ // they arrive in cents next to already-major totals, so forwarding them
1634
+ // raw makes every theme render a saving 100x too large. This function is
1635
+ // the one and only cents→major boundary for the cart.
1636
+ //
1637
+ // The backend field is `automatic_discount_cents`; themes get
1638
+ // `automatic_discount` (major), because a `_cents` name holding pounds
1639
+ // invites exactly the double-divide this conversion exists to prevent.
1640
+ ...wireAutomaticCents != null ? { automatic_discount: toMajor(wireAutomaticCents) } : {},
1641
+ ...Array.isArray(cart.applied_promotions) ? {
1642
+ applied_promotions: cart.applied_promotions.map((p) => ({
1643
+ ...p,
1644
+ amount: toMajor(p.amount)
1645
+ }))
1646
+ } : {},
1504
1647
  items: Array.isArray(cart.items) ? cart.items.map((it) => {
1505
1648
  const raw = it;
1506
1649
  return {
@@ -3851,32 +3994,6 @@ function assetUrl(name) {
3851
3994
  return `${cleanBase}${filename}`;
3852
3995
  }
3853
3996
 
3854
- // src/utils/money.ts
3855
- function resolveLocale(locale) {
3856
- return locale === "ar" ? "ar-EG" : "en-EG";
3857
- }
3858
- function formatMoney(cents, options = {}) {
3859
- const { currency, locale, fractionDigits = 0 } = options;
3860
- const safe = Number.isFinite(cents) ? cents : 0;
3861
- return new Intl.NumberFormat(resolveLocale(locale), {
3862
- style: "currency",
3863
- currency: currency || "EGP",
3864
- maximumFractionDigits: fractionDigits
3865
- }).format(safe / 100);
3866
- }
3867
- function formatMoneyMajor(amount, options = {}) {
3868
- return formatMoney(
3869
- (Number.isFinite(amount) ? amount : 0) * 100,
3870
- options
3871
- );
3872
- }
3873
- function centsToMajor(cents) {
3874
- return (Number.isFinite(cents) ? cents : 0) / 100;
3875
- }
3876
- function majorToCents(amount) {
3877
- return Math.round((Number.isFinite(amount) ? amount : 0) * 100);
3878
- }
3879
-
3880
3997
  // src/utils/templates.ts
3881
3998
  function resolveSections(group) {
3882
3999
  if (!group) return [];
@@ -3971,43 +4088,43 @@ Object.defineProperty(exports, "resolveThemeSettings", {
3971
4088
  });
3972
4089
  Object.defineProperty(exports, "KNOWN_SETTING_TYPES", {
3973
4090
  enumerable: true,
3974
- get: function () { return chunk7RZISLP2_cjs.KNOWN_SETTING_TYPES; }
4091
+ get: function () { return chunkH7RGDWSU_cjs.KNOWN_SETTING_TYPES; }
3975
4092
  });
3976
4093
  Object.defineProperty(exports, "KNOWN_TEMPLATES", {
3977
4094
  enumerable: true,
3978
- get: function () { return chunk7RZISLP2_cjs.KNOWN_TEMPLATES; }
4095
+ get: function () { return chunkH7RGDWSU_cjs.KNOWN_TEMPLATES; }
3979
4096
  });
3980
4097
  Object.defineProperty(exports, "REQUIRED_TEMPLATES", {
3981
4098
  enumerable: true,
3982
- get: function () { return chunk7RZISLP2_cjs.REQUIRED_TEMPLATES; }
4099
+ get: function () { return chunkH7RGDWSU_cjs.REQUIRED_TEMPLATES; }
3983
4100
  });
3984
4101
  Object.defineProperty(exports, "SDK_VERSION", {
3985
4102
  enumerable: true,
3986
- get: function () { return chunk7RZISLP2_cjs.SDK_VERSION; }
4103
+ get: function () { return chunkH7RGDWSU_cjs.SDK_VERSION; }
3987
4104
  });
3988
4105
  Object.defineProperty(exports, "THEME_CONTRACT_VERSION", {
3989
4106
  enumerable: true,
3990
- get: function () { return chunk7RZISLP2_cjs.THEME_CONTRACT_VERSION; }
4107
+ get: function () { return chunkH7RGDWSU_cjs.THEME_CONTRACT_VERSION; }
3991
4108
  });
3992
4109
  Object.defineProperty(exports, "mergeResults", {
3993
4110
  enumerable: true,
3994
- get: function () { return chunk7RZISLP2_cjs.mergeResults; }
4111
+ get: function () { return chunkH7RGDWSU_cjs.mergeResults; }
3995
4112
  });
3996
4113
  Object.defineProperty(exports, "validateBuiltManifest", {
3997
4114
  enumerable: true,
3998
- get: function () { return chunk7RZISLP2_cjs.validateBuiltManifest; }
4115
+ get: function () { return chunkH7RGDWSU_cjs.validateBuiltManifest; }
3999
4116
  });
4000
4117
  Object.defineProperty(exports, "validateManifest", {
4001
4118
  enumerable: true,
4002
- get: function () { return chunk7RZISLP2_cjs.validateManifest; }
4119
+ get: function () { return chunkH7RGDWSU_cjs.validateManifest; }
4003
4120
  });
4004
4121
  Object.defineProperty(exports, "validateSectionSchema", {
4005
4122
  enumerable: true,
4006
- get: function () { return chunk7RZISLP2_cjs.validateSectionSchema; }
4123
+ get: function () { return chunkH7RGDWSU_cjs.validateSectionSchema; }
4007
4124
  });
4008
4125
  Object.defineProperty(exports, "validateSettingsAgainstSchema", {
4009
4126
  enumerable: true,
4010
- get: function () { return chunk7RZISLP2_cjs.validateSettingsAgainstSchema; }
4127
+ get: function () { return chunkH7RGDWSU_cjs.validateSettingsAgainstSchema; }
4011
4128
  });
4012
4129
  Object.defineProperty(exports, "CartContext", {
4013
4130
  enumerable: true,
@@ -4142,6 +4259,7 @@ exports.defineBlock = defineBlock;
4142
4259
  exports.defineSection = defineSection;
4143
4260
  exports.defineThemeEntry = defineThemeEntry;
4144
4261
  exports.dynamicSource = dynamicSource;
4262
+ exports.eligibleUnitsInCart = eligibleUnitsInCart;
4145
4263
  exports.findVariantByOptions = findVariantByOptions;
4146
4264
  exports.flattenMessages = flattenMessages;
4147
4265
  exports.focalSrc = focalSrc;
@@ -4157,6 +4275,10 @@ exports.logoImgStyle = logoImgStyle;
4157
4275
  exports.logoStyleTokens = logoStyleTokens;
4158
4276
  exports.majorToCents = majorToCents;
4159
4277
  exports.mountTheme = mountTheme;
4278
+ exports.multibuyOffers = multibuyOffers;
4279
+ exports.offerBeatsRegularPrice = offerBeatsRegularPrice;
4280
+ exports.offerIncludesProduct = offerIncludesProduct;
4281
+ exports.offerProgress = offerProgress;
4160
4282
  exports.pickTranslations = pickTranslations;
4161
4283
  exports.productHref = productHref;
4162
4284
  exports.publishVariantSelection = publishVariantSelection;
@@ -4173,6 +4295,7 @@ exports.resolveSourcePath = resolveSourcePath;
4173
4295
  exports.sanitizeHtml = sanitizeHtml;
4174
4296
  exports.selectChromeSections = selectChromeSections;
4175
4297
  exports.selectTemplateSections = selectTemplateSections;
4298
+ exports.useActivePromotions = useActivePromotions;
4176
4299
  exports.useAnalytics = useAnalytics;
4177
4300
  exports.useApp = useApp;
4178
4301
  exports.useArticle = useArticle;