@getuserfeedback/react-native 5.0.9 → 5.1.1
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 +7 -2
- package/dist/core-webview-bridge-adapter.js +2 -0
- package/dist/flow-container.d.ts +26 -0
- package/dist/flow-container.js +75 -0
- package/dist/native-flow-container-context.d.ts +14 -0
- package/dist/native-flow-container-context.js +2 -0
- package/dist/native-provider-client.js +15 -10
- package/dist/native-provider.d.ts +2 -0
- package/dist/native-provider.js +68 -6
- package/dist/native-runtime-endpoints.d.ts +8 -0
- package/dist/native-runtime-endpoints.js +8 -0
- package/dist/native-runtime-root.d.ts +3 -2
- package/dist/native-runtime-root.js +23 -2
- package/dist/native-view-host-projection.d.ts +13 -2
- package/dist/native-view-host-projection.js +26 -17
- package/dist/native-view-presentation-surface.d.ts +3 -1
- package/dist/native-view-presentation-surface.js +31 -14
- package/dist/native-view-record-controller.js +18 -0
- package/dist/version.js +1 -1
- package/dist/view-webview-bridge-adapter.js +8 -6
- package/dist/view-webview-host-controller.d.ts +1 -0
- package/dist/view-webview-host-controller.js +21 -5
- package/dist/webview-connection-bridge-script.d.ts +2 -0
- package/dist/webview-connection-bridge-script.js +38 -12
- package/package.json +3 -3
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
|
+
}
|
|
@@ -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>;
|
|
@@ -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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
*
|
package/dist/native-provider.js
CHANGED
|
@@ -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
|
|
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
|
|
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 :
|
|
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";
|
|
@@ -6,7 +6,7 @@ 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"];
|
|
@@ -17,6 +17,7 @@ export type NativeRuntimeRootProps = {
|
|
|
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
|
};
|
|
@@ -32,6 +33,6 @@ type NativeProviderRuntimeProps = Omit<NativeRuntimeRootProps, "onCommandSession
|
|
|
32
33
|
instanceId: string;
|
|
33
34
|
onCommandSessionChange?: (session: NativeDirectCommandSession | null) => void;
|
|
34
35
|
};
|
|
35
|
-
export declare function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs, startupCommand, viewNativeViewComponent, viewWebViewComponent, }: NativeRuntimeRootProps): ReactElement;
|
|
36
|
+
export declare function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewComponent, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs, startupCommand, viewNativeViewComponent, viewWebViewComponent, }: NativeRuntimeRootProps): ReactElement;
|
|
36
37
|
export declare function NativeProviderRuntime({ directSessionController, initOptions, instanceId, onCommandSessionChange, onError, ...runtimeProps }: NativeProviderRuntimeProps): ReactElement;
|
|
37
38
|
export {};
|
|
@@ -19,7 +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
|
-
export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs = DEFAULT_STARTUP_COMMAND_SETTLEMENT_TIMEOUT_MS, startupCommand, viewNativeViewComponent, viewWebViewComponent, }) {
|
|
22
|
+
export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewComponent, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs = DEFAULT_STARTUP_COMMAND_SETTLEMENT_TIMEOUT_MS, startupCommand, viewNativeViewComponent, viewWebViewComponent, }) {
|
|
23
23
|
const [coreHostRevision, setCoreHostRevision] = useState(0);
|
|
24
24
|
const [session, setSession] = useState(null);
|
|
25
25
|
const sessionRef = useRef(null);
|
|
@@ -121,6 +121,10 @@ export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSess
|
|
|
121
121
|
};
|
|
122
122
|
const startupCommand = startupCommandRef.current;
|
|
123
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]
|
|
124
128
|
void commandSessionOwner.session
|
|
125
129
|
.dispatch(startupCommand, {
|
|
126
130
|
settlementTimeoutMs: startupCommandSettlementTimeoutMs,
|
|
@@ -198,6 +202,23 @@ export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSess
|
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
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
|
+
]);
|
|
201
222
|
return createElement(Fragment, null, createElement(CoreWebViewHost, {
|
|
202
223
|
coreUrl,
|
|
203
224
|
key: `${coreUrl}:${coreHostRevision}`,
|
|
@@ -206,7 +227,7 @@ export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, onCommandSess
|
|
|
206
227
|
onDisconnected: handleDisconnected,
|
|
207
228
|
onInvalidMessage,
|
|
208
229
|
webViewComponent: coreWebViewComponent,
|
|
209
|
-
}), session === null
|
|
230
|
+
}), session === null || attachViewProjection
|
|
210
231
|
? null
|
|
211
232
|
: createElement(NativeViewHostProjection, {
|
|
212
233
|
controller: session.controller,
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { type ComponentType, type ReactElement } from "react";
|
|
2
|
-
import type {
|
|
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
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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: (
|
|
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.
|
|
6
|
+
export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "5.1.1";
|
|
@@ -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
|
-
|
|
45
|
+
prepareActivation: function (activeTransport) {
|
|
44
46
|
transport = activeTransport;
|
|
45
47
|
if (typeof options.onMessage === "function") {
|
|
46
48
|
activeTransport.subscribe(options.onMessage);
|
|
47
49
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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) =>
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
29
|
+
var activationResult = false;
|
|
26
30
|
if (${String(activateNative)}) {
|
|
27
|
-
|
|
31
|
+
activationResult = existingBridge.activateNative(${String(activationEpoch)});
|
|
28
32
|
}
|
|
29
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
269
|
+
return null;
|
|
250
270
|
}
|
|
251
271
|
if (nativeEpoch !== null && epoch < nativeEpoch) {
|
|
252
|
-
return
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"description": "getuserfeedback React Native SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"getuserfeedback",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"lint": "biome check ."
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@getuserfeedback/protocol": "^3.31.
|
|
48
|
-
"@getuserfeedback/sdk": "^0.
|
|
47
|
+
"@getuserfeedback/protocol": "^3.31.3",
|
|
48
|
+
"@getuserfeedback/sdk": "^0.13.1",
|
|
49
49
|
"robot3": "^1.2.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|