mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 07:26:59 +00:00
Adding Local Provider
This commit is contained in:
parent
5ce1ee7aba
commit
23b69ec442
@ -1,33 +1 @@
|
|||||||
# Vendure Storefront Data Hooks
|
# Next.js Local Provider
|
||||||
|
|
||||||
UI hooks and data fetching methods built from the ground up for e-commerce applications written in React, that use [Vendure](http://vendure.io/) as a headless e-commerce platform.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. Clone this repo and install its dependencies with `yarn install` or `npm install`
|
|
||||||
2. Set the Vendure provider and API URL in your `.env.local` file:
|
|
||||||
```
|
|
||||||
COMMERCE_PROVIDER=vendure
|
|
||||||
NEXT_PUBLIC_VENDURE_SHOP_API_URL=https://demo.vendure.io/shop-api
|
|
||||||
NEXT_PUBLIC_VENDURE_LOCAL_URL=/vendure-shop-api
|
|
||||||
```
|
|
||||||
3. With the Vendure server running, start this project using `yarn dev` or `npm run dev`.
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
|
|
||||||
1. Vendure does not ship with built-in wishlist functionality.
|
|
||||||
2. Nor does it come with any kind of blog/page-building feature. Both of these can be created as Vendure plugins, however.
|
|
||||||
3. The entire Vendure customer flow is carried out via its GraphQL API. This means that there is no external, pre-existing checkout flow. The checkout flow must be created as part of the Next.js app. See https://github.com/vercel/commerce/issues/64 for further discusion.
|
|
||||||
4. By default, the sign-up flow in Vendure uses email verification. This means that using the existing "sign up" flow from this project will not grant a new user the ability to authenticate, since the new account must first be verified. Again, the necessary parts to support this flow can be created as part of the Next.js app.
|
|
||||||
|
|
||||||
## Code generation
|
|
||||||
|
|
||||||
This provider makes use of GraphQL code generation. The [schema.graphql](./schema.graphql) and [schema.d.ts](./schema.d.ts) files contain the generated types & schema introspection results.
|
|
||||||
|
|
||||||
When developing the provider, changes to any GraphQL operations should be followed by re-generation of the types and schema files:
|
|
||||||
|
|
||||||
From the project root dir, run
|
|
||||||
|
|
||||||
```sh
|
|
||||||
graphql-codegen --config ./framework/vendure/codegen.json
|
|
||||||
```
|
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import type { CommerceAPIConfig } from '@commerce/api'
|
import type { APIProvider, CommerceAPIConfig } from '@commerce/api'
|
||||||
import fetchGraphqlApi from './utils/fetch-graphql-api'
|
|
||||||
import { CommerceAPI, getCommerceApi as commerceApi } from '@commerce/api'
|
import { CommerceAPI, getCommerceApi as commerceApi } from '@commerce/api'
|
||||||
|
import fetchGraphqlApi from './utils/fetch-graphql-api'
|
||||||
|
|
||||||
import getAllPages from './operations/get-all-pages'
|
import getAllPages from './operations/get-all-pages'
|
||||||
import getPage from './operations/get-page'
|
import getPage from './operations/get-page'
|
||||||
import getSiteInfo from './operations/get-site-info'
|
import getSiteInfo from './operations/get-site-info'
|
||||||
|
import getCustomerWishlist from './operations/get-customer-wishlist'
|
||||||
import getAllProductPaths from './operations/get-all-product-paths'
|
import getAllProductPaths from './operations/get-all-product-paths'
|
||||||
import getAllProducts from './operations/get-all-products'
|
import getAllProducts from './operations/get-all-products'
|
||||||
import getProduct from './operations/get-product'
|
import getProduct from './operations/get-product'
|
||||||
|
|
||||||
export interface VendureConfig extends CommerceAPIConfig {}
|
export interface LocalConfig extends CommerceAPIConfig {}
|
||||||
|
|
||||||
const ONE_DAY = 60 * 60 * 24
|
const ONE_DAY = 60 * 60 * 24
|
||||||
const config: VendureConfig = {
|
const config: LocalConfig = {
|
||||||
commerceUrl: '',
|
commerceUrl: '',
|
||||||
apiToken: '',
|
apiToken: '',
|
||||||
cartCookie: '',
|
cartCookie: '',
|
||||||
@ -25,6 +26,7 @@ const operations = {
|
|||||||
getAllPages,
|
getAllPages,
|
||||||
getPage,
|
getPage,
|
||||||
getSiteInfo,
|
getSiteInfo,
|
||||||
|
getCustomerWishlist,
|
||||||
getAllProductPaths,
|
getAllProductPaths,
|
||||||
getAllProducts,
|
getAllProducts,
|
||||||
getProduct,
|
getProduct,
|
||||||
@ -33,9 +35,10 @@ const operations = {
|
|||||||
export const provider = { config, operations }
|
export const provider = { config, operations }
|
||||||
|
|
||||||
export type Provider = typeof provider
|
export type Provider = typeof provider
|
||||||
|
export type LocalAPI<P extends Provider = Provider> = CommerceAPI<P>
|
||||||
|
|
||||||
export function getCommerceApi<P extends Provider>(
|
export function getCommerceApi<P extends Provider>(
|
||||||
customProvider: P = provider as any
|
customProvider: P = provider as any
|
||||||
): CommerceAPI<P> {
|
): LocalAPI<P> {
|
||||||
return commerceApi(customProvider)
|
return commerceApi(customProvider)
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,10 @@ export type Page = any
|
|||||||
export type GetAllPagesResult = { pages: Page[] }
|
export type GetAllPagesResult = { pages: Page[] }
|
||||||
|
|
||||||
export default function getAllPagesOperation() {
|
export default function getAllPagesOperation() {
|
||||||
function getAllPages(): GetAllPagesResult {
|
function getAllPages(): Promise<GetAllPagesResult> {
|
||||||
return {
|
return Promise.resolve({
|
||||||
pages: [],
|
pages: [],
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
return getAllPages
|
return getAllPages
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ export type GetAllProductPathsResult = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function getAllProductPathsOperation() {
|
export default function getAllProductPathsOperation() {
|
||||||
function getAllProductPaths(): GetAllProductPathsResult {
|
function getAllProductPaths(): Promise<GetAllProductPathsResult> {
|
||||||
return {
|
return Promise.resolve({
|
||||||
products: [].map((p) => ({ path: `/hello` })),
|
products: [].map((p) => ({ path: `/hello` })),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return getAllProductPaths
|
return getAllProductPaths
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import { Product } from '@commerce/types/product'
|
import { Product } from '@commerce/types/product'
|
||||||
import { OperationContext } from '@commerce/api/operations'
|
|
||||||
|
import type { OperationContext } from '@commerce/api/operations'
|
||||||
|
import type { Provider } from '../index'
|
||||||
|
|
||||||
export type ProductVariables = { first?: number }
|
export type ProductVariables = { first?: number }
|
||||||
|
|
||||||
export default function getAllProductsOperation() {
|
export default function getAllProductsOperation({
|
||||||
function getAllProducts(): { products: Product[] | any[] } {
|
commerce,
|
||||||
return {
|
}: OperationContext<Provider>) {
|
||||||
|
async function getAllProducts(): Promise<{ products: Product[] | any[] }> {
|
||||||
|
return Promise.resolve({
|
||||||
products: [],
|
products: [],
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
return getAllProducts
|
return getAllProducts
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@ export type PageVariables = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function getPageOperation() {
|
export default function getPageOperation() {
|
||||||
function getPage(): GetPageResult {
|
function getPage(): Promise<GetPageResult> {
|
||||||
return {}
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
return getPage
|
return getPage
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,11 @@ export type GetSiteInfoResult<
|
|||||||
> = T
|
> = T
|
||||||
|
|
||||||
export default function getSiteInfoOperation({}: OperationContext<any>) {
|
export default function getSiteInfoOperation({}: OperationContext<any>) {
|
||||||
function getSiteInfo(): GetSiteInfoResult {
|
function getSiteInfo(): Promise<GetSiteInfoResult> {
|
||||||
return {
|
return Promise.resolve({
|
||||||
categories: [],
|
categories: [],
|
||||||
brands: [],
|
brands: [],
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSiteInfo
|
return getSiteInfo
|
||||||
|
6
framework/local/api/operations/index.ts
Normal file
6
framework/local/api/operations/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export { default as getPage } from './get-page'
|
||||||
|
export { default as getSiteInfo } from './get-site-info'
|
||||||
|
export { default as getAllPages } from './get-all-pages'
|
||||||
|
export { default as getProduct } from './get-product'
|
||||||
|
export { default as getAllProducts } from './get-all-products'
|
||||||
|
export { default as getAllProductPaths } from './get-all-product-paths'
|
@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"schema": {
|
|
||||||
"http://localhost:3001/shop-api": {}
|
|
||||||
},
|
|
||||||
"documents": [
|
|
||||||
{
|
|
||||||
"./framework/vendure/**/*.{ts,tsx}": {
|
|
||||||
"noRequire": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"generates": {
|
|
||||||
"./framework/vendure/schema.d.ts": {
|
|
||||||
"plugins": ["typescript", "typescript-operations"],
|
|
||||||
"config": {
|
|
||||||
"scalars": {
|
|
||||||
"ID": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"./framework/vendure/schema.graphql": {
|
|
||||||
"plugins": ["schema-ast"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hooks": {
|
|
||||||
"afterAllFileWrite": ["prettier --write"]
|
|
||||||
}
|
|
||||||
}
|
|
22
framework/local/data.json
Normal file
22
framework/local/data.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"id": "Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzU0NDczMjUwMjQ0MjA=",
|
||||||
|
"name": "New Short Sleeve T-Shirt",
|
||||||
|
"vendor": "Next.js",
|
||||||
|
"path": "/new-short-sleeve-t-shirt",
|
||||||
|
"slug": "new-short-sleeve-t-shirt",
|
||||||
|
"price": { "value": 25, "currencyCode": "USD" },
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"url": "/assets/drop-shirt-0",
|
||||||
|
"altText": null,
|
||||||
|
"width": 1000,
|
||||||
|
"height": 1000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variants": [],
|
||||||
|
"options": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -1,51 +1,19 @@
|
|||||||
import { Fetcher } from '@commerce/utils/types'
|
import { Fetcher } from '@commerce/utils/types'
|
||||||
import { FetcherError } from '@commerce/utils/errors'
|
|
||||||
|
|
||||||
async function getText(res: Response) {
|
const shopApiUrl = '/data.json'
|
||||||
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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetcher: Fetcher = async ({
|
export const fetcher: Fetcher = async ({
|
||||||
url,
|
url,
|
||||||
method = 'POST',
|
method = 'GET',
|
||||||
variables,
|
variables,
|
||||||
query,
|
query,
|
||||||
body: bodyObj,
|
body: bodyObj,
|
||||||
}) => {
|
}) => {
|
||||||
const shopApiUrl =
|
const res = await fetch(shopApiUrl)
|
||||||
process.env.NEXT_PUBLIC_VENDURE_LOCAL_URL ||
|
|
||||||
process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL
|
|
||||||
if (!shopApiUrl) {
|
|
||||||
throw new Error(
|
|
||||||
'The Vendure Shop API url has not been provided. Please define NEXT_PUBLIC_VENDURE_SHOP_API_URL in .env.local'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const hasBody = Boolean(variables || query)
|
|
||||||
const body = hasBody ? JSON.stringify({ query, variables }) : undefined
|
|
||||||
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
|
||||||
const res = await fetch(shopApiUrl, {
|
|
||||||
method,
|
|
||||||
body,
|
|
||||||
headers,
|
|
||||||
credentials: 'include',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const { data } = await res.json()
|
const { data } = await res.json()
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
throw res
|
||||||
throw await getError(res)
|
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,28 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
import { localProvider } from './provider'
|
||||||
import {
|
import {
|
||||||
CommerceConfig,
|
CommerceConfig,
|
||||||
CommerceProvider as CoreCommerceProvider,
|
CommerceProvider as CoreCommerceProvider,
|
||||||
useCommerce as useCoreCommerce,
|
useCommerce as useCoreCommerce,
|
||||||
} from '@commerce'
|
} from '@commerce'
|
||||||
import { vendureProvider } from './provider'
|
|
||||||
|
|
||||||
export const vendureConfig: CommerceConfig = {
|
export const localConfig: CommerceConfig = {
|
||||||
locale: 'en-us',
|
locale: 'en-us',
|
||||||
cartCookie: 'session',
|
cartCookie: 'session',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VendureConfig = Partial<CommerceConfig>
|
export function CommerceProvider({
|
||||||
|
children,
|
||||||
export type VendureProps = {
|
...config
|
||||||
|
}: {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
locale: string
|
locale: string
|
||||||
} & VendureConfig
|
} & Partial<CommerceConfig>) {
|
||||||
|
|
||||||
export function CommerceProvider({ children, ...config }: VendureProps) {
|
|
||||||
return (
|
return (
|
||||||
<CoreCommerceProvider
|
<CoreCommerceProvider
|
||||||
provider={vendureProvider}
|
provider={localProvider}
|
||||||
config={{ ...vendureConfig, ...config }}
|
config={{ ...localConfig, ...config }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CoreCommerceProvider>
|
</CoreCommerceProvider>
|
||||||
|
@ -3,6 +3,6 @@ const commerce = require('./commerce.config.json')
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
commerce,
|
commerce,
|
||||||
images: {
|
images: {
|
||||||
domains: ['localhost', 'demo.vendure.io'],
|
domains: ['localhost'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,12 @@ import { handler as useLogout } from './auth/use-logout'
|
|||||||
import { handler as useSignup } from './auth/use-signup'
|
import { handler as useSignup } from './auth/use-signup'
|
||||||
import { fetcher } from './fetcher'
|
import { fetcher } from './fetcher'
|
||||||
|
|
||||||
export const vendureProvider: Provider = {
|
export type Provider = typeof localProvider
|
||||||
|
|
||||||
|
export const localProvider: Provider = {
|
||||||
locale: 'en-us',
|
locale: 'en-us',
|
||||||
cartCookie: 'session',
|
cartCookie: 'session',
|
||||||
fetcher: (e) => e,
|
fetcher: fetcher,
|
||||||
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||||
customer: { useCustomer },
|
customer: { useCustomer },
|
||||||
products: { useSearch },
|
products: { useSearch },
|
||||||
|
3257
framework/local/schema.d.ts
vendored
3257
framework/local/schema.d.ts
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user