From ec4e97d196afd7a4f4822cbfd1ea9391e169d9eb Mon Sep 17 00:00:00 2001 From: Ankur Raiyani Date: Sat, 27 Feb 2021 00:07:44 +0530 Subject: [PATCH] Display page level error and disable checkout button when no available payment methods (#2873) * Added payment method query to checkoutPageFragments.gql.js to fetch all available payment methods while fetching cart data on checkout page * Added page level error message when no payment method implementations available for available methods * Adding checkout page test update * Adding useCheckoutPage talon test * executed prettier on checkoutPage.spec.js * executed prettier on useCheckoutPage.spec.js * Updated unit test * Corrected test snap to pass the unit test * Converted "CheckoutPageOperations" to peregrine. Also moved other connected queries to peregrine. * Fixed useCheckoutPage.spec.js to make it compatible with migrated "CheckoutPageOperations" in useCheckoutPage.js peregrine component * Prettier-ed useCheckoutPage.spec.js * Fixed issue with gql client after migration of CheckoutPageOperations to peregrine. * Fixed issue with gql client after migration of CheckoutPageOperations to peregrine. * Sorted the new lang definitions while adding * Used the ternary operator instead of the logical operators * Changed variable "props" to "defaultProps" * Removed empty object usage as it was not required anymore. * Corrected lint error related to array reassignment due to let declaration * Update packages/peregrine/lib/talons/CheckoutPage/useCheckoutPage.js Co-authored-by: Stephen * Re-add lost query * Add missing mock in test Signed-off-by: sirugh * Prettified * exporting default operations as root properties * Remove styles Signed-off-by: sirugh * fix operations mock in tests Signed-off-by: sirugh * Fix zero total checkout use case. Add tests * Fixed layout issue with multiple page errors on checkoutPage. * Updated test snapshots * Updated test cases snapshots Co-authored-by: Stephen Co-authored-by: sirugh Co-authored-by: Devagouda <40405790+dpatil-magento@users.noreply.github.com> --- .../ItemsReview/itemsReview.gql.js | 16 +- .../ItemsReview/itemsReviewFragments.gql.js | 0 .../orderConfirmationPageFragments.gql.js | 0 .../__tests__/usePaymentInformation.spec.js | 1 + .../useCheckoutPage.spec.js.snap | 1 + .../__tests__/useCheckoutPage.spec.js | 74 ++++---- .../talons}/CheckoutPage/checkoutPage.gql.js | 14 +- .../CheckoutPage/checkoutPageFragments.gql.js | 3 + .../talons/CheckoutPage/useCheckoutPage.js | 25 ++- packages/venia-ui/i18n/en_US.json | 1 + .../ItemsReview/itemsReview.gql.js | 16 -- .../CheckoutPage/ItemsReview/itemsReview.js | 5 - .../__snapshots__/checkoutPage.spec.js.snap | 169 ++++++++++++++++++ .../__tests__/checkoutPage.spec.js | 28 +++ .../components/CheckoutPage/checkoutPage.css | 9 + .../components/CheckoutPage/checkoutPage.js | 41 ++++- .../stockStatusMessage.spec.js.snap | 4 +- .../StockStatusMessage/stockStatusMessage.css | 5 +- .../StockStatusMessage/stockStatusMessage.js | 4 +- 19 files changed, 335 insertions(+), 81 deletions(-) rename packages/{venia-ui/lib/components => peregrine/lib/talons}/CheckoutPage/ItemsReview/itemsReviewFragments.gql.js (100%) rename packages/{venia-ui/lib/components => peregrine/lib/talons}/CheckoutPage/OrderConfirmationPage/orderConfirmationPageFragments.gql.js (100%) rename packages/{venia-ui/lib/components => peregrine/lib/talons}/CheckoutPage/checkoutPage.gql.js (84%) rename packages/{venia-ui/lib/components => peregrine/lib/talons}/CheckoutPage/checkoutPageFragments.gql.js (84%) delete mode 100644 packages/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReview.gql.js diff --git a/packages/peregrine/lib/talons/CheckoutPage/ItemsReview/itemsReview.gql.js b/packages/peregrine/lib/talons/CheckoutPage/ItemsReview/itemsReview.gql.js index 0dd7a6756b..76d2839d0c 100644 --- a/packages/peregrine/lib/talons/CheckoutPage/ItemsReview/itemsReview.gql.js +++ b/packages/peregrine/lib/talons/CheckoutPage/ItemsReview/itemsReview.gql.js @@ -1,5 +1,7 @@ import { gql } from '@apollo/client'; +import { ItemsReviewFragment } from './itemsReviewFragments.gql'; + export const GET_CONFIGURABLE_THUMBNAIL_SOURCE = gql` query getConfigurableThumbnailSource { storeConfig { @@ -9,6 +11,18 @@ export const GET_CONFIGURABLE_THUMBNAIL_SOURCE = gql` } `; +export const LIST_OF_PRODUCTS_IN_CART_QUERY = gql` + query getItemsInCart($cartId: String!) { + cart(cart_id: $cartId) { + id + ...ItemsReviewFragment + } + } + + ${ItemsReviewFragment} +`; + export default { - getConfigurableThumbnailSource: GET_CONFIGURABLE_THUMBNAIL_SOURCE + getConfigurableThumbnailSource: GET_CONFIGURABLE_THUMBNAIL_SOURCE, + getItemsInCart: LIST_OF_PRODUCTS_IN_CART_QUERY }; diff --git a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReviewFragments.gql.js b/packages/peregrine/lib/talons/CheckoutPage/ItemsReview/itemsReviewFragments.gql.js similarity index 100% rename from packages/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReviewFragments.gql.js rename to packages/peregrine/lib/talons/CheckoutPage/ItemsReview/itemsReviewFragments.gql.js diff --git a/packages/venia-ui/lib/components/CheckoutPage/OrderConfirmationPage/orderConfirmationPageFragments.gql.js b/packages/peregrine/lib/talons/CheckoutPage/OrderConfirmationPage/orderConfirmationPageFragments.gql.js similarity index 100% rename from packages/venia-ui/lib/components/CheckoutPage/OrderConfirmationPage/orderConfirmationPageFragments.gql.js rename to packages/peregrine/lib/talons/CheckoutPage/OrderConfirmationPage/orderConfirmationPageFragments.gql.js diff --git a/packages/peregrine/lib/talons/CheckoutPage/PaymentInformation/__tests__/usePaymentInformation.spec.js b/packages/peregrine/lib/talons/CheckoutPage/PaymentInformation/__tests__/usePaymentInformation.spec.js index 569f1f4b69..c6a17afb12 100644 --- a/packages/peregrine/lib/talons/CheckoutPage/PaymentInformation/__tests__/usePaymentInformation.spec.js +++ b/packages/peregrine/lib/talons/CheckoutPage/PaymentInformation/__tests__/usePaymentInformation.spec.js @@ -12,6 +12,7 @@ jest.mock('../../../../context/cart', () => ({ jest.mock('@apollo/client', () => { return { + ...jest.requireActual('@apollo/client'), useApolloClient: jest.fn(), useQuery: jest.fn().mockReturnValue({ data: { diff --git a/packages/peregrine/lib/talons/CheckoutPage/__tests__/__snapshots__/useCheckoutPage.spec.js.snap b/packages/peregrine/lib/talons/CheckoutPage/__tests__/__snapshots__/useCheckoutPage.spec.js.snap index bf8490e9cc..2fe63ba314 100644 --- a/packages/peregrine/lib/talons/CheckoutPage/__tests__/__snapshots__/useCheckoutPage.spec.js.snap +++ b/packages/peregrine/lib/talons/CheckoutPage/__tests__/__snapshots__/useCheckoutPage.spec.js.snap @@ -3,6 +3,7 @@ exports[`Should return correct shape 1`] = ` Object { "activeContent": "checkout", + "availablePaymentMethods": null, "cartItems": Array [], "checkoutStep": 1, "customer": Object { diff --git a/packages/peregrine/lib/talons/CheckoutPage/__tests__/useCheckoutPage.spec.js b/packages/peregrine/lib/talons/CheckoutPage/__tests__/useCheckoutPage.spec.js index b47b8a8506..a5ade40d7b 100644 --- a/packages/peregrine/lib/talons/CheckoutPage/__tests__/useCheckoutPage.spec.js +++ b/packages/peregrine/lib/talons/CheckoutPage/__tests__/useCheckoutPage.spec.js @@ -20,6 +20,7 @@ import CheckoutError from '../CheckoutError'; jest.mock('@apollo/client', () => { return { + ...jest.requireActual('@apollo/client'), useLazyQuery: jest.fn(), useApolloClient: jest.fn(), useMutation: jest.fn(), @@ -64,9 +65,14 @@ const getCheckoutDetailsQuery = 'getCheckoutDetailsQuery'; const getOrderDetailsQuery = 'getOrderDetailsQuery'; const getCustomerQuery = 'getCustomerQuery'; -const props = { - mutations: { createCartMutation, placeOrderMutation }, - queries: { getCheckoutDetailsQuery, getOrderDetailsQuery, getCustomerQuery } +const defaultProps = { + operations: { + createCartMutation, + getCheckoutDetailsQuery, + getOrderDetailsQuery, + getCustomerQuery, + placeOrderMutation + } }; const readQuery = jest.fn().mockReturnValue({ cart: {} }); @@ -182,7 +188,7 @@ beforeEach(() => { */ test('Should return correct shape', () => { - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps).toMatchSnapshot(); }); @@ -193,7 +199,7 @@ test('isLoading should be set to true if the checkout details query networkStatu networkStatus: 4 }); - const { talonProps, update } = getTalonProps(props); + const { talonProps, update } = getTalonProps(defaultProps); expect(talonProps.isLoading).toBeTruthy(); @@ -221,7 +227,7 @@ test('isLoading should be set to true if the customer details query is loading', loading: true }); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.isLoading).toBeTruthy(); }); @@ -231,7 +237,7 @@ test('returns cartItems from getOrderDetails query', () => { getCheckoutDetailsQueryResult.mockReturnValueOnce({ data: { cart: { items: cartItems } } }); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.cartItems).toEqual(cartItems); }); @@ -247,7 +253,7 @@ test('returned error prop should be error from place order mutation', () => { } ]); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.error).toBeInstanceOf(CheckoutError); }); @@ -258,7 +264,7 @@ test('should get order details when handlePlaceOrder called', () => { { createCart: () => {}, removeCart: () => {} } ]); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); act(() => { talonProps.handlePlaceOrder(); @@ -284,7 +290,7 @@ test("should place order and cleanup when we have order details and place order return [jest.fn(), { data: {}, loading: false }]; }); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); await act(async () => { await talonProps.handlePlaceOrder(); @@ -308,7 +314,7 @@ test('hasError should be true if place order mutation failed with errors', () => } ]); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.hasError).toBeTruthy(); }); @@ -321,7 +327,7 @@ describe('isCartEmpty', () => { loading: false }); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.isCartEmpty).toBeTruthy(); }); @@ -337,7 +343,7 @@ describe('isCartEmpty', () => { loading: false }); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.isCartEmpty).toBeTruthy(); }); @@ -353,7 +359,7 @@ describe('isCartEmpty', () => { loading: false }); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.isCartEmpty).toBeFalsy(); }); @@ -362,7 +368,7 @@ describe('isCartEmpty', () => { test('isGuestCheckout should be negation of isSignedIn from useUserContext', () => { useUserContext.mockReturnValueOnce([{ isSignedIn: false }]); - const { talonProps, update } = getTalonProps(props); + const { talonProps, update } = getTalonProps(defaultProps); expect(talonProps.isGuestCheckout).toBeTruthy(); @@ -384,7 +390,7 @@ test('orderDetailsData should be data from getOrderDetailsQuery', () => { } ]); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.orderDetailsData).toBe(data); }); @@ -399,7 +405,7 @@ test('orderDetailsLoading should be loading status of the getOrderDetailsQuery', } ]); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.orderDetailsLoading).toBeTruthy(); }); @@ -420,7 +426,7 @@ test('orderNumber should be the order_number from the place order mutation resul } ]); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.orderNumber).toBe('123'); }); @@ -435,7 +441,7 @@ test('orderNumber should be the null if place order mutation result is falsy', ( } ]); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.orderNumber).toBeNull(); }); @@ -450,14 +456,14 @@ test('placeOrderLoading should be loading status of the place order mutation', ( } ]); - const { talonProps } = getTalonProps(props); + const { talonProps } = getTalonProps(defaultProps); expect(talonProps.placeOrderLoading).toBeTruthy(); }); describe('setShippingInformationDone', () => { test('should set the checkoutStep to SHIPPING_METHOD if current checkoutStep is SHIPPING_ADDRESS', () => { - const { talonProps, update } = getTalonProps(props); + const { talonProps, update } = getTalonProps(defaultProps); talonProps.setCheckoutStep(CHECKOUT_STEP.SHIPPING_ADDRESS); @@ -473,7 +479,7 @@ describe('setShippingInformationDone', () => { }); test('should not set the checkoutStep to SHIPPING_METHOD if current checkoutStep is not SHIPPING_ADDRESS', () => { - const { talonProps, update } = getTalonProps(props); + const { talonProps, update } = getTalonProps(defaultProps); talonProps.setCheckoutStep(CHECKOUT_STEP.PAYMENT); @@ -491,7 +497,7 @@ describe('setShippingInformationDone', () => { describe('setShippingMethodDone', () => { test('should set the checkoutStep to PAYMENT if current checkoutStep is SHIPPING_METHOD', () => { - const { talonProps, update } = getTalonProps(props); + const { talonProps, update } = getTalonProps(defaultProps); talonProps.setCheckoutStep(CHECKOUT_STEP.SHIPPING_METHOD); @@ -507,7 +513,7 @@ describe('setShippingMethodDone', () => { }); test('should not set the checkoutStep to PAYMENT if current checkoutStep is not SHIPPING_METHOD', () => { - const { talonProps, update } = getTalonProps(props); + const { talonProps, update } = getTalonProps(defaultProps); talonProps.setCheckoutStep(CHECKOUT_STEP.REVIEW); @@ -525,7 +531,7 @@ describe('setShippingMethodDone', () => { describe('setPaymentInformationDone', () => { test('should set the checkoutStep to REVIEW if current checkoutStep is PAYMENT', () => { - const { talonProps, update } = getTalonProps(props); + const { talonProps, update } = getTalonProps(defaultProps); talonProps.setCheckoutStep(CHECKOUT_STEP.PAYMENT); @@ -541,7 +547,7 @@ describe('setPaymentInformationDone', () => { }); test('should not set the checkoutStep to REVIEW if current checkoutStep is not PAYMENT', () => { - const { talonProps, update } = getTalonProps(props); + const { talonProps, update } = getTalonProps(defaultProps); talonProps.setCheckoutStep(CHECKOUT_STEP.SHIPPING_METHOD); @@ -558,7 +564,7 @@ describe('setPaymentInformationDone', () => { }); test('handleReviewOrder should set reviewOrderButtonClicked to true', () => { - const { talonProps, update } = getTalonProps(props); + const { talonProps, update } = getTalonProps(defaultProps); expect(talonProps.reviewOrderButtonClicked).toBeFalsy(); @@ -570,7 +576,7 @@ test('handleReviewOrder should set reviewOrderButtonClicked to true', () => { }); test('resetReviewOrderButtonClicked should set reviewOrderButtonClicked to false', () => { - const { talonProps, update } = getTalonProps(props); + const { talonProps, update } = getTalonProps(defaultProps); expect(talonProps.reviewOrderButtonClicked).toBeFalsy(); @@ -588,7 +594,7 @@ test('resetReviewOrderButtonClicked should set reviewOrderButtonClicked to false }); test('toggles addressBook content', () => { - const { talonProps: initialProps, update } = getTalonProps(props); + const { talonProps: initialProps, update } = getTalonProps(defaultProps); initialProps.toggleAddressBookContent(); const step1Props = update(); @@ -602,7 +608,7 @@ test('toggles addressBook content', () => { }); test('toggles signIn content', () => { - const { talonProps: initialProps, update } = getTalonProps(props); + const { talonProps: initialProps, update } = getTalonProps(defaultProps); initialProps.toggleSignInContent(); const step1Props = update(); @@ -616,7 +622,7 @@ test('toggles signIn content', () => { }); test('resets active content to checkout on sign in', () => { - const { talonProps: initialProps, update } = getTalonProps(props); + const { talonProps: initialProps, update } = getTalonProps(defaultProps); initialProps.toggleSignInContent(); const step1Props = update(); @@ -628,3 +634,9 @@ test('resets active content to checkout on sign in', () => { expect(step2Props.activeContent).toBe('checkout'); }); + +test('check availablePaymentMethods, if not implemented then show page level message', () => { + const { talonProps } = getTalonProps(defaultProps); + + expect(talonProps.availablePaymentMethods).toBeNull(); +}); diff --git a/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.gql.js b/packages/peregrine/lib/talons/CheckoutPage/checkoutPage.gql.js similarity index 84% rename from packages/venia-ui/lib/components/CheckoutPage/checkoutPage.gql.js rename to packages/peregrine/lib/talons/CheckoutPage/checkoutPage.gql.js index bccf2b1c16..e35c39badf 100644 --- a/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.gql.js +++ b/packages/peregrine/lib/talons/CheckoutPage/checkoutPage.gql.js @@ -53,13 +53,9 @@ export const GET_CUSTOMER = gql` `; export default { - mutations: { - createCartMutation: CREATE_CART, - placeOrderMutation: PLACE_ORDER - }, - queries: { - getCheckoutDetailsQuery: GET_CHECKOUT_DETAILS, - getCustomerQuery: GET_CUSTOMER, - getOrderDetailsQuery: GET_ORDER_DETAILS - } + createCartMutation: CREATE_CART, + getCheckoutDetailsQuery: GET_CHECKOUT_DETAILS, + getCustomerQuery: GET_CUSTOMER, + getOrderDetailsQuery: GET_ORDER_DETAILS, + placeOrderMutation: PLACE_ORDER }; diff --git a/packages/venia-ui/lib/components/CheckoutPage/checkoutPageFragments.gql.js b/packages/peregrine/lib/talons/CheckoutPage/checkoutPageFragments.gql.js similarity index 84% rename from packages/venia-ui/lib/components/CheckoutPage/checkoutPageFragments.gql.js rename to packages/peregrine/lib/talons/CheckoutPage/checkoutPageFragments.gql.js index 16a75d230a..ea78e76806 100644 --- a/packages/venia-ui/lib/components/CheckoutPage/checkoutPageFragments.gql.js +++ b/packages/peregrine/lib/talons/CheckoutPage/checkoutPageFragments.gql.js @@ -12,5 +12,8 @@ export const CheckoutPageFragment = gql` } # If total quantity is falsy we render empty. total_quantity + available_payment_methods { + code + } } `; diff --git a/packages/peregrine/lib/talons/CheckoutPage/useCheckoutPage.js b/packages/peregrine/lib/talons/CheckoutPage/useCheckoutPage.js index 38222e5d8b..50f137ff52 100644 --- a/packages/peregrine/lib/talons/CheckoutPage/useCheckoutPage.js +++ b/packages/peregrine/lib/talons/CheckoutPage/useCheckoutPage.js @@ -9,6 +9,11 @@ import { import { clearCartDataFromCache } from '../../Apollo/clearCartDataFromCache'; import { useUserContext } from '../../context/user'; import { useCartContext } from '../../context/cart'; + +import mergeOperations from '../../util/shallowMerge'; + +import DEFAULT_OPERATIONS from './checkoutPage.gql.js'; + import CheckoutError from './CheckoutError'; export const CHECKOUT_STEP = { @@ -18,15 +23,16 @@ export const CHECKOUT_STEP = { REVIEW: 4 }; -export const useCheckoutPage = props => { +export const useCheckoutPage = (props = {}) => { + const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations); + const { - mutations: { createCartMutation, placeOrderMutation }, - queries: { - getCheckoutDetailsQuery, - getCustomerQuery, - getOrderDetailsQuery - } - } = props; + createCartMutation, + getCheckoutDetailsQuery, + getCustomerQuery, + getOrderDetailsQuery, + placeOrderMutation + } = operations; const [reviewOrderButtonClicked, setReviewOrderButtonClicked] = useState( false @@ -210,6 +216,9 @@ export const useCheckoutPage = props => { return { activeContent, + availablePaymentMethods: checkoutData + ? checkoutData.cart.available_payment_methods + : null, cartItems, checkoutStep, customer, diff --git a/packages/venia-ui/i18n/en_US.json b/packages/venia-ui/i18n/en_US.json index e74b107ddb..a341fd4607 100644 --- a/packages/venia-ui/i18n/en_US.json +++ b/packages/venia-ui/i18n/en_US.json @@ -69,6 +69,7 @@ "checkoutPage.itemsInYourOrder": " items in your order", "checkoutPage.loadingPayment": "Loading Payment", "checkoutPage.loadingPaymentInformation": "Fetching Payment Information", + "checkoutPage.noPaymentAvailable": "Payment is currently unavailable.", "checkoutPage.orderNumber": "Order Number: {orderNumber}", "checkoutPage.orderSummary": "Order Summary", "checkoutPage.paymentInformation": "Payment Information", diff --git a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReview.gql.js b/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReview.gql.js deleted file mode 100644 index 9f0efb1a77..0000000000 --- a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReview.gql.js +++ /dev/null @@ -1,16 +0,0 @@ -import { gql } from '@apollo/client'; - -import { ItemsReviewFragment } from './itemsReviewFragments.gql'; - -const LIST_OF_PRODUCTS_IN_CART_QUERY = gql` - query getItemsInCart($cartId: String!) { - cart(cart_id: $cartId) { - id - ...ItemsReviewFragment - } - } - - ${ItemsReviewFragment} -`; - -export default LIST_OF_PRODUCTS_IN_CART_QUERY; diff --git a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReview.js b/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReview.js index 8a99723127..9eb697f214 100644 --- a/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReview.js +++ b/packages/venia-ui/lib/components/CheckoutPage/ItemsReview/itemsReview.js @@ -8,8 +8,6 @@ import ShowAllButton from './showAllButton'; import LoadingIndicator from '../../LoadingIndicator'; import { mergeClasses } from '../../../classify'; -import LIST_OF_PRODUCTS_IN_CART_QUERY from './itemsReview.gql'; - import defaultClasses from './itemsReview.css'; /** @@ -22,9 +20,6 @@ const ItemsReview = props => { const classes = mergeClasses(defaultClasses, propClasses); const talonProps = useItemsReview({ - operations: { - getItemsInCart: LIST_OF_PRODUCTS_IN_CART_QUERY - }, data: props.data }); diff --git a/packages/venia-ui/lib/components/CheckoutPage/__tests__/__snapshots__/checkoutPage.spec.js.snap b/packages/venia-ui/lib/components/CheckoutPage/__tests__/__snapshots__/checkoutPage.spec.js.snap index c9c6dfd0c5..ff3fb57176 100644 --- a/packages/venia-ui/lib/components/CheckoutPage/__tests__/__snapshots__/checkoutPage.spec.js.snap +++ b/packages/venia-ui/lib/components/CheckoutPage/__tests__/__snapshots__/checkoutPage.spec.js.snap @@ -11,6 +11,14 @@ exports[`CheckoutPage renders address book for customer 1`] = `
+ `; +exports[`CheckoutPage renders an error and disables review order button if there is no payment method 1`] = ` +
+ Title +
+
+ + + + + + + + } + /> +

