astro-tractstack 2.2.10 → 2.3.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.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/bin/create-tractstack.js +2 -2
  3. package/dist/index.js +177 -18
  4. package/package.json +4 -2
  5. package/templates/custom/minimal/CodeHook.astro +22 -5
  6. package/templates/custom/shopify/Cart.tsx +372 -0
  7. package/templates/custom/shopify/CartIcon.tsx +47 -0
  8. package/templates/custom/shopify/CartModal.tsx +63 -0
  9. package/templates/custom/shopify/CheckoutModal.tsx +576 -0
  10. package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
  11. package/templates/custom/shopify/ShopifyCartManager.tsx +200 -0
  12. package/templates/custom/shopify/ShopifyCheckout.tsx +167 -0
  13. package/templates/custom/shopify/ShopifyProductGrid.tsx +247 -0
  14. package/templates/custom/shopify/ShopifyServiceList.tsx +135 -0
  15. package/templates/custom/shopify/cart.astro +23 -0
  16. package/templates/custom/with-examples/CodeHook.astro +17 -1
  17. package/templates/custom/with-examples/ProductGrid.astro +1 -1
  18. package/templates/src/client/app.js +4 -2
  19. package/templates/src/components/Footer.astro +4 -4
  20. package/templates/src/components/Header.astro +44 -12
  21. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
  22. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
  23. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
  24. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
  25. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  26. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
  27. package/templates/src/components/form/advanced/APIConfigSection.tsx +407 -38
  28. package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
  29. package/templates/src/components/storykeep/Dashboard.tsx +18 -4
  30. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -0
  31. package/templates/src/components/storykeep/Dashboard_Content.tsx +5 -96
  32. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +668 -0
  33. package/templates/src/components/storykeep/StoryKeepBackdrop.astro +43 -23
  34. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
  35. package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +0 -14
  36. package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +36 -13
  37. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
  38. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +4 -11
  39. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
  40. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +333 -0
  41. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
  42. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +108 -8
  43. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +13 -4
  44. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
  45. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +111 -0
  46. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +393 -0
  47. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
  48. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
  49. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
  50. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
  51. package/templates/src/lib/resources.ts +11 -21
  52. package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
  53. package/templates/src/pages/api/booking/availability.ts +72 -0
  54. package/templates/src/pages/api/booking/cancel.ts +73 -0
  55. package/templates/src/pages/api/booking/confirm.ts +82 -0
  56. package/templates/src/pages/api/booking/hold.ts +75 -0
  57. package/templates/src/pages/api/booking/list.ts +66 -0
  58. package/templates/src/pages/api/booking/metrics.ts +60 -0
  59. package/templates/src/pages/api/booking/release.ts +76 -0
  60. package/templates/src/pages/api/sandbox.ts +2 -2
  61. package/templates/src/pages/api/shopify/createCart.ts +69 -0
  62. package/templates/src/pages/api/shopify/getProducts.ts +64 -0
  63. package/templates/src/pages/storykeep/login.astro +26 -24
  64. package/templates/src/pages/storykeep/logout.astro +1 -10
  65. package/templates/src/pages/storykeep/manage.astro +69 -0
  66. package/templates/src/pages/storykeep/{content.astro → pages.astro} +4 -8
  67. package/templates/src/pages/storykeep/shopify.astro +101 -0
  68. package/templates/src/stores/navigation.ts +3 -42
  69. package/templates/src/stores/nodes.ts +3 -1
  70. package/templates/src/stores/resources.ts +7 -10
  71. package/templates/src/stores/shopify.ts +266 -0
  72. package/templates/src/types/tractstack.ts +75 -0
  73. package/templates/src/utils/api/advancedConfig.ts +7 -1
  74. package/templates/src/utils/api/advancedHelpers.ts +87 -7
  75. package/templates/src/utils/api/bookingHelpers.ts +125 -0
  76. package/templates/src/utils/api/brandHelpers.ts +14 -0
  77. package/templates/src/utils/api/resourceConfig.ts +13 -5
  78. package/templates/src/utils/auth.ts +29 -9
  79. package/templates/src/utils/compositor/aiGeneration.ts +3 -3
  80. package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
  81. package/templates/src/utils/customHelpers.ts +49 -0
  82. package/templates/src/utils/helpers.ts +59 -0
  83. package/templates/src/utils/profileStorage.ts +5 -0
  84. package/templates/src/utils/tenantResolver.ts +2 -1
  85. package/utils/inject-files.ts +161 -2
