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

This commit is contained in:
cond0r
2021-02-25 00:58:22 +02:00
43 changed files with 500 additions and 158 deletions

View File

@@ -1,7 +1,7 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useLogin, { UseLogin } from '@commerce/use-login'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
import type { LoginBody } from '../api/customers/login'
import useCustomer from '../customer/use-customer'

View File

@@ -1,6 +1,6 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import useLogout, { UseLogout } from '@commerce/use-logout'
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
import useCustomer from '../customer/use-customer'
export default useLogout as UseLogout<typeof handler>

View File

@@ -1,7 +1,7 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useSignup, { UseSignup } from '@commerce/use-signup'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
import type { SignupBody } from '../api/customers/signup'
import useCustomer from '../customer/use-customer'

View File

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

View File

@@ -0,0 +1,37 @@
module.exports = {
images: {
domains: ['cdn11.bigcommerce.com'],
},
i18n: {
locales: ['en-US', 'es'],
defaultLocale: 'en-US',
},
rewrites() {
return [
{
source: '/checkout',
destination: '/api/bigcommerce/checkout',
},
// The logout is also an action so this route is not required, but it's also another way
// you can allow a logout!
{
source: '/logout',
destination: '/api/bigcommerce/customers/logout?redirect_to=/',
},
// Rewrites for /search
{
source: '/search/designers/:name',
destination: '/search',
},
{
source: '/search/designers/:name/:category',
destination: '/search',
},
{
// This rewrite will also handle `/search/designers`
source: '/search/:category',
destination: '/search',
},
]
},
}

View File

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

View File

@@ -1,4 +1,3 @@
export { default as useAddItem } from './use-add-item'
export { default as useWishlist } from './use-wishlist'
export { default as useRemoveItem } from './use-remove-item'
export { default as useWishlistActions } from './use-wishlist-actions'

View File

@@ -1,11 +0,0 @@
import useAddItem from './use-add-item'
import useRemoveItem from './use-remove-item'
// This hook is probably not going to be used, but it's here
// to show how a commerce should be structuring it
export default function useWishlistActions() {
const addItem = useAddItem()
const removeItem = useRemoveItem()
return { addItem, removeItem }
}

View File

@@ -1,7 +1,7 @@
import { useHook, useMutationHook } from './utils/use-hook'
import { mutationFetcher } from './utils/default-fetcher'
import type { MutationHook, HookFetcherFn } from './utils/types'
import type { Provider } from '.'
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { MutationHook, HookFetcherFn } from '../utils/types'
import type { Provider } from '..'
export type UseLogin<
H extends MutationHook<any, any, any> = MutationHook<null, {}, {}>

View File

@@ -1,7 +1,7 @@
import { useHook, useMutationHook } from './utils/use-hook'
import { mutationFetcher } from './utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from './utils/types'
import type { Provider } from '.'
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Provider } from '..'
export type UseLogout<
H extends MutationHook<any, any, any> = MutationHook<null>

View File

@@ -1,7 +1,7 @@
import { useHook, useMutationHook } from './utils/use-hook'
import { mutationFetcher } from './utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from './utils/types'
import type { Provider } from '.'
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Provider } from '..'
export type UseSignup<
H extends MutationHook<any, any, any> = MutationHook<null>

View File

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

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react'
import { useCommerce } from '.'
import { useCommerce } from '..'
export function formatPrice({
amount,

View File

@@ -2,8 +2,10 @@ import type { Wishlist as BCWishlist } from '@framework/api/wishlist'
import type { Customer as BCCustomer } from '@framework/api/customers'
import type { SearchProductsData as BCSearchProductsData } from '@framework/api/catalog/products'
export type Features = 'wishlist' | 'checkout' | string
export type CommerceProviderConfig = {
features: Record<string, boolean>
features: Record<Features, boolean>
}
export type Discount = {

11
framework/commerce/utils/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,11 @@
module.exports = ({ features }) => {
let output = {
env: {},
}
if (!!Object.keys(features).length) {
Object.keys(features).map(
(r) => (output.env[`COMMERCE_${r.toUpperCase()}_ENABLED`] = features[r])
)
}
return output
}

View File

@@ -1,4 +1,4 @@
import commerceProviderConfig from '@framework/config.json'
import commerceProviderConfig from '../config.json'
import type { CommerceProviderConfig } from '../types'
import memo from 'lodash.memoize'
@@ -14,6 +14,16 @@ function isFeatureEnabled(config: CommerceProviderConfig) {
.includes(desideredFeature)
}
export function toEnvConfig(
configMap: CommerceProviderConfig['features']
): Map<string, boolean> {
let toEnvConfigMap = new Map<string, boolean>()
Object.keys(configMap).map((r) =>
toEnvConfigMap.set(`${r.toUpperCase()}_ENABLED`, configMap[r])
)
return toEnvConfigMap
}
function boostrap(): FeaturesAPI {
const basis = {
isEnabled: () => false,

View File

@@ -1,23 +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://commerce-theta-ashy.vercel.app).
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. Environment variables need to be set:
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=
```
2. Point the framework to `shopify` by updating `tsconfig.json`:
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,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

@@ -1,5 +1,4 @@
import { GraphQLFetcherResult } from '@commerce/api'
import { getConfig, ShopifyConfig } from '../api'
import { Product } from '../schema'
import getProductQuery from '../utils/queries/get-product-query'

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: [],
}
})
}