@getuserfeedback/react-native 4.0.29 → 5.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -150
- package/dist/client.d.ts +64 -0
- package/dist/client.js +1 -0
- package/dist/core-webview-host.d.ts +0 -1
- package/dist/core-webview-host.js +2 -12
- package/dist/native-core-command-channel.d.ts +1 -0
- package/dist/native-core-command-channel.js +20 -4
- package/dist/native-provider-client-controller.d.ts +11 -0
- package/dist/native-provider-client-controller.js +38 -0
- package/dist/native-provider-client.d.ts +3 -17
- package/dist/native-provider-command-controller.d.ts +19 -0
- package/dist/native-provider-command-controller.js +79 -0
- package/dist/native-provider-flow-registry.d.ts +7 -0
- package/dist/native-provider-flow-registry.js +70 -0
- package/dist/native-provider.d.ts +32 -0
- package/dist/native-provider.js +88 -0
- package/dist/native-runtime-root.d.ts +2 -4
- package/dist/native-runtime-root.js +6 -3
- package/dist/native-view-host-projection.d.ts +1 -2
- package/dist/native-view-host-projection.js +22 -6
- package/dist/native-view-presentation-surface.js +6 -1
- package/dist/native-view-record-controller.d.ts +9 -3
- package/dist/native-view-record-controller.js +276 -50
- package/dist/native-widget-corner-radii.d.ts +1 -2
- package/dist/native-widget-corner-radii.js +1 -1
- package/dist/provider-command-helpers.d.ts +1 -11
- package/dist/provider-command-helpers.js +1 -38
- package/dist/public-api.d.ts +2 -1
- package/dist/public-api.js +2 -1
- package/dist/runtime-endpoints.d.ts +2 -0
- package/dist/runtime-endpoints.js +2 -0
- package/dist/version.js +1 -1
- package/dist/view-orchestration-runtime.js +11 -43
- package/dist/view-webview-host-controller.d.ts +1 -0
- package/dist/view-webview-host-controller.js +1 -0
- package/dist/view-webview-host.d.ts +1 -1
- package/dist/view-webview-host.js +3 -7
- package/dist/webview-document-host.d.ts +2 -3
- package/dist/webview-document-host.js +32 -35
- package/package.json +3 -3
- package/dist/host-viewport.d.ts +0 -3
- package/dist/host-viewport.js +0 -69
- package/dist/index.d.ts +0 -91
- package/dist/index.js +0 -613
- package/dist/provider-command-transport.d.ts +0 -52
- package/dist/provider-command-transport.js +0 -150
- package/dist/provider-message-queue.d.ts +0 -11
- package/dist/provider-message-queue.js +0 -23
- package/dist/provider-pending-commands.d.ts +0 -34
- package/dist/provider-pending-commands.js +0 -58
- package/dist/provider-widget-border-radius.d.ts +0 -11
- package/dist/provider-widget-border-radius.js +0 -102
- package/dist/provider-widget-frame-appearance.d.ts +0 -17
- package/dist/provider-widget-frame-appearance.js +0 -43
- package/dist/provider-widget-host-lifecycle.d.ts +0 -46
- package/dist/provider-widget-host-lifecycle.js +0 -138
- package/dist/provider-widget-host-overlay.d.ts +0 -58
- package/dist/provider-widget-host-overlay.js +0 -616
- package/dist/use-native-direct-command-session-controller.d.ts +0 -2
- package/dist/use-native-direct-command-session-controller.js +0 -21
- package/dist/webview-bridge-script.d.ts +0 -16
- package/dist/webview-bridge-script.js +0 -283
- package/dist/widget-host-lifecycle-machine.d.ts +0 -35
- package/dist/widget-host-lifecycle-machine.js +0 -37
- package/dist/widget-host.d.ts +0 -38
- package/dist/widget-host.js +0 -327
package/README.md
CHANGED
|
@@ -1,170 +1,32 @@
|
|
|
1
1
|
# @getuserfeedback/react-native
|
|
2
2
|
|
|
3
|
-
React Native
|
|
3
|
+
In-product surveys for React Native apps, from [getuserfeedback.com](https://getuserfeedback.com).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Get started
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Install the SDK and its WebView peer dependency:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install @getuserfeedback/react-native react-native-webview
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## API
|
|
16
|
-
|
|
17
|
-
- `GetUserFeedbackProvider`
|
|
18
|
-
- `useGetUserFeedback`
|
|
19
|
-
- `WidgetHost`
|
|
20
|
-
- `useGetUserFeedbackNative`
|
|
13
|
+
Add the provider near the root of your app. You can find your Public API key in
|
|
14
|
+
[Widget settings](https://getuserfeedback.com/app/_/settings/widget).
|
|
21
15
|
|
|
22
16
|
```tsx
|
|
23
|
-
import {
|
|
24
|
-
GetUserFeedbackProvider,
|
|
25
|
-
useGetUserFeedback,
|
|
26
|
-
} from "@getuserfeedback/react-native";
|
|
27
|
-
|
|
28
|
-
function FeedbackActions() {
|
|
29
|
-
const feedback = useGetUserFeedback();
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<Button
|
|
33
|
-
title="Send feedback"
|
|
34
|
-
onPress={() => {
|
|
35
|
-
void feedback.flow("flow_123").open();
|
|
36
|
-
}}
|
|
37
|
-
/>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
17
|
+
import { GetUserFeedbackProvider } from "@getuserfeedback/react-native";
|
|
40
18
|
|
|
41
19
|
export function App() {
|
|
42
20
|
return (
|
|
43
|
-
<GetUserFeedbackProvider
|
|
44
|
-
|
|
45
|
-
loaderUrl="https://cdn.example.com/loader.js"
|
|
46
|
-
>
|
|
47
|
-
<FeedbackActions />
|
|
21
|
+
<GetUserFeedbackProvider clientOptions={{ apiKey: "YOUR_API_KEY" }}>
|
|
22
|
+
<YourApp />
|
|
48
23
|
</GetUserFeedbackProvider>
|
|
49
24
|
);
|
|
50
25
|
}
|
|
51
26
|
```
|
|
52
27
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Custom `source` documents must implement the current WebView transport contract. Provider-owned presentation is driven by `instance:view-host:snapshots-changed`; the legacy aggregate `instance:flow:state-changed:instance` event does not control the native sheet.
|
|
56
|
-
|
|
57
|
-
`WidgetHost` remains exported for advanced hosts that need direct WebView control.
|
|
58
|
-
|
|
59
|
-
The host message types are re-exported from `@getuserfeedback/protocol/webview-transport` through package-owned aliases so the React Native package stays aligned with the shared WebView transport contract.
|
|
60
|
-
|
|
61
|
-
## Local Expo smoke test
|
|
62
|
-
|
|
63
|
-
Use the repo sandbox when testing a non-ejected Expo app against the local
|
|
64
|
-
loader/core/API stack:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
bun run start:sandbox
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Create a React Native manifest:
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
curl -sS -X POST http://127.0.0.1:3710/api/sandbox/react-native-flow-assignment
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
For a physical device, use an address the device can reach:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
curl -sS -X POST "http://127.0.0.1:3710/api/sandbox/react-native-flow-assignment?publicHost=192.168.1.10"
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Then pass `manifest.initOptions` and `manifest.loaderUrl` to the provider:
|
|
28
|
+
That's it. Flows configured for automatic Widget delivery now appear based on
|
|
29
|
+
the targeting rules you set in the dashboard.
|
|
83
30
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
type GetUserFeedbackProviderProps,
|
|
87
|
-
GetUserFeedbackProvider,
|
|
88
|
-
useGetUserFeedback,
|
|
89
|
-
} from "@getuserfeedback/react-native";
|
|
90
|
-
import { Button, View } from "react-native";
|
|
91
|
-
|
|
92
|
-
const manifest = {
|
|
93
|
-
// Paste the sandbox manifest while manually testing on device.
|
|
94
|
-
initOptions: {
|
|
95
|
-
apiKey: "gx_sandbox_...",
|
|
96
|
-
defaultConsent: ["analytics.measurement", "analytics.storage"],
|
|
97
|
-
disableTelemetry: true,
|
|
98
|
-
enableDebug: true,
|
|
99
|
-
runtimeEndpoints: {
|
|
100
|
-
apiUrl: "http://192.168.1.10:3712/v1",
|
|
101
|
-
coreUrl: "http://192.168.1.10:3711/widget/core/v1/core.html",
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
loaderUrl: "http://192.168.1.10:3711/widget/loader/v1/gx_sandbox_.../loader.js",
|
|
105
|
-
commands: {
|
|
106
|
-
identify: {
|
|
107
|
-
userId: "sandbox-user-...",
|
|
108
|
-
traits: { email: "sandbox-user-...@example.test" },
|
|
109
|
-
},
|
|
110
|
-
open: { flowId: "sur_..." },
|
|
111
|
-
track: {
|
|
112
|
-
name: "Checkout Started",
|
|
113
|
-
properties: { source: "react-native-sandbox" },
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
} satisfies {
|
|
117
|
-
commands: {
|
|
118
|
-
identify: { traits: Record<string, unknown>; userId: string };
|
|
119
|
-
open: { flowId: string };
|
|
120
|
-
track: { name: string; properties: Record<string, unknown> };
|
|
121
|
-
};
|
|
122
|
-
initOptions: GetUserFeedbackProviderProps["initOptions"];
|
|
123
|
-
loaderUrl: string;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
function SandboxActions() {
|
|
127
|
-
const feedback = useGetUserFeedback();
|
|
128
|
-
|
|
129
|
-
return (
|
|
130
|
-
<View>
|
|
131
|
-
<Button
|
|
132
|
-
title="Identify"
|
|
133
|
-
onPress={() => {
|
|
134
|
-
void feedback.identify(
|
|
135
|
-
manifest.commands.identify.userId,
|
|
136
|
-
manifest.commands.identify.traits,
|
|
137
|
-
);
|
|
138
|
-
}}
|
|
139
|
-
/>
|
|
140
|
-
<Button
|
|
141
|
-
title="Track"
|
|
142
|
-
onPress={() => {
|
|
143
|
-
void feedback.track(
|
|
144
|
-
manifest.commands.track.name,
|
|
145
|
-
manifest.commands.track.properties,
|
|
146
|
-
);
|
|
147
|
-
}}
|
|
148
|
-
/>
|
|
149
|
-
<Button
|
|
150
|
-
title="Open"
|
|
151
|
-
onPress={() => {
|
|
152
|
-
void feedback.flow(manifest.commands.open.flowId).open();
|
|
153
|
-
}}
|
|
154
|
-
/>
|
|
155
|
-
</View>
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function App() {
|
|
160
|
-
return (
|
|
161
|
-
<GetUserFeedbackProvider
|
|
162
|
-
initOptions={manifest.initOptions}
|
|
163
|
-
loaderUrl={manifest.loaderUrl}
|
|
164
|
-
onCommandError={console.error}
|
|
165
|
-
>
|
|
166
|
-
<SandboxActions />
|
|
167
|
-
</GetUserFeedbackProvider>
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
```
|
|
31
|
+
For user identity, manual flow controls, and troubleshooting, read the
|
|
32
|
+
[React Native SDK guide](https://getuserfeedback.com/docs/get-started/react-native-sdk).
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AppEventExternalId, AppEventJsonValue, PublicCommandPayload } from "@getuserfeedback/protocol";
|
|
2
|
+
import type { ConfigureOptions, InitOptions } from "@getuserfeedback/sdk";
|
|
3
|
+
type IdentifyTraits = Record<string, unknown>;
|
|
4
|
+
type TrackProperties = Record<string, AppEventJsonValue>;
|
|
5
|
+
type OpenCommand = Extract<PublicCommandPayload, {
|
|
6
|
+
kind: "open";
|
|
7
|
+
}>;
|
|
8
|
+
type PrerenderCommand = Extract<PublicCommandPayload, {
|
|
9
|
+
kind: "prerender";
|
|
10
|
+
}>;
|
|
11
|
+
type IdentifyOptions = {
|
|
12
|
+
externalIds?: AppEventExternalId[];
|
|
13
|
+
};
|
|
14
|
+
type TrackOptions = {
|
|
15
|
+
externalIds?: AppEventExternalId[];
|
|
16
|
+
};
|
|
17
|
+
type OpenFlowOptions = Pick<OpenCommand, "hideCloseButton" | "metadata">;
|
|
18
|
+
type PrerenderFlowOptions = Pick<PrerenderCommand, "hideCloseButton">;
|
|
19
|
+
type HostContext = Extract<PublicCommandPayload, {
|
|
20
|
+
kind: "updateHostContext";
|
|
21
|
+
}>["context"];
|
|
22
|
+
type CommandOptions = {
|
|
23
|
+
idempotencyKey?: string;
|
|
24
|
+
};
|
|
25
|
+
/** React Native provider initialization options. */
|
|
26
|
+
export type ClientOptions = InitOptions;
|
|
27
|
+
/** Operations for one on-demand flow lifecycle. */
|
|
28
|
+
export interface FlowRun {
|
|
29
|
+
readonly flowId: string;
|
|
30
|
+
/** Opens the flow. The flow must be published with Widget / On-demand delivery. */
|
|
31
|
+
open: (options?: OpenFlowOptions) => Promise<void>;
|
|
32
|
+
/** Loads the flow's network resources before it opens. */
|
|
33
|
+
prefetch: () => Promise<void>;
|
|
34
|
+
/** Loads the flow and warms its UI before it opens. */
|
|
35
|
+
prerender: (options?: PrerenderFlowOptions) => Promise<void>;
|
|
36
|
+
/** Closes this flow lifecycle. */
|
|
37
|
+
close: () => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Public client exposed by `useGetUserFeedback`.
|
|
41
|
+
*
|
|
42
|
+
* @see https://getuserfeedback.com/docs/get-started/react-native-sdk
|
|
43
|
+
*/
|
|
44
|
+
export interface Client {
|
|
45
|
+
readonly instanceId: string;
|
|
46
|
+
/** Updates consent and other client configuration. */
|
|
47
|
+
configure: (opts: ConfigureOptions, options?: CommandOptions) => Promise<void>;
|
|
48
|
+
identify: {
|
|
49
|
+
(userId: string, traits?: IdentifyTraits, options?: IdentifyOptions): Promise<void>;
|
|
50
|
+
(traits: IdentifyTraits, placeholder: undefined, options?: IdentifyOptions): Promise<void>;
|
|
51
|
+
(traits: IdentifyTraits, options?: IdentifyOptions): Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
/** Records a product event for targeting and analysis. */
|
|
54
|
+
track: (eventName: string, properties?: TrackProperties, options?: TrackOptions) => Promise<void>;
|
|
55
|
+
/** Returns the controller for one on-demand flow lifecycle. */
|
|
56
|
+
flow: (flowId: string) => FlowRun;
|
|
57
|
+
/** Closes every open flow owned by this provider. */
|
|
58
|
+
close: () => Promise<void>;
|
|
59
|
+
/** Resets widget state and the current user identity. */
|
|
60
|
+
reset: (options?: CommandOptions) => Promise<void>;
|
|
61
|
+
updateHostContext: (context: HostContext, options?: CommandOptions) => Promise<void>;
|
|
62
|
+
emitHostSignal: (name: string, data?: unknown, options?: CommandOptions) => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -7,6 +7,5 @@ export type CoreWebViewHostProps = {
|
|
|
7
7
|
onDisconnected?: () => void;
|
|
8
8
|
onInvalidMessage?: (error: Error, value: unknown) => void;
|
|
9
9
|
webViewComponent?: ComponentType<Record<string, unknown>>;
|
|
10
|
-
webViewProps?: Record<string, unknown>;
|
|
11
10
|
};
|
|
12
11
|
export declare function CoreWebViewHost({ coreUrl, ...props }: CoreWebViewHostProps): ReactElement;
|
|
@@ -14,15 +14,7 @@ import { buildCoreWebViewBridgeScript, parseCoreWebViewBridgeMessage, toExternal
|
|
|
14
14
|
import { createCoreWebViewHostController, } from "./core-webview-host-controller.js";
|
|
15
15
|
import { WebViewDocumentHost } from "./webview-document-host.js";
|
|
16
16
|
let nextBridgeId = 0;
|
|
17
|
-
|
|
18
|
-
height: 1,
|
|
19
|
-
left: -10000,
|
|
20
|
-
opacity: 0,
|
|
21
|
-
position: "absolute",
|
|
22
|
-
top: 0,
|
|
23
|
-
width: 1,
|
|
24
|
-
};
|
|
25
|
-
function CoreWebViewHostDocument({ externalCoreUrl, onConnected, onActivationRetryExhausted, onDisconnected, onInvalidMessage, webViewComponent, webViewProps, }) {
|
|
17
|
+
function CoreWebViewHostDocument({ externalCoreUrl, onConnected, onActivationRetryExhausted, onDisconnected, onInvalidMessage, webViewComponent, }) {
|
|
26
18
|
const bridgeIdRef = useRef(null);
|
|
27
19
|
if (bridgeIdRef.current === null) {
|
|
28
20
|
nextBridgeId += 1;
|
|
@@ -55,15 +47,13 @@ function CoreWebViewHostDocument({ externalCoreUrl, onConnected, onActivationRet
|
|
|
55
47
|
const controller = controllerRef.current;
|
|
56
48
|
const buildDocumentStartScript = useMemo(() => () => buildCoreWebViewBridgeScript({ bridgeId, phase: "documentStart" }), [bridgeId]);
|
|
57
49
|
const source = useMemo(() => ({ uri: externalCoreUrl }), [externalCoreUrl]);
|
|
58
|
-
const resolvedWebViewProps = useMemo(() => (Object.assign(Object.assign({}, webViewProps), { containerStyle: CORE_WEBVIEW_HOST_STYLE, pointerEvents: "none", style: CORE_WEBVIEW_HOST_STYLE })), [webViewProps]);
|
|
59
50
|
return createElement((WebViewDocumentHost), {
|
|
60
|
-
bridgeId,
|
|
61
51
|
buildDocumentStartScript,
|
|
62
52
|
controller,
|
|
63
53
|
parseMessage: parseCoreWebViewBridgeMessage,
|
|
64
54
|
source,
|
|
65
55
|
webViewComponent,
|
|
66
|
-
|
|
56
|
+
webViewRole: "core",
|
|
67
57
|
onInvalidMessage,
|
|
68
58
|
});
|
|
69
59
|
}
|
|
@@ -14,5 +14,6 @@ export declare function createNativeCoreCommandChannel(input: {
|
|
|
14
14
|
coreConnection: CoreWebViewHostConnection;
|
|
15
15
|
onHandleInvalidated?: (message: EventHandleInvalidated) => void;
|
|
16
16
|
onInvalidMessage?: (error: Error, value: unknown) => void;
|
|
17
|
+
onSessionFailure?: (error: Error) => void;
|
|
17
18
|
}): OwnedNativeCoreCommandChannel;
|
|
18
19
|
export {};
|
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
import { coreCommandEnvelopeSchema, } from "@getuserfeedback/protocol";
|
|
2
2
|
import { eventHandleInvalidatedSchema, } from "@getuserfeedback/protocol/internal/core-handle-invalidation";
|
|
3
3
|
import { eventCommandSettledSchema, } from "@getuserfeedback/protocol/internal/core-host-command-settlement";
|
|
4
|
+
import { CORE_SESSION_FAILED_CODE, eventCoreSessionFailedSchema, } from "@getuserfeedback/protocol/internal/core-session-failure";
|
|
4
5
|
const DISPOSED_MESSAGE = "Core command channel is disposed";
|
|
5
6
|
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
6
7
|
export function createNativeCoreCommandChannel(input) {
|
|
7
8
|
const listeners = new Set();
|
|
8
9
|
let disposed = false;
|
|
9
10
|
const handleMessage = (value) => {
|
|
10
|
-
var _a, _b, _c;
|
|
11
|
+
var _a, _b, _c, _d, _e;
|
|
11
12
|
if (disposed || !isRecord(value)) {
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
15
|
+
if (value.t === "getuserfeedback:event:error" &&
|
|
16
|
+
value.code === CORE_SESSION_FAILED_CODE) {
|
|
17
|
+
const parsed = eventCoreSessionFailedSchema.safeParse(value);
|
|
18
|
+
if (!parsed.success) {
|
|
19
|
+
(_a = input.onInvalidMessage) === null || _a === void 0 ? void 0 : _a.call(input, parsed.error instanceof Error
|
|
20
|
+
? parsed.error
|
|
21
|
+
: new Error("Invalid Core session failure message"), value);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (parsed.data.gen !== input.coreConnection.generation) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
(_b = input.onSessionFailure) === null || _b === void 0 ? void 0 : _b.call(input, new Error(parsed.data.details.message));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
14
30
|
if (value.t === "getuserfeedback:event:commandSettled") {
|
|
15
31
|
const parsed = eventCommandSettledSchema.safeParse(value);
|
|
16
32
|
if (!parsed.success) {
|
|
17
|
-
(
|
|
33
|
+
(_c = input.onInvalidMessage) === null || _c === void 0 ? void 0 : _c.call(input, parsed.error instanceof Error
|
|
18
34
|
? parsed.error
|
|
19
35
|
: new Error("Invalid core command settlement message"), value);
|
|
20
36
|
return;
|
|
@@ -32,7 +48,7 @@ export function createNativeCoreCommandChannel(input) {
|
|
|
32
48
|
}
|
|
33
49
|
const parsed = eventHandleInvalidatedSchema.safeParse(value);
|
|
34
50
|
if (!parsed.success) {
|
|
35
|
-
(
|
|
51
|
+
(_d = input.onInvalidMessage) === null || _d === void 0 ? void 0 : _d.call(input, parsed.error instanceof Error
|
|
36
52
|
? parsed.error
|
|
37
53
|
: new Error("Invalid core handle invalidation message"), value);
|
|
38
54
|
return;
|
|
@@ -40,7 +56,7 @@ export function createNativeCoreCommandChannel(input) {
|
|
|
40
56
|
if (parsed.data.gen !== input.coreConnection.generation) {
|
|
41
57
|
return;
|
|
42
58
|
}
|
|
43
|
-
(
|
|
59
|
+
(_e = input.onHandleInvalidated) === null || _e === void 0 ? void 0 : _e.call(input, parsed.data);
|
|
44
60
|
};
|
|
45
61
|
const unsubscribeTransport = input.coreConnection.transport.subscribe(handleMessage);
|
|
46
62
|
const channel = {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EventHandleInvalidated } from "@getuserfeedback/protocol/internal/core-handle-invalidation";
|
|
2
|
+
export declare function createNativeProviderClientController(input: {
|
|
3
|
+
instanceId: string;
|
|
4
|
+
onError?: (error: Error) => void;
|
|
5
|
+
}): {
|
|
6
|
+
client: import("./native-provider-client.js").NativeProviderClient<void>;
|
|
7
|
+
directSessionController: import("./native-direct-command-session-controller.js").NativeDirectCommandSessionController;
|
|
8
|
+
dispose: () => void;
|
|
9
|
+
onHandleInvalidated: (message: EventHandleInvalidated) => void;
|
|
10
|
+
onSessionEnded: () => void;
|
|
11
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createNativeProviderClient, parseRequiredString, } from "./native-provider-client.js";
|
|
2
|
+
import { createNativeProviderCommandController } from "./native-provider-command-controller.js";
|
|
3
|
+
import { createNativeProviderFlowRegistry } from "./native-provider-flow-registry.js";
|
|
4
|
+
export function createNativeProviderClientController(input) {
|
|
5
|
+
const commands = createNativeProviderCommandController(input);
|
|
6
|
+
const flows = createNativeProviderFlowRegistry(commands);
|
|
7
|
+
const session = commands.directSessionController;
|
|
8
|
+
const client = createNativeProviderClient({
|
|
9
|
+
dispatchCommand: (command, options) => {
|
|
10
|
+
if (command.kind === "close" || command.kind === "reset") {
|
|
11
|
+
return commands.runBarrier(command.kind, flows.reset, options);
|
|
12
|
+
}
|
|
13
|
+
return commands
|
|
14
|
+
.runOperation((dispatch) => dispatch(command), options)
|
|
15
|
+
.then(() => undefined);
|
|
16
|
+
},
|
|
17
|
+
flow: (flowId) => flows.flow(parseRequiredString("flowId", flowId)),
|
|
18
|
+
instanceId: input.instanceId,
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
client,
|
|
22
|
+
directSessionController: session,
|
|
23
|
+
dispose: () => {
|
|
24
|
+
flows.reset();
|
|
25
|
+
commands.dispose();
|
|
26
|
+
},
|
|
27
|
+
onHandleInvalidated: (message) => {
|
|
28
|
+
if (message.instanceId === input.instanceId &&
|
|
29
|
+
message.handleKind === "flow")
|
|
30
|
+
flows.invalidate(message.handleId);
|
|
31
|
+
},
|
|
32
|
+
onSessionEnded: () => {
|
|
33
|
+
session.rejectUntilAttached(new Error("Core session ended"));
|
|
34
|
+
void commands.runBarrier(null, flows.reset);
|
|
35
|
+
session.beginAttachmentAttempt();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import type { AppEventExternalId, AppEventJsonValue, PublicCommandPayload } from "@getuserfeedback/protocol";
|
|
2
2
|
import type { ConfigureOptions } from "@getuserfeedback/sdk";
|
|
3
|
+
import type { FlowRun } from "./client.js";
|
|
3
4
|
type IdentifyTraits = Record<string, unknown>;
|
|
4
5
|
type TrackProperties = Record<string, AppEventJsonValue>;
|
|
5
|
-
type OpenCommand = Extract<PublicCommandPayload, {
|
|
6
|
-
kind: "open";
|
|
7
|
-
}>;
|
|
8
|
-
type PrerenderCommand = Extract<PublicCommandPayload, {
|
|
9
|
-
kind: "prerender";
|
|
10
|
-
}>;
|
|
11
6
|
type NativeProviderClientCommand = Extract<PublicCommandPayload, {
|
|
12
7
|
kind: "configure" | "identify" | "track" | "close" | "reset" | "updateHostContext" | "emitHostSignal";
|
|
13
8
|
}>;
|
|
@@ -17,21 +12,12 @@ type IdentifyOptions = {
|
|
|
17
12
|
type TrackOptions = {
|
|
18
13
|
externalIds?: AppEventExternalId[];
|
|
19
14
|
};
|
|
20
|
-
type OpenFlowOptions = Pick<OpenCommand, "hideCloseButton" | "metadata">;
|
|
21
|
-
type PrerenderFlowOptions = Pick<PrerenderCommand, "hideCloseButton">;
|
|
22
15
|
type HostContext = Extract<PublicCommandPayload, {
|
|
23
16
|
kind: "updateHostContext";
|
|
24
17
|
}>["context"];
|
|
25
18
|
interface NativeProviderCommandOptions {
|
|
26
19
|
idempotencyKey?: string;
|
|
27
20
|
}
|
|
28
|
-
export interface ReactNativeFlowRun {
|
|
29
|
-
readonly flowId: string;
|
|
30
|
-
open: (options?: OpenFlowOptions) => Promise<void>;
|
|
31
|
-
prefetch: () => Promise<void>;
|
|
32
|
-
prerender: (options?: PrerenderFlowOptions) => Promise<void>;
|
|
33
|
-
close: () => Promise<void>;
|
|
34
|
-
}
|
|
35
21
|
export interface NativeProviderClient<TResult> {
|
|
36
22
|
readonly instanceId: string;
|
|
37
23
|
configure: (opts: ConfigureOptions, options?: NativeProviderCommandOptions) => Promise<TResult>;
|
|
@@ -41,7 +27,7 @@ export interface NativeProviderClient<TResult> {
|
|
|
41
27
|
(traits: IdentifyTraits, options?: IdentifyOptions): Promise<TResult>;
|
|
42
28
|
};
|
|
43
29
|
track: (eventName: string, properties?: TrackProperties, options?: TrackOptions) => Promise<TResult>;
|
|
44
|
-
flow: (flowId: string) =>
|
|
30
|
+
flow: (flowId: string) => FlowRun;
|
|
45
31
|
close: () => Promise<TResult>;
|
|
46
32
|
reset: (options?: NativeProviderCommandOptions) => Promise<TResult>;
|
|
47
33
|
updateHostContext: (context: HostContext, options?: NativeProviderCommandOptions) => Promise<TResult>;
|
|
@@ -49,7 +35,7 @@ export interface NativeProviderClient<TResult> {
|
|
|
49
35
|
}
|
|
50
36
|
type NativeProviderClientOptions<TResult> = {
|
|
51
37
|
dispatchCommand: (command: NativeProviderClientCommand, options?: NativeProviderCommandOptions) => Promise<TResult>;
|
|
52
|
-
flow: (flowId: string) =>
|
|
38
|
+
flow: (flowId: string) => FlowRun;
|
|
53
39
|
instanceId: string;
|
|
54
40
|
};
|
|
55
41
|
export declare const parseRequiredString: (name: string, value?: string) => string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CommandSettledSuccessDetail } from "@getuserfeedback/protocol/host";
|
|
2
|
+
import type { NativeDirectCommandEnvelope } from "./native-direct-command-session.js";
|
|
3
|
+
type Command = NativeDirectCommandEnvelope["command"];
|
|
4
|
+
type CommandOptions = {
|
|
5
|
+
idempotencyKey?: string;
|
|
6
|
+
};
|
|
7
|
+
type Dispatch = (command: Command) => Promise<CommandSettledSuccessDetail>;
|
|
8
|
+
type RunOperation = <T>(run: (dispatch: Dispatch) => Promise<T>, options?: CommandOptions) => Promise<T | undefined>;
|
|
9
|
+
export declare function createNativeProviderCommandController(input: {
|
|
10
|
+
instanceId: string;
|
|
11
|
+
onError?: (error: Error) => void;
|
|
12
|
+
}): {
|
|
13
|
+
directSessionController: import("./native-direct-command-session-controller.js").NativeDirectCommandSessionController;
|
|
14
|
+
dispose: () => void;
|
|
15
|
+
reject: (error: unknown) => Promise<never>;
|
|
16
|
+
runBarrier: (kind: "close" | "reset" | null, onGenerationAdvanced: () => void, options?: CommandOptions) => Promise<void>;
|
|
17
|
+
runOperation: RunOperation;
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { parsePublicCommand } from "@getuserfeedback/protocol";
|
|
2
|
+
import { createCommandEnvelopeWithInstanceId } from "@getuserfeedback/protocol/host";
|
|
3
|
+
import { createClientTransportAdmission } from "@getuserfeedback/sdk/internal/client-transport-admission";
|
|
4
|
+
import { createNativeDirectCommandSessionController } from "./native-direct-command-session-controller.js";
|
|
5
|
+
import { REACT_NATIVE_CLIENT_META } from "./provider-command-helpers.js";
|
|
6
|
+
const parseIdempotencyKey = (options) => {
|
|
7
|
+
var _a;
|
|
8
|
+
const idempotencyKey = (_a = options === null || options === void 0 ? void 0 : options.idempotencyKey) === null || _a === void 0 ? void 0 : _a.trim();
|
|
9
|
+
if ((options === null || options === void 0 ? void 0 : options.idempotencyKey) !== undefined && !idempotencyKey)
|
|
10
|
+
throw new Error("idempotencyKey must be a non-empty string");
|
|
11
|
+
return idempotencyKey;
|
|
12
|
+
};
|
|
13
|
+
export function createNativeProviderCommandController(input) {
|
|
14
|
+
const directSessionController = createNativeDirectCommandSessionController();
|
|
15
|
+
const admission = createClientTransportAdmission();
|
|
16
|
+
const report = (value) => {
|
|
17
|
+
var _a;
|
|
18
|
+
const error = value instanceof Error ? value : new Error(String(value));
|
|
19
|
+
try {
|
|
20
|
+
(_a = input.onError) === null || _a === void 0 ? void 0 : _a.call(input, error);
|
|
21
|
+
}
|
|
22
|
+
catch (_b) { }
|
|
23
|
+
return error;
|
|
24
|
+
};
|
|
25
|
+
const observe = (promise) => promise.catch((error) => Promise.reject(report(error)));
|
|
26
|
+
const dispatch = (command, idempotencyKey) => {
|
|
27
|
+
const envelope = createCommandEnvelopeWithInstanceId({
|
|
28
|
+
clientMeta: REACT_NATIVE_CLIENT_META,
|
|
29
|
+
command: parsePublicCommand(command),
|
|
30
|
+
idempotencyKey,
|
|
31
|
+
instanceId: input.instanceId,
|
|
32
|
+
});
|
|
33
|
+
return directSessionController.session.dispatch(envelope);
|
|
34
|
+
};
|
|
35
|
+
const runOperation = (run, options) => {
|
|
36
|
+
let idempotencyKey;
|
|
37
|
+
try {
|
|
38
|
+
idempotencyKey = parseIdempotencyKey(options);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return observe(Promise.reject(error));
|
|
42
|
+
}
|
|
43
|
+
return observe(admission
|
|
44
|
+
.runOperation(({ attemptHandoff }) => run((command) => attemptHandoff({
|
|
45
|
+
handOff: () => ({
|
|
46
|
+
settlement: dispatch(command, idempotencyKey),
|
|
47
|
+
}),
|
|
48
|
+
prepare: () => { },
|
|
49
|
+
})))
|
|
50
|
+
.then((result) => result.status === "settled" ? result.result : undefined));
|
|
51
|
+
};
|
|
52
|
+
const runBarrier = (kind, onGenerationAdvanced, options) => {
|
|
53
|
+
let idempotencyKey;
|
|
54
|
+
try {
|
|
55
|
+
idempotencyKey = parseIdempotencyKey(options);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return observe(Promise.reject(error));
|
|
59
|
+
}
|
|
60
|
+
return observe(admission
|
|
61
|
+
.runBarrier({
|
|
62
|
+
handOff: () => ({
|
|
63
|
+
settlement: kind === null
|
|
64
|
+
? Promise.resolve()
|
|
65
|
+
: dispatch({ kind }, idempotencyKey).then(() => undefined),
|
|
66
|
+
}),
|
|
67
|
+
onGenerationAdvanced,
|
|
68
|
+
prepare: () => { },
|
|
69
|
+
})
|
|
70
|
+
.then(() => undefined));
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
directSessionController,
|
|
74
|
+
dispose: () => directSessionController.dispose(new Error("Native provider client has been disposed")),
|
|
75
|
+
reject: (error) => Promise.reject(report(error)),
|
|
76
|
+
runBarrier,
|
|
77
|
+
runOperation,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FlowRun } from "./client.js";
|
|
2
|
+
import type { createNativeProviderCommandController } from "./native-provider-command-controller.js";
|
|
3
|
+
export declare function createNativeProviderFlowRegistry(commands: ReturnType<typeof createNativeProviderCommandController>): {
|
|
4
|
+
flow: (flowId: string) => FlowRun;
|
|
5
|
+
invalidate: (handleId: string) => void;
|
|
6
|
+
reset: () => void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { parsePublicCommand } from "@getuserfeedback/protocol";
|
|
2
|
+
import { CommandSettlementError, createFlowCommandHandleController, getFlowHandleFromSettlementDetail, } from "@getuserfeedback/protocol/host";
|
|
3
|
+
import { createFlowOpenRequestController } from "@getuserfeedback/sdk/internal/flow-open-request-controller";
|
|
4
|
+
const isStaleHandleError = (error) => error instanceof CommandSettlementError
|
|
5
|
+
? error.code === "STALE_HANDLE"
|
|
6
|
+
: error instanceof Error && error.message.includes("flowHandleId is stale");
|
|
7
|
+
export function createNativeProviderFlowRegistry(commands) {
|
|
8
|
+
const create = (flowId) => {
|
|
9
|
+
const openRequests = createFlowOpenRequestController();
|
|
10
|
+
const handle = createFlowCommandHandleController({
|
|
11
|
+
getHandleIdFromSettlement: (settlement) => { var _a; return (_a = getFlowHandleFromSettlementDetail(settlement)) === null || _a === void 0 ? void 0 : _a.flowHandleId; },
|
|
12
|
+
isStaleHandleError,
|
|
13
|
+
});
|
|
14
|
+
const allocation = (command) => commands
|
|
15
|
+
.runOperation((dispatch) => handle.runCommand((flowHandleId) => (Object.assign(Object.assign({}, command), { flowHandleId })), dispatch))
|
|
16
|
+
.then(() => undefined);
|
|
17
|
+
const prepare = (build, start = allocation) => {
|
|
18
|
+
try {
|
|
19
|
+
return start(parsePublicCommand(build()));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return commands.reject(error);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const run = {
|
|
26
|
+
flowId,
|
|
27
|
+
open: (options) => prepare(() => ({
|
|
28
|
+
flowId,
|
|
29
|
+
hideCloseButton: options === null || options === void 0 ? void 0 : options.hideCloseButton,
|
|
30
|
+
kind: "open",
|
|
31
|
+
metadata: options === null || options === void 0 ? void 0 : options.metadata,
|
|
32
|
+
}), (command) => openRequests.run(() => allocation(command))),
|
|
33
|
+
prefetch: () => prepare(() => ({
|
|
34
|
+
flowId,
|
|
35
|
+
kind: "prefetch",
|
|
36
|
+
})),
|
|
37
|
+
prerender: (options) => prepare(() => ({
|
|
38
|
+
flowId,
|
|
39
|
+
hideCloseButton: options === null || options === void 0 ? void 0 : options.hideCloseButton,
|
|
40
|
+
kind: "prerender",
|
|
41
|
+
})),
|
|
42
|
+
close: () => {
|
|
43
|
+
openRequests.clear();
|
|
44
|
+
return commands
|
|
45
|
+
.runOperation((dispatch) => handle.runResolvedHandleCommand((flowHandleId) => ({ flowHandleId, kind: "close" }), async (command) => void (await dispatch(command))))
|
|
46
|
+
.then(() => undefined);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return { clearPendingOpen: openRequests.clear, handle, run };
|
|
50
|
+
};
|
|
51
|
+
const controllers = new Map();
|
|
52
|
+
return {
|
|
53
|
+
flow: (flowId) => {
|
|
54
|
+
var _a;
|
|
55
|
+
const current = (_a = controllers.get(flowId)) !== null && _a !== void 0 ? _a : create(flowId);
|
|
56
|
+
controllers.set(flowId, current);
|
|
57
|
+
return current.run;
|
|
58
|
+
},
|
|
59
|
+
invalidate: (handleId) => {
|
|
60
|
+
for (const controller of controllers.values())
|
|
61
|
+
controller.handle.invalidateResolvedHandle(handleId);
|
|
62
|
+
},
|
|
63
|
+
reset: () => {
|
|
64
|
+
for (const controller of controllers.values()) {
|
|
65
|
+
controller.clearPendingOpen();
|
|
66
|
+
controller.handle.reset();
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|