@getuserfeedback/react-native 5.12.0 → 5.14.0

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.
@@ -1,4 +1,5 @@
1
1
  import type { CoreCommandEnvelope } from "@getuserfeedback/protocol";
2
+ import { type LinkRouter } from "@getuserfeedback/protocol/host";
2
3
  import type { EventHandleInvalidated } from "@getuserfeedback/protocol/internal/core-handle-invalidation";
3
4
  import type { ConfigureOptions, InitOptions } from "@getuserfeedback/sdk";
4
5
  import type { ActionRegistry } from "@getuserfeedback/sdk/internal/action-registry";
@@ -17,6 +18,10 @@ export type NativeRuntimeRootProps = {
17
18
  getHandler: ActionRegistry["getHandler"];
18
19
  instanceId: string;
19
20
  };
21
+ linkRouter?: {
22
+ getRouter: () => LinkRouter | undefined;
23
+ instanceId: string;
24
+ };
20
25
  onCommandSessionChange?: (session: NativeCoreCommandSession | null) => void;
21
26
  onError?: (error: Error) => void;
22
27
  onHandleInvalidated?: (message: EventHandleInvalidated) => void;
@@ -40,8 +45,9 @@ type NativeProviderRuntimeProps = Omit<NativeRuntimeRootProps, "createStartupCom
40
45
  instanceId: string;
41
46
  getRuntimeConfiguration?: () => ConfigureOptions;
42
47
  getHostActionHandler?: ActionRegistry["getHandler"];
48
+ getLinkRouter?: () => LinkRouter | undefined;
43
49
  onCommandSessionChange?: (session: NativeDirectCommandSession | null) => void;
44
50
  };
45
- export declare function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewComponent, createStartupCommand, hostActions, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, observeOpenRequest, onStartupTerminalError, startupCommandSettlementTimeoutMs, viewNativeViewComponent, viewWebViewComponent, }: NativeRuntimeRootProps): ReactElement;
46
- export declare function NativeProviderRuntime({ directSessionController, getHostActionHandler, getRuntimeConfiguration, initOptions, instanceId, onCommandSessionChange, onError, ...runtimeProps }: NativeProviderRuntimeProps): ReactElement;
51
+ export declare function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewComponent, createStartupCommand, hostActions, linkRouter, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, observeOpenRequest, onStartupTerminalError, startupCommandSettlementTimeoutMs, viewNativeViewComponent, viewWebViewComponent, }: NativeRuntimeRootProps): ReactElement;
52
+ export declare function NativeProviderRuntime({ directSessionController, getHostActionHandler, getLinkRouter, getRuntimeConfiguration, initOptions, instanceId, onCommandSessionChange, onError, ...runtimeProps }: NativeProviderRuntimeProps): ReactElement;
47
53
  export {};
