astro-tractstack 2.3.2 → 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 (58) hide show
  1. package/bin/create-tractstack.js +7 -4
  2. package/dist/index.js +51 -8
  3. package/package.json +1 -1
  4. package/templates/custom/shopify/Cart.tsx +279 -118
  5. package/templates/custom/shopify/CartIcon.tsx +8 -8
  6. package/templates/custom/shopify/CheckoutModal.tsx +328 -65
  7. package/templates/custom/shopify/ShopifyCartManager.tsx +117 -60
  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/custom/shopify/cart.astro +7 -1
  12. package/templates/src/components/Header.astro +4 -2
  13. package/templates/src/components/compositor/Node.tsx +39 -9
  14. package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
  15. package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
  16. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
  17. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
  18. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  19. package/templates/src/components/form/advanced/APIConfigSection.tsx +249 -4
  20. package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
  21. package/templates/src/components/form/shopify/SchedulingSection.tsx +44 -0
  22. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +66 -21
  23. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +266 -18
  24. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +1 -0
  25. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
  26. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +240 -65
  27. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
  28. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +91 -10
  29. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
  30. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
  31. package/templates/src/constants.ts +2 -0
  32. package/templates/src/layouts/Layout.astro +26 -0
  33. package/templates/src/pages/api/auth/logout.ts +35 -2
  34. package/templates/src/pages/api/google/oauth/callback.ts +50 -0
  35. package/templates/src/pages/api/google/oauth/disconnect.ts +32 -0
  36. package/templates/src/pages/api/google/oauth/start.ts +32 -0
  37. package/templates/src/pages/api/google/oauth/status.ts +32 -0
  38. package/templates/src/pages/api/sales/list.ts +66 -0
  39. package/templates/src/pages/api/sales/metrics.ts +60 -0
  40. package/templates/src/pages/context/[...contextSlug].astro +50 -31
  41. package/templates/src/pages/privacy.astro +84 -0
  42. package/templates/src/pages/storykeep/advanced.astro +4 -1
  43. package/templates/src/pages/terms.astro +47 -0
  44. package/templates/src/stores/nodes.ts +8 -0
  45. package/templates/src/stores/shopify.ts +5 -0
  46. package/templates/src/types/tractstack.ts +87 -0
  47. package/templates/src/utils/api/advancedConfig.ts +2 -1
  48. package/templates/src/utils/api/advancedHelpers.ts +20 -0
  49. package/templates/src/utils/api/bookingHelpers.ts +3 -1
  50. package/templates/src/utils/api/brandConfig.ts +2 -0
  51. package/templates/src/utils/api/brandHelpers.ts +14 -1
  52. package/templates/src/utils/api/salesHelpers.ts +21 -0
  53. package/templates/src/utils/booking/appointmentMode.ts +135 -0
  54. package/templates/src/utils/customHelpers.ts +287 -2
  55. package/utils/inject-files.ts +47 -4
  56. package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +0 -101
  57. package/templates/src/utils/actions/actionButton.ts +0 -103
  58. package/templates/src/utils/actions/preParse_Clicked.ts +0 -87
@@ -1,4 +1,11 @@
1
- import { useState, useEffect, useMemo, type FormEvent } from 'react';
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useCallback,
7
+ type FormEvent,
8
+ } from 'react';
2
9
  import { ulid } from 'ulid';
3
10
  import { useStore } from '@nanostores/react';
4
11
  import { Dialog } from '@ark-ui/react/dialog';
@@ -9,12 +16,26 @@ import {
9
16
  customerDetails,
10
17
  setCustomerDetails,
11
18
  CART_STATES,
19
+ preferredAppointmentMode,
12
20
  transactionTraceId,
13
21
  isShopifyHandoff,
14
22
  } from '@/stores/shopify';
15
23
  import { bookingHelpers } from '@/utils/api/bookingHelpers';
