@getuserfeedback/react-native 3.0.7 → 3.0.23
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/core-webview-bridge-adapter.d.ts +19 -0
- package/dist/core-webview-bridge-adapter.js +48 -0
- package/dist/core-webview-host-controller.d.ts +16 -0
- package/dist/core-webview-host-controller.js +15 -0
- package/dist/core-webview-host.d.ts +14 -0
- package/dist/core-webview-host.js +124 -0
- package/dist/version.js +1 -1
- package/dist/webview-connection-bridge-message.d.ts +22 -0
- package/dist/webview-connection-bridge-message.js +24 -0
- package/dist/webview-connection-bridge-script.d.ts +16 -0
- package/dist/webview-connection-bridge-script.js +255 -0
- package/dist/webview-connection-host-message-script.d.ts +9 -0
- package/dist/webview-connection-host-message-script.js +20 -0
- package/dist/webview-host-controller.d.ts +45 -0
- package/dist/webview-host-controller.js +198 -0
- package/dist/webview-host-session-machine.d.ts +22 -0
- package/dist/webview-host-session-machine.js +82 -0
- package/package.json +3 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type WebViewConnectionBridgeMessage } from "./webview-connection-bridge-message.js";
|
|
2
|
+
export type CoreWebViewBridgeMessage = WebViewConnectionBridgeMessage;
|
|
3
|
+
export declare const parseCoreWebViewBridgeMessage: (value: unknown) => CoreWebViewBridgeMessage;
|
|
4
|
+
export declare const toExternalCoreUrl: (coreUrl: string) => string;
|
|
5
|
+
export declare const buildCoreWebViewBridgeScript: (input: {
|
|
6
|
+
bridgeId: string;
|
|
7
|
+
phase: "documentStart";
|
|
8
|
+
} | {
|
|
9
|
+
bridgeId: string;
|
|
10
|
+
epoch: number;
|
|
11
|
+
phase: "afterLoad";
|
|
12
|
+
}) => string;
|
|
13
|
+
export declare const buildCoreWebViewHostMessageScript: (input: {
|
|
14
|
+
bridgeId: string;
|
|
15
|
+
connectionId: string;
|
|
16
|
+
epoch: number;
|
|
17
|
+
generation: number;
|
|
18
|
+
message: unknown;
|
|
19
|
+
}) => string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CORE_HOST_BOOTSTRAP_GLOBAL_KEY, CORE_HOST_BOOTSTRAP_PROTOCOL_VERSION, CORE_HOST_BOOTSTRAP_READY_EVENT_NAME, CORE_HOST_BOOTSTRAP_REQUIRED_FRAGMENT, } from "@getuserfeedback/protocol/internal/core-host-bootstrap";
|
|
2
|
+
import { parseWebViewConnectionBridgeMessage, } from "./webview-connection-bridge-message.js";
|
|
3
|
+
import { buildWebViewConnectionBridgeScript, } from "./webview-connection-bridge-script.js";
|
|
4
|
+
import { buildWebViewConnectionHostMessageScript, serializeForJavaScript, } from "./webview-connection-host-message-script.js";
|
|
5
|
+
const CORE_WEBVIEW_BRIDGE_GLOBAL_KEY = "__getuserfeedback_core_webview_bridge";
|
|
6
|
+
const CORE_WEBVIEW_BRIDGE_PROTOCOL_VERSION = "v2";
|
|
7
|
+
const CORE_WEBVIEW_BRIDGE_CONFIG = {
|
|
8
|
+
bridgeGlobalKey: CORE_WEBVIEW_BRIDGE_GLOBAL_KEY,
|
|
9
|
+
bridgeProtocolVersion: CORE_WEBVIEW_BRIDGE_PROTOCOL_VERSION,
|
|
10
|
+
connectTimeoutMessage: "Timed out connecting core to the native host",
|
|
11
|
+
connectTimeoutMs: 30000,
|
|
12
|
+
inactiveConnectionMessage: "Core WebView bridge connection is not active",
|
|
13
|
+
};
|
|
14
|
+
export const parseCoreWebViewBridgeMessage = (value) => parseWebViewConnectionBridgeMessage(value, {
|
|
15
|
+
invalidIdentity: "Invalid core WebView bridge message identity",
|
|
16
|
+
invalidKind: "Invalid core WebView bridge message kind",
|
|
17
|
+
invalidMessage: "Invalid core WebView bridge message",
|
|
18
|
+
missingPayload: "Core WebView bridge payload is missing",
|
|
19
|
+
});
|
|
20
|
+
export const toExternalCoreUrl = (coreUrl) => {
|
|
21
|
+
const url = new URL(coreUrl);
|
|
22
|
+
url.hash = CORE_HOST_BOOTSTRAP_REQUIRED_FRAGMENT;
|
|
23
|
+
return url.toString();
|
|
24
|
+
};
|
|
25
|
+
const buildCoreHostBootstrapScript = () => {
|
|
26
|
+
const bridgeGlobalKey = serializeForJavaScript(CORE_WEBVIEW_BRIDGE_GLOBAL_KEY);
|
|
27
|
+
const bootstrapGlobalKey = serializeForJavaScript(CORE_HOST_BOOTSTRAP_GLOBAL_KEY);
|
|
28
|
+
const bootstrapProtocolVersion = serializeForJavaScript(CORE_HOST_BOOTSTRAP_PROTOCOL_VERSION);
|
|
29
|
+
const bootstrapReadyEventName = serializeForJavaScript(CORE_HOST_BOOTSTRAP_READY_EVENT_NAME);
|
|
30
|
+
return `
|
|
31
|
+
(function () {
|
|
32
|
+
var bridge = window[${bridgeGlobalKey}];
|
|
33
|
+
window[${bootstrapGlobalKey}] = {
|
|
34
|
+
protocolVersion: ${bootstrapProtocolVersion},
|
|
35
|
+
createCoreHostConnection: function (options) {
|
|
36
|
+
return bridge.createConnection({
|
|
37
|
+
generation: options.generation,
|
|
38
|
+
onActivated: options.onConnected,
|
|
39
|
+
onError: options.onError
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
window.dispatchEvent(new Event(${bootstrapReadyEventName}));
|
|
44
|
+
})();
|
|
45
|
+
true;`;
|
|
46
|
+
};
|
|
47
|
+
export const buildCoreWebViewBridgeScript = (input) => `${buildWebViewConnectionBridgeScript(Object.assign(Object.assign({}, input), { config: CORE_WEBVIEW_BRIDGE_CONFIG }))}\n${buildCoreHostBootstrapScript()}`;
|
|
48
|
+
export const buildCoreWebViewHostMessageScript = (input) => buildWebViewConnectionHostMessageScript(Object.assign(Object.assign({}, input), { bridgeGlobalKey: CORE_WEBVIEW_BRIDGE_GLOBAL_KEY }));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CoreHostBootstrapTransport } from "@getuserfeedback/protocol/internal/core-host-bootstrap";
|
|
2
|
+
import { type CoreWebViewBridgeMessage } from "./core-webview-bridge-adapter.js";
|
|
3
|
+
import { type WebViewHandle, type WebViewHostController } from "./webview-host-controller.js";
|
|
4
|
+
export type CoreWebViewHandle = WebViewHandle;
|
|
5
|
+
export type CoreWebViewHostConnection = {
|
|
6
|
+
generation: number;
|
|
7
|
+
transport: CoreHostBootstrapTransport;
|
|
8
|
+
};
|
|
9
|
+
export type CoreWebViewHostController = WebViewHostController<CoreWebViewBridgeMessage>;
|
|
10
|
+
export declare const createCoreWebViewHostController: (input: {
|
|
11
|
+
bridgeId: string;
|
|
12
|
+
onConnected: (connection: CoreWebViewHostConnection) => void;
|
|
13
|
+
onDisconnected?: () => void;
|
|
14
|
+
activationRetryIntervalMs?: number;
|
|
15
|
+
activationRetryTimeoutMs?: number;
|
|
16
|
+
}) => CoreWebViewHostController;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { buildCoreWebViewBridgeScript, buildCoreWebViewHostMessageScript, } from "./core-webview-bridge-adapter.js";
|
|
2
|
+
import { createWebViewHostController, } from "./webview-host-controller.js";
|
|
3
|
+
export const createCoreWebViewHostController = (input) => createWebViewHostController(Object.assign(Object.assign({}, input), { connectionLabel: "Core WebView host", buildActivationScript: buildCoreWebViewBridgeScript, buildHostMessageScript: buildCoreWebViewHostMessageScript, onConnected: (connection) => {
|
|
4
|
+
input.onConnected({
|
|
5
|
+
generation: connection.generation,
|
|
6
|
+
transport: {
|
|
7
|
+
postMessage: connection.postMessage,
|
|
8
|
+
subscribe: (listener) => connection.subscribe((message) => {
|
|
9
|
+
if (message.kind === "message") {
|
|
10
|
+
listener(message.message);
|
|
11
|
+
}
|
|
12
|
+
}),
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
} }));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CoreHostBootstrapTransport } from "@getuserfeedback/protocol/internal/core-host-bootstrap";
|
|
2
|
+
import { type ComponentType, type ReactElement } from "react";
|
|
3
|
+
export type CoreWebViewHostProps = {
|
|
4
|
+
coreUrl: string;
|
|
5
|
+
onConnected: (connection: {
|
|
6
|
+
generation: number;
|
|
7
|
+
transport: CoreHostBootstrapTransport;
|
|
8
|
+
}) => void;
|
|
9
|
+
onDisconnected?: () => void;
|
|
10
|
+
onInvalidMessage?: (error: Error, value: unknown) => void;
|
|
11
|
+
webViewComponent?: ComponentType<Record<string, unknown>>;
|
|
12
|
+
webViewProps?: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
export declare function CoreWebViewHost({ coreUrl, ...props }: CoreWebViewHostProps): ReactElement;
|
|
@@ -0,0 +1,124 @@
|
|
|
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, useEffect, useMemo, useRef, } from "react";
|
|
13
|
+
import { buildCoreWebViewBridgeScript, parseCoreWebViewBridgeMessage, toExternalCoreUrl, } from "./core-webview-bridge-adapter.js";
|
|
14
|
+
import { createCoreWebViewHostController, } from "./core-webview-host-controller.js";
|
|
15
|
+
let nextBridgeId = 0;
|
|
16
|
+
const resolveWebViewComponent = () => {
|
|
17
|
+
var _a;
|
|
18
|
+
const webViewModule = require("react-native-webview");
|
|
19
|
+
const WebView = (_a = webViewModule.WebView) !== null && _a !== void 0 ? _a : webViewModule.default;
|
|
20
|
+
if (!WebView) {
|
|
21
|
+
throw new Error("react-native-webview must export WebView to render the core host");
|
|
22
|
+
}
|
|
23
|
+
return WebView;
|
|
24
|
+
};
|
|
25
|
+
const toMessageData = (value) => {
|
|
26
|
+
if (typeof value !== "string") {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(value);
|
|
31
|
+
}
|
|
32
|
+
catch (_a) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const toEventHandler = (value) => typeof value === "function" ? value : undefined;
|
|
37
|
+
const toInjectionScript = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
38
|
+
const toTruthyInjectionScript = (value) => {
|
|
39
|
+
const script = toInjectionScript(value);
|
|
40
|
+
return script ? `${script}\ntrue;` : undefined;
|
|
41
|
+
};
|
|
42
|
+
const isBridgeMessageCandidate = (value, bridgeId) => typeof value === "object" &&
|
|
43
|
+
value !== null &&
|
|
44
|
+
"bridgeId" in value &&
|
|
45
|
+
value.bridgeId === bridgeId;
|
|
46
|
+
const composeInjectionScripts = (bridgeScript, callerScript) => {
|
|
47
|
+
const script = toInjectionScript(callerScript);
|
|
48
|
+
return script ? `${bridgeScript}\n${script}\ntrue;` : bridgeScript;
|
|
49
|
+
};
|
|
50
|
+
function CoreWebViewHostDocument({ externalCoreUrl, onConnected, onDisconnected, onInvalidMessage, webViewComponent, webViewProps, }) {
|
|
51
|
+
const WebView = webViewComponent !== null && webViewComponent !== void 0 ? webViewComponent : resolveWebViewComponent();
|
|
52
|
+
const bridgeIdRef = useRef(null);
|
|
53
|
+
if (bridgeIdRef.current === null) {
|
|
54
|
+
nextBridgeId += 1;
|
|
55
|
+
bridgeIdRef.current = `core-webview-${nextBridgeId}`;
|
|
56
|
+
}
|
|
57
|
+
const bridgeId = bridgeIdRef.current;
|
|
58
|
+
const onConnectedRef = useRef(onConnected);
|
|
59
|
+
const onDisconnectedRef = useRef(onDisconnected);
|
|
60
|
+
const onInvalidMessageRef = useRef(onInvalidMessage);
|
|
61
|
+
onConnectedRef.current = onConnected;
|
|
62
|
+
onDisconnectedRef.current = onDisconnected;
|
|
63
|
+
onInvalidMessageRef.current = onInvalidMessage;
|
|
64
|
+
const controllerRef = useRef(null);
|
|
65
|
+
if (controllerRef.current === null) {
|
|
66
|
+
controllerRef.current = createCoreWebViewHostController({
|
|
67
|
+
bridgeId,
|
|
68
|
+
onConnected: (connection) => {
|
|
69
|
+
onConnectedRef.current(connection);
|
|
70
|
+
},
|
|
71
|
+
onDisconnected: () => {
|
|
72
|
+
var _a;
|
|
73
|
+
(_a = onDisconnectedRef.current) === null || _a === void 0 ? void 0 : _a.call(onDisconnectedRef);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const controller = controllerRef.current;
|
|
78
|
+
const documentStartBridgeScript = useMemo(() => buildCoreWebViewBridgeScript({ bridgeId, phase: "documentStart" }), [bridgeId]);
|
|
79
|
+
const source = useMemo(() => ({ uri: externalCoreUrl }), [externalCoreUrl]);
|
|
80
|
+
const callerOnLoadStart = toEventHandler(webViewProps === null || webViewProps === void 0 ? void 0 : webViewProps.onLoadStart);
|
|
81
|
+
const callerOnLoad = toEventHandler(webViewProps === null || webViewProps === void 0 ? void 0 : webViewProps.onLoad);
|
|
82
|
+
const callerOnMessage = toEventHandler(webViewProps === null || webViewProps === void 0 ? void 0 : webViewProps.onMessage);
|
|
83
|
+
useEffect(() => () => {
|
|
84
|
+
controller.detach();
|
|
85
|
+
}, [controller]);
|
|
86
|
+
const setWebViewRef = useCallback((handle) => {
|
|
87
|
+
if (!handle || typeof handle.injectJavaScript !== "function") {
|
|
88
|
+
controller.detach();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
controller.attach(handle);
|
|
92
|
+
}, [controller]);
|
|
93
|
+
const handleLoadStart = useCallback((event) => {
|
|
94
|
+
controller.beginLoad();
|
|
95
|
+
callerOnLoadStart === null || callerOnLoadStart === void 0 ? void 0 : callerOnLoadStart(event);
|
|
96
|
+
}, [callerOnLoadStart, controller]);
|
|
97
|
+
const handleLoad = useCallback((event) => {
|
|
98
|
+
controller.completeLoad();
|
|
99
|
+
callerOnLoad === null || callerOnLoad === void 0 ? void 0 : callerOnLoad(event);
|
|
100
|
+
}, [callerOnLoad, controller]);
|
|
101
|
+
const handleMessage = useCallback((event) => {
|
|
102
|
+
var _a, _b;
|
|
103
|
+
const value = toMessageData((_a = event.nativeEvent) === null || _a === void 0 ? void 0 : _a.data);
|
|
104
|
+
let message;
|
|
105
|
+
try {
|
|
106
|
+
message = parseCoreWebViewBridgeMessage(value);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (!callerOnMessage || isBridgeMessageCandidate(value, bridgeId)) {
|
|
110
|
+
(_b = onInvalidMessageRef.current) === null || _b === void 0 ? void 0 : _b.call(onInvalidMessageRef, error instanceof Error ? error : new Error(String(error)), value);
|
|
111
|
+
}
|
|
112
|
+
callerOnMessage === null || callerOnMessage === void 0 ? void 0 : callerOnMessage(event);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
controller.handleMessage(message);
|
|
116
|
+
callerOnMessage === null || callerOnMessage === void 0 ? void 0 : callerOnMessage(event);
|
|
117
|
+
}, [bridgeId, callerOnMessage, controller]);
|
|
118
|
+
return createElement(WebView, Object.assign(Object.assign({}, webViewProps), { injectedJavaScript: toTruthyInjectionScript(webViewProps === null || webViewProps === void 0 ? void 0 : webViewProps.injectedJavaScript), injectedJavaScriptBeforeContentLoaded: composeInjectionScripts(documentStartBridgeScript, webViewProps === null || webViewProps === void 0 ? void 0 : webViewProps.injectedJavaScriptBeforeContentLoaded), onLoad: handleLoad, onLoadStart: handleLoadStart, onMessage: handleMessage, ref: setWebViewRef, source }));
|
|
119
|
+
}
|
|
120
|
+
export function CoreWebViewHost(_a) {
|
|
121
|
+
var { coreUrl } = _a, props = __rest(_a, ["coreUrl"]);
|
|
122
|
+
const externalCoreUrl = toExternalCoreUrl(coreUrl);
|
|
123
|
+
return createElement(CoreWebViewHostDocument, Object.assign(Object.assign({}, props), { externalCoreUrl, key: externalCoreUrl }));
|
|
124
|
+
}
|
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 : "3.0.
|
|
6
|
+
export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "3.0.23";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type WebViewConnectionBridgeMessageIdentity = {
|
|
2
|
+
bridgeId: string;
|
|
3
|
+
connectionId: string;
|
|
4
|
+
epoch: number;
|
|
5
|
+
generation: number;
|
|
6
|
+
};
|
|
7
|
+
export type WebViewConnectionBridgeMessage = (WebViewConnectionBridgeMessageIdentity & {
|
|
8
|
+
kind: "connected";
|
|
9
|
+
}) | (WebViewConnectionBridgeMessageIdentity & {
|
|
10
|
+
kind: "disconnected";
|
|
11
|
+
}) | (WebViewConnectionBridgeMessageIdentity & {
|
|
12
|
+
kind: "message";
|
|
13
|
+
message: unknown;
|
|
14
|
+
});
|
|
15
|
+
export type WebViewConnectionBridgeMessageParseErrors = {
|
|
16
|
+
invalidIdentity: string;
|
|
17
|
+
invalidKind: string;
|
|
18
|
+
invalidMessage: string;
|
|
19
|
+
missingPayload: string;
|
|
20
|
+
};
|
|
21
|
+
export declare const parseWebViewConnectionBridgeMessage: (value: unknown, errors: WebViewConnectionBridgeMessageParseErrors) => WebViewConnectionBridgeMessage;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
2
|
+
const isGeneration = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
3
|
+
export const parseWebViewConnectionBridgeMessage = (value, errors) => {
|
|
4
|
+
if (!isRecord(value)) {
|
|
5
|
+
throw new Error(errors.invalidMessage);
|
|
6
|
+
}
|
|
7
|
+
if (typeof value.bridgeId !== "string" ||
|
|
8
|
+
value.bridgeId.length === 0 ||
|
|
9
|
+
typeof value.connectionId !== "string" ||
|
|
10
|
+
value.connectionId.length === 0 ||
|
|
11
|
+
!isGeneration(value.epoch) ||
|
|
12
|
+
!isGeneration(value.generation)) {
|
|
13
|
+
throw new Error(errors.invalidIdentity);
|
|
14
|
+
}
|
|
15
|
+
if (value.kind !== "connected" &&
|
|
16
|
+
value.kind !== "disconnected" &&
|
|
17
|
+
value.kind !== "message") {
|
|
18
|
+
throw new Error(errors.invalidKind);
|
|
19
|
+
}
|
|
20
|
+
if (value.kind === "message" && !("message" in value)) {
|
|
21
|
+
throw new Error(errors.missingPayload);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type WebViewConnectionBridgeScriptConfig = {
|
|
2
|
+
bridgeGlobalKey: string;
|
|
3
|
+
bridgeProtocolVersion: string;
|
|
4
|
+
connectTimeoutMessage: string;
|
|
5
|
+
connectTimeoutMs: number;
|
|
6
|
+
inactiveConnectionMessage: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const buildWebViewConnectionBridgeScript: (input: ({
|
|
9
|
+
phase: "documentStart";
|
|
10
|
+
} | {
|
|
11
|
+
epoch: number;
|
|
12
|
+
phase: "afterLoad";
|
|
13
|
+
}) & {
|
|
14
|
+
bridgeId: string;
|
|
15
|
+
config: WebViewConnectionBridgeScriptConfig;
|
|
16
|
+
}) => string;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { serializeForJavaScript } from "./webview-connection-host-message-script.js";
|
|
2
|
+
export const buildWebViewConnectionBridgeScript = (input) => {
|
|
3
|
+
const bridgeId = serializeForJavaScript(input.bridgeId);
|
|
4
|
+
const activateNative = input.phase === "afterLoad";
|
|
5
|
+
const activationEpoch = activateNative ? input.epoch : null;
|
|
6
|
+
const connectTimeoutMs = String(input.config.connectTimeoutMs);
|
|
7
|
+
const bridgeGlobalKey = serializeForJavaScript(input.config.bridgeGlobalKey);
|
|
8
|
+
const bridgeProtocolVersion = serializeForJavaScript(input.config.bridgeProtocolVersion);
|
|
9
|
+
const connectTimeoutMessage = serializeForJavaScript(input.config.connectTimeoutMessage);
|
|
10
|
+
const inactiveConnectionMessage = serializeForJavaScript(input.config.inactiveConnectionMessage);
|
|
11
|
+
return `
|
|
12
|
+
(function () {
|
|
13
|
+
var bridgeId = ${bridgeId};
|
|
14
|
+
var bridgeGlobalKey = ${bridgeGlobalKey};
|
|
15
|
+
var bridgeProtocolVersion = ${bridgeProtocolVersion};
|
|
16
|
+
var connectTimeoutMessage = ${connectTimeoutMessage};
|
|
17
|
+
var inactiveConnectionMessage = ${inactiveConnectionMessage};
|
|
18
|
+
var existingBridge = window[bridgeGlobalKey];
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
existingBridge &&
|
|
22
|
+
existingBridge.protocolVersion === bridgeProtocolVersion &&
|
|
23
|
+
existingBridge.bridgeId === bridgeId
|
|
24
|
+
) {
|
|
25
|
+
var didConnect = false;
|
|
26
|
+
if (${String(activateNative)}) {
|
|
27
|
+
didConnect = existingBridge.activateNative(${String(activationEpoch)});
|
|
28
|
+
}
|
|
29
|
+
if (!didConnect) {
|
|
30
|
+
existingBridge.notifyConnected();
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var connectionSequence = 0;
|
|
36
|
+
var documentId =
|
|
37
|
+
typeof crypto === "object" && typeof crypto.randomUUID === "function"
|
|
38
|
+
? crypto.randomUUID()
|
|
39
|
+
: String(Date.now()) + ":" + String(Math.random());
|
|
40
|
+
var activeConnection = null;
|
|
41
|
+
var isNativeActivated = ${String(activateNative)};
|
|
42
|
+
var nativeEpoch = ${String(activationEpoch)};
|
|
43
|
+
|
|
44
|
+
function postToNative(message) {
|
|
45
|
+
var nativeBridge = window.ReactNativeWebView;
|
|
46
|
+
if (!nativeBridge || typeof nativeBridge.postMessage !== "function") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
nativeBridge.postMessage(JSON.stringify(message));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toConnectionMessage(kind, connection, message) {
|
|
54
|
+
var envelope = {
|
|
55
|
+
bridgeId: bridgeId,
|
|
56
|
+
connectionId: connection.connectionId,
|
|
57
|
+
epoch: nativeEpoch,
|
|
58
|
+
generation: connection.generation,
|
|
59
|
+
kind: kind
|
|
60
|
+
};
|
|
61
|
+
if (kind === "message") {
|
|
62
|
+
envelope.message = message;
|
|
63
|
+
}
|
|
64
|
+
return envelope;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function reportConnectionError(connection, error) {
|
|
68
|
+
try {
|
|
69
|
+
if (typeof connection.onError === "function") {
|
|
70
|
+
connection.onError(error);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Error observers cannot change connection settlement.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createConnection(options) {
|
|
78
|
+
var connection = {
|
|
79
|
+
connectionId:
|
|
80
|
+
bridgeId + ":" + documentId + ":" + String(++connectionSequence),
|
|
81
|
+
generation: options.generation,
|
|
82
|
+
listener: null,
|
|
83
|
+
isConnected: false,
|
|
84
|
+
connectTimeout: null,
|
|
85
|
+
onActivated: options.onActivated,
|
|
86
|
+
onError: options.onError,
|
|
87
|
+
rejectConnect: null,
|
|
88
|
+
resolveConnect: null,
|
|
89
|
+
transport: null
|
|
90
|
+
};
|
|
91
|
+
var transport = {
|
|
92
|
+
postMessage: function (message) {
|
|
93
|
+
if (activeConnection !== connection) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
postToNative(toConnectionMessage("message", connection, message));
|
|
97
|
+
},
|
|
98
|
+
subscribe: function (listener) {
|
|
99
|
+
if (activeConnection !== connection) {
|
|
100
|
+
throw new Error(inactiveConnectionMessage);
|
|
101
|
+
}
|
|
102
|
+
connection.listener = listener;
|
|
103
|
+
return function () {
|
|
104
|
+
if (connection.listener === listener) {
|
|
105
|
+
connection.listener = null;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
connection.transport = transport;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
connect: function () {
|
|
114
|
+
activeConnection = connection;
|
|
115
|
+
return new Promise(function (resolve, reject) {
|
|
116
|
+
connection.rejectConnect = reject;
|
|
117
|
+
connection.resolveConnect = resolve;
|
|
118
|
+
connection.connectTimeout = setTimeout(function () {
|
|
119
|
+
if (activeConnection !== connection || connection.isConnected) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
activeConnection = null;
|
|
123
|
+
var error = new Error(connectTimeoutMessage);
|
|
124
|
+
reportConnectionError(connection, error);
|
|
125
|
+
connection.rejectConnect(error);
|
|
126
|
+
connection.rejectConnect = null;
|
|
127
|
+
connection.resolveConnect = null;
|
|
128
|
+
connection.connectTimeout = null;
|
|
129
|
+
}, ${connectTimeoutMs});
|
|
130
|
+
bridge.activateConnection();
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
disconnect: function () {
|
|
134
|
+
if (activeConnection === connection) {
|
|
135
|
+
activeConnection = null;
|
|
136
|
+
connection.listener = null;
|
|
137
|
+
if (connection.connectTimeout) {
|
|
138
|
+
clearTimeout(connection.connectTimeout);
|
|
139
|
+
connection.connectTimeout = null;
|
|
140
|
+
}
|
|
141
|
+
if (connection.isConnected) {
|
|
142
|
+
try {
|
|
143
|
+
postToNative(toConnectionMessage("disconnected", connection));
|
|
144
|
+
} catch {
|
|
145
|
+
// Native navigation/ref lifecycle is authoritative for local teardown.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (connection.resolveConnect) {
|
|
149
|
+
connection.resolveConnect();
|
|
150
|
+
connection.rejectConnect = null;
|
|
151
|
+
connection.resolveConnect = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return Promise.resolve();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
var bridge = {
|
|
160
|
+
bridgeId: bridgeId,
|
|
161
|
+
protocolVersion: bridgeProtocolVersion,
|
|
162
|
+
createConnection: createConnection,
|
|
163
|
+
activateConnection: function () {
|
|
164
|
+
var connection = activeConnection;
|
|
165
|
+
if (
|
|
166
|
+
!isNativeActivated ||
|
|
167
|
+
!connection ||
|
|
168
|
+
connection.isConnected
|
|
169
|
+
) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
if (!postToNative(toConnectionMessage("connected", connection))) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
reportConnectionError(connection, error);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
connection.isConnected = true;
|
|
181
|
+
try {
|
|
182
|
+
connection.onActivated(connection.transport);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
connection.isConnected = false;
|
|
185
|
+
try {
|
|
186
|
+
postToNative(toConnectionMessage("disconnected", connection));
|
|
187
|
+
} catch {
|
|
188
|
+
// Preserve the original connection error.
|
|
189
|
+
}
|
|
190
|
+
reportConnectionError(connection, error);
|
|
191
|
+
if (connection.rejectConnect) {
|
|
192
|
+
connection.rejectConnect(error);
|
|
193
|
+
}
|
|
194
|
+
connection.rejectConnect = null;
|
|
195
|
+
connection.resolveConnect = null;
|
|
196
|
+
if (connection.connectTimeout) {
|
|
197
|
+
clearTimeout(connection.connectTimeout);
|
|
198
|
+
connection.connectTimeout = null;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (connection.resolveConnect) {
|
|
203
|
+
if (connection.connectTimeout) {
|
|
204
|
+
clearTimeout(connection.connectTimeout);
|
|
205
|
+
connection.connectTimeout = null;
|
|
206
|
+
}
|
|
207
|
+
connection.resolveConnect();
|
|
208
|
+
connection.rejectConnect = null;
|
|
209
|
+
connection.resolveConnect = null;
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
},
|
|
213
|
+
activateNative: function (epoch) {
|
|
214
|
+
if (typeof epoch !== "number" || !Number.isSafeInteger(epoch) || epoch < 0) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (nativeEpoch !== null && epoch < nativeEpoch) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
isNativeActivated = true;
|
|
221
|
+
nativeEpoch = epoch;
|
|
222
|
+
return bridge.activateConnection();
|
|
223
|
+
},
|
|
224
|
+
notifyConnected: function () {
|
|
225
|
+
if (activeConnection && activeConnection.isConnected) {
|
|
226
|
+
try {
|
|
227
|
+
postToNative(toConnectionMessage("connected", activeConnection));
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
},
|
|
234
|
+
receive: function (envelope) {
|
|
235
|
+
if (
|
|
236
|
+
!activeConnection ||
|
|
237
|
+
!envelope ||
|
|
238
|
+
envelope.bridgeId !== bridgeId ||
|
|
239
|
+
envelope.connectionId !== activeConnection.connectionId ||
|
|
240
|
+
envelope.epoch !== nativeEpoch ||
|
|
241
|
+
envelope.generation !== activeConnection.generation ||
|
|
242
|
+
envelope.kind !== "message" ||
|
|
243
|
+
typeof activeConnection.listener !== "function"
|
|
244
|
+
) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
activeConnection.listener(envelope.message);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
window[bridgeGlobalKey] = bridge;
|
|
253
|
+
})();
|
|
254
|
+
true;`;
|
|
255
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const serializeForJavaScript: (value: unknown) => string;
|
|
2
|
+
export declare const buildWebViewConnectionHostMessageScript: (input: {
|
|
3
|
+
bridgeGlobalKey: string;
|
|
4
|
+
bridgeId: string;
|
|
5
|
+
connectionId: string;
|
|
6
|
+
epoch: number;
|
|
7
|
+
generation: number;
|
|
8
|
+
message: unknown;
|
|
9
|
+
}) => string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const serializeForJavaScript = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
2
|
+
export const buildWebViewConnectionHostMessageScript = (input) => {
|
|
3
|
+
const envelope = serializeForJavaScript({
|
|
4
|
+
bridgeId: input.bridgeId,
|
|
5
|
+
connectionId: input.connectionId,
|
|
6
|
+
epoch: input.epoch,
|
|
7
|
+
generation: input.generation,
|
|
8
|
+
kind: "message",
|
|
9
|
+
message: input.message,
|
|
10
|
+
});
|
|
11
|
+
const bridgeGlobalKey = serializeForJavaScript(input.bridgeGlobalKey);
|
|
12
|
+
return `
|
|
13
|
+
(function () {
|
|
14
|
+
var bridge = window[${bridgeGlobalKey}];
|
|
15
|
+
if (bridge && typeof bridge.receive === "function") {
|
|
16
|
+
bridge.receive(${envelope});
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
true;`;
|
|
20
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type WebViewHandle = {
|
|
2
|
+
injectJavaScript: (script: string) => void;
|
|
3
|
+
};
|
|
4
|
+
export type WebViewBridgeMessageIdentity = {
|
|
5
|
+
bridgeId: string;
|
|
6
|
+
connectionId: string;
|
|
7
|
+
epoch: number;
|
|
8
|
+
generation: number;
|
|
9
|
+
kind: string;
|
|
10
|
+
};
|
|
11
|
+
export type WebViewHostConnection<TMessage extends WebViewBridgeMessageIdentity> = {
|
|
12
|
+
connectionId: string;
|
|
13
|
+
epoch: number;
|
|
14
|
+
generation: number;
|
|
15
|
+
postMessage: (message: unknown) => void;
|
|
16
|
+
subscribe: (listener: (message: TMessage) => void) => () => void;
|
|
17
|
+
};
|
|
18
|
+
export type WebViewHostController<TMessage extends WebViewBridgeMessageIdentity> = {
|
|
19
|
+
attach: (handle: WebViewHandle) => void;
|
|
20
|
+
beginLoad: () => void;
|
|
21
|
+
completeLoad: () => void;
|
|
22
|
+
detach: () => void;
|
|
23
|
+
disconnect: () => void;
|
|
24
|
+
handleMessage: (message: TMessage) => boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare const createWebViewHostController: <TMessage extends WebViewBridgeMessageIdentity>(input: {
|
|
27
|
+
bridgeId: string;
|
|
28
|
+
connectionLabel: string;
|
|
29
|
+
buildActivationScript: (input: {
|
|
30
|
+
bridgeId: string;
|
|
31
|
+
epoch: number;
|
|
32
|
+
phase: "afterLoad";
|
|
33
|
+
}) => string;
|
|
34
|
+
buildHostMessageScript: (input: {
|
|
35
|
+
bridgeId: string;
|
|
36
|
+
connectionId: string;
|
|
37
|
+
epoch: number;
|
|
38
|
+
generation: number;
|
|
39
|
+
message: unknown;
|
|
40
|
+
}) => string;
|
|
41
|
+
onConnected: (connection: WebViewHostConnection<TMessage>) => void;
|
|
42
|
+
onDisconnected?: () => void;
|
|
43
|
+
activationRetryIntervalMs?: number;
|
|
44
|
+
activationRetryTimeoutMs?: number;
|
|
45
|
+
}) => WebViewHostController<TMessage>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createWebViewHostSessionController } from "./webview-host-session-machine.js";
|
|
2
|
+
const DEFAULT_ACTIVATION_RETRY_INTERVAL_MS = 50;
|
|
3
|
+
const DEFAULT_ACTIVATION_RETRY_TIMEOUT_MS = 30000;
|
|
4
|
+
export const createWebViewHostController = (input) => {
|
|
5
|
+
var _a, _b;
|
|
6
|
+
let handle = null;
|
|
7
|
+
let activeConnection = null;
|
|
8
|
+
let isLoadComplete = false;
|
|
9
|
+
let activationRetryDeadline = null;
|
|
10
|
+
let activationRetryTimer = null;
|
|
11
|
+
const activationRetryIntervalMs = (_a = input.activationRetryIntervalMs) !== null && _a !== void 0 ? _a : DEFAULT_ACTIVATION_RETRY_INTERVAL_MS;
|
|
12
|
+
const activationRetryTimeoutMs = (_b = input.activationRetryTimeoutMs) !== null && _b !== void 0 ? _b : DEFAULT_ACTIVATION_RETRY_TIMEOUT_MS;
|
|
13
|
+
const session = createWebViewHostSessionController();
|
|
14
|
+
const clearActivationRetry = () => {
|
|
15
|
+
if (activationRetryTimer !== null) {
|
|
16
|
+
clearTimeout(activationRetryTimer);
|
|
17
|
+
activationRetryTimer = null;
|
|
18
|
+
}
|
|
19
|
+
activationRetryDeadline = null;
|
|
20
|
+
};
|
|
21
|
+
const injectActivation = () => {
|
|
22
|
+
const snapshot = session.getSnapshot();
|
|
23
|
+
if (!handle || !isLoadComplete || snapshot.state !== "awaitingConnection") {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const script = input.buildActivationScript({
|
|
27
|
+
bridgeId: input.bridgeId,
|
|
28
|
+
epoch: snapshot.epoch,
|
|
29
|
+
phase: "afterLoad",
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
handle.injectJavaScript(script);
|
|
33
|
+
}
|
|
34
|
+
catch (_a) {
|
|
35
|
+
// A newly attached WebView can reject injection transiently. The
|
|
36
|
+
// existing activation retry and page-side timeout own recovery.
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const scheduleActivationRetry = () => {
|
|
40
|
+
if (activationRetryTimer !== null ||
|
|
41
|
+
activationRetryDeadline === null ||
|
|
42
|
+
Date.now() >= activationRetryDeadline ||
|
|
43
|
+
!handle ||
|
|
44
|
+
!isLoadComplete ||
|
|
45
|
+
session.getSnapshot().state !== "awaitingConnection") {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
activationRetryTimer = setTimeout(() => {
|
|
49
|
+
activationRetryTimer = null;
|
|
50
|
+
if (activationRetryDeadline === null ||
|
|
51
|
+
Date.now() >= activationRetryDeadline) {
|
|
52
|
+
clearActivationRetry();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
injectActivation();
|
|
56
|
+
scheduleActivationRetry();
|
|
57
|
+
}, activationRetryIntervalMs);
|
|
58
|
+
};
|
|
59
|
+
const activateUntilConnected = () => {
|
|
60
|
+
if (session.getSnapshot().state !== "awaitingConnection") {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (activationRetryDeadline === null) {
|
|
64
|
+
activationRetryDeadline = Date.now() + activationRetryTimeoutMs;
|
|
65
|
+
}
|
|
66
|
+
injectActivation();
|
|
67
|
+
scheduleActivationRetry();
|
|
68
|
+
};
|
|
69
|
+
const announceConnection = () => {
|
|
70
|
+
const connection = activeConnection;
|
|
71
|
+
if (!connection || connection.isAnnounced || !handle) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
connection.isAnnounced = true;
|
|
75
|
+
try {
|
|
76
|
+
input.onConnected(connection);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
const pendingMessages = connection.pendingMessages;
|
|
80
|
+
connection.pendingMessages = [];
|
|
81
|
+
for (const message of pendingMessages) {
|
|
82
|
+
for (const listener of connection.listeners) {
|
|
83
|
+
listener(message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const dropActiveConnection = () => {
|
|
89
|
+
var _a;
|
|
90
|
+
if (!activeConnection) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const shouldNotify = activeConnection.isAnnounced;
|
|
94
|
+
activeConnection.listeners.clear();
|
|
95
|
+
activeConnection.pendingMessages = [];
|
|
96
|
+
activeConnection = null;
|
|
97
|
+
if (shouldNotify) {
|
|
98
|
+
(_a = input.onDisconnected) === null || _a === void 0 ? void 0 : _a.call(input);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const disconnect = () => {
|
|
102
|
+
session.close();
|
|
103
|
+
dropActiveConnection();
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
attach: (nextHandle) => {
|
|
107
|
+
handle = nextHandle;
|
|
108
|
+
if (isLoadComplete) {
|
|
109
|
+
activateUntilConnected();
|
|
110
|
+
}
|
|
111
|
+
announceConnection();
|
|
112
|
+
},
|
|
113
|
+
beginLoad: () => {
|
|
114
|
+
clearActivationRetry();
|
|
115
|
+
session.advanceEpoch();
|
|
116
|
+
isLoadComplete = false;
|
|
117
|
+
dropActiveConnection();
|
|
118
|
+
},
|
|
119
|
+
completeLoad: () => {
|
|
120
|
+
isLoadComplete = true;
|
|
121
|
+
activateUntilConnected();
|
|
122
|
+
},
|
|
123
|
+
detach: () => {
|
|
124
|
+
clearActivationRetry();
|
|
125
|
+
session.advanceEpoch();
|
|
126
|
+
handle = null;
|
|
127
|
+
dropActiveConnection();
|
|
128
|
+
},
|
|
129
|
+
disconnect,
|
|
130
|
+
handleMessage: (message) => {
|
|
131
|
+
if (message.bridgeId !== input.bridgeId) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (message.kind === "connected") {
|
|
135
|
+
const acceptance = session.acceptConnection(message);
|
|
136
|
+
if (acceptance === "duplicate") {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (acceptance === "rejected") {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
const listeners = new Set();
|
|
143
|
+
const connection = {
|
|
144
|
+
connectionId: message.connectionId,
|
|
145
|
+
epoch: message.epoch,
|
|
146
|
+
generation: message.generation,
|
|
147
|
+
isAnnounced: false,
|
|
148
|
+
listeners,
|
|
149
|
+
pendingMessages: [],
|
|
150
|
+
postMessage: (hostMessage) => {
|
|
151
|
+
if (activeConnection !== connection || !handle) {
|
|
152
|
+
throw new Error(`${input.connectionLabel} is disconnected`);
|
|
153
|
+
}
|
|
154
|
+
handle.injectJavaScript(input.buildHostMessageScript({
|
|
155
|
+
bridgeId: input.bridgeId,
|
|
156
|
+
connectionId: connection.connectionId,
|
|
157
|
+
epoch: connection.epoch,
|
|
158
|
+
generation: connection.generation,
|
|
159
|
+
message: hostMessage,
|
|
160
|
+
}));
|
|
161
|
+
},
|
|
162
|
+
subscribe: (listener) => {
|
|
163
|
+
if (activeConnection !== connection) {
|
|
164
|
+
throw new Error(`${input.connectionLabel} is disconnected`);
|
|
165
|
+
}
|
|
166
|
+
listeners.add(listener);
|
|
167
|
+
return () => {
|
|
168
|
+
listeners.delete(listener);
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
activeConnection = connection;
|
|
173
|
+
if (session.confirmActivation(message) !== "accepted") {
|
|
174
|
+
throw new Error(`${input.connectionLabel} activation was not confirmed`);
|
|
175
|
+
}
|
|
176
|
+
clearActivationRetry();
|
|
177
|
+
announceConnection();
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
if (!session.isCurrentConnection(message) || !activeConnection) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
if (message.kind === "disconnected") {
|
|
184
|
+
session.close();
|
|
185
|
+
dropActiveConnection();
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (!activeConnection.isAnnounced) {
|
|
189
|
+
activeConnection.pendingMessages.push(message);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
for (const listener of activeConnection.listeners) {
|
|
193
|
+
listener(message);
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type WebViewHostSessionIdentity = {
|
|
2
|
+
connectionId: string;
|
|
3
|
+
epoch: number;
|
|
4
|
+
generation: number;
|
|
5
|
+
};
|
|
6
|
+
type WebViewHostSessionState = "awaitingActivation" | "awaitingConnection" | "connected" | "closed";
|
|
7
|
+
type WebViewHostSessionSnapshot = {
|
|
8
|
+
connection: WebViewHostSessionIdentity | null;
|
|
9
|
+
epoch: number;
|
|
10
|
+
state: WebViewHostSessionState;
|
|
11
|
+
};
|
|
12
|
+
type WebViewHostSessionAcceptance = "accepted" | "duplicate" | "rejected";
|
|
13
|
+
export type WebViewHostSessionController = {
|
|
14
|
+
getSnapshot: () => WebViewHostSessionSnapshot;
|
|
15
|
+
advanceEpoch: () => number;
|
|
16
|
+
acceptConnection: (identity: WebViewHostSessionIdentity) => WebViewHostSessionAcceptance;
|
|
17
|
+
confirmActivation: (identity: WebViewHostSessionIdentity) => WebViewHostSessionAcceptance;
|
|
18
|
+
isCurrentConnection: (identity: WebViewHostSessionIdentity) => boolean;
|
|
19
|
+
close: () => void;
|
|
20
|
+
};
|
|
21
|
+
export declare function createWebViewHostSessionController(): WebViewHostSessionController;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createMachine, guard, interpret, reduce, state, transition, } from "robot3";
|
|
2
|
+
const SESSION_STATES = {
|
|
3
|
+
awaitingActivation: true,
|
|
4
|
+
awaitingConnection: true,
|
|
5
|
+
connected: true,
|
|
6
|
+
closed: true,
|
|
7
|
+
};
|
|
8
|
+
const identitiesMatch = (left, right) => left !== null &&
|
|
9
|
+
left.connectionId === right.connectionId &&
|
|
10
|
+
left.epoch === right.epoch &&
|
|
11
|
+
left.generation === right.generation;
|
|
12
|
+
const advanceEpoch = reduce((context) => ({
|
|
13
|
+
acceptance: null,
|
|
14
|
+
connection: null,
|
|
15
|
+
epoch: context.epoch + 1,
|
|
16
|
+
}));
|
|
17
|
+
const isCurrentEpoch = guard((context, event) => event.identity.epoch === context.epoch);
|
|
18
|
+
const acceptConnection = reduce((context, event) => (Object.assign(Object.assign({}, context), { acceptance: "accepted", connection: event.identity })));
|
|
19
|
+
const acceptDuplicateConnection = reduce((context) => (Object.assign(Object.assign({}, context), { acceptance: "duplicate" })));
|
|
20
|
+
const rejectConnection = reduce((context) => (Object.assign(Object.assign({}, context), { acceptance: "rejected" })));
|
|
21
|
+
const acceptActivation = reduce((context) => (Object.assign(Object.assign({}, context), { acceptance: "accepted" })));
|
|
22
|
+
const acceptDuplicateActivation = reduce((context) => (Object.assign(Object.assign({}, context), { acceptance: "duplicate" })));
|
|
23
|
+
const rejectActivation = reduce((context) => (Object.assign(Object.assign({}, context), { acceptance: "rejected" })));
|
|
24
|
+
const isDuplicateConnection = guard((context, event) => identitiesMatch(context.connection, event.identity));
|
|
25
|
+
const isCurrentActivation = guard((context, event) => identitiesMatch(context.connection, event.identity));
|
|
26
|
+
const closeSession = reduce((context) => (Object.assign(Object.assign({}, context), { connection: null })));
|
|
27
|
+
function createWebViewHostSessionMachine() {
|
|
28
|
+
const states = {
|
|
29
|
+
awaitingConnection: state(transition("advanceEpoch", "awaitingConnection", advanceEpoch), transition("acceptConnection", "awaitingActivation", isCurrentEpoch, acceptConnection), transition("acceptConnection", "awaitingConnection", rejectConnection), transition("confirmActivation", "awaitingConnection", rejectActivation), transition("close", "closed", closeSession)),
|
|
30
|
+
awaitingActivation: state(transition("advanceEpoch", "awaitingConnection", advanceEpoch), transition("acceptConnection", "awaitingActivation", isDuplicateConnection, acceptDuplicateConnection), transition("acceptConnection", "awaitingActivation", rejectConnection), transition("confirmActivation", "connected", isCurrentActivation, acceptActivation), transition("confirmActivation", "awaitingActivation", rejectActivation), transition("close", "closed", closeSession)),
|
|
31
|
+
connected: state(transition("advanceEpoch", "awaitingConnection", advanceEpoch), transition("acceptConnection", "connected", isDuplicateConnection, acceptDuplicateConnection), transition("acceptConnection", "connected", rejectConnection), transition("confirmActivation", "connected", isCurrentActivation, acceptDuplicateActivation), transition("confirmActivation", "connected", rejectActivation), transition("close", "closed", closeSession)),
|
|
32
|
+
closed: state(transition("advanceEpoch", "awaitingConnection", advanceEpoch), transition("acceptConnection", "closed", rejectConnection), transition("confirmActivation", "closed", rejectActivation), transition("close", "closed")),
|
|
33
|
+
};
|
|
34
|
+
return createMachine(states, () => ({
|
|
35
|
+
acceptance: null,
|
|
36
|
+
connection: null,
|
|
37
|
+
epoch: 0,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
export function createWebViewHostSessionController() {
|
|
41
|
+
const service = interpret(createWebViewHostSessionMachine(), () => { });
|
|
42
|
+
const getSnapshot = () => ({
|
|
43
|
+
connection: service.context.connection,
|
|
44
|
+
epoch: service.context.epoch,
|
|
45
|
+
state: readSessionState(service.machine.current),
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
getSnapshot,
|
|
49
|
+
advanceEpoch: () => {
|
|
50
|
+
service.send({ type: "advanceEpoch" });
|
|
51
|
+
return service.context.epoch;
|
|
52
|
+
},
|
|
53
|
+
acceptConnection: (identity) => {
|
|
54
|
+
service.send({ type: "acceptConnection", identity });
|
|
55
|
+
if (service.context.acceptance === null) {
|
|
56
|
+
throw new Error("WebView host connection was not evaluated");
|
|
57
|
+
}
|
|
58
|
+
return service.context.acceptance;
|
|
59
|
+
},
|
|
60
|
+
confirmActivation: (identity) => {
|
|
61
|
+
service.send({ type: "confirmActivation", identity });
|
|
62
|
+
if (service.context.acceptance === null) {
|
|
63
|
+
throw new Error("WebView host activation was not evaluated");
|
|
64
|
+
}
|
|
65
|
+
return service.context.acceptance;
|
|
66
|
+
},
|
|
67
|
+
isCurrentConnection: (identity) => {
|
|
68
|
+
const snapshot = getSnapshot();
|
|
69
|
+
return (snapshot.state === "connected" &&
|
|
70
|
+
identitiesMatch(snapshot.connection, identity));
|
|
71
|
+
},
|
|
72
|
+
close: () => {
|
|
73
|
+
service.send({ type: "close" });
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function readSessionState(value) {
|
|
78
|
+
if (typeof value !== "string" || !(value in SESSION_STATES)) {
|
|
79
|
+
throw new Error(`Unknown WebView host session state: ${String(value)}`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getuserfeedback/react-native",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.23",
|
|
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.
|
|
48
|
-
"@getuserfeedback/sdk": "^0.9.
|
|
47
|
+
"@getuserfeedback/protocol": "^3.6.1",
|
|
48
|
+
"@getuserfeedback/sdk": "^0.9.8",
|
|
49
49
|
"robot3": "^1.2.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|