@getuserfeedback/react-native 2.0.14 → 2.0.15

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/dist/index.d.ts CHANGED
@@ -27,6 +27,8 @@ type TrackOptions = {
27
27
  };
28
28
  type OpenOptions = Pick<OpenCommand, "hideCloseButton">;
29
29
  type PrerenderOptions = Pick<PrerenderCommand, "hideCloseButton">;
30
+ export type OpenFlowOptions = Pick<OpenCommand, "hideCloseButton" | "metadata">;
31
+ export type PrerenderFlowOptions = Pick<PrerenderCommand, "hideCloseButton">;
30
32
  type HostContext = Extract<PublicCommandPayload, {
31
33
  kind: "updateHostContext";
32
34
  }>["context"];
@@ -39,6 +41,13 @@ export interface ReactNativeCommandResult {
39
41
  export interface ReactNativeCommandOptions {
40
42
  idempotencyKey?: string;
41
43
  }
44
+ export interface ReactNativeFlowRun {
45
+ readonly flowId: string;
46
+ open: (options?: OpenFlowOptions) => Promise<void>;
47
+ prefetch: () => Promise<void>;
48
+ prerender: (options?: PrerenderFlowOptions) => Promise<void>;
49
+ close: () => Promise<void>;
50
+ }
42
51
  export interface ReactNativeGetUserFeedbackClient {
43
52
  readonly instanceId: string;
44
53
  readonly nativeMessages: readonly ReactNativeWidgetNativeMessage[];
@@ -53,6 +62,7 @@ export interface ReactNativeGetUserFeedbackClient {
53
62
  open: (flowId: string, options?: OpenOptions, commandOptions?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
54
63
  prefetch: (flowId: string, options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
55
64
  prerender: (flowId: string, options?: PrerenderOptions, commandOptions?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
65
+ flow: (flowId: string) => ReactNativeFlowRun;
56
66
  close: (flowHandleId?: string, options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
57
67
  reset: (options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
58
68
  updateHostContext: (context: HostContext, options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { assertNoSegmentExternalIdsInValueArgument, CommandSettlementTimeoutError, getWidgetLayoutPlanNowMs, } from "@getuserfeedback/protocol/host";
12
+ import { assertNoSegmentExternalIdsInValueArgument, CommandSettlementError, CommandSettlementTimeoutError, createFlowCommandHandleController, getFlowHandleFromSettlementDetail, getWidgetLayoutPlanNowMs, } from "@getuserfeedback/protocol/host";
13
13
  import { parseWebViewTransportNativeMessage } from "@getuserfeedback/protocol/webview-transport";
14
14
  import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
15
15
  import { useResolvedHostViewport } from "./host-viewport.js";
@@ -26,6 +26,102 @@ const GetUserFeedbackNativeContext = createContext(null);
26
26
  const DEFAULT_COMMAND_TIMEOUT_MS = 30000;
27
27
  const PROVIDER_UNMOUNTED_ERROR_MESSAGE = "GetUserFeedbackProvider unmounted before command settled";
28
28
  const STARTUP_COMMAND_REPLACED_ERROR_MESSAGE = "GetUserFeedbackProvider replaced startup command before settlement";
29
+ const STALE_HANDLE_ERROR_CODE = "STALE_HANDLE";
30
+ const isStaleHandleError = (error) => {
31
+ if (error instanceof CommandSettlementError) {
32
+ return error.code === STALE_HANDLE_ERROR_CODE;
33
+ }
34
+ if (!(error instanceof Error)) {
35
+ return false;
36
+ }
37
+ return error.message.includes("flowHandleId is stale");
38
+ };
39
+ const toFlowHandleIdFromResult = (result) => {
40
+ if (!result.settlement) {
41
+ return undefined;
42
+ }
43
+ const handle = getFlowHandleFromSettlementDetail(result.settlement);
44
+ return handle === null || handle === void 0 ? void 0 : handle.flowHandleId;
45
+ };
46
+ const createReactNativeFlowRunController = (flowId, { enqueueFlowCommand, enqueueScopedClose, reportCommandError, }) => {
47
+ let pendingClose = false;
48
+ const handleClient = createFlowCommandHandleController({
49
+ getHandleIdFromSettlement: toFlowHandleIdFromResult,
50
+ isStaleHandleError,
51
+ onClearResolvedHandle: () => { },
52
+ });
53
+ const reportAsyncCommandError = (promise) => promise.catch((error) => {
54
+ reportCommandError(error instanceof Error ? error : new Error(String(error)));
55
+ throw error;
56
+ });
57
+ const clearResolvedHandle = () => {
58
+ handleClient.clearResolvedHandle();
59
+ };
60
+ const reset = () => {
61
+ pendingClose = false;
62
+ handleClient.reset();
63
+ };
64
+ const resetIdleHandle = () => {
65
+ if (handleClient.hasPendingCommands()) {
66
+ return;
67
+ }
68
+ reset();
69
+ };
70
+ const enqueueAndWaitForSettlement = async (buildPayload) => {
71
+ try {
72
+ await handleClient.runCommand(buildPayload, enqueueFlowCommand);
73
+ }
74
+ catch (error) {
75
+ pendingClose = false;
76
+ throw error;
77
+ }
78
+ };
79
+ handleClient.subscribeHandle((flowHandleId) => {
80
+ if (!pendingClose) {
81
+ return;
82
+ }
83
+ pendingClose = false;
84
+ reportAsyncCommandError(enqueueScopedClose(flowHandleId)).catch(() => { });
85
+ });
86
+ return {
87
+ flowId,
88
+ open: (options) => reportAsyncCommandError(enqueueAndWaitForSettlement((flowHandleId) => ({
89
+ kind: "open",
90
+ flowId,
91
+ flowHandleId,
92
+ metadata: options === null || options === void 0 ? void 0 : options.metadata,
93
+ hideCloseButton: options === null || options === void 0 ? void 0 : options.hideCloseButton,
94
+ }))),
95
+ prefetch: () => reportAsyncCommandError(enqueueAndWaitForSettlement((flowHandleId) => ({
96
+ kind: "prefetch",
97
+ flowId,
98
+ flowHandleId,
99
+ }))),
100
+ prerender: (options) => reportAsyncCommandError(enqueueAndWaitForSettlement((flowHandleId) => ({
101
+ kind: "prerender",
102
+ flowId,
103
+ flowHandleId,
104
+ hideCloseButton: options === null || options === void 0 ? void 0 : options.hideCloseButton,
105
+ }))),
106
+ close: () => {
107
+ const flowHandleId = handleClient.getHandleId();
108
+ if (!flowHandleId) {
109
+ pendingClose = handleClient.hasPendingCommands();
110
+ return Promise.resolve();
111
+ }
112
+ if (handleClient.hasPendingCommands()) {
113
+ pendingClose = true;
114
+ return Promise.resolve();
115
+ }
116
+ pendingClose = false;
117
+ return reportAsyncCommandError(enqueueScopedClose(flowHandleId));
118
+ },
119
+ clearResolvedHandle,
120
+ hasFlowHandleId: (flowHandleId) => handleClient.getHandleId() === flowHandleId,
121
+ reset,
122
+ resetIdleHandle,
123
+ };
124
+ };
29
125
  const CLIENT_META = {
30
126
  clientName: "@getuserfeedback/react-native",
31
127
  clientVersion: REACT_NATIVE_SDK_VERSION,
@@ -124,6 +220,8 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
124
220
  const startupOptionsKey = useMemo(() => toJsonKey({ configureOptionsKey, initOptionsKey }), [configureOptionsKey, initOptionsKey]);
125
221
  const startupIdempotencyKey = useMemo(() => toStableHash({ configureOptionsKey, initOptionsKey }), [configureOptionsKey, initOptionsKey]);
126
222
  const widgetHostLifecycleRef = useRef(createProviderWidgetHostLifecycleController(hostSourceKey));
223
+ const flowRunsRef = useRef(new Map());
224
+ const flowRunControllerOptionsRef = useRef(null);
127
225
  const widgetHostLifecycle = widgetHostLifecycleRef.current;
128
226
  const { clearFlowHandleFromVisibility, getCurrentHostEpoch, isWidgetHostVisible: resolveIsWidgetHostVisible, markNotReady, markReady, syncHostSourceKey, toFlowStateVisibility, toReadyVisibility, } = widgetHostLifecycle;
129
227
  const createCommandTimeoutError = useCallback((requestId) => new CommandSettlementTimeoutError(requestId), []);
@@ -133,9 +231,20 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
133
231
  getCurrentHostEpoch,
134
232
  });
135
233
  const { nativeMessages, nativeMessagesRef, clearStartupQueuedKey, createPendingCommandPromise, getPendingCommand, getPendingCommandEntries, hasDeliveredPendingCommands, isStartupQueuedKey, pausePendingCommandTimeouts, registerNativeMessage, reissuePendingCommand, removeNativeMessageId, replaceNativeMessages, resetStartupQueuedKeyIfChanged, setStartupQueuedKey, startPendingCommandTimeouts, takePendingCommand, cancelStartupCommands, dispose: disposeCommandTransport, } = transport;
234
+ const clearIdleFlowHandles = useCallback(() => {
235
+ for (const flowRun of flowRunsRef.current.values()) {
236
+ flowRun.resetIdleHandle();
237
+ }
238
+ }, []);
239
+ const resetFlowRuns = useCallback(() => {
240
+ for (const flowRun of flowRunsRef.current.values()) {
241
+ flowRun.reset();
242
+ }
243
+ }, []);
136
244
  if (syncHostSourceKey(hostSourceKey)) {
137
245
  pausePendingCommandTimeouts();
138
246
  clearStartupQueuedKey();
247
+ clearIdleFlowHandles();
139
248
  }
140
249
  initOptionsRef.current = initOptions;
141
250
  configureOptionsRef.current = configureOptions;
@@ -152,7 +261,7 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
152
261
  throw error;
153
262
  }, 0);
154
263
  }, [onCommandError]);
155
- const enqueueTransportCommand = useCallback((command, options, { isStartup = false, prioritizeStartup = false, } = {}) => {
264
+ const enqueueTransportCommand = useCallback((command, options, { isStartup = false, preserveFlowHandle = false, prioritizeStartup = false, } = {}) => {
156
265
  var _a;
157
266
  const requestId = createCommandRequestId();
158
267
  const idempotencyKey = (_a = normalizeOptionalString(options === null || options === void 0 ? void 0 : options.idempotencyKey)) !== null && _a !== void 0 ? _a : requestId;
@@ -162,7 +271,9 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
162
271
  requestId,
163
272
  idempotencyKey,
164
273
  clientMeta: CLIENT_META,
165
- command: toReactNativeOneShotCommand(command),
274
+ command: preserveFlowHandle
275
+ ? command
276
+ : toReactNativeOneShotCommand(command),
166
277
  };
167
278
  const message = {
168
279
  kind: "enqueueCommand",
@@ -266,6 +377,50 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
266
377
  enqueueStartupCommands();
267
378
  return enqueueTransportCommand(command, options);
268
379
  }, [enqueueStartupCommands, enqueueTransportCommand]);
380
+ const enqueueFlowCommand = useCallback((command) => {
381
+ enqueueStartupCommands();
382
+ return enqueueTransportCommand(command, undefined, {
383
+ preserveFlowHandle: true,
384
+ });
385
+ }, [enqueueStartupCommands, enqueueTransportCommand]);
386
+ const enqueueScopedFlowClose = useCallback((flowHandleId) => enqueueCommand({ kind: "close", flowHandleId }).then(() => { }), [enqueueCommand]);
387
+ flowRunControllerOptionsRef.current = {
388
+ enqueueFlowCommand,
389
+ enqueueScopedClose: enqueueScopedFlowClose,
390
+ reportCommandError,
391
+ };
392
+ const flow = useCallback((flowId) => {
393
+ const normalizedFlowId = resolveRequiredString("flowId", flowId);
394
+ const existing = flowRunsRef.current.get(normalizedFlowId);
395
+ if (existing) {
396
+ return existing;
397
+ }
398
+ const created = createReactNativeFlowRunController(normalizedFlowId, {
399
+ enqueueFlowCommand: (command) => {
400
+ const currentOptions = flowRunControllerOptionsRef.current;
401
+ if (!currentOptions) {
402
+ return Promise.reject(new Error(PROVIDER_UNMOUNTED_ERROR_MESSAGE));
403
+ }
404
+ return currentOptions.enqueueFlowCommand(command);
405
+ },
406
+ enqueueScopedClose: (flowHandleId) => {
407
+ const currentOptions = flowRunControllerOptionsRef.current;
408
+ if (!currentOptions) {
409
+ return Promise.reject(new Error(PROVIDER_UNMOUNTED_ERROR_MESSAGE));
410
+ }
411
+ return currentOptions.enqueueScopedClose(flowHandleId);
412
+ },
413
+ reportCommandError: (error) => {
414
+ const currentOptions = flowRunControllerOptionsRef.current;
415
+ if (!currentOptions) {
416
+ return;
417
+ }
418
+ currentOptions.reportCommandError(error);
419
+ },
420
+ });
421
+ flowRunsRef.current.set(normalizedFlowId, created);
422
+ return created;
423
+ }, []);
269
424
  const handleWidgetReady = useCallback((event) => {
270
425
  const readyResult = markReady(event, {
271
426
  hasDeliveredPendingCommands: hasDeliveredPendingCommands(),
@@ -276,6 +431,7 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
276
431
  let didReissueDeliveredCommands = false;
277
432
  if (readyResult.didResetHostEpoch) {
278
433
  clearStartupQueuedKey();
434
+ clearIdleFlowHandles();
279
435
  didReissueDeliveredCommands = reissueDeliveredPendingActionCommands();
280
436
  }
281
437
  setWidgetHostVisibility((visibility) => toReadyVisibility(visibility, event));
@@ -285,6 +441,7 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
285
441
  }, [
286
442
  enqueueStartupCommands,
287
443
  clearStartupQueuedKey,
444
+ clearIdleFlowHandles,
288
445
  hasDeliveredPendingCommands,
289
446
  markReady,
290
447
  reissueDeliveredPendingActionCommands,
@@ -304,18 +461,25 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
304
461
  const notReadyResult = markNotReady();
305
462
  if (notReadyResult.wasReady) {
306
463
  clearStartupQueuedKey();
464
+ clearIdleFlowHandles();
307
465
  reissueDeliveredPendingActionCommands();
308
466
  }
309
467
  setWidgetHostVisibility(HIDDEN_PROVIDER_WIDGET_HOST_VISIBILITY);
310
468
  pausePendingCommandTimeouts();
311
469
  }, [
312
470
  clearStartupQueuedKey,
471
+ clearIdleFlowHandles,
313
472
  handleWidgetReady,
314
473
  markNotReady,
315
474
  pausePendingCommandTimeouts,
316
475
  reissueDeliveredPendingActionCommands,
317
476
  ]);
318
477
  const handleFlowHandleInvalidated = useCallback((flowHandleId) => {
478
+ for (const flowRun of flowRunsRef.current.values()) {
479
+ if (flowRun.hasFlowHandleId(flowHandleId)) {
480
+ flowRun.clearResolvedHandle();
481
+ }
482
+ }
319
483
  setWidgetHostVisibility((visibility) => clearFlowHandleFromVisibility(visibility, flowHandleId));
320
484
  }, [clearFlowHandleFromVisibility]);
321
485
  const handleInstanceFlowStateChanged = useCallback((detail) => {
@@ -360,6 +524,9 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
360
524
  else {
361
525
  const settlement = message.event
362
526
  .detail;
527
+ if (pendingCommand.result.envelope.command.kind === "reset") {
528
+ resetFlowRuns();
529
+ }
363
530
  const settledResult = Object.assign(Object.assign({}, pendingCommand.result), { settlement, settlementResult: settlement.result });
364
531
  pendingCommand.resolve(settledResult);
365
532
  }
@@ -368,6 +535,7 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
368
535
  onWebMessage === null || onWebMessage === void 0 ? void 0 : onWebMessage(message);
369
536
  }, [
370
537
  clearStartupQueuedKey,
538
+ resetFlowRuns,
371
539
  getCurrentHostEpoch,
372
540
  getPendingCommand,
373
541
  handleInstanceFlowStateChanged,
@@ -377,6 +545,7 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
377
545
  takePendingCommand,
378
546
  ]);
379
547
  useEffect(() => () => {
548
+ flowRunControllerOptionsRef.current = null;
380
549
  disposeCommandTransport(new Error(PROVIDER_UNMOUNTED_ERROR_MESSAGE));
381
550
  }, [disposeCommandTransport]);
382
551
  const getNativeMessages = useCallback(() => nativeMessagesRef.current, [nativeMessagesRef]);
@@ -422,6 +591,7 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
422
591
  hideCloseButton: options === null || options === void 0 ? void 0 : options.hideCloseButton,
423
592
  }, commandOptions);
424
593
  },
594
+ flow,
425
595
  close: (flowHandleId, options) => enqueueCommand(Object.assign({ kind: "close" }, (flowHandleId === undefined
426
596
  ? {}
427
597
  : {
@@ -431,7 +601,7 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
431
601
  updateHostContext: (context, options) => enqueueCommand({ kind: "updateHostContext", context }, options),
432
602
  emitHostSignal: (name, data, options) => enqueueCommand(Object.assign({ kind: "emitHostSignal", name: resolveRequiredString("name", name) }, (data === undefined ? {} : { data })), options),
433
603
  handleWebMessage,
434
- }), [enqueueCommand, getNativeMessages, handleWebMessage, instanceId]);
604
+ }), [enqueueCommand, flow, getNativeMessages, handleWebMessage, instanceId]);
435
605
  const previousSheetHostLayoutUpdateRef = useRef(undefined);
436
606
  useEffect(() => {
437
607
  resetStartupQueuedKeyIfChanged(startupOptionsKey);
package/dist/version.js CHANGED
@@ -3,4 +3,4 @@ const reactNativeSdkVersion = typeof __GX_REACT_NATIVE_SDK_VERSION__ === "string
3
3
  : "";
4
4
  // Build scripts patch this fallback to the package version for published artifacts.
5
5
  // Source-linked workspace usage keeps the local fallback.
6
- export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "2.0.14";
6
+ export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "2.0.15";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getuserfeedback/react-native",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "description": "getuserfeedback React Native SDK",
5
5
  "keywords": [
6
6
  "getuserfeedback",
@@ -44,8 +44,8 @@
44
44
  "lint": "biome check ."
45
45
  },
46
46
  "dependencies": {
47
- "@getuserfeedback/protocol": "^3.1.3",
48
- "@getuserfeedback/sdk": "^0.8.20",
47
+ "@getuserfeedback/protocol": "^3.1.4",
48
+ "@getuserfeedback/sdk": "^0.8.21",
49
49
  "robot3": "^1.2.0"
50
50
  },
51
51
  "peerDependencies": {