@abra-promotions/webhooks-for-tapcart 1.0.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/README.md +75 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.esm.js +3145 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +3168 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var mobileComponents = require('@tapcart/mobile-components');
|
|
5
|
+
|
|
6
|
+
function _interopNamespaceDefault(e) {
|
|
7
|
+
var n = Object.create(null);
|
|
8
|
+
if (e) {
|
|
9
|
+
Object.keys(e).forEach(function (k) {
|
|
10
|
+
if (k !== 'default') {
|
|
11
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return e[k]; }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
|
|
24
|
+
|
|
25
|
+
function useAbra({ useTapcart, tapcartData }) {
|
|
26
|
+
const Tapcart = useTapcart();
|
|
27
|
+
const [activePromotion, setActivePromotion] = React.useState(undefined);
|
|
28
|
+
const renderCountRef = React.useRef(0);
|
|
29
|
+
const logRef = React.useRef([]);
|
|
30
|
+
const actions = Tapcart.actions;
|
|
31
|
+
const variables = Tapcart.variables;
|
|
32
|
+
const { storeFrontAccessToken, storeUrl } =
|
|
33
|
+
tapcartData?.integrations?.find((x) => x.name === "abra-promotions") ?? {};
|
|
34
|
+
|
|
35
|
+
// Stable GraphQL fragment and functions
|
|
36
|
+
const PRODUCT_FRAGMENT = React.useMemo(
|
|
37
|
+
() => `
|
|
38
|
+
id
|
|
39
|
+
handle
|
|
40
|
+
variants(first: 100) {
|
|
41
|
+
edges {
|
|
42
|
+
node {
|
|
43
|
+
id
|
|
44
|
+
handle: title
|
|
45
|
+
price {
|
|
46
|
+
amount
|
|
47
|
+
currencyCode
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
`,
|
|
53
|
+
[],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const generateProductsQuery = React.useCallback(
|
|
57
|
+
(ids) => {
|
|
58
|
+
const productQueries = ids
|
|
59
|
+
.map(
|
|
60
|
+
(id, index) =>
|
|
61
|
+
`product_${index}: product(id: "${mobileComponents.productGidFromId(id)}") {
|
|
62
|
+
${PRODUCT_FRAGMENT}
|
|
63
|
+
}`,
|
|
64
|
+
)
|
|
65
|
+
.join("\n ");
|
|
66
|
+
|
|
67
|
+
return `
|
|
68
|
+
query Products {
|
|
69
|
+
${productQueries}
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
},
|
|
73
|
+
[PRODUCT_FRAGMENT],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const graphqlDefaultBody = React.useCallback(
|
|
77
|
+
(query, variables, storefrontAccessToken) => {
|
|
78
|
+
return {
|
|
79
|
+
async: true,
|
|
80
|
+
crossDomain: true,
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"X-Shopify-Storefront-Access-Token": storefrontAccessToken,
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
query,
|
|
88
|
+
variables,
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
[],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const fetchProducts = React.useCallback(
|
|
96
|
+
async (ids, storeUrl, storefrontAccessToken) => {
|
|
97
|
+
if (!storeUrl || !storefrontAccessToken) {
|
|
98
|
+
return { error: "Missing required configuration" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!ids || ids.length === 0) {
|
|
102
|
+
return { data: [], error: "No ids" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const GRAPHQL_URL = `https://${storeUrl}/api/2024-04/graphql.json`;
|
|
106
|
+
const query = generateProductsQuery(ids);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch(
|
|
110
|
+
GRAPHQL_URL,
|
|
111
|
+
graphqlDefaultBody(query, {}, storefrontAccessToken),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (response?.ok) {
|
|
115
|
+
const responseBody = await response.json();
|
|
116
|
+
const data = responseBody?.data;
|
|
117
|
+
|
|
118
|
+
if (!data) {
|
|
119
|
+
return { data: [], error: "No data" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const products = Object.values(data).filter((item) => item !== null);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
data: products,
|
|
126
|
+
error: null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const error = await response.text();
|
|
131
|
+
return { data: [], error: error };
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return { data: [], error: err.message };
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
[generateProductsQuery, graphqlDefaultBody],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const useProductsCustom = React.useCallback(
|
|
140
|
+
({ ids, storeUrl, storeFrontAccessToken }) => {
|
|
141
|
+
const [products, setProducts] = React.useState([]);
|
|
142
|
+
const [loading, setLoading] = React.useState(true);
|
|
143
|
+
const [error, setError] = React.useState(null);
|
|
144
|
+
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
const fetchData = async () => {
|
|
147
|
+
if (!storeUrl || !storeFrontAccessToken) {
|
|
148
|
+
if (!storeUrl) {
|
|
149
|
+
setError("no store url");
|
|
150
|
+
}
|
|
151
|
+
if (!storeFrontAccessToken) {
|
|
152
|
+
setError("no store front access token");
|
|
153
|
+
}
|
|
154
|
+
setProducts([]);
|
|
155
|
+
setLoading(false);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!ids || ids.length === 0) {
|
|
160
|
+
setProducts([]);
|
|
161
|
+
setLoading(false);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = await fetchProducts(
|
|
166
|
+
ids,
|
|
167
|
+
storeUrl,
|
|
168
|
+
storeFrontAccessToken,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
if (result.data && result.data.length > 0) {
|
|
173
|
+
const formattedProducts = result?.data?.map((product) => ({
|
|
174
|
+
id: mobileComponents.getIdFromGid(product.id),
|
|
175
|
+
handle: product?.handle,
|
|
176
|
+
variants: product?.variants?.edges?.map((variant) => ({
|
|
177
|
+
id: mobileComponents.getIdFromGid(variant?.node?.id),
|
|
178
|
+
handle: variant?.node?.handle,
|
|
179
|
+
price: variant?.node?.price,
|
|
180
|
+
})),
|
|
181
|
+
}));
|
|
182
|
+
setProducts(formattedProducts);
|
|
183
|
+
setError(result.error);
|
|
184
|
+
} else {
|
|
185
|
+
setProducts([]);
|
|
186
|
+
setError(result.error);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
setError(error.message);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setLoading(false);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
fetchData();
|
|
196
|
+
}, [ids, storeUrl, storeFrontAccessToken, fetchProducts]);
|
|
197
|
+
|
|
198
|
+
return { products, loading, error };
|
|
199
|
+
},
|
|
200
|
+
[fetchProducts],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Stable logging function that doesn't cause re-renders
|
|
204
|
+
const log = React.useCallback((message, data = null) => {
|
|
205
|
+
const timestamp = new Date().toISOString();
|
|
206
|
+
const logEntry = {
|
|
207
|
+
timestamp,
|
|
208
|
+
message,
|
|
209
|
+
data: data ? JSON.parse(JSON.stringify(data)) : null,
|
|
210
|
+
renderCount: renderCountRef.current,
|
|
211
|
+
};
|
|
212
|
+
logRef.current.push(logEntry);
|
|
213
|
+
|
|
214
|
+
// Keep only last 50 logs to prevent memory issues
|
|
215
|
+
if (logRef.current.length > 50) {
|
|
216
|
+
logRef.current = logRef.current.slice(-50);
|
|
217
|
+
}
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
220
|
+
// Create stable cart items reference to prevent infinite recreations
|
|
221
|
+
const stableCartItems = React.useMemo(() => {
|
|
222
|
+
const currentCartItems = variables?.cart?.items ?? [];
|
|
223
|
+
|
|
224
|
+
if (!currentCartItems || currentCartItems.length === 0) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const mappedItems = currentCartItems.map((item) => ({
|
|
229
|
+
productId: item.productId,
|
|
230
|
+
variantId: item.variantId,
|
|
231
|
+
quantity: item.quantity,
|
|
232
|
+
}));
|
|
233
|
+
|
|
234
|
+
return mappedItems;
|
|
235
|
+
}, [
|
|
236
|
+
(variables?.cart?.items || [])
|
|
237
|
+
?.map((item) => `${item.productId}-${item.variantId}-${item.quantity}`)
|
|
238
|
+
?.join(","),
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
const ids = React.useMemo(
|
|
242
|
+
() => (stableCartItems ?? []).map((item) => item.productId),
|
|
243
|
+
[stableCartItems],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const { products: cartItems } = useProductsCustom({
|
|
247
|
+
ids,
|
|
248
|
+
storeUrl: storeUrl,
|
|
249
|
+
storeFrontAccessToken: storeFrontAccessToken,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Render limiter to prevent infinite recreations
|
|
253
|
+
renderCountRef.current += 1;
|
|
254
|
+
|
|
255
|
+
// Create stable cart reference to prevent infinite recreations
|
|
256
|
+
const stableCart = React.useMemo(() => {
|
|
257
|
+
if (!variables?.cart) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!cartItems || cartItems.length === 0) {
|
|
262
|
+
return variables.cart;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const processedItems = (stableCartItems || []).map((item) => {
|
|
266
|
+
const _cartItem = cartItems?.find(
|
|
267
|
+
(cartItem) => mobileComponents.getIdFromGid(cartItem?.id) === item.productId,
|
|
268
|
+
);
|
|
269
|
+
if (!_cartItem) {
|
|
270
|
+
return item;
|
|
271
|
+
}
|
|
272
|
+
const _variant = _cartItem?.variants?.find(
|
|
273
|
+
(variant) => mobileComponents.getIdFromGid(variant?.id) === item?.variantId,
|
|
274
|
+
);
|
|
275
|
+
if (!_variant) {
|
|
276
|
+
return item;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const processedItem = {
|
|
280
|
+
...item,
|
|
281
|
+
handle: _cartItem?.handle,
|
|
282
|
+
price: _variant?.price,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return processedItem;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const result = {
|
|
289
|
+
...variables.cart,
|
|
290
|
+
items: processedItems,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
return result;
|
|
294
|
+
}, [stableCartItems, cartItems]);
|
|
295
|
+
|
|
296
|
+
const getDiscountForProductDiscount = ({
|
|
297
|
+
targetDiscount,
|
|
298
|
+
handle,
|
|
299
|
+
variantId,
|
|
300
|
+
selectedVariant,
|
|
301
|
+
}) => {
|
|
302
|
+
const isProductEntitledForProductDiscount = () => {
|
|
303
|
+
if (targetDiscount.entitled?.all) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (targetDiscount.entitled?.variants) {
|
|
308
|
+
if (variantId && targetDiscount.entitled.variants.includes(variantId)) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (targetDiscount.entitled?.products) {
|
|
314
|
+
if (handle && targetDiscount.entitled.products.includes(handle)) {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
};
|
|
320
|
+
if (!isProductEntitledForProductDiscount()) return 0;
|
|
321
|
+
|
|
322
|
+
if (targetDiscount.discountValue?.type === "DiscountAmount") {
|
|
323
|
+
return targetDiscount.discountValue.discountAmount.cents / 100;
|
|
324
|
+
} else if (targetDiscount.discountValue?.type === "DiscountPercentage") {
|
|
325
|
+
return (
|
|
326
|
+
parseFloat(selectedVariant.price.amount) *
|
|
327
|
+
targetDiscount.discountValue.percentage
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const getDiscountForMultiValueCollection = ({
|
|
333
|
+
targetDiscount,
|
|
334
|
+
handle,
|
|
335
|
+
selectedVariant,
|
|
336
|
+
}) => {
|
|
337
|
+
if (
|
|
338
|
+
!targetDiscount?.discountValue?.productDiscounts ||
|
|
339
|
+
targetDiscount.discountValue.productDiscounts.length === 0
|
|
340
|
+
)
|
|
341
|
+
return 0;
|
|
342
|
+
|
|
343
|
+
const entitledProductDiscounts =
|
|
344
|
+
targetDiscount.discountValue.productDiscounts.filter((discount) =>
|
|
345
|
+
(discount.products ?? []).includes(handle),
|
|
346
|
+
) || [];
|
|
347
|
+
|
|
348
|
+
return entitledProductDiscounts.reduce((max, discountValue) => {
|
|
349
|
+
let discountAmount = 0;
|
|
350
|
+
|
|
351
|
+
if (discountValue.value.type === "DiscountAmount") {
|
|
352
|
+
discountAmount = discountValue.value.discountAmount.cents / 100;
|
|
353
|
+
} else if (discountValue.value.type === "DiscountPercentage") {
|
|
354
|
+
discountAmount =
|
|
355
|
+
parseFloat(selectedVariant.price.amount) *
|
|
356
|
+
(discountValue.value.percentage / 100);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return Math.max(max, discountAmount);
|
|
360
|
+
}, 0);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const getDiscountForMultiValueProduct = ({
|
|
364
|
+
targetDiscount,
|
|
365
|
+
variantId,
|
|
366
|
+
selectedVariant,
|
|
367
|
+
}) => {
|
|
368
|
+
if (!variantId) return 0;
|
|
369
|
+
if (
|
|
370
|
+
!targetDiscount?.discountValue?.variantDiscounts ||
|
|
371
|
+
targetDiscount?.discountValue?.variantDiscounts.length === 0
|
|
372
|
+
)
|
|
373
|
+
return 0;
|
|
374
|
+
|
|
375
|
+
const entitledVariantDiscounts =
|
|
376
|
+
targetDiscount.discountValue.variantDiscounts.filter((discount) =>
|
|
377
|
+
(discount.variants ?? []).includes(variantId),
|
|
378
|
+
) ?? [];
|
|
379
|
+
|
|
380
|
+
return entitledVariantDiscounts.reduce((max, discountValue) => {
|
|
381
|
+
let discountAmount = 0;
|
|
382
|
+
|
|
383
|
+
if (discountValue.value.type === "DiscountAmount") {
|
|
384
|
+
discountAmount = discountValue.value.discountAmount.cents / 100;
|
|
385
|
+
} else if (discountValue.value.type === "DiscountPercentage") {
|
|
386
|
+
discountAmount =
|
|
387
|
+
parseFloat(selectedVariant.price.amount) *
|
|
388
|
+
(discountValue.value.percentage / 100);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return Math.max(max, discountAmount);
|
|
392
|
+
}, 0);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const getDiscountForTierDiscount = ({
|
|
396
|
+
targetDiscount,
|
|
397
|
+
handle,
|
|
398
|
+
variantId,
|
|
399
|
+
product,
|
|
400
|
+
selectedVariant,
|
|
401
|
+
cart,
|
|
402
|
+
}) => {
|
|
403
|
+
if (!targetDiscount.discountValue?.tiers) return 0;
|
|
404
|
+
if (targetDiscount.discountValue?.tiersType === "FREE_GIFT") return 0;
|
|
405
|
+
|
|
406
|
+
const currentCollectionHandle = product.sourceCollection?.handle;
|
|
407
|
+
const isProductEntitledToTierDiscount = () => {
|
|
408
|
+
if (targetDiscount.discountValue?.entitled?.all) {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (targetDiscount.discountValue?.entitled?.collections) {
|
|
413
|
+
if (
|
|
414
|
+
currentCollectionHandle &&
|
|
415
|
+
targetDiscount.discountValue?.entitled?.collections.includes(
|
|
416
|
+
currentCollectionHandle,
|
|
417
|
+
)
|
|
418
|
+
) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (targetDiscount.discountValue?.entitled?.products) {
|
|
424
|
+
if (
|
|
425
|
+
handle &&
|
|
426
|
+
targetDiscount.discountValue?.entitled?.products.includes(handle)
|
|
427
|
+
) {
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (targetDiscount.discountValue?.entitled?.variants) {
|
|
433
|
+
if (
|
|
434
|
+
variantId &&
|
|
435
|
+
targetDiscount.discountValue?.entitled?.variants.includes(variantId)
|
|
436
|
+
) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return false;
|
|
442
|
+
};
|
|
443
|
+
if (!isProductEntitledToTierDiscount()) return 0;
|
|
444
|
+
|
|
445
|
+
const getBiggestEligibleTier = () => {
|
|
446
|
+
const isItemEligibleWithSelectedPurchaseType = (item, discount) => {
|
|
447
|
+
if (
|
|
448
|
+
discount.appliesOnSubscription &&
|
|
449
|
+
!discount.appliesOnOneTimePurchase
|
|
450
|
+
) {
|
|
451
|
+
return item.sellingPlanAllocation; //TODO Verify correct structure of selling plan
|
|
452
|
+
} else if (
|
|
453
|
+
!discount.appliesOnSubscription &&
|
|
454
|
+
discount.appliesOnOneTimePurchase
|
|
455
|
+
) {
|
|
456
|
+
return !item.sellingPlanAllocation?.name;
|
|
457
|
+
} else {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
const isCartItemEntitledToGiveDiscount = (item) => {
|
|
462
|
+
if (!targetDiscount || !targetDiscount.entitled) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (targetDiscount.entitled.all) {
|
|
467
|
+
return isItemEligibleWithSelectedPurchaseType(item, targetDiscount);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (targetDiscount.entitled.variants) {
|
|
471
|
+
if (
|
|
472
|
+
targetDiscount.entitled.variants?.includes(
|
|
473
|
+
item.variantId.toString(),
|
|
474
|
+
)
|
|
475
|
+
) {
|
|
476
|
+
return isItemEligibleWithSelectedPurchaseType(item, targetDiscount);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (targetDiscount.entitled.products) {
|
|
481
|
+
if (targetDiscount.entitled.products?.includes(item.handle)) {
|
|
482
|
+
return isItemEligibleWithSelectedPurchaseType(item, targetDiscount);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return false;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const getEntitledQuantityAndTotal = () => {
|
|
490
|
+
const entitledItems = (cart?.items || []).filter((item) => {
|
|
491
|
+
return isCartItemEntitledToGiveDiscount(item);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const { entitledQuantity, entitledTotal } = entitledItems.reduce(
|
|
495
|
+
(acc, item) => {
|
|
496
|
+
acc.entitledQuantity += item.quantity;
|
|
497
|
+
acc.entitledTotal +=
|
|
498
|
+
parseFloat((item.price?.amount ?? 0) * 100) * item.quantity;
|
|
499
|
+
|
|
500
|
+
return acc;
|
|
501
|
+
},
|
|
502
|
+
{ entitledQuantity: 0, entitledTotal: 0 },
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
return { entitledQuantity, entitledTotal };
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const isPrerequisiteValid = (prerequisite, quantity, subtotal) => {
|
|
509
|
+
if (prerequisite?.minimumRequirement?.greaterThanOrEqualToQuantity) {
|
|
510
|
+
if (
|
|
511
|
+
Number(quantity) <
|
|
512
|
+
Number(prerequisite.minimumRequirement.greaterThanOrEqualToQuantity)
|
|
513
|
+
) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (prerequisite?.minimumRequirement?.greaterThanOrEqualToSubtotal) {
|
|
519
|
+
if (
|
|
520
|
+
subtotal <
|
|
521
|
+
prerequisite.minimumRequirement.greaterThanOrEqualToSubtotal.cents
|
|
522
|
+
) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return true;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const { entitledQuantity, entitledTotal } = getEntitledQuantityAndTotal();
|
|
531
|
+
|
|
532
|
+
let currentBiggestTierIndex = -1;
|
|
533
|
+
for (let i = 0; i < targetDiscount.discountValue?.tiers?.length; i++) {
|
|
534
|
+
const currentTier = targetDiscount.discountValue.tiers[i];
|
|
535
|
+
if (
|
|
536
|
+
isPrerequisiteValid(
|
|
537
|
+
currentTier.prerequisite,
|
|
538
|
+
entitledQuantity,
|
|
539
|
+
entitledTotal,
|
|
540
|
+
)
|
|
541
|
+
) {
|
|
542
|
+
currentBiggestTierIndex = i;
|
|
543
|
+
} else {
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return currentBiggestTierIndex;
|
|
548
|
+
};
|
|
549
|
+
const biggestEligibleTierIndex = getBiggestEligibleTier();
|
|
550
|
+
|
|
551
|
+
if (biggestEligibleTierIndex < 0) return 0;
|
|
552
|
+
|
|
553
|
+
const biggestEligibleTier =
|
|
554
|
+
targetDiscount.discountValue.tiers[biggestEligibleTierIndex];
|
|
555
|
+
if (!biggestEligibleTier || !biggestEligibleTier.value) return 0;
|
|
556
|
+
|
|
557
|
+
if (
|
|
558
|
+
biggestEligibleTier.value.type === "DiscountPercentage" &&
|
|
559
|
+
biggestEligibleTier.value.percentage
|
|
560
|
+
) {
|
|
561
|
+
const discountAmount =
|
|
562
|
+
parseFloat(selectedVariant.price.amount) *
|
|
563
|
+
(biggestEligibleTier.value.percentage / 100);
|
|
564
|
+
|
|
565
|
+
return discountAmount;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const getDiscountForVolumeDiscount = ({
|
|
570
|
+
targetDiscount,
|
|
571
|
+
handle,
|
|
572
|
+
variantId,
|
|
573
|
+
product,
|
|
574
|
+
selectedVariant,
|
|
575
|
+
cart,
|
|
576
|
+
}) => {
|
|
577
|
+
if (!targetDiscount.discountValue?.tiers) return 0;
|
|
578
|
+
if (targetDiscount.discountValue?.tiersType === "FREE_GIFT") return 0;
|
|
579
|
+
|
|
580
|
+
const currentCollectionHandle = product.sourceCollection?.handle;
|
|
581
|
+
const isProductEntitledToVolumeDiscount = () => {
|
|
582
|
+
if (targetDiscount.discountValue?.entitled?.all) {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (targetDiscount.discountValue?.entitled?.collections) {
|
|
587
|
+
if (
|
|
588
|
+
currentCollectionHandle &&
|
|
589
|
+
targetDiscount.discountValue?.entitled?.collections.includes(
|
|
590
|
+
currentCollectionHandle,
|
|
591
|
+
)
|
|
592
|
+
) {
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (targetDiscount?.entitled?.products) {
|
|
598
|
+
if (handle && targetDiscount?.entitled?.products.includes(handle)) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (targetDiscount?.entitled?.variants) {
|
|
604
|
+
if (
|
|
605
|
+
variantId &&
|
|
606
|
+
targetDiscount?.entitled?.variants.includes(variantId)
|
|
607
|
+
) {
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return false;
|
|
613
|
+
};
|
|
614
|
+
if (!isProductEntitledToVolumeDiscount()) return 0;
|
|
615
|
+
|
|
616
|
+
const getBiggestEligibleTier = () => {
|
|
617
|
+
const isItemEligibleWithSelectedPurchaseType = (item, discount) => {
|
|
618
|
+
if (
|
|
619
|
+
discount.appliesOnSubscription &&
|
|
620
|
+
!discount.appliesOnOneTimePurchase
|
|
621
|
+
) {
|
|
622
|
+
return item.sellingPlanAllocation; //TODO Verify correct structure of selling plan
|
|
623
|
+
} else if (
|
|
624
|
+
!discount.appliesOnSubscription &&
|
|
625
|
+
discount.appliesOnOneTimePurchase
|
|
626
|
+
) {
|
|
627
|
+
return !item.sellingPlanAllocation?.name;
|
|
628
|
+
} else {
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
const isCartItemEntitledToGiveDiscount = (item) => {
|
|
633
|
+
if (!targetDiscount || !targetDiscount.entitled) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (targetDiscount.entitled.all) {
|
|
638
|
+
return isItemEligibleWithSelectedPurchaseType(item, targetDiscount);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (targetDiscount.entitled.variants) {
|
|
642
|
+
if (
|
|
643
|
+
targetDiscount.entitled.variants?.includes(
|
|
644
|
+
item.variantId.toString(),
|
|
645
|
+
)
|
|
646
|
+
) {
|
|
647
|
+
return isItemEligibleWithSelectedPurchaseType(item, targetDiscount);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (targetDiscount.entitled.products) {
|
|
652
|
+
if (targetDiscount.entitled.products?.includes(item.handle)) {
|
|
653
|
+
return isItemEligibleWithSelectedPurchaseType(item, targetDiscount);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return false;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const getEntitledQuantityAndTotal = () => {
|
|
661
|
+
const entitledItems = (cart?.items || []).filter((item) => {
|
|
662
|
+
return isCartItemEntitledToGiveDiscount(item);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const { entitledQuantity, entitledTotal } = entitledItems.reduce(
|
|
666
|
+
(acc, item) => {
|
|
667
|
+
acc.entitledQuantity += item.quantity;
|
|
668
|
+
acc.entitledTotal +=
|
|
669
|
+
parseFloat((item.price?.amount ?? 0) * 100) * item.quantity;
|
|
670
|
+
|
|
671
|
+
return acc;
|
|
672
|
+
},
|
|
673
|
+
{ entitledQuantity: 0, entitledTotal: 0 },
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
return { entitledQuantity, entitledTotal };
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const isPrerequisiteValid = (prerequisite, quantity, subtotal) => {
|
|
680
|
+
if (prerequisite?.minimumRequirement?.greaterThanOrEqualToQuantity) {
|
|
681
|
+
if (
|
|
682
|
+
Number(quantity) <
|
|
683
|
+
Number(prerequisite.minimumRequirement.greaterThanOrEqualToQuantity)
|
|
684
|
+
) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (prerequisite?.minimumRequirement?.greaterThanOrEqualToSubtotal) {
|
|
690
|
+
if (
|
|
691
|
+
subtotal <
|
|
692
|
+
prerequisite.minimumRequirement.greaterThanOrEqualToSubtotal.cents
|
|
693
|
+
) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return true;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const { entitledQuantity, entitledTotal } = getEntitledQuantityAndTotal();
|
|
702
|
+
|
|
703
|
+
let currentBiggestTierIndex = -1;
|
|
704
|
+
for (let i = 0; i < targetDiscount.discountValue?.tiers?.length; i++) {
|
|
705
|
+
const currentTier = targetDiscount.discountValue.tiers[i];
|
|
706
|
+
if (
|
|
707
|
+
isPrerequisiteValid(
|
|
708
|
+
currentTier.prerequisite,
|
|
709
|
+
entitledQuantity,
|
|
710
|
+
entitledTotal,
|
|
711
|
+
)
|
|
712
|
+
) {
|
|
713
|
+
currentBiggestTierIndex = i;
|
|
714
|
+
} else {
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return currentBiggestTierIndex;
|
|
719
|
+
};
|
|
720
|
+
const biggestEligibleTierIndex = getBiggestEligibleTier();
|
|
721
|
+
|
|
722
|
+
if (biggestEligibleTierIndex < 0) return 0;
|
|
723
|
+
|
|
724
|
+
const biggestEligibleTier =
|
|
725
|
+
targetDiscount.discountValue.tiers[biggestEligibleTierIndex];
|
|
726
|
+
if (!biggestEligibleTier || !biggestEligibleTier.value) return 0;
|
|
727
|
+
|
|
728
|
+
if (
|
|
729
|
+
biggestEligibleTier.value.type === "DiscountPercentage" &&
|
|
730
|
+
biggestEligibleTier.value.percentage
|
|
731
|
+
) {
|
|
732
|
+
const discountAmount =
|
|
733
|
+
parseFloat(selectedVariant.price.amount) *
|
|
734
|
+
(biggestEligibleTier.value.percentage / 100);
|
|
735
|
+
|
|
736
|
+
return discountAmount;
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
const getDiscountForMultiEffectTiers = ({
|
|
741
|
+
targetDiscount,
|
|
742
|
+
handle,
|
|
743
|
+
variantId,
|
|
744
|
+
product,
|
|
745
|
+
selectedVariant,
|
|
746
|
+
cart,
|
|
747
|
+
log,
|
|
748
|
+
}) => {
|
|
749
|
+
const isProductEntitledToDiscount = (discount) => {
|
|
750
|
+
if (discount.entitled?.all) {
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (discount.entitled?.collections) {
|
|
755
|
+
if (
|
|
756
|
+
currentCollectionHandle &&
|
|
757
|
+
discount.entitled?.collections.includes(currentCollectionHandle)
|
|
758
|
+
) {
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (discount.entitled?.products) {
|
|
764
|
+
if (handle && discount.entitled?.products.includes(handle)) {
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (discount.entitled?.variants) {
|
|
770
|
+
if (variantId && discount.entitled?.variants.includes(variantId)) {
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return false;
|
|
776
|
+
};
|
|
777
|
+
const getBiggestEligibleTier = () => {
|
|
778
|
+
const isItemEligibleWithSelectedPurchaseType = (item, discount) => {
|
|
779
|
+
if (
|
|
780
|
+
discount.appliesOnSubscription &&
|
|
781
|
+
!discount.appliesOnOneTimePurchase
|
|
782
|
+
) {
|
|
783
|
+
return item.sellingPlanAllocation; //TODO Verify correct structure of selling plan
|
|
784
|
+
} else if (
|
|
785
|
+
!discount.appliesOnSubscription &&
|
|
786
|
+
discount.appliesOnOneTimePurchase
|
|
787
|
+
) {
|
|
788
|
+
return !item.sellingPlanAllocation?.name;
|
|
789
|
+
} else {
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
const isCartItemEntitledToGiveDiscount = (item) => {
|
|
794
|
+
if (!targetDiscount || !targetDiscount.entitled) {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (targetDiscount.entitled.all) {
|
|
799
|
+
return isItemEligibleWithSelectedPurchaseType(item, targetDiscount);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (targetDiscount.entitled.variants) {
|
|
803
|
+
if (
|
|
804
|
+
targetDiscount.entitled.variants?.includes(
|
|
805
|
+
item.variantId.toString(),
|
|
806
|
+
)
|
|
807
|
+
) {
|
|
808
|
+
return isItemEligibleWithSelectedPurchaseType(item, targetDiscount);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (targetDiscount.entitled.products) {
|
|
813
|
+
return isItemEligibleWithSelectedPurchaseType(item, targetDiscount);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return false;
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
const getEntitledQuantityAndTotal = () => {
|
|
820
|
+
const entitledItems = (cart?.items || []).filter((item) => {
|
|
821
|
+
return isCartItemEntitledToGiveDiscount(item);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const { entitledQuantity, entitledTotal } = entitledItems.reduce(
|
|
825
|
+
(acc, item) => {
|
|
826
|
+
acc.entitledQuantity += item.quantity;
|
|
827
|
+
acc.entitledTotal +=
|
|
828
|
+
parseFloat((item.price?.amount ?? 0) * 100) * item.quantity;
|
|
829
|
+
|
|
830
|
+
return acc;
|
|
831
|
+
},
|
|
832
|
+
{ entitledQuantity: 0, entitledTotal: 0 },
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
return { entitledQuantity, entitledTotal };
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
const isPrerequisiteValid = (prerequisite, quantity, subtotal) => {
|
|
839
|
+
if (prerequisite?.minimumRequirement?.greaterThanOrEqualToQuantity) {
|
|
840
|
+
if (
|
|
841
|
+
Number(quantity) <
|
|
842
|
+
Number(prerequisite.minimumRequirement.greaterThanOrEqualToQuantity)
|
|
843
|
+
) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (prerequisite?.minimumRequirement?.greaterThanOrEqualToSubtotal) {
|
|
849
|
+
if (
|
|
850
|
+
subtotal <
|
|
851
|
+
prerequisite.minimumRequirement.greaterThanOrEqualToSubtotal.cents
|
|
852
|
+
) {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return true;
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
log("getEntitledQuantityAndTotal");
|
|
861
|
+
const { entitledQuantity, entitledTotal } = getEntitledQuantityAndTotal();
|
|
862
|
+
|
|
863
|
+
log("getEntitledQuantityAndTotal", { entitledQuantity, entitledTotal });
|
|
864
|
+
|
|
865
|
+
let currentBiggestTierIndex = -1;
|
|
866
|
+
for (let i = 0; i < targetDiscount.discountValue?.tiers?.length; i++) {
|
|
867
|
+
const currentTier = targetDiscount.discountValue.tiers[i];
|
|
868
|
+
if (
|
|
869
|
+
isPrerequisiteValid(
|
|
870
|
+
currentTier.prerequisite,
|
|
871
|
+
entitledQuantity,
|
|
872
|
+
entitledTotal,
|
|
873
|
+
)
|
|
874
|
+
) {
|
|
875
|
+
currentBiggestTierIndex = i;
|
|
876
|
+
} else {
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return currentBiggestTierIndex;
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
if (
|
|
884
|
+
!targetDiscount.discountValue?.tiers ||
|
|
885
|
+
targetDiscount.discountValue?.tiers.length === 0
|
|
886
|
+
)
|
|
887
|
+
return 0;
|
|
888
|
+
|
|
889
|
+
log("getBiggestEligibleTier");
|
|
890
|
+
const currentCollectionHandle = product?.sourceCollection?.handle;
|
|
891
|
+
const biggestEligibleTierIndex = getBiggestEligibleTier();
|
|
892
|
+
log("getBiggestEligibleTier", { biggestEligibleTierIndex });
|
|
893
|
+
|
|
894
|
+
if (biggestEligibleTierIndex < 0) return 0;
|
|
895
|
+
|
|
896
|
+
const productDiscountsAvailableOnTier =
|
|
897
|
+
targetDiscount.discountValue.tiers
|
|
898
|
+
.slice(0, biggestEligibleTierIndex + 1)
|
|
899
|
+
?.filter((tier) =>
|
|
900
|
+
tier.discounts.some(
|
|
901
|
+
(discount) => discount.type === "ProductDiscount",
|
|
902
|
+
),
|
|
903
|
+
)
|
|
904
|
+
?.flatMap(
|
|
905
|
+
(tier) =>
|
|
906
|
+
tier.discounts.find(
|
|
907
|
+
(discount) => discount.type === "ProductDiscount",
|
|
908
|
+
)?.value ?? [],
|
|
909
|
+
) ?? [];
|
|
910
|
+
|
|
911
|
+
const eligibleProductDiscounts = productDiscountsAvailableOnTier.filter(
|
|
912
|
+
(discount) => isProductEntitledToDiscount(discount),
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
const maxDiscountAmount = eligibleProductDiscounts.reduce(
|
|
916
|
+
(max, discount) => {
|
|
917
|
+
let discountAmount = 0;
|
|
918
|
+
if (discount.value.type === "DiscountAmount") {
|
|
919
|
+
discountAmount = parseFloat(discount.value.discountAmount.amount);
|
|
920
|
+
} else if (discount.value.type === "DiscountPercentage") {
|
|
921
|
+
discountAmount =
|
|
922
|
+
parseFloat(selectedVariant.price.amount) *
|
|
923
|
+
(discount.value.percentage / 100);
|
|
924
|
+
}
|
|
925
|
+
return Math.max(max, discountAmount);
|
|
926
|
+
},
|
|
927
|
+
0,
|
|
928
|
+
);
|
|
929
|
+
return maxDiscountAmount;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const getDiscountValueForDiscount = ({
|
|
933
|
+
product,
|
|
934
|
+
selectedVariant,
|
|
935
|
+
cart,
|
|
936
|
+
discount,
|
|
937
|
+
log,
|
|
938
|
+
}) => {
|
|
939
|
+
log("getDiscountValueForDiscount");
|
|
940
|
+
const handle = product?.handle;
|
|
941
|
+
log("handle", { handle });
|
|
942
|
+
log("crudeva", { selectedVariant });
|
|
943
|
+
const variantId = mobileComponents.getIdFromGid(selectedVariant?.id);
|
|
944
|
+
log("variantId", { variantId });
|
|
945
|
+
|
|
946
|
+
if (!handle || !discount) return 0;
|
|
947
|
+
log("handle and discount exist");
|
|
948
|
+
|
|
949
|
+
if (discount.discountValue?.type === "DiscountCodeBxgy") {
|
|
950
|
+
return 0;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (discount.discountValue?.type === "Gift") {
|
|
954
|
+
return 0;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (discount.discountValue?.type === "DiscountMultiValueProduct") {
|
|
958
|
+
return getDiscountForMultiValueProduct({
|
|
959
|
+
targetDiscount: discount,
|
|
960
|
+
variantId,
|
|
961
|
+
selectedVariant,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (discount.discountValue?.type === "DiscountMultiValueCollection") {
|
|
966
|
+
return getDiscountForMultiValueCollection({
|
|
967
|
+
targetDiscount: discount,
|
|
968
|
+
handle,
|
|
969
|
+
selectedVariant,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (discount.discountValue?.type === "TieredDiscount") {
|
|
974
|
+
return getDiscountForTierDiscount({
|
|
975
|
+
targetDiscount: discount,
|
|
976
|
+
handle,
|
|
977
|
+
variantId,
|
|
978
|
+
product,
|
|
979
|
+
selectedVariant,
|
|
980
|
+
cart,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (discount.discountValue?.type === "VolumeDiscount") {
|
|
985
|
+
return getDiscountForVolumeDiscount({
|
|
986
|
+
targetDiscount: discount,
|
|
987
|
+
handle,
|
|
988
|
+
variantId,
|
|
989
|
+
product,
|
|
990
|
+
selectedVariant,
|
|
991
|
+
cart,
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (discount.discountValue?.type === "CollectionOrderDiscount") {
|
|
996
|
+
return 0;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (discount.discountValue?.type === "MultiEffectTiers") {
|
|
1000
|
+
log("getDiscountForMultiEffectTiers");
|
|
1001
|
+
return getDiscountForMultiEffectTiers({
|
|
1002
|
+
targetDiscount: discount,
|
|
1003
|
+
handle,
|
|
1004
|
+
variantId,
|
|
1005
|
+
product,
|
|
1006
|
+
selectedVariant,
|
|
1007
|
+
cart,
|
|
1008
|
+
log,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (discount.discountValue?.type === "ProductDiscount") {
|
|
1013
|
+
return getDiscountForProductDiscount({
|
|
1014
|
+
targetDiscount: discount,
|
|
1015
|
+
handle,
|
|
1016
|
+
variantId,
|
|
1017
|
+
selectedVariant,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (discount.discountClass === "ORDER") {
|
|
1022
|
+
return 0;
|
|
1023
|
+
}
|
|
1024
|
+
if (discount.discountClass === "SHIPPING") {
|
|
1025
|
+
return 0;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (discount.discountClass === "PRODUCT") {
|
|
1029
|
+
return getDiscountForProductDiscount({
|
|
1030
|
+
targetDiscount: discount,
|
|
1031
|
+
handle,
|
|
1032
|
+
variantId,
|
|
1033
|
+
selectedVariant,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return 0;
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
const getBiggestDiscount = ({ product, selectedVariant, cart, log }) => {
|
|
1041
|
+
const discounts = Object.values(activePromotion?.discounts ?? []);
|
|
1042
|
+
log("Calculating biggest discount", {
|
|
1043
|
+
discountCount: discounts.length,
|
|
1044
|
+
productId: product?.handle,
|
|
1045
|
+
collectionId: product?.sourceCollection?.handle,
|
|
1046
|
+
variantId: selectedVariant?.id,
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
const biggestDiscount = discounts.reduce((max, discount) => {
|
|
1050
|
+
const discountValue = getDiscountValueForDiscount({
|
|
1051
|
+
product,
|
|
1052
|
+
selectedVariant,
|
|
1053
|
+
cart,
|
|
1054
|
+
discount,
|
|
1055
|
+
log,
|
|
1056
|
+
});
|
|
1057
|
+
log("Discount calculated", {
|
|
1058
|
+
discountType: discount.discountValue?.type,
|
|
1059
|
+
discountValue,
|
|
1060
|
+
maxSoFar: max,
|
|
1061
|
+
});
|
|
1062
|
+
return Math.max(max, discountValue);
|
|
1063
|
+
}, 0);
|
|
1064
|
+
|
|
1065
|
+
log("Final biggest discount", { biggestDiscount });
|
|
1066
|
+
return biggestDiscount;
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
const parsePromotionIfActive = (vaultActivePromotion) => {
|
|
1070
|
+
let promotion = JSON.parse(vaultActivePromotion ?? "{}");
|
|
1071
|
+
if (!promotion.startsAt || !promotion.status || !promotion.title)
|
|
1072
|
+
return undefined;
|
|
1073
|
+
const now = new Date();
|
|
1074
|
+
const startsAt = new Date(promotion.startsAt);
|
|
1075
|
+
const endsAt = promotion.endsAt ? new Date(promotion.endsAt) : null;
|
|
1076
|
+
|
|
1077
|
+
if (
|
|
1078
|
+
(promotion.status === "ACTIVE" || promotion.status === "SCHEDULED") &&
|
|
1079
|
+
now >= startsAt &&
|
|
1080
|
+
(!endsAt || now <= endsAt)
|
|
1081
|
+
) {
|
|
1082
|
+
return promotion;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
return undefined;
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
React.useEffect(() => {
|
|
1089
|
+
async function initializeVault() {
|
|
1090
|
+
try {
|
|
1091
|
+
if (!actions?.vault) return;
|
|
1092
|
+
|
|
1093
|
+
log("Initializing vault");
|
|
1094
|
+
const vault = await actions.vault.initialize("abra", "global");
|
|
1095
|
+
const vaultActivePromotion = await vault.get("active-promotion");
|
|
1096
|
+
log("Vault initialized, active promotion retrieved", {
|
|
1097
|
+
vaultActivePromotion,
|
|
1098
|
+
});
|
|
1099
|
+
setActivePromotion(parsePromotionIfActive(vaultActivePromotion));
|
|
1100
|
+
|
|
1101
|
+
vault.observe(
|
|
1102
|
+
"active-promotion",
|
|
1103
|
+
(newPromotionValue) => {
|
|
1104
|
+
log("Vault promotion changed", { newPromotionValue });
|
|
1105
|
+
setActivePromotion(parsePromotionIfActive(newPromotionValue));
|
|
1106
|
+
},
|
|
1107
|
+
(error) => {
|
|
1108
|
+
log("Vault observe error", { error: error.message });
|
|
1109
|
+
},
|
|
1110
|
+
);
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
log("Vault initialization error", { error: error.message });
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
initializeVault();
|
|
1116
|
+
}, [actions?.vault]);
|
|
1117
|
+
|
|
1118
|
+
let getProductWithDiscount = React.useCallback(
|
|
1119
|
+
(product) => {
|
|
1120
|
+
if (!activePromotion || !activePromotion.title || !product) {
|
|
1121
|
+
log("No active promotion or product, returning original product", {
|
|
1122
|
+
hasActivePromotion: !!activePromotion,
|
|
1123
|
+
hasProduct: !!product,
|
|
1124
|
+
});
|
|
1125
|
+
return product;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const _product = JSON.parse(JSON.stringify(product));
|
|
1129
|
+
let currencyCode = undefined;
|
|
1130
|
+
const variants = _product.variants.map((variant) => {
|
|
1131
|
+
const selectedVariant = JSON.parse(JSON.stringify(variant));
|
|
1132
|
+
currencyCode = currencyCode || selectedVariant?.price?.currencyCode;
|
|
1133
|
+
let biggestDiscount = 0;
|
|
1134
|
+
try {
|
|
1135
|
+
biggestDiscount = getBiggestDiscount({
|
|
1136
|
+
product: _product,
|
|
1137
|
+
selectedVariant,
|
|
1138
|
+
cart: stableCart,
|
|
1139
|
+
log,
|
|
1140
|
+
});
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
log("Error calculating discount", {
|
|
1143
|
+
error: error.message,
|
|
1144
|
+
variantId: selectedVariant?.id,
|
|
1145
|
+
});
|
|
1146
|
+
biggestDiscount = 0;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const originalPrice = parseFloat(selectedVariant?.price?.amount ?? 0);
|
|
1150
|
+
const compareAtPrice = parseFloat(
|
|
1151
|
+
selectedVariant?.compareAtPrice?.amount ?? 0,
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
let referencePrice = originalPrice;
|
|
1155
|
+
if (compareAtPrice) {
|
|
1156
|
+
referencePrice = Math.max(originalPrice, compareAtPrice ?? 0);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
let discountedPrice = Math.max(originalPrice - biggestDiscount, 0);
|
|
1160
|
+
|
|
1161
|
+
selectedVariant.price = {
|
|
1162
|
+
amount: discountedPrice,
|
|
1163
|
+
currencyCode: selectedVariant.price.currencyCode,
|
|
1164
|
+
};
|
|
1165
|
+
selectedVariant.compareAtPrice = {
|
|
1166
|
+
amount: referencePrice,
|
|
1167
|
+
currencyCode: selectedVariant.price.currencyCode,
|
|
1168
|
+
};
|
|
1169
|
+
return selectedVariant;
|
|
1170
|
+
});
|
|
1171
|
+
_product.variants = variants;
|
|
1172
|
+
currencyCode = currencyCode || "USD";
|
|
1173
|
+
_product.compareAtPriceRange = {
|
|
1174
|
+
maxVariantPrice: {
|
|
1175
|
+
amount: variants.reduce((max, variant) => {
|
|
1176
|
+
return Math.max(
|
|
1177
|
+
max,
|
|
1178
|
+
parseFloat(variant.compareAtPrice.amount ?? variant.price.amount),
|
|
1179
|
+
);
|
|
1180
|
+
}, 0),
|
|
1181
|
+
currencyCode: currencyCode,
|
|
1182
|
+
},
|
|
1183
|
+
minVariantPrice: {
|
|
1184
|
+
amount: variants.reduce((min, variant) => {
|
|
1185
|
+
return Math.min(
|
|
1186
|
+
min,
|
|
1187
|
+
parseFloat(variant.compareAtPrice.amount ?? variant.price.amount),
|
|
1188
|
+
);
|
|
1189
|
+
}, Infinity),
|
|
1190
|
+
currencyCode: currencyCode,
|
|
1191
|
+
},
|
|
1192
|
+
};
|
|
1193
|
+
_product.priceRange = {
|
|
1194
|
+
maxVariantPrice: {
|
|
1195
|
+
amount: variants.reduce((max, variant) => {
|
|
1196
|
+
return Math.max(max, parseFloat(variant.price.amount));
|
|
1197
|
+
}, 0),
|
|
1198
|
+
currencyCode: currencyCode || "USD",
|
|
1199
|
+
},
|
|
1200
|
+
minVariantPrice: {
|
|
1201
|
+
amount: variants.reduce((min, variant) => {
|
|
1202
|
+
return Math.min(min, parseFloat(variant.price.amount));
|
|
1203
|
+
}, Infinity),
|
|
1204
|
+
currencyCode: currencyCode || "USD",
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
_product.abra = true;
|
|
1208
|
+
|
|
1209
|
+
return _product;
|
|
1210
|
+
},
|
|
1211
|
+
[activePromotion?.title, stableCart],
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
return React.useMemo(
|
|
1215
|
+
() => ({
|
|
1216
|
+
abra: {
|
|
1217
|
+
activePromotion,
|
|
1218
|
+
getProductWithDiscount,
|
|
1219
|
+
logs: logRef.current,
|
|
1220
|
+
},
|
|
1221
|
+
}),
|
|
1222
|
+
[activePromotion, getProductWithDiscount],
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const SHOP_METAFIELD_QUERY = `
|
|
1227
|
+
query ShopMetafield($namespace: String!, $key: String!) {
|
|
1228
|
+
shop {
|
|
1229
|
+
metafield(namespace: $namespace, key: $key) {
|
|
1230
|
+
id
|
|
1231
|
+
key
|
|
1232
|
+
namespace
|
|
1233
|
+
updatedAt
|
|
1234
|
+
value
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
`;
|
|
1239
|
+
|
|
1240
|
+
const SHOP_METAFIELDS_QUERY = `
|
|
1241
|
+
query ShopMetafields($identifiers: [HasMetafieldsIdentifier!]!) {
|
|
1242
|
+
shop {
|
|
1243
|
+
metafields(identifiers: $identifiers) {
|
|
1244
|
+
id
|
|
1245
|
+
key
|
|
1246
|
+
namespace
|
|
1247
|
+
updatedAt
|
|
1248
|
+
value
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
`;
|
|
1253
|
+
|
|
1254
|
+
const graphqlDefaultBody$1 = (query, variables, storefrontAccessToken) => {
|
|
1255
|
+
return {
|
|
1256
|
+
async: true,
|
|
1257
|
+
crossDomain: true,
|
|
1258
|
+
method: "POST",
|
|
1259
|
+
headers: {
|
|
1260
|
+
"X-Shopify-Storefront-Access-Token": storefrontAccessToken,
|
|
1261
|
+
"Content-Type": "application/json",
|
|
1262
|
+
},
|
|
1263
|
+
body: JSON.stringify({
|
|
1264
|
+
query,
|
|
1265
|
+
variables,
|
|
1266
|
+
}),
|
|
1267
|
+
};
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
const fetchShopPromotionMetafield = async (
|
|
1271
|
+
key,
|
|
1272
|
+
storeUrl,
|
|
1273
|
+
storefrontAccessToken,
|
|
1274
|
+
) => {
|
|
1275
|
+
if (!storeUrl || !storefrontAccessToken) {
|
|
1276
|
+
return { error: "Missing required configuration" };
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const GRAPHQL_URL = `https://${storeUrl}/api/2024-04/graphql.json`;
|
|
1280
|
+
|
|
1281
|
+
try {
|
|
1282
|
+
const response = await fetch(
|
|
1283
|
+
GRAPHQL_URL,
|
|
1284
|
+
graphqlDefaultBody$1(
|
|
1285
|
+
SHOP_METAFIELD_QUERY,
|
|
1286
|
+
{ namespace: `$app:tapcart-promotions`, key },
|
|
1287
|
+
storefrontAccessToken,
|
|
1288
|
+
),
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
if (response?.ok) {
|
|
1292
|
+
const responseBody = await response.json();
|
|
1293
|
+
const metafieldValue = responseBody?.data?.shop?.metafield?.value;
|
|
1294
|
+
|
|
1295
|
+
if (!metafieldValue) {
|
|
1296
|
+
return { error: "Metafield value not found" };
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
try {
|
|
1300
|
+
return { data: JSON.parse(metafieldValue) };
|
|
1301
|
+
} catch (parseError) {
|
|
1302
|
+
return {
|
|
1303
|
+
error: `Failed to parse metafield value: ${parseError.message}`,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const error = await response.text();
|
|
1309
|
+
return { error };
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
return { error: err.message };
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
const fetchShopDiscountMetafields = async (
|
|
1316
|
+
keys,
|
|
1317
|
+
storeUrl,
|
|
1318
|
+
storefrontAccessToken,
|
|
1319
|
+
) => {
|
|
1320
|
+
if (!storeUrl || !storefrontAccessToken) {
|
|
1321
|
+
return { error: "Missing required configuration" };
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const GRAPHQL_URL = `https://${storeUrl}/api/2024-04/graphql.json`;
|
|
1325
|
+
const namespace = `$app:discounts`;
|
|
1326
|
+
try {
|
|
1327
|
+
const identifiers = keys.map((key) => ({ namespace, key }));
|
|
1328
|
+
const response = await fetch(
|
|
1329
|
+
GRAPHQL_URL,
|
|
1330
|
+
graphqlDefaultBody$1(
|
|
1331
|
+
SHOP_METAFIELDS_QUERY,
|
|
1332
|
+
{ identifiers },
|
|
1333
|
+
storefrontAccessToken,
|
|
1334
|
+
),
|
|
1335
|
+
);
|
|
1336
|
+
|
|
1337
|
+
if (response?.ok) {
|
|
1338
|
+
const responseBody = await response.json();
|
|
1339
|
+
const metafields = responseBody?.data?.shop?.metafields;
|
|
1340
|
+
|
|
1341
|
+
if (!metafields || !Array.isArray(metafields)) {
|
|
1342
|
+
return { error: "Metafields not found or invalid format" };
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
try {
|
|
1346
|
+
const parsedMetafields = metafields.map((metafield) => {
|
|
1347
|
+
if (!metafield?.value) {
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
`Metafield ${metafield?.key || "unknown"} has no value`,
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
return JSON.parse(metafield.value);
|
|
1353
|
+
});
|
|
1354
|
+
return { data: parsedMetafields };
|
|
1355
|
+
} catch (parseError) {
|
|
1356
|
+
return { error: `Failed to parse metafields: ${parseError.message}` };
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const error = await response.text();
|
|
1361
|
+
return { error };
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
return { error: err.message };
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
const usePromotion = (key, storeUrl, storefrontAccessToken) => {
|
|
1368
|
+
const [promotion, setPromotion] = React.useState(null);
|
|
1369
|
+
const [loading, setLoading] = React.useState(true);
|
|
1370
|
+
const [error, setError] = React.useState(null);
|
|
1371
|
+
|
|
1372
|
+
React.useEffect(() => {
|
|
1373
|
+
const fetchData = async () => {
|
|
1374
|
+
const result = await fetchShopPromotionMetafield(
|
|
1375
|
+
key,
|
|
1376
|
+
storeUrl,
|
|
1377
|
+
storefrontAccessToken,
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1380
|
+
if (result.data) {
|
|
1381
|
+
const metafieldData = result.data;
|
|
1382
|
+
|
|
1383
|
+
if (
|
|
1384
|
+
metafieldData?.discountCodes &&
|
|
1385
|
+
Array.isArray(metafieldData.discountCodes)
|
|
1386
|
+
) {
|
|
1387
|
+
const metafieldsResult = await fetchShopDiscountMetafields(
|
|
1388
|
+
metafieldData.discountCodes,
|
|
1389
|
+
storeUrl,
|
|
1390
|
+
storefrontAccessToken,
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
if (metafieldsResult.error) {
|
|
1394
|
+
setError(metafieldsResult.error);
|
|
1395
|
+
} else {
|
|
1396
|
+
// Combine first metafield with additional metafields array
|
|
1397
|
+
const combinedResponse = {
|
|
1398
|
+
...metafieldData,
|
|
1399
|
+
new: true,
|
|
1400
|
+
discounts: metafieldsResult.data || [],
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
setPromotion(combinedResponse);
|
|
1404
|
+
setError(null);
|
|
1405
|
+
}
|
|
1406
|
+
} else if (
|
|
1407
|
+
!metafieldData?.discountCodes &&
|
|
1408
|
+
metafieldData?.discounts &&
|
|
1409
|
+
!Array.isArray(metafieldData?.discounts)
|
|
1410
|
+
) {
|
|
1411
|
+
const newPromotion = {
|
|
1412
|
+
...metafieldData,
|
|
1413
|
+
new: true,
|
|
1414
|
+
discounts: Object.values(metafieldData?.discounts || {}),
|
|
1415
|
+
};
|
|
1416
|
+
setPromotion(newPromotion);
|
|
1417
|
+
setError(null);
|
|
1418
|
+
}
|
|
1419
|
+
} else {
|
|
1420
|
+
setError(result.error);
|
|
1421
|
+
setPromotion(result.data);
|
|
1422
|
+
setError(result.error);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
setLoading(false);
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
if (!key) {
|
|
1429
|
+
setPromotion(null);
|
|
1430
|
+
setLoading(false);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (!storeUrl || !storefrontAccessToken) {
|
|
1435
|
+
setPromotion(null);
|
|
1436
|
+
setLoading(false);
|
|
1437
|
+
setError("Missing integration configuration");
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
fetchData();
|
|
1442
|
+
}, [key, storeUrl, storefrontAccessToken]);
|
|
1443
|
+
|
|
1444
|
+
return { promotion, loading, error };
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
// Helper functions for promotion logic
|
|
1448
|
+
const parsePromotionIfActive$1 = (promotionString) => {
|
|
1449
|
+
try {
|
|
1450
|
+
const promotion = JSON.parse(promotionString ?? "{}");
|
|
1451
|
+
if (
|
|
1452
|
+
!promotion ||
|
|
1453
|
+
!promotion.startsAt ||
|
|
1454
|
+
!promotion.status ||
|
|
1455
|
+
!promotion.title ||
|
|
1456
|
+
promotion.status === "DRAFT"
|
|
1457
|
+
) {
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
return promotion;
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
console.error("Failed to parse promotion:", error);
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
const shouldSkipPromotion = (vaultPromotion, nextPromotion) => {
|
|
1468
|
+
return (
|
|
1469
|
+
vaultPromotion &&
|
|
1470
|
+
vaultPromotion.title === nextPromotion?.title &&
|
|
1471
|
+
JSON.stringify(vaultPromotion) === JSON.stringify(nextPromotion)
|
|
1472
|
+
);
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
const isDeviceAllowed = (deviceId, allowedTestDevices) => {
|
|
1476
|
+
if (!allowedTestDevices) return true;
|
|
1477
|
+
return allowedTestDevices.includes(deviceId);
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
function useAbraActivate({
|
|
1481
|
+
useTapcart,
|
|
1482
|
+
tapcartData,
|
|
1483
|
+
discountCode = "PUBLIC",
|
|
1484
|
+
allowedTestDevices = undefined,
|
|
1485
|
+
displayToast = false,
|
|
1486
|
+
redirectPath = undefined,
|
|
1487
|
+
testDeviceId = null,
|
|
1488
|
+
testPromotionCode = null,
|
|
1489
|
+
}) {
|
|
1490
|
+
const Tapcart = useTapcart();
|
|
1491
|
+
const actions = Tapcart?.actions;
|
|
1492
|
+
const [currentCode, setCurrentCode] = React.useState(null);
|
|
1493
|
+
const [currentPromotion, setCurrentPromotion] = React.useState(null);
|
|
1494
|
+
|
|
1495
|
+
const { storeFrontAccessToken, storeUrl } =
|
|
1496
|
+
tapcartData?.integrations?.find((x) => x.name === "abra-promotions") ?? {};
|
|
1497
|
+
|
|
1498
|
+
const { promotion, error: promotionError } = usePromotion(
|
|
1499
|
+
currentCode,
|
|
1500
|
+
storeUrl,
|
|
1501
|
+
storeFrontAccessToken,
|
|
1502
|
+
);
|
|
1503
|
+
|
|
1504
|
+
const loadPromotion = React.useCallback(
|
|
1505
|
+
async (promotion) => {
|
|
1506
|
+
try {
|
|
1507
|
+
if (!promotion || !promotion.title) {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
const vault = await Tapcart?.actions?.vault?.initialize(
|
|
1511
|
+
"abra",
|
|
1512
|
+
"global",
|
|
1513
|
+
);
|
|
1514
|
+
const vaultPromotionString = await vault.get("active-promotion");
|
|
1515
|
+
const vaultPromotion = parsePromotionIfActive$1(vaultPromotionString);
|
|
1516
|
+
const nextPromotion = promotion;
|
|
1517
|
+
|
|
1518
|
+
if (shouldSkipPromotion(vaultPromotion, nextPromotion)) {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
if (!nextPromotion?.title) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
await vault.set("active-promotion", JSON.stringify(nextPromotion));
|
|
1527
|
+
|
|
1528
|
+
setCurrentCode(null);
|
|
1529
|
+
setCurrentPromotion(nextPromotion);
|
|
1530
|
+
|
|
1531
|
+
if (displayToast) {
|
|
1532
|
+
await actions.showToast({
|
|
1533
|
+
message: `Activated ${nextPromotion.title}`,
|
|
1534
|
+
type: "success",
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (redirectPath) {
|
|
1539
|
+
await actions.openScreen({
|
|
1540
|
+
destination: { type: "internal", url: redirectPath },
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
console.error("Failed to load promotion:", error);
|
|
1545
|
+
if (displayToast) {
|
|
1546
|
+
await actions.showToast({
|
|
1547
|
+
message: error.message,
|
|
1548
|
+
type: "error",
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
[displayToast, redirectPath],
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
// Store latest callback values in refs to avoid dependency issues
|
|
1557
|
+
const callbacksRef = React.useRef({
|
|
1558
|
+
loadPromotion,
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
// Update refs with latest callbacks
|
|
1562
|
+
React.useEffect(() => {
|
|
1563
|
+
callbacksRef.current = {
|
|
1564
|
+
loadPromotion,
|
|
1565
|
+
};
|
|
1566
|
+
}, [loadPromotion]);
|
|
1567
|
+
|
|
1568
|
+
React.useEffect(() => {
|
|
1569
|
+
const loadDiscountCode = async () => {
|
|
1570
|
+
if (!Tapcart?.variables?.device?.id || !Tapcart?.variables?.session?.id)
|
|
1571
|
+
return;
|
|
1572
|
+
const vault = await Tapcart?.actions?.vault?.initialize("abra", "global");
|
|
1573
|
+
const previousSessionId = await vault.get("session-id");
|
|
1574
|
+
|
|
1575
|
+
if (previousSessionId !== Tapcart?.variables?.session?.id) {
|
|
1576
|
+
await Promise.allSettled([
|
|
1577
|
+
vault.remove("active-promotion"),
|
|
1578
|
+
vault.remove("abra-latest-discount-code"),
|
|
1579
|
+
vault.remove("session-id"),
|
|
1580
|
+
]);
|
|
1581
|
+
}
|
|
1582
|
+
await Promise.allSettled([
|
|
1583
|
+
vault.set("session-id", Tapcart.variables.session.id),
|
|
1584
|
+
vault.set("storefront-access-token", storeFrontAccessToken ?? ""),
|
|
1585
|
+
vault.set(
|
|
1586
|
+
"shopify-api-graphql-url",
|
|
1587
|
+
storeUrl ? `https://${storeUrl}/api/2024-04/graphql.json` : "",
|
|
1588
|
+
),
|
|
1589
|
+
]);
|
|
1590
|
+
|
|
1591
|
+
if (
|
|
1592
|
+
!isDeviceAllowed(Tapcart?.variables?.device?.id, allowedTestDevices)
|
|
1593
|
+
) {
|
|
1594
|
+
if (currentCode !== undefined) {
|
|
1595
|
+
setCurrentCode(undefined);
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (Tapcart?.variables?.device?.id === testDeviceId) {
|
|
1601
|
+
if (currentCode !== testPromotionCode) {
|
|
1602
|
+
setCurrentCode(testPromotionCode);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
if (discountCode && currentCode !== discountCode) {
|
|
1608
|
+
setCurrentCode(discountCode);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
|
|
1613
|
+
if (!Tapcart?.variables?.device?.id || !Tapcart?.variables?.session?.id)
|
|
1614
|
+
return;
|
|
1615
|
+
loadDiscountCode();
|
|
1616
|
+
}, [
|
|
1617
|
+
Tapcart?.variables?.device?.id,
|
|
1618
|
+
Tapcart?.variables?.session?.id,
|
|
1619
|
+
storeFrontAccessToken,
|
|
1620
|
+
storeUrl,
|
|
1621
|
+
discountCode,
|
|
1622
|
+
testDeviceId,
|
|
1623
|
+
testPromotionCode,
|
|
1624
|
+
]);
|
|
1625
|
+
|
|
1626
|
+
React.useEffect(() => {
|
|
1627
|
+
callbacksRef.current.loadPromotion(promotion);
|
|
1628
|
+
}, [promotion]);
|
|
1629
|
+
|
|
1630
|
+
return { promotion: currentPromotion, error: promotionError };
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
const PRODUCT_FRAGMENT = `
|
|
1634
|
+
id
|
|
1635
|
+
handle
|
|
1636
|
+
variants(first: 100) {
|
|
1637
|
+
edges {
|
|
1638
|
+
node {
|
|
1639
|
+
id
|
|
1640
|
+
handle: title
|
|
1641
|
+
price {
|
|
1642
|
+
amount
|
|
1643
|
+
currencyCode
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
`;
|
|
1649
|
+
|
|
1650
|
+
const generateProductsQuery = (ids) => {
|
|
1651
|
+
const productQueries = ids
|
|
1652
|
+
.map(
|
|
1653
|
+
(id, index) =>
|
|
1654
|
+
`product_${index}: product(id: "${mobileComponents.productGidFromId(id)}") {
|
|
1655
|
+
${PRODUCT_FRAGMENT}
|
|
1656
|
+
}`,
|
|
1657
|
+
)
|
|
1658
|
+
.join("\n ");
|
|
1659
|
+
|
|
1660
|
+
return `
|
|
1661
|
+
query Products {
|
|
1662
|
+
${productQueries}
|
|
1663
|
+
}
|
|
1664
|
+
`;
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
const graphqlDefaultBody = (query, variables, storefrontAccessToken) => {
|
|
1668
|
+
return {
|
|
1669
|
+
async: true,
|
|
1670
|
+
crossDomain: true,
|
|
1671
|
+
method: "POST",
|
|
1672
|
+
headers: {
|
|
1673
|
+
"X-Shopify-Storefront-Access-Token": storefrontAccessToken,
|
|
1674
|
+
"Content-Type": "application/json",
|
|
1675
|
+
},
|
|
1676
|
+
body: JSON.stringify({
|
|
1677
|
+
query,
|
|
1678
|
+
variables,
|
|
1679
|
+
}),
|
|
1680
|
+
};
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
const fetchProducts = async (ids, storeUrl, storefrontAccessToken) => {
|
|
1684
|
+
if (!storeUrl || !storefrontAccessToken) {
|
|
1685
|
+
return { error: "Missing required configuration" };
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if (!ids || ids.length === 0) {
|
|
1689
|
+
return { data: [], error: "No ids" };
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
const GRAPHQL_URL = `https://${storeUrl}/api/2024-04/graphql.json`;
|
|
1693
|
+
const query = generateProductsQuery(ids);
|
|
1694
|
+
|
|
1695
|
+
try {
|
|
1696
|
+
const response = await fetch(
|
|
1697
|
+
GRAPHQL_URL,
|
|
1698
|
+
graphqlDefaultBody(query, {}, storefrontAccessToken),
|
|
1699
|
+
);
|
|
1700
|
+
|
|
1701
|
+
if (response?.ok) {
|
|
1702
|
+
const responseBody = await response.json();
|
|
1703
|
+
const data = responseBody?.data;
|
|
1704
|
+
|
|
1705
|
+
if (!data) {
|
|
1706
|
+
return { data: [], error: "No data" };
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
const products = Object.values(data).filter((item) => item !== null);
|
|
1710
|
+
|
|
1711
|
+
return {
|
|
1712
|
+
data: products,
|
|
1713
|
+
error: null,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
const error = await response.text();
|
|
1718
|
+
return { data: [], error: error };
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
return { data: [], error: err.message };
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
const useProducts = ({ ids, storeUrl, storeFrontAccessToken }) => {
|
|
1725
|
+
const [products, setProducts] = React__namespace.useState([]);
|
|
1726
|
+
const [loading, setLoading] = React__namespace.useState(true);
|
|
1727
|
+
const [error, setError] = React__namespace.useState(null);
|
|
1728
|
+
|
|
1729
|
+
// Create stable IDs string to prevent unnecessary refetches
|
|
1730
|
+
const idsString = React__namespace.useMemo(() => {
|
|
1731
|
+
return ids?.sort().join(",") || "";
|
|
1732
|
+
}, [ids]);
|
|
1733
|
+
|
|
1734
|
+
React__namespace.useEffect(() => {
|
|
1735
|
+
const fetchData = async () => {
|
|
1736
|
+
if (!ids || ids.length === 0 || !storeUrl || !storeFrontAccessToken) {
|
|
1737
|
+
if (ids.length === 0) {
|
|
1738
|
+
setError("no ids");
|
|
1739
|
+
}
|
|
1740
|
+
if (!storeUrl) {
|
|
1741
|
+
setError("no store url");
|
|
1742
|
+
}
|
|
1743
|
+
if (!storeFrontAccessToken) {
|
|
1744
|
+
setError("no store front access token");
|
|
1745
|
+
}
|
|
1746
|
+
setProducts([]);
|
|
1747
|
+
setLoading(false);
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const result = await fetchProducts(ids, storeUrl, storeFrontAccessToken);
|
|
1752
|
+
|
|
1753
|
+
try {
|
|
1754
|
+
if (result.data && result.data.length > 0) {
|
|
1755
|
+
const formattedProducts = result?.data?.map((product) => ({
|
|
1756
|
+
id: mobileComponents.getIdFromGid(product.id),
|
|
1757
|
+
handle: product?.handle,
|
|
1758
|
+
variants: product?.variants?.edges?.map((variant) => ({
|
|
1759
|
+
id: mobileComponents.getIdFromGid(variant?.node?.id),
|
|
1760
|
+
handle: variant?.node?.handle,
|
|
1761
|
+
price: variant?.node?.price,
|
|
1762
|
+
})),
|
|
1763
|
+
}));
|
|
1764
|
+
setProducts(formattedProducts);
|
|
1765
|
+
setError(result.error);
|
|
1766
|
+
} else {
|
|
1767
|
+
setProducts([]);
|
|
1768
|
+
setError(result.error);
|
|
1769
|
+
}
|
|
1770
|
+
} catch (error) {
|
|
1771
|
+
setError(error.message);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
setLoading(false);
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
fetchData();
|
|
1778
|
+
}, [idsString, storeUrl, storeFrontAccessToken]);
|
|
1779
|
+
|
|
1780
|
+
return { products, loading, error };
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
const enrichCartItems = (cartItems, productsDetails) => {
|
|
1784
|
+
return cartItems.map((lineItem) => {
|
|
1785
|
+
const product = productsDetails.find(
|
|
1786
|
+
(p) => mobileComponents.getIdFromGid(p?.id) === lineItem.productId,
|
|
1787
|
+
);
|
|
1788
|
+
const variant = product?.variants?.find(
|
|
1789
|
+
(v) => mobileComponents.getIdFromGid(v?.id) === lineItem.variantId,
|
|
1790
|
+
);
|
|
1791
|
+
return {
|
|
1792
|
+
...lineItem,
|
|
1793
|
+
productDetails: {
|
|
1794
|
+
handle: product?.handle,
|
|
1795
|
+
},
|
|
1796
|
+
variantDetails: variant,
|
|
1797
|
+
};
|
|
1798
|
+
});
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
const useCartWithPrices = ({ Tapcart, tapcartData }) => {
|
|
1802
|
+
const tapcartCart = Tapcart?.variables?.cart;
|
|
1803
|
+
const [cart, setCart] = React__namespace.useState(null);
|
|
1804
|
+
|
|
1805
|
+
// Use ref to track previous signature and avoid infinite recalculations
|
|
1806
|
+
const previousSignatureRef = React__namespace.useRef(null);
|
|
1807
|
+
const [stableCartData, setStableCartData] = React__namespace.useState(null);
|
|
1808
|
+
|
|
1809
|
+
const createCartSignatureString = (_cart) => {
|
|
1810
|
+
const items = (_cart?.items || [])
|
|
1811
|
+
.map((item) => `${item.productId}-${item.variantId}-${item.quantity}`)
|
|
1812
|
+
.sort()
|
|
1813
|
+
.join(",");
|
|
1814
|
+
|
|
1815
|
+
const discounts = (_cart?.discounts || []).join(",");
|
|
1816
|
+
const attributes = JSON.stringify(_cart?.attributes || {});
|
|
1817
|
+
|
|
1818
|
+
return `${items}|${discounts}|${attributes}`;
|
|
1819
|
+
};
|
|
1820
|
+
const currentSignatureString = createCartSignatureString(tapcartCart);
|
|
1821
|
+
|
|
1822
|
+
// Calculate new signature only when content actually changes
|
|
1823
|
+
React__namespace.useEffect(() => {
|
|
1824
|
+
if (!tapcartCart) {
|
|
1825
|
+
if (previousSignatureRef.current !== null) {
|
|
1826
|
+
previousSignatureRef.current = null;
|
|
1827
|
+
}
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// Always update on first render or when signature actually changed
|
|
1832
|
+
if (
|
|
1833
|
+
previousSignatureRef.current === null ||
|
|
1834
|
+
previousSignatureRef.current !== currentSignatureString
|
|
1835
|
+
) {
|
|
1836
|
+
setStableCartData(tapcartCart);
|
|
1837
|
+
previousSignatureRef.current = currentSignatureString;
|
|
1838
|
+
}
|
|
1839
|
+
}, [currentSignatureString]);
|
|
1840
|
+
|
|
1841
|
+
const { storeFrontAccessToken, storeUrl } =
|
|
1842
|
+
tapcartData?.integrations?.find((x) => x.name === "abra-promotions") ?? {};
|
|
1843
|
+
|
|
1844
|
+
// Stable product IDs that only change when cart items change
|
|
1845
|
+
const productIds = React__namespace.useMemo(
|
|
1846
|
+
() => stableCartData?.items?.map((item) => item.productId) ?? [],
|
|
1847
|
+
[stableCartData?.items],
|
|
1848
|
+
);
|
|
1849
|
+
|
|
1850
|
+
const { products, error, loading } = useProducts({
|
|
1851
|
+
ids: productIds,
|
|
1852
|
+
storeUrl: storeUrl,
|
|
1853
|
+
storeFrontAccessToken: storeFrontAccessToken,
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
// Update cart with enriched items only when products or cart changes
|
|
1857
|
+
React__namespace.useEffect(() => {
|
|
1858
|
+
if (!stableCartData) {
|
|
1859
|
+
setCart(null);
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
if (error || products?.length === 0) {
|
|
1864
|
+
setCart(null);
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const enrichedCart = {
|
|
1869
|
+
...stableCartData,
|
|
1870
|
+
items: enrichCartItems(stableCartData.items ?? [], products),
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
setCart(enrichedCart);
|
|
1874
|
+
}, [products, stableCartData, error, loading]);
|
|
1875
|
+
|
|
1876
|
+
return { cart, error };
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
function itemWitchCorrectEntitledQuantityToGiveDiscount(
|
|
1880
|
+
item,
|
|
1881
|
+
discount,
|
|
1882
|
+
quantitiOfItemsIgnored = 0,
|
|
1883
|
+
giftsInCart = [],
|
|
1884
|
+
) {
|
|
1885
|
+
const itemCopy = { ...item };
|
|
1886
|
+
if (!discount || !discount.entitled) {
|
|
1887
|
+
itemCopy.quantity = 0;
|
|
1888
|
+
return itemCopy;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
if (giftsInCart.some((gift) => gift.variantId === item.variantDetails.id)) {
|
|
1892
|
+
itemCopy.quantity =
|
|
1893
|
+
itemCopy.quantity -
|
|
1894
|
+
giftsInCart.find((gift) => gift.variantId === item.variantDetails.id)
|
|
1895
|
+
.quantity;
|
|
1896
|
+
return itemCopy;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
let quantityIgnored = 0;
|
|
1900
|
+
if (discount.discountValue?.type === "BXGY") {
|
|
1901
|
+
if (
|
|
1902
|
+
discount.discountValue.customerGets.value.quantity >
|
|
1903
|
+
quantitiOfItemsIgnored
|
|
1904
|
+
) {
|
|
1905
|
+
const quantityLeftToIgnore =
|
|
1906
|
+
discount.discountValue.customerGets.value.quantity -
|
|
1907
|
+
quantitiOfItemsIgnored;
|
|
1908
|
+
const isItemToIgnore =
|
|
1909
|
+
discount.discountValue.customerGets.items.products.includes(
|
|
1910
|
+
item.productDetails.handle,
|
|
1911
|
+
);
|
|
1912
|
+
|
|
1913
|
+
const quantityToIgnore = Math.min(
|
|
1914
|
+
isItemToIgnore ? quantityLeftToIgnore : 0,
|
|
1915
|
+
itemCopy.quantity,
|
|
1916
|
+
);
|
|
1917
|
+
itemCopy.quantity = itemCopy.quantity - quantityToIgnore;
|
|
1918
|
+
quantityIgnored = quantityToIgnore;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
let entitledOnItem = false;
|
|
1923
|
+
|
|
1924
|
+
let listOfEligibleItems = {};
|
|
1925
|
+
if (discount.discountValue?.type === "BXGY") {
|
|
1926
|
+
listOfEligibleItems = discount.discountValue.customerBuys.items;
|
|
1927
|
+
} else {
|
|
1928
|
+
listOfEligibleItems = discount.entitled;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
if (listOfEligibleItems.all) {
|
|
1932
|
+
entitledOnItem = true;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
if (listOfEligibleItems.variants) {
|
|
1936
|
+
if (
|
|
1937
|
+
listOfEligibleItems.variants?.includes(
|
|
1938
|
+
mobileComponents.getIdFromGid(item.variantDetails.id),
|
|
1939
|
+
)
|
|
1940
|
+
) {
|
|
1941
|
+
entitledOnItem = true;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
if (listOfEligibleItems.products) {
|
|
1946
|
+
if (listOfEligibleItems.products?.includes(item.productDetails.handle)) {
|
|
1947
|
+
entitledOnItem = true;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
if (discount.discountValue?.productDiscounts) {
|
|
1952
|
+
const entitledProducts = discount?.discountValue?.productDiscounts
|
|
1953
|
+
?.map((discount) => discount.products)
|
|
1954
|
+
.flat();
|
|
1955
|
+
|
|
1956
|
+
if (
|
|
1957
|
+
entitledProducts &&
|
|
1958
|
+
entitledProducts.length > 0 &&
|
|
1959
|
+
entitledProducts.includes(item.productDetails.handle)
|
|
1960
|
+
) {
|
|
1961
|
+
entitledOnItem = true;
|
|
1962
|
+
} else if (
|
|
1963
|
+
entitledProducts &&
|
|
1964
|
+
entitledProducts.length > 0 &&
|
|
1965
|
+
!entitledProducts.includes(item.productDetails.handle)
|
|
1966
|
+
) {
|
|
1967
|
+
entitledOnItem = false;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
if (discount.discountValue?.variantDiscounts) {
|
|
1972
|
+
const entitledVariants = discount?.discountValue?.variantDiscounts
|
|
1973
|
+
?.map((discount) => discount.variants)
|
|
1974
|
+
.flat();
|
|
1975
|
+
|
|
1976
|
+
if (
|
|
1977
|
+
entitledVariants &&
|
|
1978
|
+
entitledVariants.length > 0 &&
|
|
1979
|
+
entitledVariants.includes(mobileComponents.getIdFromGid(item.variantDetails.id))
|
|
1980
|
+
) {
|
|
1981
|
+
entitledOnItem = true;
|
|
1982
|
+
} else if (
|
|
1983
|
+
entitledVariants &&
|
|
1984
|
+
entitledVariants.length > 0 &&
|
|
1985
|
+
!entitledVariants.includes(mobileComponents.getIdFromGid(item.variantDetails.id))
|
|
1986
|
+
) {
|
|
1987
|
+
entitledOnItem = false;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (!entitledOnItem) {
|
|
1992
|
+
itemCopy.quantity = 0;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
return { item: itemCopy, quantity: quantityIgnored };
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
function getAmountPrerequisiteValid(
|
|
1999
|
+
prerequisite,
|
|
2000
|
+
entitledQuantity,
|
|
2001
|
+
entitledTotal,
|
|
2002
|
+
) {
|
|
2003
|
+
if (
|
|
2004
|
+
!prerequisite?.minimumRequirement ||
|
|
2005
|
+
Object.keys(prerequisite?.minimumRequirement).length === 0
|
|
2006
|
+
)
|
|
2007
|
+
return 1;
|
|
2008
|
+
|
|
2009
|
+
if (prerequisite?.minimumRequirement?.greaterThanOrEqualToQuantity) {
|
|
2010
|
+
if (
|
|
2011
|
+
parseInt(
|
|
2012
|
+
prerequisite.minimumRequirement.greaterThanOrEqualToQuantity,
|
|
2013
|
+
10,
|
|
2014
|
+
) === 0
|
|
2015
|
+
) {
|
|
2016
|
+
return 1;
|
|
2017
|
+
}
|
|
2018
|
+
return Math.floor(
|
|
2019
|
+
entitledQuantity /
|
|
2020
|
+
parseInt(
|
|
2021
|
+
prerequisite.minimumRequirement.greaterThanOrEqualToQuantity,
|
|
2022
|
+
10,
|
|
2023
|
+
),
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
if (prerequisite?.minimumRequirement?.greaterThanOrEqualToSubtotal) {
|
|
2028
|
+
if (
|
|
2029
|
+
Number(
|
|
2030
|
+
prerequisite.minimumRequirement.greaterThanOrEqualToSubtotal?.cents ??
|
|
2031
|
+
0,
|
|
2032
|
+
) === 0
|
|
2033
|
+
) {
|
|
2034
|
+
return 1;
|
|
2035
|
+
}
|
|
2036
|
+
return Math.floor(
|
|
2037
|
+
entitledTotal /
|
|
2038
|
+
Number(
|
|
2039
|
+
prerequisite.minimumRequirement.greaterThanOrEqualToSubtotal?.cents ??
|
|
2040
|
+
0,
|
|
2041
|
+
),
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
return 0;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function getCustomerBuysValueAmmount(
|
|
2048
|
+
customerBuys,
|
|
2049
|
+
entitledQuantity,
|
|
2050
|
+
entitledTotal,
|
|
2051
|
+
) {
|
|
2052
|
+
if (customerBuys.value.type === "DiscountQuantity") {
|
|
2053
|
+
return Math.floor(entitledQuantity / customerBuys.value.quantity);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
if (customerBuys.value.type === "DiscountAmount") {
|
|
2057
|
+
return Math.floor(entitledTotal / customerBuys.value.amount);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
return 0;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function getEligibleRewardTier({
|
|
2064
|
+
entitledQuantity,
|
|
2065
|
+
entitledTotal,
|
|
2066
|
+
discount,
|
|
2067
|
+
log,
|
|
2068
|
+
}) {
|
|
2069
|
+
log("discount type", discount?.discountValue?.type);
|
|
2070
|
+
if (discount?.discountValue?.type === "BXGY") {
|
|
2071
|
+
return getCustomerBuysValueAmmount(
|
|
2072
|
+
discount.discountValue.customerBuys,
|
|
2073
|
+
entitledQuantity,
|
|
2074
|
+
entitledTotal,
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
let amountPrerequisiteValid = getAmountPrerequisiteValid(
|
|
2079
|
+
discount?.prerequisite,
|
|
2080
|
+
entitledQuantity,
|
|
2081
|
+
entitledTotal,
|
|
2082
|
+
);
|
|
2083
|
+
|
|
2084
|
+
const discountValue = discount?.discountValue;
|
|
2085
|
+
if (
|
|
2086
|
+
(discountValue?.type === "TieredDiscount" ||
|
|
2087
|
+
discountValue?.type === "MultiEffectTiers" ||
|
|
2088
|
+
discountValue?.type === "VolumeDiscount") &&
|
|
2089
|
+
amountPrerequisiteValid > 0 &&
|
|
2090
|
+
discountValue?.tiers?.[0]?.prerequisite
|
|
2091
|
+
) {
|
|
2092
|
+
amountPrerequisiteValid = 0;
|
|
2093
|
+
for (const [i, tier] of discountValue.tiers.entries()) {
|
|
2094
|
+
let tierIsValid = getAmountPrerequisiteValid(
|
|
2095
|
+
tier?.prerequisite,
|
|
2096
|
+
entitledQuantity,
|
|
2097
|
+
entitledTotal,
|
|
2098
|
+
);
|
|
2099
|
+
if (tierIsValid <= 0) {
|
|
2100
|
+
break;
|
|
2101
|
+
}
|
|
2102
|
+
amountPrerequisiteValid = i + 1;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
return amountPrerequisiteValid;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function getTargetStateForDiscount({ cart, discount, giftsInCart, log }) {
|
|
2110
|
+
if (!discount || !discount.discountValue) {
|
|
2111
|
+
return 0;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
if (!cart || !cart.items || cart.items.length === 0) {
|
|
2115
|
+
return 0;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
let quantityOfItemsIgnored = 0;
|
|
2119
|
+
const entitledItems = cart.items
|
|
2120
|
+
.reduce((acc, item) => {
|
|
2121
|
+
const { item: itemWithCorrectQuantity, quantity: quantityIgnored } =
|
|
2122
|
+
itemWitchCorrectEntitledQuantityToGiveDiscount(
|
|
2123
|
+
item,
|
|
2124
|
+
discount,
|
|
2125
|
+
quantityOfItemsIgnored,
|
|
2126
|
+
giftsInCart,
|
|
2127
|
+
);
|
|
2128
|
+
quantityOfItemsIgnored += quantityIgnored;
|
|
2129
|
+
acc.push(itemWithCorrectQuantity);
|
|
2130
|
+
return acc;
|
|
2131
|
+
}, [])
|
|
2132
|
+
.filter((item) => (item?.quantity ?? 0) > 0);
|
|
2133
|
+
|
|
2134
|
+
const { entitledQuantity, entitledTotal } = entitledItems.reduce(
|
|
2135
|
+
(acc, item) => {
|
|
2136
|
+
acc.entitledQuantity += item?.quantity;
|
|
2137
|
+
acc.entitledTotal +=
|
|
2138
|
+
parseFloat(item?.variantDetails?.price?.amount ?? 0) * item?.quantity;
|
|
2139
|
+
return acc;
|
|
2140
|
+
},
|
|
2141
|
+
{ entitledQuantity: 0, entitledTotal: 0 },
|
|
2142
|
+
);
|
|
2143
|
+
|
|
2144
|
+
log("entitledQuantity", entitledQuantity);
|
|
2145
|
+
log("entitledTotal", entitledTotal);
|
|
2146
|
+
|
|
2147
|
+
const entitledTotalCents = entitledTotal * 100;
|
|
2148
|
+
return getEligibleRewardTier({
|
|
2149
|
+
entitledQuantity,
|
|
2150
|
+
entitledTotal: entitledTotalCents,
|
|
2151
|
+
discount,
|
|
2152
|
+
log,
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function getTargetStateForPromotion({
|
|
2157
|
+
cart,
|
|
2158
|
+
promotion,
|
|
2159
|
+
giftsInCart,
|
|
2160
|
+
log,
|
|
2161
|
+
eligibleDiscounts,
|
|
2162
|
+
}) {
|
|
2163
|
+
const discounts = Object.values(promotion?.discounts ?? {});
|
|
2164
|
+
|
|
2165
|
+
return discounts.map((discount) => {
|
|
2166
|
+
const isDiscountEligible = eligibleDiscounts.find(
|
|
2167
|
+
(d) => d.title === discount.title,
|
|
2168
|
+
).isEligible;
|
|
2169
|
+
return {
|
|
2170
|
+
title: discount.title,
|
|
2171
|
+
target: isDiscountEligible
|
|
2172
|
+
? getTargetStateForDiscount({ cart, discount, giftsInCart, log })
|
|
2173
|
+
: 0,
|
|
2174
|
+
};
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
const isItemEligibleWithSelectedPurchaseType = (item, discount) => {
|
|
2179
|
+
if (discount.appliesOnSubscription && !discount.appliesOnOneTimePurchase) {
|
|
2180
|
+
return item.sellingPlanAllocation; //TODO Verify correct structure of selling plan
|
|
2181
|
+
} else if (
|
|
2182
|
+
!discount.appliesOnSubscription &&
|
|
2183
|
+
discount.appliesOnOneTimePurchase
|
|
2184
|
+
) {
|
|
2185
|
+
return !item.sellingPlanAllocation?.name;
|
|
2186
|
+
} else {
|
|
2187
|
+
return true;
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
|
|
2191
|
+
const isItemEligibleForDiscount = (item, discount, log) => {
|
|
2192
|
+
log("isItemEligibleForDiscount", discount);
|
|
2193
|
+
if (!discount || !discount.entitled) {
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
const eligibleOnPurchaseType = isItemEligibleWithSelectedPurchaseType(
|
|
2198
|
+
item,
|
|
2199
|
+
discount,
|
|
2200
|
+
);
|
|
2201
|
+
let eligibleOnItem = false;
|
|
2202
|
+
let listOfEligibleItems = {};
|
|
2203
|
+
|
|
2204
|
+
if (discount.discountValue?.type === "BXGY") {
|
|
2205
|
+
listOfEligibleItems = discount.discountValue.customerGets.items;
|
|
2206
|
+
} else if (discount.discountValue?.type === "VolumeDiscount") {
|
|
2207
|
+
listOfEligibleItems = discount.discountValue.customerGets;
|
|
2208
|
+
} else {
|
|
2209
|
+
listOfEligibleItems = discount.entitled;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
if (listOfEligibleItems.all) {
|
|
2213
|
+
eligibleOnItem = true;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
if (listOfEligibleItems.variants) {
|
|
2217
|
+
if (
|
|
2218
|
+
listOfEligibleItems.variants?.includes(
|
|
2219
|
+
mobileComponents.getIdFromGid(item.variantDetails.id),
|
|
2220
|
+
)
|
|
2221
|
+
) {
|
|
2222
|
+
eligibleOnItem = true;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
if (listOfEligibleItems.products) {
|
|
2227
|
+
if (listOfEligibleItems.products?.includes(item.productDetails.handle)) {
|
|
2228
|
+
eligibleOnItem = true;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
if (discount.discountValue?.productDiscounts) {
|
|
2233
|
+
const entitledProducts = discount?.discountValue?.productDiscounts
|
|
2234
|
+
?.map((discount) => discount.products)
|
|
2235
|
+
.flat();
|
|
2236
|
+
|
|
2237
|
+
if (
|
|
2238
|
+
entitledProducts &&
|
|
2239
|
+
entitledProducts.length > 0 &&
|
|
2240
|
+
entitledProducts.includes(item.productDetails.handle)
|
|
2241
|
+
) {
|
|
2242
|
+
eligibleOnItem = true;
|
|
2243
|
+
} else if (
|
|
2244
|
+
entitledProducts &&
|
|
2245
|
+
entitledProducts.length > 0 &&
|
|
2246
|
+
!entitledProducts.includes(item.productDetails.handle)
|
|
2247
|
+
) {
|
|
2248
|
+
eligibleOnItem = false;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
if (discount.discountValue?.variantDiscounts) {
|
|
2253
|
+
const entitledVariants = discount?.discountValue?.variantDiscounts
|
|
2254
|
+
?.map((discount) => discount.variants)
|
|
2255
|
+
.flat();
|
|
2256
|
+
|
|
2257
|
+
if (
|
|
2258
|
+
entitledVariants &&
|
|
2259
|
+
entitledVariants.length > 0 &&
|
|
2260
|
+
entitledVariants.includes(mobileComponents.getIdFromGid(item.variantDetails.id))
|
|
2261
|
+
) {
|
|
2262
|
+
eligibleOnItem = true;
|
|
2263
|
+
} else if (
|
|
2264
|
+
entitledVariants &&
|
|
2265
|
+
entitledVariants.length > 0 &&
|
|
2266
|
+
!entitledVariants.includes(mobileComponents.getIdFromGid(item.variantDetails.id))
|
|
2267
|
+
) {
|
|
2268
|
+
eligibleOnItem = false;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
return eligibleOnItem && eligibleOnPurchaseType;
|
|
2272
|
+
};
|
|
2273
|
+
|
|
2274
|
+
function isCartEligibleForDiscount({ cart, discount, log }) {
|
|
2275
|
+
log("isCartEligibleForDiscount");
|
|
2276
|
+
if (!cart || !cart.items || cart.items.length === 0) {
|
|
2277
|
+
return false;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
const eligibleItems =
|
|
2281
|
+
cart.items?.filter((item) =>
|
|
2282
|
+
isItemEligibleForDiscount(item, discount, log),
|
|
2283
|
+
) ?? [];
|
|
2284
|
+
|
|
2285
|
+
log("eligibleItems", eligibleItems);
|
|
2286
|
+
|
|
2287
|
+
return eligibleItems.length > 0;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
function getGiftsToAdd({ promotion, targetState, log }) {
|
|
2291
|
+
const giftsToAdd = [];
|
|
2292
|
+
const giftsToRemove = [];
|
|
2293
|
+
|
|
2294
|
+
log("getGiftsToAdd", targetState);
|
|
2295
|
+
Object.values(promotion?.discounts ?? {}).forEach((discount) => {
|
|
2296
|
+
const discountTargetState = targetState?.find(
|
|
2297
|
+
(state) => state.title === discount.title,
|
|
2298
|
+
);
|
|
2299
|
+
|
|
2300
|
+
if (discount.discountValue.type === "Gift") {
|
|
2301
|
+
const giftVariantId = discount.discountValue.products
|
|
2302
|
+
?.at(0)
|
|
2303
|
+
?.variants?.at(0)?.id;
|
|
2304
|
+
|
|
2305
|
+
if (!giftVariantId) return;
|
|
2306
|
+
const compound = discount?.discountValue?.compound;
|
|
2307
|
+
const targetQuantity = discountTargetState?.target || 0;
|
|
2308
|
+
|
|
2309
|
+
if (targetQuantity > 0) {
|
|
2310
|
+
giftsToAdd.push({
|
|
2311
|
+
variantId: giftVariantId,
|
|
2312
|
+
quantity: compound ? targetQuantity : 1,
|
|
2313
|
+
});
|
|
2314
|
+
} else {
|
|
2315
|
+
giftsToRemove.push({
|
|
2316
|
+
variantId: giftVariantId,
|
|
2317
|
+
quantity: compound ? 100 : 1,
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
if (
|
|
2323
|
+
discount.discountValue.type === "TieredDiscount" &&
|
|
2324
|
+
discount.discountValue.tiersType === "FREE_GIFT"
|
|
2325
|
+
) {
|
|
2326
|
+
discount.discountValue.tiers.forEach((tier, index) => {
|
|
2327
|
+
const tierGiftVariantId = tier.value.products
|
|
2328
|
+
?.at(0)
|
|
2329
|
+
?.variants?.at(0)?.id;
|
|
2330
|
+
|
|
2331
|
+
if (!tierGiftVariantId) return;
|
|
2332
|
+
|
|
2333
|
+
let shouldReceiveGift = false;
|
|
2334
|
+
if (
|
|
2335
|
+
discount.discountValue?.compound === false &&
|
|
2336
|
+
index === discountTargetState.target - 1
|
|
2337
|
+
) {
|
|
2338
|
+
shouldReceiveGift = true;
|
|
2339
|
+
}
|
|
2340
|
+
if (
|
|
2341
|
+
discount.discountValue?.compound === true &&
|
|
2342
|
+
index <= discountTargetState.target - 1
|
|
2343
|
+
) {
|
|
2344
|
+
shouldReceiveGift = true;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
if (shouldReceiveGift) {
|
|
2348
|
+
giftsToAdd.push({
|
|
2349
|
+
variantId: tierGiftVariantId,
|
|
2350
|
+
quantity: 1,
|
|
2351
|
+
});
|
|
2352
|
+
} else {
|
|
2353
|
+
giftsToRemove.push({
|
|
2354
|
+
variantId: tierGiftVariantId,
|
|
2355
|
+
quantity: 1,
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
if (discount.discountValue.type === "MultiEffectTiers") {
|
|
2362
|
+
discount.discountValue.tiers.forEach((tier, index) => {
|
|
2363
|
+
tier.discounts.forEach((tierDiscount) => {
|
|
2364
|
+
if (tierDiscount?.value?.type !== "Gift") {
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
const tierGiftVariantId = tierDiscount.value.products
|
|
2369
|
+
?.at(0)
|
|
2370
|
+
?.variants?.at(0)?.id;
|
|
2371
|
+
|
|
2372
|
+
if (!tierGiftVariantId) return;
|
|
2373
|
+
|
|
2374
|
+
let shouldReceiveGift = false;
|
|
2375
|
+
if (
|
|
2376
|
+
!tierDiscount.value?.compound &&
|
|
2377
|
+
index === discountTargetState.target - 1
|
|
2378
|
+
) {
|
|
2379
|
+
shouldReceiveGift = true;
|
|
2380
|
+
}
|
|
2381
|
+
if (
|
|
2382
|
+
tierDiscount.value?.compound === true &&
|
|
2383
|
+
index <= discountTargetState.target - 1
|
|
2384
|
+
) {
|
|
2385
|
+
shouldReceiveGift = true;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
if (shouldReceiveGift) {
|
|
2389
|
+
giftsToAdd.push({
|
|
2390
|
+
variantId: tierGiftVariantId,
|
|
2391
|
+
quantity: 1,
|
|
2392
|
+
});
|
|
2393
|
+
} else {
|
|
2394
|
+
giftsToRemove.push({
|
|
2395
|
+
variantId: tierGiftVariantId,
|
|
2396
|
+
quantity: 1,
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
});
|
|
2403
|
+
|
|
2404
|
+
return { giftsToAdd, giftsToRemove };
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
const parsePromotionIfActive = (vaultActivePromotion) => {
|
|
2408
|
+
let promotion = JSON.parse(vaultActivePromotion ?? "{}");
|
|
2409
|
+
if (!promotion.startsAt || !promotion.status || !promotion.title)
|
|
2410
|
+
return undefined;
|
|
2411
|
+
const now = new Date();
|
|
2412
|
+
const startsAt = new Date(promotion.startsAt);
|
|
2413
|
+
const endsAt = promotion.endsAt ? new Date(promotion.endsAt) : null;
|
|
2414
|
+
|
|
2415
|
+
if (
|
|
2416
|
+
(promotion.status === "ACTIVE" || promotion.status === "SCHEDULED") &&
|
|
2417
|
+
now >= startsAt &&
|
|
2418
|
+
(!endsAt || now <= endsAt)
|
|
2419
|
+
) {
|
|
2420
|
+
return promotion;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
return undefined;
|
|
2424
|
+
};
|
|
2425
|
+
|
|
2426
|
+
const useForceRender = (intervalMs = 500) => {
|
|
2427
|
+
const hasForceRenderedRef = React__namespace.useRef(false);
|
|
2428
|
+
const [, forceUpdate] = React__namespace.useReducer((x) => x + 1, 0);
|
|
2429
|
+
|
|
2430
|
+
// Force re-render at intervals, but only if we haven't force rendered since last real render
|
|
2431
|
+
React__namespace.useEffect(() => {
|
|
2432
|
+
const interval = setInterval(() => {
|
|
2433
|
+
if (!hasForceRenderedRef.current) {
|
|
2434
|
+
hasForceRenderedRef.current = true;
|
|
2435
|
+
forceUpdate();
|
|
2436
|
+
}
|
|
2437
|
+
}, intervalMs);
|
|
2438
|
+
return () => clearInterval(interval);
|
|
2439
|
+
}, [intervalMs]);
|
|
2440
|
+
|
|
2441
|
+
// Reset force render flag when there's a real update
|
|
2442
|
+
React__namespace.useEffect(() => {
|
|
2443
|
+
hasForceRenderedRef.current = false;
|
|
2444
|
+
});
|
|
2445
|
+
};
|
|
2446
|
+
|
|
2447
|
+
function useAbraCart({
|
|
2448
|
+
useTapcart,
|
|
2449
|
+
tapcartData,
|
|
2450
|
+
addGiftOncePerSession = false,
|
|
2451
|
+
isAutomaticDiscount = false,
|
|
2452
|
+
}) {
|
|
2453
|
+
const Tapcart = useTapcart();
|
|
2454
|
+
const [activePromotion, setActivePromotion] = React__namespace.useState(null);
|
|
2455
|
+
const [logs, setLogs] = React__namespace.useState([]);
|
|
2456
|
+
const [giftsAlreadyAddedOnce, setGiftsAlreadyAddedOnce] = React__namespace.useState({});
|
|
2457
|
+
const [giftsInCart, setGiftsInCart] = React__namespace.useState([]);
|
|
2458
|
+
const [desiredState, setDesiredState] = React__namespace.useState({
|
|
2459
|
+
ok: true,
|
|
2460
|
+
giftsToAdd: [],
|
|
2461
|
+
giftsToRemove: [],
|
|
2462
|
+
discountsToApply: [],
|
|
2463
|
+
discountsToRemove: [],
|
|
2464
|
+
});
|
|
2465
|
+
|
|
2466
|
+
const renderCountRef = React__namespace.useRef(0);
|
|
2467
|
+
renderCountRef.current += 1;
|
|
2468
|
+
|
|
2469
|
+
const giftsRequested = React__namespace.useRef([]);
|
|
2470
|
+
const discountsRequested = React__namespace.useRef([]);
|
|
2471
|
+
|
|
2472
|
+
// Capture Tapcart.action once it's available and stable
|
|
2473
|
+
const stableTapcartAction = React__namespace.useRef(null);
|
|
2474
|
+
React__namespace.useEffect(() => {
|
|
2475
|
+
if (Tapcart.action && !stableTapcartAction.current) {
|
|
2476
|
+
stableTapcartAction.current = Tapcart.action;
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
|
|
2480
|
+
const actions = Tapcart.actions;
|
|
2481
|
+
|
|
2482
|
+
// Force re-render after 500ms of the last render
|
|
2483
|
+
useForceRender(1000);
|
|
2484
|
+
|
|
2485
|
+
const log = React__namespace.useCallback((message, data = null) => {
|
|
2486
|
+
const timestamp = new Date().toISOString();
|
|
2487
|
+
const logEntry = {
|
|
2488
|
+
timestamp,
|
|
2489
|
+
message,
|
|
2490
|
+
data: data ? JSON.parse(JSON.stringify(data)) : null,
|
|
2491
|
+
renderCount: renderCountRef.current,
|
|
2492
|
+
};
|
|
2493
|
+
|
|
2494
|
+
setLogs((prevLogs) => {
|
|
2495
|
+
const newLogs = [...prevLogs, logEntry];
|
|
2496
|
+
// Keep only last 50 logs to prevent memory issues
|
|
2497
|
+
return newLogs.length > 50 ? newLogs.slice(-50) : newLogs;
|
|
2498
|
+
});
|
|
2499
|
+
}, []);
|
|
2500
|
+
|
|
2501
|
+
const { cart: cartFromUseCart } = useCartWithPrices({
|
|
2502
|
+
Tapcart,
|
|
2503
|
+
tapcartData: tapcartData,
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2506
|
+
const computeDesiredCartState = React__namespace.useCallback(
|
|
2507
|
+
({ promotion, cart, giftsInCart }) => {
|
|
2508
|
+
log("computeDesiredCartState");
|
|
2509
|
+
if (
|
|
2510
|
+
!promotion ||
|
|
2511
|
+
!promotion.discounts ||
|
|
2512
|
+
promotion.discounts.length === 0 ||
|
|
2513
|
+
!cart ||
|
|
2514
|
+
!cart.items ||
|
|
2515
|
+
cart.items.length === 0
|
|
2516
|
+
) {
|
|
2517
|
+
log("no stuff");
|
|
2518
|
+
return {
|
|
2519
|
+
ok: true,
|
|
2520
|
+
giftsToAdd: [],
|
|
2521
|
+
giftsToRemove: [],
|
|
2522
|
+
discountsToApply: [],
|
|
2523
|
+
discountsToRemove: [],
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
log("has stuff 2");
|
|
2527
|
+
|
|
2528
|
+
try {
|
|
2529
|
+
log("trying", promotion);
|
|
2530
|
+
const eligibleDiscounts = promotion?.discounts
|
|
2531
|
+
?.filter((discount) => discount != null)
|
|
2532
|
+
?.map((discount) => {
|
|
2533
|
+
log("discount", discount);
|
|
2534
|
+
return {
|
|
2535
|
+
title: discount?.title,
|
|
2536
|
+
isEligible: isCartEligibleForDiscount({ cart, discount, log }),
|
|
2537
|
+
};
|
|
2538
|
+
});
|
|
2539
|
+
|
|
2540
|
+
log("eligibleDiscounts");
|
|
2541
|
+
const targetState =
|
|
2542
|
+
getTargetStateForPromotion({
|
|
2543
|
+
cart,
|
|
2544
|
+
promotion,
|
|
2545
|
+
giftsInCart,
|
|
2546
|
+
log,
|
|
2547
|
+
eligibleDiscounts,
|
|
2548
|
+
}) ?? [];
|
|
2549
|
+
|
|
2550
|
+
log("targetState");
|
|
2551
|
+
|
|
2552
|
+
const { giftsToAdd, giftsToRemove } = getGiftsToAdd({
|
|
2553
|
+
promotion,
|
|
2554
|
+
targetState,
|
|
2555
|
+
log,
|
|
2556
|
+
});
|
|
2557
|
+
|
|
2558
|
+
log("giftsToAdd");
|
|
2559
|
+
|
|
2560
|
+
if (!targetState || targetState.length === 0) {
|
|
2561
|
+
return {
|
|
2562
|
+
ok: true,
|
|
2563
|
+
giftsToAdd: [],
|
|
2564
|
+
giftsToRemove: [],
|
|
2565
|
+
discountsToApply: [],
|
|
2566
|
+
discountsToRemove: [],
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
log("toApply");
|
|
2571
|
+
|
|
2572
|
+
// Determine which discounts to apply/remove
|
|
2573
|
+
const toApply = [];
|
|
2574
|
+
const toRemove = [];
|
|
2575
|
+
eligibleDiscounts.forEach((discount) => {
|
|
2576
|
+
const discountTargetState = targetState?.find(
|
|
2577
|
+
(target) => target.title === discount.title,
|
|
2578
|
+
);
|
|
2579
|
+
const shouldApplyDiscount =
|
|
2580
|
+
discount?.isEligible && discountTargetState?.target > 0;
|
|
2581
|
+
|
|
2582
|
+
if (shouldApplyDiscount) {
|
|
2583
|
+
toApply.push(discount.title);
|
|
2584
|
+
} else {
|
|
2585
|
+
toRemove.push(discount.title);
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
|
|
2589
|
+
log("toRemove");
|
|
2590
|
+
|
|
2591
|
+
return {
|
|
2592
|
+
ok: true,
|
|
2593
|
+
giftsToAdd,
|
|
2594
|
+
giftsToRemove,
|
|
2595
|
+
discountsToApply: toApply,
|
|
2596
|
+
discountsToRemove: toRemove,
|
|
2597
|
+
};
|
|
2598
|
+
} catch (error) {
|
|
2599
|
+
log(`ComputeDesiredCartState error: ${error.message}`);
|
|
2600
|
+
return {
|
|
2601
|
+
ok: false,
|
|
2602
|
+
error: error.message,
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
},
|
|
2606
|
+
[],
|
|
2607
|
+
);
|
|
2608
|
+
|
|
2609
|
+
const applyDiscount = React__namespace.useCallback(
|
|
2610
|
+
async (discountCode) => {
|
|
2611
|
+
if (!stableTapcartAction.current) {
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
return new Promise((resolve, reject) => {
|
|
2615
|
+
stableTapcartAction.current(
|
|
2616
|
+
"cart/discount/add",
|
|
2617
|
+
{
|
|
2618
|
+
code: discountCode,
|
|
2619
|
+
},
|
|
2620
|
+
{
|
|
2621
|
+
onSuccess: (cart) => {
|
|
2622
|
+
resolve(cart);
|
|
2623
|
+
},
|
|
2624
|
+
onError: (error) => {
|
|
2625
|
+
reject(error);
|
|
2626
|
+
},
|
|
2627
|
+
},
|
|
2628
|
+
);
|
|
2629
|
+
});
|
|
2630
|
+
},
|
|
2631
|
+
[stableTapcartAction],
|
|
2632
|
+
);
|
|
2633
|
+
|
|
2634
|
+
const removeDiscount = React__namespace.useCallback(
|
|
2635
|
+
async (discountCode) => {
|
|
2636
|
+
if (!stableTapcartAction.current) {
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
return new Promise((resolve, reject) => {
|
|
2640
|
+
stableTapcartAction.current(
|
|
2641
|
+
"cart/discount/remove",
|
|
2642
|
+
{
|
|
2643
|
+
discountCode,
|
|
2644
|
+
},
|
|
2645
|
+
{
|
|
2646
|
+
onSuccess: (cart) => {
|
|
2647
|
+
resolve(cart);
|
|
2648
|
+
},
|
|
2649
|
+
onError: (error) => {
|
|
2650
|
+
log(
|
|
2651
|
+
`Failed to remove discount ${discountCode}: ${error.message}`,
|
|
2652
|
+
);
|
|
2653
|
+
reject(error);
|
|
2654
|
+
},
|
|
2655
|
+
},
|
|
2656
|
+
);
|
|
2657
|
+
});
|
|
2658
|
+
},
|
|
2659
|
+
[stableTapcartAction],
|
|
2660
|
+
);
|
|
2661
|
+
|
|
2662
|
+
const addLineItems = React__namespace.useCallback(
|
|
2663
|
+
async (items) => {
|
|
2664
|
+
if (!stableTapcartAction.current) {
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
return new Promise((resolve, reject) => {
|
|
2668
|
+
stableTapcartAction.current(
|
|
2669
|
+
"cart/add",
|
|
2670
|
+
{
|
|
2671
|
+
lineItems: items,
|
|
2672
|
+
},
|
|
2673
|
+
{
|
|
2674
|
+
onSuccess: (cart) => {
|
|
2675
|
+
resolve(cart);
|
|
2676
|
+
},
|
|
2677
|
+
onError: (error) => {
|
|
2678
|
+
log(`Failed to add items: ${error.message}`);
|
|
2679
|
+
reject(error);
|
|
2680
|
+
},
|
|
2681
|
+
},
|
|
2682
|
+
);
|
|
2683
|
+
});
|
|
2684
|
+
},
|
|
2685
|
+
[stableTapcartAction],
|
|
2686
|
+
);
|
|
2687
|
+
|
|
2688
|
+
const removeLineItems = React__namespace.useCallback(
|
|
2689
|
+
async (items) => {
|
|
2690
|
+
if (!stableTapcartAction.current) {
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
return new Promise((resolve, reject) => {
|
|
2694
|
+
stableTapcartAction.current(
|
|
2695
|
+
"cart/remove",
|
|
2696
|
+
{
|
|
2697
|
+
lineItems: items,
|
|
2698
|
+
},
|
|
2699
|
+
{
|
|
2700
|
+
onSuccess: (cart) => {
|
|
2701
|
+
resolve(cart);
|
|
2702
|
+
},
|
|
2703
|
+
onError: (error) => {
|
|
2704
|
+
log(`Failed to remove items: ${error.message}`);
|
|
2705
|
+
reject(error);
|
|
2706
|
+
},
|
|
2707
|
+
},
|
|
2708
|
+
);
|
|
2709
|
+
});
|
|
2710
|
+
},
|
|
2711
|
+
[stableTapcartAction],
|
|
2712
|
+
);
|
|
2713
|
+
|
|
2714
|
+
const upsertAttribute = React__namespace.useCallback(
|
|
2715
|
+
async (attribute) => {
|
|
2716
|
+
if (!stableTapcartAction.current) {
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
return new Promise((resolve, reject) => {
|
|
2720
|
+
const currentAttributes = cartFromUseCart?.attributes || [];
|
|
2721
|
+
const existingAttribute = currentAttributes.find(
|
|
2722
|
+
(attr) => attr.key === attribute.key,
|
|
2723
|
+
);
|
|
2724
|
+
const existingValue = existingAttribute
|
|
2725
|
+
? existingAttribute.value
|
|
2726
|
+
: null;
|
|
2727
|
+
|
|
2728
|
+
// If attribute already exists with the same value, no need to update
|
|
2729
|
+
if (existingValue && existingValue === attribute.value) {
|
|
2730
|
+
resolve(cartFromUseCart);
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
const newAttributes = [
|
|
2734
|
+
...currentAttributes.filter((attr) => attr.key !== attribute.key),
|
|
2735
|
+
{
|
|
2736
|
+
key: attribute.key,
|
|
2737
|
+
value: attribute.value,
|
|
2738
|
+
},
|
|
2739
|
+
];
|
|
2740
|
+
stableTapcartAction.current(
|
|
2741
|
+
"cart/updateAttributes",
|
|
2742
|
+
{
|
|
2743
|
+
attributes: newAttributes,
|
|
2744
|
+
},
|
|
2745
|
+
{
|
|
2746
|
+
onSuccess: (cart) => {
|
|
2747
|
+
resolve(cart);
|
|
2748
|
+
},
|
|
2749
|
+
onError: (error) => {
|
|
2750
|
+
log(`Failed to add attribute ${attribute.key}: ${error.message}`);
|
|
2751
|
+
reject(error);
|
|
2752
|
+
},
|
|
2753
|
+
},
|
|
2754
|
+
);
|
|
2755
|
+
});
|
|
2756
|
+
},
|
|
2757
|
+
[cartFromUseCart, stableTapcartAction],
|
|
2758
|
+
);
|
|
2759
|
+
|
|
2760
|
+
const removeAttribute = React__namespace.useCallback(
|
|
2761
|
+
async (attributeKey) => {
|
|
2762
|
+
if (!stableTapcartAction.current) {
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
return new Promise((resolve, reject) => {
|
|
2766
|
+
const currentAttributes = cartFromUseCart?.attributes || [];
|
|
2767
|
+
const existingAttribute = currentAttributes.find(
|
|
2768
|
+
(attr) => attr.key === attributeKey,
|
|
2769
|
+
);
|
|
2770
|
+
|
|
2771
|
+
// If attribute doesn't exist, no need to update
|
|
2772
|
+
if (!existingAttribute) {
|
|
2773
|
+
log(`Attribute ${attributeKey} doesn't exist, skipping removal`);
|
|
2774
|
+
resolve(cartFromUseCart);
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
const newAttributes = currentAttributes.filter(
|
|
2778
|
+
(attr) => attr.key !== attributeKey,
|
|
2779
|
+
);
|
|
2780
|
+
|
|
2781
|
+
stableTapcartAction.current(
|
|
2782
|
+
"cart/updateAttributes",
|
|
2783
|
+
{
|
|
2784
|
+
attributes: newAttributes,
|
|
2785
|
+
},
|
|
2786
|
+
{
|
|
2787
|
+
onSuccess: (cart) => {
|
|
2788
|
+
resolve(cart);
|
|
2789
|
+
},
|
|
2790
|
+
onError: (error) => {
|
|
2791
|
+
log(
|
|
2792
|
+
`Failed to remove attribute ${attributeKey}: ${error.message}`,
|
|
2793
|
+
);
|
|
2794
|
+
reject(error);
|
|
2795
|
+
},
|
|
2796
|
+
},
|
|
2797
|
+
);
|
|
2798
|
+
});
|
|
2799
|
+
},
|
|
2800
|
+
[cartFromUseCart, stableTapcartAction],
|
|
2801
|
+
);
|
|
2802
|
+
|
|
2803
|
+
const addAbraAttribute = React__namespace.useCallback(
|
|
2804
|
+
async (value) => {
|
|
2805
|
+
await upsertAttribute({ key: "__abra", value });
|
|
2806
|
+
},
|
|
2807
|
+
[upsertAttribute],
|
|
2808
|
+
);
|
|
2809
|
+
|
|
2810
|
+
const addTapcartAttribute = React__namespace.useCallback(async () => {
|
|
2811
|
+
await upsertAttribute({ key: "Tapcart", value: "1" });
|
|
2812
|
+
}, [upsertAttribute]);
|
|
2813
|
+
|
|
2814
|
+
const applyDesiredCartState = React__namespace.useCallback(
|
|
2815
|
+
async (newState) => {
|
|
2816
|
+
log("Applying desired cart state", newState);
|
|
2817
|
+
|
|
2818
|
+
try {
|
|
2819
|
+
if (!newState?.ok) {
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
const currentDiscounts = new Set(cartFromUseCart?.discounts || []);
|
|
2824
|
+
const currentAttributes = cartFromUseCart?.attributes || [];
|
|
2825
|
+
const currentItems = cartFromUseCart?.items || [];
|
|
2826
|
+
|
|
2827
|
+
// Current quantities by variant id
|
|
2828
|
+
const currentQtyByVariant = new Map();
|
|
2829
|
+
for (const item of currentItems) {
|
|
2830
|
+
const variantId = item?.variantDetails?.id;
|
|
2831
|
+
const qty = Number(item?.quantity || 0);
|
|
2832
|
+
if (!variantId || qty <= 0) continue;
|
|
2833
|
+
currentQtyByVariant.set(
|
|
2834
|
+
variantId,
|
|
2835
|
+
(currentQtyByVariant.get(variantId) || 0) + qty,
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// Previously requested gift quantities (to avoid double adds)
|
|
2840
|
+
const prevRequestedQtyByVariant = new Map();
|
|
2841
|
+
(giftsRequested?.current || []).forEach((req) => {
|
|
2842
|
+
if (!req?.variantId) return;
|
|
2843
|
+
const qty = Number(req?.quantity || 0);
|
|
2844
|
+
if (qty <= 0) return;
|
|
2845
|
+
prevRequestedQtyByVariant.set(
|
|
2846
|
+
req.variantId,
|
|
2847
|
+
(prevRequestedQtyByVariant.get(req.variantId) || 0) + qty,
|
|
2848
|
+
);
|
|
2849
|
+
});
|
|
2850
|
+
|
|
2851
|
+
// Desired target quantities by variant (adds minus removes)
|
|
2852
|
+
const desiredQtyByVariant = new Map();
|
|
2853
|
+
(newState?.giftsToAdd || []).forEach((g) => {
|
|
2854
|
+
if (!g?.variantId) return;
|
|
2855
|
+
const qty = Number(g?.quantity || 0);
|
|
2856
|
+
if (qty <= 0) return;
|
|
2857
|
+
desiredQtyByVariant.set(
|
|
2858
|
+
g.variantId,
|
|
2859
|
+
(desiredQtyByVariant.get(g.variantId) || 0) + qty,
|
|
2860
|
+
);
|
|
2861
|
+
});
|
|
2862
|
+
(newState?.giftsToRemove || []).forEach((g) => {
|
|
2863
|
+
if (!g?.variantId) return;
|
|
2864
|
+
const qty = Number(g?.quantity || 0);
|
|
2865
|
+
if (qty <= 0) return;
|
|
2866
|
+
const currentDesired = desiredQtyByVariant.get(g.variantId) || 0;
|
|
2867
|
+
const nextDesired = Math.max(0, currentDesired - qty);
|
|
2868
|
+
desiredQtyByVariant.set(g.variantId, nextDesired);
|
|
2869
|
+
});
|
|
2870
|
+
|
|
2871
|
+
// 1) Remove discounts that should be removed and are currently in cart
|
|
2872
|
+
const discountsToRemoveNow = Array.from(
|
|
2873
|
+
new Set(newState?.discountsToRemove || []),
|
|
2874
|
+
).filter((code) => currentDiscounts.has(code));
|
|
2875
|
+
for (const code of discountsToRemoveNow) {
|
|
2876
|
+
try {
|
|
2877
|
+
await removeDiscount(code);
|
|
2878
|
+
// Clear from pending requested
|
|
2879
|
+
discountsRequested.current = (
|
|
2880
|
+
discountsRequested.current || []
|
|
2881
|
+
).filter((c) => c !== code);
|
|
2882
|
+
} catch (e) {
|
|
2883
|
+
log(`Remove discount failed`, { code, error: e?.message });
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// 2) Remove gifts where current > desired
|
|
2888
|
+
const giftsToRemoveNow = [];
|
|
2889
|
+
desiredQtyByVariant.forEach((desired, variantId) => {
|
|
2890
|
+
const current = Number(currentQtyByVariant.get(variantId) || 0);
|
|
2891
|
+
if (current > desired) {
|
|
2892
|
+
const removeQty = current - desired;
|
|
2893
|
+
if (removeQty > 0)
|
|
2894
|
+
giftsToRemoveNow.push({ variantId, quantity: removeQty });
|
|
2895
|
+
}
|
|
2896
|
+
});
|
|
2897
|
+
if (giftsToRemoveNow.length > 0) {
|
|
2898
|
+
try {
|
|
2899
|
+
await removeLineItems(giftsToRemoveNow);
|
|
2900
|
+
} catch (e) {
|
|
2901
|
+
log(`Remove gifts failed`, {
|
|
2902
|
+
items: giftsToRemoveNow,
|
|
2903
|
+
error: e?.message,
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// 3) Add gifts where desired > current, factoring in outstanding requested adds
|
|
2909
|
+
const giftsToAddNow = [];
|
|
2910
|
+
desiredQtyByVariant.forEach((desired, variantId) => {
|
|
2911
|
+
const current = Number(currentQtyByVariant.get(variantId) || 0);
|
|
2912
|
+
const prevRequested = Number(
|
|
2913
|
+
prevRequestedQtyByVariant.get(variantId) || 0,
|
|
2914
|
+
);
|
|
2915
|
+
const outstandingAdd = Math.max(0, prevRequested - current);
|
|
2916
|
+
const neededAdd = Math.max(0, desired - current);
|
|
2917
|
+
let addNow = Math.max(0, neededAdd - outstandingAdd);
|
|
2918
|
+
|
|
2919
|
+
if (addGiftOncePerSession && giftsAlreadyAddedOnce?.[variantId]) {
|
|
2920
|
+
addNow = 0;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
if (addNow > 0) {
|
|
2924
|
+
giftsToAddNow.push({ variantId, quantity: addNow });
|
|
2925
|
+
}
|
|
2926
|
+
});
|
|
2927
|
+
if (giftsToAddNow.length > 0) {
|
|
2928
|
+
try {
|
|
2929
|
+
await addLineItems(giftsToAddNow);
|
|
2930
|
+
if (addGiftOncePerSession) {
|
|
2931
|
+
setGiftsAlreadyAddedOnce((prev) => {
|
|
2932
|
+
const next = { ...prev };
|
|
2933
|
+
giftsToAddNow.forEach((g) => {
|
|
2934
|
+
next[g.variantId] = true;
|
|
2935
|
+
});
|
|
2936
|
+
return next;
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
} catch (e) {
|
|
2940
|
+
log(`Add gifts failed`, {
|
|
2941
|
+
items: giftsToAddNow,
|
|
2942
|
+
error: e?.message,
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// Refresh requested gifts tracker to match desired target
|
|
2948
|
+
const updatedRequested = [];
|
|
2949
|
+
desiredQtyByVariant.forEach((qty, variantId) => {
|
|
2950
|
+
if (qty > 0) updatedRequested.push({ variantId, quantity: qty });
|
|
2951
|
+
});
|
|
2952
|
+
giftsRequested.current = updatedRequested;
|
|
2953
|
+
|
|
2954
|
+
// 4) Apply discounts not present or pending
|
|
2955
|
+
const pendingApplied = new Set(discountsRequested.current || []);
|
|
2956
|
+
const discountsToApplyNow = Array.from(
|
|
2957
|
+
new Set(newState?.discountsToApply || []),
|
|
2958
|
+
).filter(
|
|
2959
|
+
(code) => !currentDiscounts.has(code) && !pendingApplied.has(code),
|
|
2960
|
+
);
|
|
2961
|
+
|
|
2962
|
+
let appliedAnyDiscount = false;
|
|
2963
|
+
for (const code of discountsToApplyNow) {
|
|
2964
|
+
try {
|
|
2965
|
+
discountsRequested.current = [
|
|
2966
|
+
...(discountsRequested.current || []),
|
|
2967
|
+
code,
|
|
2968
|
+
];
|
|
2969
|
+
if (!isAutomaticDiscount) {
|
|
2970
|
+
await applyDiscount(code);
|
|
2971
|
+
}
|
|
2972
|
+
appliedAnyDiscount = true;
|
|
2973
|
+
// Clear from pending after attempt
|
|
2974
|
+
discountsRequested.current = (
|
|
2975
|
+
discountsRequested.current || []
|
|
2976
|
+
).filter((c) => c !== code);
|
|
2977
|
+
} catch (e) {
|
|
2978
|
+
log(`Apply discount failed`, { code, error: e?.message });
|
|
2979
|
+
discountsRequested.current = (
|
|
2980
|
+
discountsRequested.current || []
|
|
2981
|
+
).filter((c) => c !== code);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// 5) Ensure __abra attribute exists after discount application (idempotent)
|
|
2986
|
+
if (appliedAnyDiscount) {
|
|
2987
|
+
try {
|
|
2988
|
+
const existingAbra = currentAttributes.find(
|
|
2989
|
+
(a) => a?.key === "__abra",
|
|
2990
|
+
);
|
|
2991
|
+
await addAbraAttribute(existingAbra?.value || "1");
|
|
2992
|
+
} catch (e) {
|
|
2993
|
+
log(`Add __abra attribute failed`, { error: e?.message });
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
} catch (err) {
|
|
2997
|
+
log(`applyDesiredCartState error`, { error: err?.message });
|
|
2998
|
+
}
|
|
2999
|
+
},
|
|
3000
|
+
[
|
|
3001
|
+
cartFromUseCart,
|
|
3002
|
+
discountsRequested,
|
|
3003
|
+
giftsRequested,
|
|
3004
|
+
giftsAlreadyAddedOnce,
|
|
3005
|
+
addLineItems,
|
|
3006
|
+
removeLineItems,
|
|
3007
|
+
applyDiscount,
|
|
3008
|
+
removeDiscount,
|
|
3009
|
+
addAbraAttribute,
|
|
3010
|
+
removeAttribute,
|
|
3011
|
+
],
|
|
3012
|
+
);
|
|
3013
|
+
|
|
3014
|
+
// Store latest callback values in refs to avoid dependency issues
|
|
3015
|
+
const callbacksRef = React__namespace.useRef({
|
|
3016
|
+
addLineItems,
|
|
3017
|
+
removeLineItems,
|
|
3018
|
+
removeDiscount,
|
|
3019
|
+
applyDiscount,
|
|
3020
|
+
removeAttribute,
|
|
3021
|
+
addAbraAttribute,
|
|
3022
|
+
applyDesiredCartState,
|
|
3023
|
+
addTapcartAttribute,
|
|
3024
|
+
});
|
|
3025
|
+
|
|
3026
|
+
// Update refs with latest callbacks
|
|
3027
|
+
React__namespace.useEffect(() => {
|
|
3028
|
+
callbacksRef.current = {
|
|
3029
|
+
addLineItems,
|
|
3030
|
+
removeLineItems,
|
|
3031
|
+
removeDiscount,
|
|
3032
|
+
applyDiscount,
|
|
3033
|
+
removeAttribute,
|
|
3034
|
+
addAbraAttribute,
|
|
3035
|
+
applyDesiredCartState,
|
|
3036
|
+
addTapcartAttribute,
|
|
3037
|
+
};
|
|
3038
|
+
});
|
|
3039
|
+
|
|
3040
|
+
React__namespace.useEffect(() => {
|
|
3041
|
+
async function addTapcartAttribute() {
|
|
3042
|
+
await callbacksRef.current.addTapcartAttribute();
|
|
3043
|
+
}
|
|
3044
|
+
addTapcartAttribute();
|
|
3045
|
+
}, []);
|
|
3046
|
+
|
|
3047
|
+
React__namespace.useEffect(() => {
|
|
3048
|
+
async function initializeVault() {
|
|
3049
|
+
if (!actions?.vault) {
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
try {
|
|
3053
|
+
const vault = await actions.vault.initialize("abra", "global");
|
|
3054
|
+
const vaultActivePromotion = await vault.get("active-promotion");
|
|
3055
|
+
setActivePromotion(parsePromotionIfActive(vaultActivePromotion));
|
|
3056
|
+
|
|
3057
|
+
// // Load previous gifts state from vault
|
|
3058
|
+
// const savedGiftsRequested = await vault.get("gifts-requested");
|
|
3059
|
+
// const savedGiftsAlreadyAddedOnce = await vault.get("gifts-already-added-once");
|
|
3060
|
+
|
|
3061
|
+
// if (savedGiftsRequested) {
|
|
3062
|
+
// setGiftsRequested(JSON.parse(savedGiftsRequested));
|
|
3063
|
+
// }
|
|
3064
|
+
|
|
3065
|
+
// if (savedGiftsAlreadyAddedOnce) {
|
|
3066
|
+
// setGiftsAlreadyAddedOnce(JSON.parse(savedGiftsAlreadyAddedOnce));
|
|
3067
|
+
// }
|
|
3068
|
+
|
|
3069
|
+
vault.observe(
|
|
3070
|
+
"active-promotion",
|
|
3071
|
+
(newPromotionValue) => {
|
|
3072
|
+
setActivePromotion(parsePromotionIfActive(newPromotionValue));
|
|
3073
|
+
},
|
|
3074
|
+
(error) => {
|
|
3075
|
+
log(`Vault observation error: ${error.message}`);
|
|
3076
|
+
console.log(error);
|
|
3077
|
+
},
|
|
3078
|
+
);
|
|
3079
|
+
} catch (error) {
|
|
3080
|
+
log(`Vault initialization error: ${error.message}`);
|
|
3081
|
+
console.error(error);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
initializeVault();
|
|
3085
|
+
}, [actions?.vault]);
|
|
3086
|
+
|
|
3087
|
+
// Effect 2: Compute cart state if Tapcart attribute exists
|
|
3088
|
+
React__namespace.useEffect(() => {
|
|
3089
|
+
async function computeCartState() {
|
|
3090
|
+
try {
|
|
3091
|
+
const newDesiredState = computeDesiredCartState({
|
|
3092
|
+
promotion: activePromotion,
|
|
3093
|
+
cart: cartFromUseCart,
|
|
3094
|
+
giftsInCart,
|
|
3095
|
+
});
|
|
3096
|
+
|
|
3097
|
+
if (!newDesiredState.ok) {
|
|
3098
|
+
log(`Compute cart state failed:`, newDesiredState);
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// Only update if at least one property is different
|
|
3103
|
+
const hasChanged = Object.keys(newDesiredState).some(
|
|
3104
|
+
(key) =>
|
|
3105
|
+
JSON.stringify(desiredState[key]) !==
|
|
3106
|
+
JSON.stringify(newDesiredState[key]),
|
|
3107
|
+
);
|
|
3108
|
+
|
|
3109
|
+
if (hasChanged) {
|
|
3110
|
+
setDesiredState(newDesiredState);
|
|
3111
|
+
}
|
|
3112
|
+
} catch (error) {
|
|
3113
|
+
log(`Compute cart state error: ${error.message}`);
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
computeCartState();
|
|
3118
|
+
}, [computeDesiredCartState, activePromotion, cartFromUseCart, giftsInCart]);
|
|
3119
|
+
|
|
3120
|
+
React__namespace.useEffect(() => {
|
|
3121
|
+
callbacksRef.current.applyDesiredCartState(desiredState);
|
|
3122
|
+
}, [desiredState]);
|
|
3123
|
+
|
|
3124
|
+
React__namespace.useEffect(() => {
|
|
3125
|
+
if (
|
|
3126
|
+
!cartFromUseCart?.items ||
|
|
3127
|
+
cartFromUseCart?.items?.length == 0 ||
|
|
3128
|
+
!giftsRequested?.current ||
|
|
3129
|
+
giftsRequested?.current?.length == 0
|
|
3130
|
+
) {
|
|
3131
|
+
if (giftsInCart?.length > 0) {
|
|
3132
|
+
setGiftsInCart([]);
|
|
3133
|
+
}
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
const gifts = [];
|
|
3138
|
+
giftsRequested.current.forEach((request) => {
|
|
3139
|
+
const currentRequestInCart = cartFromUseCart?.items?.find(
|
|
3140
|
+
(item) => item?.variantDetails?.id === request.variantId,
|
|
3141
|
+
);
|
|
3142
|
+
if (!currentRequestInCart) {
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
gifts.push({
|
|
3147
|
+
variantId: request.variantId,
|
|
3148
|
+
quantity: Math.min(currentRequestInCart.quantity, request.quantity),
|
|
3149
|
+
});
|
|
3150
|
+
});
|
|
3151
|
+
|
|
3152
|
+
if (JSON.stringify(gifts) !== JSON.stringify(giftsInCart)) {
|
|
3153
|
+
setGiftsInCart(gifts);
|
|
3154
|
+
}
|
|
3155
|
+
}, [cartFromUseCart]);
|
|
3156
|
+
|
|
3157
|
+
return {
|
|
3158
|
+
logs,
|
|
3159
|
+
giftsInCart,
|
|
3160
|
+
desiredState,
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
exports.AbraCartBlock = useAbraCart;
|
|
3165
|
+
exports.useAbra = useAbra;
|
|
3166
|
+
exports.useAbraActivate = useAbraActivate;
|
|
3167
|
+
exports.useAbraCart = useAbraCart;
|
|
3168
|
+
//# sourceMappingURL=index.js.map
|