feat(provider): Implement Kibo Commerce provider (#575)

* Icky 161 folder and env setup (#1)

* folder and env setup

* codegen.json headers removed

Co-authored-by: Chandradeepta <43542673+Chandradeepta@users.noreply.github.com>

* 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>

* Icky 175 (#3)

* folder and env setup

* codegen.json headers removed

* icky-175-get-site-info

* PR comments resolved

* update category Id

Co-authored-by: Chandradeepta <43542673+Chandradeepta@users.noreply.github.com>

* Icky-169-LogIn (#4)

* Update README.md

* Initial Commit

* Commited Keys

* GraphQL Changes

* GraphQL Query

* Final Changes

* Changed login.ts

* Made changes in login.ts

* Final Changes

* Refactored login.ts

* SignUp Initial Checkin

* logout Initial

* Customer Account Initial Commit

* Logout - deleted cookie

* Reverted ReadMe and UserNav file

* Final Changes

* Resolved comments

* Resolved comments 1

* Resolved comments 2

* Resolved comments 3

* Resolved comments 4

Co-authored-by: SushantJadhav <Sushant.Jadhav@kibocommerce.com>

* ICKY-166-getProducts-and-getProduct (#6)

* GetProduct Initial Commit

* Passed productCode as Slug to get-product

* Moved currencyCode in Config file

* Icky 173 (#9)

* Initial commit related to getAllPages

* Initial Changes

* Making documentListName configurable

* fixing dynamic page rendering and adding typescript code

Co-authored-by: amolnadagonde <amol.nadagonde@kibocommerce.com>
Co-authored-by: kibo-sushant <sushant.jadhav@blueconchtech.com>

* Feature/icky 176 (#8)

* GetProduct Initial Commit

* addItemToCart function implemneted

* Add Item to cart functionality implemented

* ICKY-166-getProducts-and-getProduct (#6)

* GetProduct Initial Commit

* Passed productCode as Slug to get-product

* Moved currencyCode in Config file

* Icky 173 (#9)

* Initial commit related to getAllPages

* Initial Changes

* Making documentListName configurable

* fixing dynamic page rendering and adding typescript code

Co-authored-by: amolnadagonde <amol.nadagonde@kibocommerce.com>
Co-authored-by: kibo-sushant <sushant.jadhav@blueconchtech.com>

* addItemToCart function implemneted

Conflicts resolved

* Add Item to cart functionality implemented

* booleans removed from query

* cart size option enabled

* updated addItem for with and without variants products

Co-authored-by: kibo-sushant <sushant.jadhav@blueconchtech.com>
Co-authored-by: Chandradeepta <43542673+Chandradeepta@users.noreply.github.com>
Co-authored-by: kibo-sushant <89385472+kibo-sushant@users.noreply.github.com>
Co-authored-by: kibo-kevinwatts <85258296+kibo-kevinwatts@users.noreply.github.com>
Co-authored-by: amolnadagonde <amol.nadagonde@kibocommerce.com>

* Removed types from schema.d.ts (#11)

* Final Changes (#16)

* Icky 177 (#13)

* addItemToCart function implemneted

Conflicts resolved

* Add Item to cart functionality implemented

* remove Item from cart implemented

* removed unused code

Co-authored-by: Chandradeepta <43542673+Chandradeepta@users.noreply.github.com>

* Icky 178 - Update Cart Quantity implemented (#14)

* update cart quantity implemented

* add item to cart bug fixed

Co-authored-by: Chandradeepta <43542673+Chandradeepta@users.noreply.github.com>

* Icky 182 (#12)

* initial commit

* useSearch hook

* remove extra spaces

* revert pages and component changes

* remove extra spacing in search.tsx

* changes in catalog/products and product-search-vars

* initial commit

* useSearch hook

* remove extra spaces

* revert pages and component changes

* remove extra spacing in search.tsx

* changes in catalog/products and product-search-vars

* changes in product-search-vars

* remove unwanted boolean

* Feature/icky 179 (#17)

* initial commit

* useSearch hook

* remove extra spaces

* revert pages and component changes

* remove extra spacing in search.tsx

* changes in catalog/products and product-search-vars

* initial commit

* useSearch hook

* remove extra spaces

* revert pages and component changes

* remove extra spacing in search.tsx

* changes in catalog/products and product-search-vars

* changes in product-search-vars

* remove unwanted boolean

* initial commit

* updated Provider

* usewishlist/getwishlist

* changes in provider.ts

* initial commit

* useSearch hook

* revert pages and component changes

* remove extra spacing in search.tsx

* changes in catalog/products and product-search-vars

* initial commit

* useSearch hook

* revert pages and component changes

* remove extra spacing in search.tsx

* changes in catalog/products and product-search-vars

* initial commit

* usewishlist/getwishlist

* updated Provider

* changes in provider.ts

* normalize wishlistitem

* changes in get-customer-account

* remove unwanted code

* resolve empty wishlist case

* resolve pr comments

Co-authored-by: kibo-sushant <sushant.jadhav@blueconchtech.com>

* token encoding and decoding fixed (#19)

Co-authored-by: Chandradeepta <43542673+Chandradeepta@users.noreply.github.com>

* Feature/icky- 180 & 181 (#20)

* initial commit

* useSearch hook

* revert pages and component changes

* remove extra spacing in search.tsx

* changes in catalog/products and product-search-vars

* initial commit

* useSearch hook

* revert pages and component changes

* remove extra spacing in search.tsx

* changes in catalog/products and product-search-vars

* usewishlist/getwishlist

* changes in provider.ts

* changes in get-customer-account

* initial commit

* useSearch hook

* revert pages and component changes

* remove extra spacing in search.tsx

* changes in catalog/products and product-search-vars

* initial commit

* useSearch hook

* revert pages and component changes

* remove extra spacing in search.tsx

* changes in catalog/products and product-search-vars

* usewishlist/getwishlist

* changes in provider.ts

* remove unwanted code

* initial commit

* resolve pr comments

* changes in add-item

* remove wishlist fragment

* remove item from wishlist

* changes in normalize.ts

* changes in additemtowishlist mutation

* resove pr comments

* Feature/icky 291 (#22)

* Kibo API authentication helper handling oauth token generation / refresh and making auth ticket available to process via next server runtime config

* Update .env template with placeholder for Kibo Auth Url

* resolve ICKY-275 (#24)

* Fix/icky 276 (#21)

* remove Item from cart implemented

* update cart quantity implemented

* removed unused code

* ICKY 176 and 263 implemented

* ICKY 263 removed

* PR points resolved

Co-authored-by: Chandradeepta <43542673+Chandradeepta@users.noreply.github.com>

* ICKY-263 (#23)

* ICKY-263

* resolve pr comments

* resolve pr comments for customer typescript

* docs: update kibo commerce readme with env details (#26)

* resolve icky-264 (#25)

* chore: remove extra field from .env.template

* chore: remove extra .vscode launch json file

* chore: cleanup test message from kibocommerce fork

* chore(docs): remove extra field from .env template and .env related docs

* chore: remove test data json file

* chore: delete yarn.lock

* refactor: remove unused fetch from kibo config, remove unused CommerceProvider

* refactor: rename queries and util modules for consistency

* chore: add checkout related api noop handlers and hooks

* chore: revert modified core files

* chore(config): add kibo production sandbox cdn to image domains config

* fix: page normalizer and query for static pages

* chore: remove commented code and unnecessary imports

* fix(module paths): switch framework alias for relative path

Co-authored-by: kibo-chandradeeptalaha <89371824+kibo-chandradeeptalaha@users.noreply.github.com>
Co-authored-by: Chandradeepta <43542673+Chandradeepta@users.noreply.github.com>
Co-authored-by: kibo-geetanshuchhabra <89399259+kibo-geetanshuchhabra@users.noreply.github.com>
Co-authored-by: kibo-sushant <89385472+kibo-sushant@users.noreply.github.com>
Co-authored-by: SushantJadhav <Sushant.Jadhav@kibocommerce.com>
Co-authored-by: amolnadagonde <amol.nadagonde@kibocommerce.com>
Co-authored-by: kibo-sushant <sushant.jadhav@blueconchtech.com>
Co-authored-by: kibo-amolnadagonde <75060520+kibo-amolnadagonde@users.noreply.github.com>
This commit is contained in:
kibo-kevinwatts 2021-12-15 07:25:09 -06:00 committed by GitHub
parent d77d000431
commit 9a4772cdb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 23610 additions and 3 deletions

View File

@ -27,3 +27,10 @@ NEXT_PUBLIC_VENDURE_LOCAL_URL=
ORDERCLOUD_CLIENT_ID=
ORDERCLOUD_CLIENT_SECRET=
STRIPE_SECRET=
KIBO_API_URL=
KIBO_CLIENT_ID=
KIBO_SHARED_SECRET=
KIBO_CART_COOKIE=
KIBO_CUSTOMER_COOKIE=
KIBO_API_HOST=

View File

@ -155,4 +155,4 @@ After Email confirmation, Checkout should be manually enabled through BigCommerc
<br>
<br>
BigCommerce team has been notified and they plan to add more details about this subject.
</details>
</details>

View File

@ -15,7 +15,8 @@ const PROVIDERS = [
'swell',
'vendure',
'ordercloud',
'spree',
'kibocommerce',
'spree'
]
function getProviderName() {

View File

@ -0,0 +1,7 @@
COMMERCE_PROVIDER=kibocommerce
KIBO_API_URL=
KIBO_CART_COOKIE=
KIBO_CUSTOMER_COOKIE=
KIBO_CLIENT_ID=
KIBO_SHARED_SECRET=
KIBO_AUTH_URL=

View File

@ -0,0 +1,37 @@
# Kibo Commerce Provider
If you already have a Kibo Commerce account and want to use your current store, then copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
```bash
cp framework/kibocommerce/.env.template .env.local
```
Then, set the environment variables in `.env.local` to match the ones from your store.
```
COMMERCE_PROVIDER='kibocommerce'
KIBO_API_URL= 'https://t1234-s1234.sandbox.mozu.com/graphql'
KIBO_CART_COOKIE='kibo_cart'
KIBO_CUSTOMER_COOKIE='kibo_customer'
KIBO_CLIENT_ID='KIBO.APP.1.0.0.Release'
KIBO_SHARED_SECRET='12345secret'
KIBO_AUTH_URL='https://home.mozu.com'
```
- `KIBO_API_URL` - link to your Kibo Commerce GraphQL API instance.
- `KIBO_CART_COOKIE` - configurable cookie name for cart.
- `KIBO_CUSTOMER_COOKIE` - configurable cookie name for shopper identifier/authentication cookie
- `KIBO_CLIENT_ID` - Unique Application (Client) ID of your Application
- `KIBO_SHARED_SECRET` - Secret API key used to authenticate application/client id.
Your Kibo Client ID and Shared Secret can be found from your [Kibo eCommerce Dev Center](https://mozu.com/login)
Visit [Kibo documentation](https://apidocs.kibong-perf.com/?spec=graphql#auth) for more details on API authentication
Based on the config, this integration will handle Authenticating your application against the Kibo API using the Kibo Client ID and Kibo Shared Secret.
## Contribute
Our commitment to Open Source can be found [here](https://vercel.com/oss).
If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues).

View File

@ -0,0 +1,102 @@
import { Product } from './../../../schema.d'
import { normalizeCart } from '../../../lib/normalize'
import type { CartEndpoint } from '.'
import addToCurrentCartMutation from '../../../api/mutations/addToCart-mutation'
import { getProductQuery } from '../../../api/queries/get-product-query'
import { getCartQuery } from '../../../api/queries/get-cart-query'
import CookieHandler from '../../../api/utils/cookie-handler'
const buildAddToCartVariables = ({
productId,
variantId,
quantity = 1,
productResponse,
}: {
productId: string
variantId: string
quantity: number
productResponse: any
}) => {
const { product } = productResponse.data
const selectedOptions = product.variations?.find(
(v: any) => v.productCode === variantId
).options
let options: any[] = []
selectedOptions?.forEach((each: any) => {
product?.options
.filter((option: any) => {
return option.attributeFQN == each.attributeFQN
})
.forEach((po: any) => {
options.push({
attributeFQN: po.attributeFQN,
name: po.attributeDetail.name,
value: po.values?.find((v: any) => v.value == each.value).value,
})
})
})
return {
productToAdd: {
product: {
productCode: productId,
variationProductCode: variantId ? variantId : null,
options,
},
quantity,
fulfillmentMethod: 'Ship',
},
}
}
const addItem: CartEndpoint['handlers']['addItem'] = async ({
req,
res,
body: { cartId, item },
config,
}) => {
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
if (!item.quantity) item.quantity = 1
const productResponse = await config.fetch(getProductQuery, {
variables: { productCode: item?.productId },
})
const cookieHandler = new CookieHandler(config, req, res)
let accessToken = null
if (!cookieHandler.getAccessToken()) {
let anonymousShopperTokenResponse = await cookieHandler.getAnonymousToken()
accessToken = anonymousShopperTokenResponse.accessToken;
} else {
accessToken = cookieHandler.getAccessToken()
}
const addToCartResponse = await config.fetch(
addToCurrentCartMutation,
{
variables: buildAddToCartVariables({ ...item, productResponse }),
},
{ headers: { 'x-vol-user-claims': accessToken } }
)
let currentCart = null
if (addToCartResponse.data.addItemToCurrentCart) {
let result = await config.fetch(
getCartQuery,
{},
{ headers: { 'x-vol-user-claims': accessToken } }
)
currentCart = result?.data?.currentCart
}
res.status(200).json({ data: normalizeCart(currentCart) })
}
export default addItem

View File

@ -0,0 +1,41 @@
import CookieHandler from '../../../api/utils/cookie-handler'
import { normalizeCart } from '../../../lib/normalize'
import { Cart } from '../../../schema'
import type { CartEndpoint } from '.'
import { getCartQuery } from '../../queries/get-cart-query'
const getCart: CartEndpoint['handlers']['getCart'] = async ({
req,
res,
body: { cartId },
config,
}) => {
let currentCart: Cart = {}
try {
const cookieHandler = new CookieHandler(config, req, res)
let accessToken = null
if (!cookieHandler.getAccessToken()) {
let anonymousShopperTokenResponse = await cookieHandler.getAnonymousToken()
const response = anonymousShopperTokenResponse.response
accessToken = anonymousShopperTokenResponse.accessToken
cookieHandler.setAnonymousShopperCookie(response)
} else {
accessToken = cookieHandler.getAccessToken()
}
let result = await config.fetch(
getCartQuery,
{},
{ headers: { 'x-vol-user-claims': accessToken } }
)
currentCart = result?.data?.currentCart
} catch (error) {
throw error
}
res.status(200).json({
data: currentCart ? normalizeCart(currentCart) : null,
})
}
export default getCart

View File

@ -0,0 +1,25 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import cartEndpoint from '@commerce/api/endpoints/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

@ -0,0 +1,45 @@
import { normalizeCart } from '../../../lib/normalize'
import type { CartEndpoint } from '.'
import removeItemFromCartMutation from '../../../api/mutations/removeItemFromCart-mutation'
import { getCartQuery } from '../../../api/queries/get-cart-query'
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
req,
res,
body: { cartId, itemId },
config,
}) => {
if (!itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const encodedToken = req.cookies[config.customerCookie]
const token = encodedToken
? Buffer.from(encodedToken, 'base64').toString('ascii')
: null
const accessToken = token ? JSON.parse(token).accessToken : null
const removeItemResponse = await config.fetch(
removeItemFromCartMutation,
{
variables: { id: itemId },
},
{ headers: { 'x-vol-user-claims': accessToken } }
)
let currentCart = null
if (removeItemResponse.data.deleteCurrentCartItem) {
let result = await config.fetch(
getCartQuery,
{},
{ headers: { 'x-vol-user-claims': accessToken } }
)
currentCart = result?.data?.currentCart
}
res.status(200).json({ data: normalizeCart(currentCart) })
}
export default removeItem

View File

@ -0,0 +1,45 @@
import { normalizeCart } from '../../../lib/normalize'
import type { CartEndpoint } from '.'
import { getCartQuery } from '../../../api/queries/get-cart-query'
import updateCartItemQuantityMutation from '../../../api/mutations/updateCartItemQuantity-mutation'
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
req,
res,
body: { cartId, itemId, item },
config,
}) => {
if (!itemId || !item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const encodedToken = req.cookies[config.customerCookie]
const token = encodedToken
? Buffer.from(encodedToken, 'base64').toString('ascii')
: null
const accessToken = token ? JSON.parse(token).accessToken : null
const updateItemResponse = await config.fetch(
updateCartItemQuantityMutation,
{
variables: { itemId: itemId, quantity: item.quantity },
},
{ headers: { 'x-vol-user-claims': accessToken } }
)
let currentCart = null
if (updateItemResponse.data) {
let result = await config.fetch(
getCartQuery,
{},
{ headers: { 'x-vol-user-claims': accessToken } }
)
currentCart = result?.data?.currentCart
}
res.status(200).json({ data: normalizeCart(currentCart) })
}
export default updateItem

View File

@ -0,0 +1,17 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import productsEndpoint from '@commerce/api/endpoints/catalog/products'
import type { KiboCommerceAPI } from '../../..'
import getProducts from '../products/products'
export type ProductsAPI = GetAPISchema<KiboCommerceAPI, any>
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,31 @@
import { Product } from '@commerce/types/product'
import { ProductsEndpoint } from '.'
import productSearchQuery from '../../../queries/product-search-query'
import { buildProductSearchVars } from '../../../../lib/product-search-vars'
import {normalizeProduct} from '../../../../lib/normalize'
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
res,
body: { search, categoryId, brandId, sort },
config,
}) => {
const pageSize = 100;
const filters = {};
const startIndex = 0;
const variables = buildProductSearchVars({
categoryCode: categoryId,
pageSize,
search,
sort,
filters,
startIndex,
})
const {data} = await config.fetch(productSearchQuery, { variables });
const found = data?.products?.items?.length > 0 ? true : false;
let productsResponse= data?.products?.items.map((item: any) =>normalizeProduct(item,config));
const products: Product[] = found ? productsResponse : [];
res.status(200).json({ data: { products, found } });
}
export default getProducts

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,36 @@
import CookieHandler from '../../../api/utils/cookie-handler'
import type { CustomerEndpoint } from '.'
import { getCustomerAccountQuery } from '../../queries/get-customer-account-query'
import { normalizeCustomer } from '../../../lib/normalize'
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] = async ({
req,
res,
config,
}) => {
const cookieHandler = new CookieHandler(config, req, res)
let accessToken = cookieHandler.getAccessToken();
if (cookieHandler.getAccessToken()) {
const { data } = await config.fetch(getCustomerAccountQuery, undefined, {
headers: {
'x-vol-user-claims': accessToken,
},
})
const customer = normalizeCustomer(data?.customerAccount)
if (!customer.id) {
return res.status(400).json({
data: null,
errors: [{ message: 'Customer not found', code: 'not_found' }],
})
}
return res.status(200).json({ data: { customer } })
}
res.status(200).json({ data: null })
}
export default getLoggedInCustomer

View File

@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import customerEndpoint from '@commerce/api/endpoints/customer'
import type { CustomerSchema } from '../../../types/customer'
import type { KiboCommerceAPI } from '../..'
import getLoggedInCustomer from './customer'
export type CustomerAPI = GetAPISchema<KiboCommerceAPI, CustomerSchema>
export type CustomerEndpoint = CustomerAPI['endpoint']
export const handlers: CustomerEndpoint['handlers'] = { getLoggedInCustomer }
const customerApi = createEndpoint<CustomerAPI>({
handler: customerEndpoint,
handlers,
})
export default customerApi

View File

@ -0,0 +1,20 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import loginEndpoint from '@commerce/api/endpoints/login'
import type { LoginSchema } from '../../../types/login'
import type { KiboCommerceAPI } from '../..'
import login from './login'
export type LoginAPI = GetAPISchema<KiboCommerceAPI, LoginSchema>
export type LoginEndpoint = LoginAPI['endpoint']
export const handlers: LoginEndpoint['handlers'] = { login }
const loginApi = createEndpoint<LoginAPI>({
handler: loginEndpoint,
handlers,
})
export default loginApi;

View File

@ -0,0 +1,66 @@
import { FetcherError } from '@commerce/utils/errors'
import type { LoginEndpoint } from '.'
import { loginMutation } from '../../mutations/login-mutation'
import { prepareSetCookie } from '../../../lib/prepare-set-cookie';
import { setCookies } from '../../../lib/set-cookie'
import { getCookieExpirationDate } from '../../../lib/get-cookie-expiration-date'
const invalidCredentials = /invalid credentials/i
const login: LoginEndpoint['handlers']['login'] = async ({
req,
res,
body: { email, password },
config,
commerce,
}) => {
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
let response;
try {
const variables = { loginInput : { username: email, password }};
response = await config.fetch(loginMutation, { variables })
const { account: token } = response.data;
// Set Cookie
const cookieExpirationDate = getCookieExpirationDate(config.customerCookieMaxAgeInDays)
const authCookie = prepareSetCookie(
config.customerCookie,
JSON.stringify(token),
token.accessTokenExpiration ? { expires: cookieExpirationDate }: {},
)
setCookies(res, [authCookie])
} catch (error) {
// Check if the email and password didn't match an existing account
if (
error instanceof FetcherError &&
invalidCredentials.test(error.message)
) {
return res.status(401).json({
data: null,
errors: [
{
message:
'Cannot find an account that matches the provided credentials',
code: 'invalid_credentials',
},
],
})
}
throw error
}
res.status(200).json({ data: response })
}
export default login

View File

@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import logoutEndpoint from '@commerce/api/endpoints/logout'
import type { LogoutSchema } from '../../../types/logout'
import type { KiboCommerceAPI } from '../..'
import logout from './logout'
export type LogoutAPI = GetAPISchema<KiboCommerceAPI, LogoutSchema>
export type LogoutEndpoint = LogoutAPI['endpoint']
export const handlers: LogoutEndpoint['handlers'] = { logout }
const logoutApi = createEndpoint<LogoutAPI>({
handler: logoutEndpoint,
handlers,
})
export default logoutApi

View File

@ -0,0 +1,22 @@
import type { LogoutEndpoint } from '.'
import {prepareSetCookie} from '../../../lib/prepare-set-cookie';
import {setCookies} from '../../../lib/set-cookie'
const logout: LogoutEndpoint['handlers']['logout'] = async ({
res,
body: { redirectTo },
config,
}) => {
// Remove the cookie
const authCookie = prepareSetCookie(config.customerCookie,'',{ maxAge: -1, path: '/' })
setCookies(res, [authCookie])
// Only allow redirects to a relative URL
if (redirectTo?.startsWith('/')) {
res.redirect(redirectTo)
} else {
res.status(200).json({ data: null })
}
}
export default logout

View File

@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import signupEndpoint from '@commerce/api/endpoints/signup'
import type { SignupSchema } from '../../../types/signup'
import type { KiboCommerceAPI } from '../..'
import signup from './signup'
export type SignupAPI = GetAPISchema<KiboCommerceAPI, SignupSchema>
export type SignupEndpoint = SignupAPI['endpoint']
export const handlers: SignupEndpoint['handlers'] = { signup }
const singupApi = createEndpoint<SignupAPI>({
handler: signupEndpoint,
handlers,
})
export default singupApi

View File

@ -0,0 +1,91 @@
import { FetcherError } from '@commerce/utils/errors'
import type { SignupEndpoint } from '.'
import { registerUserMutation, registerUserLoginMutation } from '../../mutations/signup-mutation'
import { prepareSetCookie } from '../../../lib/prepare-set-cookie';
import { setCookies } from '../../../lib/set-cookie'
import { getCookieExpirationDate } from '../../../lib/get-cookie-expiration-date'
const invalidCredentials = /invalid credentials/i
const signup: SignupEndpoint['handlers']['signup'] = async ({
req,
res,
body: { email, password, firstName, lastName },
config,
commerce,
}) => {
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
let response;
try {
// Register user
const registerUserVariables = {
customerAccountInput: {
emailAddress: email,
firstName: firstName,
lastName: lastName,
acceptsMarketing: true,
id: 0
}
}
const registerUserResponse = await config.fetch(registerUserMutation, { variables: registerUserVariables})
const accountId = registerUserResponse.data?.account?.id;
// Login user
const registerUserLoginVairables = {
accountId: accountId,
customerLoginInfoInput: {
emailAddress: email,
username: email,
password: password,
isImport: false
}
}
response = await config.fetch(registerUserLoginMutation, { variables: registerUserLoginVairables})
const { account: token } = response.data;
// Set Cookie
const cookieExpirationDate = getCookieExpirationDate(config.customerCookieMaxAgeInDays)
const authCookie = prepareSetCookie(
config.customerCookie,
JSON.stringify(token),
token.accessTokenExpiration ? { expires: cookieExpirationDate }: {},
)
setCookies(res, [authCookie])
} catch (error) {
// Check if the email and password didn't match an existing account
if (
error instanceof FetcherError &&
invalidCredentials.test(error.message)
) {
return res.status(401).json({
data: null,
errors: [
{
message:
'Cannot find an account that matches the provided credentials',
code: 'invalid_credentials',
},
],
})
}
throw error
}
res.status(200).json({ data: response })
}
export default signup

View File

@ -0,0 +1,124 @@
import getCustomerWishlist from '../../operations/get-customer-wishlist'
import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.'
import { normalizeWishlistItem } from '../../../lib/normalize'
import { getProductQuery } from '../../../api/queries/get-product-query'
import addItemToWishlistMutation from '../../mutations/addItemToWishlist-mutation'
import createWishlist from '../../mutations/create-wishlist-mutation'
// Return wishlist info
const buildAddToWishlistVariables = ({
productId,
variantId,
productResponse,
wishlist
}: {
productId: string
variantId: string
productResponse: any
wishlist: any
}) => {
const { product } = productResponse.data
const selectedOptions = product.variations?.find(
(v: any) => v.productCode === variantId
).options
const quantity=1
let options: any[] = []
selectedOptions?.forEach((each: any) => {
product?.options
.filter((option: any) => {
return option.attributeFQN == each.attributeFQN
})
.forEach((po: any) => {
options.push({
attributeFQN: po.attributeFQN,
name: po.attributeDetail.name,
value: po.values?.find((v: any) => v.value == each.value).value,
})
})
})
return {
wishlistId: wishlist?.id,
wishlistItemInput: {
quantity,
product: {
productCode: productId,
variationProductCode: variantId ? variantId : null,
options,
}
},
}
}
const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
res,
body: { customerToken, item },
config,
commerce,
}) => {
const token = customerToken ? Buffer.from(customerToken, 'base64').toString('ascii'): null;
const accessToken = token ? JSON.parse(token).accessToken : null;
let result: { data?: any } = {}
let wishlist: any
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
const customerId = customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName= config.defaultWishlistName
if (!customerId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const wishlistResponse = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName },
config,
})
wishlist= wishlistResponse?.wishlist
if(Object.keys(wishlist).length === 0) {
const createWishlistResponse= await config.fetch(createWishlist, {variables: {
wishlistInput: {
customerAccountId: customerId,
name: wishlistName
}
}
}, {headers: { 'x-vol-user-claims': accessToken } })
wishlist= createWishlistResponse?.data?.createWishlist
}
const productResponse = await config.fetch(getProductQuery, {
variables: { productCode: item?.productId },
})
const addItemToWishlistResponse = await config.fetch(
addItemToWishlistMutation,
{
variables: buildAddToWishlistVariables({ ...item, productResponse, wishlist }),
},
{ headers: { 'x-vol-user-claims': accessToken } }
)
if(addItemToWishlistResponse?.data?.createWishlistItem){
const wishlistResponse= await commerce.getCustomerWishlist({
variables: { customerId, wishlistName },
config,
})
wishlist= wishlistResponse?.wishlist
}
result = { data: {...wishlist, items: wishlist?.items?.map((item:any) => normalizeWishlistItem(item, config))} }
res.status(200).json({ data: result?.data })
}
export default addItem

View File

@ -0,0 +1,35 @@
import type { WishlistEndpoint } from '.'
import getCustomerId from '../../utils/get-customer-id'
import { normalizeWishlistItem } from '../../../lib/normalize'
// Return wishlist info
const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
res,
body: { customerToken, includeProducts },
config,
commerce,
}) => {
let result: { data?: any } = {}
if (customerToken) {
const customerId = customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName= config.defaultWishlistName
if (!customerId) {
// If the customerToken is invalid, then this request is too
return res.status(404).json({
data: null,
errors: [{ message: 'Wishlist not found' }],
})
}
const { wishlist } = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName },
includeProducts,
config,
})
result = { data: {...wishlist, items: wishlist?.items?.map((item:any) => normalizeWishlistItem(item, config, includeProducts))} }
}
res.status(200).json({ data: result?.data ?? null })
}
export default getWishlist

View File

@ -0,0 +1,23 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import wishlistEndpoint from '@commerce/api/endpoints/wishlist'
import type { KiboCommerceAPI } from '../..'
import getWishlist from './get-wishlist'
import addItem from './add-item'
import removeItem from './remove-item'
export type WishlistAPI = GetAPISchema<KiboCommerceAPI, any>
export type WishlistEndpoint = WishlistAPI['endpoint']
export const handlers: WishlistEndpoint['handlers'] = {
getWishlist,
addItem,
removeItem,
}
const wishlistApi = createEndpoint<WishlistAPI>({
handler: wishlistEndpoint,
handlers,
})
export default wishlistApi

View File

@ -0,0 +1,60 @@
import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.'
import { normalizeWishlistItem } from '../../../lib/normalize'
import removeItemFromWishlistMutation from '../../mutations/removeItemFromWishlist-mutation'
// Return wishlist info
const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
res,
body: { customerToken, itemId },
config,
commerce,
}) => {
const token = customerToken ? Buffer.from(customerToken, 'base64').toString('ascii'): null;
const accessToken = token ? JSON.parse(token).accessToken : null;
let result: { data?: any } = {}
let wishlist: any
const customerId = customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName= config.defaultWishlistName
const wishlistResponse = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName },
config,
})
wishlist= wishlistResponse?.wishlist
if (!wishlist || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const removedItem = wishlist?.items?.find(
(item:any) => {
return item.product.productCode === itemId;
}
);
const removeItemFromWishlistResponse = await config.fetch(
removeItemFromWishlistMutation,
{
variables: {
wishlistId: wishlist?.id,
wishlistItemId: removedItem?.id
},
},
{ headers: { 'x-vol-user-claims': accessToken } }
)
if(removeItemFromWishlistResponse?.data?.deleteWishlistItem){
const wishlistResponse= await commerce.getCustomerWishlist({
variables: { customerId, wishlistName },
config,
})
wishlist= wishlistResponse?.wishlist
}
result = { data: {...wishlist, items: wishlist?.items?.map((item:any) => normalizeWishlistItem(item, config))} }
res.status(200).json({ data: result?.data })
}
export default removeItem

View File

@ -0,0 +1,11 @@
import { productDetails } from '../fragments/productDetails'
export const cartItemDetails = /*GraphQL*/`
fragment cartItemDetails on CartItem {
id
product {
...productDetails
}
quantity
}
${productDetails}
`;

View File

@ -0,0 +1,11 @@
export const CategoryInfo = /* GraphQL */`
fragment categoryInfo on PrCategory {
categoryId
categoryCode
isDisplayed
content {
name
slug
description
}
}`;

View File

@ -0,0 +1,98 @@
export const productPrices = /* GraphQL */`
fragment productPrices on Product {
price {
price
salePrice
}
priceRange {
lower { price, salePrice}
upper { price, salePrice }
}
}
`;
export const productAttributes = /* GraphQL */`
fragment productAttributes on Product {
properties {
attributeFQN
attributeDetail {
name
}
isHidden
values {
value
stringValue
}
}
}
`;
export const productContent = /* GraphQL */`
fragment productContent on Product {
content {
productFullDescription
productShortDescription
seoFriendlyUrl
productName
productImages {
imageUrl
imageLabel
mediaType
}
}
}
`;
export const productOptions = /* GraphQL */`
fragment productOptions on Product {
options {
attributeFQN
attributeDetail {
name
}
isProductImageGroupSelector
isRequired
isMultiValue
values {
value
isSelected
deltaPrice
stringValue
}
}
}
`;
export const productInfo = /* GraphQL */`
fragment productInfo on Product {
productCode
productUsage
purchasableState {
isPurchasable
}
variations {
productCode,
options {
__typename
attributeFQN
value
}
}
categories {
categoryCode
categoryId
content {
name
slug
}
}
...productPrices
...productAttributes
...productContent
...productOptions
}
${productPrices}
${productAttributes}
${productContent}
${productOptions}
`;

View File

@ -0,0 +1,30 @@
export const productDetails = /* GraphQL */ `
fragment productDetails on CrProduct {
productCode
name
description
imageUrl
imageAlternateText
sku
variationProductCode
price {
price
salePrice
}
options {
attributeFQN
name
value
}
properties {
attributeFQN
name
values {
value
}
}
categories {
id
}
}
`

View File

@ -0,0 +1,32 @@
import { productInfo } from './product';
export const searchFacets = /* GraphQL */`
fragment searchFacets on Facet {
label
field
values {
label
value
isApplied
filterValue
isDisplayed
count
}
}`;
export const searchResults = /* GraphQL */`
fragment searchResults on ProductSearchResult {
totalCount
pageSize
pageCount
startIndex
items {
...productInfo
}
facets {
...searchFacets
}
}
${searchFacets}
${productInfo}
`;

View File

@ -0,0 +1,64 @@
import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api'
import { getCommerceApi as commerceApi } from '@commerce/api'
import createFetchGraphqlApi from './utils/fetch-graphql-api'
import getAllPages from './operations/get-all-pages'
import getPage from './operations/get-page'
import getSiteInfo from './operations/get-site-info'
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 type { RequestInit } from '@vercel/fetch'
export interface KiboCommerceConfig extends CommerceAPIConfig {
apiHost?: string
clientId?: string
sharedSecret?: string
customerCookieMaxAgeInDays: number,
currencyCode: string,
documentListName: string,
defaultWishlistName: string,
authUrl?: string
}
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,
documentListName: 'siteSnippets@mozu',
fetch: createFetchGraphqlApi(() => getCommerceApi().getConfig()),
authUrl: process.env.KIBO_AUTH_URL || '',
// REST API
apiHost: process.env.KIBO_API_HOST || '',
clientId: process.env.KIBO_CLIENT_ID || '',
sharedSecret: process.env.KIBO_SHARED_SECRET || '',
customerCookieMaxAgeInDays: 30,
currencyCode: 'USD',
defaultWishlistName: 'My Wishlist'
}
const operations = {
getAllPages,
getPage,
getSiteInfo,
getCustomerWishlist,
getAllProductPaths,
getAllProducts,
getProduct,
}
export const provider = { config, operations }
export type KiboCommerceProvider = typeof provider
export type KiboCommerceAPI<
P extends KiboCommerceProvider = KiboCommerceProvider
> = CommerceAPI<P | any>
export function getCommerceApi<P extends KiboCommerceProvider>(
customProvider: P = provider as any
): KiboCommerceAPI<P> {
return commerceApi(customProvider as any)
}

View File

@ -0,0 +1,21 @@
import {productDetails} from '../fragments/productDetails'
const addItemToWishlistMutation = /* GraphQL */`
mutation createWishlistItem(
$wishlistId: String!
$wishlistItemInput: WishlistItemInput
) {
createWishlistItem(
wishlistId: $wishlistId
wishlistItemInput: $wishlistItemInput
) {
id
quantity
product {
...productDetails
}
}
}
${productDetails}
`;
export default addItemToWishlistMutation;

View File

@ -0,0 +1,12 @@
import { cartItemDetails } from './../fragments/cartItemDetails'
const addToCurrentCartMutation = /*GraphQL*/ `
${cartItemDetails}
mutation addToCart($productToAdd:CartItemInput!){
addItemToCurrentCart(cartItemInput: $productToAdd) {
...cartItemDetails
}
}`
export default addToCurrentCartMutation

View File

@ -0,0 +1,11 @@
const createWishlist = /*GraphQL*/`
mutation createWishlist($wishlistInput:WishlistInput!) {
createWishlist(wishlistInput:$wishlistInput){
id
name
customerAccountId
}
}
`;
export default createWishlist;

View File

@ -0,0 +1,20 @@
export const loginMutation = /* GraphQL */`
mutation login($loginInput:CustomerUserAuthInfoInput!) {
account:createCustomerAuthTicket(customerUserAuthInfoInput:$loginInput) {
accessToken
userId
refreshToken
refreshTokenExpiration
accessTokenExpiration
customerAccount {
id
firstName
lastName
emailAddress
userName
}
}
}
`

View File

@ -0,0 +1,9 @@
/*
* Delete cart based on current user session
*/
const removeItemFromCartMutation = /*GraphQL*/`
mutation deleteCartItem($id: String!) {
deleteCurrentCartItem(cartItemId:$id)
}`;
export default removeItemFromCartMutation;

View File

@ -0,0 +1,8 @@
const removeItemFromWishlistMutation = /* GraphQL */`
mutation deletewishlistitem($wishlistId: String!, $wishlistItemId: String!) {
deleteWishlistItem(wishlistId: $wishlistId, wishlistItemId:$wishlistItemId)
}
`;
export default removeItemFromWishlistMutation;

View File

@ -0,0 +1,41 @@
const registerUserMutation = /* GraphQL */`
mutation registerUser($customerAccountInput: CustomerAccountInput!) {
account:createCustomerAccount(customerAccountInput:$customerAccountInput) {
emailAddress
userName
firstName
lastName
localeCode
userId
id
isAnonymous
attributes {
values
fullyQualifiedName
}
}
}`;
const registerUserLoginMutation = /* GraphQL */`
mutation registerUserLogin($accountId: Int!, $customerLoginInfoInput: CustomerLoginInfoInput!) {
account:createCustomerAccountLogin(accountId:$accountId, customerLoginInfoInput:$customerLoginInfoInput) {
accessToken
accessTokenExpiration
refreshToken
refreshTokenExpiration
userId
customerAccount {
id
emailAddress
firstName
userName
}
}
}`;
export {
registerUserMutation,
registerUserLoginMutation
};

View File

@ -0,0 +1,9 @@
const updateCartItemQuantityMutation = /*GraphQL*/`
mutation updateCartItemQuantity($itemId:String!, $quantity: Int!){
updateCurrentCartItemQuantity(cartItemId:$itemId, quantity:$quantity){
id
quantity
}
}`;
export default updateCartItemQuantityMutation;

View File

@ -0,0 +1,38 @@
import type { OperationContext } from '@commerce/api/operations'
import type { KiboCommerceConfig } from '../index'
import { getAllPagesQuery } from '../queries/get-all-pages-query'
import { GetPagesQueryParams } from "../../types/page";
import { normalizePage } from '../../lib/normalize'
export type GetAllPagesResult<
T extends { pages: any[] } = { pages: any[] }
> = T
export default function getAllPagesOperation({
commerce,
}: OperationContext<any>) {
async function getAllPages({
query = getAllPagesQuery,
config,
variables,
}: {
url?: string
config?: Partial<KiboCommerceConfig>
variables?: GetPagesQueryParams
preview?: boolean
query?: string
} = {}): Promise<GetAllPagesResult> {
const cfg = commerce.getConfig(config)
variables = {
documentListName: cfg.documentListName
}
const { data } = await cfg.fetch(query, { variables });
const pages = data.documentListDocuments.items.map(normalizePage);
return { pages }
}
return getAllPages
}

View File

@ -0,0 +1,26 @@
import { KiboCommerceConfig } from '../index'
import { getAllProductsQuery } from '../queries/get-all-products-query';
import { normalizeProduct } from '../../lib/normalize'
export type GetAllProductPathsResult = {
products: Array<{ path: string }>
}
export default function getAllProductPathsOperation({commerce,}: any) {
async function getAllProductPaths({ config }: {config?: KiboCommerceConfig } = {}): Promise<GetAllProductPathsResult> {
const cfg = commerce.getConfig(config)
const productVariables = {startIndex: 0, pageSize: 100};
const { data } = await cfg.fetch(getAllProductsQuery, { variables: productVariables });
const normalizedProducts = data.products.items ? data.products.items.map( (item:any) => normalizeProduct(item, cfg)) : [];
const products = normalizedProducts.map((product: any) => ({ path: product.path }))
return Promise.resolve({
products: products
})
}
return getAllProductPaths
}

View File

@ -0,0 +1,32 @@
import { Product } from '@commerce/types/product'
import { GetAllProductsOperation } from '@commerce/types/product'
import type { OperationContext } from '@commerce/api/operations'
import type { KiboCommerceConfig } from '../index'
import { getAllProductsQuery } from '../queries/get-all-products-query';
import { normalizeProduct } from '../../lib/normalize'
export default function getAllProductsOperation({
commerce,
}: OperationContext<any>) {
async function getAllProducts<T extends GetAllProductsOperation>({
query = getAllProductsQuery,
variables,
config,
}: {
query?: string
variables?: T['variables']
config?: Partial<KiboCommerceConfig>
preview?: boolean
} = {}): Promise<{ products: Product[] | any[] }> {
const cfg = commerce.getConfig(config)
const { data } = await cfg.fetch(query);
let normalizedProducts = data.products.items ? data.products.items.map( (item:any) => normalizeProduct(item, cfg)) : [];
return {
products: normalizedProducts,
}
}
return getAllProducts
}

View File

@ -0,0 +1,57 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import type {
GetCustomerWishlistOperation,
Wishlist,
} from '@commerce/types/wishlist'
// import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import { KiboCommerceConfig } from '..'
// import getAllProducts, { ProductEdge } from './get-all-products'
import {getCustomerWishlistQuery} from '../queries/get-customer-wishlist-query'
export default function getCustomerWishlistOperation({
commerce,
}: OperationContext<any>) {
async function getCustomerWishlist<
T extends GetCustomerWishlistOperation
>(opts: {
variables: T['variables']
config?: KiboCommerceConfig
includeProducts?: boolean
}): Promise<T['data']>
async function getCustomerWishlist<T extends GetCustomerWishlistOperation>(
opts: {
variables: T['variables']
config?: KiboCommerceConfig
includeProducts?: boolean
} & OperationOptions
): Promise<T['data']>
async function getCustomerWishlist<T extends GetCustomerWishlistOperation>({
config,
variables,
includeProducts,
}: {
url?: string
variables: T['variables']
config?: KiboCommerceConfig
includeProducts?: boolean
}): Promise<T['data']> {
let customerWishlist ={}
try {
config = commerce.getConfig(config)
const result= await config?.fetch(getCustomerWishlistQuery,{variables})
customerWishlist= result?.data?.customerWishlist;
} catch(e) {
customerWishlist= {}
}
return { wishlist: customerWishlist as any }
}
return getCustomerWishlist
}

View File

@ -0,0 +1,40 @@
import type {
OperationContext,
} from '@commerce/api/operations'
import type { KiboCommerceConfig, KiboCommerceProvider } from '..'
import { normalizePage } from '../../lib/normalize'
import { getPageQuery } from '../queries/get-page-query'
import type { Page, GetPageQueryParams } from "../../types/page";
import type { Document } from '../../schema'
export default function getPageOperation({
commerce,
}: OperationContext<any>) {
async function getPage<T extends Page>({
url,
variables,
config,
preview,
}: {
url?: string
variables: GetPageQueryParams
config?: Partial<KiboCommerceConfig>
preview?: boolean
}): Promise<any> {
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `url`
const cfg = commerce.getConfig(config)
const pageVariables = { documentListName: cfg.documentListName, filter: `id eq ${variables.id}` }
const { data } = await cfg.fetch(getPageQuery, { variables: pageVariables })
const firstPage = data.documentListDocuments.items?.[0];
const page = firstPage as Document
if (preview || page?.properties?.is_visible) {
return { page: normalizePage(page as any) }
}
return {}
}
return getPage
}

View File

@ -0,0 +1,35 @@
import type { KiboCommerceConfig } from '../index'
import { Product } from '@commerce/types/product'
import { GetProductOperation } from '@commerce/types/product'
import type { OperationContext } from '@commerce/api/operations'
import { getProductQuery } from '../queries/get-product-query'
import { normalizeProduct } from '../../lib/normalize'
export default function getProductOperation({
commerce,
}: OperationContext<any>) {
async function getProduct<T extends GetProductOperation>({
query = getProductQuery,
variables,
config,
}: {
query?: string
variables?: T['variables']
config?: Partial<KiboCommerceConfig>
preview?: boolean
} = {}): Promise<Product | {} | any> {
const productVariables = { productCode: variables?.slug}
const cfg = commerce.getConfig(config)
const { data } = await cfg.fetch(query, { variables: productVariables });
const normalizedProduct = normalizeProduct(data.product, cfg)
return {
product: normalizedProduct
}
}
return getProduct
}

View File

@ -0,0 +1,35 @@
import { OperationContext } from '@commerce/api/operations'
import { Category } from '@commerce/types/site'
import { KiboCommerceConfig } from '../index'
import {categoryTreeQuery} from '../queries/get-categories-tree-query'
import { normalizeCategory } from '../../lib/normalize'
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[]
brands: any[]
}
> = T
export default function getSiteInfoOperation({commerce}: OperationContext<any>) {
async function getSiteInfo({
query= categoryTreeQuery,
variables,
config,
}: {
query?: string
variables?: any
config?: Partial<KiboCommerceConfig>
preview?: boolean
} = {}): Promise<GetSiteInfoResult> {
const cfg = commerce.getConfig(config)
const { data } = await cfg.fetch(query);
const categories= data.categories.items.map(normalizeCategory);
return Promise.resolve({
categories: categories ?? [],
brands: [],
})
}
return getSiteInfo
}

View 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'

View File

@ -0,0 +1,11 @@
export const getAllPagesQuery = /* GraphQL */`
query($documentListName: String!) {
documentListDocuments(documentListName:$documentListName){
items {
id
name
listFQN
properties
}
}
}`;

View File

@ -0,0 +1,21 @@
import { productInfo } from '../fragments/product';
export const getAllProductsQuery = /* GraphQL */`
${productInfo}
query products(
$filter: String
$startIndex: Int
$pageSize: Int
) {
products(
filter: $filter
startIndex: $startIndex
pageSize: $pageSize
) {
items {
...productInfo
}
}
}
`

View File

@ -0,0 +1,11 @@
export const getAnonymousShopperTokenQuery = /* GraphQL */ `
query {
getAnonymousShopperToken {
accessToken
accessTokenExpiration
refreshToken
refreshTokenExpiration
jwtAccessToken
}
}
`

View File

@ -0,0 +1,32 @@
import { productDetails } from '../fragments/productDetails'
export const getCartQuery = /* GraphQL */`
query cart {
currentCart {
id
userId
orderDiscounts {
impact
discount {
id
name
}
couponCode
}
subtotal
shippingTotal
total
items {
id
subtotal
unitPrice{
extendedAmount
}
product {
...productDetails
}
quantity
}
}
}
${productDetails}
`

View File

@ -0,0 +1,29 @@
import { CategoryInfo } from '../fragments/category'
export const categoryTreeQuery = /* GraphQL */`
query GetCategoryTree {
categories: categoriesTree {
items {
...categoryInfo
childrenCategories {
...categoryInfo
childrenCategories {
...categoryInfo
childrenCategories {
...categoryInfo
childrenCategories {
...categoryInfo
childrenCategories {
...categoryInfo
childrenCategories {
...categoryInfo
}
}
}
}
}
}
}
}
}
${CategoryInfo}`;

View File

@ -0,0 +1,12 @@
export const getCustomerAccountQuery = /* GraphQL */`
query getUser {
customerAccount:getCurrentAccount {
id
firstName
lastName
emailAddress
userName
isAnonymous
}
}
`

View File

@ -0,0 +1,25 @@
import {productDetails} from '../fragments/productDetails'
export const getCustomerWishlistQuery= /* GraphQL */`
query wishlist($customerId: Int!, $wishlistName: String!) {
customerWishlist(customerAccountId:$customerId ,wishlistName: $wishlistName){
customerAccountId
name
id
userId
items {
id
quantity
total
subtotal
unitPrice{
extendedAmount
}
quantity
product {
...productDetails
}
}
}
}
${productDetails}
`

View File

@ -0,0 +1,14 @@
export const getPageQuery = /* GraphQL */`
query($documentListName: String!, $filter: String!) {
documentListDocuments(documentListName: $documentListName, filter: $filter){
startIndex
totalCount
items {
id
name
listFQN
properties
}
}
}
`;

View File

@ -0,0 +1,15 @@
import { productInfo } from '../fragments/product';
export const getProductQuery = /* GraphQL */`
${productInfo}
query product(
$productCode: String!
) {
product(
productCode: $productCode
) {
...productInfo
}
}
`

View File

@ -0,0 +1,20 @@
import { searchResults } from '../fragments/search'
const query = /* GraphQL */`
query ProductSearch($query:String, $startIndex:Int,
$pageSize:Int, $sortBy:String, $filter:String,$facetTemplate:String,$facetValueFilter:String ) {
products:productSearch (
query:$query,
startIndex: $startIndex,
pageSize:$pageSize,
sortBy: $sortBy,
filter:$filter,
facetTemplate:$facetTemplate,
facetValueFilter:$facetValueFilter
) {
...searchResults
}
}
${searchResults}
`;
export default query;

View File

@ -0,0 +1,110 @@
import getNextConfig from 'next/config'
import type { KiboCommerceConfig } from '../index'
import type { FetchOptions } from '@vercel/fetch'
import fetch from './fetch'
interface AppAuthTicket {
access_token: string
token_type: string
expires_in: number
expires_at: number
refresh_token: string | null
}
interface AuthTicketCache {
getAuthTicket: () => Promise<AppAuthTicket>
setAuthTicket: (kiboAuthTicket: AppAuthTicket) => void
}
class RuntimeMemCache implements AuthTicketCache {
constructor() {}
async getAuthTicket() {
const { serverRuntimeConfig } = getNextConfig()
return serverRuntimeConfig.kiboAuthTicket
}
setAuthTicket(kiboAuthTicket: AppAuthTicket) {
const { serverRuntimeConfig } = getNextConfig()
serverRuntimeConfig.kiboAuthTicket = kiboAuthTicket
}
}
export class APIAuthenticationHelper {
private _clientId: string
private _sharedSecret: string
private _authUrl: string
private _authTicketCache!: AuthTicketCache
constructor(
{ clientId = '', sharedSecret = '', authUrl = '' }: KiboCommerceConfig,
authTicketCache?: AuthTicketCache
) {
this._clientId = clientId
this._sharedSecret = sharedSecret
this._authUrl = authUrl
if(!authTicketCache) {
this._authTicketCache = new RuntimeMemCache();
}
}
private _buildFetchOptions(body: any = {}): FetchOptions {
return {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}
}
private _calculateTicketExpiration(kiboAuthTicket: AppAuthTicket) {
//calculate how many milliseconds until auth expires
const millisecsUntilExpiration = kiboAuthTicket.expires_in * 1000
kiboAuthTicket.expires_at = Date.now() + millisecsUntilExpiration
return kiboAuthTicket
}
public async authenticate(): Promise<AppAuthTicket> {
// create oauth fetch options
const options = this._buildFetchOptions({
client_id: this._clientId,
client_secret: this._sharedSecret,
grant_type: 'client_credentials',
})
// perform authentication
const authTicket = await fetch(
`${this._authUrl}/api/platform/applications/authtickets/oauth`,
options
).then((response) => response.json())
// set expiration time in ms on auth ticket
this._calculateTicketExpiration(authTicket)
// set authentication ticket on next server runtime object
this._authTicketCache.setAuthTicket(authTicket)
return authTicket
}
public async refreshTicket(kiboAuthTicket: AppAuthTicket) {
// create oauth refresh fetch options
const options = this._buildFetchOptions({
refreshToken: kiboAuthTicket?.refresh_token,
})
// perform auth ticket refresh
const refreshedTicket = await fetch(
`${this._authUrl}/api/platform/applications/authtickets/refresh-ticket`,
options
).then((response) => response.json())
return refreshedTicket
}
public async getAccessToken(): Promise<string> {
// get current Kibo API auth ticket
let authTicket = await this._authTicketCache.getAuthTicket()
// if no current ticket, perform auth
// or if ticket expired, refresh auth
if (!authTicket) {
authTicket = await this.authenticate()
} else if (authTicket.expires_at < Date.now()) {
authTicket = await this.refreshTicket(authTicket)
}
return authTicket.access_token
}
}

View File

@ -0,0 +1,52 @@
import { KiboCommerceConfig } from './../index'
import { getCookieExpirationDate } from '../../lib/get-cookie-expiration-date'
import { prepareSetCookie } from '../../lib/prepare-set-cookie'
import { setCookies } from '../../lib/set-cookie'
import { NextApiRequest } from 'next'
import getAnonymousShopperToken from './get-anonymous-shopper-token'
export default class CookieHandler {
config: KiboCommerceConfig
request: NextApiRequest
response: any
accessToken: any
constructor(config: any, req: NextApiRequest, res: any) {
this.config = config
this.request = req
this.response = res
const encodedToken = req.cookies[config.customerCookie]
const token = encodedToken
? JSON.parse(Buffer.from(encodedToken, 'base64').toString('ascii'))
: null
this.accessToken = token ? token.accessToken : null
}
async getAnonymousToken() {
const response: any = await getAnonymousShopperToken({
config: this.config,
})
let anonymousAccessToken = response?.accessToken
return {
response,
accessToken: anonymousAccessToken,
}
}
setAnonymousShopperCookie(anonymousShopperTokenResponse: any) {
const cookieExpirationDate = getCookieExpirationDate(
this.config.customerCookieMaxAgeInDays
)
const authCookie = prepareSetCookie(
this.config.customerCookie,
JSON.stringify(anonymousShopperTokenResponse),
anonymousShopperTokenResponse?.accessTokenExpiration
? { expires: cookieExpirationDate }
: {}
)
setCookies(this.response, [authCookie])
}
getAccessToken() {
return this.accessToken
}
}

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 { APIAuthenticationHelper } from './api-auth-helper';
const fetchGraphqlApi: (
getConfig: () => KiboCommerceConfig
) => GraphQLFetcher = (getConfig) => async (
query: string,
{ variables, preview } = {},
fetchOptions
) => {
const config = getConfig()
const authHelper = new APIAuthenticationHelper(config);
const apiToken = await authHelper.getAccessToken();
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
method: 'POST',
headers: {
...fetchOptions?.headers,
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
const json = await res.json()
if (json.errors) {
console.warn(`Kibo API Request Correlation ID: ${res.headers.get('x-vol-correlation')}`);
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,36 @@
import { FetcherError } from '@commerce/utils/errors'
import type { GraphQLFetcher } from '@commerce/api'
import type { KiboCommerceConfig } from '../index'
import fetch from './fetch'
const fetchGraphqlApi: (getConfig: () => KiboCommerceConfig) => GraphQLFetcher =
(getConfig) =>
async (query: string, { variables, preview } = {}, fetchOptions) => {
const config = getConfig()
const res = await fetch(config.commerceUrl, {
//const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
method: 'POST',
headers: {
Authorization: `Bearer ${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 KiboCommerce API' }],
status: res.status,
})
}
return { data: json.data, res }
}
export default fetchGraphqlApi

View File

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

View File

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

View File

@ -0,0 +1,26 @@
import type { KiboCommerceConfig } from '..'
import { getCustomerAccountQuery } from '../queries/get-customer-account-query'
async function getCustomerId({
customerToken,
config,
}: {
customerToken: string
config: KiboCommerceConfig
}): Promise<string | undefined> {
const token = customerToken ? Buffer.from(customerToken, 'base64').toString('ascii'): null;
const accessToken = token ? JSON.parse(token).accessToken : null;
const { data } = await config.fetch(
getCustomerAccountQuery,
undefined,
{
headers: {
'x-vol-user-claims': accessToken,
},
}
)
return data?.customerAccount?.id
}
export default getCustomerId

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,42 @@
import { MutationHook } from '@commerce/utils/types'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
import { useCallback } from 'react'
import { CommerceError } from '@commerce/utils/errors'
import type { LoginHook } from '../types/login'
import useCustomer from '../customer/use-customer'
import useCart from '../cart/use-cart'
export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<LoginHook> = {
fetchOptions: {
url: '/api/login',
method: 'POST'
},
async fetcher({ input: { email, password }, options, fetch }) {
if (!(email && password)) {
throw new CommerceError({
message:
'An email and password are required to login',
})
}
return fetch({
...options,
body: { email, password },
})
},
useHook: ({ fetch }) => () => {
const { revalidate } = useCustomer()
const {revalidate: revalidateCart} = useCart()
return useCallback(
async function login(input) {
const data = await fetch({ input })
await revalidate()
await revalidateCart()
return data
},
[fetch, revalidate, revalidateCart]
)
},
}

View File

@ -0,0 +1,29 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
import type { LogoutHook } from '../types/logout'
import useCustomer from '../customer/use-customer'
import useCart from '../cart/use-cart'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<LogoutHook> = {
fetchOptions: {
url: '/api/logout',
method: 'GET',
},
useHook: ({ fetch }) => () => {
const { mutate } = useCustomer()
const { mutate: mutateCart } = useCart()
return useCallback(
async function logout() {
const data = await fetch()
await mutate(null, false)
await mutateCart(null, false)
return data
},
[fetch, mutate, mutateCart]
)
},
}

View File

@ -0,0 +1,44 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
import type { SignupHook } from '../types/signup'
import useCustomer from '../customer/use-customer'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<SignupHook> = {
fetchOptions: {
url: '/api/signup',
method: 'POST',
},
async fetcher({
input: { firstName, lastName, email, password },
options,
fetch,
}) {
if (!(firstName && lastName && email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to signup',
})
}
return fetch({
...options,
body: { firstName, lastName, email, password },
})
},
useHook: ({ fetch }) => () => {
const { revalidate } = useCustomer()
return useCallback(
async function signup(input) {
const data = await fetch({ input })
await revalidate()
return data
},
[fetch, revalidate]
)
},
}

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,44 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import type { AddItemHook } from '@commerce/types/cart'
import useCart from './use-cart'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
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 '@commerce/utils/types'
import useCart, { UseCart } from '@commerce/cart/use-cart'
export default useCart as UseCart<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
method: 'GET',
url: '/api/cart',
},
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]
)
},
}

View File

@ -0,0 +1,56 @@
import { useCallback } from 'react'
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
import type { Cart, LineItem, RemoveItemHook } from '@commerce/types/cart'
import useCart from './use-cart'
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
export type RemoveItemActionInput<T = any> = T extends LineItem
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/cart',
method: 'DELETE',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
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,84 @@
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
import type { LineItem, UpdateItemHook } from '@commerce/types/cart'
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 = {
fetchOptions: {
url: '/api/cart',
method: 'PUT',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateItemHook>) {
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 } = ctx
const { mutate } = useCart() as any
return useCallback(
debounce(async (input: UpdateItemActionInput<T>) => {
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
}, ctx.wait ?? 500),
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,14 @@
import { SWRHook } from '@commerce/utils/types'
import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ useData }) =>
async (input) => ({}),
}

View File

@ -0,0 +1,23 @@
{
"schema": {
"https://t17194-s21127.dev10.kubedev.kibo-dev.com/graphql": {}
},
"generates": {
"./framework/kibocommerce/schema.d.ts": {
"plugins": ["typescript", "typescript-operations"],
"config": {
"scalars": {
"ID": "string"
}
}
},
"./framework/kibocommerce/schema.graphql": {
"plugins": ["schema-ast"]
}
},
"hooks": {
"afterAllFileWrite": ["prettier --write"]
}
}

View File

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

View File

@ -0,0 +1,15 @@
import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item'
import { MutationHook } from '@commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
}

View File

@ -0,0 +1,15 @@
import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item'
import { MutationHook } from '@commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
}

View File

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

View File

@ -0,0 +1,24 @@
import { SWRHook } from '@commerce/utils/types'
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
import type { CustomerHook } from '../types/customer'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<CustomerHook> = {
fetchOptions: {
url: '/api/customer',
method: 'GET',
},
async fetcher({ options, fetch }) {
const data = await fetch(options)
return data?.customer ?? null
},
useHook: ({ useData }) => (input) => {
return useData({
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
},
}

View File

@ -0,0 +1,41 @@
import { FetcherError } from '@commerce/utils/errors'
import type { Fetcher } from '@commerce/utils/types'
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,9 @@
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
import { kiboCommerceProvider, KibocommerceProvider } from './provider'
export { kiboCommerceProvider }
export type { KibocommerceProvider }
export const CommerceProvider = getCommerceProvider(kiboCommerceProvider)
export const useCommerce = () => useCoreCommerce()

View File

@ -0,0 +1,8 @@
export function getCookieExpirationDate(maxAgeInDays: number){
const today = new Date();
const expirationDate = new Date();
const cookieExpirationDate = new Date ( expirationDate.setDate(today.getDate() + maxAgeInDays) )
return cookieExpirationDate;
}

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,194 @@
import update from './immutability'
import getSlug from './get-slug'
import type { PrCategory, CustomerAccountInput, Document } from '../schema'
import { Page } from '../types/page';
import { Customer } from '../types/customer'
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, config: any): any {
const product = {
id: productNode.productCode,
name: productNode.content.productName,
vendor: '',
path: `/${productNode.productCode}`,
slug: productNode.productCode,
price: {
value: productNode?.price?.price,
currencyCode: config.currencyCode,
},
descriptionHtml: productNode.content.productShortDescription,
images: productNode.content.productImages.map((p: any) => ({
url: `http:${p.imageUrl}`,
altText: p.imageLabel,
})),
variants: productNode.variations?.map((v: any) => ({
id: v.productCode,
options: v.options.map((o: any) => ({
['__typename']: 'MultipleChoiceOption',
id: o.attributeFQN,
displayName:
o.attributeFQN.split('~')[1][0].toUpperCase() +
o.attributeFQN.split('~')[1].slice(1).toLowerCase(),
values: [{ label: o.value.toString() }],
})),
})) || [
{
id: '',
},
],
options:
productNode.options?.map((o: any) => ({
id: o.attributeFQN,
displayName: o.attributeDetail.name,
values: o.values.map((v: any) => ({
label: v.value.toString(),
hexColors: '',
})),
})) || [],
}
return product
}
export function normalizePage(page: Document): Page {
return {
id: String(page.id),
name: String(page.name),
url: page.properties.url,
body: page.properties.body,
is_visible: page.properties.is_visible,
sort_order: page.properties.sort_order
}
}
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,
})),
}
}
export function normalizeCustomer(customer: CustomerAccountInput): Customer {
return {
id: customer.id,
firstName: customer.firstName,
lastName: customer.lastName,
email: customer.emailAddress,
userName: customer.userName,
isAnonymous: customer.isAnonymous
}
}
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,
},
options: item.product.options,
path: `${item.product.productCode}`,
discounts: item?.discounts?.map((discount: any) => ({
value: discount.discounted_amount,
})),
}
}
export function normalizeCategory(category: PrCategory): any {
return {
id: category?.categoryCode,
name: category?.content?.name,
slug: category?.content?.slug,
path: `/${category?.content?.slug}`,
}
}
export function normalizeWishlistItem(
item: any,
config: any,
includeProducts=false
): any {
if (includeProducts) {
return {
id: item.id,
product: getProuducts(item, config),
}
} else {
return getProuducts(item, config)
}
}
function getProuducts(item: any, config: any): any {
return {
variant_id: item.product.variationProductCode || '',
id: String(item.product.productCode),
product_id: String(item.product.productCode),
name: item.product.name,
quantity: item.quantity,
images: [
{
url: `http:${item.product.imageUrl}`,
alt: item.product.imageAlternateText,
},
],
price: {
value: item.product.price.price,
retailPrice: item.product.price.retailPrice || 0,
currencyCode: config.currencyCode,
},
variants: [
{
id: item.product.variationProductCode || '',
sku: item.product?.sku,
name: item.product.name,
image: {
url: item?.product.imageUrl,
},
},
],
options: item.product.options,
path: `/${item.product.productCode}`,
description: item.product.description,
}
}

View File

@ -0,0 +1,15 @@
export function prepareSetCookie(name: string, value: string, options: any = {}): string {
const encodedValue = Buffer.from(value).toString('base64')
const cookieValue = [`${name}=${encodedValue}`];
if (options.maxAge) {
cookieValue.push(`Max-Age=${options.maxAge}`);
}
if (options.expires && !options.maxAge) {
cookieValue.push(`Expires=${options.expires.toUTCString()}`);
}
const cookie = cookieValue.join('; ')
return cookie
}

View File

@ -0,0 +1,55 @@
function getFacetValueFilter(categoryCode: string, filters = []) {
let facetValueFilter = '';
if (categoryCode) {
facetValueFilter = `categoryCode:${categoryCode},`;
}
return facetValueFilter + filters.join(',');
}
export const buildProductSearchVars = ({
categoryCode = '',
pageSize = 5,
filters = {} as any,
startIndex = 0,
sort = '',
search = '',
}) => {
let facetTemplate = '';
let filter = '';
let sortBy;
if (categoryCode) {
facetTemplate = `categoryCode:${categoryCode}`;
filter = `categoryCode req ${categoryCode}`;
}
const facetFilterList = Object.keys(filters).filter(k => filters[k].length).reduce((accum, k): any => {
return [...accum, ...filters[k].map((facetValue: any) => `Tenant~${k}:${facetValue}`)];
}, []);
const facetValueFilter = getFacetValueFilter(categoryCode, facetFilterList);
switch(sort) {
case 'latest-desc':
sortBy= 'createDate desc';
break;
case 'price-asc':
sortBy= 'price asc';
break;
case 'price-desc':
sortBy= 'price desc';
break;
case 'trending-desc':
default:
sortBy= '';
break;
}
return {
query: search,
startIndex,
pageSize,
sortBy,
filter: filter,
facetTemplate,
facetValueFilter
}
}

View File

@ -0,0 +1,3 @@
export function setCookies(res: any, cookies: string[]): void {
res.setHeader('Set-Cookie', cookies);
}

View File

@ -0,0 +1,12 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
serverRuntimeConfig: {
// Will only be available on the server side
kiboAuthTicket: null
},
images: {
domains: ['d1slj7rdbjyb5l.cloudfront.net', 'cdn-tp1.mozu.com', 'cdn-sb.mozu.com'],
},
}

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 '@commerce/product/use-price'
export { default } from '@commerce/product/use-price'

View File

@ -0,0 +1,37 @@
import { SWRHook } from '@commerce/utils/types'
import useSearch, { UseSearch } from '@commerce/product/use-search'
export default useSearch as UseSearch<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
method: 'GET',
url: '/api/catalog/products',
},
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 (Number.isInteger(Number(categoryId)))
url.searchParams.set('categoryId', String(categoryId))
if (Number.isInteger(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,30 @@
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 useWishlist } from './wishlist/use-wishlist'
import { handler as useWishlistAddItem } from './wishlist/use-add-item'
import { handler as useWishlistRemoveItem } from './wishlist/use-remove-item'
export const kiboCommerceProvider = {
locale: 'en-us',
cartCookie: 'kibo_cart',
fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
wishlist: {
useWishlist,
useAddItem: useWishlistAddItem,
useRemoveItem: useWishlistRemoveItem,
},
customer: { useCustomer },
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}
export type KibocommerceProvider = typeof kiboCommerceProvider

11399
framework/kibocommerce/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,27 @@
import * as Core from '@commerce/types/customer'
export type Maybe<T> = T | null
export * from '@commerce/types/customer'
export type Scalars = {
ID: string
String: string
Boolean: boolean
Int: number
Float: number
/** The `AnyScalar` type allows any scalar value by examining the input and passing the serialize, parseValue, and parseLiteral operations to their respective types. */
AnyScalar: any
/** DateTime custom scalar type */
DateTime: any
/** Object custom scalar type */
Object: any
}
export type Customer = {
id: Scalars['Int'],
firstName?: Maybe<Scalars['String']>,
lastName?: Maybe<Scalars['String']>,
email?: Maybe<Scalars['String']>,
userName?: Maybe<Scalars['String']>,
isAnonymous?: Maybe<Scalars['Boolean']>
}
export type CustomerSchema = Core.CustomerSchema

View File

@ -0,0 +1,8 @@
import * as Core from '@commerce/types/login'
import type { CustomerUserAuthInfoInput } from '../schema'
export * from '@commerce/types/login'
export type LoginOperation = Core.LoginOperation & {
variables: CustomerUserAuthInfoInput
}

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