@doswiftly/cli 0.1.19 → 0.1.20

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 (147) hide show
  1. package/dist/commands/deploy.d.ts +20 -0
  2. package/dist/commands/deploy.d.ts.map +1 -1
  3. package/dist/commands/deploy.js +219 -6
  4. package/dist/commands/deploy.js.map +1 -1
  5. package/package.json +4 -4
  6. package/templates/storefront-minimal/.github/workflows/build-template.yml +10 -0
  7. package/templates/storefront-minimal/wrangler.toml +11 -0
  8. package/templates/storefront-nextjs/.github/workflows/build-template.yml +10 -0
  9. package/templates/storefront-nextjs/wrangler.toml +11 -0
  10. package/templates/storefront-nextjs-shadcn/.github/workflows/build-template.yml +10 -0
  11. package/templates/storefront-nextjs-shadcn/CLAUDE.md +29 -5
  12. package/templates/storefront-nextjs-shadcn/app/{about → [locale]/about}/page.tsx +17 -14
  13. package/templates/storefront-nextjs-shadcn/app/{account → [locale]/account}/addresses/page.tsx +19 -15
  14. package/templates/storefront-nextjs-shadcn/app/{account → [locale]/account}/error.tsx +8 -5
  15. package/templates/storefront-nextjs-shadcn/app/{account → [locale]/account}/loyalty/page.tsx +39 -34
  16. package/templates/storefront-nextjs-shadcn/app/{account → [locale]/account}/orders/[id]/page.tsx +9 -7
  17. package/templates/storefront-nextjs-shadcn/app/{account → [locale]/account}/orders/[id]/tracking/page.tsx +27 -25
  18. package/templates/storefront-nextjs-shadcn/app/{account → [locale]/account}/orders/page.tsx +13 -9
  19. package/templates/storefront-nextjs-shadcn/app/{account → [locale]/account}/page.tsx +1 -2
  20. package/templates/storefront-nextjs-shadcn/app/{account → [locale]/account}/settings/page.tsx +1 -1
  21. package/templates/storefront-nextjs-shadcn/app/{auth → [locale]/auth}/forgot-password/page.tsx +14 -12
  22. package/templates/storefront-nextjs-shadcn/app/{auth → [locale]/auth}/login/page.tsx +5 -2
  23. package/templates/storefront-nextjs-shadcn/app/{auth → [locale]/auth}/register/page.tsx +5 -2
  24. package/templates/storefront-nextjs-shadcn/app/{blog → [locale]/blog}/[slug]/page.tsx +1 -1
  25. package/templates/storefront-nextjs-shadcn/app/{cart → [locale]/cart}/page.tsx +14 -10
  26. package/templates/storefront-nextjs-shadcn/app/{categories → [locale]/categories}/[slug]/category-products-client.tsx +4 -2
  27. package/templates/storefront-nextjs-shadcn/app/{categories → [locale]/categories}/page.tsx +13 -8
  28. package/templates/storefront-nextjs-shadcn/app/{checkout → [locale]/checkout}/error.tsx +1 -1
  29. package/templates/storefront-nextjs-shadcn/app/{checkout → [locale]/checkout}/page.tsx +228 -184
  30. package/templates/storefront-nextjs-shadcn/app/{checkout → [locale]/checkout}/success/[orderId]/page.tsx +36 -34
  31. package/templates/storefront-nextjs-shadcn/app/{collections → [locale]/collections}/[handle]/page.tsx +5 -3
  32. package/templates/storefront-nextjs-shadcn/app/{collections → [locale]/collections}/page.tsx +13 -8
  33. package/templates/storefront-nextjs-shadcn/app/{contact → [locale]/contact}/page.tsx +24 -21
  34. package/templates/storefront-nextjs-shadcn/app/{error.tsx → [locale]/error.tsx} +13 -8
  35. package/templates/storefront-nextjs-shadcn/app/[locale]/layout.tsx +92 -0
  36. package/templates/storefront-nextjs-shadcn/app/{not-found.tsx → [locale]/not-found.tsx} +13 -18
  37. package/templates/storefront-nextjs-shadcn/app/{page.tsx → [locale]/page.tsx} +8 -4
  38. package/templates/storefront-nextjs-shadcn/app/{products → [locale]/products}/[slug]/error.tsx +1 -1
  39. package/templates/storefront-nextjs-shadcn/app/{products → [locale]/products}/[slug]/page.tsx +11 -8
  40. package/templates/storefront-nextjs-shadcn/app/{products → [locale]/products}/[slug]/product-client.tsx +3 -1
  41. package/templates/storefront-nextjs-shadcn/app/{products → [locale]/products}/page.tsx +6 -3
  42. package/templates/storefront-nextjs-shadcn/app/{products → [locale]/products}/products-client.tsx +14 -10
  43. package/templates/storefront-nextjs-shadcn/app/{wishlist → [locale]/wishlist}/page.tsx +21 -25
  44. package/templates/storefront-nextjs-shadcn/app/layout.tsx +6 -68
  45. package/templates/storefront-nextjs-shadcn/components/account/address-form.tsx +25 -20
  46. package/templates/storefront-nextjs-shadcn/components/account/address-list.tsx +11 -10
  47. package/templates/storefront-nextjs-shadcn/components/account/order-details.tsx +14 -12
  48. package/templates/storefront-nextjs-shadcn/components/account/order-history.tsx +28 -18
  49. package/templates/storefront-nextjs-shadcn/components/auth/account-menu.tsx +10 -8
  50. package/templates/storefront-nextjs-shadcn/components/auth/login-form.tsx +27 -22
  51. package/templates/storefront-nextjs-shadcn/components/auth/register-form.tsx +48 -43
  52. package/templates/storefront-nextjs-shadcn/components/blog/blog-card.tsx +1 -1
  53. package/templates/storefront-nextjs-shadcn/components/blog/blog-sidebar.tsx +1 -1
  54. package/templates/storefront-nextjs-shadcn/components/brand/brand-card.tsx +1 -1
  55. package/templates/storefront-nextjs-shadcn/components/cart/cart-drawer.tsx +7 -4
  56. package/templates/storefront-nextjs-shadcn/components/cart/cart-icon.tsx +1 -1
  57. package/templates/storefront-nextjs-shadcn/components/cart/cart-item.tsx +7 -5
  58. package/templates/storefront-nextjs-shadcn/components/cart/cart-summary.tsx +9 -7
  59. package/templates/storefront-nextjs-shadcn/components/cart/promo-code-input.tsx +8 -5
  60. package/templates/storefront-nextjs-shadcn/components/cart/shipping-estimator.tsx +18 -15
  61. package/templates/storefront-nextjs-shadcn/components/checkout/payment-method-card.tsx +15 -25
  62. package/templates/storefront-nextjs-shadcn/components/checkout/payment-step.tsx +10 -8
  63. package/templates/storefront-nextjs-shadcn/components/checkout/tax-breakdown.tsx +9 -6
  64. package/templates/storefront-nextjs-shadcn/components/commerce/currency-selector.tsx +5 -3
  65. package/templates/storefront-nextjs-shadcn/components/commerce/pagination.tsx +8 -5
  66. package/templates/storefront-nextjs-shadcn/components/commerce/product-actions.tsx +5 -3
  67. package/templates/storefront-nextjs-shadcn/components/commerce/search-input.tsx +8 -7
  68. package/templates/storefront-nextjs-shadcn/components/common/category-card.tsx +1 -1
  69. package/templates/storefront-nextjs-shadcn/components/common/collection-card.tsx +1 -1
  70. package/templates/storefront-nextjs-shadcn/components/common/social-share.tsx +9 -6
  71. package/templates/storefront-nextjs-shadcn/components/discount/discount-breakdown.tsx +21 -11
  72. package/templates/storefront-nextjs-shadcn/components/discount/discount-code-input.tsx +16 -13
  73. package/templates/storefront-nextjs-shadcn/components/error/error-boundary.tsx +53 -28
  74. package/templates/storefront-nextjs-shadcn/components/filters/dynamic-attribute-filters.tsx +7 -5
  75. package/templates/storefront-nextjs-shadcn/components/gift-card/gift-card-balance.tsx +19 -15
  76. package/templates/storefront-nextjs-shadcn/components/gift-card/gift-card-input.tsx +12 -9
  77. package/templates/storefront-nextjs-shadcn/components/home/category-grid.tsx +8 -5
  78. package/templates/storefront-nextjs-shadcn/components/home/featured-collections.tsx +1 -1
  79. package/templates/storefront-nextjs-shadcn/components/home/featured-products.tsx +12 -8
  80. package/templates/storefront-nextjs-shadcn/components/home/hero-section.tsx +13 -8
  81. package/templates/storefront-nextjs-shadcn/components/home/newsletter-signup.tsx +10 -8
  82. package/templates/storefront-nextjs-shadcn/components/layout/breadcrumbs.tsx +37 -12
  83. package/templates/storefront-nextjs-shadcn/components/layout/currency-selector.tsx +5 -2
  84. package/templates/storefront-nextjs-shadcn/components/layout/footer.tsx +24 -23
  85. package/templates/storefront-nextjs-shadcn/components/layout/header.tsx +20 -12
  86. package/templates/storefront-nextjs-shadcn/components/layout/language-switcher.tsx +54 -0
  87. package/templates/storefront-nextjs-shadcn/components/layout/mobile-menu.tsx +33 -30
  88. package/templates/storefront-nextjs-shadcn/components/layout/navigation.tsx +27 -24
  89. package/templates/storefront-nextjs-shadcn/components/loyalty/referral-section.tsx +23 -24
  90. package/templates/storefront-nextjs-shadcn/components/product/add-to-cart-button.tsx +6 -14
  91. package/templates/storefront-nextjs-shadcn/components/product/b2b-price-display.tsx +1 -1
  92. package/templates/storefront-nextjs-shadcn/components/product/filter-active-pills.tsx +4 -1
  93. package/templates/storefront-nextjs-shadcn/components/product/filter-mobile-sheet.tsx +7 -4
  94. package/templates/storefront-nextjs-shadcn/components/product/filter-price-range.tsx +5 -3
  95. package/templates/storefront-nextjs-shadcn/components/product/product-card.tsx +8 -6
  96. package/templates/storefront-nextjs-shadcn/components/product/product-filters.tsx +3 -1
  97. package/templates/storefront-nextjs-shadcn/components/product/product-image.tsx +3 -7
  98. package/templates/storefront-nextjs-shadcn/components/product/product-sort.tsx +26 -13
  99. package/templates/storefront-nextjs-shadcn/components/product/review-form.tsx +25 -27
  100. package/templates/storefront-nextjs-shadcn/components/providers/language-sync-provider.tsx +27 -0
  101. package/templates/storefront-nextjs-shadcn/components/providers/stores-provider.tsx +40 -7
  102. package/templates/storefront-nextjs-shadcn/components/returns/return-request-form.tsx +56 -70
  103. package/templates/storefront-nextjs-shadcn/components/search/search-bar.tsx +7 -4
  104. package/templates/storefront-nextjs-shadcn/components/shipping/shipping-method-selector.tsx +12 -9
  105. package/templates/storefront-nextjs-shadcn/components/ui/empty-state.tsx +23 -12
  106. package/templates/storefront-nextjs-shadcn/components/wishlist/wishlist-button.tsx +7 -4
  107. package/templates/storefront-nextjs-shadcn/components/wishlist/wishlist-icon.tsx +1 -1
  108. package/templates/storefront-nextjs-shadcn/components/wishlist/wishlist-item.tsx +2 -10
  109. package/templates/storefront-nextjs-shadcn/generated/graphql.ts +1159 -551
  110. package/templates/storefront-nextjs-shadcn/hooks/index.ts +1 -0
  111. package/templates/storefront-nextjs-shadcn/hooks/use-cart-actions.ts +22 -249
  112. package/templates/storefront-nextjs-shadcn/hooks/use-cart-di.ts +67 -0
  113. package/templates/storefront-nextjs-shadcn/hooks/use-cart-sync.ts +3 -3
  114. package/templates/storefront-nextjs-shadcn/i18n/navigation.ts +12 -0
  115. package/templates/storefront-nextjs-shadcn/i18n/request.ts +17 -0
  116. package/templates/storefront-nextjs-shadcn/i18n/routing.ts +17 -0
  117. package/templates/storefront-nextjs-shadcn/lib/graphql/config.ts +1 -0
  118. package/templates/storefront-nextjs-shadcn/lib/graphql/hooks.ts +41 -8
  119. package/templates/storefront-nextjs-shadcn/lib/graphql/query-keys.ts +20 -18
  120. package/templates/storefront-nextjs-shadcn/lib/graphql/server.ts +2 -1
  121. package/templates/storefront-nextjs-shadcn/messages/en.json +869 -0
  122. package/templates/storefront-nextjs-shadcn/messages/pl.json +869 -0
  123. package/templates/storefront-nextjs-shadcn/next.config.ts +6 -5
  124. package/templates/storefront-nextjs-shadcn/package.json +3 -2
  125. package/templates/storefront-nextjs-shadcn/proxy.ts +115 -46
  126. package/templates/storefront-nextjs-shadcn/stores/cart-store.ts +24 -58
  127. package/templates/storefront-nextjs-shadcn/wrangler.toml +11 -0
  128. /package/templates/storefront-nextjs-shadcn/app/{account → [locale]/account}/loading.tsx +0 -0
  129. /package/templates/storefront-nextjs-shadcn/app/{account → [locale]/account}/orders/[id]/loading.tsx +0 -0
  130. /package/templates/storefront-nextjs-shadcn/app/{blog → [locale]/blog}/[slug]/loading.tsx +0 -0
  131. /package/templates/storefront-nextjs-shadcn/app/{blog → [locale]/blog}/loading.tsx +0 -0
  132. /package/templates/storefront-nextjs-shadcn/app/{blog → [locale]/blog}/page.tsx +0 -0
  133. /package/templates/storefront-nextjs-shadcn/app/{brands → [locale]/brands}/[slug]/page.tsx +0 -0
  134. /package/templates/storefront-nextjs-shadcn/app/{brands → [locale]/brands}/page.tsx +0 -0
  135. /package/templates/storefront-nextjs-shadcn/app/{cart → [locale]/cart}/loading.tsx +0 -0
  136. /package/templates/storefront-nextjs-shadcn/app/{categories → [locale]/categories}/[slug]/loading.tsx +0 -0
  137. /package/templates/storefront-nextjs-shadcn/app/{categories → [locale]/categories}/[slug]/page.tsx +0 -0
  138. /package/templates/storefront-nextjs-shadcn/app/{checkout → [locale]/checkout}/loading.tsx +0 -0
  139. /package/templates/storefront-nextjs-shadcn/app/{collections → [locale]/collections}/[handle]/loading.tsx +0 -0
  140. /package/templates/storefront-nextjs-shadcn/app/{collections → [locale]/collections}/loading.tsx +0 -0
  141. /package/templates/storefront-nextjs-shadcn/app/{products → [locale]/products}/[slug]/loading.tsx +0 -0
  142. /package/templates/storefront-nextjs-shadcn/app/{products → [locale]/products}/loading.tsx +0 -0
  143. /package/templates/storefront-nextjs-shadcn/app/{returns → [locale]/returns}/page.tsx +0 -0
  144. /package/templates/storefront-nextjs-shadcn/app/{search → [locale]/search}/loading.tsx +0 -0
  145. /package/templates/storefront-nextjs-shadcn/app/{search → [locale]/search}/page.tsx +0 -0
  146. /package/templates/storefront-nextjs-shadcn/app/{search → [locale]/search}/search-client.tsx +0 -0
  147. /package/templates/storefront-nextjs-shadcn/app/{shipping → [locale]/shipping}/page.tsx +0 -0
