@granite-js/react-native 0.1.34 → 1.0.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +12 -573
  2. package/dist/app/Granite.d.ts +2 -2
  3. package/dist/async-bridges.js +6 -6
  4. package/dist/async-bridges.mjs +2 -2
  5. package/dist/chunk-7GFSQK76.mjs +7 -0
  6. package/dist/constant-bridges.js +4 -4
  7. package/dist/constant-bridges.mjs +2 -2
  8. package/dist/image/Image.d.ts +77 -0
  9. package/dist/image/SvgImage.d.ts +47 -0
  10. package/dist/image/index.d.ts +1 -0
  11. package/dist/image/types.d.ts +3 -0
  12. package/dist/index.d.ts +4 -3
  13. package/dist/intersection-observer/IOFlatList.d.ts +2 -2
  14. package/dist/lottie/Lottie.d.ts +22 -0
  15. package/dist/lottie/ensureSafeLottie.d.ts +3 -0
  16. package/dist/lottie/index.d.ts +1 -0
  17. package/dist/lottie/useFetchResource.d.ts +2 -0
  18. package/dist/native-modules/index.d.ts +0 -1
  19. package/dist/native-modules/natives/GraniteBrownfieldModule.brick.d.ts +14 -0
  20. package/dist/router/Router.d.ts +11 -3
  21. package/dist/router/components/CanGoBackGuard.d.ts +2 -1
  22. package/dist/router/components/ErrorBoundary.d.ts +16 -0
  23. package/dist/router/components/StackNavigator.d.ts +40 -43
  24. package/dist/router/components/useRouterBackHandler.d.ts +3 -3
  25. package/dist/router/createRoute.d.ts +4 -1
  26. package/dist/router/hooks/useRouterControls.d.ts +3 -2
  27. package/dist/router/types/ErrorComponent.d.ts +6 -0
  28. package/dist/router/types/RouteScreen.d.ts +6 -0
  29. package/dist/router/types/index.d.ts +1 -0
  30. package/dist/status-bar/utils.d.ts +2 -2
  31. package/dist/video/Video.d.ts +5 -18
  32. package/package.json +22 -19
  33. package/src/app/Granite.tsx +2 -2
  34. package/src/image/Image.tsx +125 -0
  35. package/src/image/SvgImage.tsx +124 -0
  36. package/src/image/index.ts +1 -0
  37. package/src/image/types.ts +6 -0
  38. package/src/index.ts +14 -3
  39. package/src/intersection-observer/IOFlatList.ts +2 -2
  40. package/src/intersection-observer/withIO.tsx +71 -17
  41. package/src/lottie/Lottie.tsx +87 -0
  42. package/src/lottie/ensureSafeLottie.ts +15 -0
  43. package/src/lottie/index.ts +1 -0
  44. package/src/lottie/useFetchResource.ts +31 -0
  45. package/src/native-modules/index.ts +0 -1
  46. package/src/native-modules/natives/GraniteBrownfieldModule.brick.ts +15 -0
  47. package/src/native-modules/natives/closeView.ts +2 -2
  48. package/src/native-modules/natives/getSchemeUri.ts +2 -2
  49. package/src/router/Router.tsx +40 -12
  50. package/src/router/components/CanGoBackGuard.tsx +2 -3
  51. package/src/router/components/ErrorBoundary.tsx +38 -0
  52. package/src/router/components/useRouterBackHandler.tsx +3 -3
  53. package/src/router/createRoute.ts +6 -2
  54. package/src/router/hooks/useRouterControls.tsx +21 -7
  55. package/src/router/types/ErrorComponent.ts +8 -0
  56. package/src/router/types/RouteScreen.ts +6 -0
  57. package/src/router/types/index.ts +1 -0
  58. package/src/router/utils/screen.tsx +2 -0
  59. package/src/status-bar/utils.ts +2 -2
  60. package/src/types/global.ts +1 -1
  61. package/src/video/Video.tsx +6 -17
  62. package/src/visibility/VisibilityProvider.tsx +3 -3
  63. package/src/visibility/utils/usePrevious.tsx +1 -1
  64. package/dist/blur/BlurView.d.ts +0 -78
  65. package/dist/blur/ReactNativeBlurModule.d.ts +0 -6
  66. package/dist/blur/constants.d.ts +0 -1
  67. package/dist/blur/index.d.ts +0 -1
  68. package/dist/chunk-A3JGM5OI.mjs +0 -7
  69. package/dist/native-event-emitter/eventEmitters/index.d.ts +0 -2
  70. package/dist/native-event-emitter/eventEmitters/types.d.ts +0 -4
  71. package/dist/native-event-emitter/eventEmitters/visibilityChanged.d.ts +0 -10
  72. package/dist/native-event-emitter/index.d.ts +0 -1
  73. package/dist/native-event-emitter/nativeEventEmitter.d.ts +0 -15
  74. package/dist/native-modules/core/GraniteCoreModule.d.ts +0 -8
  75. package/dist/native-modules/natives/GraniteModule.d.ts +0 -7
  76. package/dist/video/instance.d.ts +0 -9
  77. package/src/blur/BlurView.tsx +0 -103
  78. package/src/blur/ReactNativeBlurModule.ts +0 -19
  79. package/src/blur/constants.ts +0 -3
  80. package/src/blur/index.ts +0 -1
  81. package/src/native-event-emitter/eventEmitters/index.ts +0 -3
  82. package/src/native-event-emitter/eventEmitters/types.ts +0 -4
  83. package/src/native-event-emitter/eventEmitters/visibilityChanged.ts +0 -11
  84. package/src/native-event-emitter/index.ts +0 -1
  85. package/src/native-event-emitter/nativeEventEmitter.ts +0 -18
  86. package/src/native-modules/core/GraniteCoreModule.ts +0 -9
  87. package/src/native-modules/natives/GraniteModule.ts +0 -8
  88. package/src/video/instance.tsx +0 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@granite-js/react-native",
