@getuserfeedback/react-native 5.15.2 → 5.15.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
@@ -45,6 +45,22 @@ export function App() {
45
45
 
46
46
  That is enough to receive flows published for automatic Widget delivery.
47
47
 
48
+ ## Native color scheme
49
+
50
+ For flows using the native renderer, set `clientOptions.colorScheme` to
51
+ `"light"`, `"dark"`, or `"system"`. Omitted values and browser auto-detect
52
+ objects follow the device appearance on native (light when unavailable).
53
+
54
+ Scheme-only prop changes and `client.configure({ colorScheme: "dark" })`
55
+ repaint the SDK's default native palette without restarting the flow or
56
+ clearing its draft. An unchanged prop does not override a later configure call.
57
+ The selection belongs to that client: runtime recreation retains it, while a
58
+ replacement client (for example, a new API key) starts from its initial prop.
59
+ Native repaint does not wait for Core: configure still forwards to Core and
60
+ retains its existing promise/error behavior, but remote failure does not roll
61
+ back native appearance. WebView flows keep their existing scheme forwarding
62
+ and browser auto-detection behavior. This does not add a style override API.
63
+
48
64
  - [React Native SDK guide](https://getuserfeedback.com/docs/get-started/react-native-sdk)
49
65
  - [React Native SDK reference](https://getuserfeedback.com/docs/reference/react-native-sdk-reference)
50
66
  - [Package versions](https://www.npmjs.com/package/@getuserfeedback/react-native?activeTab=versions)
@@ -0,0 +1,31 @@
1
+ export declare const NativeColorSchemeContext: import("react").Context<import("@getuserfeedback/protocol/host").ColorSchemeConfig | undefined>;
2
+ export declare function useNativeColorScheme(): {
3
+ scheme: "dark" | "light";
4
+ palette: {
5
+ background: string;
6
+ text: string;
7
+ muted: string;
8
+ input: string;
9
+ border: string;
10
+ placeholder: string;
11
+ button: string;
12
+ buttonText: string;
13
+ accent: string;
14
+ disabled: string;
15
+ disabledText: string;
16
+ error: string;
17
+ } | {
18
+ background: string;
19
+ text: string;
20
+ muted: string;
21
+ input: string;
22
+ border: string;
23
+ placeholder: string;
24
+ button: string;
25
+ buttonText: string;
26
+ accent: string;
27
+ disabled: string;
28
+ disabledText: string;
29
+ error: string;
30
+ };
31
+ };
@@ -0,0 +1,44 @@
1
+ import { createContext, useContext } from "react";
2
+ // Host-local presentation only; never the acknowledged Core configuration.
3
+ export const NativeColorSchemeContext = createContext(undefined);
4
+ const palettes = {
5
+ light: {
6
+ background: "#ffffff",
7
+ text: "#17202a",
8
+ muted: "#475569",
9
+ input: "#ffffff",
10
+ border: "#64748b",
11
+ placeholder: "#64748b",
12
+ button: "#e7eefb",
13
+ buttonText: "#17202a",
14
+ accent: "#2563eb",
15
+ disabled: "#e5e7eb",
16
+ disabledText: "#4b5563",
17
+ error: "#aa0000",
18
+ },
19
+ dark: {
20
+ background: "#111827",
21
+ text: "#f3f4f6",
22
+ muted: "#cbd5e1",
23
+ input: "#1f2937",
24
+ border: "#94a3b8",
25
+ placeholder: "#94a3b8",
26
+ button: "#1e3a5f",
27
+ buttonText: "#f8fafc",
28
+ accent: "#93c5fd",
29
+ disabled: "#374151",
30
+ disabledText: "#cbd5e1",
31
+ error: "#fca5a5",
32
+ },
33
+ };
34
+ export function useNativeColorScheme() {
35
+ const requested = useContext(NativeColorSchemeContext);
36
+ const device = require("react-native").useColorScheme();
37
+ // Browser auto-detection has no DOM on native; use the device preference.
38
+ const scheme = requested === "light" || requested === "dark"
39
+ ? requested
40
+ : device === "dark"
41
+ ? "dark"
42
+ : "light";
43
+ return { scheme, palette: palettes[scheme] };
44
+ }
@@ -1,4 +1,5 @@
1
1
  import { createElement, useLayoutEffect, useRef, useState, } from "react";
2
+ import { useNativeColorScheme } from "./native-color-scheme.js";
2
3
  /** Thin native editor: drafts only. Core owns validation, navigation and submission. */
3
4
  export function NativeFlowContent({ controller, presentationMode = "overlay", record, }) {
4
5
  const flow = record.nativeFlow;
@@ -7,6 +8,7 @@ export function NativeFlowContent({ controller, presentationMode = "overlay", re
7
8
  const { projection, interaction } = flow;
8
9
  const { View, Text, TextInput, Pressable, ScrollView, useWindowDimensions, } = require("react-native");
9
10
  const { height: windowHeight } = useWindowDimensions();
11
+ const { scheme, palette } = useNativeColorScheme();
10
12
  const [draft, setDraft] = useState(projection.answers);
11
13
  const rawDraft = useRef(draft);
12
14
  const scroll = useRef(null);
@@ -68,16 +70,20 @@ export function NativeFlowContent({ controller, presentationMode = "overlay", re
68
70
  onPress,
69
71
  style: {
70
72
  padding: 12,
71
- backgroundColor: buttonDisabled ? "#ddd" : "#e7eefb",
73
+ backgroundColor: buttonDisabled ? palette.disabled : palette.button,
72
74
  borderRadius: 6,
73
75
  marginTop: 8,
74
76
  },
75
- }, createElement(Text, { style: { color: "#17202a" } }, label));
77
+ }, createElement(Text, {
78
+ style: {
79
+ color: buttonDisabled ? palette.disabledText : palette.buttonText,
80
+ },
81
+ }, label));
76
82
  return createElement(ScrollView, {
77
83
  ref: scroll,
78
84
  style: Object.assign(Object.assign({}, (presentationMode === "content"
79
85
  ? { flexGrow: 0, flexShrink: 1, maxHeight: windowHeight }
80
- : { flex: 1, maxHeight: "100%" })), { width: "100%", backgroundColor: "#fff" }),
86
+ : { flex: 1, maxHeight: "100%" })), { width: "100%", backgroundColor: palette.background }),
81
87
  keyboardShouldPersistTaps: "handled",
82
88
  nestedScrollEnabled: presentationMode === "content",
83
89
  // Report the actual viewport, never the window ceiling or inner content.
@@ -87,29 +93,39 @@ export function NativeFlowContent({ controller, presentationMode = "overlay", re
87
93
  testID: "native-flow-renderer",
88
94
  style: { padding: 24, gap: 12 },
89
95
  }, projection.mode === "simulated"
90
- ? createElement(Text, { testID: "native-flow-simulated" }, "SIMULATED — no response is saved")
96
+ ? createElement(Text, {
97
+ testID: "native-flow-simulated",
98
+ style: { color: palette.muted },
99
+ }, "SIMULATED — no response is saved")
91
100
  : null, ...projection.blocks.map((block) => {
92
101
  var _a;
93
102
  if (block.kind === "statement")
94
- return createElement(View, { key: block.key }, ...block.paragraphs.map((text, index) => createElement(Text, { key: index }, text)));
103
+ return createElement(View, { key: block.key }, ...block.paragraphs.map((text, index) => createElement(Text, { key: index, style: { color: palette.text } }, text)));
95
104
  const value = draft[block.key];
96
105
  const requiredLabel = block.required ? ", required" : "";
97
106
  const selected = Array.isArray(value)
98
107
  ? ((_a = value[0]) !== null && _a !== void 0 ? _a : "")
99
108
  : (value !== null && value !== void 0 ? value : "");
100
- return createElement(View, { key: block.key, style: { gap: 6 } }, createElement(Text, {}, block.label + (block.required ? " *" : "")), block.description ? createElement(Text, {}, block.description) : null, block.kind === "text"
109
+ return createElement(View, { key: block.key, style: { gap: 6 } }, createElement(Text, { style: { color: palette.text } }, block.label + (block.required ? " *" : "")), block.description
110
+ ? createElement(Text, { style: { color: palette.muted } }, block.description)
111
+ : null, block.kind === "text"
101
112
  ? createElement(TextInput, {
102
113
  accessibilityLabel: block.label + requiredLabel,
103
114
  accessibilityHint: block.description,
104
115
  value: selected,
105
116
  placeholder: block.placeholder,
117
+ placeholderTextColor: palette.placeholder,
118
+ keyboardAppearance: scheme,
119
+ selectionColor: palette.accent,
120
+ cursorColor: palette.accent,
106
121
  editable: !disabled,
107
122
  maxLength: 4000,
108
123
  onChangeText: (text) => update(block.key, text),
109
124
  style: {
110
- color: "#17202a",
125
+ color: disabled ? palette.disabledText : palette.text,
126
+ backgroundColor: disabled ? palette.disabled : palette.input,
111
127
  borderWidth: 1,
112
- borderColor: "#999",
128
+ borderColor: palette.border,
113
129
  borderRadius: 4,
114
130
  padding: 10,
115
131
  },
@@ -132,12 +148,20 @@ export function NativeFlowContent({ controller, presentationMode = "overlay", re
132
148
  disabled,
133
149
  onPress: () => update(block.key, option.value),
134
150
  style: { padding: 10 },
135
- }, createElement(Text, {}, `${selected === option.value ? "●" : "○"} ${option.label}`)))), projection.errors[block.key]
136
- ? createElement(Text, { accessibilityRole: "alert", style: { color: "#a00" } }, projection.errors[block.key])
151
+ }, createElement(Text, {
152
+ style: {
153
+ color: disabled ? palette.disabledText : palette.text,
154
+ },
155
+ }, `${selected === option.value ? "●" : "○"} ${option.label}`)))), projection.errors[block.key]
156
+ ? createElement(Text, { accessibilityRole: "alert", style: { color: palette.error } }, projection.errors[block.key])
137
157
  : null);
138
- }), ...projection.formErrors.map((text, index) => createElement(Text, { key: `error-${index}`, accessibilityRole: "alert" }, text)), ...projection.terminal.map((text, index) => createElement(Text, { key: `terminal-${index}` }, text)), projection.status === "succeeded" &&
158
+ }), ...projection.formErrors.map((text, index) => createElement(Text, {
159
+ key: `error-${index}`,
160
+ accessibilityRole: "alert",
161
+ style: { color: palette.error },
162
+ }, text)), ...projection.terminal.map((text, index) => createElement(Text, { key: `terminal-${index}`, style: { color: palette.text } }, text)), projection.status === "succeeded" &&
139
163
  (projection.mode === "simulated" || projection.terminal.length === 0)
