Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic

This commit is contained in:
Luis Alvarez 2021-02-24 23:17:29 -05:00
commit 4a1a1a515f
47 changed files with 1263 additions and 18 deletions

View File

@ -3,4 +3,7 @@ BIGCOMMERCE_STOREFRONT_API_TOKEN=
BIGCOMMERCE_STORE_API_URL=
BIGCOMMERCE_STORE_API_TOKEN=
BIGCOMMERCE_STORE_API_CLIENT_ID=
BIGCOMMERCE_CHANNEL_ID=
BIGCOMMERCE_CHANNEL_ID=
SHOPIFY_STORE_DOMAIN=
SHOPIFY_STOREFRONT_ACCESS_TOKEN=

View File

@ -1,4 +0,0 @@
## Changelog
- Select Variants Working
- Click on cart item title, closes the sidebar

View File

@ -4,11 +4,11 @@ import cn from 'classnames'
import type { LineItem } from '@framework/types'
import useCart from '@framework/cart/use-cart'
import useCustomer from '@framework/customer/use-customer'
import { Avatar } from '@components/common'
import { Heart, Bag } from '@components/icons'
import { useUI } from '@components/ui/context'
import DropdownMenu from './DropdownMenu'
import s from './UserNav.module.css'
import { Avatar } from '@components/common'
interface Props {
className?: string

View File

@ -4,9 +4,8 @@ import { NextSeo } from 'next-seo'
import { FC, useState } from 'react'
import s from './ProductView.module.css'
import { useUI } from '@components/ui'
import { Swatch, ProductSlider } from '@components/product'
import { Button, Container, Text } from '@components/ui'
import { Button, Container, Text, useUI } from '@components/ui'
import type { Product } from '@commerce/types'
import usePrice from '@framework/product/use-price'
@ -100,7 +99,6 @@ const ProductView: FC<Props> = ({ product }) => {
</ProductSlider>
</div>
</div>
<div className={s.sidebar}>
<section>
{product.options?.map((opt) => (

View File

@ -8,6 +8,7 @@ export interface State {
displayToast: boolean
modalView: string
toastText: string
userAvatar: string
}
const initialState = {
@ -17,6 +18,7 @@ const initialState = {
modalView: 'LOGIN_VIEW',
displayToast: false,
toastText: '',
userAvatar: '',
}
type Action =

View File

@ -1,11 +1,10 @@
import React, { FC, useState } from 'react'
import cn from 'classnames'
import { Heart } from '@components/icons'
import { useUI } from '@components/ui'
import type { Product, ProductVariant } from '@commerce/types'
import useCustomer from '@framework/customer/use-customer'
import useAddItem from '@framework/wishlist/use-add-item'
import useCustomer from '@framework/customer/use-customer'
import useRemoveItem from '@framework/wishlist/use-remove-item'
import useWishlist from '@framework/wishlist/use-wishlist'

View File

@ -1,5 +1,5 @@
{
"features": {
"wishlist": false
"wishlist": true
}
}

260
framework/shopify/README.md Normal file
View File

@ -0,0 +1,260 @@
## Table of Contents
- [Getting Started](#getting-started)
- [Modifications](#modifications)
- [Adding item to Cart](#adding-item-to-cart)
- [Proceed to Checkout](#proceed-to-checkout)
- [General Usage](#general-usage)
- [CommerceProvider](#commerceprovider)
- [useCommerce](#usecommerce)
- [Hooks](#hooks)
- [usePrice](#useprice)
- [useAddItem](#useadditem)
- [useRemoveItem](#useremoveitem)
- [useUpdateItem](#useupdateitem)
- [APIs](#apis)
- [getProduct](#getproduct)
- [getAllProducts](#getallproducts)
- [getAllCollections](#getallcollections)
- [getAllPages](#getallpages)
# Shopify Storefront Data Hooks
Collection of hooks and data fetching functions to integrate Shopify in a React application. Designed to work with [Next.js Commerce](https://demo.vercel.store/).
## Getting Started
1. Install dependencies:
```
yarn install shopify-buy
yarn install -D @types/shopify-buy
```
3. Environment variables need to be set:
```
SHOPIFY_STORE_DOMAIN=
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
```
4. Point the framework to `shopify` by updating `tsconfig.json`:
```
"@framework/*": ["framework/shopify/*"],
"@framework": ["framework/shopify"]
```
### 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>
)
}
```
## General Usage
### CommerceProvider
Provider component that creates the commerce context for children.
```js
import { CommerceProvider } from '@framework'
const App = ({ children }) => {
return <CommerceProvider locale={locale}>{children}</CommerceProvider>
}
export default App
```
### useCommerce
Returns the configs that are defined in the nearest `CommerceProvider`. Also provides access to Shopify's `checkout` and `shop`.
```js
import { useCommerce } from 'nextjs-commerce-shopify'
const { checkout, shop } = useCommerce()
```
- `checkout`: The information required to checkout items and pay ([Documentation](https://shopify.dev/docs/storefront-api/reference/checkouts/checkout)).
- `shop`: Represents a collection of the general settings and information about the shop ([Documentation](https://shopify.dev/docs/storefront-api/reference/online-store/shop/index)).
## Hooks
### usePrice
Display the product variant price according to currency and locale.
```js
import usePrice from '@framework/product/use-price'
const { price } = usePrice({
amount,
})
```
Takes in either `amount` or `variant`:
- `amount`: A price value for a particular item if the amount is known.
- `variant`: A shopify product variant. Price will be extracted from the variant.
### useAddItem
```js
import { useAddItem } from '@framework/cart'
const AddToCartButton = ({ variantId, quantity }) => {
const addItem = useAddItem()
const addToCart = async () => {
await addItem({
variantId,
})
}
return <button onClick={addToCart}>Add To Cart</button>
}
```
### useRemoveItem
```js
import { useRemoveItem } from '@framework/cart'
const RemoveButton = ({ item }) => {
const removeItem = useRemoveItem()
const handleRemove = async () => {
await removeItem({ id: item.id })
}
return <button onClick={handleRemove}>Remove</button>
}
```
### useUpdateItem
```js
import { useUpdateItem } from '@framework/cart'
const CartItem = ({ item }) => {
const [quantity, setQuantity] = useState(item.quantity)
const updateItem = useUpdateItem(item)
const updateQuantity = async (e) => {
const val = e.target.value
await updateItem({ quantity: val })
}
return (
<input
type="number"
max={99}
min={0}
value={quantity}
onChange={updateQuantity}
/>
)
}
```
## 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,
})
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,60 @@
import type { CommerceAPIConfig } from '@commerce/api'
import fetchGraphqlApi from './utils/fetch-graphql-api'
export interface ShopifyConfig extends CommerceAPIConfig {}
// TODO(bc)
const API_URL =
process.env.SHOPIFY_STORE_DOMAIN ||
process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN
const API_TOKEN =
process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN ||
process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN
if (!API_URL) {
throw new Error(
`The environment variable SHOPIFY_STORE_DOMAIN is missing and it's required to access your store`
)
}
if (!API_TOKEN) {
throw new Error(
`The environment variable SHOPIFY_STOREFRONT_ACCESS_TOKEN is missing and it's required to access your store`
)
}
export class Config {
private config: ShopifyConfig
constructor(config: ShopifyConfig) {
this.config = config
}
getConfig(userConfig: Partial<ShopifyConfig> = {}) {
return Object.entries(userConfig).reduce<ShopifyConfig>(
(cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
{ ...this.config }
)
}
setConfig(newConfig: Partial<ShopifyConfig>) {
Object.assign(this.config, newConfig)
}
}
const config = new Config({
commerceUrl: API_URL,
apiToken: API_TOKEN,
// TODO
// @ts-ignore
fetch: fetchGraphqlApi,
customerCookie: 'SHOP_TOKEN',
})
export function getConfig(userConfig?: Partial<ShopifyConfig>) {
return config.getConfig(userConfig)
}
export function setConfig(newConfig: Partial<ShopifyConfig>) {
return config.setConfig(newConfig)
}

View File

@ -0,0 +1,21 @@
import Client from 'shopify-buy'
import { ShopifyConfig } from '../index'
type Options = {
config: ShopifyConfig
}
const getAllCollections = async (options: Options) => {
const { config } = options
const client = Client.buildClient({
storefrontAccessToken: config.apiToken,
domain: config.commerceUrl,
})
const res = await client.collection.fetchAllWithProducts()
return JSON.parse(JSON.stringify(res))
}
export default getAllCollections

View File

@ -0,0 +1,27 @@
import { ShopifyConfig, getConfig } from '..'
import type { Page } from '../../types'
export type { Page }
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
export type PageVariables = {
id: string
}
async function getPage({
url,
variables,
config,
preview,
}: {
url?: string
variables: PageVariables
config?: ShopifyConfig
preview?: boolean
}): Promise<GetPageResult> {
config = getConfig(config)
return {}
}
export default getPage

View File

@ -0,0 +1,51 @@
import { CommerceAPIFetchOptions } from '@commerce/api'
import { FetcherError } from '@commerce/utils/errors'
import { getConfig } from '../index'
export interface GraphQLFetcherResult<Data = any> {
data: Data
res: Response
}
export type GraphQLFetcher<
Data extends GraphQLFetcherResult = GraphQLFetcherResult,
Variables = any
> = (
query: string,
queryData?: CommerceAPIFetchOptions<Variables>,
fetchOptions?: RequestInit
) => Promise<Data>
const fetchGraphqlApi: GraphQLFetcher = async (
query: string,
{ variables } = {},
fetchOptions
) => {
const config = getConfig()
const url = `https://${config.commerceUrl}/api/2020-10/graphql.json`
const res = await fetch(url, {
...fetchOptions,
method: 'POST',
headers: {
'X-Shopify-Storefront-Access-Token': config.apiToken,
...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 Shopify API' }],
status: res.status,
})
}
return { data: json.data, res }
}
export default fetchGraphqlApi

View File

@ -0,0 +1,2 @@
export type WishlistItem = { product: any; id: number }
export default function () {}

View File

@ -0,0 +1,13 @@
import { useCallback } from 'react'
export function emptyHook() {
const useEmptyHook = async (options = {}) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@ -0,0 +1,13 @@
import { useCallback } from 'react'
export function emptyHook() {
const useEmptyHook = async (options = {}) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@ -0,0 +1,13 @@
import { useCallback } from 'react'
export function emptyHook() {
const useEmptyHook = async (options = {}) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@ -0,0 +1,5 @@
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 useWishlistActions } from './use-cart-actions'
// export { default as useUpdateItem } from './use-cart-actions'

View File

@ -0,0 +1,30 @@
import { useCallback } from 'react'
import { LineItemToAdd } from 'shopify-buy'
import { useCommerce } from '../index'
type Options = {
productId: number
variantId: string | number
}
const useAddItem = () => {
const { checkout, client, updateCheckout } = useCommerce()
return useCallback(
async function addItem(options: Options) {
const lineItems: LineItemToAdd[] = [
{
variantId: `${options.variantId}`,
quantity: 1,
},
]
const cart = await client?.checkout.addLineItems(checkout.id, lineItems)
updateCheckout(cart)
return cart
},
[checkout, client]
)
}
export default useAddItem

View File

@ -0,0 +1,42 @@
import { useCommerce } from '../index'
export function emptyHook() {
const { checkout } = useCommerce()
const { lineItems, totalPriceV2 } = checkout || {}
return {
data: {
subTotal: totalPriceV2?.amount || 0,
total: totalPriceV2?.amount || 0,
currency: {
code: '',
},
line_items:
lineItems?.map((item) => {
return [
{
id: item.id,
name: item.title,
quantity: item.quantity,
},
]
}) || [],
items:
lineItems?.map((item) => {
return {
id: item.id,
name: item.title,
images: [{ url: '/jacket.png' }],
url: '/',
quantity: item.quantity,
productId: item.id,
variantId: item.id,
}
}) || [],
},
isEmpty: false,
isLoading: false,
}
}
export default emptyHook

View File

@ -0,0 +1,17 @@
import { useCallback } from 'react'
import { useCommerce } from '../index'
const useRemoveItem = () => {
const { checkout, client, updateCheckout } = useCommerce()
return useCallback(
async function removeItem({ id }: { id: string }) {
const cart = await client?.checkout.removeLineItems(checkout.id, [id])
updateCheckout(cart)
return cart
},
[checkout, client]
)
}
export default useRemoveItem

View File

@ -0,0 +1,24 @@
import { useCallback } from 'react'
import { useCommerce } from '../index'
const useUpdateItem = (item: CartItem) => {
const { checkout, client, updateCheckout } = useCommerce()
return useCallback(
async function updateItem({ quantity }: { quantity: number }) {
const lineItemsToUpdate = [{ id: item.id, quantity }]
const cart = await client?.checkout.updateLineItems(
checkout.id,
lineItemsToUpdate
)
updateCheckout(cart)
return cart
},
[checkout, client]
)
}
export default useUpdateItem

View File

@ -0,0 +1,55 @@
import { getConfig, ShopifyConfig } from '../api'
import { Page as PageType, PageEdge } from '../types'
export type Page = PageType
export const getAllPagesQuery = /* GraphQL */ `
query($first: Int!) {
pages(first: $first) {
edges {
node {
id
title
handle
body
bodySummary
url
}
}
}
}
`
type Variables = {
first?: number
}
type Options = {
variables?: Variables
config: ShopifyConfig
preview?: boolean
}
type ReturnType = {
pages: Page[]
}
const getAllPages = async (options?: Options): Promise<ReturnType> => {
let { config, variables = { first: 250 } } = options || {}
config = getConfig(config)
const { data } = await config.fetch(getAllPagesQuery, { variables })
const pages = data.pages.edges.map(({ node }: PageEdge) => {
return {
...node,
name: node.handle,
url: `${config!.locale}/${node.handle}`,
}
})
return { pages }
}
export default getAllPages

View File

@ -0,0 +1,30 @@
import { ShopifyConfig } from '../index'
type Options = {
config: ShopifyConfig
preview?: boolean
}
const getSiteInfo = async (options: Options) => {
// TODO
return {
categories: [
{
path: '',
name: '',
entityId: 0,
},
],
brands: [
{
node: {
path: '',
name: '',
entityId: 0,
},
},
],
}
}
export default getSiteInfo

View File

@ -0,0 +1,32 @@
import type { HookFetcher } from '@commerce/utils/types'
import type { SwrOptions } from '@commerce/utils/use-data'
import useCommerceCustomer from '@commerce/customer/use-customer'
const defaultOpts = {}
export type Customer = {
entityId: number
firstName: string
lastName: string
email: string
}
export type CustomerData = {}
export const fetcher: HookFetcher<Customer | null> = async () => {
return null
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<Customer | null>
) {
const useCustomer = () => {
return { data: { firstName: null, lastName: null, email: null } }
}
useCustomer.extend = extendHook
return useCustomer
}
export default extendHook(fetcher)

109
framework/shopify/index.tsx Normal file
View File

@ -0,0 +1,109 @@
import React, {
ReactNode,
createContext,
useContext,
useMemo,
useState,
useEffect,
} from 'react'
import Client from 'shopify-buy'
import { Shop, Cart, Client as ClientType } from './types'
import {
getCheckoutIdFromStorage,
setCheckoutIdInStorage,
} from './utils/storage'
import { getConfig } from '@framework/api'
const Commerce = createContext<CommerceContextValue | {}>({})
type CommerceProps = {
children?: ReactNode
locale: string
}
type CommerceContextValue = {
client: ClientType
shop: Shop
checkout: Cart
updateCheckout: (cart: Cart | undefined) => void
currencyCode: string
locale: string
sessionToken: string
}
export function CommerceProvider({
children,
locale = 'en-US',
}: CommerceProps) {
const sessionToken = 'nextjs-commerce-shopify-token'
const config = getConfig()
const client = Client.buildClient({
storefrontAccessToken: config.apiToken,
domain: config.commerceUrl,
language: locale,
}) as ClientType
const [shop, setShop] = useState<Shop>()
const [checkout, setCheckout] = useState<Cart>()
const fetchShopify = async () => {
const shopInfo: Shop = await client.shop.fetchInfo()
let checkoutResource: Cart
const checkoutOptions = {
presentmentCurrencyCode:
/*config.currencyCode ||*/ shopInfo?.currencyCode,
}
let checkoutId = getCheckoutIdFromStorage(sessionToken)
// we could have a cart id stored in session storage
// user could be refreshing or navigating back and forth
if (checkoutId) {
checkoutResource = await client.checkout.fetch(checkoutId)
// could be expired order - we will create a new order
if (checkoutResource.completedAt) {
checkoutResource = await client.checkout.create(checkoutOptions)
}
} else {
checkoutResource = await client.checkout.create(checkoutOptions)
}
setCheckoutIdInStorage(sessionToken, checkoutResource.id)
setShop(shopInfo)
setCheckout(checkoutResource)
}
useEffect(() => {
fetchShopify()
}, [])
const updateCheckout = (newCheckout: Cart) => {
setCheckout(newCheckout)
}
// Because the config is an object, if the parent re-renders this provider
// will re-render every consumer unless we memoize the config
const cfg = useMemo(
() => ({
client,
checkout,
shop,
updateCheckout: updateCheckout,
currencyCode: /*config.currencyCode ||*/ checkout?.currencyCode,
locale,
sessionToken,
}),
[client]
)
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
}
export function useCommerce<T extends CommerceContextValue>() {
return useContext(Commerce) as T
}

View File

@ -0,0 +1,31 @@
import Client from 'shopify-buy'
import { getConfig } from '../api'
import { Product } from '../types'
import toCommerceProducts from '../utils/to-commerce-products'
type ReturnType = {
products: any[]
}
const getAllProductPaths = async (): Promise<ReturnType> => {
const config = getConfig()
const client = Client.buildClient({
storefrontAccessToken: config.apiToken,
domain: config.commerceUrl,
})
const res = (await client.product.fetchAll()) as Product[]
const products = toCommerceProducts(res)
return {
products: products.map((product) => {
return {
node: { ...product },
}
}),
}
}
export default getAllProductPaths

View File

@ -0,0 +1,40 @@
import Client from 'shopify-buy'
import { ShopifyConfig } from '../api'
import { Product } from '../types'
import toCommerceProducts from '../utils/to-commerce-products'
export type ProductNode = Product
type Variables = {
first?: number
field?: string
}
type Options = {
variables: Variables
config: ShopifyConfig
preview?: boolean
}
type ReturnType = {
products: any[]
}
const getAllProducts = async (options: Options): Promise<ReturnType> => {
const { config } = options
const client = Client.buildClient({
storefrontAccessToken: config.apiToken,
domain: config.commerceUrl,
})
const res = (await client.product.fetchAll()) as Product[]
const products = toCommerceProducts(res)
return {
products,
}
}
export default getAllProducts

View File

@ -0,0 +1,37 @@
import Client from 'shopify-buy'
import { ShopifyConfig } from '../api'
import { Product } from '../types'
import toCommerceProducts from '../utils/to-commerce-products'
export type ProductNode = Product
type Variables = {
slug: string
}
type Options = {
variables: Variables
config: ShopifyConfig
preview?: boolean
}
type ReturnType = {
product: any
}
const getProduct = async (options: Options): Promise<ReturnType> => {
const { variables, config } = options
const client = Client.buildClient({
storefrontAccessToken: config.apiToken,
domain: config.commerceUrl,
})
const res = (await client.product.fetchByHandle(variables.slug)) as Product
return {
product: toCommerceProducts([res])[0],
}
}
export default getProduct

View File

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

View File

@ -0,0 +1,41 @@
import type { HookFetcher } from '@commerce/utils/types'
import type { SwrOptions } from '@commerce/utils/use-data'
import useCommerceSearch from '@commerce/product/use-search'
import { ProductEdge } from '../types'
const defaultOpts = {}
export type SearchProductsInput = {
search?: string
categoryId?: number
brandId?: number
sort?: string
}
export type SearchProductsData = {
products: ProductEdge[]
found: boolean
}
export const fetcher: HookFetcher<SearchProductsData, SearchProductsInput> = (
options,
{ search, categoryId, brandId, sort },
fetch
) => {
return { found: false, products: [] }
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<SearchProductsData, SearchProductsInput>
) {
const useSearch = (input: SearchProductsInput = {}) => {
return {}
}
useSearch.extend = extendHook
return useSearch
}
export default extendHook(fetcher)

130
framework/shopify/types.ts Normal file
View File

@ -0,0 +1,130 @@
import {
Product as BaseProduct,
ProductVariant as BaseProductVariant,
Cart as BaseCart,
CheckoutResource as BaseCheckoutResource,
AttributeInput,
Client as BaseClient,
Shop as BaseShop,
Image as BaseImage,
} from 'shopify-buy'
export type SelectedOptions = {
id: string
name: string
value: string
}
export type PresentmentPrice = {
price: PriceV2
}
export type ProductVariant = BaseProductVariant & {
selectedOptions: Array<SelectedOptions>
presentmentPrices: Array<PresentmentPrice>
}
// TODO
export type ProductOptions = {
node: {
__typename: string
displayName: string
values: {
edges: [
{
node: {
label: string
id: string
}
}
]
}
}
}
// TODO
export type ProductEdge = {
node: Product
}
export type Product = BaseProduct & {
handle: string
name: string
path: string
entityId: number
descriptionHtml: string
prices: {
price: {
value: number
currencyCode: string
}
retailPrice: {
value: number
currencyCode: string
}
}
images: {
edges: [{ node: { urlOriginal: string; altText: string } }]
}
productOptions: ProductOptions
variants: Array<ProductVariant> & {
edges: [
{
node: {
productOptions: ProductOptions[]
entityId: number
}
}
]
}
}
export type PriceV2 = {
amount: number
currencyCode: string
}
export type Cart = BaseCart & {
webUrl?: string
currencyCode?: string
lineItemsSubtotalPrice?: PriceV2
totalPriceV2?: PriceV2
}
export type Shop = BaseShop & {
currencyCode?: string
}
export type Create = {
presentmentCurrencyCode?: string
}
export type CheckoutResource = BaseCheckoutResource & {
updateLineItems(
checkoutId: string | number,
lineItems: AttributeInput[]
): Promise<Cart>
create: (input: Create) => Promise<Cart>
}
export type Client = BaseClient & {
checkout: CheckoutResource
}
export type Page = {
id: string
title: string
name: string
handle: string
body: string
bodySummary: string
url: string
sort_order: number
}
export type PageEdge = {
node: Page
}
export type Image = BaseImage

View File

@ -0,0 +1,13 @@
export const getCheckoutIdFromStorage = (token: string) => {
if (window && window.sessionStorage) {
return window.sessionStorage.getItem(token)
}
return null
}
export const setCheckoutIdInStorage = (token: string, id: string | number) => {
if (window && window.sessionStorage) {
return window.sessionStorage.setItem(token, id + '')
}
}

View File

@ -0,0 +1,60 @@
import { Product, Image } from '../types'
export default function toCommerceProducts(products: Product[]) {
return products.map((product: Product) => {
return {
id: product.id,
entityId: product.id,
name: product.title,
slug: product.handle,
title: product.title,
vendor: product.vendor,
description: product.descriptionHtml,
path: `/${product.handle}`,
price: {
value: +product.variants[0].price,
currencyCode: 'USD', // TODO
},
images: product.images.map((image: Image) => {
return {
url: image.src,
}
}),
variants: product.variants.map((variant) => {
return {
id: variant.id,
options: variant.selectedOptions.map((selectedOption) => {
return {
__typename: 'MultipleChoiceOption',
displayName: selectedOption.name,
values: [
{
node: {
id: variant.id,
label: selectedOption.value,
},
},
],
}
}),
}
}),
productOptions: product.options.map((option) => {
return {
__typename: 'MultipleChoiceOption',
displayName: option.name,
values: option.values.map((value) => {
return {
node: {
entityId: 1,
label: value.value,
hexColors: [value.value],
},
}
}),
}
}),
options: [],
}
})
}

View File

@ -0,0 +1,13 @@
import { useCallback } from 'react'
export function emptyHook() {
const useEmptyHook = async (options = {}) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@ -0,0 +1,17 @@
import { useCallback } from 'react'
type Options = {
includeProducts?: boolean
}
export function emptyHook(options?: Options) {
const useEmptyHook = async ({ id }: { id: string | number }) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@ -0,0 +1,45 @@
import { HookFetcher } from '@commerce/utils/types'
import { SwrOptions } from '@commerce/utils/use-data'
import useCommerceWishlist from '@commerce/wishlist/use-wishlist'
import { Product } from '../types'
import useCustomer from '../customer/use-customer'
const defaultOpts = {}
export type Wishlist = {
items: [
{
product_id: number
variant_id: number
id: number
product: Product
}
]
}
export interface UseWishlistOptions {
includeProducts?: boolean
}
export interface UseWishlistInput extends UseWishlistOptions {
customerId?: number
}
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = () => {
return null
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
) {
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
return { data: null }
}
useWishlist.extend = extendHook
return useWishlist
}
export default extendHook(fetcher)

View File

@ -44,6 +44,7 @@
"react-dom": "^17.0.1",
"react-merge-refs": "^1.1.0",
"react-ticker": "^1.2.2",
"shopify-buy": "^2.11.0",
"swr": "^0.4.0",
"tabbable": "^5.1.5",
"tailwindcss": "^2.0.2"

View File

@ -16,14 +16,14 @@
"jsx": "preserve",
"paths": {
"@lib/*": ["lib/*"],
"@assets/*": ["assets/*"],
"@config/*": ["config/*"],
"@components/*": ["components/*"],
"@utils/*": ["utils/*"],
"@commerce/*": ["framework/commerce/*"],
"@config/*": ["config/*"],
"@assets/*": ["assets/*"],
"@components/*": ["components/*"],
"@commerce": ["framework/commerce"],
"@framework/*": ["framework/bigcommerce/*"],
"@framework": ["framework/bigcommerce"]
"@commerce/*": ["framework/commerce/*"],
"@framework": ["framework/bigcommerce"],
"@framework/*": ["framework/bigcommerce/*"]
}
},
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],

View File

@ -6214,6 +6214,11 @@ shell-quote@1.7.2:
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
shopify-buy@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/shopify-buy/-/shopify-buy-2.11.0.tgz#0f7cb52741395e4ae778c336f32ddf3fe67c2f35"
integrity sha512-bGjS1b/VCPvCjazSstlKwgLtK1WBotWom06/12loja8yfo/cWkLuJsakBbQe1uEIDiOLhKaR0M0CAXZFheYDug==
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"