@tanstack/create 0.65.0 → 0.67.0

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 (130) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/frameworks/react/add-ons/powersync/README.md.ejs +26 -0
  3. package/dist/frameworks/react/add-ons/powersync/assets/_dot_env.local.append +3 -0
  4. package/dist/frameworks/react/add-ons/powersync/assets/powersync-vite-plugin.ts +17 -0
  5. package/dist/frameworks/react/add-ons/powersync/assets/src/integrations/powersync/provider.tsx +26 -0
  6. package/dist/frameworks/react/add-ons/powersync/assets/src/lib/powersync/AppSchema.ts +17 -0
  7. package/dist/frameworks/react/add-ons/powersync/assets/src/lib/powersync/BackendConnector.ts +52 -0
  8. package/dist/frameworks/react/add-ons/powersync/assets/src/routes/demo/powersync.tsx +129 -0
  9. package/dist/frameworks/react/add-ons/powersync/info.json +46 -0
  10. package/dist/frameworks/react/add-ons/powersync/package.json.ejs +7 -0
  11. package/dist/frameworks/react/add-ons/powersync/small-logo.svg +6 -0
  12. package/dist/frameworks/react/add-ons/shopify/README.md +86 -0
  13. package/dist/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
  14. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
  15. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
  16. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
  17. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
  18. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
  19. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
  20. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
  21. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
  22. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
  23. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
  24. package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
  25. package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
  26. package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
  27. package/dist/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
  28. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
  29. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
  30. package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
  31. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
  32. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
  33. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
  34. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
  35. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
  36. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
  37. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
  38. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
  39. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
  40. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
  41. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
  42. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
  43. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
  44. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
  45. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
  46. package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
  47. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
  48. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
  49. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
  50. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
  51. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
  52. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
  53. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
  54. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
  55. package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
  56. package/dist/frameworks/react/add-ons/shopify/info.json +104 -0
  57. package/dist/frameworks/react/add-ons/shopify/package.json +6 -0
  58. package/dist/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
  59. package/dist/frameworks/react/examples/shopify-storefront/README.md +39 -0
  60. package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
  61. package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
  62. package/dist/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
  63. package/dist/frameworks/react/examples/shopify-storefront/info.json +18 -0
  64. package/dist/frameworks/react/examples/shopify-storefront/package.json +3 -0
  65. package/dist/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
  66. package/package.json +1 -1
  67. package/src/frameworks/react/add-ons/powersync/README.md.ejs +26 -0
  68. package/src/frameworks/react/add-ons/powersync/assets/_dot_env.local.append +3 -0
  69. package/src/frameworks/react/add-ons/powersync/assets/powersync-vite-plugin.ts +17 -0
  70. package/src/frameworks/react/add-ons/powersync/assets/src/integrations/powersync/provider.tsx +26 -0
  71. package/src/frameworks/react/add-ons/powersync/assets/src/lib/powersync/AppSchema.ts +17 -0
  72. package/src/frameworks/react/add-ons/powersync/assets/src/lib/powersync/BackendConnector.ts +52 -0
  73. package/src/frameworks/react/add-ons/powersync/assets/src/routes/demo/powersync.tsx +129 -0
  74. package/src/frameworks/react/add-ons/powersync/info.json +46 -0
  75. package/src/frameworks/react/add-ons/powersync/package.json.ejs +7 -0
  76. package/src/frameworks/react/add-ons/powersync/small-logo.svg +6 -0
  77. package/src/frameworks/react/add-ons/shopify/README.md +86 -0
  78. package/src/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
  79. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
  80. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
  81. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
  82. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
  83. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
  84. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
  85. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
  86. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
  87. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
  88. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
  89. package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
  90. package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
  91. package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
  92. package/src/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
  93. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
  94. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
  95. package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
  96. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
  97. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
  98. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
  99. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
  100. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
  101. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
  102. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
  103. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
  104. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
  105. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
  106. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
  107. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
  108. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
  109. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
  110. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
  111. package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
  112. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
  113. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
  114. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
  115. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
  116. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
  117. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
  118. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
  119. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
  120. package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
  121. package/src/frameworks/react/add-ons/shopify/info.json +104 -0
  122. package/src/frameworks/react/add-ons/shopify/package.json +6 -0
  123. package/src/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
  124. package/src/frameworks/react/examples/shopify-storefront/README.md +39 -0
  125. package/src/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
  126. package/src/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
  127. package/src/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
  128. package/src/frameworks/react/examples/shopify-storefront/info.json +18 -0
  129. package/src/frameworks/react/examples/shopify-storefront/package.json +3 -0
  130. package/src/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
