astro-tractstack 2.3.3 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/bin/create-tractstack.js +5 -2
  2. package/dist/index.js +18 -0
  3. package/package.json +1 -1
  4. package/templates/custom/shopify/Cart.tsx +196 -104
  5. package/templates/custom/shopify/CartIcon.tsx +8 -8
  6. package/templates/custom/shopify/CheckoutModal.tsx +143 -66
  7. package/templates/custom/shopify/ShopifyCartManager.tsx +64 -19
  8. package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
  9. package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
  10. package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
  11. package/templates/src/components/Header.astro +1 -1
  12. package/templates/src/components/compositor/Node.tsx +39 -9
  13. package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
  14. package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
  15. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
  16. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
  17. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  18. package/templates/src/components/form/advanced/APIConfigSection.tsx +35 -8
  19. package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
  20. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +59 -23
  21. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +257 -18
  22. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
  23. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +161 -66
  24. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
  25. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +5 -2
  26. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
  27. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
  28. package/templates/src/layouts/Layout.astro +26 -0
  29. package/templates/src/pages/api/auth/logout.ts +35 -2
  30. package/templates/src/pages/api/sales/list.ts +66 -0
  31. package/templates/src/pages/api/sales/metrics.ts +60 -0
  32. package/templates/src/pages/context/[...contextSlug].astro +50 -31
  33. package/templates/src/pages/storykeep/advanced.astro +4 -1
  34. package/templates/src/stores/nodes.ts +8 -0
  35. package/templates/src/types/tractstack.ts +57 -0
  36. package/templates/src/utils/api/advancedConfig.ts +2 -1
  37. package/templates/src/utils/api/advancedHelpers.ts +4 -0
  38. package/templates/src/utils/api/brandConfig.ts +2 -0
  39. package/templates/src/utils/api/brandHelpers.ts +6 -0
  40. package/templates/src/utils/api/salesHelpers.ts +21 -0
  41. package/templates/src/utils/customHelpers.ts +285 -2
  42. package/utils/inject-files.ts +18 -0
@@ -27,6 +27,15 @@ import {
27
27
  } from '@/utils/booking/appointmentMode';
28
28
  import { NativeBookingCalendar } from './NativeBookingCalendar';
29
29
  import { ProfileStorage } from '@/utils/profileStorage';
30
+ import {
31
+ buildShopifyCheckoutLines,
32
+ getServiceLinkedProduct,
33
+ getServiceVariantIdFromCanonicalProduct,
34
+ getSharedFeeChargeLineSummary,
35
+ hasGidBackedCheckout,
36
+ isSharedFeeService,
37
+ parsePrimaryShopifyProductData,
38
+ } from '@/utils/customHelpers';
30
39
  import type { ResourceNode } from '@/types/compositorTypes';
31
40
 
32
41
  type CheckoutState =
