Feature/icky 194 (#5)

* folder and env setup

* codegen.json headers removed

* use-cart code flow updated

* use-cart code flow updated

* Implemented get-cart functionality

* removed unused file

* getAnonymousShopperToken function added

* normalization mapping updated

* PR points resolved

* Anonymous shopper token query added

* getAnonymousShopperToken function added

* Anonymous shopper token query added

Co-authored-by: Chandradeepta <43542673+Chandradeepta@users.noreply.github.com>
This commit is contained in:
kibo-chandradeeptalaha 2021-08-30 21:11:10 +05:30 committed by GitHub
parent 9e92abdda0
commit 0e5c68ef58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 452 additions and 44 deletions

View File

@ -28,3 +28,4 @@ KIBO_API_TOKEN=
KIBO_API_URL=
KIBO_CART_COOKIE=
KIBO_CUSTOMER_COOKIE=
KIBO_API_HOST=

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach to Next JS App",
"skipFiles": ["<node_internals>/**"],
"port": 9229
}
]
}

View File

@ -1 +1 @@
# Next.js Local Provider
# Next.js Kibo Provider

View File

@ -0,0 +1,23 @@
import { normalizeCart } from '@framework/lib/normalize'
import { Cart } from '@framework/schema'
import type { CartEndpoint } from '.'
import { getCartQuery } from '../../queries/getCartQuery'
const getCart: CartEndpoint['handlers']['getCart'] = async ({
res,
body: { cartId },
config,
}) => {
let currentCart: Cart = {}
try {
let result = await config.fetch(getCartQuery)
currentCart = result?.data?.currentCart
} catch (error) {
throw error
}
res.status(200).json({
data: currentCart ? normalizeCart(currentCart) : null,
})
}
export default getCart

View File

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

View File

@ -1,6 +1,6 @@
import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api'
import { getCommerceApi as commerceApi } from '@commerce/api'
import createFetcher from './utils/fetch-local'
import createFetchGraphqlApi from './utils/fetch-graphql-api'
import getAllPages from './operations/get-all-pages'
import getPage from './operations/get-page'
@ -9,15 +9,28 @@ import getCustomerWishlist from './operations/get-customer-wishlist'
import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products'
import getProduct from './operations/get-product'
import createFetchStoreApi from './utils/fetch-store-api'
import type { RequestInit } from '@vercel/fetch'
export interface KiboCommerceConfig extends CommerceAPIConfig {
apiHost?: string
clientId?: string
sharedSecret?: string
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
}
export interface KiboCommerceConfig extends CommerceAPIConfig {}
const config: KiboCommerceConfig = {
commerceUrl: process.env.KIBO_API_URL || '',
apiToken: process.env.KIBO_API_TOKEN || '',
cartCookie: process.env.KIBO_CART_COOKIE || '',
customerCookie: process.env.KIBO_CUSTOMER_COOKIE || '',
cartCookieMaxAge: 2592000,
fetch: createFetcher(() => getCommerceApi().getConfig()),
fetch: createFetchGraphqlApi(() => getCommerceApi().getConfig()),
// REST API
apiHost: process.env.KIBO_API_HOST || '',
clientId: process.env.KIBO_CLIENT_ID || '',
sharedSecret: process.env.KIBO_SHARED_SECRET || '',
storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()),
}
const operations = {

View File

@ -0,0 +1,7 @@
export const getAnonymousShopperTokenQuery = /* GraphQL */ `
query {
getAnonymousShopperToken {
accessToken
}
}
`

View File

@ -0,0 +1,54 @@
export const getCartQuery = /* GraphQL */`
query cart {
currentCart {
id
userId
orderDiscounts {
impact
discount {
id
name
}
couponCode
}
subtotal
shippingTotal
total
items {
id
subtotal
unitPrice{
extendedAmount
}
product {
productCode
variationProductCode
name
description
imageUrl
options {
attributeFQN
name
value
}
properties {
attributeFQN
name
values {
value
}
}
sku
price {
price
salePrice
}
categories {
id
}
}
quantity
}
}
}
`

View File

@ -0,0 +1,43 @@
import { FetcherError } from '@commerce/utils/errors'
import type { GraphQLFetcher } from '@commerce/api'
import type { KiboCommerceConfig } from '../index'
import fetch from './fetch'
import getAnonymousShopperToken from './get-anonymous-shopper-token'
const fetchGraphqlApi: (
getConfig: () => KiboCommerceConfig
) => GraphQLFetcher = (getConfig) => async (
query: string,
{ variables, preview } = {},
fetchOptions
) => {
const config = getConfig()
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
method: 'POST',
headers: {
Authorization: `Bearer ${config.apiToken}`,
...fetchOptions?.headers,
'Content-Type': 'application/json',
// Need to fetch access token from cookie
'x-vol-user-claims':
'z40ROeWoZYd65SxBHSqnq/j/SRP0tIBHAf/3Sxw2MJLS8lmj1sF9Y+8eWaTObnCbAtFkiNx/BPfojtUFYQj2P9aVPgHsR+IaTpeAdfG1AM0fMLFvIrDbHK6E/BKhupU5NJQAFwYsoImRzIh8jOpXrigBWH9OW/dBjOtuAJaDaDRHdZ3xyDKZQnFa24IZN6b/UZYHf4r6arUU3MjPoVibQdtBObtJPYwe3XtOI/xaInqpehTJPq9nTZlTWR8Tv59UelC4bVWIuGtSAdawmuSS7H8pb5PemmB9MwMeLkGaWZsaRdxMfdOJE8REGqOYr3j89iEj/0a6G1zraVbLzGXyW0hVkz6InxARzA4p96n2n+ZCwWI/olcQKTxJCLsoZ3dVVkWretgUJFMxzAbzDEDtUIda+VuhzhhmlY4SFgOjxtSIudlyAcYs4xwksjDhBtt8RrTyobCUUau1sfht9Zf1pw==',
},
body: JSON.stringify({
query,
variables,
}),
})
const json = await res.json()
if (json.errors) {
throw new FetcherError({
errors: json.errors ?? [{ message: 'Failed to fetch KiboCommerce API' }],
status: res.status,
})
}
return { data: json.data, res }
}
export default fetchGraphqlApi

