@shopgate/pwa-common 7.31.5-beta.1 → 7.31.5-beta.3
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 +3 -3
- package/providers/toast/index.js +39 -46
- package/subscriptions/error.js +27 -49
- package/subscriptions/helpers/pipeline.js +0 -68
- package/providers/toast/spec.js +0 -105
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopgate/pwa-common",
|
|
3
|
-
"version": "7.31.5-beta.
|
|
3
|
+
"version": "7.31.5-beta.3",
|
|
4
4
|
"description": "Common library for the Shopgate Connect PWA.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Shopgate <support@shopgate.com>",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@redux-devtools/extension": "^3.3.0",
|
|
19
19
|
"@sentry/browser": "6.0.1",
|
|
20
|
-
"@shopgate/pwa-benchmark": "7.31.5-beta.
|
|
20
|
+
"@shopgate/pwa-benchmark": "7.31.5-beta.3",
|
|
21
21
|
"@virtuous/conductor": "~2.5.0",
|
|
22
22
|
"@virtuous/react-conductor": "~2.5.0",
|
|
23
23
|
"@virtuous/redux-persister": "1.1.0-beta.7",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"swiper": "12.1.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@shopgate/pwa-core": "7.31.5-beta.
|
|
43
|
+
"@shopgate/pwa-core": "7.31.5-beta.3",
|
|
44
44
|
"@types/lodash": "^4.17.24",
|
|
45
45
|
"@types/react-portal": "^3.0.9",
|
|
46
46
|
"lodash": "^4.17.23",
|
package/providers/toast/index.js
CHANGED
|
@@ -32,41 +32,47 @@ let ToastProvider = /*#__PURE__*/function (_Component) {
|
|
|
32
32
|
if (!toast.message) {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
actionLabel: toast.actionLabel,
|
|
39
|
-
onLongPress: toast.onLongPress,
|
|
40
|
-
message: toast.message,
|
|
41
|
-
messageParams: toast.messageParams,
|
|
42
|
-
duration: toast.duration || duration
|
|
43
|
-
};
|
|
35
|
+
const {
|
|
36
|
+
toasts
|
|
37
|
+
} = _this.state;
|
|
44
38
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
// Check if the toast id already is present.
|
|
40
|
+
const found = toasts.find(({
|
|
41
|
+
id
|
|
42
|
+
}) => toast.id === id);
|
|
43
|
+
|
|
44
|
+
// If found, update the toast with the new data.
|
|
45
|
+
if (found) {
|
|
46
|
+
found.action = toast.action;
|
|
47
|
+
found.actionLabel = toast.actionLabel;
|
|
48
|
+
found.message = toast.message;
|
|
49
|
+
found.messageParams = toast.messageParams;
|
|
50
|
+
found.duration = toast.duration || duration;
|
|
51
|
+
} else {
|
|
52
|
+
toasts.push({
|
|
53
|
+
id: toast.id,
|
|
54
|
+
action: toast.action,
|
|
55
|
+
actionLabel: toast.actionLabel,
|
|
56
|
+
message: toast.message,
|
|
57
|
+
messageParams: toast.messageParams,
|
|
58
|
+
duration: toast.duration || duration
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
_this.setState({
|
|
49
62
|
toasts
|
|
50
|
-
}) => {
|
|
51
|
-
const exists = toasts.some(({
|
|
52
|
-
id
|
|
53
|
-
}) => id === toast.id);
|
|
54
|
-
return {
|
|
55
|
-
toasts: exists ? toasts.map(item => item.id === toast.id ? nextToast : item) : [].concat(toasts, [nextToast])
|
|
56
|
-
};
|
|
57
63
|
});
|
|
58
64
|
};
|
|
59
65
|
/**
|
|
60
66
|
* Removes the first toast from the list.
|
|
61
67
|
*/
|
|
62
68
|
_this.removeToast = () => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
_this.
|
|
69
|
+
const {
|
|
70
|
+
toasts
|
|
71
|
+
} = _this.state;
|
|
72
|
+
toasts.shift();
|
|
73
|
+
_this.setState({
|
|
66
74
|
toasts
|
|
67
|
-
})
|
|
68
|
-
toasts: toasts.slice(1)
|
|
69
|
-
}));
|
|
75
|
+
});
|
|
70
76
|
};
|
|
71
77
|
_this.flushToasts = () => {
|
|
72
78
|
if (_this.state.toasts.length) {
|
|
@@ -84,21 +90,11 @@ let ToastProvider = /*#__PURE__*/function (_Component) {
|
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
87
|
-
*
|
|
93
|
+
* Returns the context value to be passed to consumers.
|
|
94
|
+
* @returns {Object}
|
|
88
95
|
*/
|
|
89
96
|
_inheritsLoose(ToastProvider, _Component);
|
|
90
97
|
var _proto = ToastProvider.prototype;
|
|
91
|
-
_proto.componentWillUnmount = function componentWillUnmount() {
|
|
92
|
-
UIEvents.removeListener(this.constructor.ADD, this.addToast);
|
|
93
|
-
UIEvents.removeListener(this.constructor.FLUSH, this.flushToasts);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Returns the context value to be passed to consumers. The object reference is cached and only
|
|
98
|
-
* rebuilt when `toasts` changes, so consumers (Toaster → SnackBarContainer → SnackBar) don't
|
|
99
|
-
* re-render on every unrelated ToastProvider render.
|
|
100
|
-
* @returns {Object}
|
|
101
|
-
*/;
|
|
102
98
|
/**
|
|
103
99
|
* @returns {JSX}
|
|
104
100
|
*/
|
|
@@ -111,14 +107,11 @@ let ToastProvider = /*#__PURE__*/function (_Component) {
|
|
|
111
107
|
return _createClass(ToastProvider, [{
|
|
112
108
|
key: "provided",
|
|
113
109
|
get: function () {
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
return this.providedCache;
|
|
110
|
+
return {
|
|
111
|
+
addToast: this.addToast,
|
|
112
|
+
removeToast: this.removeToast,
|
|
113
|
+
toasts: this.state.toasts
|
|
114
|
+
};
|
|
122
115
|
}
|
|
123
116
|
}]);
|
|
124
117
|
}(Component);
|
package/subscriptions/error.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import "core-js/modules/es.array.includes.js";
|
|
2
|
+
import after from 'lodash/after';
|
|
3
|
+
import before from 'lodash/before';
|
|
4
|
+
import over from 'lodash/over';
|
|
2
5
|
import { isAvailable } from '@shopgate/native-modules';
|
|
3
6
|
import { init, addBreadcrumb, configureScope, captureException, captureMessage, captureEvent, withScope, Severity as SentrySeverity } from '@sentry/browser';
|
|
4
7
|
import { EBIGAPI, emitter, errorManager, ETIMEOUT, ENETUNREACH, EUNKNOWN, EFAVORITE } from '@shopgate/pwa-core';
|
|
@@ -9,7 +12,7 @@ import {
|
|
|
9
12
|
// eslint-disable-next-line import/no-named-default
|
|
10
13
|
default as appConfig, themeName, pckVersion } from "../helpers/config";
|
|
11
14
|
import { env } from "../helpers/environment";
|
|
12
|
-
import { transformGeneralPipelineError
|
|
15
|
+
import { transformGeneralPipelineError } from "./helpers/pipeline";
|
|
13
16
|
import { historyPop } from "../actions/router";
|
|
14
17
|
import showModal from "../actions/modal/showModal";
|
|
15
18
|
import { getUserData } from "../selectors/user";
|
|
@@ -21,9 +24,6 @@ import { getRouterStack } from "../selectors/router";
|
|
|
21
24
|
import { MODAL_PIPELINE_ERROR } from "../constants/ModalTypes";
|
|
22
25
|
import ToastProvider from "../providers/toast";
|
|
23
26
|
|
|
24
|
-
// Generic, translated fallback shown when a backend error carries no code we can map to a message.
|
|
25
|
-
const GENERIC_ERROR_MESSAGE = 'modal.body_error';
|
|
26
|
-
|
|
27
27
|
/**
|
|
28
28
|
* App errors subscriptions.
|
|
29
29
|
* @param {Function} subscribe The subscribe function.
|
|
@@ -60,34 +60,15 @@ export default subscribe => {
|
|
|
60
60
|
error
|
|
61
61
|
} = action;
|
|
62
62
|
const {
|
|
63
|
+
message,
|
|
63
64
|
code,
|
|
64
65
|
context,
|
|
65
66
|
meta = {}
|
|
66
67
|
} = error;
|
|
67
68
|
const {
|
|
68
|
-
behavior
|
|
69
|
-
message: originalMessage
|
|
69
|
+
behavior
|
|
70
70
|
} = meta;
|
|
71
|
-
|
|
72
|
-
// Never surface a raw backend message to the user: show the extension's own translated
|
|
73
|
-
// message, a code-mapped translated message, or a generic fallback. The resolver is the single
|
|
74
|
-
// source of truth for both the text and whether it is ready-to-display (`displayTranslated`) or
|
|
75
|
-
// a locale key that must still go through I18n.Text.
|
|
76
|
-
const {
|
|
77
|
-
message: displayMessage,
|
|
78
|
-
translated: displayTranslated
|
|
79
|
-
} = getDisplayErrorMessage(error, GENERIC_ERROR_MESSAGE);
|
|
80
|
-
|
|
81
|
-
// Unknown/generic and connection-style backend errors are treated as general platform issues.
|
|
82
|
-
// They have no meaningful, mappable message and are always surfaced as a toast (see below).
|
|
83
|
-
const isConnectionError = displayMessage === 'error.general' || [ETIMEOUT, ENETUNREACH].includes(code);
|
|
84
|
-
const isGeneralError = isConnectionError || displayMessage === GENERIC_ERROR_MESSAGE;
|
|
85
|
-
|
|
86
|
-
// Some pipeline actions (e.g. fetchCategory) register an error behavior for *every* error via
|
|
87
|
-
// `setResponseBehavior`. Those behaviors are only meaningful for the specific errors they
|
|
88
|
-
// expect; when the platform fails with an unexpected/unmappable message we skip the behavior
|
|
89
|
-
// and fall through to the generic toast handling instead of, for example, showing a modal.
|
|
90
|
-
if (behavior && !isGeneralError) {
|
|
71
|
+
if (behavior) {
|
|
91
72
|
behavior({
|
|
92
73
|
dispatch,
|
|
93
74
|
getState,
|
|
@@ -97,41 +78,38 @@ export default subscribe => {
|
|
|
97
78
|
return;
|
|
98
79
|
}
|
|
99
80
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
* developer detail view (pipeline, code, raw message, params) — used for the toast long-press.
|
|
103
|
-
* @param {boolean} [openWithDetails=false] Whether to open the modal in developer detail mode.
|
|
104
|
-
*/
|
|
105
|
-
const showModalError = (openWithDetails = false) => {
|
|
81
|
+
/** Show modal thunk */
|
|
82
|
+
const showModalError = () => {
|
|
106
83
|
dispatch(showModal({
|
|
107
84
|
confirm: 'modal.ok',
|
|
108
85
|
dismiss: null,
|
|
109
86
|
title: null,
|
|
110
|
-
message
|
|
87
|
+
message,
|
|
111
88
|
type: MODAL_PIPELINE_ERROR,
|
|
112
89
|
params: {
|
|
113
90
|
pipeline: context,
|
|
114
91
|
request: meta.input,
|
|
115
|
-
message:
|
|
116
|
-
code
|
|
117
|
-
translated: displayTranslated,
|
|
118
|
-
messageParams: meta.additionalParams,
|
|
119
|
-
openWithDetails
|
|
92
|
+
message: meta.message,
|
|
93
|
+
code
|
|
120
94
|
}
|
|
121
95
|
}));
|
|
122
96
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
97
|
+
let shouldShowToast = message === 'error.general';
|
|
98
|
+
if ([ETIMEOUT, ENETUNREACH].includes(code) && message === 'modal.body_error') {
|
|
99
|
+
shouldShowToast = true;
|
|
100
|
+
}
|
|
101
|
+
// It was transformed general error. let it popup after 10 toast clicks
|
|
102
|
+
if (shouldShowToast) {
|
|
103
|
+
const showToastAfter = after(9, showModalError);
|
|
104
|
+
// Recursively show same toast message until clicked 10 times
|
|
105
|
+
const showToast = before(10, () => {
|
|
106
|
+
events.emit(ToastProvider.ADD, {
|
|
107
|
+
id: 'pipeline.error',
|
|
108
|
+
message: 'error.general',
|
|
109
|
+
action: over([showToast, showToastAfter])
|
|
110
|
+
});
|
|
134
111
|
});
|
|
112
|
+
showToast();
|
|
135
113
|
return;
|
|
136
114
|
}
|
|
137
115
|
showModalError();
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { i18n } from '@shopgate/engage/core/helpers';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @param {Object} error error
|
|
5
3
|
* @returns {string|*}
|
|
@@ -14,70 +12,4 @@ export function transformGeneralPipelineError(error) {
|
|
|
14
12
|
return 'error.general';
|
|
15
13
|
}
|
|
16
14
|
return message;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {Object} DisplayErrorMessage
|
|
21
|
-
* @property {string} message The text to display — an already-translated string or a locale key.
|
|
22
|
-
* @property {boolean} translated Whether `message` is ready-to-display text (`true`) or a locale
|
|
23
|
-
* key that still needs to go through I18n.Text (`false`).
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Resolves the message to display for a pipeline error, without ever surfacing raw backend text.
|
|
28
|
-
* Also reports whether the result is ready-to-display text or a locale key
|
|
29
|
-
* Precedence:
|
|
30
|
-
* 1. An extension's own message when it is explicitly flagged as already translated.
|
|
31
|
-
* 2. A resolvable i18n key — safe to surface even when errorManager did not remap it, because a
|
|
32
|
-
* translation key (e.g. `cart.error_out_of_stock`) is not raw backend text. `i18n.has` confirms
|
|
33
|
-
* the key actually resolves in the loaded locales, rather than merely looking like a key.
|
|
34
|
-
* 3. The code-mapped message. When errorManager maps the error code it resolves a message that
|
|
35
|
-
* differs from the raw backend text (`error.meta.message`).
|
|
36
|
-
* 4. A generic, translated fallback otherwise — including when nothing mapped and errorManager fell
|
|
37
|
-
* back to the raw backend message (`error.message === error.meta.message`).
|
|
38
|
-
* @param {Object} error The queued pipeline error.
|
|
39
|
-
* @param {string} error.message The message resolved by errorManager (code-mapped or raw fallback).
|
|
40
|
-
* @param {Object} [error.meta] The error meta data.
|
|
41
|
-
* @param {boolean} [error.meta.translated] Whether the backend message is already translated.
|
|
42
|
-
* @param {string} [error.meta.message] The original, raw backend message.
|
|
43
|
-
* @param {string} genericMessage The generic fallback message key.
|
|
44
|
-
* @returns {DisplayErrorMessage}
|
|
45
|
-
*/
|
|
46
|
-
export function getDisplayErrorMessage(error, genericMessage) {
|
|
47
|
-
const {
|
|
48
|
-
message,
|
|
49
|
-
meta = {}
|
|
50
|
-
} = error;
|
|
51
|
-
const {
|
|
52
|
-
translated,
|
|
53
|
-
message: originalMessage
|
|
54
|
-
} = meta;
|
|
55
|
-
if (translated && originalMessage) {
|
|
56
|
-
return {
|
|
57
|
-
message: originalMessage,
|
|
58
|
-
translated: true
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// A translation key is never raw backend text, so surface it even when errorManager left it
|
|
63
|
-
// untouched (`message === originalMessage`).
|
|
64
|
-
if (typeof message === 'string' && i18n.has(message)) {
|
|
65
|
-
return {
|
|
66
|
-
message,
|
|
67
|
-
translated: false
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Only trust `message` when we have the raw backend message to compare against and a mapping
|
|
72
|
-
// actually replaced it. Without the raw message we cannot rule out that `message` is raw text.
|
|
73
|
-
if (originalMessage !== undefined && message !== originalMessage) {
|
|
74
|
-
return {
|
|
75
|
-
message,
|
|
76
|
-
translated: false
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
message: genericMessage,
|
|
81
|
-
translated: false
|
|
82
|
-
};
|
|
83
15
|
}
|
package/providers/toast/spec.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import React, { useContext } from 'react';
|
|
2
|
-
import { render, act } from '@testing-library/react';
|
|
3
|
-
import ToastProvider from "./index";
|
|
4
|
-
import ToastContext from "./context";
|
|
5
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
|
-
jest.mock('@shopgate/pwa-core', () => ({
|
|
7
|
-
UIEvents: {
|
|
8
|
-
addListener: jest.fn(),
|
|
9
|
-
removeListener: jest.fn(),
|
|
10
|
-
emit: jest.fn()
|
|
11
|
-
}
|
|
12
|
-
}));
|
|
13
|
-
jest.mock('@shopgate/pwa-common/helpers/config', () => ({
|
|
14
|
-
themeConfig: {
|
|
15
|
-
variables: {
|
|
16
|
-
toast: {
|
|
17
|
-
duration: 5000
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}));
|
|
22
|
-
let ctx;
|
|
23
|
-
const Capture = () => {
|
|
24
|
-
ctx = useContext(ToastContext);
|
|
25
|
-
return null;
|
|
26
|
-
};
|
|
27
|
-
const renderProvider = () => render(/*#__PURE__*/_jsx(ToastProvider, {
|
|
28
|
-
children: /*#__PURE__*/_jsx(Capture, {})
|
|
29
|
-
}));
|
|
30
|
-
const toast = (id, message) => ({
|
|
31
|
-
id,
|
|
32
|
-
message
|
|
33
|
-
});
|
|
34
|
-
describe('ToastProvider', () => {
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
ctx = undefined;
|
|
37
|
-
});
|
|
38
|
-
it('appends a toast and applies the default duration', () => {
|
|
39
|
-
renderProvider();
|
|
40
|
-
act(() => {
|
|
41
|
-
ctx.addToast(toast('a', 'A'));
|
|
42
|
-
});
|
|
43
|
-
expect(ctx.toasts).toHaveLength(1);
|
|
44
|
-
expect(ctx.toasts[0]).toMatchObject({
|
|
45
|
-
id: 'a',
|
|
46
|
-
message: 'A',
|
|
47
|
-
duration: 5000
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
it('ignores a toast without a message', () => {
|
|
51
|
-
renderProvider();
|
|
52
|
-
act(() => {
|
|
53
|
-
ctx.addToast({
|
|
54
|
-
id: 'x'
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
expect(ctx.toasts).toHaveLength(0);
|
|
58
|
-
});
|
|
59
|
-
it('replaces a same-id toast in place, keeping its length and position', () => {
|
|
60
|
-
renderProvider();
|
|
61
|
-
act(() => {
|
|
62
|
-
ctx.addToast(toast('a', 'A'));
|
|
63
|
-
});
|
|
64
|
-
act(() => {
|
|
65
|
-
ctx.addToast(toast('b', 'B'));
|
|
66
|
-
});
|
|
67
|
-
const before = ctx.toasts;
|
|
68
|
-
act(() => {
|
|
69
|
-
ctx.addToast(toast('a', 'A2'));
|
|
70
|
-
});
|
|
71
|
-
expect(ctx.toasts).toHaveLength(2);
|
|
72
|
-
expect(ctx.toasts[0]).toMatchObject(toast('a', 'A2')); // content updated
|
|
73
|
-
expect(ctx.toasts[1]).toMatchObject(toast('b', 'B')); // sibling untouched
|
|
74
|
-
expect(ctx.toasts).not.toBe(before); // new array reference (immutable update)
|
|
75
|
-
expect(ctx.toasts[0]).not.toBe(before[0]); // new head object
|
|
76
|
-
});
|
|
77
|
-
it('removes the first toast (FIFO) on removeToast', () => {
|
|
78
|
-
renderProvider();
|
|
79
|
-
act(() => {
|
|
80
|
-
ctx.addToast(toast('a', 'A'));
|
|
81
|
-
});
|
|
82
|
-
act(() => {
|
|
83
|
-
ctx.addToast(toast('b', 'B'));
|
|
84
|
-
});
|
|
85
|
-
act(() => {
|
|
86
|
-
ctx.removeToast();
|
|
87
|
-
});
|
|
88
|
-
expect(ctx.toasts).toHaveLength(1);
|
|
89
|
-
expect(ctx.toasts[0]).toMatchObject(toast('b', 'B'));
|
|
90
|
-
});
|
|
91
|
-
it('keeps the context value referentially stable when toasts do not change', () => {
|
|
92
|
-
const {
|
|
93
|
-
rerender
|
|
94
|
-
} = renderProvider();
|
|
95
|
-
act(() => {
|
|
96
|
-
ctx.addToast(toast('a', 'A'));
|
|
97
|
-
});
|
|
98
|
-
const provided = ctx;
|
|
99
|
-
// Re-render the provider without changing the queue.
|
|
100
|
-
rerender(/*#__PURE__*/_jsx(ToastProvider, {
|
|
101
|
-
children: /*#__PURE__*/_jsx(Capture, {})
|
|
102
|
-
}));
|
|
103
|
-
expect(ctx).toBe(provided); // same context object → consumers don't needlessly re-render
|
|
104
|
-
});
|
|
105
|
-
});
|