@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/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