mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 15:06:59 +00:00
Add custome checkout function
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
1090294ec1
commit
94a145b604
@ -1,4 +1,6 @@
|
||||
COMMERCE_PROVIDER=@vercel/commerce-opencommerce
|
||||
|
||||
OPENCOMMERCE_STOREFRONT_API_URL=
|
||||
OPENCOMMERCE_PRIMARY_SHOP_ID=
|
||||
OPENCOMMERCE_PRIMARY_SHOP_ID=
|
||||
OPENCOMMERCE_IMAGE_DOMAIN=
|
||||
OPENCOMMERCE_STRIPE_API_KEY=
|
1
packages/opencommerce/global.d.ts
vendored
Normal file
1
packages/opencommerce/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module '@components/checkout/context'
|
@ -50,7 +50,8 @@
|
||||
"dependencies": {
|
||||
"@vercel/commerce": "^0.0.1",
|
||||
"@vercel/fetch": "^6.1.1",
|
||||
"graphql": "^16.3.0"
|
||||
"graphql": "^16.3.0",
|
||||
"stripe": "^8.220.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12",
|
||||
|
@ -2,7 +2,6 @@ import { normalizeCart } from '../../../utils/normalize'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import updateCartItemsQuantityMutation from '../../mutations/update-cart-item-quantity'
|
||||
import type { CartEndpoint } from '.'
|
||||
import { UpdateCartItemsQuantityPayload } from '../../../../schema'
|
||||
|
||||
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||
res,
|
||||
|
@ -1 +1,23 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
|
||||
import type { CheckoutSchema } from '../../../types/checkout'
|
||||
import type { OpenCommerceAPI } from '../..'
|
||||
|
||||
import submitCheckout from './submit-checkout'
|
||||
import getCheckout from './get-checkout'
|
||||
|
||||
export type CheckoutAPI = GetAPISchema<OpenCommerceAPI, CheckoutSchema>
|
||||
|
||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||
|
||||
export const handlers: CheckoutEndpoint['handlers'] = {
|
||||
submitCheckout,
|
||||
getCheckout,
|
||||
}
|
||||
|
||||
const checkoutApi = createEndpoint<CheckoutAPI>({
|
||||
handler: checkoutEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default checkoutApi
|
||||
|
@ -0,0 +1,96 @@
|
||||
import Stripe from 'stripe'
|
||||
|
||||
import type { CardFields } from '../../../types/customer/card'
|
||||
import { LineItem } from '../../../types/cart'
|
||||
import placeOrder from '../../mutations/place-order'
|
||||
import setEmailOnAnonymousCart from '../../mutations/set-email-on-anonymous-cart'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CheckoutEndpoint } from '.'
|
||||
|
||||
const stripe = new Stripe(process.env.OPENCOMMERCE_STRIPE_API_KEY as string, {
|
||||
apiVersion: '2020-08-27',
|
||||
})
|
||||
|
||||
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
|
||||
res,
|
||||
body: { item, cartId },
|
||||
config: { fetch, shopId, anonymousCartTokenCookie, cartCookie },
|
||||
req: { cookies },
|
||||
}) => {
|
||||
await fetch(setEmailOnAnonymousCart, {
|
||||
variables: {
|
||||
input: {
|
||||
cartId,
|
||||
cartToken: cookies[anonymousCartTokenCookie],
|
||||
email: 'opencommerce@test.com',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const card = item.card as CardFields
|
||||
|
||||
const pm = await stripe.paymentMethods.create({
|
||||
type: 'card',
|
||||
card: {
|
||||
number: card.cardNumber,
|
||||
exp_month: Number(card.cardExpireDate.split('/')[0]),
|
||||
exp_year: Number(card.cardExpireDate.split('/')[1]),
|
||||
cvc: card.cardCvc,
|
||||
},
|
||||
} as Stripe.PaymentMethodCreateParams)
|
||||
|
||||
const result = await stripe.paymentIntents.create({
|
||||
confirm: true,
|
||||
amount: Math.round(item.checkout.cart.checkout.summary.total.amount * 100),
|
||||
currency: item.checkout.cart.currency.code,
|
||||
capture_method: 'manual',
|
||||
metadata: {
|
||||
integration_check: 'accept_a_payment',
|
||||
},
|
||||
payment_method: pm.id,
|
||||
})
|
||||
|
||||
if (result.status === 'succeeded' || result.status === 'requires_capture') {
|
||||
const { data } = await fetch(placeOrder, {
|
||||
variables: {
|
||||
input: {
|
||||
payments: {
|
||||
data: { stripePaymentIntentId: result.id },
|
||||
amount: item.checkout.cart.checkout.summary.total.amount,
|
||||
method: 'stripe_payment_intent',
|
||||
},
|
||||
order: {
|
||||
cartId,
|
||||
currencyCode: item.checkout.cart.currency.code,
|
||||
email: 'opencommerce@test.com',
|
||||
shopId,
|
||||
fulfillmentGroups: {
|
||||
shopId,
|
||||
data: item.checkout.cart.checkout.fulfillmentGroups[0].data,
|
||||
items: item.checkout.cart.lineItems.map((item: LineItem) => ({
|
||||
price: item.variant.price,
|
||||
quantity: item.quantity,
|
||||
productConfiguration: {
|
||||
productId: item.productId,
|
||||
productVariantId: item.variantId,
|
||||
},
|
||||
})),
|
||||
type: item.checkout.cart.checkout.fulfillmentGroups[0].type,
|
||||
selectedFulfillmentMethodId:
|
||||
item.checkout.cart.checkout.fulfillmentGroups[0]
|
||||
.selectedFulfillmentOption.fulfillmentMethod._id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
res.setHeader('Set-Cookie', [
|
||||
getCartCookie(cartCookie),
|
||||
getCartCookie(anonymousCartTokenCookie),
|
||||
])
|
||||
}
|
||||
res.status(200).json({ data: null, errors: [] })
|
||||
}
|
||||
|
||||
export default submitCheckout
|
@ -0,0 +1,73 @@
|
||||
import setShippingAddressOnCartMutation from '../../../mutations/add-shipping-address'
|
||||
import type { CustomerAddressEndpoint } from '.'
|
||||
import updateFulfillmentOptions from '../../../mutations/update-fulfillment-options'
|
||||
import selectFulfillmentOptions from '../../../mutations/select-fulfillment-options'
|
||||
|
||||
const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
|
||||
res,
|
||||
body: { item, cartId },
|
||||
config: { fetch, anonymousCartTokenCookie },
|
||||
req: { cookies },
|
||||
}) => {
|
||||
// Return an error if no cart is present
|
||||
if (!cartId) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Cookie not found' }],
|
||||
})
|
||||
}
|
||||
|
||||
// Register address
|
||||
const {
|
||||
data: { setShippingAddressOnCart },
|
||||
} = await fetch(setShippingAddressOnCartMutation, {
|
||||
variables: {
|
||||
input: {
|
||||
address: {
|
||||
address1: item.streetNumber || 'NextJS storefront',
|
||||
country: item.country,
|
||||
fullName: `${item.firstName || 'Test'} ${
|
||||
item.lastName || 'Account'
|
||||
}}`,
|
||||
city: item.city || 'LA',
|
||||
phone: '0123456789',
|
||||
postal: item.zipCode || '1234567',
|
||||
region: item.city || 'LA',
|
||||
},
|
||||
cartId,
|
||||
cartToken: cookies[anonymousCartTokenCookie],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
data: { updateFulfillmentOptionsForGroup },
|
||||
} = await fetch(updateFulfillmentOptions, {
|
||||
variables: {
|
||||
input: {
|
||||
cartId,
|
||||
fulfillmentGroupId:
|
||||
setShippingAddressOnCart.cart.checkout.fulfillmentGroups[0]._id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await fetch(selectFulfillmentOptions, {
|
||||
variables: {
|
||||
input: {
|
||||
cartId,
|
||||
cartToken: cookies[anonymousCartTokenCookie],
|
||||
fulfillmentGroupId:
|
||||
updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups[0]
|
||||
._id,
|
||||
fulfillmentMethodId:
|
||||
updateFulfillmentOptionsForGroup.cart.checkout.fulfillmentGroups[0]
|
||||
.availableFulfillmentOptions[0].fulfillmentMethod._id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return res.status(200).json({ data: null, errors: [] })
|
||||
}
|
||||
|
||||
export default addItem
|
@ -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
|
@ -0,0 +1,30 @@
|
||||
import type { CustomerAddressSchema } from '../../../../types/customer/address'
|
||||
import type { OpenCommerceAPI } from '../../..'
|
||||
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import customerAddressEndpoint from '@vercel/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<
|
||||
OpenCommerceAPI,
|
||||
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
|
@ -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
|
@ -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
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -0,0 +1,43 @@
|
||||
import type { CustomerCardEndpoint } from '.'
|
||||
import createPaymentIntent from '../../../mutations/create-payment-intent'
|
||||
|
||||
const addItem: CustomerCardEndpoint['handlers']['addItem'] = async ({
|
||||
res,
|
||||
body: { item, cartId },
|
||||
config,
|
||||
req: { cookies },
|
||||
}) => {
|
||||
// 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 cart is present
|
||||
if (!cartId) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Cookie not found' }],
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
data: { createStripePaymentIntent },
|
||||
} = await config.fetch(createPaymentIntent, {
|
||||
variables: {
|
||||
input: {
|
||||
cartId,
|
||||
shopId: config.shopId,
|
||||
cartToken: cookies[config.anonymousCartTokenCookie],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return res.status(200).json({
|
||||
data: createStripePaymentIntent.paymentIntentClientSecret,
|
||||
})
|
||||
}
|
||||
|
||||
export default addItem
|
@ -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
|
@ -0,0 +1,26 @@
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import customerCardEndpoint from '@vercel/commerce/api/endpoints/customer/card'
|
||||
import { CustomerCardSchema } from '../../../../types/customer/card'
|
||||
import { OpenCommerceAPI } from '../../..'
|
||||
|
||||
import getCards from './get-cards'
|
||||
import addItem from './add-item'
|
||||
// import updateItem from './update-item'
|
||||
// import removeItem from './remove-item'
|
||||
|
||||
export type CustomerCardAPI = GetAPISchema<OpenCommerceAPI, 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
|
@ -0,0 +1,13 @@
|
||||
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||
|
||||
const setShippingAddressOnCartMutation = `
|
||||
mutation setShippingAddressOnCartMutation($input: SetShippingAddressOnCartInput!) {
|
||||
setShippingAddressOnCart(input: $input) {
|
||||
cart {
|
||||
${cartPayloadFragment}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default setShippingAddressOnCartMutation
|
211
packages/opencommerce/src/api/mutations/place-order.ts
Normal file
211
packages/opencommerce/src/api/mutations/place-order.ts
Normal file
@ -0,0 +1,211 @@
|
||||
const orderCommon = `
|
||||
_id
|
||||
account {
|
||||
_id
|
||||
}
|
||||
cartId
|
||||
createdAt
|
||||
displayStatus(language: $language)
|
||||
email
|
||||
fulfillmentGroups {
|
||||
_id
|
||||
data {
|
||||
... on ShippingOrderFulfillmentGroupData {
|
||||
shippingAddress {
|
||||
_id
|
||||
address1
|
||||
address2
|
||||
city
|
||||
company
|
||||
country
|
||||
fullName
|
||||
isCommercial
|
||||
isShippingDefault
|
||||
phone
|
||||
postal
|
||||
region
|
||||
}
|
||||
}
|
||||
}
|
||||
items {
|
||||
nodes {
|
||||
_id
|
||||
addedAt
|
||||
createdAt
|
||||
imageURLs {
|
||||
large
|
||||
medium
|
||||
original
|
||||
small
|
||||
thumbnail
|
||||
}
|
||||
isTaxable
|
||||
optionTitle
|
||||
parcel {
|
||||
containers
|
||||
distanceUnit
|
||||
height
|
||||
length
|
||||
massUnit
|
||||
weight
|
||||
width
|
||||
}
|
||||
price {
|
||||
amount
|
||||
currency {
|
||||
code
|
||||
}
|
||||
displayAmount
|
||||
}
|
||||
productConfiguration {
|
||||
productId
|
||||
productVariantId
|
||||
}
|
||||
productSlug
|
||||
productType
|
||||
productVendor
|
||||
productTags {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
quantity
|
||||
shop {
|
||||
_id
|
||||
}
|
||||
subtotal {
|
||||
amount
|
||||
currency {
|
||||
code
|
||||
}
|
||||
displayAmount
|
||||
}
|
||||
taxCode
|
||||
title
|
||||
updatedAt
|
||||
variantTitle
|
||||
}
|
||||
}
|
||||
selectedFulfillmentOption {
|
||||
fulfillmentMethod {
|
||||
_id
|
||||
carrier
|
||||
displayName
|
||||
fulfillmentTypes
|
||||
group
|
||||
name
|
||||
}
|
||||
handlingPrice {
|
||||
amount
|
||||
currency {
|
||||
code
|
||||
}
|
||||
displayAmount
|
||||
}
|
||||
price {
|
||||
amount
|
||||
currency {
|
||||
code
|
||||
}
|
||||
displayAmount
|
||||
}
|
||||
}
|
||||
shop {
|
||||
_id
|
||||
}
|
||||
summary {
|
||||
fulfillmentTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
itemTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
surchargeTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
taxTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
total {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
}
|
||||
tracking
|
||||
type
|
||||
}
|
||||
payments {
|
||||
_id
|
||||
amount {
|
||||
displayAmount
|
||||
}
|
||||
billingAddress {
|
||||
address1
|
||||
address2
|
||||
city
|
||||
company
|
||||
country
|
||||
fullName
|
||||
isCommercial
|
||||
phone
|
||||
postal
|
||||
region
|
||||
}
|
||||
displayName
|
||||
method {
|
||||
name
|
||||
}
|
||||
}
|
||||
referenceId
|
||||
shop {
|
||||
_id
|
||||
currency {
|
||||
code
|
||||
}
|
||||
}
|
||||
status
|
||||
summary {
|
||||
fulfillmentTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
itemTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
surchargeTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
taxTotal {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
total {
|
||||
amount
|
||||
displayAmount
|
||||
}
|
||||
}
|
||||
totalItemQuantity
|
||||
updatedAt
|
||||
`
|
||||
|
||||
const placeOrder = /* GraphQL */ `
|
||||
mutation placeOrderMutation(
|
||||
$input: PlaceOrderInput!
|
||||
$language: String! = "en"
|
||||
) {
|
||||
placeOrder(input: $input) {
|
||||
orders {
|
||||
${orderCommon}
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default placeOrder
|
@ -0,0 +1,14 @@
|
||||
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||
|
||||
const selectFulfillmentOptions = /* GraphQL */ `
|
||||
mutation setFulfillmentOptionCartMutation(
|
||||
$input: SelectFulfillmentOptionForGroupInput!
|
||||
) {
|
||||
selectFulfillmentOptionForGroup(input: $input) {
|
||||
cart {
|
||||
${cartPayloadFragment}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export default selectFulfillmentOptions
|
@ -0,0 +1,15 @@
|
||||
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||
|
||||
const setEmailOnAnonymousCart = /* GraphQL */ `
|
||||
mutation setEmailOnAnonymousCartMutation(
|
||||
$input: SetEmailOnAnonymousCartInput!
|
||||
) {
|
||||
setEmailOnAnonymousCart(input: $input) {
|
||||
cart {
|
||||
${cartPayloadFragment}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default setEmailOnAnonymousCart
|
@ -0,0 +1,15 @@
|
||||
import { cartPayloadFragment } from '../queries/get-cart-query'
|
||||
|
||||
const updateFulfillmentOptions = /* GraphQL */ `
|
||||
mutation updateFulfillmentOptionsForGroup(
|
||||
$input: UpdateFulfillmentOptionsForGroupInput!
|
||||
) {
|
||||
updateFulfillmentOptionsForGroup(input: $input) {
|
||||
cart {
|
||||
${cartPayloadFragment}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default updateFulfillmentOptions
|
2
packages/opencommerce/src/checkout/index.ts
Normal file
2
packages/opencommerce/src/checkout/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as useCheckout } from './use-checkout'
|
||||
export { default as useSubmitCheckout } from './use-submit-checkout'
|
@ -1,16 +1,53 @@
|
||||
import type { GetCheckoutHook } from '@vercel/commerce/types/checkout'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCheckout, {
|
||||
UseCheckout,
|
||||
} from '@vercel/commerce/checkout/use-checkout'
|
||||
import useSubmitCheckout from './use-submit-checkout'
|
||||
import { useCheckoutContext } from '@components/checkout/context'
|
||||
|
||||
export default useCheckout as UseCheckout<typeof handler>
|
||||
|
||||
export const handler: SWRHook<any> = {
|
||||
export const handler: SWRHook<GetCheckoutHook> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
method: '',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ useData }) =>
|
||||
async (input) => ({}),
|
||||
useHook: () =>
|
||||
function useHook() {
|
||||
const { cardFields, addressFields } = useCheckoutContext()
|
||||
|
||||
// Basic validation - check that at least one field has a value.
|
||||
const hasEnteredCard = Object.values(cardFields).some(
|
||||
(fieldValue) => !!fieldValue
|
||||
)
|
||||
const hasEnteredAddress = Object.values(addressFields).some(
|
||||
(fieldValue) => !!fieldValue
|
||||
)
|
||||
|
||||
const response = useMemo(
|
||||
() => ({
|
||||
data: {
|
||||
hasPayment: hasEnteredCard,
|
||||
hasShipping: hasEnteredAddress,
|
||||
},
|
||||
}),
|
||||
[hasEnteredCard, hasEnteredAddress]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
submit: {
|
||||
get() {
|
||||
return useSubmitCheckout
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response, useSubmitCheckout]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
32
packages/opencommerce/src/checkout/use-submit-checkout.ts
Normal file
32
packages/opencommerce/src/checkout/use-submit-checkout.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import type { SubmitCheckoutHook } from '@vercel/commerce/types/checkout'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useSubmitCheckout, {
|
||||
UseSubmitCheckout,
|
||||
} from '@vercel/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 }) {
|
||||
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
|
||||
}, [])
|
||||
},
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"provider": "opencommerce",
|
||||
"features": {}
|
||||
"features": {
|
||||
"customCheckout": true
|
||||
}
|
||||
}
|
||||
|
1
packages/opencommerce/src/customer/address/index.ts
Normal file
1
packages/opencommerce/src/customer/address/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as useAddItem } from './use-add-item'
|
@ -1,17 +1,36 @@
|
||||
import type { AddItemHook } from '@vercel/commerce/types/customer/address'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { useCallback } from 'react'
|
||||
import useAddItem, {
|
||||
UseAddItem,
|
||||
} from '@vercel/commerce/customer/address/use-add-item'
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { useCheckoutContext } from '@components/checkout/context'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
url: '/api/customer/address',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ fetch }) =>
|
||||
() =>
|
||||
async () => ({}),
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
const data = await fetch({
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
|
||||
return data
|
||||
},
|
||||
useHook: ({ fetch }) =>
|
||||
function useHook() {
|
||||
const { setAddressFields } = useCheckoutContext()
|
||||
return useCallback(
|
||||
async function addItem(input) {
|
||||
await fetch({ input })
|
||||
setAddressFields(input)
|
||||
return undefined
|
||||
},
|
||||
[setAddressFields]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
2
packages/opencommerce/src/customer/card/index.ts
Normal file
2
packages/opencommerce/src/customer/card/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as useAddItem } from './use-add-item'
|
||||
export { default as useCards } from './use-cards'
|
@ -1,17 +1,27 @@
|
||||
import type { AddItemHook } from '@vercel/commerce/types/customer/card'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { useCallback } from 'react'
|
||||
import useAddItem, {
|
||||
UseAddItem,
|
||||
} from '@vercel/commerce/customer/card/use-add-item'
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { useCheckoutContext } from '@components/checkout/context'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
url: '',
|
||||
method: '',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ fetch }) =>
|
||||
() =>
|
||||
async () => ({}),
|
||||
useHook: () =>
|
||||
function useHook() {
|
||||
const { setCardFields } = useCheckoutContext()
|
||||
return useCallback(
|
||||
async function addItem(input) {
|
||||
setCardFields(input)
|
||||
return undefined
|
||||
},
|
||||
[setCardFields]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
32
packages/opencommerce/src/customer/card/use-cards.ts
Normal file
32
packages/opencommerce/src/customer/card/use-cards.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { GetCardsHook } from '@vercel/commerce/types/customer/card'
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCard, { UseCards } from '@vercel/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]
|
||||
)
|
||||
},
|
||||
}
|
@ -8,15 +8,27 @@ 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 useAddCardItem } from './customer/card/use-add-item'
|
||||
import { handler as useCards } from './customer/card/use-cards'
|
||||
import { handler as useAddAddressItem } from './customer/address/use-add-item'
|
||||
|
||||
export const openCommerceProvider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'opencommerce_cartId',
|
||||
fetcher,
|
||||
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||
customer: { useCustomer },
|
||||
customer: {
|
||||
useCustomer,
|
||||
card: { useCards, useAddItem: useAddCardItem },
|
||||
address: {
|
||||
useAddItem: useAddAddressItem,
|
||||
},
|
||||
},
|
||||
products: { useSearch },
|
||||
auth: { useLogin, useLogout, useSignup },
|
||||
checkout: { useCheckout, useSubmitCheckout },
|
||||
}
|
||||
|
||||
export type OpenCommerceProvider = typeof openCommerceProvider
|
||||
|
@ -1,11 +1,11 @@
|
||||
import * as Core from '@vercel/commerce/types/cart'
|
||||
import { Checkout } from '../../schema'
|
||||
import { ProductVariant } from './product'
|
||||
|
||||
export * from '@vercel/commerce/types/cart'
|
||||
|
||||
export type Cart = Core.Cart & {
|
||||
lineItems: Core.LineItem[]
|
||||
id: string
|
||||
checkout?: Checkout
|
||||
}
|
||||
|
||||
export type CartItemBody = Core.CartItemBody & {
|
||||
|
1
packages/opencommerce/src/types/checkout.ts
Normal file
1
packages/opencommerce/src/types/checkout.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@vercel/commerce/types/checkout'
|
1
packages/opencommerce/src/types/customer/address.ts
Normal file
1
packages/opencommerce/src/types/customer/address.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@vercel/commerce/types/customer/address'
|
1
packages/opencommerce/src/types/customer/card.ts
Normal file
1
packages/opencommerce/src/types/customer/card.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@vercel/commerce/types/customer/card'
|
@ -249,6 +249,7 @@ export function normalizeCart(cart: OCCart): Cart {
|
||||
totalPrice: cart.checkout?.summary?.total?.amount ?? 0,
|
||||
discounts: [],
|
||||
taxesIncluded: !!cart.checkout?.summary?.taxTotal?.amount,
|
||||
checkout: cart.checkout ? cart.checkout : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,11 @@
|
||||
"module": "esnext",
|
||||
"outDir": "dist",
|
||||
"baseUrl": "src",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"declaration": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
@ -16,6 +20,12 @@
|
||||
"incremental": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
"include": [
|
||||
"src",
|
||||
"global.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
@ -53,6 +53,7 @@ NEXT_PUBLIC_COMMERCEJS_DEPLOYMENT_URL=
|
||||
OPENCOMMERCE_STOREFRONT_API_URL=
|
||||
OPENCOMMERCE_PRIMARY_SHOP_ID=
|
||||
OPENCOMMERCE_IMAGE_DOMAIN=
|
||||
OPENCOMMERCE_STRIPE_API_KEY=
|
||||
|
||||
SFCC_CLIENT_ID=
|
||||
SFCC_CLIENT_SECRET=
|
||||
|
@ -7,6 +7,7 @@ import SidebarLayout from '@components/common/SidebarLayout'
|
||||
import useCart from '@framework/cart/use-cart'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import useCheckout from '@framework/checkout/use-checkout'
|
||||
import useSubmitCheckout from '@framework/checkout/use-submit-checkout'
|
||||
import ShippingWidget from '../ShippingWidget'
|
||||
import PaymentWidget from '../PaymentWidget'
|
||||
import s from './CheckoutSidebarView.module.css'
|
||||
@ -16,15 +17,21 @@ const CheckoutSidebarView: FC = () => {
|
||||
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
||||
const { setSidebarView, closeSidebar } = useUI()
|
||||
const { data: cartData, mutate: refreshCart } = useCart()
|
||||
const { data: checkoutData, submit: onCheckout } = useCheckout()
|
||||
const { clearCheckoutFields } = useCheckoutContext()
|
||||
const { data: checkoutData } = useCheckout()
|
||||
const onCheckout = useSubmitCheckout()
|
||||
|
||||
const { clearCheckoutFields, cardFields, addressFields } =
|
||||
useCheckoutContext()
|
||||
|
||||
async function handleSubmit(event: React.ChangeEvent<HTMLFormElement>) {
|
||||
try {
|
||||
setLoadingSubmit(true)
|
||||
event.preventDefault()
|
||||
|
||||
await onCheckout()
|
||||
await onCheckout({
|
||||
card: cardFields,
|
||||
address: addressFields,
|
||||
checkout: { cart: cartData },
|
||||
})
|
||||
clearCheckoutFields()
|
||||
setLoadingSubmit(false)
|
||||
refreshCart()
|
||||
|
@ -42,7 +42,6 @@ const PaymentMethodView: FC = () => {
|
||||
city: event.target.city.value,
|
||||
country: event.target.country.value,
|
||||
})
|
||||
|
||||
setSidebarView('CHECKOUT_VIEW')
|
||||
}
|
||||
|
||||
|
75
yarn.lock
75
yarn.lock
@ -552,6 +552,52 @@
|
||||
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
|
||||
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
|
||||
|
||||
"@graphql-codegen/cli@2.6.2":
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-2.6.2.tgz#a9aa4656141ee0998cae8c7ad7d0bf9ca8e0c9ae"
|
||||
integrity sha512-UO75msoVgvLEvfjCezM09cQQqp32+mR8Ma1ACsBpr7nroFvHbgcu2ulx1cMovg4sxDBCsvd9Eq/xOOMpARUxtw==
|
||||
dependencies:
|
||||
"@graphql-codegen/core" "2.5.1"
|
||||
"@graphql-codegen/plugin-helpers" "^2.4.1"
|
||||
"@graphql-tools/apollo-engine-loader" "^7.0.5"
|
||||
"@graphql-tools/code-file-loader" "^7.0.6"
|
||||
"@graphql-tools/git-loader" "^7.0.5"
|
||||
"@graphql-tools/github-loader" "^7.0.5"
|
||||
"@graphql-tools/graphql-file-loader" "^7.0.5"
|
||||
"@graphql-tools/json-file-loader" "^7.1.2"
|
||||
"@graphql-tools/load" "^7.3.0"
|
||||
"@graphql-tools/prisma-loader" "^7.0.6"
|
||||
"@graphql-tools/url-loader" "^7.0.11"
|
||||
"@graphql-tools/utils" "^8.1.1"
|
||||
ansi-escapes "^4.3.1"
|
||||
chalk "^4.1.0"
|
||||
change-case-all "1.0.14"
|
||||
chokidar "^3.5.2"
|
||||
common-tags "^1.8.0"
|
||||
cosmiconfig "^7.0.0"
|
||||
debounce "^1.2.0"
|
||||
dependency-graph "^0.11.0"
|
||||
detect-indent "^6.0.0"
|
||||
glob "^7.1.6"
|
||||
globby "^11.0.4"
|
||||
graphql-config "^4.1.0"
|
||||
inquirer "^8.0.0"
|
||||
is-glob "^4.0.1"
|
||||
json-to-pretty-yaml "^1.2.2"
|
||||
latest-version "5.1.0"
|
||||
listr "^0.14.3"
|
||||
listr-update-renderer "^0.5.0"
|
||||
log-symbols "^4.0.0"
|
||||
minimatch "^4.0.0"
|
||||
mkdirp "^1.0.4"
|
||||
string-env-interpolation "^1.0.1"
|
||||
ts-log "^2.2.3"
|
||||
tslib "~2.3.0"
|
||||
valid-url "^1.0.9"
|
||||
wrap-ansi "^7.0.0"
|
||||
yaml "^1.10.0"
|
||||
yargs "^17.0.0"
|
||||
|
||||
"@graphql-codegen/cli@^2.3.1":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-2.4.0.tgz#7df3ee2bdd5b88a5904ee6f52eafeb370ef70e51"
|
||||
@ -618,14 +664,6 @@
|
||||
"@graphql-tools/utils" "^8.1.1"
|
||||
tslib "~2.3.0"
|
||||
|
||||
"@graphql-codegen/introspection@2.1.1":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-codegen/introspection/-/introspection-2.1.1.tgz#5f3aac47ef46ed817baf969e78dd2dd6d307b18a"
|
||||
integrity sha512-O9zsy0IoFYDo37pBVF4pSvRMDx/AKdgOxyko4R/O+0DHEw9Nya/pQ3dbn+LDLj2n6X+xOXUBUfFvqhODTqU28w==
|
||||
dependencies:
|
||||
"@graphql-codegen/plugin-helpers" "^2.3.2"
|
||||
tslib "~2.3.0"
|
||||
|
||||
"@graphql-codegen/plugin-helpers@^2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-2.3.2.tgz#3f9ba625791901d19be733db1dfc9a3dbd0dac44"
|
||||
@ -681,17 +719,6 @@
|
||||
auto-bind "~4.0.0"
|
||||
tslib "~2.3.0"
|
||||
|
||||
"@graphql-codegen/typescript-react-apollo@3.2.11":
|
||||
version "3.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript-react-apollo/-/typescript-react-apollo-3.2.11.tgz#dc13abc1ec24aa78f7f0774c1f52da5d982dd1fc"
|
||||
integrity sha512-Bfo7/OprnWk/srhA/3I0cKySg/SyVPX3ZoxzTk6ChYVBsy69jKDkdPWwlmE7Fjfv7+5G+xXb99OoqUUgBLma3w==
|
||||
dependencies:
|
||||
"@graphql-codegen/plugin-helpers" "^2.4.0"
|
||||
"@graphql-codegen/visitor-plugin-common" "2.7.4"
|
||||
auto-bind "~4.0.0"
|
||||
change-case-all "1.0.14"
|
||||
tslib "~2.3.0"
|
||||
|
||||
"@graphql-codegen/typescript@2.4.8", "@graphql-codegen/typescript@^2.4.8":
|
||||
version "2.4.8"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-codegen/typescript/-/typescript-2.4.8.tgz#e8110baba9713cde72d57a5c95aa27400363ed9a"
|
||||
@ -6256,7 +6283,7 @@ qs@6.7.0:
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
||||
|
||||
qs@^6.10.2, qs@^6.6.0:
|
||||
qs@^6.10.2, qs@^6.10.3, qs@^6.6.0:
|
||||
version "6.10.3"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
|
||||
integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
|
||||
@ -7131,6 +7158,14 @@ stripe@^8.197.0:
|
||||
"@types/node" ">=8.1.0"
|
||||
qs "^6.6.0"
|
||||
|
||||
stripe@^8.220.0:
|
||||
version "8.220.0"
|
||||
resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.220.0.tgz#90bdedcb7d83e64f22c47b47f5072d837ef56357"
|
||||
integrity sha512-hE3NapEqNCiiQD1lMQPccKgJsu2aANR+oDudUHcuvRnNUJ3GrbntwACs7Op45PvHpJ/RY4l46XDwTMgdWJAm3w==
|
||||
dependencies:
|
||||
"@types/node" ">=8.1.0"
|
||||
qs "^6.10.3"
|
||||
|
||||
styled-jsx@5.0.0-beta.6:
|
||||
version "5.0.0-beta.6"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.0-beta.6.tgz#666552f8831a06f80c9084a47afc4b32b0c9f461"
|
||||
|
Loading…
x
Reference in New Issue
Block a user