3
- "version": "0.1.34",
3
+ "version": "1.0.0",
4
4
  "description": "The Granite Framework",
5
5
  "bin": {
6
6
  "granite": "./bin/cli.js"
@@ -91,46 +91,49 @@
91
91
  "@babel/core": "7.28.5",
92
92
  "@babel/preset-env": "7.28.5",
93
93
  "@babel/preset-typescript": "7.28.5",
94
- "@granite-js/native": "0.1.34",
94
+ "@granite-js/native": "1.0.0",
95
95
  "@testing-library/dom": "^10.4.0",
96
96
  "@testing-library/react": "^16.1.0",
97
97
  "@types/babel__core": "^7",
98
98
  "@types/babel__preset-env": "^7",
99
- "@types/node": "^22.10.2",
100
- "@types/react": "18.3.3",
101
- "@types/react-dom": "^18",
99
+ "@types/node": "24.10.12",
100
+ "@types/react": "19.2.0",
101
+ "@types/react-dom": "19.2.3",
102
102
  "@vitest/coverage-v8": "^4.0.12",
103
- "esbuild": "0.25.8",
103
+ "brick-module": "0.5.0",
104
+ "esbuild": "0.25.9",
104
105
  "eslint": "^9.7.0",
105
106
  "jsdom": "^25.0.1",
106
- "react": "18.2.0",
107
- "react-dom": "18.2.0",
108
- "react-native": "0.72.6",
107
+ "react": "19.2.3",
108
+ "react-dom": "19.2.3",
109
+ "react-native": "0.84.0-rc.5",
109
110
  "tsup": "^8.5.0",
110
- "typescript": "5.8.3",
111
+ "typescript": "5.9.3",
111
112
  "vitest": "^4.0.12",
112
113
  "zod": "^4.1.12"
113
114
  },
114
115
  "peerDependencies": {
115
- "@granite-js/native": "*",
116
+ "@granite-js/native": "1.0.0",
116
117
  "@types/react": "*",
118
+ "brick-module": "*",
117
119
  "react": "*",
118
120
  "react-native": "*"
119
121
  },
