@wise/dynamic-flow-client-internal 5.28.1 → 5.28.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/build/dynamicFlow/DynamicFlow.js +11 -0
  2. package/build/dynamicFlow/DynamicFlowModal.js +21 -0
  3. package/build/dynamicFlow/DynamicFlowWiseProps.js +1 -0
  4. package/build/dynamicFlow/DynamicFlowWithRef.js +19 -0
  5. package/build/dynamicFlow/context-menu/useContextMenu.js +33 -0
  6. package/build/dynamicFlow/context-menu/useDFContextMenu.js +135 -0
  7. package/build/dynamicFlow/context-menu/useDFContextMenu.test.js +101 -0
  8. package/build/dynamicFlow/getMergedRenderers.js +36 -0
  9. package/build/dynamicFlow/messages.js +13 -0
  10. package/build/dynamicFlow/renderers.js +10 -0
  11. package/build/dynamicFlow/telemetry/app-version.js +4 -0
  12. package/build/dynamicFlow/telemetry/dispatchAnalyticsEvent.js +8 -0
  13. package/build/dynamicFlow/telemetry/getLogEvent.js +22 -0
  14. package/build/dynamicFlow/telemetry/getTrackEvent.js +39 -0
  15. package/build/dynamicFlow/telemetry/index.js +2 -0
  16. package/build/dynamicFlow/useCreateSnackBar.js +9 -0
  17. package/build/dynamicFlow/useOnCopy.js +16 -0
  18. package/build/dynamicFlow/useWiseHttpClient.js +24 -0
  19. package/build/dynamicFlow/useWiseToCoreProps.js +42 -0
  20. package/build/i18n/de.json +3 -3
  21. package/build/i18n/fr.json +1 -1
  22. package/build/i18n/hu.json +1 -1
  23. package/build/i18n/id.json +1 -1
  24. package/build/i18n/index.js +43 -0
  25. package/build/i18n/pt.json +1 -1
  26. package/build/i18n/ru.json +1 -1
  27. package/build/i18n/zh_HK.json +1 -1
  28. package/build/index.js +6 -0
  29. package/build/main.js +10 -10
  30. package/build/main.mjs +10 -10
  31. package/build/stories/ButtonLoader.story.js +109 -0
  32. package/build/stories/ContainerQueries.story.js +74 -0
  33. package/build/stories/DynamicFlow.story.js +37 -0
  34. package/build/stories/DynamicFlowWithRef.story.js +72 -0
  35. package/build/stories/Editable.story.js +58 -0
  36. package/build/stories/Initialisation.story.js +33 -0
  37. package/build/stories/PinnedButtons.story.js +94 -0
  38. package/build/stories/SCAIntegration.story.js +67 -0
  39. package/build/stories/utils/fixtureHttpClient.js +59 -0
  40. package/build/test-utils/NeptuneProviders.js +11 -0
  41. package/build/test-utils/index.js +4 -0
  42. package/build/test-utils/respond-with.js +6 -0
  43. package/build/test-utils/rtl-utils.js +7 -0
  44. package/build/test-utils/wait.js +3 -0
  45. package/build/tests/DynamicFlow.test.js +177 -0
  46. package/build/tests/HideStepTitle.test.js +58 -0
  47. package/build/tests/RequestCache.test.js +34 -0
  48. package/build/tests/SCARejectionHandling.test.js +71 -0
  49. package/build/tests/Subflow.Wise.test.js +650 -0
  50. package/build/tests/events.test.js +124 -0
  51. package/build/tests/logging.test.js +12 -0
  52. package/build/tests/onNotification.test.js +96 -0
  53. package/build/tests/renderers/ControlAndTags.test.js +225 -0
  54. package/build/tests/renderers/ImageRenderer.test.js +129 -0
  55. package/build/tests/renderers/RadioInputRendererComponent.test.js +89 -0
  56. package/build/tests/renderers/ReviewRenderer.test.js +41 -0
  57. package/build/tests/renderers/TextInputRenderer.test.js +44 -0
  58. package/build/tests/renderers/TimeZoneHeader.test.js +19 -0
  59. package/build/tests/renderers/UploadInputRenderer.maxSize.test.js +22 -0
  60. package/build/tests/renderers/value-utils.test.js +24 -0
  61. package/build/tsconfig.types.tsbuildinfo +1 -1
  62. package/package.json +4 -4
