From 42058c52994fd3447bac4c7ffca0bc899e9def59 Mon Sep 17 00:00:00 2001 From: Jimmy Sanford Date: Tue, 8 Dec 2020 15:11:18 -0700 Subject: [PATCH 1/7] Add middleware to schedule sign-out --- .../lib/store/enhancers/middleware.js | 3 +- .../peregrine/lib/store/middleware/auth.js | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 packages/peregrine/lib/store/middleware/auth.js diff --git a/packages/peregrine/lib/store/enhancers/middleware.js b/packages/peregrine/lib/store/enhancers/middleware.js index 8a519526bb..116fc9dd81 100644 --- a/packages/peregrine/lib/store/enhancers/middleware.js +++ b/packages/peregrine/lib/store/enhancers/middleware.js @@ -1,9 +1,10 @@ import { applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; +import auth from '../middleware/auth'; import log from '../middleware/log'; -const middleware = [thunk]; +const middleware = [thunk, auth]; if (process.env.NODE_ENV !== 'production') { middleware.push(log); diff --git a/packages/peregrine/lib/store/middleware/auth.js b/packages/peregrine/lib/store/middleware/auth.js new file mode 100644 index 0000000000..4737a92b2e --- /dev/null +++ b/packages/peregrine/lib/store/middleware/auth.js @@ -0,0 +1,60 @@ +import BrowserPersistence from '../../util/simplePersistence'; +import userActions, { signOut } from '../actions/user'; + +const timers = new Map(); +const { KEY } = BrowserPersistence; +const SET_TOKEN = userActions.setToken.toString(); +const CLEAR_TOKEN = userActions.clearToken.toString(); +const GET_DETAILS = userActions.getDetails.request.toString(); + +const isSigningIn = type => type === SET_TOKEN || type === GET_DETAILS; +const isSigningOut = type => type === CLEAR_TOKEN; + +/** + * This function adheres to Redux's middleware pattern. + * + * @param {Store} store The store to augment. + * @returns {Function} + */ +const scheduleSignOut = store => next => action => { + const { dispatch } = store; + + if (isSigningIn(action.type)) { + // `BrowserPersistence.getItem()` only returns the value + // but we need the full item with timestamp and ttl + const item = localStorage.getItem(`${KEY}__signin_token`); + + // exit if there's nothing in storage + if (!item) return next(action); + + const { timeStored, ttl, value } = JSON.parse(item); + const parsedValue = JSON.parse(value); + const elapsed = Date.now() - timeStored; + const delay = Math.min(ttl * 1000 - elapsed, 0); + + // only set one timer per token + if (!timers.has(parsedValue)) { + const timeoutId = setTimeout(() => { + timers.delete(parsedValue); + // clear token and customer state + dispatch(signOut()).then(() => { + // refresh the page, important for checkout + history.go(0); + }); + }, delay); + + timers.set(parsedValue, timeoutId); + } + } else if (isSigningOut(action.type)) { + // clear any lingering timers when a user signs out + for (const timeoutId of timers) { + clearTimeout(timeoutId); + } + + timers.clear(); + } + + return next(action); +}; + +export default scheduleSignOut; From 33f0b908a33f0764d434a17093eb3ffc0e4c1c53 Mon Sep 17 00:00:00 2001 From: Jimmy Sanford Date: Wed, 9 Dec 2020 12:27:00 -0700 Subject: [PATCH 2/7] Augment persistence API --- packages/peregrine/lib/store/middleware/auth.js | 6 +++--- packages/peregrine/lib/util/simplePersistence.js | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/peregrine/lib/store/middleware/auth.js b/packages/peregrine/lib/store/middleware/auth.js index 4737a92b2e..aaef0027e0 100644 --- a/packages/peregrine/lib/store/middleware/auth.js +++ b/packages/peregrine/lib/store/middleware/auth.js @@ -2,7 +2,7 @@ import BrowserPersistence from '../../util/simplePersistence'; import userActions, { signOut } from '../actions/user'; const timers = new Map(); -const { KEY } = BrowserPersistence; +const storage = new BrowserPersistence(); const SET_TOKEN = userActions.setToken.toString(); const CLEAR_TOKEN = userActions.clearToken.toString(); const GET_DETAILS = userActions.getDetails.request.toString(); @@ -22,7 +22,7 @@ const scheduleSignOut = store => next => action => { if (isSigningIn(action.type)) { // `BrowserPersistence.getItem()` only returns the value // but we need the full item with timestamp and ttl - const item = localStorage.getItem(`${KEY}__signin_token`); + const item = storage.getRawItem('signin_token'); // exit if there's nothing in storage if (!item) return next(action); @@ -30,7 +30,7 @@ const scheduleSignOut = store => next => action => { const { timeStored, ttl, value } = JSON.parse(item); const parsedValue = JSON.parse(value); const elapsed = Date.now() - timeStored; - const delay = Math.min(ttl * 1000 - elapsed, 0); + const delay = Math.max(ttl * 1000 - elapsed, 0); // only set one timer per token if (!timers.has(parsedValue)) { diff --git a/packages/peregrine/lib/util/simplePersistence.js b/packages/peregrine/lib/util/simplePersistence.js index f07a08c48b..02950aaa84 100644 --- a/packages/peregrine/lib/util/simplePersistence.js +++ b/packages/peregrine/lib/util/simplePersistence.js @@ -30,6 +30,9 @@ export default class BrowserPersistence { this.constructor.KEY || BrowserPersistence.KEY ); } + getRawItem(name) { + return this.storage.getItem(name); + } getItem(name) { const now = Date.now(); const item = this.storage.getItem(name); From c7e62f89458d61d817ee6db78cff6358813db299 Mon Sep 17 00:00:00 2001 From: Jimmy Sanford Date: Wed, 6 Jan 2021 12:12:25 -0700 Subject: [PATCH 3/7] Provide client to thunk middleware --- .../lib/Apollo/attachClientToStore.js | 7 +++ packages/peregrine/lib/context/cart.js | 10 +--- .../lib/store/actions/cart/asyncActions.js | 4 +- .../lib/store/actions/user/asyncActions.js | 6 +- .../lib/store/enhancers/middleware.js | 2 +- .../peregrine/lib/store/middleware/thunk.js | 4 ++ packages/venia-ui/lib/drivers/adapter.js | 58 +++++++++---------- 7 files changed, 48 insertions(+), 43 deletions(-) create mode 100644 packages/peregrine/lib/Apollo/attachClientToStore.js create mode 100644 packages/peregrine/lib/store/middleware/thunk.js diff --git a/packages/peregrine/lib/Apollo/attachClientToStore.js b/packages/peregrine/lib/Apollo/attachClientToStore.js new file mode 100644 index 0000000000..71291d7109 --- /dev/null +++ b/packages/peregrine/lib/Apollo/attachClientToStore.js @@ -0,0 +1,7 @@ +import { extraArgument } from '../store/middleware/thunk'; + +const attachClientToStore = apolloClient => { + Object.assign(extraArgument, { apolloClient }); +}; + +export default attachClientToStore; diff --git a/packages/peregrine/lib/context/cart.js b/packages/peregrine/lib/context/cart.js index f6a9fc18ef..e0fa2b6620 100644 --- a/packages/peregrine/lib/context/cart.js +++ b/packages/peregrine/lib/context/cart.js @@ -1,6 +1,6 @@ import React, { createContext, useContext, useEffect, useMemo } from 'react'; import { connect } from 'react-redux'; -import { useApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import gql from 'graphql-tag'; import { useAwaitQuery } from '@magento/peregrine/lib/hooks/useAwaitQuery'; @@ -55,20 +55,16 @@ const CartContextProvider = props => { derivedCartState ]); - const apolloClient = useApolloClient(); const [fetchCartId] = useMutation(CREATE_CART_MUTATION); const fetchCartDetails = useAwaitQuery(CART_DETAILS_QUERY); useEffect(() => { - // cartApi.getCartDetails initializes the cart if there isn't one. Also, we pass - // apolloClient to wipe the store in event of auth token expiry which - // will only happen if the user refreshes. + // cartApi.getCartDetails initializes the cart if there isn't one. cartApi.getCartDetails({ - apolloClient, fetchCartId, fetchCartDetails }); - }, [apolloClient, cartApi, fetchCartDetails, fetchCartId]); + }, [cartApi, fetchCartDetails, fetchCartId]); return ( diff --git a/packages/peregrine/lib/store/actions/cart/asyncActions.js b/packages/peregrine/lib/store/actions/cart/asyncActions.js index 45da0d5661..fbbacbdfc1 100644 --- a/packages/peregrine/lib/store/actions/cart/asyncActions.js +++ b/packages/peregrine/lib/store/actions/cart/asyncActions.js @@ -317,9 +317,9 @@ export const removeItemFromCart = payload => { }; export const getCartDetails = payload => { - const { apolloClient, fetchCartId, fetchCartDetails } = payload; + const { fetchCartId, fetchCartDetails } = payload; - return async function thunk(dispatch, getState) { + return async function thunk(dispatch, getState, { apolloClient }) { const { cart, user } = getState(); const { cartId } = cart; const { isSignedIn } = user; diff --git a/packages/peregrine/lib/store/actions/user/asyncActions.js b/packages/peregrine/lib/store/actions/user/asyncActions.js index cd472573a5..d67bfa81d2 100755 --- a/packages/peregrine/lib/store/actions/user/asyncActions.js +++ b/packages/peregrine/lib/store/actions/user/asyncActions.js @@ -1,4 +1,6 @@ import BrowserPersistence from '../../../util/simplePersistence'; +import { clearCartDataFromCache } from '../../../Apollo/clearCartDataFromCache'; +import { clearCustomerDataFromCache } from '../../../Apollo/clearCustomerDataFromCache'; import { removeCart } from '../cart'; import { clearCheckoutDataFromStorage } from '../checkout'; @@ -7,7 +9,7 @@ import actions from './actions'; const storage = new BrowserPersistence(); export const signOut = (payload = {}) => - async function thunk(dispatch) { + async function thunk(dispatch, getState, { apolloClient }) { const { revokeToken } = payload; if (revokeToken) { @@ -23,6 +25,8 @@ export const signOut = (payload = {}) => await dispatch(clearToken()); await dispatch(actions.reset()); await clearCheckoutDataFromStorage(); + await clearCartDataFromCache(apolloClient); + await clearCustomerDataFromCache(apolloClient); // Now that we're signed out, forget the old (customer) cart. // We don't need to create a new cart here because we're going to refresh diff --git a/packages/peregrine/lib/store/enhancers/middleware.js b/packages/peregrine/lib/store/enhancers/middleware.js index 116fc9dd81..79fb6f534b 100644 --- a/packages/peregrine/lib/store/enhancers/middleware.js +++ b/packages/peregrine/lib/store/enhancers/middleware.js @@ -1,8 +1,8 @@ import { applyMiddleware } from 'redux'; -import thunk from 'redux-thunk'; import auth from '../middleware/auth'; import log from '../middleware/log'; +import thunk from '../middleware/thunk'; const middleware = [thunk, auth]; diff --git a/packages/peregrine/lib/store/middleware/thunk.js b/packages/peregrine/lib/store/middleware/thunk.js new file mode 100644 index 0000000000..42f78aa53a --- /dev/null +++ b/packages/peregrine/lib/store/middleware/thunk.js @@ -0,0 +1,4 @@ +import thunk from 'redux-thunk'; + +export const extraArgument = {}; +export default thunk.withExtraArgument(extraArgument); diff --git a/packages/venia-ui/lib/drivers/adapter.js b/packages/venia-ui/lib/drivers/adapter.js index 35320314d8..3c1bf5bdd7 100644 --- a/packages/venia-ui/lib/drivers/adapter.js +++ b/packages/venia-ui/lib/drivers/adapter.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { func, shape, string } from 'prop-types'; import { CachePersistor } from 'apollo-cache-persist'; import { ApolloProvider, createHttpLink } from '@apollo/client'; @@ -9,6 +9,7 @@ import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import { BrowserPersistence } from '@magento/peregrine/lib/util'; +import attachClient from '@magento/peregrine/lib/Apollo/attachClientToStore'; import typePolicies from '@magento/peregrine/lib/Apollo/policies'; import StoreCodeRoute from '../components/StoreCodeRoute'; @@ -46,44 +47,37 @@ const storage = new BrowserPersistence(); */ const VeniaAdapter = props => { const { apiBase, apollo = {}, children, store } = props; + const [initialized, setInitialized] = useState(false); - const cache = apollo.cache || preInstantiatedCache; - const link = apollo.link || VeniaAdapter.apolloLink(apiBase); - - const persistor = new CachePersistor({ - cache, - storage: window.localStorage, - debug: process.env.NODE_ENV === 'development' - }); - - let apolloClient; - if (apollo.client) { - apolloClient = apollo.client; - } else { - apolloClient = new ApolloClient({ + const apolloClient = useMemo(() => { + const cache = apollo.cache || preInstantiatedCache; + const link = apollo.link || VeniaAdapter.apolloLink(apiBase); + const client = apollo.client || new ApolloClient({ cache, link }); + const persistor = new CachePersistor({ cache, - link + storage: window.localStorage, + debug: process.env.NODE_ENV === 'development' }); - apolloClient.apiBase = apiBase; - } - - apolloClient.persistor = persistor; - const [initialized, setInitialized] = useState(false); + return Object.assign(client, { apiBase, persistor }); + }, [apiBase, apollo]); + // perform blocking async work here useEffect(() => { - async function initialize() { - // On load, restore the persisted data to the apollo cache and then - // allow rendering. You can do other async blocking stuff here. - if (persistor) { - await persistor.restore(); - } + if (initialized) return; + + // immediately invoke this async function + (async () => { + // restore persisted data to the Apollo cache + await apolloClient.persistor.restore(); + + // attach the Apollo client to the Redux store + attachClient(apolloClient); + + // mark this routine as complete setInitialized(true); - } - if (!initialized) { - initialize(); - } - }, [initialized, persistor]); + })(); + }, [apolloClient, initialized]); if (!initialized) { // TODO: Replace with app skeleton. See PWA-547. From 7aa638a3058bfdeab4a4bcaf1f556a84305adf37 Mon Sep 17 00:00:00 2001 From: Jimmy Sanford Date: Wed, 6 Jan 2021 14:23:45 -0700 Subject: [PATCH 4/7] Fix tests --- .../actions/cart/__tests__/asyncActions.spec.js | 8 +++++++- .../actions/user/__tests__/asyncActions.spec.js | 13 ++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/peregrine/lib/store/actions/cart/__tests__/asyncActions.spec.js b/packages/peregrine/lib/store/actions/cart/__tests__/asyncActions.spec.js index de02478984..f9a8ad8cdc 100644 --- a/packages/peregrine/lib/store/actions/cart/__tests__/asyncActions.spec.js +++ b/packages/peregrine/lib/store/actions/cart/__tests__/asyncActions.spec.js @@ -32,7 +32,13 @@ const getState = jest.fn(() => ({ cart: { cartId: 'CART_ID' }, user: { isSignedIn: false } })); -const thunkArgs = [dispatch, getState]; +const thunkArgs = [ + dispatch, + getState, + { + apolloClient: {} + } +]; describe('createCart', () => { test('it returns a thunk', () => { diff --git a/packages/peregrine/lib/store/actions/user/__tests__/asyncActions.spec.js b/packages/peregrine/lib/store/actions/user/__tests__/asyncActions.spec.js index e99b10eef3..db775a2627 100644 --- a/packages/peregrine/lib/store/actions/user/__tests__/asyncActions.spec.js +++ b/packages/peregrine/lib/store/actions/user/__tests__/asyncActions.spec.js @@ -8,7 +8,14 @@ const dispatch = jest.fn(); const getState = jest.fn(() => ({ user: { isSignedIn: false } })); -const thunkArgs = [dispatch, getState]; +const thunkArgs = [ + dispatch, + getState, + { + apolloClient: {} + } +]; + const fetchUserDetails = jest .fn() .mockResolvedValue({ data: { customer: {} } }); @@ -102,7 +109,7 @@ describe('signOut', () => { }); test('signOut thunk invokes revokeToken and dispatchs actions', async () => { - await signOut({ revokeToken })(dispatch); + await signOut({ revokeToken })(...thunkArgs); expect(revokeToken).toHaveBeenCalledTimes(1); expect(dispatch).toHaveBeenCalledTimes(3); @@ -112,7 +119,7 @@ describe('signOut', () => { const consoleSpy = jest.spyOn(console, 'error'); revokeToken.mockRejectedValueOnce(new Error('Revoke Token Error')); - await signOut({ revokeToken })(dispatch); + await signOut({ revokeToken })(...thunkArgs); expect(revokeToken).toHaveBeenCalledTimes(1); expect(consoleSpy).toHaveBeenCalledTimes(1); From c1bda3d9bedde820bc0f5333f549738d590c766e Mon Sep 17 00:00:00 2001 From: Jimmy Sanford Date: Thu, 7 Jan 2021 14:35:58 -0700 Subject: [PATCH 5/7] Cover all sign-out cases --- .../peregrine/lib/store/actions/cart/asyncActions.js | 11 ++--------- .../peregrine/lib/talons/AuthModal/useAuthModal.js | 9 ++------- .../talons/Header/__tests__/useAccountMenu.spec.js | 8 -------- .../peregrine/lib/talons/Header/useAccountMenu.js | 9 ++------- 4 files changed, 6 insertions(+), 31 deletions(-) diff --git a/packages/peregrine/lib/store/actions/cart/asyncActions.js b/packages/peregrine/lib/store/actions/cart/asyncActions.js index fbbacbdfc1..554bc28fc7 100644 --- a/packages/peregrine/lib/store/actions/cart/asyncActions.js +++ b/packages/peregrine/lib/store/actions/cart/asyncActions.js @@ -1,4 +1,3 @@ -import { clearCartDataFromCache } from '../../../Apollo/clearCartDataFromCache'; import BrowserPersistence from '../../../util/simplePersistence'; import { signOut } from '../user'; import actions from './actions'; @@ -319,7 +318,7 @@ export const removeItemFromCart = payload => { export const getCartDetails = payload => { const { fetchCartId, fetchCartDetails } = payload; - return async function thunk(dispatch, getState, { apolloClient }) { + return async function thunk(dispatch, getState) { const { cart, user } = getState(); const { cartId } = cart; const { isSignedIn } = user; @@ -367,13 +366,7 @@ export const getCartDetails = payload => { await dispatch(removeCart()); } - // Clear the cart data from apollo client if we get here and - // have an apolloClient. - if (apolloClient) { - await clearCartDataFromCache(apolloClient); - } - - // Create a new one + // Create a new cart try { await dispatch( createCart({ diff --git a/packages/peregrine/lib/talons/AuthModal/useAuthModal.js b/packages/peregrine/lib/talons/AuthModal/useAuthModal.js index aa5c358f7a..4e355ebeee 100644 --- a/packages/peregrine/lib/talons/AuthModal/useAuthModal.js +++ b/packages/peregrine/lib/talons/AuthModal/useAuthModal.js @@ -1,10 +1,8 @@ import { useCallback, useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { useApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import mergeOperations from '../../util/shallowMerge'; -import { clearCartDataFromCache } from '../../Apollo/clearCartDataFromCache'; -import { clearCustomerDataFromCache } from '../../Apollo/clearCustomerDataFromCache'; import { useUserContext } from '../../context/user'; import DEFAULT_OPERATIONS from './authModal.gql'; @@ -48,7 +46,6 @@ export const useAuthModal = props => { const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations); const { signOutMutation } = operations; - const apolloClient = useApolloClient(); const [isSigningOut, setIsSigningOut] = useState(false); const [username, setUsername] = useState(''); const [{ currentUser, isSignedIn }, { signOut }] = useUserContext(); @@ -89,14 +86,12 @@ export const useAuthModal = props => { // Delete cart/user data from the redux store. await signOut({ revokeToken }); - await clearCartDataFromCache(apolloClient); - await clearCustomerDataFromCache(apolloClient); // Refresh the page as a way to say "re-initialize". An alternative // would be to call apolloClient.resetStore() but that would require // a large refactor. history.go(0); - }, [apolloClient, history, revokeToken, signOut]); + }, [history, revokeToken, signOut]); return { handleCancel, diff --git a/packages/peregrine/lib/talons/Header/__tests__/useAccountMenu.spec.js b/packages/peregrine/lib/talons/Header/__tests__/useAccountMenu.spec.js index 8ca7ab3ce0..6ae4dd8438 100644 --- a/packages/peregrine/lib/talons/Header/__tests__/useAccountMenu.spec.js +++ b/packages/peregrine/lib/talons/Header/__tests__/useAccountMenu.spec.js @@ -46,14 +46,6 @@ jest.mock('@magento/peregrine/lib/hooks/useDropdown', () => ({ }) })); -jest.mock('@magento/peregrine/lib/Apollo/clearCartDataFromCache', () => ({ - clearCartDataFromCache: jest.fn().mockResolvedValue() -})); - -jest.mock('@magento/peregrine/lib/Apollo/clearCustomerDataFromCache', () => ({ - clearCustomerDataFromCache: jest.fn().mockResolvedValue() -})); - const defaultProps = { accountMenuIsOpen: false, setAccountMenuIsOpen: jest.fn() diff --git a/packages/peregrine/lib/talons/Header/useAccountMenu.js b/packages/peregrine/lib/talons/Header/useAccountMenu.js index 3870807596..d5409464a6 100644 --- a/packages/peregrine/lib/talons/Header/useAccountMenu.js +++ b/packages/peregrine/lib/talons/Header/useAccountMenu.js @@ -1,9 +1,7 @@ import { useState, useCallback, useEffect } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import { useApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; -import { clearCartDataFromCache } from '../../Apollo/clearCartDataFromCache'; -import { clearCustomerDataFromCache } from '../../Apollo/clearCustomerDataFromCache'; import mergeOperations from '../../util/shallowMerge'; import { useUserContext } from '../../context/user'; @@ -36,7 +34,6 @@ export const useAccountMenu = props => { const [view, setView] = useState('SIGNIN'); const [username, setUsername] = useState(''); - const apolloClient = useApolloClient(); const history = useHistory(); const location = useLocation(); const [revokeToken] = useMutation(signOutMutation); @@ -48,14 +45,12 @@ export const useAccountMenu = props => { // Delete cart/user data from the redux store. await signOut({ revokeToken }); - await clearCartDataFromCache(apolloClient); - await clearCustomerDataFromCache(apolloClient); // Refresh the page as a way to say "re-initialize". An alternative // would be to call apolloClient.resetStore() but that would require // a large refactor. history.go(0); - }, [apolloClient, history, revokeToken, setAccountMenuIsOpen, signOut]); + }, [history, revokeToken, setAccountMenuIsOpen, signOut]); const handleForgotPassword = useCallback(() => { setView('FORGOT_PASSWORD'); From 423011e46a33ce81017e7814c819ab519be4087e Mon Sep 17 00:00:00 2001 From: Jimmy Sanford Date: Thu, 7 Jan 2021 15:11:14 -0700 Subject: [PATCH 6/7] Revert cart change --- packages/peregrine/lib/store/actions/cart/asyncActions.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/peregrine/lib/store/actions/cart/asyncActions.js b/packages/peregrine/lib/store/actions/cart/asyncActions.js index 554bc28fc7..9be129b64e 100644 --- a/packages/peregrine/lib/store/actions/cart/asyncActions.js +++ b/packages/peregrine/lib/store/actions/cart/asyncActions.js @@ -1,3 +1,4 @@ +import { clearCartDataFromCache } from '../../../Apollo/clearCartDataFromCache'; import BrowserPersistence from '../../../util/simplePersistence'; import { signOut } from '../user'; import actions from './actions'; @@ -318,7 +319,7 @@ export const removeItemFromCart = payload => { export const getCartDetails = payload => { const { fetchCartId, fetchCartDetails } = payload; - return async function thunk(dispatch, getState) { + return async function thunk(dispatch, getState, { apolloClient }) { const { cart, user } = getState(); const { cartId } = cart; const { isSignedIn } = user; @@ -366,6 +367,9 @@ export const getCartDetails = payload => { await dispatch(removeCart()); } + // Clear cart data from Apollo cache + await clearCartDataFromCache(apolloClient); + // Create a new cart try { await dispatch( From ddd26b9fab449b5f0362910505db4cc0c2e8f772 Mon Sep 17 00:00:00 2001 From: Jimmy Sanford Date: Fri, 15 Jan 2021 16:08:51 -0700 Subject: [PATCH 7/7] Add interval for mobile --- .../peregrine/lib/store/middleware/auth.js | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/packages/peregrine/lib/store/middleware/auth.js b/packages/peregrine/lib/store/middleware/auth.js index aaef0027e0..97e7818ff1 100644 --- a/packages/peregrine/lib/store/middleware/auth.js +++ b/packages/peregrine/lib/store/middleware/auth.js @@ -1,7 +1,8 @@ import BrowserPersistence from '../../util/simplePersistence'; import userActions, { signOut } from '../actions/user'; -const timers = new Map(); +const timeouts = new Map(); +const intervals = new Map(); const storage = new BrowserPersistence(); const SET_TOKEN = userActions.setToken.toString(); const CLEAR_TOKEN = userActions.clearToken.toString(); @@ -29,29 +30,50 @@ const scheduleSignOut = store => next => action => { const { timeStored, ttl, value } = JSON.parse(item); const parsedValue = JSON.parse(value); + const preciseTTL = ttl * 1000; const elapsed = Date.now() - timeStored; - const delay = Math.max(ttl * 1000 - elapsed, 0); - - // only set one timer per token - if (!timers.has(parsedValue)) { - const timeoutId = setTimeout(() => { - timers.delete(parsedValue); - // clear token and customer state - dispatch(signOut()).then(() => { - // refresh the page, important for checkout - history.go(0); - }); - }, delay); - - timers.set(parsedValue, timeoutId); + const expiry = Math.max(preciseTTL - elapsed, 0); + + // establish a sign-out routine + const callback = () => { + dispatch(signOut()).then(() => { + timeouts.delete(parsedValue); + intervals.delete(parsedValue); + + // refresh the page, important for checkout + history.go(0); + }); + }; + + // set a timeout that runs once when the token expires + if (!timeouts.has(parsedValue)) { + const timeoutId = setTimeout(callback, expiry); + + timeouts.set(parsedValue, timeoutId); + } + + // then set an interval that runs once per second + // on mobile, the timeout won't fire if the tab is inactive + if (!intervals.has(parsedValue)) { + const intervalId = setInterval(() => { + const hasExpired = Date.now() - timeStored > preciseTTL; + + if (hasExpired) callback(); + }, 1000); + + intervals.set(parsedValue, intervalId); } } else if (isSigningOut(action.type)) { - // clear any lingering timers when a user signs out - for (const timeoutId of timers) { + for (const timeoutId of timeouts) { clearTimeout(timeoutId); } - timers.clear(); + for (const intervalId of intervals) { + clearInterval(intervalId); + } + + timeouts.clear(); + intervals.clear(); } return next(action);