diff --git a/CHANGELOG.md b/CHANGELOG.md index d922a46e21..50f1ebbfe8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable, unreleased changes to this project will be documented in this file. - Do not throw error if unsupported payment gateway found - #819 by @dominik-zeglen - Fix tsconfig aliases - #824 by @orzechdev - Set billing address in first checkout step - #822 by @orzechdev +- Persist payment gateways for the whole checkout - #828 by @orzechdev ## 2.10.4 diff --git a/src/@next/components/organisms/CheckoutPayment/CheckoutPayment.tsx b/src/@next/components/organisms/CheckoutPayment/CheckoutPayment.tsx index 3fedd18ab7..c5a01de04b 100755 --- a/src/@next/components/organisms/CheckoutPayment/CheckoutPayment.tsx +++ b/src/@next/components/organisms/CheckoutPayment/CheckoutPayment.tsx @@ -6,7 +6,6 @@ import { checkoutMessages } from "@temp/intl"; import { DiscountForm } from "../DiscountForm"; import { IDiscountFormData } from "../DiscountForm/types"; -import { PaymentGatewaysList } from "../PaymentGatewaysList"; import * as S from "./styles"; import { IProps } from "./types"; @@ -15,22 +14,13 @@ import { IProps } from "./types"; * Payment options used in checkout. */ const CheckoutPayment: React.FC = ({ - gatewayErrors, promoCodeErrors, - paymentGateways, promoCodeDiscountFormId, promoCodeDiscountFormRef, promoCodeDiscount, addPromoCode, removeVoucherCode, submitUnchangedDiscount, - selectedPaymentGateway, - selectedPaymentGatewayToken, - selectPaymentGateway, - gatewayFormRef, - gatewayFormId, - processPayment, - onGatewayError, }: IProps) => { const [showPromoCodeForm, setShowPromoCodeForm] = useState( !!promoCodeDiscount?.voucherCode @@ -86,17 +76,6 @@ const CheckoutPayment: React.FC = ({ )} - ); diff --git a/src/@next/components/organisms/CheckoutPayment/__snapshots__/stories.storyshot b/src/@next/components/organisms/CheckoutPayment/__snapshots__/stories.storyshot index 2dabfa004d..f64d10165e 100644 --- a/src/@next/components/organisms/CheckoutPayment/__snapshots__/stories.storyshot +++ b/src/@next/components/organisms/CheckoutPayment/__snapshots__/stories.storyshot @@ -6,7 +6,7 @@ exports[`Storyshots @components/organisms/CheckoutPayment default 1`] = ` margin: 20px; } -.c9 { +.c6 { position: absolute; width: 100%; height: 100%; @@ -105,39 +105,6 @@ exports[`Storyshots @components/organisms/CheckoutPayment default 1`] = ` background-color: rgb(33,18,94); } -.c8 { - cursor: pointer; -} - -.c8 input[type="radio"] { - opacity: 0; - position: fixed; - width: 0; -} - -.c8 > div { - display: inline-block; - width: 1em; - height: 1em; - margin: 0.25em 1em 0.25em 0.25em; - border: 0.1em solid #21125e; - border-radius: 0.5em; - background: #ffffff; - vertical-align: bottom; -} - -.c6 { - display: grid; - grid-gap: 20px; -} - -.c7 { - display: block; - background-color: #f1f5f5; - padding: 20px; - cursor: pointer; -} - .c5 { width: 100%; border-bottom: 1px solid rgba(50,50,50,0.1); @@ -189,72 +156,10 @@ exports[`Storyshots @components/organisms/CheckoutPayment default 1`] = `
-
-
- -
-
- -
-
`; diff --git a/src/@next/components/organisms/CheckoutPayment/stories.tsx b/src/@next/components/organisms/CheckoutPayment/stories.tsx index 61f02c6cae..279d96bb79 100644 --- a/src/@next/components/organisms/CheckoutPayment/stories.tsx +++ b/src/@next/components/organisms/CheckoutPayment/stories.tsx @@ -18,9 +18,6 @@ const removeVoucherCode = action("removeVoucherCode has been called"); const submitUnchangedDiscount = action( "submitUnchangedDiscount has been called" ); -const selectPaymentGateway = action("selectPaymentGateway has been called"); -const processPayment = action("processPayment has been called"); -const onGatewayError = action("onGatewayError has been called"); storiesOf("@components/organisms/CheckoutPayment", module) .addParameters({ component: CheckoutPayment }) @@ -31,9 +28,6 @@ storiesOf("@components/organisms/CheckoutPayment", module) addPromoCode={addPromoCode} removeVoucherCode={removeVoucherCode} submitUnchangedDiscount={submitUnchangedDiscount} - selectPaymentGateway={selectPaymentGateway} - processPayment={processPayment} - onGatewayError={onGatewayError} /> )); diff --git a/src/@next/components/organisms/CheckoutPayment/test.tsx b/src/@next/components/organisms/CheckoutPayment/test.tsx index d5b04877d9..9a5c3a5164 100644 --- a/src/@next/components/organisms/CheckoutPayment/test.tsx +++ b/src/@next/components/organisms/CheckoutPayment/test.tsx @@ -11,9 +11,6 @@ describe("", () => { const addPromoCode = jest.fn(); const removeVoucherCode = jest.fn(); const submitUnchangedDiscount = jest.fn(); - const selectPaymentGateway = jest.fn(); - const processPayment = jest.fn(); - const onGatewayError = jest.fn(); const wrapper = mount( ", () => { addPromoCode={addPromoCode} removeVoucherCode={removeVoucherCode} submitUnchangedDiscount={submitUnchangedDiscount} - selectPaymentGateway={selectPaymentGateway} - processPayment={processPayment} - onGatewayError={onGatewayError} /> ); - const wrapperText = wrapper.text(); - expect(wrapperText).toContain(PROPS.paymentGateways[0].name); - expect(wrapperText).toContain(PROPS.paymentGateways[1].name); + expect(wrapper.exists()).toEqual(true); }); }); diff --git a/src/@next/components/organisms/CheckoutPayment/types.ts b/src/@next/components/organisms/CheckoutPayment/types.ts index 7ab75dcd65..b73c525115 100755 --- a/src/@next/components/organisms/CheckoutPayment/types.ts +++ b/src/@next/components/organisms/CheckoutPayment/types.ts @@ -1,46 +1,15 @@ -import { ICardData, IFormError, IPaymentGateway } from "@types"; +import { IFormError } from "@types"; export interface IPromoCodeDiscount { voucherCode?: string | null; } export interface IProps { - gatewayErrors?: IFormError[]; promoCodeErrors?: IFormError[]; - paymentGateways: IPaymentGateway[]; promoCodeDiscount?: IPromoCodeDiscount; promoCodeDiscountFormRef?: React.RefObject; promoCodeDiscountFormId?: string; addPromoCode: (promoCode: string) => void; removeVoucherCode: (voucherCode: string) => void; submitUnchangedDiscount: () => void; - /** - * Selected payment gateway. - */ - selectedPaymentGateway?: string; - /** - * Selected payment gateway token. - */ - selectedPaymentGatewayToken?: string; - /** - * Called when selected payment gateway is changed. - */ - selectPaymentGateway: (paymentGateway: string) => void; - /** - * Gateway form reference on which payment might be submitted. - */ - gatewayFormRef?: React.RefObject; - gatewayFormId?: string; - /** - * Method called after the form is submitted. Passed gateway id and token attribute will be used to create payment. - */ - processPayment: ( - gateway: string, - token: string, - cardData?: ICardData - ) => void; - /** - * Method called when gateway error occured. - */ - onGatewayError: (errors: IFormError[]) => void; } diff --git a/src/@next/components/templates/Checkout/Checkout.tsx b/src/@next/components/templates/Checkout/Checkout.tsx index b8d45924db..b5de1193f9 100755 --- a/src/@next/components/templates/Checkout/Checkout.tsx +++ b/src/@next/components/templates/Checkout/Checkout.tsx @@ -14,6 +14,8 @@ const Checkout: React.FC = ({ loading, navigation, checkout, + paymentGateways, + hidePaymentGateways = false, cartSummary, button, }: IProps) => { @@ -27,6 +29,9 @@ const Checkout: React.FC = ({ {navigation} {checkout} + + {paymentGateways} + {cartSummary} {button} diff --git a/src/@next/components/templates/Checkout/__snapshots__/stories.storyshot b/src/@next/components/templates/Checkout/__snapshots__/stories.storyshot index 0e61fa8fe5..c71f61b33f 100644 --- a/src/@next/components/templates/Checkout/__snapshots__/stories.storyshot +++ b/src/@next/components/templates/Checkout/__snapshots__/stories.storyshot @@ -6,7 +6,7 @@ exports[`Storyshots @components/templates/Checkout default 1`] = ` margin: 20px; } -.c7 { +.c8 { position: absolute; width: 100%; height: 100%; @@ -30,7 +30,7 @@ exports[`Storyshots @components/templates/Checkout default 1`] = ` grid-template-columns: 8fr 4fr; grid-template-rows: 85px auto auto; grid-column-gap: 30px; - grid-template-areas: "navigation cartSummary" "checkout cartSummary" "button cartSummary"; + grid-template-areas: "navigation cartSummary" "checkout cartSummary" "paymentGateways cartSummary" "button cartSummary"; } .c3 { @@ -42,15 +42,20 @@ exports[`Storyshots @components/templates/Checkout default 1`] = ` .c4 { grid-area: checkout; - padding: 3rem 0; + padding: 3rem 0 0 0; } .c5 { - grid-area: cartSummary; + grid-area: paymentGateways; } .c6 { + grid-area: cartSummary; +} + +.c7 { grid-area: button; + margin: 3rem 0 0 0; } @media (max-width:992px) { @@ -62,12 +67,12 @@ exports[`Storyshots @components/templates/Checkout default 1`] = ` @media (max-width:720px) { .c2 { grid-template-columns: 1fr; - grid-template-areas: "navigation" "checkout" "button"; + grid-template-areas: "navigation" "checkout" "paymentGateways" "button"; } } @media (max-width:720px) { - .c5 { + .c6 { position: fixed; bottom: 0; } @@ -84,20 +89,83 @@ exports[`Storyshots @components/templates/Checkout default 1`] = ` >
+ > +
+ Navigation +
+
+ > +
+ Checkout +
+
+ > +
+ Payment gateways +
+
+ > +
+ Cart summary +
+
+
+ +
`; diff --git a/src/@next/components/templates/Checkout/stories.tsx b/src/@next/components/templates/Checkout/stories.tsx index c82e0f8b5f..35cf3cc207 100644 --- a/src/@next/components/templates/Checkout/stories.tsx +++ b/src/@next/components/templates/Checkout/stories.tsx @@ -3,6 +3,26 @@ import React from "react"; import { Checkout } from "."; +const style = { + backgroundColor: "#ccc", + border: "1px solid black", + padding: "10px", +}; + +const navigation =
Navigation
; +const checkout =
Checkout
; +const paymentGateways =
Payment gateways
; +const cartSummary =
Cart summary
; +const button = ; + storiesOf("@components/templates/Checkout", module) .addParameters({ component: Checkout }) - .add("default", () => ); + .add("default", () => ( + + )); diff --git a/src/@next/components/templates/Checkout/styles.ts b/src/@next/components/templates/Checkout/styles.ts index 6f5cfb1aba..42b3bdb00a 100755 --- a/src/@next/components/templates/Checkout/styles.ts +++ b/src/@next/components/templates/Checkout/styles.ts @@ -20,6 +20,7 @@ export const Wrapper = styled.div` grid-template-areas: "navigation cartSummary" "checkout cartSummary" + "paymentGateways cartSummary" "button cartSummary"; ${media.mediumScreen` @@ -27,6 +28,7 @@ export const Wrapper = styled.div` grid-template-areas: "navigation" "checkout" + "paymentGateways" "button"; `} `; @@ -40,7 +42,11 @@ export const Navigation = styled.div` `; export const Checkout = styled.div` grid-area: checkout; - padding: 3rem 0; + padding: 3rem 0 0 0; +`; +export const PaymentGateways = styled.div<{ hide: boolean }>` + ${props => props.hide && "display: none;"} + grid-area: paymentGateways; `; export const CartSummary = styled.div` grid-area: cartSummary; @@ -52,4 +58,5 @@ export const CartSummary = styled.div` `; export const Button = styled.div` grid-area: button; + margin: 3rem 0 0 0; `; diff --git a/src/@next/components/templates/Checkout/types.ts b/src/@next/components/templates/Checkout/types.ts index a2867d79b7..182734c1d6 100755 --- a/src/@next/components/templates/Checkout/types.ts +++ b/src/@next/components/templates/Checkout/types.ts @@ -2,6 +2,8 @@ export interface IProps { loading?: boolean; navigation?: React.ReactNode; checkout?: React.ReactNode; + paymentGateways?: React.ReactNode; + hidePaymentGateways?: boolean; cartSummary?: React.ReactNode; button?: React.ReactNode; } diff --git a/src/@next/pages/CheckoutPage/CheckoutPage.tsx b/src/@next/pages/CheckoutPage/CheckoutPage.tsx index 3ad86f8b71..881f5acc02 100755 --- a/src/@next/pages/CheckoutPage/CheckoutPage.tsx +++ b/src/@next/pages/CheckoutPage/CheckoutPage.tsx @@ -4,13 +4,13 @@ import { Redirect, useLocation, useHistory } from "react-router-dom"; import { Button, Loader } from "@components/atoms"; import { CheckoutProgressBar } from "@components/molecules"; -import { CartSummary } from "@components/organisms"; +import { CartSummary, PaymentGatewaysList } from "@components/organisms"; import { Checkout } from "@components/templates"; import { useCart, useCheckout } from "@saleor/sdk"; import { IItems } from "@saleor/sdk/lib/api/Cart/types"; import { CHECKOUT_STEPS } from "@temp/core/config"; import { checkoutMessages } from "@temp/intl"; -import { ITaxedMoney, ICheckoutStep } from "@types"; +import { ITaxedMoney, ICheckoutStep, ICardData, IFormError } from "@types"; import { CheckoutRouter } from "./CheckoutRouter"; import { @@ -101,7 +101,13 @@ const CheckoutPage: React.FC = ({}: IProps) => { totalPrice, items, } = useCart(); - const { loaded: checkoutLoaded, checkout, payment } = useCheckout(); + const { + loaded: checkoutLoaded, + checkout, + payment, + availablePaymentGateways, + createPayment, + } = useCheckout(); const intl = useIntl(); if (cartLoaded && (!items || !items?.length)) { @@ -117,6 +123,9 @@ const CheckoutPage: React.FC = ({}: IProps) => { selectedPaymentGatewayToken, setSelectedPaymentGatewayToken, ] = useState(payment?.token); + const [paymentGatewayErrors, setPaymentGatewayErrors] = useState< + IFormError[] + >([]); useEffect(() => { setSelectedPaymentGateway(payment?.gateway); @@ -151,12 +160,14 @@ const CheckoutPage: React.FC = ({}: IProps) => { null ); const checkoutReviewSubpageRef = useRef(null); + const checkoutGatewayFormId = "gateway-form"; + const checkoutGatewayFormRef = useRef(null); const handleNextStepClick = () => { // Some magic above and below ensures that the activeStepIndex will always // be in 0-3 range /* eslint-disable default-case */ - switch (steps[activeStepIndex].index) { + switch (activeStep.index) { case 0: if (checkoutAddressSubpageRef.current?.submitAddress) { checkoutAddressSubpageRef.current?.submitAddress(); @@ -227,11 +238,10 @@ const CheckoutPage: React.FC = ({}: IProps) => { renderPayment={props => ( )} @@ -249,6 +259,39 @@ const CheckoutPage: React.FC = ({}: IProps) => { ); + const handleProcessPayment = async ( + gateway: string, + token: string, + cardData?: ICardData + ) => { + const { dataError } = await createPayment(gateway, token, cardData); + const errors = dataError?.error; + setSubmitInProgress(false); + if (errors) { + setPaymentGatewayErrors(errors); + } else { + setPaymentGatewayErrors([]); + handleStepSubmitSuccess(); + } + }; + const handlePaymentGatewayError = () => { + setSubmitInProgress(false); + }; + + const paymentGatewaysView = availablePaymentGateways && ( + + ); + let buttonText = activeStep.nextActionName; /* eslint-disable default-case */ switch (activeStep.nextActionName) { @@ -282,6 +325,8 @@ const CheckoutPage: React.FC = ({}: IProps) => { items )} checkout={checkoutView} + paymentGateways={paymentGatewaysView} + hidePaymentGateways={steps[activeStepIndex].name !== "Payment"} button={getButton(buttonText.toUpperCase(), handleNextStepClick)} /> ); diff --git a/src/@next/pages/CheckoutPage/subpages/CheckoutPaymentSubpage.tsx b/src/@next/pages/CheckoutPage/subpages/CheckoutPaymentSubpage.tsx index 5c55d5875d..361030b623 100644 --- a/src/@next/pages/CheckoutPage/subpages/CheckoutPaymentSubpage.tsx +++ b/src/@next/pages/CheckoutPage/subpages/CheckoutPaymentSubpage.tsx @@ -11,17 +11,16 @@ import { RouteComponentProps } from "react-router"; import { CheckoutPayment } from "@components/organisms"; import { useCheckout } from "@saleor/sdk"; import { commonMessages } from "@temp/intl"; -import { ICardData, IFormError } from "@types"; +import { IFormError } from "@types"; export interface ICheckoutPaymentSubpageHandles { submitPayment: () => void; } interface IProps extends RouteComponentProps { - selectedPaymentGateway?: string; - selectedPaymentGatewayToken?: string; - selectPaymentGateway: (paymentGateway: string) => void; + paymentGatewayFormRef: React.RefObject; changeSubmitProgress: (submitInProgress: boolean) => void; onSubmitSuccess: () => void; + onPaymentGatewayError: (errors: IFormError[]) => void; } const CheckoutPaymentSubpageWithRef: RefForwardingComponent< @@ -29,30 +28,18 @@ const CheckoutPaymentSubpageWithRef: RefForwardingComponent< IProps > = ( { - selectedPaymentGateway, - selectedPaymentGatewayToken, + paymentGatewayFormRef, changeSubmitProgress, - selectPaymentGateway, onSubmitSuccess, + onPaymentGatewayError, ...props }: IProps, ref ) => { - const { - availablePaymentGateways, - promoCodeDiscount, - addPromoCode, - removePromoCode, - createPayment, - } = useCheckout(); + const { promoCodeDiscount, addPromoCode, removePromoCode } = useCheckout(); - const [gatewayErrors, setGatewayErrors] = useState([]); const [promoCodeErrors, setPromoCodeErrors] = useState([]); - const paymentGateways = availablePaymentGateways || []; - - const checkoutGatewayFormId = "gateway-form"; - const checkoutGatewayFormRef = useRef(null); const promoCodeDiscountFormId = "discount-form"; const promoCodeDiscountFormRef = useRef(null); const intl = useIntl(); @@ -63,37 +50,19 @@ const CheckoutPaymentSubpageWithRef: RefForwardingComponent< promoCodeDiscountFormRef.current?.dispatchEvent( new Event("submit", { cancelable: true }) ); - } else if (checkoutGatewayFormRef.current) { - checkoutGatewayFormRef.current.dispatchEvent( + } else if (paymentGatewayFormRef.current) { + paymentGatewayFormRef.current.dispatchEvent( new Event("submit", { cancelable: true }) ); } else { changeSubmitProgress(false); - setGatewayErrors([ + onPaymentGatewayError([ { message: intl.formatMessage(commonMessages.choosePaymentMethod) }, ]); } }, })); - const handleProcessPayment = async ( - gateway: string, - token: string, - cardData?: ICardData - ) => { - const { dataError } = await createPayment(gateway, token, cardData); - const errors = dataError?.error; - changeSubmitProgress(false); - if (errors) { - setGatewayErrors(errors); - } else { - setGatewayErrors([]); - onSubmitSuccess(); - } - }; - const handlePaymentGatewayError = () => { - changeSubmitProgress(false); - }; const handleAddPromoCode = async (promoCode: string) => { const { dataError } = await addPromoCode(promoCode); const errors = dataError?.error; @@ -102,13 +71,13 @@ const CheckoutPaymentSubpageWithRef: RefForwardingComponent< setPromoCodeErrors(errors); } else { setPromoCodeErrors([]); - if (checkoutGatewayFormRef.current) { - checkoutGatewayFormRef.current.dispatchEvent( + if (paymentGatewayFormRef.current) { + paymentGatewayFormRef.current.dispatchEvent( new Event("submit", { cancelable: true }) ); } else { changeSubmitProgress(false); - setGatewayErrors([ + onPaymentGatewayError([ { message: intl.formatMessage(commonMessages.choosePaymentMethod) }, ]); } @@ -122,26 +91,26 @@ const CheckoutPaymentSubpageWithRef: RefForwardingComponent< setPromoCodeErrors(errors); } else { setPromoCodeErrors([]); - if (checkoutGatewayFormRef.current) { - checkoutGatewayFormRef.current.dispatchEvent( + if (paymentGatewayFormRef.current) { + paymentGatewayFormRef.current.dispatchEvent( new Event("submit", { cancelable: true }) ); } else { changeSubmitProgress(false); - setGatewayErrors([ + onPaymentGatewayError([ { message: intl.formatMessage(commonMessages.choosePaymentMethod) }, ]); } } }; const handleSubmitUnchangedDiscount = () => { - if (checkoutGatewayFormRef.current) { - checkoutGatewayFormRef.current.dispatchEvent( + if (paymentGatewayFormRef.current) { + paymentGatewayFormRef.current.dispatchEvent( new Event("submit", { cancelable: true }) ); } else { changeSubmitProgress(false); - setGatewayErrors([ + onPaymentGatewayError([ { message: intl.formatMessage(commonMessages.choosePaymentMethod) }, ]); } @@ -150,11 +119,6 @@ const CheckoutPaymentSubpageWithRef: RefForwardingComponent< return ( ); };