@shopgate/engage 7.30.0 → 7.30.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/a11y/reducers/index.js +1 -4
  2. package/account/reducers/index.js +2 -4
  3. package/app-rating/reducers/index.js +1 -1
  4. package/back-in-stock/reducers/index.js +1 -1
  5. package/cart/components/CartItem/CartItemProductLayoutWide.js +1 -1
  6. package/cart/components/CartItem/CartItemProductLayoutWide.style.js +1 -1
  7. package/checkout/paymentMethods/paypal/PaypalPayButton.js +1 -1
  8. package/checkout/paymentMethods/stripe/StripeButton.js +6 -2
  9. package/checkout/reducers/index.js +1 -3
  10. package/components/IntersectionVisibility/index.js +14 -9
  11. package/core/actions/getGeolocation.js +4 -4
  12. package/core/actions/grantAppTrackingTransparencyPermission.d.ts +24 -0
  13. package/core/actions/grantAppTrackingTransparencyPermission.js +18 -2
  14. package/core/actions/grantCameraPermissions.d.ts +24 -0
  15. package/core/actions/grantCameraPermissions.js +5 -2
  16. package/core/actions/grantGeolocationPermissions.d.ts +36 -0
  17. package/core/actions/grantGeolocationPermissions.js +18 -6
  18. package/core/actions/grantPermissions.d.ts +142 -0
  19. package/core/actions/grantPermissions.js +132 -18
  20. package/core/actions/grantPushPermissions.d.ts +24 -0
  21. package/core/actions/grantPushPermissions.js +9 -2
  22. package/core/actions/requestAppPermission.js +12 -3
  23. package/core/actions/requestAppPermissionStatus.js +2 -1
  24. package/core/config/config.reducers.js +1 -1
  25. package/core/config/config.selectors.js +6 -1
  26. package/core/constants/index.js +24 -1
  27. package/core/reducers/app.js +1 -4
  28. package/core/reducers/merchantSettings.js +1 -4
  29. package/core/reducers/shopSettings.js +3 -6
  30. package/core/router/permissionRouteRedirectHandler.js +117 -0
  31. package/core/subscriptions/app.js +10 -2
  32. package/development/reducers/settings.js +1 -3
  33. package/development/reducers/storage.js +1 -3
  34. package/filter/components/FilterPageContent/components/ResetButton/style.js +1 -1
  35. package/locations/components/FulfillmentSlotSwitcher/time.js +1 -1
  36. package/locations/reducers/storage.js +2 -6
  37. package/locations/reducers/storeFinderSearch.js +1 -5
  38. package/locations/reducers/user.js +1 -1
  39. package/locations/reducers/userFormInput.js +1 -1
  40. package/locations/reducers/userSearch.js +1 -1
  41. package/locations/selectors/index.js +21 -26
  42. package/orders/reducers/orders.js +4 -11
  43. package/orders/reducers/ordersById.js +4 -9
  44. package/orders/reducers/ordersByNumber.js +4 -9
  45. package/package.json +14 -13
  46. package/page/reducers/index.js +1 -5
  47. package/page/selectors/index.js +2 -1
  48. package/product/components/Description/style.js +1 -1
  49. package/product/components/EffectivityDates/index.js +3 -3
  50. package/product/components/FilterBar/mock.js +1 -1
  51. package/product/components/MapPriceHint/index.js +4 -4
  52. package/product/components/ProductList/components/Item/index.js +2 -5
  53. package/product/components/Swatch/Swatch.js +1 -1
  54. package/product/components/Swatch/VariantSwatch.js +1 -1
  55. package/product/components/Swatches/Swatches.js +1 -1
  56. package/product/selectors/product.js +6 -1
  57. package/push-opt-in/reducers/optInModal.js +1 -1
  58. package/push-opt-in/reducers/optInTrigger.js +1 -1
  59. package/tracking/reducers/cookieConsentModal.js +1 -1
  60. package/tracking/reducers/cookieSettings.js +1 -1
  61. package/tracking/selectors/cookieConsent.js +2 -1
  62. package/tracking/subscriptions/cookieConsent.js +3 -9
@@ -1,22 +1,33 @@
1
1
  import event from '@shopgate/pwa-core/classes/Event';
2
2
  import { openAppSettings } from '@shopgate/engage/core/commands';
3
3
  import { showModal } from '@shopgate/engage/core/actions';
