-
Notifications
You must be signed in to change notification settings - Fork 683
Add middleware to schedule sign-out #2904
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
Changes from all commits
42058c5
33f0b90
c7e62f8
7aa638a
be96477
c1bda3d
423011e
84a6a8a
ddd26b9
a31a59f
2ab4ad0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { extraArgument } from '../store/middleware/thunk'; | ||
|
||
const attachClientToStore = apolloClient => { | ||
Object.assign(extraArgument, { apolloClient }); | ||
}; | ||
|
||
export default attachClientToStore; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 }) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that our thunk middleware includes an |
||
const { cart, user } = getState(); | ||
const { cartId } = cart; | ||
const { isSignedIn } = user; | ||
|
@@ -367,13 +367,10 @@ 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did we remove this? |
||
} | ||
// Clear cart data from Apollo cache | ||
await clearCartDataFromCache(apolloClient); | ||
|
||
// Create a new one | ||
// Create a new cart | ||
try { | ||
await dispatch( | ||
createCart({ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, we clear the rest of the cache data. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like maybe we have to add it to the |
||
|
||
// 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import BrowserPersistence from '../../util/simplePersistence'; | ||
import userActions, { signOut } from '../actions/user'; | ||
|
||
const timeouts = new Map(); | ||
const intervals = new Map(); | ||
const storage = new 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a user starts up the application, if they have a valid token in storage, we sign them in. Since they already have a token, a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it's really "is signing in or is getting user details". And in both cases we set a timer based on the time remaining on the TTL. Got it. |
||
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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So when we sign in, we create a timer to remove the token based on the TTL of the token just set? So this is similar to what getItem does but is also able to dispatch the The current iteration of the auth link also uses the Both seem to handle similar situations related to client-side invalidation of the token, but I still don't know if we gracefully handle a user token that invalidates server-side before the client-side expiry. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct, This PR doesn't handle a server-side invalidation, but I believe those are going to be much less frequent. Scheduled expiration should be the 99% case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right, Tommy's solution gets us there, assuming the error messages are correct. That said, I don't know enough about auth invalidation so I'll trust that you when you say that a timeout expiry is the most common scenario. |
||
// `BrowserPersistence.getItem()` only returns the value | ||
// but we need the full item with timestamp and ttl | ||
const item = storage.getRawItem('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 preciseTTL = ttl * 1000; | ||
const elapsed = Date.now() - timeStored; | ||
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)) { | ||
for (const timeoutId of timeouts) { | ||
clearTimeout(timeoutId); | ||
} | ||
|
||
for (const intervalId of intervals) { | ||
clearInterval(intervalId); | ||
} | ||
|
||
timeouts.clear(); | ||
intervals.clear(); | ||
} | ||
|
||
return next(action); | ||
}; | ||
|
||
export default scheduleSignOut; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import thunk from 'redux-thunk'; | ||
|
||
export const extraArgument = {}; | ||
export default thunk.withExtraArgument(extraArgument); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fortunately for us, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing things that can't be serialized, such as functions or objects containing functions, in action payloads is an anti-pattern in Redux. I understand the thought behind putting
apolloClient
in the payload, but we shouldn't have been doing this.