diff --git a/CHANGELOG.md b/CHANGELOG.md index cf193827ab..7681309d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable, unreleased changes to this project will be documented in this file. - Update collection products query - #879 by @orzechdev - Fix checkout refreshing - #865 by @orzechdev - Add purchase availability to product details page - #878 by @orzechdev +- Require payment recreate when payment price is wrong - #892 by @orzechdev ## 2.10.4 diff --git a/package-lock.json b/package-lock.json index 16834d4cbe..9fd25c9e32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5604,9 +5604,9 @@ } }, "@saleor/sdk": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@saleor/sdk/-/sdk-0.1.0.tgz", - "integrity": "sha512-ZLVqPx1K9me7fslcxrGnGJ4Gq3ydmA+Yk/kK75bdNs+J8xZwZAxLhOl3lS1C/KGu7InNQvn7zQbTULwu7Oqn/Q==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@saleor/sdk/-/sdk-0.1.1.tgz", + "integrity": "sha512-XdtJy6PVs1tskqtvNdNwIs868dS+vVglz5klxJqzTb+PqqY3CDTbEOkNjqbkMk/ndN29pKDwCuylP0vmizvdvQ==", "requires": { "apollo-cache": "^1.3.5", "apollo-cache-inmemory": "^1.6.6", diff --git a/package.json b/package.json index d3619b9dd2..27c75fae30 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dependencies": { "@babel/runtime": "^7.5.5", "@lhci/cli": "^0.4.1", - "@saleor/sdk": "^0.1.0", + "@saleor/sdk": "^0.1.1", "@sentry/apm": "^5.15.5", "@sentry/browser": "^5.15.5", "@stripe/react-stripe-js": "^1.1.2", diff --git a/src/@next/hooks/useCheckoutStepState.ts b/src/@next/hooks/useCheckoutStepState.ts index 3379757c7f..73968186b7 100644 --- a/src/@next/hooks/useCheckoutStepState.ts +++ b/src/@next/hooks/useCheckoutStepState.ts @@ -1,21 +1,31 @@ import { useEffect, useState } from "react"; -import { IItems } from "@saleor/sdk/lib/api/Cart/types"; +import { IItems, ITotalPrice } from "@saleor/sdk/lib/api/Cart/types"; import { ICheckout, IPayment } from "@saleor/sdk/lib/api/Checkout/types"; import { CheckoutStep } from "@temp/core/config"; +import { checkIfShippingRequiredForProducts } from "@utils/core"; +import { isPriceEqual } from "@utils/money"; + +interface StepState { + recommendedStep: CheckoutStep; + maxPossibleStep: CheckoutStep; +} export const useCheckoutStepState = ( items?: IItems, checkout?: ICheckout, - payment?: IPayment -): CheckoutStep => { - const isShippingRequiredForProducts = - items && - items.some( - ({ variant }) => variant.product?.productType.isShippingRequired - ); - - const getStep = () => { + payment?: IPayment, + totalPrice?: ITotalPrice +): StepState => { + const isShippingRequiredForProducts = checkIfShippingRequiredForProducts( + items + ); + const isCheckoutPriceEqualPaymentPrice = + payment?.total && + totalPrice?.gross && + isPriceEqual(payment.total, totalPrice.gross); + + const getMaxPossibleStep = () => { if (!checkout?.id && items) { // we are creating checkout during address set up return CheckoutStep.Address; @@ -26,7 +36,8 @@ export const useCheckoutStepState = ( const isBillingAddressSet = !!checkout?.billingAddress; const isShippingMethodSet = !isShippingRequiredForProducts || !!checkout?.shippingMethod; - const isPaymentMethodSet = !!payment?.id; + const isPaymentMethodSet = + !!payment?.id && isCheckoutPriceEqualPaymentPrice; if (!isShippingAddressSet || !isBillingAddressSet) { return CheckoutStep.Address; @@ -40,14 +51,35 @@ export const useCheckoutStepState = ( return CheckoutStep.Review; }; - const [step, setStep] = useState(getStep()); + const getRecommendedStep = (newMaxPossibleStep: CheckoutStep) => { + const isPaymentRecreateRequired = + newMaxPossibleStep > CheckoutStep.Shipping && + !isCheckoutPriceEqualPaymentPrice; + + if (isPaymentRecreateRequired && isShippingRequiredForProducts) { + return CheckoutStep.Shipping; + } + if (isPaymentRecreateRequired) { + return CheckoutStep.Payment; + } + return newMaxPossibleStep; + }; + + const [maxPossibleStep, setMaxPossibleStep] = useState(getMaxPossibleStep()); + const [recommendedStep, setRecommendedStep] = useState( + getRecommendedStep(maxPossibleStep) + ); useEffect(() => { - const newStep = getStep(); - if (step !== newStep) { - setStep(newStep); + const newMaxPossibleStep = getMaxPossibleStep(); + const newRecommendedStep = getRecommendedStep(newMaxPossibleStep); + if (maxPossibleStep !== newMaxPossibleStep) { + setMaxPossibleStep(newMaxPossibleStep); + } + if (recommendedStep !== newRecommendedStep) { + setRecommendedStep(newRecommendedStep); } - }, [checkout, items, payment]); + }, [checkout, items, payment, totalPrice]); - return step; + return { recommendedStep, maxPossibleStep }; }; diff --git a/src/@next/pages/CheckoutPage/CheckoutPage.tsx b/src/@next/pages/CheckoutPage/CheckoutPage.tsx index 69ff49fe40..e7af786079 100755 --- a/src/@next/pages/CheckoutPage/CheckoutPage.tsx +++ b/src/@next/pages/CheckoutPage/CheckoutPage.tsx @@ -241,6 +241,7 @@ const CheckoutPage: React.FC = ({}: IProps) => { items={items} checkout={checkout} payment={payment} + totalPrice={totalPrice} renderAddress={props => ( ) => React.ReactNode; renderShipping: (props: RouteComponentProps) => React.ReactNode; renderPayment: (props: RouteComponentProps) => React.ReactNode; @@ -26,22 +28,33 @@ const CheckoutRouter: React.FC = ({ items, checkout, payment, + totalPrice, renderAddress, renderShipping, renderPayment, renderReview, }: IRouterProps) => { const { pathname } = useLocation(); - const step = useCheckoutStepState(items, checkout, payment); + const { recommendedStep, maxPossibleStep } = useCheckoutStepState( + items, + checkout, + payment, + totalPrice + ); const stepFromPath = useCheckoutStepFromPath(pathname); + const isShippingRequiredForProducts = checkIfShippingRequiredForProducts( + items + ); + const getStepLink = () => - CHECKOUT_STEPS.find(stepObj => stepObj.step === step)?.link || + CHECKOUT_STEPS.find(stepObj => stepObj.step === recommendedStep)?.link || CHECKOUT_STEPS[0].link; if ( - pathname !== CHECKOUT_STEPS[4].link && - (!stepFromPath || (stepFromPath && step < stepFromPath)) + (pathname !== CHECKOUT_STEPS[4].link && + (!stepFromPath || (stepFromPath && maxPossibleStep < stepFromPath))) || + (pathname === CHECKOUT_STEPS[1].link && !isShippingRequiredForProducts) ) { return ; } diff --git a/src/@next/types/ITaxedMoney.ts b/src/@next/types/ITaxedMoney.ts index ee593bdff8..0ac78b480d 100644 --- a/src/@next/types/ITaxedMoney.ts +++ b/src/@next/types/ITaxedMoney.ts @@ -1,10 +1,9 @@ +export interface IMoney { + amount: number; + currency: string; +} + export interface ITaxedMoney { - net: { - amount: number; - currency: string; - }; - gross: { - amount: number; - currency: string; - }; + net: IMoney; + gross: IMoney; } diff --git a/src/@next/utils/core.ts b/src/@next/utils/core.ts index 1f88dcb731..99b35b5bd0 100644 --- a/src/@next/utils/core.ts +++ b/src/@next/utils/core.ts @@ -2,6 +2,8 @@ // @ts-ignore import { Base64 } from "js-base64"; +import { IItems } from "@saleor/sdk/lib/api/Cart/types"; + export const slugify = (text: string | number): string => text .toString() @@ -36,3 +38,6 @@ export const generatePageUrl = (slug: string) => `/page/${slug}/`; export const generateGuestOrderDetailsUrl = (token: string) => `/order-history/${token}/`; + +export const checkIfShippingRequiredForProducts = (items?: IItems) => + items?.some(({ variant }) => variant.product?.productType.isShippingRequired); diff --git a/src/@next/utils/money.test.ts b/src/@next/utils/money.test.ts new file mode 100644 index 0000000000..a40473eec4 --- /dev/null +++ b/src/@next/utils/money.test.ts @@ -0,0 +1,46 @@ +import { IMoney } from "@types"; + +import { isPriceEqual } from "./money"; + +const firstPrice: IMoney = { + amount: 100, + currency: "USD", +}; + +describe("Comparing two prices", () => { + it("Returns true if amount and currency is same", () => { + const secondPrice: IMoney = { + amount: 100, + currency: "USD", + }; + + expect(isPriceEqual(firstPrice, secondPrice)).toBeTruthy(); + }); + + it("Returns false if amount is same and currency is not", () => { + const secondPrice: IMoney = { + amount: 100, + currency: "PLN", + }; + + expect(isPriceEqual(firstPrice, secondPrice)).toBeFalsy(); + }); + + it("Returns false if currency is same and amount is not", () => { + const secondPrice: IMoney = { + amount: 200, + currency: "USD", + }; + + expect(isPriceEqual(firstPrice, secondPrice)).toBeFalsy(); + }); + + it("Returns false if amount and currency is not same", () => { + const secondPrice: IMoney = { + amount: 200, + currency: "PLN", + }; + + expect(isPriceEqual(firstPrice, secondPrice)).toBeFalsy(); + }); +}); diff --git a/src/@next/utils/money.ts b/src/@next/utils/money.ts new file mode 100644 index 0000000000..fca0b3fb0f --- /dev/null +++ b/src/@next/utils/money.ts @@ -0,0 +1,4 @@ +import { IMoney } from "@types"; + +export const isPriceEqual = (first: IMoney, second: IMoney) => + first.amount === second.amount && first.currency === second.currency;