Add Open Commerce Provider

Add Open Commerce Provider
This commit is contained in:
Chloe 2022-05-19 10:09:48 +07:00 committed by GitHub
commit faae6a1e02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
137 changed files with 28463 additions and 1600 deletions

View File

@ -5,7 +5,9 @@ import type {
HookFetcherContext, HookFetcherContext,
} from '@vercel/commerce/utils/types' } from '@vercel/commerce/utils/types'
import { ValidationError } from '@vercel/commerce/utils/errors' import { ValidationError } from '@vercel/commerce/utils/errors'
import useUpdateItem, { UseUpdateItem } from '@vercel/commerce/cart/use-update-item' import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/cart/use-update-item'
import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart' import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart'
import { handler as removeItemHandler } from './use-remove-item' import { handler as removeItemHandler } from './use-remove-item'
import useCart from './use-cart' import useCart from './use-cart'

View File

@ -15,7 +15,8 @@ A commerce provider is a headless e-commerce platform that integrates with the [
- Kibo Commerce ([packages/kibocommerce](../kibocommerce)) - Kibo Commerce ([packages/kibocommerce](../kibocommerce))
- Commerce.js ([packages/commercejs](../commercejs)) - Commerce.js ([packages/commercejs](../commercejs))
- SFCC - SalesForce Cloud Commerce ([packages/sfcc](../sfcc)) - SFCC - SalesForce Cloud Commerce ([packages/sfcc](../sfcc))
- Mailchimp Open Commerce ([packages/opencommerce](../opencommerce))
-
Adding a commerce provider means adding a new folder in `packages` with a folder structure like the next one: Adding a commerce provider means adding a new folder in `packages` with a folder structure like the next one:
- `src` - `src`

View File

@ -0,0 +1,5 @@
COMMERCE_PROVIDER=@vercel/commerce-opencommerce
OPENCOMMERCE_STOREFRONT_API_URL=
OPENCOMMERCE_PRIMARY_SHOP_ID=
OPENCOMMERCE_IMAGE_DOMAIN=

View File

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

View File

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

View File

@ -0,0 +1,56 @@
## Open Commerce Provider
**Preview:** https://commerce-pinkcloudvnn.vercel.app/
We are using this admin interface for demo: https://admin.open-commerce.io/
Storefront API URL: https://api.open-commerce.io/graphql
## Available Features
- Cart
- Search
- Custom Checkout
- Custom Navigation
## Steps to get started:
1. Duplicate `site/.env.template` and rename it to `site/.env.local`
2. Setup env variables related to open commerce provider, something looks like this:
```
COMMERCE_PROVIDER=@vercel/commerce-opencommerce
OPENCOMMERCE_STOREFRONT_API_URL=https://api.open-commerce.io/graphql
OPENCOMMERCE_PRIMARY_SHOP_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
OPENCOMMERCE_IMAGE_DOMAIN=api.open-commerce.io
```
**Note**: We can query the primary shop ID from the graphql api playground
3. Run `yarn` and `yarn dev` in the root folder
### Troubleshoot
https://github.com/reactioncommerce/commerce#troubleshoot
For more information about the repository, check out the README file here: https://github.com/reactioncommerce/commerce
## Custom Checkouts
The demo site use [Example Payment Plugin](https://github.com/reactioncommerce/api-plugin-payments-example) for processing payments. To make the checkout flow works as expected, some prerequisite steps need to be handled:
- Add and enable at least one flat-rate shipping option in the admin setting page. By default, we select the first shipping option to create the order
- Enable the `IOU Example` payment method in the admin setting page
- Fill in all the required fields in the `Add Shipping Address` step in the storefront
- After processing checkout, check the orders page in the admin interface to confirm the order has been successfully created
We can also use `Stripe` as the default payment method by following steps:
- Add and enable at least one flat-rate shipping option in the admin setting page. By default, we select the first shipping option to create the order
- Enable the `Stripe (SCA)` payment method in the admin setting page
- Add `OPENCOMMERCE_STRIPE_PUBLIC_API_KEY` env variable to the `site/.env.local` file
- Add `STRIPE_API_KEY` env variable to reaction API `.env` file with the private API key from Stripe
- Modify the `submit-checkout` api endpoint to include additional steps for Stripe payment (eg: create/confirm payment intent, pass payment intent id to placeOrder request body,...).
## Custom Navigation
By default NextJS only display two categories/tags of products in the navbar. This feature enables us to show the organized categories/tags as navigation in the admin to our storefront. Also we can leverage this feature to show URLs that are not relative to the shop (eg: admin page, documentation page, ...). This feature is enabled by default, we can turn it off anytime by modify the value in the `commerce.config.json` file.

View File

@ -0,0 +1,22 @@
{
"overwrite": true,
"schema": "http://localhost:3000/graphql",
"documents": [
{
"./src/api/**/*.ts": {
"noRequire": true
}
}
],
"generates": {
"./schema.d.ts": {
"plugins": ["typescript", "typescript-operations"]
},
"./schema.graphql": {
"plugins": ["schema-ast"]
}
},
"hooks": {
"afterAllFileWrite": ["prettier --write"]
}
}

1
packages/opencommerce/global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module '@components/checkout/context'

View File

@ -0,0 +1,84 @@
{
"name": "@vercel/commerce-opencommerce",
"version": "0.0.1",
"license": "MIT",
"scripts": {
"release": "taskr release",
"build": "taskr build",
"dev": "taskr",
"types": "tsc --emitDeclarationOnly",
"prettier-fix": "prettier --write .",
"generate": "graphql-codegen --config codegen.json"
},
"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"
],
"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.6.2",
"@graphql-codegen/schema-ast": "^2.4.1",
"@graphql-codegen/typescript": "2.4.8",
"@graphql-codegen/typescript-operations": "2.3.5",
"@taskr/clear": "^1.1.0",
"@taskr/esnext": "^1.1.0",
"@taskr/watch": "^1.1.0",
"@types/node": "^17.0.8",
"@types/react": "^17.0.38",
"lint-staged": "^12.1.7",
"next": "^12.0.8",
"prettier": "^2.5.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"taskr": "^1.1.0",
"taskr-swc": "^0.0.1",
"typescript": "^4.5.4"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx,json}": [
"prettier --write",
"git add"
]
}
}

8370
packages/opencommerce/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,76 @@
import { normalizeCart } from '../../../utils/normalize'
import getCartCookie from '../../utils/get-cart-cookie'
import addCartItemsMutation from '../../mutations/add-cart-item'
import createCartMutation from '../../mutations/create-cart'
import type { CartEndpoint } from '.'
const addItem: CartEndpoint['handlers']['addItem'] = async ({
res,
body: { cartId, item },
config,
req: { cookies },
}) => {
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
if (!item.quantity) item.quantity = 1
const variables = {
input: {
shopId: config.shopId,
items: [
{
productConfiguration: {
productId: item.productId,
productVariantId: item.variantId,
},
quantity: item.quantity,
price: {
amount: item.variant?.price,
currencyCode: item.currencyCode,
},
},
],
},
}
if (!cartId) {
const {
data: { createCart },
} = await config.fetch(createCartMutation, { variables })
res.setHeader('Set-Cookie', [
getCartCookie(
config.cartCookie,
createCart.cart._id,
config.cartCookieMaxAge
),
getCartCookie(
config.anonymousCartTokenCookie,
createCart.token,
config.cartCookieMaxAge
),
])
return res.status(200).json({ data: normalizeCart(createCart.cart) })
}
const {
data: { addCartItems },
} = await config.fetch(addCartItemsMutation, {
variables: {
input: {
items: variables.input.items,
cartId,
cartToken: cookies[config.anonymousCartTokenCookie],
},
},
})
return res.status(200).json({ data: normalizeCart(addCartItems.cart) })
}
export default addItem

