Add ordercloud provider (#500)

* Add ordercloud provider

* Fix provider errors

* Make submit checkout optional

* Make submit checkout optional

* Remove nullables when creating endpoint type

* Update readme

* Log checkout error

* Log error

* Save token to cookie

* Update fetch rest

* Use token at checkout

Co-authored-by: Luis Alvarez <luis@vercel.com>
This commit is contained in:
Gonzalo Pozzo 2021-10-05 09:49:01 -03:00 committed by GitHub
parent f9644fecef
commit 3f0c38461b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 2560 additions and 76 deletions

View File

@ -23,3 +23,7 @@ NEXT_PUBLIC_SALEOR_CHANNEL=
NEXT_PUBLIC_VENDURE_SHOP_API_URL=
NEXT_PUBLIC_VENDURE_LOCAL_URL=
ORDERCLOUD_CLIENT_ID=
ORDERCLOUD_CLIENT_SECRET=
STRIPE_SECRET=

View File

@ -65,8 +65,8 @@ export type EndpointHandlers<
[H in keyof E['handlers']]: APIHandler<
C,
EndpointHandlers<C, E>,
E['handlers'][H]['data'],
E['handlers'][H]['body'],
NonNullable<E['handlers'][H]>['data'],
NonNullable<E['handlers'][H]>['body'],
E['options']
>
}

View File

