Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CIF-1329 - Reset the context data between checking out and showing the receipt #255

Merged
merged 8 commits into from
May 8, 2020
Merged
28 changes: 28 additions & 0 deletions react-components/src/actions/user/__test__/actions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*******************************************************************************
*
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
******************************************************************************/
import { resetCustomerCart } from '../actions';

describe('User actions', () => {
it('resets the customer cart', async () => {
const dispatch = jest.fn();
const query = jest.fn(() => {
return { data: { customerCart: { id: 'my-cart-id' } } };
});

await resetCustomerCart({ dispatch, fetchCustomerCartQuery: query });

expect(query).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith({ type: 'setCartId', cartId: 'my-cart-id' });
});
});
30 changes: 30 additions & 0 deletions react-components/src/actions/user/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*******************************************************************************
*
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
******************************************************************************/

/**
* Re-fetches a customer cart.
*
* @param {Object} payload a parameters object with the following structure:
* fetchCustomerCartQuery - the query object to execute to retrieve the cart details
* dispatch - the dispatch callback for the user context
*/
export const resetCustomerCart = async payload => {
const { dispatch, fetchCustomerCartQuery } = payload;

const { data } = await fetchCustomerCartQuery({
fetchPolicy: 'network-only'
});
const cartId = data.customerCart.id;
dispatch({ type: 'setCartId', cartId });
};
14 changes: 14 additions & 0 deletions react-components/src/actions/user/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*******************************************************************************
*
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
******************************************************************************/
export * from './actions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*******************************************************************************
*
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
******************************************************************************/
import React from 'react';
import { render, fireEvent } from '@testing-library/react';

import { CheckoutProvider } from '../checkoutContext';
import useReceipt from '../useReceipt';

describe('useReceipt', () => {
it('resets the checkout context', async () => {
const MockCmp = () => {
const [{ orderId }, continueShopping] = useReceipt();

return (
<>
<div data-testid="orderId">{orderId}</div>
<button onClick={continueShopping}>Continue Shopping</button>
</>
);
};

const { getByTestId, getByRole } = render(
<CheckoutProvider initialState={{ flowState: 'receipt', order: { order_id: 'my-order' } }}>
<MockCmp />
</CheckoutProvider>
);

const orderId = getByTestId('orderId');
expect(orderId.textContent).toEqual('my-order');

expect(getByRole('button')).not.toBeUndefined();
fireEvent.click(getByRole('button'));

expect(orderId.textContent).toBe('');
});
});
16 changes: 14 additions & 2 deletions react-components/src/components/Checkout/useOverview.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@
******************************************************************************/
import { useMutation } from '@apollo/react-hooks';
import { useState } from 'react';

import MUTATION_PLACE_ORDER from '../../queries/mutation_place_order.graphql';
import QUERY_CUSTOMER_CART from '../../queries/query_customer_cart.graphql';

import { useAwaitQuery } from '../../utils/hooks';
import { useCartState } from '../Minicart/cartContext';
import { useCheckoutState } from './checkoutContext';
import { useUserContext } from '../../context/UserContext';

