@instockng/storefront-ui 1.0.92 → 1.0.94

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.
Files changed (298) hide show
  1. package/dist/components/AddOnsDiscountScreen.d.ts +10 -0
  2. package/dist/components/AddOnsDiscountScreen.d.ts.map +1 -0
  3. package/dist/components/FreeShippingProgress.d.ts +2 -0
  4. package/dist/components/FreeShippingProgress.d.ts.map +1 -0
  5. package/dist/components/ShoppingCart.d.ts.map +1 -1
  6. package/dist/components/VariantPickerModal.d.ts +8 -1
  7. package/dist/components/VariantPickerModal.d.ts.map +1 -1
  8. package/dist/contexts/CartContext.d.ts +1 -1
  9. package/dist/contexts/CartContext.d.ts.map +1 -1
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.mjs +119 -115
  13. package/dist/index10.mjs +3 -3
  14. package/dist/index100.mjs +74 -3
  15. package/dist/index101.mjs +4 -2
  16. package/dist/index102.mjs +2 -83
  17. package/dist/index103.mjs +81 -52
  18. package/dist/index104.mjs +53 -5
  19. package/dist/index105.mjs +5 -4
  20. package/dist/index106.mjs +4 -178
  21. package/dist/index107.mjs +174 -48
  22. package/dist/index108.mjs +51 -67
  23. package/dist/index109.mjs +69 -2
  24. package/dist/index11.mjs +5 -5
  25. package/dist/index110.mjs +2 -28
  26. package/dist/index111.mjs +2 -18
  27. package/dist/index112.mjs +26 -213
  28. package/dist/index113.mjs +13 -175
  29. package/dist/index114.mjs +210 -17
  30. package/dist/index115.mjs +174 -17
  31. package/dist/index116.mjs +14 -25
  32. package/dist/index117.mjs +17 -150
  33. package/dist/index118.mjs +26 -13
  34. package/dist/index119.mjs +149 -24
  35. package/dist/index12.mjs +4 -4
  36. package/dist/index120.mjs +13 -77
  37. package/dist/index121.mjs +20 -27
  38. package/dist/index122.mjs +76 -137
  39. package/dist/index123.mjs +33 -50
  40. package/dist/index124.mjs +141 -19
  41. package/dist/index125.mjs +50 -22
  42. package/dist/index126.mjs +14 -14
  43. package/dist/index127.mjs +20 -18
  44. package/dist/index128.mjs +16 -14
  45. package/dist/index129.mjs +17 -14
  46. package/dist/index13.mjs +59 -51
  47. package/dist/index130.mjs +12 -12
  48. package/dist/index131.mjs +15 -58
  49. package/dist/index132.mjs +14 -11
  50. package/dist/index133.mjs +56 -30
  51. package/dist/index134.mjs +9 -15
  52. package/dist/index135.mjs +31 -26
  53. package/dist/index136.mjs +13 -16
  54. package/dist/index137.mjs +26 -11
  55. package/dist/index138.mjs +17 -11
  56. package/dist/index139.mjs +12 -40
  57. package/dist/index14.mjs +1 -1
  58. package/dist/index140.mjs +14 -16
  59. package/dist/index141.mjs +42 -263
  60. package/dist/index142.mjs +15 -62
  61. package/dist/index143.mjs +267 -7
  62. package/dist/index144.mjs +70 -2
  63. package/dist/index145.mjs +8 -2
  64. package/dist/index146.mjs +2 -33
  65. package/dist/index147.mjs +2 -2
  66. package/dist/index148.mjs +32 -20
  67. package/dist/index149.mjs +2 -56
  68. package/dist/index15.mjs +1 -1
  69. package/dist/index150.mjs +18 -26
  70. package/dist/index151.mjs +55 -6
  71. package/dist/index152.mjs +28 -51
  72. package/dist/index153.mjs +7 -6
  73. package/dist/index154.mjs +51 -11
  74. package/dist/index155.mjs +5 -6
  75. package/dist/index156.mjs +12 -28
  76. package/dist/index157.mjs +7 -2
  77. package/dist/index158.mjs +27 -69
  78. package/dist/index159.mjs +2 -167
  79. package/dist/index16.mjs +118 -131
  80. package/dist/index160.mjs +70 -2
  81. package/dist/index161.mjs +167 -2
  82. package/dist/index162.mjs +2 -2
  83. package/dist/index163.mjs +2 -18
  84. package/dist/index164.mjs +2 -32
  85. package/dist/index165.mjs +14 -34
  86. package/dist/index166.mjs +25 -11
  87. package/dist/index167.mjs +34 -14
  88. package/dist/index168.mjs +18 -2
  89. package/dist/index169.mjs +11 -13
  90. package/dist/index17.mjs +4 -4
  91. package/dist/index170.mjs +2 -46
  92. package/dist/index171.mjs +20 -2
  93. package/dist/index172.mjs +41 -25
  94. package/dist/index173.mjs +2 -18
  95. package/dist/index174.mjs +30 -2
  96. package/dist/index175.mjs +18 -2
  97. package/dist/index176.mjs +2 -2
  98. package/dist/index177.mjs +2 -2
  99. package/dist/index178.mjs +2 -72
  100. package/dist/index179.mjs +2 -2
  101. package/dist/index18.mjs +151 -86
  102. package/dist/index180.mjs +59 -40
  103. package/dist/index182.mjs +48 -31
  104. package/dist/index183.mjs +2 -152
  105. package/dist/index184.mjs +36 -2
  106. package/dist/index185.mjs +152 -2
  107. package/dist/index186.mjs +2 -19
  108. package/dist/index187.mjs +2 -2
  109. package/dist/index188.mjs +19 -2
  110. package/dist/index189.mjs +2 -26
  111. package/dist/index19.mjs +82 -733
  112. package/dist/index190.mjs +2 -2
  113. package/dist/index191.mjs +26 -2
  114. package/dist/index192.mjs +2 -24
  115. package/dist/index193.mjs +2 -23
  116. package/dist/index194.mjs +24 -2
  117. package/dist/index195.mjs +23 -2
  118. package/dist/index196.mjs +2 -2
  119. package/dist/index197.mjs +2 -23
  120. package/dist/index199.mjs +16 -16
  121. package/dist/index2.mjs +2 -2
  122. package/dist/index20.mjs +742 -53
  123. package/dist/index200.mjs +2 -2
  124. package/dist/index201.mjs +23 -2
  125. package/dist/index202.mjs +2 -2
  126. package/dist/index203.mjs +2 -23
  127. package/dist/index204.mjs +2 -2
  128. package/dist/index205.mjs +15 -15
  129. package/dist/index207.mjs +23 -2
  130. package/dist/index208.mjs +2 -2
  131. package/dist/index209.mjs +2 -2
  132. package/dist/index21.mjs +58 -60
  133. package/dist/index210.mjs +2 -2
  134. package/dist/index212.mjs +2 -2
  135. package/dist/index214.mjs +2 -127
  136. package/dist/index215.mjs +2 -2
  137. package/dist/index216.mjs +123 -70
  138. package/dist/index217.mjs +2 -74
  139. package/dist/index218.mjs +74 -2
  140. package/dist/index219.mjs +74 -2
  141. package/dist/index22.mjs +60 -22
  142. package/dist/index220.mjs +2 -108
  143. package/dist/index221.mjs +31 -2
  144. package/dist/index222.mjs +11 -2
  145. package/dist/index223.mjs +4 -2
  146. package/dist/index224.mjs +4 -2
  147. package/dist/index225.mjs +13 -2
  148. package/dist/index226.mjs +7 -37
  149. package/dist/index227.mjs +12 -2
  150. package/dist/index228.mjs +4 -243
  151. package/dist/index229.mjs +33 -2
  152. package/dist/index23.mjs +22 -107
  153. package/dist/index230.mjs +30 -32
  154. package/dist/index231.mjs +27 -64
  155. package/dist/index232.mjs +59 -23
  156. package/dist/index233.mjs +2 -2
  157. package/dist/index234.mjs +2 -2
  158. package/dist/index235.mjs +2 -2
  159. package/dist/index236.mjs +2 -2
  160. package/dist/index237.mjs +2 -2
  161. package/dist/index238.mjs +2 -2
  162. package/dist/index24.mjs +105 -38
  163. package/dist/index240.mjs +2 -2
  164. package/dist/index241.mjs +108 -2
  165. package/dist/index242.mjs +2 -2
  166. package/dist/index244.mjs +37 -2
  167. package/dist/index246.mjs +2 -2
  168. package/dist/index247.mjs +244 -2
  169. package/dist/index248.mjs +2 -4
  170. package/dist/index249.mjs +33 -2
  171. package/dist/index25.mjs +37 -37
  172. package/dist/index250.mjs +64 -30
  173. package/dist/index251.mjs +24 -10
  174. package/dist/index252.mjs +2 -4
  175. package/dist/index253.mjs +2 -4
  176. package/dist/index254.mjs +2 -13
  177. package/dist/index255.mjs +2 -7
  178. package/dist/index256.mjs +2 -12
  179. package/dist/index257.mjs +2 -5
  180. package/dist/index258.mjs +2 -33
  181. package/dist/index259.mjs +2 -31
  182. package/dist/index26.mjs +42 -89
  183. package/dist/index260.mjs +2 -28
  184. package/dist/index261.mjs +4 -61
  185. package/dist/index262.mjs +2 -2
  186. package/dist/index263.mjs +2 -2
  187. package/dist/index265.mjs +1 -1
  188. package/dist/index266.mjs +1 -1
  189. package/dist/index267.mjs +2 -2
  190. package/dist/index269.mjs +2 -2
  191. package/dist/index27.mjs +86 -29
  192. package/dist/index270.mjs +2 -91
  193. package/dist/index271.mjs +2 -2
  194. package/dist/index272.mjs +91 -3
  195. package/dist/index273.mjs +2 -2
  196. package/dist/index274.mjs +3 -2
  197. package/dist/index275.mjs +2 -17
  198. package/dist/index276.mjs +2 -13
  199. package/dist/index277.mjs +17 -6
  200. package/dist/index278.mjs +13 -30
  201. package/dist/index279.mjs +6 -2
  202. package/dist/index28.mjs +29 -6
  203. package/dist/index280.mjs +30 -2
  204. package/dist/index281.mjs +2 -2
  205. package/dist/index282.mjs +5 -0
  206. package/dist/index283.mjs +5 -0
  207. package/dist/index29.mjs +8 -21
  208. package/dist/index3.mjs +40 -38
  209. package/dist/index30.mjs +19 -35
  210. package/dist/index31.mjs +35 -37
  211. package/dist/index32.mjs +28 -103
  212. package/dist/index33.mjs +107 -32
  213. package/dist/index34.mjs +47 -9
  214. package/dist/index35.mjs +9 -9
  215. package/dist/index36.mjs +11 -121
  216. package/dist/index37.mjs +123 -27
  217. package/dist/index38.mjs +28 -90
  218. package/dist/index39.mjs +91 -112
  219. package/dist/index4.mjs +1 -1
  220. package/dist/index40.mjs +118 -8
  221. package/dist/index41.mjs +9 -33
  222. package/dist/index42.mjs +23 -34
  223. package/dist/index43.mjs +42 -7
  224. package/dist/index44.mjs +10 -122
  225. package/dist/index45.mjs +114 -380
  226. package/dist/index46.mjs +384 -20
  227. package/dist/index47.mjs +24 -31
  228. package/dist/index48.mjs +32 -7
  229. package/dist/index49.mjs +27 -1432
  230. package/dist/index5.mjs +1 -1
  231. package/dist/index50.mjs +6 -69
  232. package/dist/index51.mjs +1433 -2
  233. package/dist/index52.mjs +68 -58
  234. package/dist/index53.mjs +2 -51
  235. package/dist/index54.mjs +60 -33
  236. package/dist/index55.mjs +49 -13
  237. package/dist/index56.mjs +32 -2262
  238. package/dist/index57.mjs +15 -36
  239. package/dist/index58.mjs +2261 -42
  240. package/dist/index59.mjs +36 -99
  241. package/dist/index6.mjs +1 -1
  242. package/dist/index60.mjs +43 -80
  243. package/dist/index61.mjs +102 -18
  244. package/dist/index62.mjs +52 -100
  245. package/dist/index63.mjs +13 -119
  246. package/dist/index64.mjs +102 -52
  247. package/dist/index65.mjs +83 -119
  248. package/dist/index66.mjs +52 -80
  249. package/dist/index67.mjs +167 -28
  250. package/dist/index68.mjs +79 -67
  251. package/dist/index69.mjs +30 -75
  252. package/dist/index7.mjs +9 -9
  253. package/dist/index70.mjs +71 -39
  254. package/dist/index71.mjs +65 -46
  255. package/dist/index72.mjs +45 -117
  256. package/dist/index73.mjs +45 -53
  257. package/dist/index74.mjs +133 -22
  258. package/dist/index75.mjs +70 -2
  259. package/dist/index76.mjs +4 -22
  260. package/dist/index77.mjs +21 -150
  261. package/dist/index78.mjs +149 -71
  262. package/dist/index79.mjs +2 -15
  263. package/dist/index8.mjs +9 -9
  264. package/dist/index80.mjs +21 -61
  265. package/dist/index81.mjs +74 -4
  266. package/dist/index82.mjs +15 -2
  267. package/dist/index83.mjs +62 -5
  268. package/dist/index84.mjs +37 -1134
  269. package/dist/index85.mjs +42 -19
  270. package/dist/index86.mjs +2 -55
  271. package/dist/index87.mjs +5 -32
  272. package/dist/index88.mjs +1134 -2
  273. package/dist/index89.mjs +20 -34
  274. package/dist/index9.mjs +4 -4
  275. package/dist/index90.mjs +54 -42
  276. package/dist/index91.mjs +33 -2
  277. package/dist/index92.mjs +2 -235
  278. package/dist/index93.mjs +233 -4
  279. package/dist/index94.mjs +5 -133
  280. package/dist/index95.mjs +129 -63
  281. package/dist/index96.mjs +67 -86
  282. package/dist/index97.mjs +85 -27
  283. package/dist/index98.mjs +28 -8
  284. package/dist/index99.mjs +8 -74
  285. package/dist/styles.css +1 -1
  286. package/package.json +3 -3
  287. package/src/components/AddOnsDiscountScreen.stories.tsx +290 -0
  288. package/src/components/AddOnsDiscountScreen.tsx +255 -0
  289. package/src/components/Checkout.stories.tsx +1 -0
  290. package/src/components/DeliveryConfirmation.tsx +3 -3
  291. package/src/components/FreeShippingProgress.tsx +50 -0
  292. package/src/components/OrderConfirmation.tsx +3 -3
  293. package/src/components/ShoppingCart.stories.tsx +1 -0
  294. package/src/components/ShoppingCart.tsx +53 -45
  295. package/src/components/VariantPickerModal.tsx +36 -9
  296. package/src/contexts/CartContext.tsx +5 -3
  297. package/src/index.ts +3 -0
  298. package/src/test-utils/MockCartProvider.tsx +4 -2