@@ -7,5 +7,6 @@ export type { LoginResult, LogoutResult, TokenRenewResult } from "./use-auth";
7
7
  export { useAuthSync } from "./use-auth-sync";
8
8
  export { useCartSync } from "./use-cart-sync";
9
9
  export { useCartActions } from "./use-cart-actions";
10
+ export { useCartDI } from "./use-cart-di";
10
11
  export { useFilterParams } from "./use-filter-params";
11
12
  export type { UseFilterParamsReturn, UseFilterParamsOptions } from "./use-filter-params";
@@ -1,261 +1,54 @@
1
1
  'use client';
2
2
 
3
3
  import { useCallback, useEffect, useRef } from 'react';
4
- import { useQueryClient } from '@tanstack/react-query';
5
4
  import { useCartStore, useCartStoreApi } from '@/stores/cart-store';
6
- import { useCartCreate, useCartLinesAdd, useCartLinesUpdate, useCartLinesRemove } from '@/lib/graphql/hooks';
7
- import { queryKeys } from '@/lib/graphql/query-keys';
8
- import { toast } from 'sonner';
9
- import { StorefrontError } from '@doswiftly/storefront-sdk';
10
5
 
11
6
  // Debounce delay for quantity updates (prevents rate limiting)
12
7
  const QUANTITY_UPDATE_DEBOUNCE_MS = 500;