export default () => {
const [{ shippingAddress, shippingMethod, paymentMethod }, checkoutDispatch] = useCheckoutState();
const [{ cart, cartId }, cartDispatch] = useCartState();
const [{ isSignedIn }, { resetCustomerCart }] = useUserContext();
const fetchCustomerCartQuery = useAwaitQuery(QUERY_CUSTOMER_CART);

const [placeOrder] = useMutation(MUTATION_PLACE_ORDER);
const [inProgress, setInProgress] = useState(false);
Expand All @@ -31,12 +37,18 @@ export default () => {
// will be unmounted and the `setInProgress` call will trigger a React warning
try {
const { data } = await placeOrder({ variables: { cartId } });
setInProgress(false);
checkoutDispatch({ type: 'placeOrder', order: data.placeOrder.order });

// if user is signed in reset the cart
if (isSignedIn) {
resetCustomerCart(fetchCustomerCartQuery);
}
cartDispatch({ type: 'reset' });
} catch (error) {
setInProgress(false);
console.error(error);
cartDispatch({ type: 'error', error: error.toString() });
} finally {
setInProgress(false);
}
};

Expand Down
24 changes: 5 additions & 19 deletions react-components/src/components/Checkout/useReceipt.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,16 @@
* governing permissions and limitations under the License.
*
******************************************************************************/
import { useCartState } from '../../components/Minicart/cartContext';
import { useCheckoutState } from './checkoutContext';
import { useUserContext } from '../../context/UserContext';
import { useAwaitQuery } from '../../utils/hooks';
import QUERY_CUSTOMER_CART from '../../queries/query_customer_cart.graphql';

export default () => {
const [, cartDispatch] = useCartState();
const [{ order }, dispatch] = useCheckoutState();
const [{ isSignedIn }, { dispatch: userDispatch }] = useUserContext();
const fetchCustomerCart = useAwaitQuery(QUERY_CUSTOMER_CART);

const continueShopping = async () => {
// if it's signed in reset the cart
if (isSignedIn) {
const { data: customerCartData } = await fetchCustomerCart({
fetchPolicy: 'network-only'
});
const customerCartId = customerCartData.customerCart.id;
userDispatch({ type: 'setCartId', cartId: customerCartId });
}
// reset the cart from the cart state and the checkout state
cartDispatch({ type: 'reset' });
const continueShopping = () => {
// Reset checkout state
dispatch({ type: 'reset' });
};
return [{ orderId: order.order_id }, continueShopping];

const orderId = order && order.order_id ? order.order_id : null;
return [{ orderId }, continueShopping];
};
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,11 @@ exports[`<Footer> renders the component 1`] = `
</div>
</DocumentFragment>
`;

exports[`<Footer> renders the component with null cart 1`] = `
<DocumentFragment>
<div
class="root"
/>
</DocumentFragment>
`;
16 changes: 16 additions & 0 deletions react-components/src/components/Minicart/__test__/footer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,20 @@ describe('<Footer>', () => {

expect(asFragment()).toMatchSnapshot();
});

it('renders the component with null cart', () => {
const { asFragment } = render(
<I18nextProvider i18n={i18n}>
<CartProvider
initialState={{
cart: null
}}
reducerFactory={() => state => state}>
<Footer />
</CartProvider>
</I18nextProvider>
);

expect(asFragment()).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { I18nextProvider } from 'react-i18next';
import Minicart from '../minicart';
import { waitForElement } from '@testing-library/dom';
import { CartProvider } from '../cartContext';
import { CheckoutProvider } from '../../Checkout/checkoutContext';
import i18n from '../../../../__mocks__/i18nForTests';

describe('<Minicart>', () => {
Expand All @@ -33,7 +34,9 @@ describe('<Minicart>', () => {
const { getByTestId } = render(
<I18nextProvider i18n={i18n}>
<CartProvider initialState={{ cartId: 'empty' }} reducerFactory={() => state => state}>
<Minicart />
<CheckoutProvider initialState={{}} reducer={state => state}>
<Minicart />
</CheckoutProvider>
</CartProvider>
</I18nextProvider>
);
Expand Down
1 change: 0 additions & 1 deletion react-components/src/components/Minicart/cartContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ export const reducerFactory = setCartCookie => {
return {
...state,
cartId: null,
isOpen: false,
errorMessage: null,
couponError: null,
cart: null
Expand Down
14 changes: 10 additions & 4 deletions react-components/src/components/Minicart/footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,21 @@ const Footer = () => {
const [{ isOpen, cart }] = useCartState();
const footerClassName = isOpen ? classes.root_open : classes.root;

const { subtotal_excluding_tax, subtotal_with_discount_excluding_tax } = cart.prices;

return (
<div className={footerClassName}>
let totalsSummary = null;
if (cart && cart.prices && cart.total_quantity) {
const { subtotal_excluding_tax, subtotal_with_discount_excluding_tax } = cart.prices;
totalsSummary = (
<TotalsSummary
subtotal={subtotal_excluding_tax}
subtotalDiscount={subtotal_with_discount_excluding_tax}
numItems={cart.total_quantity}
/>
);
}

return (
<div className={footerClassName}>
{totalsSummary}
<Checkout />
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion react-components/src/components/Minicart/minicart.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ import CartTrigger from '../CartTrigger';

import useMinicart from './useMinicart';
import LoadingIndicator from '../LoadingIndicator';
import { useCheckoutState } from '../Checkout/checkoutContext';

const MiniCart = () => {
const [createCartMutation] = useMutation(MUTATION_CREATE_CART);
const [addToCartMutation] = useMutation(MUTATION_ADD_TO_CART);
const [addVirtualItemMutation] = useMutation(MUTATION_ADD_VIRTUAL_TO_CART);
const [addSimpleAndVirtualItemMutation] = useMutation(MUTATION_ADD_SIMPLE_AND_VIRTUAL_TO_CART);
const cartDetailsQuery = useAwaitQuery(QUERY_CART_DETAILS);
const [{ flowState }] = useCheckoutState();

const [{ cart, isOpen, isLoading, isEditing, errorMessage }, { addItem, dispatch }] = useMinicart({
queries: {
Expand All @@ -58,7 +60,7 @@ const MiniCart = () => {

const rootClass = isOpen ? classes.root_open : classes.root;
const isEmpty = cart && Object.entries(cart).length > 0 ? cart.items.length === 0 : true;
const showFooter = !(isLoading || isEmpty || isEditing || errorMessage);
const showFooter = !(isLoading || isEmpty || isEditing || errorMessage) || flowState === 'receipt';
const footer = showFooter ? <Footer /> : null;

return (
Expand Down
8 changes: 7 additions & 1 deletion react-components/src/context/UserContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useCookieValue } from '../utils/hooks';
import { useMutation } from '@apollo/react-hooks';
import parseError from '../utils/parseError';
import { useAwaitQuery } from '../utils/hooks';
import { resetCustomerCart as resetCustomerCartAction } from '../actions/user';

import MUTATION_REVOKE_TOKEN from '../queries/mutation_revoke_customer_token.graphql';
import QUERY_CUSTOMER_DETAILS from '../queries/query_customer_details.graphql';
Expand Down Expand Up @@ -144,6 +145,10 @@ const UserContextProvider = props => {
}
};

const resetCustomerCart = async fetchCustomerCartQuery => {
await resetCustomerCartAction({ fetchCustomerCartQuery, dispatch });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should proxy this call. The components can bring their own query (in case of customizations) so maybe we should get that as an argument.

Copy link
Member Author

@herzog31 herzog31 Apr 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the query as a argument to the exposed function.

};

const resetPassword = async email => {
await Promise.resolve(email);
};
Expand All @@ -167,7 +172,8 @@ const UserContextProvider = props => {
signOut,
resetPassword,
setCustomerCart,
getUserDetails
getUserDetails,
resetCustomerCart
}
];
return <UserContext.Provider value={contextValue}>{children}</UserContext.Provider>;
Expand Down
31 changes: 31 additions & 0 deletions react-components/src/context/__test__/UserContext.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import MUTATION_CREATE_CART from '../../queries/mutation_create_guest_cart.graph
import QUERY_CUSTOMER_CART from '../../queries/query_customer_cart.graphql';

import UserContextProvider, { useUserContext } from '../UserContext';
import { useAwaitQuery } from '../../utils/hooks';

describe('UserContext test', () => {
beforeEach(() => {
Expand Down Expand Up @@ -159,6 +160,36 @@ describe('UserContext test', () => {
expect(result.textContent).toEqual('guest123');
});

it('resets the customer cart', async () => {
const ContextWrapper = () => {
const [{ cartId }, { resetCustomerCart }] = useUserContext();
const fetchCustomerCartQuery = useAwaitQuery(QUERY_CUSTOMER_CART);

let content;
if (cartId) {
content = <div data-testid="success">{cartId}</div>;
} else {
content = <button onClick={() => resetCustomerCart(fetchCustomerCartQuery)}>Reset cart</button>;
}

return <div>{content}</div>;
};

const { getByRole, getByTestId } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserContextProvider>
<ContextWrapper />
</UserContextProvider>
</MockedProvider>
);

expect(getByRole('button')).not.toBeUndefined();
fireEvent.click(getByRole('button'));
const result = await waitForElement(() => getByTestId('success'));
expect(result).not.toBeUndefined();
expect(result.textContent).toEqual('customercart');
});

it('performs a sign out', async () => {
const ContextWrapper = () => {
const [{ isSignedIn }, { signOut }] = useUserContext();
Expand Down