@@ -0,0 +1,11 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useDynamicFlow } from '@wise/dynamic-flow-client';
3
+ import { useDFContextMenu } from './context-menu/useDFContextMenu';
4
+ import { useWiseToCoreProps } from './useWiseToCoreProps';
5
+ export function DynamicFlow(props) {
6
+ const { className = '' } = props;
7
+ const dfProps = useWiseToCoreProps(props);
8
+ const df = useDynamicFlow(dfProps);
9
+ const { onContextMenu, contextMenu } = useDFContextMenu(df.controller);
10
+ return (_jsxs("div", { className: className, onContextMenu: onContextMenu, children: [df.view, contextMenu] }));
11
+ }
@@ -0,0 +1,21 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx } from "react/jsx-runtime";
13
+ import { useDynamicFlowModal } from '@wise/dynamic-flow-client';
14
+ import { useWiseToCoreProps } from './useWiseToCoreProps';
15
+ import { Modal } from '@transferwise/components';
16
+ export function DynamicFlowModal(props) {
17
+ const { className = '' } = props, rest = __rest(props, ["className"]);
18
+ const dfProps = useWiseToCoreProps(rest);
19
+ const df = useDynamicFlowModal(dfProps);
20
+ return (_jsx(Modal, Object.assign({ className: `dynamic-flow-modal ${className}`, disableDimmerClickToClose: true }, df.modal, { body: _jsx("div", { className: "dynamic-flow-modal", children: df.modal.body }) })));
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useImperativeHandle, } from 'react';
3
+ import { useWiseToCoreProps } from './useWiseToCoreProps';
4
+ import { useDynamicFlow } from '@wise/dynamic-flow-client';
5
+ /**
6
+ * DynamicFlowWithRef is like DynamicFlow, but it also accepts a ForwardedRef.
7
+ * This ref conforms to the DynamicFlowRef type, containing two functions: `getValue` and `validate`.
8
+ * @experimental This component may be changed in the future without notice.
9
+ */
10
+ export const DynamicFlowWithRef = forwardRef(function DynamicFlowWithRef(props, ref) {
11
+ const { className = '' } = props;
12
+ const dfProps = useWiseToCoreProps(props);
13
+ const df = useDynamicFlow(dfProps);
14
+ useImperativeHandle(ref, () => ({
15
+ getValue: () => { var _a; return (_a = df.controller.getValue()) !== null && _a !== void 0 ? _a : null; },
16
+ validate: () => df.controller.validate(),
17
+ }), [df]);
18
+ return _jsx("div", { className: className, children: df.view });
19
+ });
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ export function useContextMenu({ title, subtitle, items, }) {
4
+ const { menuPosition, openMenuAt, closeMenu } = useMenuPosition();
5
+ const onContextMenu = (event) => {
6
+ if (items.length > 0 && !event.shiftKey) {
7
+ event.preventDefault();
8
+ openMenuAt({ x: event.clientX, y: event.clientY });
9
+ }
10
+ };
11
+ const contextMenu = menuPosition ? (_jsxs("div", { className: "df-context-menu", style: { top: menuPosition.y, left: menuPosition.x }, children: [_jsx("div", { className: "menu-header", children: title }), _jsx("div", { className: "menu-subtitle", children: subtitle }), items.map(({ label, onClick }) => (_jsx("button", { type: "button", className: "menu-button", onClick: () => {
12
+ closeMenu();
13
+ onClick();
14
+ }, children: label }, label)))] })) : null;
15
+ return {
16
+ onContextMenu,
17
+ contextMenu,
18
+ };
19
+ }
20
+ const useMenuPosition = () => {
21
+ const [menuPosition, setMenuPosition] = useState(null);
22
+ const clearMenu = useCallback(() => setMenuPosition(null), []);
23
+ const openMenuAt = useCallback((position) => {
24
+ setMenuPosition(position);
25
+ window.addEventListener('click', clearMenu);
26
+ }, [clearMenu]);
27
+ const closeMenu = useCallback(() => {
28
+ setMenuPosition(null);
29
+ window.removeEventListener('click', clearMenu);
30
+ }, [clearMenu]);
31
+ useEffect(() => clearMenu, [clearMenu]);
32
+ return { menuPosition, openMenuAt, closeMenu };
33
+ };
@@ -0,0 +1,135 @@
1
+ import { appVersion } from '../telemetry/app-version';
2
+ import { useContextMenu } from './useContextMenu';
3
+ export const useDFContextMenu = (controller) => {
4
+ const getCurrentStep = () => {
5
+ const step = controller.getCurrentStep();
6
+ return step ? recursivelyRemoveNullish(step) : null;
7
+ };
8
+ const getCurrentStepWithModel = async () => {
9
+ const step = controller.getCurrentStep();
10
+ if (!step) {
11
+ return null;
12
+ }
13
+ const model = controller.getValue();
14
+ return recursivelyRemoveNullish(Object.assign(Object.assign({}, step), { model }));
15
+ };
16
+ const getEncodedCurrentStep = () => {
17
+ const step = getCurrentStep();
18
+ return step ? toBase64(JSON.stringify(step, null, 2)) : null;
19
+ };
20
+ const getEncodedCurrentStepWithModel = async () => {
21
+ const step = await getCurrentStepWithModel();
22
+ return step ? toBase64(JSON.stringify(step, null, 2)) : null;
23
+ };
24
+ return useContextMenu({
25
+ title: 'DynamicFlow Menu (Dev/Staging only)',
26
+ subtitle: 'Shift + right-click for browser menu',
27
+ items: isDevOrStaging()
28
+ ? [
29
+ {
30
+ label: `DF client version: ${appVersion}`,
31
+ onClick: () => {
32
+ openVersionPage(appVersion);
33
+ },
34
+ },
35
+ {
36
+ label: 'Copy step JSON',
37
+ onClick: () => {
38
+ copyToClipboard(getCurrentStep());
39
+ },
40
+ },
41
+ {
42
+ label: 'Open in Studio (experimental)',
43
+ onClick: () => {
44
+ openIn('studio', getEncodedCurrentStep());
45
+ },
46
+ },
47
+ {
48
+ label: 'Open in Sandbox',
49
+ onClick: () => {
50
+ openIn('sandbox', getEncodedCurrentStep());
51
+ },
52
+ },
53
+ {
54
+ label: 'Open in Sandbox (with model)',
55
+ onClick: () => {
56
+ void getEncodedCurrentStepWithModel().then((base64Step) => openIn('sandbox', base64Step));
57
+ },
58
+ },
59
+ ]
60
+ : [],
61
+ });
62
+ };
63
+ const toBase64 = (str) => {
64
+ const bytes = new TextEncoder().encode(str);
65
+ const binString = Array.from(bytes, (b) => String.fromCharCode(b)).join('');
66
+ return btoa(binString);
67
+ };
68
+ const openIn = (editor, base64Step) => {
69
+ if (base64Step) {
70
+ window.open(`https://df.wise.com/${editor}#${base64Step}`, '_blank', 'noopener,noreferrer');
71
+ }
72
+ };
73
+ const openVersionPage = (version) => {
74
+ const changelogUrl = getGitHubChangelogUrl(version);
75
+ if (changelogUrl) {
76
+ window.open(changelogUrl, '_blank', 'noopener,noreferrer');
77
+ }
78
+ else {
79
+ window.open(getNpmPackageUrl(version), '_blank', 'noopener,noreferrer');
80
+ }
81
+ };
82
+ const getGitHubChangelogUrl = (version) => {
83
+ const semverRegex = /^\d+\.\d+\.\d+$/;
84
+ if (semverRegex.test(version)) {
85
+ const hash = version.replace(/\./g, ''); // convert version like "1.2.3" to "123" for the GitHub changelog anchor
86
+ return `https://github.com/transferwise/dynamic-flow/blob/main/web/wise/CHANGELOG.md#${hash}`;
87
+ }
88
+ return undefined;
89
+ };
90
+ const getNpmPackageUrl = (version) => {
91
+ return `https://www.npmjs.com/package/@wise/dynamic-flow-client-internal/v/${version}`;
92
+ };
93
+ const copyToClipboard = (value) => {
94
+ if (typeof value === 'string') {
95
+ void navigator.clipboard.writeText(value);
96
+ return;
97
+ }
98
+ if (value) {
99
+ void navigator.clipboard.writeText(JSON.stringify(value, null, 2));
100
+ }
101
+ };
102
+ // We only want to show the context menu in dev and staging environments.
103
+ const isDevOrStaging = () => {
104
+ const validHostNames = ['localhost', 'dev-wi.se'];
105
+ try {
106
+ if (typeof window === 'undefined' || !window.location) {
107
+ return false;
108
+ }
109
+ const currentHostname = window.location.hostname;
110
+ if (!currentHostname) {
111
+ return false;
112
+ }
113
+ return validHostNames.some((hostname) => currentHostname === hostname || currentHostname.endsWith(`.${hostname}`));
114
+ }
115
+ catch (_a) {
116
+ return false;
117
+ }
118
+ };
119
+ const recursivelyRemoveNullish = (element) => {
120
+ if (Array.isArray(element)) {
121
+ return element.map(recursivelyRemoveNullish);
122
+ }
123
+ if (element !== null && typeof element === 'object') {
124
+ return Object.entries(element).reduce((acc, [key, value]) => {
125
+ if (value !== null && value !== undefined) {
126
+ // Use mutation instead of object spread to avoid O(n²) complexity.
127
+ // Object spread would rebuild the accumulator on every property, causing performance issues for large steps.
128
+ // eslint-disable-next-line functional/immutable-data, no-param-reassign
129
+ acc[key] = recursivelyRemoveNullish(value);
130
+ }
131
+ return acc;
132
+ }, {});
133
+ }
134
+ return element;
135
+ };
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { fireEvent, screen, waitFor } from '@testing-library/react';
3
+ import { vi } from 'vitest';
4
+ import { renderWithProviders } from '../../test-utils/rtl-utils';
5
+ import { useDFContextMenu } from './useDFContextMenu';
6
+ const mockedAppVersion = vi.hoisted(() => ({ current: '0.0.0' }));
7
+ vi.mock('../telemetry/app-version', () => ({
8
+ get appVersion() {
9
+ return mockedAppVersion.current;
10
+ },
11
+ }));
12
+ const targetText = 'Right-click here to see the context menu';
13
+ const step = {
14
+ id: 'step-id',
15
+ title: 'Test Step',
16
+ description: null,
17
+ layout: [],
18
+ schemas: [],
19
+ };
20
+ const base64EncodedStep = 'ewogICJpZCI6ICJzdGVwLWlkIiwKICAidGl0bGUiOiAiVGVzdCBTdGVwIiwKICAibGF5b3V0IjogW10sCiAgInNjaGVtYXMiOiBbXQp9';
21
+ const base64EncodedStepWithModel = 'ewogICJpZCI6ICJzdGVwLWlkIiwKICAidGl0bGUiOiAiVGVzdCBTdGVwIiwKICAibGF5b3V0IjogW10sCiAgInNjaGVtYXMiOiBbXSwKICAibW9kZWwiOiB7fQp9';
22
+ const controller = {
23
+ getCurrentStep: vi.fn(() => step),
24
+ getValue: vi.fn(() => ({})),
25
+ };
26
+ const TestComponent = () => {
27
+ const { onContextMenu, contextMenu } = useDFContextMenu(controller);
28
+ return (_jsxs("div", { onContextMenu: onContextMenu, children: [targetText, contextMenu] }));
29
+ };
30
+ describe('useDFContextMenu', () => {
31
+ const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ Object.defineProperty(navigator, 'clipboard', {
35
+ writable: true,
36
+ value: { writeText: vi.fn().mockResolvedValue(undefined) },
37
+ });
38
+ });
39
+ describe('when right-clicking on localhost', () => {
40
+ beforeEach(() => {
41
+ Object.defineProperty(window, 'location', {
42
+ writable: true,
43
+ value: { hostname: 'localhost' },
44
+ });
45
+ });
46
+ describe('when the version is a release version', () => {
47
+ beforeEach(() => {
48
+ mockedAppVersion.current = '1.2.3';
49
+ });
50
+ it('shows the context menu with the version item, and on click opens the GitHub changelog', () => {
51
+ renderWithProviders(_jsx(TestComponent, {}));
52
+ fireEvent.contextMenu(screen.getByText(targetText));
53
+ expect(screen.getByText('DF client version: 1.2.3')).toBeInTheDocument();
54
+ fireEvent.click(screen.getByText('DF client version: 1.2.3'));
55
+ expect(openSpy).toHaveBeenCalledWith('https://github.com/transferwise/dynamic-flow/blob/main/web/wise/CHANGELOG.md#123', '_blank', 'noopener,noreferrer');
56
+ });
57
+ });
58
+ describe('when the version is a non-release version', () => {
59
+ beforeEach(() => {
60
+ mockedAppVersion.current = '1.2.3-beta';
61
+ });
62
+ it('shows the context menu with the version item, and on click opens the npm page for the current version', () => {
63
+ renderWithProviders(_jsx(TestComponent, {}));
64
+ fireEvent.contextMenu(screen.getByText(targetText));
65
+ expect(screen.getByText('DF client version: 1.2.3-beta')).toBeInTheDocument();
66
+ fireEvent.click(screen.getByText('DF client version: 1.2.3-beta'));
67
+ expect(openSpy).toHaveBeenCalledWith('https://www.npmjs.com/package/@wise/dynamic-flow-client-internal/v/1.2.3-beta', '_blank', 'noopener,noreferrer');
68
+ });
69
+ });
70
+ it('shows a menu item to open the current step in the sandbox', () => {
71
+ renderWithProviders(_jsx(TestComponent, {}));
72
+ fireEvent.contextMenu(screen.getByText(targetText));
73
+ expect(screen.getByText('Open in Sandbox')).toBeInTheDocument();
74
+ fireEvent.click(screen.getByText('Open in Sandbox'));
75
+ expect(openSpy).toHaveBeenCalledWith(expect.stringContaining(`https://df.wise.com/sandbox#${base64EncodedStep}`), '_blank', 'noopener,noreferrer');
76
+ });
77
+ it('shows a menu item to open the current step in Studio', () => {
78
+ renderWithProviders(_jsx(TestComponent, {}));
79
+ fireEvent.contextMenu(screen.getByText(targetText));
80
+ expect(screen.getByText('Open in Studio (experimental)')).toBeInTheDocument();
81
+ fireEvent.click(screen.getByText('Open in Studio (experimental)'));
82
+ expect(openSpy).toHaveBeenCalledWith(expect.stringContaining(`https://df.wise.com/studio#${base64EncodedStep}`), '_blank', 'noopener,noreferrer');
83
+ });
84
+ it('shows a menu item to open the current step in the sandbox with model', async () => {
85
+ renderWithProviders(_jsx(TestComponent, {}));
86
+ fireEvent.contextMenu(screen.getByText(targetText));
87
+ expect(screen.getByText('Open in Sandbox (with model)')).toBeInTheDocument();
88
+ fireEvent.click(screen.getByText('Open in Sandbox (with model)'));
89
+ await waitFor(() => {
90
+ expect(openSpy).toHaveBeenCalledWith(expect.stringContaining(`https://df.wise.com/sandbox#${base64EncodedStepWithModel}`), '_blank', 'noopener,noreferrer');
91
+ });
92
+ });
93
+ it('shows a menu item to copy the current step JSON', () => {
94
+ renderWithProviders(_jsx(TestComponent, {}));
95
+ fireEvent.contextMenu(screen.getByText(targetText));
96
+ expect(screen.getByText('Copy step JSON')).toBeInTheDocument();
97
+ fireEvent.click(screen.getByText('Copy step JSON'));
98
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(JSON.stringify({ id: 'step-id', title: 'Test Step', layout: [], schemas: [] }, null, 2));
99
+ });
100
+ });
101
+ });
@@ -0,0 +1,36 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx } from "react/jsx-runtime";
13
+ import { getDynamicSubflowRenderer } from '@wise/dynamic-flow-client';
14
+ import { getWiseRenderers, InitialLoadingStateRenderer } from '@wise/dynamic-flow-renderers';
15
+ import { DynamicFlow } from './DynamicFlow';
16
+ import { DynamicFlowModal } from './DynamicFlowModal';
17
+ const wiseRenderers = getWiseRenderers();
18
+ export const getMergedRenderers = (props) => {
19
+ var _a;
20
+ var _b;
21
+ const { initialAction, initialStep } = props, restProps = __rest(props, ["initialAction", "initialStep"]);
22
+ const subflowFeatures = Object.assign(Object.assign({}, props.features), { nativeBack: true });
23
+ const subflowRenderer = getDynamicSubflowRenderer({
24
+ Component: (_a) => {
25
+ var { presentation, initialRequest } = _a, rest = __rest(_a, ["presentation", "initialRequest"]);
26
+ const action = {
27
+ url: initialRequest.url,
28
+ method: initialRequest.method,
29
+ data: initialRequest.body,
30
+ };
31
+ return presentation.type === 'push' ? (_jsx(DynamicFlow, Object.assign({}, restProps, rest, { features: subflowFeatures, initialAction: action }))) : (_jsx(DynamicFlowModal, Object.assign({}, restProps, rest, { features: subflowFeatures, initialAction: action })));
32
+ },
33
+ });
34
+ const loadingStateRenderer = ((_a = props.features) === null || _a === void 0 ? void 0 : _a.initialLoader) ? [InitialLoadingStateRenderer] : [];
35
+ return [...((_b = props.renderers) !== null && _b !== void 0 ? _b : []), subflowRenderer, ...loadingStateRenderer, ...wiseRenderers];
36
+ };
@@ -0,0 +1,13 @@
1
+ import { defineMessages } from 'react-intl';
2
+ export default defineMessages({
3
+ copied: {
4
+ id: 'df.wise.CopyFeedback.copy',
5
+ defaultMessage: 'Copied to clipboard',
6
+ description: 'Appears in a snackbar when the copy operation succeeds.',
7
+ },
8
+ copyFailed: {
9
+ id: 'df.wise.CopyFeedback.copyFailed',
10
+ defaultMessage: 'Failed to copy to clipboard',
11
+ description: 'Appears in a snackbar when the copy operation fails.',
12
+ },
13
+ });
@@ -0,0 +1,10 @@
1
+ import { Header as RendererHeader, Media as RendererMedia, getMargin as rendererGetMargin, createCollectionRenderer as rendererCreateCollectionRenderer, } from '@wise/dynamic-flow-renderers';
2
+ /**
3
+ * We need to redefine these because @wise/dynamic-flow-renderers is a private package that
4
+ * cannot be installed. There is likely a better solution, but until we find it we can
5
+ * redefine and re-export.
6
+ */
7
+ export const Header = RendererHeader;
8
+ export const Media = RendererMedia;
9
+ export const getMargin = rendererGetMargin;
10
+ export const createCollectionRenderer = rendererCreateCollectionRenderer;
@@ -0,0 +1,4 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
2
+ export const appVersion =
3
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
4
+ typeof process !== 'undefined' ? process.env.APP_VERSION : '0.0.0';
@@ -0,0 +1,8 @@
1
+ export const dispatchAnalyticsEvent = (eventName, properties) => {
2
+ const props = Object.assign({ dfFallbackAnalytics: true }, properties);
3
+ if (typeof window !== 'undefined' &&
4
+ window.mixpanel &&
5
+ typeof window.mixpanel.track === 'function') {
6
+ window.mixpanel.track(eventName, props);
7
+ }
8
+ };
@@ -0,0 +1,22 @@
1
+ import { appVersion } from './app-version';
2
+ export const getLogEvent = (onLog) => (level, message, extra) => {
3
+ const extraWithVersion = Object.assign(Object.assign({}, extra), { dfWiseVersion: appVersion });
4
+ if (level !== 'info' && onLog) {
5
+ if (onLog) {
6
+ onLog(level, message, extraWithVersion);
7
+ }
8
+ else {
9
+ logToRollbar(level, message, extraWithVersion);
10
+ }
11
+ }
12
+ };
13
+ const logToRollbar = (level, message, extra) => {
14
+ try {
15
+ const rollbar = typeof window.Rollbar !== 'undefined' ? window.Rollbar : undefined;
16
+ rollbar === null || rollbar === void 0 ? void 0 : rollbar[level](message, extra);
17
+ }
18
+ catch (error) {
19
+ // eslint-disable-next-line no-console
20
+ console.error('Failed to log to Rollbar', error);
21
+ }
22
+ };
@@ -0,0 +1,39 @@
1
+ import { eventNames } from '@wise/dynamic-flow-client';
2
+ import { dispatchAnalyticsEvent } from './dispatchAnalyticsEvent';
3
+ const prefix = 'Dynamic Flow - ';
4
+ export const getTrackEvent = (onEvent, onAnalytics) => {
5
+ return (name, properties) => {
6
+ /*
7
+ * By adding an `$insert_id`, mixpanel can deduplicate events
8
+ * https://docs.mixpanel.com/reference/event-deduplication
9
+ *
10
+ * This means we do not need to rely on onEvent/onAnalytics to track events
11
+ * correctly, and can safely send our own. For integrations that are
12
+ * tracking correctly, they will be deduplicated at query time immediately,
13
+ * or at compaction time (for data exports) after a few hours and again
14
+ * after ~20 days.
15
+ */
16
+ const propsWithUuid = Object.assign(Object.assign({}, properties), { $insert_id: crypto.randomUUID() });
17
+ onEvent === null || onEvent === void 0 ? void 0 : onEvent(name, propsWithUuid);
18
+ if (includeInAnalytics(name)) {
19
+ dispatchAnalyticsEvent(name, propsWithUuid);
20
+ onAnalytics === null || onAnalytics === void 0 ? void 0 : onAnalytics(name, propsWithUuid);
21
+ }
22
+ };
23
+ };
24
+ const includeInAnalytics = (name) => {
25
+ const eventName = name.startsWith(prefix) ? name.slice(prefix.length) : name;
26
+ if (isCoreEventName(eventName)) {
27
+ return true; // for now, all core events are sent to analytics
28
+ }
29
+ if (eventName in customEventsToAnalytics) {
30
+ return customEventsToAnalytics[eventName];
31
+ }
32
+ return true; // unknown custom events are sent to analytics by default
33
+ };
34
+ const isCoreEventName = (eventName) => {
35
+ return eventNames.includes(eventName);
36
+ };
37
+ const customEventsToAnalytics = {
38
+ 'Theme Required': false, // Excluded from analytics
39
+ };
@@ -0,0 +1,2 @@
1
+ export { getLogEvent } from './getLogEvent';
2
+ export { getTrackEvent } from './getTrackEvent';
@@ -0,0 +1,9 @@
1
+ import { useSnackBarIfAvailable } from '@wise/dynamic-flow-renderers';
2
+ /**
3
+ * Wrapper around `useSnackBarIfAvailable` so tests can mock this module without
4
+ * mocking `@wise/dynamic-flow-renderers` directly.
5
+ */
6
+ export const useCreateSnackBar = () => {
7
+ const createSnackBar = useSnackBarIfAvailable();
8
+ return createSnackBar;
9
+ };
@@ -0,0 +1,16 @@
1
+ import { useSnackBarIfAvailable } from '@wise/dynamic-flow-renderers';
2
+ import { useCallback } from 'react';
3
+ import { useIntl } from 'react-intl';
4
+ import messages from './messages';
5
+ export const useOnCopy = () => {
6
+ const { formatMessage } = useIntl();
7
+ const createSnackBar = useSnackBarIfAvailable();
8
+ return useCallback((copiedContent) => {
9
+ if (copiedContent) {
10
+ createSnackBar({ text: formatMessage(messages.copied) });
11
+ }
12
+ else {
13
+ createSnackBar({ text: formatMessage(messages.copyFailed) });
14
+ }
15
+ }, [createSnackBar, formatMessage]);
16
+ };
@@ -0,0 +1,24 @@
1
+ import { useCallback } from 'react';
2
+ import { useIntl } from 'react-intl';
3
+ export const useWiseHttpClient = (httpClient) => {
4
+ const { locale } = useIntl();
5
+ return useCallback(async (input, init = {}) => {
6
+ const headers = new Headers(init.headers);
7
+ headers.set('accept-language', locale);
8
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
9
+ if (timeZone) {
10
+ headers.set('time-zone', timeZone);
11
+ }
12
+ const requestInit = Object.assign(Object.assign({}, init), { credentials: 'include', headers });
13
+ return httpClient(input, requestInit).catch(handleRejection);
14
+ }, [httpClient, locale]);
15
+ };
16
+ const handleRejection = (error) => {
17
+ if (error instanceof Error && error.message === 'SCA_MODAL_CLOSED') {
18
+ // This is a special case for the SCA modal.
19
+ // By responding with an empty `validation` object, and an error status code,
20
+ // we can avoid showing any error messages.
21
+ return new Response(JSON.stringify({ validation: {} }), { status: 400 });
22
+ }
23
+ throw error;
24
+ };
@@ -0,0 +1,42 @@
1
+ import { useMemo } from 'react';
2
+ import { getMergedRenderers } from './getMergedRenderers';
3
+ import { getLogEvent, getTrackEvent } from './telemetry';
4
+ import { useOnCopy } from './useOnCopy';
5
+ import { useWiseHttpClient } from './useWiseHttpClient';
6
+ import { useCreateSnackBar } from './useCreateSnackBar';
7
+ export const useWiseToCoreProps = (props) => {
8
+ const { displayStepTitle = true, features = {}, customFetch = globalThis.fetch, renderers, onAnalytics, onEvent, onLink = openLinkInNewTab, onNotification, onLog, } = props;
9
+ const httpClient = useWiseHttpClient(customFetch);
10
+ // eslint-disable-next-line react-hooks/exhaustive-deps
11
+ const mergedRenderers = useMemo(() => getMergedRenderers(props), [renderers]);
12
+ const createSnackBar = useCreateSnackBar();
13
+ const logEvent = useMemo(() => getLogEvent(onLog), [onLog]);
14
+ const trackEvent = useMemo(() => getTrackEvent(onEvent, onAnalytics), [onEvent, onAnalytics]);
15
+ const onCopy = useOnCopy();
16
+ return Object.assign(Object.assign({}, props), { httpClient, features: Object.assign({ hideStepTitle: !displayStepTitle }, features), renderers: mergedRenderers, onEvent: trackEvent, onLog: logEvent, onCopy, onNotification: onNotification !== null && onNotification !== void 0 ? onNotification : createSnackBar, onLink });
17
+ };
18
+ const openLinkInNewTab = (url) => {
19
+ if (typeof window === 'undefined' || typeof window.open !== 'function') {
20
+ return false;
21
+ }
22
+ // eslint-disable-next-line no-script-url
23
+ if (url.startsWith('javascript:')) {
24
+ return false;
25
+ }
26
+ try {
27
+ const w = window.open(url, '_blank');
28
+ if (w) {
29
+ try {
30
+ // eslint-disable-next-line functional/immutable-data
31
+ w.opener = null;
32
+ }
33
+ catch (_a) {
34
+ // ignore
35
+ }
36
+ }
37
+ return Boolean(w);
38
+ }
39
+ catch (_b) {
40
+ return false;
41
+ }
42
+ };
@@ -20,7 +20,7 @@
20
20
  "df.wise.ControlFeedback.type": "Falscher Typ",
