@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.
- package/CHANGELOG.md +12 -573
- package/dist/app/Granite.d.ts +2 -2
- package/dist/async-bridges.js +6 -6
- package/dist/async-bridges.mjs +2 -2
- package/dist/chunk-7GFSQK76.mjs +7 -0
- package/dist/constant-bridges.js +4 -4
- package/dist/constant-bridges.mjs +2 -2
- package/dist/image/Image.d.ts +77 -0
- package/dist/image/SvgImage.d.ts +47 -0
- package/dist/image/index.d.ts +1 -0
- package/dist/image/types.d.ts +3 -0
- package/dist/index.d.ts +4 -3
- package/dist/intersection-observer/IOFlatList.d.ts +2 -2
- package/dist/lottie/Lottie.d.ts +22 -0
- package/dist/lottie/ensureSafeLottie.d.ts +3 -0
- package/dist/lottie/index.d.ts +1 -0
- package/dist/lottie/useFetchResource.d.ts +2 -0
- package/dist/native-modules/index.d.ts +0 -1
- package/dist/native-modules/natives/GraniteBrownfieldModule.brick.d.ts +14 -0
- package/dist/router/Router.d.ts +11 -3
- package/dist/router/components/CanGoBackGuard.d.ts +2 -1
- package/dist/router/components/ErrorBoundary.d.ts +16 -0
- package/dist/router/components/StackNavigator.d.ts +40 -43
- package/dist/router/components/useRouterBackHandler.d.ts +3 -3
- package/dist/router/createRoute.d.ts +4 -1
- package/dist/router/hooks/useRouterControls.d.ts +3 -2
- package/dist/router/types/ErrorComponent.d.ts +6 -0
- package/dist/router/types/RouteScreen.d.ts +6 -0
- package/dist/router/types/index.d.ts +1 -0
- package/dist/status-bar/utils.d.ts +2 -2
- package/dist/video/Video.d.ts +5 -18
- package/package.json +22 -19
- package/src/app/Granite.tsx +2 -2
- package/src/image/Image.tsx +125 -0
- package/src/image/SvgImage.tsx +124 -0
- package/src/image/index.ts +1 -0
- package/src/image/types.ts +6 -0
- package/src/index.ts +14 -3
- package/src/intersection-observer/IOFlatList.ts +2 -2
- package/src/intersection-observer/withIO.tsx +71 -17
- package/src/lottie/Lottie.tsx +87 -0
- package/src/lottie/ensureSafeLottie.ts +15 -0
- package/src/lottie/index.ts +1 -0
- package/src/lottie/useFetchResource.ts +31 -0
- package/src/native-modules/index.ts +0 -1
- package/src/native-modules/natives/GraniteBrownfieldModule.brick.ts +15 -0
- package/src/native-modules/natives/closeView.ts +2 -2
- package/src/native-modules/natives/getSchemeUri.ts +2 -2
- package/src/router/Router.tsx +40 -12
- package/src/router/components/CanGoBackGuard.tsx +2 -3
- package/src/router/components/ErrorBoundary.tsx +38 -0
- package/src/router/components/useRouterBackHandler.tsx +3 -3
- package/src/router/createRoute.ts +6 -2
- package/src/router/hooks/useRouterControls.tsx +21 -7
- package/src/router/types/ErrorComponent.ts +8 -0
- package/src/router/types/RouteScreen.ts +6 -0
- package/src/router/types/index.ts +1 -0
- package/src/router/utils/screen.tsx +2 -0
- package/src/status-bar/utils.ts +2 -2
- package/src/types/global.ts +1 -1
- package/src/video/Video.tsx +6 -17
- package/src/visibility/VisibilityProvider.tsx +3 -3
- package/src/visibility/utils/usePrevious.tsx +1 -1
- package/dist/blur/BlurView.d.ts +0 -78
- package/dist/blur/ReactNativeBlurModule.d.ts +0 -6
- package/dist/blur/constants.d.ts +0 -1
- package/dist/blur/index.d.ts +0 -1
- package/dist/chunk-A3JGM5OI.mjs +0 -7
- package/dist/native-event-emitter/eventEmitters/index.d.ts +0 -2
- package/dist/native-event-emitter/eventEmitters/types.d.ts +0 -4
- package/dist/native-event-emitter/eventEmitters/visibilityChanged.d.ts +0 -10
- package/dist/native-event-emitter/index.d.ts +0 -1
- package/dist/native-event-emitter/nativeEventEmitter.d.ts +0 -15
- package/dist/native-modules/core/GraniteCoreModule.d.ts +0 -8
- package/dist/native-modules/natives/GraniteModule.d.ts +0 -7
- package/dist/video/instance.d.ts +0 -9
- package/src/blur/BlurView.tsx +0 -103
- package/src/blur/ReactNativeBlurModule.ts +0 -19
- package/src/blur/constants.ts +0 -3
- package/src/blur/index.ts +0 -1
- package/src/native-event-emitter/eventEmitters/index.ts +0 -3
- package/src/native-event-emitter/eventEmitters/types.ts +0 -4
- package/src/native-event-emitter/eventEmitters/visibilityChanged.ts +0 -11
- package/src/native-event-emitter/index.ts +0 -1
- package/src/native-event-emitter/nativeEventEmitter.ts +0 -18
- package/src/native-modules/core/GraniteCoreModule.ts +0 -9
- package/src/native-modules/natives/GraniteModule.ts +0 -8
- 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.
|
|
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.
|
|
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": "
|
|
100
|
-
"@types/react": "
|
|
101
|
-
"@types/react-dom": "
|
|
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
|
-
"
|
|
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": "
|
|
107
|
-
"react-dom": "
|
|
108
|
-
"react-native": "0.
|
|
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.
|
|
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/
|
|
122
|
-
"@granite-js/
|
|
123
|
-
"@granite-js/jest": "0.
|
|
124
|
-
"@granite-js/
|
|
125
|
-
"@granite-js/
|
|
126
|
-
"@granite-js/
|
|
127
|
-
"@granite-js/
|
|
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
|
}
|
package/src/app/Granite.tsx
CHANGED
|
@@ -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
|
|
6
|
-
|
|
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 '
|
|
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,
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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');
|