View File

@ -0,0 +1,68 @@
import type { RequestInit, Response } from '@vercel/fetch'
import type { KiboCommerceConfig } from '../index'
import fetch from './fetch'
const fetchStoreApi = <T>(getConfig: () => KiboCommerceConfig) => async (
endpoint: string,
options?: RequestInit
): Promise<T> => {
const config = getConfig()
let res: Response
try {
res = await fetch(config.apiHost + endpoint, {
...options,
headers: {
...options?.headers,
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiToken}`,
},
})
} catch (error) {
throw new Error(`Fetch to Kibocommerce failed: ${error.message}`)
}
const contentType = res.headers.get('Content-Type')
const isJSON = contentType?.includes('application/json')
if (!res.ok) {
const data = isJSON ? await res.json() : await getTextOrNull(res)
console.log('-----------anon-----------', data)
const headers = getRawHeaders(res)
const msg = `Kibo Commerce API error (${
res.status
}) \nHeaders: ${JSON.stringify(headers, null, 2)}\n${
typeof data === 'string' ? data : JSON.stringify(data, null, 2)
}`
// throw new BigcommerceApiError(msg, res, data)
}
// if (res.status !== 204 && !isJSON) {
// throw new BigcommerceApiError(
// `Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
// res
// )
// }
// If something was removed, the response will be empty
return res.status === 200 && (await res.json())
}
export default fetchStoreApi
function getRawHeaders(res: Response) {
const headers: { [key: string]: string } = {}
res.headers.forEach((value, key) => {
headers[key] = value
})
return headers
}
function getTextOrNull(res: Response) {
try {
return res.text()
} catch (err) {
return null
}
}

View File

@ -0,0 +1,13 @@
import type { KiboCommerceConfig } from '../'
import { getAnonymousShopperTokenQuery } from '../queries/getAnonymousShopperTokenQuery'
async function getAnonymousShopperToken({
config,
}: {
config: KiboCommerceConfig
}): Promise<string | undefined> {
const { data } = await config.fetch(getAnonymousShopperTokenQuery)
return String(data?.getAnonymousShopperToken?.accessToken)
}
export default getAnonymousShopperToken

View File

@ -6,37 +6,28 @@ export default useCart as UseCart<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
method: 'GET',
url: '/api/cart',
},
async fetcher() {
return {
id: '',
createdAt: '',
currency: { code: '' },
taxesIncluded: '',
lineItems: [],
lineItemsSubtotalPrice: '',
subtotalPrice: 0,
totalPrice: 0,
}
async fetcher({ options, fetch }) {
return await fetch({ ...options })
},
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]
)
},
useHook:
({ useData }) =>
(input) => {
return useMemo(
() =>
Object.create(
{},
{
isEmpty: {
get() {
return true
},
enumerable: true,
},
}
),
[]
)
},
}

View File

@ -1,9 +1,9 @@
{
"provider": "kibocommerce",
"features": {
"wishlist": false,
"cart": false,
"search": false,
"customerAuth": false
"wishlist": true,
"cart": true,
"search": true,
"customerAuth": true
}
}