21
21
  "df.wise.CopyFeedback.copy": "In Zwischenablage kopiert",
22
22
  "df.wise.CopyFeedback.copyFailed": "Kopieren in Zwischenablage fehlgeschlagen",
23
- "df.wise.DateInput.unavailableDates": "Some dates are unavailable.",
23
+ "df.wise.DateInput.unavailableDates": "Einige Daten sind nicht verfügbar.",
24
24
  "df.wise.DynamicParagraph.copied": "In Zwischenablage kopiert",
25
25
  "df.wise.DynamicParagraph.copy": "Kopieren",
26
26
  "df.wise.ErrorBoundary.errorAlert": "Da ist etwas schiefgegangen.",
@@ -41,8 +41,8 @@
41
41
  "df.wise.filter.placeholder": "Gib etwas ein, um die Suche zu beginnen",
42
42
  "df.wise.filter.results": "Suchergebnisse",
43
43
  "df.wise.group.all": "Alle",
44
- "df.wise.group.currencies-with-account-details": "Guthaben mit Bankverbindung",
44
+ "df.wise.group.currencies-with-account-details": "Währungen mit Bankverbindung",
45
45
  "df.wise.group.popular": "Beliebt",
46
- "df.wise.group.recent": "Neu",
46
+ "df.wise.group.recent": "Kürzlich verwendet",
47
47
  "df.wise.step.scrollToBottom": "Nach unten scrollen"
