vendure custom checkout support #619

This commit is contained in:
Sonio 2022-03-17 18:36:18 +09:00 committed by Michael Bromley
parent 87134e2990
commit 7b4a617bde
22 changed files with 537 additions and 25 deletions

View File

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

View File

@ -1,16 +1,43 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useCheckout, {
UseCheckout,
} from '@vercel/commerce/checkout/use-checkout'
import type { GetCheckoutHook } from '@vercel/commerce/types/checkout'
import { SWRHook } from '@vercel/commerce/utils/types'
import { useMemo } from 'react'
import { getCartQuery } from '../utils/queries/get-cart-query'
import useSubmitCheckout from './use-submit-checkout'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<any> = {
export const handler: SWRHook<GetCheckoutHook> = {
fetchOptions: {
query: '',
query: getCartQuery,
},
async fetcher({ input, options, fetch }) {},
useHook:
({ useData }) =>
async (input) => ({}),
useHook: ({ useData }) =>
function useHook(input) {
const submit = useSubmitCheckout()
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return true
},
enumerable: true,
},
submit: {
get() {
return submit
},
enumerable: true,
},
}),
[response, submit]
)
},
}

View File

@ -0,0 +1,93 @@
import useSubmitCheckout, {
UseSubmitCheckout,
} from '@vercel/commerce/checkout/use-submit-checkout'
import type { SubmitCheckoutHook } from '@vercel/commerce/types/checkout'
import { CommerceError } from '@vercel/commerce/utils/errors'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import {
EligiblePaymentMethodsQuery,
TransitionOrderToStateResult,
AddPaymentToOrderResult,
} from '../../schema'
import { addPaymentToOrder } from '../utils/mutations/add-payment-to-order'
import { transitionOrderToState } from '../utils/mutations/transition-order-to-state'
import { eligiblePaymentMethods } from '../utils/queries/eligible-payment-methods'
export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
export const handler: MutationHook<SubmitCheckoutHook> = {
fetchOptions: {
query: addPaymentToOrder,
},
async fetcher({ input: item, options, fetch }) {
const transitionResponse = await fetch<TransitionOrderToStateResult>({
...options,
query: transitionOrderToState,
variables: {
state: 'ArrangingPayment',
},
})
if (transitionResponse.__typename === 'OrderStateTransitionError') {
throw new CommerceError({
code: transitionResponse.errorCode,
message: transitionResponse.message,
})
} else {
const paymentMethodsResponse = await fetch<EligiblePaymentMethodsQuery>({
...options,
query: eligiblePaymentMethods,
})
const paymentMethodCode =
paymentMethodsResponse?.eligiblePaymentMethods?.[0]?.code
if (!paymentMethodCode) {
throw new CommerceError({
message: 'No Eligible payment methods',
})
}
const paymentResponse = await fetch<AddPaymentToOrderResult>({
...options,
variables: {
input: {
method: paymentMethodCode,
metadata: {
// TODO: Payment provider's token should go in here
},
},
},
})
if (paymentResponse.__typename === 'Order') {
return {
hasPayment: true,
hasShipping: true,
}
} else if (
paymentResponse.__typename === 'IneligiblePaymentMethodError' ||
paymentResponse.__typename === 'NoActiveOrderError' ||
paymentResponse.__typename === 'OrderPaymentStateError' ||
paymentResponse.__typename === 'OrderStateTransitionError' ||
paymentResponse.__typename === 'PaymentDeclinedError' ||
paymentResponse.__typename === 'PaymentFailedError'
) {
throw new CommerceError(paymentResponse)
} else {
throw new CommerceError({
message: 'Something went wrong with Payment request',
})
}
}
},
useHook: ({ fetch }) =>
function useHook() {
return useCallback(
async function onSubmitCheckout(input) {
const data = await fetch({ input })
return data
},
[fetch]
)
},
}

View File

@ -0,0 +1 @@
export const VENDURE_TOKEN = 'vendure.Token'

View File