120
122
  "dependencies": {
121
- "@granite-js/cli": "0.1.34",
122
- "@granite-js/image": "0.1.34",
123
- "@granite-js/jest": "0.1.34",
124
- "@granite-js/lottie": "0.1.34",
125
- "@granite-js/mpack": "0.1.34",
126
- "@granite-js/plugin-core": "0.1.34",
127
- "@granite-js/style-utils": "0.1.34",
123
+ "@granite-js/blur-view": "1.0.0",
124
+ "@granite-js/cli": "1.0.0",
125
+ "@granite-js/jest": "1.0.0",
126
+ "@granite-js/mpack": "1.0.0",
127
+ "@granite-js/plugin-core": "1.0.0",
128
+ "@granite-js/style-utils": "1.0.0",
129
+ "@granite-js/video": "1.0.0",
128
130
  "@standard-schema/spec": "^1.0.0",
129
131
  "es-toolkit": "^1.39.8",
130
132
  "react-native-url-polyfill": "3.0.0"
131
133
  },
132
134
  "sideEffects": [
133
135
  "dist/async-bridges.*",
134
- "dist/constant-bridges.*"
136
+ "dist/constant-bridges.*",
137
+ "src/types/global.ts"
135
138
  ]
136
139
  }
@@ -1,4 +1,4 @@
1
- import { ComponentType, PropsWithChildren } from 'react';
1
+ import { ComponentType, type JSX, PropsWithChildren } from 'react';
2
2
  import { AppRegistry } from 'react-native';
3
3
  import { ENTRY_BUNDLE_NAME } from '../constants';
4
4
  import type { InitialProps } from '../initial-props';
