@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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  React Native bindings for getuserfeedback.
4
4
 
5
- This package is currently a scaffold for the Expo/React Native WebView integration. It exports the stable package shape and protocol-typed component APIs; WebView orchestration will land in the next implementation slice.
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,3 @@
1
+ import type { WidgetHostProps } from "./widget-host.js";
2
+ export type HostViewport = NonNullable<WidgetHostProps["hostViewport"]>;
3
+ export declare function useResolvedHostViewport(explicitHostViewport: HostViewport | undefined): HostViewport | undefined;
@@ -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 { ConfigureOptions, InitOptions, PublicCommandPayload } from "@getuserfeedback/protocol";
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
- export type ReactNativeWidgetCommand = PublicCommandPayload;
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
- export interface GetUserFeedbackProviderProps {
9
- children?: ReactNode;
10
- initOptions: InitOptions;
11
- configureOptions?: ConfigureOptions;
12
- onWebMessage?: (message: WebViewTransportWebMessage) => void;
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 WidgetHostProps {
15
- onWebMessage?: (message: WebViewTransportWebMessage) => void;
16
- nativeMessages?: readonly WebViewTransportNativeMessage[];
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
- export interface GetUserFeedbackNativeContextValue {
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 declare function GetUserFeedbackProvider({ children, configureOptions, initOptions, onWebMessage, }: GetUserFeedbackProviderProps): ReactElement;
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 function WidgetHost(_props: WidgetHostProps): null;
91
+ export declare const useGetUserFeedback: typeof useGetUserFeedbackNative;