diff --git a/package.json b/package.json index 2e66e5ee..8ef6eb41 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@elasticpath/react-shopper-hooks": "0.1.1", "@emotion/react": "^11.10.0", "@emotion/styled": "^11.10.0", - "@moltin/sdk": "^18.1.0", + "@moltin/sdk": "^20.0.0", "@stripe/react-stripe-js": "^1.11.0", "@stripe/stripe-js": "^1.38.1", "@types/react-stripe-elements": "^6.0.6", diff --git a/src/components/checkout/ManualTypeCheckoutForm.tsx b/src/components/checkout/ManualTypeCheckoutForm.tsx new file mode 100644 index 00000000..e0fa18f2 --- /dev/null +++ b/src/components/checkout/ManualTypeCheckoutForm.tsx @@ -0,0 +1,190 @@ +import { Form, Formik } from "formik"; +import { + Button, + Checkbox, + FormControl, + Grid, + GridItem, + Heading, + Flex, + Box, +} from "@chakra-ui/react"; +import { + CheckoutForm as CheckoutFormType, + checkoutFormSchema, +} from "./form-schema/checkout-form-schema"; +import { ChevronRightIcon } from "@chakra-ui/icons"; +import { ConfirmPaymentResponse, PaymentRequestBody } from "@moltin/sdk"; +import ShippingForm from "./ShippingForm"; +import CustomFormControl from "./CustomFormControl"; +import BillingForm from "./BillingForm"; +import { useCart } from "@elasticpath/react-shopper-hooks"; +import { captureOrder, makePayment } from "../../services/checkout"; +import { useState } from "react"; + +const initialValues: Partial = { + personal: { + email: "", + }, + sameAsShipping: true, + shippingAddress: { + first_name: "", + last_name: "", + line_1: "", + country: "", + region: "", + postcode: "", + }, +}; + +interface ICheckoutForm { + checkout: ReturnType["stripeIntent"]; + showCompletedOrder: ( + paymentResponse: ConfirmPaymentResponse, + checkoutForm: CheckoutFormType + ) => void; +} + +const manualPaymentRequest: PaymentRequestBody = { + gateway: "manual", + method: "authorize", +}; + +export default function ManualTypeCheckoutForm({ + checkout, + showCompletedOrder, +}: ICheckoutForm): JSX.Element { + const [paymentResponse, setPaymentResponse] = + useState(); + const [checkoutForm, setCheckoutForm] = useState(); + + const steps = [ + { + name: "Cart", + isActive: true, + handler: () => paymentResponse && setPaymentResponse(undefined), + }, + { name: "Billing Information", isActive: false }, + ]; + + const onCompletedOrder = () => { + if (paymentResponse) { + captureOrder( + paymentResponse.data.relationships.order.data.id, + paymentResponse.data.id + ).then(() => showCompletedOrder(paymentResponse, checkoutForm!)); + } + }; + + return ( + <> + + + {steps.map((step, stepIdx) => ( + + + + {stepIdx !== steps.length - 1 ? ( + + ) : null} + + ))} + + + + { + const { personal, shippingAddress, billingAddress, sameAsShipping } = + validatedValues; + setCheckoutForm(validatedValues); + + const orderResponse = await checkout( + personal.email, + shippingAddress, + sameAsShipping, + billingAddress ?? undefined + ); + + if (orderResponse?.data.id) { + const paymentResponse = await makePayment( + manualPaymentRequest, + orderResponse.data.id + ); + setPaymentResponse(paymentResponse); + } + }} + > + {({ handleChange, values, isSubmitting }) => ( +
+ + + Contact Information + + + + + + Shipping Address + + + + + + Billing Address + + + + + Same as shipping address? + + + {!values.sameAsShipping && } + + + + +
+ )} +
+ + ); +} diff --git a/src/components/checkout/StripeTypeCheckoutForm.tsx b/src/components/checkout/StripeTypeCheckoutForm.tsx deleted file mode 100644 index 81cd220d..00000000 --- a/src/components/checkout/StripeTypeCheckoutForm.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { Form, Formik } from "formik"; -import { - Button, - Checkbox, - FormControl, - Grid, - GridItem, - Heading, - Flex, - Box, -} from "@chakra-ui/react"; -import { - CheckoutForm as CheckoutFormType, - checkoutFormSchema, -} from "./form-schema/checkout-form-schema"; -import { ChevronRightIcon } from "@chakra-ui/icons"; -import { ConfirmPaymentResponse, PaymentRequestBody } from "@moltin/sdk"; -import ShippingForm from "./ShippingForm"; -import CustomFormControl from "./CustomFormControl"; -import BillingForm from "./BillingForm"; -import { useCart } from "@elasticpath/react-shopper-hooks"; -import EpStripePayment from "./payments/ep-stripe-payment/EpStripePayment"; -import { confirmOrder, makePayment } from "../../services/checkout"; -import { useState } from "react"; - -const initialValues: Partial = { - personal: { - email: "", - }, - sameAsShipping: true, - shippingAddress: { - first_name: "", - last_name: "", - line_1: "", - country: "", - region: "", - postcode: "", - }, -}; - -interface ICheckoutForm { - checkout: ReturnType["stripeIntent"]; - showCompletedOrder: ( - paymentResponse: ConfirmPaymentResponse, - checkoutForm: CheckoutFormType - ) => void; -} - -export default function StripeTypeCheckoutForm({ - checkout, - showCompletedOrder, -}: ICheckoutForm): JSX.Element { - const [paymentResponse, setPaymentResponse] = - useState(); - const [checkoutForm, setCheckoutForm] = useState(); - const clientSecret = paymentResponse?.data.payment_intent.client_secret || ""; - - const steps = [ - { - name: "Cart", - isActive: !clientSecret, - handler: () => paymentResponse && setPaymentResponse(undefined), - }, - { name: "Billing Information", isActive: clientSecret }, - ]; - - return ( - <> - - - {steps.map((step, stepIdx) => ( - - - - {stepIdx !== steps.length - 1 ? ( - - ) : null} - - ))} - - - - {clientSecret ? ( - { - if (paymentResponse) { - confirmOrder( - paymentResponse.data.relationships.order.data.id, - paymentResponse.data.id - ).then(() => showCompletedOrder(paymentResponse, checkoutForm!)); - } - }} - /> - ) : ( - { - const { - personal, - shippingAddress, - billingAddress, - sameAsShipping, - } = validatedValues; - setCheckoutForm(validatedValues); - - const orderResponse = await checkout( - personal.email, - shippingAddress, - sameAsShipping, - billingAddress ?? undefined - ); - - if (orderResponse?.data.id) { - const payment: PaymentRequestBody = { - gateway: "elastic_path_payments_stripe", - method: "purchase", - payment_method_types: ["card"], - }; - const paymentResponse = await makePayment( - payment, - orderResponse.data.id - ); - setPaymentResponse(paymentResponse); - } - }} - > - {({ handleChange, values, isSubmitting }) => ( -
- - - Contact Information - - - - - - Shipping Address - - - - - - Billing Address - - - - - Same as shipping address? - - - {!values.sameAsShipping && } - - - - -
- )} -
- )} - - ); -} diff --git a/src/components/checkout/payments/ep-stripe-payment/EpStripePayment.module.scss b/src/components/checkout/payments/ep-stripe-payment/EpStripePayment.module.scss deleted file mode 100644 index 7008bf68..00000000 --- a/src/components/checkout/payments/ep-stripe-payment/EpStripePayment.module.scss +++ /dev/null @@ -1,77 +0,0 @@ -.stripe { - button { - background: #5469d4; - font-family: Arial, sans-serif; - color: #ffffff; - border-radius: 4px; - border: 0; - padding: 12px 16px; - font-size: 16px; - font-weight: 600; - cursor: pointer; - display: block; - transition: all 0.2s ease; - box-shadow: 0 4px 5.5px 0 rgba(0, 0, 0, 0.07); - width: 100%; - } - - button:hover { - filter: contrast(115%); - } - - button:disabled { - opacity: 0.5; - cursor: default; - } - - .spinner, - .spinner:before, - .spinner:after { - border-radius: 50%; - } - .spinner { - color: #ffffff; - font-size: 22px; - text-indent: -99999px; - margin: 0 auto; - position: relative; - width: 20px; - height: 20px; - box-shadow: inset 0 0 0 2px; - -webkit-transform: translateZ(0); - -ms-transform: translateZ(0); - transform: translateZ(0); - } - - .spinner:before, - .spinner:after { - position: absolute; - content: ""; - } - - .spinner:before { - width: 10.4px; - height: 20.4px; - background: #5469d4; - border-radius: 20.4px 0 0 20.4px; - top: -0.2px; - left: -0.2px; - -webkit-transform-origin: 10.4px 10.2px; - transform-origin: 10.4px 10.2px; - -webkit-animation: loading 2s infinite ease 1.5s; - animation: loading 2s infinite ease 1.5s; - } - - .spinner:after { - width: 10.4px; - height: 10.2px; - background: #5469d4; - border-radius: 0 10.2px 10.2px 0; - top: -0.1px; - left: 10.2px; - -webkit-transform-origin: 0 10.2px; - transform-origin: 0 10.2px; - -webkit-animation: loading 2s infinite ease; - animation: loading 2s infinite ease; - } -} diff --git a/src/components/checkout/payments/ep-stripe-payment/EpStripePayment.tsx b/src/components/checkout/payments/ep-stripe-payment/EpStripePayment.tsx deleted file mode 100644 index 66652eef..00000000 --- a/src/components/checkout/payments/ep-stripe-payment/EpStripePayment.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import { - Appearance, - loadStripe, - StripeElementsOptions, -} from "@stripe/stripe-js"; -import { Elements } from "@stripe/react-stripe-js"; -import StripeCheckoutForm from "./StripeCheckoutForm"; -import { STRIPE_PUBLISHABLE_KEY } from "../../../../lib/resolve-ep-stripe-env"; -import styles from "./EpStripePayment.module.scss"; - -const stripePromise = loadStripe(STRIPE_PUBLISHABLE_KEY); - -export default function EpStripePayment({ - clientSecret, - showCompletedOrder, -}: { - clientSecret: string; - showCompletedOrder: () => void; -}) { - const appearance: Appearance = { - theme: "stripe", - }; - const options: StripeElementsOptions = { - clientSecret, - appearance, - }; - - return ( -
- {clientSecret && ( - - - - )} -
- ); -} diff --git a/src/components/checkout/payments/ep-stripe-payment/StripeCheckoutForm.tsx b/src/components/checkout/payments/ep-stripe-payment/StripeCheckoutForm.tsx deleted file mode 100644 index ef66ce5e..00000000 --- a/src/components/checkout/payments/ep-stripe-payment/StripeCheckoutForm.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { - PaymentElement, - useStripe, - useElements, -} from "@stripe/react-stripe-js"; -import { useCart } from "@elasticpath/react-shopper-hooks"; -import { Text } from "@chakra-ui/react"; - -export default function StripeCheckoutForm({ - showCompletedOrder, -}: { - showCompletedOrder: () => void; -}) { - const stripe = useStripe(); - const elements = useElements(); - const { emptyCart } = useCart(); - - const [message, setMessage] = useState(""); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - if (!stripe) { - return; - } - - const clientSecret = new URLSearchParams(window.location.search).get( - "payment_intent_client_secret" - ); - - if (!clientSecret) { - return; - } - - stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => { - switch (paymentIntent?.status) { - case "succeeded": - setMessage("Payment succeeded!"); - break; - case "processing": - setMessage("Your payment is processing."); - break; - case "requires_payment_method": - setMessage("Your payment was not successful, please try again."); - break; - default: - setMessage("Something went wrong."); - break; - } - }); - }, [stripe]); - - const handleSubmit = async (e: any) => { - e.preventDefault(); - - if (!stripe || !elements) { - return; - } - - setIsLoading(true); - - const { error } = await stripe.confirmPayment({ - elements, - redirect: "if_required", - }); - - if (error?.type === "card_error" || error?.type === "validation_error") { - setMessage(error?.message || ""); - } else { - setMessage("An unexpected error occurred."); - } - setIsLoading(false); - if (error) { - return; - } - await emptyCart(); - showCompletedOrder(); - }; - - return ( -
- {message && ( - - {message} - - )} - - - - ); -} diff --git a/src/pages/checkout/[cartId].tsx b/src/pages/checkout/[cartId].tsx index e975acff..2a4930f2 100644 --- a/src/pages/checkout/[cartId].tsx +++ b/src/pages/checkout/[cartId].tsx @@ -21,7 +21,7 @@ import { OrderCompleteState } from "../../components/checkout/types/order-pendin import { PresentCartState } from "@elasticpath/react-shopper-hooks"; import { CheckoutForm as CheckoutFormType } from "../../components/checkout/form-schema/checkout-form-schema"; import OrderComplete from "../../components/cart/OrderComplete"; -import StripeTypeCheckoutForm from "../../components/checkout/StripeTypeCheckoutForm"; +import ManualTypeCheckoutForm from "../../components/checkout/ManualTypeCheckoutForm"; interface ICheckout { cart: ResourceIncluded; @@ -82,7 +82,7 @@ export const Checkout: NextPage = () => { colStart={{ base: 1, md: 1 }} > {showEpStripePaymentGateway ? ( - diff --git a/src/services/checkout.ts b/src/services/checkout.ts index 2dbed4b4..d43424c9 100644 --- a/src/services/checkout.ts +++ b/src/services/checkout.ts @@ -5,6 +5,7 @@ import type { CheckoutCustomerObject, Moltin as EPCCClient, ConfirmPaymentResponse, + TransactionsResponse, } from "@moltin/sdk"; import { getEpccImplicitClient } from "../lib/epcc-implicit-client"; @@ -39,3 +40,14 @@ export function confirmOrder( { data: {} } ); } + +export function captureOrder( + order: string, + transaction: string, + client?: EPCCClient +): Promise { + return (client ?? getEpccImplicitClient()).Transactions.Capture({ + order, + transaction, + }); +}