@ -1,17 +1,64 @@
import useAddItem, {
UseAddItem,
UseAddItem
} from '@vercel/commerce/customer/address/use-add-item'
import { MutationHook } from '@vercel/commerce/utils/types'
import type { AddItemHook } from '@vercel/commerce/types/customer/address'
import { CommerceError } from '@vercel/commerce/utils/errors'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import {
ActiveOrderResult,
MutationSetOrderShippingAddressArgs
} from '../../../schema'
import { setOrderShippingAddress } from '../../utils/mutations/set-order-shipping-address'
import { normalizeAddress } from '../../utils/normalize'
import useAddresses from './use-addresses'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
query: '',
query: setOrderShippingAddress,
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
async fetcher({ input: item, options, fetch }) {
const variables: MutationSetOrderShippingAddressArgs = {
input: {
fullName: `${item.firstName || ''} ${item.lastName || ''}`,
company: item.company,
streetLine1: item.streetNumber,
streetLine2: item.apartments,
postalCode: item.zipCode,
city: item.city,
// TODO: Since country is statically coming as a HongKong
countryCode: 'JP',
},
}
const data = await fetch<ActiveOrderResult>({
...options,
variables,
})
if (data.__typename === 'Order') {
return normalizeAddress(data)
} else if (data.__typename === 'NoActiveOrderError') {
throw new CommerceError({
code: data.errorCode,
message: data.message,
})
}
},
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,36 @@
import type { GetAddressesHook } from '@vercel/commerce/types/customer/address'
import { useMemo } from 'react'
import { SWRHook } from '@vercel/commerce/utils/types'
import useAddresses, {
UseAddresses,
} from '@vercel/commerce/customer/address/use-addresses'
import { activeCustomerQuery } from '../../utils/queries/active-customer-query'
export default useAddresses as UseAddresses<typeof handler>
export const handler: SWRHook<GetAddressesHook> = {
fetchOptions: {
query: activeCustomerQuery,
},
useHook: ({ useData }) =>
function useHook(input) {
console.log(input, 'hello')
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

@ -1,17 +1,88 @@
import useAddItem, {
UseAddItem,
} from '@vercel/commerce/customer/card/use-add-item'
import useCards from '@vercel/commerce/customer/card/use-cards'
import { AddItemHook } from '@vercel/commerce/types/customer/card'
import { CommerceError } from '@vercel/commerce/utils/errors'
import { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import { setOrderBillingAddress } from '../../utils/mutations/set-order-billing-address'
import {
ActiveOrderResult,
EligibleShippingMethodsQuery,
MutationSetOrderBillingAddressArgs,
SetOrderShippingMethodResult,
} from '../../../schema'
import { eligibleShippingMethods } from '../../utils/queries/eligible-shipping-methods'
import { setOrderShippingMethod } from '../../utils/mutations/set-order-shipping-method'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
query: '',
query: setOrderBillingAddress,
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
async fetcher({ input: item, options, fetch }) {
const variables: MutationSetOrderBillingAddressArgs = {
input: {
fullName: `${item.firstName || ''} ${item.lastName || ''}`,
company: item.company,
streetLine1: item.streetNumber,
// TODO: Because of TS error apartments
// streetLine2: item.apartments,
postalCode: item.zipCode,
city: item.city,
// TODO: Since country is statically coming as a HongKong
countryCode: 'JP',
},
}
const data = await fetch<ActiveOrderResult>({
...options,
variables,
})
const eligibleMethods = await fetch<EligibleShippingMethodsQuery>({
...options,
query: eligibleShippingMethods,
})
const shippingMethodId =
eligibleMethods?.['eligibleShippingMethods']?.[0].id
if (shippingMethodId) {
await fetch<SetOrderShippingMethodResult>({
...options,
query: setOrderShippingMethod,
variables: {
shippingMethodId,
},
})
}
if (data.__typename === 'Order') {
// TODO: Not sure what card we should return
return {
id: '',
mask: '',
provider: '',
}
} else if (data.__typename === 'NoActiveOrderError') {
throw new CommerceError({
code: data.errorCode,
message: data.message,
})
}
},
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 '@vercel/commerce/types/customer/card'
import { useMemo } from 'react'
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]
)
},
}

View File

