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