13
8
 
14
9
  /**
15
- * Hook for cart mutations — server-only, no local state manipulation.
10
+ * Hook for cart mutations — delegates orchestration to SDK cart store.
16
11
  *
17
- * All mutations go through GraphQL. React Query cache invalidation
18
- * (configured in hooks.ts) automatically updates all consumers via useCartSync.
12
+ * SDK handles: cart init, DI actions, error state, callbacks.
13
+ * Template handles: debounce, openCart UX.
19
14
  *
20
15
  * @example
21
16
  * ```typescript
22
17
  * const { addToCart, updateQuantity, removeFromCart } = useCartActions();
23
- *
24
- * await addToCart({
25
- * variantId: 'variant-123',
26
- * productId: 'product-456',
27
- * productTitle: 'T-Shirt',
28
- * variantTitle: 'Large / Blue',
29
- * price: { amount: '29.99', currencyCode: 'USD' },
30
- * quantity: 1
31
- * });
32
- *
33
- * // updateQuantity and removeFromCart take lineId (not variantId)
34
- * updateQuantity('line-abc', 3);
35
- * removeFromCart('line-abc');
18
+ * await addToCart('variant-123', 1);
36
19
  * ```
37
20
  */
38
21
  export function useCartActions() {
39
- const queryClient = useQueryClient();
40
- const cartStoreApi = useCartStoreApi();
41
- const {
42
- setCartId,
43
- clearCart,
44
- openCart,
45
- } = useCartStore();
22
+ const api = useCartStoreApi();
23
+ const isLoading = useCartStore((s) => s.isLoading);
46
24
 
47
- // GraphQL mutations
48
- const createCartMutation = useCartCreate();
49
- const addLinesMutation = useCartLinesAdd();
50
- const updateLinesMutation = useCartLinesUpdate();
51
- const removeLinesMutation = useCartLinesRemove();
52
-
53
- // Debounce refs for quantity updates (prevents ThrottlerException on rapid clicks)
25
+ // Debounce refs for quantity updates
54
26
  const updateTimeoutRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
55
- const pendingUpdatesRef = useRef<Map<string, { lineId: string; quantity: number }>>(new Map());
56
-
57
- /**
58
- * Get or create cart ID.
59
- * Reads cartId from store (fresh, not stale closure) or creates a new cart.
60
- */
61
- const getOrCreateCartId = useCallback(async (forceNew: boolean = false): Promise<string> => {
62
- const currentCartId = cartStoreApi.getState().cartId;
63
- if (currentCartId && !forceNew) {
64
- return currentCartId;
65
- }
66
-
67
- try {
68
- const result = await createCartMutation.mutateAsync({ input: {} });
69
-
70
- if (result.cartCreate.cart) {
71
- const newCartId = result.cartCreate.cart.id;
72
- setCartId(newCartId);
73
- return newCartId;
74
- }
75
-
76
- if (result.cartCreate.userErrors?.length > 0) {
77
- throw new Error(result.cartCreate.userErrors[0].message);
78
- }
79
-
80
- throw new Error('Failed to create cart');
81
- } catch (error: unknown) {
82
- console.error('Cart creation failed:', error);
83
- throw error;
84
- }
85
- }, [cartStoreApi, setCartId, createCartMutation]);
86
27
 
87
28
  /**
88
- * Check if error is a "Cart not found" error (stale/expired cart)
29
+ * Add item to cart by variant ID, delegates to SDK store.
89
30
  */
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;
100
- };
101
-
102
- /**
103
- * Add item to cart (server-only).
104
- *
105
- * Creates cart if needed. On "cart not found", clears cartId, creates new cart, retries once.
106
- * React Query cache invalidation in hooks.ts updates all useCartSync consumers.
107
- */
108
- const addToCart = useCallback(async (item: {
109
- variantId: string;
110
- productId: string;
111
- productHandle?: string;
112
- productTitle: string;
113
- variantTitle: string;
114
- price: { amount: string; currencyCode: string };
115
- image?: { url: string; altText?: string | null } | null;
116
- available?: boolean;
117
- quantity?: number;
118
- }, _options?: { _forceNewCart?: boolean }) => {
119
- const forceNewCart = _options?._forceNewCart ?? false;
120
-
121
- try {
122
- // Cancel in-flight cart queries to prevent stale data overwriting
123
- await queryClient.cancelQueries({ queryKey: queryKeys.cart.all() });
124
-
125
- const cartId = await getOrCreateCartId(forceNewCart);
126
-
127
- const result = await addLinesMutation.mutateAsync({
128
- cartId,
129
- lines: [{
130
- merchandiseId: item.variantId,
131
- quantity: item.quantity ?? 1,
132
- }],
133
- });
134
-
135
- if (result.cartLinesAdd.userErrors?.length > 0) {
136
- const errorMessage = result.cartLinesAdd.userErrors[0].message;
137
-
138
- if (isCartNotFoundError({ message: errorMessage }) && !forceNewCart) {
139
- console.warn('Cart expired, creating new cart and retrying...');
140
- setCartId(null);
141
- return addToCart(item, { _forceNewCart: true });
142
- }
143
-
144
- throw new Error(errorMessage);
145
- }
146
-
147
- // Open cart drawer to show the added item
148
- openCart();
149
- toast.success('Added to cart');
150
- } catch (error: unknown) {
151
- if (isCartNotFoundError(error) && !forceNewCart) {
152
- console.warn('Cart expired (caught), creating new cart and retrying...');
153
- setCartId(null);
154
- return addToCart(item, { _forceNewCart: true });
155
- }
156
-
157
- console.error('Add to cart failed:', error);
158
- const message = error instanceof Error ? error.message : 'Failed to add to cart';
159
- toast.error(message);
160
- throw error;
161
- }
162
- }, [setCartId, openCart, getOrCreateCartId, addLinesMutation]);
31
+ const addToCart = useCallback(async (variantId: string, quantity = 1) => {
32
+ await api.getState().addToCart([{ merchandiseId: variantId, quantity }]);
33
+ api.getState().openCart();
34
+ }, [api]);
163
35
 