@ -14,6 +14,7 @@ const PROVIDERS = [
'shopify',
'swell',
'vendure',
'ordercloud',
]
function getProviderName() {

View File

@ -30,7 +30,7 @@ export type GetCheckoutHook<T extends CheckoutTypes = CheckoutTypes> = {
}
export type CheckoutHooks<T extends CheckoutTypes = CheckoutTypes> = {
submitCheckout: SubmitCheckoutHook<T>
submitCheckout?: SubmitCheckoutHook<T>
getCheckout: GetCheckoutHook<T>
}

View File

@ -1,41 +1,46 @@
export interface Address {
id: string;
mask: string;
id: string
mask: string
}
export interface AddressFields {
type: string;
firstName: string;
lastName: string;
company: string;
streetNumber: string;
apartments: string;
zipCode: string;
city: string;
country: string;
type: string
firstName: string
lastName: string
company: string
streetNumber: string
apartments: string
zipCode: string
city: string
country: string
}
export type CustomerAddressTypes = {
address?: Address;
fields: AddressFields;
address?: Address
fields: AddressFields
}
export type GetAddressesHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
data: T['address'] | null
export type GetAddressesHook<
T extends CustomerAddressTypes = CustomerAddressTypes
> = {
data: T['address'][] | null
input: {}
fetcherInput: { cartId?: string }
swrState: { isEmpty: boolean }
}
export type AddItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
data: T['address']
input?: T['fields']
fetcherInput: T['fields']
body: { item: T['fields'] }
actionInput: T['fields']
}
export type AddItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> =
{
data: T['address']
input?: T['fields']
fetcherInput: T['fields']
body: { item: T['fields'] }
actionInput: T['fields']
}
export type UpdateItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
export type UpdateItemHook<
T extends CustomerAddressTypes = CustomerAddressTypes
> = {
data: T['address'] | null
input: { item?: T['fields']; wait?: number }
fetcherInput: { itemId: string; item: T['fields'] }
@ -43,49 +48,62 @@ export type UpdateItemHook<T extends CustomerAddressTypes = CustomerAddressTypes
actionInput: T['fields'] & { id: string }
}
export type RemoveItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
export type RemoveItemHook<
T extends CustomerAddressTypes = CustomerAddressTypes
> = {
data: T['address'] | null
input: { item?: T['fields'] }
input: { item?: T['address'] }
fetcherInput: { itemId: string }
body: { itemId: string }
actionInput: { id: string }
}
export type CustomerAddressHooks<T extends CustomerAddressTypes = CustomerAddressTypes> = {
export type CustomerAddressHooks<
T extends CustomerAddressTypes = CustomerAddressTypes
> = {
getAddresses: GetAddressesHook<T>
addItem: AddItemHook<T>
updateItem: UpdateItemHook<T>
removeItem: RemoveItemHook<T>
}
export type AddresssHandler<T extends CustomerAddressTypes = CustomerAddressTypes> = GetAddressesHook<T> & {
export type AddressHandler<
T extends CustomerAddressTypes = CustomerAddressTypes
> = GetAddressesHook<T> & {
body: { cartId?: string }
}
export type AddItemHandler<T extends CustomerAddressTypes = CustomerAddressTypes> = AddItemHook<T> & {
export type AddItemHandler<
T extends CustomerAddressTypes = CustomerAddressTypes
> = AddItemHook<T> & {
body: { cartId: string }
}
export type UpdateItemHandler<T extends CustomerAddressTypes = CustomerAddressTypes> =
UpdateItemHook<T> & {
data: T['address']
body: { cartId: string }
}
export type UpdateItemHandler<
T extends CustomerAddressTypes = CustomerAddressTypes
> = UpdateItemHook<T> & {
data: T['address']
body: { cartId: string }
}
export type RemoveItemHandler<T extends CustomerAddressTypes = CustomerAddressTypes> =
RemoveItemHook<T> & {
body: { cartId: string }
}
export type RemoveItemHandler<
T extends CustomerAddressTypes = CustomerAddressTypes
> = RemoveItemHook<T> & {
body: { cartId: string }
}
export type CustomerAddressHandlers<T extends CustomerAddressTypes = CustomerAddressTypes> = {
export type CustomerAddressHandlers<
T extends CustomerAddressTypes = CustomerAddressTypes
> = {
getAddresses: GetAddressesHook<T>
addItem: AddItemHandler<T>
updateItem: UpdateItemHandler<T>
removeItem: RemoveItemHandler<T>
}
export type CustomerAddressSchema<T extends CustomerAddressTypes = CustomerAddressTypes> = {
export type CustomerAddressSchema<
T extends CustomerAddressTypes = CustomerAddressTypes
> = {
endpoint: {
options: {}
handlers: CustomerAddressHandlers<T>

View File

@ -1,30 +1,30 @@
export interface Card {
id: string;
mask: string;
provider: string;
id: string
mask: string
provider: string
}
export interface CardFields {
cardHolder: string;
cardNumber: string;
cardExpireDate: string;
cardCvc: string;
firstName: string;
lastName: string;
company: string;
streetNumber: string;
zipCode: string;
city: string;
country: string;
cardHolder: string
cardNumber: string
cardExpireDate: string
cardCvc: string
firstName: string
lastName: string
company: string
streetNumber: string
zipCode: string
city: string
country: string
}
export type CustomerCardTypes = {
card?: Card;
fields: CardFields;
card?: Card
fields: CardFields
}
export type GetCardsHook<T extends CustomerCardTypes = CustomerCardTypes> = {
data: T['card'] | null
data: T['card'][] | null
input: {}
fetcherInput: { cartId?: string }
swrState: { isEmpty: boolean }
@ -48,26 +48,29 @@ export type UpdateItemHook<T extends CustomerCardTypes = CustomerCardTypes> = {
export type RemoveItemHook<T extends CustomerCardTypes = CustomerCardTypes> = {
data: T['card'] | null
input: { item?: T['fields'] }
input: { item?: T['card'] }
fetcherInput: { itemId: string }
body: { itemId: string }
actionInput: { id: string }
}
export type CustomerCardHooks<T extends CustomerCardTypes = CustomerCardTypes> = {
getCards: GetCardsHook<T>
addItem: AddItemHook<T>
updateItem: UpdateItemHook<T>
removeItem: RemoveItemHook<T>
}
export type CustomerCardHooks<T extends CustomerCardTypes = CustomerCardTypes> =
{
getCards: GetCardsHook<T>
addItem: AddItemHook<T>
updateItem: UpdateItemHook<T>
removeItem: RemoveItemHook<T>
}
export type CardsHandler<T extends CustomerCardTypes = CustomerCardTypes> = GetCardsHook<T> & {
body: { cartId?: string }
}
export type CardsHandler<T extends CustomerCardTypes = CustomerCardTypes> =
GetCardsHook<T> & {
body: { cartId?: string }
}
export type AddItemHandler<T extends CustomerCardTypes = CustomerCardTypes> = AddItemHook<T> & {
body: { cartId: string }
}
export type AddItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
AddItemHook<T> & {
body: { cartId: string }
}
export type UpdateItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
UpdateItemHook<T> & {
@ -80,15 +83,18 @@ export type RemoveItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
body: { cartId: string }
}
export type CustomerCardHandlers<T extends CustomerCardTypes = CustomerCardTypes> = {
export type CustomerCardHandlers<
T extends CustomerCardTypes = CustomerCardTypes
> = {
getCards: GetCardsHook<T>
addItem: AddItemHandler<T>
updateItem: UpdateItemHandler<T>
removeItem: RemoveItemHandler<T>
}
export type CustomerCardSchema<T extends CustomerCardTypes = CustomerCardTypes> = {
export type CustomerCardSchema<
T extends CustomerCardTypes = CustomerCardTypes
> = {
endpoint: {
options: {}
handlers: CustomerCardHandlers<T>

View File

@ -0,0 +1,5 @@
COMMERCE_PROVIDER=ordercloud
ORDERCLOUD_CLIENT_ID=
ORDERCLOUD_CLIENT_SECRET=
STRIPE_SECRET=

View File

@ -0,0 +1,3 @@
# Next.js Ordercloud Provider
Create your own store from [here](https://nextjs.org/commerce)

View File

@ -0,0 +1,99 @@
import type { CartEndpoint } from '.'
import type { RawVariant } from '../../../types/product'
import type { OrdercloudLineItem } from '../../../types/cart'
import { serialize } from 'cookie'
import { formatCart } from '../../utils/cart'
const addItem: CartEndpoint['handlers']['addItem'] = async ({
res,
body: { cartId, item },
config: { restBuyerFetch, cartCookie, tokenCookie },
}) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Store token
let token
// Set the quantity if not present
if (!item.quantity) item.quantity = 1
// Create an order if it doesn't exist
if (!cartId) {
const { ID, meta } = await restBuyerFetch(
'POST',
`/orders/Outgoing`,
{}
).then((response: { ID: string; meta: { token: string } }) => response)
// Set the cart id and token
cartId = ID
token = meta.token
// Set the cart and token cookie
res.setHeader('Set-Cookie', [
serialize(tokenCookie, meta.token, {
maxAge: 60 * 60 * 24 * 30,
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
}),
serialize(cartCookie, cartId, {
maxAge: 60 * 60 * 24 * 30,
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
}),
])
}
// Store specs
let specs: RawVariant['Specs'] = []
// If a variant is present, fetch its specs
if (item.variantId) {
specs = await restBuyerFetch(
'GET',
`/me/products/${item.productId}/variants/${item.variantId}`,
null,
{ token }
).then((res: RawVariant) => res.Specs)
}
// Add the item to the order
await restBuyerFetch(
'POST',
`/orders/Outgoing/${cartId}/lineitems`,
{
ProductID: item.productId,
Quantity: item.quantity,
Specs: specs,
},
{ token }
)
// Get cart
const [cart, lineItems] = await Promise.all([
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
token,
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
])
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
}
export default addItem

View File

@ -0,0 +1,65 @@
import type { OrdercloudLineItem } from '../../../types/cart'
import type { CartEndpoint } from '.'
import { serialize } from 'cookie'
import { formatCart } from '../../utils/cart'
// Return current cart info
const getCart: CartEndpoint['handlers']['getCart'] = async ({
req,
res,
body: { cartId },
config: { restBuyerFetch, cartCookie, tokenCookie },
}) => {
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
try {
// Get token from cookies
const token = req.cookies[tokenCookie]
// Get cart
const cart = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}`,
null,
{ token }
)
// Get line items
const lineItems = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}/lineitems`,
null,
{ token }
).then((response: { Items: OrdercloudLineItem[] }) => response.Items)
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
} catch (error) {
// Reset cart and token cookie
res.setHeader('Set-Cookie', [
serialize(cartCookie, cartId, {
maxAge: -1,
path: '/',
}),
serialize(tokenCookie, cartId, {
maxAge: -1,
path: '/',
}),
])
// Return empty cart
res.status(200).json({ data: null, errors: [] })
}
}
export default getCart

View File

@ -0,0 +1,28 @@
import type { CartSchema } from '../../../types/cart'
import type { OrdercloudAPI } from '../..'
import { GetAPISchema, createEndpoint } from '@commerce/api'
import cartEndpoint from '@commerce/api/endpoints/cart'
import getCart from './get-cart'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CartAPI = GetAPISchema<OrdercloudAPI, CartSchema>
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 type { CartEndpoint } from '.'
import { formatCart } from '../../utils/cart'
import { OrdercloudLineItem } from '../../../types/cart'
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
req,
res,
body: { cartId, itemId },
config: { restBuyerFetch, tokenCookie },
}) => {
if (!cartId || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Remove the item to the order
await restBuyerFetch(
'DELETE',
`/orders/Outgoing/${cartId}/lineitems/${itemId}`,
null,
{ token }
)
// Get cart
const [cart, lineItems] = await Promise.all([
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
token,
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
])
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
}
export default removeItem

View File

@ -0,0 +1,63 @@
import type { OrdercloudLineItem } from '../../../types/cart'
import type { RawVariant } from '../../../types/product'
import type { CartEndpoint } from '.'
import { formatCart } from '../../utils/cart'
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
req,
res,
body: { cartId, itemId, item },
config: { restBuyerFetch, tokenCookie },
}) => {
if (!cartId || !itemId || !item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Store specs
let specs: RawVariant['Specs'] = []
// If a variant is present, fetch its specs
if (item.variantId) {
specs = await restBuyerFetch(
'GET',
`/me/products/${item.productId}/variants/${item.variantId}`,
null,
{ token }
).then((res: RawVariant) => res.Specs)
}
// Add the item to the order
await restBuyerFetch(
'PATCH',
`/orders/Outgoing/${cartId}/lineitems/${itemId}`,
{
ProductID: item.productId,
Quantity: item.quantity,
Specs: specs,
},
{ token }
)
// Get cart
const [cart, lineItems] = await Promise.all([
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
token,
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
])
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
}
export default updateItem

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,47 @@
import type { CheckoutEndpoint } from '.'
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
req,
res,
body: { cartId },
config: { restBuyerFetch, tokenCookie },
}) => {
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing cookie' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Register credit card
const payments = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}/payments`,
null,
{ token }
).then((response: { Items: unknown[] }) => response.Items)
const address = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}`,
null,
{ token }
).then(
(response: { ShippingAddressID: string }) => response.ShippingAddressID
)
// Return cart and errors
res.status(200).json({
data: {
hasPayment: payments.length > 0,
hasShipping: Boolean(address),
},
errors: [],
})
}
export default getCheckout

View File

@ -0,0 +1,23 @@
import type { CheckoutSchema } from '../../../types/checkout'
import type { OrdercloudAPI } from '../..'
import { GetAPISchema, createEndpoint } from '@commerce/api'
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
import getCheckout from './get-checkout'
import submitCheckout from './submit-checkout'
export type CheckoutAPI = GetAPISchema<OrdercloudAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = {
getCheckout,
submitCheckout,
}
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

View File

@ -0,0 +1,32 @@
import type { CheckoutEndpoint } from '.'
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
req,
res,
body: { cartId },
config: { restBuyerFetch, tokenCookie },
}) => {
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Submit order
await restBuyerFetch(
'POST',
`/orders/Outgoing/${cartId}/submit`,
{},
{ token }
)
// Return cart and errors
res.status(200).json({ data: null, errors: [] })
}
export default submitCheckout

View File

@ -0,0 +1,47 @@
import type { CustomerAddressEndpoint } from '.'
const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
res,
body: { item, cartId },
config: { restBuyerFetch },
}) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
// Register address
const address = await restBuyerFetch('POST', `/me/addresses`, {
AddressName: 'main address',
CompanyName: item.company,
FirstName: item.firstName,
LastName: item.lastName,
Street1: item.streetNumber,
Street2: item.streetNumber,
City: item.city,
State: item.city,
Zip: item.zipCode,
Country: item.country.slice(0, 2).toLowerCase(),
Shipping: true,
}).then((response: { ID: string }) => response.ID)
// Assign address to order
await restBuyerFetch('PATCH', `/orders/Outgoing/${cartId}`, {
ShippingAddressID: address,
})
return res.status(200).json({ data: null, errors: [] })
}
export default addItem