@@ -102,21 +111,35 @@ export default function CheckoutModal({
102
111
  const enrichedCart = useMemo(() => {
103
112
  return Object.values($cartItems).map((item: any) => {
104
113
  const resource = resources.find((r) => r.id === item.resourceId);
105
- let productData: any = {};
106
- if (resource?.optionsPayload?.shopifyData) {
107
- try {
108
- productData = JSON.parse(resource.optionsPayload.shopifyData);
109
- } catch (e) {
110
- console.error('Failed to parse Shopify data', item.resourceId);
111
- }
112
- }
114
+ const productResources = resources.filter(
115
+ (r) => r.categorySlug === 'product'
116
+ );
117
+ const priceSource =
118
+ resource?.categorySlug === 'service'
119
+ ? getServiceLinkedProduct(resource, resources) || resource
120
+ : resource;
121
+ const productData = parsePrimaryShopifyProductData(priceSource) || {};
122
+ const fallbackVariantId =
123
+ resource?.categorySlug === 'service'
124
+ ? getServiceVariantIdFromCanonicalProduct(resource, resources)
125
+ : undefined;
126
+ const resolvedVariantId = item.variantId || fallbackVariantId;
113
127
  const variant = (productData?.variants || []).find(
114
- (v: any) => v.id === item.variantId
128
+ (v: any) => v.id === resolvedVariantId
115
129
  );
130
+ const sharedFeeService =
131
+ resource?.categorySlug === 'service' &&
132
+ isSharedFeeService(resource, productResources);
133
+ const resolvedTitle =
134
+ resource?.categorySlug === 'service'
135
+ ? resource.title
136
+ : productData?.title || resource?.title || 'Loading...';
116
137
  return {
117
138
  ...item,
118
- title: productData?.title || resource?.title || 'Loading...',
139
+ title: resolvedTitle,
119
140
  price: variant?.price?.amount || '0.00',
141
+ currencyCode: variant?.price?.currencyCode || 'USD',
142
+ sharedFeeService,
120
143
  resourceNode: resource,
121
144
  resource: {
122
145
  id: item.resourceId,
@@ -134,9 +157,18 @@ export default function CheckoutModal({
134
157
  () => enrichedCart.some((item) => item.resource?.needsBooking),
135
158
  [enrichedCart]
136
159
  );
160
+ const checkoutLines = useMemo(
161
+ () => buildShopifyCheckoutLines($cartItems, resources),
162
+ [$cartItems, resources]
163
+ );
164
+ const sharedFeeChargeLine = useMemo(
165
+ () => getSharedFeeChargeLineSummary($cartItems, resources),
166
+ [$cartItems, resources]
167
+ );
137
168
  const needsPayment = useMemo(
138
- () => enrichedCart.some((item) => !!item.variantId),
139
- [enrichedCart]
169
+ () =>
170
+ hasGidBackedCheckout($cartItems, resources) || checkoutLines.length > 0,
171
+ [$cartItems, resources, checkoutLines]
140
172
  );
141
173
  const totalDuration = useMemo(() => {
142
174
  const rawMinutes = enrichedCart.reduce(
@@ -201,6 +233,21 @@ export default function CheckoutModal({
201
233
  }
202
234
  }, [ProfileStorage.isProfileUnlocked(), $customer.email]);
203
235
 
236
+ useEffect(() => {
237
+ if (needsBooking) {
238
+ return;
239
+ }
240
+ if (selectedSlot) {
241
+ setSelectedSlot(null);
242
+ }
243
+ if (shopTimeZone) {
244
+ setShopTimeZone(undefined);
245
+ }
246
+ if (internalState === 'BOOKING') {
247
+ setInternalState('SUMMARY');
248
+ }
249
+ }, [needsBooking, selectedSlot, shopTimeZone, internalState]);
250
+
204
251
  useEffect(() => {
205
252
  if (
206
253
  $globalCartState !== CART_STATES.CHECKOUT ||
@@ -274,6 +321,8 @@ export default function CheckoutModal({
274
321
  transactionTraceId.set('');
275
322
  cartState.set(CART_STATES.READY);
276
323
  setInternalState('IDENTITY_EMAIL');
324
+ setSelectedSlot(null);
325
+ setShopTimeZone(undefined);
277
326
  setError(null);
278
327
  if (redirect) window.location.href = `/`;
279
328
  };
@@ -405,6 +454,8 @@ export default function CheckoutModal({
405
454
  );
406
455
  if (response && response.success) {
407
456
  cartStore.set({});
457
+ setSelectedSlot(null);
458
+ setShopTimeZone(undefined);
408
459
  setInternalState('SUCCESS');
409
460
  } else {
410
461
  setError(response?.error || 'Failed to confirm booking.');
@@ -420,53 +471,43 @@ export default function CheckoutModal({
420
471
  setError(null);
421
472
  setInternalState('PROCESSING');
422
473
  try {
474
+ const lines = buildShopifyCheckoutLines(cartStore.get(), resources);
475
+ if (lines.length === 0) {
476
+ throw new Error('No checkout lines available.');
477
+ }
423
478
  const response = await fetch('/api/shopify/createCart', {
424
479
  method: 'POST',
425
480
  headers: { 'Content-Type': 'application/json' },
426
481
  body: JSON.stringify({
427
- lines: enrichedCart
428
- .filter((i) => i.variantId)
429
- .map((i) => ({
430
- merchandiseId: i.variantId,
431
- quantity: i.quantity || 1,
432
- })),
482
+ lines,
433
483
  email: $customer.email,
434
- ...(needsBooking
435
- ? {
436
- attributes: [
437
- { key: 'bookingId', value: transactionTraceId.get() },
438
- ...(selectedSlot
439
- ? [
440
- {
441
- key: 'Appointment Date',
442
- value: selectedSlot.start.toLocaleDateString(
443
- 'en-US',
444
- { timeZone: shopTimeZone }
445
- ),
446
- },
447
- {
448
- key: 'Appointment Time',
449
- value: selectedSlot.start.toLocaleTimeString(
450
- 'en-US',
451
- {
452
- hour: '2-digit',
453
- minute: '2-digit',
454
- timeZone: shopTimeZone,
455
- }
456
- ),
457
- },
458
- {
459
- key: 'Appointment Mode',
460
- value:
461
- appointmentMode === 'REMOTE'
462
- ? 'Remote'
463
- : 'In Person',
464
- },
465
- ]
466
- : []),
467
- ],
468
- }
469
- : {}),
484
+ attributes: [
485
+ { key: 'bookingId', value: transactionTraceId.get() },
486
+ { key: 'leadId', value: $customer.leadId },
487
+ ...(needsBooking && selectedSlot
488
+ ? [
489
+ {
490
+ key: 'Appointment Date',
491
+ value: selectedSlot.start.toLocaleDateString('en-US', {
492
+ timeZone: shopTimeZone,
493
+ }),
494
+ },
495
+ {
496
+ key: 'Appointment Time',
497
+ value: selectedSlot.start.toLocaleTimeString('en-US', {
498
+ hour: '2-digit',
499
+ minute: '2-digit',
500
+ timeZone: shopTimeZone,
501
+ }),
502
+ },
503
+ {
504
+ key: 'Appointment Mode',
505
+ value:
506
+ appointmentMode === 'REMOTE' ? 'Remote' : 'In Person',
507
+ },
508
+ ]
509
+ : []),
510
+ ],
470
511
  }),
471
512
  });
472
513
  const result = await response.json();
@@ -476,6 +517,8 @@ export default function CheckoutModal({
476
517
  isShopifyHandoff.set(true);
477
518
  cartStore.set({});
478
519
  cartState.set(CART_STATES.READY);
520
+ setSelectedSlot(null);
521
+ setShopTimeZone(undefined);
479
522
  window.location.href = result.checkoutUrl;
480
523
  } else {
481
524
  throw new Error('No checkout URL');
@@ -654,21 +697,55 @@ export default function CheckoutModal({
654
697
  <h3 className="mb-4 font-bold text-gray-900">
655
698
  Order Summary
656
699
  </h3>
657
- {enrichedCart.map((item, idx) => (
658
- <div
659
- key={idx}
660
- className="flex justify-between text-sm text-gray-700"
661
- >
662
- <span>{item.title}</span>
700
+ {enrichedCart.map((item, idx) => {
701
+ if (item.sharedFeeService) {
702
+ return (
703
+ <div
704
+ key={idx}
705
+ className="flex justify-between text-sm text-gray-700"
706
+ >
707
+ <span>
708
+ {item.resourceNode?.title || item.title}
709
+ </span>
710
+ </div>
711
+ );
712
+ }
713
+
714
+ return (
715
+ <div
716
+ key={idx}
717
+ className="flex justify-between text-sm text-gray-700"
718
+ >
719
+ <span>{item.title}</span>
720
+ <span className="font-bold">
721
+ $
722
+ {(
723
+ parseFloat(item.price) * (item.quantity || 1)
724
+ ).toFixed(2)}
725
+ {item.currencyCode ? ` ${item.currencyCode}` : ''}
726
+ </span>
727
+ </div>
728
+ );
729
+ })}
730
+ {sharedFeeChargeLine && (
731
+ <div className="mt-2 flex justify-between border-t border-gray-200 pt-2 text-sm text-gray-700">
732
+ <div>
733
+ <span className="font-bold">
734
+ {sharedFeeChargeLine.title}
735
+ </span>
736
+ {sharedFeeChargeLine.description && (
737
+ <p className="text-xs text-gray-500">
738
+ {sharedFeeChargeLine.description}
739
+ </p>
740
+ )}
741
+ </div>
663
742
  <span className="font-bold">
664
- $
665
- {(
666
- parseFloat(item.price) * (item.quantity || 1)
667
- ).toFixed(2)}
743
+ ${parseFloat(sharedFeeChargeLine.amount).toFixed(2)}{' '}
744
+ {sharedFeeChargeLine.currencyCode}
668
745
  </span>
669
746
  </div>
670
- ))}
671
- {selectedSlot && (
747
+ )}
748
+ {needsBooking && selectedSlot && (
672
749
  <div className="mt-6 border-t border-gray-200 pt-6">
673
750
  <p className="text-xs font-bold uppercase text-gray-500">
674
751
  Appointment
@@ -5,12 +5,13 @@ import {
5
5
  cartStore,
6
6
  modalState,
7
7
  transactionTraceId,
8
- getCartItemKey,
9
8
  } from '@/stores/shopify';
10
9
  import { bookingHelpers } from '@/utils/api/bookingHelpers';
11
10
  import {
12
11
  RESTRICTION_MESSAGES,
13
12
  calculateCartDuration,
13
+ getCartItemKey,
14
+ isSharedFeeService,
14
15
  } from '@/utils/customHelpers';
15
16
  import { wouldCartHaveImpossibleRemoteMix } from '@/utils/booking/appointmentMode';
16
17
  import type { ResourceNode } from '@/types/compositorTypes';
@@ -27,6 +28,9 @@ export default function ShopifyCartManager({
27
28
  brandConfig,
28
29
  }: ShopifyCartManagerProps) {
29
30
  const queue = useStore(addQueue);
31
+ const productResources = resources.filter(
32
+ (r) => r.categorySlug === 'product'
33
+ );
30
34
 
31
35
  useEffect(() => {
32
36
  if (queue.length > 0) {
@@ -39,19 +43,37 @@ export default function ShopifyCartManager({
39
43
  return;
40
44
  }
41
45
 
42
- const key = getCartItemKey(actionItem);
46
+ const key = getCartItemKey(actionItem, resource, productResources);
43
47
  const currentCart = cartStore.get();
44
48
  const currentItem = currentCart[key];
45
- const currentQty = currentItem?.quantity || 0;
49
+ const isSharedFeeResource = isSharedFeeService(
50
+ resource,
51
+ productResources
52
+ );
53
+ const legacySharedKeys = isSharedFeeResource
54
+ ? Object.keys(currentCart).filter(
55
+ (cartKey) =>
56
+ cartKey !== key &&
57
+ currentCart[cartKey]?.resourceId === actionItem.resourceId
58
+ )
59
+ : [];
60
+ const legacySharedItems = legacySharedKeys
61
+ .map((cartKey) => currentCart[cartKey])
62
+ .filter((item): item is CartItemState => !!item);
63
+ const mergedCurrentItem = currentItem || legacySharedItems[0];
64
+ const currentQty = isSharedFeeResource
65
+ ? currentItem?.quantity ||
66
+ legacySharedItems.reduce((total, item) => total + item.quantity, 0)
67
+ : currentItem?.quantity || 0;
46
68
  const nextCart = { ...currentCart };
47
69
 
48
70
  if (actionItem.action === 'remove') {
49
- const newQty = Math.max(0, currentQty - 1);
71
+ const newQty = isSharedFeeResource ? 0 : Math.max(0, currentQty - 1);
50
72
 
51
73
  if (newQty === 0) {
52
74
  if (
53
75
  resource?.optionsPayload?.needsBooking ||
54
- currentItem?.boundResourceId
76
+ mergedCurrentItem?.boundResourceId
55
77
  ) {
56
78
  const traceId = transactionTraceId.get();
57
79
  if (traceId) {
@@ -59,23 +81,34 @@ export default function ShopifyCartManager({
59
81
  .releaseHold(traceId)
60
82
  .catch((err) =>
61
83
  console.error('Failed to release hold on cart removal:', err)
62
- );
84
+ )
85
+ .finally(() => {
86
+ transactionTraceId.set('');
87
+ });
63
88
  }
64
89
  }
65
90
  delete nextCart[key];
91
+ legacySharedKeys.forEach((legacyKey) => {
92
+ delete nextCart[legacyKey];
93
+ });
66
94
  } else {
67
95
  nextCart[key] = {
68
- ...currentItem,
96
+ ...mergedCurrentItem,
69
97
  resourceId: actionItem.resourceId,
70
98
  quantity: newQty,
71
99
  };
72
100
  }
73
101
 
74
- if (currentItem?.boundResourceId || actionItem.boundResourceId) {
102
+ if (mergedCurrentItem?.boundResourceId || actionItem.boundResourceId) {
75
103
  const boundId =
76
- currentItem?.boundResourceId || actionItem.boundResourceId;
104
+ mergedCurrentItem?.boundResourceId || actionItem.boundResourceId;
77
105
  if (boundId) {
78
- const serviceKey = getCartItemKey({ resourceId: boundId });
106
+ const boundResource = resources.find((r) => r.id === boundId);
107
+ const serviceKey = getCartItemKey(
108
+ { resourceId: boundId },
109
+ boundResource,
110
+ productResources
111
+ );
79
112
  const serviceItem = nextCart[serviceKey];
80
113
  if (serviceItem) {
81
114
  const newServiceQty = Math.max(0, serviceItem.quantity - 1);
@@ -95,27 +128,39 @@ export default function ShopifyCartManager({
95
128
  addQueue.set(remaining);
96
129
  } else if (actionItem.action === 'add') {
97
130
  transactionTraceId.set('');
98
- const newQty = currentQty + 1;
131
+ const newQty = isSharedFeeResource ? 1 : currentQty + 1;
99
132
 
100
133
  const newItem: CartItemState = {
101
134
  resourceId: actionItem.resourceId,
102
135
  quantity: newQty,
103
- gid: actionItem.gid || currentItem?.gid,
104
- variantId: actionItem.variantId || currentItem?.variantId,
136
+ gid: actionItem.gid || mergedCurrentItem?.gid,
137
+ variantId: isSharedFeeResource
138
+ ? undefined
139
+ : actionItem.variantId || mergedCurrentItem?.variantId,
105
140
  variantIdShipped:
106
- actionItem.variantIdShipped || currentItem?.variantIdShipped,
141
+ actionItem.variantIdShipped || mergedCurrentItem?.variantIdShipped,
107
142
  variantIdPickup:
108
- actionItem.variantIdPickup || currentItem?.variantIdPickup,
143
+ actionItem.variantIdPickup || mergedCurrentItem?.variantIdPickup,
109
144
  boundResourceId:
110
- actionItem.boundResourceId || currentItem?.boundResourceId,
145
+ actionItem.boundResourceId || mergedCurrentItem?.boundResourceId,
111
146
  };
112
147
 
148
+ legacySharedKeys.forEach((legacyKey) => {
149
+ delete nextCart[legacyKey];
150
+ });
113
151
  nextCart[key] = newItem;
114
152
 
115
153
  if (newItem.boundResourceId) {
116
- const serviceKey = getCartItemKey({
117
- resourceId: newItem.boundResourceId,
118
- });
154
+ const boundResource = resources.find(
155
+ (r) => r.id === newItem.boundResourceId
156
+ );
157
+ const serviceKey = getCartItemKey(
158
+ {
159
+ resourceId: newItem.boundResourceId,
160
+ },
161
+ boundResource,
162
+ productResources
163
+ );
119
164
  const serviceItem = nextCart[serviceKey];
120
165
 
121
166
  if (serviceItem) {
@@ -6,6 +6,7 @@ import {
6
6
  CART_STATES,
7
7
  isShopifyHandoff,
8
8
  } from '@/stores/shopify';
9
+ import { buildShopifyCheckoutLines } from '@/utils/customHelpers';
9
10
  import type { ResourceNode } from '@/types/compositorTypes';
10
11
 
11
12
  interface ShopifyCheckoutProps {
@@ -33,32 +34,7 @@ export default function ShopifyCheckout({
33
34
  setStatus('PROCESSING');
34
35
 
35
36
  try {
36
- const cartItems = Object.values(cart);
37
-
38
- const lines = cartItems
39
- .map((item) => {
40
- // Resolve the ResourceNode for this item
41
- const resource = resources.find((r) => r.id === item.resourceId);
42
-
43
- if (!resource) {
44
- return null;
45
- }
46
-
47
- // Use the explicitly sanitized variant ID from the cart state
48
- const merchandiseId = item.variantId;
49
-
50
- // If we have no ID, we cannot add this item to the Shopify cart.
51
- if (!merchandiseId) return null;
52
-
53
- return {
54
- merchandiseId,
55
- quantity: item.quantity,
56
- };
57
- })
58
- .filter((line) => line !== null) as Array<{
59
- merchandiseId: string;
60
- quantity: number;
61
- }>;
37
+ const lines = buildShopifyCheckoutLines(cart, resources);
62
38
 
63
39
  if (lines.length === 0) {
64
40
  throw new Error(
@@ -1,6 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { useStore } from '@nanostores/react';
3
3
  import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
4
+ import { collectServiceGids } from '@/utils/customHelpers';
4
5
  import { getShopifyImage } from '@/utils/helpers';
5
6
  import type { ResourceNode } from '@/types/compositorTypes';
6
7
 
@@ -240,6 +241,13 @@ export default function ShopifyProductGrid({ resources = {}, options }: Props) {
240
241
  products = products.filter((p) => p.optionsPayload?.group === group);
241
242
  }
242
243
 
244
+ const serviceGids = collectServiceGids(services);
245
+ products = products.filter((p) => {
246
+ const gid =
247
+ typeof p.optionsPayload?.gid === 'string' ? p.optionsPayload.gid : '';
248
+ return gid === '' || !serviceGids.has(gid);
249
+ });
250
+
243
251
  if (products.length === 0) return null;
244
252
 
245
253
  return (
@@ -1,10 +1,10 @@
1
1
  import { useStore } from '@nanostores/react';
2
+ import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
2
3
  import {
3
- cartStore,
4
- addQueue,
5
4
  getCartItemKey,
6
- type CartAction,
7
- } from '@/stores/shopify';
5
+ getServiceVariantIdFromCanonicalProduct,
6
+ isSharedFeeService,
7
+ } from '@/utils/customHelpers';
8
8
  import type { ResourceNode } from '@/types/compositorTypes';
9
9
 
10
10
  interface Props {
@@ -55,33 +55,17 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
55
55
  (s) => !boundServiceSlugs.has(s.slug)
56
56
  );
57
57
 
58
- const getServiceVariantId = (resource: ResourceNode): string | undefined => {
59
- try {
60
- if (resource.optionsPayload?.shopifyData) {
61
- const data = JSON.parse(resource.optionsPayload.shopifyData);
62
- // Handle both raw product data and simplified product objects
63
- const product = data.products?.[0] || data;
64
- return product?.variants?.[0]?.id;
65
- }
66
- } catch (e) {
67
- return undefined;
68
- }
69
- return undefined;
70
- };
71
-
72
58
  const handleToggle = (resource: ResourceNode, currentQuantity: number) => {
73
59
  const actionType = currentQuantity > 0 ? 'remove' : 'add';
60
+ const gid =
61
+ typeof resource.optionsPayload?.gid === 'string'
62
+ ? resource.optionsPayload.gid
63
+ : undefined;
74
64
 
75
- const variantId = getServiceVariantId(resource);
76
- let gid: string | undefined;
77
-
78
- try {
79
- if (resource.optionsPayload?.shopifyData) {
80
- const data = JSON.parse(resource.optionsPayload.shopifyData);
81
- const product = data.products?.[0] || data;
82
- gid = product?.id;
83
- }
84
- } catch (e) {}
65
+ const sharedFee = isSharedFeeService(resource, products);
66
+ const variantId = sharedFee
67
+ ? undefined
68
+ : getServiceVariantIdFromCanonicalProduct(resource, products);
85
69
 
86
70
  const newAction: CartAction = {
87
71
  resourceId: resource.id,
@@ -115,14 +99,20 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
115
99
  <section className="w-full">
116
100
  <div className="space-y-4">
117
101
  {displayServices.map((resource) => {
118
- const variantId = getServiceVariantId(resource);
119
- const key = getCartItemKey({
120
- resourceId: resource.id,
121
- variantId,
122
- });
102
+ const key = getCartItemKey(
103
+ { resourceId: resource.id },
104
+ resource,
105
+ products
106
+ );
123
107
 
124
108
  const cartItem = cart[key];
125
- const isSelected = (cartItem?.quantity || 0) > 0;
109
+ const legacyCartItem = Object.values(cart).find(
110
+ (item) =>
111
+ item.resourceId === resource.id && (item.quantity || 0) > 0
112
+ );
113
+ const selectedQuantity =
114
+ cartItem?.quantity || legacyCartItem?.quantity || 0;
115
+ const isSelected = selectedQuantity > 0;
126
116
  const duration = resource.optionsPayload?.bookingLengthMinutes;
127
117
 
128
118
  return (
@@ -152,9 +142,7 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
152
142
 
153
143
  <div className="ml-4 flex-shrink-0">
154
144
  <button
155
- onClick={() =>
156
- handleToggle(resource, cartItem?.quantity || 0)
157
- }
145
+ onClick={() => handleToggle(resource, selectedQuantity)}
158
146
  className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
159
147
  isSelected ? 'bg-black' : 'bg-gray-200'
160
148
  }`}
@@ -76,7 +76,7 @@ if (hasShopify) {
76
76
  <>
77
77
  {slug !== `cart` ? (
78
78
  <div class="flex w-full justify-end px-4 py-2 md:px-8">
79
- <CartIcon client:only="react" />
79
+ <CartIcon client:only="react" resources={shopifyResources} />
80
80
  </div>
81
81
  ) : (
82
82
  <CheckoutModal