@getuserfeedback/react-native 3.0.81 → 3.0.91

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/index.js CHANGED
@@ -9,10 +9,11 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { assertNoSegmentExternalIdsInValueArgument, CommandSettlementError, CommandSettlementTimeoutError, createFlowCommandHandleController, getFlowHandleFromSettlementDetail, getWidgetLayoutPlanNowMs, } from "@getuserfeedback/protocol/host";
12
+ import { CommandSettlementError, CommandSettlementTimeoutError, createFlowCommandHandleController, getFlowHandleFromSettlementDetail, getWidgetLayoutPlanNowMs, } from "@getuserfeedback/protocol/host";
13
13
  import { parseWebViewTransportNativeMessage } from "@getuserfeedback/protocol/webview-transport";
14
14
  import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
15
15
  import { useResolvedHostViewport } from "./host-viewport.js";
16
+ import { createNativeProviderClient, parseRequiredString, } from "./native-provider-client.js";
16
17
  import { createProviderCommandEnvelope, REACT_NATIVE_LOADER_CLIENT_META, toCommandSettlementError, toReactNativeOneShotCommand, } from "./provider-command-helpers.js";
17
18
  import { useProviderCommandTransport } from "./provider-command-transport.js";
18
19
  import { createProviderWidgetHostLifecycleController, HIDDEN_PROVIDER_WIDGET_HOST_VISIBILITY, } from "./provider-widget-host-lifecycle.js";
@@ -136,13 +137,6 @@ const normalizeOptionalString = (value) => {
136
137
  const normalized = value.trim();
137
138
  return normalized.length > 0 ? normalized : undefined;
138
139
  };
139
- const resolveRequiredString = (name, value) => {
140
- const normalized = normalizeOptionalString(value);
141
- if (normalized === undefined) {
142
- throw new Error(`${name} must be a non-empty string`);
143
- }
144
- return normalized;
145
- };
146
140
  const toJsonKey = (value) => JSON.stringify(value);
147
141
  const toStableHash = (value) => {
148
142
  const json = toJsonKey(value);
@@ -171,33 +165,6 @@ function resolveReactNativeComponents() {
171
165
  };
172
166
  }
173
167
  }
174
- const toIdentifyCommandPayload = (identifyInput, traitsOrOptions, options) => {
175
- if (typeof identifyInput === "string") {
176
- assertNoSegmentExternalIdsInValueArgument({
177
- argument: traitsOrOptions,
178
- commandName: "identify",
179
- valueArgumentName: "traits",
180
- callShape: "identify(userId, traits, { externalIds })",
181
- });
182
- return {
183
- kind: "identify",
184
- userId: resolveRequiredString("userId", identifyInput),
185
- traits: traitsOrOptions,
186
- options,
187
- };
188
- }
189
- assertNoSegmentExternalIdsInValueArgument({
190
- argument: identifyInput,
191
- commandName: "identify",
192
- valueArgumentName: "traits",
193
- callShape: "identify(traits, { externalIds })",
194
- });
195
- return {
196
- kind: "identify",
197
- traits: identifyInput,
198
- options: options !== null && options !== void 0 ? options : traitsOrOptions,
199
- };
200
- };
201
168
  export function GetUserFeedbackProvider({ autoInit = true, children, commandTimeoutMs = DEFAULT_COMMAND_TIMEOUT_MS, configureOptions, hostViewport, instanceId: instanceIdProp, initOptions, loaderUrl, onCommandError, onInvalidWebMessage, onWebMessage, source, webViewComponent, webViewProps, }) {
202
169
  var _a;
203
170
  const { View: NativeView } = useMemo(resolveReactNativeComponents, []);
@@ -383,7 +350,7 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
383
350
  reportCommandError,
384
351
  };
