@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,244 @@
1
+ import { createServerFn } from '@tanstack/react-start'
2
+ import { setResponseHeaders } from '@tanstack/react-start/server'
3
+ import * as v from 'valibot'
4
+
5
+ import { shopifyServerFetch } from '#/server/shopify/storefront-client'
6
+ import {
7
+ COLLECTION_QUERY,
8
+ COLLECTIONS_QUERY,
9
+ PAGE_QUERY,
10
+ PRODUCT_QUERY,
11
+ PRODUCTS_QUERY,
12
+ SEARCH_QUERY,
13
+ SHOP_POLICIES_QUERY,
14
+ SHOP_QUERY,
15
+ flattenPolicies,
16
+ type CollectionDetail,
17
+ type CollectionListItem,
18
+ type CollectionQueryResult,
19
+ type CollectionsQueryResult,
20
+ type PageDetail,
21
+ type PageQueryResult,
22
+ type PolicySummary,
23
+ type ProductDetail,
24
+ type ProductListPage,
25
+ type ProductQueryResult,
26
+ type ProductsQueryResult,
27
+ type ProductsQueryVariables,
28
+ type SearchQueryResult,
29
+ type ShopPoliciesQueryResult,
30
+ type ShopPolicy,
31
+ type ShopQueryResult,
32
+ } from '#/lib/shopify/queries'
33
+
34
+ /**
35
+ * Edge-cache catalog responses for a few minutes. Catalog data doesn't change
36
+ * often. CDN-Cache-Control is the runtime-portable header (Cloudflare, Vercel,
37
+ * Netlify all read it); we also set a conservative public Cache-Control so
38
+ * intermediaries don't cache aggressively without explicit opt-in.
39
+ */
40
+ function setBrowseCacheHeaders() {
41
+ setResponseHeaders(
42
+ new Headers({
43
+ 'Cache-Control': 'public, max-age=0, must-revalidate',
44
+ 'CDN-Cache-Control': 'public, max-age=300, stale-while-revalidate=600',
45
+ }),
46
+ )
47
+ }
48
+
49
+ const productSortKeys = [
50
+ 'BEST_SELLING',
51
+ 'CREATED_AT',
52
+ 'ID',
53
+ 'PRICE',
54
+ 'PRODUCT_TYPE',
55
+ 'RELEVANCE',
56
+ 'TITLE',
57
+ 'UPDATED_AT',
58
+ 'VENDOR',
59
+ ] as const
60
+
61
+ const collectionSortKeys = [
62
+ 'BEST_SELLING',
63
+ 'COLLECTION_DEFAULT',
64
+ 'CREATED',
65
+ 'ID',
66
+ 'MANUAL',
67
+ 'PRICE',
68
+ 'RELEVANCE',
69
+ 'TITLE',
70
+ ] as const
71
+
72
+ export const getShop = createServerFn({ method: 'GET' }).handler(
73
+ async (): Promise<ShopQueryResult['shop']> => {
74
+ setBrowseCacheHeaders()
75
+ const data = await shopifyServerFetch<ShopQueryResult>({ query: SHOP_QUERY })
76
+ return data.shop
77
+ },
78
+ )
79
+
80
+ export const getProducts = createServerFn({ method: 'POST' })
81
+ .inputValidator(
82
+ v.object({
83
+ first: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 24),
84
+ after: v.optional(v.nullable(v.string())),
85
+ sortKey: v.optional(v.picklist(productSortKeys)),
86
+ reverse: v.optional(v.boolean()),
87
+ }),
88
+ )
89
+ .handler(async ({ data }): Promise<ProductListPage> => {
90
+ setBrowseCacheHeaders()
91
+ const result = await shopifyServerFetch<
92
+ ProductsQueryResult,
93
+ ProductsQueryVariables
94
+ >({
95
+ query: PRODUCTS_QUERY,
96
+ variables: {
97
+ first: data.first,
98
+ after: data.after ?? null,
99
+ sortKey: data.sortKey ?? null,
100
+ reverse: data.reverse ?? null,
101
+ },
102
+ })
103
+ return result.products
104
+ })
105
+
106
+ export const getCollections = createServerFn({ method: 'GET' }).handler(
107
+ async (): Promise<Array<CollectionListItem>> => {
108
+ setBrowseCacheHeaders()
109
+ const result = await shopifyServerFetch<
110
+ CollectionsQueryResult,
111
+ { first: number }
112
+ >({
113
+ query: COLLECTIONS_QUERY,
114
+ variables: { first: 50 },
115
+ })
116
+ return result.collections.nodes
117
+ },
118
+ )
119
+
120
+ export const getProduct = createServerFn({ method: 'POST' })
121
+ .inputValidator(v.object({ handle: v.string() }))
122
+ .handler(async ({ data }): Promise<ProductDetail | null> => {
123
+ setBrowseCacheHeaders()
124
+ const result = await shopifyServerFetch<
125
+ ProductQueryResult,
126
+ { handle: string }
127
+ >({
128
+ query: PRODUCT_QUERY,
129
+ variables: { handle: data.handle },
130
+ })
131
+ return result.product
132
+ })
133
+
134
+ export const getCollection = createServerFn({ method: 'POST' })
135
+ .inputValidator(
136
+ v.object({
137
+ handle: v.string(),
138
+ first: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 24),
139
+ after: v.optional(v.nullable(v.string())),
140
+ sortKey: v.optional(v.picklist(collectionSortKeys)),
141
+ reverse: v.optional(v.boolean()),
142
+ }),
143
+ )
144
+ .handler(async ({ data }): Promise<CollectionDetail | null> => {
145
+ setBrowseCacheHeaders()
146
+ const result = await shopifyServerFetch<
147
+ CollectionQueryResult,
148
+ {
149
+ handle: string
150
+ first: number
151
+ after: string | null
152
+ sortKey: (typeof collectionSortKeys)[number] | null
153
+ reverse: boolean | null
154
+ }
155
+ >({
156
+ query: COLLECTION_QUERY,
157
+ variables: {
158
+ handle: data.handle,
159
+ first: data.first,
160
+ after: data.after ?? null,
161
+ sortKey: data.sortKey ?? null,
162
+ reverse: data.reverse ?? null,
163
+ },
164
+ })
165
+ return result.collection
166
+ })
167
+
168
+ export const getPage = createServerFn({ method: 'POST' })
169
+ .inputValidator(v.object({ handle: v.string() }))
170
+ .handler(async ({ data }): Promise<PageDetail | null> => {
171
+ setBrowseCacheHeaders()
172
+ const result = await shopifyServerFetch<
173
+ PageQueryResult,
174
+ { handle: string }
175
+ >({
176
+ query: PAGE_QUERY,
177
+ variables: { handle: data.handle },
178
+ })
179
+ return result.page
180
+ })
181
+
182
+ export const getShopPolicies = createServerFn({ method: 'GET' }).handler(
183
+ async (): Promise<Array<PolicySummary>> => {
184
+ setBrowseCacheHeaders()
185
+ const result = await shopifyServerFetch<ShopPoliciesQueryResult>({
186
+ query: SHOP_POLICIES_QUERY,
187
+ })
188
+ return flattenPolicies(result.shop)
189
+ },
190
+ )
191
+
192
+ export const getShopPolicy = createServerFn({ method: 'POST' })
193
+ .inputValidator(v.object({ handle: v.string() }))
194
+ .handler(
195
+ async ({ data }): Promise<{ title: string; body: string; handle: string } | null> => {
196
+ setBrowseCacheHeaders()
197
+ const result = await shopifyServerFetch<ShopPoliciesQueryResult>({
198
+ query: SHOP_POLICIES_QUERY,
199
+ })
200
+ const all = [
201
+ result.shop.privacyPolicy,
202
+ result.shop.refundPolicy,
203
+ result.shop.termsOfService,
204
+ result.shop.shippingPolicy,
205
+ ].filter((p): p is NonNullable<ShopPolicy> => p !== null)
206
+ return all.find((p) => p.handle === data.handle) ?? null
207
+ },
208
+ )
209
+
210
+ export const searchProducts = createServerFn({ method: 'POST' })
211
+ .inputValidator(
212
+ v.object({
213
+ query: v.pipe(v.string(), v.minLength(1)),
214
+ first: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 24),
215
+ after: v.optional(v.nullable(v.string())),
216
+ }),
217
+ )
218
+ .handler(
219
+ async ({
220
+ data,
221
+ }): Promise<{
222
+ totalCount: number
223
+ pageInfo: { hasNextPage: boolean; endCursor: string | null }
224
+ products: ProductListPage['nodes']
225
+ }> => {
226
+ setBrowseCacheHeaders()
227
+ const result = await shopifyServerFetch<
228
+ SearchQueryResult,
229
+ { query: string; first: number; after: string | null }
230
+ >({
231
+ query: SEARCH_QUERY,
232
+ variables: {
233
+ query: data.query,
234
+ first: data.first,
235
+ after: data.after ?? null,
236
+ },
237
+ })
238
+ return {
239
+ totalCount: result.search.totalCount,
240
+ pageInfo: result.search.pageInfo,
241
+ products: result.search.nodes,
242
+ }
243
+ },
244
+ )
@@ -0,0 +1,29 @@
1
+ import {
2
+ deleteCookie,
3
+ getCookie,
4
+ setCookie,
5
+ } from '@tanstack/react-start/server'
6
+
7
+ export const CART_COOKIE_NAME = 'tanstack_cart_id'
8
+
9
+ const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365
10
+
11
+ const cartCookieOptions = () => ({
12
+ path: '/' as const,
13
+ maxAge: ONE_YEAR_SECONDS,
14
+ sameSite: 'lax' as const,
15
+ httpOnly: true,
16
+ secure: import.meta.env.PROD,
17
+ })
18
+
19
+ export function getCartId() {
20
+ return getCookie(CART_COOKIE_NAME)
21
+ }
22
+
23
+ export function setCartId(id: string) {
24
+ setCookie(CART_COOKIE_NAME, id, cartCookieOptions())
25
+ }
26
+
27
+ export function clearCartId() {
28
+ deleteCookie(CART_COOKIE_NAME, { path: '/' })
29
+ }
@@ -0,0 +1,99 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import {
3
+ buildSession,
4
+ getDiscovery,
5
+ refreshTokens,
6
+ signSession,
7
+ verifySession,
8
+ type CustomerSession,
9
+ } from '#/server/shopify/oauth'
10
+ import {
11
+ clearSessionCookie,
12
+ getSessionCookie,
13
+ setSessionCookie,
14
+ } from '#/server/shopify/customer-cookies'
15
+
16
+ const REFRESH_LEEWAY_MS = 60_000 // refresh if expiring within 60s
17
+
18
+ class CustomerAuthError extends Error {
19
+ constructor(message: string) {
20
+ super(message)
21
+ this.name = 'CustomerAuthError'
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Read the current customer session, refreshing tokens if they're close to
27
+ * expiry. Returns null when the user is not signed in.
28
+ */
29
+ export async function getCustomerSession(): Promise<CustomerSession | null> {
30
+ const cookie = getSessionCookie()
31
+ if (!cookie) return null
32
+
33
+ const session = await verifySession(cookie)
34
+ if (!session) {
35
+ clearSessionCookie()
36
+ return null
37
+ }
38
+
39
+ if (session.expiresAt - REFRESH_LEEWAY_MS > Date.now()) {
40
+ return session
41
+ }
42
+
43
+ // Token expired or near-expiry — refresh.
44
+ try {
45
+ const refreshed = await refreshTokens(session.refreshToken)
46
+ const next = buildSession(refreshed)
47
+ setSessionCookie(await signSession(next))
48
+ return next
49
+ } catch {
50
+ clearSessionCookie()
51
+ return null
52
+ }
53
+ }
54
+
55
+ export async function requireCustomerSession(): Promise<CustomerSession> {
56
+ const session = await getCustomerSession()
57
+ if (!session) throw new CustomerAuthError('Not signed in')
58
+ return session
59
+ }
60
+
61
+ type CustomerFetchInput<TVariables> = {
62
+ query: string
63
+ variables?: TVariables
64
+ session: CustomerSession
65
+ }
66
+
67
+ type CustomerResponse<TData> = {
68
+ data?: TData
69
+ errors?: Array<{ message: string }>
70
+ }
71
+
72
+ /**
73
+ * Authenticated GraphQL fetch against the Customer Account API. Caller must
74
+ * pass a verified session (obtained via getCustomerSession).
75
+ */
76
+ export async function customerFetch<TData, TVariables = Record<string, unknown>>(
77
+ input: CustomerFetchInput<TVariables>,
78
+ ): Promise<TData> {
79
+ const discovery = await getDiscovery()
80
+ const res = await fetch(discovery.graphqlEndpoint, {
81
+ method: 'POST',
82
+ headers: {
83
+ 'Content-Type': 'application/json',
84
+ Authorization: input.session.accessToken,
85
+ },
86
+ body: JSON.stringify({ query: input.query, variables: input.variables }),
87
+ })
88
+ if (!res.ok) {
89
+ throw new Error(
90
+ `Customer Account API error: ${res.status} ${res.statusText}`,
91
+ )
92
+ }
93
+ const json = (await res.json()) as CustomerResponse<TData>
94
+ if (json.errors?.length) {
95
+ throw new Error(json.errors.map((e) => e.message).join('\n'))
96
+ }
97
+ if (!json.data) throw new Error('Customer Account API returned no data.')
98
+ return json.data
99
+ }
@@ -0,0 +1,49 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import {
3
+ deleteCookie,
4
+ getCookie,
5
+ setCookie,
6
+ } from '@tanstack/react-start/server'
7
+
8
+ const SESSION_COOKIE = 'shopify_customer_session'
9
+ const FLIGHT_COOKIE = 'shopify_oauth_state'
10
+
11
+ const sessionCookieOptions = () => ({
12
+ path: '/' as const,
13
+ maxAge: 60 * 60 * 24 * 30, // 30 days
14
+ sameSite: 'lax' as const,
15
+ httpOnly: true,
16
+ secure: import.meta.env.PROD,
17
+ })
18
+
19
+ const flightCookieOptions = () => ({
20
+ path: '/' as const,
21
+ maxAge: 60 * 10, // 10 min
22
+ sameSite: 'lax' as const,
23
+ httpOnly: true,
24
+ secure: import.meta.env.PROD,
25
+ })
26
+
27
+ export function getSessionCookie() {
28
+ return getCookie(SESSION_COOKIE)
29
+ }
30
+
31
+ export function setSessionCookie(value: string) {
32
+ setCookie(SESSION_COOKIE, value, sessionCookieOptions())
33
+ }
34
+
35
+ export function clearSessionCookie() {
36
+ deleteCookie(SESSION_COOKIE, { path: '/' })
37
+ }
38
+
39
+ export function getFlightCookie() {
40
+ return getCookie(FLIGHT_COOKIE)
41
+ }
42
+
43
+ export function setFlightCookie(value: string) {
44
+ setCookie(FLIGHT_COOKIE, value, flightCookieOptions())
45
+ }
46
+
47
+ export function clearFlightCookie() {
48
+ deleteCookie(FLIGHT_COOKIE, { path: '/' })
49
+ }
@@ -0,0 +1,168 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { createServerFn } from '@tanstack/react-start'
3
+ import { setResponseHeaders } from '@tanstack/react-start/server'
4
+ import * as v from 'valibot'
5
+
6
+ import {
7
+ customerFetch,
8
+ getCustomerSession,
9
+ requireCustomerSession,
10
+ } from '#/server/shopify/customer-client'
11
+ import {
12
+ clearFlightCookie,
13
+ clearSessionCookie,
14
+ getFlightCookie,
15
+ setFlightCookie,
16
+ setSessionCookie,
17
+ } from '#/server/shopify/customer-cookies'
18
+ import {
19
+ buildAuthorizationUrl,
20
+ buildEndSessionUrl,
21
+ buildSession,
22
+ createPkcePair,
23
+ createState,
24
+ exchangeCodeForTokens,
25
+ signFlightState,
26
+ signSession,
27
+ verifyFlightState,
28
+ } from '#/server/shopify/oauth'
29
+ import {
30
+ CUSTOMER_ADDRESSES_QUERY,
31
+ CUSTOMER_QUERY,
32
+ ORDER_QUERY,
33
+ ORDERS_QUERY,
34
+ type CustomerAddress,
35
+ type CustomerAddressesQueryResult,
36
+ type CustomerProfile,
37
+ type CustomerQueryResult,
38
+ type OrderDetail,
39
+ type OrderListItem,
40
+ type OrderQueryResult,
41
+ type OrdersQueryResult,
42
+ } from '#/lib/shopify/customer-queries'
43
+
44
+ function setPrivateHeaders() {
45
+ setResponseHeaders(
46
+ new Headers({ 'Cache-Control': 'private, no-store, must-revalidate' }),
47
+ )
48
+ }
49
+
50
+ export const startLogin = createServerFn({ method: 'POST' })
51
+ .inputValidator(
52
+ v.object({
53
+ redirectAfter: v.optional(v.string(), '/shop/account'),
54
+ }),
55
+ )
56
+ .handler(async ({ data }): Promise<{ authorizationUrl: string }> => {
57
+ setPrivateHeaders()
58
+ const { codeVerifier, codeChallenge } = await createPkcePair()
59
+ const state = createState()
60
+ const flight = await signFlightState({
61
+ state,
62
+ codeVerifier,
63
+ redirectAfter: data.redirectAfter,
64
+ })
65
+ setFlightCookie(flight)
66
+ const authorizationUrl = await buildAuthorizationUrl({ state, codeChallenge })
67
+ return { authorizationUrl }
68
+ })
69
+
70
+ export const completeLogin = createServerFn({ method: 'POST' })
71
+ .inputValidator(v.object({ code: v.string(), state: v.string() }))
72
+ .handler(async ({ data }): Promise<{ redirectAfter: string }> => {
73
+ setPrivateHeaders()
74
+ const flightCookie = getFlightCookie()
75
+ if (!flightCookie) throw new Error('OAuth flight cookie missing or expired.')
76
+ const flight = await verifyFlightState(flightCookie)
77
+ if (!flight) throw new Error('OAuth flight cookie failed verification.')
78
+ if (flight.state !== data.state) throw new Error('OAuth state mismatch.')
79
+
80
+ const tokens = await exchangeCodeForTokens({
81
+ code: data.code,
82
+ codeVerifier: flight.codeVerifier,
83
+ })
84
+ const session = buildSession(tokens)
85
+ setSessionCookie(await signSession(session))
86
+ clearFlightCookie()
87
+ return { redirectAfter: flight.redirectAfter }
88
+ })
89
+
90
+ export const logout = createServerFn({ method: 'POST' }).handler(
91
+ async (): Promise<{ endSessionUrl: string | null }> => {
92
+ setPrivateHeaders()
93
+ const session = await getCustomerSession()
94
+ clearSessionCookie()
95
+ if (!session) return { endSessionUrl: null }
96
+ return { endSessionUrl: await buildEndSessionUrl(session.idToken) }
97
+ },
98
+ )
99
+
100
+ export const getCustomer = createServerFn({ method: 'GET' }).handler(
101
+ async (): Promise<CustomerProfile | null> => {
102
+ setPrivateHeaders()
103
+ const session = await getCustomerSession()
104
+ if (!session) return null
105
+ const result = await customerFetch<CustomerQueryResult>({
106
+ query: CUSTOMER_QUERY,
107
+ session,
108
+ })
109
+ return result.customer
110
+ },
111
+ )
112
+
113
+ export const getOrders = createServerFn({ method: 'POST' })
114
+ .inputValidator(
115
+ v.object({
116
+ first: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 20),
117
+ after: v.optional(v.nullable(v.string())),
118
+ }),
119
+ )
120
+ .handler(
121
+ async ({
122
+ data,
123
+ }): Promise<{
124
+ nodes: Array<OrderListItem>
125
+ pageInfo: { hasNextPage: boolean; endCursor: string | null }
126
+ }> => {
127
+ setPrivateHeaders()
128
+ const session = await requireCustomerSession()
129
+ const result = await customerFetch<OrdersQueryResult>({
130
+ query: ORDERS_QUERY,
131
+ variables: { first: data.first, after: data.after ?? null },
132
+ session,
133
+ })
134
+ return result.customer.orders
135
+ },
136
+ )
137
+
138
+ export const getOrder = createServerFn({ method: 'POST' })
139
+ .inputValidator(v.object({ id: v.string() }))
140
+ .handler(async ({ data }): Promise<OrderDetail | null> => {
141
+ setPrivateHeaders()
142
+ const session = await requireCustomerSession()
143
+ const result = await customerFetch<OrderQueryResult>({
144
+ query: ORDER_QUERY,
145
+ variables: { id: data.id },
146
+ session,
147
+ })
148
+ return result.order
149
+ })
150
+
151
+ export const getAddresses = createServerFn({ method: 'GET' }).handler(
152
+ async (): Promise<{
153
+ addresses: Array<CustomerAddress>
154
+ defaultAddressId: string | null
155
+ }> => {
156
+ setPrivateHeaders()
157
+ const session = await requireCustomerSession()
158
+ const result = await customerFetch<CustomerAddressesQueryResult>({
159
+ query: CUSTOMER_ADDRESSES_QUERY,
160
+ variables: { first: 50 },
161
+ session,
162
+ })
163
+ return {
164
+ addresses: result.customer.addresses.nodes,
165
+ defaultAddressId: result.customer.defaultAddress?.id ?? null,
166
+ }
167
+ },
168
+ )
@@ -0,0 +1,89 @@
1
+ import * as v from 'valibot'
2
+
3
+ /**
4
+ * Defaults point to Shopify's public Hydrogen demo store so the storefront
5
+ * works on first run with zero setup. Override by setting the matching env
6
+ * vars in .env.local (or the deploy target's env config).
7
+ *
8
+ * Note: we don't rely on Vite's .env loading reaching `process.env` at runtime
9
+ * — different runtimes handle that differently. Reading .env.local through
10
+ * `process.env` works on most adapters; falling back to the demo values keeps
11
+ * the first-run experience unbroken when it doesn't.
12
+ */
13
+ const DEMO_STORE_DOMAIN = 'hydrogen-preview.myshopify.com'
14
+ const DEMO_API_VERSION = '2026-01'
15
+ const DEMO_PUBLIC_TOKEN = '3b580e70970c4528da70c98e097c2fa0'
16
+
17
+ const StorefrontEnvSchema = v.object({
18
+ SHOPIFY_STORE_DOMAIN: v.pipe(v.string(), v.minLength(1)),
19
+ SHOPIFY_STOREFRONT_API_VERSION: v.pipe(v.string(), v.minLength(1)),
20
+ SHOPIFY_PUBLIC_STOREFRONT_TOKEN: v.optional(v.string()),
21
+ SHOPIFY_PRIVATE_STOREFRONT_TOKEN: v.optional(v.string()),
22
+ })
23
+
24
+ const CustomerEnvSchema = v.object({
25
+ SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID: v.pipe(
26
+ v.string(),
27
+ v.minLength(1, 'SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID is required for customer accounts'),
28
+ ),
29
+ SHOPIFY_CUSTOMER_ACCOUNT_SHOP_ID: v.pipe(
30
+ v.string(),
31
+ v.minLength(1, 'SHOPIFY_CUSTOMER_ACCOUNT_SHOP_ID is required for customer accounts'),
32
+ ),
33
+ SHOPIFY_CUSTOMER_ACCOUNT_REDIRECT_URI: v.pipe(
34
+ v.string(),
35
+ v.url('SHOPIFY_CUSTOMER_ACCOUNT_REDIRECT_URI must be a valid URL'),
36
+ ),
37
+ SHOPIFY_SESSION_SECRET: v.pipe(
38
+ v.string(),
39
+ v.minLength(32, 'SHOPIFY_SESSION_SECRET must be at least 32 characters'),
40
+ ),
41
+ })
42
+
43
+ let cachedStorefront: v.InferOutput<typeof StorefrontEnvSchema> | null = null
44
+ let cachedCustomer: v.InferOutput<typeof CustomerEnvSchema> | null = null
45
+
46
+ function readEnv(name: string): string | undefined {
47
+ // process.env on most server runtimes; falls back to undefined on edge
48
+ // workers where process.env doesn't exist.
49
+ if (typeof process !== 'undefined' && process.env) {
50
+ const value = process.env[name]
51
+ if (value && value.length > 0) return value
52
+ }
53
+ return undefined
54
+ }
55
+
56
+ export function getStorefrontEnv() {
57
+ if (cachedStorefront) return cachedStorefront
58
+ cachedStorefront = v.parse(StorefrontEnvSchema, {
59
+ SHOPIFY_STORE_DOMAIN: readEnv('SHOPIFY_STORE_DOMAIN') ?? DEMO_STORE_DOMAIN,
60
+ SHOPIFY_STOREFRONT_API_VERSION:
61
+ readEnv('SHOPIFY_STOREFRONT_API_VERSION') ?? DEMO_API_VERSION,
62
+ SHOPIFY_PUBLIC_STOREFRONT_TOKEN:
63
+ readEnv('SHOPIFY_PUBLIC_STOREFRONT_TOKEN') ?? DEMO_PUBLIC_TOKEN,
64
+ SHOPIFY_PRIVATE_STOREFRONT_TOKEN: readEnv('SHOPIFY_PRIVATE_STOREFRONT_TOKEN'),
65
+ })
66
+ return cachedStorefront
67
+ }
68
+
69
+ export function getCustomerEnv() {
70
+ if (cachedCustomer) return cachedCustomer
71
+ cachedCustomer = v.parse(CustomerEnvSchema, {
72
+ SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID: readEnv('SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID'),
73
+ SHOPIFY_CUSTOMER_ACCOUNT_SHOP_ID: readEnv('SHOPIFY_CUSTOMER_ACCOUNT_SHOP_ID'),
74
+ SHOPIFY_CUSTOMER_ACCOUNT_REDIRECT_URI:
75
+ readEnv('SHOPIFY_CUSTOMER_ACCOUNT_REDIRECT_URI') ??
76
+ 'http://localhost:3000/shop/account/callback',
77
+ SHOPIFY_SESSION_SECRET: readEnv('SHOPIFY_SESSION_SECRET'),
78
+ })
79
+ return cachedCustomer
80
+ }
81
+
82
+ export function isCustomerAccountConfigured() {
83
+ try {
84
+ getCustomerEnv()
85
+ return true
86
+ } catch {
87
+ return false
88
+ }
89
+ }