@shopgate/engage 7.30.1 → 7.30.2-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.
- package/category/actions/index.js +2 -1
- package/components/View/components/Content/index.js +1 -1
- package/core/actions/getGeolocation.d.ts +34 -0
- package/core/actions/getGeolocation.js +12 -12
- package/core/actions/grantAppTrackingTransparencyPermission.d.ts +25 -0
- package/core/actions/grantAppTrackingTransparencyPermission.js +21 -2
- package/core/actions/grantCameraPermissions.d.ts +25 -0
- package/core/actions/grantCameraPermissions.js +16 -10
- package/core/actions/grantGeolocationPermissions.d.ts +37 -0
- package/core/actions/grantGeolocationPermissions.js +30 -15
- package/core/actions/grantPermissions.d.ts +155 -0
- package/core/actions/grantPermissions.js +163 -31
- package/core/actions/grantPushPermissions.d.ts +25 -0
- package/core/actions/grantPushPermissions.js +19 -11
- package/core/actions/requestAppPermission.js +12 -3
- package/core/actions/requestAppPermissionStatus.js +2 -1
- package/core/constants/index.js +24 -1
- package/core/router/permissionRouteRedirectHandler.js +117 -0
- package/core/subscriptions/app.js +23 -2
- package/package.json +8 -7
- package/tracking/subscriptions/cookieConsent.js +3 -9
|
@@ -2,4 +2,5 @@ export { default as fetchCategory } from '@shopgate/pwa-common-commerce/category
|
|
|
2
2
|
export { default as fetchCategoryOrRootCategories } from '@shopgate/pwa-common-commerce/category/actions/fetchCategoryOrRootCategories';
|
|
3
3
|
export { default as fetchCategoryChildren } from '@shopgate/pwa-common-commerce/category/actions/fetchCategoryChildren';
|
|
4
4
|
export { default as fetchCategoryProducts } from '@shopgate/pwa-common-commerce/category/actions/fetchCategoryProducts';
|
|
5
|
-
export { default as fetchRootCategories } from '@shopgate/pwa-common-commerce/category/actions/fetchRootCategories';
|
|
5
|
+
export { default as fetchRootCategories } from '@shopgate/pwa-common-commerce/category/actions/fetchRootCategories';
|
|
6
|
+
export { default as receiveCategories } from '@shopgate/pwa-common-commerce/category/action-creators/receiveCategories';
|
|
@@ -110,7 +110,7 @@ let ViewContent = /*#__PURE__*/function (_Component) {
|
|
|
110
110
|
window.requestAnimationFrame(() => {
|
|
111
111
|
if (this.ref.current === window) {
|
|
112
112
|
window.scrollTo(0, scrollTop || 0);
|
|
113
|
-
} else if (this.ref.current?.scrollTop) {
|
|
113
|
+
} else if (typeof this.ref.current?.scrollTop !== 'undefined') {
|
|
114
114
|
this.ref.current.scrollTop = scrollTop || 0;
|
|
115
115
|
}
|
|
116
116
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { UnknownAction } from 'redux';
|
|
2
|
+
import type { ThunkAction } from 'redux-thunk';
|
|
3
|
+
|
|
4
|
+
import type { GrantPermissionsOptions } from './grantPermissions';
|
|
5
|
+
|
|
6
|
+
export type GetGeolocationResult = {
|
|
7
|
+
/**
|
|
8
|
+
* The longitude of the user's location.
|
|
9
|
+
*/
|
|
10
|
+
longitude: number;
|
|
11
|
+
/**
|
|
12
|
+
* The latitude of the user's location.
|
|
13
|
+
*/
|
|
14
|
+
latitude: number;
|
|
15
|
+
/**
|
|
16
|
+
* The accuracy of the geolocation data, in meters.
|
|
17
|
+
*/
|
|
18
|
+
accuracy: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type GetGeolocationReject = Error & { code: string }
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Determines the current geolocation of the user. When geolocation permissions are not granted yet,
|
|
25
|
+
* the user will be prompted to grant access.
|
|
26
|
+
*
|
|
27
|
+
* When the permissions are granted, the action will resolve with the geolocation data.
|
|
28
|
+
* Otherwise, it will reject with an error.
|
|
29
|
+
*/
|
|
30
|
+
declare function getGeolocation<State = unknown>(
|
|
31
|
+
options: Omit<GrantPermissionsOptions, 'permissionId'|'permissionOptions'>
|
|
32
|
+
): ThunkAction<Promise<GetGeolocationResult, GetGeolocationReject>, State, unknown, UnknownAction>;
|
|
33
|
+
|
|
34
|
+
export default getGeolocation;
|
|
@@ -9,24 +9,24 @@ import GeolocationRequest from "../classes/GeolocationRequest";
|
|
|
9
9
|
* @param {Object} options Action options.
|
|
10
10
|
* @param {boolean} [options.useSettingsModal=false] Whether in case of declined permissions a modal
|
|
11
11
|
* shall be presented, which redirects to the app settings.
|
|
12
|
-
* @param {Object} [options.
|
|
13
|
-
* @param {string} options.
|
|
14
|
-
* @param {string} options.
|
|
15
|
-
* @param {string} options.
|
|
16
|
-
* @param {string} options.
|
|
17
|
-
* @param {Object} options.
|
|
12
|
+
* @param {Object} [options.settingsModal={}] Options for the settings modal.
|
|
13
|
+
* @param {string} options.settingsModal.title Modal title.
|
|
14
|
+
* @param {string} options.settingsModal.message Modal message.
|
|
15
|
+
* @param {string} options.settingsModal.confirm Label for the confirm button.
|
|
16
|
+
* @param {string} options.settingsModal.dismiss Label for the dismiss button.
|
|
17
|
+
* @param {Object} options.settingsModal.params Additional parameters for i18n strings.
|
|
18
18
|
* @return { Function } A redux thunk.
|
|
19
19
|
*/
|
|
20
20
|
const getGeolocation = (options = {}) => async dispatch => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const result = await dispatch(grantGeolocationPermissions({
|
|
22
|
+
...options,
|
|
23
|
+
resolveWithData: true
|
|
24
24
|
}));
|
|
25
|
-
if (
|
|
25
|
+
if (result?.data) {
|
|
26
26
|
// Other than the app, within browsers we might already have a geolocation.
|
|
27
|
-
return Promise.resolve(
|
|
27
|
+
return Promise.resolve(result.data);
|
|
28
28
|
}
|
|
29
|
-
if (!
|
|
29
|
+
if (!result.success) {
|
|
30
30
|
const error = new Error('Geolocation permissions not granted.');
|
|
31
31
|
error.code = GEOLOCATION_ERROR_DENIED;
|
|
32
32
|
throw error;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { UnknownAction } from 'redux';
|
|
2
|
+
import type { ThunkAction } from 'redux-thunk';
|
|
3
|
+
|
|
4
|
+
import type { GrantPermissionsOptions, GrantPermissionsResult } from './grantPermissions';
|
|
5
|
+
|
|
6
|
+
export type GrantAppTrackingTransparencyPermissionsResult = GrantPermissionsResult;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Determines the current state of the app tracking transparency permissions.
|
|
10
|
+
* If not already happened, the user will be prompted to grant permissions.
|
|
11
|
+
*
|
|
12
|
+
* The action returns a promise which resolves with a boolean value, that indicates the state.
|
|
13
|
+
*
|
|
14
|
+
* When the "resolveWithData" option is set to TRUE, the promise will resolve with an object
|
|
15
|
+
* containing the permission status and additional data, instead of a boolean value.
|
|
16
|
+
*/
|
|
17
|
+
declare function grantAppTrackingTransparencyPermission<State = unknown>(
|
|
18
|
+
options: Omit<GrantPermissionsOptions, 'permissionId'|'permissionOptions'> & { resolveWithData: true }
|
|
19
|
+
): ThunkAction<Promise<GrantAppTrackingTransparencyPermissionsResult>, State, unknown, UnknownAction>;
|
|
20
|
+
|
|
21
|
+
declare function grantAppTrackingTransparencyPermission<State = unknown>(
|
|
22
|
+
options?: Omit<GrantPermissionsOptions, 'permissionId'|'permissionOptions'> & { resolveWithData?: false | undefined }
|
|
23
|
+
): ThunkAction<Promise<boolean>, State, unknown, UnknownAction>;
|
|
24
|
+
|
|
25
|
+
export default grantAppTrackingTransparencyPermission;
|
|
@@ -7,15 +7,34 @@ import grantPermissions from "./grantPermissions";
|
|
|
7
7
|
* The action returns a promise which resolves with a boolean value, that indicates the state.
|
|
8
8
|
* @param {Object} options Action options.
|
|
9
9
|
* @param {Object} [options.meta={}] Additional meta data used for opt-in tracking actions
|
|
10
|
+
* @param {Object} [options.settingsModal={}] Options for the settings modal.
|
|
11
|
+
* @param {string} options.settingsModal.title Modal title.
|
|
12
|
+
* @param {string} options.settingsModal.message Modal message.
|
|
13
|
+
* @param {string} options.settingsModal.confirm Label for the confirm button.
|
|
14
|
+
* @param {string} options.settingsModal.dismiss Label for the dismiss button.
|
|
15
|
+
* @param {Object} options.settingsModal.params Additional parameters for i18n strings.
|
|
10
16
|
* @return { Function } A redux thunk.
|
|
11
17
|
*/
|
|
12
18
|
const grantAppTrackingTransparencyPermission = (options = {}) => dispatch => {
|
|
13
19
|
const {
|
|
14
|
-
|
|
20
|
+
permissionId,
|
|
21
|
+
meta = {},
|
|
22
|
+
settingsModal,
|
|
23
|
+
// @deprecated modal setting, to be removed in future major release
|
|
24
|
+
modal,
|
|
25
|
+
...rest
|
|
15
26
|
} = options;
|
|
27
|
+
const settingsModalOptions = settingsModal || modal || {};
|
|
16
28
|
return dispatch(grantPermissions({
|
|
17
29
|
permissionId: PERMISSION_ID_APP_TRACKING_TRANSPARENCY,
|
|
18
|
-
meta
|
|
30
|
+
meta,
|
|
31
|
+
settingsModal: {
|
|
32
|
+
message: 'permissions.access_denied.trackingMessage',
|
|
33
|
+
confirm: 'permissions.access_denied.settings_button',
|
|
34
|
+
dismiss: 'modal.dismiss',
|
|
35
|
+
...settingsModalOptions
|
|
36
|
+
},
|
|
37
|
+
...rest
|
|
19
38
|
}));
|
|
20
39
|
};
|
|
21
40
|
export default grantAppTrackingTransparencyPermission;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { UnknownAction } from 'redux';
|
|
2
|
+
import type { ThunkAction } from 'redux-thunk';
|
|
3
|
+
|
|
4
|
+
import type { GrantPermissionsOptions, GrantPermissionsResult } from './grantPermissions';
|
|
5
|
+
|
|
6
|
+
export type GrantCameraPermissionsResult = GrantPermissionsResult;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Determines the current state of the camera permissions.
|
|
10
|
+
* If not already happened, the user will be prompted to grant permissions.
|
|
11
|
+
*
|
|
12
|
+
* The action returns a promise which resolves with a boolean value, that indicates the state.
|
|
13
|
+
*
|
|
14
|
+
* When the "resolveWithData" option is set to TRUE, the promise will resolve with an object
|
|
15
|
+
* containing the permission status and additional data, instead of a boolean value.
|
|
16
|
+
*/
|
|
17
|
+
declare function grantCameraPermissions<State = unknown>(
|
|
18
|
+
options: Omit<GrantPermissionsOptions, 'permissionId'|'permissionOptions'> & { resolveWithData: true }
|
|
19
|
+
): ThunkAction<Promise<GrantCameraPermissionsResult>, State, unknown, UnknownAction>;
|
|
20
|
+
|
|
21
|
+
declare function grantCameraPermissions<State = unknown>(
|
|
22
|
+
options?: Omit<GrantPermissionsOptions, 'permissionId'|'permissionOptions'> & { resolveWithData?: false | undefined }
|
|
23
|
+
): ThunkAction<Promise<boolean>, State, unknown, UnknownAction>;
|
|
24
|
+
|
|
25
|
+
export default grantCameraPermissions;
|
|
@@ -8,29 +8,35 @@ import grantPermissions from "./grantPermissions";
|
|
|
8
8
|
* @param {Object} options Action options.
|
|
9
9
|
* @param {boolean} [options.useSettingsModal=false] Whether in case of declined permissions a modal
|
|
10
10
|
* shall be presented, which redirects to the app settings.
|
|
11
|
-
* @param {Object} [options.
|
|
12
|
-
* @param {string} options.
|
|
13
|
-
* @param {string} options.
|
|
14
|
-
* @param {string} options.
|
|
15
|
-
* @param {string} options.
|
|
16
|
-
* @param {Object} options.
|
|
11
|
+
* @param {Object} [options.settingsModal={}] Options for the settings modal.
|
|
12
|
+
* @param {string} options.settingsModal.title Modal title.
|
|
13
|
+
* @param {string} options.settingsModal.message Modal message.
|
|
14
|
+
* @param {string} options.settingsModal.confirm Label for the confirm button.
|
|
15
|
+
* @param {string} options.settingsModal.dismiss Label for the dismiss button.
|
|
16
|
+
* @param {Object} options.settingsModal.params Additional parameters for i18n strings.
|
|
17
17
|
* @return { Function } A redux thunk.
|
|
18
18
|
*/
|
|
19
19
|
const grantCameraPermissions = (options = {}) => dispatch => {
|
|
20
20
|
const {
|
|
21
|
+
permissionId,
|
|
21
22
|
useSettingsModal = false,
|
|
22
|
-
|
|
23
|
+
settingsModal,
|
|
24
|
+
// @deprecated modal setting, to be removed in future major release
|
|
25
|
+
modal,
|
|
26
|
+
...rest
|
|
23
27
|
} = options;
|
|
28
|
+
const settingsModalOptions = settingsModal || modal || {};
|
|
24
29
|
return dispatch(grantPermissions({
|
|
25
30
|
permissionId: PERMISSION_ID_CAMERA,
|
|
26
31
|
useSettingsModal,
|
|
27
|
-
|
|
32
|
+
settingsModal: {
|
|
28
33
|
title: null,
|
|
29
34
|
message: 'permissions.access_denied.camera_message',
|
|
30
35
|
confirm: 'permissions.access_denied.settings_button',
|
|
31
36
|
dismiss: 'common.close',
|
|
32
|
-
...
|
|
33
|
-
}
|
|
37
|
+
...settingsModalOptions
|
|
38
|
+
},
|
|
39
|
+
...rest
|
|
34
40
|
}));
|
|
35
41
|
};
|
|
36
42
|
export default grantCameraPermissions;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { UnknownAction } from 'redux';
|
|
2
|
+
import type { ThunkAction } from 'redux-thunk';
|
|
3
|
+
|
|
4
|
+
import type { GrantPermissionsOptions, GrantPermissionsResult } from './grantPermissions';
|
|
5
|
+
|
|
6
|
+
export type GrantGeolocationPermissionsResult = GrantPermissionsResult;
|
|
7
|
+
|
|
8
|
+
type GeolocationOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Whether to request background location access.
|
|
11
|
+
*
|
|
12
|
+
* **Note:** Since background location access will cause extended reviews by Google and Apple,
|
|
13
|
+
* this feature needs to be explicitly enabled by Shopgate.
|
|
14
|
+
*/
|
|
15
|
+
requireBackgroundAccess?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type GeolocationGrantOptions = Omit<GrantPermissionsOptions, 'permissionId'|'permissionOptions'> & GeolocationOptions;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Determines the current state of the geolocation permissions.
|
|
22
|
+
* If not already happened, the user will be prompted to grant permissions.
|
|
23
|
+
*
|
|
24
|
+
* The action returns a promise which resolves with a boolean value, that indicates the state.
|
|
25
|
+
*
|
|
26
|
+
* When the "resolveWithData" option is set to TRUE, the promise will resolve with an object
|
|
27
|
+
* containing the permission status and additional data, instead of a boolean value.
|
|
28
|
+
*/
|
|
29
|
+
declare function grantGeolocationPermissions<State = unknown>(
|
|
30
|
+
options: GeolocationGrantOptions & { resolveWithData: true }
|
|
31
|
+
): ThunkAction<Promise<GrantGeolocationPermissionsResult>, State, unknown, UnknownAction>;
|
|
32
|
+
|
|
33
|
+
declare function grantGeolocationPermissions<State = unknown>(
|
|
34
|
+
options?: GeolocationGrantOptions & { resolveWithData?: false | undefined }
|
|
35
|
+
): ThunkAction<Promise<boolean>, State, unknown, UnknownAction>;
|
|
36
|
+
|
|
37
|
+
export default grantGeolocationPermissions;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PERMISSION_ID_LOCATION } from '@shopgate/engage/core/constants';
|
|
1
|
+
import { PERMISSION_ID_LOCATION, PERMISSION_USAGE_ALWAYS } from '@shopgate/engage/core/constants';
|
|
2
2
|
import { hasWebBridge } from '@shopgate/engage/core';
|
|
3
3
|
import grantPermissions from "./grantPermissions";
|
|
4
4
|
|
|
@@ -7,39 +7,54 @@ import grantPermissions from "./grantPermissions";
|
|
|
7
7
|
* If not already happened, the user will be prompted to grant permissions.
|
|
8
8
|
* The action returns a promise which resolves with a boolean value, that indicates the state.
|
|
9
9
|
* @param {Object} options Action options.
|
|
10
|
+
* @param {boolean} [options.requireBackgroundAccess=false] When set to TRUE, the action will
|
|
11
|
+
* attempt to request background location permissions,
|
|
10
12
|
* @param {boolean} [options.useSettingsModal=false] Whether in case of declined permissions a modal
|
|
11
13
|
* shall be presented, which redirects to the app settings.
|
|
12
14
|
* @param {boolean} [options.requestPermissions=true] When set to TRUE the logic will not
|
|
13
15
|
* attempt to request permissions, if they are not granted or determined yet.
|
|
14
|
-
* @param {boolean} [options.resolveWithData=
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {string} options.
|
|
19
|
-
* @param {string} options.
|
|
20
|
-
* @param {string} options.
|
|
21
|
-
* @param {
|
|
16
|
+
* @param {boolean} [options.resolveWithData=true] When set to TRUE the promise will resolve with
|
|
17
|
+
* an object containing the permission status and whether the opt-in dialog was shown,
|
|
18
|
+
* instead of a boolean value.
|
|
19
|
+
* @param {Object} [options.settingsModal={}] Options for the settings modal.
|
|
20
|
+
* @param {string} options.settingsModal.title Modal title.
|
|
21
|
+
* @param {string} options.settingsModal.message Modal message.
|
|
22
|
+
* @param {string} options.settingsModal.confirm Label for the confirm button.
|
|
23
|
+
* @param {string} options.settingsModal.dismiss Label for the dismiss button.
|
|
24
|
+
* @param {Object} options.settingsModal.params Additional parameters for i18n strings.
|
|
22
25
|
* @return { Function } A redux thunk.
|
|
23
26
|
*/
|
|
24
27
|
const grantGeolocationPermissions = (options = {}) => dispatch => {
|
|
25
28
|
const {
|
|
29
|
+
permissionId,
|
|
26
30
|
useSettingsModal = false,
|
|
27
|
-
|
|
31
|
+
settingsModal,
|
|
32
|
+
// @deprecated modal setting, to be removed in future major release
|
|
33
|
+
modal,
|
|
28
34
|
requestPermissions = true,
|
|
29
|
-
resolveWithData = false
|
|
35
|
+
resolveWithData = false,
|
|
36
|
+
requireBackgroundAccess = false,
|
|
37
|
+
...rest
|
|
30
38
|
} = options;
|
|
39
|
+
const settingsModalOptions = settingsModal || modal || {};
|
|
31
40
|
return dispatch(grantPermissions({
|
|
32
41
|
permissionId: PERMISSION_ID_LOCATION,
|
|
42
|
+
...(requireBackgroundAccess ? {
|
|
43
|
+
permissionOptions: {
|
|
44
|
+
usage: PERMISSION_USAGE_ALWAYS
|
|
45
|
+
}
|
|
46
|
+
} : {}),
|
|
33
47
|
requestPermissions,
|
|
34
48
|
resolveWithData,
|
|
35
49
|
useSettingsModal,
|
|
36
|
-
|
|
50
|
+
settingsModal: {
|
|
37
51
|
title: null,
|
|
38
|
-
message:
|
|
52
|
+
message: `permissions.access_denied.${requireBackgroundAccess ? 'geolocationMessageBackground' : 'geolocation_message'}`,
|
|
39
53
|
confirm: !hasWebBridge() ? 'permissions.access_denied.settings_button' : null,
|
|
40
54
|
dismiss: 'modal.dismiss',
|
|
41
|
-
...
|
|
42
|
-
}
|
|
55
|
+
...settingsModalOptions
|
|
56
|
+
},
|
|
57
|
+
...rest
|
|
43
58
|
}));
|
|
44
59
|
};
|
|
45
60
|
export default grantGeolocationPermissions;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { UnknownAction } from 'redux';
|
|
2
|
+
import type { ThunkAction } from 'redux-thunk';
|
|
3
|
+
|
|
4
|
+
type PermissionStatus =
|
|
5
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_STATUS_NOT_DETERMINED
|
|
6
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_STATUS_DENIED
|
|
7
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_STATUS_GRANTED
|
|
8
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_STATUS_NOT_SUPPORTED
|
|
9
|
+
|
|
10
|
+
type PermissionId =
|
|
11
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_ID_LOCATION
|
|
12
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_ID_BACKGROUND_LOCATION
|
|
13
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_ID_CAMERA
|
|
14
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_ID_PUSH
|
|
15
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_ID_APP_TRACKING_TRANSPARENCY
|
|
16
|
+
|
|
17
|
+
type PermissionUsage =
|
|
18
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_USAGE_ALWAYS
|
|
19
|
+
| typeof import('@shopgate/engage/core/constants').PERMISSION_USAGE_WHEN_IN_USE
|
|
20
|
+
|
|
21
|
+
export interface PermissionMeta {
|
|
22
|
+
permission: 'push' | 'location' | 'backgroundLocation' | 'camera' | 'tracking' ;
|
|
23
|
+
context: string;
|
|
24
|
+
contextCounter?: number;
|
|
25
|
+
usesSoftPushOptIn?: boolean;
|
|
26
|
+
usesSoftTrackingOptIn?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PermissionModalOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Modal title.
|
|
32
|
+
*/
|
|
33
|
+
title?: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Modal message.
|
|
36
|
+
*/
|
|
37
|
+
message?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Label for the confirm button.
|
|
40
|
+
*/
|
|
41
|
+
confirm?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Label for the dismiss button.
|
|
44
|
+
*/
|
|
45
|
+
dismiss?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Additional parameters for i18n strings.
|
|
48
|
+
*/
|
|
49
|
+
params?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface GrantPermissionsOptions {
|
|
53
|
+
/**
|
|
54
|
+
* The id of the permission to request.
|
|
55
|
+
*/
|
|
56
|
+
permissionId: PermissionId;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Additional options for the permission request.
|
|
60
|
+
*/
|
|
61
|
+
permissionOptions?: Record<string, unknown>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether in case of declined permissions a modal shall be presented, which redirects to the app settings.
|
|
65
|
+
* Options for the settings modal can be set via `modal` property.
|
|
66
|
+
* @default false
|
|
67
|
+
*/
|
|
68
|
+
useSettingsModal?: boolean;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether a rationale modal should be shown before requesting the permission.
|
|
72
|
+
* Options for the rationale modal can be set via `rationaleModal` property.
|
|
73
|
+
* @default false
|
|
74
|
+
*/
|
|
75
|
+
useRationaleModal?: boolean;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Options for the rationale modal.
|
|
79
|
+
* @default {}
|
|
80
|
+
*/
|
|
81
|
+
rationaleModal?: PermissionModalOptions;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Options for the settings modal.
|
|
85
|
+
* @default {}
|
|
86
|
+
*/
|
|
87
|
+
settingsModal?: PermissionModalOptions;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Options for the settings modal.
|
|
91
|
+
* @deprecated use `settingsModal` instead
|
|
92
|
+
* @default {}
|
|
93
|
+
*/
|
|
94
|
+
modal?: PermissionModalOptions;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* If set to TRUE no permissions will be requested if not already granted. Otherwise only
|
|
98
|
+
* the current permission status will be checked without prompting the user.
|
|
99
|
+
* @default false
|
|
100
|
+
*/
|
|
101
|
+
requestPermissions?: boolean;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* When set to TRUE the promise will resolve with an object
|
|
105
|
+
* containing the permission status and additional data, instead of a boolean value.
|
|
106
|
+
* @default false
|
|
107
|
+
*/
|
|
108
|
+
resolveWithData?: boolean;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Meta data used for opt-in tracking actions.
|
|
112
|
+
*/
|
|
113
|
+
meta?: PermissionMeta & Record<string, unknown>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface GrantPermissionsResult {
|
|
117
|
+
/**
|
|
118
|
+
* Whether the permission is granted after the action has been processed.
|
|
119
|
+
*/
|
|
120
|
+
success: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* The current status of the permission after the action has been processed.
|
|
123
|
+
*/
|
|
124
|
+
status?: PermissionStatus;
|
|
125
|
+
/**
|
|
126
|
+
* Whether the user was prompted to grant permissions during the process.
|
|
127
|
+
*/
|
|
128
|
+
optInRequested: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Additional options for the permission, e.g. usage (forwarded from native).
|
|
131
|
+
*/
|
|
132
|
+
options?: {
|
|
133
|
+
usage: PermissionUsage;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Might be set when PWA runs in browser mode. In that cause it can contain resolved
|
|
137
|
+
* location data.
|
|
138
|
+
*/
|
|
139
|
+
data?: unknown;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Determines the current state of a specific permission for an app feature. If not already
|
|
144
|
+
* happened, the user will be prompted to grant permissions.
|
|
145
|
+
*
|
|
146
|
+
* The action returns a promise which resolves with a boolean value, that indicates the state.
|
|
147
|
+
*
|
|
148
|
+
* When the "resolveWithData" option is set to TRUE, the promise will resolve with an object
|
|
149
|
+
* containing the permission status and additional data, instead of a boolean value.
|
|
150
|
+
*/
|
|
151
|
+
declare function grantPermissions<State = unknown>(
|
|
152
|
+
options: GrantPermissionsOptions
|
|
153
|
+
): ThunkAction<Promise<GrantPermissionsResult|boolean>, State, unknown, UnknownAction>;
|
|
154
|
+
|
|
155
|
+
export default grantPermissions;
|
|
@@ -1,53 +1,78 @@
|
|
|
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 {
|
|
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.
|
|
23
34
|
* @param {string} options.rationaleModal.confirm Label for the confirm button.
|
|
24
35
|
* @param {string} options.rationaleModal.dismiss Label for the dismiss button.
|
|
25
36
|
* @param {Object} options.rationaleModal.params Additional parameters for i18n strings.
|
|
26
|
-
* @param {Object} [options.
|
|
27
|
-
* @param {string} options.
|
|
28
|
-
* @param {string} options.
|
|
29
|
-
* @param {string} options.
|
|
30
|
-
* @param {string} options.
|
|
31
|
-
* @param {Object} options.
|
|
37
|
+
* @param {Object} [options.settingsModal={}] Options for the settings modal.
|
|
38
|
+
* @param {string} options.settingsModal.title Modal title.
|
|
39
|
+
* @param {string} options.settingsModal.message Modal message.
|
|
40
|
+
* @param {string} options.settingsModal.confirm Label for the confirm button.
|
|
41
|
+
* @param {string} options.settingsModal.dismiss Label for the dismiss button.
|
|
42
|
+
* @param {Object} options.settingsModal.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=
|
|
35
|
-
*
|
|
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
|
|
51
|
+
const grantPermissions = (options = {}) => (dispatch, getState
|
|
52
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
53
|
+
) => new Promise(async resolve => {
|
|
40
54
|
const {
|
|
41
55
|
permissionId,
|
|
56
|
+
permissionOptions,
|
|
42
57
|
useSettingsModal = false,
|
|
43
58
|
useRationaleModal = false,
|
|
44
59
|
rationaleModal: rationaleModalOptions = {},
|
|
45
|
-
|
|
60
|
+
settingsModal,
|
|
61
|
+
// @deprecated options, to be removed in future major release
|
|
62
|
+
modal,
|
|
46
63
|
requestPermissions = true,
|
|
47
64
|
resolveWithData = false,
|
|
48
65
|
meta = {}
|
|
49
66
|
} = options;
|
|
67
|
+
if (modal) {
|
|
68
|
+
logger.warn('grantPermissions: The "modal" option is deprecated and will be removed in the future. Please use the "settingsModal" option instead.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// In case the deprecated "modal" option is used, we want to use this for the settings modal, to
|
|
72
|
+
const settingsModalOptions = settingsModal || modal || {};
|
|
50
73
|
let dispatchMock;
|
|
74
|
+
let optInRequested = false;
|
|
75
|
+
const isAndroidApp = getIsAndroidApp(getState());
|
|
51
76
|
if (!hasSGJavaScriptBridge() || hasWebBridge()) {
|
|
52
77
|
/**
|
|
53
78
|
* The fallbackStatus will be used at browsers that don't support the permissions API. By
|
|
@@ -60,30 +85,54 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
|
|
|
60
85
|
}
|
|
61
86
|
if (!availablePermissionsIds.includes(permissionId)) {
|
|
62
87
|
logger.error('grandPermissions: %s is no valid permission id', permissionId);
|
|
63
|
-
resolve(
|
|
88
|
+
resolve(resolveWithData ? {
|
|
89
|
+
success: false,
|
|
90
|
+
optInRequested
|
|
91
|
+
} : false);
|
|
64
92
|
return;
|
|
65
93
|
}
|
|
66
94
|
let status;
|
|
95
|
+
let appPermissionOptions;
|
|
67
96
|
let data;
|
|
68
97
|
|
|
69
98
|
// Check the current status of the camera permissions.
|
|
70
99
|
({
|
|
71
|
-
status
|
|
100
|
+
status,
|
|
101
|
+
options: appPermissionOptions
|
|
72
102
|
} = await dispatch(requestAppPermissionStatus({
|
|
73
103
|
permissionId,
|
|
74
104
|
dispatchMock
|
|
75
105
|
})));
|
|
76
106
|
|
|
107
|
+
// When the location permission is requested for "always" usage and the permissions where already
|
|
108
|
+
// granted for "whenInUse" we need to trigger the permission request again to get extended
|
|
109
|
+
// permissions.
|
|
110
|
+
const upgradeLocationPermission = permissionId === PERMISSION_ID_LOCATION && permissionOptions?.usage === PERMISSION_USAGE_ALWAYS && status === PERMISSION_STATUS_GRANTED;
|
|
111
|
+
|
|
77
112
|
// Stop the process when the permission type is not supported.
|
|
78
113
|
if (status === PERMISSION_STATUS_NOT_SUPPORTED) {
|
|
79
|
-
resolve(
|
|
114
|
+
resolve(resolveWithData ? {
|
|
115
|
+
success: false,
|
|
116
|
+
optInRequested,
|
|
117
|
+
status,
|
|
118
|
+
...(appPermissionOptions ? {
|
|
119
|
+
options: appPermissionOptions
|
|
120
|
+
} : {})
|
|
121
|
+
} : false);
|
|
80
122
|
return;
|
|
81
123
|
}
|
|
82
124
|
|
|
83
125
|
// The user never seen the permissions dialog yet, or temporary denied the permissions (Android).
|
|
84
|
-
if (status === PERMISSION_STATUS_NOT_DETERMINED) {
|
|
126
|
+
if (status === PERMISSION_STATUS_NOT_DETERMINED || upgradeLocationPermission) {
|
|
85
127
|
if (!requestPermissions) {
|
|
86
|
-
resolve(
|
|
128
|
+
resolve(resolveWithData ? {
|
|
129
|
+
success: false,
|
|
130
|
+
optInRequested,
|
|
131
|
+
status,
|
|
132
|
+
...(appPermissionOptions ? {
|
|
133
|
+
options: appPermissionOptions
|
|
134
|
+
} : {})
|
|
135
|
+
} : false);
|
|
87
136
|
return;
|
|
88
137
|
}
|
|
89
138
|
if (useRationaleModal) {
|
|
@@ -101,7 +150,14 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
|
|
|
101
150
|
meta
|
|
102
151
|
}));
|
|
103
152
|
if (requestAllowed === false) {
|
|
104
|
-
resolve(
|
|
153
|
+
resolve(resolveWithData ? {
|
|
154
|
+
success: false,
|
|
155
|
+
optInRequested,
|
|
156
|
+
status,
|
|
157
|
+
...(appPermissionOptions ? {
|
|
158
|
+
options: appPermissionOptions
|
|
159
|
+
} : {})
|
|
160
|
+
} : false);
|
|
105
161
|
return;
|
|
106
162
|
}
|
|
107
163
|
}
|
|
@@ -109,15 +165,42 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
|
|
|
109
165
|
permissionId,
|
|
110
166
|
meta
|
|
111
167
|
}));
|
|
168
|
+
const tsBeforeRequest = Date.now();
|
|
169
|
+
let nativeRequestDuration;
|
|
112
170
|
|
|
113
171
|
// Trigger the native permissions dialog.
|
|
114
172
|
({
|
|
115
173
|
status,
|
|
116
|
-
data
|
|
174
|
+
data,
|
|
175
|
+
options: appPermissionOptions,
|
|
176
|
+
duration: nativeRequestDuration
|
|
117
177
|
} = await dispatch(requestAppPermission({
|
|
118
178
|
permissionId,
|
|
119
|
-
dispatchMock
|
|
179
|
+
dispatchMock,
|
|
180
|
+
...(permissionOptions ? {
|
|
181
|
+
options: permissionOptions
|
|
182
|
+
} : {})
|
|
120
183
|
})));
|
|
184
|
+
let wasUserInteraction = true;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* On iOS it's not possible to get "notDetermined" status for "always" location permissions.
|
|
188
|
+
* So we might run into this decision branch of the code for when "always" permissions are
|
|
189
|
+
* requested, even if we don't know if a dialog will be shown.
|
|
190
|
+
* We can only guess that the user interacted with the dialog and if we might need to show
|
|
191
|
+
* the settings modal.
|
|
192
|
+
*
|
|
193
|
+
* Additionally, at some permissions requests on Android we can't prevent that the
|
|
194
|
+
* getPermissions request returns "notDetermined" since there is an user option to ask for
|
|
195
|
+
* permission acceptance every time its needed. So we also need to try a user interaction guess.
|
|
196
|
+
*/
|
|
197
|
+
if (upgradeLocationPermission || isAndroidApp && ANDROID_PERMISSIONS_WITH_USER_INTERACTION_CHECK.includes(permissionId)) {
|
|
198
|
+
// When available, use the duration of the native request as heuristic for user interaction.
|
|
199
|
+
// It's more accurate, since it won't include the time that communication with the app takes.
|
|
200
|
+
const requestDuration = typeof nativeRequestDuration === 'number' ? nativeRequestDuration : Date.now() - tsBeforeRequest;
|
|
201
|
+
wasUserInteraction = requestDuration > 1000;
|
|
202
|
+
}
|
|
203
|
+
optInRequested = wasUserInteraction;
|
|
121
204
|
dispatch(hardOptInSelected({
|
|
122
205
|
permissionId,
|
|
123
206
|
status,
|
|
@@ -126,34 +209,67 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
|
|
|
126
209
|
|
|
127
210
|
// The user denied the permissions within the native dialog.
|
|
128
211
|
if ([PERMISSION_STATUS_DENIED, PERMISSION_STATUS_NOT_DETERMINED].includes(status)) {
|
|
129
|
-
|
|
130
|
-
|
|
212
|
+
if (wasUserInteraction) {
|
|
213
|
+
resolve(resolveWithData ? {
|
|
214
|
+
success: false,
|
|
215
|
+
optInRequested,
|
|
216
|
+
status,
|
|
217
|
+
...(appPermissionOptions ? {
|
|
218
|
+
options: appPermissionOptions
|
|
219
|
+
} : {})
|
|
220
|
+
} : false);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
131
223
|
}
|
|
132
224
|
}
|
|
133
225
|
if (status === PERMISSION_STATUS_GRANTED) {
|
|
134
|
-
resolve(resolveWithData
|
|
226
|
+
resolve(resolveWithData ? {
|
|
227
|
+
success: true,
|
|
228
|
+
optInRequested,
|
|
229
|
+
status,
|
|
230
|
+
...(appPermissionOptions ? {
|
|
231
|
+
options: appPermissionOptions
|
|
232
|
+
} : {}),
|
|
233
|
+
...(data !== undefined ? {
|
|
234
|
+
data
|
|
235
|
+
} : {})
|
|
236
|
+
} : true);
|
|
135
237
|
return;
|
|
136
238
|
}
|
|
137
239
|
|
|
138
240
|
// The user permanently denied the permissions before.
|
|
139
241
|
if (status === PERMISSION_STATUS_DENIED) {
|
|
140
242
|
if (!useSettingsModal) {
|
|
141
|
-
resolve(
|
|
243
|
+
resolve(resolveWithData ? {
|
|
244
|
+
success: false,
|
|
245
|
+
optInRequested,
|
|
246
|
+
status,
|
|
247
|
+
...(appPermissionOptions ? {
|
|
248
|
+
options: appPermissionOptions
|
|
249
|
+
} : {})
|
|
250
|
+
} : false);
|
|
142
251
|
return;
|
|
143
252
|
}
|
|
144
253
|
|
|
145
254
|
// Present a modal that describes the situation, and allows the user to enter the app settings.
|
|
146
255
|
const openSettings = await dispatch(showModal({
|
|
147
|
-
title:
|
|
148
|
-
message:
|
|
149
|
-
confirm:
|
|
150
|
-
dismiss:
|
|
151
|
-
params:
|
|
256
|
+
title: settingsModalOptions.title || null,
|
|
257
|
+
message: settingsModalOptions.message,
|
|
258
|
+
confirm: settingsModalOptions.confirm ?? 'permissions.access_denied.settings_button',
|
|
259
|
+
dismiss: settingsModalOptions.dismiss ?? 'modal.dismiss',
|
|
260
|
+
params: settingsModalOptions.params
|
|
152
261
|
}));
|
|
153
262
|
|
|
154
263
|
// The user just closed the modal.
|
|
155
264
|
if (!openSettings) {
|
|
156
|
-
resolve(
|
|
265
|
+
resolve(resolveWithData ? {
|
|
266
|
+
success: false,
|
|
267
|
+
optInRequested,
|
|
268
|
+
status,
|
|
269
|
+
...(appPermissionOptions ? {
|
|
270
|
+
options: appPermissionOptions
|
|
271
|
+
} : {})
|
|
272
|
+
} : false);
|
|
157
273
|
return;
|
|
158
274
|
}
|
|
159
275
|
|
|
@@ -167,7 +283,14 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
|
|
|
167
283
|
} = await dispatch(requestAppPermissionStatus({
|
|
168
284
|
permissionId
|
|
169
285
|
})));
|
|
170
|
-
resolve(
|
|
286
|
+
resolve(resolveWithData ? {
|
|
287
|
+
success: status === PERMISSION_STATUS_GRANTED,
|
|
288
|
+
optInRequested,
|
|
289
|
+
status,
|
|
290
|
+
...(appPermissionOptions ? {
|
|
291
|
+
options: appPermissionOptions
|
|
292
|
+
} : {})
|
|
293
|
+
} : status === PERMISSION_STATUS_GRANTED);
|
|
171
294
|
};
|
|
172
295
|
|
|
173
296
|
/**
|
|
@@ -180,6 +303,15 @@ const grantPermissions = (options = {}) => dispatch => new Promise(async resolve
|
|
|
180
303
|
setTimeout(() => {
|
|
181
304
|
openAppSettings();
|
|
182
305
|
}, 0);
|
|
306
|
+
return;
|
|
183
307
|
}
|
|
308
|
+
resolve(resolveWithData ? {
|
|
309
|
+
success: false,
|
|
310
|
+
optInRequested,
|
|
311
|
+
status,
|
|
312
|
+
...(appPermissionOptions ? {
|
|
313
|
+
options: appPermissionOptions
|
|
314
|
+
} : {})
|
|
315
|
+
} : false);
|
|
184
316
|
});
|
|
185
317
|
export default grantPermissions;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { UnknownAction } from 'redux';
|
|
2
|
+
import type { ThunkAction } from 'redux-thunk';
|
|
3
|
+
|
|
4
|
+
import type { GrantPermissionsOptions, GrantPermissionsResult } from './grantPermissions';
|
|
5
|
+
|
|
6
|
+
export type GrantPushPermissionsResult = GrantPermissionsResult;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Determines the current state of the push permissions.
|
|
10
|
+
* If not already happened, the user will be prompted to grant permissions.
|
|
11
|
+
*
|
|
12
|
+
* The action returns a promise which resolves with a boolean value, that indicates the state.
|
|
13
|
+
*
|
|
14
|
+
* When the "resolveWithData" option is set to TRUE, the promise will resolve with an object
|
|
15
|
+
* containing the permission status and additional data, instead of a boolean value.
|
|
16
|
+
*/
|
|
17
|
+
declare function grantPushPermissions<State = unknown>(
|
|
18
|
+
options: Omit<GrantPermissionsOptions, 'permissionId'|'permissionOptions'> & { resolveWithData: true }
|
|
19
|
+
): ThunkAction<Promise<GrantPushPermissionsResult>, State, unknown, UnknownAction>;
|
|
20
|
+
|
|
21
|
+
declare function grantPushPermissions<State = unknown>(
|
|
22
|
+
options?: Omit<GrantPermissionsOptions, 'permissionId'|'permissionOptions'> & { resolveWithData?: false | undefined }
|
|
23
|
+
): ThunkAction<Promise<boolean>, State, unknown, UnknownAction>;
|
|
24
|
+
|
|
25
|
+
export default grantPushPermissions;
|
|
@@ -15,35 +15,43 @@ import grantPermissions from "./grantPermissions";
|
|
|
15
15
|
* @param {string} options.rationaleModal.confirm Label for the confirm button.
|
|
16
16
|
* @param {string} options.rationaleModal.dismiss Label for the dismiss button.
|
|
17
17
|
* @param {Object} options.rationaleModal.params Additional parameters for i18n strings.
|
|
18
|
-
* @param {Object} [options.
|
|
19
|
-
* @param {string} options.
|
|
20
|
-
* @param {string} options.
|
|
21
|
-
* @param {string} options.
|
|
22
|
-
* @param {string} options.
|
|
23
|
-
* @param {Object} options.
|
|
18
|
+
* @param {Object} [options.settingsModal={}] Options for the settings modal.
|
|
19
|
+
* @param {string} options.settingsModal.title Modal title.
|
|
20
|
+
* @param {string} options.settingsModal.message Modal message.
|
|
21
|
+
* @param {string} options.settingsModal.confirm Label for the confirm button.
|
|
22
|
+
* @param {string} options.settingsModal.dismiss Label for the dismiss button.
|
|
23
|
+
* @param {Object} options.settingsModal.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
|
+
settingsModal = {},
|
|
32
36
|
rationaleModal = {},
|
|
33
|
-
meta = {}
|
|
37
|
+
meta = {},
|
|
38
|
+
...rest
|
|
34
39
|
} = options;
|
|
40
|
+
const settingsModalOptions = settingsModal || {};
|
|
35
41
|
return dispatch(grantPermissions({
|
|
36
42
|
permissionId: PERMISSION_ID_PUSH,
|
|
37
43
|
useSettingsModal,
|
|
38
44
|
useRationaleModal,
|
|
39
|
-
|
|
45
|
+
settingsModal: {
|
|
40
46
|
title: null,
|
|
41
47
|
message: 'permissions.access_denied.push_message',
|
|
42
48
|
confirm: 'permissions.access_denied.settings_button',
|
|
43
|
-
|
|
49
|
+
dismiss: 'modal.dismiss',
|
|
50
|
+
...settingsModalOptions
|
|
44
51
|
},
|
|
45
52
|
rationaleModal,
|
|
46
|
-
meta
|
|
53
|
+
meta,
|
|
54
|
+
...rest
|
|
47
55
|
}));
|
|
48
56
|
};
|
|
49
57
|
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
|
-
|
|
49
|
+
options,
|
|
50
|
+
data,
|
|
51
|
+
duration
|
|
43
52
|
};
|
|
44
53
|
};
|
|
45
54
|
export default requestAppPermission;
|
package/core/constants/index.js
CHANGED
|
@@ -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];
|
|
@@ -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,26 @@ 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
|
+
showLoading: false
|
|
37
|
+
});
|
|
38
|
+
redirects.set(PERMISSION_REQUEST_ROUTE_LOCATION_BACKGROUND, permissionRouteRedirectHandler, {
|
|
39
|
+
showLoading: false
|
|
40
|
+
});
|
|
41
|
+
redirects.set(PERMISSION_REQUEST_ROUTE_PUSH, permissionRouteRedirectHandler, {
|
|
42
|
+
showLoading: false
|
|
43
|
+
});
|
|
44
|
+
redirects.set(PERMISSION_REQUEST_ROUTE_TRACKING, permissionRouteRedirectHandler, {
|
|
45
|
+
showLoading: false
|
|
46
|
+
});
|
|
47
|
+
redirects.set(PERMISSION_REQUEST_ROUTE_CAMERA, permissionRouteRedirectHandler, {
|
|
48
|
+
showLoading: false
|
|
49
|
+
});
|
|
50
|
+
redirects.set(PERMISSION_REQUEST_ROUTE_CAMERA, permissionRouteRedirectHandler, {
|
|
51
|
+
showLoading: false
|
|
52
|
+
});
|
|
32
53
|
});
|
|
33
54
|
subscribe(reloadApp$, () => {
|
|
34
55
|
// A deployed app can only be reloaded on the base url. So we first set this as current route
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopgate/engage",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.2-beta.2",
|
|
4
4
|
"description": "Shopgate's ENGAGE library.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Shopgate <support@shopgate.com>",
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@emotion/react": "^11.14.0",
|
|
19
19
|
"@shopgate/native-modules": "1.0.0-beta.25",
|
|
20
|
-
"@shopgate/pwa-common": "7.30.
|
|
21
|
-
"@shopgate/pwa-common-commerce": "7.30.
|
|
22
|
-
"@shopgate/pwa-core": "7.30.
|
|
23
|
-
"@shopgate/pwa-ui-ios": "7.30.
|
|
24
|
-
"@shopgate/pwa-ui-material": "7.30.
|
|
25
|
-
"@shopgate/pwa-ui-shared": "7.30.
|
|
20
|
+
"@shopgate/pwa-common": "7.30.2-beta.2",
|
|
21
|
+
"@shopgate/pwa-common-commerce": "7.30.2-beta.2",
|
|
22
|
+
"@shopgate/pwa-core": "7.30.2-beta.2",
|
|
23
|
+
"@shopgate/pwa-ui-ios": "7.30.2-beta.2",
|
|
24
|
+
"@shopgate/pwa-ui-material": "7.30.2-beta.2",
|
|
25
|
+
"@shopgate/pwa-ui-shared": "7.30.2-beta.2",
|
|
26
26
|
"@stripe/react-stripe-js": "^1.16.5",
|
|
27
27
|
"@stripe/stripe-js": "^1.44.1",
|
|
28
28
|
"@virtuous/conductor": "~2.5.0",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"lodash": "^4.17.23",
|
|
50
50
|
"prop-types": "~15.8.1",
|
|
51
51
|
"react": "^17.0.2",
|
|
52
|
+
"react-dom": "^17.0.2",
|
|
52
53
|
"react-redux": "^8.1.3",
|
|
53
54
|
"redux": "^5.0.1",
|
|
54
55
|
"redux-mock-store": "^1.5.4",
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { appDidStart$ } from '@shopgate/engage/core/streams';
|
|
2
|
-
import {
|
|
3
|
-
import { grantAppTrackingTransparencyPermission, historyReset, historyPop } from '@shopgate/engage/core/actions';
|
|
2
|
+
import { historyReset, historyPop } from '@shopgate/engage/core/actions';
|
|
4
3
|
import { softOptInShown } from '@shopgate/engage/core/action-creators';
|
|
5
4
|
import { handleCookieConsent, showCookieConsentModal, hideCookieConsentModal } from "../action-creators";
|
|
6
|
-
import { getIsCookieConsentHandled, getAreComfortCookiesAccepted, getAreStatisticsCookiesAccepted, getCookieConsentTrackingMeta } from "../selectors/cookieConsent";
|
|
5
|
+
import { getIsCookieConsentHandled, getAreComfortCookiesAccepted, getAreStatisticsCookiesAccepted, getCookieConsentTrackingMeta, getIsCookieConsentActivated } from "../selectors/cookieConsent";
|
|
7
6
|
import { cookieConsentInitializedByUserInternal$, privacySettingsConfirmedWithoutChangeInternal$, cookieConsentModalShouldToggleInternal$ } from "../streams/cookieConsent";
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -31,12 +30,7 @@ export default function cookieConsent(subscribe) {
|
|
|
31
30
|
comfortCookiesAccepted,
|
|
32
31
|
statisticsCookiesAccepted
|
|
33
32
|
}));
|
|
34
|
-
|
|
35
|
-
await dispatch(grantAppTrackingTransparencyPermission({
|
|
36
|
-
meta: trackingMeta
|
|
37
|
-
}));
|
|
38
|
-
}
|
|
39
|
-
} else {
|
|
33
|
+
} else if (getIsCookieConsentActivated(state)) {
|
|
40
34
|
// if merchant has activated cookie feature but user has not chosen cookies yet:
|
|
41
35
|
// show cookie consent modal to make user choose them
|
|
42
36
|
dispatch(softOptInShown({
|