@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,301 @@
1
+ <% if (addOnOption.shopify.customerAccount !== 'enabled') { ignoreFile(); return; } %>
2
+ import { getCustomerEnv } from '#/server/shopify/env'
3
+
4
+ /**
5
+ * OAuth 2.1 Authorization Code + PKCE flow for the Shopify Customer Account
6
+ * API. We're a public client (browser + Start backend), so no client secret —
7
+ * security comes from the PKCE verifier proof.
8
+ *
9
+ * No usable npm client exists for Customer Account API as of 2026-05; this is
10
+ * a hand-rolled implementation using Web APIs only (portable across Node,
11
+ * Workers, Vercel, Netlify, Bun, Deno).
12
+ */
13
+
14
+ const SCOPE = 'openid email customer-account-api:full'
15
+
16
+ type DiscoveryDocument = {
17
+ authorizationEndpoint: string
18
+ tokenEndpoint: string
19
+ endSessionEndpoint: string
20
+ graphqlEndpoint: string
21
+ }
22
+
23
+ let discoveryCache: DiscoveryDocument | null = null
24
+
25
+ async function fetchDiscovery(): Promise<DiscoveryDocument> {
26
+ if (discoveryCache) return discoveryCache
27
+ const env = getCustomerEnv()
28
+ const base = `https://shopify.com/${env.SHOPIFY_CUSTOMER_ACCOUNT_SHOP_ID}`
29
+ const [oidcRes, customerRes] = await Promise.all([
30
+ fetch(`${base}/.well-known/openid-configuration`),
31
+ fetch(`${base}/.well-known/customer-account-api`),
32
+ ])
33
+ if (!oidcRes.ok || !customerRes.ok) {
34
+ throw new Error(
35
+ `Customer Account discovery failed (${oidcRes.status} / ${customerRes.status}). Verify SHOPIFY_CUSTOMER_ACCOUNT_SHOP_ID.`,
36
+ )
37
+ }
38
+ const oidc = (await oidcRes.json()) as {
39
+ authorization_endpoint: string
40
+ token_endpoint: string
41
+ end_session_endpoint: string
42
+ }
43
+ const customer = (await customerRes.json()) as {
44
+ graphql_api: { endpoint: string }
45
+ }
46
+ discoveryCache = {
47
+ authorizationEndpoint: oidc.authorization_endpoint,
48
+ tokenEndpoint: oidc.token_endpoint,
49
+ endSessionEndpoint: oidc.end_session_endpoint,
50
+ graphqlEndpoint: customer.graphql_api.endpoint,
51
+ }
52
+ return discoveryCache
53
+ }
54
+
55
+ export async function getDiscovery() {
56
+ return fetchDiscovery()
57
+ }
58
+
59
+ /* ─── Base64url + crypto helpers (Web Crypto only) ──────────────────────── */
60
+
61
+ function bufferToBase64Url(buffer: ArrayBuffer): string {
62
+ const bytes = new Uint8Array(buffer)
63
+ let str = ''
64
+ for (let i = 0; i < bytes.byteLength; i++) {
65
+ str += String.fromCharCode(bytes[i]!)
66
+ }
67
+ return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
68
+ }
69
+
70
+ function base64UrlEncode(input: string): string {
71
+ return bufferToBase64Url(new TextEncoder().encode(input).buffer as ArrayBuffer)
72
+ }
73
+
74
+ function base64UrlDecode(input: string): string {
75
+ const padded = input.replace(/-/g, '+').replace(/_/g, '/')
76
+ const padding = (4 - (padded.length % 4)) % 4
77
+ return atob(padded + '='.repeat(padding))
78
+ }
79
+
80
+ function randomBytes(n: number): Uint8Array {
81
+ const arr = new Uint8Array(n)
82
+ crypto.getRandomValues(arr)
83
+ return arr
84
+ }
85
+
86
+ function randomString(byteLength: number): string {
87
+ return bufferToBase64Url(randomBytes(byteLength).buffer as ArrayBuffer)
88
+ }
89
+
90
+ async function sha256(input: string): Promise<ArrayBuffer> {
91
+ return crypto.subtle.digest('SHA-256', new TextEncoder().encode(input))
92
+ }
93
+
94
+ /* ─── PKCE ──────────────────────────────────────────────────────────────── */
95
+
96
+ export type PkcePair = { codeVerifier: string; codeChallenge: string }
97
+
98
+ export async function createPkcePair(): Promise<PkcePair> {
99
+ const codeVerifier = randomString(48)
100
+ const codeChallenge = bufferToBase64Url(await sha256(codeVerifier))
101
+ return { codeVerifier, codeChallenge }
102
+ }
103
+
104
+ export function createState(): string {
105
+ return randomString(24)
106
+ }
107
+
108
+ /* ─── Authorize / token / logout URLs ───────────────────────────────────── */
109
+
110
+ export async function buildAuthorizationUrl(input: {
111
+ state: string
112
+ codeChallenge: string
113
+ }): Promise<string> {
114
+ const env = getCustomerEnv()
115
+ const discovery = await fetchDiscovery()
116
+ const url = new URL(discovery.authorizationEndpoint)
117
+ url.searchParams.set('client_id', env.SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID)
118
+ url.searchParams.set('scope', SCOPE)
119
+ url.searchParams.set('response_type', 'code')
120
+ url.searchParams.set('redirect_uri', env.SHOPIFY_CUSTOMER_ACCOUNT_REDIRECT_URI)
121
+ url.searchParams.set('state', input.state)
122
+ url.searchParams.set('code_challenge', input.codeChallenge)
123
+ url.searchParams.set('code_challenge_method', 'S256')
124
+ return url.toString()
125
+ }
126
+
127
+ type TokenResponse = {
128
+ access_token: string
129
+ refresh_token: string
130
+ id_token: string
131
+ expires_in: number
132
+ token_type: 'Bearer'
133
+ }
134
+
135
+ export async function exchangeCodeForTokens(input: {
136
+ code: string
137
+ codeVerifier: string
138
+ }): Promise<TokenResponse> {
139
+ const env = getCustomerEnv()
140
+ const discovery = await fetchDiscovery()
141
+ const body = new URLSearchParams({
142
+ grant_type: 'authorization_code',
143
+ client_id: env.SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID,
144
+ code: input.code,
145
+ redirect_uri: env.SHOPIFY_CUSTOMER_ACCOUNT_REDIRECT_URI,
146
+ code_verifier: input.codeVerifier,
147
+ })
148
+ const res = await fetch(discovery.tokenEndpoint, {
149
+ method: 'POST',
150
+ headers: {
151
+ 'Content-Type': 'application/x-www-form-urlencoded',
152
+ Origin: new URL(env.SHOPIFY_CUSTOMER_ACCOUNT_REDIRECT_URI).origin,
153
+ },
154
+ body,
155
+ })
156
+ if (!res.ok) {
157
+ throw new Error(
158
+ `Token exchange failed: ${res.status} ${res.statusText} ${await res.text()}`,
159
+ )
160
+ }
161
+ return (await res.json()) as TokenResponse
162
+ }
163
+
164
+ export async function refreshTokens(refreshToken: string): Promise<TokenResponse> {
165
+ const env = getCustomerEnv()
166
+ const discovery = await fetchDiscovery()
167
+ const body = new URLSearchParams({
168
+ grant_type: 'refresh_token',
169
+ client_id: env.SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID,
170
+ refresh_token: refreshToken,
171
+ })
172
+ const res = await fetch(discovery.tokenEndpoint, {
173
+ method: 'POST',
174
+ headers: {
175
+ 'Content-Type': 'application/x-www-form-urlencoded',
176
+ Origin: new URL(env.SHOPIFY_CUSTOMER_ACCOUNT_REDIRECT_URI).origin,
177
+ },
178
+ body,
179
+ })
180
+ if (!res.ok) {
181
+ throw new Error(`Token refresh failed: ${res.status} ${res.statusText}`)
182
+ }
183
+ return (await res.json()) as TokenResponse
184
+ }
185
+
186
+ export async function buildEndSessionUrl(idToken: string): Promise<string> {
187
+ const env = getCustomerEnv()
188
+ const discovery = await fetchDiscovery()
189
+ const url = new URL(discovery.endSessionEndpoint)
190
+ url.searchParams.set('id_token_hint', idToken)
191
+ url.searchParams.set(
192
+ 'post_logout_redirect_uri',
193
+ new URL('/', env.SHOPIFY_CUSTOMER_ACCOUNT_REDIRECT_URI).toString(),
194
+ )
195
+ return url.toString()
196
+ }
197
+
198
+ /* ─── Signed session cookie helpers (HS256) ─────────────────────────────── */
199
+
200
+ let cachedHmacKey: CryptoKey | null = null
201
+
202
+ async function getHmacKey(): Promise<CryptoKey> {
203
+ if (cachedHmacKey) return cachedHmacKey
204
+ const env = getCustomerEnv()
205
+ cachedHmacKey = await crypto.subtle.importKey(
206
+ 'raw',
207
+ new TextEncoder().encode(env.SHOPIFY_SESSION_SECRET),
208
+ { name: 'HMAC', hash: 'SHA-256' },
209
+ false,
210
+ ['sign', 'verify'],
211
+ )
212
+ return cachedHmacKey
213
+ }
214
+
215
+ export type CustomerSession = {
216
+ accessToken: string
217
+ refreshToken: string
218
+ idToken: string
219
+ expiresAt: number
220
+ }
221
+
222
+ export async function signSession(session: CustomerSession): Promise<string> {
223
+ const key = await getHmacKey()
224
+ const payload = base64UrlEncode(JSON.stringify(session))
225
+ const sig = await crypto.subtle.sign(
226
+ 'HMAC',
227
+ key,
228
+ new TextEncoder().encode(payload),
229
+ )
230
+ return `${payload}.${bufferToBase64Url(sig)}`
231
+ }
232
+
233
+ export async function verifySession(
234
+ signed: string,
235
+ ): Promise<CustomerSession | null> {
236
+ const [payload, sig] = signed.split('.')
237
+ if (!payload || !sig) return null
238
+ const key = await getHmacKey()
239
+ const sigBytes = Uint8Array.from(base64UrlDecode(sig), (c) => c.charCodeAt(0))
240
+ const ok = await crypto.subtle.verify(
241
+ 'HMAC',
242
+ key,
243
+ sigBytes,
244
+ new TextEncoder().encode(payload),
245
+ )
246
+ if (!ok) return null
247
+ try {
248
+ return JSON.parse(base64UrlDecode(payload)) as CustomerSession
249
+ } catch {
250
+ return null
251
+ }
252
+ }
253
+
254
+ export function buildSession(token: TokenResponse): CustomerSession {
255
+ return {
256
+ accessToken: token.access_token,
257
+ refreshToken: token.refresh_token,
258
+ idToken: token.id_token,
259
+ expiresAt: Date.now() + token.expires_in * 1000,
260
+ }
261
+ }
262
+
263
+ export type OAuthFlightState = {
264
+ state: string
265
+ codeVerifier: string
266
+ redirectAfter: string
267
+ }
268
+
269
+ export async function signFlightState(
270
+ state: OAuthFlightState,
271
+ ): Promise<string> {
272
+ const key = await getHmacKey()
273
+ const payload = base64UrlEncode(JSON.stringify(state))
274
+ const sig = await crypto.subtle.sign(
275
+ 'HMAC',
276
+ key,
277
+ new TextEncoder().encode(payload),
278
+ )
279
+ return `${payload}.${bufferToBase64Url(sig)}`
280
+ }
281
+
282
+ export async function verifyFlightState(
283
+ signed: string,
284
+ ): Promise<OAuthFlightState | null> {
285
+ const [payload, sig] = signed.split('.')
286
+ if (!payload || !sig) return null
287
+ const key = await getHmacKey()
288
+ const sigBytes = Uint8Array.from(base64UrlDecode(sig), (c) => c.charCodeAt(0))
289
+ const ok = await crypto.subtle.verify(
290
+ 'HMAC',
291
+ key,
292
+ sigBytes,
293
+ new TextEncoder().encode(payload),
294
+ )
295
+ if (!ok) return null
296
+ try {
297
+ return JSON.parse(base64UrlDecode(payload)) as OAuthFlightState
298
+ } catch {
299
+ return null
300
+ }
301
+ }
@@ -0,0 +1,101 @@
1
+ import { getStorefrontEnv } from './env'
2
+
3
+ type ShopifyFetchInput<TVariables> = {
4
+ query: string
5
+ variables?: TVariables
6
+ /**
7
+ * Optional buyer IP, forwarded to Shopify's bot-protection headers.
8
+ * Only meaningful with the private token.
9
+ */
10
+ buyerIp?: string
11
+ }
12
+
13
+ type ShopifyResponse<TData> = {
14
+ data?: TData
15
+ errors?: Array<{ message: string; locations?: unknown; path?: unknown }>
16
+ }
17
+
18
+ export class ShopifyError extends Error {
19
+ constructor(
20
+ message: string,
21
+ public readonly errors?: ShopifyResponse<unknown>['errors'],
22
+ ) {
23
+ super(message)
24
+ this.name = 'ShopifyError'
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Read the buyer's IP from a Request's headers in a runtime-portable way.
30
+ * Cloudflare uses `cf-connecting-ip`; Vercel/Netlify/Node behind a proxy use
31
+ * `x-forwarded-for`. Returns undefined if neither header is present.
32
+ */
33
+ export function getBuyerIp(headers: Headers): string | undefined {
34
+ const cf = headers.get('cf-connecting-ip')
35
+ if (cf) return cf
36
+ const fwd = headers.get('x-forwarded-for')
37
+ if (fwd) return fwd.split(',')[0]?.trim()
38
+ return undefined
39
+ }
40
+
41
+ /**
42
+ * Server-side Storefront API client. Prefers the private access token (higher
43
+ * rate limits + buyer-IP forwarding); falls back to the public token so the
44
+ * default demo store works with zero setup.
45
+ *
46
+ * Use in route loaders and server functions only — never in browser code.
47
+ */
48
+ export async function shopifyServerFetch<
49
+ TData,
50
+ TVariables = Record<string, unknown>,
51
+ >(input: ShopifyFetchInput<TVariables>): Promise<TData> {
52
+ const env = getStorefrontEnv()
53
+ const usingPrivate = Boolean(env.SHOPIFY_PRIVATE_STOREFRONT_TOKEN)
54
+ const token =
55
+ env.SHOPIFY_PRIVATE_STOREFRONT_TOKEN ?? env.SHOPIFY_PUBLIC_STOREFRONT_TOKEN
56
+
57
+ if (!token) {
58
+ throw new ShopifyError(
59
+ 'Shopify Storefront token missing. Set SHOPIFY_PUBLIC_STOREFRONT_TOKEN in .env.local.',
60
+ )
61
+ }
62
+
63
+ const headers: Record<string, string> = {
64
+ 'Content-Type': 'application/json',
65
+ Accept: 'application/json',
66
+ }
67
+ if (usingPrivate) {
68
+ headers['Shopify-Storefront-Private-Token'] = token
69
+ if (input.buyerIp) headers['Shopify-Storefront-Buyer-IP'] = input.buyerIp
70
+ } else {
71
+ headers['X-Shopify-Storefront-Access-Token'] = token
72
+ }
73
+
74
+ const url = `https://${env.SHOPIFY_STORE_DOMAIN}/api/${env.SHOPIFY_STOREFRONT_API_VERSION}/graphql.json`
75
+ const response = await fetch(url, {
76
+ method: 'POST',
77
+ headers,
78
+ body: JSON.stringify({ query: input.query, variables: input.variables }),
79
+ })
80
+
81
+ if (!response.ok) {
82
+ throw new ShopifyError(
83
+ `Shopify API error: ${response.status} ${response.statusText}`,
84
+ )
85
+ }
86
+
87
+ const json = (await response.json()) as ShopifyResponse<TData>
88
+
89
+ if (json.errors?.length) {
90
+ throw new ShopifyError(
91
+ json.errors.map((e) => e.message).join('\n'),
92
+ json.errors,
93
+ )
94
+ }
95
+
96
+ if (!json.data) {
97
+ throw new ShopifyError('Shopify API returned no data and no errors.')
98
+ }
99
+
100
+ return json.data
101
+ }
@@ -0,0 +1,104 @@
1
+ {
2
+ "name": "Shopify",
3
+ "description": "Headless Shopify storefront — products, cart, Shopify-hosted checkout, and optional customer accounts. Mounts /shop/* routes alongside your existing app.",
4
+ "phase": "add-on",
5
+ "modes": ["file-router"],
6
+ "type": "add-on",
7
+ "category": "other",
8
+ "color": "#5A31F4",
9
+ "priority": 80,
10
+ "link": "https://shopify.dev/docs/storefronts/headless",
11
+ "dependsOn": ["tanstack-query"],
12
+ "options": {
13
+ "customerAccount": {
14
+ "type": "select",
15
+ "label": "Customer Accounts",
16
+ "description": "Add /shop/account/* routes (Shopify Customer Account API, OAuth setup required).",
17
+ "default": "disabled",
18
+ "options": [
19
+ { "value": "disabled", "label": "No (skip account routes)" },
20
+ { "value": "enabled", "label": "Yes (include OAuth + account dashboard)" }
21
+ ]
22
+ }
23
+ },
24
+ "integrations": [
25
+ {
26
+ "type": "header-user",
27
+ "jsName": "ShopifyHeaderCart",
28
+ "path": "src/integrations/shopify/header-cart.tsx"
29
+ }
30
+ ],
31
+ "routes": [
32
+ {
33
+ "icon": "ShoppingBag",
34
+ "url": "/shop",
35
+ "name": "Shop",
36
+ "path": "src/routes/shop.index.tsx",
37
+ "jsName": "ShopIndex"
38
+ },
39
+ {
40
+ "url": "/shop/cart",
41
+ "name": "Cart",
42
+ "path": "src/routes/shop.cart.tsx",
43
+ "jsName": "ShopCart"
44
+ }
45
+ ],
46
+ "envVars": [
47
+ {
48
+ "name": "SHOPIFY_STORE_DOMAIN",
49
+ "description": "Your store's myshopify.com domain. Defaults to Shopify's public Hydrogen demo store.",
50
+ "required": false,
51
+ "secret": false,
52
+ "file": ".env.local"
53
+ },
54
+ {
55
+ "name": "SHOPIFY_STOREFRONT_API_VERSION",
56
+ "description": "Storefront API version (e.g. 2026-01).",
57
+ "required": false,
58
+ "secret": false,
59
+ "file": ".env.local"
60
+ },
61
+ {
62
+ "name": "SHOPIFY_PUBLIC_STOREFRONT_TOKEN",
63
+ "description": "Public Storefront API token. Defaults to Shopify's public Hydrogen demo token.",
64
+ "required": false,
65
+ "secret": false,
66
+ "file": ".env.local"
67
+ },
68
+ {
69
+ "name": "SHOPIFY_PRIVATE_STOREFRONT_TOKEN",
70
+ "description": "Optional private Storefront API token. Higher rate limits + buyer-IP forwarding when set.",
71
+ "required": false,
72
+ "secret": true,
73
+ "file": ".env.local"
74
+ },
75
+ {
76
+ "name": "SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID",
77
+ "description": "Customer Account API client ID. Required when customerAccount=enabled.",
78
+ "required": false,
79
+ "secret": true,
80
+ "file": ".env.local"
81
+ },
82
+ {
83
+ "name": "SHOPIFY_CUSTOMER_ACCOUNT_SHOP_ID",
84
+ "description": "Numeric Shop ID (Shopify admin > Settings > Customer accounts). Required when customerAccount=enabled.",
85
+ "required": false,
86
+ "secret": false,
87
+ "file": ".env.local"
88
+ },
89
+ {
90
+ "name": "SHOPIFY_CUSTOMER_ACCOUNT_REDIRECT_URI",
91
+ "description": "OAuth callback URL. Defaults to http://localhost:3000/shop/account/callback.",
92
+ "required": false,
93
+ "secret": false,
94
+ "file": ".env.local"
95
+ },
96
+ {
97
+ "name": "SHOPIFY_SESSION_SECRET",
98
+ "description": "32+ char secret used to sign customer session cookies. Required when customerAccount=enabled.",
99
+ "required": false,
100
+ "secret": true,
101
+ "file": ".env.local"
102
+ }
103
+ ]
104
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "dependencies": {
3
+ "@shopify/hydrogen-react": "^2026.1.0",
4
+ "valibot": "^1.0.0"
5
+ }
6
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 109 124" width="32" height="36"><path fill="#95BF47" d="M74.7 14.8c0-.4-.4-.6-.7-.7-.3-.1-6.2-.5-6.2-.5s-4.1-4.1-4.6-4.6c-.5-.5-1.4-.3-1.7-.2 0 0-.9.3-2.4.7-.2-.8-.6-1.7-1.1-2.7-1.6-3.1-4-4.7-6.9-4.8-.2 0-.4 0-.6.1-.1-.1-.2-.2-.2-.3C49.1.7 47.5.1 45.6.1c-3.7.1-7.5 2.8-10.5 7.6-2.1 3.4-3.7 7.6-4.2 10.9-4.3 1.3-7.3 2.3-7.4 2.3-2.2.7-2.2.7-2.5 2.8-.2 1.5-5.8 45.4-5.8 45.4l47.3 8.2 20.5-5.1c0-.1-8.3-56-8.3-56.4zm-15-3.7c-1.1.3-2.4.7-3.7 1.2 0-1.9-.3-4.5-1.1-6.7 2.7.4 4 3.5 4.8 5.5zm-6.2 1.9c-2.5.8-5.3 1.6-8.1 2.5.8-3 2.3-6 4.1-8 .7-.7 1.7-1.6 2.8-2.1 1.2 2.4 1.4 5.7 1.2 7.6zm-5.1-9.7c.9 0 1.7.2 2.4.6-1.1.6-2.1 1.4-3.1 2.4-2.4 2.6-4.3 6.7-5 10.6-2.4.7-4.7 1.5-6.8 2.1 1.4-6.4 6.6-15.5 12.5-15.7z"/><path fill="#5E8E3E" d="M74 14.1c-.3-.1-6.2-.5-6.2-.5s-4.1-4.1-4.6-4.6c-.2-.2-.4-.3-.7-.3v75l20.5-5.1S74.4 22.7 74.4 22.3c0-.4-.4-.6-.4-.6.1.1-.4-7.6 0-7.6z"/><path fill="#FFF" d="M51.4 25.7l-2.4 8.9s-2.7-1.2-5.8-1c-4.6.3-4.6 3.2-4.6 3.9.2 3.9 10.6 4.8 11.2 14.1.5 7.3-3.9 12.3-10.1 12.7-7.5.5-11.6-3.9-11.6-3.9l1.6-6.8s4.2 3.1 7.5 2.9c2.2-.1 2.9-1.9 2.9-3.1-.3-5.1-8.7-4.8-9.3-13.3-.4-7.2 4.3-14.4 14.6-15.1 4-.3 6 .7 6 .7z"/></svg>
@@ -0,0 +1,39 @@
1
+ # Shopify Storefront
2
+
3
+ A storefront-first TanStack Start template. Bundles the [Shopify add-on](../../add-ons/shopify)
4
+ plus a polished home page so the home route (`/`) is your shop landing.
5
+
6
+ ```bash
7
+ npx @tanstack/cli create my-shop --template shopify-storefront
8
+ ```
9
+
10
+ ## What you get
11
+
12
+ - `/` — Storefront landing (replaces the default home)
13
+ - `/shop` — Catalog landing with collections sidebar
14
+ - `/shop/products/$handle`, `/shop/collections/$handle`, `/shop/cart`, `/shop/search`,
15
+ `/shop/pages/$handle`, `/shop/policies/$handle` — full Hydrogen-demo parity
16
+ - `/shop/account/*` — optional, if you opted into customer accounts during scaffold
17
+
18
+ The default `.env.local` points at Shopify's public Hydrogen demo store, so you'll
19
+ see real products on first run with zero setup. See
20
+ [the add-on README](../../add-ons/shopify/README.md) for connecting your own store.
21
+
22
+ ## Brand swap-out
23
+
24
+ Three files own the look-and-feel:
25
+
26
+ - `src/routes/index.tsx` — the home page hero + featured collections
27
+ - `src/components/ShopHero.tsx` — the marquee
28
+ - `src/components/FeaturedCollections.tsx` — the collection cards
29
+
30
+ The shop's design tokens (`--storefront-bg`, `--storefront-fg`, `--storefront-accent`,
31
+ etc.) are defined in the Shopify add-on's `src/components/shop/shop.css`. Override
32
+ those six variables in your own CSS to re-skin the entire storefront.
33
+
34
+ ## Removing demo content
35
+
36
+ To strip the hero/landing and use the bare add-on instead:
37
+
38
+ 1. Delete `src/components/ShopHero.tsx` and `src/components/FeaturedCollections.tsx`.
39
+ 2. Replace `src/routes/index.tsx` with a redirect to `/shop` (or your preferred home).
@@ -0,0 +1,43 @@
1
+ import { Link } from '@tanstack/react-router'
2
+
3
+ import { ShopImage } from '#/components/shop/shop-image'
4
+ import type { CollectionListItem } from '#/lib/shopify/queries'
5
+
6
+ type FeaturedCollectionsProps = {
7
+ collections: ReadonlyArray<CollectionListItem>
8
+ }
9
+
10
+ export function FeaturedCollections({ collections }: FeaturedCollectionsProps) {
11
+ if (collections.length === 0) return null
12
+ return (
13
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
14
+ {collections.map((collection) => (
15
+ <Link
16
+ key={collection.id}
17
+ to="/shop/collections/$handle"
18
+ params={{ handle: collection.handle }}
19
+ className="group relative block overflow-hidden rounded-2xl bg-[var(--storefront-line)] no-underline"
20
+ style={{ aspectRatio: '4 / 5' }}
21
+ >
22
+ {collection.image && (
23
+ <ShopImage
24
+ src={collection.image.url}
25
+ alt={collection.image.altText ?? collection.title}
26
+ width={800}
27
+ height={1000}
28
+ sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
29
+ className="absolute inset-0 h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.04]"
30
+ />
31
+ )}
32
+ <div className="absolute inset-0 bg-gradient-to-t from-black/55 via-black/0 to-black/0" />
33
+ <div className="absolute inset-x-0 bottom-0 p-6 text-white">
34
+ <h3 className="text-2xl font-medium tracking-tight">
35
+ {collection.title}
36
+ </h3>
37
+ <p className="mt-1 text-sm text-white/80">Shop the collection →</p>
38
+ </div>
39
+ </Link>
40
+ ))}
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,39 @@
1
+ import { Link } from '@tanstack/react-router'
2
+
3
+ type ShopHeroProps = {
4
+ title: string
5
+ tagline: string
6
+ }
7
+
8
+ export function ShopHero({ title, tagline }: ShopHeroProps) {
9
+ return (
10
+ <section className="relative isolate overflow-hidden border-b border-[var(--storefront-line)]">
11
+ <div className="mx-auto flex max-w-7xl flex-col items-start gap-8 px-4 py-24 sm:px-6 sm:py-32 lg:px-8 lg:py-40">
12
+ <p className="text-xs font-semibold uppercase tracking-[0.2em] text-[var(--storefront-fg-muted)]">
13
+ New season · 2026
14
+ </p>
15
+ <h1 className="max-w-3xl text-5xl font-medium tracking-tight sm:text-6xl lg:text-7xl">
16
+ {title}
17
+ </h1>
18
+ <p className="max-w-xl text-lg text-[var(--storefront-fg-muted)]">
19
+ {tagline}
20
+ </p>
21
+ <div className="mt-2 flex flex-wrap gap-3">
22
+ <Link
23
+ to="/shop"
24
+ className="rounded-full bg-[var(--storefront-accent)] px-6 py-3 text-sm font-medium text-[var(--storefront-accent-fg)] no-underline transition hover:opacity-90"
25
+ >
26
+ Shop all
27
+ </Link>
28
+ <Link
29
+ to="/shop/search"
30
+ search={{ q: '' }}
31
+ className="rounded-full border border-[var(--storefront-fg)] px-6 py-3 text-sm font-medium no-underline text-[var(--storefront-fg)] transition hover:bg-[var(--storefront-fg)] hover:text-[var(--storefront-bg)]"
32
+ >
33
+ Search
34
+ </Link>
35
+ </div>
36
+ </div>
37
+ </section>
38
+ )
39
+ }