@@ -10,6 +10,7 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { CommandSettlementError, CommandSettlementTimeoutError, } from "@getuserfeedback/protocol/host";
13
+ import { BROWSER_NAVIGATION_AVAILABILITY_EVENT } from "@getuserfeedback/protocol/internal/navigation-rpc";
13
14
  import { createElement, Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react";
14
15
  import { CoreWebViewHost, } from "./core-webview-host.js";
15
16
  import { createNativeCoreCommandChannel } from "./native-core-command-channel.js";
@@ -20,7 +21,7 @@ import { createNativeViewRecordController, } from "./native-view-record-controll
20
21
  import { createProviderCommandEnvelope, REACT_NATIVE_CLIENT_META, } from "./provider-command-helpers.js";
21
22
  const DEFAULT_STARTUP_COMMAND_SETTLEMENT_TIMEOUT_MS = 30000;
22
23
  const getEmptyRuntimeConfiguration = () => ({});
23
- export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewComponent, createStartupCommand, hostActions, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, observeOpenRequest, onStartupTerminalError, startupCommandSettlementTimeoutMs = DEFAULT_STARTUP_COMMAND_SETTLEMENT_TIMEOUT_MS, viewNativeViewComponent, viewWebViewComponent, }) {
24
+ export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewComponent, createStartupCommand, hostActions, linkRouter, onCommandSessionChange, onError, onHandleInvalidated, onInvalidMessage, observeOpenRequest, onStartupTerminalError, startupCommandSettlementTimeoutMs = DEFAULT_STARTUP_COMMAND_SETTLEMENT_TIMEOUT_MS, viewNativeViewComponent, viewWebViewComponent, }) {
24
25
  const [coreHostRevision, setCoreHostRevision] = useState(0);
25
26
  const [session, setSession] = useState(null);
26
27
  const sessionRef = useRef(null);
@@ -42,6 +43,8 @@ export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewCo
42
43
  createStartupCommandRef.current = createStartupCommand;
43
44
  const hostActionsRef = useRef(hostActions);
44
45
  hostActionsRef.current = hostActions;
46
+ const linkRouterRef = useRef(linkRouter);
47
+ linkRouterRef.current = linkRouter;
45
48
  const reportError = useCallback((error) => {
46
49
  var _a;
47
50
  try {
@@ -84,8 +87,20 @@ export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewCo
84
87
  }
85
88
  const controller = createNativeViewRecordController({
86
89
  coreConnection,
90
+ getBrowserNavigationAvailability: linkRouterRef.current
91
+ ? () => {
92
+ var _a;
93
+ const available = ((_a = linkRouterRef.current) === null || _a === void 0 ? void 0 : _a.getRouter()) !== undefined;
94
+ return {
95
+ available,
96
+ explicitTargetAvailable: available,
97
+ hostActionLinkAvailable: false,
98
+ type: BROWSER_NAVIGATION_AVAILABILITY_EVENT,
99
+ };
100
+ }
101
+ : undefined,
87
102
  onError: reportError,
88
- observeOpenRequest: (payload) => { var _a, _b; return (_b = (_a = observeOpenRequestRef.current) === null || _a === void 0 ? void 0 : _a.call(observeOpenRequestRef, payload)) !== null && _b !== void 0 ? _b : { allowed: true }; },
103
+ observeOpenRequest: (payload) => { var _a; var _b; return (_b = (_a = observeOpenRequestRef.current) === null || _a === void 0 ? void 0 : _a.call(observeOpenRequestRef, payload)) !== null && _b !== void 0 ? _b : { allowed: true }; },
89
104
  });
90
105
  const commandChannelOwner = createNativeCoreCommandChannel({
91
106
  coreConnection,
@@ -100,6 +115,16 @@ export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewCo
100
115
  subscribeToViewActivity: controller.subscribe,
101
116
  }
102
117
  : undefined,
118
+ linkRouter: linkRouterRef.current
119
+ ? {
120
+ getActiveViewIds: () => controller
121
+ .getSnapshot()
122
+ .filter((record) => record.isActive)
123
+ .map((record) => record.viewId),
124
+ getRouter: () => { var _a; return (_a = linkRouterRef.current) === null || _a === void 0 ? void 0 : _a.getRouter(); },
125
+ instanceId: linkRouterRef.current.instanceId,
126
+ }
127
+ : undefined,
103
128
  onHostActionError: reportError,
104
129
  onSessionFailure: (error) => {
105
130
  reportError(error);
@@ -140,10 +165,8 @@ export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewCo
140
165
  };
141
166
  const startupCommand = (_a = createStartupCommandRef.current) === null || _a === void 0 ? void 0 : _a.call(createStartupCommandRef);
142
167
  if (startupCommand) {
143
- // TODO(architecture): Reconcile direct-runtime startup command settlement
144
- // with view projection. Android smoke can project the opened view while the
145
- // public open request still times out waiting for Core's settlement.
146
- // [principle=one-authoritative-command-lifecycle] [scope=slice] [priority=high] [impact=high] [risk=high] [epic=runtime-neutral-view-host-orchestration] [epic_order=2]
168
+ // TODO(testing): Run and record the authored direct-runtime open and embedded-close smoke path on Android and iOS devices as device evidence.
169
+ // [seam=native-direct-runtime] [layer=smoke] [workflow=test:react-native-expo-smoke:e2e] [priority=medium] [impact=medium] [risk=medium] [epic=runtime-neutral-view-host-orchestration] [epic_order=2]
147
170
  void commandSessionOwner.session
148
171
  .dispatch(startupCommand, {
149
172
  settlementTimeoutMs: startupCommandSettlementTimeoutMs,
@@ -257,7 +280,7 @@ export function NativeRuntimeRoot({ attachViewProjection, coreUrl, coreWebViewCo
257
280
  }));
258
281
  }
259
282
  export function NativeProviderRuntime(_a) {
260
- var { directSessionController, getHostActionHandler, getRuntimeConfiguration = getEmptyRuntimeConfiguration, initOptions, instanceId, onCommandSessionChange, onError } = _a, runtimeProps = __rest(_a, ["directSessionController", "getHostActionHandler", "getRuntimeConfiguration", "initOptions", "instanceId", "onCommandSessionChange", "onError"]);
283
+ var { directSessionController, getHostActionHandler, getLinkRouter, getRuntimeConfiguration = getEmptyRuntimeConfiguration, initOptions, instanceId, onCommandSessionChange, onError } = _a, runtimeProps = __rest(_a, ["directSessionController", "getHostActionHandler", "getLinkRouter", "getRuntimeConfiguration", "initOptions", "instanceId", "onCommandSessionChange", "onError"]);
261
284
  const directSessionRef = useRef(null);
262
285
  useLayoutEffect(() => {
263
286
  directSessionController.beginAttachmentAttempt();
@@ -298,5 +321,7 @@ export function NativeProviderRuntime(_a) {
298
321
  }, [getRuntimeConfiguration, initOptions, instanceId]);
299
322
  return createElement(NativeRuntimeRoot, Object.assign(Object.assign({}, runtimeProps), { hostActions: getHostActionHandler
300
323
  ? { getHandler: getHostActionHandler, instanceId }
324
+ : undefined, linkRouter: getLinkRouter
325
+ ? { getRouter: getLinkRouter, instanceId }
301
326
  : undefined, onCommandSessionChange: handleCommandSessionChange, onError, onStartupTerminalError: handleStartupTerminalError, createStartupCommand }));
302
327
  }
@@ -36,8 +36,9 @@ export function NativeProjectedViewContent({ controller, onInvalidMessage, recor
36
36
  });
37
37
  }
38
38
  function NativeViewHostProjectionEntry({ controller, nativeViewComponent, onInvalidMessage, presentationMode, record, webViewComponent, }) {
39
- var _a;
39
+ var _a, _b;
40
40
  const layoutTransactionId = (_a = record.size) === null || _a === void 0 ? void 0 : _a.layoutTransactionId;
41
+ const transitionSchedule = (_b = record.size) === null || _b === void 0 ? void 0 : _b.transitionSchedule;
41
42
  useLayoutEffect(() => {
42
43
  if (!layoutTransactionId) {
43
44
  return;
@@ -67,7 +68,12 @@ function NativeViewHostProjectionEntry({ controller, nativeViewComponent, onInva
67
68
  viewId: record.viewId,
68
69
  });
69
70
  });
70
- return createElement(NativeViewPresentationSurface, { nativeViewComponent, presentationMode, record }, createElement(NativeProjectedViewContent, {
71
+ return createElement(NativeViewPresentationSurface, {
72
+ nativeViewComponent,
73
+ presentationMode,
74
+ record,
75
+ transitionSchedule,
76
+ }, createElement(NativeProjectedViewContent, {
71
77
  controller,
72
78
  onInvalidMessage,
73
79
  record,
@@ -0,0 +1,25 @@
1
+ import { type WidgetLayoutTimedTransitionPolicy, type WidgetLayoutTransitionSchedule } from "@getuserfeedback/protocol/host";
2
+ import { type ComponentType } from "react";
3
+ export type NativeViewFrame = Readonly<{
4
+ bottomLeftRadius: number;
5
+ bottomRightRadius: number;
6
+ height: number;
7
+ topLeftRadius: number;
8
+ topRightRadius: number;
9
+ width: number;
10
+ }>;
11
+ export type NativeViewFrameSurfaceStyles = Readonly<{
12
+ border: Record<string, unknown>;
13
+ content: Record<string, unknown>;
14
+ frame: Record<string, unknown>;
15
+ }>;
16
+ export declare function useNativeViewFrameTransition(input: {
17
+ frame: NativeViewFrame | null;
18
+ nativeView: ComponentType<Record<string, unknown>>;
19
+ revision: number;
20
+ schedule?: WidgetLayoutTransitionSchedule;
21
+ styles: NativeViewFrameSurfaceStyles;
22
+ transitionPolicy?: WidgetLayoutTimedTransitionPolicy;
23
+ }): NativeViewFrameSurfaceStyles & {
24
+ frameView: ComponentType<Record<string, unknown>>;
25
+ };
@@ -0,0 +1,244 @@
1
+ import { resolveWidgetLayoutResumedEasing, resolveWidgetLayoutTransitionExecution, } from "@getuserfeedback/protocol/host";
2
+ import { useLayoutEffect, useMemo, useRef, useState, } from "react";
3
+ const resolveNativeAnimationRuntime = () => {
4
+ try {
5
+ const reactNativeModule = require("react-native");
6
+ return reactNativeModule.Animated
7
+ ? {
8
+ Animated: reactNativeModule.Animated,
9
+ Easing: reactNativeModule.Easing,
10
+ }
11
+ : null;
12
+ }
13
+ catch (_a) {
14
+ return null;
15
+ }
16
+ };
17
+ const FRAME_PROPERTIES = [
18
+ "width",
19
+ "height",
20
+ "topLeftRadius",
21
+ "topRightRadius",
22
+ "bottomLeftRadius",
23
+ "bottomRightRadius",
24
+ ];
25
+ export function useNativeViewFrameTransition(input) {
26
+ const animationRuntime = useMemo(resolveNativeAnimationRuntime, []);
27
+ const animatedFrameView = useMemo(() => animationRuntime
28
+ ? animationRuntime.Animated.createAnimatedComponent(input.nativeView)
29
+ : input.nativeView, [animationRuntime, input.nativeView]);
30
+ const animatedValuesRef = useRef(null);
31
+ const previousFrameRef = useRef(null);
32
+ const presentedStylesRef = useRef(input.styles);
33
+ const consumedScheduleRef = useRef(undefined);
34
+ const activeScheduleRef = useRef(undefined);
35
+ const activeAnimationsRef = useRef([]);
36
+ const pendingStartRef = useRef(undefined);
37
+ const animationAttemptRef = useRef(0);
38
+ const [isAnimating, setIsAnimating] = useState(false);
39
+ const latestInputRef = useRef(input);
40
+ latestInputRef.current = input;
41
+ const hasFrame = input.frame !== null;
42
+ if (animationRuntime && input.frame && animatedValuesRef.current === null) {
43
+ animatedValuesRef.current = {
44
+ bottomLeftRadius: new animationRuntime.Animated.Value(input.frame.bottomLeftRadius),
45
+ bottomRightRadius: new animationRuntime.Animated.Value(input.frame.bottomRightRadius),
46
+ height: new animationRuntime.Animated.Value(input.frame.height),
47
+ topLeftRadius: new animationRuntime.Animated.Value(input.frame.topLeftRadius),
48
+ topRightRadius: new animationRuntime.Animated.Value(input.frame.topRightRadius),
49
+ width: new animationRuntime.Animated.Value(input.frame.width),
50
+ };
51
+ }
52
+ const hasPendingSchedule = input.schedule !== undefined &&
53
+ consumedScheduleRef.current !== input.schedule;
54
+ const shouldUseAnimatedStyles = animationRuntime !== null &&
55
+ input.frame !== null &&
56
+ animatedValuesRef.current !== null &&
57
+ (hasPendingSchedule ||
58
+ (isAnimating && activeScheduleRef.current === input.schedule));
59
+ // Report revision and frame presence are the occurrence boundaries. The
60
+ // latter cancels active motion when a connection loses its measured frame.
61
+ // latestInputRef carries the complete snapshot without making styles effect keys.
62
+ // biome-ignore lint/correctness/useExhaustiveDependencies: revision intentionally owns the occurrence
63
+ useLayoutEffect(() => {
64
+ const currentInput = latestInputRef.current;
65
+ const stopAnimations = () => {
66
+ var _a;
67
+ clearTimeout(pendingStartRef.current);
68
+ pendingStartRef.current = undefined;
69
+ for (const animation of activeAnimationsRef.current) {
70
+ (_a = animation.stop) === null || _a === void 0 ? void 0 : _a.call(animation);
71
+ }
72
+ activeAnimationsRef.current = [];
73
+ };
74
+ const frame = currentInput.frame;
75
+ const animatedValues = animatedValuesRef.current;
76
+ const previousFrame = previousFrameRef.current;
77
+ previousFrameRef.current = frame;
78
+ if (!animationRuntime || !frame || !animatedValues) {
79
+ animationAttemptRef.current += 1;
80
+ stopAnimations();
81
+ activeScheduleRef.current = undefined;
82
+ presentedStylesRef.current = currentInput.styles;
83
+ setIsAnimating(false);
84
+ return;
85
+ }
86
+ const schedule = currentInput.schedule;
87
+ const transitionPolicy = currentInput.transitionPolicy;
88
+ if (schedule === undefined ||
89
+ transitionPolicy === undefined ||
90
+ consumedScheduleRef.current === schedule ||
91
+ previousFrame === null) {
92
+ animationAttemptRef.current += 1;
93
+ stopAnimations();
94
+ activeScheduleRef.current = undefined;
95
+ presentedStylesRef.current = currentInput.styles;
96
+ for (const property of FRAME_PROPERTIES) {
97
+ animatedValues[property].setValue(frame[property]);
98
+ }
99
+ setIsAnimating(false);
100
+ return;
101
+ }
102
+ consumedScheduleRef.current = schedule;
103
+ const execution = resolveWidgetLayoutTransitionExecution({
104
+ durationMs: transitionPolicy.durationMs,
105
+ nowEpochMs: Date.now(),
106
+ schedule,
107
+ });
108
+ if (execution.isComplete) {
109
+ animationAttemptRef.current += 1;
110
+ stopAnimations();
111
+ activeScheduleRef.current = undefined;
112
+ presentedStylesRef.current = currentInput.styles;
113
+ for (const property of FRAME_PROPERTIES) {
114
+ animatedValues[property].setValue(frame[property]);
115
+ }
116
+ setIsAnimating(false);
117
+ return;
118
+ }
119
+ animationAttemptRef.current += 1;
120
+ const animationAttempt = animationAttemptRef.current;
121
+ stopAnimations();
122
+ activeScheduleRef.current = schedule;
123
+ setIsAnimating(true);
124
+ const startAnimations = () => {
125
+ if (animationAttemptRef.current !== animationAttempt) {
126
+ return;
127
+ }
128
+ pendingStartRef.current = undefined;
129
+ const liveFrame = {};
130
+ let remainingSamples = FRAME_PROPERTIES.length;
131
+ for (const property of FRAME_PROPERTIES) {
132
+ animatedValues[property].stopAnimation((value) => {
133
+ var _a, _b;
134
+ if (animationAttemptRef.current !== animationAttempt) {
135
+ return;
136
+ }
137
+ liveFrame[property] = value;
138
+ remainingSamples -= 1;
139
+ if (remainingSamples !== 0) {
140
+ return;
141
+ }
142
+ const currentExecution = resolveWidgetLayoutTransitionExecution({
143
+ durationMs: transitionPolicy.durationMs,
144
+ nowEpochMs: Date.now(),
145
+ schedule,
146
+ });
147
+ if (currentExecution.isComplete) {
148
+ presentedStylesRef.current = currentInput.styles;
149
+ for (const frameProperty of FRAME_PROPERTIES) {
150
+ animatedValues[frameProperty].setValue(frame[frameProperty]);
151
+ }
152
+ activeScheduleRef.current = undefined;
153
+ setIsAnimating(false);
154
+ return;
155
+ }
156
+ const elapsedProgress = Math.min(1, currentExecution.elapsedMs / transitionPolicy.durationMs);
157
+ const resumedEasing = currentExecution.elapsedMs > 0
158
+ ? resolveWidgetLayoutResumedEasing({
159
+ easing: transitionPolicy.easing,
160
+ elapsedProgress,
161
+ remainingProgress: 0,
162
+ })
163
+ : undefined;
164
+ if (resumedEasing) {
165
+ for (const frameProperty of FRAME_PROPERTIES) {
166
+ animatedValues[frameProperty].setValue(liveFrame[frameProperty] +
167
+ (frame[frameProperty] - liveFrame[frameProperty]) *
168
+ resumedEasing.initialProgress);
169
+ }
170
+ }
171
+ const easing = resumedEasing
172
+ ? (remainingProgress) => resolveWidgetLayoutResumedEasing({
173
+ easing: transitionPolicy.easing,
174
+ elapsedProgress,
175
+ remainingProgress,
176
+ }).progress
177
+ : (_b = (_a = animationRuntime.Easing) === null || _a === void 0 ? void 0 : _a.bezier) === null || _b === void 0 ? void 0 : _b.call(_a, ...transitionPolicy.easing);
178
+ let remainingAnimations = FRAME_PROPERTIES.length;
179
+ let allAnimationsFinished = true;
180
+ const targetStyles = currentInput.styles;
181
+ const animations = FRAME_PROPERTIES.map((frameProperty) => animationRuntime.Animated.timing(animatedValues[frameProperty], {
182
+ duration: currentExecution.durationMs,
183
+ easing,
184
+ toValue: frame[frameProperty],
185
+ useNativeDriver: false,
186
+ }));
187
+ activeAnimationsRef.current = animations;
188
+ for (const animation of animations) {
189
+ animation.start((result) => {
190
+ if (animationAttemptRef.current !== animationAttempt) {
191
+ return;
192
+ }
193
+ allAnimationsFinished && (allAnimationsFinished = (result === null || result === void 0 ? void 0 : result.finished) !== false);
194
+ remainingAnimations -= 1;
195
+ if (remainingAnimations !== 0) {
196
+ return;
197
+ }
198
+ activeAnimationsRef.current = [];
199
+ activeScheduleRef.current = undefined;
200
+ if (allAnimationsFinished) {
201
+ presentedStylesRef.current = targetStyles;
202
+ }
203
+ setIsAnimating(false);
204
+ });
205
+ }
206
+ });
207
+ }
208
+ };
209
+ // RN's timing delay starts a fresh duration after its timer fires. Own the
210
+ // wait so a late JS startup samples and resumes the original schedule.
211
+ if (execution.delayMs > 0) {
212
+ pendingStartRef.current = setTimeout(startAnimations, execution.delayMs);
213
+ }
214
+ else {
215
+ startAnimations();
216
+ }
217
+ }, [animationRuntime, input.revision, hasFrame]);
218
+ useLayoutEffect(() => () => {
219
+ var _a;
220
+ animationAttemptRef.current += 1;
221
+ clearTimeout(pendingStartRef.current);
222
+ pendingStartRef.current = undefined;
223
+ for (const animation of activeAnimationsRef.current) {
224
+ (_a = animation.stop) === null || _a === void 0 ? void 0 : _a.call(animation);
225
+ }
226
+ activeAnimationsRef.current = [];
227
+ }, []);
228
+ const animatedValues = animatedValuesRef.current;
229
+ if (!shouldUseAnimatedStyles || animatedValues === null) {
230
+ return Object.assign(Object.assign({}, input.styles), { frameView: animatedFrameView });
231
+ }
232
+ const radiusStyles = {
233
+ borderBottomLeftRadius: animatedValues.bottomLeftRadius,
234
+ borderBottomRightRadius: animatedValues.bottomRightRadius,
235
+ borderTopLeftRadius: animatedValues.topLeftRadius,
236
+ borderTopRightRadius: animatedValues.topRightRadius,
237
+ };
238
+ return {
239
+ border: Object.assign(Object.assign({}, presentedStylesRef.current.border), radiusStyles),
240
+ content: Object.assign(Object.assign({}, presentedStylesRef.current.content), radiusStyles),
241
+ frame: Object.assign(Object.assign(Object.assign({}, presentedStylesRef.current.frame), radiusStyles), { height: animatedValues.height, width: animatedValues.width }),
242
+ frameView: animatedFrameView,
243
+ };
244
+ }
@@ -1,9 +1,11 @@
1
+ import type { WidgetLayoutTransitionSchedule } from "@getuserfeedback/protocol/host";
1
2
  import { type ComponentType, type ReactElement, type ReactNode } from "react";
2
3
  import type { NativeViewRecord } from "./native-view-record-controller.js";
3
4
  export type NativeViewPresentationMode = "content" | "overlay";
4
- export declare function NativeViewPresentationSurface({ children, presentationMode, nativeViewComponent, record, }: {
5
+ export declare function NativeViewPresentationSurface({ children, presentationMode, nativeViewComponent, record, transitionSchedule, }: {
5
6
  children?: ReactNode;
6
7
  presentationMode?: NativeViewPresentationMode;
7
8
  nativeViewComponent?: ComponentType<Record<string, unknown>>;
8
9
  record: NativeViewRecord;
10
+ transitionSchedule?: WidgetLayoutTransitionSchedule;
9
11
  }): ReactElement;
@@ -1,4 +1,5 @@
1
1
  import { createElement, } from "react";
2
+ import { useNativeViewFrameTransition, } from "./native-view-layout-transition.js";
2
3
  import { toNativeWidgetCornerRadii } from "./native-widget-corner-radii.js";
3
4
  import { toNativeWidgetResolvedBorderStyle } from "./native-widget-frame-border.js";
4
5
  import { toNativeWidgetFrameShadow, toNativeWidgetSupportedBoxShadow, } from "./native-widget-frame-shadow.js";
@@ -55,16 +56,20 @@ const resolveNativeView = () => {
55
56
  }
56
57
  return reactNativeModule.View;
57
58
  };
58
- export function NativeViewPresentationSurface({ children, presentationMode = "overlay", nativeViewComponent, record, }) {
59
- var _a, _b, _c;
59
+ // TODO(architecture): Land native-host dismissal and controller dismissal in the same vertical slice.
60
+ // [principle=host-adapter-boundary] [scope=slice]
61
+ // [epic=runtime-neutral-view-host-orchestration] [epic_order=3] [priority=medium]
62
+ export function NativeViewPresentationSurface({ children, presentationMode = "overlay", nativeViewComponent, record, transitionSchedule, }) {
63
+ var _a;
60
64
  const NativeView = nativeViewComponent !== null && nativeViewComponent !== void 0 ? nativeViewComponent : resolveNativeView();
61
65
  const isPresented = record.isActive && record.size !== null;
62
66
  const hostStyle = presentationMode === "content" && isPresented
63
- ? Object.assign(Object.assign({}, CONTENT_HOST_SURFACE_STYLE), { height: (_a = record.size) === null || _a === void 0 ? void 0 : _a.height, width: (_b = record.size) === null || _b === void 0 ? void 0 : _b.width }) : Object.assign(Object.assign(Object.assign({}, HOST_SURFACE_STYLE), (((_c = record.size) === null || _c === void 0 ? void 0 : _c.placement) === "docked-top"
64
- ? TOP_PLACEMENT_STYLE
65
- : BOTTOM_PLACEMENT_STYLE)), (isPresented
66
- ? PRESENTED_HOST_SURFACE_STYLE
67
- : HIDDEN_HOST_SURFACE_STYLE));
67
+ ? CONTENT_HOST_SURFACE_STYLE
68
+ : Object.assign(Object.assign(Object.assign({}, HOST_SURFACE_STYLE), (((_a = record.size) === null || _a === void 0 ? void 0 : _a.placement) === "docked-top"
69
+ ? TOP_PLACEMENT_STYLE
70
+ : BOTTOM_PLACEMENT_STYLE)), (isPresented
71
+ ? PRESENTED_HOST_SURFACE_STYLE
72
+ : HIDDEN_HOST_SURFACE_STYLE));
68
73
  return createElement(NativeView, {
69
74
  accessibilityElementsHidden: !isPresented,
70
75
  importantForAccessibility: isPresented ? "auto" : "no-hide-descendants",
@@ -75,10 +80,11 @@ export function NativeViewPresentationSurface({ children, presentationMode = "ov
75
80
  children,
76
81
  nativeViewComponent,
77
82
  record,
83
+ transitionSchedule,
78
84
  }));
79
85
  }
80
- function NativeViewPresentationFrame({ children, nativeViewComponent, record, }) {
81
- var _a, _b, _c, _d, _e, _f, _g, _h;
86
+ function NativeViewPresentationFrame({ children, nativeViewComponent, record, transitionSchedule, }) {
87
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
82
88
  const NativeView = nativeViewComponent !== null && nativeViewComponent !== void 0 ? nativeViewComponent : resolveNativeView();
83
89
  const isPresented = record.isActive && record.size !== null;
84
90
  const hasMeasuredFrame = isPresented || ((_a = record.size) === null || _a === void 0 ? void 0 : _a.layoutTransactionId) !== undefined;
@@ -100,17 +106,43 @@ function NativeViewPresentationFrame({ children, nativeViewComponent, record, })
100
106
  borderTopLeftRadius: cornerRadii.topLeft,
101
107
  borderTopRightRadius: cornerRadii.topRight,
102
108
  };
103
- return createElement(NativeView, {
104
- pointerEvents: isPresented ? "auto" : "none",
105
- style: hasMeasuredFrame
109
+ const targetStyles = {
110
+ border: Object.assign(Object.assign(Object.assign(Object.assign({}, VIEW_FRAME_BORDER_OVERLAY_STYLE), cornerRadiusStyle), borderStyle), frameOverlayShadowStyle),
111
+ content: Object.assign(Object.assign({}, VIEW_FRAME_CONTENT_STYLE), cornerRadiusStyle),
112
+ frame: hasMeasuredFrame
106
113
  ? Object.assign(Object.assign(Object.assign(Object.assign({}, MEASURED_VIEW_FRAME_STYLE), cornerRadiusStyle), frameShadowStyle), { 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,
114
+ };
115
+ const frame = hasMeasuredFrame && record.size
116
+ ? {
117
+ bottomLeftRadius: cornerRadii.bottomLeft,
118
+ bottomRightRadius: cornerRadii.bottomRight,
119
+ height: record.size.height,
120
+ topLeftRadius: cornerRadii.topLeft,
121
+ topRightRadius: cornerRadii.topRight,
122
+ width: record.size.width,
123
+ }
124
+ : null;
125
+ const transition = useNativeViewFrameTransition({
126
+ frame,
127
+ nativeView: NativeView,
128
+ revision: record.layoutReportRevision,
129
+ schedule: transitionSchedule,
130
+ styles: targetStyles,
131
+ transitionPolicy: ((_k = (_j = record.size) === null || _j === void 0 ? void 0 : _j.transitionPolicy) === null || _k === void 0 ? void 0 : _k.kind) === "timed"
132
+ ? record.size.transitionPolicy
133
+ : undefined,
134
+ });
135
+ const FrameView = transition.frameView;
136
+ return createElement(FrameView, {
137
+ pointerEvents: isPresented ? "auto" : "none",
138
+ style: transition.frame,
107
139
  testID: `gx-native-view-frame-${record.viewId}`,
108
- }, createElement(NativeView, {
109
- style: Object.assign(Object.assign({}, VIEW_FRAME_CONTENT_STYLE), cornerRadiusStyle),
140
+ }, createElement(FrameView, {
141
+ style: transition.content,
110
142
  testID: `gx-native-view-frame-content-${record.viewId}`,
111
- }, children), createElement(NativeView, {
143
+ }, children), createElement(FrameView, {
112
144
  pointerEvents: "none",
113
- style: Object.assign(Object.assign(Object.assign(Object.assign({}, VIEW_FRAME_BORDER_OVERLAY_STYLE), cornerRadiusStyle), borderStyle), frameOverlayShadowStyle),
145
+ style: transition.border,
114
146
  testID: `gx-native-view-frame-border-${record.viewId}`,
115
147
  }));
116
148
  }
@@ -1,10 +1,15 @@
1
+ import { type WidgetLayoutTransitionSchedule } from "@getuserfeedback/protocol/host";
2
+ import type { BrowserNavigationAvailabilityPayload } from "@getuserfeedback/protocol/internal/navigation-rpc";
1
3
  import { type EventViewSize } from "@getuserfeedback/protocol/internal/view-host-control-messages";
2
4
  import type { OpenRequestDispatcher } from "@getuserfeedback/sdk/internal/open-request-dispatcher";
3
5
  import type { CoreWebViewHostConnection } from "./core-webview-host-controller.js";
4
6
  import type { ViewWebViewHostConnection } from "./view-webview-host-controller.js";
5
- type NativeViewSizeReport = Omit<EventViewSize, "gen" | "t" | "viewId">;
7
+ type NativeViewSizeReport = Omit<EventViewSize, "gen" | "t" | "viewId"> & {
8
+ transitionSchedule?: WidgetLayoutTransitionSchedule;
9
+ };
6
10
  export type NativeViewRecord = Readonly<{
7
11
  allocationId: number;
12
+ flowId: string | null;
8
13
  generation: number;
9
14
  isActive: boolean;
10
15
  layoutReportRevision: number;
@@ -39,6 +44,7 @@ export type NativeViewRecordController = {
39
44
  type NativeOpenRequestObserver = (payload: Parameters<OpenRequestDispatcher["dispatch"]>[0]) => ReturnType<OpenRequestDispatcher["dispatch"]>;
40
45
  export declare function createNativeViewRecordController(input: {
41
46
  coreConnection: CoreWebViewHostConnection;
47
+ getBrowserNavigationAvailability?: () => BrowserNavigationAvailabilityPayload | undefined;
42
48
  onError: (error: Error) => void;
43
49
  observeOpenRequest?: NativeOpenRequestObserver;
44
50
  }): NativeViewRecordController;