@ -1,5 +1,6 @@
import { Fetcher } from '@vercel/commerce/utils/types'
import { FetcherError } from '@vercel/commerce/utils/errors'
import { getToken, setToken } from './utils/token'
async function getText(res: Response) {
try {
@ -35,12 +36,21 @@ export const fetcher: Fetcher = async ({
const hasBody = Boolean(variables || query)
const body = hasBody ? JSON.stringify({ query, variables }) : undefined
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
const token = getToken()
const res = await fetch(shopApiUrl, {
method,
body,
headers,
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
credentials: 'include',
})
// We're using Vendure Bearer token method
const authToken = res.headers.get('vendure-auth-token')
if (authToken != null) {
setToken(authToken)
}
if (res.ok) {
const { data, errors } = await res.json()
if (errors) {

View File

@ -7,6 +7,12 @@ 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 useAddresses } from './customer/address/use-addresses'
import { handler as useAddAddressItem } from './customer/address/use-add-item'
import { fetcher } from './fetcher'
export const vendureProvider = {
@ -14,8 +20,22 @@ export const vendureProvider = {
cartCookie: 'session',
fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer },
customer: {
useCustomer,
card: {
useCards,
useAddItem: useAddCardItem,
},
address: {
useAddresses,
useAddItem: useAddAddressItem,
},
},
products: { useSearch },
checkout: {
useCheckout,
useSubmitCheckout,
},
auth: { useLogin, useLogout, useSignup },
}

View File

@ -1,3 +1,5 @@
import { orderAddressFragment } from './order-address-fragment'
export const cartFragment = /* GraphQL */ `
fragment Cart on Order {
id
@ -12,6 +14,12 @@ export const cartFragment = /* GraphQL */ `
customer {
id
}
shippingAddress {
...OrderAddressFragment
}
billingAddress {
...OrderAddressFragment
}
lines {
id
quantity
@ -41,4 +49,5 @@ export const cartFragment = /* GraphQL */ `
}
}
}
${orderAddressFragment}
`

View File

@ -0,0 +1,14 @@
export const orderAddressFragment = /* GraphQL */ `
fragment OrderAddressFragment on OrderAddress {
fullName
company
streetLine1
streetLine2
city
province
postalCode
country
countryCode
phoneNumber
}
`

View File

@ -0,0 +1,34 @@
import { cartFragment } from '../fragments/cart-fragment'
export const addPaymentToOrder = /* GraphQL */ `
mutation addPaymentToOrder($input: PaymentInput!) {
addPaymentToOrder(input: $input) {
...Cart
... on OrderPaymentStateError {
errorCode
message
}
... on IneligiblePaymentMethodError {
errorCode
message
}
... on PaymentFailedError {
errorCode
message
}
... on PaymentDeclinedError {
errorCode
message
}
... on OrderStateTransitionError {
errorCode
message
}
... on NoActiveOrderError {
errorCode
message
}
}
}
${cartFragment}
`

View File

@ -0,0 +1,14 @@
import { cartFragment } from '../../utils/fragments/cart-fragment'
export const setOrderBillingAddress = /* GraphQL */ `
mutation setOrderBillingAddress ($input: CreateAddressInput!){
setOrderBillingAddress(input: $input) {
...Cart
... on NoActiveOrderError {
errorCode
message
}
}
}
${cartFragment}
`

View File

@ -0,0 +1,10 @@
import { cartFragment } from '../fragments/cart-fragment'
export const setCustomerForOrder = /* GraphQL */ `
mutation setCustomerForOrder($input: CreateCustomerInput!) {
setCustomerForOrder(input: $input) {
...Cart
}
}
${cartFragment}
`

View File

@ -0,0 +1,14 @@
import { cartFragment } from '../../utils/fragments/cart-fragment'
export const setOrderShippingAddress = /* GraphQL */ `
mutation setOrderShippingAddress($input: CreateAddressInput!) {
setOrderShippingAddress(input: $input) {
...Cart
... on NoActiveOrderError {
errorCode
message
}
}
}
${cartFragment}
`

View File

@ -0,0 +1,22 @@
import { cartFragment } from '../../utils/fragments/cart-fragment'
export const setOrderShippingMethod = /* GraphQL */ `
mutation setOrderShippingMethod($shippingMethodId: ID!) {
setOrderShippingMethod(shippingMethodId: $shippingMethodId) {
...Cart
... on OrderModificationError {
errorCode
message
}
... on IneligibleShippingMethodError {
errorCode
message
}
... on NoActiveOrderError {
errorCode
message
}
}
}
${cartFragment}
`

View File

@ -0,0 +1,14 @@
import { cartFragment } from '../fragments/cart-fragment'
export const transitionOrderToState = /* GraphQL */ `
mutation transitionOrderToState($state: String!) {
transitionOrderToState(state: $state) {
...Cart
... on OrderStateTransitionError {
errorCode
message
}
}
}
${cartFragment}
`

View File

@ -1,6 +1,7 @@
import { Product } from '@vercel/commerce/types/product'
import { Cart } from '@vercel/commerce/types/cart'
import { CartFragment, SearchResultFragment } from '../../schema'
import { CustomerAddressTypes } from '@vercel/commerce/types/customer/address'
export function normalizeSearchResult(item: SearchResultFragment): Product {
return {
@ -26,7 +27,10 @@ export function normalizeSearchResult(item: SearchResultFragment): Product {
}
}
export function normalizeCart(order: CartFragment): Cart {
export function normalizeCart(order: CartFragment): Cart & {
hasShipping: boolean
hasPayment: boolean
} {
return {
id: order.id.toString(),
createdAt: order.createdAt,
@ -58,5 +62,17 @@ export function normalizeCart(order: CartFragment): Cart {
requiresShipping: true,
},
})),
hasShipping: !!order.shippingAddress?.fullName,
hasPayment: !!order.billingAddress?.fullName,
}
}
export function normalizeAddress(
order: CartFragment
): CustomerAddressTypes['address'] {
return {
// TODO: Not sure what should return.
id: '',
mask: '',
}
}

View File

@ -0,0 +1,8 @@
export const eligiblePaymentMethods = /* GraphQL */ `
query eligiblePaymentMethods {
eligiblePaymentMethods {
id
code
}
}
`

View File

@ -0,0 +1,8 @@
export const eligibleShippingMethods = /* GraphQL */ `
query eligibleShippingMethods {
eligibleShippingMethods {
id
code
}
}
`

View File

@ -0,0 +1,9 @@
import { VENDURE_TOKEN } from '../const'
export const getToken = () => {
return localStorage.getItem(VENDURE_TOKEN)
}
export const setToken = (token: string) => {
localStorage.setItem(VENDURE_TOKEN, token)
}