@momo-kits/foundation 0.162.2-test.9 → 0.162.3-fontscale.1

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,7 +1,12 @@
1
- import React, { Ref, useContext, useEffect, useRef } from 'react';
1
+ import React, { Ref, useContext } from 'react';
2
2
  import LinearGradient from 'react-native-linear-gradient';
3
+ import { Dimensions, Platform, StyleSheet, View } from 'react-native';
4
+ import Animated, {
5
+ Extrapolation,
6
+ interpolate,
7
+ useAnimatedStyle,
8
+ } from 'react-native-reanimated';
3
9
  import { ApplicationContext, MiniAppContext } from '../../Context';
4
- import { Animated, Dimensions, Platform, StyleSheet, View } from 'react-native';
5
10
  import { HeaderType } from '../../Layout/types';
6
11
  import { InputRef, InputSearch } from '../../Input';
7
12
  import Navigation from '../Navigation';
@@ -9,20 +14,19 @@ import { Colors, Radius, Spacing } from '../../Consts';
9
14
  import { Image } from '../../Image';
10
15
  import { SearchHeaderProps } from '../types';
11
16
  import BackgroundImageView from './BackgroundImageView';
17
+ import {
18
+ AnimatedCompatValue,
19
+ useNormalizedSharedValue,
20
+ } from '../utils';
12
21
 
13
22
  const SCREEN_PADDING = 12;
14
23
  const BACK_WIDTH = 28;
15
24
 
16
25
  const { width: screenWidth } = Dimensions.get('window');
17
- const LinearGradientAnimated = Animated.createAnimatedComponent(LinearGradient);
18
26
 
