Monorepo with Turborepo (#651)

* Moved everything

* Figuring out how to make imports work

* Updated exports

* Added missing exports

* Added @vercel/commerce-local to `site`

* Updated commerce config

* Updated exports and commerce config

* Updated commerce hoc

* Fixed exports in local

* Added publish config

* Updated imports in site

* It's actually working

* Don't use debugger in dev for better speeds

* Improved DX when editing packages

* Set up eslint with husky

* Updated prettier config

* Added prettier setup to every package

* Moved bigcommerce

* Moved Bigcommerce to src and package updates

* Updated setup of bigcommerce

* Moved definitions script

* Moved commercejs

* Move to src

* Fixed types in commercejs

* Moved kibocommerce

* Moved kibocommerce to src

* Added package/tsconfig to kibocommerce

* Fixed imports and other things

* Moved ordercloud

* Moved ordercloud to src

* Fixed imports

* Added missing prettier files

* Moved Saleor

* Moved Saleor to src

* Fixed imports

* Replaced all imports to @commerce

* Added prettierignore/rc to all providers

* Moved shopify to src

* Build shopify in packages

* Moved Spree

* Moved spree to src

* Updated spree

* Moved swell

* Moved swell to src

* Fixed type imports in swell

* Moved Vendure to packages

* Moved vendure to src

* Fixed imports in vendure

* Added codegen to saleor

* Updated codegen setup for shopify

* Added codegen to vendure

* Added codegen to kibocommerce

* Added all packages to site's deps

* Updated codegen setup in bigcommerce

* Minor fixes

* Updated providers' names in site

* Updated packages based on Bel's changes

* Updated turbo to latest

* Fixed ts complains

* Set npm engine in root

* New lockfile install

* remove engines

* Regen lockfile

* Switched from npm to yarn

* Updated typesVersions in all packages

* Moved dep

* Updated SWR to the just released 1.2.0

* Removed "isolatedModules" from packages

* Updated list of providers and default

* Updated swell declaration

* Removed next import from kibocommerce

* Added COMMERCE_PROVIDER log

* Added another log

* Updated turbo config

* Updated docs

* Removed test logs

Co-authored-by: Jared Palmer <jared@jaredpalmer.com>
This commit is contained in:
Luis Alvarez D
2022-02-01 14:14:05 -05:00
committed by GitHub
parent d0ef346189
commit 0afe686fe9
1326 changed files with 9109 additions and 19494 deletions

View File

@@ -0,0 +1,4 @@
COMMERCE_PROVIDER=shopify
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=

View File

@@ -0,0 +1,2 @@
node_modules
dist

View File

@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false
}

135
packages/shopify/README.md Normal file
View File

@@ -0,0 +1,135 @@
## Shopify Provider
**Demo:** https://shopify.demo.vercel.store/
Before getting started, a [Shopify](https://www.shopify.com/) account and store is required before using the provider.
Next, copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
```bash
cp packages/shopify/.env.template .env.local
```
Then, set the environment variables in `.env.local` to match the ones from your store.
## Contribute
Our commitment to Open Source can be found [here](https://vercel.com/oss).
If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues).
## Modifications
These modifications are temporarily until contributions are made to remove them.
### Adding item to Cart
```js
// components/product/ProductView/ProductView.tsx
const ProductView: FC<Props> = ({ product }) => {
const addToCart = async () => {
setLoading(true)
try {
await addItem({
productId: product.id,
variantId: variant ? variant.id : product.variants[0].id,
})
openSidebar()
setLoading(false)
} catch (err) {
setLoading(false)
}
}
}
```
### Proceed to Checkout
```js
// components/cart/CartSidebarView/CartSidebarView.tsx
import { useCommerce } from '@framework'
const CartSidebarView: FC = () => {
const { checkout } = useCommerce()
return (
<Button href={checkout.webUrl} Component="a" width="100%">
Proceed to Checkout
</Button>
)
}
```
## APIs
Collections of APIs to fetch data from a Shopify store.
The data is fetched using the [Shopify JavaScript Buy SDK](https://github.com/Shopify/js-buy-sdk#readme). Read the [Shopify Storefront API reference](https://shopify.dev/docs/storefront-api/reference) for more information.
### getProduct
Get a single product by its `handle`.
```js
import getProduct from '@framework/product/get-product'
import { getConfig } from '@framework/api'
const config = getConfig()
const product = await getProduct({
variables: { slug },
config,
})
```
### getAllProducts
```js
import getAllProducts from '@framework/product/get-all-products'
import { getConfig } from '@framework/api'
const config = getConfig()
const { products } = await getAllProducts({
variables: { first: 12 },
config,
})
```
### getAllCollections
```js
import getAllCollections from '@framework/product/get-all-collections'
import { getConfig } from '@framework/api'
const config = getConfig()
const collections = await getAllCollections({
config,
})
```
### getAllPages
```js
import getAllPages from '@framework/common/get-all-pages'
import { getConfig } from '@framework/api'
const config = getConfig()
const pages = await getAllPages({
variables: { first: 12 },
config,
})
```
## Code generation
This provider makes use of GraphQL code generation. The [schema.graphql](./schema.graphql) and [schema.d.ts](./schema.d.ts) files contain the generated types & schema introspection results.
When developing the provider, changes to any GraphQL operations should be followed by re-generation of the types and schema files:
From the project root dir, run:
```sh
yarn generate:shopify
```

View File

@@ -0,0 +1,32 @@
{
"schema": {
"https://${NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/api/2021-07/graphql.json": {
"headers": {
"X-Shopify-Storefront-Access-Token": "${NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN}"
}
}
},
"documents": [
{
"./src/**/*.{ts,tsx}": {
"noRequire": true
}
}
],
"generates": {
"./schema.d.ts": {
"plugins": ["typescript", "typescript-operations"],
"config": {
"scalars": {
"ID": "string"
}
}
},
"./schema.graphql": {
"plugins": ["schema-ast"]
}
},
"hooks": {
"afterAllFileWrite": ["prettier --write"]
}
}

View File

@@ -0,0 +1,79 @@
{
"name": "@vercel/commerce-shopify",
"version": "0.0.1",
"license": "MIT",
"scripts": {
"build": "rm -fr dist/* && tsc",
"dev": "npm run build -- --watch",
"prettier-fix": "prettier --write .",
"generate": "DOTENV_CONFIG_PATH=./.env graphql-codegen -r dotenv/config"
},
"sideEffects": false,
"type": "module",
"exports": {
".": "./dist/index.js",
"./*": [
"./dist/*.js",
"./dist/*/index.js"
],
"./next.config": "./dist/next.config.cjs"
},
"typesVersions": {
"*": {
"*": [
"src/*",
"src/*/index"
],
"next.config": [
"dist/next.config.d.cts"
]
}
},
"files": [
"dist",
"schema.d.ts"
],
"publishConfig": {
"typesVersions": {
"*": {
"*": [
"dist/*.d.ts",
"dist/*/index.d.ts"
],
"next.config": [
"dist/next.config.d.cts"
]
}
}
},
"dependencies": {
"@vercel/commerce": "^0.0.1",
"@vercel/fetch": "^6.1.1"
},
"peerDependencies": {
"next": "^12",
"react": "^17",
"react-dom": "^17"
},
"devDependencies": {
"@graphql-codegen/cli": "^2.3.1",
"@graphql-codegen/schema-ast": "^2.4.1",
"@graphql-codegen/typescript": "^2.4.2",
"@graphql-codegen/typescript-operations": "^2.2.2",
"@types/node": "^17.0.8",
"@types/react": "^17.0.38",
"dotenv": "^12.0.3",
"lint-staged": "^12.1.7",
"next": "^12.0.8",
"prettier": "^2.5.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.5.4"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx,json}": [
"prettier --write",
"git add"
]
}
}