@@ -0,0 +1,276 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import type {
3
+ Image as StorefrontImage,
4
+ MoneyV2,
5
+ } from '@shopify/hydrogen-react/storefront-api-types'
6
+
7
+ import {
8
+ addToCart,
9
+ applyDiscountCode,
10
+ getCart,
11
+ removeCartLine,
12
+ removeDiscountCode,
13
+ updateCartLine,
14
+ } from '#/server/shopify/cart.functions'
15
+ import type { CartDetail, CartLineDetail } from '#/lib/shopify/queries'
16
+
17
+ /**
18
+ * Shared React Query key for the current user's cart.
19
+ *
20
+ * The cart ID lives in an httpOnly cookie on the server, so the client never
21
+ * needs to know it — a single cache key is enough. Route loaders can prefetch
22
+ * into this key so the first render already has the data.
23
+ */
24
+ export const CART_QUERY_KEY = ['shopify', 'cart'] as const
25
+
26
+ const CART_MUTATION_KEY = ['shopify', 'cart', 'mutate'] as const
27
+
28
+ /**
29
+ * Explicit in-flight counter. We don't rely on `queryClient.isMutating()`
30
+ * because its semantics at `onSettled` time vary across React Query versions.
31
+ * A module-level counter is unambiguous: increment in onMutate, decrement in
32
+ * onSettled, invalidate when the count hits zero.
33
+ *
34
+ * @see https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query
35
+ */
36
+ let cartMutationsInFlight = 0
37
+
38
+ function trackMutationStart() {
39
+ cartMutationsInFlight++
40
+ }
41
+
42
+ function settleWhenIdle(qc: ReturnType<typeof useQueryClient>) {
43
+ cartMutationsInFlight = Math.max(0, cartMutationsInFlight - 1)
44
+ if (cartMutationsInFlight === 0) {
45
+ return qc.invalidateQueries({ queryKey: CART_QUERY_KEY })
46
+ }
47
+ }
48
+
49
+ export function useCart() {
50
+ const query = useQuery<CartDetail | null>({
51
+ queryKey: CART_QUERY_KEY,
52
+ queryFn: () => getCart(),
53
+ staleTime: 30_000,
54
+ })
55
+
56
+ return {
57
+ cart: query.data ?? null,
58
+ isLoading: query.isLoading,
59
+ isFetching: query.isFetching,
60
+ isError: query.isError,
61
+ error: query.error,
62
+ refetch: query.refetch,
63
+ totalQuantity: query.data?.totalQuantity ?? 0,
64
+ }
65
+ }
66
+
67
+ type AddToCartLineSnapshot = {
68
+ productTitle: string
69
+ productHandle: string
70
+ variantTitle: string
71
+ price: Pick<MoneyV2, 'amount' | 'currencyCode'>
72
+ image: Pick<StorefrontImage, 'url' | 'altText' | 'width' | 'height'> | null
73
+ selectedOptions: Array<{ name: string; value: string }>
74
+ }
75
+
76
+ type AddToCartInput = {
77
+ variantId: string
78
+ quantity?: number
79
+ /** Product snapshot for optimistic line rendering. */
80
+ line?: AddToCartLineSnapshot
81
+ }
82
+
83
+ export function useAddToCart() {
84
+ const qc = useQueryClient()
85
+
86
+ return useMutation({
87
+ mutationKey: CART_MUTATION_KEY,
88
+ mutationFn: (input: AddToCartInput) =>
89
+ addToCart({
90
+ data: { variantId: input.variantId, quantity: input.quantity ?? 1 },
91
+ }),
92
+
93
+ onMutate: async (input) => {
94
+ trackMutationStart()
95
+ const quantity = input.quantity ?? 1
96
+ await qc.cancelQueries({ queryKey: CART_QUERY_KEY })
97
+ const previous = qc.getQueryData<CartDetail | null>(CART_QUERY_KEY)
98
+
99
+ if (previous && input.line) {
100
+ const snap = input.line
101
+ const existingIdx = previous.lines.nodes.findIndex(
102
+ (l) => l.merchandise.id === input.variantId,
103
+ )
104
+
105
+ let nextLines: CartDetail['lines']['nodes']
106
+ if (existingIdx >= 0) {
107
+ nextLines = previous.lines.nodes.map((l, i) =>
108
+ i === existingIdx ? { ...l, quantity: l.quantity + quantity } : l,
109
+ )
110
+ } else {
111
+ const lineTotal = String(Number(snap.price.amount) * quantity)
112
+ nextLines = [
113
+ {
114
+ id: `optimistic-${Date.now()}`,
115
+ quantity,
116
+ merchandise: {
117
+ id: input.variantId,
118
+ title: snap.variantTitle,
119
+ availableForSale: true,
120
+ selectedOptions: snap.selectedOptions,
121
+ price: snap.price,
122
+ image: snap.image,
123
+ product: {
124
+ handle: snap.productHandle,
125
+ title: snap.productTitle,
126
+ },
127
+ },
128
+ cost: {
129
+ totalAmount: {
130
+ amount: lineTotal,
131
+ currencyCode: snap.price.currencyCode,
132
+ },
133
+ },
134
+ } satisfies CartLineDetail,
135
+ ...previous.lines.nodes,
136
+ ]
137
+ }
138
+
139
+ qc.setQueryData<CartDetail | null>(CART_QUERY_KEY, {
140
+ ...previous,
141
+ totalQuantity: nextLines.reduce((s, l) => s + l.quantity, 0),
142
+ lines: { ...previous.lines, nodes: nextLines },
143
+ })
144
+ } else if (previous) {
145
+ qc.setQueryData<CartDetail | null>(CART_QUERY_KEY, {
146
+ ...previous,
147
+ totalQuantity: (previous.totalQuantity ?? 0) + quantity,
148
+ })
149
+ }
150
+
151
+ return { previous }
152
+ },
153
+
154
+ onError: (_err, _input, ctx) => {
155
+ if (ctx?.previous !== undefined)
156
+ qc.setQueryData(CART_QUERY_KEY, ctx.previous)
157
+ },
158
+
159
+ onSuccess: (cart) => {
160
+ qc.setQueryData(CART_QUERY_KEY, cart)
161
+ },
162
+
163
+ onSettled: () => settleWhenIdle(qc),
164
+ })
165
+ }
166
+
167
+ export function useUpdateCartLine() {
168
+ const qc = useQueryClient()
169
+ return useMutation({
170
+ mutationKey: CART_MUTATION_KEY,
171
+ mutationFn: (input: { lineId: string; quantity: number }) =>
172
+ updateCartLine({ data: input }),
173
+
174
+ onMutate: async (input) => {
175
+ trackMutationStart()
176
+ await qc.cancelQueries({ queryKey: CART_QUERY_KEY })
177
+ const previous = qc.getQueryData<CartDetail | null>(CART_QUERY_KEY)
178
+ if (previous) {
179
+ const nextLines = previous.lines.nodes.map((line) =>
180
+ line.id === input.lineId
181
+ ? { ...line, quantity: input.quantity }
182
+ : line,
183
+ )
184
+ const nextQty = nextLines.reduce((sum, line) => sum + line.quantity, 0)
185
+ qc.setQueryData<CartDetail | null>(CART_QUERY_KEY, {
186
+ ...previous,
187
+ totalQuantity: nextQty,
188
+ lines: { ...previous.lines, nodes: nextLines },
189
+ })
190
+ }
191
+ return { previous }
192
+ },
193
+
194
+ onError: (_err, _input, ctx) => {
195
+ if (ctx?.previous !== undefined)
196
+ qc.setQueryData(CART_QUERY_KEY, ctx.previous)
197
+ },
198
+
199
+ onSettled: () => settleWhenIdle(qc),
200
+ })
201
+ }
202
+
203
+ export function useRemoveCartLine() {
204
+ const qc = useQueryClient()
205
+ return useMutation({
206
+ mutationKey: CART_MUTATION_KEY,
207
+ mutationFn: (input: { lineId: string }) => removeCartLine({ data: input }),
208
+
209
+ onMutate: async (input) => {
210
+ trackMutationStart()
211
+ await qc.cancelQueries({ queryKey: CART_QUERY_KEY })
212
+ const previous = qc.getQueryData<CartDetail | null>(CART_QUERY_KEY)
213
+ if (previous) {
214
+ const nextLines = previous.lines.nodes.filter(
215
+ (line) => line.id !== input.lineId,
216
+ )
217
+ const nextQty = nextLines.reduce((sum, line) => sum + line.quantity, 0)
218
+ qc.setQueryData<CartDetail | null>(CART_QUERY_KEY, {
219
+ ...previous,
220
+ totalQuantity: nextQty,
221
+ lines: { ...previous.lines, nodes: nextLines },
222
+ })
223
+ }
224
+ return { previous }
225
+ },
226
+
227
+ onError: (_err, _input, ctx) => {
228
+ if (ctx?.previous !== undefined)
229
+ qc.setQueryData(CART_QUERY_KEY, ctx.previous)
230
+ },
231
+
232
+ onSettled: () => settleWhenIdle(qc),
233
+ })
234
+ }
235
+
236
+ export function useApplyDiscountCode() {
237
+ const qc = useQueryClient()
238
+ return useMutation({
239
+ mutationKey: CART_MUTATION_KEY,
240
+ mutationFn: (input: { code: string }) =>
241
+ applyDiscountCode({ data: { code: input.code } }),
242
+ onMutate: async () => {
243
+ trackMutationStart()
244
+ await qc.cancelQueries({ queryKey: CART_QUERY_KEY })
245
+ },
246
+ onSuccess: (cart) => {
247
+ qc.setQueryData(CART_QUERY_KEY, cart)
248
+ },
249
+ onSettled: () => settleWhenIdle(qc),
250
+ })
251
+ }
252
+
253
+ export function useRemoveDiscountCode() {
254
+ const qc = useQueryClient()
255
+ return useMutation({
256
+ mutationKey: CART_MUTATION_KEY,
257
+ mutationFn: () => removeDiscountCode(),
258
+ onMutate: async () => {
259
+ trackMutationStart()
260
+ await qc.cancelQueries({ queryKey: CART_QUERY_KEY })
261
+ const previous = qc.getQueryData<CartDetail | null>(CART_QUERY_KEY)
262
+ if (previous) {
263
+ qc.setQueryData<CartDetail | null>(CART_QUERY_KEY, {
264
+ ...previous,
265
+ discountCodes: [],
266
+ })
267
+ }
268
+ return { previous }
269
+ },
270
+ onError: (_err, _input, ctx) => {
271
+ if (ctx?.previous !== undefined)
272
+ qc.setQueryData(CART_QUERY_KEY, ctx.previous)
273
+ },
274
+ onSettled: () => settleWhenIdle(qc),
275
+ })
276
+ }
@@ -0,0 +1,22 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { useQuery } from '@tanstack/react-query'
3
+
4
+ import { getCustomer } from '#/server/shopify/customer.functions'
5
+ import type { CustomerProfile } from '#/lib/shopify/customer-queries'
6
+
7
+ export const CUSTOMER_QUERY_KEY = ['shopify', 'customer'] as const
8
+
9
+ export function useCustomer() {
10
+ const query = useQuery<CustomerProfile | null>({
11
+ queryKey: CUSTOMER_QUERY_KEY,
12
+ queryFn: () => getCustomer(),
13
+ staleTime: 60_000,
14
+ })
15
+ return {
16
+ customer: query.data ?? null,
17
+ isLoading: query.isLoading,
18
+ isFetching: query.isFetching,
19
+ isError: query.isError,
20
+ error: query.error,
21
+ }
22
+ }
@@ -0,0 +1,37 @@
1
+ import { Link } from '@tanstack/react-router'
2
+
3
+ import { useCart } from '#/hooks/use-cart'
4
+
5
+ export default function ShopifyHeaderCart() {
6
+ const { totalQuantity } = useCart()
7
+ return (
8
+ <Link
9
+ to="/shop/cart"
10
+ className="relative inline-flex items-center justify-center rounded-xl p-2 transition hover:bg-[var(--storefront-line)]/40"
11
+ aria-label={`Cart, ${totalQuantity} item${totalQuantity === 1 ? '' : 's'}`}
12
+ >
13
+ <svg
14
+ viewBox="0 0 24 24"
15
+ width="22"
16
+ height="22"
17
+ fill="none"
18
+ stroke="currentColor"
19
+ strokeWidth="1.6"
20
+ aria-hidden="true"
21
+ >
22
+ <path
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ d="M3 4h2.5l1.5 12.5a1.5 1.5 0 0 0 1.5 1.3h9.4a1.5 1.5 0 0 0 1.5-1.2L21 7H6"
26
+ />
27
+ <circle cx="9" cy="20.5" r="1.2" />
28
+ <circle cx="18" cy="20.5" r="1.2" />
29
+ </svg>
30
+ {totalQuantity > 0 && (
31
+ <span className="absolute -right-0.5 -top-0.5 inline-flex min-w-[1.1rem] items-center justify-center rounded-full bg-[var(--storefront-accent,#0a0a0a)] px-1 text-[10px] font-semibold leading-[1.1rem] text-[var(--storefront-accent-fg,#fff)]">
32
+ {totalQuantity}
33
+ </span>
34
+ )}
35
+ </Link>
36
+ )
37
+ }
@@ -0,0 +1,228 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ /**
3
+ * GraphQL queries for the Shopify Customer Account API.
4
+ *
5
+ * The Customer Account API has its own schema, distinct from the Storefront
6
+ * API. We hand-type the slices we use; if the surface area grows we can wire
7
+ * up @shopify/customer-account-api-codegen later.
8
+ */
9
+
10
+ export const CUSTOMER_QUERY = /* GraphQL */ `
11
+ query Customer {
12
+ customer {
13
+ id
14
+ firstName
15
+ lastName
16
+ emailAddress {
17
+ emailAddress
18
+ }
19
+ phoneNumber {
20
+ phoneNumber
21
+ }
22
+ defaultAddress {
23
+ id
24
+ firstName
25
+ lastName
26
+ address1
27
+ address2
28
+ city
29
+ zoneCode
30
+ zip
31
+ territoryCode
32
+ phoneNumber
33
+ }
34
+ }
35
+ }
36
+ `
37
+
38
+ export type CustomerProfile = {
39
+ id: string
40
+ firstName: string | null
41
+ lastName: string | null
42
+ emailAddress: { emailAddress: string } | null
43
+ phoneNumber: { phoneNumber: string } | null
44
+ defaultAddress: CustomerAddress | null
45
+ }
46
+
47
+ export type CustomerQueryResult = { customer: CustomerProfile }
48
+
49
+ export type CustomerAddress = {
50
+ id: string
51
+ firstName: string | null
52
+ lastName: string | null
53
+ address1: string | null
54
+ address2: string | null
55
+ city: string | null
56
+ zoneCode: string | null
57
+ zip: string | null
58
+ territoryCode: string | null
59
+ phoneNumber: string | null
60
+ }
61
+
62
+ export const CUSTOMER_ADDRESSES_QUERY = /* GraphQL */ `
63
+ query CustomerAddresses($first: Int!) {
64
+ customer {
65
+ addresses(first: $first) {
66
+ nodes {
67
+ id
68
+ firstName
69
+ lastName
70
+ address1
71
+ address2
72
+ city
73
+ zoneCode
74
+ zip
75
+ territoryCode
76
+ phoneNumber
77
+ }
78
+ }
79
+ defaultAddress {
80
+ id
81
+ }
82
+ }
83
+ }
84
+ `
85
+
86
+ export type CustomerAddressesQueryResult = {
87
+ customer: {
88
+ addresses: { nodes: Array<CustomerAddress> }
89
+ defaultAddress: { id: string } | null
90
+ }
91
+ }
92
+
93
+ export const ORDERS_QUERY = /* GraphQL */ `
94
+ query CustomerOrders($first: Int!, $after: String) {
95
+ customer {
96
+ orders(first: $first, after: $after, sortKey: PROCESSED_AT, reverse: true) {
97
+ nodes {
98
+ id
99
+ name
100
+ processedAt
101
+ financialStatus
102
+ fulfillmentStatus
103
+ totalPrice {
104
+ amount
105
+ currencyCode
106
+ }
107
+ }
108
+ pageInfo {
109
+ hasNextPage
110
+ endCursor
111
+ }
112
+ }
113
+ }
114
+ }
115
+ `
116
+
117
+ export type OrderListItem = {
118
+ id: string
119
+ name: string
120
+ processedAt: string
121
+ financialStatus: string | null
122
+ fulfillmentStatus: string | null
123
+ totalPrice: { amount: string; currencyCode: string }
124
+ }
125
+
126
+ export type OrdersQueryResult = {
127
+ customer: {
128
+ orders: {
129
+ nodes: Array<OrderListItem>
130
+ pageInfo: { hasNextPage: boolean; endCursor: string | null }
131
+ }
132
+ }
133
+ }
134
+
135
+ export const ORDER_QUERY = /* GraphQL */ `
136
+ query Order($id: ID!) {
137
+ order(id: $id) {
138
+ id
139
+ name
140
+ processedAt
141
+ financialStatus
142
+ fulfillmentStatus
143
+ totalPrice {
144
+ amount
145
+ currencyCode
146
+ }
147
+ subtotal {
148
+ amount
149
+ currencyCode
150
+ }
151
+ totalShipping {
152
+ amount
153
+ currencyCode
154
+ }
155
+ totalTax {
156
+ amount
157
+ currencyCode
158
+ }
159
+ shippingAddress {
160
+ firstName
161
+ lastName
162
+ address1
163
+ address2
164
+ city
165
+ zoneCode
166
+ zip
167
+ territoryCode
168
+ }
169
+ lineItems(first: 50) {
170
+ nodes {
171
+ id
172
+ title
173
+ quantity
174
+ variantTitle
175
+ price {
176
+ amount
177
+ currencyCode
178
+ }
179
+ image {
180
+ url
181
+ altText
182
+ width
183
+ height
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ `
190
+
191
+ export type OrderDetailLine = {
192
+ id: string
193
+ title: string
194
+ quantity: number
195
+ variantTitle: string | null
196
+ price: { amount: string; currencyCode: string }
197
+ image: {
198
+ url: string
199
+ altText: string | null
200
+ width: number | null
201
+ height: number | null
202
+ } | null
203
+ }
204
+
205
+ export type OrderDetail = {
206
+ id: string
207
+ name: string
208
+ processedAt: string
209
+ financialStatus: string | null
210
+ fulfillmentStatus: string | null
211
+ totalPrice: { amount: string; currencyCode: string }
212
+ subtotal: { amount: string; currencyCode: string } | null
213
+ totalShipping: { amount: string; currencyCode: string } | null
214
+ totalTax: { amount: string; currencyCode: string } | null
215
+ shippingAddress: {
216
+ firstName: string | null
217
+ lastName: string | null
218
+ address1: string | null
219
+ address2: string | null
220
+ city: string | null
221
+ zoneCode: string | null
222
+ zip: string | null
223
+ territoryCode: string | null
224
+ } | null
225
+ lineItems: { nodes: Array<OrderDetailLine> }
226
+ }
227
+
228
+ export type OrderQueryResult = { order: OrderDetail | null }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Browser-safe formatting helpers for Shopify Storefront API responses.
3
+ * Framework-free — no React, no provider context required.
4
+ */
5
+
6
+ export function formatMoney(amount: string | number, currencyCode: string) {
7
+ return new Intl.NumberFormat(undefined, {
8
+ style: 'currency',
9
+ currency: currencyCode,
10
+ minimumFractionDigits: 0,
11
+ maximumFractionDigits: 0,
12
+ }).format(typeof amount === 'string' ? Number(amount) : amount)
13
+ }
14
+
15
+ type ShopifyImageOptions = {
16
+ width?: number
17
+ height?: number
18
+ format?: 'webp' | 'jpg' | 'png'
19
+ crop?: 'center' | 'top' | 'bottom' | 'left' | 'right'
20
+ }
21
+
22
+ /**
23
+ * Append Shopify CDN transform parameters to a product image URL.
24
+ * Shopify's CDN serves resized/reformatted versions automatically.
25
+ */
26
+ export function shopifyImageUrl(url: string, opts: ShopifyImageOptions = {}) {
27
+ const u = new URL(url)
28
+ if (opts.width) u.searchParams.set('width', String(opts.width))
29
+ if (opts.height) u.searchParams.set('height', String(opts.height))
30
+ if (opts.format) u.searchParams.set('format', opts.format)
31
+ if (opts.crop) u.searchParams.set('crop', opts.crop)
32
+ return u.toString()
33
+ }