From 2dad05ce7219c61b067e139160faec0d2114bf03 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:22:12 -0800 Subject: [PATCH 1/9] improved logging --- src/API.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/API.js b/src/API.js index b1fa2eb6c..e8e8afe08 100644 --- a/src/API.js +++ b/src/API.js @@ -124,7 +124,6 @@ Client.prototype.createRequest = function(method, url, data, options = {}) { } const authorized = !(!this.credentials.token || options.anonymous) - console.log(`${method} ${url}${authorized ? '' : ' (anon.)'}`) if (authorized && this.credentials.token) { headers.Authorization = `Bearer ${this.credentials.token}` @@ -137,6 +136,8 @@ Client.prototype.createRequest = function(method, url, data, options = {}) { } } + console.log(`REQUEST: ${method} ${url}${headers.Authorization ? '' : ' (anon.)'}`) + let req = { method, url, @@ -159,9 +160,24 @@ Client.prototype.createRequest = function(method, url, data, options = {}) { return req } -Client.prototype.request = function(method, uri, data, options = {}) { +Client.prototype.request = function (method, uri, data, options = {}) { const req = this.createRequest(method, uri, data, options) - return this.axios.request(req); + return this.axios + .request(req) + .then(response => { + console.log(`RESPONSE: ${method} ${uri}: ${response.status}`) + + return response + }) + .catch(error => { + if (error.response) { + console.warn(`RESPONSE: ${method} ${uri}: ${error.response.status}`) + } else { + console.warn(`RESPONSE: ${method} ${uri}: ${error.message}`) + } + + return Promise.reject(error) + }) } Client.prototype.get = function(uri, data, options = {}) { From 8ba8228094034b2f5c92c3be7f935efa2b702ffe Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:23:13 -0800 Subject: [PATCH 2/9] persist 'resumeCheckoutAfterActivation' field --- src/redux/reducers.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/redux/reducers.js b/src/redux/reducers.js index bea55a5c5..43df500a7 100644 --- a/src/redux/reducers.js +++ b/src/redux/reducers.js @@ -107,7 +107,14 @@ const appPersistConfig = { key: 'app', version: 0, storage: AsyncStorage, - whitelist: [ 'baseURL', 'settings', 'pushNotificationToken', 'hasDisclosedBackgroundPermission', 'firstRun' ], + whitelist: [ + 'baseURL', + 'settings', + 'pushNotificationToken', + 'hasDisclosedBackgroundPermission', + 'firstRun', + 'resumeCheckoutAfterActivation' + ], migrate: (state) => { if (!state) { From 0f39aaf035ae14a3a4c596f63390c2292f365530 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:31:32 -0800 Subject: [PATCH 3/9] set session authorization token explicitly per request --- src/API.js | 12 +- src/navigation/checkout/Restaurant.js | 2 +- src/redux/Account/actions.js | 18 ++- src/redux/App/selectors.js | 21 +-- src/redux/Checkout/actions.js | 208 +++++++++++++++----------- src/redux/Checkout/selectors.js | 17 +++ 6 files changed, 156 insertions(+), 122 deletions(-) diff --git a/src/API.js b/src/API.js index e8e8afe08..91437aa82 100644 --- a/src/API.js +++ b/src/API.js @@ -180,14 +180,14 @@ Client.prototype.request = function (method, uri, data, options = {}) { }) } -Client.prototype.get = function(uri, data, options = {}) { +Client.prototype.get = function(uri, options = {}) { - return enhanceRequest(this, 'GET', uri, data, options); + return enhanceRequest(this, 'GET', uri, {}, options) } -Client.prototype.post = function(uri, data) { +Client.prototype.post = function(uri, data, options = {}) { - return enhanceRequest(this, 'POST', uri, data); + return enhanceRequest(this, 'POST', uri, data, options); } Client.prototype.put = function(uri, data, options = {}) { @@ -195,9 +195,9 @@ Client.prototype.put = function(uri, data, options = {}) { return enhanceRequest(this, 'PUT', uri, data, options); } -Client.prototype.delete = function(uri) { +Client.prototype.delete = function(uri, options = {}) { - return enhanceRequest(this, 'DELETE', uri); + return enhanceRequest(this, 'DELETE', uri, {}, options); } function enhanceRequest(client, method, uri, data, options = {}) { diff --git a/src/navigation/checkout/Restaurant.js b/src/navigation/checkout/Restaurant.js index 4c29620a4..64030a92d 100644 --- a/src/navigation/checkout/Restaurant.js +++ b/src/navigation/checkout/Restaurant.js @@ -54,7 +54,7 @@ function Restaurant(props) { const { showFooter, httpClient, restaurant, openingHoursSpecification } = props const { isLoading, isError, data } = useQuery([ 'menus', restaurant.hasMenu ], async () => { - return await httpClient.get(restaurant.hasMenu, {}, { anonymous: true }) + return await httpClient.get(restaurant.hasMenu, { anonymous: true }) }) const currentTimeSlot = useMemo( diff --git a/src/redux/Account/actions.js b/src/redux/Account/actions.js index 7e99c932a..193a52845 100644 --- a/src/redux/Account/actions.js +++ b/src/redux/Account/actions.js @@ -36,6 +36,16 @@ const disconnected = createAction(DISCONNECTED) const setOrderAccessToken = createAction(SET_ORDER_ACCESS_TOKEN) +function orderRequestAuthorizationHeaders(order, sessionToken) { + if (order.customer) { + return {} // use the user's token from the httpClient + } else { + return { + 'Authorization': `Bearer ${sessionToken}`, + } + } +} + export function loadOrders(cb) { return function (dispatch, getState) { @@ -56,9 +66,9 @@ export function loadOrders(cb) { export function loadOrder(order, cb) { return (dispatch, getState) => { const orderAccessToken = selectOrderAccessTokensById(getState(), order['@id']) - const httpClient = selectHttpClient(getState(), orderAccessToken) + const httpClient = selectHttpClient(getState()) - httpClient.get(order['@id']) + httpClient.get(order['@id'], { headers: orderRequestAuthorizationHeaders(order, orderAccessToken) }) .then(res => { dispatch(updateOrderSuccess(res)) if (cb) { cb() } @@ -171,9 +181,9 @@ export function subscribe(order, onMessage) { const baseURL = getState().app.baseURL const orderAccessToken = selectOrderAccessTokensById(getState(), order['@id']) - const httpClient = selectHttpClient(getState(), orderAccessToken) + const httpClient = selectHttpClient(getState()) - httpClient.get(`${order['@id']}/centrifugo`) + httpClient.get(`${order['@id']}/centrifugo`, { headers: orderRequestAuthorizationHeaders(order, orderAccessToken) }) .then(res => { const url = parseUrl(baseURL) diff --git a/src/redux/App/selectors.js b/src/redux/App/selectors.js index 49d7a031e..800a2e066 100644 --- a/src/redux/App/selectors.js +++ b/src/redux/App/selectors.js @@ -5,7 +5,7 @@ import { selectIsTasksLoading } from '../Courier/taskSelectors' import { selectIsDispatchFetching } from '../Dispatch/selectors' export const selectUser = state => state.app.user -const selectDefaultHttpClient = state => state.app.httpClient +export const selectHttpClient = state => state.app.httpClient export const selectIsAuthenticated = createSelector( selectUser, @@ -17,25 +17,6 @@ export const selectIsGuest = createSelector( (user) => !!(user && user.isGuest()) ) -const selectFallbackToken = (state, fallbackToken) => fallbackToken - -export const selectHttpClient = createSelector( - selectDefaultHttpClient, - selectIsAuthenticated, - selectFallbackToken, - (defaultHttpClient, isAuthenticated, fallbackToken) => { - if (isAuthenticated) { - return defaultHttpClient - } else { - if (fallbackToken) { - return defaultHttpClient.cloneWithToken(fallbackToken) - } else { - return defaultHttpClient - } - } - }, -) - export const selectHttpClientHasCredentials = createSelector( selectHttpClient, (httpClient) => !!(httpClient && !!httpClient.getToken()) diff --git a/src/redux/Checkout/actions.js b/src/redux/Checkout/actions.js index a8bdeb20e..ac775b28f 100644 --- a/src/redux/Checkout/actions.js +++ b/src/redux/Checkout/actions.js @@ -5,7 +5,13 @@ import { createPaymentMethod, handleNextAction, initStripe } from '@stripe/strip import NavigationHolder from '../../NavigationHolder' import i18n from '../../i18n' -import { selectBillingEmail, selectCart, selectCartFulfillmentMethod, selectCartWithHours } from './selectors' +import { + selectBillingEmail, + selectCart, + selectCartByVendor, + selectCartFulfillmentMethod, + selectCartWithHours, +} from './selectors' import { selectHttpClient, selectIsAuthenticated, @@ -186,17 +192,12 @@ function validateAddress(httpClient, cart, address) { return new Promise((resolve, reject) => { httpClient - .get(`${cart.restaurant}/can-deliver/${latitude},${longitude}`, {}, { anonymous: true }) + .get(`${cart.restaurant}/can-deliver/${latitude},${longitude}`, { anonymous: true }) .then(resolve) .catch(() => reject(i18n.t('CHECKOUT_ADDRESS_NOT_VALID'))) }) } -function createHttpClientWithSessionToken(state) { - const { token } = state.checkout - return selectHttpClient(state, token) -} - let listeners = [] function replaceListeners(cb) { @@ -216,12 +217,22 @@ function notifyListeners(address) { listeners = [] } +function cartRequestAuthorizationHeaders(cart, sessionToken) { + if (cart.customer) { + return {} // use the user's token from the httpClient + } else { + return { + 'Authorization': `Bearer ${sessionToken}`, + } + } +} + export function addItemV2(item, quantity = 1, restaurant, options) { return async (dispatch, getState) => { const { carts, address } = getState().checkout - let httpClient = createHttpClientWithSessionToken(getState()) + const httpClient = selectHttpClient(getState()) const requestAddress = (closureAddress) => { if (_.has(closureAddress, '@id')) { @@ -234,7 +245,6 @@ export function addItemV2(item, quantity = 1, restaurant, options) { if (!_.has(carts, restaurant['@id'])) { dispatch(initCartRequest(restaurant['@id'])) dispatch(setToken(null)) - httpClient = createHttpClientWithSessionToken(getState()) try { const response = await httpClient.post('/api/carts/session', { restaurant: restaurant['@id'], @@ -252,16 +262,14 @@ export function addItemV2(item, quantity = 1, restaurant, options) { return } } - const { cart, token } = getState().checkout.carts[restaurant['@id']] + const { cart, token } = selectCartByVendor(getState(), restaurant['@id']) dispatch(setToken(token)) - // Reload httpclient with new token - httpClient = createHttpClientWithSessionToken(getState()) const response = await httpClient .post(`${cart['@id']}/items`, { product: item.identifier, quantity, options, - }) + }, { headers: cartRequestAuthorizationHeaders(cart, token) }) dispatch(addItemRequestFinished(item)) dispatch(updateCartSuccess(response)) } @@ -352,8 +360,8 @@ function queueAddItem(item, quantity = 1, options = []) { queue: 'ADD_ITEM', callback: (next, dispatch, getState) => { - const { cart } = getState().checkout - const httpClient = createHttpClientWithSessionToken(getState()) + const { cart, token } = getState().checkout + const httpClient = selectHttpClient(getState()) dispatch(setCheckoutLoading(true)) @@ -362,7 +370,7 @@ function queueAddItem(item, quantity = 1, options = []) { product: item.identifier, quantity, options, - }) + }, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => { dispatch(setCheckoutLoading(false)) dispatch(addItemRequestFinished(item)) @@ -380,7 +388,8 @@ function queueAddItem(item, quantity = 1, options = []) { const fetchValidation = _.throttle((dispatch, getState, cart) => { - const httpClient = createHttpClientWithSessionToken(getState()) + const { token } = selectCartByVendor(getState(), cart.restaurant) + const httpClient = selectHttpClient(getState()) // No need to validate when cart is empty if (cart.items.length === 0) { @@ -389,7 +398,7 @@ const fetchValidation = _.throttle((dispatch, getState, cart) => { const doTiming = () => new Promise((resolve) => { httpClient - .get(`${cart['@id']}/timing`) + .get(`${cart['@id']}/timing`, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(timing => dispatch(setTiming(timing))) // .catch(error => dispatch(setCartValidation(false, error.violations))) .finally(resolve) @@ -397,7 +406,7 @@ const fetchValidation = _.throttle((dispatch, getState, cart) => { const doValidate = () => new Promise((resolve) => { httpClient - .get(`${cart['@id']}/validate`) + .get(`${cart['@id']}/validate`, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(() => dispatch(setCartValidation(true))) .catch(error => { if (error.response && error.response.status === 400) { @@ -423,9 +432,9 @@ function syncItem(item) { queue: 'UPDATE_CART', callback: (next, dispatch, getState) => { - const { cart, token } = getState().checkout.carts[item.vendor['@id']] + const { cart, token } = selectCartByVendor(getState(), item.vendor['@id']) dispatch(setToken(token)) - const httpClient = createHttpClientWithSessionToken(getState()) + const httpClient = selectHttpClient(getState()) // We make sure to get item from state, // because it may have been updated @@ -441,7 +450,7 @@ function syncItem(item) { // FIXME We should have the "@id" property .put(`${cart['@id']}/items/${item.id}`, { quantity: itemFromState.quantity, - }) + }, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(setCheckoutLoading(false)) @@ -487,15 +496,15 @@ function queueRemoveItem(item) { queue: 'UPDATE_CART', callback: (next, dispatch, getState) => { - const { cart, token } = getState().checkout.carts[item.vendor['@id']] + const { cart, token } = selectCartByVendor(getState(), item.vendor['@id']) dispatch(setToken(token)) - const httpClient = createHttpClientWithSessionToken(getState()) + const httpClient = selectHttpClient(getState()) dispatch(setCheckoutLoading(true)) httpClient // FIXME We should have the "@id" property - .delete(`${cart['@id']}/items/${item.id}`) + .delete(`${cart['@id']}/items/${item.id}`, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(setCheckoutLoading(false)) @@ -531,7 +540,8 @@ export function removeItem(item) { // Dispatch an action to "virtually" remove the item, // so that the user has a UI feedback dispatch(removeItemRequest(item)) - const { items } = getState().checkout.carts[item.vendor['@id']].cart + const { cart } = selectCartByVendor(getState(), item.vendor['@id']) + const { items } = cart if (items.length === 0) { dispatch(deleteCartRequest(item.vendor['@id'])) NavigationHolder.goBack() @@ -543,10 +553,11 @@ export function removeItem(item) { export function setTip(order, tipAmount) { return (dispatch, getState) => { - const httpClient = createHttpClientWithSessionToken(getState()) + const { token } = selectCartByVendor(getState(), order.restaurant) + const httpClient = selectHttpClient(getState()) dispatch(checkoutRequest()) - httpClient.put(`${order['@id']}/tip`, { tipAmount }) + httpClient.put(`${order['@id']}/tip`, { tipAmount }, { headers: cartRequestAuthorizationHeaders(order, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(checkoutSuccess()) @@ -581,11 +592,12 @@ function syncAddress(cart, address) { queue: 'UPDATE_CART', callback: (next, dispatch, getState) => { - const { carts } = getState().checkout - dispatch(setToken(carts[cart.restaurant].token)) - const httpClient = createHttpClientWithSessionToken(getState()) + const { token } = selectCartByVendor(getState(), cart.restaurant) + dispatch(setToken(token)) + + const httpClient = selectHttpClient(getState()) - httpClient.put(carts[cart.restaurant].cart['@id'], { shippingAddress: address }) + httpClient.put(cart['@id'], { shippingAddress: address }, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(setCheckoutLoading(false)) @@ -642,7 +654,7 @@ function wrapRestaurantsWithTiming(restaurants) { const { httpClient } = getState().app const promises = restaurants.map(restaurant => new Promise((resolve) => { - httpClient.get(restaurant['@id'] + '/timing', {}, { anonymous: true }) + httpClient.get(restaurant['@id'] + '/timing', { anonymous: true }) .then(res => resolve(res)) .catch(e => resolve({ delivery: null, collection: null })) })) @@ -683,7 +695,7 @@ export function searchRestaurantsForAddress(address, options = {}) { const uri = options && options.baseURL ? `${options.baseURL}/api/restaurants` : '/api/restaurants' - httpClient.get(uri + (queryString ? `?${queryString}` : ''), {}, { anonymous: true }) + httpClient.get(uri + (queryString ? `?${queryString}` : ''), { anonymous: true }) .then(res => { dispatch(_setAddress(address)) dispatch(wrapRestaurantsWithTiming(res['hydra:member'])) @@ -706,7 +718,7 @@ export function searchRestaurants(options = {}) { const uri = options && options.baseURL ? `${options.baseURL}/api/restaurants` : '/api/restaurants' const reqs = [ - httpClient.get(uri, {}, { anonymous: true }), + httpClient.get(uri, { anonymous: true }), ] if (selectIsAuthenticated(getState())) { @@ -742,7 +754,7 @@ export function mercadopagoCheckout(payment) { } return (dispatch, getState) => { - const { cart } = getState().checkout; + const { cart, token } = getState().checkout; const { id, status, statusDetail } = payment; @@ -756,10 +768,10 @@ export function mercadopagoCheckout(payment) { paymentMethodId: 'CARD', } - const httpClient = createHttpClientWithSessionToken(getState()) + const httpClient = selectHttpClient(getState()) httpClient - .put(cart['@id'] + '/pay', params) + .put(cart['@id'] + '/pay', params, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(order => { dispatch(handleSuccessNav(order)); }) @@ -771,7 +783,7 @@ export function mercadopagoCheckout(payment) { function handleSuccessNav(order) { return (dispatch, getState) => { - const { token } = getState().checkout + const { token } = selectCartByVendor(getState(), order.restaurant) dispatch(setNewOrder(order, token)) @@ -817,12 +829,12 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = dispatch(checkoutRequest()); const { restaurant, paymentDetails } = getState().checkout; - const { cart } = getState().checkout.carts[restaurant]; + const { cart, token } = selectCartByVendor(getState(), restaurant); const billingEmail = selectBillingEmail(getState()); const loggedOrderId = cart['@id']; - const httpClient = createHttpClientWithSessionToken(getState()); + const httpClient = selectHttpClient(getState()) if (!validateCart(cart)) { NavigationHolder.dispatch( @@ -843,7 +855,7 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = if (isFree(cart)) { httpClient - .put(cart['@id'] + '/pay', {}) + .put(cart['@id'] + '/pay', {}, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(o => dispatch(handleSuccessNav(o))) .catch(e => dispatch(checkoutFailure(e))); @@ -855,7 +867,7 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = if (paymentDetails.stripeAccount) { // for connected account we have to clone the platform payment method return httpClient - .get(`${cart['@id']}/stripe/clone-payment-method/${paymentMethodId}`) + .get(`${cart['@id']}/stripe/clone-payment-method/${paymentMethodId}`, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(clonnedPaymentMethod => { return [paymentMethodId, clonnedPaymentMethod.id]; }); @@ -868,7 +880,7 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = httpClient.put(cart['@id'] + '/pay', { paymentMethodId: clonnedPaymentMethodId || platformAccountPaymentMethodId, saveCard, - }), + }, { headers: cartRequestAuthorizationHeaders(cart, token) }), ) .then(stripeResponse => { if (stripeResponse.requiresAction) { @@ -879,8 +891,8 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = throw new Error('handleNextAction error:', { cause: error }); } else { dispatch(handleSuccess( - httpClient, cart, + token, paymentIntent.id, saveCard, cardholderName, @@ -899,8 +911,8 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = }); } else { dispatch(handleSuccess( - httpClient, cart, + token, stripeResponse.paymentIntentId, saveCard, cardholderName, @@ -952,16 +964,17 @@ function configureStripe(state, paymentDetails = null) { } function handleSuccess( - httpClient, cart, + token, paymentIntentId, saveCard = false, cardholderName, ) { return (dispatch, getState) => { const loggedOrderId = cart['@id']; + const httpClient = selectHttpClient(getState()) - handleSaveOfPaymentMethod(saveCard, cardholderName, httpClient, getState()) + handleSaveOfPaymentMethod(saveCard, cardholderName, getState) .catch(e => { // do not interrupt flow if there is an error with this const err = new Error(`order: ${loggedOrderId}; failed to save a payment method`, { @@ -972,7 +985,7 @@ function handleSuccess( }) .then(() => { httpClient - .put(cart['@id'] + '/pay', { paymentIntentId }) + .put(cart['@id'] + '/pay', { paymentIntentId }, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(order => { dispatch(handleSuccessNav(order)); }) @@ -989,15 +1002,16 @@ function handleSuccess( } } -function handleSaveOfPaymentMethod(saveCard, cardholderName, httpClient, state) { +function handleSaveOfPaymentMethod(saveCard, cardholderName, getState) { return new Promise(resolve => { - const { restaurant, paymentDetails } = state.checkout; + const httpClient = selectHttpClient(getState()) + const { restaurant, paymentDetails } = getState().checkout; if (saveCard && paymentDetails.stripeAccount) { - const billingEmail = selectBillingEmail(state); - const { cart } = state.checkout.carts[restaurant]; + const billingEmail = selectBillingEmail(getState()); + const { cart, token } = selectCartByVendor(getState(), restaurant); - return configureStripe(state) + return configureStripe(getState()) .then(() => createPaymentMethod({ paymentMethodType: 'Card', @@ -1017,7 +1031,7 @@ function handleSaveOfPaymentMethod(saveCard, cardholderName, httpClient, state) httpClient .post(cart['@id'] + '/stripe/create-setup-intent-or-attach-pm', { payment_method_to_save: paymentMethod.id, - }) + }, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(() => resolve()); } }); @@ -1032,15 +1046,24 @@ export function assignAllCarts() { const { carts } = getState().checkout - _.forEach(carts, cartContainer => dispatch(assignCustomer({}, cartContainer))) + _.forEach(carts, cartContainer => { + const { cart, token } = cartContainer + return dispatch(_assignCustomer(cart, token, {})) + }) + } +} + +export function assignCustomer({ email, telephone }) { + return async (dispatch, getState) => { + const { cart, token } = selectCart(getState()) + return dispatch(_assignCustomer(cart, token, { email, telephone })) } } -export function assignCustomer({ email, telephone }, cartContainer = null) { +function _assignCustomer(cart, token, { email, telephone }) { return async (dispatch, getState) => { - const { cart, token } = selectCart(getState()) const user = selectUser(getState()) if (!user.isGuest() && cart.customer) { @@ -1049,24 +1072,28 @@ export function assignCustomer({ email, telephone }, cartContainer = null) { dispatch(checkoutRequest()) - let httpClient + const httpClient = selectHttpClient(getState()) let body = {} + let authorizationHeaders = {} if (user.isGuest()) { - httpClient = createHttpClientWithSessionToken(getState()) body = { guest: true, email, telephone, }; + authorizationHeaders = { + 'Authorization': `Bearer ${token}`, + } } else { - httpClient = selectHttpClient(getState()) + authorizationHeaders = {} // use the user's token from the httpClient } return httpClient .put(cart['@id'] + '/assign', body, { headers: { 'X-CoopCycle-Session': `Bearer ${token}`, + ...authorizationHeaders, }, }) .then(res => { @@ -1106,9 +1133,9 @@ export function resetSearch(options = {}) { } } -const doUpdateCart = (dispatch, httpClient, cart, payload, cb) => { +const doUpdateCart = (dispatch, httpClient, cart, token, payload, cb) => { httpClient - .put(cart['@id'], payload) + .put(cart['@id'], payload, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(checkoutSuccess()) @@ -1123,9 +1150,9 @@ export function updateCart(payload, cb) { const { restaurant } = getState().checkout - const { cart, token } = getState().checkout.carts[restaurant] + const { cart, token } = selectCartByVendor(getState(), restaurant) - const httpClient = createHttpClientWithSessionToken(getState()) + const httpClient = selectHttpClient(getState()) if (payload.shippingAddress) { const shippingAddress = { @@ -1147,12 +1174,12 @@ export function updateCart(payload, cb) { const { telephone, ...payloadWithoutTelephone } = payload httpClient - .put(cart.customer, { telephone }) - .then(res => doUpdateCart(dispatch, httpClient, cart, payloadWithoutTelephone, cb)) + .put(cart.customer, { telephone }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .then(res => doUpdateCart(dispatch, httpClient, cart, token, payloadWithoutTelephone, cb)) .catch(e => dispatch(checkoutFailure(e))) } else { - doUpdateCart(dispatch, httpClient, cart, payload, cb) + doUpdateCart(dispatch, httpClient, cart, token, payload, cb) } } } @@ -1162,15 +1189,15 @@ export function setDate(shippingTimeRange, cb) { return (dispatch, getState) => { - const { cart } = selectCartWithHours(getState()) - const httpClient = createHttpClientWithSessionToken(getState()) + const { cart, token } = selectCartWithHours(getState()) + const httpClient = selectHttpClient(getState()) dispatch(checkoutRequest()) httpClient .put(cart['@id'], { shippingTimeRange, - }) + }, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) setTimeout(() => { @@ -1186,16 +1213,15 @@ export function setDateAsap(cart, cb) { return (dispatch, getState) => { - const httpClient = createHttpClientWithSessionToken(getState()) - - //const { cart } = getState().checkout + const { token } = selectCartByVendor(getState(), cart.restaurant) + const httpClient = selectHttpClient(getState()) dispatch(checkoutRequest()) httpClient .put(cart['@id'], { shippingTimeRange: null, - }) + }, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) setTimeout(() => { @@ -1217,15 +1243,15 @@ export function setFulfillmentMethod(method) { //dispatch(checkoutRequest()) dispatch(setToken(token)) - const httpClient = createHttpClientWithSessionToken(getState()) + const httpClient = selectHttpClient(getState()) httpClient .put(cart['@id'], { fulfillmentMethod: method, - }) + }, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => { httpClient - .get(`${cart['@id']}/timing`) + .get(`${cart['@id']}/timing`, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(timing => { dispatch(setCheckoutLoading(false)) dispatch(setTiming(timing)) @@ -1260,14 +1286,14 @@ export function loadPaymentMethods(method) { return (dispatch, getState) => { - const { cart } = selectCartWithHours(getState()) + const { cart, token } = selectCartWithHours(getState()) - const httpClient = createHttpClientWithSessionToken(getState()) + const httpClient = selectHttpClient(getState()) dispatch(loadPaymentMethodsRequest()) httpClient - .get(`${cart['@id']}/payment_methods`) + .get(`${cart['@id']}/payment_methods`, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => dispatch(loadPaymentMethodsSuccess(res))) .catch(e => dispatch(loadPaymentMethodsFailure(e))) } @@ -1277,13 +1303,13 @@ export function checkoutWithCash() { return (dispatch, getState) => { - const { cart } = selectCartWithHours(getState()) - const httpClient = createHttpClientWithSessionToken(getState()) + const { cart, token } = selectCartWithHours(getState()) + const httpClient = selectHttpClient(getState()) dispatch(checkoutRequest()) httpClient - .put(cart['@id'] + '/pay', { cashOnDelivery: true }) + .put(cart['@id'] + '/pay', { cashOnDelivery: true }, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(order => dispatch(handleSuccessNav(order))) .catch(e => dispatch(checkoutFailure(e))) } @@ -1293,13 +1319,13 @@ export function loadPaymentDetails() { return (dispatch, getState) => { - const { cart } = selectCartWithHours(getState()) - const httpClient = createHttpClientWithSessionToken(getState()) + const { cart, token } = selectCartWithHours(getState()) + const httpClient = selectHttpClient(getState()) dispatch(loadPaymentDetailsRequest()) httpClient - .get(`${cart['@id']}/payment`) + .get(`${cart['@id']}/payment`, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => dispatch(loadPaymentDetailsSuccess(res))) .catch(e => dispatch(loadPaymentDetailsFailure(e))) } @@ -1388,7 +1414,7 @@ export function loadAndNavigateToRestaurante(id) { return httpClient .get(`/api/restaurants/${id}`) .then(restaurant => { - return httpClient.get(restaurant['@id'] + '/timing', {}, { anonymous: true }) + return httpClient.get(restaurant['@id'] + '/timing', { anonymous: true }) .then(timing => { restaurant.timing = timing return restaurant @@ -1408,15 +1434,15 @@ export function updateLoopeatReturns(returns) { return (dispatch, getState) => { - const httpClient = createHttpClientWithSessionToken(getState()) - const { cart } = selectCart(getState()) + const { cart, token } = selectCart(getState()) + const httpClient = selectHttpClient(getState()) dispatch(checkoutRequest()) httpClient .post(cart['@id'] + '/loopeat_returns', { returns, - }) + }, { headers: cartRequestAuthorizationHeaders(cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) }) diff --git a/src/redux/Checkout/selectors.js b/src/redux/Checkout/selectors.js index 3d1be3771..03c866780 100644 --- a/src/redux/Checkout/selectors.js +++ b/src/redux/Checkout/selectors.js @@ -39,6 +39,23 @@ export const selectCartWithHours = createSelector( } ) +const selectVendorId = (state, vendorId) => vendorId + +export const selectCartByVendor = createSelector( + state => state.checkout.carts, + selectVendorId, + (carts, vendor) => { + if (carts.hasOwnProperty(vendor)) { + return carts[vendor] + } + return { + cart: null, + restaurant: null, + token: null, + } + } +) + export const selectRestaurant = createSelector( state => state.checkout.restaurants, state => state.checkout.restaurant, From c532bf6501416d3dd5efc6e0f51b47709a0ee2c9 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:54:47 -0800 Subject: [PATCH 4/9] added: deprecation comments --- src/redux/Checkout/reducers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/redux/Checkout/reducers.js b/src/redux/Checkout/reducers.js index 33f391116..dfbf52283 100644 --- a/src/redux/Checkout/reducers.js +++ b/src/redux/Checkout/reducers.js @@ -82,7 +82,8 @@ const initialState = { violations: [], isLoading: false, itemRequestStack: [], - token: null, + cart: null, // deprecated, use cart from carts ("cart container") instead + token: null, // deprecated, use token from carts ("cart container") instead isExpiredSessionModalVisible: false, isSessionExpired: false, paymentMethods: [], From 64c7e84ba026390928b2fb8a64d9f0cdb8cead4f Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 9 Jan 2024 10:50:05 -0800 Subject: [PATCH 5/9] fixed: resume checkout after a cold boot --- src/redux/App/actions.js | 89 +++++++++++--- src/redux/App/reducers.js | 2 +- src/redux/App/selectors.js | 2 + .../Checkout/__tests__/selectors.test.js | 111 ++++++++++++++++++ src/redux/Checkout/selectors.js | 7 +- 5 files changed, 190 insertions(+), 21 deletions(-) diff --git a/src/redux/App/actions.js b/src/redux/App/actions.js index 77f60f373..33574d090 100644 --- a/src/redux/App/actions.js +++ b/src/redux/App/actions.js @@ -1,5 +1,5 @@ import { createAction } from 'redux-actions' -import { CommonActions, StackActions } from '@react-navigation/native' +import { CommonActions } from '@react-navigation/native' import tracker from '../../analytics/Tracker' import analyticsEvent from '../../analytics/Event' import userProperty from '../../analytics/UserProperty' @@ -10,9 +10,19 @@ import Settings from '../../Settings' import NavigationHolder from '../../NavigationHolder' import i18n from '../../i18n' import { setCurrencyCode } from '../../utils/formatting' -import { selectInitialRouteName, selectIsAuthenticated } from './selectors' -import { assignAllCarts, updateCarts, clearAddress } from '../Checkout/actions'; +import { + selectHttpClient, + selectInitialRouteName, + selectIsAuthenticated, selectResumeCheckoutAfterActivation, +} from './selectors' +import { + assignAllCarts, + updateCarts, + clearAddress, + setRestaurant, +} from '../Checkout/actions' import { loadAddresses, loadAddressesSuccess } from '../Account/actions'; +import { selectRestaurant } from '../Checkout/selectors' /* * Action Types @@ -457,7 +467,11 @@ export function register(data, checkEmailRouteName, loginRouteName, resumeChecko } else { dispatch(setLoading(false)) - dispatch(_resumeCheckoutAfterActivation(resumeCheckoutAfterActivation)) + + if (resumeCheckoutAfterActivation) { + const restaurant = selectRestaurant(getState()) + dispatch(_resumeCheckoutAfterActivation(restaurant['@id'])); + } // FIXME When using navigation, we can still go back to the filled form NavigationHolder.navigate(checkEmailRouteName, { email: user.email, loginRouteName }) @@ -475,8 +489,8 @@ export function register(data, checkEmailRouteName, loginRouteName, resumeChecko export function confirmRegistration(token) { return (dispatch, getState) => { - const { app } = getState() - const { httpClient, resumeCheckoutAfterActivation } = app + const httpClient = selectHttpClient(getState()) + const checkoutToResumeAfterActivation = selectResumeCheckoutAfterActivation(getState()) dispatch(setLoading(true)) dispatch(authenticationRequest()) @@ -493,8 +507,8 @@ export function confirmRegistration(token) { ], })) - if (resumeCheckoutAfterActivation) { - dispatch(resumeCheckout()) + if (checkoutToResumeAfterActivation) { + dispatch(resumeCheckout(checkoutToResumeAfterActivation)) } else { navigateToHome(dispatch, getState) } @@ -530,12 +544,47 @@ export function guestModeOn() { } } -export function resumeCheckout() { +function resumeCheckout(vendorId) { return (dispatch, getState) => { - dispatch(_resumeCheckoutAfterActivation(false)) - NavigationHolder.dispatch(CommonActions.navigate({ - name: 'CheckoutNav', - })) + dispatch(_resumeCheckoutAfterActivation(null)) + + dispatch(setRestaurant(vendorId)) + + NavigationHolder.dispatch( + CommonActions.reset({ + routes: [ + { + name: 'CheckoutNav', + state: { + routes: [ + { + name: 'Main', + state: { + routes: [ + { name: 'CheckoutHome' }, + { + name: 'CheckoutRestaurant', + }, + { + name: 'CheckoutSummary', + }, + ], + } + }, + { + name: 'CheckoutSubmitOrder', + state: { + routes: [ + { name: 'CheckoutMoreInfos' }, + ] + } + }, + ], + } + }, + ], + }), + ) } } @@ -550,7 +599,11 @@ export function resetPassword(username, checkEmailRouteName, resumeCheckoutAfter .resetPassword(username) .then(response => { dispatch(resetPasswordRequestSuccess()); - dispatch(_resumeCheckoutAfterActivation(resumeCheckoutAfterActivation)); + + if (resumeCheckoutAfterActivation) { + const restaurant = selectRestaurant(getState()) + dispatch(_resumeCheckoutAfterActivation(restaurant['@id'])); + } NavigationHolder.navigate(checkEmailRouteName, { email: username }); }) @@ -563,8 +616,8 @@ export function resetPassword(username, checkEmailRouteName, resumeCheckoutAfter export function setNewPassword(token, password) { return (dispatch, getState) => { - const { app } = getState(); - const { httpClient, resumeCheckoutAfterActivation } = app; + const httpClient = selectHttpClient(getState()) + const checkoutToResumeAfterActivation = selectResumeCheckoutAfterActivation(getState()) dispatch(authenticationRequest()); @@ -573,8 +626,8 @@ export function setNewPassword(token, password) { .then(user => { dispatch(authenticationSuccess(user)); - if (resumeCheckoutAfterActivation) { - dispatch(resumeCheckout()); + if (checkoutToResumeAfterActivation) { + dispatch(resumeCheckout(checkoutToResumeAfterActivation)); } else { navigateToHome(dispatch, getState); } diff --git a/src/redux/App/reducers.js b/src/redux/App/reducers.js index 4325d0c97..218ddbd80 100644 --- a/src/redux/App/reducers.js +++ b/src/redux/App/reducers.js @@ -61,7 +61,7 @@ const initialState = { nonInputError: null, requested: false, }, - resumeCheckoutAfterActivation: false, + resumeCheckoutAfterActivation: null, // vendor id where checkout was initiated servers: [], selectServerError: null, settings: { diff --git a/src/redux/App/selectors.js b/src/redux/App/selectors.js index 800a2e066..db0de117d 100644 --- a/src/redux/App/selectors.js +++ b/src/redux/App/selectors.js @@ -7,6 +7,8 @@ import { selectIsDispatchFetching } from '../Dispatch/selectors' export const selectUser = state => state.app.user export const selectHttpClient = state => state.app.httpClient +export const selectResumeCheckoutAfterActivation = state => state.app.resumeCheckoutAfterActivation + export const selectIsAuthenticated = createSelector( selectUser, (user) => !!(user && user.isAuthenticated()) diff --git a/src/redux/Checkout/__tests__/selectors.test.js b/src/redux/Checkout/__tests__/selectors.test.js index 1312e612c..6467d6cb6 100644 --- a/src/redux/Checkout/__tests__/selectors.test.js +++ b/src/redux/Checkout/__tests__/selectors.test.js @@ -1,6 +1,7 @@ import moment from 'moment' import { + selectRestaurant, selectShippingTimeRangeLabel, } from '../selectors' @@ -21,12 +22,16 @@ describe('Redux | Checkout | Selectors', () => { expect(selectShippingTimeRangeLabel({ checkout: { timing: {}, + restaurants: [], cart: { shippingTimeRange: null, }, carts: { '/api/restaurants/1': { cart: { shippingTimeRange: null }, + restaurant: { + '@id': '/api/restaurants/1', + }, }, }, }, @@ -34,6 +39,7 @@ describe('Redux | Checkout | Selectors', () => { expect(selectShippingTimeRangeLabel({ checkout: { + restaurants: [], restaurant: '/api/restaurants/1', timing: { today: true, @@ -50,6 +56,9 @@ describe('Redux | Checkout | Selectors', () => { carts: { '/api/restaurants/1': { cart: { shippingTimeRange: null }, + restaurant: { + '@id': '/api/restaurants/1', + }, } }, }, @@ -57,6 +66,7 @@ describe('Redux | Checkout | Selectors', () => { expect(selectShippingTimeRangeLabel({ checkout: { + restaurants: [], restaurant: '/api/restaurants/1', timing: { today: false, @@ -72,6 +82,9 @@ describe('Redux | Checkout | Selectors', () => { carts: { '/api/restaurants/1': { cart: { shippingTimeRange: null }, + restaurant: { + '@id': '/api/restaurants/1', + }, } }, }, @@ -79,6 +92,7 @@ describe('Redux | Checkout | Selectors', () => { expect(selectShippingTimeRangeLabel({ checkout: { + restaurants: [], restaurant: '/api/restaurants/1', timing: { today: true, @@ -94,6 +108,9 @@ describe('Redux | Checkout | Selectors', () => { carts: { '/api/restaurants/1': { cart: { shippingTimeRange: null }, + restaurant: { + '@id': '/api/restaurants/1', + }, } }, }, @@ -101,6 +118,7 @@ describe('Redux | Checkout | Selectors', () => { expect(selectShippingTimeRangeLabel({ checkout: { + restaurants: [], restaurant: '/api/restaurants/1', timing: { today: true, @@ -124,6 +142,9 @@ describe('Redux | Checkout | Selectors', () => { '2021-01-26T14:40:00+01:00', ], }, + restaurant: { + '@id': '/api/restaurants/1', + }, } } }, @@ -131,4 +152,94 @@ describe('Redux | Checkout | Selectors', () => { }) + describe('selectRestaurant', () => { + + describe('restaurant selected', () => { + describe('restaurant in list', () => { + it('should return the restaurant', () => { + expect(selectRestaurant({ + checkout: { + restaurants: [{ + '@id': '/api/restaurants/1', + }], + restaurant: '/api/restaurants/1', + carts: {}, + }, + })).toEqual({ + '@id': '/api/restaurants/1', + }) + }) + }) + + describe('restaurant in carts', () => { + it('should return the restaurant', () => { + expect(selectRestaurant({ + checkout: { + restaurants: [], + restaurant: '/api/restaurants/1', + carts: { + '/api/restaurants/1': { + cart: { + }, + restaurant: { + '@id': '/api/restaurants/1', + }, + } + } + }, + })).toEqual({ + '@id': '/api/restaurants/1', + }) + }) + }) + + describe('restaurant in both list and carts', () => { + it('should return the restaurant', () => { + expect(selectRestaurant({ + checkout: { + restaurants: [{ + '@id': '/api/restaurants/1', + }], + restaurant: '/api/restaurants/1', + carts: { + '/api/restaurants/1': { + cart: { + }, + restaurant: { + '@id': '/api/restaurants/1', + }, + } + } + }, + })).toEqual({ + '@id': '/api/restaurants/1', + }) + }) + }) + + describe('restaurant is NOT in list or carts', () => { + it('should return null', () => { + expect(selectRestaurant({ + checkout: { + restaurants: [], + restaurant: '/api/restaurants/1', + carts: {}, + }, + })).toEqual(null) + }) + }) + }) + + describe('restaurant not selected', () => { + it('should return null', () => { + expect(selectRestaurant({ + checkout: { + restaurants: [], + restaurant: null, + carts: {}, + }, + })).toEqual(null) + }) + }) + }) }) diff --git a/src/redux/Checkout/selectors.js b/src/redux/Checkout/selectors.js index 03c866780..ddb5169d1 100644 --- a/src/redux/Checkout/selectors.js +++ b/src/redux/Checkout/selectors.js @@ -58,11 +58,14 @@ export const selectCartByVendor = createSelector( export const selectRestaurant = createSelector( state => state.checkout.restaurants, + state => state.checkout.carts, state => state.checkout.restaurant, - (restaurants, restaurant) => { - return _.find(restaurants, { '@id': restaurant }) ?? null + (restaurants, carts, restaurant) => { + const restaurantsWithCarts = Object.values(carts).map(c => c.restaurant) + return _.find(_.uniqBy(restaurants.concat(restaurantsWithCarts), '@id'), { '@id': restaurant }) ?? null } ) + export const selectRestaurantWithHours = createSelector( selectRestaurant, (selected_restaurant) => { From 8ec5b6a98bb9a29cb528c5b23d194c05793ef575 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:18:37 -0800 Subject: [PATCH 6/9] updated: logs --- src/API.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/API.js b/src/API.js index 91437aa82..d22d3794e 100644 --- a/src/API.js +++ b/src/API.js @@ -136,7 +136,7 @@ Client.prototype.createRequest = function(method, url, data, options = {}) { } } - console.log(`REQUEST: ${method} ${url}${headers.Authorization ? '' : ' (anon.)'}`) + console.log(`→ ${method} ${url}${headers.Authorization ? '' : ' (anon.)'}`) let req = { method, @@ -165,15 +165,15 @@ Client.prototype.request = function (method, uri, data, options = {}) { return this.axios .request(req) .then(response => { - console.log(`RESPONSE: ${method} ${uri}: ${response.status}`) + console.log(`⬅ ${method} ${uri} | ${response.status}`) return response }) .catch(error => { if (error.response) { - console.warn(`RESPONSE: ${method} ${uri}: ${error.response.status}`) + console.warn(`⬅ ${method} ${uri} | ${error.response.status}`) } else { - console.warn(`RESPONSE: ${method} ${uri}: ${error.message}`) + console.warn(`⬅ ${method} ${uri} | ${error.message}`) } return Promise.reject(error) From d4e70383d1d3b8069d9babaf522ed04f1101bc28 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:23:01 -0800 Subject: [PATCH 7/9] fixed: wait for promises before proceeding --- src/redux/Account/actions.js | 3 +-- src/redux/App/actions.js | 45 ++++++++++++++--------------------- src/redux/Checkout/actions.js | 16 ++++++------- 3 files changed, 27 insertions(+), 37 deletions(-) diff --git a/src/redux/Account/actions.js b/src/redux/Account/actions.js index 193a52845..934efa5de 100644 --- a/src/redux/Account/actions.js +++ b/src/redux/Account/actions.js @@ -109,8 +109,7 @@ export function loadAddresses() { const httpClient = getState().app.httpClient - - httpClient.get('/api/me') + return httpClient.get('/api/me') .then(res => { dispatch(loadAddressesSuccess(res.addresses)) }) diff --git a/src/redux/App/actions.js b/src/redux/App/actions.js index 33574d090..ed28e1fee 100644 --- a/src/redux/App/actions.js +++ b/src/redux/App/actions.js @@ -174,16 +174,16 @@ function authenticationRequest() { } function authenticationSuccess(user) { - return (dispatch, getState) => { - dispatch(setLoading(true)) - dispatch(_authenticationSuccess()) - dispatch(loadAddresses()) - dispatch(assignAllCarts()) - dispatch(setLoading(false)) + return async (dispatch, getState) => { + await dispatch(loadAddresses()) + await dispatch(assignAllCarts()) + setRolesProperty(user) tracker.logEvent( analyticsEvent.user.login._category, analyticsEvent.user.login.success) + + dispatch(_authenticationSuccess()) } } @@ -414,8 +414,8 @@ export function login(email, password, navigate = true) { dispatch(authenticationRequest()) httpClient.login(email, password) - .then(user => { - dispatch(authenticationSuccess(user)); + .then(user => dispatch(authenticationSuccess(user))) + .then(() => { if (navigate) { // FIXME // Use setTimeout() to let room for loader to hide @@ -492,13 +492,11 @@ export function confirmRegistration(token) { const httpClient = selectHttpClient(getState()) const checkoutToResumeAfterActivation = selectResumeCheckoutAfterActivation(getState()) - dispatch(setLoading(true)) dispatch(authenticationRequest()) httpClient.confirmRegistration(token) - .then(user => { - dispatch(authenticationSuccess(user)) - + .then(user => dispatch(authenticationSuccess(user))) + .then(() => { //remove RegisterConfirmScreen from stack NavigationHolder.dispatch(CommonActions.reset({ index: 0, @@ -623,9 +621,8 @@ export function setNewPassword(token, password) { httpClient .setNewPassword(token, password) - .then(user => { - dispatch(authenticationSuccess(user)); - + .then(user => dispatch(authenticationSuccess(user))) + .then(() => { if (checkoutToResumeAfterActivation) { dispatch(resumeCheckout(checkoutToResumeAfterActivation)); } else { @@ -678,10 +675,8 @@ export function loginWithFacebook(accessToken, navigate = true) { dispatch(authenticationRequest()) httpClient.loginWithFacebook(accessToken) - .then(user => { - - dispatch(authenticationSuccess(user)); - + .then(user => dispatch(authenticationSuccess(user))) + .then(() => { if (navigate) { // FIXME // Use setTimeout() to let room for loader to hide @@ -711,10 +706,8 @@ export function signInWithApple(identityToken, navigate = true) { dispatch(authenticationRequest()) httpClient.signInWithApple(identityToken) - .then(user => { - - dispatch(authenticationSuccess(user)); - + .then(user => dispatch(authenticationSuccess(user))) + .then(() => { if (navigate) { // FIXME // Use setTimeout() to let room for loader to hide @@ -744,10 +737,8 @@ export function googleSignIn(idToken, navigate = true) { dispatch(authenticationRequest()) httpClient.googleSignIn(idToken) - .then(user => { - - dispatch(authenticationSuccess(user)); - + .then(user => dispatch(authenticationSuccess(user))) + .then(() => { if (navigate) { // FIXME // Use setTimeout() to let room for loader to hide diff --git a/src/redux/Checkout/actions.js b/src/redux/Checkout/actions.js index ac775b28f..c2c741a7e 100644 --- a/src/redux/Checkout/actions.js +++ b/src/redux/Checkout/actions.js @@ -1042,27 +1042,27 @@ function handleSaveOfPaymentMethod(saveCard, cardholderName, getState) { } export function assignAllCarts() { - return async (dispatch, getState) => { - + return (dispatch, getState) => { const { carts } = getState().checkout - _.forEach(carts, cartContainer => { - const { cart, token } = cartContainer - return dispatch(_assignCustomer(cart, token, {})) - }) + return Promise.all( + Object.values(carts).map(({ cart, token }) => + dispatch(_assignCustomer(cart, token, {})), + ), + ) } } export function assignCustomer({ email, telephone }) { return async (dispatch, getState) => { const { cart, token } = selectCart(getState()) - return dispatch(_assignCustomer(cart, token, { email, telephone })) + await dispatch(_assignCustomer(cart, token, { email, telephone })) } } function _assignCustomer(cart, token, { email, telephone }) { - return async (dispatch, getState) => { + return (dispatch, getState) => { const user = selectUser(getState()) From 5c00a96e609c4fed8a4998eb92f2185d0c5e5c4c Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:26:30 -0800 Subject: [PATCH 8/9] set session authorization token explicitly per request --- src/redux/Account/actions.js | 15 +-- src/redux/App/selectors.js | 1 + .../Checkout/__tests__/selectors.test.js | 119 ++++++++++++++++++ src/redux/Checkout/actions.js | 83 ++++++------ src/redux/Checkout/selectors.js | 24 +++- 5 files changed, 188 insertions(+), 54 deletions(-) diff --git a/src/redux/Account/actions.js b/src/redux/Account/actions.js index 934efa5de..ab5b2e626 100644 --- a/src/redux/Account/actions.js +++ b/src/redux/Account/actions.js @@ -6,6 +6,7 @@ import _ from 'lodash' import { logout, setLoading } from '../App/actions' import { selectHttpClient, selectIsAuthenticated } from '../App/selectors' import { selectOrderAccessTokensById } from './selectors' +import { selectCheckoutAuthorizationHeaders } from '../Checkout/selectors' /* * Action Types @@ -36,16 +37,6 @@ const disconnected = createAction(DISCONNECTED) const setOrderAccessToken = createAction(SET_ORDER_ACCESS_TOKEN) -function orderRequestAuthorizationHeaders(order, sessionToken) { - if (order.customer) { - return {} // use the user's token from the httpClient - } else { - return { - 'Authorization': `Bearer ${sessionToken}`, - } - } -} - export function loadOrders(cb) { return function (dispatch, getState) { @@ -68,7 +59,7 @@ export function loadOrder(order, cb) { const orderAccessToken = selectOrderAccessTokensById(getState(), order['@id']) const httpClient = selectHttpClient(getState()) - httpClient.get(order['@id'], { headers: orderRequestAuthorizationHeaders(order, orderAccessToken) }) + httpClient.get(order['@id'], { headers: selectCheckoutAuthorizationHeaders(getState(), order, orderAccessToken) }) .then(res => { dispatch(updateOrderSuccess(res)) if (cb) { cb() } @@ -182,7 +173,7 @@ export function subscribe(order, onMessage) { const orderAccessToken = selectOrderAccessTokensById(getState(), order['@id']) const httpClient = selectHttpClient(getState()) - httpClient.get(`${order['@id']}/centrifugo`, { headers: orderRequestAuthorizationHeaders(order, orderAccessToken) }) + httpClient.get(`${order['@id']}/centrifugo`, { headers: selectCheckoutAuthorizationHeaders(getState(), order, orderAccessToken) }) .then(res => { const url = parseUrl(baseURL) diff --git a/src/redux/App/selectors.js b/src/redux/App/selectors.js index db0de117d..29376e679 100644 --- a/src/redux/App/selectors.js +++ b/src/redux/App/selectors.js @@ -9,6 +9,7 @@ export const selectHttpClient = state => state.app.httpClient export const selectResumeCheckoutAfterActivation = state => state.app.resumeCheckoutAfterActivation +// a user with an account export const selectIsAuthenticated = createSelector( selectUser, (user) => !!(user && user.isAuthenticated()) diff --git a/src/redux/Checkout/__tests__/selectors.test.js b/src/redux/Checkout/__tests__/selectors.test.js index 6467d6cb6..a2bbdcb42 100644 --- a/src/redux/Checkout/__tests__/selectors.test.js +++ b/src/redux/Checkout/__tests__/selectors.test.js @@ -1,6 +1,7 @@ import moment from 'moment' import { + selectCheckoutAuthorizationHeaders, selectRestaurant, selectShippingTimeRangeLabel, } from '../selectors' @@ -242,4 +243,122 @@ describe('Redux | Checkout | Selectors', () => { }) }) }) + + describe('selectCheckoutAuthorizationHeaders', () => { + describe('a user with an account', () => { + const user = { + isAuthenticated: () => true, + } + + describe('cart is assigned to a user', () => { + it('should use default token', () => { + expect( + selectCheckoutAuthorizationHeaders( + { + app: { + user: user, + }, + }, + { + customer: '/api/customers/1', + }, + 'session-token', + ), + ).toEqual({}) + }) + }) + + describe('cart is NOT assigned to a user', () => { + it('should use a session token', () => { + expect( + selectCheckoutAuthorizationHeaders( + { + app: { + user: user, + }, + }, + { + customer: null, + }, + 'session-token', + ), + ).toEqual({ + Authorization: 'Bearer session-token', + }) + }) + }) + }) + + describe('a user in a guest mode', () => { + const user = { + isAuthenticated: () => false, + isGuest: () => true, + } + + describe('cart is assigned to a user', () => { + it('should use a session token', () => { + expect( + selectCheckoutAuthorizationHeaders( + { + app: { + user: user, + }, + }, + { + customer: '/api/customers/1', + }, + 'session-token', + ), + ).toEqual({ + Authorization: 'Bearer session-token', + }) + }) + }) + + describe('cart is NOT assigned to a user', () => { + it('should use a session token', () => { + expect( + selectCheckoutAuthorizationHeaders( + { + app: { + user: user, + }, + }, + { + customer: null, + }, + 'session-token', + ), + ).toEqual({ + Authorization: 'Bearer session-token', + }) + }) + }) + }) + + describe('unauthenticated user (!= guest mode)', () => { + const user = { + isAuthenticated: () => false, + isGuest: () => false, + } + + it('should use a session token', () => { + expect( + selectCheckoutAuthorizationHeaders( + { + app: { + user: user, + }, + }, + { + customer: null, + }, + 'session-token', + ), + ).toEqual({ + Authorization: 'Bearer session-token', + }) + }) + }) + }) }) diff --git a/src/redux/Checkout/actions.js b/src/redux/Checkout/actions.js index c2c741a7e..94bf98a87 100644 --- a/src/redux/Checkout/actions.js +++ b/src/redux/Checkout/actions.js @@ -11,6 +11,7 @@ import { selectCartByVendor, selectCartFulfillmentMethod, selectCartWithHours, + selectCheckoutAuthorizationHeaders } from './selectors' import { selectHttpClient, @@ -217,16 +218,6 @@ function notifyListeners(address) { listeners = [] } -function cartRequestAuthorizationHeaders(cart, sessionToken) { - if (cart.customer) { - return {} // use the user's token from the httpClient - } else { - return { - 'Authorization': `Bearer ${sessionToken}`, - } - } -} - export function addItemV2(item, quantity = 1, restaurant, options) { return async (dispatch, getState) => { @@ -269,7 +260,7 @@ export function addItemV2(item, quantity = 1, restaurant, options) { product: item.identifier, quantity, options, - }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) dispatch(addItemRequestFinished(item)) dispatch(updateCartSuccess(response)) } @@ -370,7 +361,7 @@ function queueAddItem(item, quantity = 1, options = []) { product: item.identifier, quantity, options, - }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(setCheckoutLoading(false)) dispatch(addItemRequestFinished(item)) @@ -398,7 +389,7 @@ const fetchValidation = _.throttle((dispatch, getState, cart) => { const doTiming = () => new Promise((resolve) => { httpClient - .get(`${cart['@id']}/timing`, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .get(`${cart['@id']}/timing`, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(timing => dispatch(setTiming(timing))) // .catch(error => dispatch(setCartValidation(false, error.violations))) .finally(resolve) @@ -406,7 +397,7 @@ const fetchValidation = _.throttle((dispatch, getState, cart) => { const doValidate = () => new Promise((resolve) => { httpClient - .get(`${cart['@id']}/validate`, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .get(`${cart['@id']}/validate`, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(() => dispatch(setCartValidation(true))) .catch(error => { if (error.response && error.response.status === 400) { @@ -450,7 +441,7 @@ function syncItem(item) { // FIXME We should have the "@id" property .put(`${cart['@id']}/items/${item.id}`, { quantity: itemFromState.quantity, - }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(setCheckoutLoading(false)) @@ -504,7 +495,7 @@ function queueRemoveItem(item) { httpClient // FIXME We should have the "@id" property - .delete(`${cart['@id']}/items/${item.id}`, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .delete(`${cart['@id']}/items/${item.id}`, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(setCheckoutLoading(false)) @@ -551,13 +542,13 @@ export function removeItem(item) { } } -export function setTip(order, tipAmount) { +export function setTip(cart, tipAmount) { return (dispatch, getState) => { - const { token } = selectCartByVendor(getState(), order.restaurant) + const { token } = selectCartByVendor(getState(), cart.restaurant) const httpClient = selectHttpClient(getState()) dispatch(checkoutRequest()) - httpClient.put(`${order['@id']}/tip`, { tipAmount }, { headers: cartRequestAuthorizationHeaders(order, token) }) + httpClient.put(`${cart['@id']}/tip`, { tipAmount }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(checkoutSuccess()) @@ -592,12 +583,18 @@ function syncAddress(cart, address) { queue: 'UPDATE_CART', callback: (next, dispatch, getState) => { + if (!cart) { + console.error(new Error('syncAddress: cart is undefined')) + next() + return + } + const { token } = selectCartByVendor(getState(), cart.restaurant) dispatch(setToken(token)) const httpClient = selectHttpClient(getState()) - httpClient.put(cart['@id'], { shippingAddress: address }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + httpClient.put(cart['@id'], { shippingAddress: address }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(setCheckoutLoading(false)) @@ -771,7 +768,7 @@ export function mercadopagoCheckout(payment) { const httpClient = selectHttpClient(getState()) httpClient - .put(cart['@id'] + '/pay', params, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .put(cart['@id'] + '/pay', params, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(order => { dispatch(handleSuccessNav(order)); }) @@ -783,7 +780,7 @@ export function mercadopagoCheckout(payment) { function handleSuccessNav(order) { return (dispatch, getState) => { - const { token } = selectCartByVendor(getState(), order.restaurant) + const { token } = selectCartByVendor(getState(), order.restaurant['@id']) dispatch(setNewOrder(order, token)) @@ -855,7 +852,7 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = if (isFree(cart)) { httpClient - .put(cart['@id'] + '/pay', {}, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .put(cart['@id'] + '/pay', {}, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(o => dispatch(handleSuccessNav(o))) .catch(e => dispatch(checkoutFailure(e))); @@ -867,7 +864,7 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = if (paymentDetails.stripeAccount) { // for connected account we have to clone the platform payment method return httpClient - .get(`${cart['@id']}/stripe/clone-payment-method/${paymentMethodId}`, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .get(`${cart['@id']}/stripe/clone-payment-method/${paymentMethodId}`, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(clonnedPaymentMethod => { return [paymentMethodId, clonnedPaymentMethod.id]; }); @@ -880,7 +877,7 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = httpClient.put(cart['@id'] + '/pay', { paymentMethodId: clonnedPaymentMethodId || platformAccountPaymentMethodId, saveCard, - }, { headers: cartRequestAuthorizationHeaders(cart, token) }), + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }), ) .then(stripeResponse => { if (stripeResponse.requiresAction) { @@ -985,7 +982,7 @@ function handleSuccess( }) .then(() => { httpClient - .put(cart['@id'] + '/pay', { paymentIntentId }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .put(cart['@id'] + '/pay', { paymentIntentId }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(order => { dispatch(handleSuccessNav(order)); }) @@ -1031,7 +1028,7 @@ function handleSaveOfPaymentMethod(saveCard, cardholderName, getState) { httpClient .post(cart['@id'] + '/stripe/create-setup-intent-or-attach-pm', { payment_method_to_save: paymentMethod.id, - }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(() => resolve()); } }); @@ -1133,15 +1130,19 @@ export function resetSearch(options = {}) { } } -const doUpdateCart = (dispatch, httpClient, cart, token, payload, cb) => { - httpClient - .put(cart['@id'], payload, { headers: cartRequestAuthorizationHeaders(cart, token) }) +const doUpdateCart = (cart, token, payload, cb) => { + return (dispatch, getState) => { + const httpClient = selectHttpClient(getState()) + + return httpClient + .put(cart['@id'], payload, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(checkoutSuccess()) _.isFunction(cb) && cb(res) }) .catch(e => dispatch(checkoutFailure(e))) + } } export function updateCart(payload, cb) { @@ -1174,12 +1175,12 @@ export function updateCart(payload, cb) { const { telephone, ...payloadWithoutTelephone } = payload httpClient - .put(cart.customer, { telephone }, { headers: cartRequestAuthorizationHeaders(cart, token) }) - .then(res => doUpdateCart(dispatch, httpClient, cart, token, payloadWithoutTelephone, cb)) + .put(cart.customer, { telephone }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) + .then(res => dispatch(doUpdateCart(cart, token, payloadWithoutTelephone, cb))) .catch(e => dispatch(checkoutFailure(e))) } else { - doUpdateCart(dispatch, httpClient, cart, token, payload, cb) + dispatch(doUpdateCart(cart, token, payload, cb)) } } } @@ -1197,7 +1198,7 @@ export function setDate(shippingTimeRange, cb) { httpClient .put(cart['@id'], { shippingTimeRange, - }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) setTimeout(() => { @@ -1221,7 +1222,7 @@ export function setDateAsap(cart, cb) { httpClient .put(cart['@id'], { shippingTimeRange: null, - }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) setTimeout(() => { @@ -1248,10 +1249,10 @@ export function setFulfillmentMethod(method) { httpClient .put(cart['@id'], { fulfillmentMethod: method, - }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { httpClient - .get(`${cart['@id']}/timing`, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .get(`${cart['@id']}/timing`, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(timing => { dispatch(setCheckoutLoading(false)) dispatch(setTiming(timing)) @@ -1293,7 +1294,7 @@ export function loadPaymentMethods(method) { dispatch(loadPaymentMethodsRequest()) httpClient - .get(`${cart['@id']}/payment_methods`, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .get(`${cart['@id']}/payment_methods`, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => dispatch(loadPaymentMethodsSuccess(res))) .catch(e => dispatch(loadPaymentMethodsFailure(e))) } @@ -1309,7 +1310,7 @@ export function checkoutWithCash() { dispatch(checkoutRequest()) httpClient - .put(cart['@id'] + '/pay', { cashOnDelivery: true }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .put(cart['@id'] + '/pay', { cashOnDelivery: true }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(order => dispatch(handleSuccessNav(order))) .catch(e => dispatch(checkoutFailure(e))) } @@ -1325,7 +1326,7 @@ export function loadPaymentDetails() { dispatch(loadPaymentDetailsRequest()) httpClient - .get(`${cart['@id']}/payment`, { headers: cartRequestAuthorizationHeaders(cart, token) }) + .get(`${cart['@id']}/payment`, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => dispatch(loadPaymentDetailsSuccess(res))) .catch(e => dispatch(loadPaymentDetailsFailure(e))) } @@ -1442,7 +1443,7 @@ export function updateLoopeatReturns(returns) { httpClient .post(cart['@id'] + '/loopeat_returns', { returns, - }, { headers: cartRequestAuthorizationHeaders(cart, token) }) + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) }) diff --git a/src/redux/Checkout/selectors.js b/src/redux/Checkout/selectors.js index ddb5169d1..721c471ab 100644 --- a/src/redux/Checkout/selectors.js +++ b/src/redux/Checkout/selectors.js @@ -3,7 +3,11 @@ import _ from 'lodash' import moment from 'moment' import i18n from '../../i18n' -import { selectIsAuthenticated, selectUser } from '../App/selectors' +import { + selectIsAuthenticated, + selectIsGuest, + selectUser, +} from '../App/selectors' import OpeningHoursSpecification from '../../utils/OpeningHoursSpecification'; import Address from '../../utils/Address'; @@ -265,3 +269,21 @@ export const selectAvailableRestaurants = createSelector( state => state.checkout.restaurants, (restaurants) => _.map(restaurants, r => r.id) ) + +const _selectCartParam = (state, cart) => cart +const _selectTokenParam = (state, cart, token) => token + +export const selectCheckoutAuthorizationHeaders = createSelector( + selectIsAuthenticated, + _selectCartParam, + _selectTokenParam, + (isAuthenticatedUser, cart, sessionToken) => { + if (isAuthenticatedUser && cart.customer) { + return {} // use the user's token from the httpClient + } else { + return { + 'Authorization': `Bearer ${sessionToken}`, + } + } + } +) From 48e478e68539e8a4995fd098e66813b39a8d6dc9 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:46:54 -0800 Subject: [PATCH 9/9] improved logging --- src/API.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/API.js b/src/API.js index d22d3794e..c544b544e 100644 --- a/src/API.js +++ b/src/API.js @@ -162,10 +162,12 @@ Client.prototype.createRequest = function(method, url, data, options = {}) { Client.prototype.request = function (method, uri, data, options = {}) { const req = this.createRequest(method, uri, data, options) + const start = Date.now() return this.axios .request(req) .then(response => { - console.log(`⬅ ${method} ${uri} | ${response.status}`) + const duration = Date.now() - start + console.log(`⬅ ${method} ${uri} | ${response.status} | ${duration}ms`) return response })