164
36
  /**
165
- * Execute the actual GraphQL update for a pending quantity change
37
+ * Execute the actual quantity update via SDK store.
166
38
  */
167
39
  const executeQuantityUpdate = useCallback(async (lineId: string, quantity: number) => {
168
- try {
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;
173
- if (!currentCartId) {
174
- throw new Error('No cart found');
175
- }
176
-
177
- const result = await updateLinesMutation.mutateAsync({
178
- cartId: currentCartId,
179
- lines: [{
180
- id: lineId,
181
- quantity,
182
- }],
183
- });
184
-
185
- if (result.cartLinesUpdate.userErrors?.length > 0) {
186
- const errorMessage = result.cartLinesUpdate.userErrors[0].message;
187
-
188
- if (isCartNotFoundError({ message: errorMessage })) {
189
- console.warn('Cart expired during update, clearing cart');
190
- clearCart();
191
- toast.error('Your cart has expired. Please add items again.');
192
- return;
193
- }
194
-
195
- throw new Error(errorMessage);
196
- }
197
- } catch (error: unknown) {
198
- if (isCartNotFoundError(error)) {
199
- console.warn('Cart expired during update (caught), clearing cart');
200
- clearCart();
201
- toast.error('Your cart has expired. Please add items again.');
202
- return;
203
- }
204
-
205
- console.error('Update quantity failed:', error);
206
- const message = error instanceof Error ? error.message : 'Failed to update quantity';
207
- toast.error(message);
208
- }
209
- }, [cartStoreApi, queryClient, updateLinesMutation, clearCart]);
40
+ await api.getState().updateQuantity([{ id: lineId, quantity }]);
41
+ }, [api]);
210
42
 