5586
packages/shopify/schema.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@@ -0,0 +1,38 @@
import {
SHOPIFY_CHECKOUT_ID_COOKIE,
SHOPIFY_CHECKOUT_URL_COOKIE,
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
} from '../../../const'
import associateCustomerWithCheckoutMutation from '../../../utils/mutations/associate-customer-with-checkout'
import type { CheckoutEndpoint } from '.'
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
req,
res,
config,
}) => {
const { cookies } = req
const checkoutUrl = cookies[SHOPIFY_CHECKOUT_URL_COOKIE]
const customerCookie = cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE]
if (customerCookie) {
try {
await config.fetch(associateCustomerWithCheckoutMutation, {
variables: {
checkoutId: cookies[SHOPIFY_CHECKOUT_ID_COOKIE],
customerAccessToken: cookies[SHOPIFY_CUSTOMER_TOKEN_COOKIE],
},
})
} catch (error) {
console.error(error)
}
}
if (checkoutUrl) {
res.redirect(checkoutUrl)
} else {
res.redirect('/cart')
}
}
export default getCheckout

View File

@@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
import type { CheckoutSchema } from '../../../types/checkout'
import type { ShopifyAPI } from '../..'
import getCheckout from './get-checkout'
export type CheckoutAPI = GetAPISchema<ShopifyAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = { getCheckout }
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@@ -0,0 +1,55 @@
import {
CommerceAPI,
CommerceAPIConfig,
getCommerceApi as commerceApi,
} from '@vercel/commerce/api'
import {
API_URL,
API_TOKEN,
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
SHOPIFY_CHECKOUT_ID_COOKIE,
} from '../const'
import fetchGraphqlApi from './utils/fetch-graphql-api'
import * as operations from './operations'
if (!API_URL) {
throw new Error(
`The environment variable NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN is missing and it's required to access your store`
)
}
if (!API_TOKEN) {
throw new Error(
`The environment variable NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN is missing and it's required to access your store`
)
}
export interface ShopifyConfig extends CommerceAPIConfig {}
const ONE_DAY = 60 * 60 * 24
const config: ShopifyConfig = {
commerceUrl: API_URL,
apiToken: API_TOKEN,
customerCookie: SHOPIFY_CUSTOMER_TOKEN_COOKIE,
cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE,
cartCookieMaxAge: ONE_DAY * 30,
fetch: fetchGraphqlApi,
}
export const provider = {
config,
operations,
}
export type Provider = typeof provider
export type ShopifyAPI<P extends Provider = Provider> = CommerceAPI<P>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): ShopifyAPI<P> {
return commerceApi(customProvider)
}

View File

@@ -0,0 +1,71 @@
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import {
GetAllPagesQuery,
GetAllPagesQueryVariables,
PageEdge,
} from '../../../schema'
import { normalizePages } from '../../utils'
import type { ShopifyConfig, Provider } from '..'
import type { GetAllPagesOperation, Page } from '../../types/page'
import getAllPagesQuery from '../../utils/queries/get-all-pages-query'
export default function getAllPagesOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllPages<T extends GetAllPagesOperation>(opts?: {
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>(
opts: {
config?: Partial<ShopifyConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>({
query = getAllPagesQuery,
config,
variables,
}: {
url?: string
config?: Partial<ShopifyConfig>
variables?: GetAllPagesQueryVariables
preview?: boolean
query?: string
} = {}): Promise<T['data']> {
const {
fetch,
locale,
locales = ['en-US', 'es'],
} = commerce.getConfig(config)
const { data } = await fetch<GetAllPagesQuery, GetAllPagesQueryVariables>(
query,
{
variables,
},
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
return {
pages: locales.reduce<Page[]>(
(arr, locale) =>
arr.concat(normalizePages(data.pages.edges as PageEdge[], locale)),
[]
),
}
}
return getAllPages
}

View File

@@ -0,0 +1,55 @@
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import { GetAllProductPathsOperation } from '../../types/product'
import {
GetAllProductPathsQuery,
GetAllProductPathsQueryVariables,
ProductEdge,
} from '../../../schema'
import type { ShopifyConfig, Provider } from '..'
import { getAllProductsQuery } from '../../utils'
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<
T extends GetAllProductPathsOperation
>(opts?: {
variables?: T['variables']
config?: ShopifyConfig
}): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
opts: {
variables?: T['variables']
config?: ShopifyConfig
} & OperationOptions
): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
query = getAllProductsQuery,
config,
variables,
}: {
query?: string
config?: ShopifyConfig
variables?: T['variables']
} = {}): Promise<T['data']> {
config = commerce.getConfig(config)
const { data } = await config.fetch<
GetAllProductPathsQuery,
GetAllProductPathsQueryVariables
>(query, { variables })
return {
products: data.products.edges.map(({ node: { handle } }) => ({
path: `/${handle}`,
})),
}
}
return getAllProductPaths
}

View File