140
- ? createElement(Text, { testID: "native-flow-complete" }, projection.mode === "simulated"
164
+ ? createElement(Text, { testID: "native-flow-complete", style: { color: palette.text } }, projection.mode === "simulated"
141
165
  ? "SIMULATED completion — nothing saved"
142
166
  : "Thank you")
143
167
  : null, projection.canGoBack ? button("Back", () => interact("back")) : null, interaction === "draft-failed"
@@ -155,6 +179,10 @@ export function NativeFlowContent({ controller, presentationMode = "overlay", re
155
179
  disabled: !record.isActive,
156
180
  onPress: () => controller.closeNativeFlow(record),
157
181
  style: { padding: 12 },
158
- }, createElement(Text, {}, "Close"))
182
+ }, createElement(Text, {
183
+ style: {
184
+ color: !record.isActive ? palette.disabledText : palette.text,
185
+ },
186
+ }, "Close"))
159
187
  : null));
160
188
  }
@@ -4,10 +4,13 @@ import { type OpenRequestDispatcher } from "@getuserfeedback/sdk/internal/open-r
4
4
  export declare function createNativeProviderClientController(input: {
5
5
  instanceId: string;
6
6
  onError?: (error: Error) => void;
7
+ initialColorScheme?: ConfigureOptions["colorScheme"];
7
8
  }): {
8
9
  client: import("./client.js").Client;
9
10
  directSessionController: import("./native-direct-command-session-controller.js").NativeDirectCommandSessionController;
10
11
  flowRegistry: import("./native-provider-flow-registry.js").NativeProviderFlowRegistry;
12
+ getNativeColorScheme: () => import("@getuserfeedback/protocol/host").ColorSchemeConfig | undefined;
13
+ subscribeNativeColorScheme: (listener: () => void) => () => void;
11
14
  getRuntimeConfiguration: () => ConfigureOptions;
12
15
  observeOpenRequest: (payload: Parameters<OpenRequestDispatcher["dispatch"]>[0]) => ReturnType<OpenRequestDispatcher["dispatch"]>;
13
16
  dispose: () => void;
@@ -4,6 +4,9 @@ import { createNativeProviderClient, parseRequiredString, } from "./native-provi
4
4
  import { createNativeProviderCommandController } from "./native-provider-command-controller.js";
5
5
  import { createNativeProviderFlowRegistry } from "./native-provider-flow-registry.js";
6
6
  export function createNativeProviderClientController(input) {
7
+ // Presentation belongs to this client, independently of Core acknowledgement.
8
+ let nativeColorScheme = input.initialColorScheme;
9
+ const colorSchemeListeners = new Set();
7
10
  let runtimeConfiguration = {};
8
11
  const updateRuntimeConfiguration = (updates) => {
9
12
  runtimeConfiguration = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, runtimeConfiguration), (updates.auth === undefined ? {} : { auth: updates.auth })), (updates.capabilities === undefined
@@ -44,7 +47,15 @@ export function createNativeProviderClientController(input) {
44
47
  catch (error) {
45
48
  return commands.reject(error);
46
49
  }
47
- const settlement = await commands.runOperation((dispatch) => dispatch(canonicalCommand), options);
50
+ const settlement = await commands.runOperation((dispatch) => {
51
+ if (canonicalCommand.kind === "configure" &&
52
+ canonicalCommand.opts.colorScheme !== undefined) {
53
+ nativeColorScheme = canonicalCommand.opts.colorScheme;
54
+ for (const listener of colorSchemeListeners)
55
+ listener();
56
+ }
57
+ return dispatch(canonicalCommand);
58
+ }, options);
48
59
  if (canonicalCommand.kind === "configure" && settlement !== undefined) {
49
60
  updateRuntimeConfiguration(canonicalCommand.opts);
50
61
  }
@@ -57,9 +68,17 @@ export function createNativeProviderClientController(input) {
57
68
  client,
58
69
  directSessionController: session,
59
70
  flowRegistry: flows,
71
+ getNativeColorScheme: () => nativeColorScheme,
72
+ subscribeNativeColorScheme: (listener) => {
73
+ colorSchemeListeners.add(listener);
74
+ return () => {
75
+ colorSchemeListeners.delete(listener);
76
+ };
77
+ },
60
78
  getRuntimeConfiguration: () => (Object.assign({}, runtimeConfiguration)),
61
79
  observeOpenRequest,
62
80
  dispose: () => {
81
+ colorSchemeListeners.clear();
63
82
  flows.reset();
64
83
  commands.dispose();
65
84
  },
@@ -9,12 +9,13 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { parseInitOptions } from "@getuserfeedback/protocol";
12
+ import { DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS, parseInitOptions, } from "@getuserfeedback/protocol";
13
13
  import { createUniqueId } from "@getuserfeedback/protocol/host";
14
14
  import { createActionRegistry, } from "@getuserfeedback/sdk/internal/action-registry";
15
15
  import { resolveLinkRouter } from "@getuserfeedback/sdk/internal/link-router";
16
- import { createContext, createElement, Fragment, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
16
+ import { createContext, createElement, Fragment, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
17
17
  import { NativeStandardFlowContainer } from "./flow-container.js";
18
+ import { NativeColorSchemeContext } from "./native-color-scheme.js";
18
19
  import { NativeFlowContainerContext, } from "./native-flow-container-context.js";
19
20
  import { createNativeProviderClientController } from "./native-provider-client-controller.js";
20
21
  import { REACT_NATIVE_PINNED_CORE_URL } from "./native-runtime-endpoints.js";
@@ -123,7 +124,9 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
123
124
  const actionRegistry = useNativeActionRegistry(clientOptions.actions);
124
125
  const linkRouter = useNativeLinkRouter(clientOptions.links);
125
126
  const normalizedClientOptions = parseNativeInitOptions(clientOptions, actionRegistry.definitions);
126
- const clientOptionsKey = toClientOptionsKey(normalizedClientOptions);
127
+ const { colorScheme } = normalizedClientOptions, runtimeIdentityOptions = __rest(normalizedClientOptions, ["colorScheme"]);
128
+ const clientOptionsKey = toClientOptionsKey(runtimeIdentityOptions);
129
+ const colorSchemePropKey = toClientOptionsKey(colorScheme);
127
130
  const coreUrl = (_b = (_a = normalizedClientOptions.runtimeEndpoints) === null || _a === void 0 ? void 0 : _a.coreUrl) !== null && _b !== void 0 ? _b : REACT_NATIVE_PINNED_CORE_URL;
128
131
  const onErrorRef = useRef(onError);
129
132
  onErrorRef.current = onError;
@@ -140,14 +143,40 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
140
143
  apiKey: normalizedClientOptions.apiKey,
141
144
  hostCapabilities: normalizedClientOptions.hostCapabilities,
142
145
  });
143
- const controllerOwner = useMemo(() => ({
146
+ const createControllerOwner = () => ({
144
147
  identityKey: clientIdentityKey,
148
+ colorSchemePropKey,
145
149
  controller: createNativeProviderClientController({
146
150
  instanceId,
147
151
  onError: reportError,
152
+ initialColorScheme: colorScheme,
148
153
  }),
149
- }), [clientIdentityKey, instanceId, reportError]);
154
+ });
155
+ let [controllerOwner, setControllerOwner] = useState(createControllerOwner);
156
+ // Initialize the whole owner together, before committing a replacement client.
157
+ // Scheme-only prop changes update that owner without replacing it.
158
+ if (controllerOwner.identityKey !== clientIdentityKey) {
159
+ controllerOwner = createControllerOwner();
160
+ setControllerOwner(controllerOwner);
161
+ }
150
162
  const controller = controllerOwner.controller;
163
+ const hostColorScheme = useSyncExternalStore(controller.subscribeNativeColorScheme, controller.getNativeColorScheme, controller.getNativeColorScheme);
164
+ useLayoutEffect(() => {
165
+ if (controllerOwner.colorSchemePropKey === colorSchemePropKey)
166
+ return;
167
+ controllerOwner.colorSchemePropKey = colorSchemePropKey;
168
+ // Prop changes share configure's forwarding path, not runtime identity.
169
+ // Unchanged props must not undo a later imperative configure.
170
+ void controller.client
171
+ .configure({
172
+ colorScheme: colorScheme !== null && colorScheme !== void 0 ? colorScheme : {
173
+ autoDetectColorScheme: [...DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS],
174
+ },
175
+ })
176
+ .catch(() => {
177
+ // The controller reports remote failures; local appearance is retained.
178
+ });
179
+ }, [colorScheme, colorSchemePropKey, controller, controllerOwner]);
151
180
  const attachViewProjection = useCallback((nextProjection) => {
152
181
  setProjection(nextProjection);
153
182
  return () => setProjection((current) => current === nextProjection ? null : current);
@@ -199,7 +228,7 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
199
228
  }, [controller]);
200
229
  const getRuntimeConfiguration = useCallback(() => controller.getRuntimeConfiguration(), [controller]);
201
230
  const handleHandleInvalidated = useCallback((message) => controller.onHandleInvalidated(message), [controller]);
202
- return createElement(GetUserFeedbackContext.Provider, { value: controller.client }, createElement(NativeFlowContainerContext.Provider, { value: flowContainerContext }, createElement(Fragment, null, children, createElement(NativeStandardFlowContainer), createElement(NativeProviderRuntime, {
231
+ return createElement(NativeColorSchemeContext.Provider, { value: hostColorScheme }, createElement(GetUserFeedbackContext.Provider, { value: controller.client }, createElement(NativeFlowContainerContext.Provider, { value: flowContainerContext }, createElement(Fragment, null, children, createElement(NativeStandardFlowContainer), createElement(NativeProviderRuntime, {
203
232
  attachViewProjection,
204
233
  coreUrl,
205
234
  coreWebViewComponent: webViewComponent,
@@ -215,7 +244,7 @@ export function GetUserFeedbackProvider({ children, clientOptions, instanceId: i
215
244
  onHandleInvalidated: handleHandleInvalidated,
216
245
  observeOpenRequest: controller.observeOpenRequest,
217
246
  viewWebViewComponent: webViewComponent,
218
- }))));
247
+ })))));
219
248
  }
220
249
  /**
221
250
  * Returns the React Native SDK client from the nearest provider.
@@ -40,7 +40,9 @@ export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewCo
40
40
  const onStartupTerminalErrorRef = useRef(onStartupTerminalError);
41
41
  onStartupTerminalErrorRef.current = onStartupTerminalError;
42
42
  const createStartupCommandRef = useRef(createStartupCommand);
43
- createStartupCommandRef.current = createStartupCommand;
43
+ useLayoutEffect(() => {
44
+ createStartupCommandRef.current = createStartupCommand;
45
+ }, [createStartupCommand]);
44
46
  const hostActionsRef = useRef(hostActions);
45
47
  hostActionsRef.current = hostActions;
46
48
  const linkRouterRef = useRef(linkRouter);
package/dist/version.js CHANGED
@@ -3,4 +3,4 @@ const reactNativeSdkVersion = typeof __GX_REACT_NATIVE_SDK_VERSION__ === "string
3
3
  : "";
4
4
  // Build scripts patch this fallback to the package version for published artifacts.
5
5
  // Source-linked workspace usage keeps the local fallback.
6
- export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "5.15.2";
6
+ export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "5.15.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getuserfeedback/react-native",
3
- "version": "5.15.2",
3
+ "version": "5.15.3",
4
4
  "description": "getuserfeedback React Native SDK",
5
5
  "keywords": [
6
6
  "getuserfeedback",