211
43
  /**
212
44
  * Remove item from cart by line ID.
213
- *
214
- * Silently handles expired cart (just clears stale cartId).
215
45
  */
216
46
  const removeFromCart = useCallback(async (lineId: string) => {
217
- try {
218
- const currentCartId = cartStoreApi.getState().cartId;
219
- if (!currentCartId) {
220
- return;
221
- }
222
-
223
- const result = await removeLinesMutation.mutateAsync({
224
- cartId: currentCartId,
225
- lineIds: [lineId],
226
- });
227
-
228
- if (result.cartLinesRemove.userErrors?.length > 0) {
229
- const errorMessage = result.cartLinesRemove.userErrors[0].message;
230
-
231
- if (isCartNotFoundError({ message: errorMessage })) {
232
- console.warn('Cart expired during remove, clearing stale cartId');
233
- setCartId(null);
234
- return;
235
- }
236
-
237
- throw new Error(errorMessage);
238
- }
239
-
240
- toast.success('Removed from cart');
241
- } catch (error: unknown) {
242
- if (isCartNotFoundError(error)) {
243
- console.warn('Cart expired during remove (caught), clearing stale cartId');
244
- setCartId(null);
245
- return;
246
- }
247
-
248
- console.error('Remove from cart failed:', error);
249
- const message = error instanceof Error ? error.message : 'Failed to remove from cart';
250
- toast.error(message);
251
- throw error;
252
- }
253
- }, [cartStoreApi, setCartId, removeLinesMutation]);
47
+ await api.getState().removeFromCart([lineId]);
48
+ }, [api]);
254
49
 
255
50
  /**
256
- * Update item quantity (debounced, takes lineId).
257
- *
258
- * Debounces GraphQL API calls to prevent ThrottlerException on rapid clicks.
51
+ * Update item quantity (debounced).
259
52
  * If quantity <= 0, removes the item instead.
260
53
  */