@@ -0,0 +1,266 @@
1
+ import { atom, map, onMount } from 'nanostores';
2
+ import { persistentAtom } from '@nanostores/persistent';
3
+
4
+ export interface ShopifyVariant {
5
+ id: string;
6
+ title: string;
7
+ price: {
8
+ amount: string;
9
+ currencyCode: string;
10
+ };
11
+ compareAtPrice?: {
12
+ amount: string;
13
+ currencyCode: string;
14
+ };
15
+ sku?: string;
16
+ availableForSale: boolean;
17
+ requiresShipping: boolean;
18
+ selectedOptions: {
19
+ name: string;
20
+ value: string;
21
+ }[];
22
+ }
23
+
24
+ export const modalState = atom<{
25
+ isOpen: boolean;
26
+ type: 'success' | 'restriction';
27
+ title: string;
28
+ message: string;
29
+ }>({
30
+ isOpen: false,
31
+ type: 'success',
32
+ title: '',
33
+ message: '',
34
+ });
35
+
36
+ export interface ShopifyOption {
37
+ name: string;
38
+ values: string[];
39
+ }
40
+
41
+ export interface ShopifyImage {
42
+ url: string;
43
+ altText?: string;
44
+ }
45
+
46
+ export interface ShopifyProduct {
47
+ id: string;
48
+ title: string;
49
+ handle: string;
50
+ description: string;
51
+ options: ShopifyOption[];
52
+ images: ShopifyImage[];
53
+ variants: ShopifyVariant[];
54
+ }
55
+
56
+ export interface ShopifyPageInfo {
57
+ hasNextPage: boolean;
58
+ endCursor: string;
59
+ }
60
+
61
+ export type CartActionType = 'add' | 'remove';
62
+
63
+ export interface CartAction {
64
+ resourceId: string;
65
+ gid?: string;
66
+ variantId?: string;
67
+ variantIdShipped?: string;
68
+ variantIdPickup?: string;
69
+ boundResourceId?: string;
70
+ action: CartActionType;
71
+ suppressModal?: boolean;
72
+ }
73
+
74
+ export interface CartItemState {
75
+ resourceId: string;
76
+ quantity: number;
77
+ variantId?: string;
78
+ gid?: string;
79
+ variantIdShipped?: string;
80
+ variantIdPickup?: string;
81
+ boundResourceId?: string;
82
+ }
83
+
84
+ export const QUEUE_STATES = {
85
+ READY: 'READY',
86
+ ADDING: 'ADDING',
87
+ HAS_REQUIREMENTS: 'HAS_REQUIREMENTS',
88
+ } as const;
89
+
90
+ export type QueueState = (typeof QUEUE_STATES)[keyof typeof QUEUE_STATES];
91
+
92
+ export const CART_STATES = {
93
+ INIT: 'INIT',
94
+ READY: 'READY',
95
+ LOADED: 'LOADED',
96
+ CHECKOUT: 'CHECKOUT',
97
+ BOOKING: 'BOOKING',
98
+ SHOPIFY_HANDOFF: 'SHOPIFY_HANDOFF',
99
+ } as const;
100
+
101
+ export type CartState = (typeof CART_STATES)[keyof typeof CART_STATES];
102
+
103
+ export const isShopifyHandoff = atom<boolean>(false);
104
+ export const shopifyActiveTabStore = atom<string>('dashboards');
105
+
106
+ export const shopifyData = atom<{
107
+ products: ShopifyProduct[];
108
+ pageInfo?: ShopifyPageInfo;
109
+ lastFetched: number;
110
+ }>({
111
+ products: [],
112
+ lastFetched: 0,
113
+ });
114
+
115
+ export const shopifyStatus = map<{
116
+ isLoading: boolean;
117
+ error: string | null;
118
+ }>({
119
+ isLoading: false,
120
+ error: null,
121
+ });
122
+
123
+ export const addQueue = persistentAtom<CartAction[]>(
124
+ 'tractstack_shopify_queue',
125
+ [],
126
+ {
127
+ encode: JSON.stringify,
128
+ decode: JSON.parse,
129
+ }
130
+ );
131
+
132
+ export const cartPersistence = persistentAtom<Record<string, CartItemState>>(
133
+ 'tractstack_shopify_cart',
134
+ {},
135
+ {
136
+ encode: JSON.stringify,
137
+ decode: JSON.parse,
138
+ }
139
+ );
140
+
141
+ export const cartStore = map<Record<string, CartItemState>>(
142
+ cartPersistence.get()
143
+ );
144
+
145
+ onMount(cartStore, () => {
146
+ cartStore.set(cartPersistence.get());
147
+
148
+ const unbind = cartStore.listen((value) => {
149
+ cartPersistence.set(value);
150
+ });
151
+
152
+ return () => unbind();
153
+ });
154
+
155
+ export const queueState = persistentAtom<QueueState>(
156
+ 'tractstack_shopify_queue_state',
157
+ QUEUE_STATES.READY
158
+ );
159
+ export const cartState = persistentAtom<CartState>(
160
+ 'tractstack_shopify_cart_state',
161
+ CART_STATES.INIT
162
+ );
163
+
164
+ let currentAbortController: AbortController | null = null;
165
+
166
+ export async function fetchShopifyProducts(
167
+ q: string = '',
168
+ cursor: string | null = null
169
+ ) {
170
+ if (currentAbortController) {
171
+ currentAbortController.abort();
172
+ }
173
+ currentAbortController = new AbortController();
174
+ const signal = currentAbortController.signal;
175
+
176
+ shopifyStatus.set({ isLoading: true, error: null });
177
+
178
+ try {
179
+ const params = new URLSearchParams();
180
+ if (q) params.set('q', q);
181
+ if (cursor) params.set('cursor', cursor);
182
+
183
+ const queryString = params.toString();
184
+ const url = `/api/shopify/getProducts${queryString ? `?${queryString}` : ''}`;
185
+
186
+ const response = await fetch(url, { signal });
187
+ const result = await response.json();
188
+
189
+ if (!response.ok) {
190
+ throw new Error(result.error || 'Failed to fetch products');
191
+ }
192
+
193
+ shopifyData.set({
194
+ products: result.products || [],
195
+ pageInfo: result.pageInfo,
196
+ lastFetched: Date.now(),
197
+ });
198
+
199
+ shopifyStatus.set({ isLoading: false, error: null });
200
+ } catch (error: unknown) {
201
+ if (error instanceof DOMException && error.name === 'AbortError') {
202
+ return;
203
+ }
204
+
205
+ console.error('Shopify fetch failed:', error);
206
+ shopifyStatus.set({
207
+ isLoading: false,
208
+ error:
209
+ error instanceof Error ? error.message : 'Failed to fetch products',
210
+ });
211
+ }
212
+ }
213
+
214
+ export function clearShopifySearch() {
215
+ if (currentAbortController) {
216
+ currentAbortController.abort();
217
+ }
218
+ shopifyData.set({
219
+ products: [],
220
+ pageInfo: undefined,
221
+ lastFetched: 0,
222
+ });
223
+ shopifyStatus.set({ isLoading: false, error: null });
224
+ }
225
+
226
+ export const transactionTraceId = persistentAtom<string>(
227
+ 'tractstack_shopify_trace_id',
228
+ ''
229
+ );
230
+
231
+ export interface CustomerDetails {
232
+ name: string;
233
+ email: string;
234
+ leadId: string;
235
+ }
236
+
237
+ export const customerDetails = persistentAtom<CustomerDetails>(
238
+ 'tractstack_shopify_customer',
239
+ {
240
+ name: '',
241
+ email: '',
242
+ leadId: '',
243
+ },
244
+ {
245
+ encode: JSON.stringify,
246
+ decode: JSON.parse,
247
+ }
248
+ );
249
+
250
+ export function setCustomerDetails(details: Partial<CustomerDetails>) {
251
+ customerDetails.set({
252
+ ...customerDetails.get(),
253
+ ...details,
254
+ });
255
+ }
256
+
257
+ export function clearCommerceState() {
258
+ cartStore.set({});
259
+ customerDetails.set({
260
+ name: '',
261
+ email: '',
262
+ leadId: '',
263
+ });
264
+ transactionTraceId.set('');
265
+ cartState.set(CART_STATES.READY);
266
+ }
@@ -152,6 +152,19 @@ export interface FullContentMapItem {
152
152
  scale?: string;
153
153
  }
