From 30744c5a6c7800626718384fea4a880009b2e2ce Mon Sep 17 00:00:00 2001 From: goncy Date: Tue, 14 Sep 2021 15:37:17 -0300 Subject: [PATCH] Implements custom checkout along with ordercloud provider --- .env.template | 1 + .../CheckoutSidebarView.tsx | 46 ++--- .../PaymentMethodView/PaymentMethodView.tsx | 162 +++++++++++------- .../checkout/PaymentWidget/PaymentWidget.tsx | 13 +- .../checkout/ShippingView/ShippingView.tsx | 154 ++++++++++------- .../ShippingWidget/ShippingWidget.tsx | 11 +- framework/commerce/api/endpoints/checkout.ts | 24 ++- .../api/endpoints/customer/address.ts | 63 +++++++ .../commerce/api/endpoints/customer/card.ts | 63 +++++++ .../{customer.ts => customer/index.ts} | 8 +- framework/commerce/api/index.ts | 4 + framework/commerce/checkout/use-checkout.tsx | 19 ++ .../commerce/checkout/use-get-checkout.ts | 34 ++++ .../commerce/checkout/use-submit-checkout.tsx | 21 +++ .../customer/address/use-add-item.tsx | 21 +++ .../customer/address/use-addresses.tsx | 32 ++++ .../customer/address/use-remove-item.tsx | 20 +++ .../customer/address/use-update-item.tsx | 20 +++ .../commerce/customer/card/use-add-item.tsx | 21 +++ .../commerce/customer/card/use-cards.tsx | 32 ++++ .../customer/card/use-remove-item.tsx | 20 +++ .../customer/card/use-update-item.tsx | 20 +++ framework/commerce/index.tsx | 18 ++ framework/commerce/types/checkout.ts | 50 +++++- framework/commerce/types/customer/address.ts | 93 ++++++++++ framework/commerce/types/customer/card.ts | 96 +++++++++++ .../types/{customer.ts => customer/index.ts} | 3 + framework/ordercloud/.env.template | 2 + .../api/endpoints/checkout/get-checkout.ts | 35 ++++ .../api/endpoints/checkout/index.ts | 24 ++- .../api/endpoints/checkout/submit-checkout.ts | 23 +++ .../endpoints/customer/address/add-item.ts | 49 ++++++ .../customer/address/get-addresses.ts | 9 + .../api/endpoints/customer/address/index.ts | 27 +++ .../endpoints/customer/address/remove-item.ts | 9 + .../endpoints/customer/address/update-item.ts | 9 + .../api/endpoints/customer/card/add-item.ts | 61 +++++++ .../api/endpoints/customer/card/get-cards.ts | 9 + .../api/endpoints/customer/card/index.ts | 27 +++ .../endpoints/customer/card/remove-item.ts | 9 + .../endpoints/customer/card/update-item.ts | 9 + framework/ordercloud/api/utils/fetch-rest.ts | 2 + framework/ordercloud/checkout/index.ts | 2 + .../ordercloud/checkout/use-checkout.tsx | 6 + .../ordercloud/checkout/use-get-checkout.tsx | 33 ++++ .../checkout/use-submit-checkout.tsx | 36 ++++ framework/ordercloud/commerce.config.json | 2 +- .../ordercloud/customer/address/index.ts | 4 + .../customer/address/use-add-item.tsx | 48 ++++++ .../customer/address/use-addresses.tsx | 33 ++++ .../customer/address/use-remove-item.tsx | 60 +++++++ .../customer/address/use-update-item.tsx | 93 ++++++++++ framework/ordercloud/customer/card/index.ts | 4 + .../ordercloud/customer/card/use-add-item.tsx | 48 ++++++ .../ordercloud/customer/card/use-cards.tsx | 33 ++++ .../customer/card/use-remove-item.tsx | 60 +++++++ .../customer/card/use-update-item.tsx | 93 ++++++++++ framework/ordercloud/provider.ts | 48 +++++- framework/ordercloud/types/checkout.ts | 4 + .../ordercloud/types/customer/address.ts | 31 ++++ framework/ordercloud/types/customer/card.ts | 16 ++ package.json | 1 + pages/api/customer/address.ts | 4 + pages/api/customer/card.ts | 4 + pages/api/{customer.ts => customer/index.ts} | 0 tsconfig.json | 4 +- yarn.lock | 20 +++ 67 files changed, 1886 insertions(+), 174 deletions(-) create mode 100644 framework/commerce/api/endpoints/customer/address.ts create mode 100644 framework/commerce/api/endpoints/customer/card.ts rename framework/commerce/api/endpoints/{customer.ts => customer/index.ts} (75%) create mode 100644 framework/commerce/checkout/use-checkout.tsx create mode 100644 framework/commerce/checkout/use-get-checkout.ts create mode 100644 framework/commerce/checkout/use-submit-checkout.tsx create mode 100644 framework/commerce/customer/address/use-add-item.tsx create mode 100644 framework/commerce/customer/address/use-addresses.tsx create mode 100644 framework/commerce/customer/address/use-remove-item.tsx create mode 100644 framework/commerce/customer/address/use-update-item.tsx create mode 100644 framework/commerce/customer/card/use-add-item.tsx create mode 100644 framework/commerce/customer/card/use-cards.tsx create mode 100644 framework/commerce/customer/card/use-remove-item.tsx create mode 100644 framework/commerce/customer/card/use-update-item.tsx create mode 100644 framework/commerce/types/customer/address.ts create mode 100644 framework/commerce/types/customer/card.ts rename framework/commerce/types/{customer.ts => customer/index.ts} (87%) create mode 100644 framework/ordercloud/api/endpoints/checkout/get-checkout.ts create mode 100644 framework/ordercloud/api/endpoints/checkout/submit-checkout.ts create mode 100644 framework/ordercloud/api/endpoints/customer/address/add-item.ts create mode 100644 framework/ordercloud/api/endpoints/customer/address/get-addresses.ts create mode 100644 framework/ordercloud/api/endpoints/customer/address/index.ts create mode 100644 framework/ordercloud/api/endpoints/customer/address/remove-item.ts create mode 100644 framework/ordercloud/api/endpoints/customer/address/update-item.ts create mode 100644 framework/ordercloud/api/endpoints/customer/card/add-item.ts create mode 100644 framework/ordercloud/api/endpoints/customer/card/get-cards.ts create mode 100644 framework/ordercloud/api/endpoints/customer/card/index.ts create mode 100644 framework/ordercloud/api/endpoints/customer/card/remove-item.ts create mode 100644 framework/ordercloud/api/endpoints/customer/card/update-item.ts create mode 100644 framework/ordercloud/checkout/index.ts create mode 100644 framework/ordercloud/checkout/use-checkout.tsx create mode 100644 framework/ordercloud/checkout/use-get-checkout.tsx create mode 100644 framework/ordercloud/checkout/use-submit-checkout.tsx create mode 100644 framework/ordercloud/customer/address/index.ts create mode 100644 framework/ordercloud/customer/address/use-add-item.tsx create mode 100644 framework/ordercloud/customer/address/use-addresses.tsx create mode 100644 framework/ordercloud/customer/address/use-remove-item.tsx create mode 100644 framework/ordercloud/customer/address/use-update-item.tsx create mode 100644 framework/ordercloud/customer/card/index.ts create mode 100644 framework/ordercloud/customer/card/use-add-item.tsx create mode 100644 framework/ordercloud/customer/card/use-cards.tsx create mode 100644 framework/ordercloud/customer/card/use-remove-item.tsx create mode 100644 framework/ordercloud/customer/card/use-update-item.tsx create mode 100644 framework/ordercloud/types/checkout.ts create mode 100644 framework/ordercloud/types/customer/address.ts create mode 100644 framework/ordercloud/types/customer/card.ts create mode 100644 pages/api/customer/address.ts create mode 100644 pages/api/customer/card.ts rename pages/api/{customer.ts => customer/index.ts} (100%) diff --git a/.env.template b/.env.template index be91ec165..92de521fa 100644 --- a/.env.template +++ b/.env.template @@ -25,3 +25,4 @@ NEXT_PUBLIC_VENDURE_SHOP_API_URL= NEXT_PUBLIC_VENDURE_LOCAL_URL= NEXT_PUBLIC_ORDERCLOUD_CLIENT_ID= +STRIPE_SECRET= diff --git a/components/checkout/CheckoutSidebarView/CheckoutSidebarView.tsx b/components/checkout/CheckoutSidebarView/CheckoutSidebarView.tsx index fb562e7af..3be28ddf7 100644 --- a/components/checkout/CheckoutSidebarView/CheckoutSidebarView.tsx +++ b/components/checkout/CheckoutSidebarView/CheckoutSidebarView.tsx @@ -1,4 +1,3 @@ -import cn from 'classnames' import Link from 'next/link' import { FC } from 'react' import CartItem from '@components/cart/CartItem' @@ -7,24 +6,34 @@ import { useUI } from '@components/ui/context' import useCart from '@framework/cart/use-cart' import usePrice from '@framework/product/use-price' import ShippingWidget from '../ShippingWidget' +import useCheckout from '@framework/checkout/use-checkout' import PaymentWidget from '../PaymentWidget' import SidebarLayout from '@components/common/SidebarLayout' import s from './CheckoutSidebarView.module.css' const CheckoutSidebarView: FC = () => { - const { setSidebarView } = useUI() - const { data } = useCart() + const { setSidebarView, closeSidebar } = useUI() + const { data: cartData } = useCart() + const { data: checkoutData, submit: onCheckout } = useCheckout(); + + async function handleSubmit(event: React.ChangeEvent) { + event.preventDefault(); + + await onCheckout(); + + closeSidebar(); + } const { price: subTotal } = usePrice( - data && { - amount: Number(data.subtotalPrice), - currencyCode: data.currency.code, + cartData && { + amount: Number(cartData.subtotalPrice), + currencyCode: cartData.currency.code, } ) const { price: total } = usePrice( - data && { - amount: Number(data.totalPrice), - currencyCode: data.currency.code, + cartData && { + amount: Number(cartData.totalPrice), + currencyCode: cartData.currency.code, } ) @@ -38,22 +47,22 @@ const CheckoutSidebarView: FC = () => { Checkout - setSidebarView('PAYMENT_VIEW')} /> - setSidebarView('SHIPPING_VIEW')} /> + setSidebarView('PAYMENT_VIEW')} /> + setSidebarView('SHIPPING_VIEW')} />
    - {data!.lineItems.map((item: any) => ( + {cartData!.lineItems.map((item: any) => ( ))}
-
+
  • Subtotal @@ -74,14 +83,11 @@ const CheckoutSidebarView: FC = () => {
{/* Once data is correcly filled */} - {/* */} -
- + ) } diff --git a/components/checkout/PaymentMethodView/PaymentMethodView.tsx b/components/checkout/PaymentMethodView/PaymentMethodView.tsx index a5f6f4b51..93486a694 100644 --- a/components/checkout/PaymentMethodView/PaymentMethodView.tsx +++ b/components/checkout/PaymentMethodView/PaymentMethodView.tsx @@ -1,83 +1,123 @@ import { FC } from 'react' import cn from 'classnames' + +import useCheckout from '@framework/checkout/use-checkout' import { Button, Text } from '@components/ui' import { useUI } from '@components/ui/context' -import s from './PaymentMethodView.module.css' import SidebarLayout from '@components/common/SidebarLayout' +import s from './PaymentMethodView.module.css' + +interface Form extends HTMLFormElement { + cardHolder: HTMLInputElement + cardNumber: HTMLInputElement + cardExpireDate: HTMLInputElement + cardCvc: HTMLInputElement + firstName: HTMLInputElement + lastName: HTMLInputElement + company: HTMLInputElement + streetNumber: HTMLInputElement + zipCode: HTMLInputElement + city: HTMLInputElement + country: HTMLSelectElement +} + const PaymentMethodView: FC = () => { const { setSidebarView } = useUI() + const [, {addPayment}] = useCheckout() + + async function handleSubmit(event: React.ChangeEvent
) { + event.preventDefault(); + + await addPayment({ + cardHolder: event.target.cardHolder.value, + cardNumber: event.target.cardNumber.value, + cardExpireDate: event.target.cardExpireDate.value, + cardCvc: event.target.cardCvc.value, + firstName: event.target.firstName.value, + lastName: event.target.lastName.value, + company: event.target.company.value, + streetNumber: event.target.streetNumber.value, + zipCode: event.target.zipCode.value, + city: event.target.city.value, + country: event.target.country.value + }); + + setSidebarView('CHECKOUT_VIEW') + } return ( - setSidebarView('CHECKOUT_VIEW')}> -
- Payment Method -
-
- - -
-
-
- - + + setSidebarView('CHECKOUT_VIEW')}> +
+ Payment Method +
+
+ +
-
- - +
+
+ + +
+
+ + +
+
+ + +
-
- - +
+
+
+ + +
+
+ + +
-
-
-
-
- - +
+ +
-
- - +
+ +
-
-
- - -
-
- - -
-
- - -
-
-
- - +
+ +
-
- - +
+
+ + +
+
+ + +
+
+
+ +
-
-
- -
-
-
- -
- +
+ +
+ + ) } diff --git a/components/checkout/PaymentWidget/PaymentWidget.tsx b/components/checkout/PaymentWidget/PaymentWidget.tsx index e1892934e..09afe4158 100644 --- a/components/checkout/PaymentWidget/PaymentWidget.tsx +++ b/components/checkout/PaymentWidget/PaymentWidget.tsx @@ -1,14 +1,15 @@ import { FC } from 'react' import s from './PaymentWidget.module.css' -import { ChevronRight, CreditCard } from '@components/icons' +import { ChevronRight, CreditCard, Check } from '@components/icons' interface ComponentProps { - onClick?: () => any + onClick?: () => any; + isValid?: boolean; } -const PaymentWidget: FC = ({ onClick }) => { - /* Shipping Address - Only available with checkout set to true - +const PaymentWidget: FC = ({ onClick, isValid }) => { + /* Shipping Address + Only available with checkout set to true - This means that the provider does offer checkout functionality. */ return (
@@ -20,7 +21,7 @@ const PaymentWidget: FC = ({ onClick }) => { {/* VISA #### #### #### 2345 */}
- + {isValid ? : }
) diff --git a/components/checkout/ShippingView/ShippingView.tsx b/components/checkout/ShippingView/ShippingView.tsx index 1d03a2aac..6cd6ae662 100644 --- a/components/checkout/ShippingView/ShippingView.tsx +++ b/components/checkout/ShippingView/ShippingView.tsx @@ -1,77 +1,115 @@ import { FC } from 'react' import cn from 'classnames' -import s from './ShippingView.module.css' + import Button from '@components/ui/Button' import { useUI } from '@components/ui/context' import SidebarLayout from '@components/common/SidebarLayout' +import useCheckout from '@framework/checkout/use-checkout' + +import s from './ShippingView.module.css' + +interface Form extends HTMLFormElement { + cardHolder: HTMLInputElement + cardNumber: HTMLInputElement + cardExpireDate: HTMLInputElement + cardCvc: HTMLInputElement + firstName: HTMLInputElement + lastName: HTMLInputElement + company: HTMLInputElement + streetNumber: HTMLInputElement + zipCode: HTMLInputElement + city: HTMLInputElement + country: HTMLSelectElement +} const PaymentMethodView: FC = () => { const { setSidebarView } = useUI() + const [, {addAddress}] = useCheckout() + + async function handleSubmit(event: React.ChangeEvent
) { + event.preventDefault(); + + await addAddress({ + type: event.target.type.value, + firstName: event.target.firstName.value, + lastName: event.target.lastName.value, + company: event.target.company.value, + streetNumber: event.target.streetNumber.value, + apartments: event.target.streetNumber.value, + zipCode: event.target.zipCode.value, + city: event.target.city.value, + country: event.target.country.value + }); + + setSidebarView('CHECKOUT_VIEW') + } return ( - setSidebarView('CHECKOUT_VIEW')}> -
-

- Shipping -

-
-
- - Same as billing address -
-
- - - Use a different shipping address - -
-
-
-
- - + + setSidebarView('CHECKOUT_VIEW')}> +
+

+ Shipping +

+
+
+ + Same as billing address
-
- - +
+ + + Use a different shipping address +
-
-
- - -
-
- - -
-
- - -
-
-
- - +
+
+
+ + +
+
+ + +
-
- - +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
-
-
- -
-
-
- -
- +
+ +
+ + ) } diff --git a/components/checkout/ShippingWidget/ShippingWidget.tsx b/components/checkout/ShippingWidget/ShippingWidget.tsx index b072178b0..b7684a1b1 100644 --- a/components/checkout/ShippingWidget/ShippingWidget.tsx +++ b/components/checkout/ShippingWidget/ShippingWidget.tsx @@ -1,15 +1,16 @@ import { FC } from 'react' import s from './ShippingWidget.module.css' -import { ChevronRight, MapPin } from '@components/icons' +import { ChevronRight, MapPin, Check } from '@components/icons' import cn from 'classnames' interface ComponentProps { onClick?: () => any + isValid?: boolean; } -const ShippingWidget: FC = ({ onClick }) => { - /* Shipping Address - Only available with checkout set to true - +const ShippingWidget: FC = ({ onClick, isValid }) => { + /* Shipping Address + Only available with checkout set to true - This means that the provider does offer checkout functionality. */ return (
@@ -24,7 +25,7 @@ const ShippingWidget: FC = ({ onClick }) => { */}
- + {isValid ? : }
) diff --git a/framework/commerce/api/endpoints/checkout.ts b/framework/commerce/api/endpoints/checkout.ts index b39239a6a..09ebff621 100644 --- a/framework/commerce/api/endpoints/checkout.ts +++ b/framework/commerce/api/endpoints/checkout.ts @@ -1,25 +1,39 @@ import type { CheckoutSchema } from '../../types/checkout' +import type { GetAPISchema } from '..' + import { CommerceAPIError } from '../utils/errors' import isAllowedOperation from '../utils/is-allowed-operation' -import type { GetAPISchema } from '..' const checkoutEndpoint: GetAPISchema< any, CheckoutSchema >['endpoint']['handler'] = async (ctx) => { - const { req, res, handlers } = ctx + const { req, res, handlers, config } = ctx if ( !isAllowedOperation(req, res, { - GET: handlers['checkout'], + GET: handlers['getCheckout'], + POST: handlers['submitCheckout'], }) ) { return } + const { cookies } = req + const cartId = cookies[config.cartCookie] + try { - const body = null - return await handlers['checkout']({ ...ctx, body }) + // Create checkout + if (req.method === 'GET') { + const body = { ...req.body, cartId } + return await handlers['getCheckout']({ ...ctx, body }) + } + + // Create checkout + if (req.method === 'POST') { + const body = { ...req.body, cartId } + return await handlers['submitCheckout']({ ...ctx, body }) + } } catch (error) { console.error(error) diff --git a/framework/commerce/api/endpoints/customer/address.ts b/framework/commerce/api/endpoints/customer/address.ts new file mode 100644 index 000000000..2ca02bfcd --- /dev/null +++ b/framework/commerce/api/endpoints/customer/address.ts @@ -0,0 +1,63 @@ +import type { CustomerAddressSchema } from '../../../types/customer/address' +import type { GetAPISchema } from '../..' + +import { CommerceAPIError } from '../../utils/errors' +import isAllowedOperation from '../../utils/is-allowed-operation' + +const customerShippingEndpoint: GetAPISchema< + any, + CustomerAddressSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers, config } = ctx + + if ( + !isAllowedOperation(req, res, { + GET: handlers['getAddresses'], + POST: handlers['addItem'], + PUT: handlers['updateItem'], + DELETE: handlers['removeItem'], + }) + ) { + return + } + + const { cookies } = req + const cartId = cookies[config.cartCookie] + + try { + // Return current cart info + if (req.method === 'GET') { + const body = { cartId } + return await handlers['getAddresses']({ ...ctx, body }) + } + + // Create or add an item to the cart + if (req.method === 'POST') { + const body = { ...req.body, cartId } + return await handlers['addItem']({ ...ctx, body }) + } + + // Update item in cart + if (req.method === 'PUT') { + const body = { ...req.body, cartId } + return await handlers['updateItem']({ ...ctx, body }) + } + + // Remove an item from the cart + if (req.method === 'DELETE') { + const body = { ...req.body, cartId } + return await handlers['removeItem']({ ...ctx, body }) + } + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default customerShippingEndpoint diff --git a/framework/commerce/api/endpoints/customer/card.ts b/framework/commerce/api/endpoints/customer/card.ts new file mode 100644 index 000000000..78daf396e --- /dev/null +++ b/framework/commerce/api/endpoints/customer/card.ts @@ -0,0 +1,63 @@ +import type { CustomerCardSchema } from '../../../types/customer/card' +import type { GetAPISchema } from '../..' + +import { CommerceAPIError } from '../../utils/errors' +import isAllowedOperation from '../../utils/is-allowed-operation' + +const customerCardEndpoint: GetAPISchema< + any, + CustomerCardSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers, config } = ctx + + if ( + !isAllowedOperation(req, res, { + GET: handlers['getCards'], + POST: handlers['addItem'], + PUT: handlers['updateItem'], + DELETE: handlers['removeItem'], + }) + ) { + return + } + + const { cookies } = req + const cartId = cookies[config.cartCookie] + + try { + // Create or add a payment + if (req.method === 'GET') { + const body = { ...req.body } + return await handlers['getCards']({ ...ctx, body }) + } + + // Create or add an item to the cart + if (req.method === 'POST') { + const body = { ...req.body, cartId } + return await handlers['addItem']({ ...ctx, body }) + } + + // Update item in cart + if (req.method === 'PUT') { + const body = { ...req.body, cartId } + return await handlers['updateItem']({ ...ctx, body }) + } + + // Remove an item from the cart + if (req.method === 'DELETE') { + const body = { ...req.body, cartId } + return await handlers['removeItem']({ ...ctx, body }) + } + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default customerCardEndpoint diff --git a/framework/commerce/api/endpoints/customer.ts b/framework/commerce/api/endpoints/customer/index.ts similarity index 75% rename from framework/commerce/api/endpoints/customer.ts rename to framework/commerce/api/endpoints/customer/index.ts index 6372c494f..ee8b9ba00 100644 --- a/framework/commerce/api/endpoints/customer.ts +++ b/framework/commerce/api/endpoints/customer/index.ts @@ -1,7 +1,7 @@ -import type { CustomerSchema } from '../../types/customer' -import { CommerceAPIError } from '../utils/errors' -import isAllowedOperation from '../utils/is-allowed-operation' -import type { GetAPISchema } from '..' +import type { CustomerSchema } from '../../../types/customer' +import { CommerceAPIError } from '../../utils/errors' +import isAllowedOperation from '../../utils/is-allowed-operation' +import type { GetAPISchema } from '../..' const customerEndpoint: GetAPISchema< any, diff --git a/framework/commerce/api/index.ts b/framework/commerce/api/index.ts index 32fe8cf80..716c11ed5 100644 --- a/framework/commerce/api/index.ts +++ b/framework/commerce/api/index.ts @@ -9,6 +9,8 @@ import type { SignupSchema } from '../types/signup' import type { ProductsSchema } from '../types/product' import type { WishlistSchema } from '../types/wishlist' import type { CheckoutSchema } from '../types/checkout' +import type { CustomerCardSchema } from '../types/customer/card' +import type { CustomerAddressSchema } from '../types/customer/address' import { defaultOperations, OPERATIONS, @@ -25,6 +27,8 @@ export type APISchemas = | ProductsSchema | WishlistSchema | CheckoutSchema + | CustomerCardSchema + | CustomerAddressSchema export type GetAPISchema< C extends CommerceAPI, diff --git a/framework/commerce/checkout/use-checkout.tsx b/framework/commerce/checkout/use-checkout.tsx new file mode 100644 index 000000000..09c4e6243 --- /dev/null +++ b/framework/commerce/checkout/use-checkout.tsx @@ -0,0 +1,19 @@ +import useGetCheckout from "./use-get-checkout" +import useSubmitCheckout from "./use-submit-checkout"; +import useAddPayment from "../customer/card/use-add-item" +import useAddShipping from "../customer/address/use-add-item" + +export type UseCheckout = any; + +function useCheckout(): UseCheckout { + const state = useGetCheckout() + const actions = { + submit: useSubmitCheckout(), + addPayment: useAddPayment(), + addShipping: useAddShipping() + } + + return {...state, ...actions} +} + +export default useCheckout diff --git a/framework/commerce/checkout/use-get-checkout.ts b/framework/commerce/checkout/use-get-checkout.ts new file mode 100644 index 000000000..df11a8052 --- /dev/null +++ b/framework/commerce/checkout/use-get-checkout.ts @@ -0,0 +1,34 @@ +import type { SWRHook, HookFetcherFn } from '../utils/types' +import type { GetCheckoutHook } from '../types/checkout' + +import Cookies from 'js-cookie' + +import { useHook, useSWRHook } from '../utils/use-hook' +import { Provider, useCommerce } from '..' + +export type UseGetCheckout< + H extends SWRHook> = SWRHook +> = ReturnType + +export const fetcher: HookFetcherFn = async ({ + options, + input: { cartId }, + fetch, +}) => { + return cartId ? await fetch(options) : null +} + +const fn = (provider: Provider) => provider.checkout?.useGetCheckout! + +const useGetCheckout: UseGetCheckout = (input) => { + const hook = useHook(fn) + const { cartCookie } = useCommerce() + const fetcherFn = hook.fetcher ?? fetcher + const wrapper: typeof fetcher = (context) => { + context.input.cartId = Cookies.get(cartCookie) + return fetcherFn(context) + } + return useSWRHook({ ...hook, fetcher: wrapper })(input) +} + +export default useGetCheckout diff --git a/framework/commerce/checkout/use-submit-checkout.tsx b/framework/commerce/checkout/use-submit-checkout.tsx new file mode 100644 index 000000000..da609b906 --- /dev/null +++ b/framework/commerce/checkout/use-submit-checkout.tsx @@ -0,0 +1,21 @@ +import type { HookFetcherFn, MutationHook } from '../utils/types' +import type { SubmitCheckoutHook } from '../types/checkout' +import type { Provider } from '..' + +import { useHook, useMutationHook } from '../utils/use-hook' +import { mutationFetcher } from '../utils/default-fetcher' + +export type UseSubmitCheckout< + H extends MutationHook> = MutationHook +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider.checkout?.useSubmitCheckout! + +const useSubmitCheckout: UseSubmitCheckout = (...args) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(...args) +} + +export default useSubmitCheckout diff --git a/framework/commerce/customer/address/use-add-item.tsx b/framework/commerce/customer/address/use-add-item.tsx new file mode 100644 index 000000000..94c45142e --- /dev/null +++ b/framework/commerce/customer/address/use-add-item.tsx @@ -0,0 +1,21 @@ +import type { HookFetcherFn, MutationHook } from '../../utils/types' +import type { AddItemHook } from '../../types/customer/address' +import type { Provider } from '../..' + +import { useHook, useMutationHook } from '../../utils/use-hook' +import { mutationFetcher } from '../../utils/default-fetcher' + +export type UseAddItem< + H extends MutationHook> = MutationHook +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider.customer?.address?.useAddItem! + +const useAddItem: UseAddItem = (...args) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(...args) +} + +export default useAddItem diff --git a/framework/commerce/customer/address/use-addresses.tsx b/framework/commerce/customer/address/use-addresses.tsx new file mode 100644 index 000000000..2f49b8de7 --- /dev/null +++ b/framework/commerce/customer/address/use-addresses.tsx @@ -0,0 +1,32 @@ +import Cookies from 'js-cookie' +import { useHook, useSWRHook } from '../../utils/use-hook' +import type { SWRHook, HookFetcherFn } from '../../utils/types' +import type { GetAddressesHook } from '../../types/customer/address' +import { Provider, useCommerce } from '../..' + +export type UseAddresses< + H extends SWRHook> = SWRHook +> = ReturnType + +export const fetcher: HookFetcherFn = async ({ + options, + input: { cartId }, + fetch, +}) => { + return cartId ? await fetch(options) : null +} + +const fn = (provider: Provider) => provider.customer?.address.useAddresses! + +const useAddresses: UseAddresses = (input) => { + const hook = useHook(fn) + const { cartCookie } = useCommerce() + const fetcherFn = hook.fetcher ?? fetcher + const wrapper: typeof fetcher = (context) => { + context.input.cartId = Cookies.get(cartCookie) + return fetcherFn(context) + } + return useSWRHook({ ...hook, fetcher: wrapper })(input) +} + +export default useAddresses diff --git a/framework/commerce/customer/address/use-remove-item.tsx b/framework/commerce/customer/address/use-remove-item.tsx new file mode 100644 index 000000000..885874ab9 --- /dev/null +++ b/framework/commerce/customer/address/use-remove-item.tsx @@ -0,0 +1,20 @@ +import { useHook, useMutationHook } from '../../utils/use-hook' +import { mutationFetcher } from '../../utils/default-fetcher' +import type { HookFetcherFn, MutationHook } from '../../utils/types' +import type { RemoveItemHook } from '../../types/customer/address' +import type { Provider } from '../..' + +export type UseRemoveItem< + H extends MutationHook> = MutationHook +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider.customer?.address?.useRemoveItem! + +const useRemoveItem: UseRemoveItem = (input) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(input) +} + +export default useRemoveItem diff --git a/framework/commerce/customer/address/use-update-item.tsx b/framework/commerce/customer/address/use-update-item.tsx new file mode 100644 index 000000000..5aa1d1309 --- /dev/null +++ b/framework/commerce/customer/address/use-update-item.tsx @@ -0,0 +1,20 @@ +import { useHook, useMutationHook } from '../../utils/use-hook' +import { mutationFetcher } from '../../utils/default-fetcher' +import type { HookFetcherFn, MutationHook } from '../../utils/types' +import type { UpdateItemHook } from '../../types/customer/address' +import type { Provider } from '../..' + +export type UseUpdateItem< + H extends MutationHook> = MutationHook +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider?.customer?.address?.useUpdateItem! + +const useUpdateItem: UseUpdateItem = (input) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(input) +} + +export default useUpdateItem diff --git a/framework/commerce/customer/card/use-add-item.tsx b/framework/commerce/customer/card/use-add-item.tsx new file mode 100644 index 000000000..7b4ffdb17 --- /dev/null +++ b/framework/commerce/customer/card/use-add-item.tsx @@ -0,0 +1,21 @@ +import type { HookFetcherFn, MutationHook } from '../../utils/types' +import type { AddItemHook } from '../../types/customer/card' +import type { Provider } from '../..' + +import { useHook, useMutationHook } from '../../utils/use-hook' +import { mutationFetcher } from '../../utils/default-fetcher' + +export type UseAddItem< + H extends MutationHook> = MutationHook +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider.customer?.card?.useAddItem! + +const useAddItem: UseAddItem = (...args) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(...args) +} + +export default useAddItem diff --git a/framework/commerce/customer/card/use-cards.tsx b/framework/commerce/customer/card/use-cards.tsx new file mode 100644 index 000000000..b0f36ada0 --- /dev/null +++ b/framework/commerce/customer/card/use-cards.tsx @@ -0,0 +1,32 @@ +import Cookies from 'js-cookie' +import { useHook, useSWRHook } from '../../utils/use-hook' +import type { SWRHook, HookFetcherFn } from '../../utils/types' +import type { GetCardsHook } from '../../types/customer/card' +import { Provider, useCommerce } from '../..' + +export type UseCards< + H extends SWRHook> = SWRHook +> = ReturnType + +export const fetcher: HookFetcherFn = async ({ + options, + input: { cartId }, + fetch, +}) => { + return cartId ? await fetch(options) : null +} + +const fn = (provider: Provider) => provider.customer?.card.useCards! + +const useCards: UseCards = (input) => { + const hook = useHook(fn) + const { cartCookie } = useCommerce() + const fetcherFn = hook.fetcher ?? fetcher + const wrapper: typeof fetcher = (context) => { + context.input.cartId = Cookies.get(cartCookie) + return fetcherFn(context) + } + return useSWRHook({ ...hook, fetcher: wrapper })(input) +} + +export default useCards diff --git a/framework/commerce/customer/card/use-remove-item.tsx b/framework/commerce/customer/card/use-remove-item.tsx new file mode 100644 index 000000000..030b57d68 --- /dev/null +++ b/framework/commerce/customer/card/use-remove-item.tsx @@ -0,0 +1,20 @@ +import { useHook, useMutationHook } from '../../utils/use-hook' +import { mutationFetcher } from '../../utils/default-fetcher' +import type { HookFetcherFn, MutationHook } from '../../utils/types' +import type { RemoveItemHook } from '../../types/customer/card' +import type { Provider } from '../..' + +export type UseRemoveItem< + H extends MutationHook> = MutationHook +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider.customer?.card?.useRemoveItem! + +const useRemoveItem: UseRemoveItem = (input) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(input) +} + +export default useRemoveItem diff --git a/framework/commerce/customer/card/use-update-item.tsx b/framework/commerce/customer/card/use-update-item.tsx new file mode 100644 index 000000000..650668355 --- /dev/null +++ b/framework/commerce/customer/card/use-update-item.tsx @@ -0,0 +1,20 @@ +import { useHook, useMutationHook } from '../../utils/use-hook' +import { mutationFetcher } from '../../utils/default-fetcher' +import type { HookFetcherFn, MutationHook } from '../../utils/types' +import type { UpdateItemHook } from '../../types/customer/card' +import type { Provider } from '../..' + +export type UseUpdateItem< + H extends MutationHook> = MutationHook +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider?.customer?.card?.useUpdateItem! + +const useUpdateItem: UseUpdateItem = (input) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(input) +} + +export default useUpdateItem diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx index dd740809f..e3997b01d 100644 --- a/framework/commerce/index.tsx +++ b/framework/commerce/index.tsx @@ -15,6 +15,7 @@ import type { Signup, Login, Logout, + Checkout, } from '@commerce/types' import type { Fetcher, SWRHook, MutationHook } from './utils/types' @@ -29,6 +30,11 @@ export type Provider = CommerceConfig & { useUpdateItem?: MutationHook useRemoveItem?: MutationHook } + checkout?: { + useSubmitCheckout?: MutationHook + useGetCheckout?: SWRHook + useCheckout?: any; + } wishlist?: { useWishlist?: SWRHook useAddItem?: MutationHook @@ -36,6 +42,18 @@ export type Provider = CommerceConfig & { } customer?: { useCustomer?: SWRHook + card: { + useCards?: SWRHook + useAddItem?: MutationHook + useUpdateItem?: MutationHook + useRemoveItem?: MutationHook + } + address: { + useAddresses?: SWRHook + useAddItem?: MutationHook + useUpdateItem?: MutationHook + useRemoveItem?: MutationHook + } } products?: { useSearch?: SWRHook diff --git a/framework/commerce/types/checkout.ts b/framework/commerce/types/checkout.ts index 9e3c7ecfa..8672f93b9 100644 --- a/framework/commerce/types/checkout.ts +++ b/framework/commerce/types/checkout.ts @@ -1,10 +1,48 @@ -export type CheckoutSchema = { +// Index +export type CheckoutTypes = { + card?: any; + address?: any; + checkout?: any; + hasPayment?: boolean; + hasShipping?: boolean; +} + +export type SubmitCheckoutHook = { + data: T + input?: T + fetcherInput: T + body: { item: T } + actionInput: T +} + +export type GetCheckoutHook = { + data: T['checkout'] | null + input: {} + fetcherInput: { cartId?: string } + swrState: { isEmpty: boolean } +} + +export type CheckoutHooks = { + submitCheckout: SubmitCheckoutHook + getCheckout: GetCheckoutHook +} + +export type GetCheckoutHandler = GetCheckoutHook & { + body: { cartId: string } +} + +export type SubmitCheckoutHandler = SubmitCheckoutHook & { + body: { cartId: string } +} + +export type CheckoutHandlers = { + getCheckout: GetCheckoutHandler + submitCheckout: SubmitCheckoutHandler +} + +export type CheckoutSchema = { endpoint: { options: {} - handlers: { - checkout: { - data: null - } - } + handlers: CheckoutHandlers } } diff --git a/framework/commerce/types/customer/address.ts b/framework/commerce/types/customer/address.ts new file mode 100644 index 000000000..5b6ca4b49 --- /dev/null +++ b/framework/commerce/types/customer/address.ts @@ -0,0 +1,93 @@ +export interface Address { + 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; +} + +export type CustomerAddressTypes = { + address?: Address; + fields: AddressFields; +} + +export type GetAddressesHook = { + data: T['address'] | null + input: {} + fetcherInput: { cartId?: string } + swrState: { isEmpty: boolean } +} + +export type AddItemHook = { + data: T['address'] + input?: T['fields'] + fetcherInput: T['fields'] + body: { item: T['fields'] } + actionInput: T['fields'] +} + +export type UpdateItemHook = { + data: T['address'] | null + input: { item?: T['fields']; wait?: number } + fetcherInput: { itemId: string; item: T['fields'] } + body: { itemId: string; item: T['fields'] } + actionInput: T['fields'] & { id: string } +} + +export type RemoveItemHook = { + data: T['address'] | null + input: { item?: T['fields'] } + fetcherInput: { itemId: string } + body: { itemId: string } + actionInput: { id: string } +} + +export type CustomerAddressHooks = { + getAddresses: GetAddressesHook + addItem: AddItemHook + updateItem: UpdateItemHook + removeItem: RemoveItemHook +} + +export type AddresssHandler = GetAddressesHook & { + body: { cartId?: string } +} + +export type AddItemHandler = AddItemHook & { + body: { cartId: string } +} + +export type UpdateItemHandler = + UpdateItemHook & { + data: T['address'] + body: { cartId: string } + } + +export type RemoveItemHandler = + RemoveItemHook & { + body: { cartId: string } + } + + +export type CustomerAddressHandlers = { + getAddresses: GetAddressesHook + addItem: AddItemHandler + updateItem: UpdateItemHandler + removeItem: RemoveItemHandler +} + +export type CustomerAddressSchema = { + endpoint: { + options: {} + handlers: CustomerAddressHandlers + } +} diff --git a/framework/commerce/types/customer/card.ts b/framework/commerce/types/customer/card.ts new file mode 100644 index 000000000..a8731411f --- /dev/null +++ b/framework/commerce/types/customer/card.ts @@ -0,0 +1,96 @@ +export interface Card { + 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; +} + +export type CustomerCardTypes = { + card?: Card; + fields: CardFields; +} + +export type GetCardsHook = { + data: T['card'] | null + input: {} + fetcherInput: { cartId?: string } + swrState: { isEmpty: boolean } +} + +export type AddItemHook = { + data: T['card'] + input?: T['fields'] + fetcherInput: T['fields'] + body: { item: T['fields'] } + actionInput: T['fields'] +} + +export type UpdateItemHook = { + data: T['card'] | null + input: { item?: T['fields']; wait?: number } + fetcherInput: { itemId: string; item: T['fields'] } + body: { itemId: string; item: T['fields'] } + actionInput: T['fields'] & { id: string } +} + +export type RemoveItemHook = { + data: T['card'] | null + input: { item?: T['fields'] } + fetcherInput: { itemId: string } + body: { itemId: string } + actionInput: { id: string } +} + +export type CustomerCardHooks = { + getCards: GetCardsHook + addItem: AddItemHook + updateItem: UpdateItemHook + removeItem: RemoveItemHook +} + +export type CardsHandler = GetCardsHook & { + body: { cartId?: string } +} + +export type AddItemHandler = AddItemHook & { + body: { cartId: string } +} + +export type UpdateItemHandler = + UpdateItemHook & { + data: T['card'] + body: { cartId: string } + } + +export type RemoveItemHandler = + RemoveItemHook & { + body: { cartId: string } + } + + +export type CustomerCardHandlers = { + getCards: GetCardsHook + addItem: AddItemHandler + updateItem: UpdateItemHandler + removeItem: RemoveItemHandler +} + +export type CustomerCardSchema = { + endpoint: { + options: {} + handlers: CustomerCardHandlers + } +} diff --git a/framework/commerce/types/customer.ts b/framework/commerce/types/customer/index.ts similarity index 87% rename from framework/commerce/types/customer.ts rename to framework/commerce/types/customer/index.ts index ba90acdf4..70c437c29 100644 --- a/framework/commerce/types/customer.ts +++ b/framework/commerce/types/customer/index.ts @@ -1,3 +1,6 @@ +export * as Card from "./card" +export * as Address from "./address" + // TODO: define this type export type Customer = any diff --git a/framework/ordercloud/.env.template b/framework/ordercloud/.env.template index 2f3b5ddc6..d65ef40a2 100644 --- a/framework/ordercloud/.env.template +++ b/framework/ordercloud/.env.template @@ -1 +1,3 @@ COMMERCE_PROVIDER=ordercloud + +NEXT_PUBLIC_ORDERCLOUD_CLIENT_ID= diff --git a/framework/ordercloud/api/endpoints/checkout/get-checkout.ts b/framework/ordercloud/api/endpoints/checkout/get-checkout.ts new file mode 100644 index 000000000..90e33813d --- /dev/null +++ b/framework/ordercloud/api/endpoints/checkout/get-checkout.ts @@ -0,0 +1,35 @@ +import type { CheckoutEndpoint } from '.' + +const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({ + res, + body: {cartId}, + config: { restFetch }, +}) => { + // Return an error if no item is present + if (!cartId) { + return res.status(400).json({ + data: null, + errors: [{ message: 'Missing cookie' }], + }) + } + + // Register credit card + const payments = await restFetch('GET', `/orders/Outgoing/${cartId}/payments`).then( + (response: {Items: unknown[]}) => response.Items + ) + + const address = await restFetch('GET', `/orders/Outgoing/${cartId}`).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 diff --git a/framework/ordercloud/api/endpoints/checkout/index.ts b/framework/ordercloud/api/endpoints/checkout/index.ts index 491bf0ac9..e1b8a9f1c 100644 --- a/framework/ordercloud/api/endpoints/checkout/index.ts +++ b/framework/ordercloud/api/endpoints/checkout/index.ts @@ -1 +1,23 @@ -export default function noopApi(...args: any[]): void {} +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 +export type CheckoutEndpoint = CheckoutAPI['endpoint'] + +export const handlers: CheckoutEndpoint['handlers'] = { + getCheckout, + submitCheckout, +} + +const checkoutApi = createEndpoint({ + handler: checkoutEndpoint, + handlers, +}) + +export default checkoutApi diff --git a/framework/ordercloud/api/endpoints/checkout/submit-checkout.ts b/framework/ordercloud/api/endpoints/checkout/submit-checkout.ts new file mode 100644 index 000000000..ada347f67 --- /dev/null +++ b/framework/ordercloud/api/endpoints/checkout/submit-checkout.ts @@ -0,0 +1,23 @@ +import type { CheckoutEndpoint } from '.' + +const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({ + res, + body: { cartId }, + config: { restFetch }, +}) => { + // Return an error if no item is present + if (!cartId) { + return res.status(400).json({ + data: null, + errors: [{ message: 'Missing item' }], + }) + } + + // Submit order + await restFetch('POST', `/orders/Outgoing/${cartId}/submit`, {}) + + // Return cart and errors + res.status(200).json({ data: null, errors: [] }) +} + +export default submitCheckout diff --git a/framework/ordercloud/api/endpoints/customer/address/add-item.ts b/framework/ordercloud/api/endpoints/customer/address/add-item.ts new file mode 100644 index 000000000..7e913e8a0 --- /dev/null +++ b/framework/ordercloud/api/endpoints/customer/address/add-item.ts @@ -0,0 +1,49 @@ +import type { CustomerAddressEndpoint } from '.' + +const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({ + res, + body: { item, cartId }, + config: { restFetch }, +}) => { + // 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 restFetch('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 restFetch('PATCH', `/orders/Outgoing/${cartId}`, { + ShippingAddressID: address + }) + + return res.status(200).json({ data: null, errors: [] }) +} + +export default addItem diff --git a/framework/ordercloud/api/endpoints/customer/address/get-addresses.ts b/framework/ordercloud/api/endpoints/customer/address/get-addresses.ts new file mode 100644 index 000000000..2e27591c0 --- /dev/null +++ b/framework/ordercloud/api/endpoints/customer/address/get-addresses.ts @@ -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 diff --git a/framework/ordercloud/api/endpoints/customer/address/index.ts b/framework/ordercloud/api/endpoints/customer/address/index.ts new file mode 100644 index 000000000..385bc57f1 --- /dev/null +++ b/framework/ordercloud/api/endpoints/customer/address/index.ts @@ -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 +export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint'] + +export const handlers: CustomerAddressEndpoint['handlers'] = { + getAddresses, + addItem, + updateItem, + removeItem, +} + +const customerAddressApi = createEndpoint({ + handler: customerAddressEndpoint, + handlers, +}) + +export default customerAddressApi diff --git a/framework/ordercloud/api/endpoints/customer/address/remove-item.ts b/framework/ordercloud/api/endpoints/customer/address/remove-item.ts new file mode 100644 index 000000000..fba4e1154 --- /dev/null +++ b/framework/ordercloud/api/endpoints/customer/address/remove-item.ts @@ -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 diff --git a/framework/ordercloud/api/endpoints/customer/address/update-item.ts b/framework/ordercloud/api/endpoints/customer/address/update-item.ts new file mode 100644 index 000000000..4c4b4b9ae --- /dev/null +++ b/framework/ordercloud/api/endpoints/customer/address/update-item.ts @@ -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 diff --git a/framework/ordercloud/api/endpoints/customer/card/add-item.ts b/framework/ordercloud/api/endpoints/customer/card/add-item.ts new file mode 100644 index 000000000..9fc7af8f7 --- /dev/null +++ b/framework/ordercloud/api/endpoints/customer/card/add-item.ts @@ -0,0 +1,61 @@ +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: { restFetch }, +}) => { + // 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 restFetch('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 + await restFetch('POST', `/orders/Outgoing/${cartId}/payments`, { + "Type": "CreditCard", + CreditCardID: creditCard + }) + + return res.status(200).json({ data: null, errors: [] }) +} + +export default addItem diff --git a/framework/ordercloud/api/endpoints/customer/card/get-cards.ts b/framework/ordercloud/api/endpoints/customer/card/get-cards.ts new file mode 100644 index 000000000..e77520803 --- /dev/null +++ b/framework/ordercloud/api/endpoints/customer/card/get-cards.ts @@ -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 diff --git a/framework/ordercloud/api/endpoints/customer/card/index.ts b/framework/ordercloud/api/endpoints/customer/card/index.ts new file mode 100644 index 000000000..672939a8b --- /dev/null +++ b/framework/ordercloud/api/endpoints/customer/card/index.ts @@ -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 +export type CustomerCardEndpoint = CustomerCardAPI['endpoint'] + +export const handlers: CustomerCardEndpoint['handlers'] = { + getCards, + addItem, + updateItem, + removeItem, +} + +const customerCardApi = createEndpoint({ + handler: customerCardEndpoint, + handlers, +}) + +export default customerCardApi diff --git a/framework/ordercloud/api/endpoints/customer/card/remove-item.ts b/framework/ordercloud/api/endpoints/customer/card/remove-item.ts new file mode 100644 index 000000000..1a81d1cf4 --- /dev/null +++ b/framework/ordercloud/api/endpoints/customer/card/remove-item.ts @@ -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 diff --git a/framework/ordercloud/api/endpoints/customer/card/update-item.ts b/framework/ordercloud/api/endpoints/customer/card/update-item.ts new file mode 100644 index 000000000..9770644aa --- /dev/null +++ b/framework/ordercloud/api/endpoints/customer/card/update-item.ts @@ -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 diff --git a/framework/ordercloud/api/utils/fetch-rest.ts b/framework/ordercloud/api/utils/fetch-rest.ts index 3002b16ef..c8b4c678e 100644 --- a/framework/ordercloud/api/utils/fetch-rest.ts +++ b/framework/ordercloud/api/utils/fetch-rest.ts @@ -94,6 +94,8 @@ export async function fetchData( return fetchData(opts, retries + 1) } + console.log('dataResponse.text:', await dataResponse.textConverted()); + // Get the body of it const error = await dataResponse.json() diff --git a/framework/ordercloud/checkout/index.ts b/framework/ordercloud/checkout/index.ts new file mode 100644 index 000000000..ee73127e0 --- /dev/null +++ b/framework/ordercloud/checkout/index.ts @@ -0,0 +1,2 @@ +export { default as useCheckout } from './use-submit-checkout' +export { default as useGetCheckout } from './use-get-checkout' diff --git a/framework/ordercloud/checkout/use-checkout.tsx b/framework/ordercloud/checkout/use-checkout.tsx new file mode 100644 index 000000000..08f047416 --- /dev/null +++ b/framework/ordercloud/checkout/use-checkout.tsx @@ -0,0 +1,6 @@ +import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout' + +export default useCheckout as UseCheckout + +export const handler = useCheckout + diff --git a/framework/ordercloud/checkout/use-get-checkout.tsx b/framework/ordercloud/checkout/use-get-checkout.tsx new file mode 100644 index 000000000..a14670be2 --- /dev/null +++ b/framework/ordercloud/checkout/use-get-checkout.tsx @@ -0,0 +1,33 @@ +import type { GetCheckoutHook } from '@commerce/types/checkout' + +import { useMemo } from 'react' +import { SWRHook } from '@commerce/utils/types' +import useGetCheckout, { UseGetCheckout } from '@commerce/checkout/use-get-checkout' + +export default useGetCheckout as UseGetCheckout + +export const handler: SWRHook = { + fetchOptions: { + url: '/api/checkout', + 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] + ) + }, +} diff --git a/framework/ordercloud/checkout/use-submit-checkout.tsx b/framework/ordercloud/checkout/use-submit-checkout.tsx new file mode 100644 index 000000000..47644de8e --- /dev/null +++ b/framework/ordercloud/checkout/use-submit-checkout.tsx @@ -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 + +export const handler: MutationHook = { + 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] + ) + }, +} diff --git a/framework/ordercloud/commerce.config.json b/framework/ordercloud/commerce.config.json index bdbd67ea9..d93afa783 100644 --- a/framework/ordercloud/commerce.config.json +++ b/framework/ordercloud/commerce.config.json @@ -5,6 +5,6 @@ "cart": true, "search": false, "customerAuth": false, - "customCheckout": false + "customCheckout": true } } diff --git a/framework/ordercloud/customer/address/index.ts b/framework/ordercloud/customer/address/index.ts new file mode 100644 index 000000000..02c73e53b --- /dev/null +++ b/framework/ordercloud/customer/address/index.ts @@ -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' diff --git a/framework/ordercloud/customer/address/use-add-item.tsx b/framework/ordercloud/customer/address/use-add-item.tsx new file mode 100644 index 000000000..d8234a0ac --- /dev/null +++ b/framework/ordercloud/customer/address/use-add-item.tsx @@ -0,0 +1,48 @@ +import type { AddItemHook } from '@commerce/types/customer/address' +import type { MutationHook } from '@commerce/utils/types' + +import { useCallback } from 'react' +import { CommerceError } from '@commerce/utils/errors' +import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item' +import useAddresses from './use-addresses' + +export default useAddItem as UseAddItem + +export const handler: MutationHook = { + fetchOptions: { + url: '/api/customer/address', + 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 } = useAddresses() + + return useCallback( + async function addItem(input) { + const data = await fetch({ input }) + + await mutate(data, false) + + return data + }, + [fetch, mutate] + ) + }, +} diff --git a/framework/ordercloud/customer/address/use-addresses.tsx b/framework/ordercloud/customer/address/use-addresses.tsx new file mode 100644 index 000000000..dc17c9f00 --- /dev/null +++ b/framework/ordercloud/customer/address/use-addresses.tsx @@ -0,0 +1,33 @@ +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 + +export const handler: SWRHook = { + 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?.lineItems?.length ?? 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, +} diff --git a/framework/ordercloud/customer/address/use-remove-item.tsx b/framework/ordercloud/customer/address/use-remove-item.tsx new file mode 100644 index 000000000..b818497a8 --- /dev/null +++ b/framework/ordercloud/customer/address/use-remove-item.tsx @@ -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/customer/address/use-remove-item' + +import useAddresses from './use-addresses' + +export type RemoveItemFn = T extends LineItem + ? (input?: RemoveItemActionInput) => Promise + : (input: RemoveItemActionInput) => Promise + +export type RemoveItemActionInput = T extends LineItem + ? Partial + : RemoveItemHook['actionInput'] + +export default useRemoveItem as UseRemoveItem + +export const handler = { + fetchOptions: { + url: '/api/customer/address', + method: 'DELETE', + }, + async fetcher({ + input: { itemId }, + options, + fetch, + }: HookFetcherContext) { + return await fetch({ ...options, body: { itemId } }) + }, + useHook: ({ fetch }: MutationHookContext) => + function useHook( + ctx: { item?: T } = {} + ) { + const { item } = ctx + const { mutate } = useAddresses() + const removeItem: RemoveItemFn = 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, [fetch, mutate]) + }, +} diff --git a/framework/ordercloud/customer/address/use-update-item.tsx b/framework/ordercloud/customer/address/use-update-item.tsx new file mode 100644 index 000000000..8bd0c98e4 --- /dev/null +++ b/framework/ordercloud/customer/address/use-update-item.tsx @@ -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/customer/address/use-update-item' + +import { handler as removeItemHandler } from './use-remove-item' +import useAddresses from './use-addresses' + +export type UpdateItemActionInput = T extends LineItem + ? Partial + : UpdateItemHook['actionInput'] + +export default useUpdateItem as UseUpdateItem + +export const handler: MutationHook = { + fetchOptions: { + url: '/api/customer/address', + method: 'PUT', + }, + async fetcher({ + input: { itemId, item }, + options, + fetch, + }: HookFetcherContext) { + 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) => + function useHook( + ctx: { + item?: T + wait?: number + } = {} + ) { + const { item } = ctx + const { mutate } = useAddresses() as any + + return useCallback( + debounce(async (input: UpdateItemActionInput) => { + 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] + ) + }, +} diff --git a/framework/ordercloud/customer/card/index.ts b/framework/ordercloud/customer/card/index.ts new file mode 100644 index 000000000..357d30500 --- /dev/null +++ b/framework/ordercloud/customer/card/index.ts @@ -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' diff --git a/framework/ordercloud/customer/card/use-add-item.tsx b/framework/ordercloud/customer/card/use-add-item.tsx new file mode 100644 index 000000000..466ce8b5b --- /dev/null +++ b/framework/ordercloud/customer/card/use-add-item.tsx @@ -0,0 +1,48 @@ +import type { AddItemHook } from '@commerce/types/customer/card' +import type { MutationHook } from '@commerce/utils/types' + +import { useCallback } from 'react' +import { CommerceError } from '@commerce/utils/errors' +import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item' +import useCards from './use-cards' + +export default useAddItem as UseAddItem + +export const handler: MutationHook = { + fetchOptions: { + url: '/api/customer/card', + 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 } = useCards() + + return useCallback( + async function addItem(input) { + const data = await fetch({ input }) + + await mutate(data, false) + + return data + }, + [fetch, mutate] + ) + }, +} diff --git a/framework/ordercloud/customer/card/use-cards.tsx b/framework/ordercloud/customer/card/use-cards.tsx new file mode 100644 index 000000000..76f030462 --- /dev/null +++ b/framework/ordercloud/customer/card/use-cards.tsx @@ -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 + +export const handler: SWRHook = { + 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?.lineItems?.length ?? 0) <= 0 + }, + enumerable: true, + }, + }), + [response] + ) + }, +} diff --git a/framework/ordercloud/customer/card/use-remove-item.tsx b/framework/ordercloud/customer/card/use-remove-item.tsx new file mode 100644 index 000000000..cf46404db --- /dev/null +++ b/framework/ordercloud/customer/card/use-remove-item.tsx @@ -0,0 +1,60 @@ +import type { + MutationHookContext, + HookFetcherContext, +} from '@commerce/utils/types' +import type { Cart, LineItem, 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 extends LineItem + ? (input?: RemoveItemActionInput) => Promise + : (input: RemoveItemActionInput) => Promise + +export type RemoveItemActionInput = T extends LineItem + ? Partial + : RemoveItemHook['actionInput'] + +export default useRemoveItem as UseRemoveItem + +export const handler = { + fetchOptions: { + url: '/api/customer/card', + method: 'DELETE', + }, + async fetcher({ + input: { itemId }, + options, + fetch, + }: HookFetcherContext) { + return await fetch({ ...options, body: { itemId } }) + }, + useHook: ({ fetch }: MutationHookContext) => + function useHook( + ctx: { item?: T } = {} + ) { + const { item } = ctx + const { mutate } = useCards() + const removeItem: RemoveItemFn = 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, [fetch, mutate]) + }, +} diff --git a/framework/ordercloud/customer/card/use-update-item.tsx b/framework/ordercloud/customer/card/use-update-item.tsx new file mode 100644 index 000000000..88d59aa78 --- /dev/null +++ b/framework/ordercloud/customer/card/use-update-item.tsx @@ -0,0 +1,93 @@ +import type { + HookFetcherContext, + MutationHookContext, +} from '@commerce/utils/types' +import type { UpdateItemHook, LineItem } from '@commerce/types/customer/card' + +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/customer/card/use-update-item' + +import { handler as removeItemHandler } from './use-remove-item' +import useCards from './use-cards' + +export type UpdateItemActionInput = T extends LineItem + ? Partial + : UpdateItemHook['actionInput'] + +export default useUpdateItem as UseUpdateItem + +export const handler: MutationHook = { + fetchOptions: { + url: '/api/customer/card', + method: 'PUT', + }, + async fetcher({ + input: { itemId, item }, + options, + fetch, + }: HookFetcherContext) { + 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) => + function useHook( + ctx: { + item?: T + wait?: number + } = {} + ) { + const { item } = ctx + const { mutate } = useCards() as any + + return useCallback( + debounce(async (input: UpdateItemActionInput) => { + 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] + ) + }, +} diff --git a/framework/ordercloud/provider.ts b/framework/ordercloud/provider.ts index 09cea42eb..b8a79b0f9 100644 --- a/framework/ordercloud/provider.ts +++ b/framework/ordercloud/provider.ts @@ -1,7 +1,7 @@ import { handler as useCart } from './cart/use-cart' -import { handler as useAddItem } from './cart/use-add-item' -import { handler as useUpdateItem } from './cart/use-update-item' -import { handler as useRemoveItem } from './cart/use-remove-item' +import { handler as 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' @@ -10,6 +10,20 @@ 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 useGetCheckout } from './checkout/use-get-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' @@ -17,8 +31,32 @@ export const ordercloudProvider = { locale: LOCALE, cartCookie: CART_COOKIE, fetcher, - cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, - customer: { useCustomer }, + cart: { + useCart, + useAddItem: useAddCartItem, + useUpdateItem: useUpdateCartItem, + useRemoveItem: useRemoveCartItem + }, + checkout: { + useCheckout, + useSubmitCheckout, + useGetCheckout, + }, + customer: { + useCustomer, + card: { + useCards, + useAddItem: useAddCardItem, + useUpdateItem: useUpdateCardItem, + useRemoveItem: useRemoveCardItem + }, + address: { + useAddresses, + useAddItem: useAddAddressItem, + useUpdateItem: useUpdateAddressItem, + useRemoveItem: useRemoveAddressItem + } + }, products: { useSearch }, auth: { useLogin, useLogout, useSignup }, } diff --git a/framework/ordercloud/types/checkout.ts b/framework/ordercloud/types/checkout.ts new file mode 100644 index 000000000..17cbf43de --- /dev/null +++ b/framework/ordercloud/types/checkout.ts @@ -0,0 +1,4 @@ +import * as Core from '@commerce/types/checkout' + +export type CheckoutTypes = Core.CheckoutTypes +export type CheckoutSchema = Core.CheckoutSchema diff --git a/framework/ordercloud/types/customer/address.ts b/framework/ordercloud/types/customer/address.ts new file mode 100644 index 000000000..3aaddc9a2 --- /dev/null +++ b/framework/ordercloud/types/customer/address.ts @@ -0,0 +1,31 @@ +import * as Core from '@commerce/types/customer/address' + +export type CustomerAddressTypes = Core.CustomerAddressTypes +export type CustomerAddressSchema = Core.CustomerAddressSchema + +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 +} diff --git a/framework/ordercloud/types/customer/card.ts b/framework/ordercloud/types/customer/card.ts new file mode 100644 index 000000000..eb1abffbb --- /dev/null +++ b/framework/ordercloud/types/customer/card.ts @@ -0,0 +1,16 @@ +import * as Core from '@commerce/types/customer/card' + +export type CustomerCardTypes = Core.CustomerCardTypes +export type CustomerCardSchema = Core.CustomerCardSchema + +export interface OredercloudCreditCard { + "ID": string; + "Editable": boolean; + "Token": string; + "DateCreated": string; + "CardType": string; + "PartialAccountNumber": string; + "CardholderName": string; + "ExpirationDate": string; + "xp": null +} diff --git a/package.json b/package.json index 68bf0059d..674cbcf25 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "react-fast-marquee": "^1.1.4", "react-merge-refs": "^1.1.0", "react-use-measure": "^2.0.4", + "stripe": "^8.174.0", "swell-js": "^4.0.0-next.0", "swr": "^0.5.6", "tabbable": "^5.2.0", diff --git a/pages/api/customer/address.ts b/pages/api/customer/address.ts new file mode 100644 index 000000000..5815ea462 --- /dev/null +++ b/pages/api/customer/address.ts @@ -0,0 +1,4 @@ +import customerAddressApi from '@framework/api/endpoints/customer/address' +import commerce from '@lib/api/commerce' + +export default customerAddressApi(commerce) diff --git a/pages/api/customer/card.ts b/pages/api/customer/card.ts new file mode 100644 index 000000000..6f88b8c74 --- /dev/null +++ b/pages/api/customer/card.ts @@ -0,0 +1,4 @@ +import customerCardApi from '@framework/api/endpoints/customer/card' +import commerce from '@lib/api/commerce' + +export default customerCardApi(commerce) diff --git a/pages/api/customer.ts b/pages/api/customer/index.ts similarity index 100% rename from pages/api/customer.ts rename to pages/api/customer/index.ts diff --git a/tsconfig.json b/tsconfig.json index 340929669..340dad193 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,8 +23,8 @@ "@components/*": ["components/*"], "@commerce": ["framework/commerce"], "@commerce/*": ["framework/commerce/*"], - "@framework": ["framework/local"], - "@framework/*": ["framework/local/*"] + "@framework": ["framework/ordercloud"], + "@framework/*": ["framework/ordercloud/*"] } }, "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], diff --git a/yarn.lock b/yarn.lock index 35e9ca835..80f763957 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708" + integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g== + "@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.174.0: + version "8.174.0" + resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.174.0.tgz#91d2e61b0217b1ee9fde2842582e0f1cf1dddc94" + integrity sha512-UFU5TuYH7XwUmSllUIcIKhhsvvhhjw9D6ZwVdfB74wU4VOOaWBiQqszkw6chaEFpdulUmbcAH5eZltV3HwOi7g== + 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"