@getuserfeedback/react-native 5.0.8 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AppEventExternalId, AppEventJsonValue, PublicCommandPayload } from "@getuserfeedback/protocol";
2
- import type { ConfigureOptions, InitOptions } from "@getuserfeedback/sdk";
2
+ import type { CapabilitiesInput, ConfigureOptions, GraphOperations, InitOptions } from "@getuserfeedback/sdk";
3
3
  type IdentifyTraits = Record<string, unknown>;
4
4
  type TrackProperties = Record<string, AppEventJsonValue>;
5
5
  type OpenCommand = Extract<PublicCommandPayload, {
@@ -23,7 +23,10 @@ type CommandOptions = {
23
23
  idempotencyKey?: string;
24
24
  };
25
25
  /** React Native provider initialization options. */
26
- export type ClientOptions = InitOptions;
26
+ export type ClientOptions = Omit<InitOptions, "capabilities" | "hostCapabilities"> & {
27
+ /** Capabilities supported by the current app version. */
28
+ capabilities?: CapabilitiesInput | undefined;
29
+ };
27
30
  /** Operations for one on-demand flow lifecycle. */
28
31
  export interface FlowRun {
29
32
  readonly flowId: string;
@@ -52,6 +55,8 @@ export interface Client {
52
55
  };
53
56
  /** Records a product event for targeting and analysis. */
54
57
  track: (eventName: string, properties?: TrackProperties, options?: TrackOptions) => Promise<void>;
58
+ /** Records directed relationship observations through the analytics pipeline. */
59
+ graph: GraphOperations;
55
60
  /** Returns the controller for one on-demand flow lifecycle. */
56
61
  flow: (flowId: string) => FlowRun;
57
62
  /** Closes every open flow owned by this provider. */
@@ -10,6 +10,8 @@ const CORE_WEBVIEW_BRIDGE_CONFIG = {
10
10
  connectTimeoutMessage: "Timed out connecting core to the native host",
11
11
  connectTimeoutMs: 30000,
12
12
  inactiveConnectionMessage: "Core WebView bridge connection is not active",
13
+ reactivateOnNativeEpochChange: false,
14
+ throwOnUnavailablePost: false,
13
15
  };
14
16
  export const parseCoreWebViewBridgeMessage = (value) => parseWebViewConnectionBridgeMessage(value, {
15
17
  invalidIdentity: "Invalid core WebView bridge message identity",
@@ -0,0 +1,26 @@
1
+ import { type ReactElement } from "react";
2
+ /** Aggregate state and controls for a customer-owned flow container. */
3
+ export type UseFlowContainerReturn = Readonly<{
4
+ /** True when an active flow is visible. */
5
+ isOpen: boolean;
6
+ /** True when an active flow is waiting for its measured size. */
7
+ isLoading: boolean;
8
+ /** True when the customer container should be visible. */
9
+ shouldRenderContainer: boolean;
10
+ /** Close every active flow rendered by this container. */
11
+ close: () => Promise<void>;
12
+ }>;
13
+ export declare function NativeStandardFlowContainer(): ReactElement | null;
14
+ /**
15
+ * Observe and control the provider's customer-owned flow container.
16
+ *
17
+ * @see https://getuserfeedback.com/docs/guides/advanced/containers
18
+ */
19
+ export declare function useFlowContainer(): UseFlowContainerReturn;
20
+ /**
21
+ * Render the active flow at its measured size inside a custom container.
22
+ * Keep this component mounted when the projected WebView must retain its state.
23
+ *
24
+ * @see https://getuserfeedback.com/docs/guides/advanced/containers
25
+ */
26
+ export declare function FlowContent(): ReactElement | null;
@@ -0,0 +1,75 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { createElement, useCallback, useContext, useLayoutEffect, useRef, useSyncExternalStore, } from "react";
13
+ import { NativeFlowContainerContext } from "./native-flow-container-context.js";
14
+ import { NativeViewHostProjection, } from "./native-view-host-projection.js";
15
+ const EMPTY_RECORDS = Object.freeze([]);
16
+ const subscribeNoop = () => () => { };
17
+ const getEmptyRecords = () => EMPTY_RECORDS;
18
+ const renderProjection = (projection, presentationMode) => {
19
+ if (!projection)
20
+ return null;
21
+ const { key } = projection, props = __rest(projection, ["key"]);
22
+ return createElement(NativeViewHostProjection, Object.assign(Object.assign({}, props), { key,
23
+ presentationMode }));
24
+ };
25
+ const useRecords = (projection) => {
26
+ var _a, _b, _c;
27
+ return useSyncExternalStore((_a = projection === null || projection === void 0 ? void 0 : projection.controller.subscribe) !== null && _a !== void 0 ? _a : subscribeNoop, (_b = projection === null || projection === void 0 ? void 0 : projection.controller.getSnapshot) !== null && _b !== void 0 ? _b : getEmptyRecords, (_c = projection === null || projection === void 0 ? void 0 : projection.controller.getSnapshot) !== null && _c !== void 0 ? _c : getEmptyRecords);
28
+ };
29
+ export function NativeStandardFlowContainer() {
30
+ const context = useContext(NativeFlowContainerContext);
31
+ return (context === null || context === void 0 ? void 0 : context.contentOwner) === null
32
+ ? renderProjection(context.projection, "overlay")
33
+ : null;
34
+ }
35
+ /**
36
+ * Observe and control the provider's customer-owned flow container.
37
+ *
38
+ * @see https://getuserfeedback.com/docs/guides/advanced/containers
39
+ */
40
+ export function useFlowContainer() {
41
+ const context = useContext(NativeFlowContainerContext);
42
+ if (!context) {
43
+ throw new Error("useFlowContainer must be used inside GetUserFeedbackProvider");
44
+ }
45
+ const records = useRecords(context.projection);
46
+ const isOpen = records.some((record) => record.isActive && record.size !== null);
47
+ const isLoading = records.some((record) => record.isActive && record.size === null);
48
+ const close = useCallback(() => context.client.close(), [context.client]);
49
+ return {
50
+ close,
51
+ isLoading,
52
+ isOpen,
53
+ shouldRenderContainer: isLoading || isOpen,
54
+ };
55
+ }
56
+ /**
57
+ * Render the active flow at its measured size inside a custom container.
58
+ * Keep this component mounted when the projected WebView must retain its state.
59
+ *
60
+ * @see https://getuserfeedback.com/docs/guides/advanced/containers
61
+ */
62
+ export function FlowContent() {
63
+ var _a;
64
+ const context = useContext(NativeFlowContainerContext);
65
+ if (!context) {
66
+ throw new Error("FlowContent must be used inside GetUserFeedbackProvider");
67
+ }
68
+ const ownerRef = useRef(null);
69
+ (_a = ownerRef.current) !== null && _a !== void 0 ? _a : (ownerRef.current = {});
70
+ const owner = ownerRef.current;
71
+ useLayoutEffect(() => context.registerContent(owner), [context.registerContent, owner]);
72
+ return context.contentOwner === owner
73
+ ? renderProjection(context.projection, "content")
74
+ : null;
75
+ }
@@ -1,22 +1,60 @@
1
- import { coreCommandEnvelopeSchema, } from "@getuserfeedback/protocol";
1
+ import { appEventPayloadSchema, coreCommandEnvelopeSchema, } from "@getuserfeedback/protocol";
2
2
  import { eventHandleInvalidatedSchema, } from "@getuserfeedback/protocol/internal/core-handle-invalidation";
3
3
  import { eventCommandSettledSchema, } from "@getuserfeedback/protocol/internal/core-host-command-settlement";
4
4
  import { CORE_SESSION_FAILED_CODE, eventCoreSessionFailedSchema, } from "@getuserfeedback/protocol/internal/core-session-failure";
5
+ import { buildRpcResponse, rpcRequestSchema, } from "@getuserfeedback/protocol/internal/v3-rpc-messages";
5
6
  const DISPOSED_MESSAGE = "Core command channel is disposed";
7
+ const TRAFFIC_APP_EVENT_RPC_METHOD = "traffic.appEvent";
6
8
  const isRecord = (value) => typeof value === "object" && value !== null;
7
9
  export function createNativeCoreCommandChannel(input) {
8
10
  const listeners = new Set();
9
11
  let disposed = false;
10
12
  const handleMessage = (value) => {
11
- var _a, _b, _c, _d, _e;
13
+ var _a, _b, _c, _d, _e, _f;
12
14
  if (disposed || !isRecord(value)) {
13
15
  return;
14
16
  }
17
+ if (value.t === "getuserfeedback:rpc:request") {
18
+ const parsed = rpcRequestSchema.safeParse(value);
19
+ if (!parsed.success ||
20
+ parsed.data.gen !== input.coreConnection.generation ||
21
+ parsed.data.method !== TRAFFIC_APP_EVENT_RPC_METHOD) {
22
+ return;
23
+ }
24
+ const params = isRecord(parsed.data.params) ? parsed.data.params : null;
25
+ const instanceId = params && typeof params.instanceId === "string"
26
+ ? params.instanceId.trim()
27
+ : "";
28
+ const payload = params
29
+ ? appEventPayloadSchema.safeParse(params.payload)
30
+ : { success: false };
31
+ const result = instanceId.length > 0 && payload.success
32
+ ? { allowed: true, payload: payload.data }
33
+ : { allowed: false };
34
+ try {
35
+ input.coreConnection.transport.postMessage(buildRpcResponse({
36
+ gen: parsed.data.gen,
37
+ rpcId: parsed.data.rpcId,
38
+ ok: true,
39
+ result,
40
+ viewId: parsed.data.viewId,
41
+ }));
42
+ }
43
+ catch (error) {
44
+ try {
45
+ (_a = input.onInvalidMessage) === null || _a === void 0 ? void 0 : _a.call(input, error instanceof Error ? error : new Error(String(error)), value);
46
+ }
47
+ catch (_g) {
48
+ // Best-effort analytics and diagnostics cannot fail the Core session.
49
+ }
50
+ }
51
+ return;
52
+ }
15
53
  if (value.t === "getuserfeedback:event:error" &&
16
54
  value.code === CORE_SESSION_FAILED_CODE) {
17
55
  const parsed = eventCoreSessionFailedSchema.safeParse(value);
18
56
  if (!parsed.success) {
19
- (_a = input.onInvalidMessage) === null || _a === void 0 ? void 0 : _a.call(input, parsed.error instanceof Error
57
+ (_b = input.onInvalidMessage) === null || _b === void 0 ? void 0 : _b.call(input, parsed.error instanceof Error
20
58
  ? parsed.error
21
59
  : new Error("Invalid Core session failure message"), value);
22
60
  return;
@@ -24,13 +62,13 @@ export function createNativeCoreCommandChannel(input) {
24
62
  if (parsed.data.gen !== input.coreConnection.generation) {
25
63
  return;
26
64
  }
27
- (_b = input.onSessionFailure) === null || _b === void 0 ? void 0 : _b.call(input, new Error(parsed.data.details.message));
65
+ (_c = input.onSessionFailure) === null || _c === void 0 ? void 0 : _c.call(input, new Error(parsed.data.details.message));
28
66
  return;
29
67
  }
30
68
  if (value.t === "getuserfeedback:event:commandSettled") {
31
69
  const parsed = eventCommandSettledSchema.safeParse(value);
32
70
  if (!parsed.success) {
33
- (_c = input.onInvalidMessage) === null || _c === void 0 ? void 0 : _c.call(input, parsed.error instanceof Error
71
+ (_d = input.onInvalidMessage) === null || _d === void 0 ? void 0 : _d.call(input, parsed.error instanceof Error
34
72
  ? parsed.error
35
73
  : new Error("Invalid core command settlement message"), value);
36
74
  return;
@@ -48,7 +86,7 @@ export function createNativeCoreCommandChannel(input) {
48
86
  }
49
87
  const parsed = eventHandleInvalidatedSchema.safeParse(value);
50
88
  if (!parsed.success) {
51
- (_d = input.onInvalidMessage) === null || _d === void 0 ? void 0 : _d.call(input, parsed.error instanceof Error
89
+ (_e = input.onInvalidMessage) === null || _e === void 0 ? void 0 : _e.call(input, parsed.error instanceof Error
52
90
  ? parsed.error
53
91
  : new Error("Invalid core handle invalidation message"), value);
54
92
  return;
@@ -56,7 +94,7 @@ export function createNativeCoreCommandChannel(input) {
56
94
  if (parsed.data.gen !== input.coreConnection.generation) {
57
95
  return;
58
96
  }
59
- (_e = input.onHandleInvalidated) === null || _e === void 0 ? void 0 : _e.call(input, parsed.data);
97
+ (_f = input.onHandleInvalidated) === null || _f === void 0 ? void 0 : _f.call(input, parsed.data);
60
98
  };
61
99
  const unsubscribeTransport = input.coreConnection.transport.subscribe(handleMessage);
62
100
  const channel = {
@@ -0,0 +1,14 @@
1
+ import type { Client } from "./client.js";
2
+ import type { NativeViewProjection } from "./native-view-host-projection.js";
3
+ export type NativeFlowContainerContextValue = Readonly<{
4
+ client: Client;
5
+ contentOwner: object | null;
6
+ projection: NativeViewProjection | null;
7
+ registerContent: (owner: object) => () => void;
8
+ }>;
9
+ export declare const NativeFlowContainerContext: import("react").Context<Readonly<{
10
+ client: Client;
11
+ contentOwner: object | null;
12
+ projection: NativeViewProjection | null;
13
+ registerContent: (owner: object) => () => void;
14
+ }> | null>;
@@ -0,0 +1,2 @@
1
+ import { createContext } from "react";
2
+ export const NativeFlowContainerContext = createContext(null);
@@ -3,7 +3,7 @@ export declare function createNativeProviderClientController(input: {
3
3
  instanceId: string;
4
4
  onError?: (error: Error) => void;
5
5
  }): {
6
- client: import("./native-provider-client.js").NativeProviderClient<void>;
6
+ client: import("./client.js").Client;
7
7
  directSessionController: import("./native-direct-command-session-controller.js").NativeDirectCommandSessionController;
8
8
  dispose: () => void;
9
9
  onHandleInvalidated: (message: EventHandleInvalidated) => void;
@@ -1,43 +1,14 @@
1
- import type { AppEventExternalId, AppEventJsonValue, PublicCommandPayload } from "@getuserfeedback/protocol";
2
- import type { ConfigureOptions } from "@getuserfeedback/sdk";
3
- import type { FlowRun } from "./client.js";
4
- type IdentifyTraits = Record<string, unknown>;
5
- type TrackProperties = Record<string, AppEventJsonValue>;
1
+ import type { PublicCommandPayload } from "@getuserfeedback/protocol";
2
+ import type { Client } from "./client.js";
6
3
  type NativeProviderClientCommand = Extract<PublicCommandPayload, {
7
4
  kind: "configure" | "identify" | "track" | "close" | "reset" | "updateHostContext" | "emitHostSignal";
8
5
  }>;
9
- type IdentifyOptions = {
10
- externalIds?: AppEventExternalId[];
11
- };
12
- type TrackOptions = {
13
- externalIds?: AppEventExternalId[];
14
- };
15
- type HostContext = Extract<PublicCommandPayload, {
16
- kind: "updateHostContext";
17
- }>["context"];
18
- interface NativeProviderCommandOptions {
19
- idempotencyKey?: string;
20
- }
21
- export interface NativeProviderClient<TResult> {
22
- readonly instanceId: string;
23
- configure: (opts: ConfigureOptions, options?: NativeProviderCommandOptions) => Promise<TResult>;
24
- identify: {
25
- (userId: string, traits?: IdentifyTraits, options?: IdentifyOptions): Promise<TResult>;
26
- (traits: IdentifyTraits, placeholder: undefined, options?: IdentifyOptions): Promise<TResult>;
27
- (traits: IdentifyTraits, options?: IdentifyOptions): Promise<TResult>;
28
- };
29
- track: (eventName: string, properties?: TrackProperties, options?: TrackOptions) => Promise<TResult>;
30
- flow: (flowId: string) => FlowRun;
31
- close: () => Promise<TResult>;
32
- reset: (options?: NativeProviderCommandOptions) => Promise<TResult>;
33
- updateHostContext: (context: HostContext, options?: NativeProviderCommandOptions) => Promise<TResult>;
34
- emitHostSignal: (name: string, data?: unknown, options?: NativeProviderCommandOptions) => Promise<TResult>;
35
- }
36
- type NativeProviderClientOptions<TResult> = {
37
- dispatchCommand: (command: NativeProviderClientCommand, options?: NativeProviderCommandOptions) => Promise<TResult>;
38
- flow: (flowId: string) => FlowRun;
6
+ type NativeProviderCommandOptions = NonNullable<Parameters<Client["configure"]>[1]>;
7
+ type NativeProviderClientOptions = {
8
+ dispatchCommand: (command: NativeProviderClientCommand, options?: NativeProviderCommandOptions) => Promise<void>;
9
+ flow: Client["flow"];
39
10
  instanceId: string;
40
11
  };
41
12
  export declare const parseRequiredString: (name: string, value?: string) => string;
42
- export declare function createNativeProviderClient<TResult>({ dispatchCommand, flow, instanceId, }: NativeProviderClientOptions<TResult>): NativeProviderClient<TResult>;
13
+ export declare function createNativeProviderClient({ dispatchCommand, flow, instanceId, }: NativeProviderClientOptions): Client;
43
14
  export {};
@@ -34,20 +34,25 @@ const toIdentifyCommandPayload = (identifyInput, traitsOrOptions, options) => {
34
34
  };
35
35
  };
36
36
  export function createNativeProviderClient({ dispatchCommand, flow, instanceId, }) {
37
+ const track = (eventName, properties, options) => {
38
+ assertNoSegmentExternalIdsInValueArgument({
39
+ argument: properties,
40
+ commandName: "track",
41
+ valueArgumentName: "properties",
42
+ callShape: "track(eventName, properties, { externalIds })",
43
+ });
44
+ return dispatchCommand(Object.assign(Object.assign({ kind: "track", origin: "customer", event: parseRequiredString("eventName", eventName) }, (properties === undefined ? {} : { properties })), ((options === null || options === void 0 ? void 0 : options.externalIds) === undefined
45
+ ? {}
46
+ : { context: { externalIds: options.externalIds } })));
47
+ };
37
48
  return {
38
49
  instanceId,
39
50
  configure: (opts, options) => dispatchCommand({ kind: "configure", opts }, options),
40
51
  identify: ((identifyInput, traitsOrOptions, options) => dispatchCommand(toIdentifyCommandPayload(identifyInput, traitsOrOptions, options))),
41
- track: (eventName, properties, options) => {
42
- assertNoSegmentExternalIdsInValueArgument({
43
- argument: properties,
44
- commandName: "track",
45
- valueArgumentName: "properties",
46
- callShape: "track(eventName, properties, { externalIds })",
47
- });
48
- return dispatchCommand(Object.assign(Object.assign({ kind: "track", origin: "customer", event: parseRequiredString("eventName", eventName) }, (properties === undefined ? {} : { properties })), ((options === null || options === void 0 ? void 0 : options.externalIds) === undefined
49
- ? {}
50
- : { context: { externalIds: options.externalIds } })));
52
+ track,
53
+ graph: {
54
+ connect: (relationship) => track("gx.graph.relationship.connected", relationship),
55
+ disconnect: (relationship) => track("gx.graph.relationship.disconnected", relationship),
51
56
  },
52
57
  flow,
53
58
  close: () => dispatchCommand({ kind: "close" }),
@@ -1,6 +1,8 @@
1
1
  import { type ComponentType, type ReactElement, type ReactNode } from "react";
2
2
  import type { Client, ClientOptions } from "./client.js";
3
3
  export type { Client, ClientOptions, FlowRun } from "./client.js";
4
+ export type { UseFlowContainerReturn } from "./flow-container.js";
5
+ export { FlowContent, useFlowContainer } from "./flow-container.js";
4
6
  /**
5
7
  * Custom WebView implementation used by the native runtime.
6
8
  *
@@ -1,8 +1,23 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
1
12
  import { parseInitOptions } from "@getuserfeedback/protocol";
2
- import { createUniqueId, PRODUCTION_WIDGET_RUNTIME_ENDPOINTS, } from "@getuserfeedback/protocol/host";
3
- import { createContext, createElement, Fragment, useCallback, useContext, useEffect, useRef, useState, } from "react";
13
+ import { createUniqueId } from "@getuserfeedback/protocol/host";
14
+ import { createContext, createElement, Fragment, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
15
+ import { NativeStandardFlowContainer } from "./flow-container.js";
16
+ import { NativeFlowContainerContext, } from "./native-flow-container-context.js";
4
17
  import { createNativeProviderClientController } from "./native-provider-client-controller.js";
18
+ import { REACT_NATIVE_PINNED_CORE_URL } from "./native-runtime-endpoints.js";
5
19
  import { NativeProviderRuntime } from "./native-runtime-root.js";
20
+ export { FlowContent, useFlowContainer } from "./flow-container.js";
6
21
  const GetUserFeedbackContext = createContext(null);
7
22
  const toClientOptionsKey = (value) => {
8
23
  if (value === null || typeof value !== "object") {
@@ -18,6 +33,26 @@ const toClientOptionsKey = (value) => {
18
33
  .map((key) => `${JSON.stringify(key)}:${toClientOptionsKey(record[key])}`)
19
34
  .join(",")}}`;
20
35
  };
36
+ const compareClientOptionsKeys = (left, right) => left < right ? -1 : left > right ? 1 : 0;
37
+ const parseNativeInitOptions = (_a) => {
38
+ var { capabilities } = _a, clientOptions = __rest(_a, ["capabilities"]);
39
+ const initOptions = parseInitOptions(Object.assign(Object.assign({}, clientOptions), (capabilities !== undefined
40
+ ? {
41
+ hostCapabilities: capabilities.map((capability) => typeof capability === "string"
42
+ ? { key: capability }
43
+ : Object.assign({}, capability)),
44
+ }
45
+ : {})));
46
+ const hostCapabilities = initOptions.hostCapabilities;
47
+ const normalizedHostCapabilities = hostCapabilities === null || hostCapabilities === void 0 ? void 0 : hostCapabilities.map((capability) => ({
48
+ capability,
49
+ key: toClientOptionsKey(capability),
50
+ })).sort((left, right) => compareClientOptionsKeys(left.key, right.key)).filter((entry, index, entries) => { var _a; return ((_a = entries[index - 1]) === null || _a === void 0 ? void 0 : _a.key) !== entry.key; }).map(({ capability }) => capability);
51
+ return Object.assign(Object.assign({}, initOptions), { hostCapabilities: normalizedHostCapabilities === undefined ||
52
+ normalizedHostCapabilities.length === 0
53
+ ? undefined
54
+ : normalizedHostCapabilities });
55
+ };
21
56
  export function GetUserFeedbackProvider({ children, clientOptions, instanceId: instanceIdProp, onError, webViewComponent, }) {
22
57
  var _a, _b;
23
58
  const [instanceId] = useState(() => {
@@ -26,9 +61,12 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
26
61
  ? normalizedInstanceId
27
62
  : createUniqueId("rn");
28
63
  });
29
- const normalizedClientOptions = parseInitOptions(clientOptions);
64
+ const [contentOwner, setContentOwner] = useState(null);
65
+ const [projection, setProjection] = useState(null);
66
+ const contentOwnerRef = useRef(null);
67
+ const normalizedClientOptions = parseNativeInitOptions(clientOptions);
30
68
  const clientOptionsKey = toClientOptionsKey(normalizedClientOptions);
31
- const coreUrl = (_b = (_a = normalizedClientOptions.runtimeEndpoints) === null || _a === void 0 ? void 0 : _a.coreUrl) !== null && _b !== void 0 ? _b : PRODUCTION_WIDGET_RUNTIME_ENDPOINTS.coreUrl;
69
+ const coreUrl = (_b = (_a = normalizedClientOptions.runtimeEndpoints) === null || _a === void 0 ? void 0 : _a.coreUrl) !== null && _b !== void 0 ? _b : REACT_NATIVE_PINNED_CORE_URL;
32
70
  const onErrorRef = useRef(onError);
33
71
  onErrorRef.current = onError;
34
72
  const reportError = useCallback((error) => {
@@ -48,6 +86,29 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
48
86
  });
49
87
  }
50
88
  const controller = controllerRef.current;
89
+ const attachViewProjection = useCallback((nextProjection) => {
90
+ setProjection(nextProjection);
91
+ return () => setProjection((current) => current === nextProjection ? null : current);
92
+ }, []);
93
+ const registerContent = useCallback((owner) => {
94
+ if (contentOwnerRef.current && contentOwnerRef.current !== owner) {
95
+ throw new Error("Only one FlowContent can be mounted in a GetUserFeedbackProvider");
96
+ }
97
+ contentOwnerRef.current = owner;
98
+ setContentOwner(owner);
99
+ return () => {
100
+ if (contentOwnerRef.current !== owner)
101
+ return;
102
+ contentOwnerRef.current = null;
103
+ setContentOwner(null);
104
+ };
105
+ }, []);
106
+ const flowContainerContext = useMemo(() => ({
107
+ client: controller.client,
108
+ contentOwner,
109
+ projection,
110
+ registerContent,
111
+ }), [contentOwner, controller.client, projection, registerContent]);
51
112
  const lifecycleRevisionRef = useRef(0);
52
113
  useEffect(() => {
53
114
  lifecycleRevisionRef.current += 1;
@@ -66,7 +127,8 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
66
127
  }
67
128
  }, [controller]);
68
129
  const handleHandleInvalidated = useCallback((message) => controller.onHandleInvalidated(message), [controller]);
69
- return createElement(GetUserFeedbackContext.Provider, { value: controller.client }, createElement(Fragment, null, children, createElement(NativeProviderRuntime, {
130
+ return createElement(GetUserFeedbackContext.Provider, { value: controller.client }, createElement(NativeFlowContainerContext.Provider, { value: flowContainerContext }, createElement(Fragment, null, children, createElement(NativeStandardFlowContainer), createElement(NativeProviderRuntime, {
131
+ attachViewProjection,
70
132
  coreUrl,
71
133
  coreWebViewComponent: webViewComponent,
72
134
  directSessionController: controller.directSessionController,
@@ -77,7 +139,7 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
77
139
  onError: reportError,
78
140
  onHandleInvalidated: handleHandleInvalidated,
79
141
  viewWebViewComponent: webViewComponent,
80
- })));
142
+ }))));
81
143
  }
82
144
  export function useGetUserFeedback() {
83
145
  const client = useContext(GetUserFeedbackContext);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * React Native is an independently deployed direct host, so this stays pinned
3
+ * to a retained Core artifact matching its projection contract. Advance it
4
+ * only after the matching artifact exists on the CDN; see
5
+ * docs/specs/2026-08-02-flow-interaction-model.md (Projection protocol and
6
+ * rollout).
7
+ */
8
+ export declare const REACT_NATIVE_PINNED_CORE_URL: "https://cdn.getuserfeedback.com/widget/core/v3/core.html";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * React Native is an independently deployed direct host, so this stays pinned
3
+ * to a retained Core artifact matching its projection contract. Advance it
4
+ * only after the matching artifact exists on the CDN; see
5
+ * docs/specs/2026-08-02-flow-interaction-model.md (Projection protocol and
6
+ * rollout).
7
+ */
8
+ export const REACT_NATIVE_PINNED_CORE_URL = "https://cdn.getuserfeedback.com/widget/core/v3/core.html";
@@ -1,22 +1,23 @@
1
1
  import type { CoreCommandEnvelope } from "@getuserfeedback/protocol";
2
2
  import type { EventHandleInvalidated } from "@getuserfeedback/protocol/internal/core-handle-invalidation";
3
- import type { ConfigureOptions, InitOptions } from "@getuserfeedback/sdk";
3
+ import type { InitOptions } from "@getuserfeedback/sdk";
4
4
  import { type ReactElement } from "react";
5
5
  import { type CoreWebViewHostProps } from "./core-webview-host.js";
6
6
  import { type NativeCoreCommandSession } from "./native-core-command-session.js";
7
7
  import { type NativeDirectCommandSession } from "./native-direct-command-session.js";
8
8
  import type { NativeDirectCommandSessionController } from "./native-direct-command-session-controller.js";
9
- import { type NativeViewHostProjectionProps } from "./native-view-host-projection.js";
9
+ import { type NativeViewHostProjectionProps, type NativeViewProjection } from "./native-view-host-projection.js";
10
10
  export type NativeRuntimeRootProps = {
11
11
  coreUrl: string;
12
12
  coreWebViewComponent?: CoreWebViewHostProps["webViewComponent"];
13
- startupCommands?: NativeRuntimeStartupCommands;
13
+ startupCommand?: NativeRuntimeInitCommand;
14
14
  onCommandSessionChange?: (session: NativeCoreCommandSession | null) => void;
15
15
  onError?: (error: Error) => void;
16
16
  onHandleInvalidated?: (message: EventHandleInvalidated) => void;
17
17
  onInvalidMessage?: (error: Error, value: unknown) => void;
18
18
  onStartupTerminalError?: (error: Error) => void;
19
19
  startupCommandSettlementTimeoutMs?: number;
20
+ attachViewProjection?: (projection: NativeViewProjection) => () => void;
20
21
  viewNativeViewComponent?: NativeViewHostProjectionProps["nativeViewComponent"];
21
22
  viewWebViewComponent?: NativeViewHostProjectionProps["webViewComponent"];
22
23
  };
@@ -25,20 +26,13 @@ type NativeRuntimeInitCommand = Omit<CoreCommandEnvelope, "command"> & {
25
26
  kind: "init";
26
27
  }>;
27
28
  };
28
- type NativeRuntimeConfigureCommand = Omit<CoreCommandEnvelope, "command"> & {
29
- command: Extract<CoreCommandEnvelope["command"], {
30
- kind: "configure";
31
- }>;
32
- };
33
- type NativeRuntimeStartupCommands = readonly [NativeRuntimeInitCommand] | readonly [NativeRuntimeInitCommand, NativeRuntimeConfigureCommand];
34
- type NativeProviderRuntimeProps = Omit<NativeRuntimeRootProps, "onCommandSessionChange" | "onStartupTerminalError" | "startupCommands"> & {
29
+ type NativeProviderRuntimeProps = Omit<NativeRuntimeRootProps, "onCommandSessionChange" | "onStartupTerminalError" | "startupCommand"> & {
35
30
  /** Stable, exclusive controller retained and disposed by the component that owns this runtime's mounted lifetime. */
36
31
  directSessionController: NativeDirectCommandSessionController;
37
- initialConfigureOptions?: ConfigureOptions;
38
32
  initOptions: InitOptions;
39
33
  instanceId: string;
40
34
  onCommandSessionChange?: (session: NativeDirectCommandSession | null) => void;
41
35
  };
42
- export declare function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs, startupCommands, viewNativeViewComponent, viewWebViewComponent, }: NativeRuntimeRootProps): ReactElement;
43
- export declare function NativeProviderRuntime({ directSessionController, initOptions, initialConfigureOptions, instanceId, onCommandSessionChange, onError, ...runtimeProps }: NativeProviderRuntimeProps): ReactElement;
36
+ export declare function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewComponent, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs, startupCommand, viewNativeViewComponent, viewWebViewComponent, }: NativeRuntimeRootProps): ReactElement;
37
+ export declare function NativeProviderRuntime({ directSessionController, initOptions, instanceId, onCommandSessionChange, onError, ...runtimeProps }: NativeProviderRuntimeProps): ReactElement;
44
38
  export {};
@@ -19,14 +19,7 @@ import { NativeViewHostProjection, } from "./native-view-host-projection.js";
19
19
  import { createNativeViewRecordController, } from "./native-view-record-controller.js";
20
20
  import { createProviderCommandEnvelope, REACT_NATIVE_CLIENT_META, } from "./provider-command-helpers.js";
21
21
  const DEFAULT_STARTUP_COMMAND_SETTLEMENT_TIMEOUT_MS = 30000;
22
- async function dispatchStartupCommands(input) {
23
- for (const command of input.commands) {
24
- await input.commandSession.dispatch(command, {
25
- settlementTimeoutMs: input.timeoutMs,
26
- });
27
- }
28
- }
29
- export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs = DEFAULT_STARTUP_COMMAND_SETTLEMENT_TIMEOUT_MS, startupCommands, viewNativeViewComponent, viewWebViewComponent, }) {
22
+ export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewComponent, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs = DEFAULT_STARTUP_COMMAND_SETTLEMENT_TIMEOUT_MS, startupCommand, viewNativeViewComponent, viewWebViewComponent, }) {
30
23
  const [coreHostRevision, setCoreHostRevision] = useState(0);
31
24
  const [session, setSession] = useState(null);
32
25
  const sessionRef = useRef(null);
@@ -42,8 +35,8 @@ export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSess
42
35
  onInvalidMessageRef.current = onInvalidMessage;
43
36
  const onStartupTerminalErrorRef = useRef(onStartupTerminalError);
44
37
  onStartupTerminalErrorRef.current = onStartupTerminalError;
45
- const startupCommandsRef = useRef(startupCommands);
46
- startupCommandsRef.current = startupCommands;
38
+ const startupCommandRef = useRef(startupCommand);
39
+ startupCommandRef.current = startupCommand;
47
40
  const reportError = useCallback((error) => {
48
41
  var _a;
49
42
  try {
@@ -126,12 +119,15 @@ export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSess
126
119
  setSession(nextSession);
127
120
  notifyCommandSessionObserver(nextSession.commandSessionObserver, commandSessionOwner.session);
128
121
  };
129
- const startupCommands = startupCommandsRef.current;
130
- if (startupCommands) {
131
- void dispatchStartupCommands({
132
- commands: startupCommands,
133
- commandSession: commandSessionOwner.session,
134
- timeoutMs: startupCommandSettlementTimeoutMs,
122
+ const startupCommand = startupCommandRef.current;
123
+ if (startupCommand) {
124
+ // TODO(architecture): Reconcile direct-runtime startup command settlement
125
+ // with view projection. Android smoke can project the opened view while the
126
+ // public open request still times out waiting for Core's settlement.
127
+ // [principle=one-authoritative-command-lifecycle] [scope=slice] [priority=high] [impact=high] [risk=high] [epic=runtime-neutral-view-host-orchestration] [epic_order=2]
128
+ void commandSessionOwner.session
129
+ .dispatch(startupCommand, {
130
+ settlementTimeoutMs: startupCommandSettlementTimeoutMs,
135
131
  })
136
132
  .then(commitSession)
137
133
  .catch((error) => {
@@ -206,6 +202,23 @@ export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSess
206
202
  }
207
203
  }
208
204
  }, [notifyCommandSessionObserver]);
205
+ useLayoutEffect(() => {
206
+ if (!session || !attachViewProjection)
207
+ return;
208
+ return attachViewProjection({
209
+ controller: session.controller,
210
+ key: session.key,
211
+ nativeViewComponent: viewNativeViewComponent,
212
+ onInvalidMessage,
213
+ webViewComponent: viewWebViewComponent,
214
+ });
215
+ }, [
216
+ attachViewProjection,
217
+ onInvalidMessage,
218
+ session,
219
+ viewNativeViewComponent,
220
+ viewWebViewComponent,
221
+ ]);
209
222
  return createElement(Fragment, null, createElement(CoreWebViewHost, {
210
223
  coreUrl,
211
224
  key: `${coreUrl}:${coreHostRevision}`,
@@ -214,7 +227,7 @@ export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSess
214
227
  onDisconnected: handleDisconnected,
215
228
  onInvalidMessage,
216
229
  webViewComponent: coreWebViewComponent,
217
- }), session === null
230
+ }), session === null || attachViewProjection
218
231
  ? null
219
232
  : createElement(NativeViewHostProjection, {
220
233
  controller: session.controller,
@@ -225,7 +238,7 @@ export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSess
225
238
  }));
226
239
  }
227
240
  export function NativeProviderRuntime(_a) {
228
- var { directSessionController, initOptions, initialConfigureOptions, instanceId, onCommandSessionChange, onError } = _a, runtimeProps = __rest(_a, ["directSessionController", "initOptions", "initialConfigureOptions", "instanceId", "onCommandSessionChange", "onError"]);
241
+ var { directSessionController, initOptions, instanceId, onCommandSessionChange, onError } = _a, runtimeProps = __rest(_a, ["directSessionController", "initOptions", "instanceId", "onCommandSessionChange", "onError"]);
229
242
  const directSessionRef = useRef(null);
230
243
  useLayoutEffect(() => {
231
244
  directSessionController.beginAttachmentAttempt();
@@ -254,22 +267,12 @@ export function NativeProviderRuntime(_a) {
254
267
  }
255
268
  onCommandSessionChange === null || onCommandSessionChange === void 0 ? void 0 : onCommandSessionChange(directSessionController.session);
256
269
  }, [directSessionController, onCommandSessionChange]);
257
- const startupCommands = useMemo(() => {
258
- const initCommand = createProviderCommandEnvelope({
270
+ const startupCommand = useMemo(() => {
271
+ return createProviderCommandEnvelope({
259
272
  clientMeta: REACT_NATIVE_CLIENT_META,
260
273
  command: { kind: "init", opts: initOptions },
261
274
  instanceId,
262
275
  });
263
- if (initialConfigureOptions === undefined) {
264
- return [initCommand];
265
- }
266
- return [
267
- initCommand,
268
- createProviderCommandEnvelope({
269
- command: { kind: "configure", opts: initialConfigureOptions },
270
- instanceId,
271
- }),
272
- ];
273
- }, [initialConfigureOptions, initOptions, instanceId]);
274
- return createElement(NativeRuntimeRoot, Object.assign(Object.assign({}, runtimeProps), { onCommandSessionChange: handleCommandSessionChange, onError, onStartupTerminalError: handleStartupTerminalError, startupCommands }));
276
+ }, [initOptions, instanceId]);
277
+ return createElement(NativeRuntimeRoot, Object.assign(Object.assign({}, runtimeProps), { onCommandSessionChange: handleCommandSessionChange, onError, onStartupTerminalError: handleStartupTerminalError, startupCommand }));
275
278
  }
@@ -1,9 +1,20 @@
1
1
  import { type ComponentType, type ReactElement } from "react";
2
- import type { NativeViewRecordController } from "./native-view-record-controller.js";
2
+ import type { NativeViewPresentationMode } from "./native-view-presentation-surface.js";
3
+ import type { NativeViewRecord, NativeViewRecordController } from "./native-view-record-controller.js";
3
4
  export type NativeViewHostProjectionProps = {
4
5
  controller: NativeViewRecordController;
5
6
  onInvalidMessage?: (error: Error, value: unknown) => void;
6
7
  nativeViewComponent?: ComponentType<Record<string, unknown>>;
8
+ presentationMode?: NativeViewPresentationMode;
7
9
  webViewComponent?: ComponentType<Record<string, unknown>>;
8
10
  };
9
- export declare function NativeViewHostProjection({ controller, nativeViewComponent, onInvalidMessage, webViewComponent, }: NativeViewHostProjectionProps): ReactElement;
11
+ export type NativeViewProjection = Readonly<Omit<NativeViewHostProjectionProps, "presentationMode"> & {
12
+ key: number;
13
+ }>;
14
+ export declare function NativeProjectedViewContent({ controller, onInvalidMessage, record, webViewComponent, }: {
15
+ controller: NativeViewRecordController;
16
+ onInvalidMessage?: (error: Error, value: unknown) => void;
17
+ record: NativeViewRecord;
18
+ webViewComponent?: ComponentType<Record<string, unknown>>;
19
+ }): ReactElement;
20
+ export declare function NativeViewHostProjection({ controller, nativeViewComponent, onInvalidMessage, presentationMode, webViewComponent, }: NativeViewHostProjectionProps): ReactElement;
@@ -1,8 +1,7 @@
1
1
  import { createElement, Fragment, useCallback, useEffect, useLayoutEffect, useRef, useSyncExternalStore, } from "react";
2
2
  import { NativeViewPresentationSurface } from "./native-view-presentation-surface.js";
3
3
  import { ViewWebViewHost } from "./view-webview-host.js";
4
- function NativeViewHostProjectionEntry({ controller, nativeViewComponent, onInvalidMessage, record, webViewComponent, }) {
5
- var _a;
4
+ export function NativeProjectedViewContent({ controller, onInvalidMessage, record, webViewComponent, }) {
6
5
  const detachConnectionRef = useRef(null);
7
6
  const detachConnection = useCallback(() => {
8
7
  const detach = detachConnectionRef.current;
@@ -18,6 +17,26 @@ function NativeViewHostProjectionEntry({ controller, nativeViewComponent, onInva
18
17
  detachConnectionRef.current = controllerDetach;
19
18
  }
20
19
  }, [controller, record.allocationId]);
20
+ useEffect(() => detachConnection, [detachConnection]);
21
+ return createElement(ViewWebViewHost, {
22
+ generation: record.generation,
23
+ onActivationRetryExhausted: (error) => {
24
+ controller.failViewAllocation({
25
+ allocationId: record.allocationId,
26
+ error,
27
+ viewId: record.viewId,
28
+ });
29
+ },
30
+ onConnected: handleConnected,
31
+ onDisconnected: detachConnection,
32
+ onInvalidMessage,
33
+ srcdoc: record.srcdoc,
34
+ viewId: record.viewId,
35
+ webViewComponent,
36
+ });
37
+ }
38
+ function NativeViewHostProjectionEntry({ controller, nativeViewComponent, onInvalidMessage, presentationMode, record, webViewComponent, }) {
39
+ var _a;
21
40
  const layoutTransactionId = (_a = record.size) === null || _a === void 0 ? void 0 : _a.layoutTransactionId;
22
41
  useLayoutEffect(() => {
23
42
  if (!layoutTransactionId) {
@@ -48,30 +67,20 @@ function NativeViewHostProjectionEntry({ controller, nativeViewComponent, onInva
48
67
  viewId: record.viewId,
49
68
  });
50
69
  });
51
- useEffect(() => detachConnection, [detachConnection]);
52
- return createElement(NativeViewPresentationSurface, { nativeViewComponent, record }, createElement(ViewWebViewHost, {
53
- generation: record.generation,
54
- onActivationRetryExhausted: (error) => {
55
- controller.failViewAllocation({
56
- allocationId: record.allocationId,
57
- error,
58
- viewId: record.viewId,
59
- });
60
- },
61
- onConnected: handleConnected,
62
- onDisconnected: detachConnection,
70
+ return createElement(NativeViewPresentationSurface, { nativeViewComponent, presentationMode, record }, createElement(NativeProjectedViewContent, {
71
+ controller,
63
72
  onInvalidMessage,
64
- srcdoc: record.srcdoc,
65
- viewId: record.viewId,
73
+ record,
66
74
  webViewComponent,
67
75
  }));
68
76
  }
69
- export function NativeViewHostProjection({ controller, nativeViewComponent, onInvalidMessage, webViewComponent, }) {
77
+ export function NativeViewHostProjection({ controller, nativeViewComponent, onInvalidMessage, presentationMode, webViewComponent, }) {
70
78
  const records = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
71
79
  return createElement(Fragment, null, records.map((record) => createElement(NativeViewHostProjectionEntry, {
72
80
  controller,
73
81
  nativeViewComponent,
74
82
  onInvalidMessage,
83
+ presentationMode,
75
84
  record,
76
85
  webViewComponent,
77
86
  key: record.allocationId,
@@ -1,7 +1,9 @@
1
1
  import { type ComponentType, type ReactElement, type ReactNode } from "react";
2
2
  import type { NativeViewRecord } from "./native-view-record-controller.js";
3
- export declare function NativeViewPresentationSurface({ children, nativeViewComponent, record, }: {
3
+ export type NativeViewPresentationMode = "content" | "overlay";
4
+ export declare function NativeViewPresentationSurface({ children, presentationMode, nativeViewComponent, record, }: {
4
5
  children?: ReactNode;
6
+ presentationMode?: NativeViewPresentationMode;
5
7
  nativeViewComponent?: ComponentType<Record<string, unknown>>;
6
8
  record: NativeViewRecord;
7
9
  }): ReactElement;
@@ -18,6 +18,11 @@ const HIDDEN_HOST_SURFACE_STYLE = {
18
18
  const PRESENTED_HOST_SURFACE_STYLE = {
19
19
  zIndex: 2147483647,
20
20
  };
21
+ const CONTENT_HOST_SURFACE_STYLE = {
22
+ alignItems: "center",
23
+ maxHeight: "100%",
24
+ maxWidth: "100%",
25
+ };
21
26
  const TOP_PLACEMENT_STYLE = {
22
27
  justifyContent: "flex-start",
23
28
  };
@@ -50,8 +55,30 @@ const resolveNativeView = () => {
50
55
  }
51
56
  return reactNativeModule.View;
52
57
  };
53
- export function NativeViewPresentationSurface({ children, nativeViewComponent, record, }) {
54
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
58
+ export function NativeViewPresentationSurface({ children, presentationMode = "overlay", nativeViewComponent, record, }) {
59
+ var _a, _b, _c;
60
+ const NativeView = nativeViewComponent !== null && nativeViewComponent !== void 0 ? nativeViewComponent : resolveNativeView();
61
+ const isPresented = record.isActive && record.size !== null;
62
+ const hostStyle = presentationMode === "content" && isPresented
63
+ ? Object.assign(Object.assign({}, CONTENT_HOST_SURFACE_STYLE), { height: (_a = record.size) === null || _a === void 0 ? void 0 : _a.height, width: (_b = record.size) === null || _b === void 0 ? void 0 : _b.width }) : Object.assign(Object.assign(Object.assign({}, HOST_SURFACE_STYLE), (((_c = record.size) === null || _c === void 0 ? void 0 : _c.placement) === "docked-top"
64
+ ? TOP_PLACEMENT_STYLE
65
+ : BOTTOM_PLACEMENT_STYLE)), (isPresented
66
+ ? PRESENTED_HOST_SURFACE_STYLE
67
+ : HIDDEN_HOST_SURFACE_STYLE));
68
+ return createElement(NativeView, {
69
+ accessibilityElementsHidden: !isPresented,
70
+ importantForAccessibility: isPresented ? "auto" : "no-hide-descendants",
71
+ pointerEvents: isPresented ? "box-none" : "none",
72
+ style: hostStyle,
73
+ testID: `gx-native-view-presentation-${record.viewId}`,
74
+ }, createElement(NativeViewPresentationFrame, {
75
+ children,
76
+ nativeViewComponent,
77
+ record,
78
+ }));
79
+ }
80
+ function NativeViewPresentationFrame({ children, nativeViewComponent, record, }) {
81
+ var _a, _b, _c, _d, _e, _f, _g, _h;
55
82
  const NativeView = nativeViewComponent !== null && nativeViewComponent !== void 0 ? nativeViewComponent : resolveNativeView();
56
83
  const isPresented = record.isActive && record.size !== null;
57
84
  const hasMeasuredFrame = isPresented || ((_a = record.size) === null || _a === void 0 ? void 0 : _a.layoutTransactionId) !== undefined;
@@ -74,19 +101,9 @@ export function NativeViewPresentationSurface({ children, nativeViewComponent, r
74
101
  borderTopRightRadius: cornerRadii.topRight,
75
102
  };
76
103
  return createElement(NativeView, {
77
- accessibilityElementsHidden: !isPresented,
78
- importantForAccessibility: isPresented ? "auto" : "no-hide-descendants",
79
- pointerEvents: isPresented ? "box-none" : "none",
80
- style: Object.assign(Object.assign(Object.assign({}, HOST_SURFACE_STYLE), (((_g = record.size) === null || _g === void 0 ? void 0 : _g.placement) === "docked-top"
81
- ? TOP_PLACEMENT_STYLE
82
- : BOTTOM_PLACEMENT_STYLE)), (isPresented
83
- ? PRESENTED_HOST_SURFACE_STYLE
84
- : HIDDEN_HOST_SURFACE_STYLE)),
85
- testID: `gx-native-view-presentation-${record.viewId}`,
86
- }, createElement(NativeView, {
87
104
  pointerEvents: isPresented ? "auto" : "none",
88
105
  style: hasMeasuredFrame
89
- ? Object.assign(Object.assign(Object.assign(Object.assign({}, MEASURED_VIEW_FRAME_STYLE), cornerRadiusStyle), frameShadowStyle), { height: (_h = record.size) === null || _h === void 0 ? void 0 : _h.height, width: (_j = record.size) === null || _j === void 0 ? void 0 : _j.width }) : PREPARING_VIEW_FRAME_STYLE,
106
+ ? 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,
90
107
  testID: `gx-native-view-frame-${record.viewId}`,
91
108
  }, createElement(NativeView, {
92
109
  style: Object.assign(Object.assign({}, VIEW_FRAME_CONTENT_STYLE), cornerRadiusStyle),
@@ -95,5 +112,5 @@ export function NativeViewPresentationSurface({ children, nativeViewComponent, r
95
112
  pointerEvents: "none",
96
113
  style: Object.assign(Object.assign(Object.assign(Object.assign({}, VIEW_FRAME_BORDER_OVERLAY_STYLE), cornerRadiusStyle), borderStyle), frameOverlayShadowStyle),
97
114
  testID: `gx-native-view-frame-border-${record.viewId}`,
98
- })));
115
+ }));
99
116
  }
@@ -236,6 +236,7 @@ export function createNativeViewRecordController(input) {
236
236
  layoutReportRevision: 0,
237
237
  pendingCoreToViewMessages: [],
238
238
  preparation,
239
+ retainedSize: null,
239
240
  size: null,
240
241
  srcdoc: action.srcdoc,
241
242
  committedLayoutReportOccurrence: null,
@@ -541,6 +542,7 @@ export function createNativeViewRecordController(input) {
541
542
  }
542
543
  };
543
544
  const handleMessage = (value) => {
545
+ var _a;
544
546
  if (coreConnectionFailed ||
545
547
  detached ||
546
548
  disposed ||
@@ -554,6 +556,14 @@ export function createNativeViewRecordController(input) {
554
556
  connected.data.viewId === viewId) {
555
557
  const wasViewConnected = entry.isViewConnected;
556
558
  entry.isViewConnected = true;
559
+ if (entry.size === null &&
560
+ ((_a = entry.retainedSize) === null || _a === void 0 ? void 0 : _a.connectionId) === connection.connectionId) {
561
+ entry.size = entry.retainedSize.size;
562
+ const readySize = connectionReadiness.recordSize(entry.size);
563
+ if (readySize) {
564
+ deliverReadiness(readySize);
565
+ }
566
+ }
557
567
  drainCoreToViewMessages(entry);
558
568
  if (!wasViewConnected &&
559
569
  !coreConnectionFailed &&
@@ -600,6 +610,10 @@ export function createNativeViewRecordController(input) {
600
610
  }
601
611
  const size = toViewSizeReport(parsed.data);
602
612
  entry.size = size;
613
+ entry.retainedSize = {
614
+ connectionId: connection.connectionId,
615
+ size,
616
+ };
603
617
  entry.layoutReportRevision += 1;
604
618
  const readySize = connectionReadiness.recordSize(size);
605
619
  if (readySize) {
@@ -636,6 +650,10 @@ export function createNativeViewRecordController(input) {
636
650
  entry.viewConnection = connection;
637
651
  entry.isViewConnected = false;
638
652
  entry.committedLayoutReportOccurrence = null;
653
+ const retainedSize = entry.retainedSize;
654
+ if ((retainedSize === null || retainedSize === void 0 ? void 0 : retainedSize.connectionId) !== connection.connectionId) {
655
+ entry.retainedSize = null;
656
+ }
639
657
  previousDetach === null || previousDetach === void 0 ? void 0 : previousDetach();
640
658
  if (entry.size !== null) {
641
659
  entry.size = null;
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.0.8";
6
+ export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "5.1.0";
@@ -10,6 +10,8 @@ const VIEW_WEBVIEW_BRIDGE_CONFIG = {
10
10
  connectTimeoutMessage: "Timed out connecting view to the native host",
11
11
  connectTimeoutMs: 30000,
12
12
  inactiveConnectionMessage: "View WebView bridge connection is not active",
13
+ reactivateOnNativeEpochChange: true,
14
+ throwOnUnavailablePost: true,
13
15
  };
14
16
  export const parseViewWebViewBridgeMessage = (value) => parseWebViewConnectionBridgeMessage(value, {
15
17
  invalidIdentity: "Invalid view WebView bridge message identity",
@@ -40,16 +42,16 @@ const buildViewHostBootstrapScript = () => {
40
42
  var connection = bridge.createConnection({
41
43
  connectionIdentity: { viewId: config.viewId },
42
44
  generation: config.generation,
43
- onActivated: function (activeTransport) {
45
+ prepareActivation: function (activeTransport) {
44
46
  transport = activeTransport;
45
47
  if (typeof options.onMessage === "function") {
46
48
  activeTransport.subscribe(options.onMessage);
47
49
  }
48
- activeTransport.postMessage({
49
- t: "getuserfeedback:action:viewConnect",
50
- gen: config.generation,
51
- viewId: config.viewId
52
- });
50
+ },
51
+ onActivated: function (_activeTransport, reactivate) {
52
+ if (reactivate) {
53
+ window.dispatchEvent(new Event("resize"));
54
+ }
53
55
  },
54
56
  onError: options.onError
55
57
  });
@@ -1,6 +1,7 @@
1
1
  import { type ViewWebViewBridgeMessage } from "./view-webview-bridge-adapter.js";
2
2
  import { type WebViewHostController } from "./webview-host-controller.js";
3
3
  export type ViewWebViewHostConnection = {
4
+ connectionId: string;
4
5
  generation: number;
5
6
  transport: {
6
7
  postMessage: (message: unknown) => void;
@@ -11,18 +11,34 @@ export const createViewWebViewHostController = (input) => {
11
11
  buildActivationScript: buildViewWebViewBridgeScript,
12
12
  buildHostMessageScript: buildViewWebViewHostMessageScript,
13
13
  onConnected: (connection) => {
14
+ const listeners = new Set();
14
15
  input.onConnected({
16
+ connectionId: connection.connectionId,
15
17
  generation: connection.generation,
16
18
  transport: {
17
19
  postMessage: connection.postMessage,
18
- subscribe: (listener) => connection.subscribe((message) => {
19
- if (message.kind === "message") {
20
- listener(message.message);
21
- }
22
- }),
20
+ subscribe: (listener) => {
21
+ listeners.add(listener);
22
+ const unsubscribe = connection.subscribe((message) => {
23
+ if (message.kind === "message") {
24
+ listener(message.message);
25
+ }
26
+ });
27
+ return () => {
28
+ listeners.delete(listener);
29
+ unsubscribe();
30
+ };
31
+ },
23
32
  },
24
33
  viewId: input.viewId,
25
34
  });
35
+ for (const listener of listeners) {
36
+ listener({
37
+ gen: input.generation,
38
+ t: "getuserfeedback:action:viewConnect",
39
+ viewId: input.viewId,
40
+ });
41
+ }
26
42
  },
27
43
  onDisconnected: input.onDisconnected,
28
44
  onActivationRetryExhausted: input.onActivationRetryExhausted,
@@ -4,6 +4,8 @@ export type WebViewConnectionBridgeScriptConfig = {
4
4
  connectTimeoutMessage: string;
5
5
  connectTimeoutMs: number;
6
6
  inactiveConnectionMessage: string;
7
+ reactivateOnNativeEpochChange: boolean;
8
+ throwOnUnavailablePost: boolean;
7
9
  };
8
10
  export declare const buildWebViewConnectionBridgeScript: (input: ({
9
11
  phase: "documentStart";
@@ -8,6 +8,8 @@ export const buildWebViewConnectionBridgeScript = (input) => {
8
8
  const bridgeProtocolVersion = serializeForJavaScript(input.config.bridgeProtocolVersion);
9
9
  const connectTimeoutMessage = serializeForJavaScript(input.config.connectTimeoutMessage);
10
10
  const inactiveConnectionMessage = serializeForJavaScript(input.config.inactiveConnectionMessage);
11
+ const reactivateOnNativeEpochChange = String(input.config.reactivateOnNativeEpochChange);
12
+ const throwOnUnavailablePost = String(input.config.throwOnUnavailablePost);
11
13
  return `
12
14
  (function () {
13
15
  var bridgeId = ${bridgeId};
@@ -15,6 +17,8 @@ export const buildWebViewConnectionBridgeScript = (input) => {
15
17
  var bridgeProtocolVersion = ${bridgeProtocolVersion};
16
18
  var connectTimeoutMessage = ${connectTimeoutMessage};
17
19
  var inactiveConnectionMessage = ${inactiveConnectionMessage};
20
+ var reactivateOnNativeEpochChange = ${reactivateOnNativeEpochChange};
21
+ var throwOnUnavailablePost = ${throwOnUnavailablePost};
18
22
  var existingBridge = window[bridgeGlobalKey];
19
23
 
20
24
  if (
@@ -22,11 +26,11 @@ export const buildWebViewConnectionBridgeScript = (input) => {
22
26
  existingBridge.protocolVersion === bridgeProtocolVersion &&
23
27
  existingBridge.bridgeId === bridgeId
24
28
  ) {
25
- var didConnect = false;
29
+ var activationResult = false;
26
30
  if (${String(activateNative)}) {
27
- didConnect = existingBridge.activateNative(${String(activationEpoch)});
31
+ activationResult = existingBridge.activateNative(${String(activationEpoch)});
28
32
  }
29
- if (!didConnect) {
33
+ if (activationResult === false) {
30
34
  existingBridge.notifyConnected();
31
35
  }
32
36
  return;
@@ -41,6 +45,7 @@ export const buildWebViewConnectionBridgeScript = (input) => {
41
45
  var isClosed = false;
42
46
  var isNativeActivated = ${String(activateNative)};
43
47
  var nativeEpoch = ${String(activationEpoch)};
48
+ var activatedNativeEpoch = null;
44
49
 
45
50
  function postToNative(message) {
46
51
  var nativeBridge = window.ReactNativeWebView;
@@ -90,6 +95,7 @@ export const buildWebViewConnectionBridgeScript = (input) => {
90
95
  connectPromise: null,
91
96
  connectTimeout: null,
92
97
  onActivated: options.onActivated,
98
+ prepareActivation: options.prepareActivation,
93
99
  onError: options.onError,
94
100
  rejectConnect: null,
95
101
  resolveConnect: null,
@@ -103,7 +109,12 @@ export const buildWebViewConnectionBridgeScript = (input) => {
103
109
  }
104
110
  return;
105
111
  }
106
- postToNative(toConnectionMessage("message", connection, message));
112
+ if (
113
+ !postToNative(toConnectionMessage("message", connection, message)) &&
114
+ throwOnUnavailablePost
115
+ ) {
116
+ throw new Error(inactiveConnectionMessage);
117
+ }
107
118
  },
108
119
  subscribe: function (listener) {
109
120
  if (activeConnection !== connection) {
@@ -154,7 +165,7 @@ export const buildWebViewConnectionBridgeScript = (input) => {
154
165
  }, ${connectTimeoutMs});
155
166
  });
156
167
  connection.connectPromise = connectPromise;
157
- bridge.activateConnection();
168
+ bridge.activateConnection(false);
158
169
  return connectPromise;
159
170
  },
160
171
  disconnect: function () {
@@ -190,26 +201,34 @@ export const buildWebViewConnectionBridgeScript = (input) => {
190
201
  bridgeId: bridgeId,
191
202
  protocolVersion: bridgeProtocolVersion,
192
203
  createConnection: createConnection,
193
- activateConnection: function () {
204
+ activateConnection: function (reactivate) {
194
205
  var connection = activeConnection;
195
206
  if (
196
207
  !isNativeActivated ||
197
208
  !connection ||
198
- connection.isConnected
209
+ (connection.isConnected && !reactivate)
199
210
  ) {
200
211
  return false;
201
212
  }
213
+ if (typeof connection.prepareActivation === "function") {
214
+ try {
215
+ connection.prepareActivation(connection.transport, reactivate);
216
+ } catch {
217
+ return false;
218
+ }
219
+ }
202
220
  try {
203
221
  if (!postToNative(toConnectionMessage("connected", connection))) {
204
222
  return false;
205
223
  }
206
224
  } catch (error) {
207
- reportConnectionError(connection, error);
208
225
  return false;
209
226
  }
210
227
  connection.isConnected = true;
211
228
  try {
212
- connection.onActivated(connection.transport);
229
+ if (typeof connection.onActivated === "function") {
230
+ connection.onActivated(connection.transport, reactivate);
231
+ }
213
232
  } catch (error) {
214
233
  connection.isConnected = false;
215
234
  connection.isDisconnected = true;
@@ -233,6 +252,7 @@ export const buildWebViewConnectionBridgeScript = (input) => {
233
252
  connection.connectPromise = null;
234
253
  return false;
235
254
  }
255
+ activatedNativeEpoch = nativeEpoch;
236
256
  if (connection.resolveConnect) {
237
257
  if (connection.connectTimeout) {
238
258
  clearTimeout(connection.connectTimeout);
@@ -246,14 +266,20 @@ export const buildWebViewConnectionBridgeScript = (input) => {
246
266
  },
247
267
  activateNative: function (epoch) {
248
268
  if (typeof epoch !== "number" || !Number.isSafeInteger(epoch) || epoch < 0) {
249
- return false;
269
+ return null;
250
270
  }
251
271
  if (nativeEpoch !== null && epoch < nativeEpoch) {
252
- return false;
272
+ return null;
253
273
  }
274
+ var shouldReactivate =
275
+ reactivateOnNativeEpochChange &&
276
+ activeConnection &&
277
+ activeConnection.isConnected &&
278
+ activatedNativeEpoch !== epoch;
254
279
  isNativeActivated = true;
255
280
  nativeEpoch = epoch;
256
- return bridge.activateConnection();
281
+ var didActivate = bridge.activateConnection(shouldReactivate);
282
+ return shouldReactivate && !didActivate ? null : didActivate;
257
283
  },
258
284
  notifyConnected: function () {
259
285
  if (activeConnection && activeConnection.isConnected) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getuserfeedback/react-native",
3
- "version": "5.0.8",
3
+ "version": "5.1.0",
4
4
  "description": "getuserfeedback React Native SDK",
5
5
  "keywords": [
6
6
  "getuserfeedback",
@@ -38,14 +38,14 @@
38
38
  "publish:dry-run": "node ../../scripts/publish-package.cjs . --verify-packed-runtime scripts/verify-packed-runtime.ts --expect-dependency @getuserfeedback/protocol:../protocol/package.json --expect-dependency @getuserfeedback/sdk:../sdk/package.json -- --dry-run",
39
39
  "publish:npm": "node ../../scripts/publish-package.cjs . --verify-packed-runtime scripts/verify-packed-runtime.ts --expect-dependency @getuserfeedback/protocol:../protocol/package.json --expect-dependency @getuserfeedback/sdk:../sdk/package.json",
40
40
  "build": "bun scripts/build.ts",
41
- "typecheck": "tsc -p tsconfig.json --noEmit --tsBuildInfoFile tsconfig.typecheck.tsbuildinfo && tsc -p tsconfig-scripts.json",
41
+ "typecheck": "tsc -p tsconfig.json --noEmit --composite false --incremental false && tsc -p tsconfig-scripts.json --noEmit --composite false --incremental false",
42
42
  "test": "bun test --dots",
43
43
  "test:changed": "bun test --changed=${TEST_CHANGED_BASE:-HEAD} --pass-with-no-tests --dots",
44
44
  "lint": "biome check ."
45
45
  },
46
46
  "dependencies": {
47
- "@getuserfeedback/protocol": "^3.31.0",
48
- "@getuserfeedback/sdk": "^0.12.35",
47
+ "@getuserfeedback/protocol": "^3.31.2",
48
+ "@getuserfeedback/sdk": "^0.13.0",
49
49
  "robot3": "^1.2.0"
50
50
  },
51
51
  "peerDependencies": {