4
- import { PERMISSION_STATUS_DENIED, PERMISSION_STATUS_GRANTED, PERMISSION_STATUS_NOT_DETERMINED, PERMISSION_STATUS_NOT_SUPPORTED, APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND, availablePermissionsIds } from '@shopgate/engage/core/constants';
4
+ import { getIsAndroidApp } from '@shopgate/engage/core/selectors';
5
+ import { PERMISSION_ID_LOCATION, PERMISSION_ID_BACKGROUND_LOCATION, PERMISSION_ID_CAMERA, PERMISSION_STATUS_DENIED, PERMISSION_STATUS_GRANTED, PERMISSION_STATUS_NOT_DETERMINED, PERMISSION_STATUS_NOT_SUPPORTED, PERMISSION_USAGE_ALWAYS, APP_EVENT_APPLICATION_WILL_ENTER_FOREGROUND, availablePermissionsIds } from '@shopgate/engage/core/constants';
5
6
  import { logger, hasSGJavaScriptBridge, hasWebBridge } from '@shopgate/engage/core/helpers';
6
7
  import { softOptInShown, softOptInSelected, hardOptInShown, hardOptInSelected } from "../action-creators";
7
8
  import requestAppPermission from "./requestAppPermission";
8
9
  import requestAppPermissionStatus from "./requestAppPermissionStatus";
9
10
  import { createMockedPermissions } from "../helpers/appPermissions";
10
11
 
12
+ // List of permissions that need a timing based user interaction check on Android, since we can't
13
+ // reliably handle this in the native layer.
14
+ const ANDROID_PERMISSIONS_WITH_USER_INTERACTION_CHECK = [PERMISSION_ID_LOCATION, PERMISSION_ID_BACKGROUND_LOCATION, PERMISSION_ID_CAMERA];
15
+
11
16
  /**
12
17
  * Determines the current state of a specific permission for an app feature. If not already
13
18
  * happened, the user will be prompted to grant permissions.
19
+ *
14
20
  * The action returns a promise which resolves with a boolean value, that indicates the state.
21
+ *
22
+ * When the "resolveWithData" option is set to TRUE, the promise will resolve with an object
23
+ * containing the permission status and additional data, instead of a boolean value.
15
24
  * @param {Object} options Action options.
16
25
  * @param {string} options.permissionId The id of the permission to request.
26
+ * @param {Object} [options.permissionOptions={}] Additional options for the permission request.
17
27
  * @param {boolean} [options.useSettingsModal=false] Whether in case of declined permissions a modal
18
28
  * shall be presented, which redirects to the app settings.
19
- * @param {boolean} [options.useRationaleModal=false] Whether a rational modal should be shown
29
+ * @param {boolean} [options.useRationaleModal=false] Whether a rational modal should be shown that
30
+ * describes why the permission is needed before requesting the permission.
20
31
  * @param {Object} [options.rationaleModal={}] Options for the rationale modal.
21
32
  * @param {string} options.rationaleModal.title Modal title.
22
33
  * @param {string} options.rationaleModal.message Modal message.
@@ -31,14 +42,16 @@ import { createMockedPermissions } from "../helpers/appPermissions";
31
42
  * @param {Object} options.modal.params Additional parameters for i18n strings.
32
43
  * @param {boolean} [options.requestPermissions=true] If set to TRUE no permissions will be
33
44
  * requested if not already granted,
34
- * @param {boolean} [options.resolveWithData=false] If set to TRUE the Promise will resolve with
35
- * data if available (e.g. geolocation).
45
+ * @param {boolean} [options.resolveWithData=true] When set to TRUE the promise will resolve with
46
+ * an object containing the permission status and whether the opt-in dialog was shown,
47
+ * instead of a boolean value.
36
48
  * @param {Object} [options.meta={}] Additional meta data used for opt-in tracking actions
37
49
  * @return { Function } A redux thunk.
38
50
  */
