@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.
- package/CHANGELOG.md +72 -0
- package/dist/frameworks/react/add-ons/powersync/README.md.ejs +26 -0
- package/dist/frameworks/react/add-ons/powersync/assets/_dot_env.local.append +3 -0
- package/dist/frameworks/react/add-ons/powersync/assets/powersync-vite-plugin.ts +17 -0
- package/dist/frameworks/react/add-ons/powersync/assets/src/integrations/powersync/provider.tsx +26 -0
- package/dist/frameworks/react/add-ons/powersync/assets/src/lib/powersync/AppSchema.ts +17 -0
- package/dist/frameworks/react/add-ons/powersync/assets/src/lib/powersync/BackendConnector.ts +52 -0
- package/dist/frameworks/react/add-ons/powersync/assets/src/routes/demo/powersync.tsx +129 -0
- package/dist/frameworks/react/add-ons/powersync/info.json +46 -0
- package/dist/frameworks/react/add-ons/powersync/package.json.ejs +7 -0
- package/dist/frameworks/react/add-ons/powersync/small-logo.svg +6 -0
- package/dist/frameworks/react/add-ons/shopify/README.md +86 -0
- package/dist/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
- package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
- package/dist/frameworks/react/add-ons/shopify/info.json +104 -0
- package/dist/frameworks/react/add-ons/shopify/package.json +6 -0
- package/dist/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
- package/dist/frameworks/react/examples/shopify-storefront/README.md +39 -0
- package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
- package/dist/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
- package/dist/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
- package/dist/frameworks/react/examples/shopify-storefront/info.json +18 -0
- package/dist/frameworks/react/examples/shopify-storefront/package.json +3 -0
- package/dist/frameworks/react/project/base/src/components/Header.tsx.ejs +34 -34
- package/package.json +1 -1
- package/src/frameworks/react/add-ons/powersync/README.md.ejs +26 -0
- package/src/frameworks/react/add-ons/powersync/assets/_dot_env.local.append +3 -0
- package/src/frameworks/react/add-ons/powersync/assets/powersync-vite-plugin.ts +17 -0
- package/src/frameworks/react/add-ons/powersync/assets/src/integrations/powersync/provider.tsx +26 -0
- package/src/frameworks/react/add-ons/powersync/assets/src/lib/powersync/AppSchema.ts +17 -0
- package/src/frameworks/react/add-ons/powersync/assets/src/lib/powersync/BackendConnector.ts +52 -0
- package/src/frameworks/react/add-ons/powersync/assets/src/routes/demo/powersync.tsx +129 -0
- package/src/frameworks/react/add-ons/powersync/info.json +46 -0
- package/src/frameworks/react/add-ons/powersync/package.json.ejs +7 -0
- package/src/frameworks/react/add-ons/powersync/small-logo.svg +6 -0
- package/src/frameworks/react/add-ons/shopify/README.md +86 -0
- package/src/frameworks/react/add-ons/shopify/assets/_dot_env.local.append +19 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/account-nav.tsx.ejs +41 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/add-to-cart-button.tsx +48 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-line-item.tsx +94 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/cart-summary.tsx +111 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/empty-state.tsx +29 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/money.tsx +11 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-card.tsx +74 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/product-grid.tsx +24 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop-image.tsx +57 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/shop.css +58 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/components/shop/variant-selector.tsx +79 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-cart.ts +276 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/hooks/use-customer.ts.ejs +22 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/integrations/shopify/header-cart.tsx +37 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/customer-queries.ts.ejs +228 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/format.ts +33 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/lib/shopify/queries.ts +684 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.addresses.tsx.ejs +67 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.callback.tsx.ejs +45 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.index.tsx.ejs +70 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.login.tsx.ejs +59 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.logout.tsx.ejs +16 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.$id.tsx.ejs +126 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.orders.tsx.ejs +50 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.account.tsx.ejs +34 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.cart.tsx +45 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.collections.$handle.tsx +66 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.index.tsx +36 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.pages.$handle.tsx +39 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.policies.$handle.tsx +30 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.products.$handle.tsx +106 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.search.tsx +75 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/routes/shop.tsx +78 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cart.functions.ts +207 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/catalog.functions.ts +244 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/cookies.ts +29 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs +99 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs +49 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs +168 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/env.ts +89 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/oauth.ts.ejs +301 -0
- package/src/frameworks/react/add-ons/shopify/assets/src/server/shopify/storefront-client.ts +101 -0
- package/src/frameworks/react/add-ons/shopify/info.json +104 -0
- package/src/frameworks/react/add-ons/shopify/package.json +6 -0
- package/src/frameworks/react/add-ons/shopify/small-logo.svg +1 -0
- package/src/frameworks/react/examples/shopify-storefront/README.md +39 -0
- package/src/frameworks/react/examples/shopify-storefront/assets/src/components/FeaturedCollections.tsx +43 -0
- package/src/frameworks/react/examples/shopify-storefront/assets/src/components/ShopHero.tsx +39 -0
- package/src/frameworks/react/examples/shopify-storefront/assets/src/routes/index.tsx +65 -0
- package/src/frameworks/react/examples/shopify-storefront/info.json +18 -0
- package/src/frameworks/react/examples/shopify-storefront/package.json +3 -0
- 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
|
+
}
|
package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-client.ts.ejs
ADDED
|
@@ -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
|
+
}
|
package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer-cookies.ts.ejs
ADDED
|
@@ -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
|
+
}
|
package/dist/frameworks/react/add-ons/shopify/assets/src/server/shopify/customer.functions.ts.ejs
ADDED
|
@@ -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
|
+
}
|