48
48
  }
@@ -20,7 +20,7 @@
20
20
  "df.wise.ControlFeedback.type": "Type incorrect",
21
21
  "df.wise.CopyFeedback.copy": "Copié dans le presse-papier",
22
22
  "df.wise.CopyFeedback.copyFailed": "Impossible de copier dans le presse-papier",
23
- "df.wise.DateInput.unavailableDates": "Some dates are unavailable.",
23
+ "df.wise.DateInput.unavailableDates": "Certaines dates ne sont pas disponibles.",
24
24
  "df.wise.DynamicParagraph.copied": "Copié dans le presse-papier",
25
25
  "df.wise.DynamicParagraph.copy": "Copier",
26
26
  "df.wise.ErrorBoundary.errorAlert": "Une erreur s'est produite.",
@@ -20,7 +20,7 @@
20
20
  "df.wise.ControlFeedback.type": "Helytelen típus",
21
21
  "df.wise.CopyFeedback.copy": "Vágólapra másolva",
22
22
  "df.wise.CopyFeedback.copyFailed": "Nem sikerült a vágólapra másolni",
23
- "df.wise.DateInput.unavailableDates": "Some dates are unavailable.",
23
+ "df.wise.DateInput.unavailableDates": "Egyes dátumok nem elérhetők.",
24
24
  "df.wise.DynamicParagraph.copied": "Vágólapra másolva",
25
25
  "df.wise.DynamicParagraph.copy": "Másolás",
26
26
  "df.wise.ErrorBoundary.errorAlert": "Valami hiba történt.",
@@ -20,7 +20,7 @@
20
20
  "df.wise.ControlFeedback.type": "Tipe salah",
21
21
  "df.wise.CopyFeedback.copy": "Disalin ke clipboard",
22
22
  "df.wise.CopyFeedback.copyFailed": "Gagal menyalin ke clipboard",
23
- "df.wise.DateInput.unavailableDates": "Some dates are unavailable.",
23
+ "df.wise.DateInput.unavailableDates": "Beberapa tanggal tidak tersedia.",
24
24
  "df.wise.DynamicParagraph.copied": "Disalin ke clipboard",
25
25
  "df.wise.DynamicParagraph.copy": "Salin",
26
26
  "df.wise.ErrorBoundary.errorAlert": "Terjadi kesalahan.",