@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.
- package/dist/components/AddOnsDiscountScreen.d.ts +10 -0
- package/dist/components/AddOnsDiscountScreen.d.ts.map +1 -0
- package/dist/components/FreeShippingProgress.d.ts +2 -0
- package/dist/components/FreeShippingProgress.d.ts.map +1 -0
- package/dist/components/ShoppingCart.d.ts.map +1 -1
- package/dist/components/VariantPickerModal.d.ts +8 -1
- package/dist/components/VariantPickerModal.d.ts.map +1 -1
- package/dist/contexts/CartContext.d.ts +1 -1
- package/dist/contexts/CartContext.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +119 -115
- package/dist/index10.mjs +3 -3
- package/dist/index100.mjs +74 -3
- package/dist/index101.mjs +4 -2
- package/dist/index102.mjs +2 -83
- package/dist/index103.mjs +81 -52
- package/dist/index104.mjs +53 -5
- package/dist/index105.mjs +5 -4
- package/dist/index106.mjs +4 -178
- package/dist/index107.mjs +174 -48
- package/dist/index108.mjs +51 -67
- package/dist/index109.mjs +69 -2
- package/dist/index11.mjs +5 -5
- package/dist/index110.mjs +2 -28
- package/dist/index111.mjs +2 -18
- package/dist/index112.mjs +26 -213
- package/dist/index113.mjs +13 -175
- package/dist/index114.mjs +210 -17
- package/dist/index115.mjs +174 -17
- package/dist/index116.mjs +14 -25
- package/dist/index117.mjs +17 -150
- package/dist/index118.mjs +26 -13
- package/dist/index119.mjs +149 -24
- package/dist/index12.mjs +4 -4
- package/dist/index120.mjs +13 -77
- package/dist/index121.mjs +20 -27
- package/dist/index122.mjs +76 -137
- package/dist/index123.mjs +33 -50
- package/dist/index124.mjs +141 -19
- package/dist/index125.mjs +50 -22
- package/dist/index126.mjs +14 -14
- package/dist/index127.mjs +20 -18
- package/dist/index128.mjs +16 -14
- package/dist/index129.mjs +17 -14
- package/dist/index13.mjs +59 -51
- package/dist/index130.mjs +12 -12
- package/dist/index131.mjs +15 -58
- package/dist/index132.mjs +14 -11
- package/dist/index133.mjs +56 -30
- package/dist/index134.mjs +9 -15
- package/dist/index135.mjs +31 -26
- package/dist/index136.mjs +13 -16
- package/dist/index137.mjs +26 -11
- package/dist/index138.mjs +17 -11
- package/dist/index139.mjs +12 -40
- package/dist/index14.mjs +1 -1
- package/dist/index140.mjs +14 -16
- package/dist/index141.mjs +42 -263
- package/dist/index142.mjs +15 -62
- package/dist/index143.mjs +267 -7
- package/dist/index144.mjs +70 -2
- package/dist/index145.mjs +8 -2
- package/dist/index146.mjs +2 -33
- package/dist/index147.mjs +2 -2
- package/dist/index148.mjs +32 -20
- package/dist/index149.mjs +2 -56
- package/dist/index15.mjs +1 -1
- package/dist/index150.mjs +18 -26
- package/dist/index151.mjs +55 -6
- package/dist/index152.mjs +28 -51
- package/dist/index153.mjs +7 -6
- package/dist/index154.mjs +51 -11
- package/dist/index155.mjs +5 -6
- package/dist/index156.mjs +12 -28
- package/dist/index157.mjs +7 -2
- package/dist/index158.mjs +27 -69
- package/dist/index159.mjs +2 -167
- package/dist/index16.mjs +118 -131
- package/dist/index160.mjs +70 -2
- package/dist/index161.mjs +167 -2
- package/dist/index162.mjs +2 -2
- package/dist/index163.mjs +2 -18
- package/dist/index164.mjs +2 -32
- package/dist/index165.mjs +14 -34
- package/dist/index166.mjs +25 -11
- package/dist/index167.mjs +34 -14
- package/dist/index168.mjs +18 -2
- package/dist/index169.mjs +11 -13
- package/dist/index17.mjs +4 -4
- package/dist/index170.mjs +2 -46
- package/dist/index171.mjs +20 -2
- package/dist/index172.mjs +41 -25
- package/dist/index173.mjs +2 -18
- package/dist/index174.mjs +30 -2
- package/dist/index175.mjs +18 -2
- package/dist/index176.mjs +2 -2
- package/dist/index177.mjs +2 -2
- package/dist/index178.mjs +2 -72
- package/dist/index179.mjs +2 -2
- package/dist/index18.mjs +151 -86
- package/dist/index180.mjs +59 -40
- package/dist/index182.mjs +48 -31
- package/dist/index183.mjs +2 -152
- package/dist/index184.mjs +36 -2
- package/dist/index185.mjs +152 -2
- package/dist/index186.mjs +2 -19
- package/dist/index187.mjs +2 -2
- package/dist/index188.mjs +19 -2
- package/dist/index189.mjs +2 -26
- package/dist/index19.mjs +82 -733
- package/dist/index190.mjs +2 -2
- package/dist/index191.mjs +26 -2
- package/dist/index192.mjs +2 -24
- package/dist/index193.mjs +2 -23
- package/dist/index194.mjs +24 -2
- package/dist/index195.mjs +23 -2
- package/dist/index196.mjs +2 -2
- package/dist/index197.mjs +2 -23
- package/dist/index199.mjs +16 -16
- package/dist/index2.mjs +2 -2
- package/dist/index20.mjs +742 -53
- package/dist/index200.mjs +2 -2
- package/dist/index201.mjs +23 -2
- package/dist/index202.mjs +2 -2
- package/dist/index203.mjs +2 -23
- package/dist/index204.mjs +2 -2
- package/dist/index205.mjs +15 -15
- package/dist/index207.mjs +23 -2
- package/dist/index208.mjs +2 -2
- package/dist/index209.mjs +2 -2
- package/dist/index21.mjs +58 -60
- package/dist/index210.mjs +2 -2
- package/dist/index212.mjs +2 -2
- package/dist/index214.mjs +2 -127
- package/dist/index215.mjs +2 -2
- package/dist/index216.mjs +123 -70
- package/dist/index217.mjs +2 -74
- package/dist/index218.mjs +74 -2
- package/dist/index219.mjs +74 -2
- package/dist/index22.mjs +60 -22
- package/dist/index220.mjs +2 -108
- package/dist/index221.mjs +31 -2
- package/dist/index222.mjs +11 -2
- package/dist/index223.mjs +4 -2
- package/dist/index224.mjs +4 -2
- package/dist/index225.mjs +13 -2
- package/dist/index226.mjs +7 -37
- package/dist/index227.mjs +12 -2
- package/dist/index228.mjs +4 -243
- package/dist/index229.mjs +33 -2
- package/dist/index23.mjs +22 -107
- package/dist/index230.mjs +30 -32
- package/dist/index231.mjs +27 -64
- package/dist/index232.mjs +59 -23
- package/dist/index233.mjs +2 -2
- package/dist/index234.mjs +2 -2
- package/dist/index235.mjs +2 -2
- package/dist/index236.mjs +2 -2
- package/dist/index237.mjs +2 -2
- package/dist/index238.mjs +2 -2
- package/dist/index24.mjs +105 -38
- package/dist/index240.mjs +2 -2
- package/dist/index241.mjs +108 -2
- package/dist/index242.mjs +2 -2
- package/dist/index244.mjs +37 -2
- package/dist/index246.mjs +2 -2
- package/dist/index247.mjs +244 -2
- package/dist/index248.mjs +2 -4
- package/dist/index249.mjs +33 -2
- package/dist/index25.mjs +37 -37
- package/dist/index250.mjs +64 -30
- package/dist/index251.mjs +24 -10
- package/dist/index252.mjs +2 -4
- package/dist/index253.mjs +2 -4
- package/dist/index254.mjs +2 -13
- package/dist/index255.mjs +2 -7
- package/dist/index256.mjs +2 -12
- package/dist/index257.mjs +2 -5
- package/dist/index258.mjs +2 -33
- package/dist/index259.mjs +2 -31
- package/dist/index26.mjs +42 -89
- package/dist/index260.mjs +2 -28
- package/dist/index261.mjs +4 -61
- package/dist/index262.mjs +2 -2
- package/dist/index263.mjs +2 -2
- package/dist/index265.mjs +1 -1
- package/dist/index266.mjs +1 -1
- package/dist/index267.mjs +2 -2
- package/dist/index269.mjs +2 -2
- package/dist/index27.mjs +86 -29
- package/dist/index270.mjs +2 -91
- package/dist/index271.mjs +2 -2
- package/dist/index272.mjs +91 -3
- package/dist/index273.mjs +2 -2
- package/dist/index274.mjs +3 -2
- package/dist/index275.mjs +2 -17
- package/dist/index276.mjs +2 -13
- package/dist/index277.mjs +17 -6
- package/dist/index278.mjs +13 -30
- package/dist/index279.mjs +6 -2
- package/dist/index28.mjs +29 -6
- package/dist/index280.mjs +30 -2
- package/dist/index281.mjs +2 -2
- package/dist/index282.mjs +5 -0
- package/dist/index283.mjs +5 -0
- package/dist/index29.mjs +8 -21
- package/dist/index3.mjs +40 -38
- package/dist/index30.mjs +19 -35
- package/dist/index31.mjs +35 -37
- package/dist/index32.mjs +28 -103
- package/dist/index33.mjs +107 -32
- package/dist/index34.mjs +47 -9
- package/dist/index35.mjs +9 -9
- package/dist/index36.mjs +11 -121
- package/dist/index37.mjs +123 -27
- package/dist/index38.mjs +28 -90
- package/dist/index39.mjs +91 -112
- package/dist/index4.mjs +1 -1
- package/dist/index40.mjs +118 -8
- package/dist/index41.mjs +9 -33
- package/dist/index42.mjs +23 -34
- package/dist/index43.mjs +42 -7
- package/dist/index44.mjs +10 -122
- package/dist/index45.mjs +114 -380
- package/dist/index46.mjs +384 -20
- package/dist/index47.mjs +24 -31
- package/dist/index48.mjs +32 -7
- package/dist/index49.mjs +27 -1432
- package/dist/index5.mjs +1 -1
- package/dist/index50.mjs +6 -69
- package/dist/index51.mjs +1433 -2
- package/dist/index52.mjs +68 -58
- package/dist/index53.mjs +2 -51
- package/dist/index54.mjs +60 -33
- package/dist/index55.mjs +49 -13
- package/dist/index56.mjs +32 -2262
- package/dist/index57.mjs +15 -36
- package/dist/index58.mjs +2261 -42
- package/dist/index59.mjs +36 -99
- package/dist/index6.mjs +1 -1
- package/dist/index60.mjs +43 -80
- package/dist/index61.mjs +102 -18
- package/dist/index62.mjs +52 -100
- package/dist/index63.mjs +13 -119
- package/dist/index64.mjs +102 -52
- package/dist/index65.mjs +83 -119
- package/dist/index66.mjs +52 -80
- package/dist/index67.mjs +167 -28
- package/dist/index68.mjs +79 -67
- package/dist/index69.mjs +30 -75
- package/dist/index7.mjs +9 -9
- package/dist/index70.mjs +71 -39
- package/dist/index71.mjs +65 -46
- package/dist/index72.mjs +45 -117
- package/dist/index73.mjs +45 -53
- package/dist/index74.mjs +133 -22
- package/dist/index75.mjs +70 -2
- package/dist/index76.mjs +4 -22
- package/dist/index77.mjs +21 -150
- package/dist/index78.mjs +149 -71
- package/dist/index79.mjs +2 -15
- package/dist/index8.mjs +9 -9
- package/dist/index80.mjs +21 -61
- package/dist/index81.mjs +74 -4
- package/dist/index82.mjs +15 -2
- package/dist/index83.mjs +62 -5
- package/dist/index84.mjs +37 -1134
- package/dist/index85.mjs +42 -19
- package/dist/index86.mjs +2 -55
- package/dist/index87.mjs +5 -32
- package/dist/index88.mjs +1134 -2
- package/dist/index89.mjs +20 -34
- package/dist/index9.mjs +4 -4
- package/dist/index90.mjs +54 -42
- package/dist/index91.mjs +33 -2
- package/dist/index92.mjs +2 -235
- package/dist/index93.mjs +233 -4
- package/dist/index94.mjs +5 -133
- package/dist/index95.mjs +129 -63
- package/dist/index96.mjs +67 -86
- package/dist/index97.mjs +85 -27
- package/dist/index98.mjs +28 -8
- package/dist/index99.mjs +8 -74
- package/dist/styles.css +1 -1
- package/package.json +3 -3
- package/src/components/AddOnsDiscountScreen.stories.tsx +290 -0
- package/src/components/AddOnsDiscountScreen.tsx +255 -0
- package/src/components/Checkout.stories.tsx +1 -0
- package/src/components/DeliveryConfirmation.tsx +3 -3
- package/src/components/FreeShippingProgress.tsx +50 -0
- package/src/components/OrderConfirmation.tsx +3 -3
- package/src/components/ShoppingCart.stories.tsx +1 -0
- package/src/components/ShoppingCart.tsx +53 -45
- package/src/components/VariantPickerModal.tsx +36 -9
- package/src/contexts/CartContext.tsx +5 -3
- package/src/index.ts +3 -0
- 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
|
|
195
|
+
aria-label="Back"
|
|
164
196
|
>
|
|
165
197
|
<ArrowLeft className="h-5 w-5" />
|
|
166
198
|
</button>
|
|
167
|
-
<h2 className="text-lg font-medium">
|
|
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 &&
|
|
192
|
-
<
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
134
|
-
? "border-
|
|
135
|
-
:
|
|
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'} - {
|
|
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) => {
|