@shopgate/engage 7.31.7 → 7.31.8
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopgate/engage",
|
|
3
|
-
"version": "7.31.
|
|
3
|
+
"version": "7.31.8",
|
|
4
4
|
"description": "Shopgate's ENGAGE library.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Shopgate <support@shopgate.com>",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@emotion/react": "^11.14.0",
|
|
20
20
|
"@shopgate/native-modules": "1.0.0-beta.34",
|
|
21
|
-
"@shopgate/pwa-common": "7.31.
|
|
22
|
-
"@shopgate/pwa-common-commerce": "7.31.
|
|
23
|
-
"@shopgate/pwa-core": "7.31.
|
|
24
|
-
"@shopgate/pwa-ui-ios": "7.31.
|
|
25
|
-
"@shopgate/pwa-ui-material": "7.31.
|
|
26
|
-
"@shopgate/pwa-ui-shared": "7.31.
|
|
21
|
+
"@shopgate/pwa-common": "7.31.8",
|
|
22
|
+
"@shopgate/pwa-common-commerce": "7.31.8",
|
|
23
|
+
"@shopgate/pwa-core": "7.31.8",
|
|
24
|
+
"@shopgate/pwa-ui-ios": "7.31.8",
|
|
25
|
+
"@shopgate/pwa-ui-material": "7.31.8",
|
|
26
|
+
"@shopgate/pwa-ui-shared": "7.31.8",
|
|
27
27
|
"@stripe/react-stripe-js": "^1.16.5",
|
|
28
28
|
"@stripe/stripe-js": "^1.44.1",
|
|
29
29
|
"@virtuous/conductor": "~2.5.0",
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* List of allowed
|
|
2
|
+
* List of allowed origin patterns for cms page preview iFrame communication. Both directions are
|
|
3
|
+
* validated against these patterns: incoming messages are only processed when their origin matches,
|
|
4
|
+
* and outgoing messages are only posted to a matching origin - either the origin of the last
|
|
5
|
+
* accepted incoming message, or the origin of the embedding document.
|
|
6
|
+
*
|
|
7
|
+
* A "*" acts as a wildcard for one or more domain labels, so "https://*.shopgate.com" matches
|
|
8
|
+
* "https://app.shopgate.com" as well as "https://next.us.admin.shopgate.com", but neither
|
|
9
|
+
* "https://shopgate.com" nor "https://evil.shopgate.com.attacker.example". Every other character
|
|
10
|
+
* is matched literally, which means that scheme and port always have to match exactly.
|
|
3
11
|
*/
|
|
4
|
-
export const ALLOWED_PAGE_PREVIEW_ORIGINS = ['https
|
|
12
|
+
export const ALLOWED_PAGE_PREVIEW_ORIGINS = ['https://*.shopgate.com', 'https://*.shopgatedev.com', 'https://*.shopgatepg.com', 'http://localhost.localdev.cc', 'http://localhost:1337'];
|
|
5
13
|
|
|
6
14
|
// Whether to consider vertical margins when calculating the overlay position.
|
|
7
15
|
export const CONSIDER_CONTAINER_MARGINS_ON_SCROLL_DEFAULT = false;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import "core-js/modules/es.array.includes.js";
|
|
1
2
|
import "core-js/modules/es.string.replace.js";
|
|
3
|
+
import "core-js/modules/web.url.js";
|
|
4
|
+
import "core-js/modules/web.url.to-json.js";
|
|
5
|
+
import "core-js/modules/web.url-search-params.js";
|
|
2
6
|
import { PAGE_PREVIEW_PATTERN } from '@shopgate/engage/page/constants';
|
|
3
7
|
|
|
4
8
|
/**
|
|
@@ -50,4 +54,62 @@ export function checkScheduled({
|
|
|
50
54
|
isActive,
|
|
51
55
|
isExpired
|
|
52
56
|
};
|
|
53
|
-
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Regular expression source that a "*" within an allowed origin pattern is replaced with. It
|
|
61
|
+
* matches one or more domain labels, but never a dot at the very end, so that the suffix of the
|
|
62
|
+
* pattern stays anchored to the end of the origin.
|
|
63
|
+
*/
|
|
64
|
+
const ORIGIN_WILDCARD_SOURCE = '(?:[a-zA-Z0-9-]+\\.)*[a-zA-Z0-9-]+';
|
|
65
|
+
|
|
66
|
+
// Cache for regular expressions that were already created from an origin pattern.
|
|
67
|
+
const originPatternCache = new Map();
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Converts an allowed origin pattern into an anchored regular expression. Every character except
|
|
71
|
+
* the "*" wildcard is matched literally.
|
|
72
|
+
* @param {string} pattern The origin pattern e.g. "https://*.shopgate.com".
|
|
73
|
+
* @returns {RegExp} The regular expression for the pattern.
|
|
74
|
+
*/
|
|
75
|
+
const createOriginRegExp = pattern => {
|
|
76
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
77
|
+
return new RegExp(`^${escaped.split('\\*').join(ORIGIN_WILDCARD_SOURCE)}$`);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Checks if an origin is covered by a list of allowed origin patterns.
|
|
82
|
+
* @param {string} origin The origin to check e.g. "https://app.shopgate.com".
|
|
83
|
+
* @param {Array<string>} [patterns] The allowed origin patterns.
|
|
84
|
+
* @returns {boolean} Whether the origin is allowed.
|
|
85
|
+
*/
|
|
86
|
+
export const isAllowedOrigin = (origin, patterns = []) => {
|
|
87
|
+
// Opaque origins are serialized as "null" and must never be trusted.
|
|
88
|
+
if (typeof origin !== 'string' || origin === '' || origin === 'null') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return patterns.some(pattern => {
|
|
92
|
+
if (typeof pattern !== 'string' || pattern === '') {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (!pattern.includes('*')) {
|
|
96
|
+
return pattern === origin;
|
|
97
|
+
}
|
|
98
|
+
if (!originPatternCache.has(pattern)) {
|
|
99
|
+
originPatternCache.set(pattern, createOriginRegExp(pattern));
|
|
100
|
+
}
|
|
101
|
+
return originPatternCache.get(pattern).test(origin);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Determines the origin of the document that embeds the current page.
|
|
107
|
+
* @returns {string|null} The referrer origin, or null when it can't be determined.
|
|
108
|
+
*/
|
|
109
|
+
export const getReferrerOrigin = () => {
|
|
110
|
+
try {
|
|
111
|
+
return new URL(document.referrer).origin;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import "core-js/modules/es.array.includes.js";
|
|
2
|
-
import "core-js/modules/web.url.js";
|
|
3
|
-
import "core-js/modules/web.url.to-json.js";
|
|
4
|
-
import "core-js/modules/web.url-search-params.js";
|
|
5
1
|
import { useEffect, useCallback, useRef, useContext, useMemo } from 'react';
|
|
6
2
|
import { logger } from '@shopgate/engage/core/helpers';
|
|
7
3
|
import { useDispatch } from 'react-redux';
|
|
@@ -9,7 +5,7 @@ import { useRoute } from '@shopgate/engage/core/hooks';
|
|
|
9
5
|
import { receivePageConfigV2 } from '@shopgate/engage/page/action-creators';
|
|
10
6
|
import { PAGE_PREVIEW_SLUG } from '@shopgate/engage/page/constants';
|
|
11
7
|
import { ALLOWED_PAGE_PREVIEW_ORIGINS, CONSIDER_CONTAINER_MARGINS_ON_SCROLL_DEFAULT } from "./constants";
|
|
12
|
-
import { getScrollContainer } from "./helpers";
|
|
8
|
+
import { getScrollContainer, isAllowedOrigin, getReferrerOrigin } from "./helpers";
|
|
13
9
|
import { WidgetsPreviewContext } from "./WidgetsPreviewContext";
|
|
14
10
|
import { dispatchWidgetPreviewEvent, useWidgetPreviewEvent } from "./events";
|
|
15
11
|
|
|
@@ -30,22 +26,26 @@ import { dispatchWidgetPreviewEvent, useWidgetPreviewEvent } from "./events";
|
|
|
30
26
|
/**
|
|
31
27
|
* Hook for postMessage communication when your component is inside an iframe.
|
|
32
28
|
*
|
|
33
|
-
* Listens on window for "message" events
|
|
34
|
-
*
|
|
29
|
+
* Listens on window for "message" events, and only calls onMessage(data, rawEvent) if the origin
|
|
30
|
+
* of the event is covered by parentOrigins and the message actually came from window.parent.
|
|
35
31
|
*
|
|
36
32
|
* @param {function(MessageData, any): void} onMessage
|
|
37
33
|
* Callback invoked when a trusted message arrives. Receives data and the
|
|
38
34
|
* raw event (so you can inspect origin, source, etc.).
|
|
39
35
|
* @param {string[]} parentOrigins
|
|
40
|
-
* Array of allowed parent origin
|
|
41
|
-
* ['https://a.example.com','https
|
|
36
|
+
* Array of allowed parent origin patterns (e.g.
|
|
37
|
+
* ['https://a.example.com','https://*.example.com']). See ALLOWED_PAGE_PREVIEW_ORIGINS for
|
|
38
|
+
* details about the supported wildcard syntax.
|
|
39
|
+
* @param {boolean} [enabled]
|
|
40
|
+
* Whether the message listener is supposed to be attached.
|
|
42
41
|
* @returns {IframeMessengerResult}
|
|
43
42
|
* An object with a single method:
|
|
44
43
|
* • sendToParent(data, [targetOrigin]): void
|
|
45
44
|
* – Posts data up to window.parent. By default it uses the most recently
|
|
46
|
-
* seen origin (from an incoming message)
|
|
45
|
+
* seen origin (from an incoming message), otherwise the origin of the embedding document.
|
|
46
|
+
* Nothing is sent when neither of them is covered by parentOrigins.
|
|
47
47
|
*/
|
|
48
|
-
function useIframeMessenger(onMessage, parentOrigins) {
|
|
48
|
+
function useIframeMessenger(onMessage, parentOrigins, enabled = true) {
|
|
49
49
|
// Keep a ref to the latest onMessage callback so the listener always has it.
|
|
50
50
|
const onMessageRef = useRef(onMessage);
|
|
51
51
|
useEffect(() => {
|
|
@@ -59,15 +59,15 @@ function useIframeMessenger(onMessage, parentOrigins) {
|
|
|
59
59
|
* Send a message up to the parent window.
|
|
60
60
|
* @param {MessageData} data - The data object to post.
|
|
61
61
|
* @param {string} [targetOrigin]
|
|
62
|
-
* Optional override for the origin to post to. Must be
|
|
63
|
-
*
|
|
64
|
-
* or parentOrigins[0], or "*" if array is empty.
|
|
62
|
+
* Optional override for the origin to post to. Must be covered by parentOrigins. If omitted,
|
|
63
|
+
* the last seen origin (lastOriginRef) or the origin of the embedding document is used.
|
|
65
64
|
*/
|
|
66
65
|
const sendToParent = useCallback((data, targetOrigin) => {
|
|
67
|
-
// Determine which origin to use: explicit, then last seen, then
|
|
68
|
-
|
|
66
|
+
// Determine which origin to use: explicit, then last seen, then the embedding document.
|
|
67
|
+
// Patterns are no valid postMessage targets, so an unresolved origin aborts the send.
|
|
68
|
+
const originToUse = [targetOrigin, lastOriginRef.current, getReferrerOrigin()].find(origin => isAllowedOrigin(origin, parentOrigins));
|
|
69
69
|
if (!originToUse) {
|
|
70
|
-
logger.warn('useIframeMessenger: no targetOrigin available. ' + 'Provide parentOrigins or pass targetOrigin.');
|
|
70
|
+
logger.warn('useIframeMessenger: no allowed targetOrigin available. ' + 'Provide parentOrigins or pass an allowed targetOrigin.');
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
window.parent.postMessage(data, originToUse);
|
|
@@ -75,13 +75,17 @@ function useIframeMessenger(onMessage, parentOrigins) {
|
|
|
75
75
|
|
|
76
76
|
// Attach / detach the "message" listener.
|
|
77
77
|
useEffect(() => {
|
|
78
|
+
if (!enabled) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
78
82
|
/**
|
|
79
83
|
* Handler for incoming postMessage events.
|
|
80
84
|
* @param {any} rawEvent – The original MessageEvent object.
|
|
81
85
|
*/
|
|
82
86
|
function handler(rawEvent) {
|
|
83
|
-
// Only proceed if the origin is
|
|
84
|
-
if (!
|
|
87
|
+
// Only proceed if the origin is covered by our whitelist.
|
|
88
|
+
if (!isAllowedOrigin(rawEvent.origin, parentOrigins)) return;
|
|
85
89
|
// Ensure the message actually came from window.parent.
|
|
86
90
|
if (rawEvent.source !== window.parent) return;
|
|
87
91
|
|
|
@@ -95,7 +99,7 @@ function useIframeMessenger(onMessage, parentOrigins) {
|
|
|
95
99
|
return () => {
|
|
96
100
|
window.removeEventListener('message', handler);
|
|
97
101
|
};
|
|
98
|
-
}, [
|
|
102
|
+
}, [enabled, parentOrigins]);
|
|
99
103
|
return {
|
|
100
104
|
sendToParent
|
|
101
105
|
};
|
|
@@ -123,6 +127,10 @@ export const usePreviewIframeCommunication = (isActive = false) => {
|
|
|
123
127
|
const {
|
|
124
128
|
sendToParent
|
|
125
129
|
} = useIframeMessenger(data => {
|
|
130
|
+
// Allowed parents can also emit unrelated messages with arbitrary payloads.
|
|
131
|
+
if (typeof data !== 'object' || data === null) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
126
134
|
if (data.type === 'receivePageConfig') {
|
|
127
135
|
// Page preview config received from the parent window.
|
|
128
136
|
dispatch(receivePageConfigV2({
|
|
@@ -194,7 +202,7 @@ export const usePreviewIframeCommunication = (isActive = false) => {
|
|
|
194
202
|
});
|
|
195
203
|
}
|
|
196
204
|
}
|
|
197
|
-
}, ALLOWED_PAGE_PREVIEW_ORIGINS);
|
|
205
|
+
}, ALLOWED_PAGE_PREVIEW_ORIGINS, isActive);
|
|
198
206
|
useWidgetPreviewEvent('widget-clicked', e => {
|
|
199
207
|
if (!isActive) {
|
|
200
208
|
return;
|