@@ -9,13 +9,15 @@
9
9
 
10
10
  import { useEffect, useState } from 'react';
11
11
  import type { Order } from '@instockng/api-client';
12
- import { useGetCartRecommendations } from '@instockng/api-client';
12
+ import { useGetCartRecommendations, useGetCartUpsellAddons } from '@instockng/api-client';
13
13
  import { useCart } from '../contexts/CartContext';
14
14
  import { CartItem } from './CartItem';
15
15
  import { DiscountCodeInput } from './DiscountCodeInput';
16
16
  import { Checkout } from './Checkout';
17
17
  import { RecommendedProducts } from './RecommendedProducts';
18
18
  import { CartUpsellScreen } from './CartUpsellScreen';
19
+ import { AddOnsDiscountScreen } from './AddOnsDiscountScreen';
20
+ import { FreeShippingProgress } from './FreeShippingProgress';
19
21
  import { Button } from './ui/button';
20
22
  import { X, Package, Loader2, ArrowLeft } from 'lucide-react';
21
23
  import { formatCurrency, cn } from '../lib/utils';
@@ -74,19 +76,26 @@ export function ShoppingCart({
74
76
  // Fetch recommendations to decide whether to show upsell
75
77
  const { data: recommendations } = useGetCartRecommendations(cart?.id, 100);
76
78
 
79
+ // Prefetch upsell addons alongside recommendations
80
+ const { data: upsellAddons } = useGetCartUpsellAddons(cart?.id);
81
+
77
82
  // State for checkout modal
78
83
  const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
79
84
 
80
- // State for upsell interstitial
85
+ // State for upsell interstitial (stage 1)
81
86
  const [showUpsell, setShowUpsell] = useState(false);
82
87
 
88
+ // State for discounted add-ons interstitial (stage 2)
89
+ const [showDiscountedAddons, setShowDiscountedAddons] = useState(false);
90
+
83
91
  // Prevent body scroll when cart is open
84
92
  useHideBodyOverflow(isOpen);
85
93
 
86
- // Reset upsell when cart closes
94
+ // Reset upsell screens when cart closes
87
95
  useEffect(() => {
88
96
  if (!isOpen) {
89
97
  setShowUpsell(false);
98
+ setShowDiscountedAddons(false);
90
99
  }
91
100
  }, [isOpen]);
92
101
 
@@ -105,12 +114,17 @@ export function ShoppingCart({
105
114
  const subtotal = cart?.pricing?.subtotal || 0;
106
115
  const discount = cart?.pricing.discount?.amount || 0;
107
116
 
117
+ const hasDiscountedAddons = upsellAddons && upsellAddons.length > 0 &&
118
+ upsellAddons.some((group: any) => group.addons && group.addons.length > 0);
119
+
108
120
  const handleCheckout = () => {
109
121
  // Track InitiateCheckout with Meta Pixel (includes event ID for CAPI deduplication)
110
122
  trackCheckoutInitiated();
111
123
 
112
124
  if (recommendations && recommendations.length > 0) {
113
125
  setShowUpsell(true);
126
+ } else if (hasDiscountedAddons) {
127
+ setShowDiscountedAddons(true);
114
128
  } else {
115
129
  setIsCheckoutOpen(true);
116
130
  }
@@ -119,6 +133,15 @@ export function ShoppingCart({
119
133
 
120
134
  const handleSkipUpsell = () => {
121
135
  setShowUpsell(false);
136
+ if (hasDiscountedAddons) {
137
+ setShowDiscountedAddons(true);
138
+ } else {
139
+ setIsCheckoutOpen(true);
140
+ }
141
+ };
142
+
143
+ const handleSkipDiscountedAddons = () => {
144
+ setShowDiscountedAddons(false);
122
145
  setIsCheckoutOpen(true);
123
146
  };
124
147
 
@@ -126,6 +149,15 @@ export function ShoppingCart({
126
149
  setShowUpsell(false);
127
150
  };
128
151
 
152
+ const handleBackFromDiscountedAddons = () => {
153
+ if (recommendations && recommendations.length > 0) {
154
+ setShowDiscountedAddons(false);
155
+ setShowUpsell(true);
156
+ } else {
157
+ setShowDiscountedAddons(false);
158
+ }
159
+ };
160
+
129
161
  const handleContinueShopping = () => {
130
162
  onContinueShopping?.();
131
163
  onClose();
@@ -155,16 +187,18 @@ export function ShoppingCart({
155
187
  {/* Header */}
156
188
  <div className="border-b border-gray-300 px-4 py-2">
157
189
  <div className="flex items-center justify-between">
158
- {showUpsell ? (
190
+ {showUpsell || showDiscountedAddons ? (
159
191
  <div className="flex items-center gap-2">
160
192
  <button
161
- onClick={handleBackFromUpsell}
193
+ onClick={showDiscountedAddons ? handleBackFromDiscountedAddons : handleBackFromUpsell}
162
194
  className="rounded-full p-2 transition-colors hover:bg-gray-100"
163
- aria-label="Back to cart"
195
+ aria-label="Back"
164
196
  >
165
197
  <ArrowLeft className="h-5 w-5" />
166
198
  </button>
167
- <h2 className="text-lg font-medium">Before you go...</h2>
199
+ <h2 className="text-lg font-medium">
200
+ {showDiscountedAddons ? 'Exclusive deals for you' : 'Before you go...'}
201
+ </h2>
168
202
  </div>
169
203
  ) : (
170
204
  <div className="flex items-center gap-2">
@@ -182,49 +216,23 @@ export function ShoppingCart({
182
216
  </div>
183
217
  </div>
184
218
 
185
- {/* Upsell Screen */}
219
+ {/* Upsell Screen (Stage 1) */}
186
220
  {showUpsell && (
187
221
  <CartUpsellScreen onSkip={handleSkipUpsell} limit={100} checkoutButtonClassName={checkoutButtonClassName} />
188
222
  )}
189
223
 
224
+ {/* Discounted Add-Ons Screen (Stage 2) */}
225
+ {showDiscountedAddons && (
226
+ <AddOnsDiscountScreen onSkip={handleSkipDiscountedAddons} checkoutButtonClassName={checkoutButtonClassName} />
227
+ )}
228
+
190
229
  {/* Free Shipping Progress */}
191
- {!showUpsell && !isLoading && !error && cart && cart.items && cart.items.length > 0 && cart.brand?.freeShippingThreshold && (
192
- <div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
193
- {(() => {
194
- const threshold = cart.brand.freeShippingThreshold;
195
- const current = cart.pricing?.subtotal || 0;
196
- const remaining = Math.max(0, threshold - current);
197
- const progress = Math.min(100, (current / threshold) * 100);
198
- const isFreeShipping = remaining <= 0;
199
-
200
- return (
201
- <div className="space-y-2">
202
- <p className="text-sm font-bold text-center">
203
- {isFreeShipping ? (
204
- <span className="text-green-600">You've unlocked FREE delivery! 🎉</span>
205
- ) : (
206
- <span>
207
- You're <span className="text-red-500">{formatCurrency(remaining)}</span> away from FREE delivery
208
- </span>
209
- )}
210
- </p>
211
- <div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden">
212
- <div
213
- className={cn(
214
- "h-full transition-all duration-500 ease-out",
215
- isFreeShipping ? "bg-green-500" : "bg-red-500"
216
- )}
217
- style={{ width: `${progress}%` }}
218
- />
219
- </div>
220
- </div>
221
- );
222
- })()}
223
- </div>
230
+ {!showUpsell && !showDiscountedAddons && !isLoading && !error && cart && cart.items && cart.items.length > 0 && (
231
+ <FreeShippingProgress />
224
232
  )}
225
233
 
226
234
  {/* Loading State */}
227
- {!showUpsell && isLoading && (
235
+ {!showUpsell && !showDiscountedAddons && isLoading && (
228
236
  <div className="flex flex-1 items-center justify-center">
229
237
  <div className="text-center">
230
238
  <Loader2 className="mx-auto mb-4 h-12 w-12 animate-spin text-accent-500" />
@@ -234,7 +242,7 @@ export function ShoppingCart({
234
242
  )}
235
243
 
236
244
  {/* Error State */}
237
- {!showUpsell && error && !isLoading && (
245
+ {!showUpsell && !showDiscountedAddons && error && !isLoading && (
238
246
  <div className="flex flex-1 items-center justify-center">
239
247
  <div className="text-center">
240
248
  <Package className="mx-auto mb-4 h-12 w-12 text-red-600" />
@@ -244,7 +252,7 @@ export function ShoppingCart({
244
252
  )}
245
253
 
246
254
  {/* Empty State */}
247
- {!showUpsell && !isLoading && !error && (!cart || !cart.items || cart.items.length === 0) && (
255
+ {!showUpsell && !showDiscountedAddons && !isLoading && !error && (!cart || !cart.items || cart.items.length === 0) && (
248
256
  <div className="flex flex-1 items-center justify-center">
249
257
  <div className="text-center">
250
258
  <Package className="mx-auto mb-4 h-12 w-12 text-gray-400" />
@@ -254,7 +262,7 @@ export function ShoppingCart({
254
262
  )}
255
263
 
256
264
  {/* Cart Content */}
257
- {!showUpsell && !isLoading && !error && cart && cart.items && cart.items.length > 0 && (
265
+ {!showUpsell && !showDiscountedAddons && !isLoading && !error && cart && cart.items && cart.items.length > 0 && (
258
266
  <>
259
267
  {/* Cart Items - Scrollable */}
260
268
  <div className="flex-1 overflow-y-auto">
@@ -19,6 +19,11 @@ export interface Variant {
19
19
  price: number;
20
20
  sku: string;
21
21
  thumbnailUrl?: string | null;
22
+ availability?: {
23
+ available: boolean;
24
+ trackInventory: boolean;
25
+ totalInventory: number | null;
26
+ };
22
27
  }
23
28
 
24
29
  export interface VariantPickerProduct {
@@ -40,6 +45,8 @@ export interface VariantPickerModalProps {
40
45
  onConfirm: (product: VariantPickerProduct, variant: Variant) => void;
41
46
  /** Whether add operation is in progress */
42
47
  isLoading?: boolean;
48
+ /** Optional discount percentage to show discounted prices */
49
+ discountPercent?: number | null;
43
50
  }
44
51
 
45
52
  export function VariantPickerModal({
@@ -48,11 +55,13 @@ export function VariantPickerModal({
48
55
  product,
49
56
  onConfirm,
50
57
  isLoading = false,
58
+ discountPercent,
51
59
  }: VariantPickerModalProps) {
52
60
  const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
53
61
 
54
- // Reset selection when product changes
55
- const effectiveVariantId = selectedVariantId || product?.variants[0]?.id || null;
62
+ // Reset selection when product changes — default to first available variant
63
+ const firstAvailableId = product?.variants.find(v => !v.availability || v.availability.available)?.id || product?.variants[0]?.id || null;
64
+ const effectiveVariantId = selectedVariantId || firstAvailableId;
56
65
  const selectedVariant = product?.variants.find(v => v.id === effectiveVariantId);
57
66
 
58
67
  const handleConfirm = () => {
@@ -72,7 +81,7 @@ export function VariantPickerModal({
72
81
  footer={
73
82
  <Button
74
83
  onClick={handleConfirm}
75
- disabled={!selectedVariant || isLoading}
84
+ disabled={!selectedVariant || isLoading || (selectedVariant?.availability && !selectedVariant.availability.available)}
76
85
  className="w-full bg-accent-500 hover:bg-accent-600 text-white"
77
86
  size="lg"
78
87
  >
@@ -84,7 +93,14 @@ export function VariantPickerModal({
84
93
  ) : (
85
94
  <>
86
95
  <ShoppingCart className="h-4 w-4 mr-2" />
87
- Add to Cart - {selectedVariant && formatCurrency(selectedVariant.price)}
96
+ Add to Cart - {selectedVariant && (
97
+ discountPercent ? (
98
+ <span>
99
+ <span className="line-through opacity-60 mr-1">{formatCurrency(selectedVariant.price)}</span>
100
+ {formatCurrency(Math.round(selectedVariant.price * (1 - discountPercent / 100)))}
101
+ </span>
102
+ ) : formatCurrency(selectedVariant.price)
103
+ )}
88
104
  </>
89
105
  )}
90
106
  </Button>
@@ -123,16 +139,20 @@ export function VariantPickerModal({
123
139
  <div className="flex flex-wrap gap-2">
124
140
  {product.variants.map((variant) => {
125
141
  const isSelected = variant.id === effectiveVariantId;
142
+ const isOutOfStock = variant.availability && !variant.availability.available;
126
143
  const variantImage = variant.thumbnailUrl || product.thumbnailUrl;
127
144
  return (
128
145
  <button
129
146
  key={variant.id}
130
- onClick={() => setSelectedVariantId(variant.id)}
147
+ onClick={() => !isOutOfStock && setSelectedVariantId(variant.id)}
148
+ disabled={!!isOutOfStock}
131
149
  className={cn(
132
150
  "flex items-center gap-2 px-3 py-2 rounded-full text-sm font-medium border-2 transition-all",
133
- isSelected
134
- ? "border-accent-500 bg-accent-50 text-accent-700"
135
- : "border-gray-200 bg-white text-gray-700 hover:border-gray-300"
151
+ isOutOfStock
152
+ ? "border-gray-200 bg-gray-50 text-gray-400 cursor-not-allowed opacity-60"
153
+ : isSelected
154
+ ? "border-accent-500 bg-accent-50 text-accent-700"
155
+ : "border-gray-200 bg-white text-gray-700 hover:border-gray-300"
136
156
  )}
137
157
  >
138
158
  {variantImage && (
@@ -142,7 +162,14 @@ export function VariantPickerModal({
142
162
  className="w-6 h-6 rounded-full object-cover flex-shrink-0"
143
163
  />
144
164
  )}
145
- {variant.name || 'Default'} - {formatCurrency(variant.price)}
165
+ {variant.name || 'Default'} - {isOutOfStock ? (
166
+ <span className="text-gray-400">Sold out</span>
167
+ ) : discountPercent ? (
168
+ <span>
169
+ <span className="line-through opacity-60 mr-1">{formatCurrency(variant.price)}</span>
170
+ {formatCurrency(Math.round(variant.price * (1 - discountPercent / 100)))}
171
+ </span>
172
+ ) : formatCurrency(variant.price)}
146
173
  </button>
147
174
  );
148
175
  })}
@@ -45,7 +45,7 @@ interface CartContextValue {
45
45
  /** Checkout mutation (from useCheckoutCart) */
46
46
  checkoutMutation: ReturnType<typeof useCheckoutCart>;
47
47
  /** Add item to cart by SKU with Meta Pixel tracking */
48
- addItem: (productSlug: string, productName: string, price: number, sku: string, quantity: number) => Promise<void>;
48
+ addItem: (productSlug: string, productName: string, price: number, sku: string, quantity: number, fromUpsell?: boolean, upsellDiscountPercent?: number) => Promise<void>;
49
49
  /** Update item quantity */
50
50
  updateItem: (itemId: string, quantity: number) => Promise<void>;
51
51
  /** Remove item from cart */
@@ -253,7 +253,7 @@ export function CartProvider({ children, brandSlug, initialCartId, shoppingCartP
253
253
  }, [cartId]);
254
254
 
255
255
  const addItem = useCallback(
256
- async (productSlug: string, productName: string, price: number, sku: string, quantity: number) => {
256
+ async (productSlug: string, productName: string, price: number, sku: string, quantity: number, fromUpsell?: boolean, upsellDiscountPercent?: number) => {
257
257
  if (!cartId) throw new Error('No cart ID');
258
258
 
259
259
  // Get Facebook cookies for attribution
@@ -272,7 +272,9 @@ export function CartProvider({ children, brandSlug, initialCartId, shoppingCartP
272
272
  fbp,
273
273
  ttp,
274
274
  ttclid,
275
- refreshRecommendations: shouldRefreshRecommendations
275
+ refreshRecommendations: shouldRefreshRecommendations,
276
+ fromUpsell,
277
+ upsellDiscountPercent,
276
278
  });
277
279
 
278
280
  // Invalidate recommendations cache only if cart was closed when adding
package/src/index.ts CHANGED
@@ -55,6 +55,9 @@ export type { ShoppingCartProps } from './components/ShoppingCart';
55
55
  export { CartUpsellScreen } from './components/CartUpsellScreen';
56
56
  export type { CartUpsellScreenProps } from './components/CartUpsellScreen';
57
57
 
58
+ export { AddOnsDiscountScreen } from './components/AddOnsDiscountScreen';
59
+ export type { AddOnsDiscountScreenProps } from './components/AddOnsDiscountScreen';
60
+
58
61
  export { DiscountCodeInput } from './components/DiscountCodeInput';
59
62
  export type { DiscountCodeInputProps } from './components/DiscountCodeInput';
60
63
 
@@ -27,6 +27,8 @@ const mockCart: Cart = {
27
27
  tiktokPixelId: null as any,
28
28
  paystackPublicKey: null as any,
29
29
  paystackSecretKey: null as any,
30
+ upsellDiscountPercent: 10,
31
+ freeShippingThreshold: 15000,
30
32
  createdAt: new Date().toISOString(),
31
33
  updatedAt: new Date().toISOString(),
32
34
  deletedAt: null as any,
@@ -342,8 +344,8 @@ export function MockCartProvider({
342
344
  updateCartMutation: mockUpdateCartMutation as any,
343
345
  checkoutMutation: mockCheckoutMutation as any,
344
346
 
345
- addItem: async (productSlug: string, productName: string, price: number, sku: string, quantity: number) => {
346
- console.log('Mock addItem:', { productSlug, productName, price, sku, quantity });
347
+ addItem: async (productSlug: string, productName: string, price: number, sku: string, quantity: number, fromUpsell?: boolean, upsellDiscountPercent?: number) => {
348
+ console.log('Mock addItem:', { productSlug, productName, price, sku, quantity, fromUpsell, upsellDiscountPercent });
347
349
  },
348
350
 
349
351
  updateItem: async (itemId: string, quantity: number) => {