385
352
  const flow = useCallback((flowId) => {
386
- const normalizedFlowId = resolveRequiredString("flowId", flowId);
353
+ const normalizedFlowId = parseRequiredString("flowId", flowId);
387
354
  const existing = flowRunsRef.current.get(normalizedFlowId);
388
355
  if (existing) {
389
356
  return existing;
@@ -541,59 +508,32 @@ export function GetUserFeedbackProvider({ autoInit = true, children, commandTime
541
508
  disposeCommandTransport(new Error(PROVIDER_UNMOUNTED_ERROR_MESSAGE));
542
509
  }, [disposeCommandTransport]);
543
510
  const getNativeMessages = useCallback(() => nativeMessagesRef.current, [nativeMessagesRef]);
544
- const contextValue = useMemo(() => ({
545
- instanceId,
546
- get nativeMessages() {
547
- return getNativeMessages();
548
- },
549
- enqueueCommand,
550
- configure: (opts, options) => enqueueCommand({ kind: "configure", opts }, options),
551
- identify: ((identifyInput, traitsOrOptions, options) => enqueueCommand(toIdentifyCommandPayload(identifyInput, traitsOrOptions, options))),
552
- track: (eventName, properties, options) => {
553
- assertNoSegmentExternalIdsInValueArgument({
554
- argument: properties,
555
- commandName: "track",
556
- valueArgumentName: "properties",
557
- callShape: "track(eventName, properties, { externalIds })",
558
- });
559
- return enqueueCommand(Object.assign(Object.assign({ kind: "track", origin: "customer", event: resolveRequiredString("eventName", eventName) }, (properties === undefined ? {} : { properties })), ((options === null || options === void 0 ? void 0 : options.externalIds) === undefined
560
- ? {}
561
- : { context: { externalIds: options.externalIds } })));
562
- },
563
- open: (flowId, options, commandOptions) => {
564
- const normalizedFlowId = resolveRequiredString("flowId", flowId);
565
- return enqueueCommand({
566
- kind: "open",
567
- flowId: normalizedFlowId,
568
- hideCloseButton: options === null || options === void 0 ? void 0 : options.hideCloseButton,
569
- }, commandOptions);
570
- },
571
- prefetch: (flowId, options) => {
572
- const normalizedFlowId = resolveRequiredString("flowId", flowId);
573
- return enqueueCommand({
574
- kind: "prefetch",
575
- flowId: normalizedFlowId,
576
- }, options);
577
- },
578
- prerender: (flowId, options, commandOptions) => {
579
- const normalizedFlowId = resolveRequiredString("flowId", flowId);
580
- return enqueueCommand({
581
- kind: "prerender",
582
- flowId: normalizedFlowId,
583
- hideCloseButton: options === null || options === void 0 ? void 0 : options.hideCloseButton,
584
- }, commandOptions);
585
- },
586
- flow,
587
- close: (flowHandleId, options) => enqueueCommand(Object.assign({ kind: "close" }, (flowHandleId === undefined
588
- ? {}
589
- : {
590
- flowHandleId: resolveRequiredString("flowHandleId", flowHandleId),
591
- })), options),
592
- reset: (options) => enqueueCommand({ kind: "reset" }, options),
593
- updateHostContext: (context, options) => enqueueCommand({ kind: "updateHostContext", context }, options),
594
- emitHostSignal: (name, data, options) => enqueueCommand(Object.assign({ kind: "emitHostSignal", name: resolveRequiredString("name", name) }, (data === undefined ? {} : { data })), options),
595
- handleWebMessage,
596
- }), [enqueueCommand, flow, getNativeMessages, handleWebMessage, instanceId]);
511
+ const contextValue = useMemo(() => {
512
+ const client = createNativeProviderClient({
513
+ dispatchCommand: enqueueCommand,
514
+ flow,
515
+ instanceId,
516
+ });
517
+ return {
518
+ close: client.close,
519
+ configure: client.configure,
520
+ emitHostSignal: client.emitHostSignal,
521
+ enqueueCommand,
522
+ flow: client.flow,
523
+ handleWebMessage,
524
+ identify: client.identify,
525
+ instanceId: client.instanceId,
526
+ get nativeMessages() {
527
+ return getNativeMessages();
528
+ },
529
+ open: client.open,
530
+ prefetch: client.prefetch,
531
+ prerender: client.prerender,
532
+ reset: client.reset,
533
+ track: client.track,
534
+ updateHostContext: client.updateHostContext,
535
+ };
536
+ }, [enqueueCommand, flow, getNativeMessages, handleWebMessage, instanceId]);
597
537
  const previousSheetHostLayoutUpdateRef = useRef(undefined);
598
538
  const consumedTransitionPolicySequenceRef = useRef(undefined);
599
539
  useEffect(() => {
@@ -1,7 +1,7 @@
1
1
  import { getFlowHandleFromSettlementDetail } from "@getuserfeedback/protocol/host";
2
2
  import { toCoreCommandPayload, } from "@getuserfeedback/protocol/internal/public-core-command";
3
- // TODO(migration): Route the public provider client through this core-ordered direct session.
4
- // [from=native-core-ordered-command-session] [to=native-provider-direct-session] [scope=slice] [principle=core-owned-flow-lifecycle] [priority=high] [impact=high] [risk=high] [epic=runtime-neutral-view-host-orchestration] [epic_order=180]
3
+ // TODO(migration): Route the public provider client through this core-ordered direct session after native per-view presentation is complete.
4
+ // [from=native-view-presentation-behavior] [to=native-provider-direct-session] [scope=slice] [principle=core-owned-flow-lifecycle] [priority=high] [impact=high] [risk=high] [epic=runtime-neutral-view-host-orchestration] [epic_order=190]
5
5
  export function createNativeDirectCommandSession(input) {
6
6
  const dispatchToCore = (envelope, command) => {
7
7
  const coreEnvelope = Object.assign(Object.assign({}, envelope), { command: toCoreCommandPayload(command) });
@@ -0,0 +1,64 @@
1
+ import type { AppEventExternalId, AppEventJsonValue, PublicCommandPayload } from "@getuserfeedback/protocol";
2
+ import type { ConfigureOptions } 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 NativeProviderClientCommand = Exclude<Extract<PublicCommandPayload, {
12
+ kind: "configure" | "identify" | "track" | "open" | "prefetch" | "prerender" | "close" | "reset" | "updateHostContext" | "emitHostSignal";
13
+ }>, OpenCommand> | (Omit<OpenCommand, "container"> & {
14
+ container?: never;
15
+ });
16
+ type IdentifyOptions = {
17
+ externalIds?: AppEventExternalId[];
18
+ };
19
+ type TrackOptions = {
20
+ externalIds?: AppEventExternalId[];
21
+ };
22
+ type OpenOptions = Pick<OpenCommand, "hideCloseButton">;
23
+ type PrerenderOptions = Pick<PrerenderCommand, "hideCloseButton">;
24
+ type OpenFlowOptions = Pick<OpenCommand, "hideCloseButton" | "metadata">;
25
+ type PrerenderFlowOptions = Pick<PrerenderCommand, "hideCloseButton">;
26
+ type HostContext = Extract<PublicCommandPayload, {
27
+ kind: "updateHostContext";
28
+ }>["context"];
29
+ interface NativeProviderCommandOptions {
30
+ idempotencyKey?: string;
31
+ }
32
+ export interface ReactNativeFlowRun {
33
+ readonly flowId: string;
34
+ open: (options?: OpenFlowOptions) => Promise<void>;
35
+ prefetch: () => Promise<void>;
36
+ prerender: (options?: PrerenderFlowOptions) => Promise<void>;
37
+ close: () => Promise<void>;
38
+ }
39
+ export interface NativeProviderClient<TResult> {
40
+ readonly instanceId: string;
41
+ configure: (opts: ConfigureOptions, options?: NativeProviderCommandOptions) => Promise<TResult>;
42
+ identify: {
43
+ (userId: string, traits?: IdentifyTraits, options?: IdentifyOptions): Promise<TResult>;
44
+ (traits: IdentifyTraits, placeholder: undefined, options?: IdentifyOptions): Promise<TResult>;
45
+ (traits: IdentifyTraits, options?: IdentifyOptions): Promise<TResult>;
46
+ };
47
+ track: (eventName: string, properties?: TrackProperties, options?: TrackOptions) => Promise<TResult>;
48
+ open: (flowId: string, options?: OpenOptions, commandOptions?: NativeProviderCommandOptions) => Promise<TResult>;
49
+ prefetch: (flowId: string, options?: NativeProviderCommandOptions) => Promise<TResult>;
50
+ prerender: (flowId: string, options?: PrerenderOptions, commandOptions?: NativeProviderCommandOptions) => Promise<TResult>;
51
+ flow: (flowId: string) => ReactNativeFlowRun;
52
+ close: (flowHandleId?: string, options?: NativeProviderCommandOptions) => Promise<TResult>;
53
+ reset: (options?: NativeProviderCommandOptions) => Promise<TResult>;
54
+ updateHostContext: (context: HostContext, options?: NativeProviderCommandOptions) => Promise<TResult>;
55
+ emitHostSignal: (name: string, data?: unknown, options?: NativeProviderCommandOptions) => Promise<TResult>;
56
+ }
57
+ type NativeProviderClientOptions<TResult> = {
58
+ dispatchCommand: (command: NativeProviderClientCommand, options?: NativeProviderCommandOptions) => Promise<TResult>;
59
+ flow: (flowId: string) => ReactNativeFlowRun;
60
+ instanceId: string;
61
+ };
62
+ export declare const parseRequiredString: (name: string, value?: string) => string;
63
+ export declare function createNativeProviderClient<TResult>({ dispatchCommand, flow, instanceId, }: NativeProviderClientOptions<TResult>): NativeProviderClient<TResult>;
64
+ export {};
@@ -0,0 +1,76 @@
1
+ import { assertNoSegmentExternalIdsInValueArgument } from "@getuserfeedback/protocol/host";
2
+ export const parseRequiredString = (name, value) => {
3
+ const normalized = value === null || value === void 0 ? void 0 : value.trim();
4
+ if (!normalized) {
5
+ throw new Error(`${name} must be a non-empty string`);
6
+ }
7
+ return normalized;
8
+ };
9
+ const toIdentifyCommandPayload = (identifyInput, traitsOrOptions, options) => {
10
+ if (typeof identifyInput === "string") {
11
+ assertNoSegmentExternalIdsInValueArgument({
12
+ argument: traitsOrOptions,
13
+ commandName: "identify",
14
+ valueArgumentName: "traits",
15
+ callShape: "identify(userId, traits, { externalIds })",
16
+ });
17
+ return {
18
+ kind: "identify",
19
+ userId: parseRequiredString("userId", identifyInput),
20
+ traits: traitsOrOptions,
21
+ options,
22
+ };
23
+ }
24
+ assertNoSegmentExternalIdsInValueArgument({
25
+ argument: identifyInput,
26
+ commandName: "identify",
27
+ valueArgumentName: "traits",
28
+ callShape: "identify(traits, { externalIds })",
29
+ });
30
+ return {
31
+ kind: "identify",
32
+ traits: identifyInput,
33
+ options: options !== null && options !== void 0 ? options : traitsOrOptions,
34
+ };
35
+ };
36
+ export function createNativeProviderClient({ dispatchCommand, flow, instanceId, }) {
37
+ return {
38
+ instanceId,
39
+ configure: (opts, options) => dispatchCommand({ kind: "configure", opts }, options),
40
+ identify: ((identifyInput, traitsOrOptions, options) => dispatchCommand(toIdentifyCommandPayload(identifyInput, traitsOrOptions, options))),
41
+ track: (eventName, properties, options) => {
42
+ assertNoSegmentExternalIdsInValueArgument({
43
+ argument: properties,
44
+ commandName: "track",
45
+ valueArgumentName: "properties",
46
+ callShape: "track(eventName, properties, { externalIds })",
47
+ });
48
+ return dispatchCommand(Object.assign(Object.assign({ kind: "track", origin: "customer", event: parseRequiredString("eventName", eventName) }, (properties === undefined ? {} : { properties })), ((options === null || options === void 0 ? void 0 : options.externalIds) === undefined
49
+ ? {}
50
+ : { context: { externalIds: options.externalIds } })));
51
+ },
52
+ open: (flowId, options, commandOptions) => dispatchCommand({
53
+ kind: "open",
54
+ flowId: parseRequiredString("flowId", flowId),
55
+ hideCloseButton: options === null || options === void 0 ? void 0 : options.hideCloseButton,
56
+ }, commandOptions),
57
+ prefetch: (flowId, options) => dispatchCommand({
58
+ kind: "prefetch",
59
+ flowId: parseRequiredString("flowId", flowId),
60
+ }, options),
61
+ prerender: (flowId, options, commandOptions) => dispatchCommand({
62
+ kind: "prerender",
63
+ flowId: parseRequiredString("flowId", flowId),
64
+ hideCloseButton: options === null || options === void 0 ? void 0 : options.hideCloseButton,
65
+ }, commandOptions),
66
+ flow,
67
+ close: (flowHandleId, options) => dispatchCommand(Object.assign({ kind: "close" }, (flowHandleId === undefined
68
+ ? {}
69
+ : {
70
+ flowHandleId: parseRequiredString("flowHandleId", flowHandleId),
71
+ })), options),
72
+ reset: (options) => dispatchCommand({ kind: "reset" }, options),
73
+ updateHostContext: (context, options) => dispatchCommand({ kind: "updateHostContext", context }, options),
74
+ emitHostSignal: (name, data, options) => dispatchCommand(Object.assign({ kind: "emitHostSignal", name: parseRequiredString("name", name) }, (data === undefined ? {} : { data })), options),
75
+ };
76
+ }
@@ -15,6 +15,7 @@ export type NativeRuntimeRootProps = {
15
15
  onInvalidMessage?: (error: Error, value: unknown) => void;
16
16
  onStartupTerminalError?: (error: Error) => void;
17
17
  startupCommandSettlementTimeoutMs?: number;
18
+ viewNativeViewComponent?: NativeViewHostProjectionProps["nativeViewComponent"];
18
19
  viewWebViewComponent?: NativeViewHostProjectionProps["webViewComponent"];
19
20
  viewWebViewProps?: NativeViewHostProjectionProps["webViewProps"];
20
21
  };
@@ -35,6 +36,6 @@ type NativeProviderRuntimeProps = Omit<NativeRuntimeRootProps, "onCommandSession
35
36
  instanceId: string;
36
37
  onCommandSessionChange?: (session: NativeDirectCommandSession | null) => void;
37
38
  };
38
- export declare function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, coreWebViewProps, onCommandSessionChange, onError, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs, startupCommands, viewWebViewComponent, viewWebViewProps, }: NativeRuntimeRootProps): ReactElement;
39
+ export declare function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, coreWebViewProps, onCommandSessionChange, onError, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs, startupCommands, viewNativeViewComponent, viewWebViewComponent, viewWebViewProps, }: NativeRuntimeRootProps): ReactElement;
39
40
  export declare function NativeProviderRuntime({ initOptions, initialConfigureOptions, instanceId, onCommandSessionChange, onError, ...runtimeProps }: NativeProviderRuntimeProps): ReactElement;
40
41
  export {};
@@ -43,7 +43,7 @@ async function dispatchStartupCommands(input) {
43
43
  });
44
44
  }