261
54
  const updateQuantity = useCallback((lineId: string, quantity: number) => {
@@ -264,23 +57,14 @@ export function useCartActions() {
264
57
  return;
265
58
  }
266
59
 
267
- // Cancel any pending update for this line
268
60
  const existingTimeout = updateTimeoutRef.current.get(lineId);
269
61
  if (existingTimeout) {
270
62
  clearTimeout(existingTimeout);
271
63
  }
272
64
 
273
- // Store the pending update
274
- pendingUpdatesRef.current.set(lineId, { lineId, quantity });
275
-
276
- // Schedule debounced API call
277
65
  const timeout = setTimeout(() => {
278
- const pending = pendingUpdatesRef.current.get(lineId);
279
- if (pending) {
280
- pendingUpdatesRef.current.delete(lineId);
281
- updateTimeoutRef.current.delete(lineId);
282
- executeQuantityUpdate(pending.lineId, pending.quantity);
283
- }
66
+ updateTimeoutRef.current.delete(lineId);
67
+ executeQuantityUpdate(lineId, quantity);
284
68
  }, QUANTITY_UPDATE_DEBOUNCE_MS);
285
69
 
286
70
  updateTimeoutRef.current.set(lineId, timeout);
@@ -294,25 +78,14 @@ export function useCartActions() {
294
78
  clearTimeout(timeout);
295
79
  }
296
80
  timeouts.clear();
297
- pendingUpdatesRef.current.clear();
298
81
  };
299
82
  }, []);
300
83
 
301
- /**
302
- * Clear entire cart.
303
- * Clears cartId in zustand persist → useCartSync returns empty.
304
- */
305
- const clearEntireCart = useCallback(() => {
306
- clearCart();
307
- toast.success('Cart cleared');
308
- }, [clearCart]);
309
-
310
84
  return {
311
85
  addToCart,
312
86
  updateQuantity,
313
87
  removeFromCart,
314
- clearCart: clearEntireCart,
315
- isLoading: createCartMutation.isPending || addLinesMutation.isPending ||
316
- updateLinesMutation.isPending || removeLinesMutation.isPending,
88
+ clearCart: useCallback(() => api.getState().clearCart(), [api]),
89
+ isLoading,
317
90
  };