24
+ import {
25
+ deriveAppointmentConstraints,
26
+ pickInitialAppointmentMode,
27
+ } from '@/utils/booking/appointmentMode';
16
28
  import { NativeBookingCalendar } from './NativeBookingCalendar';
17
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';
18
39
  import type { ResourceNode } from '@/types/compositorTypes';
19
40
 
20
41
  type CheckoutState =
@@ -24,14 +45,19 @@ type CheckoutState =
24
45
  | 'SUMMARY'
25
46
  | 'PROCESSING'
26
47
  | 'SUCCESS';
48
+ type AppointmentMode = 'IN_PERSON' | 'REMOTE';
27
49
 
28
50
  interface CheckoutModalProps {
29
51
  maxLength: number;
52
+ allowRemote?: boolean;
53
+ remoteOnly?: boolean;
30
54
  resources?: ResourceNode[];
31
55
  }
32
56
 
33
57
  export default function CheckoutModal({
34
58
  maxLength,
59
+ allowRemote = false,
60
+ remoteOnly = false,
35
61
  resources = [],
36
62
  }: CheckoutModalProps) {
37
63
  const $globalCartState = useStore(cartState);
@@ -57,6 +83,24 @@ export default function CheckoutModal({
57
83
  start: Date;
58
84
  end: Date;
59
85
  } | null>(null);
86
+ const [appointmentMode, setAppointmentMode] = useState<AppointmentMode>(() =>
87
+ pickInitialAppointmentMode(
88
+ deriveAppointmentConstraints(cartStore.get(), resources, {
89
+ allowRemote,
90
+ remoteOnly,
91
+ }),
92
+ preferredAppointmentMode.get()
93
+ )
94
+ );
95
+ const appointmentModeRef = useRef<AppointmentMode>(
96
+ pickInitialAppointmentMode(
97
+ deriveAppointmentConstraints(cartStore.get(), resources, {
98
+ allowRemote,
99
+ remoteOnly,
100
+ }),
101
+ preferredAppointmentMode.get()
102
+ )
103
+ );
60
104
  const [error, setError] = useState<string | null>(null);
61
105
 
62
106
  const isOpen =
@@ -67,21 +111,36 @@ export default function CheckoutModal({
67
111
  const enrichedCart = useMemo(() => {
68
112
  return Object.values($cartItems).map((item: any) => {
69
113
  const resource = resources.find((r) => r.id === item.resourceId);
70
- let productData: any = {};
71
- if (resource?.optionsPayload?.shopifyData) {
72
- try {
73
- productData = JSON.parse(resource.optionsPayload.shopifyData);
74
- } catch (e) {
75
- console.error('Failed to parse Shopify data', item.resourceId);
76
- }
77
- }
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;
78
127
  const variant = (productData?.variants || []).find(
79
- (v: any) => v.id === item.variantId
128
+ (v: any) => v.id === resolvedVariantId
80
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...';
81
137
  return {
82
138
  ...item,
83
- title: productData?.title || resource?.title || 'Loading...',
139
+ title: resolvedTitle,
84
140
  price: variant?.price?.amount || '0.00',
141
+ currencyCode: variant?.price?.currencyCode || 'USD',
142
+ sharedFeeService,
143
+ resourceNode: resource,
85
144
  resource: {
86
145
  id: item.resourceId,
87
146
  needsBooking:
@@ -98,9 +157,18 @@ export default function CheckoutModal({
98
157
  () => enrichedCart.some((item) => item.resource?.needsBooking),
99
158
  [enrichedCart]
100
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
+ );
101
168
  const needsPayment = useMemo(
102
- () => enrichedCart.some((item) => !!item.variantId),
103
- [enrichedCart]
169
+ () =>
170
+ hasGidBackedCheckout($cartItems, resources) || checkoutLines.length > 0,
171
+ [$cartItems, resources, checkoutLines]
104
172
  );
105
173
  const totalDuration = useMemo(() => {
106
174
  const rawMinutes = enrichedCart.reduce(
@@ -111,6 +179,51 @@ export default function CheckoutModal({
111
179
  return Math.min(Math.ceil(rawMinutes / 15) * 15, maxLength);
112
180
  }, [enrichedCart]);
113
181
 
182
+ const appointmentConstraints = useMemo(
183
+ () =>
184
+ deriveAppointmentConstraints($cartItems, resources, {
185
+ allowRemote,
186
+ remoteOnly,
187
+ }),
188
+ [$cartItems, resources, allowRemote, remoteOnly]
189
+ );
190
+
191
+ const { effectiveRemoteOnly, inPersonAvailable } = appointmentConstraints;
192
+
193
+ const remoteAvailable =
194
+ appointmentConstraints.effectiveRemoteOnly ||
195
+ (needsBooking &&
196
+ appointmentConstraints.effectiveAllowRemote &&
197
+ appointmentConstraints.serviceResources.length > 0 &&
198
+ appointmentConstraints.allServicesAllowRemote);
199
+
200
+ const applyAppointmentMode = useCallback((nextMode: AppointmentMode) => {
201
+ appointmentModeRef.current = nextMode;
202
+ setAppointmentMode((currentMode) =>
203
+ currentMode === nextMode ? currentMode : nextMode
204
+ );
205
+ preferredAppointmentMode.set(nextMode);
206
+ }, []);
207
+
208
+ useEffect(() => {
209
+ if (selectedSlot) {
210
+ return;
211
+ }
212
+ if (effectiveRemoteOnly) {
213
+ applyAppointmentMode('REMOTE');
214
+ return;
215
+ }
216
+ if (!remoteAvailable && appointmentMode === 'REMOTE') {
217
+ applyAppointmentMode('IN_PERSON');
218
+ }
219
+ }, [
220
+ effectiveRemoteOnly,
221
+ remoteAvailable,
222
+ appointmentMode,
223
+ selectedSlot,
224
+ applyAppointmentMode,
225
+ ]);
226
+
114
227
  useEffect(() => {
115
228
  const profile = ProfileStorage.getProfileData();
116
229
  if (ProfileStorage.isProfileUnlocked() && profile.email) {
@@ -120,6 +233,21 @@ export default function CheckoutModal({
120
233
  }
121
234
  }, [ProfileStorage.isProfileUnlocked(), $customer.email]);
122
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
+
123
251
  useEffect(() => {
124
252
  if (
125
253
  $globalCartState !== CART_STATES.CHECKOUT ||
@@ -146,6 +274,36 @@ export default function CheckoutModal({
146
274
  }
147
275
  }, [$globalCartState, needsBooking, $customer.leadId, internalState]);
148
276
 
277
+ useEffect(() => {
278
+ if (!isOpen || selectedSlot) {
279
+ return;
280
+ }
281
+
282
+ if (
283
+ internalState !== 'IDENTITY_EMAIL' &&
284
+ internalState !== 'IDENTITY_NEW_USER'
285
+ ) {
286
+ return;
287
+ }
288
+
289
+ if (effectiveRemoteOnly) {
290
+ applyAppointmentMode('REMOTE');
291
+ return;
292
+ }
293
+
294
+ const next = pickInitialAppointmentMode(
295
+ appointmentConstraints,
296
+ preferredAppointmentMode.get()
297
+ );
298
+ applyAppointmentMode(next);
299
+ }, [
300
+ isOpen,
301
+ selectedSlot,
302
+ internalState,
303
+ appointmentConstraints,
304
+ applyAppointmentMode,
305
+ ]);
306
+
149
307
  const handleClose = async () => {
150
308
  const redirect = internalState === 'SUCCESS';
151
309
  const currentTraceId = transactionTraceId.get();
@@ -163,6 +321,8 @@ export default function CheckoutModal({
163
321
  transactionTraceId.set('');
164
322
  cartState.set(CART_STATES.READY);
165
323
  setInternalState('IDENTITY_EMAIL');
324
+ setSelectedSlot(null);
325
+ setShopTimeZone(undefined);
166
326
  setError(null);
167
327
  if (redirect) window.location.href = `/`;
168
328
  };
@@ -249,7 +409,8 @@ export default function CheckoutModal({
249
409
  transactionTraceId.get(),
250
410
  start.toISOString(),
251
411
  end.toISOString(),
252
- cartResourceIds
412
+ cartResourceIds,
413
+ appointmentModeRef.current
253
414
  );
254
415
  if (response && (response.success || response.status === 'PENDING')) {
255
416
  setInternalState('SUMMARY');
@@ -293,6 +454,8 @@ export default function CheckoutModal({
293
454
  );
294
455
  if (response && response.success) {
295
456
  cartStore.set({});
457
+ setSelectedSlot(null);
458
+ setShopTimeZone(undefined);
296
459
  setInternalState('SUCCESS');
297
460
  } else {
298
461
  setError(response?.error || 'Failed to confirm booking.');
@@ -308,46 +471,43 @@ export default function CheckoutModal({
308
471
  setError(null);
309
472
  setInternalState('PROCESSING');
310
473
  try {
474
+ const lines = buildShopifyCheckoutLines(cartStore.get(), resources);
475
+ if (lines.length === 0) {
476
+ throw new Error('No checkout lines available.');
477
+ }
311
478
  const response = await fetch('/api/shopify/createCart', {
312
479
  method: 'POST',
313
480
  headers: { 'Content-Type': 'application/json' },
314
481
  body: JSON.stringify({
315
- lines: enrichedCart
316
- .filter((i) => i.variantId)
317
- .map((i) => ({
318
- merchandiseId: i.variantId,
319
- quantity: i.quantity || 1,
320
- })),
482
+ lines,
321
483
  email: $customer.email,
322
- ...(needsBooking
323
- ? {
324
- attributes: [
325
- { key: 'bookingId', value: transactionTraceId.get() },
326
- ...(selectedSlot
327
- ? [
328
- {
329
- key: 'Appointment Date',
330
- value: selectedSlot.start.toLocaleDateString(
331
- 'en-US',
332
- { timeZone: shopTimeZone }
333
- ),
334
- },
335
- {
336
- key: 'Appointment Time',
337
- value: selectedSlot.start.toLocaleTimeString(
338
- 'en-US',
339
- {
340
- hour: '2-digit',
341
- minute: '2-digit',
342
- timeZone: shopTimeZone,
343
- }
344
- ),
345
- },
346
- ]
347
- : []),
348
- ],
349
- }
350
- : {}),
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
+ ],
351
511
  }),
352
512
  });
353
513
  const result = await response.json();
@@ -357,6 +517,8 @@ export default function CheckoutModal({
357
517
  isShopifyHandoff.set(true);
358
518
  cartStore.set({});
359
519
  cartState.set(CART_STATES.READY);
520
+ setSelectedSlot(null);
521
+ setShopTimeZone(undefined);
360
522
  window.location.href = result.checkoutUrl;
361
523
  } else {
362
524
  throw new Error('No checkout URL');
@@ -462,36 +624,137 @@ export default function CheckoutModal({
462
624
  </form>
463
625
  )}
464
626
  {internalState === 'BOOKING' && (
465
- <NativeBookingCalendar
466
- totalDurationMinutes={totalDuration}
467
- onSlotSelected={handleSlotSelection}
468
- />
627
+ <div className="space-y-6">
628
+ {needsBooking && (inPersonAvailable || remoteAvailable) && (
629
+ <div className="rounded-xl border border-gray-200 bg-white p-4">
630
+ <p className="text-xs font-bold uppercase tracking-wide text-gray-500">
631
+ Appointment Mode
632
+ </p>
633
+ <div className="mt-3 flex flex-wrap gap-2">
634
+ {inPersonAvailable && (
635
+ <button
636
+ type="button"
637
+ onClick={() => {
638
+ applyAppointmentMode('IN_PERSON');
639
+ }}
640
+ className={`rounded-md px-3 py-2 text-sm font-bold ${
641
+ appointmentMode === 'IN_PERSON'
642
+ ? 'bg-black text-white'
643
+ : 'border border-gray-300 bg-white text-gray-700'
644
+ }`}
645
+ >
646
+ In Person
647
+ </button>
648
+ )}
649
+ {remoteAvailable && (
650
+ <button
651
+ type="button"
652
+ onClick={() => {
653
+ applyAppointmentMode('REMOTE');
654
+ }}
655
+ className={`rounded-md px-3 py-2 text-sm font-bold ${
656
+ appointmentMode === 'REMOTE'
657
+ ? 'bg-black text-white'
658
+ : 'border border-gray-300 bg-white text-gray-700'
659
+ }`}
660
+ >
661
+ Remote
662
+ </button>
663
+ )}
664
+ </div>
665
+ {effectiveRemoteOnly && (
666
+ <p className="mt-2 text-xs font-bold text-gray-500">
667
+ Remote mode is required by shop or service settings.
668
+ </p>
669
+ )}
670
+ </div>
671
+ )}
672
+ <NativeBookingCalendar
673
+ totalDurationMinutes={totalDuration}
674
+ onSlotSelected={handleSlotSelection}
675
+ />
676
+ </div>
469
677
  )}
470
678
  {internalState === 'SUMMARY' && (
471
679
  <div className="space-y-6">
680
+ {needsBooking && (
681
+ <div className="rounded-xl border border-gray-200 bg-white p-4">
682
+ <p className="text-xs font-bold uppercase tracking-wide text-gray-500">
683
+ Appointment Mode
684
+ </p>
685
+ <p className="mt-3 text-sm font-bold text-gray-900">
686
+ {appointmentMode === 'REMOTE' ? 'Remote' : 'In Person'}
687
+ </p>
688
+ {effectiveRemoteOnly && (
689
+ <p className="mt-2 text-xs font-bold text-gray-500">
690
+ Remote mode is required by shop or service settings.
691
+ </p>
692
+ )}
693
+ </div>
694
+ )}
695
+
472
696
  <div className="rounded-xl border border-gray-200 bg-gray-50 p-6">
473
697
  <h3 className="mb-4 font-bold text-gray-900">
474
698
  Order Summary
475
699
  </h3>
476
- {enrichedCart.map((item, idx) => (
477
- <div
478
- key={idx}
479
- className="flex justify-between text-sm text-gray-700"
480
- >
481
- <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>
482
742
  <span className="font-bold">
483
- $
484
- {(
485
- parseFloat(item.price) * (item.quantity || 1)
486
- ).toFixed(2)}
743
+ ${parseFloat(sharedFeeChargeLine.amount).toFixed(2)}{' '}
744
+ {sharedFeeChargeLine.currencyCode}
487
745
  </span>
488
746
  </div>
489
- ))}
490
- {selectedSlot && (
747
+ )}
748
+ {needsBooking && selectedSlot && (
491
749
  <div className="mt-6 border-t border-gray-200 pt-6">
492
750
  <p className="text-xs font-bold uppercase text-gray-500">
493
751
  Appointment
494
752
  </p>
753
+ <p className="mt-1 text-xs font-bold uppercase tracking-wide text-gray-500">
754
+ {appointmentMode === 'REMOTE'
755
+ ? 'Remote'
756
+ : 'In Person'}
757
+ </p>
495
758
  <p className="mt-1 text-sm font-bold text-gray-900">
496
759
  {selectedSlot.start.toLocaleDateString('en-US', {
497
760
  timeZone: shopTimeZone,