Skip to content

Commit b62ec2c

Browse files
authored
CIF-1072 - Support Registered Cart+Checkout (#234)
* Add the customer cart and the merge carts related queries * Add the user token for the cart queries. * Add the token to every request, if available. * Refresh the cart id in the state object after sign-in / sign-out * Reset the cart cookie after signing out * Update unit tests * Update dependencies versions
1 parent a119fc9 commit b62ec2c

30 files changed

+3881
-2517
lines changed

react-components/package-lock.json

Lines changed: 2745 additions & 1810 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react-components/package.json

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,53 +23,53 @@
2323
"snapshot:update": "jest --updateSnapshot"
2424
},
2525
"devDependencies": {
26-
"@apollo/react-hooks": "^3.0.0",
27-
"@apollo/react-testing": "^3.0.0",
28-
"@babel/core": "^7.5.5",
29-
"@babel/plugin-proposal-class-properties": "^7.5.5",
30-
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
31-
"@babel/preset-env": "^7.5.5",
32-
"@babel/preset-react": "^7.0.0",
33-
"@babel/runtime": "^7.5.5",
26+
"@apollo/react-hooks": "^3.1.3",
27+
"@apollo/react-testing": "^3.1.3",
28+
"@babel/core": "^7.8.7",
29+
"@babel/plugin-proposal-class-properties": "^7.8.3",
30+
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
31+
"@babel/preset-env": "^7.8.7",
32+
"@babel/preset-react": "^7.8.3",
33+
"@babel/runtime": "^7.8.7",
3434
"@magento/peregrine": "^3.0.0",
35-
"@testing-library/react": "^9.1.4",
35+
"@testing-library/react": "^9.5.0",
36+
"apollo-boost": "^0.4.7",
37+
"apollo-client": "^2.6.8",
38+
"babel-eslint": "^10.1.0",
3639
"acorn": "^6.3.0",
37-
"apollo-boost": "^0.4.3",
38-
"apollo-client": "^2.6.3",
39-
"babel-eslint": "^10.0.2",
4040
"babel-loader": "^8.0.6",
4141
"babel-plugin-graphql-tag": "^2.5.0",
42-
"braintree-web-drop-in": "^1.20.4",
42+
"braintree-web-drop-in": "^1.22.1",
4343
"clean-webpack-plugin": "^3.0.0",
44-
"css-loader": "^3.1.0",
44+
"css-loader": "^3.4.2",
4545
"dotenv-webpack": "^1.7.0",
46-
"eslint": "^6.2.0",
46+
"eslint": "^6.8.0",
4747
"eslint-plugin-header": "^3.0.0",
48-
"eslint-plugin-jest": "^22.15.1",
49-
"eslint-plugin-react": "^7.14.3",
48+
"eslint-plugin-jest": "^22.21.0",
49+
"eslint-plugin-react": "^7.19.0",
5050
"eslint-plugin-react-hooks": "^1.6.1",
51-
"file-loader": "^4.1.0",
52-
"graphql": "^14.4.2",
51+
"file-loader": "^4.3.0",
52+
"graphql": "^14.6.0",
5353
"i18next": "^19.3.2",
5454
"i18next-scanner": "^2.10.3",
5555
"identity-obj-proxy": "^3.0.0",
56-
"informed": "^2.11.8",
56+
"informed": "^2.11.17",
5757
"jest": "^24.9.0",
5858
"jest-junit": "^10.0.0",
5959
"jest-transform-graphql": "^2.1.0",
60-
"mini-css-extract-plugin": "^0.8.0",
60+
"mini-css-extract-plugin": "^0.8.2",
6161
"prettier": "^1.19.1",
6262
"prop-types": "^15.7.2",
63-
"react": "^16.11.0",
63+
"react": "^16.13.0",
6464
"react-apollo": "^2.5.8",
65-
"react-dom": "^16.11.0",
65+
"react-dom": "^16.13.0",
6666
"react-feather": "^2.0.3",
6767
"react-i18next": "^11.3.3",
6868
"react-router-dom": "^5.1.2",
69-
"react-test-renderer": "^16.11.0",
69+
"react-test-renderer": "^16.13.0",
7070
"style-loader": "^0.23.1",
71-
"webpack": "^4.38.0",
72-
"webpack-cli": "^3.3.10",
71+
"webpack": "^4.42.0",
72+
"webpack-cli": "^3.3.11",
7373
"webpack-node-externals": "^1.7.2"
7474
},
7575
"peerDependencies": {

react-components/src/components/App/app.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,35 @@ import ApolloClient from 'apollo-boost';
2020
import { CartProvider, CartInitializer } from '../Minicart';
2121
import { CheckoutProvider } from '../Checkout';
2222
import UserContextProvider from '../../context/UserContext';
23+
import { checkCookie, cookieValue } from '../../utils/cookieUtils';
2324

2425
const App = props => {
2526
const { uri, storeView = 'default' } = props;
2627

2728
const client = new ApolloClient({
2829
uri,
29-
headers: { Store: storeView }
30+
headers: { Store: storeView },
31+
request: operation => {
32+
let token = checkCookie('cif.userToken') ? cookieValue('cif.userToken') : '';
33+
if (token.length > 0) {
34+
operation.setContext({
35+
headers: {
36+
authorization: `Bearer ${token && token.length > 0 ? token : ''}`
37+
}
38+
});
39+
}
40+
}
3041
});
3142

3243
return (
3344
<ApolloProvider client={client}>
34-
<CartProvider>
35-
<UserContextProvider>
45+
<UserContextProvider>
46+
<CartProvider>
3647
<CartInitializer>
3748
<CheckoutProvider>{props.children}</CheckoutProvider>
3849
</CartInitializer>
39-
</UserContextProvider>
40-
</CartProvider>
50+
</CartProvider>
51+
</UserContextProvider>
4152
</ApolloProvider>
4253
);
4354
};

react-components/src/components/AuthBar/authBar.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* governing permissions and limitations under the License.
1212
*
1313
******************************************************************************/
14-
import React from 'react';
14+
import React, { useEffect } from 'react';
1515
import { useTranslation } from 'react-i18next';
1616

1717
import Button from '../Button';
@@ -22,8 +22,13 @@ import { func } from 'prop-types';
2222

2323
const AuthBar = props => {
2424
const { showSignIn, showMyAccount } = props;
25+
const [{ currentUser, isSignedIn }, { getUserDetails }] = useUserContext();
2526

26-
const [{ currentUser, isSignedIn }] = useUserContext();
27+
useEffect(() => {
28+
if (isSignedIn && currentUser.email === '') {
29+
getUserDetails();
30+
}
31+
}, [getUserDetails]);
2732
const [t] = useTranslation('account');
2833

2934
const disabled = false;

react-components/src/components/Checkout/__test__/editableForm.test.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { render, waitForElement } from '@testing-library/react';
1717
import EditableForm from '../editableForm';
1818
import { CartProvider } from '../../Minicart/cartContext';
1919
import { CheckoutProvider } from '../checkoutContext';
20+
import UserContextProvider from '../../../context/UserContext';
2021

2122
import QUERY_COUNTRIES from '../../../queries/query_countries.graphql';
2223

@@ -45,11 +46,15 @@ describe('<EditableForm />', () => {
4546

4647
const { queryByText } = render(
4748
<MockedProvider mocks={mocks} addTypename={false}>
48-
<CartProvider initialState={{}} reducerFactory={() => state => state}>
49-
<CheckoutProvider initialState={{ editing: 'address', flowState: 'form' }} reducer={state => state}>
50-
<EditableForm />
51-
</CheckoutProvider>
52-
</CartProvider>
49+
<UserContextProvider>
50+
<CartProvider initialState={{}} reducerFactory={() => state => state}>
51+
<CheckoutProvider
52+
initialState={{ editing: 'address', flowState: 'form' }}
53+
reducer={state => state}>
54+
<EditableForm />
55+
</CheckoutProvider>
56+
</CartProvider>
57+
</UserContextProvider>
5358
</MockedProvider>
5459
);
5560

@@ -76,11 +81,15 @@ describe('<EditableForm />', () => {
7681

7782
const { asFragment } = render(
7883
<MockedProvider mocks={mocks} addTypename={false}>
79-
<CartProvider initialState={{}} reducerFactory={() => state => state}>
80-
<CheckoutProvider initialState={{ editing: 'address', flowState: 'form' }} reducer={state => state}>
81-
<EditableForm />
82-
</CheckoutProvider>
83-
</CartProvider>
84+
<UserContextProvider>
85+
<CartProvider initialState={{}} reducerFactory={() => state => state}>
86+
<CheckoutProvider
87+
initialState={{ editing: 'address', flowState: 'form' }}
88+
reducer={state => state}>
89+
<EditableForm />
90+
</CheckoutProvider>
91+
</CartProvider>
92+
</UserContextProvider>
8493
</MockedProvider>
8594
);
8695

react-components/src/components/Checkout/editableForm.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import MUTATION_SET_BILLING_ADDRESS from '../../queries/mutation_set_billing_add
2929
import MUTATION_SET_SHIPPING_METHOD from '../../queries/mutation_set_shipping_method.graphql';
3030
import MUTATION_SET_EMAIL from '../../queries/mutation_set_email_on_cart.graphql';
3131
import { useCheckoutState } from './checkoutContext';
32+
import { useUserContext } from '../../context/UserContext';
3233

3334
/**
3435
* The EditableForm component renders the actual edit forms for the sections
@@ -39,7 +40,7 @@ const EditableForm = props => {
3940
const [{ cart, cartId }, cartDispatch] = useCartState();
4041
const [{ editing, shippingAddress, shippingMethod, paymentMethod, billingAddress }, dispatch] = useCheckoutState();
4142
const { error: countriesError, countries } = useCountries();
42-
43+
const [{ isSignedIn }] = useUserContext();
4344
const [setShippingAddressesOnCart, { data, error }] = useMutation(MUTATION_SET_SHIPPING_ADDRESS);
4445

4546
const [
@@ -88,7 +89,9 @@ const EditableForm = props => {
8889
const handleSubmitAddressForm = useCallback(
8990
formValues => {
9091
setShippingAddressesOnCart({ variables: { cartId: cartId, countryCode: 'US', ...formValues } });
91-
setGuestEmailOnCart({ variables: { cartId: cartId, email: formValues.email } });
92+
if (!isSignedIn) {
93+
setGuestEmailOnCart({ variables: { cartId: cartId, email: formValues.email } });
94+
}
9295
},
9396
[dispatch, setShippingAddressesOnCart]
9497
);
@@ -150,13 +153,14 @@ const EditableForm = props => {
150153
[dispatch, submitShippingMethod]
151154
);
152155

153-
if (data && guestEmailResult) {
156+
if (data && (isSignedIn || guestEmailResult)) {
157+
const guestEmail = guestEmailResult ? { email: guestEmailResult.setGuestEmailOnCart.cart.email } : {};
154158
const newShippingAddress = data.setShippingAddressesOnCart.cart.shipping_addresses[0];
155159
dispatch({
156160
type: 'setShippingAddress',
157161
shippingAddress: {
158162
...newShippingAddress,
159-
email: guestEmailResult.setGuestEmailOnCart.cart.email,
163+
...guestEmail,
160164
country: newShippingAddress.country.code,
161165
region_code: newShippingAddress.region.code
162166
}

react-components/src/components/Checkout/overview.js

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,60 +13,55 @@
1313
******************************************************************************/
1414
import React, { Fragment, useCallback } from 'react';
1515
import { shape, string } from 'prop-types';
16-
import { useMutation } from '@apollo/react-hooks';
16+
17+
import { Price } from '@magento/peregrine';
1718
import { useTranslation } from 'react-i18next';
1819

1920
import PaymentMethodSummary from './paymentMethodSummary';
2021
import ShippingAddressSummary from './shippingAddressSummary';
2122
import ShippingMethodSummary from './shippingMethodSummary';
23+
import LoadingIndicator from '../LoadingIndicator';
2224
import Section from './section';
2325
import Button from '../Button';
24-
import { Price } from '@magento/peregrine';
25-
import MUTATION_PLACE_ORDER from '../../queries/mutation_place_order.graphql';
26-
import { useCartState } from '../Minicart/cartContext';
27-
import { useCheckoutState } from './checkoutContext';
26+
import useOverview from './useOverview';
2827

2928
/**
3029
* The Overview component renders summaries for each section of the editable
3130
* form.
3231
*/
3332
const Overview = props => {
3433
const { classes } = props;
35-
const [{ cart, cartId }, cartDispatch] = useCartState();
36-
const [{ shippingAddress, shippingMethod, paymentMethod }, dispatch] = useCheckoutState();
3734
const [t] = useTranslation('checkout');
3835

39-
const [placeOrder, { data, error }] = useMutation(MUTATION_PLACE_ORDER);
36+
const [
37+
{ shippingAddress, shippingMethod, paymentMethod, cart, inProgress },
38+
{ placeOrder, checkoutDispatch }
39+
] = useOverview();
4040

4141
const ready = shippingAddress && paymentMethod && shippingMethod;
4242

43-
const submitOrder = useCallback(() => {
44-
placeOrder({ variables: { cartId: cartId } });
45-
}, [placeOrder]);
46-
47-
if (error) {
48-
cartDispatch({ type: 'error', error: error.toString() });
49-
}
50-
51-
if (data) {
52-
dispatch({ type: 'placeOrder', order: data.placeOrder.order });
43+
if (inProgress) {
44+
return <LoadingIndicator message="Placing order"></LoadingIndicator>;
5345
}
46+
const submitOrder = async () => {
47+
await placeOrder(cart.id);
48+
};
5449

5550
return (
5651
<Fragment>
5752
<div className={classes.body}>
5853
<Section
5954
label={t('checkout:ship-to', 'Ship To')}
6055
onClick={() => {
61-
dispatch({ type: 'setEditing', editing: 'address' });
56+
checkoutDispatch({ type: 'setEditing', editing: 'address' });
6257
}}
6358
showEditIcon={!!shippingAddress}>
6459
<ShippingAddressSummary classes={classes} />
6560
</Section>
6661
<Section
6762
label={t('checkout:pay-with', 'Pay With')}
6863
onClick={() => {
69-
dispatch({ type: 'setEditing', editing: 'paymentMethod' });
64+
checkoutDispatch({ type: 'setEditing', editing: 'paymentMethod' });
7065
}}
7166
showEditIcon={!!paymentMethod}
7267
disabled={!shippingAddress}>
@@ -75,7 +70,7 @@ const Overview = props => {
7570
<Section
7671
label={t('checkout:use', 'Use')}
7772
onClick={() => {
78-
dispatch({ type: 'setEditing', editing: 'shippingMethod' });
73+
checkoutDispatch({ type: 'setEditing', editing: 'shippingMethod' });
7974
}}
8075
showEditIcon={!!shippingMethod}
8176
disabled={!shippingAddress}>
@@ -88,9 +83,11 @@ const Overview = props => {
8883
</Section>
8984
</div>
9085
<div className={classes.footer}>
91-
<Button onClick={() => dispatch({ type: 'cancelCheckout' })}>
86+
<Button onClick={() => checkoutDispatch({ type: 'cancelCheckout' })}>
87+
{' '}
9288
{t('checkout:back-to-cart', 'Back to cart')}
9389
</Button>
90+
9491
<Button priority="high" disabled={!ready} onClick={submitOrder}>
9592
{t('checkout:confirm-order', 'Confirm Order')}
9693
</Button>

react-components/src/components/Checkout/paymentProviders/__test__/braintree.test.js

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,13 @@ describe('<Braintree />', () => {
7979
let mockCartDispatchFn = jest.fn(state => state);
8080

8181
render(
82-
<CartProvider initialState={{}} reducerFactory={() => mockCartDispatchFn}>
83-
<CheckoutProvider initialState={{ braintreeToken: 'my-sample-token' }} reducer={state => state}>
84-
<Braintree accept="card" />
85-
</CheckoutProvider>
86-
</CartProvider>
82+
<MockedProvider>
83+
<CartProvider initialState={{}} reducerFactory={() => mockCartDispatchFn}>
84+
<CheckoutProvider initialState={{ braintreeToken: 'my-sample-token' }} reducer={state => state}>
85+
<Braintree accept="card" />
86+
</CheckoutProvider>
87+
</CartProvider>
88+
</MockedProvider>
8789
);
8890

8991
await wait(() => dropIn.create.mock.calls.length === 1 && mockOnFn.mock.calls.length === 2);
@@ -120,11 +122,13 @@ describe('<Braintree />', () => {
120122
};
121123

122124
render(
123-
<CartProvider initialState={cartState} reducerFactory={() => mockCartDispatchFn}>
124-
<CheckoutProvider initialState={{ braintreeToken: 'my-sample-token' }} reducer={state => state}>
125-
<Braintree accept="paypal" />
126-
</CheckoutProvider>
127-
</CartProvider>
125+
<MockedProvider>
126+
<CartProvider initialState={cartState} reducerFactory={() => mockCartDispatchFn}>
127+
<CheckoutProvider initialState={{ braintreeToken: 'my-sample-token' }} reducer={state => state}>
128+
<Braintree accept="paypal" />
129+
</CheckoutProvider>
130+
</CartProvider>
131+
</MockedProvider>
128132
);
129133

130134
await wait(() => dropIn.create.mock.calls.length === 1 && mockOnFn.mock.calls.length === 2);
@@ -160,11 +164,13 @@ describe('<Braintree />', () => {
160164

161165
// Step 1, create Dropin
162166
render(
163-
<CartProvider initialState={{}} reducerFactory={() => mockCartDispatchFn}>
164-
<CheckoutProvider initialState={{ braintreeToken: 'my-sample-token' }} reducer={state => state}>
165-
<Braintree accept="card" />
166-
</CheckoutProvider>
167-
</CartProvider>
167+
<MockedProvider>
168+
<CartProvider initialState={{}} reducerFactory={() => mockCartDispatchFn}>
169+
<CheckoutProvider initialState={{ braintreeToken: 'my-sample-token' }} reducer={state => state}>
170+
<Braintree accept="card" />
171+
</CheckoutProvider>
172+
</CartProvider>
173+
</MockedProvider>
168174
);
169175

170176
await wait(() => dropIn.create.mock.calls.length === 1 && mockOnFn.mock.calls.length === 2);

0 commit comments

Comments
 (0)