154
154
 
155
+ export interface TimeBlock {
156
+ start: string; // "09:00" for business hours or ISO-8601 for unavailable blocks
157
+ end: string;
158
+ }
159
+
160
+ export interface SchedulingConfig {
161
+ timezone: string;
162
+ bufferGapsMinutes: number;
163
+ maxLengthMinutes: number;
164
+ businessHours: Record<string, TimeBlock>;
165
+ unavailableHours: TimeBlock[];
166
+ }
167
+
155
168
  export interface BrandConfig {
156
169
  TENANT_ID: string;
157
170
  SITE_INIT?: boolean;
@@ -183,7 +196,13 @@ export interface BrandConfig {
183
196
  KNOWN_RESOURCES?: KnownResourcesConfig;
184
197
  DESIGN_LIBRARY?: DesignLibraryConfig;
185
198
  HAS_AAI?: boolean;
199
+ HAS_SHOPIFY?: boolean;
200
+ SHOW_SHOPIFY_HELPER?: boolean;
201
+ SHOPIFY_ADMIN_SLUG?: string;
202
+ USER_SETUP_WEBHOOKS?: boolean;
203
+ HAS_RESEND?: boolean;
186
204
  HAS_HYDRATION_TOKEN?: boolean;
205
+ SCHEDULING?: SchedulingConfig;
187
206
  }
188
207
 
189
208
  export interface BrandConfigState {
@@ -217,7 +236,11 @@ export interface BrandConfigState {
217
236
  knownResources: KnownResourcesConfig;
218
237
  designLibrary?: DesignLibraryConfig;
219
238
  hasAAI: boolean;
239
+ hasShopify: boolean;
240
+ showShopifyHelper: boolean;
241
+ hasResend: boolean;
220
242
  hasHydrationToken: boolean;
243
+ scheduling: SchedulingConfig;
221
244
  }
222
245
 
223
246
  // Form validation types
@@ -251,6 +274,14 @@ export interface AdvancedConfigStatus {
251
274
  editorPasswordSet: boolean;
252
275
  aaiAPIKeySet: boolean;
253
276
  tursoEnabled: boolean;
277
+ shopifyStorefrontTokenSet: boolean;
278
+ shopifyAdminApiKeySet: boolean;
279
+ shopifyApiSecretSet: boolean;
280
+ shopifyApiVersion: string;
281
+ shopifyStoreDomainSet: boolean;
282
+ resendApiKeySet: boolean;
283
+ shopifyAdminSlugSet: boolean;
284
+ userSetupWebhooks: boolean;
254
285
  }
255
286
 
256
287
  export interface AdvancedConfigState {
@@ -259,6 +290,14 @@ export interface AdvancedConfigState {
259
290
  adminPassword: string;
260
291
  editorPassword: string;
261
292
  aaiApiKey: string;
293
+ shopifyStorefrontToken: string;
294
+ shopifyAdminApiKey: string;
295
+ shopifyApiSecret: string;
296
+ shopifyApiVersion: string;
297
+ shopifyStoreDomain: string;
298
+ shopifyAdminSlug: string;
299
+ userSetupWebhooks: boolean;
300
+ resendApiKey: string;
262
301
  }
263
302
 
264
303
  export interface AdvancedConfigUpdateRequest {
@@ -269,6 +308,13 @@ export interface AdvancedConfigUpdateRequest {
269
308
  AAI_API_KEY?: string;
270
309
  HOME_SLUG?: string;
271
310
  TRACTSTACK_HOME_SLUG?: string;
311
+ SHOPIFY_STOREFRONT_TOKEN?: string;
312
+ SHOPIFY_API_SECRET?: string;
313
+ SHOPIFY_API_VERSION?: string;
314
+ SHOPIFY_STORE_DOMAIN?: string;
315
+ SHOPIFY_ADMIN_SLUG?: string;
316
+ USER_SETUP_WEBHOOKS?: boolean;
317
+ RESEND_API_KEY?: string;
272
318
  }
273
319
 
274
320
  export interface MenuNodeState {
@@ -469,3 +515,32 @@ export interface CategorizedResults {
469
515
  contextPaneResults: FTSResult[];
470
516
  resourceResults: FTSResult[];
471
517
  }
518
+
519
+ export type BookingStatus = 'PENDING' | 'CONFIRMED' | 'CANCELLED';
520
+
521
+ export interface BookingEntity {
522
+ id: string; // traceId
523
+ resourceIds: string[];
524
+ leadId: string;
525
+ startTime: string; // ISO-8601 UTC string
526
+ endTime: string; // ISO-8601 UTC string
527
+ status: BookingStatus;
528
+ shopifyOrderId?: string;
529
+ createdAt: string; // ISO-8601 UTC string
530
+ leadEmail?: string;
531
+ leadName?: string;
532
+ }
533
+
534
+ export interface BookingListResponse {
535
+ data: BookingEntity[];
536
+ totalCount: number;
537
+ }
538
+
539
+ export interface BookingMetricsResponse {
540
+ totalMonthlyConfirmed: number;
541
+ totalAnnualConfirmed: number;
542
+ totalWeeklyConfirmed: number;
543
+ leadConversionAnchor: number;
544
+ pendingLast24h: number;
545
+ confirmedLast24h: number;
546
+ }
@@ -28,7 +28,13 @@ export async function getAdvancedConfigStatus(
28
28
  typeof data.tursoTokenSet !== 'boolean' ||
29
29
  typeof data.adminPasswordSet !== 'boolean' ||
30
30
  typeof data.editorPasswordSet !== 'boolean' ||
31
- typeof data.aaiAPIKeySet !== 'boolean'
31
+ typeof data.aaiAPIKeySet !== 'boolean' ||
32
+ typeof data.shopifyStorefrontTokenSet !== 'boolean' ||
33
+ typeof data.shopifyApiSecretSet !== 'boolean' ||
34
+ typeof data.shopifyStoreDomainSet !== 'boolean' ||
35
+ typeof data.shopifyAdminSlugSet !== 'boolean' ||
36
+ typeof data.userSetupWebhooks !== 'boolean' ||
37
+ typeof data.resendApiKeySet !== 'boolean'
32
38
  ) {
33
39
  throw new Error('Invalid response format from server');
34
40
  }
@@ -19,6 +19,14 @@ export function convertToLocalState(
19
19
  adminPassword: '',
20
20
  editorPassword: '',
21
21
  aaiApiKey: '',
22
+ shopifyStorefrontToken: '',
23
+ shopifyAdminApiKey: '',
24
+ shopifyApiSecret: '',
25
+ shopifyApiVersion: '',
26
+ shopifyStoreDomain: '',
27
+ resendApiKey: '',
28
+ shopifyAdminSlug: '',
29
+ userSetupWebhooks: false,
22
30
  };
23
31
  }
24
32
 
@@ -31,6 +39,14 @@ export function convertToLocalState(
31
39
  adminPassword: '',
32
40
  editorPassword: '',
33
41
  aaiApiKey: '',
42
+ shopifyStorefrontToken: '',
43
+ shopifyAdminApiKey: '',
44
+ shopifyApiSecret: '',
45
+ shopifyApiVersion: status.shopifyApiVersion || '',
46
+ shopifyStoreDomain: '',
47
+ resendApiKey: '',
48
+ shopifyAdminSlug: '',
49
+ userSetupWebhooks: status.userSetupWebhooks,
34
50
  };
35
51
  }
36
52
 
@@ -64,6 +80,34 @@ export function convertToBackendFormat(
64
80
  request.AAI_API_KEY = state.aaiApiKey.trim();
65
81
  }
66
82
 
83
+ if (state.shopifyStorefrontToken?.trim()) {
84
+ request.SHOPIFY_STOREFRONT_TOKEN = state.shopifyStorefrontToken.trim();
85
+ }
86
+
87
+ if (state.shopifyApiSecret?.trim()) {
88
+ request.SHOPIFY_API_SECRET = state.shopifyApiSecret.trim();
89
+ }
90
+
91
+ if (state.shopifyApiVersion?.trim()) {
92
+ request.SHOPIFY_API_VERSION = state.shopifyApiVersion.trim();
93
+ }
94
+
95
+ if (state.shopifyStoreDomain?.trim()) {
96
+ request.SHOPIFY_STORE_DOMAIN = state.shopifyStoreDomain.trim();
97
+ }
98
+
99
+ if (state.shopifyAdminSlug?.trim()) {
100
+ request.SHOPIFY_ADMIN_SLUG = state.shopifyAdminSlug.trim();
101
+ }
102
+
103
+ if (state.userSetupWebhooks !== undefined) {
104
+ request.USER_SETUP_WEBHOOKS = state.userSetupWebhooks;
105
+ }
106
+
107
+ if (state.resendApiKey?.trim()) {
108
+ request.RESEND_API_KEY = state.resendApiKey.trim();
109
+ }
110
+
67
111
  return request;
68
112
  }
69
113
 
@@ -75,21 +119,16 @@ export function validateAdvancedConfig(
75
119
  ): FieldErrors {
76
120
  const errors: FieldErrors = {};
77
121
 
78
- // Turso validation: both URL and token must be provided together
79
122
  const hasTursoUrl = Boolean(state.tursoUrl?.trim());
80
123
  const hasTursoToken = Boolean(state.tursoToken?.trim());
81
-
82
124
  if (hasTursoUrl && !hasTursoToken) {
83
125
  errors.tursoToken =
84
126
  'Turso Auth Token is required when Turso URL is provided';
85
127
  }
86
-
87
128
  if (hasTursoToken && !hasTursoUrl) {
88
129
  errors.tursoUrl =
89
130
  'Turso Database URL is required when Turso Token is provided';
90
131
  }
91
-
92
- // Basic URL validation for Turso
93
132
  if (hasTursoUrl && state.tursoUrl) {
94
133
  const urlPattern = /^libsql:\/\/.+/;
95
134
  if (!urlPattern.test(state.tursoUrl.trim())) {
@@ -97,12 +136,53 @@ export function validateAdvancedConfig(
97
136
  }
98
137
  }
99
138
 
100
- // Password strength validation (optional but recommended)
139
+ if (state.shopifyStoreDomain?.trim()) {
140
+ const domainPattern = /^[a-zA-Z0-9-]+\.myshopify\.com$/;
141
+ if (!domainPattern.test(state.shopifyStoreDomain.trim())) {
142
+ errors.shopifyStoreDomain =
143
+ 'Store domain must be in the format "your-shop.myshopify.com"';
144
+ }
145
+ }
146
+
147
+ if (state.shopifyApiVersion?.trim()) {
148
+ if (!/^\d{4}-\d{2}$/.test(state.shopifyApiVersion.trim())) {
149
+ errors.shopifyApiVersion =
150
+ 'Version must match YYYY-MM format (e.g. 2026-01)';
151
+ }
152
+ }
153
+
154
+ if (state.shopifyAdminSlug?.trim()) {
155
+ if (
156
+ state.shopifyAdminSlug.includes('/') ||
157
+ state.shopifyAdminSlug.includes('.')
158
+ ) {
159
+ errors.shopifyAdminSlug =
160
+ 'Please enter only the store slug, not the full URL';
161
+ }
162
+ }
163
+
164
+ const isConfiguringShopify = Boolean(
165
+ state.shopifyStoreDomain?.trim() ||
166
+ state.shopifyStorefrontToken?.trim() ||
167
+ state.shopifyApiSecret?.trim() ||
168
+ state.shopifyAdminSlug?.trim()
169
+ );
170
+
171
+ if (isConfiguringShopify) {
172
+ if (!state.shopifyAdminSlug?.trim()) {
173
+ errors.shopifyAdminSlug =
174
+ 'Admin slug is required to generate webhook links.';
175
+ }
176
+ if (!state.userSetupWebhooks) {
177
+ errors.userSetupWebhooks =
178
+ 'You must confirm webhooks are configured to ensure synchronization.';
179
+ }
180
+ }
181
+
101
182
  if (state.adminPassword && state.adminPassword.length < 8) {
102
183
  errors.adminPassword =
103
184
  'Admin password should be at least 8 characters long';
104
185
  }
105
-
106
186
  if (state.editorPassword && state.editorPassword.length < 8) {
107
187
  errors.editorPassword =
108
188
  'Editor password should be at least 8 characters long';
@@ -0,0 +1,125 @@
1
+ import { customerDetails } from '@/stores/shopify';
2
+
3
+ export const bookingHelpers = {
4
+ /**
5
+ * Checks if a user profile already exists for a given email via the BFF.
6
+ */
7
+ lookupLead: async (email: string) => {
8
+ const response = await fetch('/api/auth/lookup-lead', {
9
+ method: 'POST',
10
+ headers: {
11
+ 'Content-Type': 'application/json',
12
+ },
13
+ body: JSON.stringify({ email }),
14
+ });
15
+ return await response.json();
16
+ },
17
+
18
+ /**
19
+ * Asks backend for availability slots
20
+ */
21
+ getAvailability: async (start: string, end: string) => {
22
+ const query = new URLSearchParams({
23
+ start,
24
+ end,
25
+ });
26
+ const response = await fetch(
27
+ `/api/booking/availability?${query.toString()}`
28
+ );
29
+ return await response.json();
30
+ },
31
+
32
+ /**
33
+ * Places a temporary hold on a specific slot for the manifest of services and pickup products.
34
+ */
35
+ holdSlot: async (
36
+ traceId: string,
37
+ startTime: string,
38
+ endTime: string,
39
+ resourceIds: string[]
40
+ ) => {
41
+ const details = customerDetails.get();
42
+
43
+ const response = await fetch('/api/booking/hold', {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ },
48
+ body: JSON.stringify({
49
+ traceId,
50
+ leadId: details.leadId,
51
+ resourceIds,
52
+ startTime,
53
+ endTime,
54
+ }),
55
+ });
56
+ return await response.json();
57
+ },
58
+
59
+ /**
60
+ * Releases a temporary hold, typically used when a user abandons checkout.
61
+ */
62
+ releaseHold: async (traceId: string) => {
63
+ const response = await fetch('/api/booking/release', {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ },
68
+ body: JSON.stringify({ traceId }),
69
+ });
70
+ return await response.json();
71
+ },
72
+
73
+ /**
74
+ * Confirms a pending hold, finalizing the booking for free services.
75
+ */
76
+ confirmBooking: async (traceId: string) => {
77
+ const response = await fetch('/api/booking/confirm', {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ },
82
+ body: JSON.stringify({ traceId }),
83
+ });
84
+ return await response.json();
85
+ },
86
+
87
+ /**
88
+ * Retrieves a paginated list of all bookings for the administrative dashboard.
89
+ */
90
+ listBookings: async (
91
+ limit: number = 50,
92
+ offset: number = 0,
93
+ status: string = 'ALL'
94
+ ) => {
95
+ const query = new URLSearchParams({
96
+ limit: limit.toString(),
97
+ offset: offset.toString(),
98
+ status,
99
+ });
100
+ const response = await fetch(`/api/booking/list?${query.toString()}`);
101
+ return await response.json();
102
+ },
103
+
104
+ /**
105
+ * Retrieves aggregated booking volume and conversion statistics.
106
+ */
107
+ getMetrics: async () => {
108
+ const response = await fetch('/api/booking/metrics');
109
+ return await response.json();
110
+ },
111
+
112
+ /**
113
+ * Manually cancels an existing booking from the administrative dashboard.
114
+ */
115
+ cancelBooking: async (traceId: string) => {
116
+ const response = await fetch('/api/booking/cancel', {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ },
121
+ body: JSON.stringify({ traceId }),
122
+ });
123
+ return await response.json();
124
+ },
125
+ };
@@ -40,7 +40,17 @@ export function convertToLocalState(
40
40
  knownResources: brandConfig.KNOWN_RESOURCES ?? {},
41
41
  designLibrary: brandConfig.DESIGN_LIBRARY ?? undefined,
42
42
  hasAAI: brandConfig.HAS_AAI ?? false,
43
+ hasShopify: brandConfig.HAS_SHOPIFY ?? false,
44
+ showShopifyHelper: brandConfig.SHOW_SHOPIFY_HELPER ?? false,
45
+ hasResend: brandConfig.HAS_RESEND ?? false,
43
46
  hasHydrationToken: brandConfig.HAS_HYDRATION_TOKEN ?? false,
47
+ scheduling: brandConfig.SCHEDULING ?? {
48
+ timezone: 'UTC',
49
+ bufferGapsMinutes: 15,
50
+ maxLengthMinutes: 0,
51
+ businessHours: {},
52
+ unavailableHours: [],
53
+ },
44
54
  };
45
55
  }
46
56
 
@@ -72,6 +82,10 @@ export function convertToBackendFormat(
72
82
  KNOWN_RESOURCES: localState.knownResources,
73
83
  DESIGN_LIBRARY: localState.designLibrary,
74
84
  HAS_AAI: localState.hasAAI,
85
+ HAS_SHOPIFY: localState.hasShopify,
86
+ SHOW_SHOPIFY_HELPER: localState.showShopifyHelper,
87
+ HAS_RESEND: localState.hasResend,
88
+ SCHEDULING: localState.scheduling,
75
89
 
76
90
  // ALWAYS send asset paths (current state)
77
91
  LOGO: localState.logo,