diff --git a/src/API.js b/src/API.js index b1fa2eb6c..c544b544e 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(`→ ${method} ${url}${headers.Authorization ? '' : ' (anon.)'}`) + let req = { method, url, @@ -159,19 +160,36 @@ 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); + const start = Date.now() + return this.axios + .request(req) + .then(response => { + const duration = Date.now() - start + console.log(`⬅ ${method} ${uri} | ${response.status} | ${duration}ms`) + + return response + }) + .catch(error => { + if (error.response) { + console.warn(`⬅ ${method} ${uri} | ${error.response.status}`) + } else { + console.warn(`⬅ ${method} ${uri} | ${error.message}`) + } + + return Promise.reject(error) + }) } -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 = {}) { @@ -179,9 +197,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..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 @@ -56,9 +57,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: selectCheckoutAuthorizationHeaders(getState(), order, orderAccessToken) }) .then(res => { dispatch(updateOrderSuccess(res)) if (cb) { cb() } @@ -99,8 +100,7 @@ export function loadAddresses() { const httpClient = getState().app.httpClient - - httpClient.get('/api/me') + return httpClient.get('/api/me') .then(res => { dispatch(loadAddressesSuccess(res.addresses)) }) @@ -171,9 +171,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: selectCheckoutAuthorizationHeaders(getState(), order, orderAccessToken) }) .then(res => { const url = parseUrl(baseURL) diff --git a/src/redux/App/actions.js b/src/redux/App/actions.js index df4347f1c..333486386 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 @@ -168,16 +178,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()) } } @@ -408,8 +418,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 @@ -461,7 +471,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 }) @@ -479,16 +493,14 @@ 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()) httpClient.confirmRegistration(token) - .then(user => { - dispatch(authenticationSuccess(user)) - + .then(user => dispatch(authenticationSuccess(user))) + .then(() => { //remove RegisterConfirmScreen from stack NavigationHolder.dispatch(CommonActions.reset({ index: 0, @@ -497,8 +509,8 @@ export function confirmRegistration(token) { ], })) - if (resumeCheckoutAfterActivation) { - dispatch(resumeCheckout()) + if (checkoutToResumeAfterActivation) { + dispatch(resumeCheckout(checkoutToResumeAfterActivation)) } else { navigateToHome(dispatch, getState) } @@ -534,12 +546,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' }, + ] + } + }, + ], + } + }, + ], + }), + ) } } @@ -554,7 +601,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 }); }) @@ -567,18 +618,17 @@ 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()); httpClient .setNewPassword(token, password) - .then(user => { - dispatch(authenticationSuccess(user)); - - if (resumeCheckoutAfterActivation) { - dispatch(resumeCheckout()); + .then(user => dispatch(authenticationSuccess(user))) + .then(() => { + if (checkoutToResumeAfterActivation) { + dispatch(resumeCheckout(checkoutToResumeAfterActivation)); } else { navigateToHome(dispatch, getState); } @@ -629,10 +679,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 @@ -662,10 +710,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 @@ -695,10 +741,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/App/reducers.js b/src/redux/App/reducers.js index 667bca3f8..cc54f204b 100644 --- a/src/redux/App/reducers.js +++ b/src/redux/App/reducers.js @@ -67,7 +67,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 b7525cd18..e560c692e 100644 --- a/src/redux/App/selectors.js +++ b/src/redux/App/selectors.js @@ -5,10 +5,13 @@ 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 selectCustomBuild = state => state.app.customBuild +export const selectResumeCheckoutAfterActivation = state => state.app.resumeCheckoutAfterActivation + +// a user with an account export const selectIsAuthenticated = createSelector( selectUser, (user) => !!(user && user.isAuthenticated()) @@ -19,25 +22,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/__tests__/selectors.test.js b/src/redux/Checkout/__tests__/selectors.test.js index 1312e612c..a2bbdcb42 100644 --- a/src/redux/Checkout/__tests__/selectors.test.js +++ b/src/redux/Checkout/__tests__/selectors.test.js @@ -1,6 +1,8 @@ import moment from 'moment' import { + selectCheckoutAuthorizationHeaders, + selectRestaurant, selectShippingTimeRangeLabel, } from '../selectors' @@ -21,12 +23,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 +40,7 @@ describe('Redux | Checkout | Selectors', () => { expect(selectShippingTimeRangeLabel({ checkout: { + restaurants: [], restaurant: '/api/restaurants/1', timing: { today: true, @@ -50,6 +57,9 @@ describe('Redux | Checkout | Selectors', () => { carts: { '/api/restaurants/1': { cart: { shippingTimeRange: null }, + restaurant: { + '@id': '/api/restaurants/1', + }, } }, }, @@ -57,6 +67,7 @@ describe('Redux | Checkout | Selectors', () => { expect(selectShippingTimeRangeLabel({ checkout: { + restaurants: [], restaurant: '/api/restaurants/1', timing: { today: false, @@ -72,6 +83,9 @@ describe('Redux | Checkout | Selectors', () => { carts: { '/api/restaurants/1': { cart: { shippingTimeRange: null }, + restaurant: { + '@id': '/api/restaurants/1', + }, } }, }, @@ -79,6 +93,7 @@ describe('Redux | Checkout | Selectors', () => { expect(selectShippingTimeRangeLabel({ checkout: { + restaurants: [], restaurant: '/api/restaurants/1', timing: { today: true, @@ -94,6 +109,9 @@ describe('Redux | Checkout | Selectors', () => { carts: { '/api/restaurants/1': { cart: { shippingTimeRange: null }, + restaurant: { + '@id': '/api/restaurants/1', + }, } }, }, @@ -101,6 +119,7 @@ describe('Redux | Checkout | Selectors', () => { expect(selectShippingTimeRangeLabel({ checkout: { + restaurants: [], restaurant: '/api/restaurants/1', timing: { today: true, @@ -124,6 +143,9 @@ describe('Redux | Checkout | Selectors', () => { '2021-01-26T14:40:00+01:00', ], }, + restaurant: { + '@id': '/api/restaurants/1', + }, } } }, @@ -131,4 +153,212 @@ 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) + }) + }) + }) + + 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 cb893a221..89821233f 100644 --- a/src/redux/Checkout/actions.js +++ b/src/redux/Checkout/actions.js @@ -5,7 +5,14 @@ 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, + selectCheckoutAuthorizationHeaders +} from './selectors' import { selectHttpClient, selectIsAuthenticated, @@ -186,17 +193,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) { @@ -221,7 +223,7 @@ 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 +236,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 +253,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) dispatch(addItemRequestFinished(item)) dispatch(updateCartSuccess(response)) } @@ -352,8 +351,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 +361,7 @@ function queueAddItem(item, quantity = 1, options = []) { product: item.identifier, quantity, options, - }) + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(setCheckoutLoading(false)) dispatch(addItemRequestFinished(item)) @@ -380,7 +379,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 +389,7 @@ const fetchValidation = _.throttle((dispatch, getState, cart) => { const doTiming = () => new Promise((resolve) => { httpClient - .get(`${cart['@id']}/timing`) + .get(`${cart['@id']}/timing`, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(timing => dispatch(setTiming(timing))) // .catch(error => dispatch(setCartValidation(false, error.violations))) .finally(resolve) @@ -397,7 +397,7 @@ const fetchValidation = _.throttle((dispatch, getState, cart) => { const doValidate = () => new Promise((resolve) => { httpClient - .get(`${cart['@id']}/validate`) + .get(`${cart['@id']}/validate`, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(() => dispatch(setCartValidation(true))) .catch(error => { if (error.response && error.response.status === 400) { @@ -423,9 +423,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 +441,7 @@ function syncItem(item) { // FIXME We should have the "@id" property .put(`${cart['@id']}/items/${item.id}`, { quantity: itemFromState.quantity, - }) + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(setCheckoutLoading(false)) @@ -487,15 +487,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(setCheckoutLoading(false)) @@ -531,7 +531,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() @@ -541,12 +542,13 @@ export function removeItem(item) { } } -export function setTip(order, tipAmount) { +export function setTip(cart, tipAmount) { return (dispatch, getState) => { - const httpClient = createHttpClientWithSessionToken(getState()) + const { token } = selectCartByVendor(getState(), cart.restaurant) + const httpClient = selectHttpClient(getState()) dispatch(checkoutRequest()) - httpClient.put(`${order['@id']}/tip`, { tipAmount }) + httpClient.put(`${cart['@id']}/tip`, { tipAmount }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(checkoutSuccess()) @@ -581,11 +583,18 @@ 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()) + 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(carts[cart.restaurant].cart['@id'], { shippingAddress: address }) + httpClient.put(cart['@id'], { shippingAddress: address }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) dispatch(setCheckoutLoading(false)) @@ -642,7 +651,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 +692,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 +715,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 +751,7 @@ export function mercadopagoCheckout(payment) { } return (dispatch, getState) => { - const { cart } = getState().checkout; + const { cart, token } = getState().checkout; const { id, status, statusDetail } = payment; @@ -756,10 +765,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(order => { dispatch(handleSuccessNav(order)); }) @@ -771,7 +780,7 @@ export function mercadopagoCheckout(payment) { function handleSuccessNav(order) { return (dispatch, getState) => { - const { token } = getState().checkout + const { token } = selectCartByVendor(getState(), order.restaurant['@id']) dispatch(setNewOrder(order, token)) @@ -817,12 +826,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)) { dispatch(checkoutFailure()); @@ -844,7 +853,7 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = if (isFree(cart)) { httpClient - .put(cart['@id'] + '/pay', {}) + .put(cart['@id'] + '/pay', {}, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(o => dispatch(handleSuccessNav(o))) .catch(e => dispatch(checkoutFailure(e))); @@ -856,7 +865,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(clonnedPaymentMethod => { return [paymentMethodId, clonnedPaymentMethod.id]; }); @@ -869,7 +878,7 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = httpClient.put(cart['@id'] + '/pay', { paymentMethodId: clonnedPaymentMethodId || platformAccountPaymentMethodId, saveCard, - }), + }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }), ) .then(stripeResponse => { if (stripeResponse.requiresAction) { @@ -880,8 +889,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, @@ -900,8 +909,8 @@ export function checkout(cardholderName, savedPaymentMethodId = null, saveCard = }); } else { dispatch(handleSuccess( - httpClient, cart, + token, stripeResponse.paymentIntentId, saveCard, cardholderName, @@ -953,16 +962,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`, { @@ -973,7 +983,7 @@ function handleSuccess( }) .then(() => { httpClient - .put(cart['@id'] + '/pay', { paymentIntentId }) + .put(cart['@id'] + '/pay', { paymentIntentId }, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(order => { dispatch(handleSuccessNav(order)); }) @@ -990,15 +1000,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', @@ -1018,7 +1029,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(() => resolve()); } }); @@ -1029,19 +1040,28 @@ function handleSaveOfPaymentMethod(saveCard, cardholderName, httpClient, state) } export function assignAllCarts() { - return async (dispatch, getState) => { - + return (dispatch, getState) => { const { carts } = getState().checkout - _.forEach(carts, cartContainer => dispatch(assignCustomer({}, cartContainer))) + return Promise.all( + Object.values(carts).map(({ cart, token }) => + dispatch(_assignCustomer(cart, token, {})), + ), + ) } } -export function assignCustomer({ email, telephone }, cartContainer = null) { - +export function assignCustomer({ email, telephone }) { return async (dispatch, getState) => { - const { cart, token } = selectCart(getState()) + await dispatch(_assignCustomer(cart, token, { email, telephone })) + } +} + +function _assignCustomer(cart, token, { email, telephone }) { + + return (dispatch, getState) => { + const user = selectUser(getState()) if (!user.isGuest() && cart.customer) { @@ -1050,24 +1070,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 => { @@ -1107,15 +1131,19 @@ export function resetSearch(options = {}) { } } -const doUpdateCart = (dispatch, httpClient, cart, payload, cb) => { - httpClient - .put(cart['@id'], payload) +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) { @@ -1124,9 +1152,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 = { @@ -1148,12 +1176,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) + .then(res => dispatch(doUpdateCart(cart, token, payloadWithoutTelephone, cb))) .catch(e => dispatch(checkoutFailure(e))) } else { - doUpdateCart(dispatch, httpClient, cart, payload, cb) + dispatch(doUpdateCart(cart, token, payload, cb)) } } } @@ -1163,15 +1191,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) setTimeout(() => { @@ -1187,16 +1215,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) setTimeout(() => { @@ -1218,15 +1245,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { httpClient - .get(`${cart['@id']}/timing`) + .get(`${cart['@id']}/timing`, { headers: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(timing => { dispatch(setCheckoutLoading(false)) dispatch(setTiming(timing)) @@ -1261,14 +1288,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => dispatch(loadPaymentMethodsSuccess(res))) .catch(e => dispatch(loadPaymentMethodsFailure(e))) } @@ -1278,13 +1305,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(order => dispatch(handleSuccessNav(order))) .catch(e => dispatch(checkoutFailure(e))) } @@ -1294,13 +1321,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => dispatch(loadPaymentDetailsSuccess(res))) .catch(e => dispatch(loadPaymentDetailsFailure(e))) } @@ -1389,7 +1416,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 @@ -1409,15 +1436,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: selectCheckoutAuthorizationHeaders(getState(), cart, token) }) .then(res => { dispatch(updateCartSuccess(res)) }) 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: [], diff --git a/src/redux/Checkout/selectors.js b/src/redux/Checkout/selectors.js index 3d1be3771..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'; @@ -39,13 +43,33 @@ 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.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) => { @@ -245,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}`, + } + } + } +) diff --git a/src/redux/reducers.js b/src/redux/reducers.js index a46317180..268394c12 100644 --- a/src/redux/reducers.js +++ b/src/redux/reducers.js @@ -113,6 +113,7 @@ const appPersistConfig = { 'pushNotificationToken', 'hasDisclosedBackgroundPermission', 'firstRun', + 'resumeCheckoutAfterActivation', 'isSpinnerDelayEnabled' ], migrate: (state) => {