@getuserfeedback/react-native 5.14.2 → 5.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,22 @@ export function App() {
45
45
 
46
46
  That is enough to receive flows published for automatic Widget delivery.
47
47
 
48
+ ## Native color scheme
49
+
50
+ For flows using the native renderer, set `clientOptions.colorScheme` to
51
+ `"light"`, `"dark"`, or `"system"`. Omitted values and browser auto-detect
52
+ objects follow the device appearance on native (light when unavailable).
53
+
54
+ Scheme-only prop changes and `client.configure({ colorScheme: "dark" })`
55
+ repaint the SDK's default native palette without restarting the flow or
56
+ clearing its draft. An unchanged prop does not override a later configure call.
57
+ The selection belongs to that client: runtime recreation retains it, while a
58
+ replacement client (for example, a new API key) starts from its initial prop.
59
+ Native repaint does not wait for Core: configure still forwards to Core and
60
+ retains its existing promise/error behavior, but remote failure does not roll
61
+ back native appearance. WebView flows keep their existing scheme forwarding
62
+ and browser auto-detection behavior. This does not add a style override API.
63
+
48
64
  - [React Native SDK guide](https://getuserfeedback.com/docs/get-started/react-native-sdk)
49
65
  - [React Native SDK reference](https://getuserfeedback.com/docs/reference/react-native-sdk-reference)
50
66
  - [Package versions](https://www.npmjs.com/package/@getuserfeedback/react-native?activeTab=versions)
@@ -0,0 +1,31 @@
1
+ export declare const NativeColorSchemeContext: import("react").Context<import("@getuserfeedback/protocol/host").ColorSchemeConfig | undefined>;
2
+ export declare function useNativeColorScheme(): {
3
+ scheme: "dark" | "light";
4
+ palette: {
5
+ background: string;
6
+ text: string;
7
+ muted: string;
8
+ input: string;
9
+ border: string;
10
+ placeholder: string;
11
+ button: string;
12
+ buttonText: string;
13
+ accent: string;
14
+ disabled: string;
15
+ disabledText: string;
16
+ error: string;
17
+ } | {
18
+ background: string;
19
+ text: string;
20
+ muted: string;
21
+ input: string;
22
+ border: string;
23
+ placeholder: string;
24
+ button: string;
25
+ buttonText: string;
26
+ accent: string;
27
+ disabled: string;
28
+ disabledText: string;
29
+ error: string;
30
+ };
31
+ };
@@ -0,0 +1,44 @@
1
+ import { createContext, useContext } from "react";
2
+ // Host-local presentation only; never the acknowledged Core configuration.
3
+ export const NativeColorSchemeContext = createContext(undefined);
4
+ const palettes = {
5
+ light: {
6
+ background: "#ffffff",
7
+ text: "#17202a",
8
+ muted: "#475569",
9
+ input: "#ffffff",
10
+ border: "#64748b",
11
+ placeholder: "#64748b",
12
+ button: "#e7eefb",
13
+ buttonText: "#17202a",
14
+ accent: "#2563eb",
15
+ disabled: "#e5e7eb",
16
+ disabledText: "#4b5563",
17
+ error: "#aa0000",
18
+ },
19
+ dark: {
20
+ background: "#111827",
21
+ text: "#f3f4f6",
22
+ muted: "#cbd5e1",
23
+ input: "#1f2937",
24
+ border: "#94a3b8",
25
+ placeholder: "#94a3b8",
26
+ button: "#1e3a5f",
27
+ buttonText: "#f8fafc",
28
+ accent: "#93c5fd",
29
+ disabled: "#374151",
30
+ disabledText: "#cbd5e1",
31
+ error: "#fca5a5",
32
+ },
33
+ };
34
+ export function useNativeColorScheme() {
35
+ const requested = useContext(NativeColorSchemeContext);
36
+ const device = require("react-native").useColorScheme();
37
+ // Browser auto-detection has no DOM on native; use the device preference.
38
+ const scheme = requested === "light" || requested === "dark"
39
+ ? requested
40
+ : device === "dark"
41
+ ? "dark"
42
+ : "light";
43
+ return { scheme, palette: palettes[scheme] };
44
+ }
@@ -12,7 +12,15 @@ class NativeCoreCommandSessionClosedError extends Error {
12
12
  function toCommandSettledDetail(settlement) {
13
13
  var _a, _b;
14
14
  if (settlement.ok) {
15
- return Object.assign({ instanceId: (_a = settlement.instanceId) !== null && _a !== void 0 ? _a : null, kind: settlement.kind, ok: true, requestId: settlement.requestId }, (settlement.result === undefined ? {} : { result: settlement.result }));
15
+ const detail = {
16
+ instanceId: (_a = settlement.instanceId) !== null && _a !== void 0 ? _a : null,
17
+ kind: settlement.kind,
18
+ ok: true,
19
+ requestId: settlement.requestId,
20
+ };
21
+ if (settlement.result !== undefined)
22
+ detail.result = settlement.result;
23
+ return detail;
16
24
  }
17
25
  return {
18
26
  error: settlement.error,
@@ -0,0 +1,8 @@
1
+ import type { NativeViewPresentationMode } from "./native-view-presentation-surface.js";
2
+ import type { NativeViewRecord, NativeViewRecordController } from "./native-view-record-controller.js";
3
+ /** Thin native editor: drafts only. Core owns validation, navigation and submission. */
4
+ export declare function NativeFlowContent({ controller, presentationMode, record, }: {
5
+ controller: NativeViewRecordController;
6
+ presentationMode?: NativeViewPresentationMode;
7
+ record: NativeViewRecord;
8
+ }): import("react").ReactElement<Record<string, unknown>, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,188 @@
1
+ import { createElement, useLayoutEffect, useRef, useState, } from "react";
2
+ import { useNativeColorScheme } from "./native-color-scheme.js";
3
+ /** Thin native editor: drafts only. Core owns validation, navigation and submission. */
4
+ export function NativeFlowContent({ controller, presentationMode = "overlay", record, }) {
5
+ const flow = record.nativeFlow;
6
+ if (!flow)
7
+ throw new Error("Native content requires a native allocation");
8
+ const { projection, interaction } = flow;
9
+ const { View, Text, TextInput, Pressable, ScrollView, useWindowDimensions, } = require("react-native");
10
+ const { height: windowHeight } = useWindowDimensions();
11
+ const { scheme, palette } = useNativeColorScheme();
12
+ const [draft, setDraft] = useState(projection.answers);
13
+ const rawDraft = useRef(draft);
14
+ const scroll = useRef(null);
15
+ const editorPage = useRef({
16
+ allocationId: record.allocationId,
17
+ nodeId: projection.nodeId,
18
+ });
19
+ const binding = useRef(null);
20
+ useLayoutEffect(() => {
21
+ const mounted = controller.attachNativeContent({
22
+ viewId: record.viewId,
23
+ allocationId: record.allocationId,
24
+ });
25
+ binding.current = mounted;
26
+ return () => {
27
+ binding.current = null;
28
+ mounted === null || mounted === void 0 ? void 0 : mounted.dispose();
29
+ };
30
+ }, [controller, record.viewId, record.allocationId]);
31
+ useLayoutEffect(() => {
32
+ var _a;
33
+ if (editorPage.current.allocationId === record.allocationId &&
34
+ editorPage.current.nodeId === projection.nodeId)
35
+ return;
36
+ editorPage.current = {
37
+ allocationId: record.allocationId,
38
+ nodeId: projection.nodeId,
39
+ };
40
+ rawDraft.current = projection.answers;
41
+ setDraft(projection.answers);
42
+ (_a = scroll.current) === null || _a === void 0 ? void 0 : _a.scrollTo({ x: 0, y: 0, animated: false });
43
+ }, [projection, record.allocationId]);
44
+ const update = (key, value) => {
45
+ const nextDraft = Object.assign(Object.assign({}, rawDraft.current), { [key]: value });
46
+ if (!controller.interactNativeFlow({
47
+ viewId: record.viewId,
48
+ allocationId: record.allocationId,
49
+ projection,
50
+ intent: "draft",
51
+ draft: nextDraft,
52
+ }))
53
+ return;
54
+ rawDraft.current = nextDraft;
55
+ setDraft(rawDraft.current);
56
+ };
57
+ const interact = (intent) => controller.interactNativeFlow({
58
+ viewId: record.viewId,
59
+ allocationId: record.allocationId,
60
+ projection,
61
+ intent,
62
+ draft: Object.assign({}, rawDraft.current),
63
+ });
64
+ const disabled = interaction === "action" || !record.isActive;
65
+ const button = (label, onPress, buttonDisabled = interaction !== "idle" || !record.isActive) => createElement(Pressable, {
66
+ key: label,
67
+ accessibilityRole: "button",
68
+ accessibilityLabel: label,
69
+ disabled: buttonDisabled,
70
+ onPress,
71
+ style: {
72
+ padding: 12,
73
+ backgroundColor: buttonDisabled ? palette.disabled : palette.button,
74
+ borderRadius: 6,
75
+ marginTop: 8,
76
+ },
77
+ }, createElement(Text, {
78
+ style: {
79
+ color: buttonDisabled ? palette.disabledText : palette.buttonText,
80
+ },
81
+ }, label));
82
+ return createElement(ScrollView, {
83
+ ref: scroll,
84
+ style: Object.assign(Object.assign({}, (presentationMode === "content"
85
+ ? { flexGrow: 0, flexShrink: 1, maxHeight: windowHeight }
86
+ : { flex: 1, maxHeight: "100%" })), { width: "100%", backgroundColor: palette.background }),
87
+ keyboardShouldPersistTaps: "handled",
88
+ nestedScrollEnabled: presentationMode === "content",
89
+ // Report the actual viewport, never the window ceiling or inner content.
90
+ // Content mode can measure before its auto-height parent has any height.
91
+ onLayout: (event) => { var _a; return (_a = binding.current) === null || _a === void 0 ? void 0 : _a.onLayout(event.nativeEvent.layout); },
92
+ }, createElement(View, {
93
+ testID: "native-flow-renderer",
94
+ style: { padding: 24, gap: 12 },
95
+ }, projection.mode === "simulated"
96
+ ? createElement(Text, {
97
+ testID: "native-flow-simulated",
98
+ style: { color: palette.muted },
99
+ }, "SIMULATED — no response is saved")
100
+ : null, ...projection.blocks.map((block) => {
101
+ var _a;
102
+ if (block.kind === "statement")
103
+ return createElement(View, { key: block.key }, ...block.paragraphs.map((text, index) => createElement(Text, { key: index, style: { color: palette.text } }, text)));
104
+ const value = draft[block.key];
105
+ const requiredLabel = block.required ? ", required" : "";
106
+ const selected = Array.isArray(value)
107
+ ? ((_a = value[0]) !== null && _a !== void 0 ? _a : "")
108
+ : (value !== null && value !== void 0 ? value : "");
109
+ return createElement(View, { key: block.key, style: { gap: 6 } }, createElement(Text, { style: { color: palette.text } }, block.label + (block.required ? " *" : "")), block.description
110
+ ? createElement(Text, { style: { color: palette.muted } }, block.description)
111
+ : null, block.kind === "text"
112
+ ? createElement(TextInput, {
113
+ accessibilityLabel: block.label + requiredLabel,
114
+ accessibilityHint: block.description,
115
+ value: selected,
116
+ placeholder: block.placeholder,
117
+ placeholderTextColor: palette.placeholder,
118
+ keyboardAppearance: scheme,
119
+ selectionColor: palette.accent,
120
+ cursorColor: palette.accent,
121
+ editable: !disabled,
122
+ maxLength: 4000,
123
+ onChangeText: (text) => update(block.key, text),
124
+ style: {
125
+ color: disabled ? palette.disabledText : palette.text,
126
+ backgroundColor: disabled ? palette.disabled : palette.input,
127
+ borderWidth: 1,
128
+ borderColor: palette.border,
129
+ borderRadius: 4,
130
+ padding: 10,
131
+ },
132
+ })
133
+ : createElement(View, {
134
+ accessibilityRole: "radiogroup",
135
+ accessibilityLabel: block.label + requiredLabel,
136
+ accessibilityHint: block.description,
137
+ accessible: false,
138
+ }, ...block.options.map((option) => createElement(Pressable, {
139
+ key: option.value,
140
+ accessibilityRole: "radio",
141
+ accessibilityLabel: `${block.label}: ${option.label}${requiredLabel}`,
142
+ accessibilityHint: block.description,
143
+ accessible: true,
144
+ accessibilityState: {
145
+ checked: selected === option.value,
146
+ disabled,
147
+ },
148
+ disabled,
149
+ onPress: () => update(block.key, option.value),
150
+ style: { padding: 10 },
151
+ }, createElement(Text, {
152
+ style: {
153
+ color: disabled ? palette.disabledText : palette.text,
154
+ },
155
+ }, `${selected === option.value ? "●" : "○"} ${option.label}`)))), projection.errors[block.key]
156
+ ? createElement(Text, { accessibilityRole: "alert", style: { color: palette.error } }, projection.errors[block.key])
157
+ : null);
158
+ }), ...projection.formErrors.map((text, index) => createElement(Text, {
159
+ key: `error-${index}`,
160
+ accessibilityRole: "alert",
161
+ style: { color: palette.error },
162
+ }, text)), ...projection.terminal.map((text, index) => createElement(Text, { key: `terminal-${index}`, style: { color: palette.text } }, text)), projection.status === "succeeded" &&
163
+ (projection.mode === "simulated" || projection.terminal.length === 0)
164
+ ? createElement(Text, { testID: "native-flow-complete", style: { color: palette.text } }, projection.mode === "simulated"
165
+ ? "SIMULATED completion — nothing saved"
166
+ : "Thank you")
167
+ : null, projection.canGoBack ? button("Back", () => interact("back")) : null, interaction === "draft-failed"
168
+ ? button("Retry refresh", () => interact("draft"), !record.isActive)
169
+ : null, projection.status !== "succeeded"
170
+ ? button(interaction === "action"
171
+ ? "Working…"
172
+ : projection.status === "ready-to-submit"
173
+ ? "Submit"
174
+ : "Continue", () => interact(projection.status === "ready-to-submit" ? "submit" : "continue"))
175
+ : null, projection.canClose
176
+ ? createElement(Pressable, {
177
+ accessibilityRole: "button",
178
+ accessibilityLabel: "Close",
179
+ disabled: !record.isActive,
180
+ onPress: () => controller.closeNativeFlow(record),
181
+ style: { padding: 12 },
182
+ }, createElement(Text, {
183
+ style: {
184
+ color: !record.isActive ? palette.disabledText : palette.text,
185
+ },
186
+ }, "Close"))
187
+ : null));
188
+ }
@@ -138,7 +138,11 @@ export const createNativeLinkRouterRpcHandler = (input) => {
138
138
  }
139
139
  let handled = false;
140
140
  try {
141
- invokeLinkRouter(router, Object.assign({ url: params.url }, (params.target === undefined ? {} : { target: params.target })));
141
+ const linkRequest = { url: params.url };
142
+ if (params.target !== undefined) {
143
+ linkRequest.target = params.target;
144
+ }
145
+ invokeLinkRouter(router, linkRequest);
142
146
  handled = true;
143
147
  }
144
148
  catch (error) {
@@ -4,10 +4,13 @@ import { type OpenRequestDispatcher } from "@getuserfeedback/sdk/internal/open-r
4
4
  export declare function createNativeProviderClientController(input: {
5
5
  instanceId: string;
6
6
  onError?: (error: Error) => void;
7
+ initialColorScheme?: ConfigureOptions["colorScheme"];
7
8
  }): {
8
9
  client: import("./client.js").Client;
9
10
  directSessionController: import("./native-direct-command-session-controller.js").NativeDirectCommandSessionController;
10
11
  flowRegistry: import("./native-provider-flow-registry.js").NativeProviderFlowRegistry;
12
+ getNativeColorScheme: () => import("@getuserfeedback/protocol/host").ColorSchemeConfig | undefined;
13
+ subscribeNativeColorScheme: (listener: () => void) => () => void;
11
14
  getRuntimeConfiguration: () => ConfigureOptions;
12
15
  observeOpenRequest: (payload: Parameters<OpenRequestDispatcher["dispatch"]>[0]) => ReturnType<OpenRequestDispatcher["dispatch"]>;
13
16
  dispose: () => void;
@@ -4,6 +4,9 @@ import { createNativeProviderClient, parseRequiredString, } from "./native-provi
4
4
  import { createNativeProviderCommandController } from "./native-provider-command-controller.js";
5
5
  import { createNativeProviderFlowRegistry } from "./native-provider-flow-registry.js";
6
6
  export function createNativeProviderClientController(input) {
7
+ // Presentation belongs to this client, independently of Core acknowledgement.
8
+ let nativeColorScheme = input.initialColorScheme;
9
+ const colorSchemeListeners = new Set();
7
10
  let runtimeConfiguration = {};
8
11
  const updateRuntimeConfiguration = (updates) => {
9
12
  runtimeConfiguration = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, runtimeConfiguration), (updates.auth === undefined ? {} : { auth: updates.auth })), (updates.capabilities === undefined
@@ -44,7 +47,15 @@ export function createNativeProviderClientController(input) {
44
47
  catch (error) {
45
48
  return commands.reject(error);
46
49
  }
47
- const settlement = await commands.runOperation((dispatch) => dispatch(canonicalCommand), options);
50
+ const settlement = await commands.runOperation((dispatch) => {
51
+ if (canonicalCommand.kind === "configure" &&
52
+ canonicalCommand.opts.colorScheme !== undefined) {
53
+ nativeColorScheme = canonicalCommand.opts.colorScheme;
54
+ for (const listener of colorSchemeListeners)
55
+ listener();
56
+ }
57
+ return dispatch(canonicalCommand);
58
+ }, options);
48
59
  if (canonicalCommand.kind === "configure" && settlement !== undefined) {
49
60
  updateRuntimeConfiguration(canonicalCommand.opts);
50
61
  }
@@ -57,9 +68,17 @@ export function createNativeProviderClientController(input) {
57
68
  client,
58
69
  directSessionController: session,
59
70
  flowRegistry: flows,
71
+ getNativeColorScheme: () => nativeColorScheme,
72
+ subscribeNativeColorScheme: (listener) => {
73
+ colorSchemeListeners.add(listener);
74
+ return () => {
75
+ colorSchemeListeners.delete(listener);
76
+ };
77
+ },
60
78
  getRuntimeConfiguration: () => (Object.assign({}, runtimeConfiguration)),
61
79
  observeOpenRequest,
62
80
  dispose: () => {
81
+ colorSchemeListeners.clear();
63
82
  flows.reset();
64
83
  commands.dispose();
65
84
  },
@@ -9,12 +9,13 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { parseInitOptions } from "@getuserfeedback/protocol";
12
+ import { DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS, parseInitOptions, } from "@getuserfeedback/protocol";
13
13
  import { createUniqueId } from "@getuserfeedback/protocol/host";
14
14
  import { createActionRegistry, } from "@getuserfeedback/sdk/internal/action-registry";
15
15
  import { resolveLinkRouter } from "@getuserfeedback/sdk/internal/link-router";
16
- import { createContext, createElement, Fragment, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
16
+ import { createContext, createElement, Fragment, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
17
17
  import { NativeStandardFlowContainer } from "./flow-container.js";
18
+ import { NativeColorSchemeContext } from "./native-color-scheme.js";
18
19
  import { NativeFlowContainerContext, } from "./native-flow-container-context.js";
19
20
  import { createNativeProviderClientController } from "./native-provider-client-controller.js";
20
21
  import { REACT_NATIVE_PINNED_CORE_URL } from "./native-runtime-endpoints.js";
@@ -87,15 +88,17 @@ const toInitFlags = (flags) => {
87
88
  const parseNativeInitOptions = (_a, hostActions) => {
88
89
  var { actions: _actions, capabilities, flags, links: _links } = _a, clientOptions = __rest(_a, ["actions", "capabilities", "flags", "links"]);
89
90
  const normalizedFlags = toInitFlags(flags);
90
- const initOptions = parseInitOptions(Object.assign(Object.assign(Object.assign(Object.assign({}, clientOptions), (normalizedFlags === undefined ? {} : { flags: normalizedFlags })), (hostActions !== undefined && hostActions.length > 0
91
- ? { hostActions }
92
- : {})), (capabilities !== undefined
93
- ? {
94
- hostCapabilities: capabilities.map((capability) => typeof capability === "string"
95
- ? { key: capability }
96
- : Object.assign({}, capability)),
97
- }
98
- : {})));
91
+ const initInput = Object.assign({}, clientOptions);
92
+ if (normalizedFlags !== undefined) {
93
+ initInput.flags = normalizedFlags;
94
+ }
95
+ if (hostActions !== undefined && hostActions.length > 0) {
96
+ initInput.hostActions = hostActions;
97
+ }
98
+ if (capabilities !== undefined) {
99
+ initInput.hostCapabilities = capabilities.map((capability) => typeof capability === "string" ? { key: capability } : Object.assign({}, capability));
100
+ }
101
+ const initOptions = parseInitOptions(initInput);
99
102
  const hostCapabilities = initOptions.hostCapabilities;
100
103
  const normalizedHostCapabilities = hostCapabilities === null || hostCapabilities === void 0 ? void 0 : hostCapabilities.map((capability) => ({
101
104
  capability,
@@ -121,7 +124,9 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
121
124
  const actionRegistry = useNativeActionRegistry(clientOptions.actions);
122
125
  const linkRouter = useNativeLinkRouter(clientOptions.links);
123
126
  const normalizedClientOptions = parseNativeInitOptions(clientOptions, actionRegistry.definitions);
124
- const clientOptionsKey = toClientOptionsKey(normalizedClientOptions);
127
+ const { colorScheme } = normalizedClientOptions, runtimeIdentityOptions = __rest(normalizedClientOptions, ["colorScheme"]);
128
+ const clientOptionsKey = toClientOptionsKey(runtimeIdentityOptions);
129
+ const colorSchemePropKey = toClientOptionsKey(colorScheme);
125
130
  const coreUrl = (_b = (_a = normalizedClientOptions.runtimeEndpoints) === null || _a === void 0 ? void 0 : _a.coreUrl) !== null && _b !== void 0 ? _b : REACT_NATIVE_PINNED_CORE_URL;
126
131
  const onErrorRef = useRef(onError);
127
132
  onErrorRef.current = onError;
@@ -138,14 +143,40 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
138
143
  apiKey: normalizedClientOptions.apiKey,
139
144
  hostCapabilities: normalizedClientOptions.hostCapabilities,
140
145
  });
141
- const controllerOwner = useMemo(() => ({
146
+ const createControllerOwner = () => ({
142
147
  identityKey: clientIdentityKey,
148
+ colorSchemePropKey,
143
149
  controller: createNativeProviderClientController({
144
150
  instanceId,
145
151
  onError: reportError,
152
+ initialColorScheme: colorScheme,
146
153
  }),
147
- }), [clientIdentityKey, instanceId, reportError]);
154
+ });
155
+ let [controllerOwner, setControllerOwner] = useState(createControllerOwner);
156
+ // Initialize the whole owner together, before committing a replacement client.
157
+ // Scheme-only prop changes update that owner without replacing it.
158
+ if (controllerOwner.identityKey !== clientIdentityKey) {
159
+ controllerOwner = createControllerOwner();
160
+ setControllerOwner(controllerOwner);
161
+ }
148
162
  const controller = controllerOwner.controller;
163
+ const hostColorScheme = useSyncExternalStore(controller.subscribeNativeColorScheme, controller.getNativeColorScheme, controller.getNativeColorScheme);
164
+ useLayoutEffect(() => {
165
+ if (controllerOwner.colorSchemePropKey === colorSchemePropKey)
166
+ return;
167
+ controllerOwner.colorSchemePropKey = colorSchemePropKey;
168
+ // Prop changes share configure's forwarding path, not runtime identity.
169
+ // Unchanged props must not undo a later imperative configure.
170
+ void controller.client
171
+ .configure({
172
+ colorScheme: colorScheme !== null && colorScheme !== void 0 ? colorScheme : {
173
+ autoDetectColorScheme: [...DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS],
174
+ },
175
+ })
176
+ .catch(() => {
177
+ // The controller reports remote failures; local appearance is retained.
178
+ });
179
+ }, [colorScheme, colorSchemePropKey, controller, controllerOwner]);
149
180
  const attachViewProjection = useCallback((nextProjection) => {
150
181
  setProjection(nextProjection);
151
182
  return () => setProjection((current) => current === nextProjection ? null : current);
@@ -197,7 +228,7 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
197
228
  }, [controller]);
198
229
  const getRuntimeConfiguration = useCallback(() => controller.getRuntimeConfiguration(), [controller]);
199
230
  const handleHandleInvalidated = useCallback((message) => controller.onHandleInvalidated(message), [controller]);
200
- return createElement(GetUserFeedbackContext.Provider, { value: controller.client }, createElement(NativeFlowContainerContext.Provider, { value: flowContainerContext }, createElement(Fragment, null, children, createElement(NativeStandardFlowContainer), createElement(NativeProviderRuntime, {
231
+ return createElement(NativeColorSchemeContext.Provider, { value: hostColorScheme }, createElement(GetUserFeedbackContext.Provider, { value: controller.client }, createElement(NativeFlowContainerContext.Provider, { value: flowContainerContext }, createElement(Fragment, null, children, createElement(NativeStandardFlowContainer), createElement(NativeProviderRuntime, {
201
232
  attachViewProjection,
202
233
  coreUrl,
203
234
  coreWebViewComponent: webViewComponent,
@@ -213,7 +244,7 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
213
244
  onHandleInvalidated: handleHandleInvalidated,
214
245
  observeOpenRequest: controller.observeOpenRequest,
215
246
  viewWebViewComponent: webViewComponent,
216
- }))));
247
+ })))));
217
248
  }
218
249
  /**
219
250
  * Returns the React Native SDK client from the nearest provider.
@@ -40,7 +40,9 @@ export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewCo
40
40
  const onStartupTerminalErrorRef = useRef(onStartupTerminalError);
41
41
  onStartupTerminalErrorRef.current = onStartupTerminalError;
42
42
  const createStartupCommandRef = useRef(createStartupCommand);
43
- createStartupCommandRef.current = createStartupCommand;
43
+ useLayoutEffect(() => {
44
+ createStartupCommandRef.current = createStartupCommand;
45
+ }, [createStartupCommand]);
44
46
  const hostActionsRef = useRef(hostActions);
45
47
  hostActionsRef.current = hostActions;
46
48
  const linkRouterRef = useRef(linkRouter);
@@ -11,9 +11,10 @@ export type NativeViewHostProjectionProps = {
11
11
  export type NativeViewProjection = Readonly<Omit<NativeViewHostProjectionProps, "presentationMode"> & {
12
12
  key: number;
13
13
  }>;
14
- export declare function NativeProjectedViewContent({ controller, onInvalidMessage, record, webViewComponent, }: {
14
+ export declare function NativeProjectedViewContent({ controller, onInvalidMessage, presentationMode, record, webViewComponent, }: {
15
15
  controller: NativeViewRecordController;
16
16
  onInvalidMessage?: (error: Error, value: unknown) => void;
17
+ presentationMode?: NativeViewPresentationMode;
17
18
  record: NativeViewRecord;
18
19
  webViewComponent?: ComponentType<Record<string, unknown>>;
19
20
  }): ReactElement;
@@ -1,7 +1,8 @@
1
1
  import { createElement, Fragment, useCallback, useEffect, useLayoutEffect, useRef, useSyncExternalStore, } from "react";
2
+ import { NativeFlowContent } from "./native-flow-content.js";
2
3
  import { NativeViewPresentationSurface } from "./native-view-presentation-surface.js";
3
4
  import { ViewWebViewHost } from "./view-webview-host.js";
4
- export function NativeProjectedViewContent({ controller, onInvalidMessage, record, webViewComponent, }) {
5
+ export function NativeProjectedViewContent({ controller, onInvalidMessage, presentationMode, record, webViewComponent, }) {
5
6
  const detachConnectionRef = useRef(null);
6
7
  const detachConnection = useCallback(() => {
7
8
  const detach = detachConnectionRef.current;
@@ -18,6 +19,12 @@ export function NativeProjectedViewContent({ controller, onInvalidMessage, recor
18
19
  }
19
20
  }, [controller, record.allocationId]);
20
21
  useEffect(() => detachConnection, [detachConnection]);
22
+ if (record.nativeFlow)
23
+ return createElement(NativeFlowContent, {
24
+ controller,
25
+ record,
26
+ presentationMode,
27
+ });
21
28
  return createElement(ViewWebViewHost, {
22
29
  generation: record.generation,
23
30
  onActivationRetryExhausted: (error) => {
@@ -76,6 +83,7 @@ function NativeViewHostProjectionEntry({ controller, nativeViewComponent, onInva
76
83
  }, createElement(NativeProjectedViewContent, {
77
84
  controller,
78
85
  onInvalidMessage,
86
+ presentationMode,
79
87
  record,
80
88
  webViewComponent,
81
89
  }));
@@ -24,13 +24,24 @@ const CONTENT_HOST_SURFACE_STYLE = {
24
24
  maxHeight: "100%",
25
25
  maxWidth: "100%",
26
26
  };
27
+ const NATIVE_CONTENT_PREPARATION_STYLE = {
28
+ position: "absolute",
29
+ top: 0,
30
+ left: 0,
31
+ right: 0,
32
+ opacity: 0,
33
+ };
34
+ const NATIVE_CONTENT_FRAME_STYLE = {
35
+ flexShrink: 1,
36
+ width: "100%",
37
+ };
27
38
  const TOP_PLACEMENT_STYLE = {
28
39
  justifyContent: "flex-start",
29
40
  };
30
41
  const BOTTOM_PLACEMENT_STYLE = {
31
42
  justifyContent: "flex-end",
32
43
  };
33
- const PREPARING_VIEW_FRAME_STYLE = {
44
+ const FILL_AVAILABLE_BOUNDS_STYLE = {
34
45
  height: "100%",
35
46
  width: "100%",
36
47
  };
@@ -63,9 +74,11 @@ export function NativeViewPresentationSurface({ children, presentationMode = "ov
63
74
  var _a;
64
75
  const NativeView = nativeViewComponent !== null && nativeViewComponent !== void 0 ? nativeViewComponent : resolveNativeView();
65
76
  const isPresented = record.isActive && record.size !== null;
66
- const hostStyle = presentationMode === "content" && isPresented
67
- ? CONTENT_HOST_SURFACE_STYLE
68
- : Object.assign(Object.assign(Object.assign({}, HOST_SURFACE_STYLE), (((_a = record.size) === null || _a === void 0 ? void 0 : _a.placement) === "docked-top"
77
+ const isNativeContent = presentationMode === "content" && !!record.nativeFlow;
78
+ const hostStyle = isNativeContent && !isPresented
79
+ ? NATIVE_CONTENT_PREPARATION_STYLE
80
+ : presentationMode === "content" && isPresented
81
+ ? Object.assign(Object.assign({}, CONTENT_HOST_SURFACE_STYLE), (isNativeContent ? NATIVE_CONTENT_FRAME_STYLE : {})) : Object.assign(Object.assign(Object.assign({}, HOST_SURFACE_STYLE), (((_a = record.size) === null || _a === void 0 ? void 0 : _a.placement) === "docked-top"
69
82
  ? TOP_PLACEMENT_STYLE
70
83
  : BOTTOM_PLACEMENT_STYLE)), (isPresented
71
84
  ? PRESENTED_HOST_SURFACE_STYLE
@@ -78,16 +91,20 @@ export function NativeViewPresentationSurface({ children, presentationMode = "ov
78
91
  testID: `gx-native-view-presentation-${record.viewId}`,
79
92
  }, createElement(NativeViewPresentationFrame, {
80
93
  children,
94
+ isNativeContent,
81
95
  nativeViewComponent,
82
96
  record,
83
97
  transitionSchedule,
84
98
  }));
85
99
  }
86
- function NativeViewPresentationFrame({ children, nativeViewComponent, record, transitionSchedule, }) {
100
+ function NativeViewPresentationFrame({ children, isNativeContent, nativeViewComponent, record, transitionSchedule, }) {
87
101
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
88
102
  const NativeView = nativeViewComponent !== null && nativeViewComponent !== void 0 ? nativeViewComponent : resolveNativeView();
89
103
  const isPresented = record.isActive && record.size !== null;
90
- const hasMeasuredFrame = isPresented || ((_a = record.size) === null || _a === void 0 ? void 0 : _a.layoutTransactionId) !== undefined;
104
+ // Native size reports describe this frame's viewport, not a content-size
105
+ // request. Keep it host-relative so a smaller layout cannot cap later growth.
106
+ const hasMeasuredFrame = !record.nativeFlow &&
107
+ (isPresented || ((_a = record.size) === null || _a === void 0 ? void 0 : _a.layoutTransactionId) !== undefined);
91
108
  const cornerRadii = toNativeWidgetCornerRadii((_b = record.size) === null || _b === void 0 ? void 0 : _b.frameAppearance);
92
109
  const shadow = toNativeWidgetFrameShadow(((_d = (_c = record.size) === null || _c === void 0 ? void 0 : _c.frameAppearance) === null || _d === void 0 ? void 0 : _d.kind) === "page"
93
110
  ? record.size.frameAppearance.resolvedBoxShadow
@@ -108,9 +125,13 @@ function NativeViewPresentationFrame({ children, nativeViewComponent, record, tr
108
125
  };
109
126
  const targetStyles = {
110
127
  border: Object.assign(Object.assign(Object.assign(Object.assign({}, VIEW_FRAME_BORDER_OVERLAY_STYLE), cornerRadiusStyle), borderStyle), frameOverlayShadowStyle),
111
- content: Object.assign(Object.assign({}, VIEW_FRAME_CONTENT_STYLE), cornerRadiusStyle),
128
+ content: Object.assign(Object.assign({}, (isNativeContent
129
+ ? { flexShrink: 1, overflow: "hidden" }
130
+ : VIEW_FRAME_CONTENT_STYLE)), cornerRadiusStyle),
112
131
  frame: hasMeasuredFrame
113
- ? Object.assign(Object.assign(Object.assign(Object.assign({}, MEASURED_VIEW_FRAME_STYLE), cornerRadiusStyle), frameShadowStyle), { height: (_g = record.size) === null || _g === void 0 ? void 0 : _g.height, width: (_h = record.size) === null || _h === void 0 ? void 0 : _h.width }) : PREPARING_VIEW_FRAME_STYLE,
132
+ ? Object.assign(Object.assign(Object.assign(Object.assign({}, MEASURED_VIEW_FRAME_STYLE), cornerRadiusStyle), frameShadowStyle), { height: (_g = record.size) === null || _g === void 0 ? void 0 : _g.height, width: (_h = record.size) === null || _h === void 0 ? void 0 : _h.width }) : isNativeContent
133
+ ? NATIVE_CONTENT_FRAME_STYLE
134
+ : FILL_AVAILABLE_BOUNDS_STYLE,
114
135
  };
115
136
  const frame = hasMeasuredFrame && record.size
116
137
  ? {
@@ -1,4 +1,5 @@
1
1
  import { type WidgetLayoutTransitionSchedule } from "@getuserfeedback/protocol/host";
2
+ import { type NativeFlowInteraction, type NativeFlowProjection } from "@getuserfeedback/protocol/internal/native-flow-session";
2
3
  import type { BrowserNavigationAvailabilityPayload } from "@getuserfeedback/protocol/internal/navigation-rpc";
3
4
  import { type EventViewSize } from "@getuserfeedback/protocol/internal/view-host-control-messages";
4
5
  import type { OpenRequestDispatcher } from "@getuserfeedback/sdk/internal/open-request-dispatcher";
@@ -8,6 +9,11 @@ type NativeViewSizeReport = Omit<EventViewSize, "gen" | "t" | "viewId"> & {
8
9
  transitionSchedule?: WidgetLayoutTransitionSchedule;
9
10
  };
10
11
  export type NativeViewRecord = Readonly<{
12
+ nativeFlow?: Readonly<{
13
+ projection: NativeFlowProjection;
14
+ interaction: "idle" | "draft" | "action" | "draft-failed";
15
+ pending: boolean;
16
+ }>;
11
17
  allocationId: number;
12
18
  flowId: string | null;
13
19
  generation: number;
@@ -18,6 +24,21 @@ export type NativeViewRecord = Readonly<{
18
24
  viewId: string;
19
25
  }>;
20
26
  export type NativeViewRecordController = {
27
+ attachNativeContent(input: {
28
+ allocationId: number;
29
+ viewId: string;
30
+ }): NativeContentBinding | null;
31
+ interactNativeFlow(input: {
32
+ allocationId: number;
33
+ viewId: string;
34
+ projection: NativeFlowProjection;
35
+ intent: NativeFlowInteraction["intent"];
36
+ draft: NativeFlowInteraction["draft"];
37
+ }): boolean;
38
+ closeNativeFlow(input: {
39
+ allocationId: number;
40
+ viewId: string;
41
+ }): void;
21
42
  attachViewConnection(input: {
22
43
  allocationId: number;
23
44
  connection: ViewWebViewHostConnection;
@@ -41,6 +62,13 @@ export type NativeViewRecordController = {
41
62
  getSnapshot(): readonly NativeViewRecord[];
42
63
  subscribe(listener: () => void): () => void;
43
64
  };
65
+ type NativeContentBinding = {
66
+ onLayout(size: {
67
+ width: number;
68
+ height: number;
69
+ }): void;
70
+ dispose(): void;
71
+ };
44
72
  type NativeOpenRequestObserver = (payload: Parameters<OpenRequestDispatcher["dispatch"]>[0]) => ReturnType<OpenRequestDispatcher["dispatch"]>;
45
73
  export declare function createNativeViewRecordController(input: {
46
74
  coreConnection: CoreWebViewHostConnection;
@@ -1,9 +1,29 @@
1
1
  import { createWidgetLayoutTransitionSchedule, } from "@getuserfeedback/protocol/host";
2
2
  import { coreViewHostActionSchema, } from "@getuserfeedback/protocol/internal/core-view-host-actions-v3";
3
+ import { actionPrerenderNativeFlowSchema, NATIVE_FLOW_SESSION_RPC_METHOD, nativeFlowInteractionSchema, nativeFlowProjectionSchema, } from "@getuserfeedback/protocol/internal/native-flow-session";
4
+ import { createGeneration } from "@getuserfeedback/protocol/internal/v3-generation";
5
+ import { buildRpcRequest, rpcRequestSchema, rpcResponseSchema, } from "@getuserfeedback/protocol/internal/v3-rpc-messages";
3
6
  import { actionRequestCloseViewSchema, actionViewConnectSchema, buildEventLayoutTransactionCommitted, buildEventViewSize, eventViewSizeSchema, } from "@getuserfeedback/protocol/internal/view-host-control-messages";
4
7
  import { buildEventViewActivated, buildEventViewActivationDecided, buildEventViewAllocationFailed, buildEventViewClosed, buildEventViewCloseFailed, buildEventViewPrerendered, } from "@getuserfeedback/protocol/internal/view-host-lifecycle-events-v3";
5
8
  import { bindViewHostControlEnvelope, bindViewToCoreRelayEnvelope, parseCoreToViewRelayEnvelope, } from "@getuserfeedback/protocol/internal/view-relay-contract";
6
9
  import { createViewPreparationReadinessRegistry, createViewPreparationRegistry, } from "./view-orchestration-runtime.js";
10
+ // RPC IDs are UUIDs, unlike command IDs whose non-WebCrypto fallback is prefixed.
11
+ // This is live request correlation, not an authentication token.
12
+ function createNativeInteractionRpcId() {
13
+ var _a, _b;
14
+ if (typeof ((_a = globalThis.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === "function")
15
+ return globalThis.crypto.randomUUID();
16
+ const bytes = new Uint8Array(16);
17
+ if (typeof ((_b = globalThis.crypto) === null || _b === void 0 ? void 0 : _b.getRandomValues) === "function")
18
+ globalThis.crypto.getRandomValues(bytes);
19
+ else
20
+ for (let index = 0; index < bytes.length; index++)
21
+ bytes[index] = Math.floor(Math.random() * 256);
22
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
23
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
24
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
25
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
26
+ }
7
27
  const createNativeViewReadinessRegistry = () => createViewPreparationReadinessRegistry();
8
28
  class NativeViewCleanupError extends Error {
9
29
  constructor(resourceErrors, observerErrors, viewId) {
@@ -63,6 +83,10 @@ export function createNativeViewRecordController(input) {
63
83
  if (entries.get(entry.viewId) !== entry) {
64
84
  return false;
65
85
  }
86
+ if (entry.nativeInteraction &&
87
+ entry.nativeInteraction.kind !== "draft-failed")
88
+ clearTimeout(entry.nativeInteraction.timeout);
89
+ entry.nativeInteraction = undefined;
66
90
  const detachViewConnection = entry.viewConnectionDetach;
67
91
  const resourceErrors = [];
68
92
  const observerErrors = [];
@@ -226,11 +250,18 @@ export function createNativeViewRecordController(input) {
226
250
  const handlePrerender = (action) => {
227
251
  var _a;
228
252
  var _b;
253
+ const srcdoc = action.t === "getuserfeedback:action:prerenderView" ? action.srcdoc : "";
254
+ if (srcdoc === undefined) {
255
+ failCoreConnection(new Error("Native prerenderView requires srcdoc; hosted URLs are unsupported"), "Unsupported native view document source");
256
+ return;
257
+ }
229
258
  const existing = entries.get(action.viewId);
230
259
  if (existing) {
231
260
  if (existing.generation !== action.gen ||
232
261
  existing.instanceId !== action.instanceId ||
233
- existing.flowRunId !== action.flowRunId) {
262
+ existing.flowRunId !== action.flowRunId ||
263
+ !!existing.nativeProjection !==
264
+ (action.t === "getuserfeedback:action:prerenderNativeFlow")) {
234
265
  failCoreConnection(new Error(`Conflicting native projection binding ${action.viewId}`), "Conflicting native projection binding");
235
266
  }
236
267
  return;
@@ -240,31 +271,11 @@ export function createNativeViewRecordController(input) {
240
271
  return;
241
272
  }
242
273
  nextAllocationId += 1;
243
- const entry = {
244
- allocationId: nextAllocationId,
245
- flowId: ((_a = action.openRequest) === null || _a === void 0 ? void 0 : _a.source) === "command"
274
+ const entry = Object.assign(Object.assign({}, (action.t === "getuserfeedback:action:prerenderNativeFlow"
275
+ ? { nativeProjection: action.projection }
276
+ : {})), { allocationId: nextAllocationId, flowId: ((_a = action.openRequest) === null || _a === void 0 ? void 0 : _a.source) === "command"
246
277
  ? ((_b = action.openRequest.flowId) !== null && _b !== void 0 ? _b : null)
247
- : null,
248
- generation: action.gen,
249
- flowRunId: action.flowRunId,
250
- instanceId: action.instanceId,
251
- isActive: false,
252
- isClosing: false,
253
- isPreparationCommitted: false,
254
- isPresentationAcknowledged: false,
255
- isViewConnected: false,
256
- isViewRelayDraining: false,
257
- layoutReportRevision: 0,
258
- pendingCoreToViewMessages: [],
259
- preparation,
260
- retainedSize: null,
261
- size: null,
262
- srcdoc: action.srcdoc,
263
- committedLayoutReportOccurrence: null,
264
- viewConnection: null,
265
- viewConnectionDetach: null,
266
- viewId: action.viewId,
267
- };
278
+ : null, generation: action.gen, flowRunId: action.flowRunId, instanceId: action.instanceId, isActive: false, isClosing: false, isPreparationCommitted: false, isPresentationAcknowledged: false, isViewConnected: false, isViewRelayDraining: false, layoutReportRevision: 0, pendingCoreToViewMessages: [], preparation, retainedSize: null, size: null, srcdoc, committedLayoutReportOccurrence: null, viewConnection: null, viewConnectionDetach: null, viewId: action.viewId });
268
279
  entries.set(action.viewId, entry);
269
280
  try {
270
281
  preparation.addCleanup(() => {
@@ -383,6 +394,39 @@ export function createNativeViewRecordController(input) {
383
394
  if (coreConnectionFailed || disposed) {
384
395
  return;
385
396
  }
397
+ const nativeAction = actionPrerenderNativeFlowSchema.safeParse(value);
398
+ if (nativeAction.success &&
399
+ nativeAction.data.gen === input.coreConnection.generation) {
400
+ handlePrerender(nativeAction.data);
401
+ return;
402
+ }
403
+ const response = rpcResponseSchema.safeParse(value);
404
+ if (response.success &&
405
+ response.data.gen === input.coreConnection.generation &&
406
+ response.data.viewId) {
407
+ const entry = entries.get(response.data.viewId);
408
+ const interaction = entry === null || entry === void 0 ? void 0 : entry.nativeInteraction;
409
+ if (entry &&
410
+ interaction &&
411
+ interaction.kind !== "draft-failed" &&
412
+ interaction.rpcId === response.data.rpcId &&
413
+ entry.nativeProjection &&
414
+ !entry.isClosing &&
415
+ entry.isActive) {
416
+ const result = response.data.ok
417
+ ? nativeFlowProjectionSchema.safeParse(response.data.result)
418
+ : null;
419
+ if (!(result === null || result === void 0 ? void 0 : result.success)) {
420
+ interaction.fail();
421
+ return;
422
+ }
423
+ clearTimeout(interaction.timeout);
424
+ entry.nativeInteraction = undefined;
425
+ entry.nativeProjection = result.data;
426
+ notify();
427
+ return;
428
+ }
429
+ }
386
430
  const action = coreViewHostActionSchema.safeParse(value);
387
431
  if (action.success && action.data.gen === input.coreConnection.generation) {
388
432
  switch (action.data.t) {
@@ -403,12 +447,12 @@ export function createNativeViewRecordController(input) {
403
447
  });
404
448
  if (relay) {
405
449
  const entry = entries.get(relay.viewId);
406
- if (entry) {
450
+ if (entry && !entry.nativeProjection) {
407
451
  enqueueCoreToViewMessage(entry, relay.message);
408
452
  }
409
453
  }
410
454
  };
411
- const completeConnectionReadiness = (entry, size) => {
455
+ const completePreparationReadiness = (entry, size) => {
412
456
  if (disposed || entries.get(entry.viewId) !== entry) {
413
457
  return;
414
458
  }
@@ -460,6 +504,144 @@ export function createNativeViewRecordController(input) {
460
504
  };
461
505
  const stopCoreTransport = input.coreConnection.transport.subscribe(handleCoreMessage);
462
506
  return {
507
+ attachNativeContent({ allocationId, viewId }) {
508
+ const entry = entries.get(viewId);
509
+ if (disposed ||
510
+ coreConnectionFailed ||
511
+ !(entry === null || entry === void 0 ? void 0 : entry.nativeProjection) ||
512
+ entry.allocationId !== allocationId ||
513
+ entry.isClosing)
514
+ return null;
515
+ const binding = {
516
+ onLayout(size) {
517
+ var _a, _b;
518
+ if (disposed ||
519
+ coreConnectionFailed ||
520
+ entries.get(viewId) !== entry ||
521
+ entry.isClosing ||
522
+ entry.nativeContentBinding !== binding)
523
+ return;
524
+ if (!Number.isFinite(size.width) ||
525
+ !Number.isFinite(size.height) ||
526
+ size.width <= 0 ||
527
+ size.height <= 0 ||
528
+ size.width > 4096 ||
529
+ size.height > 4096)
530
+ return;
531
+ if (((_a = entry.size) === null || _a === void 0 ? void 0 : _a.width) === size.width &&
532
+ ((_b = entry.size) === null || _b === void 0 ? void 0 : _b.height) === size.height)
533
+ return;
534
+ entry.size = Object.assign({}, size);
535
+ entry.layoutReportRevision += 1;
536
+ try {
537
+ if (!entry.isPreparationCommitted)
538
+ completePreparationReadiness(entry, entry.size);
539
+ else
540
+ input.coreConnection.transport.postMessage(buildEventViewSize(Object.assign({ gen: entry.generation, viewId }, entry.size)));
541
+ notify();
542
+ }
543
+ catch (error) {
544
+ if (entry.isPreparationCommitted)
545
+ failCoreConnection(error, "Native content readiness failed");
546
+ else
547
+ failAllocation(entry, error);
548
+ }
549
+ },
550
+ dispose() {
551
+ if (entry.nativeContentBinding !== binding)
552
+ return;
553
+ entry.nativeContentBinding = undefined;
554
+ entry.size = null;
555
+ if (!disposed && entries.get(viewId) === entry)
556
+ notify();
557
+ },
558
+ };
559
+ entry.nativeContentBinding = binding;
560
+ return binding;
561
+ },
562
+ interactNativeFlow({ allocationId, viewId, projection, intent, draft }) {
563
+ var _a, _b;
564
+ const entry = entries.get(viewId);
565
+ if (disposed ||
566
+ coreConnectionFailed ||
567
+ !entry ||
568
+ entry.allocationId !== allocationId ||
569
+ entry.isClosing ||
570
+ !entry.isActive ||
571
+ !entry.isPreparationCommitted ||
572
+ !entry.nativeContentBinding ||
573
+ !entry.nativeProjection ||
574
+ (intent === "draft"
575
+ ? ((_a = entry.nativeInteraction) === null || _a === void 0 ? void 0 : _a.kind) === "action" ||
576
+ entry.nativeProjection.nodeId !== projection.nodeId
577
+ : entry.nativeInteraction !== undefined ||
578
+ entry.nativeProjection !== projection))
579
+ return false;
580
+ const parsed = nativeFlowInteractionSchema.safeParse({
581
+ flowRunId: entry.flowRunId,
582
+ nodeId: projection.nodeId,
583
+ intent,
584
+ draft,
585
+ });
586
+ if (!parsed.success)
587
+ return false;
588
+ const rpcId = createNativeInteractionRpcId();
589
+ const currentProjection = entry.nativeProjection;
590
+ if (((_b = entry.nativeInteraction) === null || _b === void 0 ? void 0 : _b.kind) === "draft")
591
+ clearTimeout(entry.nativeInteraction.timeout);
592
+ // Match the ordinary RPC budget. Correlation already rejects late replies;
593
+ // Timeout or a failed reply restores editing for an explicit manual retry.
594
+ const settleFailure = () => {
595
+ var _a, _b;
596
+ if (disposed ||
597
+ entries.get(viewId) !== entry ||
598
+ ((_a = entry.nativeInteraction) === null || _a === void 0 ? void 0 : _a.kind) === "draft-failed" ||
599
+ ((_b = entry.nativeInteraction) === null || _b === void 0 ? void 0 : _b.rpcId) !== rpcId)
600
+ return;
601
+ clearTimeout(entry.nativeInteraction.timeout);
602
+ entry.nativeInteraction =
603
+ intent === "draft" ? { kind: "draft-failed" } : undefined;
604
+ entry.nativeProjection = Object.assign(Object.assign({}, currentProjection), { answers: Object.assign(Object.assign({}, currentProjection.answers), parsed.data.draft), formErrors: ["Something went wrong. Please try again."] });
605
+ notify();
606
+ };
607
+ entry.nativeInteraction = {
608
+ kind: intent === "draft" ? "draft" : "action",
609
+ rpcId,
610
+ fail: settleFailure,
611
+ timeout: setTimeout(settleFailure, 10000),
612
+ };
613
+ try {
614
+ notify();
615
+ input.coreConnection.transport.postMessage(rpcRequestSchema.parse(buildRpcRequest({
616
+ gen: entry.generation,
617
+ viewId,
618
+ rpcId,
619
+ idemKey: rpcId,
620
+ method: NATIVE_FLOW_SESSION_RPC_METHOD,
621
+ params: parsed.data,
622
+ })));
623
+ }
624
+ catch (error) {
625
+ failCoreConnection(error, "Native interaction delivery failed");
626
+ }
627
+ return true;
628
+ },
629
+ closeNativeFlow({ allocationId, viewId }) {
630
+ var _a;
631
+ const entry = entries.get(viewId);
632
+ if (disposed ||
633
+ coreConnectionFailed ||
634
+ !entry ||
635
+ entry.allocationId !== allocationId ||
636
+ !((_a = entry.nativeProjection) === null || _a === void 0 ? void 0 : _a.canClose) ||
637
+ !entry.isActive)
638
+ return;
639
+ closeViewFromRequest(entry, {
640
+ t: "getuserfeedback:action:requestCloseView",
641
+ gen: createGeneration(input.coreConnection.generation),
642
+ viewId,
643
+ });
644
+ },
463
645
  failViewAllocation({ allocationId, error, viewId }) {
464
646
  const entry = entries.get(viewId);
465
647
  if (!entry || entry.allocationId !== allocationId) {
@@ -515,9 +697,11 @@ export function createNativeViewRecordController(input) {
515
697
  entry.isClosing ||
516
698
  !entry.isActive ||
517
699
  !entry.isPreparationCommitted ||
518
- !entry.isViewConnected ||
519
- entry.viewConnection === null ||
520
- entry.viewConnectionDetach === null ||
700
+ (entry.nativeProjection
701
+ ? !entry.nativeContentBinding
702
+ : !entry.isViewConnected ||
703
+ entry.viewConnection === null ||
704
+ entry.viewConnectionDetach === null) ||
521
705
  entry.size === null ||
522
706
  entry.isPresentationAcknowledged) {
523
707
  return;
@@ -540,6 +724,7 @@ export function createNativeViewRecordController(input) {
540
724
  const { viewId } = connection;
541
725
  const entry = entries.get(viewId);
542
726
  if (!entry ||
727
+ entry.nativeProjection ||
543
728
  entry.allocationId !== allocationId ||
544
729
  connection.generation !== input.coreConnection.generation) {
545
730
  return null;
@@ -584,7 +769,7 @@ export function createNativeViewRecordController(input) {
584
769
  return;
585
770
  }
586
771
  try {
587
- completeConnectionReadiness(entry, size);
772
+ completePreparationReadiness(entry, size);
588
773
  }
589
774
  catch (error) {
590
775
  failCoreConnection(error, "Native view readiness delivery failed");
@@ -777,16 +962,18 @@ export function createNativeViewRecordController(input) {
777
962
  };
778
963
  }
779
964
  function toRecord(entry) {
780
- return Object.freeze({
781
- allocationId: entry.allocationId,
782
- flowId: entry.flowId,
783
- generation: entry.generation,
784
- isActive: entry.isActive,
785
- layoutReportRevision: entry.layoutReportRevision,
786
- size: entry.size,
787
- srcdoc: entry.srcdoc,
788
- viewId: entry.viewId,
789
- });
965
+ var _a, _b, _c;
966
+ var _d;
967
+ return Object.freeze(Object.assign(Object.assign({}, (entry.nativeProjection
968
+ ? {
969
+ nativeFlow: {
970
+ projection: entry.nativeProjection,
971
+ interaction: (_d = (_a = entry.nativeInteraction) === null || _a === void 0 ? void 0 : _a.kind) !== null && _d !== void 0 ? _d : "idle",
972
+ pending: ((_b = entry.nativeInteraction) === null || _b === void 0 ? void 0 : _b.kind) === "draft" ||
973
+ ((_c = entry.nativeInteraction) === null || _c === void 0 ? void 0 : _c.kind) === "action",
974
+ },
975
+ }
976
+ : {})), { allocationId: entry.allocationId, flowId: entry.flowId, generation: entry.generation, isActive: entry.isActive, layoutReportRevision: entry.layoutReportRevision, size: entry.size, srcdoc: entry.srcdoc, viewId: entry.viewId }));
790
977
  }
791
978
  function toViewSizeReport(event, context) {
792
979
  var _a;
@@ -1,10 +1,12 @@
1
1
  import { createCommandRequestId, REACT_NATIVE_WEBVIEW_TRANSPORT_CAPABILITIES, } from "@getuserfeedback/protocol/host";
2
+ import { NATIVE_FLOW_SESSION_PROTOCOL } from "@getuserfeedback/protocol/internal/native-flow-session";
2
3
  import { REACT_NATIVE_SDK_VERSION } from "./version.js";
3
4
  export const REACT_NATIVE_CLIENT_META = {
4
5
  clientName: "@getuserfeedback/react-native",
5
6
  clientVersion: REACT_NATIVE_SDK_VERSION,
6
7
  loader: "sdk",
7
8
  protocolCapabilities: [
9
+ NATIVE_FLOW_SESSION_PROTOCOL,
8
10
  REACT_NATIVE_WEBVIEW_TRANSPORT_CAPABILITIES.LAYOUT_TRANSITION_POLICY,
9
11
  REACT_NATIVE_WEBVIEW_TRANSPORT_CAPABILITIES.LAYOUT_TRANSITION_SCHEDULE,
10
12
  ],
@@ -18,5 +20,15 @@ export function createProviderCommandEnvelope({ clientMeta, command, idempotency
18
20
  const resolvedIdempotencyKey = (idempotencyKey === null || idempotencyKey === void 0 ? void 0 : idempotencyKey.trim()) || resolvedRequestId;
19
21
  const resolvedCommand = clientMeta !== undefined
20
22
  ? Object.assign(Object.assign({}, command), { opts: Object.assign(Object.assign({}, command.opts), { clientMeta }) }) : command;
21
- return Object.assign({ version: "1", command: resolvedCommand, idempotencyKey: resolvedIdempotencyKey, instanceId: resolvedInstanceId, requestId: resolvedRequestId }, (clientMeta === undefined ? {} : { clientMeta }));
23
+ const envelope = {
24
+ version: "1",
25
+ command: resolvedCommand,
26
+ idempotencyKey: resolvedIdempotencyKey,
27
+ instanceId: resolvedInstanceId,
28
+ requestId: resolvedRequestId,
29
+ };
30
+ if (clientMeta !== undefined) {
31
+ envelope.clientMeta = clientMeta;
32
+ }
33
+ return envelope;
22
34
  }
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 : "5.14.2";
6
+ export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "5.15.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getuserfeedback/react-native",
3
- "version": "5.14.2",
3
+ "version": "5.15.3",
4
4
  "description": "getuserfeedback React Native SDK",
5
5
  "keywords": [
6
6
  "getuserfeedback",
@@ -54,8 +54,8 @@
54
54
  "lint": "biome check ."
55
55
  },
56
56
  "dependencies": {
57
- "@getuserfeedback/protocol": "^6.0.0",
58
- "@getuserfeedback/sdk": "^0.19.1",
57
+ "@getuserfeedback/protocol": "^6.0.3",
58
+ "@getuserfeedback/sdk": "^0.19.4",
59
59
  "robot3": "^1.2.0"
60
60
  },
61
61
  "peerDependencies": {