View File

@ -0,0 +1,9 @@
import type { CustomerAddressEndpoint } from '.'
const getCards: CustomerAddressEndpoint['handlers']['getAddresses'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default getCards

View File

@ -0,0 +1,27 @@
import type { CustomerAddressSchema } from '../../../../types/customer/address'
import type { OrdercloudAPI } from '../../..'
import { GetAPISchema, createEndpoint } from '@commerce/api'
import customerAddressEndpoint from '@commerce/api/endpoints/customer/address'
import getAddresses from './get-addresses'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CustomerAddressAPI = GetAPISchema<OrdercloudAPI, CustomerAddressSchema>
export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint']
export const handlers: CustomerAddressEndpoint['handlers'] = {
getAddresses,
addItem,
updateItem,
removeItem,
}
const customerAddressApi = createEndpoint<CustomerAddressAPI>({
handler: customerAddressEndpoint,
handlers,
})
export default customerAddressApi

View File

@ -0,0 +1,9 @@
import type { CustomerAddressEndpoint } from '.'
const removeItem: CustomerAddressEndpoint['handlers']['removeItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default removeItem

View File

@ -0,0 +1,9 @@
import type { CustomerAddressEndpoint } from '.'
const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default updateItem

View File

@ -0,0 +1,74 @@
import type { CustomerCardEndpoint } from '.'
import type { OredercloudCreditCard } from '../../../../types/customer/card'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET as string, {
apiVersion: '2020-08-27',
})
const addItem: CustomerCardEndpoint['handlers']['addItem'] = async ({
res,
body: { item, cartId },
config: { restBuyerFetch, restMiddlewareFetch },
}) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
// Get token
const token = await stripe.tokens
.create({
card: {
number: item.cardNumber,
exp_month: item.cardExpireDate.split('/')[0],
exp_year: item.cardExpireDate.split('/')[1],
cvc: item.cardCvc,
},
})
.then((res: { id: string }) => res.id)
// Register credit card
const creditCard = await restBuyerFetch('POST', `/me/creditcards`, {
Token: token,
CardType: 'credit',
PartialAccountNumber: item.cardNumber.slice(-4),
CardholderName: item.cardHolder,
ExpirationDate: item.cardExpireDate,
}).then((response: OredercloudCreditCard) => response.ID)
// Assign payment to order
const payment = await restBuyerFetch(
'POST',
`/orders/All/${cartId}/payments`,
{
Type: 'CreditCard',
CreditCardID: creditCard,
}
).then((response: { ID: string }) => response.ID)
// Accept payment to order
await restMiddlewareFetch(
'PATCH',
`/orders/All/${cartId}/payments/${payment}`,
{
Accepted: true,
}
)
return res.status(200).json({ data: null, errors: [] })
}
export default addItem

