@wise/dynamic-flow-client 5.9.2 → 5.10.0

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.
@@ -273,6 +273,10 @@ export const createFlowController = (props) => {
273
273
  rootComponent.setLoadingState('idle');
274
274
  break;
275
275
  }
276
+ case 'no-op': {
277
+ rootComponent.setLoadingState('idle');
278
+ break;
279
+ }
276
280
  }
277
281
  }
278
282
  catch (error) {
@@ -301,7 +305,7 @@ export const createFlowController = (props) => {
301
305
  updateStep(Object.assign(Object.assign({}, command.step), { errors }), command.etag);
302
306
  }
303
307
  break;
304
- case 'noop':
308
+ case 'no-op':
305
309
  rootComponent.setLoadingState('idle');
306
310
  break;
307
311
  case 'error': {
@@ -1,5 +1,6 @@
1
1
  import { getErrorMessage } from './getErrorMessage';
2
2
  import { assertStepResponseBody, isErrorResponseBody, parseResponseBodyAsJsonElement, } from './response-utils';
3
+ import { getResponseType } from './getResponseType';
3
4
  export const executeRefresh = async (props) => {
4
5
  const { abortSignal, url, model, etag, analytics, httpClient, trackEvent, logEvent } = props;
5
6
  trackEvent('Refresh Triggered', analytics);
@@ -12,7 +13,7 @@ export const executeRefresh = async (props) => {
12
13
  });
13
14
  if (response.status === 304) {
14
15
  trackEvent('Refresh Succeeded', analytics);
15
- return { type: 'noop' };
16
+ return { type: 'no-op' };
16
17
  }
17
18
  if (!response.ok) {
18
19
  const responseBody = await parseResponseBodyAsJsonElement(response).catch(() => ({}));
@@ -23,6 +24,11 @@ export const executeRefresh = async (props) => {
23
24
  }
24
25
  const newEtag = response.headers.get('etag') || null;
25
26
  const body = await parseResponseBodyAsJsonElement(response);
27
+ const responseType = getResponseType(response.headers, body);
28
+ if (responseType === 'no-op') {
29
+ trackEvent('Refresh Succeeded', analytics);
30
+ return { type: 'no-op' };
31
+ }
26
32
  assertStepResponseBody(body);
27
33
  trackEvent('Refresh Succeeded', analytics);
28
34
  return { type: 'refresh-step', step: body, etag: newEtag };
@@ -30,7 +36,7 @@ export const executeRefresh = async (props) => {
30
36
  catch (error) {
31
37
  if (error instanceof DOMException && error.name === 'AbortError') {
32
38
  trackEvent('Refresh Aborted', analytics);
33
- return { type: 'noop' };
39
+ return { type: 'no-op' };
34
40
  }
35
41
  trackEvent('Refresh Failed', analytics);
36
42
  logEvent('error', 'Dynamic Flow - Refresh Failed', Object.assign(Object.assign({}, analytics), { errorMessage: getErrorMessage(error) }));
@@ -60,6 +60,9 @@ export const executeRequest = async (props) => {
60
60
  assertModalResponseBody(responseBody);
61
61
  return { type: 'behavior', behavior: Object.assign(Object.assign({}, responseBody), { type: 'modal' }) };
62
62
  }
63
+ case 'no-op': {
64
+ return { type: 'no-op' };
65
+ }
63
66
  default: {
64
67
  throw new Error(`Unsupported response type: ${String(responseType)}`);
65
68
  }
@@ -48,6 +48,7 @@ export const executeSubmission = async (props) => {
48
48
  trackSubmissionEvent('Action Succeeded', { actionId });
49
49
  return Object.assign(Object.assign({}, command), { result: recursiveMerge(command.result, result) });
50
50
  }
51
+ case 'no-op':
51
52
  default: {
52
53
  trackSubmissionEvent('Action Succeeded', { actionId });
53
54
  return command;
@@ -1,7 +1,7 @@
1
1
  import { isObject } from '../utils/type-validators';
2
- const responseTypes = ['step', 'action', 'exit', 'modal', 'subflow'];
2
+ const responseTypes = ['step', 'action', 'exit', 'modal', 'subflow', 'no-op'];
3
3
  /**
4
- * Returns either 'step', 'action', or 'exit' based on the response headers and body.
4
+ * Returns either 'step', 'action', 'exit', 'modal', 'subflow', or 'no-op' based on the response headers and body.
5
5
  * This function takes a response body parameter because, for legacy reasons, we still need to consider the body
6
6
  * to determine the response type, in cases where the response headers are not set.
7
7
  * Ideally it should just be a matter of checking the "X-Df-Response-Type" response header.
@@ -30,6 +30,6 @@ export const getResponseTypeFromHeader = (headers) => {
30
30
  };
31
31
  function assertDFResponseType(type) {
32
32
  if (!responseTypes.includes(type)) {
33
- throw new Error("Unsupported X-Df-Response-Type. Allowed values are 'step', 'action', 'exit', 'error', 'modal', 'subflow'.");
33
+ throw new Error("Unsupported X-Df-Response-Type. Allowed values are 'step', 'action', 'exit', 'modal', 'subflow', 'no-op'.");
34
34
  }
35
35
  }
package/build/main.js CHANGED
@@ -6006,7 +6006,7 @@ var handleErrorResponse = async (response, actionId, trackEvent) => {
6006
6006
  };
6007
6007
 
6008
6008
  // src/controller/getResponseType.ts
6009
- var responseTypes = ["step", "action", "exit", "modal", "subflow"];
6009
+ var responseTypes = ["step", "action", "exit", "modal", "subflow", "no-op"];
6010
6010
  var getResponseType = (headers, body) => {
6011
6011
  const headerResponseType = getResponseTypeFromHeader(headers);
6012
6012
  if (headerResponseType) {
@@ -6031,7 +6031,7 @@ var getResponseTypeFromHeader = (headers) => {
6031
6031
  function assertDFResponseType(type) {
6032
6032
  if (!responseTypes.includes(type)) {
6033
6033
  throw new Error(
6034
- "Unsupported X-Df-Response-Type. Allowed values are 'step', 'action', 'exit', 'error', 'modal', 'subflow'."
6034
+ "Unsupported X-Df-Response-Type. Allowed values are 'step', 'action', 'exit', 'modal', 'subflow', 'no-op'."
6035
6035
  );
6036
6036
  }
6037
6037
  }
@@ -6109,6 +6109,9 @@ var executeRequest = async (props) => {
6109
6109
  assertModalResponseBody(responseBody);
6110
6110
  return { type: "behavior", behavior: __spreadProps(__spreadValues({}, responseBody), { type: "modal" }) };
6111
6111
  }
6112
+ case "no-op": {
6113
+ return { type: "no-op" };
6114
+ }
6112
6115
  default: {
6113
6116
  throw new Error(`Unsupported response type: ${String(responseType)}`);
6114
6117
  }
@@ -6177,6 +6180,7 @@ var executeSubmission = async (props) => {
6177
6180
  trackSubmissionEvent("Action Succeeded", { actionId });
6178
6181
  return __spreadProps(__spreadValues({}, command), { result: recursiveMerge(command.result, result) });
6179
6182
  }
6183
+ case "no-op":
6180
6184
  default: {
6181
6185
  trackSubmissionEvent("Action Succeeded", { actionId });
6182
6186
  return command;
@@ -6503,7 +6507,7 @@ var executeRefresh = async (props) => {
6503
6507
  });
6504
6508
  if (response.status === 304) {
6505
6509
  trackEvent("Refresh Succeeded", analytics);
6506
- return { type: "noop" };
6510
+ return { type: "no-op" };
6507
6511
  }
6508
6512
  if (!response.ok) {
6509
6513
  const responseBody = await parseResponseBodyAsJsonElement(response).catch(() => ({}));
@@ -6518,13 +6522,18 @@ var executeRefresh = async (props) => {
6518
6522
  }
6519
6523
  const newEtag = response.headers.get("etag") || null;
6520
6524
  const body = await parseResponseBodyAsJsonElement(response);
6525
+ const responseType = getResponseType(response.headers, body);
6526
+ if (responseType === "no-op") {
6527
+ trackEvent("Refresh Succeeded", analytics);
6528
+ return { type: "no-op" };
6529
+ }
6521
6530
  assertStepResponseBody(body);
6522
6531
  trackEvent("Refresh Succeeded", analytics);
6523
6532
  return { type: "refresh-step", step: body, etag: newEtag };
6524
6533
  } catch (error) {
6525
6534
  if (error instanceof DOMException && error.name === "AbortError") {
6526
6535
  trackEvent("Refresh Aborted", analytics);
6527
- return { type: "noop" };
6536
+ return { type: "no-op" };
6528
6537
  }
6529
6538
  trackEvent("Refresh Failed", analytics);
6530
6539
  logEvent("error", "Dynamic Flow - Refresh Failed", __spreadProps(__spreadValues({}, analytics), {
@@ -6889,6 +6898,10 @@ var createFlowController = (props) => {
6889
6898
  rootComponent.setLoadingState("idle");
6890
6899
  break;
6891
6900
  }
6901
+ case "no-op": {
6902
+ rootComponent.setLoadingState("idle");
6903
+ break;
6904
+ }
6892
6905
  }
6893
6906
  } catch (error) {
6894
6907
  closeWithError(error);
@@ -6920,7 +6933,7 @@ var createFlowController = (props) => {
6920
6933
  updateStep(__spreadProps(__spreadValues({}, command.step), { errors }), command.etag);
6921
6934
  }
6922
6935
  break;
6923
- case "noop":
6936
+ case "no-op":
6924
6937
  rootComponent.setLoadingState("idle");
6925
6938
  break;
6926
6939
  case "error": {
package/build/main.mjs CHANGED
@@ -5976,7 +5976,7 @@ var handleErrorResponse = async (response, actionId, trackEvent) => {
5976
5976
  };
5977
5977
 
5978
5978
  // src/controller/getResponseType.ts
5979
- var responseTypes = ["step", "action", "exit", "modal", "subflow"];
5979
+ var responseTypes = ["step", "action", "exit", "modal", "subflow", "no-op"];
5980
5980
  var getResponseType = (headers, body) => {
5981
5981
  const headerResponseType = getResponseTypeFromHeader(headers);
5982
5982
  if (headerResponseType) {
@@ -6001,7 +6001,7 @@ var getResponseTypeFromHeader = (headers) => {
6001
6001
  function assertDFResponseType(type) {
6002
6002
  if (!responseTypes.includes(type)) {
6003
6003
  throw new Error(
6004
- "Unsupported X-Df-Response-Type. Allowed values are 'step', 'action', 'exit', 'error', 'modal', 'subflow'."
6004
+ "Unsupported X-Df-Response-Type. Allowed values are 'step', 'action', 'exit', 'modal', 'subflow', 'no-op'."
6005
6005
  );
6006
6006
  }
6007
6007
  }
@@ -6079,6 +6079,9 @@ var executeRequest = async (props) => {
6079
6079
  assertModalResponseBody(responseBody);
6080
6080
  return { type: "behavior", behavior: __spreadProps(__spreadValues({}, responseBody), { type: "modal" }) };
6081
6081
  }
6082
+ case "no-op": {
6083
+ return { type: "no-op" };
6084
+ }
6082
6085
  default: {
6083
6086
  throw new Error(`Unsupported response type: ${String(responseType)}`);
6084
6087
  }
@@ -6147,6 +6150,7 @@ var executeSubmission = async (props) => {
6147
6150
  trackSubmissionEvent("Action Succeeded", { actionId });
6148
6151
  return __spreadProps(__spreadValues({}, command), { result: recursiveMerge(command.result, result) });
6149
6152
  }
6153
+ case "no-op":
6150
6154
  default: {
6151
6155
  trackSubmissionEvent("Action Succeeded", { actionId });
6152
6156
  return command;
@@ -6473,7 +6477,7 @@ var executeRefresh = async (props) => {
6473
6477
  });
6474
6478
  if (response.status === 304) {
6475
6479
  trackEvent("Refresh Succeeded", analytics);
6476
- return { type: "noop" };
6480
+ return { type: "no-op" };
6477
6481
  }
6478
6482
  if (!response.ok) {
6479
6483
  const responseBody = await parseResponseBodyAsJsonElement(response).catch(() => ({}));
@@ -6488,13 +6492,18 @@ var executeRefresh = async (props) => {
6488
6492
  }
6489
6493
  const newEtag = response.headers.get("etag") || null;
6490
6494
  const body = await parseResponseBodyAsJsonElement(response);
6495
+ const responseType = getResponseType(response.headers, body);
6496
+ if (responseType === "no-op") {
6497
+ trackEvent("Refresh Succeeded", analytics);
6498
+ return { type: "no-op" };
6499
+ }
6491
6500
  assertStepResponseBody(body);
6492
6501
  trackEvent("Refresh Succeeded", analytics);
6493
6502
  return { type: "refresh-step", step: body, etag: newEtag };
6494
6503
  } catch (error) {
6495
6504
  if (error instanceof DOMException && error.name === "AbortError") {
6496
6505
  trackEvent("Refresh Aborted", analytics);
6497
- return { type: "noop" };
6506
+ return { type: "no-op" };
6498
6507
  }
6499
6508
  trackEvent("Refresh Failed", analytics);
6500
6509
  logEvent("error", "Dynamic Flow - Refresh Failed", __spreadProps(__spreadValues({}, analytics), {
@@ -6859,6 +6868,10 @@ var createFlowController = (props) => {
6859
6868
  rootComponent.setLoadingState("idle");
6860
6869
  break;
6861
6870
  }
6871
+ case "no-op": {
6872
+ rootComponent.setLoadingState("idle");
6873
+ break;
6874
+ }
6862
6875
  }
6863
6876
  } catch (error) {
6864
6877
  closeWithError(error);
@@ -6890,7 +6903,7 @@ var createFlowController = (props) => {
6890
6903
  updateStep(__spreadProps(__spreadValues({}, command.step), { errors }), command.etag);
6891
6904
  }
6892
6905
  break;
6893
- case "noop":
6906
+ case "no-op":
6894
6907
  rootComponent.setLoadingState("idle");
6895
6908
  break;
6896
6909
  case "error": {
@@ -7,8 +7,11 @@ export default {
7
7
  chromatic: { disableSnapshot: true },
8
8
  },
9
9
  };
10
+ const httpClient = async () => new Promise((resolve) => {
11
+ setTimeout(resolve, 1000);
12
+ });
10
13
  export function Copy() {
11
- return renderWithStep(copyStep);
14
+ return renderWithStep(copyStep, httpClient);
12
15
  }
13
16
  const copyStep = {
14
17
  id: 'copy',
@@ -49,11 +52,20 @@ const copyStep = {
49
52
  {
50
53
  type: 'button',
51
54
  title: 'Click to copy',
52
- control: 'primary',
55
+ control: 'secondary',
53
56
  behavior: {
54
57
  type: 'copy',
55
58
  content: 'This is a button',
56
59
  },
57
60
  },
61
+ {
62
+ type: 'button',
63
+ title: 'Submit',
64
+ control: 'primary',
65
+ behavior: {
66
+ type: 'action',
67
+ action: { url: '/submit' },
68
+ },
69
+ },
58
70
  ],
59
71
  };
@@ -0,0 +1,40 @@
1
+ import DynamicFlowWise from '../../../test-utils/DynamicFlowWise';
2
+ import { renderWithStep } from '../../utils/render-utils';
3
+ export default {
4
+ component: DynamicFlowWise,
5
+ title: 'Spec/Behavior',
6
+ parameters: {
7
+ chromatic: { disableSnapshot: true },
8
+ },
9
+ };
10
+ const httpClient = async () => new Promise((resolve) => {
11
+ setTimeout(resolve, 1000);
12
+ });
13
+ export function Link() {
14
+ return renderWithStep(step, httpClient);
15
+ }
16
+ const step = {
17
+ id: 'copy',
18
+ title: 'Link Behavior',
19
+ schemas: [],
20
+ layout: [
21
+ {
22
+ type: 'button',
23
+ title: 'Open Google',
24
+ control: 'secondary',
25
+ behavior: {
26
+ type: 'link',
27
+ url: 'https://www.google.com',
28
+ },
29
+ },
30
+ {
31
+ type: 'button',
32
+ title: 'Submit',
33
+ control: 'primary',
34
+ behavior: {
35
+ type: 'action',
36
+ action: { url: '/submit' },
37
+ },
38
+ },
39
+ ],
40
+ };
@@ -7,8 +7,11 @@ export default {
7
7
  chromatic: { disableSnapshot: true },
8
8
  },
9
9
  };
10
+ const httpClient = async () => new Promise((resolve) => {
11
+ setTimeout(resolve, 1000);
12
+ });
10
13
  export function Modal() {
11
- return renderWithStep(initialStep);
14
+ return renderWithStep(initialStep, httpClient);
12
15
  }
13
16
  const initialStep = {
14
17
  id: 'modal',
@@ -33,7 +33,7 @@ const getStep = (urns, dismissable) => ({
33
33
  },
34
34
  callToAction: {
35
35
  title: 'Do the thing!',
36
- behavior: { type: 'action', action: { url: '/submit' } },
36
+ behavior: { type: 'link', url: 'https://google.com' },
37
37
  },
38
38
  onDismiss: dismissable ? { url: '/upsell-dismissed', method: 'POST' } : undefined,
39
39
  margin: 'lg',
@@ -0,0 +1,194 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import { userEvent } from '@testing-library/user-event';
4
+ import { vi } from 'vitest';
5
+ import { renderWithProviders, respondWith } from '../test-utils';
6
+ import DynamicFlowWise from '../test-utils/DynamicFlowWise';
7
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
8
+ const getProps = () => ({
9
+ flowId: 'flow-id',
10
+ onCompletion: vi.fn(),
11
+ onError: vi.fn(),
12
+ onEvent: vi.fn(),
13
+ onLog: vi.fn(),
14
+ });
15
+ const noOpHeaders = { 'X-Df-Response-Type': 'no-op' };
16
+ describe('No-op responses', () => {
17
+ describe('Action submissions', () => {
18
+ const buildInitialStep = () => ({
19
+ id: 'no-op-action-step',
20
+ title: 'No-Op Action Demo',
21
+ model: { message: '' },
22
+ schemas: [
23
+ {
24
+ $id: '#action-form',
25
+ type: 'object',
26
+ displayOrder: ['message'],
27
+ required: ['message'],
28
+ properties: {
29
+ message: {
30
+ title: 'Message',
31
+ type: 'string',
32
+ },
33
+ },
34
+ },
35
+ ],
36
+ layout: [
37
+ { type: 'form', schemaId: '#action-form' },
38
+ {
39
+ type: 'button',
40
+ title: 'Submit',
41
+ action: { id: 'noop-action-id', url: '/action', method: 'POST' },
42
+ },
43
+ {
44
+ type: 'button',
45
+ title: 'Submit elsewhere',
46
+ action: { id: 'other-action-id', url: '/other-action', method: 'POST' },
47
+ },
48
+ ],
49
+ });
50
+ it('should keep the current step when an action responds with no-op', async () => {
51
+ const initialStep = buildInitialStep();
52
+ const httpClient = vi.fn().mockResolvedValue(respondWith({}, { headers: noOpHeaders }));
53
+ const props = Object.assign(Object.assign({}, getProps()), { initialStep, httpClient });
54
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props)));
55
+ await waitFor(() => {
56
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
57
+ });
58
+ const input = screen.getByLabelText('Message');
59
+ await user.type(input, 'first attempt');
60
+ await user.click(screen.getByText('Submit'));
61
+ await waitFor(() => {
62
+ expect(httpClient).toHaveBeenCalledTimes(1);
63
+ });
64
+ expect(httpClient).toHaveBeenCalledWith('/action', expect.objectContaining({ body: '{"message":"first attempt"}' }));
65
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
66
+ expect(props.onError).not.toHaveBeenCalled();
67
+ expect(props.onEvent).toHaveBeenCalledWith('Dynamic Flow - Action Succeeded', expect.objectContaining({ actionId: 'noop-action-id' }));
68
+ });
69
+ it('should allow submitting another action after a no-op response', async () => {
70
+ const initialStep = buildInitialStep();
71
+ const nextStep = {
72
+ id: 'post-no-op-step',
73
+ title: 'Step after No-Op',
74
+ layout: [],
75
+ schemas: [],
76
+ };
77
+ const httpClient = vi
78
+ .fn()
79
+ .mockResolvedValueOnce(respondWith({}, { headers: noOpHeaders }))
80
+ .mockResolvedValueOnce(respondWith(nextStep));
81
+ const props = Object.assign(Object.assign({}, getProps()), { initialStep, httpClient });
82
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props)));
83
+ await waitFor(() => {
84
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
85
+ });
86
+ const input = screen.getByLabelText('Message');
87
+ await user.type(input, 'first attempt');
88
+ await user.click(screen.getByText('Submit'));
89
+ await waitFor(() => {
90
+ expect(httpClient).toHaveBeenCalledTimes(1);
91
+ });
92
+ await user.clear(input);
93
+ await user.type(input, 'second attempt');
94
+ await user.click(screen.getByText('Submit elsewhere'));
95
+ await waitFor(() => {
96
+ expect(screen.getByText('Step after No-Op')).toBeInTheDocument();
97
+ });
98
+ expect(httpClient).toHaveBeenCalledTimes(2);
99
+ expect(httpClient).toHaveBeenCalledWith('/other-action', expect.objectContaining({ body: '{"message":"second attempt"}' }));
100
+ expect(props.onError).not.toHaveBeenCalled();
101
+ expect(props.onEvent).toHaveBeenCalledWith('Dynamic Flow - Action Succeeded', expect.objectContaining({ actionId: 'other-action-id' }));
102
+ });
103
+ });
104
+ describe('Refresh requests', () => {
105
+ const buildInitialStep = () => ({
106
+ id: 'no-op-refresh-step',
107
+ title: 'No-Op Refresh Demo',
108
+ schemas: [
109
+ {
110
+ $id: '#refresh-form',
111
+ type: 'object',
112
+ displayOrder: ['color'],
113
+ properties: {
114
+ color: {
115
+ title: 'Color',
116
+ control: 'radio',
117
+ analyticsId: '#color-schema',
118
+ oneOf: [
119
+ { const: 'red', title: 'Red' },
120
+ { const: 'blue', title: 'Blue' },
121
+ ],
122
+ onChange: { type: 'refresh' },
123
+ },
124
+ },
125
+ },
126
+ ],
127
+ layout: [
128
+ { type: 'form', schemaId: '#refresh-form' },
129
+ {
130
+ type: 'button',
131
+ title: 'Submit',
132
+ action: { id: 'submit-id', url: '/submit', method: 'POST' },
133
+ },
134
+ {
135
+ type: 'button',
136
+ title: 'Submit step',
137
+ action: { id: 'step-submit-id', url: '/step-submit', method: 'POST' },
138
+ },
139
+ ],
140
+ refreshUrl: '/refresh',
141
+ });
142
+ it('should treat a no-op refresh like a 304 response', async () => {
143
+ const initialStep = buildInitialStep();
144
+ const httpClient = vi.fn().mockResolvedValue(respondWith({}, { headers: noOpHeaders }));
145
+ const props = Object.assign(Object.assign({}, getProps()), { initialStep, httpClient });
146
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props)));
147
+ await waitFor(() => {
148
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
149
+ });
150
+ await user.click(screen.getByText('Red'));
151
+ await waitFor(() => {
152
+ expect(httpClient).toHaveBeenCalledTimes(1);
153
+ });
154
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
155
+ expect(httpClient).toHaveBeenNthCalledWith(1, '/refresh', expect.objectContaining({ body: '{"color":"red"}' }));
156
+ expect(props.onError).not.toHaveBeenCalled();
157
+ expect(props.onEvent).toHaveBeenCalledWith('Dynamic Flow - Refresh Succeeded', expect.objectContaining({
158
+ flowId: 'flow-id',
159
+ stepId: initialStep.id,
160
+ schema: '#color-schema',
161
+ }));
162
+ });
163
+ it('should allow refreshing again and submitting after a no-op response', async () => {
164
+ const initialStep = buildInitialStep();
165
+ const refreshedStep = Object.assign(Object.assign({}, initialStep), { id: 'refreshed-step', title: 'Refreshed after No-Op', model: { color: 'blue' } });
166
+ const httpClient = vi
167
+ .fn()
168
+ .mockResolvedValueOnce(respondWith({}, { headers: noOpHeaders }))
169
+ .mockResolvedValueOnce(respondWith(refreshedStep))
170
+ .mockResolvedValueOnce(respondWith(refreshedStep));
171
+ const props = Object.assign(Object.assign({}, getProps()), { initialStep, httpClient });
172
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props)));
173
+ await waitFor(() => {
174
+ expect(screen.getByText(initialStep.title)).toBeInTheDocument();
175
+ });
176
+ await user.click(screen.getByText('Red'));
177
+ await waitFor(() => {
178
+ expect(httpClient).toHaveBeenCalledTimes(1);
179
+ });
180
+ await user.click(screen.getByText('Blue'));
181
+ await waitFor(() => {
182
+ expect(screen.getByText('Refreshed after No-Op')).toBeInTheDocument();
183
+ });
184
+ expect(httpClient).toHaveBeenCalledTimes(2);
185
+ expect(httpClient).toHaveBeenCalledWith('/refresh', expect.objectContaining({ body: '{"color":"blue"}' }));
186
+ await user.click(screen.getByText('Submit step'));
187
+ await waitFor(() => {
188
+ expect(httpClient).toHaveBeenCalledTimes(3);
189
+ });
190
+ expect(httpClient).toHaveBeenCalledWith('/step-submit', expect.objectContaining({ body: '{"color":"blue"}' }));
191
+ expect(props.onError).not.toHaveBeenCalled();
192
+ });
193
+ });
194
+ });
@@ -3,11 +3,11 @@ import { act, screen, waitFor } from '@testing-library/react';
3
3
  import { userEvent } from '@testing-library/user-event';
4
4
  import { Button } from '@transferwise/components';
5
5
  import { useState } from 'react';
6
+ import { vi } from 'vitest';
6
7
  import { renderWithProviders, respondWith } from '../test-utils';
7
- import { abortableDelay, respondWithDelay } from '../test-utils/fetch-utils';
8
8
  import DynamicFlowWise from '../test-utils/DynamicFlowWise';
9
+ import { abortableDelay, respondWithDelay } from '../test-utils/fetch-utils';
9
10
  import { createEmptyStep } from '../test-utils/step-utils';
10
- import { vi } from 'vitest';
11
11
  const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
12
12
  const emptyStep = createEmptyStep();
13
13
  const getDefaultProps = () => ({
@@ -16,6 +16,7 @@ const getDefaultProps = () => ({
16
16
  onError: vi.fn(),
17
17
  onEvent: vi.fn(),
18
18
  onLog: vi.fn(),
19
+ onLink: vi.fn(),
19
20
  });
20
21
  const getStep = (model, schema) => {
21
22
  var _a;
@@ -158,9 +159,7 @@ describe('Data submission', () => {
158
159
  });
159
160
  expect(screen.getByText('Please select a currency.')).toBeInTheDocument();
160
161
  });
161
- it('should set loading state on action buttons that were clicked (and set disabled on other buttons) while submitting, and then re-enable them', async () => {
162
- const model = { name: 'Name', number: '12345' };
163
- const step = getStep(model, schema);
162
+ describe('loading state', () => {
164
163
  const httpClient = vi.fn().mockImplementation(async (input, init) => {
165
164
  var _a;
166
165
  return abortableDelay(1, (_a = init === null || init === void 0 ? void 0 : init.signal) !== null && _a !== void 0 ? _a : null).then(async () => respondWith({
@@ -172,16 +171,164 @@ describe('Data submission', () => {
172
171
  schemas: [],
173
172
  }));
174
173
  });
175
- const props = getDefaultProps();
176
- renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ httpClient: httpClient, initialStep: step }, props)));
177
- const submitButton = screen.getByRole('button', { name: 'Submit' });
178
- const otherButton = screen.getByRole('button', { name: 'Other' });
179
- await user.click(submitButton);
180
- expect(submitButton).toHaveAttribute('aria-busy', 'true');
181
- expect(otherButton).toBeDisabled();
182
- await waitFor(() => {
183
- const submitButton2 = screen.getByRole('button', { name: 'Finish' });
184
- expect(submitButton2).toBeEnabled();
174
+ it('should set loading state on action buttons that were clicked (and set disabled on other buttons) while submitting, and then re-enable them', async () => {
175
+ const model = { name: 'Name', number: '12345' };
176
+ const step = getStep(model, schema);
177
+ const props = getDefaultProps();
178
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ httpClient: httpClient, initialStep: step }, props)));
179
+ const submitButton = screen.getByRole('button', { name: 'Submit' });
180
+ const otherButton = screen.getByRole('button', { name: 'Other' });
181
+ await user.click(submitButton);
182
+ expect(submitButton).toHaveAttribute('aria-busy', 'true');
183
+ expect(otherButton).toBeDisabled();
184
+ expect(otherButton).not.toHaveAttribute('aria-busy');
185
+ await waitFor(() => {
186
+ expect(screen.getByText('Next step')).toBeInTheDocument();
187
+ });
188
+ });
189
+ describe('when attempting submission with one button, but validation fails, then submissting with another button', () => {
190
+ it('should only set the loading state of the button that was last pressed', async () => {
191
+ const step = {
192
+ id: 'step-id',
193
+ title: 'Submission test',
194
+ layout: [
195
+ { type: 'box', components: [{ type: 'form', schemaId: '#schema' }] },
196
+ {
197
+ type: 'button',
198
+ title: 'Submit',
199
+ action: { id: 'submit', url: '/submit', method: 'POST' },
200
+ },
201
+ {
202
+ type: 'button',
203
+ title: 'Submit without validation',
204
+ action: {
205
+ id: 'submit-without-validation',
206
+ url: '/submit',
207
+ method: 'POST',
208
+ skipValidation: true,
209
+ },
210
+ },
211
+ ],
212
+ refreshUrl: '/refresh',
213
+ schemas: [
214
+ {
215
+ $id: '#schema',
216
+ title: 'Name',
217
+ type: 'string',
218
+ validationMessages: { required: 'Please enter your name!' },
219
+ },
220
+ ],
221
+ };
222
+ const props = getDefaultProps();
223
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ httpClient: httpClient, initialStep: step }, props)));
224
+ const submitButton = screen.getByRole('button', { name: 'Submit' });
225
+ const submitWithoutValidationButton = screen.getByRole('button', {
226
+ name: 'Submit without validation',
227
+ });
228
+ await user.click(submitButton);
229
+ expect(screen.getByText('Please enter your name!')).toBeInTheDocument();
230
+ await user.click(submitWithoutValidationButton);
231
+ // expect(screen.queryByText('Please enter your name!')).not.toBeInTheDocument();
232
+ expect(submitWithoutValidationButton).toHaveAttribute('aria-busy', 'true');
233
+ expect(submitButton).toBeDisabled();
234
+ expect(submitButton).not.toHaveAttribute('aria-busy');
235
+ });
236
+ });
237
+ describe('link buttons', () => {
238
+ it('should NOT set loading state on link buttons that have been previously clicked', async () => {
239
+ var _a;
240
+ const model = { name: 'Name', number: '12345' };
241
+ const step = {
242
+ id: 'step-id',
243
+ title: 'Submission test',
244
+ layout: [
245
+ { type: 'box', components: [{ type: 'form', schemaId: (_a = schema.$id) !== null && _a !== void 0 ? _a : '' }] },
246
+ {
247
+ type: 'button',
248
+ title: 'Submit',
249
+ behavior: {
250
+ type: 'action',
251
+ action: { id: 'action-id', url: '/submit', method: 'POST' },
252
+ },
253
+ },
254
+ {
255
+ type: 'button',
256
+ title: 'Link',
257
+ behavior: { type: 'link', url: 'https://www.google.com' },
258
+ },
259
+ ],
260
+ schemas: [schema],
261
+ model,
262
+ };
263
+ const props = getDefaultProps();
264
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ httpClient: httpClient, initialStep: step }, props)));
265
+ const submitButton = screen.getByRole('button', { name: 'Submit' });
266
+ const linkButton = screen.getByRole('button', { name: 'Link' });
267
+ await user.click(linkButton);
268
+ expect(props.onLink).toHaveBeenCalled();
269
+ await user.click(submitButton);
270
+ expect(submitButton).toHaveAttribute('aria-busy', 'true');
271
+ expect(linkButton).toBeDisabled();
272
+ expect(linkButton).not.toHaveAttribute('aria-busy');
273
+ await waitFor(() => {
274
+ expect(screen.getByText('Next step')).toBeInTheDocument();
275
+ });
276
+ });
277
+ });
278
+ describe('buttons that trigger modals', () => {
279
+ it('should NOT set loading state on buttons that trigger modals that have been previously clicked', async () => {
280
+ var _a;
281
+ const model = { name: 'Name', number: '12345' };
282
+ const step = {
283
+ id: 'step-id',
284
+ title: 'Submission test',
285
+ layout: [
286
+ { type: 'box', components: [{ type: 'form', schemaId: (_a = schema.$id) !== null && _a !== void 0 ? _a : '' }] },
287
+ {
288
+ type: 'button',
289
+ title: 'Submit',
290
+ behavior: {
291
+ type: 'action',
292
+ action: { id: 'action-id', url: '/submit', method: 'POST' },
293
+ },
294
+ },
295
+ {
296
+ type: 'button',
297
+ title: 'Open Modal',
298
+ behavior: {
299
+ type: 'modal',
300
+ title: 'Open Modal',
301
+ content: [
302
+ {
303
+ type: 'paragraph',
304
+ text: 'This is a modal opened by the submit button.',
305
+ },
306
+ ],
307
+ },
308
+ },
309
+ ],
310
+ schemas: [schema],
311
+ model,
312
+ };
313
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({ httpClient: httpClient, initialStep: step }, getDefaultProps())));
314
+ const submitButton = screen.getByRole('button', { name: 'Submit' });
315
+ const modalButton = screen.getByRole('button', { name: 'Open Modal' });
316
+ await user.click(modalButton);
317
+ await waitFor(() => {
318
+ expect(screen.getByText('This is a modal opened by the submit button.')).toBeInTheDocument();
319
+ });
320
+ await user.keyboard('{Escape}');
321
+ await waitFor(() => {
322
+ expect(screen.queryByText('This is a modal opened by the submit button.')).not.toBeInTheDocument();
323
+ });
324
+ await user.click(submitButton);
325
+ expect(submitButton).toHaveAttribute('aria-busy', 'true');
326
+ expect(modalButton).toBeDisabled();
327
+ expect(modalButton).not.toHaveAttribute('aria-busy');
328
+ await waitFor(() => {
329
+ expect(screen.getByText('Next step')).toBeInTheDocument();
330
+ });
331
+ });
185
332
  });
186
333
  });
187
334
  describe('with unreferenced schemas', () => {
@@ -345,9 +492,7 @@ describe('Data submission', () => {
345
492
  const initialStep = {
346
493
  id: 'step-id',
347
494
  title: 'Footer submission test',
348
- layout: [
349
- { type: 'box', components: [{ type: 'form', schemaId: '#schema' }] },
350
- ],
495
+ layout: [{ type: 'box', components: [{ type: 'form', schemaId: '#schema' }] }],
351
496
  footer: [
352
497
  { type: 'form', schemaId: '#footer-schema' },
353
498
  {
@@ -43,12 +43,11 @@ describe('Upsell component', () => {
43
43
  expect(getCloseButton()).not.toBeInTheDocument();
44
44
  });
45
45
  describe('when the call to action is clicked', () => {
46
- it('triggers the specified behavior', async () => {
47
- const onLink = vi.fn();
48
- renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props, { onLink: onLink })));
49
- const upgradeButton = screen.getByText('Upgrade now');
50
- await user.click(upgradeButton);
51
- expect(onLink).toHaveBeenCalledWith('/upgrade');
46
+ it('triggers the specified behavior (or for a link, has the correct attributes)', () => {
47
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props)));
48
+ const link = screen.getByRole('link', { name: /upgrade now/i });
49
+ expect(link).toHaveAttribute('href', '/upgrade');
50
+ expect(link).toHaveAttribute('target', '_blank');
52
51
  });
53
52
  });
54
53
  });
@@ -123,4 +122,33 @@ describe('Upsell component', () => {
123
122
  });
124
123
  });
125
124
  });
125
+ describe('when the upsell has a link behavior', () => {
126
+ it('does not trigger twice (the renderer handles the href, so the onLink is not called)', async () => {
127
+ const onLink = vi.fn();
128
+ const props = getProps({
129
+ id: 'step-1',
130
+ title: 'Step 1',
131
+ schemas: [],
132
+ layout: [
133
+ {
134
+ type: 'upsell',
135
+ text: 'Try our premium features!',
136
+ callToAction: {
137
+ title: 'Upgrade now',
138
+ behavior: {
139
+ type: 'link',
140
+ url: '/upgrade',
141
+ },
142
+ },
143
+ },
144
+ ],
145
+ });
146
+ renderWithProviders(_jsx(DynamicFlowWise, Object.assign({}, props, { onLink: onLink })));
147
+ const link = screen.getByRole('link');
148
+ expect(link).toHaveAttribute('href', '/upgrade');
149
+ expect(link).toHaveAttribute('target', '_blank');
150
+ await user.click(link);
151
+ expect(onLink).not.toHaveBeenCalled();
152
+ });
153
+ });
126
154
  });
@@ -1 +1 @@
1
- {"version":3,"file":"FlowController.d.ts","sourceRoot":"","sources":["../../../src/controller/FlowController.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAA6B,MAAM,0CAA0C,CAAC;AAMjG,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AACpE,OAAO,EAKL,aAAa,EACb,WAAW,EACZ,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAShD,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,oBAAoB,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG;IACvF,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,wBAAwB,EAAE,wBAAwB,CAAC;IACnD,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CAClC,CAAC;AAIF,eAAO,MAAM,oBAAoB,GAAI,OAAO,mBAAmB;;;;CA4b9D,CAAC"}
1
+ {"version":3,"file":"FlowController.d.ts","sourceRoot":"","sources":["../../../src/controller/FlowController.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAA6B,MAAM,0CAA0C,CAAC;AAMjG,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AACpE,OAAO,EAKL,aAAa,EACb,WAAW,EACZ,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAShD,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,oBAAoB,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG;IACvF,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,wBAAwB,EAAE,wBAAwB,CAAC;IACnD,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,EAAE,WAAW,CAAC;IACzB,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CAClC,CAAC;AAIF,eAAO,MAAM,oBAAoB,GAAI,OAAO,mBAAmB;;;;CAgc9D,CAAC"}
@@ -5,7 +5,7 @@ type Command = {
5
5
  step: Step;
6
6
  etag: string | null;
7
7
  } | {
8
- type: 'noop';
8
+ type: 'no-op';
9
9
  } | {
10
10
  type: 'error';
11
11
  body: ErrorResponseBody;
@@ -1 +1 @@
1
- {"version":3,"file":"executeRefresh.d.ts","sourceRoot":"","sources":["../../../src/controller/executeRefresh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,+BAA+B,CAAC;AACpF,OAAO,KAAK,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAQlG,KAAK,OAAO,GACR;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE,eAAO,MAAM,cAAc,GAAU,OAAO;IAC1C,WAAW,EAAE,WAAW,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,UAAU,EAAE,OAAO,KAAK,CAAC;IACzB,UAAU,EAAE,wBAAwB,CAAC;IACrC,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG,OAAO,CAAC,OAAO,CAqDlB,CAAC"}
1
+ {"version":3,"file":"executeRefresh.d.ts","sourceRoot":"","sources":["../../../src/controller/executeRefresh.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,+BAA+B,CAAC;AACpF,OAAO,KAAK,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AASlG,KAAK,OAAO,GACR;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE,eAAO,MAAM,cAAc,GAAU,OAAO;IAC1C,WAAW,EAAE,WAAW,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,UAAU,EAAE,OAAO,KAAK,CAAC;IACzB,UAAU,EAAE,wBAAwB,CAAC;IACrC,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG,OAAO,CAAC,OAAO,CA4DlB,CAAC"}
@@ -28,6 +28,8 @@ export type Command = {
28
28
  } | {
29
29
  type: 'behavior';
30
30
  behavior: Behavior;
31
+ } | {
32
+ type: 'no-op';
31
33
  };
32
34
  export declare const executeRequest: (props: {
33
35
  exit?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"executeRequest.d.ts","sourceRoot":"","sources":["../../../src/controller/executeRequest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,+BAA+B,CAAC;AACxF,OAAO,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAU7F,OAAO,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAIzE,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3C,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,KAAK,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACzD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE;QACL,SAAS,CAAC,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAC;QAC3C,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;KACzB,CAAC;IACF,SAAS,CAAC,EAAE;QACV,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;KACzB,CAAC;CACH,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;CACpB,CAAC;AAEN,eAAO,MAAM,cAAc,GAAU,OAAO;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,OAAO,KAAK,CAAC;IACzB,UAAU,EAAE,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAC7C,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG,OAAO,CAAC,OAAO,CA4ElB,CAAC"}
1
+ {"version":3,"file":"executeRequest.d.ts","sourceRoot":"","sources":["../../../src/controller/executeRequest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,+BAA+B,CAAC;AACxF,OAAO,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAU7F,OAAO,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAIzE,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3C,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,KAAK,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACzD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE;QACL,SAAS,CAAC,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAC;QAC3C,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;KACzB,CAAC;IACF,SAAS,CAAC,EAAE;QACV,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE;QACJ,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;KACzB,CAAC;CACH,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;CACpB,GACD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB,eAAO,MAAM,cAAc,GAAU,OAAO;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,OAAO,KAAK,CAAC;IACzB,UAAU,EAAE,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAC7C,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG,OAAO,CAAC,OAAO,CA+ElB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"executeSubmission.d.ts","sourceRoot":"","sources":["../../../src/controller/executeSubmission.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,KAAK,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAGzE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,OAAO,EAAkB,MAAM,kBAAkB,CAAC;AAE3D,eAAO,MAAM,iBAAiB,GAAU,OAAO;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAC7C,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG,OAAO,CAAC,OAAO,CA0ElB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,MAAM,EAAE,OAAO,KAAK,KAAG,OAKrE,CAAC"}
1
+ {"version":3,"file":"executeSubmission.d.ts","sourceRoot":"","sources":["../../../src/controller/executeSubmission.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,KAAK,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAGzE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,OAAO,EAAkB,MAAM,kBAAkB,CAAC;AAE3D,eAAO,MAAM,iBAAiB,GAAU,OAAO;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAC7C,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG,OAAO,CAAC,OAAO,CA2ElB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,MAAM,EAAE,OAAO,KAAK,KAAG,OAKrE,CAAC"}
@@ -1,8 +1,8 @@
1
1
  import { JsonElement } from '@wise/dynamic-flow-types/spec';
2
- declare const responseTypes: readonly ["step", "action", "exit", "modal", "subflow"];
2
+ declare const responseTypes: readonly ["step", "action", "exit", "modal", "subflow", "no-op"];
3
3
  export type ResponseType = (typeof responseTypes)[number];
4
4
  /**
5
- * Returns either 'step', 'action', or 'exit' based on the response headers and body.
5
+ * Returns either 'step', 'action', 'exit', 'modal', 'subflow', or 'no-op' based on the response headers and body.
6
6
  * This function takes a response body parameter because, for legacy reasons, we still need to consider the body
7
7
  * to determine the response type, in cases where the response headers are not set.
8
8
  * Ideally it should just be a matter of checking the "X-Df-Response-Type" response header.
@@ -1 +1 @@
1
- {"version":3,"file":"getResponseType.d.ts","sourceRoot":"","sources":["../../../src/controller/getResponseType.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAG5D,QAAA,MAAM,aAAa,yDAA0D,CAAC;AAC9E,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,EAAE,MAAM,WAAW,KAAG,YAYrE,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,SAAS,OAAO,KAAG,YAAY,GAAG,SAY3E,CAAC"}
1
+ {"version":3,"file":"getResponseType.d.ts","sourceRoot":"","sources":["../../../src/controller/getResponseType.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAG5D,QAAA,MAAM,aAAa,kEAAmE,CAAC;AACvF,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,EAAE,MAAM,WAAW,KAAG,YAYrE,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,SAAS,OAAO,KAAG,YAAY,GAAG,SAY3E,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wise/dynamic-flow-client",
3
- "version": "5.9.2",
3
+ "version": "5.10.0",
4
4
  "description": "Dynamic Flow web client",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./build/main.js",
@@ -73,8 +73,8 @@
73
73
  "typescript": "5.9.3",
74
74
  "vitest": "4.0.18",
75
75
  "vitest-fetch-mock": "0.4.5",
76
- "@wise/dynamic-flow-fixtures": "0.0.1",
77
- "@wise/dynamic-flow-renderers": "0.0.0"
76
+ "@wise/dynamic-flow-renderers": "0.0.0",
77
+ "@wise/dynamic-flow-fixtures": "0.0.1"
78
78
  },
79
79
  "peerDependencies": {
80
80
  "@transferwise/components": "^46.104.0",
@@ -87,7 +87,7 @@
87
87
  "react-intl": "^6 || ^7"
88
88
  },
89
89
  "dependencies": {
90
- "@wise/dynamic-flow-types": "4.9.0"
90
+ "@wise/dynamic-flow-types": "4.10.0"
91
91
  },
92
92
  "scripts": {
93
93
  "dev": "EXCLUDE_VISUAL_TESTS=true pnpm storybook dev -p 3003",