318
91
  }
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import type { CartActions, CartData } from '@doswiftly/storefront-sdk/react';
5
+ import { assertNoUserErrors } from '@doswiftly/storefront-sdk';
6
+ import { useExecute } from '@/lib/graphql/client';
7
+ import type { CartQuery, CartCreateMutation, CartLinesAddMutation, CartLinesUpdateMutation, CartLinesRemoveMutation } from '@/generated/graphql';
8
+ import {
9
+ CartDocument,
10
+ CartCreateDocument,
11
+ CartLinesAddDocument,
12
+ CartLinesUpdateDocument,
13
+ CartLinesRemoveDocument,
14
+ } from '@/generated/graphql';
15
+
16
+ interface CartMutationResult {
17
+ cart: { id: string; totalQuantity: number } | null;
18
+ userErrors: Array<{ message: string; field?: string[]; code?: string }>;
19
+ }
20
+
21
+ function extractCartData(result: CartMutationResult): CartData {
22
+ assertNoUserErrors(result);
23
+ return { id: result.cart!.id, totalQuantity: result.cart!.totalQuantity };
24
+ }
25
+
26
+ /**
27
+ * CartActions DI implementation using template's GraphQL operations.
28
+ *
29
+ * Maps generated GraphQL documents to the SDK CartActions interface.
30
+ * Used by StoresProvider to wire createCartStore with template transport.
31
+ */
32
+ export function useCartDI(): CartActions {
33
+ const execute = useExecute();
34
+
35
+ return useMemo(
36
+ (): CartActions => ({
37
+ fetchCart: async (cartId) => {
38
+ const data = await execute<CartQuery>(CartDocument.toString(), { id: cartId });
39
+ if (!data.cart) return null;
40
+ return { id: data.cart.id, totalQuantity: data.cart.totalQuantity };
41
+ },
42
+
43
+ createCart: async () => {
44
+ const data = await execute<CartCreateMutation>(CartCreateDocument.toString(), { input: {} });
45
+ assertNoUserErrors(data.cartCreate);
46
+ if (!data.cartCreate.cart) throw new Error('Failed to create cart');
47
+ return data.cartCreate.cart.id;
48
+ },
49
+
50
+ addLines: async (cartId, lines) => {
51
+ const data = await execute<CartLinesAddMutation>(CartLinesAddDocument.toString(), { cartId, lines });
52
+ return extractCartData(data.cartLinesAdd);
53
+ },
54
+
55
+ updateLines: async (cartId, lines) => {
56
+ const data = await execute<CartLinesUpdateMutation>(CartLinesUpdateDocument.toString(), { cartId, lines });
57
+ return extractCartData(data.cartLinesUpdate);
58
+ },
59
+
60
+ removeLines: async (cartId, lineIds) => {
61
+ const data = await execute<CartLinesRemoveMutation>(CartLinesRemoveDocument.toString(), { cartId, lineIds });
62
+ return extractCartData(data.cartLinesRemove);
63
+ },
64
+ }),
65
+ [execute],
66
+ );
67
+ }
@@ -33,7 +33,7 @@ export interface CartItemData {
33
33
  */
34
34
  export function useCartSync() {
35
35
  const cartId = useCartStore((state) => state.cartId);
36
- const setCartId = useCartStore((state) => state.setCartId);
36
+ const clearCart = useCartStore((state) => state.clearCart);
37
37
  const isHydrated = useHydrated();
38
38
 
39
39
  const { data, isLoading, isSuccess, error, refetch } = useCart(cartId, {
@@ -51,9 +51,9 @@ export function useCartSync() {
51
51
  // Auto-clear stale cartId
52
52
  useEffect(() => {
53
53
  if (isStaleCart) {
54
- setCartId(null);
54
+ clearCart();
55
55
  }
56
- }, [isStaleCart, setCartId]);
56
+ }, [isStaleCart, clearCart]);
57
57
 
58
58
  // Map GraphQL lines to display-friendly items
59
59
  const items: CartItemData[] = (cart?.lines ?? []).map((line: CartLineFields) => {
@@ -0,0 +1,12 @@
1
+ import { createNavigation } from 'next-intl/navigation';
2
+ import { defaultLocale, localePrefix } from './routing';
3
+
4
+ /**
5
+ * Navigation APIs for dynamic locales.
6
+ *
7
+ * No `locales` argument — any locale string accepted at runtime.
8
+ * Backend is SSOT for supportedLanguages.
9
+ * `defaultLocale` required for `as-needed` prefix mode (hides default in URL).
10
+ */
11
+ export const { Link, redirect, usePathname, useRouter, getPathname } =
12
+ createNavigation({ defaultLocale, localePrefix });
@@ -0,0 +1,17 @@
1
+ import { getRequestConfig } from 'next-intl/server';
2
+ import { defaultLocale } from './routing';
3
+
4
+ export default getRequestConfig(async ({ requestLocale }) => {
5
+ // requestLocale is set by next-intl middleware (proxy.ts) which validates
6
+ // against dynamic locales from backend. If somehow invalid, fall back.
7
+ const locale = (await requestLocale) || defaultLocale;
8
+
9
+ let messages;
10
+ try {
11
+ messages = (await import(`../messages/${locale}.json`)).default;
12
+ } catch {
13
+ messages = (await import(`../messages/${defaultLocale}.json`)).default;
14
+ }
15
+
16
+ return { locale, messages };
17
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * next-intl routing configuration (dynamic locales).
3
+ *
4
+ * - defaultLocale: from NEXT_PUBLIC_DEFAULT_LOCALE env var (set by CLI `doswiftly init`), fallback 'pl'.
5
+ * - localePrefix: 'as-needed' — default locale hidden in URL, others prefixed.
6
+ * - locales: NOT defined here — backend is SSOT for supportedLanguages.
7
+ * - Middleware (proxy.ts) fetches supportedLanguages dynamically per-request.
8
+ * - Navigation API (navigation.ts) accepts any locale string at runtime.
9
+ * - Layout validates against backend's shop.supportedLanguages.
10
+ *
11
+ * No `defineRouting` — we export primitives consumed by createNavigation and createMiddleware.
12
+ */
13
+
14
+ export const defaultLocale =
15
+ (process.env.NEXT_PUBLIC_DEFAULT_LOCALE as string) || 'pl';
16
+
17
+ export const localePrefix = 'as-needed' as const;
@@ -30,3 +30,4 @@ export const graphqlConfig = {
30
30
  apiUrl: configApiUrl,
31
31
  shopSlug: configShopSlug,
32
32
  } as const;
33
+
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useQuery, useMutation, useQueryClient, type UseQueryOptions, type UseMutationOptions } from '@tanstack/react-query';
4
4
  import { useExecute } from './client';
5
- import { useCurrencyStore, useAuthStore } from '@doswiftly/storefront-sdk/react';
5
+ import { useCurrencyStore, useAuthStore, useLanguageStore } from '@doswiftly/storefront-sdk/react';
6
6
  import { normalizeConnection, type NormalizedConnection } from '@doswiftly/storefront-sdk';
7
7
  import { queryKeys } from './query-keys';
8
8
  import type { TypedDocumentString } from '@/generated/graphql';
@@ -34,6 +34,7 @@ import {
34
34
  CheckoutCompleteDocument,
35
35
  CheckoutGiftCardApplyDocument,
36
36
  CheckoutGiftCardRemoveDocument,
37
+ CheckoutGiftCardRecipientUpdateDocument,
37
38
  CustomerDocument,
38
39
  CustomerProfileDocument,
39
40
  CustomerOrderDocument,
@@ -106,6 +107,8 @@ import type {
106
107
  CheckoutGiftCardApplyMutationVariables,
107
108
  CheckoutGiftCardRemoveMutation,
108
109
  CheckoutGiftCardRemoveMutationVariables,
110
+ CheckoutGiftCardRecipientUpdateMutation,
111
+ CheckoutGiftCardRecipientUpdateMutationVariables,
109
112
  CustomerQuery,
110
113
  CustomerQueryVariables,
111
114
  CustomerProfileQuery,
@@ -173,12 +176,14 @@ export function useGraphQLQuery<TResult, TVariables>(
173
176
  ) {
174
177
  const execute = useExecute();
175
178
  const currency = useCurrencyStore((s) => s.currency);
179
+ const language = useLanguageStore((s) => s.language);
176
180
  const operationName = getOperationName(document as TypedDocumentString<unknown, unknown>);
177
- const queryKey = [operationName, variables, currency];
181
+ const queryKey = [operationName, variables, currency, language];
178
182
 
179
183
  return useQuery({
180
184
  queryKey,
181
185
  queryFn: () => execute<TResult>(document.toString(), variables as Record<string, unknown>),
186
+ enabled: !!language && (options?.enabled ?? true),
182
187
  ...options,
183
188
  });
184
189
  }
@@ -223,11 +228,12 @@ export function useProduct(
223
228
  ) {
224
229
  const execute = useExecute();
225
230
  const currency = useCurrencyStore((s) => s.currency);
231
+ const language = useLanguageStore((s) => s.language);
226
232
 
227
233
  const isId = handleOrId.startsWith('gid://');
228
234
 
229
235
  return useQuery({
230
- queryKey: queryKeys.products.detail(handleOrId, currency),
236
+ queryKey: queryKeys.products.detail(handleOrId, currency, language),
231
237
  queryFn: () => {
232
238
  const variables: ProductQueryVariables = isId
233
239
  ? { id: handleOrId }
@@ -280,9 +286,10 @@ export function useProducts(
280
286
  ) {
281
287
  const execute = useExecute();
282
288
  const currency = useCurrencyStore((s) => s.currency);
289
+ const language = useLanguageStore((s) => s.language);
283
290
 
284
291
  return useQuery({
285
- queryKey: queryKeys.products.list(variables as Record<string, unknown>, currency),
292
+ queryKey: queryKeys.products.list(variables as Record<string, unknown>, currency, language),
286
293
  queryFn: async () => {
287
294
  const { sortKey: normalizedSortKey, reverse: normalizedReverse } = normalizeSortKey(variables?.sortKey);
288
295
 
@@ -325,11 +332,12 @@ export function useCollection(
325
332
  ) {
326
333
  const execute = useExecute();
327
334
  const currency = useCurrencyStore((s) => s.currency);
335
+ const language = useLanguageStore((s) => s.language);
328
336
 
329
337
  const isId = handleOrId.startsWith('gid://');
330
338
 
331
339
  return useQuery({
332
- queryKey: queryKeys.collections.detail(handleOrId, currency),
340
+ queryKey: queryKeys.collections.detail(handleOrId, currency, language),
333
341
  queryFn: () => {
334
342
  const variables: CollectionQueryVariables = isId
335
343
  ? { id: handleOrId }
@@ -360,9 +368,10 @@ export function useCollections(
360
368
  ) {
361
369
  const execute = useExecute();
362
370
  const currency = useCurrencyStore((s) => s.currency);
371
+ const language = useLanguageStore((s) => s.language);
363
372
 
364
373
  return useQuery({
365
- queryKey: queryKeys.collections.list(variables as Record<string, unknown>, currency),
374
+ queryKey: queryKeys.collections.list(variables as Record<string, unknown>, currency, language),
366
375
  queryFn: async () => {
367
376
  const graphqlVariables: CollectionsQueryVariables = {
368
377
  first: variables?.first ?? 20,
@@ -431,9 +440,10 @@ export function useCategories(
431
440
  ) {
432
441
  const execute = useExecute();
433
442
  const currency = useCurrencyStore((s) => s.currency);
443
+ const language = useLanguageStore((s) => s.language);
434
444
 
435
445
  return useQuery({
436
- queryKey: queryKeys.categories.list(currency),
446
+ queryKey: queryKeys.categories.list(currency, language),
437
447
  queryFn: async () => {
438
448
  const data = await execute<CategoriesQuery>(CategoriesDocument.toString());
439
449
 
@@ -478,9 +488,10 @@ export function useAvailableFilters(
478
488
  ) {
479
489
  const execute = useExecute();
480
490
  const currency = useCurrencyStore((s) => s.currency);
491
+ const language = useLanguageStore((s) => s.language);
481
492
 
482
493
  return useQuery({
483
- queryKey: queryKeys.filters.forContext(input as Record<string, unknown>, currency),
494
+ queryKey: queryKeys.filters.forContext(input as Record<string, unknown>, currency, language),
484
495
  queryFn: () => {
485
496
  const variables: AvailableFiltersQueryVariables = { input: input || null };
486
497
  return execute<AvailableFiltersQuery>(AvailableFiltersDocument.toString(), variables);
@@ -879,6 +890,28 @@ export function useCheckoutGiftCardRemove(
879
890
  });
880
891
  }
881
892
 
893
+ /**
894
+ * Update gift card recipient data on checkout line item
895
+ */
896
+ export function useCheckoutGiftCardRecipientUpdate(
897
+ options?: UseMutationOptions<CheckoutGiftCardRecipientUpdateMutation, Error, CheckoutGiftCardRecipientUpdateMutationVariables>
898
+ ) {
899
+ const execute = useExecute();
900
+ const queryClient = useQueryClient();
901
+
902
+ return useMutation({
903
+ mutationFn: (variables: CheckoutGiftCardRecipientUpdateMutationVariables) =>
904
+ execute<CheckoutGiftCardRecipientUpdateMutation>(CheckoutGiftCardRecipientUpdateDocument.toString(), variables),
905
+ onMutate: async () => {
906
+ await queryClient.cancelQueries({ queryKey: queryKeys.checkout.all() });
907
+ },
908
+ onSettled: () => {
909
+ queryClient.invalidateQueries({ queryKey: queryKeys.checkout.all() });
910
+ },
911
+ ...options,
912
+ });
913
+ }
914
+
882
915
  // ============================================================================
883
916
  // CUSTOMER HOOKS
884
917
  // ============================================================================