View File

@ -0,0 +1,9 @@
import type { CustomerCardEndpoint } from '.'
const getCards: CustomerCardEndpoint['handlers']['getCards'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default getCards

View File

@ -0,0 +1,27 @@
import type { CustomerCardSchema } from '../../../../types/customer/card'
import type { OrdercloudAPI } from '../../..'
import { GetAPISchema, createEndpoint } from '@commerce/api'
import customerCardEndpoint from '@commerce/api/endpoints/customer/card'
import getCards from './get-cards'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CustomerCardAPI = GetAPISchema<OrdercloudAPI, CustomerCardSchema>
export type CustomerCardEndpoint = CustomerCardAPI['endpoint']
export const handlers: CustomerCardEndpoint['handlers'] = {
getCards,
addItem,
updateItem,
removeItem,
}
const customerCardApi = createEndpoint<CustomerCardAPI>({
handler: customerCardEndpoint,
handlers,
})
export default customerCardApi

View File

@ -0,0 +1,9 @@
import type { CustomerCardEndpoint } from '.'
const removeItem: CustomerCardEndpoint['handlers']['removeItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default removeItem

View File

@ -0,0 +1,9 @@
import type { CustomerCardEndpoint } from '.'
const updateItem: CustomerCardEndpoint['handlers']['updateItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default updateItem

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 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1,71 @@
import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api'
import { getCommerceApi as commerceApi } from '@commerce/api'
import { createBuyerFetcher, createMiddlewareFetcher } from './utils/fetch-rest'
import createGraphqlFetcher from './utils/fetch-graphql'
import getAllPages from './operations/get-all-pages'
import getPage from './operations/get-page'
import getSiteInfo from './operations/get-site-info'
import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products'
import getProduct from './operations/get-product'
import {
API_URL,
API_VERSION,
CART_COOKIE,
CUSTOMER_COOKIE,
TOKEN_COOKIE,
} from '../constants'
export interface OrdercloudConfig extends CommerceAPIConfig {
restBuyerFetch: <T>(
method: string,
resource: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T>
restMiddlewareFetch: <T>(
method: string,
resource: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T>
apiVersion: string
tokenCookie: string
}
const config: OrdercloudConfig = {
commerceUrl: API_URL,
apiToken: '',
apiVersion: API_VERSION,
cartCookie: CART_COOKIE,
customerCookie: CUSTOMER_COOKIE,
tokenCookie: TOKEN_COOKIE,
cartCookieMaxAge: 2592000,
restBuyerFetch: createBuyerFetcher(() => getCommerceApi().getConfig()),
restMiddlewareFetch: createMiddlewareFetcher(() =>
getCommerceApi().getConfig()
),
fetch: createGraphqlFetcher(() => getCommerceApi().getConfig()),
}
const operations = {
getAllPages,
getPage,
getSiteInfo,
getAllProductPaths,
getAllProducts,
getProduct,
}
export const provider = { config, operations }
export type Provider = typeof provider
export type OrdercloudAPI<P extends Provider = Provider> = CommerceAPI<P | any>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): OrdercloudAPI<P> {
return commerceApi(customProvider as any)
}

View File

@ -0,0 +1,22 @@
import type { OrdercloudConfig } from '../'
import { GetAllPagesOperation } from '@commerce/types/page'
export type Page = { url: string }
export type GetAllPagesResult = { pages: Page[] }
export default function getAllPagesOperation() {
async function getAllPages<T extends GetAllPagesOperation>({
config,
preview,
}: {
url?: string
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
return Promise.resolve({
pages: [],
})
}
return getAllPages
}

View File

@ -0,0 +1,34 @@
import type { OperationContext } from '@commerce/api/operations'
import type { GetAllProductPathsOperation } from '@commerce/types/product'
import type { RawProduct } from '../../types/product'
import type { OrdercloudConfig, Provider } from '../'
export type GetAllProductPathsResult = {
products: Array<{ path: string }>
}
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
config,
}: {
config?: Partial<OrdercloudConfig>
} = {}): Promise<T['data']> {
// Get fetch from the config
const { restBuyerFetch } = commerce.getConfig(config)
// Get all products
const rawProducts: RawProduct[] = await restBuyerFetch<{
Items: RawProduct[]
}>('GET', '/me/products').then((response) => response.Items)
return {
// Match a path for every product retrieved
products: rawProducts.map((product) => ({ path: `/${product.ID}` })),
}
}
return getAllProductPaths
}

View File

@ -0,0 +1,35 @@
import type { GetAllProductsOperation } from '@commerce/types/product'
import type { OperationContext } from '@commerce/api/operations'
import type { RawProduct } from '../../types/product'
import type { OrdercloudConfig, Provider } from '../index'
import { normalize as normalizeProduct } from '../../utils/product'
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>({
config,
}: {
query?: string
variables?: T['variables']
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
// Get fetch from the config
const { restBuyerFetch } = commerce.getConfig(config)
// Get all products
const rawProducts: RawProduct[] = await restBuyerFetch<{
Items: RawProduct[]
}>('GET', '/me/products').then((response) => response.Items)
return {
// Normalize products to commerce schema
products: rawProducts.map(normalizeProduct),
}
}
return getAllProducts
}

View File

@ -0,0 +1,15 @@
import { GetPageOperation } from "@commerce/types/page"
export type Page = any
export type GetPageResult = { page?: Page }
export type PageVariables = {
id: number
}
export default function getPageOperation() {
async function getPage<T extends GetPageOperation>(): Promise<T['data']> {
return Promise.resolve({})
}
return getPage
}

View File

@ -0,0 +1,60 @@
import type { OperationContext } from '@commerce/api/operations'
import type { GetProductOperation } from '@commerce/types/product'
import type { RawProduct, RawSpec, RawVariant } from '../../types/product'
import type { OrdercloudConfig, Provider } from '../index'
import { normalize as normalizeProduct } from '../../utils/product'
export default function getProductOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct<T extends GetProductOperation>({
config,
variables,
}: {
query?: string
variables?: T['variables']
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
// Get fetch from the config
const { restBuyerFetch } = commerce.getConfig(config)
// Get a single product
const productPromise = restBuyerFetch<RawProduct>(
'GET',
`/me/products/${variables?.slug}`
)
// Get product specs
const specsPromise = restBuyerFetch<{ Items: RawSpec[] }>(
'GET',
`/me/products/${variables?.slug}/specs`
).then((res) => res.Items)
// Get product variants
const variantsPromise = restBuyerFetch<{ Items: RawVariant[] }>(
'GET',
`/me/products/${variables?.slug}/variants`
).then((res) => res.Items)
// Execute all promises in parallel
const [product, specs, variants] = await Promise.all([
productPromise,
specsPromise,
variantsPromise,
])
// Hydrate product
product.xp.Specs = specs
product.xp.Variants = variants
return {
// Normalize product to commerce schema
product: normalizeProduct(product),
}
}
return getProduct
}

View File

@ -0,0 +1,46 @@
import type { OperationContext } from '@commerce/api/operations'
import type { Category, GetSiteInfoOperation } from '@commerce/types/site'
import type { RawCategory } from '../../types/category'
import type { OrdercloudConfig, Provider } from '../index'
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[]
brands: any[]
}
> = T
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo<T extends GetSiteInfoOperation>({
config,
}: {
query?: string
variables?: any
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<T['data']> {
// Get fetch from the config
const { restBuyerFetch } = commerce.getConfig(config)
// Get list of categories
const rawCategories: RawCategory[] = await restBuyerFetch<{
Items: RawCategory[]
}>('GET', `/me/categories`).then((response) => response.Items)
return {
// Normalize categories
categories: rawCategories.map((category) => ({
id: category.ID,
name: category.Name,
slug: category.ID,
path: `/${category.ID}`,
})),
brands: [],
}
}
return getSiteInfo
}

View File

@ -0,0 +1,6 @@
export { default as getAllPages } from './get-all-pages'
export { default as getPage } from './get-page'
export { default as getSiteInfo } from './get-site-info'
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,41 @@
import type { Cart, OrdercloudCart, OrdercloudLineItem } from '../../types/cart'
export function formatCart(
cart: OrdercloudCart,
lineItems: OrdercloudLineItem[]
): Cart {
return {
id: cart.ID,
customerId: cart.FromUserID,
email: cart.FromUser.Email,
createdAt: cart.DateCreated,
currency: {
code: cart.FromUser?.xp?.currency ?? 'USD',
},
taxesIncluded: cart.TaxCost === 0,
lineItems: lineItems.map((lineItem) => ({
id: lineItem.ID,
variantId: lineItem.Variant ? String(lineItem.Variant.ID) : '',
productId: lineItem.ProductID,
name: lineItem.Product.Name,
quantity: lineItem.Quantity,
discounts: [],
path: lineItem.ProductID,
variant: {
id: lineItem.Variant ? String(lineItem.Variant.ID) : '',
sku: lineItem.ID,
name: lineItem.Product.Name,
image: {
url: lineItem.Product.xp?.Images?.[0]?.url,
},
requiresShipping: Boolean(lineItem.ShippingAddress),
price: lineItem.UnitPrice,
listPrice: lineItem.UnitPrice,
},
})),
lineItemsSubtotalPrice: cart.Subtotal,
subtotalPrice: cart.Subtotal,
totalPrice: cart.Total,
discounts: [],
}
}

View File

@ -0,0 +1,14 @@
import type { GraphQLFetcher } from '@commerce/api'
import type { OrdercloudConfig } from '../'
import { FetcherError } from '@commerce/utils/errors'
const fetchGraphqlApi: (getConfig: () => OrdercloudConfig) => GraphQLFetcher =
() => async () => {
throw new FetcherError({
errors: [{ message: 'GraphQL fetch is not implemented' }],
status: 500,
})
}
export default fetchGraphqlApi

View File

@ -0,0 +1,176 @@
import vercelFetch from '@vercel/fetch'
import { FetcherError } from '@commerce/utils/errors'
import { OrdercloudConfig } from '../index'
// Get an instance to vercel fetch
const fetch = vercelFetch()
// Get token util
async function getToken({
baseUrl,
clientId,
clientSecret,
}: {
baseUrl: string
clientId: string
clientSecret?: string
}): Promise<string> {
// If not, get a new one and store it
const authResponse = await fetch(`${baseUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: `client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`,
})
// If something failed getting the auth response
if (!authResponse.ok) {
// Get the body of it
const error = await authResponse.json()
// And return an error
throw new FetcherError({
errors: [{ message: error.error_description.Code }],
status: error.error_description.HttpStatus,
})
}
// Return the token
return authResponse
.json()
.then((response: { access_token: string }) => response.access_token)
}
export async function fetchData<T>(opts: {
token: string
path: string
method: string
config: OrdercloudConfig
fetchOptions?: Record<string, any>
body?: Record<string, unknown>
}): Promise<T> {
// Destructure opts
const { path, body, fetchOptions, config, token, method = 'GET' } = opts
// Do the request with the correct headers
const dataResponse = await fetch(
`${config.commerceUrl}/${config.apiVersion}${path}`,
{
...fetchOptions,
method,
headers: {
...fetchOptions?.headers,
'Content-Type': 'application/json',
accept: 'application/json, text/plain, */*',
authorization: `Bearer ${token}`,
},
body: body ? JSON.stringify(body) : undefined,
}
)
// If something failed getting the data response
if (!dataResponse.ok) {
// Get the body of it
const error = await dataResponse.textConverted()
// And return an error
throw new FetcherError({
errors: [{ message: error || dataResponse.statusText }],
status: dataResponse.status,
})
}
try {
// Return data response as json
return (await dataResponse.json()) as Promise<T>
} catch (error) {
// If response is empty return it as text
return null as unknown as Promise<T>
}
}
export const createMiddlewareFetcher: (
getConfig: () => OrdercloudConfig
) => <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T> =
(getConfig) =>
async <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => {
// Get provider config
const config = getConfig()
// Get a token
const token = await getToken({
baseUrl: config.commerceUrl,
clientId: process.env.ORDERCLOUD_MIDDLEWARE_CLIENT_ID as string,
clientSecret: process.env.ORDERCLOUD_MIDDLEWARE_CLIENT_SECRET,
})
// Return the data and specify the expected type
return fetchData<T>({
token,
fetchOptions,
method,
config,
path,
body,
})
}
export const createBuyerFetcher: (
getConfig: () => OrdercloudConfig
) => <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T> =
(getConfig) =>
async <T>(
method: string,
path: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => {
// Get provider config
const config = getConfig()
// If a token was passed, set it on global
if (fetchOptions?.token) {
global.token = fetchOptions.token
}
// Get a token
if (!global.token) {
global.token = await getToken({
baseUrl: config.commerceUrl,
clientId: process.env.ORDERCLOUD_BUYER_CLIENT_ID as string,
})
}
// Return the data and specify the expected type
const data = await fetchData<T>({
token: global.token as string,
fetchOptions,
config,
method,
path,
body,
})
return {
...data,
meta: { token: global.token as string },
}
}

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,16 @@
import { MutationHook } from '@commerce/utils/types'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook: () => () => {
return async function () {}
},
}

View File

@ -0,0 +1,17 @@
import { MutationHook } from '@commerce/utils/types'
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook:
({ fetch }) =>
() =>
async () => {},
}

View File

@ -0,0 +1,19 @@
import { useCallback } from 'react'
import useCustomer from '../customer/use-customer'
import { MutationHook } from '@commerce/utils/types'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook:
({ fetch }) =>
() =>
() => {},
}

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,48 @@
import type { AddItemHook } from '@commerce/types/cart'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
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 }) =>
function useHook() {
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 type { GetCartHook } from '@commerce/types/cart'
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<GetCartHook> = {
fetchOptions: {
url: '/api/cart',
method: 'GET',
},
useHook: ({ useData }) =>
function useHook(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,60 @@
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import type { Cart, LineItem, RemoveItemHook } from '@commerce/types/cart'
import { useCallback } from 'react'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
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>) =>
function useHook<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,93 @@
import type {
HookFetcherContext,
MutationHookContext,
} from '@commerce/utils/types'
import type { UpdateItemHook, LineItem } from '@commerce/types/cart'
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import { MutationHook } from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
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<any>
export const handler: MutationHook<any> = {
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>) =>
function useHook<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) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({
input: {
itemId,
item: {
productId,
variantId: variantId || '',
quantity: input.quantity,
},
},
})
await mutate(data, false)
return data
}, ctx.wait ?? 500),
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,2 @@
export { default as useSubmitCheckout } from './use-submit-checkout'
export { default as useCheckout } from './use-checkout'

View File

@ -0,0 +1,41 @@
import type { GetCheckoutHook } from '@commerce/types/checkout'
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout'
import useSubmitCheckout from './use-submit-checkout'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<GetCheckoutHook> = {
fetchOptions: {
url: '/api/checkout',
method: 'GET',
},
useHook: ({ useData }) =>
function useHook(input) {
const submit = useSubmitCheckout();
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems?.length ?? 0) <= 0
},
enumerable: true,
},
submit: {
get() {
return submit
},
enumerable: true,
},
}),
[response, submit]
)
},
}

View File

@ -0,0 +1,36 @@
import type { SubmitCheckoutHook } from '@commerce/types/checkout'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import useSubmitCheckout, { UseSubmitCheckout } from '@commerce/checkout/use-submit-checkout'
export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
export const handler: MutationHook<SubmitCheckoutHook> = {
fetchOptions: {
url: '/api/checkout',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {
// @TODO: Make form validations in here, import generic error like import { CommerceError } from '@commerce/utils/errors'
// Get payment and delivery information in here
const data = await fetch({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
return useCallback(
async function onSubmitCheckout(input) {
const data = await fetch({ input })
return data
},
[fetch]
)
},
}

View File

@ -0,0 +1,10 @@
{
"provider": "ordercloud",
"features": {
"wishlist": false,
"cart": true,
"search": false,
"customerAuth": false,
"customCheckout": true
}
}

View File

@ -0,0 +1,6 @@
export const CART_COOKIE = 'ordercloud.cart'
export const TOKEN_COOKIE = 'ordercloud.token'
export const CUSTOMER_COOKIE = 'ordercloud.customer'
export const API_URL = 'https://sandboxapi.ordercloud.io'
export const API_VERSION = 'v1'
export const LOCALE = 'en-us'

View File

@ -0,0 +1,4 @@
export { default as useAddresses } from './use-addresses'
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,38 @@
import type { AddItemHook } from '@commerce/types/customer/address'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item'
import useAddresses from './use-addresses'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '/api/customer/address',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {
const data = await fetch({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
const { mutate } = useAddresses()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate([data], false)
return data
},
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,35 @@
import type { GetAddressesHook } from '@commerce/types/customer/address'
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useAddresses, {
UseAddresses,
} from '@commerce/customer/address/use-addresses'
export default useAddresses as UseAddresses<typeof handler>
export const handler: SWRHook<GetAddressesHook> = {
fetchOptions: {
url: '/api/customer/address',
method: 'GET',
},
useHook: ({ useData }) =>
function useHook(input) {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.length ?? 0) <= 0
},
enumerable: true,
},
}),
[response]
)
},
}

View File

@ -0,0 +1,62 @@
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import type { Address, RemoveItemHook } from '@commerce/types/customer/address'
import { useCallback } from 'react'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, {
UseRemoveItem,
} from '@commerce/customer/address/use-remove-item'
import useAddresses from './use-addresses'
export type RemoveItemFn<T = any> = T extends Address
? (input?: RemoveItemActionInput<T>) => Promise<Address | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Address | null>
export type RemoveItemActionInput<T = any> = T extends Address
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/customer/address',
method: 'DELETE',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
return await fetch({ ...options, body: { itemId } })
},
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) =>
function useHook<T extends Address | undefined = undefined>(
ctx: { item?: T } = {}
) {
const { item } = ctx
const { mutate } = useAddresses()
const removeItem: RemoveItemFn<Address> = 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([], false)
return data
}
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
}

View File

@ -0,0 +1,52 @@
import type {
HookFetcherContext,
MutationHookContext,
} from '@commerce/utils/types'
import type { UpdateItemHook, Address } from '@commerce/types/customer/address'
import { useCallback } from 'react'
import { MutationHook } from '@commerce/utils/types'
import useUpdateItem, {
UseUpdateItem,
} from '@commerce/customer/address/use-update-item'
import useAddresses from './use-addresses'
export type UpdateItemActionInput<T = any> = T extends Address
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = {
fetchOptions: {
url: '/api/customer/address',
method: 'PUT',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateItemHook>) {
return await fetch({
...options,
body: { itemId, item },
})
},
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) =>
function useHook() {
const { mutate } = useAddresses()
return useCallback(
async function updateItem(input) {
const data = await fetch({ input })
await mutate([], false)
return data
},
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,4 @@
export { default as useCards } from './use-cards'
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,38 @@
import type { AddItemHook } from '@commerce/types/customer/card'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item'
import useCards from './use-cards'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '/api/customer/card',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {
const data = await fetch({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
const { mutate } = useCards()
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 type { GetCardsHook } from '@commerce/types/customer/card'
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useCard, { UseCards } from '@commerce/customer/card/use-cards'
export default useCard as UseCards<typeof handler>
export const handler: SWRHook<GetCardsHook> = {
fetchOptions: {
url: '/api/customer/card',
method: 'GET',
},
useHook: ({ useData }) =>
function useHook(input) {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.length ?? 0) <= 0
},
enumerable: true,
},
}),
[response]
)
},
}

View File

@ -0,0 +1,62 @@
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import type { Card, RemoveItemHook } from '@commerce/types/customer/card'
import { useCallback } from 'react'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, {
UseRemoveItem,
} from '@commerce/customer/card/use-remove-item'
import useCards from './use-cards'
export type RemoveItemFn<T = any> = T extends Card
? (input?: RemoveItemActionInput<T>) => Promise<Card | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Card | null>
export type RemoveItemActionInput<T = any> = T extends Card
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/customer/card',
method: 'DELETE',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
return await fetch({ ...options, body: { itemId } })
},
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) =>
function useHook<T extends Card | undefined = undefined>(
ctx: { item?: T } = {}
) {
const { item } = ctx
const { mutate } = useCards()
const removeItem: RemoveItemFn<Card> = 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([], false)
return data
}
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
}

View File

@ -0,0 +1,52 @@
import type {
HookFetcherContext,
MutationHookContext,
} from '@commerce/utils/types'
import type { UpdateItemHook, Card } from '@commerce/types/customer/card'
import { useCallback } from 'react'
import { MutationHook } from '@commerce/utils/types'
import useUpdateItem, {
UseUpdateItem,
} from '@commerce/customer/card/use-update-item'
import useCards from './use-cards'
export type UpdateItemActionInput<T = any> = T extends Card
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = {
fetchOptions: {
url: '/api/customer/card',
method: 'PUT',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateItemHook>) {
return await fetch({
...options,
body: { itemId, item },
})
},
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) =>
function useHook() {
const { mutate } = useCards()
return useCallback(
async function updateItem(input) {
const data = await fetch({ input })
await mutate([], false)
return data
},
[fetch, mutate]
)
},
}

View File

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

View File

@ -0,0 +1,15 @@
import { SWRHook } from '@commerce/utils/types'
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook: () => () => {
return async function addItem() {
return {}
}
},
}

View File

@ -0,0 +1,17 @@
import { Fetcher } from '@commerce/utils/types'
const clientFetcher: Fetcher = async ({ method, url, body }) => {
const response = await fetch(url!, {
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => response.json())
.then((response) => response.data)
return response
}
export default clientFetcher

View File

@ -0,0 +1,9 @@
import { ordercloudProvider, OrdercloudProvider } from './provider'
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
export { ordercloudProvider }
export type { OrdercloudProvider }
export const CommerceProvider = getCommerceProvider(ordercloudProvider)
export const useCommerce = () => useCoreCommerce()

View File

@ -0,0 +1,8 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
images: {
domains: ['localhost', 'ocdevops.blob.core.windows.net'],
},
}

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,17 @@
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: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook: () => () => {
return {
data: {
products: [],
},
}
},
}

View File

@ -0,0 +1,62 @@
import { handler as useCart } from './cart/use-cart'
import { handler as useAddCartItem } from './cart/use-add-item'
import { handler as useUpdateCartItem } from './cart/use-update-item'
import { handler as useRemoveCartItem } 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 useCheckout } from './checkout/use-checkout'
import { handler as useSubmitCheckout } from './checkout/use-submit-checkout'
import { handler as useCards } from './customer/card/use-cards'
import { handler as useAddCardItem } from './customer/card/use-add-item'
import { handler as useUpdateCardItem } from './customer/card/use-update-item'
import { handler as useRemoveCardItem } from './customer/card/use-remove-item'
import { handler as useAddresses } from './customer/address/use-addresses'
import { handler as useAddAddressItem } from './customer/address/use-add-item'
import { handler as useUpdateAddressItem } from './customer/address/use-update-item'
import { handler as useRemoveAddressItem } from './customer/address/use-remove-item'
import { CART_COOKIE, LOCALE } from './constants'
import { default as fetcher } from './fetcher'
export const ordercloudProvider = {
locale: LOCALE,
cartCookie: CART_COOKIE,
fetcher,
cart: {
useCart,
useAddItem: useAddCartItem,
useUpdateItem: useUpdateCartItem,
useRemoveItem: useRemoveCartItem
},
checkout: {
useCheckout,
useSubmitCheckout,
},
customer: {
useCustomer,
card: {
useCards,
useAddItem: useAddCardItem,
useUpdateItem: useUpdateCardItem,
useRemoveItem: useRemoveCardItem
},
address: {
useAddresses,
useAddItem: useAddAddressItem,
useUpdateItem: useUpdateAddressItem,
useRemoveItem: useRemoveAddressItem
}
},
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}
export type OrdercloudProvider = typeof ordercloudProvider

View File

@ -0,0 +1,126 @@
import * as Core from '@commerce/types/cart'
export * from '@commerce/types/cart'
export interface OrdercloudCart {
ID: string
FromUser: {
ID: string
Username: string
Password: null
FirstName: string
LastName: string
Email: string
Phone: null
TermsAccepted: null
Active: true
xp: {
something: string
currency: string
}
AvailableRoles: null
DateCreated: string
PasswordLastSetDate: null
}
FromCompanyID: string
ToCompanyID: string
FromUserID: string
BillingAddressID: null
BillingAddress: null
ShippingAddressID: null
Comments: null
LineItemCount: number
Status: string
DateCreated: string
DateSubmitted: null
DateApproved: null
DateDeclined: null
DateCanceled: null
DateCompleted: null
LastUpdated: string
Subtotal: number
ShippingCost: number
TaxCost: number
PromotionDiscount: number
Total: number
IsSubmitted: false
xp: {
productId: string
variantId: string
quantity: 1
}
}
export interface OrdercloudLineItem {
ID: string
ProductID: string
Quantity: 1
DateAdded: string
QuantityShipped: number
UnitPrice: number
PromotionDiscount: number
LineTotal: number
LineSubtotal: number
CostCenter: null
DateNeeded: null
ShippingAccount: null
ShippingAddressID: null
ShipFromAddressID: null
Product: {
ID: string
Name: string
Description: string
QuantityMultiplier: number
ShipWeight: number
ShipHeight: null
ShipWidth: null
ShipLength: null
xp: {
Images: {
url: string
}[]
}
}
Variant: null | {
ID: string
Name: null
Description: null
ShipWeight: null
ShipHeight: null
ShipWidth: null
ShipLength: null
xp: null
}
ShippingAddress: null
ShipFromAddress: null
SupplierID: null
Specs: []
xp: null
}
/**
* Extend core cart types
*/
export type Cart = Core.Cart & {
lineItems: Core.LineItem[]
url?: string
}
export type CartTypes = Core.CartTypes
export type CartHooks = Core.CartHooks<CartTypes>
export type GetCartHook = CartHooks['getCart']
export type AddItemHook = CartHooks['addItem']
export type UpdateItemHook = CartHooks['updateItem']
export type RemoveItemHook = CartHooks['removeItem']
export type CartSchema = Core.CartSchema<CartTypes>
export type CartHandlers = Core.CartHandlers<CartTypes>
export type GetCartHandler = CartHandlers['getCart']
export type AddItemHandler = CartHandlers['addItem']
export type UpdateItemHandler = CartHandlers['updateItem']
export type RemoveItemHandler = CartHandlers['removeItem']

View File

@ -0,0 +1,10 @@
export interface RawCategory {
ID: string
Name: string
Description: null | string
ListOrder: number
Active: boolean
ParentID: null
ChildCount: number
xp: null
}

View File

@ -0,0 +1,4 @@
import * as Core from '@commerce/types/checkout'
export type CheckoutTypes = Core.CheckoutTypes
export type CheckoutSchema = Core.CheckoutSchema<CheckoutTypes>

View File

@ -0,0 +1,31 @@
import * as Core from '@commerce/types/customer/address'
export type CustomerAddressTypes = Core.CustomerAddressTypes
export type CustomerAddressSchema = Core.CustomerAddressSchema<CustomerAddressTypes>
export interface OrdercloudAddress {
ID: string;
"FromCompanyID": string;
"ToCompanyID": string;
"FromUserID": string;
"BillingAddressID": null,
"BillingAddress": null,
"ShippingAddressID": null,
"Comments": null,
"LineItemCount": number;
"Status": string;
"DateCreated": string;
"DateSubmitted": null,
"DateApproved": null,
"DateDeclined": null,
"DateCanceled": null,
"DateCompleted": null,
"LastUpdated": string;
"Subtotal": number
"ShippingCost": number
"TaxCost": number
"PromotionDiscount": number
"Total": number
"IsSubmitted": false,
"xp": null
}

View File

@ -0,0 +1,16 @@
import * as Core from '@commerce/types/customer/card'
export type CustomerCardTypes = Core.CustomerCardTypes
export type CustomerCardSchema = Core.CustomerCardSchema<CustomerCardTypes>
export interface OredercloudCreditCard {
"ID": string;
"Editable": boolean;
"Token": string;
"DateCreated": string;
"CardType": string;
"PartialAccountNumber": string;
"CardholderName": string;
"ExpirationDate": string;
"xp": null
}

5
framework/ordercloud/types/node.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module NodeJS {
interface Global {
token: string | null | undefined
}
}

View File

@ -0,0 +1,55 @@
interface RawVariantSpec {
SpecID: string
Name: string
OptionID: string
Value: string
PriceMarkupType: string
PriceMarkup: string | null
}
export interface RawSpec {
ID: string
Name: string
Options: {
ID: string
Value: string
xp: {
hexColor?: string
}
}[]
}
export interface RawVariant {
ID: string
Specs: RawVariantSpec[]
}
export interface RawProduct {
OwnerID: string
DefaultPriceScheduleID: string | null
AutoForward: boolean
ID: string
Name: string
Description: string
QuantityMultiplier: number
ShipWeight: null
ShipHeight: null
ShipWidth: null
ShipLength: null
Active: boolean
SpecCount: number
VariantCount: number
ShipFromAddressID: null
Inventory: null
DefaultSupplierID: null
AllSuppliersCanSell: boolean
xp: {
Price: number
PriceCurrency: string
Images: {
url: string
}[]
Variants?: RawVariant[]
Specs?: RawSpec[]
}
}

View File

@ -0,0 +1,47 @@
import type { Product } from '@commerce/types/product'
import type { RawProduct } from '../types/product'
export function normalize(product: RawProduct): Product {
return {
id: product.ID,
name: product.Name,
description: product.Description,
slug: product.ID,
images: product.xp.Images,
price: {
value: product.xp.Price,
currencyCode: product.xp.PriceCurrency,
},
variants: product.xp.Variants?.length
? product.xp.Variants.map((variant) => ({
id: variant.ID,
options: variant.Specs.map((spec) => ({
id: spec.SpecID,
__typename: 'MultipleChoiceOption',
displayName: spec.Name,
values: [
{
label: spec.Value,
},
],
})),
}))
: [
{
id: '',
options: [],
},
],
options: product.xp.Specs?.length
? product.xp.Specs.map((spec) => ({
id: spec.ID,
displayName: spec.Name,
values: spec.Options.map((option) => ({
label: option.Value,
...(option.xp?.hexColor && { hexColors: [option.xp.hexColor] }),
})),
}))
: [],
}
}

View File

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

View File

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

View File

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

View File

@ -43,6 +43,7 @@
"react-fast-marquee": "^1.1.4",
"react-merge-refs": "^1.1.0",
"react-use-measure": "^2.0.4",
"stripe": "^8.176.0",
"swell-js": "^4.0.0-next.0",
"swr": "^0.5.6",
"tabbable": "^5.2.0",

View File

@ -1170,6 +1170,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
"@types/node@>=8.1.0":
version "16.9.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.6.tgz#040a64d7faf9e5d9e940357125f0963012e66f04"
integrity sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -5686,6 +5691,13 @@ qs@6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@^6.6.0:
version "6.10.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
dependencies:
side-channel "^1.0.4"
querystring-es3@0.2.1, querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@ -6487,6 +6499,14 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
stripe@^8.176.0:
version "8.176.0"
resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.176.0.tgz#2f4980ab49acbfe6d67ecaddd54c05e20de9532c"
integrity sha512-0KCDo8TWFgeNWU7cPaqdjO2u2OSth0cmWYZmA7xsuxRCk7/lgWbJ/UbeSphx74cCIjFCmGuzDoNuNxqon9lEbg==
dependencies:
"@types/node" ">=8.1.0"
qs "^6.6.0"
styled-jsx@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018"