@getuserfeedback/react-native 5.14.1 → 5.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,160 @@
1
+ import { createElement, useLayoutEffect, useRef, useState, } from "react";
2
+ /** Thin native editor: drafts only. Core owns validation, navigation and submission. */
3
+ export function NativeFlowContent({ controller, presentationMode = "overlay", record, }) {
4
+ const flow = record.nativeFlow;
5
+ if (!flow)
6
+ throw new Error("Native content requires a native allocation");
7
+ const { projection, interaction } = flow;
8
+ const { View, Text, TextInput, Pressable, ScrollView, useWindowDimensions, } = require("react-native");
9
+ const { height: windowHeight } = useWindowDimensions();
10
+ const [draft, setDraft] = useState(projection.answers);
11
+ const rawDraft = useRef(draft);
12
+ const scroll = useRef(null);
13
+ const editorPage = useRef({
14
+ allocationId: record.allocationId,
15
+ nodeId: projection.nodeId,
16
+ });
17
+ const binding = useRef(null);
18
+ useLayoutEffect(() => {
19
+ const mounted = controller.attachNativeContent({
20
+ viewId: record.viewId,
21
+ allocationId: record.allocationId,
22
+ });
23
+ binding.current = mounted;
24
+ return () => {
25
+ binding.current = null;
26
+ mounted === null || mounted === void 0 ? void 0 : mounted.dispose();
27
+ };
28
+ }, [controller, record.viewId, record.allocationId]);
29
+ useLayoutEffect(() => {
30
+ var _a;
31
+ if (editorPage.current.allocationId === record.allocationId &&
32
+ editorPage.current.nodeId === projection.nodeId)
33
+ return;
34
+ editorPage.current = {
35
+ allocationId: record.allocationId,
36
+ nodeId: projection.nodeId,
37
+ };
38
+ rawDraft.current = projection.answers;
39
+ setDraft(projection.answers);
40
+ (_a = scroll.current) === null || _a === void 0 ? void 0 : _a.scrollTo({ x: 0, y: 0, animated: false });
41
+ }, [projection, record.allocationId]);
42
+ const update = (key, value) => {
43
+ const nextDraft = Object.assign(Object.assign({}, rawDraft.current), { [key]: value });
44
+ if (!controller.interactNativeFlow({
45
+ viewId: record.viewId,
46
+ allocationId: record.allocationId,
47
+ projection,
48
+ intent: "draft",
49
+ draft: nextDraft,
50
+ }))
51
+ return;
52
+ rawDraft.current = nextDraft;
53
+ setDraft(rawDraft.current);
54
+ };
55
+ const interact = (intent) => controller.interactNativeFlow({
56
+ viewId: record.viewId,
57
+ allocationId: record.allocationId,
58
+ projection,
59
+ intent,
60
+ draft: Object.assign({}, rawDraft.current),
61
+ });
62
+ const disabled = interaction === "action" || !record.isActive;
63
+ const button = (label, onPress, buttonDisabled = interaction !== "idle" || !record.isActive) => createElement(Pressable, {
64
+ key: label,
65
+ accessibilityRole: "button",
66
+ accessibilityLabel: label,
67
+ disabled: buttonDisabled,
68
+ onPress,
69
+ style: {
70
+ padding: 12,
71
+ backgroundColor: buttonDisabled ? "#ddd" : "#e7eefb",
72
+ borderRadius: 6,
73
+ marginTop: 8,
74
+ },
75
+ }, createElement(Text, { style: { color: "#17202a" } }, label));
76
+ return createElement(ScrollView, {
77
+ ref: scroll,
78
+ style: Object.assign(Object.assign({}, (presentationMode === "content"
79
+ ? { flexGrow: 0, flexShrink: 1, maxHeight: windowHeight }
80
+ : { flex: 1, maxHeight: "100%" })), { width: "100%", backgroundColor: "#fff" }),
81
+ keyboardShouldPersistTaps: "handled",
82
+ nestedScrollEnabled: presentationMode === "content",
83
+ // Report the actual viewport, never the window ceiling or inner content.
84
+ // Content mode can measure before its auto-height parent has any height.
85
+ onLayout: (event) => { var _a; return (_a = binding.current) === null || _a === void 0 ? void 0 : _a.onLayout(event.nativeEvent.layout); },
86
+ }, createElement(View, {
87
+ testID: "native-flow-renderer",
88
+ style: { padding: 24, gap: 12 },
89
+ }, projection.mode === "simulated"
90
+ ? createElement(Text, { testID: "native-flow-simulated" }, "SIMULATED — no response is saved")
91
+ : null, ...projection.blocks.map((block) => {
92
+ var _a;
93
+ if (block.kind === "statement")
94
+ return createElement(View, { key: block.key }, ...block.paragraphs.map((text, index) => createElement(Text, { key: index }, text)));
95
+ const value = draft[block.key];
96
+ const requiredLabel = block.required ? ", required" : "";
97
+ const selected = Array.isArray(value)
98
+ ? ((_a = value[0]) !== null && _a !== void 0 ? _a : "")
99
+ : (value !== null && value !== void 0 ? value : "");
100
+ return createElement(View, { key: block.key, style: { gap: 6 } }, createElement(Text, {}, block.label + (block.required ? " *" : "")), block.description ? createElement(Text, {}, block.description) : null, block.kind === "text"
101
+ ? createElement(TextInput, {
102
+ accessibilityLabel: block.label + requiredLabel,
103
+ accessibilityHint: block.description,
104
+ value: selected,
105
+ placeholder: block.placeholder,
106
+ editable: !disabled,
107
+ maxLength: 4000,
108
+ onChangeText: (text) => update(block.key, text),
109
+ style: {
110
+ color: "#17202a",
111
+ borderWidth: 1,
112
+ borderColor: "#999",
113
+ borderRadius: 4,
114
+ padding: 10,
115
+ },
116
+ })
117
+ : createElement(View, {
118
+ accessibilityRole: "radiogroup",
119
+ accessibilityLabel: block.label + requiredLabel,
120
+ accessibilityHint: block.description,
121
+ accessible: false,
122
+ }, ...block.options.map((option) => createElement(Pressable, {
123
+ key: option.value,
124
+ accessibilityRole: "radio",
125
+ accessibilityLabel: `${block.label}: ${option.label}${requiredLabel}`,
126
+ accessibilityHint: block.description,
127
+ accessible: true,
128
+ accessibilityState: {
129
+ checked: selected === option.value,
130
+ disabled,
131
+ },
132
+ disabled,
133
+ onPress: () => update(block.key, option.value),
134
+ style: { padding: 10 },
135
+ }, createElement(Text, {}, `${selected === option.value ? "●" : "○"} ${option.label}`)))), projection.errors[block.key]
136
+ ? createElement(Text, { accessibilityRole: "alert", style: { color: "#a00" } }, projection.errors[block.key])
137
+ : null);
138
+ }), ...projection.formErrors.map((text, index) => createElement(Text, { key: `error-${index}`, accessibilityRole: "alert" }, text)), ...projection.terminal.map((text, index) => createElement(Text, { key: `terminal-${index}` }, text)), projection.status === "succeeded" &&
139
+ (projection.mode === "simulated" || projection.terminal.length === 0)
140
+ ? createElement(Text, { testID: "native-flow-complete" }, projection.mode === "simulated"
141
+ ? "SIMULATED completion — nothing saved"
142
+ : "Thank you")
143
+ : null, projection.canGoBack ? button("Back", () => interact("back")) : null, interaction === "draft-failed"
144
+ ? button("Retry refresh", () => interact("draft"), !record.isActive)
145
+ : null, projection.status !== "succeeded"
146
+ ? button(interaction === "action"
147
+ ? "Working…"
148
+ : projection.status === "ready-to-submit"
149
+ ? "Submit"
150
+ : "Continue", () => interact(projection.status === "ready-to-submit" ? "submit" : "continue"))
151
+ : null, projection.canClose
152
+ ? createElement(Pressable, {
153
+ accessibilityRole: "button",
154
+ accessibilityLabel: "Close",
155
+ disabled: !record.isActive,
156
+ onPress: () => controller.closeNativeFlow(record),
157
+ style: { padding: 12 },
158
+ }, createElement(Text, {}, "Close"))
159
+ : null));
160
+ }
@@ -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) {
@@ -87,15 +87,17 @@ const toInitFlags = (flags) => {
87
87
  const parseNativeInitOptions = (_a, hostActions) => {
88
88
  var { actions: _actions, capabilities, flags, links: _links } = _a, clientOptions = __rest(_a, ["actions", "capabilities", "flags", "links"]);
89
89
  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
- : {})));
90
+ const initInput = Object.assign({}, clientOptions);
91
+ if (normalizedFlags !== undefined) {
92
+ initInput.flags = normalizedFlags;
93
+ }
94
+ if (hostActions !== undefined && hostActions.length > 0) {
95
+ initInput.hostActions = hostActions;
96
+ }
97
+ if (capabilities !== undefined) {
98
+ initInput.hostCapabilities = capabilities.map((capability) => typeof capability === "string" ? { key: capability } : Object.assign({}, capability));
99
+ }
100
+ const initOptions = parseInitOptions(initInput);
99
101
  const hostCapabilities = initOptions.hostCapabilities;
100
102
  const normalizedHostCapabilities = hostCapabilities === null || hostCapabilities === void 0 ? void 0 : hostCapabilities.map((capability) => ({
101
103
  capability,
@@ -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.1";
6
+ export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "5.15.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getuserfeedback/react-native",
3
- "version": "5.14.1",
3
+ "version": "5.15.2",
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": "^5.5.0",
58
- "@getuserfeedback/sdk": "^0.19.0",
57
+ "@getuserfeedback/protocol": "^6.0.3",
58
+ "@getuserfeedback/sdk": "^0.19.4",
59
59
  "robot3": "^1.2.0"
60
60
  },
61
61
  "peerDependencies": {