@doswiftly/cli 0.1.18 → 0.1.19
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/README.md +23 -323
- package/dist/commands/check.js +1 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +39 -20
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +4 -4
- package/dist/commands/sdk.js +5 -5
- package/dist/commands/sdk.js.map +1 -1
- package/dist/commands/template.js +4 -4
- package/dist/commands/template.js.map +1 -1
- package/dist/commands/types.js +5 -5
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/verify.js +2 -2
- package/dist/commands/verify.js.map +1 -1
- package/dist/lib/package-manager.d.ts +1 -1
- package/dist/lib/package-manager.js +1 -1
- package/package.json +1 -1
- package/templates/storefront-nextjs/README.md +16 -12
- package/templates/storefront-nextjs/app/account/orders/page.tsx +2 -2
- package/templates/storefront-nextjs/app/account/page.tsx +2 -2
- package/templates/storefront-nextjs/app/auth/login/page.tsx +1 -1
- package/templates/storefront-nextjs/app/auth/register/page.tsx +1 -1
- package/templates/storefront-nextjs/app/cart/page.tsx +1 -1
- package/templates/storefront-nextjs/app/categories/[slug]/page.tsx +2 -2
- package/templates/storefront-nextjs/app/categories/page.tsx +1 -1
- package/templates/storefront-nextjs/app/collections/[slug]/page.tsx +1 -1
- package/templates/storefront-nextjs/app/collections/page.tsx +1 -1
- package/templates/storefront-nextjs/app/page.tsx +1 -1
- package/templates/storefront-nextjs/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-nextjs/app/products/page.tsx +2 -2
- package/templates/storefront-nextjs/app/search/page.tsx +1 -1
- package/templates/storefront-nextjs/components/auth/auth-guard.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/add-to-cart-button.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/cart-icon.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/currency-selector.tsx +2 -2
- package/templates/storefront-nextjs/components/commerce/product-filters.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/product-price.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/search-input.tsx +1 -1
- package/templates/storefront-nextjs/components/commerce/sort-select.tsx +1 -1
- package/templates/storefront-nextjs/components/providers.tsx +1 -1
- package/templates/storefront-nextjs/lib/currency.tsx +3 -3
- package/templates/storefront-nextjs/lib/format.ts +1 -1
- package/templates/storefront-nextjs/lib/graphql-queries.ts +3 -3
- package/templates/storefront-nextjs/package.dev.json +1 -1
- package/templates/storefront-nextjs/package.json +1 -1
- package/templates/storefront-nextjs/package.json.template +1 -1
- package/templates/storefront-nextjs-shadcn/.github/workflows/deploy.yml +47 -0
- package/templates/storefront-nextjs-shadcn/.github/workflows/preview.yml +47 -0
- package/templates/storefront-nextjs-shadcn/CLAUDE.md +148 -35
- package/templates/storefront-nextjs-shadcn/README.md +29 -162
- package/templates/storefront-nextjs-shadcn/app/account/addresses/page.tsx +98 -91
- package/templates/storefront-nextjs-shadcn/app/account/error.tsx +43 -0
- package/templates/storefront-nextjs-shadcn/app/account/loading.tsx +19 -0
- package/templates/storefront-nextjs-shadcn/app/account/loyalty/page.tsx +53 -162
- package/templates/storefront-nextjs-shadcn/app/account/orders/[id]/loading.tsx +60 -0
- package/templates/storefront-nextjs-shadcn/app/account/orders/[id]/page.tsx +36 -47
- package/templates/storefront-nextjs-shadcn/app/account/orders/page.tsx +46 -29
- package/templates/storefront-nextjs-shadcn/app/account/page.tsx +8 -5
- package/templates/storefront-nextjs-shadcn/app/account/settings/page.tsx +108 -71
- package/templates/storefront-nextjs-shadcn/app/api/auth/clear-token/route.ts +2 -86
- package/templates/storefront-nextjs-shadcn/app/api/auth/set-token/route.ts +2 -124
- package/templates/storefront-nextjs-shadcn/app/auth/forgot-password/page.tsx +10 -5
- package/templates/storefront-nextjs-shadcn/app/blog/[slug]/loading.tsx +17 -0
- package/templates/storefront-nextjs-shadcn/app/blog/[slug]/page.tsx +43 -2
- package/templates/storefront-nextjs-shadcn/app/blog/loading.tsx +19 -0
- package/templates/storefront-nextjs-shadcn/app/brands/page.tsx +2 -1
- package/templates/storefront-nextjs-shadcn/app/cart/loading.tsx +26 -0
- package/templates/storefront-nextjs-shadcn/app/cart/page.tsx +6 -3
- package/templates/storefront-nextjs-shadcn/app/categories/[slug]/category-products-client.tsx +56 -0
- package/templates/storefront-nextjs-shadcn/app/categories/[slug]/loading.tsx +32 -0
- package/templates/storefront-nextjs-shadcn/app/categories/[slug]/page.tsx +76 -59
- package/templates/storefront-nextjs-shadcn/app/categories/page.tsx +8 -4
- package/templates/storefront-nextjs-shadcn/app/checkout/error.tsx +43 -0
- package/templates/storefront-nextjs-shadcn/app/checkout/loading.tsx +31 -0
- package/templates/storefront-nextjs-shadcn/app/checkout/page.tsx +116 -79
- package/templates/storefront-nextjs-shadcn/app/collections/[handle]/loading.tsx +19 -0
- package/templates/storefront-nextjs-shadcn/app/collections/[handle]/page.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/app/collections/loading.tsx +18 -0
- package/templates/storefront-nextjs-shadcn/app/collections/page.tsx +7 -4
- package/templates/storefront-nextjs-shadcn/app/global-error.tsx +117 -0
- package/templates/storefront-nextjs-shadcn/app/globals.css +8 -0
- package/templates/storefront-nextjs-shadcn/app/layout.tsx +46 -11
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/error.tsx +43 -0
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/loading.tsx +29 -0
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/page.tsx +6 -6
- package/templates/storefront-nextjs-shadcn/app/products/[slug]/product-client.tsx +15 -61
- package/templates/storefront-nextjs-shadcn/app/products/loading.tsx +32 -0
- package/templates/storefront-nextjs-shadcn/app/products/products-client.tsx +405 -151
- package/templates/storefront-nextjs-shadcn/app/search/loading.tsx +18 -0
- package/templates/storefront-nextjs-shadcn/app/wishlist/page.tsx +8 -5
- package/templates/storefront-nextjs-shadcn/codegen.ts +48 -31
- package/templates/storefront-nextjs-shadcn/components/account/customer-info.fragment.graphql +36 -0
- package/templates/storefront-nextjs-shadcn/components/account/order-details.tsx +3 -1
- package/templates/storefront-nextjs-shadcn/components/account/order-history.tsx +26 -24
- package/templates/storefront-nextjs-shadcn/components/account/order-summary.fragment.graphql +36 -0
- package/templates/storefront-nextjs-shadcn/components/auth/account-menu.tsx +9 -9
- package/templates/storefront-nextjs-shadcn/components/auth/login-form.tsx +11 -37
- package/templates/storefront-nextjs-shadcn/components/auth/register-form.tsx +37 -23
- package/templates/storefront-nextjs-shadcn/components/cart/cart-drawer.tsx +4 -3
- package/templates/storefront-nextjs-shadcn/components/cart/cart-icon.tsx +8 -5
- package/templates/storefront-nextjs-shadcn/components/cart/cart-item.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/cart/cart-line.fragment.graphql +53 -0
- package/templates/storefront-nextjs-shadcn/components/cart/cart-summary.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/cart/shipping-estimator.tsx +22 -7
- package/templates/storefront-nextjs-shadcn/components/commerce/currency-selector.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/commerce/product-actions.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/commerce/search-input.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/common/price-display.tsx +35 -11
- package/templates/storefront-nextjs-shadcn/components/discount/discount-breakdown.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/discount/discount-code-input.tsx +3 -3
- package/templates/storefront-nextjs-shadcn/components/filters/range-slider-filter.tsx +5 -5
- package/templates/storefront-nextjs-shadcn/components/gift-card/gift-card-input.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/home/category-grid.tsx +2 -1
- package/templates/storefront-nextjs-shadcn/components/home/collection-card.fragment.graphql +21 -0
- package/templates/storefront-nextjs-shadcn/components/home/featured-collections.tsx +2 -12
- package/templates/storefront-nextjs-shadcn/components/home/index.ts +0 -1
- package/templates/storefront-nextjs-shadcn/components/hydrated.tsx +24 -0
- package/templates/storefront-nextjs-shadcn/components/layout/breadcrumbs.tsx +4 -4
- package/templates/storefront-nextjs-shadcn/components/layout/category-node.fragment.graphql +22 -0
- package/templates/storefront-nextjs-shadcn/components/layout/currency-selector.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/layout/header.tsx +33 -23
- package/templates/storefront-nextjs-shadcn/components/loyalty/points-balance.tsx +2 -11
- package/templates/storefront-nextjs-shadcn/components/loyalty/points-history.tsx +8 -25
- package/templates/storefront-nextjs-shadcn/components/loyalty/referral-section.tsx +10 -19
- package/templates/storefront-nextjs-shadcn/components/loyalty/rewards-catalog.tsx +17 -41
- package/templates/storefront-nextjs-shadcn/components/loyalty/tier-progress.tsx +2 -29
- package/templates/storefront-nextjs-shadcn/components/order/index.ts +6 -1
- package/templates/storefront-nextjs-shadcn/components/product/b2b-price-display.tsx +3 -1
- package/templates/storefront-nextjs-shadcn/components/product/filter-active-pills.tsx +69 -0
- package/templates/storefront-nextjs-shadcn/components/product/filter-mobile-sheet.tsx +84 -0
- package/templates/storefront-nextjs-shadcn/components/product/filter-price-range.tsx +138 -0
- package/templates/storefront-nextjs-shadcn/components/product/index.ts +9 -2
- package/templates/storefront-nextjs-shadcn/components/product/product-card.fragment.graphql +49 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-card.tsx +3 -31
- package/templates/storefront-nextjs-shadcn/components/product/product-detail.fragment.graphql +52 -0
- package/templates/storefront-nextjs-shadcn/components/product/product-filters.tsx +176 -123
- package/templates/storefront-nextjs-shadcn/components/product/product-grid.tsx +3 -5
- package/templates/storefront-nextjs-shadcn/components/product/product-image.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/product/product-price.tsx +2 -2
- package/templates/storefront-nextjs-shadcn/components/product/product-reviews.tsx +5 -4
- package/templates/storefront-nextjs-shadcn/components/product/product-sort.tsx +19 -7
- package/templates/storefront-nextjs-shadcn/components/product/product-variant-selector.tsx +8 -23
- package/templates/storefront-nextjs-shadcn/components/product/product-variant.fragment.graphql +51 -0
- package/templates/storefront-nextjs-shadcn/components/product/review-card.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/product/review-form.tsx +1 -7
- package/templates/storefront-nextjs-shadcn/components/product/savings-display.tsx +17 -2
- package/templates/storefront-nextjs-shadcn/components/product/similar-products.tsx +3 -2
- package/templates/storefront-nextjs-shadcn/components/providers/index.ts +1 -1
- package/templates/storefront-nextjs-shadcn/components/providers/stores-provider.tsx +30 -0
- package/templates/storefront-nextjs-shadcn/components/providers/theme-provider.tsx +1 -1
- package/templates/storefront-nextjs-shadcn/components/returns/index.ts +2 -2
- package/templates/storefront-nextjs-shadcn/components/returns/return-request-form.tsx +3 -2
- package/templates/storefront-nextjs-shadcn/components/search/search-results.tsx +3 -2
- package/templates/storefront-nextjs-shadcn/components/ui/form.tsx +174 -0
- package/templates/storefront-nextjs-shadcn/components/ui/index.ts +30 -2
- package/templates/storefront-nextjs-shadcn/components/ui/progress.tsx +40 -0
- package/templates/storefront-nextjs-shadcn/components/ui/sheet.tsx +107 -0
- package/templates/storefront-nextjs-shadcn/components/ui/slider.tsx +33 -0
- package/templates/storefront-nextjs-shadcn/components/ui/textarea.tsx +24 -0
- package/templates/storefront-nextjs-shadcn/components/wishlist/wishlist-icon.tsx +3 -1
- package/templates/storefront-nextjs-shadcn/generated/graphql.ts +12779 -0
- package/templates/storefront-nextjs-shadcn/graphql/custom.example.graphql +159 -0
- package/templates/storefront-nextjs-shadcn/hooks/index.ts +2 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-auth-sync.ts +42 -0
- package/templates/storefront-nextjs-shadcn/hooks/use-auth.ts +17 -295
- package/templates/storefront-nextjs-shadcn/hooks/use-cart-actions.ts +51 -19
- package/templates/storefront-nextjs-shadcn/hooks/use-cart-sync.ts +13 -9
- package/templates/storefront-nextjs-shadcn/lib/auth/routes.ts +4 -17
- package/templates/storefront-nextjs-shadcn/lib/graphql/client.ts +22 -99
- package/templates/storefront-nextjs-shadcn/lib/graphql/config.ts +32 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/fragments.ts +34 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/hooks.ts +687 -632
- package/templates/storefront-nextjs-shadcn/lib/graphql/query-keys.ts +86 -0
- package/templates/storefront-nextjs-shadcn/lib/graphql/server.ts +131 -182
- package/templates/storefront-nextjs-shadcn/lib/graphql/types.ts +62 -0
- package/templates/storefront-nextjs-shadcn/lib/theme/theme-config.ts +0 -17
- package/templates/storefront-nextjs-shadcn/next-env.d.ts +6 -0
- package/templates/storefront-nextjs-shadcn/package.dev.json +1 -3
- package/templates/storefront-nextjs-shadcn/package.json +12 -13
- package/templates/storefront-nextjs-shadcn/package.json.template +6 -7
- package/templates/storefront-nextjs-shadcn/proxy.ts +3 -4
- package/templates/storefront-nextjs-shadcn/stores/cart-store.ts +41 -39
- package/templates/storefront-nextjs-shadcn/stores/checkout-store.ts +64 -75
- package/templates/storefront-nextjs-shadcn/stores/wishlist-store.ts +178 -177
- package/templates/storefront-nextjs-shadcn/tsconfig.json +23 -5
- package/templates/storefront-nextjs-shadcn/CART_INTEGRATION.md +0 -282
- package/templates/storefront-nextjs-shadcn/GRAPHQL_DOCUMENT_NAMES.md +0 -190
- package/templates/storefront-nextjs-shadcn/GRAPHQL_ERROR_HANDLING.md +0 -263
- package/templates/storefront-nextjs-shadcn/GRAPHQL_FIXES_SUMMARY.md +0 -135
- package/templates/storefront-nextjs-shadcn/GRAPHQL_INTEGRATION_COMPLETE.md +0 -142
- package/templates/storefront-nextjs-shadcn/INTEGRATION_CHECKLIST.md +0 -448
- package/templates/storefront-nextjs-shadcn/PRODUCT_DETAIL_PAGE_IMPLEMENTATION.md +0 -307
- package/templates/storefront-nextjs-shadcn/THEME_CUSTOMIZATION.md +0 -245
- package/templates/storefront-nextjs-shadcn/components/providers/currency-provider.tsx +0 -103
- package/templates/storefront-nextjs-shadcn/graphql/collections.example.ts +0 -168
- package/templates/storefront-nextjs-shadcn/graphql/products.example.ts +0 -160
- package/templates/storefront-nextjs-shadcn/lib/auth/cookies.ts +0 -220
- package/templates/storefront-nextjs-shadcn/lib/config.ts +0 -46
- package/templates/storefront-nextjs-shadcn/lib/currency/IMPLEMENTATION_SUMMARY.md +0 -254
- package/templates/storefront-nextjs-shadcn/lib/currency/README.md +0 -464
- package/templates/storefront-nextjs-shadcn/lib/currency/cookie-manager.test.ts +0 -328
- package/templates/storefront-nextjs-shadcn/lib/currency/cookie-manager.ts +0 -295
- package/templates/storefront-nextjs-shadcn/lib/currency/index.ts +0 -27
- package/templates/storefront-nextjs-shadcn/lib/format.ts +0 -226
- package/templates/storefront-nextjs-shadcn/lib/hooks.ts +0 -30
- package/templates/storefront-nextjs-shadcn/stores/auth-store.ts +0 -66
- package/templates/storefront-nextjs-shadcn/stores/currency-store.ts +0 -103
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback, useRef } from 'react';
|
|
4
|
-
import {
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
5
|
+
import { useCartStore, useCartStoreApi } from '@/stores/cart-store';
|
|
5
6
|
import { useCartCreate, useCartLinesAdd, useCartLinesUpdate, useCartLinesRemove } from '@/lib/graphql/hooks';
|
|
7
|
+
import { queryKeys } from '@/lib/graphql/query-keys';
|
|
6
8
|
import { toast } from 'sonner';
|
|
9
|
+
import { StorefrontError } from '@doswiftly/storefront-sdk';
|
|
7
10
|
|
|
8
11
|
// Debounce delay for quantity updates (prevents rate limiting)
|
|
9
12
|
const QUANTITY_UPDATE_DEBOUNCE_MS = 500;
|
|
@@ -33,6 +36,8 @@ const QUANTITY_UPDATE_DEBOUNCE_MS = 500;
|
|
|
33
36
|
* ```
|
|
34
37
|
*/
|
|
35
38
|
export function useCartActions() {
|
|
39
|
+
const queryClient = useQueryClient();
|
|
40
|
+
const cartStoreApi = useCartStoreApi();
|
|
36
41
|
const {
|
|
37
42
|
setCartId,
|
|
38
43
|
clearCart,
|
|
@@ -54,7 +59,7 @@ export function useCartActions() {
|
|
|
54
59
|
* Reads cartId from store (fresh, not stale closure) or creates a new cart.
|
|
55
60
|
*/
|
|
56
61
|
const getOrCreateCartId = useCallback(async (forceNew: boolean = false): Promise<string> => {
|
|
57
|
-
const currentCartId =
|
|
62
|
+
const currentCartId = cartStoreApi.getState().cartId;
|
|
58
63
|
if (currentCartId && !forceNew) {
|
|
59
64
|
return currentCartId;
|
|
60
65
|
}
|
|
@@ -73,19 +78,25 @@ export function useCartActions() {
|
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
throw new Error('Failed to create cart');
|
|
76
|
-
} catch (error:
|
|
81
|
+
} catch (error: unknown) {
|
|
77
82
|
console.error('Cart creation failed:', error);
|
|
78
83
|
throw error;
|
|
79
84
|
}
|
|
80
|
-
}, [setCartId, createCartMutation]);
|
|
85
|
+
}, [cartStoreApi, setCartId, createCartMutation]);
|
|
81
86
|
|
|
82
87
|
/**
|
|
83
88
|
* Check if error is a "Cart not found" error (stale/expired cart)
|
|
84
89
|
*/
|
|
85
|
-
const isCartNotFoundError = (error:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
const isCartNotFoundError = (error: unknown): boolean => {
|
|
91
|
+
if (error instanceof StorefrontError) {
|
|
92
|
+
const msg = error.message.toLowerCase();
|
|
93
|
+
return msg.includes('cart not found') || msg.includes('cart does not exist');
|
|
94
|
+
}
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
const msg = error.message.toLowerCase();
|
|
97
|
+
return msg.includes('cart not found') || msg.includes('cart does not exist');
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
89
100
|
};
|
|
90
101
|
|
|
91
102
|
/**
|
|
@@ -108,6 +119,9 @@ export function useCartActions() {
|
|
|
108
119
|
const forceNewCart = _options?._forceNewCart ?? false;
|
|
109
120
|
|
|
110
121
|
try {
|
|
122
|
+
// Cancel in-flight cart queries to prevent stale data overwriting
|
|
123
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.cart.all() });
|
|
124
|
+
|
|
111
125
|
const cartId = await getOrCreateCartId(forceNewCart);
|
|
112
126
|
|
|
113
127
|
const result = await addLinesMutation.mutateAsync({
|
|
@@ -133,7 +147,7 @@ export function useCartActions() {
|
|
|
133
147
|
// Open cart drawer to show the added item
|
|
134
148
|
openCart();
|
|
135
149
|
toast.success('Added to cart');
|
|
136
|
-
} catch (error:
|
|
150
|
+
} catch (error: unknown) {
|
|
137
151
|
if (isCartNotFoundError(error) && !forceNewCart) {
|
|
138
152
|
console.warn('Cart expired (caught), creating new cart and retrying...');
|
|
139
153
|
setCartId(null);
|
|
@@ -141,7 +155,8 @@ export function useCartActions() {
|
|
|
141
155
|
}
|
|
142
156
|
|
|
143
157
|
console.error('Add to cart failed:', error);
|
|
144
|
-
|
|
158
|
+
const message = error instanceof Error ? error.message : 'Failed to add to cart';
|
|
159
|
+
toast.error(message);
|
|
145
160
|
throw error;
|
|
146
161
|
}
|
|
147
162
|
}, [setCartId, openCart, getOrCreateCartId, addLinesMutation]);
|
|
@@ -151,7 +166,10 @@ export function useCartActions() {
|
|
|
151
166
|
*/
|
|
152
167
|
const executeQuantityUpdate = useCallback(async (lineId: string, quantity: number) => {
|
|
153
168
|
try {
|
|
154
|
-
|
|
169
|
+
// Cancel in-flight cart queries to prevent race conditions
|
|
170
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.cart.all() });
|
|
171
|
+
|
|
172
|
+
const currentCartId = cartStoreApi.getState().cartId;
|
|
155
173
|
if (!currentCartId) {
|
|
156
174
|
throw new Error('No cart found');
|
|
157
175
|
}
|
|
@@ -176,7 +194,7 @@ export function useCartActions() {
|
|
|
176
194
|
|
|
177
195
|
throw new Error(errorMessage);
|
|
178
196
|
}
|
|
179
|
-
} catch (error:
|
|
197
|
+
} catch (error: unknown) {
|
|
180
198
|
if (isCartNotFoundError(error)) {
|
|
181
199
|
console.warn('Cart expired during update (caught), clearing cart');
|
|
182
200
|
clearCart();
|
|
@@ -185,9 +203,10 @@ export function useCartActions() {
|
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
console.error('Update quantity failed:', error);
|
|
188
|
-
|
|
206
|
+
const message = error instanceof Error ? error.message : 'Failed to update quantity';
|
|
207
|
+
toast.error(message);
|
|
189
208
|
}
|
|
190
|
-
}, [updateLinesMutation, clearCart]);
|
|
209
|
+
}, [cartStoreApi, queryClient, updateLinesMutation, clearCart]);
|
|
191
210
|
|
|
192
211
|
/**
|
|
193
212
|
* Remove item from cart by line ID.
|
|
@@ -196,7 +215,7 @@ export function useCartActions() {
|
|
|
196
215
|
*/
|
|
197
216
|
const removeFromCart = useCallback(async (lineId: string) => {
|
|
198
217
|
try {
|
|
199
|
-
const currentCartId =
|
|
218
|
+
const currentCartId = cartStoreApi.getState().cartId;
|
|
200
219
|
if (!currentCartId) {
|
|
201
220
|
return;
|
|
202
221
|
}
|
|
@@ -219,7 +238,7 @@ export function useCartActions() {
|
|
|
219
238
|
}
|
|
220
239
|
|
|
221
240
|
toast.success('Removed from cart');
|
|
222
|
-
} catch (error:
|
|
241
|
+
} catch (error: unknown) {
|
|
223
242
|
if (isCartNotFoundError(error)) {
|
|
224
243
|
console.warn('Cart expired during remove (caught), clearing stale cartId');
|
|
225
244
|
setCartId(null);
|
|
@@ -227,10 +246,11 @@ export function useCartActions() {
|
|
|
227
246
|
}
|
|
228
247
|
|
|
229
248
|
console.error('Remove from cart failed:', error);
|
|
230
|
-
|
|
249
|
+
const message = error instanceof Error ? error.message : 'Failed to remove from cart';
|
|
250
|
+
toast.error(message);
|
|
231
251
|
throw error;
|
|
232
252
|
}
|
|
233
|
-
}, [setCartId, removeLinesMutation]);
|
|
253
|
+
}, [cartStoreApi, setCartId, removeLinesMutation]);
|
|
234
254
|
|
|
235
255
|
/**
|
|
236
256
|
* Update item quantity (debounced, takes lineId).
|
|
@@ -266,6 +286,18 @@ export function useCartActions() {
|
|
|
266
286
|
updateTimeoutRef.current.set(lineId, timeout);
|
|
267
287
|
}, [removeFromCart, executeQuantityUpdate]);
|
|
268
288
|
|
|
289
|
+
// Cleanup pending debounce timeouts on unmount
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
const timeouts = updateTimeoutRef.current;
|
|
292
|
+
return () => {
|
|
293
|
+
for (const timeout of timeouts.values()) {
|
|
294
|
+
clearTimeout(timeout);
|
|
295
|
+
}
|
|
296
|
+
timeouts.clear();
|
|
297
|
+
pendingUpdatesRef.current.clear();
|
|
298
|
+
};
|
|
299
|
+
}, []);
|
|
300
|
+
|
|
269
301
|
/**
|
|
270
302
|
* Clear entire cart.
|
|
271
303
|
* Clears cartId in zustand persist → useCartSync returns empty.
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { useEffect } from "react";
|
|
4
4
|
import { useCart } from "@/lib/graphql/hooks";
|
|
5
5
|
import { useCartStore } from "@/stores/cart-store";
|
|
6
|
+
import { useHydrated } from "@doswiftly/storefront-sdk/react";
|
|
7
|
+
import type { CartLineFields } from "@/lib/graphql/fragments";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Mapped cart item for display components.
|
|
@@ -31,18 +33,20 @@ export interface CartItemData {
|
|
|
31
33
|
*/
|
|
32
34
|
export function useCartSync() {
|
|
33
35
|
const cartId = useCartStore((state) => state.cartId);
|
|
34
|
-
const isHydrated = useCartStore((state) => state.isHydrated);
|
|
35
36
|
const setCartId = useCartStore((state) => state.setCartId);
|
|
37
|
+
const isHydrated = useHydrated();
|
|
36
38
|
|
|
37
|
-
const { data, isLoading, error, refetch } = useCart(cartId, {
|
|
39
|
+
const { data, isLoading, isSuccess, error, refetch } = useCart(cartId, {
|
|
38
40
|
enabled: isHydrated && Boolean(cartId),
|
|
39
41
|
retry: false,
|
|
40
42
|
});
|
|
41
43
|
|
|
42
44
|
const cart = data?.cart;
|
|
43
45
|
|
|
44
|
-
// Detect stale cart: cartId exists but server
|
|
45
|
-
|
|
46
|
+
// Detect stale cart: cartId exists but server confirmed no cart (isSuccess + !cart).
|
|
47
|
+
// Using isSuccess instead of !isLoading prevents false positives when queries are
|
|
48
|
+
// cancelled by mutation optimistic updates (cancelled query → status='pending', not 'success').
|
|
49
|
+
const isStaleCart = isHydrated && Boolean(cartId) && isSuccess && !cart;
|
|
46
50
|
|
|
47
51
|
// Auto-clear stale cartId
|
|
48
52
|
useEffect(() => {
|
|
@@ -52,7 +56,7 @@ export function useCartSync() {
|
|
|
52
56
|
}, [isStaleCart, setCartId]);
|
|
53
57
|
|
|
54
58
|
// Map GraphQL lines to display-friendly items
|
|
55
|
-
const items: CartItemData[] = (cart?.lines ?? []).map((line:
|
|
59
|
+
const items: CartItemData[] = (cart?.lines ?? []).map((line: CartLineFields) => {
|
|
56
60
|
const merchandiseTitle = line.merchandise.title ?? "";
|
|
57
61
|
// Hide generic variant names like "Default" or "Default Title"
|
|
58
62
|
const isDefaultVariant = /^default(\s+title)?$/i.test(merchandiseTitle);
|
|
@@ -63,10 +67,10 @@ export function useCartSync() {
|
|
|
63
67
|
lineId: line.id,
|
|
64
68
|
variantId: line.merchandise.id,
|
|
65
69
|
productId: line.productId || line.merchandise.id,
|
|
66
|
-
productHandle: line.productHandle,
|
|
70
|
+
productHandle: line.productHandle ?? undefined,
|
|
67
71
|
productTitle,
|
|
68
72
|
variantTitle,
|
|
69
|
-
productType: line.productType,
|
|
73
|
+
productType: line.productType ?? undefined,
|
|
70
74
|
quantity: line.quantity,
|
|
71
75
|
price: {
|
|
72
76
|
amount: line.merchandise.price.amount,
|
|
@@ -88,8 +92,8 @@ export function useCartSync() {
|
|
|
88
92
|
|
|
89
93
|
// Discount data from server
|
|
90
94
|
const discountCodes: string[] = (cart?.discountCodes ?? [])
|
|
91
|
-
.filter((dc
|
|
92
|
-
.map((dc
|
|
95
|
+
.filter((dc) => dc.applicable)
|
|
96
|
+
.map((dc) => dc.code);
|
|
93
97
|
const totalDiscount = subtotal - total;
|
|
94
98
|
|
|
95
99
|
return {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Auth Routes Configuration (SSOT)
|
|
3
3
|
*
|
|
4
4
|
* This file is the single source of truth for authentication routes.
|
|
5
|
-
* Used by
|
|
5
|
+
* Used by proxy.ts and can be imported by components if needed.
|
|
6
6
|
*
|
|
7
7
|
* IMPORTANT: Checkout is NOT protected to allow guest checkout (e-commerce best practice).
|
|
8
8
|
* Users can optionally log in during checkout to use saved addresses.
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* export const guestOnlyRoutes = ['/auth/login', '/auth/register', '/auth/forgot-password'];
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
// Re-export platform constants and matching utility from SDK
|
|
20
|
+
export { AUTH_COOKIE_NAME, matchesRoute } from '@doswiftly/storefront-sdk';
|
|
21
|
+
|
|
19
22
|
/**
|
|
20
23
|
* Routes that require authentication.
|
|
21
24
|
* Unauthenticated users will be redirected to login.
|
|
@@ -30,12 +33,6 @@ export const protectedRoutes = ["/account"];
|
|
|
30
33
|
*/
|
|
31
34
|
export const guestOnlyRoutes = ["/auth/login", "/auth/register"];
|
|
32
35
|
|
|
33
|
-
/**
|
|
34
|
-
* Cookie name for customer access token.
|
|
35
|
-
* Must match the cookie name used by commerce-sdk.
|
|
36
|
-
*/
|
|
37
|
-
export const AUTH_COOKIE_NAME = "customerAccessToken";
|
|
38
|
-
|
|
39
36
|
/**
|
|
40
37
|
* Default redirect paths
|
|
41
38
|
*/
|
|
@@ -45,13 +42,3 @@ export const redirects = {
|
|
|
45
42
|
/** Where to redirect authenticated users trying to access guest-only routes */
|
|
46
43
|
authenticated: "/account",
|
|
47
44
|
} as const;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Check if a pathname matches any route in the list.
|
|
51
|
-
* Supports both exact matches and prefix matches (e.g., /account matches /account/orders).
|
|
52
|
-
*/
|
|
53
|
-
export function matchesRoute(pathname: string, routes: string[]): boolean {
|
|
54
|
-
return routes.some(
|
|
55
|
-
(route) => pathname === route || pathname.startsWith(`${route}/`)
|
|
56
|
-
);
|
|
57
|
-
}
|
|
@@ -1,109 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
// Import config - will be injected by CLI during template generation
|
|
7
|
-
let config: {
|
|
8
|
-
shop: { slug: string };
|
|
9
|
-
api: { url: string };
|
|
10
|
-
} | null = null;
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
// Dynamic import to handle cases where config doesn't exist yet
|
|
14
|
-
const configModule = require('@/doswiftly.config');
|
|
15
|
-
config = configModule.default || configModule;
|
|
16
|
-
} catch (error) {
|
|
17
|
-
// Fallback to environment variables if config file doesn't exist
|
|
18
|
-
config = {
|
|
19
|
-
shop: {
|
|
20
|
-
slug: process.env.NEXT_PUBLIC_SHOP_SLUG || 'demo-shop',
|
|
21
|
-
},
|
|
22
|
-
api: {
|
|
23
|
-
url: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Create a GraphQL client with dynamic currency header
|
|
30
|
-
*
|
|
31
|
-
* This factory creates a new client instance that reads the current
|
|
32
|
-
* preferred currency from cookies on each request, ensuring SSR compatibility.
|
|
33
|
-
*
|
|
34
|
-
* The client injects:
|
|
35
|
-
* - X-Shop-Slug: Shop identifier for multi-tenancy
|
|
36
|
-
* - X-Preferred-Currency: User's preferred currency (from cookie, SSR-safe)
|
|
37
|
-
*
|
|
38
|
-
* @returns GraphQL client configured for client-side usage with currency support
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```typescript
|
|
42
|
-
* const client = createGraphQLClient();
|
|
43
|
-
* const data = await client.request(ProductDocument, { handle: 'my-product' });
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
|
-
export function createGraphQLClient(): GraphQLClient {
|
|
47
|
-
return new GraphQLClient(`${config!.api.url}/storefront/graphql`, {
|
|
48
|
-
headers: () => {
|
|
49
|
-
// Read current currency from cookie (SSR-safe, always fresh)
|
|
50
|
-
const currency = getCurrencyFromCookie();
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
'X-Shop-Slug': config!.shop.slug,
|
|
54
|
-
// Dynamic currency header - reads from cookie on each request
|
|
55
|
-
...(currency && { 'X-Preferred-Currency': currency }),
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
}
|
|
3
|
+
import { useStorefrontClient } from '@doswiftly/storefront-sdk/react';
|
|
4
|
+
import { useCallback } from 'react';
|
|
60
5
|
|
|
61
6
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*/
|
|
69
|
-
let clientInstance: GraphQLClient | null = null;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get or create the singleton GraphQL client
|
|
73
|
-
*
|
|
74
|
-
* Returns a cached client instance that automatically includes
|
|
75
|
-
* the current preferred currency in request headers.
|
|
76
|
-
*
|
|
77
|
-
* The client is currency-aware and will include the user's
|
|
78
|
-
* preferred currency in the X-Preferred-Currency header on
|
|
79
|
-
* every request.
|
|
80
|
-
*
|
|
81
|
-
* @returns Singleton GraphQL client instance
|
|
82
|
-
*
|
|
7
|
+
* Execute GraphQL operations using SDK's StorefrontClient from Context.
|
|
8
|
+
* Includes auth + currency middleware automatically.
|
|
9
|
+
*
|
|
10
|
+
* Replaces the old module-level singleton client which was incompatible
|
|
11
|
+
* with the Context-based store pattern.
|
|
12
|
+
*
|
|
83
13
|
* @example
|
|
84
14
|
* ```typescript
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* const client = getGraphQLClient();
|
|
92
|
-
*
|
|
93
|
-
* const fetchProduct = async () => {
|
|
94
|
-
* return client.request(ProductDocument, { handle });
|
|
95
|
-
* };
|
|
96
|
-
*
|
|
97
|
-
* // Product will be fetched with current preferred currency
|
|
98
|
-
* const { data } = useQuery(['product', handle], fetchProduct);
|
|
99
|
-
*
|
|
100
|
-
* return <div>{data.product.priceRange.minVariantPrice.amount}</div>;
|
|
15
|
+
* function useMyQuery() {
|
|
16
|
+
* const execute = useExecute();
|
|
17
|
+
* return useQuery({
|
|
18
|
+
* queryKey: ['my-query'],
|
|
19
|
+
* queryFn: () => execute<MyQueryResult>(MyDocument.toString(), variables),
|
|
20
|
+
* });
|
|
101
21
|
* }
|
|
102
22
|
* ```
|
|
103
23
|
*/
|
|
104
|
-
export function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
24
|
+
export function useExecute() {
|
|
25
|
+
const client = useStorefrontClient();
|
|
26
|
+
|
|
27
|
+
return useCallback(
|
|
28
|
+
<TResult>(query: string, variables?: Record<string, unknown>): Promise<TResult> =>
|
|
29
|
+
client.query<TResult>(query, variables),
|
|
30
|
+
[client],
|
|
31
|
+
);
|
|
109
32
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized GraphQL configuration.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for API URL and shop slug.
|
|
5
|
+
* Used by both client.ts (browser) and server.ts (SSR).
|
|
6
|
+
*
|
|
7
|
+
* Resolution order:
|
|
8
|
+
* 1. doswiftly.config.ts (CLI-injected, highest priority)
|
|
9
|
+
* 2. Environment variables (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_SHOP_SLUG)
|
|
10
|
+
* 3. Defaults (localhost:8000, demo-shop)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
|
14
|
+
const shopSlug = process.env.NEXT_PUBLIC_SHOP_SLUG || 'demo-shop';
|
|
15
|
+
|
|
16
|
+
let configApiUrl = apiUrl;
|
|
17
|
+
let configShopSlug = shopSlug;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
+
const configModule = require('@/doswiftly.config');
|
|
22
|
+
const config = configModule.default || configModule;
|
|
23
|
+
configApiUrl = config.api?.url || apiUrl;
|
|
24
|
+
configShopSlug = config.shop?.slug || shopSlug;
|
|
25
|
+
} catch {
|
|
26
|
+
// doswiftly.config.ts not available — use env vars
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const graphqlConfig = {
|
|
30
|
+
apiUrl: configApiUrl,
|
|
31
|
+
shopSlug: configShopSlug,
|
|
32
|
+
} as const;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fragment type re-exports for component data contracts.
|
|
3
|
+
*
|
|
4
|
+
* Import fragment types from here instead of @/generated/graphql.
|
|
5
|
+
* Each fragment maps to a colocated .fragment.graphql file next to its component.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import type { ProductCardFields } from '@/lib/graphql/fragments';
|
|
9
|
+
* interface Props { product: ProductCardFields }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Template-local component fragments (colocated at components/)
|
|
13
|
+
export type { ProductCardFieldsFragment as ProductCardFields } from '@/generated/graphql';
|
|
14
|
+
export type { ProductDetailFieldsFragment as ProductDetailFields } from '@/generated/graphql';
|
|
15
|
+
export type { ProductVariantFieldsFragment as ProductVariantFields } from '@/generated/graphql';
|
|
16
|
+
export type { CollectionCardFieldsFragment as CollectionCardFields } from '@/generated/graphql';
|
|
17
|
+
export type { CartLineFieldsFragment as CartLineFields } from '@/generated/graphql';
|
|
18
|
+
export type { OrderSummaryFieldsFragment as OrderSummaryFields } from '@/generated/graphql';
|
|
19
|
+
export type { CustomerInfoFieldsFragment as CustomerInfoFields } from '@/generated/graphql';
|
|
20
|
+
export type { CategoryNodeFieldsFragment as CategoryNodeFields } from '@/generated/graphql';
|
|
21
|
+
|
|
22
|
+
// Backend SSOT fragments — lightweight price for listing views
|
|
23
|
+
export type { PriceFragment as Price } from '@/generated/graphql';
|
|
24
|
+
|
|
25
|
+
// Backend SSOT fragments (used by loyalty domain components)
|
|
26
|
+
// Same naming convention — clean names without Fragment suffix
|
|
27
|
+
export type { LoyaltyPointsSummaryFragment as LoyaltyPointsSummary } from '@/generated/graphql';
|
|
28
|
+
export type { LoyaltyTierFragment as LoyaltyTier } from '@/generated/graphql';
|
|
29
|
+
export type { TierProgressFragment as TierProgress } from '@/generated/graphql';
|
|
30
|
+
export type { LoyaltyMemberFragment as LoyaltyMember } from '@/generated/graphql';
|
|
31
|
+
export type { LoyaltyRewardFragment as LoyaltyReward } from '@/generated/graphql';
|
|
32
|
+
export type { LoyaltyTransactionFragment as LoyaltyTransaction } from '@/generated/graphql';
|
|
33
|
+
export type { LoyaltySettingsFragment as LoyaltySettings } from '@/generated/graphql';
|
|
34
|
+
export type { ReferralStatsFragment as ReferralStats } from '@/generated/graphql';
|