45
45
  }
46
- export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, coreWebViewProps, onCommandSessionChange, onError, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs = DEFAULT_STARTUP_COMMAND_SETTLEMENT_TIMEOUT_MS, startupCommands, viewWebViewComponent, viewWebViewProps, }) {
46
+ export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, coreWebViewProps, onCommandSessionChange, onError, onInvalidMessage, onStartupTerminalError, startupCommandSettlementTimeoutMs = DEFAULT_STARTUP_COMMAND_SETTLEMENT_TIMEOUT_MS, startupCommands, viewNativeViewComponent, viewWebViewComponent, viewWebViewProps, }) {
47
47
  const [coreHostRevision, setCoreHostRevision] = useState(0);
48
48
  const [session, setSession] = useState(null);
49
49
  const sessionRef = useRef(null);
@@ -213,6 +213,7 @@ export function NativeRuntimeRoot({ coreUrl, coreWebViewComponent, coreWebViewPr
213
213
  : createElement(NativeViewHostProjection, {
214
214
  controller: session.controller,
215
215
  key: session.key,
216
+ nativeViewComponent: viewNativeViewComponent,
216
217
  onInvalidMessage,
217
218
  webViewComponent: viewWebViewComponent,
218
219
  webViewProps: viewWebViewProps,
@@ -3,7 +3,8 @@ import type { NativeViewRecordController } from "./native-view-record-controller
3
3
  export type NativeViewHostProjectionProps = {
4
4
  controller: NativeViewRecordController;
5
5
  onInvalidMessage?: (error: Error, value: unknown) => void;
6
+ nativeViewComponent?: ComponentType<Record<string, unknown>>;
6
7
  webViewComponent?: ComponentType<Record<string, unknown>>;
7
8
  webViewProps?: Record<string, unknown>;
8
9
  };
9
- export declare function NativeViewHostProjection({ controller, onInvalidMessage, webViewComponent, webViewProps, }: NativeViewHostProjectionProps): ReactElement;
10
+ export declare function NativeViewHostProjection({ controller, nativeViewComponent, onInvalidMessage, webViewComponent, webViewProps, }: NativeViewHostProjectionProps): ReactElement;
@@ -1,6 +1,7 @@
1
- import { createElement, Fragment, useCallback, useEffect, useRef, useSyncExternalStore, } from "react";
1
+ import { createElement, Fragment, useCallback, useEffect, useMemo, useRef, useSyncExternalStore, } from "react";
2
+ import { NativeViewPresentationSurface } from "./native-view-presentation-surface.js";
2
3
  import { ViewWebViewHost } from "./view-webview-host.js";
3
- function NativeViewHostProjectionEntry({ controller, onInvalidMessage, record, webViewComponent, webViewProps, }) {
4
+ function NativeViewHostProjectionEntry({ controller, nativeViewComponent, onInvalidMessage, record, webViewComponent, webViewProps, }) {
4
5
  const detachConnectionRef = useRef(null);
5
6
  const detachConnection = useCallback(() => {
6
7
  const detach = detachConnectionRef.current;
@@ -8,15 +9,17 @@ function NativeViewHostProjectionEntry({ controller, onInvalidMessage, record, w
8
9
  detach === null || detach === void 0 ? void 0 : detach();
9
10
  }, []);
10
11
  const handleConnected = useCallback((connection) => {
11
- detachConnection();
12
12
  const controllerDetach = controller.attachViewReadinessConnection({
13
13
  allocationId: record.allocationId,
14
14
  connection,
15
15
  });
16
- detachConnectionRef.current = controllerDetach;
17
- }, [controller, detachConnection, record.allocationId]);
16
+ if (controllerDetach) {
17
+ detachConnectionRef.current = controllerDetach;
18
+ }
19
+ }, [controller, record.allocationId]);
20
+ const presentedWebViewProps = useMemo(() => (Object.assign(Object.assign({}, webViewProps), { style: [webViewProps === null || webViewProps === void 0 ? void 0 : webViewProps.style, { flex: 1 }] })), [webViewProps]);
18
21
  useEffect(() => detachConnection, [detachConnection]);
19
- return createElement(ViewWebViewHost, {
22
+ return createElement(NativeViewPresentationSurface, { nativeViewComponent, record }, createElement(ViewWebViewHost, {
20
23
  generation: record.generation,
21
24
  onConnected: handleConnected,
22
25
  onDisconnected: detachConnection,
@@ -24,13 +27,14 @@ function NativeViewHostProjectionEntry({ controller, onInvalidMessage, record, w
24
27
  srcdoc: record.srcdoc,
25
28
  viewId: record.viewId,
26
29
  webViewComponent,
27
- webViewProps,
28
- });
30
+ webViewProps: presentedWebViewProps,
31
+ }));
29
32
  }
30
- export function NativeViewHostProjection({ controller, onInvalidMessage, webViewComponent, webViewProps, }) {
33
+ export function NativeViewHostProjection({ controller, nativeViewComponent, onInvalidMessage, webViewComponent, webViewProps, }) {
31
34
  const records = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
32
35
  return createElement(Fragment, null, records.map((record) => createElement(NativeViewHostProjectionEntry, {
33
36
  controller,
37
+ nativeViewComponent,
34
38
  onInvalidMessage,
35
39
  record,
36
40
  webViewComponent,
@@ -0,0 +1,7 @@
1
+ import { type ComponentType, type ReactElement, type ReactNode } from "react";
2
+ import type { NativeViewRecord } from "./native-view-record-controller.js";
3
+ export declare function NativeViewPresentationSurface({ children, nativeViewComponent, record, }: {
4
+ children?: ReactNode;
5
+ nativeViewComponent?: ComponentType<Record<string, unknown>>;
6
+ record: NativeViewRecord;
7
+ }): ReactElement;
@@ -0,0 +1,93 @@
1
+ import { createElement, } from "react";
2
+ import { toNativeWidgetCornerRadii } from "./native-widget-corner-radii.js";
3
+ import { toNativeWidgetResolvedBorderStyle } from "./native-widget-frame-border.js";
4
+ import { toNativeWidgetFrameShadow, toNativeWidgetSupportedBoxShadow, } from "./native-widget-frame-shadow.js";
5
+ import { resolveNativeWidgetBoxShadowSupport } from "./native-widget-frame-shadow-support.js";
6
+ const HOST_SURFACE_STYLE = {
7
+ alignItems: "center",
8
+ bottom: 0,
9
+ justifyContent: "flex-end",
10
+ left: 0,
11
+ position: "absolute",
12
+ right: 0,
13
+ top: 0,
14
+ };
15
+ const HIDDEN_HOST_SURFACE_STYLE = {
16
+ opacity: 0,
17
+ };
18
+ const PRESENTED_HOST_SURFACE_STYLE = {
19
+ zIndex: 2147483647,
20
+ };
21
+ const TOP_PLACEMENT_STYLE = {
22
+ justifyContent: "flex-start",
23
+ };
24
+ const BOTTOM_PLACEMENT_STYLE = {
25
+ justifyContent: "flex-end",
26
+ };
27
+ const PREPARING_VIEW_FRAME_STYLE = {
28
+ height: "100%",
29
+ width: "100%",
30
+ };
31
+ const PRESENTED_VIEW_FRAME_STYLE = {
32
+ maxHeight: "100%",
33
+ maxWidth: "100%",
34
+ };
35
+ const VIEW_FRAME_CONTENT_STYLE = {
36
+ flex: 1,
37
+ overflow: "hidden",
38
+ };
39
+ const VIEW_FRAME_BORDER_OVERLAY_STYLE = {
40
+ bottom: 0,
41
+ left: 0,
42
+ position: "absolute",
43
+ right: 0,
44
+ top: 0,
45
+ };
46
+ const resolveNativeView = () => {
47
+ const reactNativeModule = require("react-native");
48
+ if (!reactNativeModule.View) {
49
+ throw new Error("react-native must export View to present native views");
50
+ }
51
+ return reactNativeModule.View;
52
+ };
53
+ export function NativeViewPresentationSurface({ children, nativeViewComponent, record, }) {
54
+ var _a, _b, _c, _d, _e, _f, _g, _h;
55
+ const NativeView = nativeViewComponent !== null && nativeViewComponent !== void 0 ? nativeViewComponent : resolveNativeView();
56
+ const isPresented = record.activationState === "active" && record.size !== null;
57
+ const cornerRadii = toNativeWidgetCornerRadii((_a = record.size) === null || _a === void 0 ? void 0 : _a.frameAppearance);
58
+ const shadow = toNativeWidgetFrameShadow(((_c = (_b = record.size) === null || _b === void 0 ? void 0 : _b.frameAppearance) === null || _c === void 0 ? void 0 : _c.kind) === "page"
59
+ ? record.size.frameAppearance.resolvedBoxShadow
60
+ : undefined);
61
+ const nativeBoxShadow = toNativeWidgetSupportedBoxShadow(shadow, resolveNativeWidgetBoxShadowSupport());
62
+ const frameOverlayShadowStyle = nativeBoxShadow === undefined ? {} : { boxShadow: nativeBoxShadow };
63
+ const borderStyle = toNativeWidgetResolvedBorderStyle(((_e = (_d = record.size) === null || _d === void 0 ? void 0 : _d.frameAppearance) === null || _e === void 0 ? void 0 : _e.kind) === "page"
64
+ ? record.size.frameAppearance.resolvedBorder
65
+ : undefined);
66
+ const cornerRadiusStyle = {
67
+ borderBottomLeftRadius: cornerRadii.bottomLeft,
68
+ borderBottomRightRadius: cornerRadii.bottomRight,
69
+ borderTopLeftRadius: cornerRadii.topLeft,
70
+ borderTopRightRadius: cornerRadii.topRight,
71
+ };
72
+ return createElement(NativeView, {
73
+ pointerEvents: isPresented ? "box-none" : "none",
74
+ style: Object.assign(Object.assign(Object.assign({}, HOST_SURFACE_STYLE), (((_f = record.size) === null || _f === void 0 ? void 0 : _f.placement) === "docked-top"
75
+ ? TOP_PLACEMENT_STYLE
76
+ : BOTTOM_PLACEMENT_STYLE)), (isPresented
77
+ ? PRESENTED_HOST_SURFACE_STYLE
78
+ : HIDDEN_HOST_SURFACE_STYLE)),
79
+ testID: `gx-native-view-presentation-${record.viewId}`,
80
+ }, createElement(NativeView, {
81
+ pointerEvents: isPresented ? "auto" : "none",
82
+ style: isPresented
83
+ ? Object.assign(Object.assign(Object.assign({}, PRESENTED_VIEW_FRAME_STYLE), cornerRadiusStyle), { height: (_g = record.size) === null || _g === void 0 ? void 0 : _g.height, width: (_h = record.size) === null || _h === void 0 ? void 0 : _h.width }) : PREPARING_VIEW_FRAME_STYLE,
84
+ testID: `gx-native-view-frame-${record.viewId}`,
85
+ }, createElement(NativeView, {
86
+ style: Object.assign(Object.assign({}, VIEW_FRAME_CONTENT_STYLE), cornerRadiusStyle),
87
+ testID: `gx-native-view-frame-content-${record.viewId}`,
88
+ }, children), createElement(NativeView, {
89
+ pointerEvents: "none",
90
+ style: Object.assign(Object.assign(Object.assign(Object.assign({}, VIEW_FRAME_BORDER_OVERLAY_STYLE), cornerRadiusStyle), borderStyle), frameOverlayShadowStyle),
91
+ testID: `gx-native-view-frame-border-${record.viewId}`,
92
+ })));
93
+ }
@@ -1,3 +1,4 @@
1
+ import { type OpenRequestMetadata } from "@getuserfeedback/protocol/internal/core-view-host-actions";
1
2
  import { type EventViewSize } from "@getuserfeedback/protocol/internal/view-host-control-messages";
2
3
  import type { CoreWebViewHostConnection } from "./core-webview-host-controller.js";
3
4
  import type { ViewWebViewHostConnection } from "./view-webview-host-controller.js";
@@ -8,6 +9,7 @@ export type NativeViewRecord = Readonly<{
8
9
  activationState: NativeViewActivationState;
9
10
  generation: number;
10
11
  instanceId: string;
12
+ openRequest: Readonly<OpenRequestMetadata> | undefined;
11
13
  size: NativeViewSizeReport | null;
12
14
  srcdoc: string;
13
15
  viewId: string;
@@ -28,21 +28,26 @@ export function createNativeViewRecordController(input) {
28
28
  listener();
29
29
  }
30
30
  };
31
- const ensurePendingActivation = (viewId) => {
31
+ const ensurePendingActivation = (viewId, openRequest) => {
32
32
  const existing = pendingActivations.get(viewId);
33
33
  if (existing) {
34
+ existing.openRequest = openRequest;
34
35
  return existing;
35
36
  }
36
- const created = createViewActivationController();
37
+ const created = {
38
+ activation: createViewActivationController(),
39
+ openRequest,
40
+ };
37
41
  pendingActivations.set(viewId, created);
38
42
  return created;
39
43
  };
40
44
  const removeEntry = (entry) => {
41
- var _a;
42
45
  if (entries.get(entry.viewId) !== entry) {
43
46
  return false;
44
47
  }
45
- (_a = entry.viewReadinessDetach) === null || _a === void 0 ? void 0 : _a.call(entry);
48
+ const detachViewReadiness = entry.viewReadinessDetach;
49
+ entry.viewReadinessDetach = null;
50
+ detachViewReadiness === null || detachViewReadiness === void 0 ? void 0 : detachViewReadiness();
46
51
  entries.delete(entry.viewId);
47
52
  if (!disposed) {
48
53
  notify();
@@ -58,7 +63,8 @@ export function createNativeViewRecordController(input) {
58
63
  if (!preparation) {
59
64
  return;
60
65
  }
61
- const activation = (_a = pendingActivations.get(action.viewId)) !== null && _a !== void 0 ? _a : createViewActivationController();
66
+ const pendingActivation = pendingActivations.get(action.viewId);
67
+ const activation = (_a = pendingActivation === null || pendingActivation === void 0 ? void 0 : pendingActivation.activation) !== null && _a !== void 0 ? _a : createViewActivationController();
62
68
  pendingActivations.delete(action.viewId);
63
69
  activation.startPrerender();
64
70
  nextAllocationId += 1;
@@ -67,6 +73,9 @@ export function createNativeViewRecordController(input) {
67
73
  allocationId: nextAllocationId,
68
74
  generation: action.gen,
69
75
  instanceId: action.instanceId,
76
+ openRequest: pendingActivation
77
+ ? pendingActivation.openRequest
78
+ : toOpenRequestMetadata(action.openRequest),
70
79
  preparation,
71
80
  preparationState: "claimed",
72
81
  size: null,
@@ -81,12 +90,19 @@ export function createNativeViewRecordController(input) {
81
90
  notify();
82
91
  };
83
92
  const handleActivate = (action) => {
84
- var _a;
85
93
  const entry = entries.get(action.viewId);
86
- const activation = (_a = entry === null || entry === void 0 ? void 0 : entry.activation) !== null && _a !== void 0 ? _a : ensurePendingActivation(action.viewId);
94
+ const openRequest = toOpenRequestMetadata(action.openRequest);
95
+ const activation = entry
96
+ ? entry.activation
97
+ : ensurePendingActivation(action.viewId, openRequest).activation;
87
98
  const previousState = activation.getState();
88
99
  activation.requestActivation();
89
- if (entry && activation.getState() !== previousState) {
100
+ if (!entry) {
101
+ return;
102
+ }
103
+ const openRequestChanged = !areOpenRequestsEqual(entry.openRequest, openRequest);
104
+ entry.openRequest = openRequest;
105
+ if (activation.getState() !== previousState || openRequestChanged) {
90
106
  notify();
91
107
  }
92
108
  };
@@ -187,6 +203,12 @@ export function createNativeViewRecordController(input) {
187
203
  connectionReadiness.release();
188
204
  if (entry.viewReadinessDetach === detach) {
189
205
  entry.viewReadinessDetach = null;
206
+ if (entry.size !== null) {
207
+ entry.size = null;
208
+ if (!disposed && entries.get(viewId) === entry) {
209
+ notify();
210
+ }
211
+ }
190
212
  }
191
213
  };
192
214
  const attemptReadinessDelivery = (isRetry = false) => {
@@ -281,6 +303,10 @@ export function createNativeViewRecordController(input) {
281
303
  }
282
304
  entry.viewReadinessDetach = detach;
283
305
  previousDetach === null || previousDetach === void 0 ? void 0 : previousDetach();
306
+ if (entry.size !== null) {
307
+ entry.size = null;
308
+ notify();
309
+ }
284
310
  subscribed = true;
285
311
  for (const message of bufferedMessages) {
286
312
  handleMessage(message);
@@ -332,11 +358,35 @@ function toRecord(entry) {
332
358
  allocationId: entry.allocationId,
333
359
  generation: entry.generation,
334
360
  instanceId: entry.instanceId,
361
+ openRequest: entry.openRequest,
335
362
  size: entry.size,
336
363
  srcdoc: entry.srcdoc,
337
364
  viewId: entry.viewId,
338
365
  });
339
366
  }
367
+ function toOpenRequestMetadata(openRequest) {
368
+ return openRequest === undefined
369
+ ? undefined
370
+ : Object.freeze(Object.assign({}, openRequest));
371
+ }
372
+ function areOpenRequestsEqual(left, right) {
373
+ if (left === right) {
374
+ return true;
375
+ }
376
+ if (!left || !right || left.source !== right.source) {
377
+ return false;
378
+ }
379
+ if (left.source === "targeting" && right.source === "targeting") {
380
+ return left.flowId === right.flowId;
381
+ }
382
+ if (left.source !== "command" || right.source !== "command") {
383
+ return false;
384
+ }
385
+ return (left.flowId === right.flowId &&
386
+ left.requestId === right.requestId &&
387
+ left.flowHandleId === right.flowHandleId &&
388
+ left.hideCloseButton === right.hideCloseButton);
389
+ }
340
390
  function toViewSizeReport(event) {
341
391
  return {
342
392
  width: event.width,
@@ -0,0 +1,10 @@
1
+ import type { WidgetFrameAppearance, WidgetFrameResolvedBorderRadius } from "@getuserfeedback/protocol/host";
2
+ export type NativeWidgetCornerRadii = {
3
+ bottomLeft: number;
4
+ bottomRight: number;
5
+ topLeft: number;
6
+ topRight: number;
7
+ };
8
+ export declare function safeParseNativeWidgetCanonicalRadii(value: string): NativeWidgetCornerRadii | undefined;
9
+ export declare function toNativeWidgetConservativeResolvedCornerRadii(resolved: WidgetFrameResolvedBorderRadius): NativeWidgetCornerRadii;
10
+ export declare function toNativeWidgetCornerRadii(appearance?: WidgetFrameAppearance): NativeWidgetCornerRadii;
@@ -0,0 +1,54 @@
1
+ const CLEARED_NATIVE_WIDGET_CORNER_RADII = {
2
+ bottomLeft: 0,
3
+ bottomRight: 0,
4
+ topLeft: 0,
5
+ topRight: 0,
6
+ };
7
+ const CANONICAL_PX_PATTERN = /^(?:0|[1-9]\d*)(?:\.\d+)?px$/;
8
+ const safeParseCanonicalPx = (value) => {
9
+ if (!CANONICAL_PX_PATTERN.test(value)) {
10
+ return undefined;
11
+ }
12
+ const parsed = Number.parseFloat(value);
13
+ return Number.isFinite(parsed) ? parsed : undefined;
14
+ };
15
+ export function safeParseNativeWidgetCanonicalRadii(value) {
16
+ if (value.includes("/")) {
17
+ return undefined;
18
+ }
19
+ const radii = value.trim().split(/\s+/).map(safeParseCanonicalPx);
20
+ if (radii.length === 0 ||
21
+ radii.length > 4 ||
22
+ radii.some((radius) => radius === undefined)) {
23
+ return undefined;
24
+ }
25
+ const [topLeft, topRight = topLeft, bottomRight = topLeft, bottomLeft = topRight,] = radii;
26
+ if (topLeft === undefined ||
27
+ topRight === undefined ||
28
+ bottomRight === undefined ||
29
+ bottomLeft === undefined) {
30
+ return undefined;
31
+ }
32
+ return { bottomLeft, bottomRight, topLeft, topRight };
33
+ }
34
+ export function toNativeWidgetConservativeResolvedCornerRadii(resolved) {
35
+ // React Native exposes one scalar per corner, so use a smaller circle that
36
+ // does not exceed the authored ellipse on either axis.
37
+ return {
38
+ bottomLeft: Math.min(resolved.bottomLeft.horizontalPx, resolved.bottomLeft.verticalPx),
39
+ bottomRight: Math.min(resolved.bottomRight.horizontalPx, resolved.bottomRight.verticalPx),
40
+ topLeft: Math.min(resolved.topLeft.horizontalPx, resolved.topLeft.verticalPx),
41
+ topRight: Math.min(resolved.topRight.horizontalPx, resolved.topRight.verticalPx),
42
+ };
43
+ }
44
+ export function toNativeWidgetCornerRadii(appearance) {
45
+ var _a;
46
+ if (appearance === undefined || appearance.kind === "none") {
47
+ return Object.assign({}, CLEARED_NATIVE_WIDGET_CORNER_RADII);
48
+ }
49
+ const resolved = appearance.resolvedBorderRadius;
50
+ if (resolved !== undefined) {
51
+ return toNativeWidgetConservativeResolvedCornerRadii(resolved);
52
+ }
53
+ return ((_a = safeParseNativeWidgetCanonicalRadii(appearance.borderRadius)) !== null && _a !== void 0 ? _a : Object.assign({}, CLEARED_NATIVE_WIDGET_CORNER_RADII));
54
+ }
@@ -0,0 +1,7 @@
1
+ import type { WidgetFrameResolvedBorder } from "@getuserfeedback/protocol/host";
2
+ export type NativeWidgetFrameBorderStyle = {
3
+ borderColor: string;
4
+ borderStyle: "dashed" | "dotted" | "solid";
5
+ borderWidth: number;
6
+ };
7
+ export declare function toNativeWidgetResolvedBorderStyle(border?: WidgetFrameResolvedBorder): NativeWidgetFrameBorderStyle;
@@ -0,0 +1,21 @@
1
+ const CLEARED_NATIVE_WIDGET_FRAME_BORDER_STYLE = {
2
+ borderColor: "transparent",
3
+ borderStyle: "solid",
4
+ borderWidth: 0,
5
+ };
6
+ export function toNativeWidgetResolvedBorderStyle(border) {
7
+ if (border === undefined ||
8
+ border.widthPx === 0 ||
9
+ border.style === "none" ||
10
+ border.style === "hidden") {
11
+ return Object.assign({}, CLEARED_NATIVE_WIDGET_FRAME_BORDER_STYLE);
12
+ }
13
+ const { alpha, blue, green, red } = border.color;
14
+ return {
15
+ borderColor: `rgba(${red}, ${green}, ${blue}, ${alpha})`,
16
+ borderStyle: border.style === "dashed" || border.style === "dotted"
17
+ ? border.style
18
+ : "solid",
19
+ borderWidth: border.widthPx,
20
+ };
21
+ }
@@ -0,0 +1,16 @@
1
+ import type { NativeWidgetBoxShadowSupport } from "./native-widget-frame-shadow.js";
2
+ type NativeReactVersion = {
3
+ major?: unknown;
4
+ minor?: unknown;
5
+ };
6
+ export type NativeWidgetBoxShadowSupportInput = {
7
+ hasFabricUIManager: boolean;
8
+ platform: {
9
+ os?: unknown;
10
+ version?: unknown;
11
+ };
12
+ reactNativeVersion?: NativeReactVersion;
13
+ };
14
+ export declare function toNativeWidgetBoxShadowSupport(input: NativeWidgetBoxShadowSupportInput): NativeWidgetBoxShadowSupport;
15
+ export declare function resolveNativeWidgetBoxShadowSupport(): NativeWidgetBoxShadowSupport;
16
+ export {};
@@ -0,0 +1,54 @@
1
+ export function toNativeWidgetBoxShadowSupport(input) {
2
+ var _a, _b;
3
+ if (!input.hasFabricUIManager) {
4
+ return "none";
5
+ }
6
+ const major = (_a = input.reactNativeVersion) === null || _a === void 0 ? void 0 : _a.major;
7
+ const minor = (_b = input.reactNativeVersion) === null || _b === void 0 ? void 0 : _b.minor;
8
+ const supportsBoxShadow = typeof major === "number" &&
9
+ Number.isInteger(major) &&
10
+ (major > 0 ||
11
+ (major === 0 &&
12
+ typeof minor === "number" &&
13
+ Number.isInteger(minor) &&
14
+ minor >= 76));
15
+ if (!supportsBoxShadow) {
16
+ return "none";
17
+ }
18
+ if (input.platform.os === "ios") {
19
+ return "all";
20
+ }
21
+ if (input.platform.os !== "android") {
22
+ return "none";
23
+ }
24
+ const androidApiLevel = input.platform.version;
25
+ if (typeof androidApiLevel !== "number" ||
26
+ !Number.isInteger(androidApiLevel)) {
27
+ return "none";
28
+ }
29
+ return androidApiLevel >= 29
30
+ ? "all"
31
+ : androidApiLevel >= 28
32
+ ? "outset"
33
+ : "none";
34
+ }
35
+ export function resolveNativeWidgetBoxShadowSupport() {
36
+ var _a, _b, _c, _d;
37
+ try {
38
+ const reactNativeModule = require("react-native");
39
+ // React Native exposes renderer capability to JavaScript process-wide.
40
+ // There is no stable public API for querying the renderer of one View.
41
+ const fabricUIManager = Reflect.get(globalThis, "nativeFabricUIManager");
42
+ return toNativeWidgetBoxShadowSupport({
43
+ hasFabricUIManager: fabricUIManager !== undefined && fabricUIManager !== null,
44
+ platform: {
45
+ os: (_a = reactNativeModule.Platform) === null || _a === void 0 ? void 0 : _a.OS,
46
+ version: (_b = reactNativeModule.Platform) === null || _b === void 0 ? void 0 : _b.Version,
47
+ },
48
+ reactNativeVersion: (_d = (_c = reactNativeModule.Platform) === null || _c === void 0 ? void 0 : _c.constants) === null || _d === void 0 ? void 0 : _d.reactNativeVersion,
49
+ });
50
+ }
51
+ catch (_e) {
52
+ return "none";
53
+ }
54
+ }
@@ -0,0 +1,30 @@
1
+ import type { WidgetFrameResolvedBoxShadowLayer } from "@getuserfeedback/protocol/host";
2
+ export type NativeWidgetFrameBoxShadowValue = {
3
+ blurRadius: number;
4
+ color: string;
5
+ inset: boolean;
6
+ offsetX: number;
7
+ offsetY: number;
8
+ spreadDistance: number;
9
+ };
10
+ type NativeWidgetFrameLegacyShadowStyle = {
11
+ elevation: number;
12
+ shadowColor: string;
13
+ shadowOffset: {
14
+ height: number;
15
+ width: number;
16
+ };
17
+ shadowOpacity: number;
18
+ shadowRadius: number;
19
+ };
20
+ export type NativeWidgetFrameShadowProjection = {
21
+ kind: "clear";
22
+ } | {
23
+ boxShadow: NativeWidgetFrameBoxShadowValue[];
24
+ fallbackStyle: NativeWidgetFrameLegacyShadowStyle;
25
+ kind: "resolved";
26
+ };
27
+ export type NativeWidgetBoxShadowSupport = "all" | "none" | "outset";
28
+ export declare function toNativeWidgetFrameShadow(layers?: WidgetFrameResolvedBoxShadowLayer[]): NativeWidgetFrameShadowProjection;
29
+ export declare function toNativeWidgetSupportedBoxShadow(shadow: NativeWidgetFrameShadowProjection, support?: NativeWidgetBoxShadowSupport): NativeWidgetFrameBoxShadowValue[] | undefined;
30
+ export {};
@@ -0,0 +1,49 @@
1
+ const CLEARED_NATIVE_WIDGET_FRAME_SHADOW_STYLE = {
2
+ elevation: 0,
3
+ shadowColor: "transparent",
4
+ shadowOffset: { height: 0, width: 0 },
5
+ shadowOpacity: 0,
6
+ shadowRadius: 0,
7
+ };
8
+ const toNativeWidgetFrameColor = (color) => `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha})`;
9
+ const toNativeWidgetFrameBoxShadow = (layers) => layers.map((layer) => ({
10
+ blurRadius: layer.blurRadiusPx,
11
+ color: toNativeWidgetFrameColor(layer.color),
12
+ inset: layer.inset,
13
+ offsetX: layer.offsetXPx,
14
+ offsetY: layer.offsetYPx,
15
+ spreadDistance: layer.spreadRadiusPx,
16
+ }));
17
+ const toNativeWidgetFrameLegacyShadow = (layers) => {
18
+ const layer = layers.find((candidate) => !candidate.inset && candidate.color.alpha > 0);
19
+ if (layer === undefined) {
20
+ return Object.assign({}, CLEARED_NATIVE_WIDGET_FRAME_SHADOW_STYLE);
21
+ }
22
+ return {
23
+ elevation: Math.max(1, Math.ceil(layer.blurRadiusPx + Math.max(0, layer.spreadRadiusPx))),
24
+ shadowColor: `rgb(${layer.color.red}, ${layer.color.green}, ${layer.color.blue})`,
25
+ shadowOffset: { height: layer.offsetYPx, width: layer.offsetXPx },
26
+ shadowOpacity: layer.color.alpha,
27
+ shadowRadius: layer.blurRadiusPx,
28
+ };
29
+ };
30
+ export function toNativeWidgetFrameShadow(layers) {
31
+ if (layers === undefined) {
32
+ return { kind: "clear" };
33
+ }
34
+ return {
35
+ boxShadow: toNativeWidgetFrameBoxShadow(layers),
36
+ fallbackStyle: toNativeWidgetFrameLegacyShadow(layers),
37
+ kind: "resolved",
38
+ };
39
+ }
40
+ export function toNativeWidgetSupportedBoxShadow(shadow, support) {
41
+ if (shadow.kind !== "resolved" ||
42
+ support === undefined ||
43
+ support === "none") {
44
+ return undefined;
45
+ }
46
+ return support === "all"
47
+ ? shadow.boxShadow
48
+ : shadow.boxShadow.filter((layer) => !layer.inset);
49
+ }
@@ -1,26 +1,13 @@
1
- const CANONICAL_PX_PATTERN = /^(?:0|[1-9]\d*)(?:\.\d+)?px$/;
1
+ import { safeParseNativeWidgetCanonicalRadii } from "./native-widget-corner-radii.js";
2
2
  const CANONICAL_RADIUS_COMPONENT_PATTERN = /^((?:\d+(?:\.\d+)?|\.\d+))(px|%)$/;
3
- const safeParseCanonicalPx = (value) => {
4
- if (!CANONICAL_PX_PATTERN.test(value)) {
5
- return undefined;
6
- }
7
- const parsed = Number.parseFloat(value);
8
- return Number.isFinite(parsed) ? parsed : undefined;
9
- };
10
3
  export function safeParseProviderWidgetCanonicalTopRadii(value) {
11
- if (value.includes("/")) {
12
- return undefined;
13
- }
14
- const radii = value.split(/\s+/).map(safeParseCanonicalPx);
15
- if (radii.length === 0 ||
16
- radii.length > 4 ||
17
- radii.some((radius) => radius === undefined)) {
4
+ if (value !== value.trim()) {
18
5
  return undefined;
19
6
  }
20
- const [topLeft, topRight = topLeft] = radii;
21
- return topLeft === undefined || topRight === undefined
7
+ const radii = safeParseNativeWidgetCanonicalRadii(value);
8
+ return radii === undefined
22
9
  ? undefined
23
- : { topLeft, topRight };
10
+ : { topLeft: radii.topLeft, topRight: radii.topRight };
24
11
  }
25
12
  const parseCanonicalRadiusComponent = (value) => {
26
13
  var _a;
@@ -1,44 +1,15 @@
1
- import type { WidgetFrameAppearance, WidgetFrameResolvedBorder, WidgetFrameResolvedBoxShadowLayer } from "@getuserfeedback/protocol/host";
2
- export type ProviderWidgetFrameBorderStyle = {
3
- borderColor: string;
4
- borderStyle: "dashed" | "dotted" | "solid";
5
- borderWidth: number;
6
- };
7
- type ProviderWidgetFrameBoxShadowValue = {
8
- blurRadius: number;
9
- color: string;
10
- inset: boolean;
11
- offsetX: number;
12
- offsetY: number;
13
- spreadDistance: number;
14
- };
15
- type ProviderWidgetFrameLegacyShadowStyle = {
16
- elevation: number;
17
- shadowColor: string;
18
- shadowOffset: {
19
- height: number;
20
- width: number;
21
- };
22
- shadowOpacity: number;
23
- shadowRadius: number;
24
- };
25
- export type ProviderWidgetFrameShadowProjection = {
26
- kind: "clear";
27
- } | {
1
+ import type { WidgetFrameAppearance } from "@getuserfeedback/protocol/host";
2
+ import { type NativeWidgetFrameBorderStyle } from "./native-widget-frame-border.js";
3
+ import { type NativeWidgetFrameShadowProjection } from "./native-widget-frame-shadow.js";
4
+ type ProviderWidgetFrameShadowProjection = NativeWidgetFrameShadowProjection | {
28
5
  kind: "legacy";
29
- } | {
30
- boxShadow: ProviderWidgetFrameBoxShadowValue[];
31
- fallbackStyle: ProviderWidgetFrameLegacyShadowStyle;
32
- kind: "resolved";
33
6
  };
34
7
  export type ProviderWidgetFrameAppearanceProjection = {
35
- borderStyle?: ProviderWidgetFrameBorderStyle;
8
+ borderStyle?: NativeWidgetFrameBorderStyle;
36
9
  shadow: ProviderWidgetFrameShadowProjection;
37
10
  topLeftRadius: number;
38
11
  topRightRadius: number;
39
12
  };
40
- export declare function toProviderWidgetFrameShadow(layers?: WidgetFrameResolvedBoxShadowLayer[]): ProviderWidgetFrameShadowProjection;
41
- export declare function toProviderWidgetFrameBorderStyle(border?: WidgetFrameResolvedBorder): ProviderWidgetFrameBorderStyle;
42
13
  export declare function toProviderWidgetFrameAppearance(appearance?: WidgetFrameAppearance, nativeSheetSize?: {
43
14
  height: number;
44
15
  width: number;
@@ -1,71 +1,12 @@
1
+ import { toNativeWidgetConservativeResolvedCornerRadii } from "./native-widget-corner-radii.js";
2
+ import { toNativeWidgetResolvedBorderStyle, } from "./native-widget-frame-border.js";
3
+ import { toNativeWidgetFrameShadow, } from "./native-widget-frame-shadow.js";
1
4
  import { resolveProviderWidgetCanonicalBorderRadius, safeParseProviderWidgetCanonicalTopRadii, } from "./provider-widget-border-radius.js";
2
5
  const LEGACY_WIDGET_SHEET_RADIUS = 24;
3
- const CLEARED_WIDGET_FRAME_BORDER_STYLE = {
4
- borderColor: "transparent",
5
- borderStyle: "solid",
6
- borderWidth: 0,
6
+ const toProviderWidgetConservativeResolvedTopRadii = (radius) => {
7
+ const resolved = toNativeWidgetConservativeResolvedCornerRadii(radius);
8
+ return { topLeft: resolved.topLeft, topRight: resolved.topRight };
7
9
  };
8
- const CLEARED_WIDGET_FRAME_SHADOW_STYLE = {
9
- elevation: 0,
10
- shadowColor: "transparent",
11
- shadowOffset: { height: 0, width: 0 },
12
- shadowOpacity: 0,
13
- shadowRadius: 0,
14
- };
15
- const toProviderWidgetFrameColor = (color) => `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha})`;
16
- const toProviderWidgetFrameBoxShadow = (layers) => layers.map((layer) => ({
17
- blurRadius: layer.blurRadiusPx,
18
- color: toProviderWidgetFrameColor(layer.color),
19
- inset: layer.inset,
20
- offsetX: layer.offsetXPx,
21
- offsetY: layer.offsetYPx,
22
- spreadDistance: layer.spreadRadiusPx,
23
- }));
24
- const toProviderWidgetFrameLegacyShadow = (layers) => {
25
- const layer = layers.find((candidate) => !candidate.inset && candidate.color.alpha > 0);
26
- if (layer === undefined) {
27
- return Object.assign({}, CLEARED_WIDGET_FRAME_SHADOW_STYLE);
28
- }
29
- return {
30
- elevation: Math.max(1, Math.ceil(layer.blurRadiusPx + Math.max(0, layer.spreadRadiusPx))),
31
- shadowColor: `rgb(${layer.color.red}, ${layer.color.green}, ${layer.color.blue})`,
32
- shadowOffset: { height: layer.offsetYPx, width: layer.offsetXPx },
33
- shadowOpacity: layer.color.alpha,
34
- shadowRadius: layer.blurRadiusPx,
35
- };
36
- };
37
- export function toProviderWidgetFrameShadow(layers) {
38
- if (layers === undefined) {
39
- return { kind: "clear" };
40
- }
41
- return {
42
- boxShadow: toProviderWidgetFrameBoxShadow(layers),
43
- fallbackStyle: toProviderWidgetFrameLegacyShadow(layers),
44
- kind: "resolved",
45
- };
46
- }
47
- const toProviderWidgetConservativeResolvedTopRadii = (radius) => ({
48
- // React Native exposes one scalar per corner, so use a deliberately smaller
49
- // circle that does not exceed the authored ellipse on either axis.
50
- topLeft: Math.min(radius.topLeft.horizontalPx, radius.topLeft.verticalPx),
51
- topRight: Math.min(radius.topRight.horizontalPx, radius.topRight.verticalPx),
52
- });
53
- export function toProviderWidgetFrameBorderStyle(border) {
54
- if (border === undefined ||
55
- border.widthPx === 0 ||
56
- border.style === "none" ||
57
- border.style === "hidden") {
58
- return Object.assign({}, CLEARED_WIDGET_FRAME_BORDER_STYLE);
59
- }
60
- const { alpha, blue, green, red } = border.color;
61
- return {
62
- borderColor: `rgba(${red}, ${green}, ${blue}, ${alpha})`,
63
- borderStyle: border.style === "dashed" || border.style === "dotted"
64
- ? border.style
65
- : "solid",
66
- borderWidth: border.widthPx,
67
- };
68
- }
69
10
  export function toProviderWidgetFrameAppearance(appearance, nativeSheetSize) {
70
11
  var _a, _b;
71
12
  if (appearance === undefined) {
@@ -77,8 +18,8 @@ export function toProviderWidgetFrameAppearance(appearance, nativeSheetSize) {
77
18
  }
78
19
  if (appearance.kind === "none") {
79
20
  return {
80
- borderStyle: toProviderWidgetFrameBorderStyle(),
81
- shadow: { kind: "clear" },
21
+ borderStyle: toNativeWidgetResolvedBorderStyle(),
22
+ shadow: toNativeWidgetFrameShadow(),
82
23
  topLeftRadius: 0,
83
24
  topRightRadius: 0,
84
25
  };
@@ -94,8 +35,8 @@ export function toProviderWidgetFrameAppearance(appearance, nativeSheetSize) {
94
35
  ? safeParseProviderWidgetCanonicalTopRadii(appearance.borderRadius)
95
36
  : toProviderWidgetConservativeResolvedTopRadii(appearance.resolvedBorderRadius);
96
37
  return {
97
- borderStyle: toProviderWidgetFrameBorderStyle(appearance.resolvedBorder),
98
- shadow: toProviderWidgetFrameShadow(appearance.resolvedBoxShadow),
38
+ borderStyle: toNativeWidgetResolvedBorderStyle(appearance.resolvedBorder),
39
+ shadow: toNativeWidgetFrameShadow(appearance.resolvedBoxShadow),
99
40
  topLeftRadius: (_a = radius === null || radius === void 0 ? void 0 : radius.topLeft) !== null && _a !== void 0 ? _a : 0,
100
41
  topRightRadius: (_b = radius === null || radius === void 0 ? void 0 : radius.topRight) !== null && _b !== void 0 ? _b : 0,
101
42
  };
@@ -1,12 +1,8 @@
1
1
  import { resolveWidgetLayoutTransitionExecution, type WidgetFrameAppearance, type WidgetLayoutComparableSizeUpdate, type WidgetLayoutSizeUpdate, type WidgetLayoutTimedCapableTransitionPlan, type WidgetLayoutTransitionPolicy, type WidgetLayoutTransitionSchedule } from "@getuserfeedback/protocol/host";
2
2
  import { type ComponentType, type ReactElement, type ReactNode } from "react";
3
+ import { type NativeWidgetBoxShadowSupport } from "./native-widget-frame-shadow.js";
3
4
  import type { WidgetHostProps } from "./widget-host.js";
4
5
  type NativeViewComponent = ComponentType<Record<string, unknown>>;
5
- type NativeReactVersion = {
6
- major?: unknown;
7
- minor?: unknown;
8
- };
9
- export type ProviderWidgetNativeBoxShadowSupport = "all" | "none" | "outset";
10
6
  export declare function toProviderWidgetResumedTransitionTiming(input: {
11
7
  durationMs: number;
12
8
  easing: readonly [number, number, number, number];
@@ -17,14 +13,6 @@ export declare function toProviderWidgetResumedTransitionTiming(input: {
17
13
  easing: (remainingProgress: number) => number;
18
14
  initialProgress: number;
19
15
  };
20
- export declare function toProviderWidgetNativeBoxShadowSupport(input: {
21
- hasFabricUIManager: boolean;
22
- platform: {
23
- os?: unknown;
24
- version?: unknown;
25
- };
26
- reactNativeVersion?: NativeReactVersion;
27
- }): ProviderWidgetNativeBoxShadowSupport;
28
16
  export declare function toProviderWidgetSheetHostViewport(viewport: WidgetHostProps["hostViewport"], flowSize?: {
29
17
  height?: number;
30
18
  }): WidgetHostProps["hostViewport"];
@@ -51,7 +39,7 @@ export declare function toProviderWidgetSheetSurfaceStyles(input: {
51
39
  frameAppearance?: WidgetFrameAppearance;
52
40
  isVisible: boolean;
53
41
  sheetHostLayoutUpdate: WidgetLayoutComparableSizeUpdate | undefined;
54
- nativeBoxShadowSupport?: ProviderWidgetNativeBoxShadowSupport;
42
+ nativeBoxShadowSupport?: NativeWidgetBoxShadowSupport;
55
43
  }): {
56
44
  contentStyle: Record<string, unknown>;
57
45
  frameStyle: Record<string, unknown>;
@@ -1,5 +1,7 @@
1
1
  import { areWidgetFrameAppearancesEqual, planWidgetLayoutTransition, resolveWidgetLayoutResumedEasing, resolveWidgetLayoutTransitionExecution, toInstantWidgetLayoutSizeUpdate, } from "@getuserfeedback/protocol/host";
2
2
  import { createElement, useCallback, useEffect, useMemo, useRef, useState, } from "react";
3
+ import { toNativeWidgetSupportedBoxShadow, } from "./native-widget-frame-shadow.js";
4
+ import { resolveNativeWidgetBoxShadowSupport } from "./native-widget-frame-shadow-support.js";
3
5
  import { toProviderWidgetFrameAppearance } from "./provider-widget-frame-appearance.js";
4
6
  const HIDDEN_WIDGET_HOST_STYLE = {
5
7
  bottom: 0,
@@ -152,60 +154,6 @@ export function toProviderWidgetResumedTransitionTiming(input) {
152
154
  initialProgress,
153
155
  };
154
156
  }
155
- export function toProviderWidgetNativeBoxShadowSupport(input) {
156
- var _a, _b;
157
- if (!input.hasFabricUIManager) {
158
- return "none";
159
- }
160
- const major = (_a = input.reactNativeVersion) === null || _a === void 0 ? void 0 : _a.major;
161
- const minor = (_b = input.reactNativeVersion) === null || _b === void 0 ? void 0 : _b.minor;
162
- const supportsBoxShadow = typeof major === "number" &&
163
- Number.isInteger(major) &&
164
- (major > 0 ||
165
- (major === 0 &&
166
- typeof minor === "number" &&
167
- Number.isInteger(minor) &&
168
- minor >= 76));
169
- if (!supportsBoxShadow) {
170
- return "none";
171
- }
172
- if (input.platform.os === "ios") {
173
- return "all";
174
- }
175
- if (input.platform.os !== "android") {
176
- return "none";
177
- }
178
- const androidApiLevel = input.platform.version;
179
- if (typeof androidApiLevel !== "number" ||
180
- !Number.isInteger(androidApiLevel)) {
181
- return "none";
182
- }
183
- return androidApiLevel >= 29
184
- ? "all"
185
- : androidApiLevel >= 28
186
- ? "outset"
187
- : "none";
188
- }
189
- function resolveNativeBoxShadowSupport() {
190
- var _a, _b, _c, _d;
191
- try {
192
- const reactNativeModule = require("react-native");
193
- // React Native exposes renderer capability to JavaScript process-wide.
194
- // There is no stable public API for querying the renderer of this View.
195
- const fabricUIManager = Reflect.get(globalThis, "nativeFabricUIManager");
196
- return toProviderWidgetNativeBoxShadowSupport({
197
- hasFabricUIManager: fabricUIManager !== undefined && fabricUIManager !== null,
198
- platform: {
199
- os: (_a = reactNativeModule.Platform) === null || _a === void 0 ? void 0 : _a.OS,
200
- version: (_b = reactNativeModule.Platform) === null || _b === void 0 ? void 0 : _b.Version,
201
- },
202
- reactNativeVersion: (_d = (_c = reactNativeModule.Platform) === null || _c === void 0 ? void 0 : _c.constants) === null || _d === void 0 ? void 0 : _d.reactNativeVersion,
203
- });
204
- }
205
- catch (_e) {
206
- return "none";
207
- }
208
- }
209
157
  function toResponderPageY(event) {
210
158
  var _a, _b, _c;
211
159
  const nativeEvent = event === null || event === void 0 ? void 0 : event.nativeEvent;
@@ -289,13 +237,9 @@ export function toProviderWidgetSheetSurfaceStyles(input) {
289
237
  borderTopLeftRadius: appearance.topLeftRadius,
290
238
  borderTopRightRadius: appearance.topRightRadius,
291
239
  };
292
- const nativeBoxShadow = appearance.shadow.kind !== "resolved" ||
293
- input.nativeBoxShadowSupport === undefined ||
294
- input.nativeBoxShadowSupport === "none"
240
+ const nativeBoxShadow = appearance.shadow.kind === "legacy"
295
241
  ? undefined
296
- : input.nativeBoxShadowSupport === "all"
297
- ? appearance.shadow.boxShadow
298
- : appearance.shadow.boxShadow.filter((shadow) => !shadow.inset);
242
+ : toNativeWidgetSupportedBoxShadow(appearance.shadow, input.nativeBoxShadowSupport);
299
243
  const usesNativeBoxShadow = nativeBoxShadow !== undefined;
300
244
  const sheetShadowStyle = appearance.shadow.kind === "legacy"
301
245
  ? WIDGET_SHEET_LEGACY_FRAME_STYLE
@@ -623,7 +567,7 @@ export function ProviderWidgetHostOverlay({ children, frameAppearance, isVisible
623
567
  const sheetHostViewport = sheetHostLayoutUpdate === null || sheetHostLayoutUpdate === void 0 ? void 0 : sheetHostLayoutUpdate.size;
624
568
  const isSheetVisible = isVisible && sheetHostViewport !== undefined;
625
569
  const pointerEvents = isSheetVisible ? "auto" : "none";
626
- const nativeBoxShadowSupport = useMemo(resolveNativeBoxShadowSupport, []);
570
+ const nativeBoxShadowSupport = useMemo(resolveNativeWidgetBoxShadowSupport, []);
627
571
  const surfaceStyles = useMemo(() => toProviderWidgetSheetSurfaceStyles({
628
572
  frameAppearance,
629
573
  isVisible,
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.81";
6
+ export const REACT_NATIVE_SDK_VERSION = reactNativeSdkVersion.length > 0 ? reactNativeSdkVersion : "3.0.91";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getuserfeedback/react-native",
3
- "version": "3.0.81",
3
+ "version": "3.0.91",
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.20.0",
48
- "@getuserfeedback/sdk": "^0.12.5",
47
+ "@getuserfeedback/protocol": "^3.21.0",
48
+ "@getuserfeedback/sdk": "^0.12.6",
49
49
  "robot3": "^1.2.0"
50
50
  },
51
51
  "peerDependencies": {