39
- const grantPermissions = (options = {}) => dispatch => new Promise(async resolve => {
51
+ const grantPermissions = (options = {}) => (dispatch, getState) => new Promise(async resolve => {
40
52
  const {
41
53
  permissionId,
54
+ permissionOptions,
42
55
  useSettingsModal = false,
43
56
  useRationaleModal = false,
44
57
  rationaleModal: rationaleModalOptions = {},
@@ -48,6 +61,8 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
48
61
  meta = {}
49
62
  } = options;
50
63
  let dispatchMock;
64
+ let optInRequested = false;
65
+ const isAndroidApp = getIsAndroidApp(getState());
51
66
  if (!hasSGJavaScriptBridge() || hasWebBridge()) {
52
67
  /**
53
68
  * The fallbackStatus will be used at browsers that don't support the permissions API. By
@@ -60,30 +75,54 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
60
75
  }
61
76
  if (!availablePermissionsIds.includes(permissionId)) {
62
77
  logger.error('grandPermissions: %s is no valid permission id', permissionId);
63
- resolve(false);
78
+ resolve(resolveWithData ? {
79
+ success: false,
80
+ optInRequested
81
+ } : false);
64
82
  return;
65
83
  }
66
84
  let status;
85
+ let appPermissionOptions;
67
86
  let data;
68
87
 
69
88
  // Check the current status of the camera permissions.
70
89
  ({
71
- status
90
+ status,
91
+ options: appPermissionOptions
72
92
  } = await dispatch(requestAppPermissionStatus({
73
93
  permissionId,
74
94
  dispatchMock
75
95
  })));
76
96
 
97
+ // When the location permission is requested for "always" usage and the permissions where already
98
+ // granted for "whenInUse" we need to trigger the permission request again to get extended
99
+ // permissions.
100
+ const upgradeLocationPermission = permissionId === PERMISSION_ID_LOCATION && permissionOptions?.usage === PERMISSION_USAGE_ALWAYS && status === PERMISSION_STATUS_GRANTED;
101
+
77
102
  // Stop the process when the permission type is not supported.
78
103
  if (status === PERMISSION_STATUS_NOT_SUPPORTED) {
79
- resolve(false);
104
+ resolve(resolveWithData ? {
105
+ success: false,
106
+ optInRequested,
107
+ status,
108
+ ...(appPermissionOptions ? {
109
+ options: appPermissionOptions
110
+ } : {})
111
+ } : false);
80
112
  return;
81
113
  }
82
114
 
83
115
  // The user never seen the permissions dialog yet, or temporary denied the permissions (Android).
84
- if (status === PERMISSION_STATUS_NOT_DETERMINED) {
116
+ if (status === PERMISSION_STATUS_NOT_DETERMINED || upgradeLocationPermission) {
85
117
  if (!requestPermissions) {
86
- resolve(false);
118
+ resolve(resolveWithData ? {
119
+ success: false,
120
+ optInRequested,
121
+ status,
122
+ ...(appPermissionOptions ? {
123
+ options: appPermissionOptions
124
+ } : {})
125
+ } : false);
87
126
  return;
88
127
  }
89
128
  if (useRationaleModal) {
@@ -101,7 +140,14 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
101
140
  meta
102
141
  }));
103
142
  if (requestAllowed === false) {
104
- resolve(false);
143
+ resolve(resolveWithData ? {
144
+ success: false,
145
+ optInRequested,
146
+ status,
147
+ ...(appPermissionOptions ? {
148
+ options: appPermissionOptions
149
+ } : {})
150
+ } : false);
105
151
  return;
106
152
  }
107
153
  }
@@ -109,15 +155,42 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
109
155
  permissionId,
110
156
  meta
111
157
  }));
158
+ const tsBeforeRequest = Date.now();
159
+ let nativeRequestDuration;
112
160
 
113
161
  // Trigger the native permissions dialog.
114
162
  ({
115
163
  status,
116
- data
164
+ data,
165
+ options: appPermissionOptions,
166
+ duration: nativeRequestDuration
117
167
  } = await dispatch(requestAppPermission({
118
168
  permissionId,
119
- dispatchMock
169
+ dispatchMock,
170
+ ...(permissionOptions ? {
171
+ options: permissionOptions
172
+ } : {})
120
173
  })));
174
+ let wasUserInteraction = true;
175
+
176
+ /**
177
+ * On iOS it's not possible to get "notDetermined" status for "always" location permissions.
178
+ * So we might run into this decision branch of the code for when "always" permissions are
179
+ * requested, even if we don't know if a dialog will be shown.
180
+ * We can only guess that the user interacted with the dialog and if we might need to show
181
+ * the settings modal.
182
+ *
183
+ * Additionally, at some permissions requests on Android we can't prevent that the
184
+ * getPermissions request returns "notDetermined" since there is an user option to ask for
185
+ * permission acceptance every time its needed. So we also need to try a user interaction guess.
186
+ */
187
+ if (upgradeLocationPermission || isAndroidApp && ANDROID_PERMISSIONS_WITH_USER_INTERACTION_CHECK.includes(permissionId)) {
188
+ // When available, use the duration of the native request as heuristic for user interaction.
189
+ // It's more accurate, since it won't include the time that communication with the app takes.
190
+ const requestDuration = typeof nativeRequestDuration === 'number' ? nativeRequestDuration : Date.now() - tsBeforeRequest;
191
+ wasUserInteraction = requestDuration > 1000;
192
+ }
193
+ optInRequested = wasUserInteraction;
121
194
  dispatch(hardOptInSelected({
122
195
  permissionId,
123
196
  status,
@@ -126,19 +199,45 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
126
199
 
127
200
  // The user denied the permissions within the native dialog.
128
201
  if ([PERMISSION_STATUS_DENIED, PERMISSION_STATUS_NOT_DETERMINED].includes(status)) {
129
- resolve(false);
130
- return;
202
+ if (wasUserInteraction) {
203
+ resolve(resolveWithData ? {
204
+ success: false,
205
+ optInRequested,
206
+ status,
207
+ ...(appPermissionOptions ? {
208
+ options: appPermissionOptions
209
+ } : {})
210
+ } : false);
211
+ return;
212
+ }
131
213
  }
132
214
  }
133
215
  if (status === PERMISSION_STATUS_GRANTED) {
134
- resolve(resolveWithData && data ? data : true);
216
+ resolve(resolveWithData ? {
217
+ success: true,
218
+ optInRequested,
219
+ status,
220
+ ...(appPermissionOptions ? {
221
+ options: appPermissionOptions
222
+ } : {}),
223
+ ...(data !== undefined ? {
224
+ data
225
+ } : {})
226
+ } : true);
135
227
  return;
136
228
  }
137
229
 
138
230
  // The user permanently denied the permissions before.
139
231
  if (status === PERMISSION_STATUS_DENIED) {
140
232
  if (!useSettingsModal) {
141
- resolve(false);
233
+ resolve(resolveWithData ? {
234
+ success: false,
235
+ optInRequested,
236
+ status,
237
+ ...(appPermissionOptions ? {
238
+ options: appPermissionOptions
239
+ } : {})
240
+ } : false);
142
241
  return;
143
242
  }
144
243
 
@@ -153,7 +252,14 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
153
252
 
154
253
  // The user just closed the modal.
155
254
  if (!openSettings) {
156
- resolve(false);
255
+ resolve(resolveWithData ? {
256
+ success: false,
257
+ optInRequested,
258
+ status,
259
+ ...(appPermissionOptions ? {
260
+ options: appPermissionOptions
261
+ } : {})
262
+ } : false);
157
263
  return;
158
264
  }
159
265
 
@@ -168,6 +274,14 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
168
274
  permissionId
169
275
  })));
170
276
  resolve(status === PERMISSION_STATUS_GRANTED);
277
+ resolve(resolveWithData ? {
278
+ success: status === PERMISSION_STATUS_GRANTED,
279
+ optInRequested,
280
+ status,
281
+ ...(appPermissionOptions ? {
282
+ options: appPermissionOptions
283
+ } : {})
284
+ } : status === PERMISSION_STATUS_GRANTED);
171
285
  };
172
286
 
173
287
  /**
@@ -0,0 +1,24 @@
1
+ import type { UnknownAction } from 'redux';
2
+ import type { ThunkAction } from 'redux-thunk';
3
+
4
+ export type { GrantPermissionsOptions, GrantPermissionsResult } from './grantPermissions';
5
+ export type { GrantPermissionsOptions, GrantPermissionsResult };
6
+
7
+ /**
8
+ * Determines the current state of the push permissions.
9
+ * If not already happened, the user will be prompted to grant permissions.
10
+ *
11
+ * The action returns a promise which resolves with a boolean value, that indicates the state.
12
+ *
13
+ * When the "resolveWithData" option is set to TRUE, the promise will resolve with an object
14
+ * containing the permission status and additional data, instead of a boolean value.
15
+ */
16
+ declare function grantPushPermissions<State = unknown>(
17
+ options: Omit<GrantPermissionsOptions, 'permissionId'> & { resolveWithData: true }
18
+ ): ThunkAction<Promise<GrantPermissionsResult>, State, unknown, UnknownAction>;
19
+
20
+ declare function grantPushPermissions<State = unknown>(
21
+ options?: Omit<GrantPermissionsOptions, 'permissionId'> & { resolveWithData?: false | undefined }
22
+ ): ThunkAction<Promise<boolean>, State, unknown, UnknownAction>;
23
+
24
+ export default grantPushPermissions;
@@ -22,15 +22,20 @@ import grantPermissions from "./grantPermissions";
22
22
  * @param {string} options.modal.dismiss Label for the dismiss button.
23
23
  * @param {Object} options.modal.params Additional parameters for i18n strings.
24
24
  * @param {Object} [options.meta={}] Additional meta data used for opt-in tracking actions
25
+ * @param {boolean} [options.resolveWithData=true] When set to TRUE the promise will resolve with
26
+ * an object containing the permission status and whether the opt-in dialog was shown,
27
+ * instead of a boolean value.
25
28
  * @return { Function } A redux thunk.
26
29
  */
27
30
  const grantPushPermissions = (options = {}) => dispatch => {
28
31
  const {
32
+ permissionId,
29
33
  useSettingsModal = true,
30
34
  useRationaleModal = false,
31
35
  modal = {},
32
36
  rationaleModal = {},
33
- meta = {}
37
+ meta = {},
38
+ ...rest
34
39
  } = options;
35
40
  return dispatch(grantPermissions({
36
41
  permissionId: PERMISSION_ID_PUSH,
@@ -40,10 +45,12 @@ const grantPushPermissions = (options = {}) => dispatch => {
40
45
  title: null,
41
46
  message: 'permissions.access_denied.push_message',
42
47
  confirm: 'permissions.access_denied.settings_button',
48
+ dismiss: 'modal.dismiss',
43
49
  ...modal
44
50
  },
45
51
  rationaleModal,
46
- meta
52
+ meta,
53
+ ...rest
47
54
  }));
48
55
  };
49
56
  export default grantPushPermissions;
@@ -8,6 +8,8 @@ import { appPermissionStatusReceived } from "../action-creators";
8
8
  * Additionally it propagates the received status via the APP_PERMISSION_STATUS_RECEIVED action.
9
9
  * @param {Object} params The action params
10
10
  * @param {string} params.permissionId The desired app permission id
11
+ * @param {Object} [params.options={}] Additional options for the permission request.
12
+ * E.g. the usage object for geolocation permissions.
11
13
  * @param {Function} [params.dispatchMock=null] An optional mock for the request dispatch logic.
12
14
  * Usually used when PWA is running inside a browser and app command logic can be simulated via
13
15
  * browser APIs like for geolocation access.
@@ -15,6 +17,7 @@ import { appPermissionStatusReceived } from "../action-creators";
15
17
  */
16
18
  const requestAppPermission = ({
17
19
  permissionId,
20
+ options: requestOptions,
18
21
  dispatchMock: dispatchMockParam
19
22
  }) => async dispatch => {
20
23
  let dispatchMock = dispatchMockParam;
@@ -26,11 +29,15 @@ const requestAppPermission = ({
26
29
  const [{
27
30
  status,
28
31
  options,
29
- data
32
+ data,
33
+ duration
30
34
  } = {
31
35
  status: PERMISSION_STATUS_NOT_SUPPORTED
32
36
  }] = (await requestAppPermissions([{
33
- permissionId
37
+ permissionId,
38
+ ...(requestOptions ? {
39
+ options: requestOptions
40
+ } : {})
34
41
  }], dispatchMock)) ?? [];
35
42
  dispatch(appPermissionStatusReceived({
36
43
  permissionId,
@@ -39,7 +46,9 @@ const requestAppPermission = ({
39
46
  }));
40
47
  return {
41
48
  status,
42
- data
49
+ options,
50
+ data,
51
+ duration
43
52
  };
44
53
  };
45
54
  export default requestAppPermission;
@@ -36,7 +36,8 @@ const requestAppPermissionStatus = ({
36
36
  options
37
37
  }));
38
38
  return {
39
- status
39
+ status,
40
+ options
40
41
  };
41
42
  };
42
43
  export default requestAppPermissionStatus;
@@ -6,7 +6,7 @@ import { REQUEST_CORE_CONFIG, RECEIVE_CORE_CONFIG, ERROR_CORE_CONFIG, CONFIG_LIF
6
6
  * @param {Object} action The action object.
7
7
  * @return {Object} The new state.
8
8
  */
9
- export default function config(state = {}, action) {
9
+ export default function config(state = {}, action = {}) {
10
10
  switch (action.type) {
11
11
  case REQUEST_CORE_CONFIG:
12
12
  return {
@@ -1,12 +1,17 @@
1
1
  import { createSelector } from 'reselect';
2
2
 
3
+ // Use a shared empty object so selectors that fall back to "no config"
4
+ // remain referentially stable across calls with the same state value.
5
+ // Fixes reselect warning about selector returning new value each time.
6
+ const EMPTY_CONFIG = {};
7
+
3
8
  /**
4
9
  * Retrieves the config state from the store.
5
10
  * @param {Object} state The current application state.
6
11
  * @return {Object} The locations state.
7
12
  */
8
13
  function getState(state) {
9
- return state?.settings?.config || {};
14
+ return state?.settings?.config || EMPTY_CONFIG;
10
15
  }
11
16
 
12
17
  /**
@@ -45,4 +45,27 @@ export const EMPTY_OBJECT = Object.freeze({});
45
45
  * Helper constant that can be used as a return value in Redux selectors to prevent
46
46
  * unnecessary re-renders.
47
47
  */
48
- export const EMPTY_ARRAY = Object.freeze([]);
48
+ export const EMPTY_ARRAY = Object.freeze([]);
49
+
50
+ /**
51
+ * Virtual route to trigger a location permission request to the app.
52
+ */
53
+ export const PERMISSION_REQUEST_ROUTE_LOCATION = '/permissions/location';
54
+ /**
55
+ * Virtual route to trigger a background location permission request to the app.
56
+ */
57
+ export const PERMISSION_REQUEST_ROUTE_LOCATION_BACKGROUND = '/permissions/location_background';
58
+ /**
59
+ * Virtual route to trigger a push permission request to the app.
60
+ */
61
+ export const PERMISSION_REQUEST_ROUTE_PUSH = '/permissions/push';
62
+ /**
63
+ * Virtual route to trigger an app tracking transparency permission request to the app.
64
+ */
65
+ export const PERMISSION_REQUEST_ROUTE_TRACKING = '/permissions/tracking';
66
+
67
+ /**
68
+ * Virtual route to trigger a camera permission request to the app.
69
+ */
70
+ export const PERMISSION_REQUEST_ROUTE_CAMERA = '/permissions/camera';
71
+ export const PERMISSION_REQUEST_ROUTES = [PERMISSION_REQUEST_ROUTE_LOCATION, PERMISSION_REQUEST_ROUTE_LOCATION_BACKGROUND, PERMISSION_REQUEST_ROUTE_PUSH, PERMISSION_REQUEST_ROUTE_TRACKING, PERMISSION_REQUEST_ROUTE_CAMERA];
@@ -10,8 +10,7 @@ const defaultState = {
10
10
  * @param {Object} action The action object.
11
11
  * @returns {Object} The new state.
12
12
  */
13
- export default function app(state = defaultState, action) {
14
- /* eslint-disable no-param-reassign */
13
+ export default function app(state = defaultState, action = {}) {
15
14
  const producer = produce(draft => {
16
15
  switch (action.type) {
17
16
  case PWA_DID_APPEAR:
@@ -28,7 +27,5 @@ export default function app(state = defaultState, action) {
28
27
  break;
29
28
  }
30
29
  });
31
- /* eslint-enable no-param-reassign */
32
-
33
30
  return producer(state);
34
31
  }
@@ -21,8 +21,7 @@ const defaultState = {
21
21
  * @param {Object} action The action object.
22
22
  * @returns {Object} The new state.
23
23
  */
24
- export default function merchantSettings(state = defaultState, action) {
25
- /* eslint-disable no-param-reassign */
24
+ export default function merchantSettings(state = defaultState, action = {}) {
26
25
  const producer = produce(draft => {
27
26
  switch (action.type) {
28
27
  case RECEIVE_MERCHANT_SETTINGS:
@@ -36,7 +35,5 @@ export default function merchantSettings(state = defaultState, action) {
36
35
  break;
37
36
  }
38
37
  });
39
-
40
- /* eslint-enable no-param-reassign */
41
38
  return producer(state);
42
39
  }
@@ -27,8 +27,7 @@ const defaultState = {
27
27
  * @param {Object} action The action object.
28
28
  * @returns {Object} The new state.
29
29
  */
30
- export default function shopSettings(state = defaultState, action) {
31
- /* eslint-disable no-param-reassign */
30
+ export default function shopSettings(state = defaultState, action = {}) {
32
31
  const producer = produce(draft => {
33
32
  switch (action.type) {
34
33
  case RECEIVE_SHOP_SETTINGS:
@@ -40,17 +39,15 @@ export default function shopSettings(state = defaultState, action) {
40
39
  }
41
40
  case ERROR_SHOP_SETTINGS:
42
41
  {
43
- draft = {
42
+ Object.assign(draft, {
44
43
  ...defaultState,
45
44
  ...state
46
- };
45
+ });
47
46
  break;
48
47
  }
49
48
  default:
50
49
  break;
51
50
  }
52
51
  });
53
-
54
- /* eslint-enable no-param-reassign */
55
52
  return producer(state);
56
53
  }
@@ -0,0 +1,117 @@
1
+ import { showModal } from '@shopgate/engage/core/actions';
2
+ import { logger } from "../helpers";
3
+ import { PERMISSION_REQUEST_ROUTE_LOCATION, PERMISSION_REQUEST_ROUTE_LOCATION_BACKGROUND, PERMISSION_REQUEST_ROUTE_PUSH, PERMISSION_REQUEST_ROUTE_TRACKING, PERMISSION_REQUEST_ROUTE_CAMERA, PERMISSION_STATUS_GRANTED } from "../constants";
4
+ import { grantPushPermissions, grantAppTrackingTransparencyPermission, grantGeolocationPermissions, grantCameraPermissions } from "../actions";
5
+
6
+ /**
7
+ * @typedef {import('redux-thunk').ThunkDispatch<
8
+ * import('../store').RootState,
9
+ * any,
10
+ * import('redux').AnyAction
11
+ * >} AppDispatch
12
+ */
13
+
14
+ /**
15
+ * @typedef {import('../actions/grantPermissions').GrantPermissionsResult} GrantPermissionsResult
16
+ */
17
+
18
+ /**
19
+ * Handler for redirect collection handlers related to permission requests.
20
+ * @param {{ action: any, dispatch: AppDispatch }} params Handler params
21
+ */
22
+ export const permissionRouteRedirectHandler = async ({
23
+ action,
24
+ dispatch
25
+ }) => {
26
+ const {
27
+ route: {
28
+ pathname
29
+ } = {}
30
+ } = action;
31
+
32
+ /**
33
+ * @type {GrantPermissionsResult | undefined}
34
+ */
35
+ let result;
36
+ let grantedMessage;
37
+ const metaBase = {
38
+ context: 'permissionRoute'
39
+ };
40
+ switch (pathname) {
41
+ case PERMISSION_REQUEST_ROUTE_LOCATION:
42
+ {
43
+ result = await dispatch(grantGeolocationPermissions({
44
+ useSettingsModal: true,
45
+ resolveWithData: true,
46
+ meta: {
47
+ ...metaBase,
48
+ permission: 'location'
49
+ }
50
+ }));
51
+ grantedMessage = 'permissions.accessGranted.locationMessage';
52
+ break;
53
+ }
54
+ case PERMISSION_REQUEST_ROUTE_LOCATION_BACKGROUND:
55
+ {
56
+ result = await dispatch(grantGeolocationPermissions({
57
+ useSettingsModal: true,
58
+ requireBackgroundAccess: true,
59
+ resolveWithData: true,
60
+ meta: {
61
+ ...metaBase,
62
+ permission: 'backgroundLocation'
63
+ }
64
+ }));
65
+ grantedMessage = 'permissions.accessGranted.backgroundLocationMessage';
66
+ break;
67
+ }
68
+ case PERMISSION_REQUEST_ROUTE_PUSH:
69
+ {
70
+ result = await dispatch(grantPushPermissions({
71
+ useSettingsModal: true,
72
+ resolveWithData: true,
73
+ meta: {
74
+ ...metaBase,
75
+ permission: 'push'
76
+ }
77
+ }));
78
+ grantedMessage = 'permissions.accessGranted.pushMessage';
79
+ break;
80
+ }
81
+ case PERMISSION_REQUEST_ROUTE_TRACKING:
82
+ {
83
+ result = await dispatch(grantAppTrackingTransparencyPermission({
84
+ useSettingsModal: true,
85
+ resolveWithData: true,
86
+ meta: {
87
+ ...metaBase,
88
+ permission: 'tracking'
89
+ }
90
+ }));
91
+ grantedMessage = 'permissions.accessGranted.trackingMessage';
92
+ break;
93
+ }
94
+ case PERMISSION_REQUEST_ROUTE_CAMERA:
95
+ {
96
+ result = await dispatch(grantCameraPermissions({
97
+ useSettingsModal: true,
98
+ resolveWithData: true,
99
+ meta: {
100
+ ...metaBase,
101
+ permission: 'camera'
102
+ }
103
+ }));
104
+ grantedMessage = 'permissions.accessGranted.cameraMessage';
105
+ break;
106
+ }
107
+ default:
108
+ logger.warn('Unknown permission request route', pathname);
109
+ }
110
+ if (result?.status === PERMISSION_STATUS_GRANTED && result?.optInRequested === false) {
111
+ dispatch(showModal({
112
+ message: grantedMessage,
113
+ confirm: 'modal.ok',
114
+ dismiss: ''
115
+ }));
116
+ }
117
+ };
@@ -1,14 +1,15 @@
1
1
  import { appWillStart$ } from '@shopgate/engage/core/streams';
2
- import { configuration } from '@shopgate/engage/core/collections';
2
+ import { configuration, redirects } from '@shopgate/engage/core/collections';
3
3
  import { hasNewServices, appSupportsAndroidEdgeToEdge, updateAndroidNavigationBarColor } from '@shopgate/engage/core/helpers';
4
4
  import { CONFIGURATION_COLLECTION_KEY_BASE_URL } from '@shopgate/engage/core/constants';
5
5
  import { appConfig } from '@shopgate/engage';
6
6
  import { reloadApp$ } from "../streams";
7
7
  import { reloadApp } from "../action-creators";
8
+ import { PERMISSION_REQUEST_ROUTE_LOCATION, PERMISSION_REQUEST_ROUTE_LOCATION_BACKGROUND, PERMISSION_REQUEST_ROUTE_PUSH, PERMISSION_REQUEST_ROUTE_TRACKING, PERMISSION_REQUEST_ROUTE_CAMERA } from "../constants";
9
+ import { permissionRouteRedirectHandler } from "../router/permissionRouteRedirectHandler";
8
10
  const {
9
11
  androidNavigationBarDefaultColor
10
12
  } = appConfig;
11
-
12
13
  /**
13
14
  * App subscriptions
14
15
  * @param {Function} subscribe The subscribe function
@@ -29,6 +30,13 @@ export default function app(subscribe) {
29
30
  color: androidNavigationBarDefaultColor
30
31
  });
31
32
  }
33
+
34
+ // Register redirects for app permission request routes
35
+ redirects.set(PERMISSION_REQUEST_ROUTE_LOCATION, permissionRouteRedirectHandler);
36
+ redirects.set(PERMISSION_REQUEST_ROUTE_LOCATION_BACKGROUND, permissionRouteRedirectHandler);
37
+ redirects.set(PERMISSION_REQUEST_ROUTE_PUSH, permissionRouteRedirectHandler);
38
+ redirects.set(PERMISSION_REQUEST_ROUTE_TRACKING, permissionRouteRedirectHandler);
39
+ redirects.set(PERMISSION_REQUEST_ROUTE_CAMERA, permissionRouteRedirectHandler);
32
40
  });
33
41
  subscribe(reloadApp$, () => {
34
42
  // A deployed app can only be reloaded on the base url. So we first set this as current route
@@ -21,8 +21,7 @@ const initialState = {
21
21
  * @param {Object} action The redux action.
22
22
  * @returns {Object}
23
23
  */
24
- export default function settingsReducer(state = initialState, action) {
25
- /* eslint-disable no-param-reassign */
24
+ export default function settingsReducer(state = initialState, action = {}) {
26
25
  const producer = produce(/** @param {DevToolsSettingsState} draft The draft */draft => {
27
26
  switch (action.type) {
28
27
  case DEVELOPMENT_TOOLS_TOGGLE_INSETS:
@@ -48,6 +47,5 @@ export default function settingsReducer(state = initialState, action) {
48
47
  break;
49
48
  }
50
49
  });
51
- /* eslint-enable no-param-reassign */
52
50
  return producer(state);
53
51
  }