@@ -0,0 +1,67 @@
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import { GetAllProductsOperation } from '../../types/product'
import {
GetAllProductsQuery,
GetAllProductsQueryVariables,
Product as ShopifyProduct,
} from '../../../schema'
import type { ShopifyConfig, Provider } from '..'
import getAllProductsQuery from '../../utils/queries/get-all-products-query'
import { normalizeProduct } from '../../utils'
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>(opts?: {
variables?: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']>
async function getAllProducts<T extends GetAllProductsOperation>(
opts: {
variables?: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getAllProducts<T extends GetAllProductsOperation>({
query = getAllProductsQuery,
variables,
config,
}: {
query?: string
variables?: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const { fetch, locale } = commerce.getConfig(config)
const { data } = await fetch<
GetAllProductsQuery,
GetAllProductsQueryVariables
>(
query,
{ variables },
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
return {
products: data.products.edges.map(({ node }) =>
normalizeProduct(node as ShopifyProduct)
),
}
}
return getAllProducts
}

View File

@@ -0,0 +1,64 @@
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import { normalizePage } from '../../utils'
import type { ShopifyConfig, Provider } from '..'
import {
GetPageQuery,
GetPageQueryVariables,
Page as ShopifyPage,
} from '../../../schema'
import { GetPageOperation } from '../../types/page'
import getPageQuery from '../../utils/queries/get-page-query'
export default function getPageOperation({
commerce,
}: OperationContext<Provider>) {
async function getPage<T extends GetPageOperation>(opts: {
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']>
async function getPage<T extends GetPageOperation>(
opts: {
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getPage<T extends GetPageOperation>({
query = getPageQuery,
variables,
config,
}: {
query?: string
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']> {
const { fetch, locale } = commerce.getConfig(config)
const {
data: { node: page },
} = await fetch<GetPageQuery, GetPageQueryVariables>(
query,
{
variables,
},
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
return page ? { page: normalizePage(page as ShopifyPage, locale) } : {}
}
return getPage
}

View File

@@ -0,0 +1,66 @@
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import { GetProductOperation } from '../../types/product'
import { normalizeProduct, getProductQuery } from '../../utils'
import type { ShopifyConfig, Provider } from '..'
import {
GetProductBySlugQuery,
Product as ShopifyProduct,
} from '../../../schema'
export default function getProductOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct<T extends GetProductOperation>(opts: {
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']>
async function getProduct<T extends GetProductOperation>(
opts: {
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getProduct<T extends GetProductOperation>({
query = getProductQuery,
variables,
config: cfg,
}: {
query?: string
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']> {
const { fetch, locale } = commerce.getConfig(cfg)
const {
data: { productByHandle },
} = await fetch<GetProductBySlugQuery>(
query,
{
variables,
},
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
return {
...(productByHandle && {
product: normalizeProduct(productByHandle as ShopifyProduct),
}),
}
}
return getProduct
}

View File

@@ -0,0 +1,62 @@
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import { GetSiteInfoQueryVariables } from '../../../schema'
import type { ShopifyConfig, Provider } from '..'
import { GetSiteInfoOperation } from '../../types/site'
import { getCategories, getBrands, getSiteInfoQuery } from '../../utils'
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo<T extends GetSiteInfoOperation>(opts?: {
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']>
async function getSiteInfo<T extends GetSiteInfoOperation>(
opts: {
config?: Partial<ShopifyConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getSiteInfo<T extends GetSiteInfoOperation>({
query = getSiteInfoQuery,
config,
variables,
}: {
query?: string
config?: Partial<ShopifyConfig>
preview?: boolean
variables?: GetSiteInfoQueryVariables
} = {}): Promise<T['data']> {
const cfg = commerce.getConfig(config)
const categoriesPromise = getCategories(cfg)
const brandsPromise = getBrands(cfg)
/*
const { fetch, locale } = cfg
const { data } = await fetch<GetSiteInfoQuery, GetSiteInfoQueryVariables>(
query,
{ variables },
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
*/
return {
categories: await categoriesPromise,
brands: await brandsPromise,
}
}
return getSiteInfo
}

View File

@@ -0,0 +1,7 @@
export { default as getAllPages } from './get-all-pages'
export { default as getPage } from './get-page'
export { default as getAllProducts } from './get-all-products'
export { default as getAllProductPaths } from './get-all-product-paths'
export { default as getProduct } from './get-product'
export { default as getSiteInfo } from './get-site-info'
export { default as login } from './login'

View File

@@ -0,0 +1,48 @@
import type { ServerResponse } from 'http'
import type { OperationContext } from '@vercel/commerce/api/operations'
import type { LoginOperation } from '../../types/login'
import type { ShopifyConfig, Provider } from '..'
import {
customerAccessTokenCreateMutation,
setCustomerToken,
throwUserErrors,
} from '../../utils'
import { CustomerAccessTokenCreateMutation } from '../../../schema'
export default function loginOperation({
commerce,
}: OperationContext<Provider>) {
async function login<T extends LoginOperation>({
query = customerAccessTokenCreateMutation,
variables,
config,
}: {
query?: string
variables: T['variables']
res: ServerResponse
config?: ShopifyConfig
}): Promise<T['data']> {
config = commerce.getConfig(config)
const {
data: { customerAccessTokenCreate },
} = await config.fetch<CustomerAccessTokenCreateMutation>(query, {
variables,
})
throwUserErrors(customerAccessTokenCreate?.customerUserErrors)
const customerAccessToken = customerAccessTokenCreate?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
if (accessToken) {
setCustomerToken(accessToken)
}
return {
result: customerAccessToken?.accessToken,
}
}
return login
}

View File

@@ -0,0 +1,45 @@
import type { GraphQLFetcher } from '@vercel/commerce/api'
import fetch from './fetch'
import { API_URL, API_TOKEN } from '../../const'
import { getError } from '../../utils/handle-fetch-response'
const fetchGraphqlApi: GraphQLFetcher = async (
query: string,
{ variables } = {},
fetchOptions
) => {
try {
const res = await fetch(API_URL, {
...fetchOptions,
method: 'POST',
headers: {
'X-Shopify-Storefront-Access-Token': API_TOKEN!,
...fetchOptions?.headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
const { data, errors, status } = await res.json()
if (errors) {
throw getError(errors, status)
}
return { data, res }
} catch (err) {
throw getError(
[
{
message: `${err} \n Most likely related to an unexpected output. e.g the store might be protected with password or not available.`,
},
],
500
)
}
}
export default fetchGraphqlApi

View File

@@ -0,0 +1,2 @@
import zeitFetch from '@vercel/fetch'
export default zeitFetch()

View File

@@ -0,0 +1,63 @@
import { useCallback } from 'react'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { CommerceError } from '@vercel/commerce/utils/errors'
import useLogin, { UseLogin } from '@vercel/commerce/auth/use-login'
import type { LoginHook } from '../types/login'
import useCustomer from '../customer/use-customer'
import {
setCustomerToken,
throwUserErrors,
customerAccessTokenCreateMutation,
} from '../utils'
import { Mutation, MutationCustomerAccessTokenCreateArgs } from '../../schema'
export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<LoginHook> = {
fetchOptions: {
query: customerAccessTokenCreateMutation,
},
async fetcher({ input: { email, password }, options, fetch }) {
if (!(email && password)) {
throw new CommerceError({
message: 'An email and password are required to login',
})
}
const { customerAccessTokenCreate } = await fetch<
Mutation,
MutationCustomerAccessTokenCreateArgs
>({
...options,
variables: {
input: { email, password },
},
})
throwUserErrors(customerAccessTokenCreate?.customerUserErrors)
const customerAccessToken = customerAccessTokenCreate?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
if (accessToken) {
setCustomerToken(accessToken)
}
return null
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCustomer()
return useCallback(
async function login(input) {
const data = await fetch({ input })
await mutate()
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,39 @@
import { useCallback } from 'react'
import type { MutationHook } from '@vercel/commerce/utils/types'
import useLogout, { UseLogout } from '@vercel/commerce/auth/use-logout'
import type { LogoutHook } from '../types/logout'
import useCustomer from '../customer/use-customer'
import customerAccessTokenDeleteMutation from '../utils/mutations/customer-access-token-delete'
import { getCustomerToken, setCustomerToken } from '../utils/customer-token'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<LogoutHook> = {
fetchOptions: {
query: customerAccessTokenDeleteMutation,
},
async fetcher({ options, fetch }) {
await fetch({
...options,
variables: {
customerAccessToken: getCustomerToken(),
},
})
setCustomerToken(null)
return null
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCustomer()
return useCallback(
async function logout() {
const data = await fetch()
await mutate(null, false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,67 @@
import { useCallback } from 'react'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { CommerceError } from '@vercel/commerce/utils/errors'
import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup'
import type { SignupHook } from '../types/signup'
import useCustomer from '../customer/use-customer'
import { Mutation, MutationCustomerCreateArgs } from '../../schema'
import {
handleAutomaticLogin,
throwUserErrors,
customerCreateMutation,
} from '../utils'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<SignupHook> = {
fetchOptions: {
query: customerCreateMutation,
},
async fetcher({
input: { firstName, lastName, email, password },
options,
fetch,
}) {
if (!(firstName && lastName && email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to signup',
})
}
const { customerCreate } = await fetch<
Mutation,
MutationCustomerCreateArgs
>({
...options,
variables: {
input: {
firstName,
lastName,
email,
password,
},
},
})
throwUserErrors(customerCreate?.customerUserErrors)
await handleAutomaticLogin(fetch, { email, password })
return null
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCustomer()
return useCallback(
async function signup(input) {
const data = await fetch({ input })
await mutate()
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,4 @@
export { default as useCart } from './use-cart'
export { default as useAddItem } from './use-add-item'
export { default as useUpdateItem } from './use-update-item'
export { default as useRemoveItem } from './use-remove-item'

View File

@@ -0,0 +1,70 @@
import { useCallback } from 'react'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { CommerceError } from '@vercel/commerce/utils/errors'
import useAddItem, { UseAddItem } from '@vercel/commerce/cart/use-add-item'
import type { AddItemHook } from '../types/cart'
import useCart from './use-cart'
import {
checkoutLineItemAddMutation,
getCheckoutId,
checkoutToCart,
checkoutCreate,
} from '../utils'
import { Mutation, MutationCheckoutLineItemsAddArgs } from '../../schema'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
query: checkoutLineItemAddMutation,
},
async fetcher({ input: item, options, fetch }) {
if (
item.quantity &&
(!Number.isInteger(item.quantity) || item.quantity! < 1)
) {
throw new CommerceError({
message: 'The item quantity has to be a valid integer greater than 0',
})
}
const lineItems = [
{
variantId: item.variantId,
quantity: item.quantity ?? 1,
},
]
let checkoutId = getCheckoutId()
if (!checkoutId) {
return checkoutToCart(await checkoutCreate(fetch, lineItems))
} else {
const { checkoutLineItemsAdd } = await fetch<
Mutation,
MutationCheckoutLineItemsAddArgs
>({
...options,
variables: {
checkoutId,
lineItems,
},
})
return checkoutToCart(checkoutLineItemsAdd)
}
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCart()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,60 @@
import { useMemo } from 'react'
import useCommerceCart, { UseCart } from '@vercel/commerce/cart/use-cart'
import { SWRHook } from '@vercel/commerce/utils/types'
import { checkoutToCart } from '../utils'
import getCheckoutQuery from '../utils/queries/get-checkout-query'
import { GetCartHook } from '../types/cart'
import Cookies from 'js-cookie'
import {
SHOPIFY_CHECKOUT_ID_COOKIE,
SHOPIFY_CHECKOUT_URL_COOKIE,
} from '../const'
export default useCommerceCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = {
fetchOptions: {
query: getCheckoutQuery,
},
async fetcher({ input: { cartId }, options, fetch }) {
if (cartId) {
const { node: checkout } = await fetch({
...options,
variables: {
checkoutId: cartId,
},
})
if (checkout?.completedAt) {
Cookies.remove(SHOPIFY_CHECKOUT_ID_COOKIE)
Cookies.remove(SHOPIFY_CHECKOUT_URL_COOKIE)
return null
} else {
return checkoutToCart({
checkout,
})
}
}
return null
},
useHook:
({ useData }) =>
(input) => {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems.length ?? 0) <= 0
},
enumerable: true,
},
}),
[response]
)
},
}

View File

@@ -0,0 +1,67 @@
import { useCallback } from 'react'
import type {
MutationHookContext,
HookFetcherContext,
} from '@vercel/commerce/utils/types'
import { ValidationError } from '@vercel/commerce/utils/errors'
import useRemoveItem, {
UseRemoveItem,
} from '@vercel/commerce/cart/use-remove-item'
import type { Cart, LineItem, RemoveItemHook } from '../types/cart'
import useCart from './use-cart'
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
export type RemoveItemActionInput<T = any> = T extends LineItem
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
import {
checkoutLineItemRemoveMutation,
getCheckoutId,
checkoutToCart,
} from '../utils'
import { Mutation, MutationCheckoutLineItemsRemoveArgs } from '../../schema'
export const handler = {
fetchOptions: {
query: checkoutLineItemRemoveMutation,
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
const data = await fetch<Mutation, MutationCheckoutLineItemsRemoveArgs>({
...options,
variables: { checkoutId: getCheckoutId(), lineItemIds: [itemId] },
})
return checkoutToCart(data.checkoutLineItemsRemove)
},
useHook:
({ fetch }: MutationHookContext<RemoveItemHook>) =>
<T extends LineItem | undefined = undefined>(ctx: { item?: T } = {}) => {
const { item } = ctx
const { mutate } = useCart()
const removeItem: RemoveItemFn<LineItem> = async (input) => {
const itemId = input?.id ?? item?.id
if (!itemId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({ input: { itemId } })
await mutate(data, false)
return data
}
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
}

View File

@@ -0,0 +1,107 @@
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import type {
HookFetcherContext,
MutationHookContext,
} from '@vercel/commerce/utils/types'
import { ValidationError } from '@vercel/commerce/utils/errors'
import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/cart/use-update-item'
import useCart from './use-cart'
import { handler as removeItemHandler } from './use-remove-item'
import type { UpdateItemHook, LineItem } from '../types/cart'
import {
getCheckoutId,
checkoutLineItemUpdateMutation,
checkoutToCart,
} from '../utils'
import { Mutation, MutationCheckoutLineItemsUpdateArgs } from '../../schema'
export type UpdateItemActionInput<T = any> = T extends LineItem
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler = {
fetchOptions: {
query: checkoutLineItemUpdateMutation,
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateItemHook>) {
if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) {
return removeItemHandler.fetcher({
options: removeItemHandler.fetchOptions,
input: { itemId },
fetch,
})
}
} else if (item.quantity) {
throw new ValidationError({
message: 'The item quantity has to be a valid integer',
})
}
const { checkoutLineItemsUpdate } = await fetch<
Mutation,
MutationCheckoutLineItemsUpdateArgs
>({
...options,
variables: {
checkoutId: getCheckoutId(),
lineItems: [
{
id: itemId,
quantity: item.quantity,
},
],
},
})
return checkoutToCart(checkoutLineItemsUpdate)
},
useHook:
({ fetch }: MutationHookContext<UpdateItemHook>) =>
<T extends LineItem | undefined = undefined>(
ctx: {
item?: T
wait?: number
} = {}
) => {
const { item } = ctx
const { mutate } = useCart() as any
return useCallback(
debounce(async (input: UpdateItemActionInput<T>) => {
const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId
if (!itemId || !productId || !variantId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({
input: {
item: {
productId,
variantId,
quantity: input.quantity,
},
itemId,
},
})
await mutate(data, false)
return data
}, ctx.wait ?? 500),
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,16 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useCheckout, {
UseCheckout,
} from '@vercel/commerce/checkout/use-checkout'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ useData }) =>
async (input) => ({}),
}

View File

@@ -0,0 +1,7 @@
{
"provider": "shopify",
"features": {
"wishlist": false,
"customerAuth": true
}
}

View File

@@ -0,0 +1,13 @@
export const SHOPIFY_CHECKOUT_ID_COOKIE = 'shopify_checkoutId'
export const SHOPIFY_CHECKOUT_URL_COOKIE = 'shopify_checkoutUrl'
export const SHOPIFY_CUSTOMER_TOKEN_COOKIE = 'shopify_customerToken'
export const STORE_DOMAIN = process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN
export const SHOPIFY_COOKIE_EXPIRE = 30
export const API_URL = `https://${STORE_DOMAIN}/api/2021-07/graphql.json`
export const API_TOKEN = process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN

View File

@@ -0,0 +1,17 @@
import useAddItem, {
UseAddItem,
} from '@vercel/commerce/customer/address/use-add-item'
import { MutationHook } from '@vercel/commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
}

View File

@@ -0,0 +1,17 @@
import useAddItem, {
UseAddItem,
} from '@vercel/commerce/customer/card/use-add-item'
import { MutationHook } from '@vercel/commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
}

View File

@@ -0,0 +1 @@
export { default as useCustomer } from './use-customer'

View File

@@ -0,0 +1,36 @@
import useCustomer, {
UseCustomer,
} from '@vercel/commerce/customer/use-customer'
import type { CustomerHook } from '../types/customer'
import { SWRHook } from '@vercel/commerce/utils/types'
import { getCustomerQuery, getCustomerToken } from '../utils'
import { GetCustomerQuery, GetCustomerQueryVariables } from '../../schema'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<CustomerHook> = {
fetchOptions: {
query: getCustomerQuery,
},
async fetcher({ options, fetch }) {
const customerAccessToken = getCustomerToken()
if (customerAccessToken) {
const data = await fetch<GetCustomerQuery, GetCustomerQueryVariables>({
...options,
variables: { customerAccessToken: getCustomerToken() },
})
return data.customer
}
return null
},
useHook:
({ useData }) =>
(input) => {
return useData({
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
},
}

View File

@@ -0,0 +1,27 @@
import { Fetcher } from '@vercel/commerce/utils/types'
import { API_TOKEN, API_URL } from './const'
import { handleFetchResponse } from './utils'
const fetcher: Fetcher = async ({
url = API_URL,
method = 'POST',
variables,
query,
}) => {
const { locale, ...vars } = variables ?? {}
return handleFetchResponse(
await fetch(url, {
method,
body: JSON.stringify({ query, variables: vars }),
headers: {
'X-Shopify-Storefront-Access-Token': API_TOKEN!,
'Content-Type': 'application/json',
...(locale && {
'Accept-Language': locale,
}),
},
})
)
}
export default fetcher

View File

@@ -0,0 +1,12 @@
import {
getCommerceProvider,
useCommerce as useCoreCommerce,
} from '@vercel/commerce'
import { shopifyProvider, ShopifyProvider } from './provider'
export { shopifyProvider }
export type { ShopifyProvider }
export const CommerceProvider = getCommerceProvider(shopifyProvider)
export const useCommerce = () => useCoreCommerce<ShopifyProvider>()

View File

@@ -0,0 +1,8 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
images: {
domains: ['cdn.shopify.com'],
},
}

View File

@@ -0,0 +1,2 @@
export * from '@vercel/commerce/product/use-price'
export { default } from '@vercel/commerce/product/use-price'

View File

@@ -0,0 +1,91 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useSearch, { UseSearch } from '@vercel/commerce/product/use-search'
import {
CollectionEdge,
GetAllProductsQuery,
GetProductsFromCollectionQueryVariables,
Product as ShopifyProduct,
ProductEdge,
} from '../../schema'
import {
getAllProductsQuery,
getCollectionProductsQuery,
getSearchVariables,
normalizeProduct,
} from '../utils'
import type { SearchProductsHook } from '../types/product'
export type SearchProductsInput = {
search?: string
categoryId?: number
brandId?: number
sort?: string
locale?: string
}
export default useSearch as UseSearch<typeof handler>
export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: {
query: getAllProductsQuery,
},
async fetcher({ input, options, fetch }) {
const { categoryId, brandId } = input
const method = options?.method
const variables = getSearchVariables(input)
let products
// change the query to getCollectionProductsQuery when categoryId is set
if (categoryId) {
const data = await fetch<
CollectionEdge,
GetProductsFromCollectionQueryVariables
>({
query: getCollectionProductsQuery,
method,
variables,
})
// filter on client when brandId & categoryId are set since is not available on collection product query
products = brandId
? data.node?.products?.edges?.filter(
({ node: { vendor } }: ProductEdge) =>
vendor.replace(/\s+/g, '-').toLowerCase() === brandId
)
: data.node?.products?.edges
} else {
const data = await fetch<GetAllProductsQuery>({
query: options.query,
method,
variables,
})
products = data.products?.edges
}
return {
products: products?.map(({ node }) =>
normalizeProduct(node as ShopifyProduct)
),
found: !!products?.length,
}
},
useHook:
({ useData }) =>
(input = {}) => {
return useData({
input: [
['search', input.search],
['categoryId', input.categoryId],
['brandId', input.brandId],
['sort', input.sort],
['locale', input.locale],
],
swrOptions: {
revalidateOnFocus: false,
...input.swrOptions,
},
})
},
}

View File

@@ -0,0 +1,27 @@
import { SHOPIFY_CHECKOUT_ID_COOKIE } from './const'
import { handler as useCart } from './cart/use-cart'
import { handler as useAddItem } from './cart/use-add-item'
import { handler as useUpdateItem } from './cart/use-update-item'
import { handler as useRemoveItem } from './cart/use-remove-item'
import { handler as useCustomer } from './customer/use-customer'
import { handler as useSearch } from './product/use-search'
import { handler as useLogin } from './auth/use-login'
import { handler as useLogout } from './auth/use-logout'
import { handler as useSignup } from './auth/use-signup'
import fetcher from './fetcher'
export const shopifyProvider = {
locale: 'en-us',
cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE,
fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer },
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}
export type ShopifyProvider = typeof shopifyProvider

View File

@@ -0,0 +1,32 @@
import * as Core from '@vercel/commerce/types/cart'
export * from '@vercel/commerce/types/cart'
export type ShopifyCart = {}
/**
* Extend core cart types
*/
export type Cart = Core.Cart & {
lineItems: Core.LineItem[]
url?: string
}
export type CartTypes = Core.CartTypes
export type CartHooks = Core.CartHooks<CartTypes>
export type GetCartHook = CartHooks['getCart']
export type AddItemHook = CartHooks['addItem']
export type UpdateItemHook = CartHooks['updateItem']
export type RemoveItemHook = CartHooks['removeItem']
export type CartSchema = Core.CartSchema<CartTypes>
export type CartHandlers = Core.CartHandlers<CartTypes>
export type GetCartHandler = CartHandlers['getCart']
export type AddItemHandler = CartHandlers['addItem']
export type UpdateItemHandler = CartHandlers['updateItem']
export type RemoveItemHandler = CartHandlers['removeItem']

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/checkout'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/common'

View File

@@ -0,0 +1,5 @@
import * as Core from '@vercel/commerce/types/customer'
export * from '@vercel/commerce/types/customer'
export type CustomerSchema = Core.CustomerSchema

View File

@@ -0,0 +1,25 @@
import * as Cart from './cart'
import * as Checkout from './checkout'
import * as Common from './common'
import * as Customer from './customer'
import * as Login from './login'
import * as Logout from './logout'
import * as Page from './page'
import * as Product from './product'
import * as Signup from './signup'
import * as Site from './site'
import * as Wishlist from './wishlist'
export type {
Cart,
Checkout,
Common,
Customer,
Login,
Logout,
Page,
Product,
Signup,
Site,
Wishlist,
}

View File

@@ -0,0 +1,8 @@
import * as Core from '@vercel/commerce/types/login'
import type { CustomerAccessTokenCreateInput } from '../../schema'
export * from '@vercel/commerce/types/login'
export type LoginOperation = Core.LoginOperation & {
variables: CustomerAccessTokenCreateInput
}

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/logout'

View File

@@ -0,0 +1,11 @@
import * as Core from '@vercel/commerce/types/page'
export * from '@vercel/commerce/types/page'
export type Page = Core.Page
export type PageTypes = {
page: Page
}
export type GetAllPagesOperation = Core.GetAllPagesOperation<PageTypes>
export type GetPageOperation = Core.GetPageOperation<PageTypes>

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/product'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/signup'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/site'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/wishlist'

View File

@@ -0,0 +1,45 @@
import Cookies from 'js-cookie'
import {
SHOPIFY_CHECKOUT_ID_COOKIE,
SHOPIFY_CHECKOUT_URL_COOKIE,
SHOPIFY_COOKIE_EXPIRE,
} from '../const'
import checkoutCreateMutation from './mutations/checkout-create'
import {
CheckoutCreatePayload,
CheckoutLineItemInput,
Mutation,
MutationCheckoutCreateArgs,
} from '../../schema'
import { FetcherOptions } from '@vercel/commerce/utils/types'
export const checkoutCreate = async (
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
lineItems: CheckoutLineItemInput[]
): Promise<CheckoutCreatePayload> => {
const { checkoutCreate } = await fetch<Mutation, MutationCheckoutCreateArgs>({
query: checkoutCreateMutation,
variables: {
input: { lineItems },
},
})
const checkout = checkoutCreate?.checkout
if (checkout) {
const checkoutId = checkout?.id
const options = {
expires: SHOPIFY_COOKIE_EXPIRE,
}
Cookies.set(SHOPIFY_CHECKOUT_ID_COOKIE, checkoutId, options)
if (checkout?.webUrl) {
Cookies.set(SHOPIFY_CHECKOUT_URL_COOKIE, checkout.webUrl, options)
}
}
return checkoutCreate!
}
export default checkoutCreate

View File

@@ -0,0 +1,41 @@
import type { Cart } from '../types/cart'
import { CommerceError } from '@vercel/commerce/utils/errors'
import {
CheckoutLineItemsAddPayload,
CheckoutLineItemsRemovePayload,
CheckoutLineItemsUpdatePayload,
CheckoutCreatePayload,
CheckoutUserError,
Checkout,
Maybe,
} from '../../schema'
import { normalizeCart } from './normalize'
import throwUserErrors from './throw-user-errors'
export type CheckoutQuery = {
checkout: Checkout
checkoutUserErrors?: Array<CheckoutUserError>
}
export type CheckoutPayload =
| CheckoutLineItemsAddPayload
| CheckoutLineItemsUpdatePayload
| CheckoutLineItemsRemovePayload
| CheckoutCreatePayload
| CheckoutQuery
const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => {
throwUserErrors(checkoutPayload?.checkoutUserErrors)
if (!checkoutPayload?.checkout) {
throw new CommerceError({
message: 'Missing checkout object from response',
})
}
return normalizeCart(checkoutPayload?.checkout)
}
export default checkoutToCart

View File

@@ -0,0 +1,154 @@
export const colorMap: Record<string, string> = {
aliceblue: '#F0F8FF',
antiquewhite: '#FAEBD7',
aqua: '#00FFFF',
aquamarine: '#7FFFD4',
azure: '#F0FFFF',
beige: '#F5F5DC',
bisque: '#FFE4C4',
black: '#000000',
blanchedalmond: '#FFEBCD',
blue: '#0000FF',
blueviolet: '#8A2BE2',
brown: '#A52A2A',
burlywood: '#DEB887',
burgandy: '#800020',
burgundy: '#800020',
cadetblue: '#5F9EA0',
chartreuse: '#7FFF00',
chocolate: '#D2691E',
coral: '#FF7F50',
cornflowerblue: '#6495ED',
cornsilk: '#FFF8DC',
crimson: '#DC143C',
cyan: '#00FFFF',
darkblue: '#00008B',
darkcyan: '#008B8B',
darkgoldenrod: '#B8860B',
darkgray: '#A9A9A9',
darkgreen: '#006400',
darkgrey: '#A9A9A9',
darkkhaki: '#BDB76B',
darkmagenta: '#8B008B',
darkolivegreen: '#556B2F',
darkorange: '#FF8C00',
darkorchid: '#9932CC',
darkred: '#8B0000',
darksalmon: '#E9967A',
darkseagreen: '#8FBC8F',
darkslateblue: '#483D8B',
darkslategray: '#2F4F4F',
darkslategrey: '#2F4F4F',
darkturquoise: '#00CED1',
darkviolet: '#9400D3',
deeppink: '#FF1493',
deepskyblue: '#00BFFF',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1E90FF',
firebrick: '#B22222',
floralwhite: '#FFFAF0',
forestgreen: '#228B22',
fuchsia: '#FF00FF',
gainsboro: '#DCDCDC',
ghostwhite: '#F8F8FF',
gold: '#FFD700',
goldenrod: '#DAA520',
gray: '#808080',
green: '#008000',
greenyellow: '#ADFF2F',
grey: '#808080',
honeydew: '#F0FFF0',
hotpink: '#FF69B4',
indianred: '#CD5C5C',
indigo: '#4B0082',
ivory: '#FFFFF0',
khaki: '#F0E68C',
lavender: '#E6E6FA',
lavenderblush: '#FFF0F5',
lawngreen: '#7CFC00',
lemonchiffon: '#FFFACD',
lightblue: '#ADD8E6',
lightcoral: '#F08080',
lightcyan: '#E0FFFF',
lightgoldenrodyellow: '#FAFAD2',
lightgray: '#D3D3D3',
lightgreen: '#90EE90',
lightgrey: '#D3D3D3',
lightpink: '#FFB6C1',
lightsalmon: '#FFA07A',
lightseagreen: '#20B2AA',
lightskyblue: '#87CEFA',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#B0C4DE',
lightyellow: '#FFFFE0',
lime: '#00FF00',
limegreen: '#32CD32',
linen: '#FAF0E6',
magenta: '#FF00FF',
maroon: '#800000',
mediumaquamarine: '#66CDAA',
mediumblue: '#0000CD',
mediumorchid: '#BA55D3',
mediumpurple: '#9370DB',
mediumseagreen: '#3CB371',
mediumslateblue: '#7B68EE',
mediumspringgreen: '#00FA9A',
mediumturquoise: '#48D1CC',
mediumvioletred: '#C71585',
midnightblue: '#191970',
mintcream: '#F5FFFA',
mistyrose: '#FFE4E1',
moccasin: '#FFE4B5',
navajowhite: '#FFDEAD',
navy: '#000080',
oldlace: '#FDF5E6',
olive: '#808000',
olivedrab: '#6B8E23',
orange: '#FFA500',
orangered: '#FF4500',
orchid: '#DA70D6',
palegoldenrod: '#EEE8AA',
palegreen: '#98FB98',
paleturquoise: '#AFEEEE',
palevioletred: '#DB7093',
papayawhip: '#FFEFD5',
peachpuff: '#FFDAB9',
peru: '#CD853F',
pink: '#FFC0CB',
plum: '#DDA0DD',
powderblue: '#B0E0E6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#FF0000',
rosybrown: '#BC8F8F',
royalblue: '#4169E1',
saddlebrown: '#8B4513',
salmon: '#FA8072',
sandybrown: '#F4A460',
seagreen: '#2E8B57',
seashell: '#FFF5EE',
sienna: '#A0522D',
silver: '#C0C0C0',
skyblue: '#87CEEB',
slateblue: '#6A5ACD',
slategray: '#708090',
slategrey: '#708090',
spacegrey: '#65737e',
spacegray: '#65737e',
snow: '#FFFAFA',
springgreen: '#00FF7F',
steelblue: '#4682B4',
tan: '#D2B48C',
teal: '#008080',
thistle: '#D8BFD8',
tomato: '#FF6347',
turquoise: '#40E0D0',
violet: '#EE82EE',
wheat: '#F5DEB3',
white: '#FFFFFF',
whitesmoke: '#F5F5F5',
yellow: '#FFFF00',
yellowgreen: '#9ACD32',
}

View File

@@ -0,0 +1,21 @@
import Cookies, { CookieAttributes } from 'js-cookie'
import { SHOPIFY_COOKIE_EXPIRE, SHOPIFY_CUSTOMER_TOKEN_COOKIE } from '../const'
export const getCustomerToken = () => Cookies.get(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
export const setCustomerToken = (
token: string | null,
options?: CookieAttributes
) => {
if (!token) {
Cookies.remove(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
} else {
Cookies.set(
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
token,
options ?? {
expires: SHOPIFY_COOKIE_EXPIRE,
}
)
}
}

View File

@@ -0,0 +1,44 @@
import {
GetAllProductVendorsQuery,
GetAllProductVendorsQueryVariables,
} from '../../schema'
import { ShopifyConfig } from '../api'
import getAllProductVendors from './queries/get-all-product-vendors-query'
export type Brand = {
entityId: string
name: string
path: string
}
export type BrandEdge = {
node: Brand
}
export type Brands = BrandEdge[]
const getBrands = async (config: ShopifyConfig): Promise<BrandEdge[]> => {
const { data } = await config.fetch<
GetAllProductVendorsQuery,
GetAllProductVendorsQueryVariables
>(getAllProductVendors, {
variables: {
first: 250,
},
})
let vendorsStrings = data.products.edges.map(({ node: { vendor } }) => vendor)
return [...new Set(vendorsStrings)].map((v) => {
const id = v.replace(/\s+/g, '-').toLowerCase()
return {
node: {
entityId: id,
name: v,
path: `brands/${id}`,
},
}
})
}
export default getBrands

View File

@@ -0,0 +1,34 @@
import type { Category } from '../types/site'
import { ShopifyConfig } from '../api'
import { CollectionEdge } from '../../schema'
import { normalizeCategory } from './normalize'
import getSiteCollectionsQuery from './queries/get-all-collections-query'
const getCategories = async ({
fetch,
locale,
}: ShopifyConfig): Promise<Category[]> => {
const { data } = await fetch(
getSiteCollectionsQuery,
{
variables: {
first: 250,
},
},
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
return (
data.collections?.edges?.map(({ node }: CollectionEdge) =>
normalizeCategory(node)
) ?? []
)
}
export default getCategories

View File

@@ -0,0 +1,8 @@
import Cookies from 'js-cookie'
import { SHOPIFY_CHECKOUT_ID_COOKIE } from '../const'
const getCheckoutId = (id?: string) => {
return id ?? Cookies.get(SHOPIFY_CHECKOUT_ID_COOKIE)
}
export default getCheckoutId

View File

@@ -0,0 +1,31 @@
import getSortVariables from './get-sort-variables'
import { SearchProductsBody } from '../types/product'
export const getSearchVariables = ({
brandId,
search,
categoryId,
sort,
locale,
}: SearchProductsBody) => {
let query = ''
if (search) {
query += `product_type:${search} OR title:${search} OR tag:${search} `
}
if (brandId) {
query += `${search ? 'AND ' : ''}vendor:${brandId}`
}
return {
categoryId,
query,
...getSortVariables(sort, !!categoryId),
...(locale && {
locale,
}),
}
}
export default getSearchVariables

View File

@@ -0,0 +1,32 @@
const getSortVariables = (sort?: string, isCategory: boolean = false) => {
let output = {}
switch (sort) {
case 'price-asc':
output = {
sortKey: 'PRICE',
reverse: false,
}
break
case 'price-desc':
output = {
sortKey: 'PRICE',
reverse: true,
}
break
case 'trending-desc':
output = {
sortKey: 'BEST_SELLING',
reverse: false,
}
break
case 'latest-desc':
output = {
sortKey: isCategory ? 'CREATED' : 'CREATED_AT',
reverse: true,
}
break
}
return output
}
export default getSortVariables

View File

@@ -0,0 +1,30 @@
import { FetcherOptions } from '@vercel/commerce/utils/types'
import throwUserErrors from './throw-user-errors'
import {
MutationCustomerActivateArgs,
MutationCustomerActivateByUrlArgs,
} from '../../schema'
import { Mutation } from '../../schema'
import { customerActivateByUrlMutation } from './mutations'
const handleAccountActivation = async (
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
input: MutationCustomerActivateByUrlArgs
) => {
try {
const { customerActivateByUrl } = await fetch<
Mutation,
MutationCustomerActivateArgs
>({
query: customerActivateByUrlMutation,
variables: {
input,
},
})
throwUserErrors(customerActivateByUrl?.customerUserErrors)
} catch (error) {}
}
export default handleAccountActivation

View File

@@ -0,0 +1,27 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
export function getError(errors: any[] | null, status: number) {
errors = errors ?? [{ message: 'Failed to fetch Shopify API' }]
return new FetcherError({ errors, status })
}
export async function getAsyncError(res: Response) {
const data = await res.json()
return getError(data.errors, res.status)
}
const handleFetchResponse = async (res: Response) => {
if (res.ok) {
const { data, errors } = await res.json()
if (errors && errors.length) {
throw getError(errors, res.status)
}
return data
}
throw await getAsyncError(res)
}
export default handleFetchResponse

View File

@@ -0,0 +1,36 @@
import { FetcherOptions } from '@vercel/commerce/utils/types'
import { CustomerAccessTokenCreateInput } from '../../schema'
import { setCustomerToken } from './customer-token'
import { customerAccessTokenCreateMutation } from './mutations'
import throwUserErrors from './throw-user-errors'
const handleLogin = (data: any) => {
const response = data.customerAccessTokenCreate
throwUserErrors(response?.customerUserErrors)
const customerAccessToken = response?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
if (accessToken) {
setCustomerToken(accessToken)
}
return customerAccessToken
}
export const handleAutomaticLogin = async (
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
input: CustomerAccessTokenCreateInput
) => {
try {
const loginData = await fetch({
query: customerAccessTokenCreateMutation,
variables: {
input,
},
})
handleLogin(loginData)
} catch (error) {}
}
export default handleLogin

View File

@@ -0,0 +1,15 @@
export { default as handleFetchResponse } from './handle-fetch-response'
export { default as getSearchVariables } from './get-search-variables'
export { default as getSortVariables } from './get-sort-variables'
export { default as getBrands } from './get-brands'
export { default as getCategories } from './get-categories'
export { default as getCheckoutId } from './get-checkout-id'
export { default as checkoutCreate } from './checkout-create'
export { default as checkoutToCart } from './checkout-to-cart'
export { default as handleLogin, handleAutomaticLogin } from './handle-login'
export { default as handleAccountActivation } from './handle-account-activation'
export { default as throwUserErrors } from './throw-user-errors'
export * from './queries'
export * from './mutations'
export * from './normalize'
export * from './customer-token'

View File

@@ -0,0 +1,18 @@
const associateCustomerWithCheckoutMutation = /* GraphQl */ `
mutation associateCustomerWithCheckout($checkoutId: ID!, $customerAccessToken: String!) {
checkoutCustomerAssociateV2(checkoutId: $checkoutId, customerAccessToken: $customerAccessToken) {
checkout {
id
}
checkoutUserErrors {
code
field
message
}
customer {
id
}
}
}
`
export default associateCustomerWithCheckoutMutation

View File

@@ -0,0 +1,19 @@
import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutCreateMutation = /* GraphQL */ `
mutation checkoutCreate($input: CheckoutCreateInput = {}) {
checkoutCreate(input: $input) {
checkoutUserErrors {
code
field
message
}
checkout {
...checkoutDetails
}
}
}
${checkoutDetailsFragment}
`
export default checkoutCreateMutation

View File

@@ -0,0 +1,22 @@
import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutLineItemAddMutation = /* GraphQL */ `
mutation checkoutLineItemAdd(
$checkoutId: ID!
$lineItems: [CheckoutLineItemInput!]!
) {
checkoutLineItemsAdd(checkoutId: $checkoutId, lineItems: $lineItems) {
checkoutUserErrors {
code
field
message
}
checkout {
...checkoutDetails
}
}
}
${checkoutDetailsFragment}
`
export default checkoutLineItemAddMutation

View File

@@ -0,0 +1,21 @@
import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutLineItemRemoveMutation = /* GraphQL */ `
mutation checkoutLineItemRemove($checkoutId: ID!, $lineItemIds: [ID!]!) {
checkoutLineItemsRemove(
checkoutId: $checkoutId
lineItemIds: $lineItemIds
) {
checkoutUserErrors {
code
field
message
}
checkout {
...checkoutDetails
}
}
}
${checkoutDetailsFragment}
`
export default checkoutLineItemRemoveMutation

View File

@@ -0,0 +1,22 @@
import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutLineItemUpdateMutation = /* GraphQL */ `
mutation checkoutLineItemUpdate(
$checkoutId: ID!
$lineItems: [CheckoutLineItemUpdateInput!]!
) {
checkoutLineItemsUpdate(checkoutId: $checkoutId, lineItems: $lineItems) {
checkoutUserErrors {
code
field
message
}
checkout {
...checkoutDetails
}
}
}
${checkoutDetailsFragment}
`
export default checkoutLineItemUpdateMutation

View File

@@ -0,0 +1,16 @@
const customerAccessTokenCreateMutation = /* GraphQL */ `
mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) {
customerAccessTokenCreate(input: $input) {
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerAccessTokenCreateMutation

View File

@@ -0,0 +1,14 @@
const customerAccessTokenDeleteMutation = /* GraphQL */ `
mutation customerAccessTokenDelete($customerAccessToken: String!) {
customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
deletedAccessToken
deletedCustomerAccessTokenId
userErrors {
field
message
}
}
}
`
export default customerAccessTokenDeleteMutation

View File

@@ -0,0 +1,19 @@
const customerActivateByUrlMutation = /* GraphQL */ `
mutation customerActivateByUrl($activationUrl: URL!, $password: String!) {
customerActivateByUrl(activationUrl: $activationUrl, password: $password) {
customer {
id
}
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerActivateByUrlMutation

View File

@@ -0,0 +1,19 @@
const customerActivateMutation = /* GraphQL */ `
mutation customerActivate($id: ID!, $input: CustomerActivateInput!) {
customerActivate(id: $id, input: $input) {
customer {
id
}
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerActivateMutation

View File

@@ -0,0 +1,15 @@
const customerCreateMutation = /* GraphQL */ `
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
customerUserErrors {
code
field
message
}
customer {
id
}
}
}
`
export default customerCreateMutation

View File

@@ -0,0 +1,9 @@
export { default as customerCreateMutation } from './customer-create'
export { default as checkoutCreateMutation } from './checkout-create'
export { default as checkoutLineItemAddMutation } from './checkout-line-item-add'
export { default as checkoutLineItemUpdateMutation } from './checkout-line-item-update'
export { default as checkoutLineItemRemoveMutation } from './checkout-line-item-remove'
export { default as customerAccessTokenCreateMutation } from './customer-access-token-create'
export { default as customerAccessTokenDeleteMutation } from './customer-access-token-delete'
export { default as customerActivateMutation } from './customer-activate'
export { default as customerActivateByUrlMutation } from './customer-activate-by-url'

View File

@@ -0,0 +1,197 @@
import type { Page } from '../types/page'
import type { Product } from '../types/product'
import type { Cart, LineItem } from '../types/cart'
import type { Category } from '../types/site'
import {
Product as ShopifyProduct,
Checkout,
CheckoutLineItemEdge,
SelectedOption,
ImageConnection,
ProductVariantConnection,
MoneyV2,
ProductOption,
Page as ShopifyPage,
PageEdge,
Collection,
} from '../../schema'
import { colorMap } from './colors'
const money = ({ amount, currencyCode }: MoneyV2) => {
return {
value: +amount,
currencyCode,
}
}
const normalizeProductOption = ({
id,
name: displayName,
values,
}: ProductOption) => {
return {
__typename: 'MultipleChoiceOption',
id,
displayName: displayName.toLowerCase(),
values: values.map((value) => {
let output: any = {
label: value,
}
if (displayName.match(/colou?r/gi)) {
const mapedColor = colorMap[value.toLowerCase().replace(/ /g, '')]
if (mapedColor) {
output = {
...output,
hexColors: [mapedColor],
}
}
}
return output
}),
}
}
const normalizeProductImages = ({ edges }: ImageConnection) =>
edges?.map(({ node: { originalSrc: url, ...rest } }) => ({
url,
...rest,
}))
const normalizeProductVariants = ({ edges }: ProductVariantConnection) => {
return edges?.map(
({
node: {
id,
selectedOptions,
sku,
title,
priceV2,
compareAtPriceV2,
requiresShipping,
availableForSale,
},
}) => {
return {
id,
name: title,
sku: sku ?? id,
price: +priceV2.amount,
listPrice: +compareAtPriceV2?.amount,
requiresShipping,
availableForSale,
options: selectedOptions.map(({ name, value }: SelectedOption) => {
const options = normalizeProductOption({
id,
name,
values: [value],
})
return options
}),
}
}
)
}
export function normalizeProduct({
id,
title: name,
vendor,
images,
variants,
description,
descriptionHtml,
handle,
priceRange,
options,
metafields,
...rest
}: ShopifyProduct): Product {
return {
id,
name,
vendor,
path: `/${handle}`,
slug: handle?.replace(/^\/+|\/+$/g, ''),
price: money(priceRange?.minVariantPrice),
images: normalizeProductImages(images),
variants: variants ? normalizeProductVariants(variants) : [],
options: options
? options
.filter((o) => o.name !== 'Title') // By default Shopify adds a 'Title' name when there's only one option. We don't need it. https://community.shopify.com/c/Shopify-APIs-SDKs/Adding-new-product-variant-is-automatically-adding-quot-Default/td-p/358095
.map((o) => normalizeProductOption(o))
: [],
...(description && { description }),
...(descriptionHtml && { descriptionHtml }),
...rest,
}
}
export function normalizeCart(checkout: Checkout): Cart {
return {
id: checkout.id,
url: checkout.webUrl,
customerId: '',
email: '',
createdAt: checkout.createdAt,
currency: {
code: checkout.totalPriceV2?.currencyCode,
},
taxesIncluded: checkout.taxesIncluded,
lineItems: checkout.lineItems?.edges.map(normalizeLineItem),
lineItemsSubtotalPrice: +checkout.subtotalPriceV2?.amount,
subtotalPrice: +checkout.subtotalPriceV2?.amount,
totalPrice: checkout.totalPriceV2?.amount,
discounts: [],
}
}
function normalizeLineItem({
node: { id, title, variant, quantity },
}: CheckoutLineItemEdge): LineItem {
return {
id,
variantId: String(variant?.id),
productId: String(variant?.id),
name: `${title}`,
quantity,
variant: {
id: String(variant?.id),
sku: variant?.sku ?? '',
name: variant?.title!,
image: {
url: variant?.image?.originalSrc || '/product-img-placeholder.svg',
},
requiresShipping: variant?.requiresShipping ?? false,
price: variant?.priceV2?.amount,
listPrice: variant?.compareAtPriceV2?.amount,
},
path: String(variant?.product?.handle),
discounts: [],
options: variant?.title == 'Default Title' ? [] : variant?.selectedOptions,
}
}
export const normalizePage = (
{ title: name, handle, ...page }: ShopifyPage,
locale: string = 'en-US'
): Page => ({
...page,
url: `/${locale}/${handle}`,
name,
})
export const normalizePages = (edges: PageEdge[], locale?: string): Page[] =>
edges?.map((edge) => normalizePage(edge.node, locale))
export const normalizeCategory = ({
title: name,
handle,
id,
}: Collection): Category => ({
id,
name,
slug: handle,
path: `/${handle}`,
})

View File

@@ -0,0 +1,14 @@
const getSiteCollectionsQuery = /* GraphQL */ `
query getSiteCollections($first: Int!) {
collections(first: $first) {
edges {
node {
id
title
handle
}
}
}
}
`
export default getSiteCollectionsQuery

View File

@@ -0,0 +1,14 @@
export const getAllPagesQuery = /* GraphQL */ `
query getAllPages($first: Int = 250) {
pages(first: $first) {
edges {
node {
id
title
handle
}
}
}
}
`
export default getAllPagesQuery

View File

@@ -0,0 +1,17 @@
const getAllProductVendors = /* GraphQL */ `
query getAllProductVendors($first: Int = 250, $cursor: String) {
products(first: $first, after: $cursor) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
vendor
}
cursor
}
}
}
`
export default getAllProductVendors

View File

@@ -0,0 +1,17 @@
const getAllProductsPathsQuery = /* GraphQL */ `
query getAllProductPaths($first: Int = 250, $cursor: String) {
products(first: $first, after: $cursor) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
handle
}
cursor
}
}
}
`
export default getAllProductsPathsQuery

View File

@@ -0,0 +1,57 @@
export const productConnectionFragment = /* GraphQL */ `
fragment productConnection on ProductConnection {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
id
title
vendor
handle
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 1) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
originalSrc
altText
width
height
}
}
}
}
}
}
`
const getAllProductsQuery = /* GraphQL */ `
query getAllProducts(
$first: Int = 250
$query: String = ""
$sortKey: ProductSortKeys = RELEVANCE
$reverse: Boolean = false
) {
products(
first: $first
sortKey: $sortKey
reverse: $reverse
query: $query
) {
...productConnection
}
}
${productConnectionFragment}
`
export default getAllProductsQuery

View File

@@ -0,0 +1,70 @@
export const checkoutDetailsFragment = /* GraphQL */ `
fragment checkoutDetails on Checkout {
id
webUrl
subtotalPriceV2 {
amount
currencyCode
}
totalTaxV2 {
amount
currencyCode
}
totalPriceV2 {
amount
currencyCode
}
completedAt
createdAt
taxesIncluded
lineItems(first: 250) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
id
title
variant {
id
sku
title
selectedOptions {
name
value
}
image {
originalSrc
altText
width
height
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
product {
handle
}
}
quantity
}
}
}
}
`
const getCheckoutQuery = /* GraphQL */ `
query getCheckout($checkoutId: ID!) {
node(id: $checkoutId) {
...checkoutDetails
}
}
${checkoutDetailsFragment}
`
export default getCheckoutQuery

View File

@@ -0,0 +1,21 @@
import { productConnectionFragment } from './get-all-products-query'
const getCollectionProductsQuery = /* GraphQL */ `
query getProductsFromCollection(
$categoryId: ID!
$first: Int = 250
$sortKey: ProductCollectionSortKeys = RELEVANCE
$reverse: Boolean = false
) {
node(id: $categoryId) {
id
... on Collection {
products(first: $first, sortKey: $sortKey, reverse: $reverse) {
...productConnection
}
}
}
}
${productConnectionFragment}
`
export default getCollectionProductsQuery

View File

@@ -0,0 +1,8 @@
export const getCustomerQuery = /* GraphQL */ `
query getCustomerId($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
id
}
}
`
export default getCustomerQuery

View File

@@ -0,0 +1,16 @@
export const getCustomerQuery = /* GraphQL */ `
query getCustomer($customerAccessToken: String!) {
customer(customerAccessToken: $customerAccessToken) {
id
firstName
lastName
displayName
email
phone
tags
acceptsMarketing
createdAt
}
}
`
export default getCustomerQuery

View File

@@ -0,0 +1,14 @@
export const getPageQuery = /* GraphQL */ `
query getPage($id: ID!) {
node(id: $id) {
id
... on Page {
title
handle
body
bodySummary
}
}
}
`
export default getPageQuery

View File

@@ -0,0 +1,72 @@
const getProductQuery = /* GraphQL */ `
query getProductBySlug($slug: String!) {
productByHandle(handle: $slug) {
id
handle
availableForSale
title
productType
vendor
description
descriptionHtml
options {
id
name
values
}
priceRange {
maxVariantPrice {
amount
currencyCode
}
minVariantPrice {
amount
currencyCode
}
}
variants(first: 250) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
id
title
sku
availableForSale
requiresShipping
selectedOptions {
name
value
}
priceV2 {
amount
currencyCode
}
compareAtPriceV2 {
amount
currencyCode
}
}
}
}
images(first: 250) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
originalSrc
altText
width
height
}
}
}
}
}
`
export default getProductQuery

View File

@@ -0,0 +1,8 @@
const getSiteInfoQuery = /* GraphQL */ `
query getSiteInfo {
shop {
name
}
}
`
export default getSiteInfoQuery

Some files were not shown because too many files have changed in this diff Show More