View File

@ -0,0 +1,5 @@
// Remove trailing and leading slash, usually included in nodes
// returned by the BigCommerce API
const getSlug = (path: string) => path.replace(/^\/|\/$/g, '')
export default getSlug

View File

@ -0,0 +1,13 @@
import update, { Context } from 'immutability-helper'
const c = new Context()
c.extend('$auto', function (value, object) {
return object ? c.update(object, value) : c.update({}, value)
})
c.extend('$autoArray', function (value, object) {
return object ? c.update(object, value) : c.update([], value)
})
export default c.update

View File

@ -0,0 +1,137 @@
// import type { Product } from '../types/product'
// import type { Cart, BigcommerceCart, LineItem } from '../types/cart'
// import type { Page } from '../types/page'
// import type { BCCategory, Category } from '../types/site'
// import { definitions } from '../api/definitions/store-content'
import update from './immutability'
import getSlug from './get-slug'
function normalizeProductOption(productOption: any) {
const {
node: { entityId, values: { edges = [] } = {}, ...rest },
} = productOption
return {
id: entityId,
values: edges?.map(({ node }: any) => node),
...rest,
}
}
export function normalizeProduct(productNode: any): any {
const {
entityId: id,
productOptions,
prices,
path,
id: _,
options: _0,
} = productNode
return update(productNode, {
id: { $set: String(id) },
images: {
$apply: ({ edges }: any) =>
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
url: urlOriginal,
alt: altText,
...rest,
})),
},
variants: {
$apply: ({ edges }: any) =>
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({
id: entityId,
options: productOptions?.edges
? productOptions.edges.map(normalizeProductOption)
: [],
...rest,
})),
},
options: {
$set: productOptions.edges
? productOptions?.edges.map(normalizeProductOption)
: [],
},
brand: {
$apply: (brand: any) => (brand?.entityId ? brand?.entityId : null),
},
slug: {
$set: path?.replace(/^\/+|\/+$/g, ''),
},
price: {
$set: {
value: prices?.price.value,
currencyCode: prices?.price.currencyCode,
},
},
$unset: ['entityId'],
})
}
export function normalizePage(page: any): any {
return {
id: String(page.id),
name: page.name,
is_visible: page.is_visible,
sort_order: page.sort_order,
body: page.body,
}
}
export function normalizeCart(data: any): any {
return {
id: data.id,
customerId: data.userId,
email: data?.email,
createdAt: data?.created_time,
currency: {
code: 'USD',
},
taxesIncluded: true,
lineItems: data.items.map(normalizeLineItem),
lineItemsSubtotalPrice: data?.items.reduce(
(acc: number, obj: { subtotal: number }) => acc + obj.subtotal,
0
),
subtotalPrice: data?.subtotal,
totalPrice: data?.total,
discounts: data.orderDiscounts?.map((discount: any) => ({
value: discount.impact,
})),
}
}
function normalizeLineItem(item: any): any {
return {
id: item.id,
variantId: item.product.variationProductCode,
productId: String(item.product.productCode),
name: item.product.name,
quantity: item.quantity,
variant: {
id: item.product.variationProductCode,
sku: item.product?.sku,
name: item.product.name,
image: {
url: item?.product.imageUrl,
},
requiresShipping: item?.is_require_shipping,
price: item?.unitPrice.extendedAmount,
listPrice: 0,
},
path: `${item.product.productCode}/na`,
discounts: item?.discounts?.map((discount: any) => ({
value: discount.discounted_amount,
})),
}
}
export function normalizeCategory(category: any): any {
return {
id: `${category.entityId}`,
name: category.name,
slug: getSlug(category.path),
path: category.path,
}
}

View File

@ -11,7 +11,7 @@ import { handler as useSignup } from './auth/use-signup'
export const kiboCommerceProvider = {
locale: 'en-us',
cartCookie: 'bc_cartId',
cartCookie: 'kibo_cart',
fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer },

View File

@ -30,13 +30,13 @@ export default function Cart() {
const { price: subTotal } = usePrice(
data && {
amount: Number(data.subtotalPrice),
currencyCode: data.currency.code,
currencyCode: data?.currency?.code,
}
)
const { price: total } = usePrice(
data && {
amount: Number(data.totalPrice),
currencyCode: data.currency.code,
currencyCode: data?.currency?.code,
}
)
@ -83,7 +83,7 @@ export default function Cart() {
<CartItem
key={item.id}
item={item}
currencyCode={data?.currency.code!}
currencyCode={data?.currency?.code!}
/>
))}
</ul>