@@ -95,7 +95,7 @@ const createApp = () => {
95
95
  registerHostApp(
96
96
  AppContainer: ComponentType<PropsWithChildren<InitialProps>>,
97
97
  { appName }: Pick<GraniteProps, 'appName'>
98
- ): (initialProps: InitialProps) => JSX.Element {
98
+ ): (initialProps: InitialProps) => React.JSX.Element {
99
99
  if (appName !== ENTRY_BUNDLE_NAME) {
100
100
  throw new Error(`Host appName must be 'shared'`);
101
101
  }
@@ -0,0 +1,125 @@
1
+ import GraniteImage, {
2
+ GraniteImageProps,
3
+ OnErrorEvent,
4
+ OnLoadEndEvent,
5
+ OnLoadStartEvent,
6
+ type GraniteImageSource,
7
+ } from '@granite-js/native/react-native-fast-image';
8
+ import { NativeSyntheticEvent, StyleSheet } from 'react-native';
9
+ import { SvgImage } from './SvgImage';
10
+
11
+ type Source = {
12
+ uri?: string;
13
+ headers?: Record<string, string>;
14
+ priority?: 'low' | 'normal' | 'high';
15
+ cache?: 'immutable' | 'web' | 'cacheOnly';
16
+ };
17
+
18
+ export interface ImageProps extends Omit<GraniteImageProps, 'source' | 'onError'> {
19
+ source?: Source;
20
+
21
+ onLoadStart?: (event: NativeSyntheticEvent<OnLoadStartEvent> | Readonly<{ type: 'svg' }>) => void;
22
+ onLoadEnd?: (event: NativeSyntheticEvent<OnLoadEndEvent> | Readonly<{ type: 'svg' }>) => void;
23
+ onError?: (event: NativeSyntheticEvent<OnErrorEvent> | Readonly<{ type: 'svg' }>) => void;
24
+ }
25
+
26
+ /**
27
+ * @public
28
+ * @category UI
29
+ * @name Image
30
+ * @description You can use the `Image` component to load and render bitmap images (such as PNG, JPG) or vector images (SVG). It automatically renders with the appropriate method depending on the image format.
31
+ *
32
+ * @param {object} [props] - The `props` object passed to the component.
33
+ * @param {object} [props.style] - An object that defines the style for the image component. It can include layout-related properties like `width` and `height`.
34
+ * @param {object} [props.source] - An object containing information about the image resource to load.
35
+ * @param {string} [props.source.uri] - The URI address representing the image resource to load.
36
+ * @param {'immutable' | 'web' | 'cacheOnly'} [props.source.cache = 'immutable'] - An option to set the image caching strategy. This applies only to bitmap images. The default value is `immutable`.
37
+ * @param {() => void} [props.onLoadStart] - A callback function that is called when image loading starts.
38
+ * @param {() => void} [props.onLoadEnd] - A callback function that is called when image loading finishes.
39
+ * @param {() => void} [props.onError] - A callback function that is called when an error occurs during image loading.
40
+ *
41
+ * @example
42
+ * ### Example: Loading and rendering an image
43
+ *
44
+ * The following example shows how to load bitmap and vector image resources, and how to print an error message to `console.log` if an error occurs.
45
+ *
46
+ * ```tsx
47
+ * import { Image } from '@granite-js/react-native';
48
+ * import { View } from 'react-native';
49
+ *
50
+ * export function ImageExample() {
51
+ * return (
52
+ * <View>
53
+ * <Image
54
+ * source={{ uri: 'my-image-link' }}
55
+ * style={{
56
+ * width: 300,
57
+ * height: 300,
58
+ * borderWidth: 1,
59
+ * }}
60
+ * onError={() => {
61
+ * console.log('Failed to load image');
62
+ * }}
63
+ * />
64
+ *
65
+ * <Image
66
+ * source={{ uri: 'my-svg-link' }}
67
+ * style={{
68
+ * width: 300,
69
+ * height: 300,
70
+ * borderWidth: 1,
71
+ * }}
72
+ * onError={() => {
73
+ * console.log('Failed to load image');
74
+ * }}
75
+ * />
76
+ * </View>
77
+ * );
78
+ * }
79
+ * ```
80
+ */
81
+ function Image(props: ImageProps) {
82
+ if (typeof props.source === 'object' && props.source.uri?.endsWith('.svg')) {
83
+ const style = StyleSheet.flatten(props.style);
84
+ const width = style?.width;
85
+ const height = style?.height;
86
+
87
+ return (
88
+ <SvgImage
89
+ testID={props.testID}
90
+ url={props.source.uri!}
91
+ width={width}
92
+ height={height}
93
+ style={props.style}
94
+ onLoadStart={props.onLoadStart ? (e) => props.onLoadStart?.({ type: 'svg', ...e }) : undefined}
95
+ onLoadEnd={props.onLoadEnd ? (e) => props.onLoadEnd?.({ type: 'svg', ...e }) : undefined}
96
+ onError={props.onError ? () => props.onError?.({ type: 'svg' }) : undefined}
97
+ />
98
+ );
99
+ }
100
+
101
+ const source: GraniteImageSource | string | undefined = props.source
102
+ ? props.source.uri
103
+ ? {
104
+ uri: props.source.uri,
105
+ headers: props.source.headers,
106
+ priority: props.source.priority,
107
+ cache: props.source.cache,
108
+ }
109
+ : undefined
110
+ : undefined;
111
+
112
+ if (!source) {
113
+ return null;
114
+ }
115
+
116
+ const handleError = props.onError
117
+ ? (event: NativeSyntheticEvent<OnErrorEvent>) => {
118
+ props.onError?.(event);
119
+ }
120
+ : undefined;
121
+
122
+ return <GraniteImage {...props} source={source} onError={handleError} />;
123
+ }
124
+
125
+ export { Image };
@@ -0,0 +1,124 @@
1
+ import { SvgUri, SvgXml } from '@granite-js/native/react-native-svg';
2
+ import { createElement, useEffect, useCallback, useState } from 'react';
3
+ import { View, type ViewStyle, type StyleProp } from 'react-native';
4
+ import type { DimensionValue, NumberValue } from './types';
5
+ import { usePreservedCallback } from '../utils/usePreservedCallback';
6
+
7
+ export interface SvgImageProps {
8
+ url: string;
9
+ width?: DimensionValue;
10
+ height?: DimensionValue;
11
+ style?: StyleProp<any>;
12
+ testID?: string;
13
+ onLoadStart?: (event: object) => void;
14
+ onLoadEnd?: (event: object) => void;
15
+ onError?: () => void;
16
+ }
17
+
18
+ /**
19
+ * @name SvgImage
20
+ * @category Components
21
+ * @description The `SvgImage` component loads and renders SVG images from a given external URL.
22
+ * @link https://github.com/software-mansion/react-native-svg/tree/v13.14.0/README.md
23
+ *
24
+ * @param {object} props - The `props` object passed to the component.
25
+ * @param {string} props.url - The URI address of the SVG image to load.
26
+ * @param {number | string} [props.width = '100%'] - Sets the horizontal size of the SVG image. Default value is '`100%`'.
27
+ * @param {number | string} [props.height = '100%'] - Sets the vertical size of the SVG image. Default value is '`100%`'.
28
+ * @param {object} props.style - Sets the style of the image component.
29
+ * @param {() => void} props.onLoadStart - A callback function called when the SVG image resource starts loading.
30
+ * @param {() => void} props.onLoadEnd - A callback function called after the SVG image resource is loaded.
31
+ * @param {() => void} props.onError - A callback function called when an error occurs during SVG image loading.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * import { SvgImage } from './SvgImage';
36
+ * import { View } from 'react-native';
37
+ *
38
+ * function MyComponent() {
39
+ * return (
40
+ * <View>
41
+ * <SvgImage
42
+ * url="https://example.com/icon.svg"
43
+ * width={100}
44
+ * height={100}
45
+ * onError={() => console.log('An error occurred while loading the SVG')}
46
+ * />
47
+ * </View>
48
+ * );
49
+ * }
50
+ * ```
51
+ */
52
+ export function SvgImage({
53
+ url,
54
+ width = '100%',
55
+ height = '100%',
56
+ style,
57
+ testID,
58
+ onLoadStart: _onLoadStart,
59
+ onLoadEnd: _onLoadEnd,
60
+ onError: _onError,
61
+ }: SvgImageProps) {
62
+ const svgStyle = { width, height } as { width: NumberValue; height: NumberValue };
63
+ const [data, setData] = useState<string | undefined>(undefined);
64
+ const [isError, setIsError] = useState(false);
65
+
66
+ const onLoadStart = usePreservedCallback(() => _onLoadStart?.({}));
67
+ const onLoadEnd = usePreservedCallback(() => _onLoadEnd?.({}));
68
+ const onError = usePreservedCallback(() => _onError?.());
69
+
70
+ // Component to occupy layout space when the image is not yet rendered
71
+ const Fallback = useCallback(
72
+ () => createElement(View, { style: { width, height } as ViewStyle }, null),
73
+ [width, height]
74
+ );
75
+
76
+ useEffect(() => {
77
+ let isMounted = true;
78
+
79
+ /**
80
+ * First attempts to fetch the XML resource, and if that fails, tries to load directly by passing the URI to the Svg component
81
+ */
82
+ async function fetchSvg() {
83
+ onLoadStart();
84
+
85
+ try {
86
+ const response = await fetch(url);
87
+ const svg = await response.text();
88
+
89
+ if (isMounted) {
90
+ onLoadEnd();
91
+ setData(svg);
92
+ }
93
+ } catch {
94
+ setIsError(true);
95
+ }
96
+ }
97
+
98
+ fetchSvg();
99
+
100
+ return () => {
101
+ isMounted = false;
102
+ };
103
+ }, [onLoadStart, onLoadEnd, url]);
104
+
105
+ if (data == null) {
106
+ return <Fallback />;
107
+ }
108
+
109
+ if (isError) {
110
+ return (
111
+ <SvgUri
112
+ testID={testID}
113
+ uri={url}
114
+ style={style}
115
+ {...svgStyle}
116
+ onError={onError}
117
+ onLoad={onLoadEnd}
118
+ fallback={<Fallback />}
119
+ />
120
+ );
121
+ }
122
+
123
+ return <SvgXml testID={testID} xml={data} style={style} {...svgStyle} fallback={<Fallback />} />;
124
+ }
@@ -0,0 +1 @@
1
+ export { Image, type ImageProps } from './Image';
@@ -0,0 +1,6 @@
1
+ import type { Animated } from 'react-native';
2
+
3
+ // FIXME: DimensionValue type is not available in React Native 0.68, so it's defined separately
4
+ export type DimensionValue = string | number | 'auto' | `${number}%` | Animated.AnimatedNode | null;
5
+
6
+ export type NumberValue = number | string;
package/src/index.ts CHANGED
@@ -2,8 +2,19 @@ import './types/global';
2
2
 
3
3
  export { Granite, useInitialSearchParams, useInitialProps } from './app';
4
4
  export * from '@granite-js/style-utils';
5
- export * from '@granite-js/image';
6
- export * from '@granite-js/lottie';
5
+ export {
6
+ type GraniteImageSource,
7
+ type GraniteImageStatic,
8
+ type ResizeMode,
9
+ type CachePolicy,
10
+ type Priority,
11
+ type OnLoadEvent,
12
+ type OnProgressEvent,
13
+ } from '@granite-js/native/react-native-fast-image';
14
+
15
+ // Image with SVG support
16
+ export * from './image';
17
+ export * from './lottie';
7
18
 
8
19
  export * from './dev-entrypoint';
9
20
  export * from './native-modules/natives';
@@ -18,7 +29,7 @@ export * from './router/hooks/useIsInitialScreen';
18
29
  export * from './event';
19
30
  export * from './video';
20
31
  export * from './status-bar';
21
- export * from './blur';
32
+ export * from '@granite-js/blur-view';
22
33
 
23
34
  export { BackButton, useRouterBackHandler } from './router';
24
35
 
@@ -1,4 +1,4 @@
1
- import { RefAttributes } from 'react';
1
+ import React, { RefAttributes } from 'react';
2
2
  import { FlatList, FlatListProps } from 'react-native';
3
3
  import withIO, { IOComponentProps } from './withIO';
4
4
 
@@ -67,6 +67,6 @@ const IOFlatList = withIO(FlatList, [
67
67
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
68
68
  declare function IOFlatListFunction<ItemT = any>(
69
69
  props: IOFlatListProps<ItemT> & RefAttributes<IOFlatListController>
70
- ): JSX.Element;
70
+ ): React.JSX.Element;
71
71
 
72
72
  export default IOFlatList;
@@ -1,5 +1,5 @@
1
1
  import { type ComponentProps, PureComponent, RefObject, createRef } from 'react';
2
- import { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView, findNodeHandle } from 'react-native';
2
+ import { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView, View } from 'react-native';
3
3
  import IOContext, { IOContextValue } from './IOContext';
4
4
  import IOManager from './IOManager';
5
5
  import { Root, RootMargin } from './IntersectionObserver';
@@ -40,6 +40,7 @@ function withIO<
40
40
  // eslint-disable-next-line @typescript-eslint/no-this-alias
41
41
  const self = this;
42
42
  this.scroller = createRef();
43
+ this.node = null;
43
44
  this.root = {
44
45
  get node() {
45
46
  return self.node;
@@ -82,7 +83,9 @@ function withIO<
82
83
  }
83
84
 
84
85
  componentDidMount() {
85
- this.node = findNodeHandle(this.scroller.current);
86
+ // Prefer a native scroll ref (FlatList/VirtualizedList),
87
+ // otherwise fall back to the host ref (ScrollView).
88
+ this.node = this.resolveRootNode();
86
89
  methods.forEach((method) => {
87
90
  (this as any)[method] = (...args: any) => {
88
91
  this.scroller.current?.[method]?.(...args);
@@ -90,6 +93,72 @@ function withIO<
90
93
  });
91
94
  }
92
95
 
96
+ render() {
97
+ return (
98
+ <IOContext.Provider value={this.contextValue}>
99
+ <BaseComponent
100
+ scrollEventThrottle={16}
101
+ {...this.props}
102
+ ref={this.scroller}
103
+ onContentSizeChange={this.handleContentSizeChange}
104
+ onLayout={this.handleLayout}
105
+ onScroll={this.handleScroll}
106
+ />
107
+ </IOContext.Provider>
108
+ );
109
+ }
110
+
111
+ // Private helpers to keep type-safety encapsulated
112
+ private isRefObject<T>(v: unknown): v is RefObject<T> {
113
+ return typeof v === 'object' && v !== null && 'current' in (v as Record<string, unknown>);
114
+ }
115
+
116
+ private toView(v: RefObject<View> | View | null | undefined): View | null {
117
+ if (!v) {
118
+ return null;
119
+ }
120
+ return this.isRefObject<View>(v) ? (v.current ?? null) : v;
121
+ }
122
+
123
+ private callIfFunction<T extends object, K extends string>(obj: T | null | undefined, key: K): unknown {
124
+ if (!obj) {
125
+ return null;
126
+ }
127
+ const rec = obj as unknown as Record<string, unknown>;
128
+ const fn = rec[String(key)];
129
+ if (typeof fn === 'function') {
130
+ return fn.call(obj);
131
+ }
132
+ return null;
133
+ }
134
+
135
+ protected resolveRootNode = (): View | null => {
136
+ const instance = this.scroller.current as unknown;
137
+
138
+ // 1) Prefer native scroll ref (FlatList/VirtualizedList on Fabric)
139
+ const viaNativeRef = this.callIfFunction(instance as object, 'getNativeScrollRef');
140
+ const nativeFromNativeRef = this.toView(viaNativeRef as RefObject<View> | View | null | undefined);
141
+ if (nativeFromNativeRef) {
142
+ return nativeFromNativeRef;
143
+ }
144
+
145
+ // 2) Fallback to getScrollRef
146
+ const viaScrollRef = this.callIfFunction(instance as object, 'getScrollRef');
147
+ const nativeFromScrollRef = this.toView(viaScrollRef as RefObject<View> | View | null | undefined);
148
+ if (nativeFromScrollRef) {
149
+ return nativeFromScrollRef;
150
+ }
151
+
152
+ // 3) Fallback to getScrollableNode (exclude numeric handles)
153
+ const scrollable = this.callIfFunction(instance as object, 'getScrollableNode');
154
+ if (scrollable && typeof scrollable !== 'number') {
155
+ return scrollable as View;
156
+ }
157
+
158
+ // 4) Lastly, treat the instance itself as a View or RefObject<View>
159
+ return this.toView(instance as RefObject<View> | View | null | undefined);
160
+ };
161
+
93
162
  protected handleContentSizeChange = (width: number, height: number) => {
94
163
  const { contentSize } = this.root.current;
95
164
  if (width !== contentSize.width || height !== contentSize.height) {
@@ -128,21 +197,6 @@ function withIO<
128
197
  onScroll(event);
129
198
  }
130
199
  };
131
-
132
- render() {
133
- return (
134
- <IOContext.Provider value={this.contextValue}>
135
- <BaseComponent
136
- scrollEventThrottle={16}
137
- {...this.props}
138
- ref={this.scroller}
139
- onContentSizeChange={this.handleContentSizeChange}
140
- onLayout={this.handleLayout}
141
- onScroll={this.handleScroll}
142
- />
143
- </IOContext.Provider>
144
- );
145
- }
146
200
  };
147
201
 
148
202
  return IOScrollableComponent as typeof BaseComponent;
@@ -0,0 +1,87 @@
1
+ import LottieView, { type AnimationObject } from '@granite-js/native/lottie-react-native';
2
+ import type { ComponentProps } from 'react';
3
+ import { View } from 'react-native';
4
+ import { ensureSafeLottie, hasFonts } from './ensureSafeLottie';
5
+ import { useFetchResource } from './useFetchResource';
6
+
7
+ type LottieViewProps = ComponentProps<typeof LottieView>;
8
+ type BaseProps = Omit<LottieViewProps, 'source'> & {
9
+ /**
10
+ * Height is required to prevent layout shifting.
11
+ */
12
+ height: number | '100%';
13
+ width?: number | '100%';
14
+ maxWidth?: number;
15
+ };
16
+
17
+ export type RemoteLottieProps = BaseProps & {
18
+ src: string;
19
+ };
20
+
21
+ export type AnimationObjectLottieProps = BaseProps & {
22
+ animationObject: AnimationObject;
23
+ };
24
+
25
+ export function Lottie({
26
+ width,
27
+ maxWidth,
28
+ height,
29
+ src,
30
+ autoPlay = true,
31
+ speed = 1,
32
+ style,
33
+ onAnimationFailure,
34
+ ...props
35
+ }: RemoteLottieProps) {
36
+ const handleAnimationFailure = onAnimationFailure
37
+ ? (error: string) => {
38
+ onAnimationFailure(error);
39
+ }
40
+ : undefined;
41
+
42
+ const jsonData = useFetchResource(src, handleAnimationFailure);
43
+
44
+ if (jsonData == null) {
45
+ return <View testID="lottie-placeholder" style={[{ opacity: 1, width, height }, style]} />;
46
+ }
47
+
48
+ if (hasFonts(jsonData) && __DEV__) {
49
+ throw new Error(
50
+ `The Lottie resource contains custom fonts which is unsafe. Please remove the custom fonts. source: ${src}`
51
+ );
52
+ }
53
+
54
+ return (
55
+ <LottieView
56
+ source={ensureSafeLottie(jsonData)}
57
+ autoPlay={autoPlay}
58
+ speed={speed}
59
+ style={[{ width, height, maxWidth }, style]}
60
+ onAnimationFailure={onAnimationFailure}
61
+ {...props}
62
+ />
63
+ );
64
+ }
65
+
66
+ Lottie.AnimationObject = function LottieWithAnimationObject({
67
+ width,
68
+ maxWidth,
69
+ height,
70
+ animationObject,
71
+ autoPlay = true,
72
+ speed = 1,
73
+ style,
74
+ onAnimationFailure,
75
+ ...props
76
+ }: AnimationObjectLottieProps) {
77
+ return (
78
+ <LottieView
79
+ source={animationObject}
80
+ autoPlay={autoPlay}
81
+ speed={speed}
82
+ style={[{ width, height, maxWidth }, style]}
83
+ onAnimationFailure={onAnimationFailure}
84
+ {...props}
85
+ />
86
+ );
87
+ };
@@ -0,0 +1,15 @@
1
+ import type { AnimationObject } from '@granite-js/native/lottie-react-native';
2
+
3
+ type AnimationObjectWithFonts = AnimationObject & {
4
+ fonts?: { list?: unknown[] };
5
+ };
6
+
7
+ export function hasFonts(animationData: AnimationObject): boolean {
8
+ const data = animationData as AnimationObjectWithFonts;
9
+ return Array.isArray(data.fonts?.list) && data.fonts.list.length > 0;
10
+ }
11
+
12
+ export function ensureSafeLottie(animationData: AnimationObject): AnimationObject {
13
+ const { fonts, ...safeData } = animationData as AnimationObjectWithFonts;
14
+ return safeData as AnimationObject;
15
+ }
@@ -0,0 +1 @@
1
+ export { Lottie, type RemoteLottieProps, type AnimationObjectLottieProps } from './Lottie';
@@ -0,0 +1,31 @@
1
+ import type { AnimationObject } from '@granite-js/native/lottie-react-native';
2
+ import { useState, useEffect } from 'react';
3
+
4
+ export function useFetchResource(src: string, onAnimationFailure?: (error: string) => void): AnimationObject | null {
5
+ const [jsonData, setJsonData] = useState<AnimationObject | null>(null);
6
+
7
+ useEffect(() => {
8
+ let canceled = false;
9
+
10
+ fetch(src)
11
+ .then((res) => res.json())
12
+ .then((data) => {
13
+ if (!canceled) {
14
+ setJsonData(data);
15
+ }
16
+ })
17
+ .catch((error) => {
18
+ if (error instanceof Error) {
19
+ onAnimationFailure?.(error.message);
20
+ } else {
21
+ onAnimationFailure?.('Unknown error');
22
+ }
23
+ });
24
+
25
+ return () => {
26
+ canceled = true;
27
+ };
28
+ }, [src, onAnimationFailure]);
29
+
30
+ return jsonData;
31
+ }
@@ -1,3 +1,2 @@
1
1
  /** Bridges API */
2
2
  export * from './natives';
3
- export { GraniteCoreModule } from './core/GraniteCoreModule';
@@ -0,0 +1,15 @@
1
+ import { BrickModule, BrickModuleSpec } from 'brick-module';
2
+ import { CodegenTypes } from 'react-native';
3
+
4
+ interface GraniteBrownfieldModuleSpec extends BrickModuleSpec {
5
+ readonly moduleName: 'GraniteBrownfieldModule';
6
+ readonly onVisibilityChanged: CodegenTypes.EventEmitter<{ visible: boolean }>;
7
+
8
+ getConstants(): {
9
+ schemeUri: string;
10
+ };
11
+
12
+ closeView(): Promise<void>;
13
+ }
14
+
15
+ export const GraniteModule = BrickModule.get<GraniteBrownfieldModuleSpec>('GraniteBrownfieldModule');