+ Guest Checkout +

+
+
+ + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+`; + exports[`CheckoutPage renders checkout content for customer - default address 1`] = `
+ + + { const actual = jest.requireActual('@magento/peregrine'); @@ -36,6 +37,7 @@ jest.mock('@magento/peregrine/lib/talons/CheckoutPage/useCheckoutPage', () => { jest.mock('../../../classify'); jest.mock('../../../components/Head', () => ({ Title: () => 'Title' })); +jest.mock('../../FormError', () => 'FormError'); jest.mock('../../StockStatusMessage', () => 'StockStatusMessage'); jest.mock('../ItemsReview', () => 'ItemsReview'); jest.mock('../GuestSignIn', () => 'GuestSignIn'); @@ -44,11 +46,15 @@ jest.mock('../OrderConfirmationPage', () => 'OrderConfirmationPage'); jest.mock('../ShippingInformation', () => 'ShippingInformation'); jest.mock('../ShippingMethod', () => 'ShippingMethod'); jest.mock('../PaymentInformation', () => 'PaymentInformation'); +jest.mock('../PaymentInformation/paymentMethodCollection', () => ({ + braintree: {} +})); jest.mock('../PriceAdjustments', () => 'PriceAdjustments'); jest.mock('../AddressBook', () => 'AddressBook'); const defaultTalonProps = { activeContent: 'checkout', + availablePaymentMethods: [{ code: 'braintree' }], cartItems: [], checkoutStep: 1, customer: null, @@ -73,6 +79,7 @@ const defaultTalonProps = { toggleAddressBookContent: jest.fn().mockName('toggleAddressBookContent'), toggleSignInContent: jest.fn().mockName('toggleSignInContent') }; + describe('CheckoutPage', () => { test('throws a toast if there is an error', () => { useCheckoutPage.mockReturnValueOnce({ @@ -211,4 +218,25 @@ describe('CheckoutPage', () => { expect(priceAdjustmentsComponent.props).toMatchSnapshot(); expect(reviewOrderButtonComponent.props).toMatchSnapshot(); }); + + test('renders an error and disables review order button if there is no payment method', () => { + useCheckoutPage.mockReturnValueOnce({ + ...defaultTalonProps, + checkoutStep: CHECKOUT_STEP.PAYMENT, + isUpdating: true, + availablePaymentMethods: [] + }); + + const tree = createTestInstance(); + const formErrorComponent = tree.root.findByType(FormError); + const reviewOrderButtonComponent = tree.root.findByProps({ + className: 'review_order_button' + }); + + expect(tree).toMatchSnapshot(); + expect(formErrorComponent.props.errors[0]).toEqual( + new Error('Payment is currently unavailable.') + ); + expect(reviewOrderButtonComponent.props.disabled).toBe(true); + }); }); diff --git a/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.css b/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.css index c8387c44e2..6ab398be24 100644 --- a/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.css +++ b/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.css @@ -94,6 +94,15 @@ grid-column: 1 / span 1; } +.formErrors { + border-color: rgb(var(--venia-global-color-error)); + border-style: solid; + border-width: 0 0 0 5px; + padding: 1rem 0 1rem 1rem; + display: grid; + grid-column: 1 / span 1; +} + @media (min-width: 961px) { .summaryContainer { grid-column: 2 / span 1; diff --git a/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.js b/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.js index cff4161a63..f79566247a 100644 --- a/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.js +++ b/packages/venia-ui/lib/components/CheckoutPage/checkoutPage.js @@ -15,26 +15,26 @@ import { Title } from '../Head'; import Icon from '../Icon'; import { fullPageLoadingIndicator } from '../LoadingIndicator'; import StockStatusMessage from '../StockStatusMessage'; +import FormError from '../FormError'; import AddressBook from './AddressBook'; import GuestSignIn from './GuestSignIn'; import OrderSummary from './OrderSummary'; import PaymentInformation from './PaymentInformation'; +import payments from './PaymentInformation/paymentMethodCollection'; import PriceAdjustments from './PriceAdjustments'; import ShippingMethod from './ShippingMethod'; import ShippingInformation from './ShippingInformation'; import OrderConfirmationPage from './OrderConfirmationPage'; import ItemsReview from './ItemsReview'; + import defaultClasses from './checkoutPage.css'; -import CheckoutPageOperations from './checkoutPage.gql.js'; const errorIcon = ; const CheckoutPage = props => { const { classes: propClasses } = props; const { formatMessage } = useIntl(); - const talonProps = useCheckoutPage({ - ...CheckoutPageOperations - }); + const talonProps = useCheckoutPage(); const { /** @@ -42,6 +42,7 @@ const CheckoutPage = props => { * SHIPPING_ADDRESS, SHIPPING_METHOD, PAYMENT, REVIEW */ activeContent, + availablePaymentMethods, cartItems, checkoutStep, customer, @@ -172,6 +173,26 @@ const CheckoutPage = props => { ); + const formErrors = []; + const paymentMethods = Object.keys(payments); + + // If we have an implementation, or if this is a "zero" checkout, + // we can allow checkout to proceed. + const isPaymentAvailable = !!availablePaymentMethods.find( + ({ code }) => code === 'free' || paymentMethods.includes(code) + ); + + if (!isPaymentAvailable) { + formErrors.push( + new Error( + formatMessage({ + id: 'checkoutPage.noPaymentAvailable', + defaultMessage: 'Payment is currently unavailable.' + }) + ) + ); + } + const paymentInformationSection = checkoutStep >= CHECKOUT_STEP.PAYMENT ? ( { onClick={handleReviewOrder} priority="high" className={classes.review_order_button} - disabled={reviewOrderButtonClicked || isUpdating} + disabled={ + reviewOrderButtonClicked || + isUpdating || + !isPaymentAvailable + } > { checkoutContent = (
+ -

+
`; exports[`renders null with no out of stock products 1`] = `null`; diff --git a/packages/venia-ui/lib/components/StockStatusMessage/stockStatusMessage.css b/packages/venia-ui/lib/components/StockStatusMessage/stockStatusMessage.css index 1da1c16f22..01b089cc10 100644 --- a/packages/venia-ui/lib/components/StockStatusMessage/stockStatusMessage.css +++ b/packages/venia-ui/lib/components/StockStatusMessage/stockStatusMessage.css @@ -1,7 +1,8 @@ .root { - border-left: 4px solid rgb(var(--venia-global-color-error)); + border-left: 5px solid rgb(var(--venia-global-color-error)); color: rgb(var(--venia-global-color-error)); font-size: var(--venia-typography-body-S-fontSize); font-weight: var(--venia-global-fontWeight-semibold); - padding: 0.625rem 0 0.625rem 1rem; + line-height: var(--venia-global-lineHeight-300); + padding: 1rem 0 1rem 1rem; } diff --git a/packages/venia-ui/lib/components/StockStatusMessage/stockStatusMessage.js b/packages/venia-ui/lib/components/StockStatusMessage/stockStatusMessage.js index 324d65a852..ac0832bdf1 100644 --- a/packages/venia-ui/lib/components/StockStatusMessage/stockStatusMessage.js +++ b/packages/venia-ui/lib/components/StockStatusMessage/stockStatusMessage.js @@ -14,9 +14,9 @@ const StockStatusMessage = props => { const { hasOutOfStockItem } = talonProps; const stockStatusMessageElement = hasOutOfStockItem ? ( -

+

-

+
) : null; return stockStatusMessageElement;