@getuserfeedback/react-native 1.0.0 → 1.3.3
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 +148 -1
- package/dist/host-viewport.d.ts +3 -0
- package/dist/host-viewport.js +69 -0
- package/dist/index.d.ts +80 -14
- package/dist/index.js +813 -10
- package/dist/provider-overlay-state.d.ts +43 -0
- package/dist/provider-overlay-state.js +134 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +6 -0
- package/dist/webview-bridge-script.d.ts +16 -0
- package/dist/webview-bridge-script.js +290 -0
- package/dist/widget-host-lifecycle-machine.d.ts +34 -0
- package/dist/widget-host-lifecycle-machine.js +37 -0
- package/dist/widget-host.d.ts +38 -0
- package/dist/widget-host.js +327 -0
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
React Native bindings for getuserfeedback.
|
|
4
4
|
|
|
5
|
-
This package
|
|
5
|
+
This package hosts the existing getuserfeedback WebView runtime from React Native and exposes a provider/hook API for enqueueing widget commands from non-ejected Expo or React Native apps.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -15,7 +15,154 @@ Requires React, React Native, and `react-native-webview`.
|
|
|
15
15
|
## API
|
|
16
16
|
|
|
17
17
|
- `GetUserFeedbackProvider`
|
|
18
|
+
- `useGetUserFeedback`
|
|
18
19
|
- `WidgetHost`
|
|
19
20
|
- `useGetUserFeedbackNative`
|
|
20
21
|
|
|
22
|
+
```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.open("flow_123");
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function App() {
|
|
42
|
+
return (
|
|
43
|
+
<GetUserFeedbackProvider
|
|
44
|
+
initOptions={{ apiKey: "YOUR_API_KEY" }}
|
|
45
|
+
loaderUrl="https://cdn.example.com/loader.js"
|
|
46
|
+
>
|
|
47
|
+
<FeedbackActions />
|
|
48
|
+
</GetUserFeedbackProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`GetUserFeedbackProvider` renders `WidgetHost` internally, auto-enqueues `init`, measures the React Native window for the hosted WebView viewport, shows the hosted WebView inside a native bottom sheet while a flow is loading or open, and resolves command promises when the WebView transport reports command settlement. Pass either `loaderUrl` or a custom WebView `source` so the hosted WebView has a loader runtime to execute commands. Use `onCommandError` to receive automatic setup failures from `init` or `configure`.
|
|
54
|
+
|
|
55
|
+
`WidgetHost` remains exported for advanced hosts that need direct WebView control.
|
|
56
|
+
|
|
21
57
|
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.
|
|
58
|
+
|
|
59
|
+
## Local Expo smoke test
|
|
60
|
+
|
|
61
|
+
Use the repo sandbox when testing a non-ejected Expo app against the local
|
|
62
|
+
loader/core/API stack:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bun run start:sandbox
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Create a React Native manifest:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl -sS -X POST http://127.0.0.1:3710/api/sandbox/react-native-flow-assignment
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For a physical device, use an address the device can reach:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
curl -sS -X POST "http://127.0.0.1:3710/api/sandbox/react-native-flow-assignment?publicHost=192.168.1.10"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Then pass `manifest.initOptions` and `manifest.loaderUrl` to the provider:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import {
|
|
84
|
+
type GetUserFeedbackProviderProps,
|
|
85
|
+
GetUserFeedbackProvider,
|
|
86
|
+
useGetUserFeedback,
|
|
87
|
+
} from "@getuserfeedback/react-native";
|
|
88
|
+
import { Button, View } from "react-native";
|
|
89
|
+
|
|
90
|
+
const manifest = {
|
|
91
|
+
// Paste the sandbox manifest while manually testing on device.
|
|
92
|
+
initOptions: {
|
|
93
|
+
apiKey: "gx_sandbox_...",
|
|
94
|
+
defaultConsent: ["analytics.measurement", "analytics.storage"],
|
|
95
|
+
disableTelemetry: true,
|
|
96
|
+
enableDebug: true,
|
|
97
|
+
runtimeEndpoints: {
|
|
98
|
+
apiUrl: "http://192.168.1.10:3712/v1",
|
|
99
|
+
coreUrl: "http://192.168.1.10:3711/widget/core/v1/core.html",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
loaderUrl: "http://192.168.1.10:3711/widget/loader/v1/gx_sandbox_.../loader.js",
|
|
103
|
+
commands: {
|
|
104
|
+
identify: {
|
|
105
|
+
userId: "sandbox-user-...",
|
|
106
|
+
traits: { email: "sandbox-user-...@example.test" },
|
|
107
|
+
},
|
|
108
|
+
open: { flowId: "sur_..." },
|
|
109
|
+
track: {
|
|
110
|
+
name: "Checkout Started",
|
|
111
|
+
properties: { source: "react-native-sandbox" },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
} satisfies {
|
|
115
|
+
commands: {
|
|
116
|
+
identify: { traits: Record<string, unknown>; userId: string };
|
|
117
|
+
open: { flowId: string };
|
|
118
|
+
track: { name: string; properties: Record<string, unknown> };
|
|
119
|
+
};
|
|
120
|
+
initOptions: GetUserFeedbackProviderProps["initOptions"];
|
|
121
|
+
loaderUrl: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function SandboxActions() {
|
|
125
|
+
const feedback = useGetUserFeedback();
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<View>
|
|
129
|
+
<Button
|
|
130
|
+
title="Identify"
|
|
131
|
+
onPress={() => {
|
|
132
|
+
void feedback.identify(
|
|
133
|
+
manifest.commands.identify.userId,
|
|
134
|
+
manifest.commands.identify.traits,
|
|
135
|
+
);
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
<Button
|
|
139
|
+
title="Track"
|
|
140
|
+
onPress={() => {
|
|
141
|
+
void feedback.track(
|
|
142
|
+
manifest.commands.track.name,
|
|
143
|
+
manifest.commands.track.properties,
|
|
144
|
+
);
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
<Button
|
|
148
|
+
title="Open"
|
|
149
|
+
onPress={() => {
|
|
150
|
+
void feedback.open(manifest.commands.open.flowId);
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
</View>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function App() {
|
|
158
|
+
return (
|
|
159
|
+
<GetUserFeedbackProvider
|
|
160
|
+
initOptions={manifest.initOptions}
|
|
161
|
+
loaderUrl={manifest.loaderUrl}
|
|
162
|
+
onCommandError={console.error}
|
|
163
|
+
>
|
|
164
|
+
<SandboxActions />
|
|
165
|
+
</GetUserFeedbackProvider>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
function isHostViewport(value) {
|
|
3
|
+
return (typeof value === "object" &&
|
|
4
|
+
value !== null &&
|
|
5
|
+
"width" in value &&
|
|
6
|
+
typeof value.width === "number" &&
|
|
7
|
+
Number.isFinite(value.width) &&
|
|
8
|
+
value.width > 0 &&
|
|
9
|
+
"height" in value &&
|
|
10
|
+
typeof value.height === "number" &&
|
|
11
|
+
Number.isFinite(value.height) &&
|
|
12
|
+
value.height > 0);
|
|
13
|
+
}
|
|
14
|
+
function toHostViewport(value) {
|
|
15
|
+
return isHostViewport(value)
|
|
16
|
+
? {
|
|
17
|
+
height: value.height,
|
|
18
|
+
width: value.width,
|
|
19
|
+
}
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
function resolveReactNativeDimensions() {
|
|
23
|
+
var _a;
|
|
24
|
+
try {
|
|
25
|
+
const reactNativeModule = require("react-native");
|
|
26
|
+
return (_a = reactNativeModule.Dimensions) !== null && _a !== void 0 ? _a : null;
|
|
27
|
+
}
|
|
28
|
+
catch (_b) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function readWindowViewport(dimensions) {
|
|
33
|
+
if (!dimensions) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return toHostViewport(dimensions.get("window"));
|
|
37
|
+
}
|
|
38
|
+
function useReactNativeWindowViewport(enabled) {
|
|
39
|
+
const dimensions = useMemo(() => (enabled ? resolveReactNativeDimensions() : null), [enabled]);
|
|
40
|
+
const [viewport, setViewport] = useState(() => readWindowViewport(dimensions));
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!enabled) {
|
|
43
|
+
setViewport(undefined);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setViewport(readWindowViewport(dimensions));
|
|
47
|
+
if (!(dimensions === null || dimensions === void 0 ? void 0 : dimensions.addEventListener)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const handleChange = (event) => {
|
|
51
|
+
var _a;
|
|
52
|
+
setViewport((_a = toHostViewport(event.window)) !== null && _a !== void 0 ? _a : readWindowViewport(dimensions));
|
|
53
|
+
};
|
|
54
|
+
const subscription = dimensions.addEventListener("change", handleChange);
|
|
55
|
+
return () => {
|
|
56
|
+
var _a;
|
|
57
|
+
if (subscription === null || subscription === void 0 ? void 0 : subscription.remove) {
|
|
58
|
+
subscription.remove();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
(_a = dimensions.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(dimensions, "change", handleChange);
|
|
62
|
+
};
|
|
63
|
+
}, [dimensions, enabled]);
|
|
64
|
+
return viewport !== null && viewport !== void 0 ? viewport : readWindowViewport(dimensions);
|
|
65
|
+
}
|
|
66
|
+
export function useResolvedHostViewport(explicitHostViewport) {
|
|
67
|
+
const measuredHostViewport = useReactNativeWindowViewport(explicitHostViewport === undefined);
|
|
68
|
+
return explicitHostViewport !== null && explicitHostViewport !== void 0 ? explicitHostViewport : measuredHostViewport;
|
|
69
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,91 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { WebViewCommandEnvelope, WebViewTransportNativeMessage, WebViewTransportWebMessage } from "@getuserfeedback/protocol/webview-transport";
|
|
1
|
+
import type { AppEventExternalId, AppEventJsonValue, PublicCommandPayload } from "@getuserfeedback/protocol";
|
|
2
|
+
import type { WebViewCommandEnvelope, WebViewTransportHostEvent, WebViewTransportNativeMessage, WebViewTransportWebMessage } from "@getuserfeedback/protocol/webview-transport";
|
|
3
|
+
import type { ConfigureOptions, InitOptions } from "@getuserfeedback/sdk";
|
|
3
4
|
import { type ReactElement, type ReactNode } from "react";
|
|
4
|
-
|
|
5
|
+
import { WidgetHost, type WidgetHostProps, type WidgetHostSource } from "./widget-host.js";
|
|
6
|
+
export { REACT_NATIVE_SDK_VERSION } from "./version.js";
|
|
7
|
+
export { buildNativeMessageInjectionScript, buildWebViewBridgeScript, } from "./webview-bridge-script.js";
|
|
8
|
+
export { WidgetHost, type WidgetHostProps, type WidgetHostSource };
|
|
5
9
|
export type ReactNativeWidgetCommandEnvelope = WebViewCommandEnvelope;
|
|
10
|
+
export type ReactNativeWidgetCommand = ReactNativeWidgetCommandEnvelope["command"];
|
|
6
11
|
export type ReactNativeWidgetNativeMessage = WebViewTransportNativeMessage;
|
|
7
12
|
export type ReactNativeWidgetWebMessage = WebViewTransportWebMessage;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
type IdentifyTraits = Record<string, unknown>;
|
|
14
|
+
type TrackProperties = Record<string, AppEventJsonValue>;
|
|
15
|
+
type OpenCommand = Extract<PublicCommandPayload, {
|
|
16
|
+
kind: "open";
|
|
17
|
+
}>;
|
|
18
|
+
type PrerenderCommand = Extract<PublicCommandPayload, {
|
|
19
|
+
kind: "prerender";
|
|
20
|
+
}>;
|
|
21
|
+
type IdentifyOptions = {
|
|
22
|
+
externalIds?: AppEventExternalId[];
|
|
23
|
+
};
|
|
24
|
+
type TrackOptions = {
|
|
25
|
+
externalIds?: AppEventExternalId[];
|
|
26
|
+
};
|
|
27
|
+
type OpenOptions = Pick<OpenCommand, "hideCloseButton">;
|
|
28
|
+
type PrerenderOptions = Pick<PrerenderCommand, "hideCloseButton">;
|
|
29
|
+
type HostContext = Extract<PublicCommandPayload, {
|
|
30
|
+
kind: "updateHostContext";
|
|
31
|
+
}>["context"];
|
|
32
|
+
type CommandSettledEvent = Extract<WebViewTransportHostEvent, {
|
|
33
|
+
name: "instance:command:settled";
|
|
34
|
+
}>;
|
|
35
|
+
type CommandSettledSuccessDetail = Extract<CommandSettledEvent["detail"], {
|
|
36
|
+
ok: true;
|
|
37
|
+
}>;
|
|
38
|
+
export interface ReactNativeCommandResult {
|
|
39
|
+
envelope: ReactNativeWidgetCommandEnvelope;
|
|
40
|
+
message: ReactNativeWidgetNativeMessage;
|
|
41
|
+
settlement?: CommandSettledEvent["detail"];
|
|
42
|
+
settlementResult?: CommandSettledSuccessDetail["result"];
|
|
13
43
|
}
|
|
14
|
-
export interface
|
|
15
|
-
|
|
16
|
-
|
|
44
|
+
export interface ReactNativeCommandOptions {
|
|
45
|
+
idempotencyKey?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ReactNativeGetUserFeedbackClient {
|
|
48
|
+
readonly instanceId: string;
|
|
49
|
+
readonly nativeMessages: readonly ReactNativeWidgetNativeMessage[];
|
|
50
|
+
enqueueCommand: (command: ReactNativeWidgetCommand, options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
|
|
51
|
+
configure: (opts: ConfigureOptions, options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
|
|
52
|
+
identify: {
|
|
53
|
+
(userId: string, traits?: IdentifyTraits, options?: IdentifyOptions): Promise<ReactNativeCommandResult>;
|
|
54
|
+
(traits: IdentifyTraits, placeholder: undefined, options?: IdentifyOptions): Promise<ReactNativeCommandResult>;
|
|
55
|
+
(traits: IdentifyTraits, options?: IdentifyOptions): Promise<ReactNativeCommandResult>;
|
|
56
|
+
};
|
|
57
|
+
track: (eventName: string, properties?: TrackProperties, options?: TrackOptions) => Promise<ReactNativeCommandResult>;
|
|
58
|
+
open: (flowId: string, options?: OpenOptions, commandOptions?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
|
|
59
|
+
prefetch: (flowId: string, options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
|
|
60
|
+
prerender: (flowId: string, options?: PrerenderOptions, commandOptions?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
|
|
61
|
+
close: (flowHandleId?: string, options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
|
|
62
|
+
reset: (options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
|
|
63
|
+
updateHostContext: (context: HostContext, options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
|
|
64
|
+
emitHostSignal: (name: string, data?: unknown, options?: ReactNativeCommandOptions) => Promise<ReactNativeCommandResult>;
|
|
65
|
+
handleWebMessage: (message: ReactNativeWidgetWebMessage) => void;
|
|
17
66
|
}
|
|
18
|
-
|
|
67
|
+
interface GetUserFeedbackProviderBaseProps {
|
|
68
|
+
children?: ReactNode;
|
|
19
69
|
initOptions: InitOptions;
|
|
20
70
|
configureOptions?: ConfigureOptions;
|
|
71
|
+
instanceId?: string;
|
|
21
72
|
onWebMessage?: (message: WebViewTransportWebMessage) => void;
|
|
73
|
+
onInvalidWebMessage?: (error: Error, rawMessage: unknown) => void;
|
|
74
|
+
onCommandError?: (error: Error) => void;
|
|
75
|
+
autoInit?: boolean;
|
|
76
|
+
commandTimeoutMs?: number;
|
|
77
|
+
hostViewport?: WidgetHostProps["hostViewport"];
|
|
78
|
+
webViewComponent?: WidgetHostProps["webViewComponent"];
|
|
79
|
+
webViewProps?: WidgetHostProps["webViewProps"];
|
|
22
80
|
}
|
|
23
|
-
export
|
|
81
|
+
export type GetUserFeedbackProviderProps = GetUserFeedbackProviderBaseProps & ({
|
|
82
|
+
loaderUrl: string;
|
|
83
|
+
source?: WidgetHostSource;
|
|
84
|
+
} | {
|
|
85
|
+
loaderUrl?: string;
|
|
86
|
+
source: WidgetHostSource;
|
|
87
|
+
});
|
|
88
|
+
export type GetUserFeedbackNativeContextValue = ReactNativeGetUserFeedbackClient;
|
|
89
|
+
export declare function GetUserFeedbackProvider({ autoInit, children, commandTimeoutMs, configureOptions, hostViewport, instanceId: instanceIdProp, initOptions, loaderUrl, onCommandError, onInvalidWebMessage, onWebMessage, source, webViewComponent, webViewProps, }: GetUserFeedbackProviderProps): ReactElement;
|
|
24
90
|
export declare function useGetUserFeedbackNative(): GetUserFeedbackNativeContextValue;
|
|
25
|
-
export declare
|
|
91
|
+
export declare const useGetUserFeedback: typeof useGetUserFeedbackNative;
|