View File

@ -0,0 +1,32 @@
import { normalizeCart } from '../../../utils/normalize'
import getAnonymousCartQuery from '../../queries/get-anonymous-cart'
import type { CartEndpoint } from '.'
// Return current cart info
const getCart: CartEndpoint['handlers']['getCart'] = async ({
res,
req: { cookies },
body: { cartId },
config,
}) => {
if (cartId && cookies[config.anonymousCartTokenCookie]) {
const {
data: { cart: rawAnonymousCart },
} = await config.fetch(getAnonymousCartQuery, {
variables: {
cartId,
cartToken: cookies[config.anonymousCartTokenCookie],
},
})
return res.status(200).json({
data: rawAnonymousCart ? normalizeCart(rawAnonymousCart) : null,
})
}
res.status(200).json({
data: null,
})
}
export default getCart

View File

@ -0,0 +1,26 @@
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
import type { CartSchema } from '../../../types/cart'
import type { OpenCommerceAPI } from '../../index'
import getCart from './get-cart'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CartAPI = GetAPISchema<OpenCommerceAPI, CartSchema>
export type CartEndpoint = CartAPI['endpoint']
export const handlers: CartEndpoint['handlers'] = {
addItem,
getCart,
updateItem,
removeItem,
}
const cartApi = createEndpoint<CartAPI>({
handler: cartEndpoint,
handlers,
})
export default cartApi

View File

@ -0,0 +1,39 @@
import { normalizeCart } from '../../../utils/normalize'
import getCartCookie from '../../utils/get-cart-cookie'
import removeCartItemsMutation from '../../mutations/remove-cart-item'
import type { CartEndpoint } from '.'
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
res,
body: { cartId, itemId },
config,
req: { cookies },
}) => {
if (!cartId || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const {
data: { removeCartItems },
} = await config.fetch(removeCartItemsMutation, {
variables: {
input: {
cartId,
cartItemIds: [itemId],
cartToken: cookies[config.anonymousCartTokenCookie],
},
},
})
res.setHeader(
'Set-Cookie',
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
)
res.status(200).json({ data: normalizeCart(removeCartItems.cart) })
}
export default removeItem

View File

@ -0,0 +1,39 @@
import { normalizeCart } from '../../../utils/normalize'
import getCartCookie from '../../utils/get-cart-cookie'
import updateCartItemsQuantityMutation from '../../mutations/update-cart-item-quantity'
import type { CartEndpoint } from '.'
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
res,
body: { cartId, itemId, item },
config,
req: { cookies },
}) => {
if (!cartId || !itemId || !item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const {
data: { updateCartItemsQuantity },
} = await config.fetch(updateCartItemsQuantityMutation, {
variables: {
updateCartItemsQuantityInput: {
cartId,
cartToken: cookies[config.anonymousCartTokenCookie],
items: [{ cartItemId: itemId, quantity: item.quantity }],
},
},
})
// Update the cart cookie
res.setHeader(
'Set-Cookie',
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
)
res.status(200).json({ data: normalizeCart(updateCartItemsQuantity.cart) })
}
export default updateItem

View File

@ -0,0 +1,56 @@
import type { ProductsEndpoint } from './products'
import getSearchVariables from '../../utils/get-search-variables'
import getSortVariables from '../../utils/get-sort-variables'
import { CatalogItemsQueryVariables } from '../../../../schema'
import getShopCurrencyQuery from '../../queries/get-shop-currency-query'
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
body: { brandId, search, sort, categoryId },
res,
config,
commerce,
}) => {
let sortParams = getSortVariables(sort)
if (sortParams?.sortBy === 'featured' && !categoryId) {
sortParams = null
}
let currency: string | null = null
if (sortParams?.sortBy === 'minPrice') {
const {
data: {
shop: {
currency: { code },
},
},
} = await config.fetch(getShopCurrencyQuery, {
variables: { id: config.shopId },
})
currency = code
}
const { products } = await commerce.getAllProducts({
variables: {
...getSearchVariables({ brandId, search, categoryId }),
...(sortParams
? {
sortBy: sortParams.sortBy as CatalogItemsQueryVariables['sortBy'],
sortOrder:
sortParams.sortOrder as CatalogItemsQueryVariables['sortOrder'],
}
: {}),
...(currency ? { sortByPriceCurrencyCode: currency } : {}),
},
config,
})
res.status(200).json({
data: {
products,
found: !!products.length,
},
})
}
export default getProducts

View File

@ -0,0 +1,20 @@
import { CommerceAPI, createEndpoint, GetAPISchema } from '@vercel/commerce/api'
import productsEndpoint from '@vercel/commerce/api/endpoints/catalog/products'
import type { ProductsSchema } from '../../../types/product'
import type { OpenCommerceAPI } from '../../index'
import getProducts from './get-products'
export type ProductsAPI = GetAPISchema<OpenCommerceAPI, ProductsSchema>
export type ProductsEndpoint = ProductsAPI['endpoint']
export const handlers: ProductsEndpoint['handlers'] = {
getProducts,
}
const productsApi = createEndpoint<ProductsAPI>({
handler: productsEndpoint,
handlers,
})
export default productsApi

View File

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

View File

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

View File

@ -0,0 +1,65 @@
import { LineItem } from '../../../types/cart'
import placeOrder from '../../mutations/place-order'
import setEmailOnAnonymousCart from '../../mutations/set-email-on-anonymous-cart'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CheckoutEndpoint } from '.'
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
res,
body: { item, cartId },
config: { fetch, shopId, anonymousCartTokenCookie, cartCookie },
req: { cookies },
}) => {
await fetch(setEmailOnAnonymousCart, {
variables: {
input: {
cartId,
cartToken: cookies[anonymousCartTokenCookie],
email: 'opencommerce@test.com',
},
},
})
const { data } = await fetch(placeOrder, {
variables: {
input: {
payments: {
data: { fullName: 'Open Commerce Demo Site' },
amount: item.checkout.cart.checkout.summary.total.amount,
method: 'iou_example',
},
order: {
cartId,
currencyCode: item.checkout.cart.currency.code,
email: 'opencommerce@test.com',
shopId,
fulfillmentGroups: {
shopId,
data: item.checkout.cart.checkout.fulfillmentGroups[0].data,
items: item.checkout.cart.lineItems.map((item: LineItem) => ({
price: item.variant.price,
quantity: item.quantity,
productConfiguration: {
productId: item.productId,
productVariantId: item.variantId,
},
})),
type: item.checkout.cart.checkout.fulfillmentGroups[0].type,
selectedFulfillmentMethodId:
item.checkout.cart.checkout.fulfillmentGroups[0]
.selectedFulfillmentOption.fulfillmentMethod._id,
},
},
},
},
})
res.setHeader('Set-Cookie', [
getCartCookie(cartCookie),
getCartCookie(anonymousCartTokenCookie),
])
res.status(200).json({ data: null, errors: [] })
}
export default submitCheckout

