From 29d83a22ae68144dce3249472d370700684a5170 Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Thu, 17 Jun 2021 19:01:47 +0200 Subject: [PATCH 01/10] Add 3ds support to Stripe - test commit --- .../AdyenPaymentGateway.tsx | 28 +---- .../organisms/CheckoutPayment/fixtures.ts | 5 +- .../PaymentGatewaysList.tsx | 2 + .../organisms/PaymentGatewaysList/fixtures.ts | 5 +- .../organisms/PaymentGatewaysList/types.ts | 5 +- .../StripePaymentGateway.tsx | 119 +++++++++++++++++- .../organisms/StripePaymentGateway/types.ts | 12 ++ src/@next/pages/CheckoutPage/CheckoutPage.tsx | 14 ++- .../subpages/CheckoutReviewSubpage.tsx | 11 +- src/constants.ts | 6 + src/intl.ts | 15 +++ 11 files changed, 185 insertions(+), 37 deletions(-) diff --git a/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx b/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx index 99b306b139..caad15bf59 100755 --- a/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx +++ b/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState } from "react"; import { defineMessages, IntlShape, useIntl } from "react-intl"; import { ErrorMessage } from "@components/atoms"; +import { paymentStatusMessages } from "@temp/intl"; import { IFormError, IPaymentGatewayConfig } from "@types"; export const adyenNotNegativeConfirmationStatusCodes = [ @@ -25,21 +26,6 @@ export const adyenErrorMessages = defineMessages({ defaultMessage: "Invalid payment submission.", description: messageDescription, }, - cannotHandlePaymentConfirmation: { - defaultMessage: - "Payment gateway did not provide payment confirmation handler.", - description: messageDescription, - }, - paymentMalformedConfirmationData: { - defaultMessage: - "Payment needs confirmation but data required for confirmation received from the server is malformed.", - description: messageDescription, - }, - paymentNoConfirmationData: { - defaultMessage: - "Payment needs confirmation but data required for confirmation not received from the server.", - description: messageDescription, - }, }); export const adyenConfirmationErrorMessages = defineMessages({ @@ -119,10 +105,7 @@ export interface IProps { /** * Method to call on gateway payment submission. */ - submitPayment: (data: { - confirmationData: any; - confirmationNeeded: boolean; - }) => Promise; + submitPayment: (data: object) => Promise; /** * Method called after succesful gateway payment submission. This is the case when no confirmation is needed. */ @@ -222,14 +205,14 @@ const AdyenPaymentGateway: React.FC = ({ onError([ new Error( intl.formatMessage( - adyenErrorMessages.cannotHandlePaymentConfirmation + paymentStatusMessages.cannotHandlePaymentConfirmation ) ), ]); } else if (!payment?.confirmationData) { onError([ new Error( - intl.formatMessage(adyenErrorMessages.paymentNoConfirmationData) + intl.formatMessage(paymentStatusMessages.paymentNoConfirmationData) ), ]); } else { @@ -240,10 +223,11 @@ const AdyenPaymentGateway: React.FC = ({ onError([ new Error( intl.formatMessage( - adyenErrorMessages.paymentMalformedConfirmationData + paymentStatusMessages.paymentMalformedConfirmationData ) ), ]); + return; } try { dropin.handleAction(paymentAction); diff --git a/src/@next/components/organisms/CheckoutPayment/fixtures.ts b/src/@next/components/organisms/CheckoutPayment/fixtures.ts index 36dbac6ba9..97333641a4 100644 --- a/src/@next/components/organisms/CheckoutPayment/fixtures.ts +++ b/src/@next/components/organisms/CheckoutPayment/fixtures.ts @@ -1,3 +1,4 @@ +import { paymentGatewayNames } from "@temp/constants"; import { IPaymentGateway } from "@types"; export const paymentGateways: IPaymentGateway[] = [ @@ -8,7 +9,7 @@ export const paymentGateways: IPaymentGateway[] = [ value: "false", }, ], - id: "mirumee.payments.dummy", + id: paymentGatewayNames.dummy, name: "Dummy", }, { @@ -22,7 +23,7 @@ export const paymentGateways: IPaymentGateway[] = [ value: "false", }, ], - id: "mirumee.payments.stripe", + id: paymentGatewayNames.stripe, name: "Stripe", }, ]; diff --git a/src/@next/components/organisms/PaymentGatewaysList/PaymentGatewaysList.tsx b/src/@next/components/organisms/PaymentGatewaysList/PaymentGatewaysList.tsx index 0892a16acc..a9e1ca2fab 100755 --- a/src/@next/components/organisms/PaymentGatewaysList/PaymentGatewaysList.tsx +++ b/src/@next/components/organisms/PaymentGatewaysList/PaymentGatewaysList.tsx @@ -125,6 +125,8 @@ const PaymentGatewaysList: React.FC = ({ processPayment={(token, cardData) => processPayment(id, token, cardData) } + submitPayment={submitPayment} + submitPaymentSuccess={submitPaymentSuccess} errors={errors} onError={onError} /> diff --git a/src/@next/components/organisms/PaymentGatewaysList/fixtures.ts b/src/@next/components/organisms/PaymentGatewaysList/fixtures.ts index 701a340768..98758db1fb 100644 --- a/src/@next/components/organisms/PaymentGatewaysList/fixtures.ts +++ b/src/@next/components/organisms/PaymentGatewaysList/fixtures.ts @@ -1,3 +1,4 @@ +import { paymentGatewayNames } from "@temp/constants"; import { IPaymentGateway } from "@types"; export const paymentGateways: IPaymentGateway[] = [ @@ -8,7 +9,7 @@ export const paymentGateways: IPaymentGateway[] = [ value: "false", }, ], - id: "mirumee.payments.dummy", + id: paymentGatewayNames.dummy, name: "Dummy", }, { @@ -22,7 +23,7 @@ export const paymentGateways: IPaymentGateway[] = [ value: "false", }, ], - id: "mirumee.payments.stripe", + id: paymentGatewayNames.stripe, name: "Stripe", }, ]; diff --git a/src/@next/components/organisms/PaymentGatewaysList/types.ts b/src/@next/components/organisms/PaymentGatewaysList/types.ts index d65ea965ec..768a6f0972 100755 --- a/src/@next/components/organisms/PaymentGatewaysList/types.ts +++ b/src/@next/components/organisms/PaymentGatewaysList/types.ts @@ -39,10 +39,7 @@ export interface IProps { token?: string, cardData?: ICardData ) => void; - submitPayment: (data: { - confirmationData: any; - confirmationNeeded: boolean; - }) => Promise; + submitPayment: (data?: object) => Promise; submitPaymentSuccess: ( order?: CompleteCheckout_checkoutComplete_order ) => void; diff --git a/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx b/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx index e1f1f490e3..6cc73529ff 100755 --- a/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx +++ b/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx @@ -1,24 +1,49 @@ -import { CardNumberElement, Elements } from "@stripe/react-stripe-js"; -import { loadStripe, Stripe, StripeElements } from "@stripe/stripe-js"; -import React, { useMemo, useState } from "react"; +import { + CardElement, + CardNumberElement, + Elements, +} from "@stripe/react-stripe-js"; +import { + loadStripe, + PaymentIntent, + PaymentMethod, + Stripe, + StripeCardElement, + StripeElements, +} from "@stripe/stripe-js"; +import React, { useEffect, useMemo, useState } from "react"; +import { useIntl } from "react-intl"; +import { paymentStatusMessages } from "@temp/intl"; import { IFormError } from "@types"; import { StripeCreditCardForm } from "../StripeCreditCardForm"; import { IProps } from "./types"; +interface StripeConfirmationData { + client_secret: string; + id: string; +} + /** * Stripe payment gateway. */ const StripePaymentGateway: React.FC = ({ config, processPayment, + submitPayment, + submitPaymentSuccess, formRef, formId, errors = [], onError, }: IProps) => { + const intl = useIntl(); + const [submitErrors, setSubmitErrors] = useState([]); + const [paymentMethod, setPaymentMethod] = useState(); + + console.log("stripe GLOBAL paymentMethod", paymentMethod); const apiKey = config.find(({ field }) => field === "api_key")?.value; @@ -40,6 +65,8 @@ const StripePaymentGateway: React.FC = ({ stripe: Stripe | null, elements: StripeElements | null ) => { + console.log("stripe handleFormSubmit"); + const cartNumberElement = elements?.getElement(CardNumberElement); if (cartNumberElement) { @@ -66,6 +93,8 @@ const StripePaymentGateway: React.FC = ({ firstDigits: null, lastDigits: card?.last4, }); + console.log("stripe setPaymentMethod", payload.paymentMethod); + setPaymentMethod(payload.paymentMethod); } } else { const stripePayloadErrors = [ @@ -89,6 +118,90 @@ const StripePaymentGateway: React.FC = ({ } }; + const handleFormCompleteSubmit = async () => { + console.log("stripe handleFormCompleteSubmit 1"); + + const stripe = await stripePromise; + + const payment = await submitPayment(); + + console.log("stripe handleFormCompleteSubmit 2", payment); + + if (payment.errors?.length) { + onError(payment.errors); + } else if (!payment?.confirmationNeeded) { + submitPaymentSuccess(payment?.order); + } else if (!stripe?.confirmCardPayment) { + onError([ + new Error( + intl.formatMessage( + paymentStatusMessages.cannotHandlePaymentConfirmation + ) + ), + ]); + } else if (!payment?.confirmationData) { + onError([ + new Error( + intl.formatMessage(paymentStatusMessages.paymentNoConfirmationData) + ), + ]); + } else { + let paymentAction; + try { + paymentAction = JSON.parse( + payment.confirmationData + ) as StripeConfirmationData; + } catch (parseError) { + onError([ + new Error( + intl.formatMessage( + paymentStatusMessages.paymentMalformedConfirmationData + ) + ), + ]); + return; + } + console.log("stripe paymentMethod", paymentMethod); + if (!paymentMethod?.id) { + console.log("stripe handleFormCompleteSubmit no card", paymentAction); + return; + } + console.log("stripe handleFormCompleteSubmit 3", paymentAction); + let confirmation; + try { + confirmation = await stripe.confirmCardPayment( + paymentAction.client_secret, + { + payment_method: paymentMethod.id, + } + ); + } catch (error) { + onError([new Error(error)]); + return; + } + if (confirmation.error) { + onError([new Error(confirmation.error.message)]); + } else { + handleFormCompleteSubmit(); + } + } + }; + + useEffect(() => { + if (stripePromise) { + (formRef?.current as any)?.addEventListener( + "submitComplete", + handleFormCompleteSubmit + ); + } + return () => { + (formRef?.current as any)?.removeEventListener( + "submitComplete", + handleFormCompleteSubmit + ); + }; + }, [formRef, stripePromise, paymentMethod]); + const allErrors = [...errors, ...submitErrors]; return ( diff --git a/src/@next/components/organisms/StripePaymentGateway/types.ts b/src/@next/components/organisms/StripePaymentGateway/types.ts index 938f081652..c984391772 100755 --- a/src/@next/components/organisms/StripePaymentGateway/types.ts +++ b/src/@next/components/organisms/StripePaymentGateway/types.ts @@ -1,3 +1,5 @@ +import { CompleteCheckout_checkoutComplete_order } from "@saleor/sdk/lib/mutations/gqlTypes/CompleteCheckout"; + import { ICardData, IFormError, IPaymentGatewayConfig } from "@types"; export interface IProps { @@ -21,6 +23,16 @@ export interface IProps { * Method called after the form is submitted. Passed token attribute will be used to create payment. */ processPayment: (token: string, cardData: ICardData) => void; + /** + * Method to call on gateway payment submission. + */ + submitPayment: (data?: object) => Promise; + /** + * Method called after succesful gateway payment submission. This is the case when no confirmation is needed. + */ + submitPaymentSuccess: ( + order?: CompleteCheckout_checkoutComplete_order + ) => void; /** * Method called when gateway error occured. */ diff --git a/src/@next/pages/CheckoutPage/CheckoutPage.tsx b/src/@next/pages/CheckoutPage/CheckoutPage.tsx index 76b12a5279..28e6e8b7fe 100755 --- a/src/@next/pages/CheckoutPage/CheckoutPage.tsx +++ b/src/@next/pages/CheckoutPage/CheckoutPage.tsx @@ -16,6 +16,7 @@ import { import { Checkout } from "@components/templates"; import { useRedirectToCorrectCheckoutStep } from "@hooks"; import { paths } from "@paths"; +import { paymentGatewayNames } from "@temp/constants"; import { ICardData, IFormError } from "@types"; import { @@ -124,6 +125,7 @@ const CheckoutPage: React.FC = () => { token?: string, cardData?: ICardData ) => { + console.log("handleProcessPayment", gateway, token, cardData); const paymentConfirmStepLink = CHECKOUT_STEPS.find( step => step.step === CheckoutStep.PaymentConfirm )?.link; @@ -144,6 +146,7 @@ const CheckoutPage: React.FC = () => { }; const handleSubmitPayment = async (paymentData?: object) => { + console.log("handleSubmitPayment", paymentData); const response = await completeCheckout({ paymentData }); return { confirmationData: response.data?.confirmationData, @@ -156,6 +159,7 @@ const CheckoutPage: React.FC = () => { const handleSubmitPaymentSuccess = ( order?: CompleteCheckout_checkoutComplete_order ) => { + console.log("handleSubmitPaymentSuccess", order); setSubmitInProgress(false); setPaymentGatewayErrors([]); handleStepSubmitSuccess(CheckoutStep.Review, { @@ -196,7 +200,10 @@ const CheckoutPage: React.FC = () => { /** * Prevent proceeding in confirmation flow in case of gateways that don't support it to prevent unknown bugs. */ - if (payment?.gateway !== "mirumee.payments.adyen") { + if ( + payment?.gateway !== paymentGatewayNames.adyen && + payment?.gateway !== paymentGatewayNames.stripe + ) { const paymentStepLink = steps.find( step => step.step === CheckoutStep.Payment )?.link; @@ -205,6 +212,11 @@ const CheckoutPage: React.FC = () => { } } + if (payment?.gateway === paymentGatewayNames.stripe) { + console.log("handlePaymentConfirm"); + return; + } + setSubmitInProgress(true); setPaymentConfirmation(true); /** diff --git a/src/@next/pages/CheckoutPage/subpages/CheckoutReviewSubpage.tsx b/src/@next/pages/CheckoutPage/subpages/CheckoutReviewSubpage.tsx index bda4563db8..968c3a1ffb 100644 --- a/src/@next/pages/CheckoutPage/subpages/CheckoutReviewSubpage.tsx +++ b/src/@next/pages/CheckoutPage/subpages/CheckoutReviewSubpage.tsx @@ -8,6 +8,7 @@ import React, { import { CheckoutReview } from "@components/organisms"; import { statuses as dummyStatuses } from "@components/organisms/DummyPaymentGateway"; +import { paymentGatewayNames } from "@temp/constants"; import { IFormError } from "@types"; import { @@ -59,14 +60,14 @@ const CheckoutReviewSubpageWithRef: RefForwardingComponent< : undefined; const getPaymentMethodDescription = () => { - if (payment?.gateway === "mirumee.payments.dummy") { + if (payment?.gateway === paymentGatewayNames.dummy) { return `Dummy: ${ dummyStatuses.find( status => status.token === selectedPaymentGatewayToken )?.label }`; } - if (payment?.gateway === "mirumee.payments.adyen") { + if (payment?.gateway === paymentGatewayNames.adyen) { return `Adyen payments`; } if (payment?.creditCard) { @@ -79,7 +80,11 @@ const CheckoutReviewSubpageWithRef: RefForwardingComponent< changeSubmitProgress(true); let data; let dataError; - if (payment?.gateway === "mirumee.payments.adyen") { + if (payment?.gateway === paymentGatewayNames.adyen) { + paymentGatewayFormRef.current?.dispatchEvent( + new Event("submitComplete", { cancelable: true }) + ); + } else if (payment?.gateway === paymentGatewayNames.stripe) { paymentGatewayFormRef.current?.dispatchEvent( new Event("submitComplete", { cancelable: true }) ); diff --git a/src/constants.ts b/src/constants.ts index 889a3089cc..5fbc5ba8ac 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -27,3 +27,9 @@ export const staticPathsFetchBatch = 50; export const staticPathsFallback = (exportMode ? false : process.env.NEXT_PUBLIC_STATIC_PATHS_FALLBACK) as boolean | "blocking"; + +export const paymentGatewayNames = { + dummy: "mirumee.payments.dummy", + adyen: "mirumee.payments.adyen", + stripe: "saleor.payments.stripe", +}; diff --git a/src/intl.ts b/src/intl.ts index 370d505d34..64f0436d33 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -220,6 +220,21 @@ export const paymentStatusMessages = defineMessages({ fullyRefunded: { defaultMessage: "Fully refunded", }, + paymentNoConfirmationData: { + defaultMessage: + "Payment needs confirmation but data required for confirmation not received from the server.", + description: "payment gateway error", + }, + paymentMalformedConfirmationData: { + defaultMessage: + "Payment needs confirmation but data required for confirmation received from the server is malformed.", + description: "payment gateway error", + }, + cannotHandlePaymentConfirmation: { + defaultMessage: + "Payment gateway did not provide payment confirmation handler.", + description: "payment gateway error", + }, }); export const orderStatusMessages = defineMessages({ From 4a7e50d6d8d89309a08a1997df50664408ebdc6f Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Fri, 18 Jun 2021 18:21:54 +0200 Subject: [PATCH 02/10] Clean ups --- .../StripePaymentGateway.tsx | 66 ++++++++++--------- .../StripePaymentGateway/stories.tsx | 6 ++ .../organisms/StripePaymentGateway/test.tsx | 17 +++-- src/@next/pages/CheckoutPage/CheckoutPage.tsx | 8 --- 4 files changed, 53 insertions(+), 44 deletions(-) diff --git a/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx b/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx index 6cc73529ff..d0de11f38a 100755 --- a/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx +++ b/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx @@ -1,18 +1,12 @@ -import { - CardElement, - CardNumberElement, - Elements, -} from "@stripe/react-stripe-js"; +import { CardNumberElement, Elements } from "@stripe/react-stripe-js"; import { loadStripe, - PaymentIntent, PaymentMethod, Stripe, - StripeCardElement, StripeElements, } from "@stripe/stripe-js"; import React, { useEffect, useMemo, useState } from "react"; -import { useIntl } from "react-intl"; +import { defineMessages, useIntl } from "react-intl"; import { paymentStatusMessages } from "@temp/intl"; import { IFormError } from "@types"; @@ -20,6 +14,29 @@ import { IFormError } from "@types"; import { StripeCreditCardForm } from "../StripeCreditCardForm"; import { IProps } from "./types"; +const messageDescription = "Stripe payment gateway error"; + +export const stripeErrorMessages = defineMessages({ + gatewayMisconfigured: { + defaultMessage: "Stripe gateway misconfigured. Api key not provided.", + description: messageDescription, + }, + paymentSubmissionError: { + defaultMessage: + "Payment submission error. Stripe gateway returned no payment method in payload.", + description: messageDescription, + }, + geytwayDisplayError: { + defaultMessage: + "Stripe payment gateway couldn't be displayed. Stripe elements were not provided.", + description: messageDescription, + }, + paymentMethodNotCreated: { + defaultMessage: "Payment method has not been created.", + description: messageDescription, + }, +}); + interface StripeConfirmationData { client_secret: string; id: string; @@ -43,8 +60,6 @@ const StripePaymentGateway: React.FC = ({ const [submitErrors, setSubmitErrors] = useState([]); const [paymentMethod, setPaymentMethod] = useState(); - console.log("stripe GLOBAL paymentMethod", paymentMethod); - const apiKey = config.find(({ field }) => field === "api_key")?.value; const stripePromise = useMemo(() => { @@ -52,9 +67,7 @@ const StripePaymentGateway: React.FC = ({ return loadStripe(apiKey); } const stripeApiKeyErrors = [ - { - message: "Stripe gateway misconfigured. Api key not provided.", - }, + new Error(intl.formatMessage(stripeErrorMessages.gatewayMisconfigured)), ]; setSubmitErrors(stripeApiKeyErrors); onError(stripeApiKeyErrors); @@ -65,8 +78,6 @@ const StripePaymentGateway: React.FC = ({ stripe: Stripe | null, elements: StripeElements | null ) => { - console.log("stripe handleFormSubmit"); - const cartNumberElement = elements?.getElement(CardNumberElement); if (cartNumberElement) { @@ -93,25 +104,20 @@ const StripePaymentGateway: React.FC = ({ firstDigits: null, lastDigits: card?.last4, }); - console.log("stripe setPaymentMethod", payload.paymentMethod); setPaymentMethod(payload.paymentMethod); } } else { const stripePayloadErrors = [ - { - message: - "Payment submission error. Stripe gateway returned no payment method in payload.", - }, + new Error( + intl.formatMessage(stripeErrorMessages.paymentSubmissionError) + ), ]; setSubmitErrors(stripePayloadErrors); onError(stripePayloadErrors); } } else { const stripeElementsErrors = [ - { - message: - "Stripe gateway improperly rendered. Stripe elements were not provided.", - }, + new Error(intl.formatMessage(stripeErrorMessages.geytwayDisplayError)), ]; setSubmitErrors(stripeElementsErrors); onError(stripeElementsErrors); @@ -119,14 +125,10 @@ const StripePaymentGateway: React.FC = ({ }; const handleFormCompleteSubmit = async () => { - console.log("stripe handleFormCompleteSubmit 1"); - const stripe = await stripePromise; const payment = await submitPayment(); - console.log("stripe handleFormCompleteSubmit 2", payment); - if (payment.errors?.length) { onError(payment.errors); } else if (!payment?.confirmationNeeded) { @@ -161,12 +163,14 @@ const StripePaymentGateway: React.FC = ({ ]); return; } - console.log("stripe paymentMethod", paymentMethod); if (!paymentMethod?.id) { - console.log("stripe handleFormCompleteSubmit no card", paymentAction); + onError([ + new Error( + intl.formatMessage(stripeErrorMessages.paymentMethodNotCreated) + ), + ]); return; } - console.log("stripe handleFormCompleteSubmit 3", paymentAction); let confirmation; try { confirmation = await stripe.confirmCardPayment( diff --git a/src/@next/components/organisms/StripePaymentGateway/stories.tsx b/src/@next/components/organisms/StripePaymentGateway/stories.tsx index c9a40903b8..ba32e5bdda 100644 --- a/src/@next/components/organisms/StripePaymentGateway/stories.tsx +++ b/src/@next/components/organisms/StripePaymentGateway/stories.tsx @@ -1,6 +1,7 @@ import { action } from "@storybook/addon-actions"; import { storiesOf } from "@storybook/react"; import React from "react"; +import { IntlProvider } from "react-intl"; import { StripePaymentGateway } from "."; @@ -8,14 +9,19 @@ const config = [ { field: "api_key", value: "pk_test_6pRNASCoBOKtIshFeQd4XMUh" }, ]; const processPayment = action("processPayment"); +const submitPayment = async () => action("submitPayment"); +const submitPaymentSuccess = action("submitPaymentSuccess"); const onError = action("onError"); storiesOf("@components/organisms/StripePaymentGateway", module) .addParameters({ component: StripePaymentGateway }) + .addDecorator(story => {story()}) .add("default", () => ( )); diff --git a/src/@next/components/organisms/StripePaymentGateway/test.tsx b/src/@next/components/organisms/StripePaymentGateway/test.tsx index 1ac67f2db8..81363993a6 100644 --- a/src/@next/components/organisms/StripePaymentGateway/test.tsx +++ b/src/@next/components/organisms/StripePaymentGateway/test.tsx @@ -2,6 +2,7 @@ import "jest-styled-components"; import { shallow } from "enzyme"; import React from "react"; +import { IntlProvider } from "react-intl"; import { StripePaymentGateway } from "."; @@ -12,13 +13,19 @@ const config = [ describe("", () => { it("exists", () => { const processPayment = jest.fn(); + const submitPayment = jest.fn(); + const submitPaymentSuccess = jest.fn(); const onError = jest.fn(); const wrapper = shallow( - + + + ); expect(wrapper.exists()).toEqual(true); diff --git a/src/@next/pages/CheckoutPage/CheckoutPage.tsx b/src/@next/pages/CheckoutPage/CheckoutPage.tsx index 28e6e8b7fe..d9101a114d 100755 --- a/src/@next/pages/CheckoutPage/CheckoutPage.tsx +++ b/src/@next/pages/CheckoutPage/CheckoutPage.tsx @@ -125,7 +125,6 @@ const CheckoutPage: React.FC = () => { token?: string, cardData?: ICardData ) => { - console.log("handleProcessPayment", gateway, token, cardData); const paymentConfirmStepLink = CHECKOUT_STEPS.find( step => step.step === CheckoutStep.PaymentConfirm )?.link; @@ -146,7 +145,6 @@ const CheckoutPage: React.FC = () => { }; const handleSubmitPayment = async (paymentData?: object) => { - console.log("handleSubmitPayment", paymentData); const response = await completeCheckout({ paymentData }); return { confirmationData: response.data?.confirmationData, @@ -159,7 +157,6 @@ const CheckoutPage: React.FC = () => { const handleSubmitPaymentSuccess = ( order?: CompleteCheckout_checkoutComplete_order ) => { - console.log("handleSubmitPaymentSuccess", order); setSubmitInProgress(false); setPaymentGatewayErrors([]); handleStepSubmitSuccess(CheckoutStep.Review, { @@ -212,11 +209,6 @@ const CheckoutPage: React.FC = () => { } } - if (payment?.gateway === paymentGatewayNames.stripe) { - console.log("handlePaymentConfirm"); - return; - } - setSubmitInProgress(true); setPaymentConfirmation(true); /** From 489fc864dc3a05e92235c425c06bad10f29a7297 Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Fri, 18 Jun 2021 18:36:48 +0200 Subject: [PATCH 03/10] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da351f1b8d..f4f32b2252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable, unreleased changes to this project will be documented in this file. - Update register mutation with channel slug - #1045 by @orzechdev - Update queries with new channel API - #1072 by @orzechdev - Change order number format - #1073 by @kamilpastuszka +- Add 3DS support to Stripe - #1057 by @orzechdev ## 2.11.0 From 2cdf77722d56b0b1e0ac41d2e778d7d3c900a1bb Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Fri, 18 Jun 2021 18:40:38 +0200 Subject: [PATCH 04/10] Create common payment error messages --- .../organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx | 8 ++++---- .../StripePaymentGateway/StripePaymentGateway.tsx | 8 ++++---- src/intl.ts | 3 +++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx b/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx index caad15bf59..611887a0da 100755 --- a/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx +++ b/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from "react"; import { defineMessages, IntlShape, useIntl } from "react-intl"; import { ErrorMessage } from "@components/atoms"; -import { paymentStatusMessages } from "@temp/intl"; +import { paymentErrorMessages } from "@temp/intl"; import { IFormError, IPaymentGatewayConfig } from "@types"; export const adyenNotNegativeConfirmationStatusCodes = [ @@ -205,14 +205,14 @@ const AdyenPaymentGateway: React.FC = ({ onError([ new Error( intl.formatMessage( - paymentStatusMessages.cannotHandlePaymentConfirmation + paymentErrorMessages.cannotHandlePaymentConfirmation ) ), ]); } else if (!payment?.confirmationData) { onError([ new Error( - intl.formatMessage(paymentStatusMessages.paymentNoConfirmationData) + intl.formatMessage(paymentErrorMessages.paymentNoConfirmationData) ), ]); } else { @@ -223,7 +223,7 @@ const AdyenPaymentGateway: React.FC = ({ onError([ new Error( intl.formatMessage( - paymentStatusMessages.paymentMalformedConfirmationData + paymentErrorMessages.paymentMalformedConfirmationData ) ), ]); diff --git a/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx b/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx index d0de11f38a..7f87040693 100755 --- a/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx +++ b/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx @@ -8,7 +8,7 @@ import { import React, { useEffect, useMemo, useState } from "react"; import { defineMessages, useIntl } from "react-intl"; -import { paymentStatusMessages } from "@temp/intl"; +import { paymentErrorMessages } from "@temp/intl"; import { IFormError } from "@types"; import { StripeCreditCardForm } from "../StripeCreditCardForm"; @@ -137,14 +137,14 @@ const StripePaymentGateway: React.FC = ({ onError([ new Error( intl.formatMessage( - paymentStatusMessages.cannotHandlePaymentConfirmation + paymentErrorMessages.cannotHandlePaymentConfirmation ) ), ]); } else if (!payment?.confirmationData) { onError([ new Error( - intl.formatMessage(paymentStatusMessages.paymentNoConfirmationData) + intl.formatMessage(paymentErrorMessages.paymentNoConfirmationData) ), ]); } else { @@ -157,7 +157,7 @@ const StripePaymentGateway: React.FC = ({ onError([ new Error( intl.formatMessage( - paymentStatusMessages.paymentMalformedConfirmationData + paymentErrorMessages.paymentMalformedConfirmationData ) ), ]); diff --git a/src/intl.ts b/src/intl.ts index 64f0436d33..8903e5b4a4 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -220,6 +220,9 @@ export const paymentStatusMessages = defineMessages({ fullyRefunded: { defaultMessage: "Fully refunded", }, +}); + +export const paymentErrorMessages = defineMessages({ paymentNoConfirmationData: { defaultMessage: "Payment needs confirmation but data required for confirmation not received from the server.", From 2d86253dfbf4dc791bf8841145fdb1dcdffb52eb Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Wed, 23 Jun 2021 11:11:50 +0200 Subject: [PATCH 05/10] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f32b2252..06b1668076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ All notable, unreleased changes to this project will be documented in this file. - Update register mutation with channel slug - #1045 by @orzechdev - Update queries with new channel API - #1072 by @orzechdev - Change order number format - #1073 by @kamilpastuszka -- Add 3DS support to Stripe - #1057 by @orzechdev +- Use new Stripe payment gateway - #1057 by @orzechdev ## 2.11.0 From 959184331323feac5f6149e72a4a5e5b4f7b48c9 Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Tue, 6 Jul 2021 18:37:15 +0200 Subject: [PATCH 06/10] Update payment gateways types and messages --- .../AdyenPaymentGateway.tsx | 63 +++---------------- .../organisms/AdyenPaymentGateway/index.ts | 1 + .../organisms/AdyenPaymentGateway/intl.ts | 50 +++++++++++++++ .../organisms/AdyenPaymentGateway/stories.tsx | 2 +- .../organisms/PaymentGatewaysList/stories.tsx | 2 +- .../organisms/PaymentGatewaysList/types.ts | 11 +++- .../StripePaymentGateway.tsx | 26 +------- .../organisms/StripePaymentGateway/intl.ts | 24 +++++++ .../StripePaymentGateway/stories.tsx | 2 +- .../organisms/StripePaymentGateway/types.ts | 11 +++- src/@next/pages/CheckoutPage/CheckoutPage.tsx | 6 +- src/@next/types/IPaymentGateway.ts | 11 ++++ 12 files changed, 120 insertions(+), 89 deletions(-) create mode 100644 src/@next/components/organisms/AdyenPaymentGateway/intl.ts create mode 100644 src/@next/components/organisms/StripePaymentGateway/intl.ts diff --git a/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx b/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx index 611887a0da..a82c067e33 100755 --- a/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx +++ b/src/@next/components/organisms/AdyenPaymentGateway/AdyenPaymentGateway.tsx @@ -1,10 +1,16 @@ import { CompleteCheckout_checkoutComplete_order } from "@saleor/sdk/lib/mutations/gqlTypes/CompleteCheckout"; import React, { useEffect, useRef, useState } from "react"; -import { defineMessages, IntlShape, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; import { ErrorMessage } from "@components/atoms"; import { paymentErrorMessages } from "@temp/intl"; -import { IFormError, IPaymentGatewayConfig } from "@types"; +import { + IFormError, + IPaymentGatewayConfig, + IPaymentSubmitResult, +} from "@types"; + +import { adyenErrorMessages } from "./intl"; export const adyenNotNegativeConfirmationStatusCodes = [ "Authorised", @@ -15,55 +21,6 @@ export const adyenNotNegativeConfirmationStatusCodes = [ "PresentToShopper", ]; -const messageDescription = "Adyen payment gateway error"; - -export const adyenErrorMessages = defineMessages({ - unknownPayment: { - defaultMessage: "Unknown payment submission error occured.", - description: messageDescription, - }, - invalidPaymentSubmission: { - defaultMessage: "Invalid payment submission.", - description: messageDescription, - }, -}); - -export const adyenConfirmationErrorMessages = defineMessages({ - error: { - defaultMessage: "Error processing payment occured.", - description: messageDescription, - }, - refused: { - defaultMessage: - "The payment was refused. Try the payment again using a different payment method or card.", - description: messageDescription, - }, - cancelled: { - defaultMessage: "Payment was cancelled.", - description: messageDescription, - }, - general: { - defaultMessage: "Payment confirmation went wrong.", - description: messageDescription, - }, -}); - -export function translateAdyenConfirmationError( - status: string, - intl: IntlShape -): string { - switch (status) { - case "Error": - return intl.formatMessage(adyenConfirmationErrorMessages.error); - case "Refused": - return intl.formatMessage(adyenConfirmationErrorMessages.refused); - case "Cancelled": - return intl.formatMessage(adyenConfirmationErrorMessages.cancelled); - default: - return intl.formatMessage(adyenConfirmationErrorMessages.general); - } -} - interface IResourceConfig { src: string; integrity: string; @@ -105,12 +62,12 @@ export interface IProps { /** * Method to call on gateway payment submission. */ - submitPayment: (data: object) => Promise; + submitPayment: (data: object) => Promise; /** * Method called after succesful gateway payment submission. This is the case when no confirmation is needed. */ submitPaymentSuccess: ( - order?: CompleteCheckout_checkoutComplete_order + order?: CompleteCheckout_checkoutComplete_order | null ) => void; /** * Errors returned by the payment gateway. diff --git a/src/@next/components/organisms/AdyenPaymentGateway/index.ts b/src/@next/components/organisms/AdyenPaymentGateway/index.ts index fb8a3cd3db..9843f68f1f 100755 --- a/src/@next/components/organisms/AdyenPaymentGateway/index.ts +++ b/src/@next/components/organisms/AdyenPaymentGateway/index.ts @@ -1 +1,2 @@ export * from "./AdyenPaymentGateway"; +export * from "./intl"; diff --git a/src/@next/components/organisms/AdyenPaymentGateway/intl.ts b/src/@next/components/organisms/AdyenPaymentGateway/intl.ts new file mode 100644 index 0000000000..616d0f54a0 --- /dev/null +++ b/src/@next/components/organisms/AdyenPaymentGateway/intl.ts @@ -0,0 +1,50 @@ +import { defineMessages, IntlShape } from "react-intl"; + +const messageDescription = "Adyen payment gateway error"; + +export const adyenErrorMessages = defineMessages({ + unknownPayment: { + defaultMessage: "Unknown payment submission error occured.", + description: messageDescription, + }, + invalidPaymentSubmission: { + defaultMessage: "Invalid payment submission.", + description: messageDescription, + }, +}); + +export const adyenConfirmationErrorMessages = defineMessages({ + error: { + defaultMessage: "Error processing payment occured.", + description: messageDescription, + }, + refused: { + defaultMessage: + "The payment was refused. Try the payment again using a different payment method or card.", + description: messageDescription, + }, + cancelled: { + defaultMessage: "Payment was cancelled.", + description: messageDescription, + }, + general: { + defaultMessage: "Payment confirmation went wrong.", + description: messageDescription, + }, +}); + +export function translateAdyenConfirmationError( + status: string, + intl: IntlShape +): string { + switch (status) { + case "Error": + return intl.formatMessage(adyenConfirmationErrorMessages.error); + case "Refused": + return intl.formatMessage(adyenConfirmationErrorMessages.refused); + case "Cancelled": + return intl.formatMessage(adyenConfirmationErrorMessages.cancelled); + default: + return intl.formatMessage(adyenConfirmationErrorMessages.general); + } +} diff --git a/src/@next/components/organisms/AdyenPaymentGateway/stories.tsx b/src/@next/components/organisms/AdyenPaymentGateway/stories.tsx index d81b8e9cce..509bb546ef 100644 --- a/src/@next/components/organisms/AdyenPaymentGateway/stories.tsx +++ b/src/@next/components/organisms/AdyenPaymentGateway/stories.tsx @@ -33,7 +33,7 @@ const PROPS = { ], }; const processPayment = action("processPayment"); -const submitPayment = async () => action("submitPayment"); +const submitPayment = async () => Promise.resolve({}); const submitPaymentSuccess = action("submitPaymentSuccess"); const onError = action("onError"); diff --git a/src/@next/components/organisms/PaymentGatewaysList/stories.tsx b/src/@next/components/organisms/PaymentGatewaysList/stories.tsx index a65ad66d1a..cb8d43c83f 100644 --- a/src/@next/components/organisms/PaymentGatewaysList/stories.tsx +++ b/src/@next/components/organisms/PaymentGatewaysList/stories.tsx @@ -6,7 +6,7 @@ import { PaymentGatewaysList } from "."; import { paymentGateways } from "./fixtures"; const processPayment = action("processPayment"); -const submitPayment = async () => action("submitPayment"); +const submitPayment = async () => Promise.resolve({}); const submitPaymentSuccess = action("submitPaymentSuccess"); const selectPaymentGateway = action("selectPaymentGateway"); const onError = action("onError"); diff --git a/src/@next/components/organisms/PaymentGatewaysList/types.ts b/src/@next/components/organisms/PaymentGatewaysList/types.ts index 768a6f0972..eb6407d0ef 100755 --- a/src/@next/components/organisms/PaymentGatewaysList/types.ts +++ b/src/@next/components/organisms/PaymentGatewaysList/types.ts @@ -1,6 +1,11 @@ import { CompleteCheckout_checkoutComplete_order } from "@saleor/sdk/lib/mutations/gqlTypes/CompleteCheckout"; -import { ICardData, IFormError, IPaymentGateway } from "@types"; +import { + ICardData, + IFormError, + IPaymentGateway, + IPaymentSubmitResult, +} from "@types"; export interface IProps { /** @@ -39,9 +44,9 @@ export interface IProps { token?: string, cardData?: ICardData ) => void; - submitPayment: (data?: object) => Promise; + submitPayment: (data?: object) => Promise; submitPaymentSuccess: ( - order?: CompleteCheckout_checkoutComplete_order + order?: CompleteCheckout_checkoutComplete_order | null ) => void; /** * Method called when gateway error occured. diff --git a/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx b/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx index 7f87040693..fc17cc8661 100755 --- a/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx +++ b/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx @@ -6,37 +6,15 @@ import { StripeElements, } from "@stripe/stripe-js"; import React, { useEffect, useMemo, useState } from "react"; -import { defineMessages, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; import { paymentErrorMessages } from "@temp/intl"; import { IFormError } from "@types"; import { StripeCreditCardForm } from "../StripeCreditCardForm"; +import { stripeErrorMessages } from "./intl"; import { IProps } from "./types"; -const messageDescription = "Stripe payment gateway error"; - -export const stripeErrorMessages = defineMessages({ - gatewayMisconfigured: { - defaultMessage: "Stripe gateway misconfigured. Api key not provided.", - description: messageDescription, - }, - paymentSubmissionError: { - defaultMessage: - "Payment submission error. Stripe gateway returned no payment method in payload.", - description: messageDescription, - }, - geytwayDisplayError: { - defaultMessage: - "Stripe payment gateway couldn't be displayed. Stripe elements were not provided.", - description: messageDescription, - }, - paymentMethodNotCreated: { - defaultMessage: "Payment method has not been created.", - description: messageDescription, - }, -}); - interface StripeConfirmationData { client_secret: string; id: string; diff --git a/src/@next/components/organisms/StripePaymentGateway/intl.ts b/src/@next/components/organisms/StripePaymentGateway/intl.ts new file mode 100644 index 0000000000..b2e98f1981 --- /dev/null +++ b/src/@next/components/organisms/StripePaymentGateway/intl.ts @@ -0,0 +1,24 @@ +import { defineMessages } from "react-intl"; + +const messageDescription = "Stripe payment gateway error"; + +export const stripeErrorMessages = defineMessages({ + gatewayMisconfigured: { + defaultMessage: "Stripe gateway misconfigured. Api key not provided.", + description: messageDescription, + }, + paymentSubmissionError: { + defaultMessage: + "Payment submission error. Stripe gateway returned no payment method in payload.", + description: messageDescription, + }, + geytwayDisplayError: { + defaultMessage: + "Stripe payment gateway couldn't be displayed. Stripe elements were not provided.", + description: messageDescription, + }, + paymentMethodNotCreated: { + defaultMessage: "Payment method has not been created.", + description: messageDescription, + }, +}); diff --git a/src/@next/components/organisms/StripePaymentGateway/stories.tsx b/src/@next/components/organisms/StripePaymentGateway/stories.tsx index ba32e5bdda..760b0d9a97 100644 --- a/src/@next/components/organisms/StripePaymentGateway/stories.tsx +++ b/src/@next/components/organisms/StripePaymentGateway/stories.tsx @@ -9,7 +9,7 @@ const config = [ { field: "api_key", value: "pk_test_6pRNASCoBOKtIshFeQd4XMUh" }, ]; const processPayment = action("processPayment"); -const submitPayment = async () => action("submitPayment"); +const submitPayment = async () => Promise.resolve({}); const submitPaymentSuccess = action("submitPaymentSuccess"); const onError = action("onError"); diff --git a/src/@next/components/organisms/StripePaymentGateway/types.ts b/src/@next/components/organisms/StripePaymentGateway/types.ts index c984391772..f8ddbaaeaf 100755 --- a/src/@next/components/organisms/StripePaymentGateway/types.ts +++ b/src/@next/components/organisms/StripePaymentGateway/types.ts @@ -1,6 +1,11 @@ import { CompleteCheckout_checkoutComplete_order } from "@saleor/sdk/lib/mutations/gqlTypes/CompleteCheckout"; -import { ICardData, IFormError, IPaymentGatewayConfig } from "@types"; +import { + ICardData, + IFormError, + IPaymentGatewayConfig, + IPaymentSubmitResult, +} from "@types"; export interface IProps { /** @@ -26,12 +31,12 @@ export interface IProps { /** * Method to call on gateway payment submission. */ - submitPayment: (data?: object) => Promise; + submitPayment: () => Promise; /** * Method called after succesful gateway payment submission. This is the case when no confirmation is needed. */ submitPaymentSuccess: ( - order?: CompleteCheckout_checkoutComplete_order + order?: CompleteCheckout_checkoutComplete_order | null ) => void; /** * Method called when gateway error occured. diff --git a/src/@next/pages/CheckoutPage/CheckoutPage.tsx b/src/@next/pages/CheckoutPage/CheckoutPage.tsx index d9101a114d..06f96003e2 100755 --- a/src/@next/pages/CheckoutPage/CheckoutPage.tsx +++ b/src/@next/pages/CheckoutPage/CheckoutPage.tsx @@ -17,7 +17,7 @@ import { Checkout } from "@components/templates"; import { useRedirectToCorrectCheckoutStep } from "@hooks"; import { paths } from "@paths"; import { paymentGatewayNames } from "@temp/constants"; -import { ICardData, IFormError } from "@types"; +import { ICardData, IFormError, IPaymentSubmitResult } from "@types"; import { CheckoutAddressSubpage, @@ -151,11 +151,11 @@ const CheckoutPage: React.FC = () => { confirmationNeeded: response.data?.confirmationNeeded, order: response.data?.order, errors: response.dataError?.error, - }; + } as IPaymentSubmitResult; }; const handleSubmitPaymentSuccess = ( - order?: CompleteCheckout_checkoutComplete_order + order?: CompleteCheckout_checkoutComplete_order | null ) => { setSubmitInProgress(false); setPaymentGatewayErrors([]); diff --git a/src/@next/types/IPaymentGateway.ts b/src/@next/types/IPaymentGateway.ts index c99cc30f5a..f252f032ed 100644 --- a/src/@next/types/IPaymentGateway.ts +++ b/src/@next/types/IPaymentGateway.ts @@ -1,3 +1,7 @@ +import { CompleteCheckout_checkoutComplete_order } from "@saleor/sdk/lib/mutations/gqlTypes/CompleteCheckout"; + +import { IFormError } from "@types"; + export interface IPaymentGatewayConfig { /** * Gateway config key. @@ -23,3 +27,10 @@ export interface IPaymentGateway { */ config: IPaymentGatewayConfig[]; } + +export interface IPaymentSubmitResult { + confirmationData?: string | null; + confirmationNeeded?: boolean; + order?: CompleteCheckout_checkoutComplete_order | null; + errors?: IFormError[]; +} From ffffa9281f5a726289339724d09dec4b5c35d80e Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Wed, 7 Jul 2021 10:23:43 +0200 Subject: [PATCH 07/10] Add early returns in Stripe submit logic --- .../StripePaymentGateway.tsx | 186 ++++++++++-------- .../organisms/StripePaymentGateway/utils.ts | 43 ++++ 2 files changed, 144 insertions(+), 85 deletions(-) create mode 100644 src/@next/components/organisms/StripePaymentGateway/utils.ts diff --git a/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx b/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx index fc17cc8661..05b3c202a2 100755 --- a/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx +++ b/src/@next/components/organisms/StripePaymentGateway/StripePaymentGateway.tsx @@ -14,11 +14,10 @@ import { IFormError } from "@types"; import { StripeCreditCardForm } from "../StripeCreditCardForm"; import { stripeErrorMessages } from "./intl"; import { IProps } from "./types"; - -interface StripeConfirmationData { - client_secret: string; - id: string; -} +import { + handleConfirmCardPayment, + parsePaymentConfirmationData, +} from "./utils"; /** * Stripe payment gateway. @@ -58,47 +57,53 @@ const StripePaymentGateway: React.FC = ({ ) => { const cartNumberElement = elements?.getElement(CardNumberElement); - if (cartNumberElement) { - const payload = await stripe?.createPaymentMethod({ - card: cartNumberElement, - type: "card", - }); - if (payload?.error) { - const errors = [ - { - ...payload.error, - message: payload.error.message || "", - }, - ]; - setSubmitErrors(errors); - onError(errors); - } else if (payload?.paymentMethod) { - const { card, id } = payload.paymentMethod; - if (card?.brand && card?.last4) { - processPayment(id, { - brand: card?.brand, - expMonth: card?.exp_month || null, - expYear: card?.exp_year || null, - firstDigits: null, - lastDigits: card?.last4, - }); - setPaymentMethod(payload.paymentMethod); - } - } else { - const stripePayloadErrors = [ - new Error( - intl.formatMessage(stripeErrorMessages.paymentSubmissionError) - ), - ]; - setSubmitErrors(stripePayloadErrors); - onError(stripePayloadErrors); - } - } else { + if (!cartNumberElement) { const stripeElementsErrors = [ new Error(intl.formatMessage(stripeErrorMessages.geytwayDisplayError)), ]; setSubmitErrors(stripeElementsErrors); onError(stripeElementsErrors); + return; + } + + const payload = await stripe?.createPaymentMethod({ + card: cartNumberElement, + type: "card", + }); + + if (payload?.error) { + const errors = [ + { + ...payload.error, + message: payload.error.message || "", + }, + ]; + setSubmitErrors(errors); + onError(errors); + return; + } + + if (!payload?.paymentMethod) { + const stripePayloadErrors = [ + new Error( + intl.formatMessage(stripeErrorMessages.paymentSubmissionError) + ), + ]; + setSubmitErrors(stripePayloadErrors); + onError(stripePayloadErrors); + return; + } + + const { card, id } = payload.paymentMethod; + if (card?.brand && card?.last4) { + processPayment(id, { + brand: card?.brand, + expMonth: card?.exp_month || null, + expYear: card?.exp_year || null, + firstDigits: null, + lastDigits: card?.last4, + }); + setPaymentMethod(payload.paymentMethod); } }; @@ -109,9 +114,15 @@ const StripePaymentGateway: React.FC = ({ if (payment.errors?.length) { onError(payment.errors); - } else if (!payment?.confirmationNeeded) { + return; + } + + if (!payment?.confirmationNeeded) { submitPaymentSuccess(payment?.order); - } else if (!stripe?.confirmCardPayment) { + return; + } + + if (!stripe?.confirmCardPayment) { onError([ new Error( intl.formatMessage( @@ -119,54 +130,59 @@ const StripePaymentGateway: React.FC = ({ ) ), ]); - } else if (!payment?.confirmationData) { + return; + } + + if (!payment?.confirmationData) { onError([ new Error( intl.formatMessage(paymentErrorMessages.paymentNoConfirmationData) ), ]); - } else { - let paymentAction; - try { - paymentAction = JSON.parse( - payment.confirmationData - ) as StripeConfirmationData; - } catch (parseError) { - onError([ - new Error( - intl.formatMessage( - paymentErrorMessages.paymentMalformedConfirmationData - ) - ), - ]); - return; - } - if (!paymentMethod?.id) { - onError([ - new Error( - intl.formatMessage(stripeErrorMessages.paymentMethodNotCreated) - ), - ]); - return; - } - let confirmation; - try { - confirmation = await stripe.confirmCardPayment( - paymentAction.client_secret, - { - payment_method: paymentMethod.id, - } - ); - } catch (error) { - onError([new Error(error)]); - return; - } - if (confirmation.error) { - onError([new Error(confirmation.error.message)]); - } else { - handleFormCompleteSubmit(); - } + return; } + + const { parseError, paymentAction } = parsePaymentConfirmationData( + payment.confirmationData + ); + + if (parseError || !paymentAction) { + onError([ + new Error( + intl.formatMessage( + paymentErrorMessages.paymentMalformedConfirmationData + ) + ), + ]); + return; + } + + if (!paymentMethod?.id) { + onError([ + new Error( + intl.formatMessage(stripeErrorMessages.paymentMethodNotCreated) + ), + ]); + return; + } + + const { confirmation, confirmationError } = await handleConfirmCardPayment( + stripe, + paymentAction, + paymentMethod + ); + + if (confirmationError) { + onError([new Error(confirmationError)]); + return; + } + + if (confirmation?.error) { + onError([new Error(confirmation.error.message)]); + return; + } + + handleFormCompleteSubmit(); }; useEffect(() => { diff --git a/src/@next/components/organisms/StripePaymentGateway/utils.ts b/src/@next/components/organisms/StripePaymentGateway/utils.ts new file mode 100644 index 0000000000..d2c8064c29 --- /dev/null +++ b/src/@next/components/organisms/StripePaymentGateway/utils.ts @@ -0,0 +1,43 @@ +import { PaymentMethod, Stripe } from "@stripe/stripe-js"; + +interface StripeConfirmationData { + client_secret: string; + id: string; +} + +export const parsePaymentConfirmationData = (confirmationData: string) => { + try { + const paymentAction = JSON.parse( + confirmationData + ) as StripeConfirmationData; + return { + paymentAction, + }; + } catch (parseError) { + return { + parseError, + }; + } +}; + +export const handleConfirmCardPayment = async ( + stripe: Stripe, + paymentAction: StripeConfirmationData, + paymentMethod: PaymentMethod +) => { + try { + const confirmation = await stripe.confirmCardPayment( + paymentAction.client_secret, + { + payment_method: paymentMethod.id, + } + ); + return { + confirmation, + }; + } catch (confirmationError) { + return { + confirmationError, + }; + } +}; From c3da7d1dd5f9a3ec1103e562d1ac6f56154eb5d7 Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Wed, 7 Jul 2021 10:54:14 +0200 Subject: [PATCH 08/10] Shorthand notation for description --- .../organisms/AdyenPaymentGateway/intl.ts | 14 +++++++------- .../organisms/StripePaymentGateway/intl.ts | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/@next/components/organisms/AdyenPaymentGateway/intl.ts b/src/@next/components/organisms/AdyenPaymentGateway/intl.ts index 616d0f54a0..8fdbe62155 100644 --- a/src/@next/components/organisms/AdyenPaymentGateway/intl.ts +++ b/src/@next/components/organisms/AdyenPaymentGateway/intl.ts @@ -1,35 +1,35 @@ import { defineMessages, IntlShape } from "react-intl"; -const messageDescription = "Adyen payment gateway error"; +const description = "Adyen payment gateway error"; export const adyenErrorMessages = defineMessages({ unknownPayment: { defaultMessage: "Unknown payment submission error occured.", - description: messageDescription, + description, }, invalidPaymentSubmission: { defaultMessage: "Invalid payment submission.", - description: messageDescription, + description, }, }); export const adyenConfirmationErrorMessages = defineMessages({ error: { defaultMessage: "Error processing payment occured.", - description: messageDescription, + description, }, refused: { defaultMessage: "The payment was refused. Try the payment again using a different payment method or card.", - description: messageDescription, + description, }, cancelled: { defaultMessage: "Payment was cancelled.", - description: messageDescription, + description, }, general: { defaultMessage: "Payment confirmation went wrong.", - description: messageDescription, + description, }, }); diff --git a/src/@next/components/organisms/StripePaymentGateway/intl.ts b/src/@next/components/organisms/StripePaymentGateway/intl.ts index b2e98f1981..79d483cd49 100644 --- a/src/@next/components/organisms/StripePaymentGateway/intl.ts +++ b/src/@next/components/organisms/StripePaymentGateway/intl.ts @@ -1,24 +1,24 @@ import { defineMessages } from "react-intl"; -const messageDescription = "Stripe payment gateway error"; +const description = "Stripe payment gateway error"; export const stripeErrorMessages = defineMessages({ gatewayMisconfigured: { defaultMessage: "Stripe gateway misconfigured. Api key not provided.", - description: messageDescription, + description, }, paymentSubmissionError: { defaultMessage: "Payment submission error. Stripe gateway returned no payment method in payload.", - description: messageDescription, + description, }, geytwayDisplayError: { defaultMessage: "Stripe payment gateway couldn't be displayed. Stripe elements were not provided.", - description: messageDescription, + description, }, paymentMethodNotCreated: { defaultMessage: "Payment method has not been created.", - description: messageDescription, + description, }, }); From f46e96ba36ca393f825b7056865a8208b6d42782 Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Thu, 8 Jul 2021 19:26:31 +0200 Subject: [PATCH 09/10] Update Adyen error translation --- src/@next/components/organisms/AdyenPaymentGateway/intl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/@next/components/organisms/AdyenPaymentGateway/intl.ts b/src/@next/components/organisms/AdyenPaymentGateway/intl.ts index 8fdbe62155..e22db97daa 100644 --- a/src/@next/components/organisms/AdyenPaymentGateway/intl.ts +++ b/src/@next/components/organisms/AdyenPaymentGateway/intl.ts @@ -28,7 +28,7 @@ export const adyenConfirmationErrorMessages = defineMessages({ description, }, general: { - defaultMessage: "Payment confirmation went wrong.", + defaultMessage: "Payment could not be confirmed.", description, }, }); From a938f992262cbdd648d7e003d4a36eb5c4e3c53c Mon Sep 17 00:00:00 2001 From: Dawid Tarasiuk Date: Wed, 14 Jul 2021 10:49:05 +0200 Subject: [PATCH 10/10] Trigger CI