19
- /**
20
- * Header extended with background image
21
- * @constructor
22
- */
23
27
  const HeaderExtendHeader: React.FC<{
24
28
  headerType?: HeaderType;
25
- animatedValue: Animated.Value;
29
+ animatedValue: AnimatedCompatValue;
26
30
  heightHeader: number;
27
31
  headerRightWidth: number;
28
32
  inputSearchProps?: SearchHeaderProps;
@@ -38,118 +42,131 @@ const HeaderExtendHeader: React.FC<{
38
42
  headerRightWidth = 73,
39
43
  inputSearchProps,
40
44
  inputSearchRef,
41
- useShadowHeader = true,
45
+ useShadowHeader: useShadowHeaderProp = true,
42
46
  gradientColor: customGradientColor,
43
47
  headerBackground: customBackground,
44
48
  }) => {
45
49
  const { theme } = useContext(ApplicationContext);
46
50
  const context = useContext<any>(MiniAppContext);
47
- const animated = useRef(new Animated.Value(0));
51
+ const sharedValue = useNormalizedSharedValue(animatedValue);
48
52
  const gradientColor = customGradientColor ?? theme.colors.gradient;
49
53
  const headerBackground = customBackground ?? theme.assets?.headerBackground;
50
54
  const leftPosition = inputSearchProps?.leftPosition || BACK_WIDTH + 20;
51
55
 
52
- const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
56
+ let useShadowHeader = useShadowHeaderProp;
57
+ if (inputSearchProps && Platform.OS === 'android') {
58
+ useShadowHeader = false;
59
+ }
53
60
 
54
- const opacityBackground = animatedValue?.interpolate({
55
- inputRange: [0, 52],
56
- outputRange: [0, 1],
57
- extrapolate: 'clamp',
58
- });
59
- const opacityGradient = animatedValue?.interpolate({
60
- inputRange: [0, 52],
61
- outputRange: [1, 0],
62
- extrapolate: 'clamp',
63
- });
61
+ const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
64
62
 
65
- useEffect(() => {
66
- const listener = animatedValue.addListener(({ value }) => {
67
- animated.current.setValue(value);
68
- });
69
- return () => {
70
- animatedValue?.removeListener(listener);
71
- };
72
- }, [animatedValue]);
63
+ const heightStyle = useAnimatedStyle(() => ({
64
+ height: interpolate(
65
+ sharedValue.value,
66
+ [0, 100],
67
+ [heightHeader + 52, heightHeader],
68
+ Extrapolation.CLAMP,
69
+ ),
70
+ }));
73
71
 
74
- const height = animated.current.interpolate({
75
- inputRange: [0, 100],
76
- outputRange: [heightHeader + 52, heightHeader],
77
- extrapolate: 'clamp',
78
- });
72
+ const gradientOpacityStyle = useAnimatedStyle(() => ({
73
+ opacity: interpolate(sharedValue.value, [0, 52], [1, 0], Extrapolation.CLAMP),
74
+ }));
79
75
 
80
- const translateX = animated.current.interpolate({
81
- inputRange: [0, 100],
82
- outputRange: [SCREEN_PADDING, leftPosition],
83
- extrapolate: 'clamp',
84
- });
85
-
86
- const backgroundColor = animated.current.interpolate({
87
- inputRange: [0, 100],
88
- outputRange: [
89
- theme.colors.background.surface,
90
- theme.colors.background.default,
76
+ const searchTranslateStyle = useAnimatedStyle(() => ({
77
+ transform: [
78
+ {
79
+ translateX: interpolate(
80
+ sharedValue.value,
81
+ [0, 100],
82
+ [SCREEN_PADDING, leftPosition],
83
+ Extrapolation.CLAMP,
84
+ ),
85
+ },
91
86
  ],
92
- extrapolate: 'clamp',
87
+ width: interpolate(
88
+ sharedValue.value,
89
+ [0, 100],
90
+ [
91
+ screenWidth - SCREEN_PADDING * 2,
92
+ screenWidth - leftPosition - 12 - headerRightWidth,
93
+ ],
94
+ Extrapolation.CLAMP,
95
+ ),
96
+ }));
97
+
98
+ const searchBackgroundStyle = useAnimatedStyle(() => {
99
+ const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
100
+ return { backgroundColor: t === 0
101
+ ? theme.colors.background.surface
102
+ : theme.colors.background.default };
93
103
  });
94
104
 
95
- if (inputSearchProps && Platform.OS === 'android') {
96
- useShadowHeader = false;
97
- }
105
+ const extendedHeightStyle = useAnimatedStyle(() => ({
106
+ height: interpolate(
107
+ sharedValue.value,
108
+ [0, 100],
109
+ [heightHeader + 52, heightHeader],
110
+ Extrapolation.CLAMP,
111
+ ),
112
+ }));
98
113
 
99
114
  if (inputSearchProps) {
100
115
  return (
101
116
  <View style={[{ zIndex: 0 }, showBaseLineDebug && styles.debugBaseLine]}>
102
- <Animated.View style={{ height: height }} />
117
+ <Animated.View style={heightStyle} />
103
118
  <BackgroundImageView
104
119
  useShadowHeader={useShadowHeader}
105
120
  heightHeader={heightHeader}
106
- opacityBackground={opacityBackground}
121
+ animatedValue={sharedValue}
107
122
  headerBackground={headerBackground}
108
123
  />
109
124
  <Animated.View
110
125
  style={[
111
126
  styles.headerBox,
112
- { height: headerType === 'extended' ? height : heightHeader },
127
+ headerType === 'extended'
128
+ ? heightStyle
129
+ : { height: heightHeader },
113
130
  ]}
114
131
  >
115
132
  {!!gradientColor && (
116
- <LinearGradientAnimated
117
- colors={[gradientColor, gradientColor + '00']}
118
- style={[styles.extendedHeader, { opacity: opacityGradient }]}
133
+ <Animated.View
134
+ style={[styles.extendedHeader, gradientOpacityStyle]}
119
135
  >
120
- {!!theme.assets?.headerBackground && (
121
- <Image
122
- style={styles.headerBackground}
123
- source={{ uri: theme.assets?.headerBackground }}
124
- loading={false}
125
- />
126
- )}
127
- </LinearGradientAnimated>
136
+ <LinearGradient
137
+ colors={[gradientColor, gradientColor + '00']}
138
+ style={StyleSheet.absoluteFill}
139
+ >
140
+ {!!theme.assets?.headerBackground && (
141
+ <Image
142
+ style={styles.headerBackground}
143
+ source={{ uri: theme.assets?.headerBackground }}
144
+ loading={false}
145
+ />
146
+ )}
147
+ </LinearGradient>
148
+ </Animated.View>
128
149
  )}
129
150
  </Animated.View>
130
151
  <Animated.View
131
- style={{
132
- justifyContent: 'flex-end',
133
- height,
134
- position: 'absolute',
135
- zIndex: 2,
136
- }}
152
+ style={[
153
+ {
154
+ justifyContent: 'flex-end',
155
+ position: 'absolute',
156
+ zIndex: 2,
157
+ },
158
+ heightStyle,
159
+ ]}
137
160
  >
138
161
  <Animated.View
139
- style={{
140
- transform: [{ translateX }],
141
- marginVertical: Spacing.S,
142
- width: animated.current.interpolate({
143
- inputRange: [0, 100],
144
- outputRange: [
145
- screenWidth - SCREEN_PADDING * 2,
146
- screenWidth - leftPosition - 12 - headerRightWidth,
147
- ],
148
- extrapolate: 'clamp',
149
- }),
150
- }}
162
+ style={[{ marginVertical: Spacing.S }, searchTranslateStyle]}
151
163
  >
152
- <Animated.View style={{ backgroundColor, borderRadius: Radius.XL }}>
164
+ <Animated.View
165
+ style={[
166
+ { borderRadius: Radius.XL },
167
+ searchBackgroundStyle,
168
+ ]}
169
+ >
153
170
  <InputSearch
154
171
  {...inputSearchProps}
155
172
  ref={inputSearchRef}
@@ -169,22 +186,26 @@ const HeaderExtendHeader: React.FC<{
169
186
  <BackgroundImageView
170
187
  useShadowHeader={useShadowHeader}
171
188
  heightHeader={heightHeader}
172
- opacityBackground={opacityBackground}
189
+ animatedValue={sharedValue}
173
190
  headerBackground={headerBackground}
174
191
  />
175
192
  {!!gradientColor && (
176
- <LinearGradientAnimated
177
- colors={[gradientColor, gradientColor + '00']}
178
- style={[styles.extendedHeader, { opacity: opacityGradient }]}
193
+ <Animated.View
194
+ style={[styles.extendedHeader, gradientOpacityStyle, extendedHeightStyle]}
179
195
  >
180
- {!!headerBackground && (
181
- <Image
182
- style={styles.headerBackground}
183
- source={{ uri: headerBackground }}
184
- loading={false}
185
- />
186
- )}
187
- </LinearGradientAnimated>
196
+ <LinearGradient
197
+ colors={[gradientColor, gradientColor + '00']}
198
+ style={StyleSheet.absoluteFill}
199
+ >
200
+ {!!headerBackground && (
201
+ <Image
202
+ style={styles.headerBackground}
203
+ source={{ uri: headerBackground }}
204
+ loading={false}
205
+ />
206
+ )}
207
+ </LinearGradient>
208
+ </Animated.View>
188
209
  )}
189
210
  <View style={{ height: heightHeader }} />
190
211
  </View>
@@ -86,7 +86,7 @@ const HeaderLeft: React.FC<HeaderBackProps> = ({
86
86
 
87
87
  const styles = StyleSheet.create({
88
88
  headerLeft: {
89
- marginLeft: 8,
89
+ marginLeft: 12,
90
90
  },
91
91
  });
92
92
 
@@ -1,11 +1,15 @@
1
1
  import React, { useContext } from 'react';
2
2
  import {
3
- Animated,
4
3
  Dimensions,
5
4
  StyleSheet,
6
5
  TouchableOpacity,
7
6
  View,
8
7
  } from 'react-native';
8
+ import Animated, {
9
+ Extrapolation,
10
+ interpolate,
11
+ useAnimatedStyle,
12
+ } from 'react-native-reanimated';
9
13
  import { ApplicationContext, MiniAppContext } from '../../Context';
10
14
  import { exportFontFamily, Text, useScaleSize } from '../../Text';
11
15
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
@@ -17,22 +21,51 @@ import {
17
21
  import { Image } from '../../Image';
18
22
  import { Icon } from '../../Icon';
19
23
  import { Skeleton } from '../../Skeleton';
24
+ import {
25
+ AnimatedCompatValue,
26
+ useNormalizedSharedValue,
27
+ } from '../utils';
28
+
29
+ type HeaderTitleInterpolate = {
30
+ inputRange: number[];
31
+ outputRange: number[];
32
+ };
33
+
34
+ type HeaderTitleExtraProps = {
35
+ animatedValue?: AnimatedCompatValue;
36
+ interpolate?: HeaderTitleInterpolate;
37
+ tintColor?: string;
38
+ children?: React.ReactNode;
39
+ };
20
40
 
21
41
  /**
22
42
  * default header title used for nav
23
43
  */
24
- const HeaderTitle: React.FC<any> = props => {
44
+ const HeaderTitle: React.FC<HeaderTitleExtraProps & { [key: string]: any }> = props => {
25
45
  const context = useContext<any>(MiniAppContext);
26
46
 
27
47
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
28
48
 
29
- const opacity = props.animatedValue?.interpolate(
30
- props.interpolate ?? {
31
- inputRange: [0, 200],
32
- outputRange: [0, 1],
33
- extrapolate: 'clamp',
34
- },
35
- );
49
+ const interpolateConfig = props.interpolate ?? {
50
+ inputRange: [0, 200],
51
+ outputRange: [0, 1],
52
+ };
53
+ const hasAnimated = props.animatedValue != null;
54
+ const sharedValue = useNormalizedSharedValue(props.animatedValue);
55
+
56
+ const animatedStyle = useAnimatedStyle(() => {
57
+ if (!hasAnimated) {
58
+ return { opacity: 1 };
59
+ }
60
+ return {
61
+ opacity: interpolate(
62
+ sharedValue.value,
63
+ interpolateConfig.inputRange,
64
+ interpolateConfig.outputRange,
65
+ Extrapolation.CLAMP,
66
+ ),
67
+ };
68
+ });
36
69
 
37
70
  return (
38
71
  <View
@@ -57,9 +90,9 @@ const HeaderTitle: React.FC<any> = props => {
57
90
  },
58
91
  {
59
92
  fontFamily: exportFontFamily('bold', 'action_xs_bold'),
60
- opacity,
61
93
  color: props.tintColor,
62
94
  },
95
+ animatedStyle,
63
96
  ]}
64
97
  numberOfLines={1}
65
98
  />
@@ -1,16 +1,17 @@
1
1
  import { Colors, Radius, Spacing, Styles } from '../../Consts';
2
2
  import { InputRef, InputSearch } from '../../Input';
3
3
  import {
4
- Animated,
5
4
  Dimensions,
6
5
  StyleSheet,
7
6
  TouchableOpacity,
8
7
  View,
9
8
  } from 'react-native';
10
- import React, { useContext, useEffect, useRef } from 'react';
9
+ import Animated, { useAnimatedStyle } from 'react-native-reanimated';
10
+ import React, { useContext } from 'react';
11
11
  import { SearchHeaderProps } from '../types';
12
12
  import { ApplicationContext, MiniAppContext } from '../../Context';
13
13
  import { Text } from '../../Text';
14
+ import { useNormalizedSharedValue } from '../utils';
14
15
 
15
16
  const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
16
17
  (
@@ -28,28 +29,17 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
28
29
  const BACK_WIDTH = 28;
29
30
  const { width: screenWidth } = Dimensions.get('window');
30
31
 
31
- const animated = useRef(new Animated.Value(0));
32
+ const sharedValue = useNormalizedSharedValue(animatedValue);
32
33
  const leftPosition = props?.leftPosition ?? BACK_WIDTH + 20;
33
34
  const headerRightWidth = props?.headerRightWidth ?? 73;
34
35
 
35
- useEffect(() => {
36
- const listener = animatedValue?.addListener(({ value }) => {
37
- animated.current.setValue(value);
38
- });
39
- return () => {
40
- if (listener) {
41
- animatedValue?.removeListener(listener);
42
- }
36
+ const backgroundStyle = useAnimatedStyle(() => {
37
+ const t = Math.min(Math.max(sharedValue.value / 100, 0), 1);
38
+ return {
39
+ backgroundColor: t === 0
40
+ ? theme.colors.background.surface
41
+ : theme.colors.background.default,
43
42
  };
44
- }, [animatedValue]);
45
-
46
- const backgroundColor = animated.current?.interpolate({
47
- inputRange: [0, 100],
48
- outputRange: [
49
- theme.colors.background.surface,
50
- theme.colors.background.default,
51
- ],
52
- extrapolate: 'clamp',
53
43
  });
54
44
 
55
45
  const goBack = () => {
@@ -91,7 +81,9 @@ const SearchHeader = React.forwardRef<InputRef, SearchHeaderProps>(
91
81
  },
92
82
  ]}
93
83
  >
94
- <Animated.View style={{ backgroundColor, borderRadius: Radius.XL }}>
84
+ <Animated.View
85
+ style={[{ borderRadius: Radius.XL }, backgroundStyle]}
86
+ >
95
87
  <InputSearch
96
88
  ref={ref}
97
89
  {...props}
@@ -19,7 +19,7 @@ import Navigator from './Navigator';
19
19
  import ScaleSizeProvider from './ScaleSizeProvider';
20
20
  import { getModalOptions, getStackOptions } from './utils';
21
21
  import { NavigationContainerProps } from './types';
22
- import { ApplicationContext, FontScaleConfig, MiniAppContext } from '../Context';
22
+ import { ApplicationContext, MiniAppContext } from '../Context';
23
23
  import Localize from './Localize';
24
24
  import { Colors, defaultTheme } from '../Consts';
25
25
  import { HeaderLeft } from './Components/HeaderLeft';
@@ -29,9 +29,6 @@ import { HeaderBackground } from './Components/HeaderBackground';
29
29
 
30
30
  const Stack = createStackNavigator();
31
31
 
32
- // Observer storage key the host app writes FontScaleConfig to. Lowercased natively.
33
- const FONT_SCALE_OBSERVER_KEY = 'font_scale_config';
34
-
35
32
  const NavigationContainer: React.FC<NavigationContainerProps> = ({
36
33
  screen,
37
34
  theme = defaultTheme,
@@ -48,11 +45,6 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
48
45
  }) => {
49
46
  const context = useContext<any>(MiniAppContext);
50
47
  const [currentContext, setCurrentContext] = useState<any>({});
51
- // Default = the host-seeded fontScaleConfig from MiniAppContext, so the first frame is already
52
- // scaled (no flash). The observer below overwrites it on later host writes (live rate changes).
53
- const [observerFontScaleConfig, setObserverFontScaleConfig] = useState<
54
- FontScaleConfig | undefined
55
- >(context?.fontScaleConfig);
56
48
 
57
49
  const mergedContext = {
58
50
  ...context,
@@ -63,27 +55,10 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
63
55
  },
64
56
  };
65
57
 
66
- useEffect(() => {
67
- // The initial value is seeded by the host into MiniAppContext and used as this state's default
68
- // above, so the first frame is already scaled (no flash). Here we only subscribe to later host
69
- // writes so a rate change applies live while the miniapp is open.
70
- const sub = maxApi?.observer?.(
71
- FONT_SCALE_OBSERVER_KEY,
72
- (data: FontScaleConfig) => {
73
- setObserverFontScaleConfig(data ?? undefined);
74
- },
75
- );
76
- return () => {
77
- sub?.remove?.();
78
- };
79
- }, [maxApi]);
80
-
81
58
  return (
82
59
  <SafeAreaProvider>
83
60
  <MiniAppContext.Provider value={mergedContext}>
84
- <ScaleSizeProvider
85
- fontScaleConfig={observerFontScaleConfig}
86
- >
61
+ <ScaleSizeProvider scaleSizeMaxRate={mergedContext?.scaleSizeMaxRate}>
87
62
  <Navigation
88
63
  screen={screen}
89
64
  theme={theme}
@@ -142,6 +117,12 @@ const Navigation: React.FC<any> = ({
142
117
  },
143
118
  );
144
119
 
120
+ // TEMP: verify FontScale MaxApi end-to-end — remove after test
121
+ // @ts-ignore getFontScale added in newer @momo-platform/api
122
+ maxApi?.getFontScale?.((scale: any) =>
123
+ console.log('[FontScale][momo-kit] getFontScale ->', scale),
124
+ );
125
+
145
126
  return () => {
146
127
  subscription?.remove?.();
147
128
  };
@@ -1,15 +1,13 @@
1
- import React, { FC, useContext } from 'react';
1
+ import React, { FC } from 'react';
2
2
  import { ScaleSizeContext } from '../Context';
3
3
  import { ScaleSizeProviderProps } from './types';
4
4
 
5
5
  const ScaleSizeProvider: FC<ScaleSizeProviderProps> = ({
6
- fontScaleConfig,
6
+ scaleSizeMaxRate,
7
7
  children,
8
8
  }) => {
9
- // Inherit the config from an upper layer (e.g. app root) when the host doesn't set it via MiniAppContext.
10
- const parent = useContext(ScaleSizeContext);
11
9
  return (
12
- <ScaleSizeContext.Provider value={fontScaleConfig ?? parent}>
10
+ <ScaleSizeContext.Provider value={{ scaleSizeMaxRate }}>
13
11
  {children}
14
12
  </ScaleSizeContext.Provider>
15
13
  );
@@ -78,7 +78,7 @@ const WidgetContainer: React.FC<WidgetContainerProps> = ({
78
78
  <SafeAreaProvider>
79
79
  <MiniAppContext.Provider value={mergedContext}>
80
80
  <ScaleSizeProvider
81
- fontScaleConfig={mergedContext?.fontScaleConfig}
81
+ scaleSizeMaxRate={mergedContext?.scaleSizeMaxRate}
82
82
  >
83
83
  <ApplicationContext.Provider
84
84
  value={{
@@ -7,9 +7,9 @@ import {
7
7
  ViewProps,
8
8
  ViewStyle,
9
9
  } from 'react-native';
10
+ import type { SharedValue } from 'react-native-reanimated';
10
11
  import { PopupNotifyProps } from '../Popup/types';
11
12
  import { InputRef, InputSearchProps } from '../Input';
12
- import { FontScaleConfig } from '../Context';
13
13
  import Navigation from './Navigation';
14
14
 
15
15
  export type NavigationProps = {
@@ -109,7 +109,7 @@ export type Theme = {
109
109
  };
110
110
 
111
111
  export type ScaleSizeProviderProps = {
112
- fontScaleConfig?: FontScaleConfig;
112
+ scaleSizeMaxRate?: number;
113
113
  children: ViewProps['children'];
114
114
  };
115
115
 
@@ -259,7 +259,7 @@ export interface HeaderBackProps extends NavigationButtonProps {
259
259
  }
260
260
 
261
261
  export type HeaderBackgroundProps = {
262
- animatedValue?: Animated.Value;
262
+ animatedValue?: Animated.Value | SharedValue<number>;
263
263
  useGradient?: boolean;
264
264
  useShadowHeader?: boolean;
265
265
  backgroundColor?: string;
@@ -299,7 +299,7 @@ export type TitleJourneyProps = {
299
299
  };
300
300
 
301
301
  export interface HeaderAnimatedProps extends ViewProps {
302
- animatedValue: Animated.Value;
302
+ animatedValue: Animated.Value | SharedValue<number>;
303
303
  image: string;
304
304
  useScale?: boolean;
305
305
  }
@@ -343,7 +343,7 @@ export type AnimatedHeader = {
343
343
 
344
344
  export interface SearchHeaderProps extends InputSearchProps {
345
345
  ref?: React.RefObject<InputRef>;
346
- animatedValue?: Animated.Value;
346
+ animatedValue?: Animated.Value | SharedValue<number>;
347
347
  headerRightWidth?: 0 | 74 | 110 | number;
348
348
  leftPosition?: 12 | 48 | number;
349
349
  renderButtons?: () => ReactNode;
@@ -6,6 +6,8 @@ import {
6
6
  import type { HeaderTitleProps, NavigationOptions } from './types';
7
7
  import { Colors, Spacing } from '../Consts';
8
8
  import { Animated, AppState, Platform } from 'react-native';
9
+ import type { SharedValue } from 'react-native-reanimated';
10
+ import { useSharedValue } from 'react-native-reanimated';
9
11
  import {
10
12
  MiniAppContext,
11
13
  ScreenContext,
@@ -60,7 +62,7 @@ const getModalOptions = (): StackNavigationOptions => {
60
62
  */
61
63
  const getOptions = (
62
64
  params: NavigationOptions,
63
- animatedValue?: Animated.Value,
65
+ animatedValue?: SharedValue<number>,
64
66
  ) => {
65
67
  let options: StackNavigationOptions = {};
66
68
 
@@ -131,7 +133,7 @@ const getOptions = (
131
133
 
132
134
  const exportHeaderTitle = (
133
135
  params: NavigationOptions,
134
- animatedValue?: Animated.Value,
136
+ animatedValue?: SharedValue<number>,
135
137
  ): StackNavigationOptions => {
136
138
  if (typeof params.headerTitle === 'object') {
137
139
  return {
@@ -228,6 +230,43 @@ const useAppState = () => {
228
230
  };
229
231
  };
230
232
 
233
+ /**
234
+ * Union of the two animation runtimes an internal foundation component might
235
+ * receive. Public Screen props still accept React Native Animated.Value only;
236
+ * this type keeps lower-level header/floating components compatible while they
237
+ * render with reanimated.
238
+ */
239
+ export type AnimatedCompatValue = Animated.Value | SharedValue<number>;
240
+
241
+ const isRNAnimatedValue = (v: unknown): v is Animated.Value =>
242
+ !!v &&
243
+ typeof (v as Animated.Value).setValue === 'function' &&
244
+ typeof (v as Animated.Value).interpolate === 'function';
245
+
246
+ const isSharedValue = (v: unknown): v is SharedValue<number> =>
247
+ !!v && !isRNAnimatedValue(v) && 'value' in (v as object);
248
+
249
+ const useNormalizedSharedValue = (
250
+ input?: AnimatedCompatValue,
251
+ ): SharedValue<number> => {
252
+ const fallback = useSharedValue(0);
253
+
254
+ useEffect(() => {
255
+ if (!isRNAnimatedValue(input)) {
256
+ return;
257
+ }
258
+ const id = input.addListener(({ value }) => {
259
+ fallback.value = value;
260
+ });
261
+ return () => input.removeListener(id);
262
+ }, [input, fallback]);
263
+
264
+ if (isSharedValue(input)) {
265
+ return input;
266
+ }
267
+ return fallback;
268
+ };
269
+
231
270
  export {
232
271
  getStackOptions,
233
272
  getModalOptions,
@@ -236,4 +275,5 @@ export {
236
275
  setAutomationID,
237
276
  useComponentId,
238
277
  useAppState,
278
+ useNormalizedSharedValue,
239
279
  };