View File

@ -0,0 +1,73 @@
import setShippingAddressOnCartMutation from '../../../mutations/add-shipping-address'
import type { CustomerAddressEndpoint } from '.'
import updateFulfillmentOptions from '../../../mutations/update-fulfillment-options'
import selectFulfillmentOptions from '../../../mutations/select-fulfillment-options'
const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
res,
body: { item, cartId },
config: { fetch, anonymousCartTokenCookie },
req: { cookies },
}) => {
// Return an error if no cart is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
// Register address
const {
data: { setShippingAddressOnCart },
} = await fetch(setShippingAddressOnCartMutation, {
variables: {
input: {
address: {
address1: item.streetNumber || 'NextJS storefront',
country: item.country,
fullName: `${item.firstName || 'Test'} ${
item.lastName || 'Account'
}}`,
city: item.city || 'LA',
phone: '0123456789',
postal: item.zipCode || '1234567',
region: item.city || 'LA',
},
cartId,
cartToken: cookies[anonymousCartTokenCookie],
},
},
})
const {
data: { updateFulfillmentOptionsForGroup },
} = await fetch(updateFulfillmentOptions, {
variables: {
input: {
cartId,
fulfillmentGroupId:
setShippingAddressOnCart.cart.checkout.fulfillmentGroups[0]._id,
},
},
})
await fetch(selectFulfillmentOptions, {
variables: {
input: {
cartId,
cartToken: cookies[anonymousCartTokenCookie],
fulfillmentGroupId:
updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups[0]
._id,
fulfillmentMethodId:
updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups[0]
.availableFulfillmentOptions[0].fulfillmentMethod._id,
},
},
})
return res.status(200).json({ data: null, errors: [] })
}
export default addItem

View File

@ -0,0 +1,9 @@
import type { CustomerAddressEndpoint } from '.'
const getCards: CustomerAddressEndpoint['handlers']['getAddresses'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default getCards

View File

@ -0,0 +1,33 @@
import type {
CustomerAddressSchema,
CustomerAddressTypes,
} from '../../../../types/customer/address'
import type { OpenCommerceAPI } from '../../..'
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import customerAddressEndpoint from '@vercel/commerce/api/endpoints/customer/address'
import getAddresses from './get-addresses'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CustomerAddressAPI = GetAPISchema<
OpenCommerceAPI,
CustomerAddressSchema<CustomerAddressTypes>
>
export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint']
export const handlers: CustomerAddressEndpoint['handlers'] = {
getAddresses,
addItem,
updateItem,
removeItem,
}
const customerAddressApi = createEndpoint<CustomerAddressAPI>({
handler: customerAddressEndpoint,
handlers,
})
export default customerAddressApi

View File

@ -0,0 +1,9 @@
import type { CustomerAddressEndpoint } from '.'
const removeItem: CustomerAddressEndpoint['handlers']['removeItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default removeItem

View File

@ -0,0 +1,34 @@
import selectFulfillmentOptions from '../../../mutations/select-fulfillment-options'
import type { CustomerAddressEndpoint } from '.'
const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = async ({
res,
body: { item, cartId },
config: { fetch, anonymousCartTokenCookie },
req: { cookies },
}) => {
// Return an error if no cart is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
if (item.shippingMethod) {
await fetch(selectFulfillmentOptions, {
variables: {
input: {
cartId,
cartToken: cookies[anonymousCartTokenCookie],
fulfillmentGroupId: item.shippingMethod.fulfillmentGroupId,
fulfillmentMethodId: item.shippingMethod.id,
},
},
})
}
return res.status(200).json({ data: null, errors: [] })
}
export default updateItem

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 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,47 @@
import {
CommerceAPI,
CommerceAPIConfig,
getCommerceApi as commerceApi,
} from '@vercel/commerce/api'
import createFetchGraphqlApi from './utils/fetch-grapql-api'
import * as operations from './operations'
const API_URL = process.env.OPENCOMMERCE_STOREFRONT_API_URL
const SHOP_ID = process.env.OPENCOMMERCE_PRIMARY_SHOP_ID
if (!API_URL) {
throw new Error(
`The environment variable OPENCOMMERCE_STOREFRONT_API_URL is missing and it's required to access your store`
)
}
export interface OpenCommerceConfig extends CommerceAPIConfig {
shopId: string
anonymousCartTokenCookie: string
}
const ONE_DAY = 60 * 60 * 24
const config: OpenCommerceConfig = {
commerceUrl: API_URL,
apiToken: '',
shopId: SHOP_ID ?? '',
customerCookie: 'opencommerce_customerToken',
cartCookie: 'opencommerce_cartId',
cartCookieMaxAge: ONE_DAY * 30,
anonymousCartTokenCookie: 'opencommerce_anonymousCartToken',
fetch: createFetchGraphqlApi(() => getCommerceApi().getConfig()),
}
export const provider = { config, operations }
export type Provider = typeof provider
export type OpenCommerceAPI<P extends Provider = Provider> = CommerceAPI<P>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): OpenCommerceAPI<P> {
return commerceApi(customProvider)
}

View File

@ -0,0 +1,24 @@
import {
cartPayloadFragment,
incorrectPriceFailureDetailsFragment,
minOrderQuantityFailureDetailsFragment,
} from '../queries/get-cart-query'
const addCartItemsMutation = /* GraphQL */ `
mutation addCartItemsMutation($input: AddCartItemsInput!) {
addCartItems(input: $input) {
cart {
${cartPayloadFragment}
}
incorrectPriceFailures {
${incorrectPriceFailureDetailsFragment}
}
minOrderQuantityFailures {
${minOrderQuantityFailureDetailsFragment}
}
clientMutationId
}
}
`
export default addCartItemsMutation

View File

@ -0,0 +1,13 @@
import { cartPayloadFragment } from '../queries/get-cart-query'
const setShippingAddressOnCartMutation = `
mutation setShippingAddressOnCartMutation($input: SetShippingAddressOnCartInput!) {
setShippingAddressOnCart(input: $input) {
cart {
${cartPayloadFragment}
}
}
}
`
export default setShippingAddressOnCartMutation

View File

@ -0,0 +1,15 @@
const authenticateMutation = /* GraphQL */ `
mutation authenticate(
$serviceName: String!
$params: AuthenticateParamsInput!
) {
authenticate(serviceName: $serviceName, params: $params) {
sessionId
tokens {
refreshToken
accessToken
}
}
}
`
export default authenticateMutation

View File

@ -0,0 +1,24 @@
import {
cartPayloadFragment,
incorrectPriceFailureDetailsFragment,
minOrderQuantityFailureDetailsFragment,
} from '../queries/get-cart-query'
const createCartMutation = /* GraphQL */ `
mutation createCartMutation($input: CreateCartInput!) {
createCart(input: $input) {
cart {
${cartPayloadFragment}
}
incorrectPriceFailures {
${incorrectPriceFailureDetailsFragment}
}
minOrderQuantityFailures {
${minOrderQuantityFailureDetailsFragment}
}
clientMutationId
token
}
}
`
export default createCartMutation

View File

@ -0,0 +1,211 @@
const orderCommon = `
_id
account {
_id
}
cartId
createdAt
displayStatus(language: $language)
email
fulfillmentGroups {
_id
data {
... on ShippingOrderFulfillmentGroupData {
shippingAddress {
_id
address1
address2
city
company
country
fullName
isCommercial
isShippingDefault
phone
postal
region
}
}
}
items {
nodes {
_id
addedAt
createdAt
imageURLs {
large
medium
original
small
thumbnail
}
isTaxable
optionTitle
parcel {
containers
distanceUnit
height
length
massUnit
weight
width
}
price {
amount
currency {
code
}
displayAmount
}
productConfiguration {
productId
productVariantId
}
productSlug
productType
productVendor
productTags {
nodes {
name
}
}
quantity
shop {
_id
}
subtotal {
amount
currency {
code
}
displayAmount
}
taxCode
title
updatedAt
variantTitle
}
}
selectedFulfillmentOption {
fulfillmentMethod {
_id
carrier
displayName
fulfillmentTypes
group
name
}
handlingPrice {
amount
currency {
code
}
displayAmount
}
price {
amount
currency {
code
}
displayAmount
}
}
shop {
_id
}
summary {
fulfillmentTotal {
amount
displayAmount
}
itemTotal {
amount
displayAmount
}
surchargeTotal {
amount
displayAmount
}
taxTotal {
amount
displayAmount
}
total {
amount
displayAmount
}
}
tracking
type
}
payments {
_id
amount {
displayAmount
}
billingAddress {
address1
address2
city
company
country
fullName
isCommercial
phone
postal
region
}
displayName
method {
name
}
}
referenceId
shop {
_id
currency {
code
}
}
status
summary {
fulfillmentTotal {
amount
displayAmount
}
itemTotal {
amount
displayAmount
}
surchargeTotal {
amount
displayAmount
}
taxTotal {
amount
displayAmount
}
total {
amount
displayAmount
}
}
totalItemQuantity
updatedAt
`
const placeOrder = /* GraphQL */ `
mutation placeOrderMutation(
$input: PlaceOrderInput!
$language: String! = "en"
) {
placeOrder(input: $input) {
orders {
${orderCommon}
}
token
}
}
`
export default placeOrder

View File

@ -0,0 +1,13 @@
import { cartPayloadFragment } from '../queries/get-cart-query'
const removeCartItemsMutation = `
mutation removeCartItemsMutation($input: RemoveCartItemsInput!) {
removeCartItems(input: $input) {
cart {
${cartPayloadFragment}
}
}
}
`
export default removeCartItemsMutation

View File

@ -0,0 +1,14 @@
import { cartPayloadFragment } from '../queries/get-cart-query'
const selectFulfillmentOptions = /* GraphQL */ `
mutation setFulfillmentOptionCartMutation(
$input: SelectFulfillmentOptionForGroupInput!
) {
selectFulfillmentOptionForGroup(input: $input) {
cart {
${cartPayloadFragment}
}
}
}
`
export default selectFulfillmentOptions

View File

@ -0,0 +1,15 @@
import { cartPayloadFragment } from '../queries/get-cart-query'
const setEmailOnAnonymousCart = /* GraphQL */ `
mutation setEmailOnAnonymousCartMutation(
$input: SetEmailOnAnonymousCartInput!
) {
setEmailOnAnonymousCart(input: $input) {
cart {
${cartPayloadFragment}
}
}
}
`
export default setEmailOnAnonymousCart

View File

@ -0,0 +1,13 @@
import { cartPayloadFragment } from '../queries/get-cart-query'
const updateCartItemsQuantityMutation = /* GraphQL */ `
mutation updateCartItemsQuantity($updateCartItemsQuantityInput: UpdateCartItemsQuantityInput!) {
updateCartItemsQuantity(input: $updateCartItemsQuantityInput) {
cart {
${cartPayloadFragment}
}
}
}
`
export default updateCartItemsQuantityMutation

View File

@ -0,0 +1,15 @@
import { cartPayloadFragment } from '../queries/get-cart-query'
const updateFulfillmentOptions = /* GraphQL */ `
mutation updateFulfillmentOptionsForGroup(
$input: UpdateFulfillmentOptionsForGroupInput!
) {
updateFulfillmentOptionsForGroup(input: $input) {
cart {
${cartPayloadFragment}
}
}
}
`
export default updateFulfillmentOptions

View File

@ -0,0 +1,47 @@
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import { GetAllPagesOperation } from '../../types/page'
import type { OpenCommerceConfig, Provider } from '../index'
export type Page = { url: string }
export type GetAllPagesResult = { pages: Page[] }
export default function getAllPagesOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllPages<T extends GetAllPagesOperation>(opts?: {
config?: Partial<OpenCommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>(
opts: {
config?: Partial<OpenCommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>(
opts: {
config?: Partial<OpenCommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>({
config,
preview,
}: {
url?: string
config?: Partial<OpenCommerceConfig>
preview?: boolean
} = {}): Promise<GetAllPagesResult> {
return Promise.resolve({
pages: [],
})
}
return getAllPages
}

View File

@ -0,0 +1,63 @@
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import type {
CatalogItemsQuery,
CatalogItemsQueryVariables,
CatalogItemProduct,
} from '../../../schema'
import type { GetAllProductPathsOperation } from '../../types/product'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
import { OpenCommerceConfig, Provider } from '..'
import getAllProductPathsQuery from '../queries/get-all-product-paths-query'
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<
T extends GetAllProductPathsOperation
>(opts?: {
variables?: CatalogItemsQueryVariables
config?: OpenCommerceConfig
}): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
opts: {
variables?: CatalogItemsQueryVariables
config?: OpenCommerceConfig
} & OperationOptions
): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
query = getAllProductPathsQuery,
variables,
config,
}: {
query?: string
variables?: CatalogItemsQueryVariables
config?: OpenCommerceConfig
} = {}): Promise<T['data']> {
const { fetch, shopId } = commerce.getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await fetch<
RecursivePartial<CatalogItemsQuery>,
CatalogItemsQueryVariables
>(query, {
variables: { ...variables, shopIds: [shopId] },
})
const products = data.catalogItems?.edges
return {
products: filterEdges(products as RecursiveRequired<typeof products>).map(
({ node }) => ({
path: `/${(node as CatalogItemProduct).product!.slug}`,
})
),
}
}
return getAllProductPaths
}

View File

@ -0,0 +1,62 @@
import type { OperationContext } from '@vercel/commerce/api/operations'
import type { GetAllProductsOperation } from '../../types/product'
import {
CatalogItemProduct,
CatalogItemsQuery,
CatalogItemsQueryVariables,
} from '../../../schema'
import catalogItemsQuery from '../queries/get-all-products-query'
import type { OpenCommerceConfig, Provider } from '..'
import { normalizeProduct } from '../../utils/normalize'
import { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>(opts?: {
variables?: Omit<CatalogItemsQueryVariables, 'shopIds'>
config?: Partial<OpenCommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getAllProducts<T extends GetAllProductsOperation>({
query = catalogItemsQuery,
variables,
config,
}: {
query?: string
variables?: Omit<CatalogItemsQueryVariables, 'shopIds'>
config?: Partial<OpenCommerceConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const { fetch, locale, shopId } = commerce.getConfig(config)
const { data } = await fetch<
RecursivePartial<CatalogItemsQuery>,
CatalogItemsQueryVariables
>(
query,
{ variables: { ...variables, shopIds: [shopId] } },
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
const edges = data.catalogItems?.edges
return {
products: filterEdges(edges as RecursiveRequired<typeof edges>).map(
(item) =>
normalizeProduct(
item?.node ? (item.node as CatalogItemProduct) : null
)
),
}
}
return getAllProducts
}

View File

@ -0,0 +1,6 @@
export default function getCustomerWishlistOperation() {
function getCustomerWishlist(): any {
return { wishlist: {} }
}
return getCustomerWishlist
}

View File

@ -0,0 +1,32 @@
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import { GetPageOperation } from '../../types/page'
import { Provider, OpenCommerceConfig } from '..'
type Page = any
type GetPageResult = { page?: Page }
export default function getPageOperation({
commerce,
}: OperationContext<Provider>) {
async function getPage<T extends GetPageOperation>(opts: {
variables: T['variables']
config?: Partial<OpenCommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getPage<T extends GetPageOperation>(
opts: {
variables: T['variables']
config?: Partial<OpenCommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
function getPage(): Promise<GetPageResult> {
return Promise.resolve({})
}
return getPage
}

View File

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

View File

@ -0,0 +1,78 @@
import {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import {
GetTagsQuery,
GetAllProductVendorsQuery,
GetTagsQueryVariables,
GetAllProductVendorsQueryVariables,
} from '../../../schema'
import getTagsQuery from '../queries/get-tags-query'
import { GetSiteInfoOperation, OCCategory, SiteTypes } from '../../types/site'
import {
normalizeCategory,
normalizeNavigation,
normalizeVendors,
} from '../../utils/normalize'
import type { OpenCommerceConfig, Provider } from '..'
import filterEdges from '../utils/filter-edges'
import getAllProductVendors from '../queries/get-vendors-query'
import getPrimaryShopQuery from '../queries/get-primary-shop-query'
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo<T extends GetSiteInfoOperation>(opts?: {
config?: Partial<OpenCommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getSiteInfo<T extends GetSiteInfoOperation>(
opts: {
config?: Partial<OpenCommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getSiteInfo<T extends GetSiteInfoOperation>({
config: cfg,
}: {
query?: string
config?: Partial<OpenCommerceConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const { fetch, shopId } = commerce.getConfig(cfg)
const [categoriesResponse, vendorsResponse, primaryShopResponse] =
await Promise.all([
await fetch<GetTagsQuery, GetTagsQueryVariables>(getTagsQuery, {
variables: { first: 250, shopId },
}),
await fetch<
GetAllProductVendorsQuery,
GetAllProductVendorsQueryVariables
>(getAllProductVendors, { variables: { shopIds: [shopId] } }),
await fetch(getPrimaryShopQuery),
])
const categories = filterEdges(categoriesResponse.data.tags?.edges).map(
(edge) => normalizeCategory(edge.node! as OCCategory)
)
const brands = [
...new Set(filterEdges(vendorsResponse.data.vendors?.nodes)),
].map(normalizeVendors)
const navigationItems =
primaryShopResponse.data.primaryShop.defaultNavigationTree.items ?? []
return {
categories,
brands,
navigation: normalizeNavigation(navigationItems),
}
}
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,53 @@
import type { ServerResponse } from 'http'
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import type { LoginOperation } from '../../types/login'
import type { AuthenticateMutation } from '../../../schema'
import type { RecursivePartial } from '../utils/types'
import loginMutation from '../mutations/authenticate'
import type { OpenCommerceConfig, Provider } from '..'
export default function loginOperation({
commerce,
}: OperationContext<Provider>) {
async function login<T extends LoginOperation>(opts: {
variables: T['variables']
config?: OpenCommerceConfig
res: ServerResponse
}): Promise<T['data']>
async function login<T extends LoginOperation>(
opts: {
variables: T['variables']
config?: OpenCommerceConfig
res: ServerResponse
} & OperationOptions
): Promise<T['data']>
async function login<T extends LoginOperation>({
query = loginMutation,
variables,
res: response,
config: cfg,
}: {
query?: string
variables: T['variables']
res: ServerResponse
config?: OpenCommerceConfig
}): Promise<T['data']> {
const { fetch } = commerce.getConfig(cfg)
const { data } = await fetch<RecursivePartial<AuthenticateMutation>>(
query,
{ variables }
)
return {
result: data.authenticate?.tokens?.accessToken ?? undefined,
}
}
return login
}

View File

@ -0,0 +1,25 @@
const getAllProductsPathsQuery = `
query catalogItems(
$first: ConnectionLimitInt = 250
$sortBy: CatalogItemSortByField = updatedAt
$shopIds: [ID]!
) {
catalogItems(first: $first, sortBy: $sortBy, shopIds: $shopIds) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
... on CatalogItemProduct {
product {
slug
}
}
}
cursor
}
}
}
`
export default getAllProductsPathsQuery

View File

@ -0,0 +1,66 @@
const catalogItemsQuery = /* GraphQL */ `
query catalogItems(
$first: ConnectionLimitInt = 250
$sortBy: CatalogItemSortByField = updatedAt
$sortOrder: SortOrder = desc
$sortByPriceCurrencyCode: String
$tagIds: [ID]
$shopIds: [ID]!
$searchQuery: String
) {
catalogItems(
first: $first
sortBy: $sortBy
sortOrder: $sortOrder
tagIds: $tagIds
shopIds: $shopIds
searchQuery: $searchQuery
sortByPriceCurrencyCode: $sortByPriceCurrencyCode
) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
_id
... on CatalogItemProduct {
product {
_id
title
slug
description
vendor
isLowQuantity
isSoldOut
isBackorder
shop {
currency {
code
}
}
pricing {
currency {
code
}
displayPrice
minPrice
maxPrice
}
media {
URLs {
thumbnail
small
medium
large
original
}
}
}
}
}
}
}
}
`
export default catalogItemsQuery

View File

@ -0,0 +1,11 @@
import { cartQueryFragment } from './get-cart-query'
export const getAnonymousCart = `
query anonymousCartByCartIdQuery($cartId: ID!, $cartToken: String!, $itemsAfterCursor: ConnectionCursor) {
cart: anonymousCartByCartId(cartId: $cartId, cartToken: $cartToken) {
${cartQueryFragment}
}
}
`
export default getAnonymousCart

View File

@ -0,0 +1,11 @@
import { cartQueryFragment } from './get-cart-query'
const accountCartByAccountIdQuery = `
query accountCartByAccountIdQuery($accountId: ID!, $shopId: ID!, $itemsAfterCursor: ConnectionCursor) {
cart: accountCartByAccountId(accountId: $accountId, shopId: $shopId) {
${cartQueryFragment}
}
}
`
export default accountCartByAccountIdQuery

View File

@ -0,0 +1,241 @@
export const cartCommon = `
_id
createdAt
account {
_id
emailRecords {
address
}
}
shop {
_id
currency {
code
}
}
email
updatedAt
expiresAt
checkout {
fulfillmentGroups {
_id
type
data {
shippingAddress {
address1
address2
city
company
country
fullName
isBillingDefault
isCommercial
isShippingDefault
phone
postal
region
}
}
availableFulfillmentOptions {
price {
amount
displayAmount
}
fulfillmentMethod {
_id
name
displayName
}
}
selectedFulfillmentOption {
fulfillmentMethod {
_id
name
displayName
}
price {
amount
displayAmount
}
handlingPrice {
amount
displayAmount
}
}
shop {
_id
}
shippingAddress {
address1
address2
city
company
country
fullName
isBillingDefault
isCommercial
isShippingDefault
phone
postal
region
}
}
summary {
fulfillmentTotal {
displayAmount
}
itemTotal {
amount
displayAmount
}
surchargeTotal {
amount
displayAmount
}
taxTotal {
amount
displayAmount
}
total {
amount
currency {
code
}
displayAmount
}
}
}
totalItemQuantity
`
const cartItemConnectionFragment = `
pageInfo {
hasNextPage
endCursor
}
edges {
node {
_id
productConfiguration {
productId
productVariantId
}
addedAt
attributes {
label
value
}
createdAt
isBackorder
isLowQuantity
isSoldOut
imageURLs {
large
small
original
medium
thumbnail
}
metafields {
value
key
}
parcel {
length
width
weight
height
}
price {
amount
displayAmount
currency {
code
}
}
priceWhenAdded {
amount
displayAmount
currency {
code
}
}
productSlug
productType
quantity
shop {
_id
}
subtotal {
displayAmount
}
title
productTags {
nodes {
name
}
}
productVendor
variantTitle
optionTitle
updatedAt
inventoryAvailableToSell
}
}
`
export const cartPayloadFragment = `
${cartCommon}
items {
${cartItemConnectionFragment}
}
`
export const incorrectPriceFailureDetailsFragment = `
currentPrice {
amount
currency {
code
}
displayAmount
}
productConfiguration {
productId
productVariantId
}
providedPrice {
amount
currency {
code
}
displayAmount
}
`
export const minOrderQuantityFailureDetailsFragment = `
minOrderQuantity
productConfiguration {
productId
productVariantId
}
quantity
`
const getCartQuery = /* GraphQL */ `
query($checkoutId: ID!) {
node(id: $checkoutId) {
... on Checkout {
${cartCommon}
}
}
}
`
export const cartQueryFragment = `
${cartCommon}
items(first: 20, after: $itemsAfterCursor) {
${cartItemConnectionFragment}
}
`
export default getCartQuery

View File

@ -0,0 +1,50 @@
const getPrimaryShopQuery = /* GraphQL */ `
query primaryShop($language: String! = "en") {
primaryShop {
_id
currency {
code
}
defaultNavigationTree(language: $language) {
...NavigationTreeFragment
}
description
name
}
}
fragment NavigationTreeFragment on NavigationTree {
_id
shopId
name
items {
navigationItem {
data {
...NavigationItemFields
}
}
items {
navigationItem {
data {
...NavigationItemFields
}
}
items {
navigationItem {
data {
...NavigationItemFields
}
}
}
}
}
}
fragment NavigationItemFields on NavigationItemData {
contentForLanguage
classNames
url
isUrlRelative
shouldOpenInNewWindow
}
`
export default getPrimaryShopQuery

View File

@ -0,0 +1,181 @@
const getProductQuery = /* GraphQL */ `
query getProductBySlug($slug: String!) {
catalogItemProduct(slugOrId: $slug) {
product {
_id
productId
title
slug
description
vendor
isLowQuantity
isSoldOut
isBackorder
metafields {
description
key
namespace
scope
value
valueType
}
pricing {
currency {
code
}
displayPrice
minPrice
maxPrice
}
shop {
currency {
code
}
}
primaryImage {
URLs {
large
medium
original
small
thumbnail
}
priority
productId
variantId
}
media {
priority
productId
variantId
URLs {
thumbnail
small
medium
large
original
}
}
tags {
nodes {
name
slug
position
}
}
variants {
_id
variantId
attributeLabel
title
optionTitle
index
pricing {
compareAtPrice {
displayAmount
}
price
currency {
code
}
displayPrice
}
canBackorder
inventoryAvailableToSell
isBackorder
isSoldOut
isLowQuantity
options {
_id
variantId
attributeLabel
title
index
pricing {
compareAtPrice {
displayAmount
}
price
currency {
code
}
displayPrice
}
optionTitle
canBackorder
inventoryAvailableToSell
isBackorder
isSoldOut
isLowQuantity
media {
priority
productId
variantId
URLs {
thumbnail
small
medium
large
original
}
}
metafields {
description
key
namespace
scope
value
valueType
}
primaryImage {
URLs {
large
medium
original
small
thumbnail
}
priority
productId
variantId
}
}
media {
priority
productId
variantId
URLs {
thumbnail
small
medium
large
original
}
}
metafields {
description
key
namespace
scope
value
valueType
}
primaryImage {
URLs {
large
medium
original
small
thumbnail
}
priority
productId
variantId
}
}
}
}
}
`
export default getProductQuery

View File

@ -0,0 +1,11 @@
const getShopCurrencyQuery = /* GraphQL */ `
query getShopCurrency($id: ID!) {
shop(id: $id) {
currency {
code
}
}
}
`
export default getShopCurrencyQuery

View File

@ -0,0 +1,14 @@
const getTagsQuery = /* GraphQL */ `
query getTags($first: ConnectionLimitInt!, $shopId: ID!) {
tags(first: $first, shopId: $shopId) {
edges {
node {
_id
displayTitle
slug
}
}
}
}
`
export default getTagsQuery

View File

@ -0,0 +1,10 @@
const getAllProductVendors = /* GraphQL */ `
query getAllProductVendors($shopIds: [ID]!) {
vendors(shopIds: $shopIds) {
nodes {
name
}
}
}
`
export default getAllProductVendors

View File

@ -0,0 +1,39 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
import type { GraphQLFetcher } from '@vercel/commerce/api'
import type { OpenCommerceConfig } from '../index'
import fetch from './fetch'
const fetchGraphqlApi: (
getConfig: () => OpenCommerceConfig
) => GraphQLFetcher =
(getConfig) =>
async (query: string, { variables, preview } = {}, fetchOptions) => {
// log.warn(query)
const config = getConfig()
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
method: 'POST',
headers: {
...fetchOptions?.headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
const json = await res.json()
if (json.errors) {
throw new FetcherError({
errors: json.errors ?? [
{ message: 'Failed to fetch OpenCommerce API' },
],
status: res.status,
})
}
return { data: json.data, res }
}
export default fetchGraphqlApi

View File

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

View File

@ -0,0 +1,5 @@
export default function filterEdges<T>(
edges: (T | null | undefined)[] | null | undefined
) {
return edges?.filter((edge): edge is T => !!edge) ?? []
}

View File

@ -0,0 +1,20 @@
import { serialize, CookieSerializeOptions } from 'cookie'
export default function getCartCookie(
name: string,
cartId?: string,
maxAge?: number
) {
const options: CookieSerializeOptions =
cartId && maxAge
? {
maxAge,
expires: new Date(Date.now() + maxAge * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
}
: { maxAge: -1, path: '/' } // Removes the cookie
return serialize(name, cartId || '', options)
}

View File

@ -0,0 +1,36 @@
export type SearchProductsInput = {
search?: string
categoryId?: number | string
brandId?: number | string
locale?: string
}
const getSearchVariables = ({
brandId,
search,
categoryId,
}: SearchProductsInput) => {
let searchQuery = ''
let tagIdsParam = {}
if (search) {
searchQuery += search
}
if (brandId) {
searchQuery += `${search ? ' ' : ''}${brandId}`
}
if (categoryId) {
tagIdsParam = {
tagIds: [categoryId],
}
}
return {
searchQuery,
...tagIdsParam,
}
}
export default getSearchVariables

View File

@ -0,0 +1,25 @@
type SortByField = 'minPrice' | 'featured' | 'createdAt'
const getSortVariables = (sort?: string) => {
if (!sort) return null
const [_sort, direction] = sort.split('-')
const SORT: { [key: string]: SortByField | undefined } = {
price: 'minPrice',
trending: 'featured',
latest: 'createdAt',
}
const sortValue = SORT[_sort]
if (sortValue && direction) {
return {
sortBy: sortValue,
sortOrder: direction,
}
}
return null
}
export default getSortVariables

View File

@ -0,0 +1,7 @@
export type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>
}
export type RecursiveRequired<T> = {
[P in keyof T]-?: RecursiveRequired<T[P]>
}

View File

@ -0,0 +1,3 @@
export { default as useLogin } from './use-login'
export { default as useLogout } from './use-logout'
export { default as useSignup } from './use-signup'

View File

@ -0,0 +1,16 @@
import { MutationHook } from '@vercel/commerce/utils/types'
import useLogin, { UseLogin } from '@vercel/commerce/auth/use-login'
export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook: () => () => {
return async function () {}
},
}

View File

@ -0,0 +1,17 @@
import { MutationHook } from '@vercel/commerce/utils/types'
import useLogout, { UseLogout } from '@vercel/commerce/auth/use-logout'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook:
({ fetch }) =>
() =>
async () => {},
}

View File

@ -0,0 +1,19 @@
import { useCallback } from 'react'
import useCustomer from '../customer/use-customer'
import { MutationHook } from '@vercel/commerce/utils/types'
import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook:
({ fetch }) =>
() =>
() => {},
}

View File

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

View File

@ -0,0 +1,43 @@
import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@vercel/commerce/cart/use-add-item'
import type { AddItemHook } from '@vercel/commerce/types/cart'
import { CommerceError } from '@vercel/commerce/utils/errors'
import { MutationHook } from '@vercel/commerce/utils/types'
import { CartTypes } from '../types/cart'
import useCart from './use-cart'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook<CartTypes>> = {
fetchOptions: {
url: '/api/cart',
method: 'POST',
},
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 data = await fetch({ ...options, body: { item } })
return data
},
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,33 @@
import { useMemo } from 'react'
import { SWRHook } from '@vercel/commerce/utils/types'
import useCart, { UseCart } from '@vercel/commerce/cart/use-cart'
import type { GetCartHook } from '@vercel/commerce/types/cart'
import { CartTypes } from '../types/cart'
export default useCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook<CartTypes>> = {
fetchOptions: {
url: '/api/cart',
method: 'GET',
},
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,56 @@
import { useCallback } from 'react'
import {
HookFetcherContext,
MutationHook,
MutationHookContext,
} from '@vercel/commerce/utils/types'
import useRemoveItem, {
UseRemoveItem,
} from '@vercel/commerce/cart/use-remove-item'
import type {
LineItem,
RemoveItemHook,
Cart,
} from '@vercel/commerce/types/cart'
import { ValidationError } from '@vercel/commerce/utils/errors'
import useCart from './use-cart'
export type RemoveItemActionInput<T = any> = T extends LineItem
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler: MutationHook<RemoveItemHook> = {
fetchOptions: {
url: '/api/cart',
method: 'DELETE',
},
async fetcher({ input: { itemId }, options, fetch }) {
return await fetch({ ...options, body: { itemId } })
},
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,76 @@
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import { MutationHook, MutationHookContext } from '@vercel/commerce/utils/types'
import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/cart/use-update-item'
import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart'
import { ValidationError } from '@vercel/commerce/utils/errors'
import { handler as removeItemHandler } from './use-remove-item'
import useCart from './use-cart'
export type UpdateItemActionInput<T = any> = T extends LineItem
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler: MutationHook<UpdateItemHook> = {
fetchOptions: {
url: '/api/cart',
method: 'PUT',
},
async fetcher({ input: { itemId, item }, options, fetch }) {
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',
})
}
return await fetch({
...options,
body: { itemId, item },
})
},
useHook:
({ fetch }: MutationHookContext<UpdateItemHook>) =>
<T extends LineItem | undefined = undefined>(
ctx: { item?: T; wait?: number } = {}
) => {
const { item, wait } = ctx
const { mutate } = useCart()
return useCallback(
debounce(async (input: UpdateItemActionInput) => {
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: {
itemId,
item: { productId, variantId, quantity: input.quantity },
},
})
await mutate(data, false)
return data
}, wait ?? 500),
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,2 @@
export { default as useCheckout } from './use-checkout'
export { default as useSubmitCheckout } from './use-submit-checkout'

View File

@ -0,0 +1,64 @@
import type { GetCheckoutHook } from '@vercel/commerce/types/checkout'
import { useMemo } from 'react'
import { SWRHook } from '@vercel/commerce/utils/types'
import useCheckout, {
UseCheckout,
} from '@vercel/commerce/checkout/use-checkout'
import useSubmitCheckout from './use-submit-checkout'
import { useCheckoutContext } from '@components/checkout/context'
import { useCart } from '../cart'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<GetCheckoutHook> = {
fetchOptions: {
query: '',
method: '',
},
useHook: () =>
function useHook() {
const { data: cart } = useCart()
const shippingTypeMethod =
cart?.checkout?.fulfillmentGroups &&
cart.checkout.fulfillmentGroups.find(
(group) => group?.type === 'shipping'
)
const hasShippingMethods =
!!shippingTypeMethod?.availableFulfillmentOptions.length
const { addressFields } = useCheckoutContext()
const { shippingMethod, ...restAddressFields } = addressFields
const hasEnteredAddress = Object.values(restAddressFields).some(
(fieldValue) => !!fieldValue
)
const response = useMemo(
() => ({
data: {
// example payment plugin does not need payment info
hasPayment: true,
hasShipping: hasEnteredAddress,
hasShippingMethods,
hasSelectedShippingMethod: !!shippingMethod?.id,
},
}),
[hasEnteredAddress, hasShippingMethods, shippingMethod]
)
return useMemo(
() =>
Object.create(response, {
submit: {
get() {
return useSubmitCheckout
},
enumerable: true,
},
}),
[response, useSubmitCheckout]
)
},
}

View File

@ -0,0 +1,32 @@
import { useCallback } from 'react'
import type { SubmitCheckoutHook } from '@vercel/commerce/types/checkout'
import type { MutationHook } from '@vercel/commerce/utils/types'
import useSubmitCheckout, {
UseSubmitCheckout,
} from '@vercel/commerce/checkout/use-submit-checkout'
export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
export const handler: MutationHook<SubmitCheckoutHook> = {
fetchOptions: {
url: '/api/checkout',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {
const data = await fetch({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
return useCallback(async function onSubmitCheckout(input) {
const data = await fetch({
input,
})
return data
}, [])
},
}

View File

@ -0,0 +1,7 @@
{
"provider": "opencommerce",
"features": {
"customCheckout": true,
"customNavigation": true
}
}

View File

@ -0,0 +1,2 @@
export { default as useAddItem } from './use-add-item'
export { default as useUpdateItem } from './use-update-item'

View File

@ -0,0 +1,37 @@
import type { AddItemHook } from '@vercel/commerce/types/customer/address'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import useAddItem, {
UseAddItem,
} from '@vercel/commerce/customer/address/use-add-item'
import { useCheckoutContext } from '@components/checkout/context'
import { CustomerAddressTypes } from '../../types/customer/address'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook<CustomerAddressTypes>> = {
fetchOptions: {
url: '/api/customer/address',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {
const data = await fetch({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
const { setAddressFields, addressFields } = useCheckoutContext()
return useCallback(
async function addItem(input) {
await fetch({ input })
setAddressFields({ ...addressFields, ...input })
return undefined
},
[setAddressFields]
)
},
}

View File

@ -0,0 +1,44 @@
import type { UpdateItemHook } from '@vercel/commerce/types/customer/address'
import type {
MutationHook,
MutationHookContext,
} from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/customer/address/use-update-item'
import { useCheckoutContext } from '@components/checkout/context'
import { CustomerAddressTypes } from '../../types/customer/address'
export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler: MutationHook<UpdateItemHook<CustomerAddressTypes>> = {
fetchOptions: {
url: '/api/customer/address',
method: 'PUT',
},
async fetcher({ input: { item, itemId }, options, fetch }) {
const data = await fetch({
...options,
body: { item, itemId },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
const { setAddressFields, addressFields } = useCheckoutContext()
return useCallback(
async function updateItem(input) {
const { id, ...rest } = input
await fetch({ input: { item: rest, itemId: id } })
setAddressFields({
...addressFields,
shippingMethod: rest.shippingMethod,
})
return undefined
},
[setAddressFields]
)
},
}

View File

@ -0,0 +1,2 @@
export { default as useAddItem } from './use-add-item'
export { default as useCards } from './use-cards'

View File

@ -0,0 +1,27 @@
import type { AddItemHook } from '@vercel/commerce/types/customer/card'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import useAddItem, {
UseAddItem,
} from '@vercel/commerce/customer/card/use-add-item'
import { useCheckoutContext } from '@components/checkout/context'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '',
method: '',
},
useHook: () =>
function useHook() {
const { setCardFields } = useCheckoutContext()
return useCallback(
async function addItem(input) {
setCardFields(input)
return undefined
},
[setCardFields]
)
},
}

View File

@ -0,0 +1,32 @@
import { useMemo } from 'react'
import type { GetCardsHook } from '@vercel/commerce/types/customer/card'
import { SWRHook } from '@vercel/commerce/utils/types'
import useCard, { UseCards } from '@vercel/commerce/customer/card/use-cards'
export default useCard as UseCards<typeof handler>
export const handler: SWRHook<GetCardsHook> = {
fetchOptions: {
url: '/api/customer/card',
method: 'GET',
},
useHook: ({ useData }) =>
function useHook(input) {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.length ?? 0) <= 0
},
enumerable: true,
},
}),
[response]
)
},
}

View File

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

View File

@ -0,0 +1,17 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useCustomer, {
UseCustomer,
} from '@vercel/commerce/customer/use-customer'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook: () => () => {
return async function addItem() {
return {}
}
},
}

View File

@ -0,0 +1,41 @@
import type { Fetcher } from '@vercel/commerce/utils/types'
import { FetcherError } from '@vercel/commerce/utils/errors'
async function getText(res: Response) {
try {
return (await res.text()) || res.statusText
} catch (error) {
return res.statusText
}
}
async function getError(res: Response) {
if (res.headers.get('Content-Type')?.includes('application/json')) {
const data = await res.json()
return new FetcherError({ errors: data.errors, status: res.status })
}
return new FetcherError({ message: await getText(res), status: res.status })
}
const fetcher: Fetcher = async ({
url,
method = 'GET',
variables,
body: bodyObj,
}) => {
const hasBody = Boolean(variables || bodyObj)
const body = hasBody
? JSON.stringify(variables ? { variables } : bodyObj)
: undefined
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
const res = await fetch(url!, { method, body, headers })
if (res.ok) {
const { data } = await res.json()
return data
}
throw await getError(res)
}
export default fetcher

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export { default as usePrice } from './use-price'
export { default as useSearch } from './use-search'

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,45 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useSearch, { UseSearch } from '@vercel/commerce/product/use-search'
import type { SearchProductsHook } from '../types/product'
export default useSearch as UseSearch<typeof handler>
export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: {
url: '/api/catalog/products',
method: 'GET',
},
async fetcher({
input: { search, categoryId, brandId, sort },
options,
fetch,
}) {
// Use a dummy base as we only care about the relative path
const url = new URL(options.url!, 'http://a')
if (search) url.searchParams.set('search', search)
if (categoryId) url.searchParams.set('categoryId', String(categoryId))
if (brandId) url.searchParams.set('brandId', String(brandId))
if (sort) url.searchParams.set('sort', sort)
return fetch({
url: url.pathname + url.search,
method: options.method,
})
},
useHook:
({ useData }) =>
(input = {}) => {
return useData({
input: [
['search', input.search],
['categoryId', input.categoryId],
['brandId', input.brandId],
['sort', input.sort],
],
swrOptions: {
revalidateOnFocus: false,
...input.swrOptions,
},
})
},
}

View File

@ -0,0 +1,36 @@
import fetcher from './fetcher'
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 { handler as useCheckout } from './checkout/use-checkout'
import { handler as useSubmitCheckout } from './checkout/use-submit-checkout'
import { handler as useAddCardItem } from './customer/card/use-add-item'
import { handler as useCards } from './customer/card/use-cards'
import { handler as useAddAddressItem } from './customer/address/use-add-item'
import { handler as useUpdateAddressItem } from './customer/address/use-update-item'
export const openCommerceProvider = {
locale: 'en-us',
cartCookie: 'opencommerce_cartId',
fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: {
useCustomer,
card: { useCards, useAddItem: useAddCardItem },
address: {
useAddItem: useAddAddressItem,
useUpdateItem: useUpdateAddressItem,
},
},
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
checkout: { useCheckout, useSubmitCheckout },
}
export type OpenCommerceProvider = typeof openCommerceProvider

View File

@ -0,0 +1,21 @@
import * as Core from '@vercel/commerce/types/cart'
import { Checkout } from '../../schema'
import { ProductVariant } from './product'
export * from '@vercel/commerce/types/cart'
export type Cart = Core.Cart & {
checkout?: Checkout
}
export type CartItemBody = Core.CartItemBody & {
currencyCode?: string
variant?: ProductVariant
}
export type CartTypes = Core.CartTypes & {
itemBody: CartItemBody
cart?: Cart
}
export type CartSchema = Core.CartSchema<CartTypes>

View File

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

View File

@ -0,0 +1,14 @@
import * as Core from '@vercel/commerce/types/customer/address'
export * from '@vercel/commerce/types/customer/address'
export type AddressFields = Core.AddressFields & {
shippingMethod?: {
id: string
fulfillmentGroupId: string
}
}
export type CustomerAddressTypes = Core.CustomerAddressTypes & {
fields: AddressFields
}

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