@molecule/app-react-native 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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * React Native hook for app foreground/background state.
3
+ *
4
+ * @module
5
+ */
6
+ import type { UseAppStateResult } from '../types.js';
7
+ /**
8
+ * Tracks whether the app is in the foreground, background, or inactive.
9
+ *
10
+ * Uses React Native's `AppState` API under the hood. Falls back to
11
+ * 'active' on platforms where AppState is unavailable.
12
+ *
13
+ * @returns Current app state and whether the app is active.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const { appState, isActive } = useAppState()
18
+ *
19
+ * useEffect(() => {
20
+ * if (isActive) {
21
+ * refreshData()
22
+ * }
23
+ * }, [isActive])
24
+ * ```
25
+ */
26
+ export declare function useAppState(): UseAppStateResult;
27
+ //# sourceMappingURL=useAppState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAppState.d.ts","sourceRoot":"","sources":["../../src/hooks/useAppState.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAIpD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAmC/C"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * React Native hook for app foreground/background state.
3
+ *
4
+ * @module
5
+ */
6
+ import { useEffect, useState } from 'react';
7
+ /**
8
+ * Tracks whether the app is in the foreground, background, or inactive.
9
+ *
10
+ * Uses React Native's `AppState` API under the hood. Falls back to
11
+ * 'active' on platforms where AppState is unavailable.
12
+ *
13
+ * @returns Current app state and whether the app is active.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const { appState, isActive } = useAppState()
18
+ *
19
+ * useEffect(() => {
20
+ * if (isActive) {
21
+ * refreshData()
22
+ * }
23
+ * }, [isActive])
24
+ * ```
25
+ */
26
+ export function useAppState() {
27
+ const [appState, setAppState] = useState('active');
28
+ useEffect(() => {
29
+ let cleanup;
30
+ try {
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ const { AppState } = require('react-native');
33
+ setAppState(AppState.currentState || 'active');
34
+ const subscription = AppState.addEventListener('change', (nextState) => {
35
+ setAppState(nextState || 'active');
36
+ });
37
+ cleanup = () => subscription.remove();
38
+ }
39
+ catch (_error) {
40
+ // Not running in React Native — stay 'active'
41
+ }
42
+ return cleanup;
43
+ }, []);
44
+ return {
45
+ appState,
46
+ isActive: appState === 'active',
47
+ };
48
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * React Native hook for Android hardware back button.
3
+ *
4
+ * @module
5
+ */
6
+ import type { UseBackHandlerOptions } from '../types.js';
7
+ /**
8
+ * Registers a handler for the Android hardware back button.
9
+ *
10
+ * Return `true` from the handler to prevent the default back behavior,
11
+ * or `false` to allow it.
12
+ *
13
+ * @param handler - Callback invoked when the hardware back button is pressed.
14
+ * @param options - Optional configuration.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * useBackHandler(() => {
19
+ * if (isModalOpen) {
20
+ * closeModal()
21
+ * return true // Prevent default back
22
+ * }
23
+ * return false // Allow default back
24
+ * })
25
+ * ```
26
+ */
27
+ export declare function useBackHandler(handler: () => boolean, options?: UseBackHandlerOptions): void;
28
+ //# sourceMappingURL=useBackHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBackHandler.d.ts","sourceRoot":"","sources":["../../src/hooks/useBackHandler.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAExD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,OAAO,EAAE,OAAO,GAAE,qBAA0B,GAAG,IAAI,CAyBhG"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * React Native hook for Android hardware back button.
3
+ *
4
+ * @module
5
+ */
6
+ import { useEffect } from 'react';
7
+ /**
8
+ * Registers a handler for the Android hardware back button.
9
+ *
10
+ * Return `true` from the handler to prevent the default back behavior,
11
+ * or `false` to allow it.
12
+ *
13
+ * @param handler - Callback invoked when the hardware back button is pressed.
14
+ * @param options - Optional configuration.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * useBackHandler(() => {
19
+ * if (isModalOpen) {
20
+ * closeModal()
21
+ * return true // Prevent default back
22
+ * }
23
+ * return false // Allow default back
24
+ * })
25
+ * ```
26
+ */
27
+ export function useBackHandler(handler, options = {}) {
28
+ const { enabled = true } = options;
29
+ useEffect(() => {
30
+ if (!enabled)
31
+ return;
32
+ // Dynamic import to avoid crashes on non-RN platforms
33
+ let cleanup;
34
+ try {
35
+ // BackHandler is available from react-native
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
37
+ const { BackHandler } = require('react-native');
38
+ const subscription = BackHandler.addEventListener('hardwareBackPress', handler);
39
+ cleanup = () => subscription.remove();
40
+ }
41
+ catch (_error) {
42
+ // Not running in React Native — no-op
43
+ }
44
+ return cleanup;
45
+ }, [handler, enabled]);
46
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * React Native hook for keyboard visibility and height.
3
+ *
4
+ * @module
5
+ */
6
+ import type { UseKeyboardHeightResult } from '../types.js';
7
+ /**
8
+ * Tracks soft keyboard visibility and height.
9
+ *
10
+ * Uses React Native's `Keyboard` API. Falls back to hidden state
11
+ * on platforms where Keyboard is unavailable.
12
+ *
13
+ * @returns Current keyboard height and visibility state.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const { keyboardHeight, isKeyboardVisible } = useKeyboardHeight()
18
+ *
19
+ * return (
20
+ * <View style={{ paddingBottom: keyboardHeight }}>
21
+ * <TextInput />
22
+ * </View>
23
+ * )
24
+ * ```
25
+ */
26
+ export declare function useKeyboardHeight(): UseKeyboardHeightResult;
27
+ //# sourceMappingURL=useKeyboardHeight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useKeyboardHeight.d.ts","sourceRoot":"","sources":["../../src/hooks/useKeyboardHeight.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAE1D;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,IAAI,uBAAuB,CA2C3D"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * React Native hook for keyboard visibility and height.
3
+ *
4
+ * @module
5
+ */
6
+ import { useEffect, useState } from 'react';
7
+ /**
8
+ * Tracks soft keyboard visibility and height.
9
+ *
10
+ * Uses React Native's `Keyboard` API. Falls back to hidden state
11
+ * on platforms where Keyboard is unavailable.
12
+ *
13
+ * @returns Current keyboard height and visibility state.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const { keyboardHeight, isKeyboardVisible } = useKeyboardHeight()
18
+ *
19
+ * return (
20
+ * <View style={{ paddingBottom: keyboardHeight }}>
21
+ * <TextInput />
22
+ * </View>
23
+ * )
24
+ * ```
25
+ */
26
+ export function useKeyboardHeight() {
27
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
28
+ useEffect(() => {
29
+ const cleanups = [];
30
+ try {
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ const { Keyboard, Platform } = require('react-native');
33
+ const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
34
+ const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
35
+ const showSub = Keyboard.addListener(showEvent, (e) => {
36
+ setKeyboardHeight(e.endCoordinates.height);
37
+ });
38
+ cleanups.push(() => showSub.remove());
39
+ const hideSub = Keyboard.addListener(hideEvent, () => {
40
+ setKeyboardHeight(0);
41
+ });
42
+ cleanups.push(() => hideSub.remove());
43
+ }
44
+ catch (_error) {
45
+ // Not running in React Native (e.g. web/SSR); the Keyboard API is
46
+ // unavailable and the hook falls back to the hidden-state default.
47
+ // Ignoring is safe because no keyboard subscriptions were registered.
48
+ }
49
+ return () => cleanups.forEach((fn) => fn());
50
+ }, []);
51
+ return {
52
+ keyboardHeight,
53
+ isKeyboardVisible: keyboardHeight > 0,
54
+ };
55
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * React Native hook for safe area insets.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * Safe area inset values in points.
8
+ */
9
+ export interface SafeAreaInsets {
10
+ top: number;
11
+ right: number;
12
+ bottom: number;
13
+ left: number;
14
+ }
15
+ /**
16
+ * Returns the current safe area insets from `react-native-safe-area-context`.
17
+ *
18
+ * Must be used within a `SafeAreaProvider`. Falls back to zero insets
19
+ * if the context is unavailable.
20
+ *
21
+ * @returns Safe area insets for the current device.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * const insets = useSafeArea()
26
+ *
27
+ * return (
28
+ * <View style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}>
29
+ * <App />
30
+ * </View>
31
+ * )
32
+ * ```
33
+ */
34
+ export declare function useSafeArea(): SafeAreaInsets;
35
+ //# sourceMappingURL=useSafeArea.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSafeArea.d.ts","sourceRoot":"","sources":["../../src/hooks/useSafeArea.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,IAAI,cAAc,CAY5C"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * React Native hook for safe area insets.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * Returns the current safe area insets from `react-native-safe-area-context`.
8
+ *
9
+ * Must be used within a `SafeAreaProvider`. Falls back to zero insets
10
+ * if the context is unavailable.
11
+ *
12
+ * @returns Safe area insets for the current device.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const insets = useSafeArea()
17
+ *
18
+ * return (
19
+ * <View style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}>
20
+ * <App />
21
+ * </View>
22
+ * )
23
+ * ```
24
+ */
25
+ export function useSafeArea() {
26
+ try {
27
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
28
+ const { useSafeAreaInsets } = require('react-native-safe-area-context');
29
+ return useSafeAreaInsets();
30
+ }
31
+ catch (_error) {
32
+ // Safe to ignore: react-native-safe-area-context is an optional peer dep;
33
+ // if it is absent or the provider is not mounted, zero insets are the documented fallback.
34
+ return { top: 0, right: 0, bottom: 0, left: 0 };
35
+ }
36
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * React Native framework bindings for molecule.dev.
3
+ *
4
+ * Re-exports every hook and provider from `@molecule/app-react` (they are pure
5
+ * React — no DOM dependency) and adds RN-specific hooks: `useAppState`
6
+ * (foreground/background), `useBackHandler` (Android back button),
7
+ * `useKeyboardHeight`, and `useSafeArea`.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { MoleculeProvider, useAuth, useAppState, useSafeArea } from '@molecule/app-react-native'
12
+ * import { SafeAreaProvider } from 'react-native-safe-area-context'
13
+ * import { View } from 'react-native'
14
+ * import { provider as stateProvider } from '@molecule/app-state-zustand'
15
+ * import { createJWTAuthClient } from '@molecule/app-auth'
16
+ *
17
+ * const authClient = createJWTAuthClient({ baseURL: 'https://api.example.com' })
18
+ *
19
+ * function Shell() {
20
+ * const { isAuthenticated } = useAuth()
21
+ * const { isActive } = useAppState()
22
+ * const insets = useSafeArea()
23
+ * return (
24
+ * <View style={{ paddingTop: insets.top }}>
25
+ * {isAuthenticated && isActive ? <View /> : null}
26
+ * </View>
27
+ * )
28
+ * }
29
+ *
30
+ * function App() {
31
+ * return (
32
+ * <SafeAreaProvider>
33
+ * <MoleculeProvider state={stateProvider} auth={authClient}>
34
+ * <Shell />
35
+ * </MoleculeProvider>
36
+ * </SafeAreaProvider>
37
+ * )
38
+ * }
39
+ * ```
40
+ *
41
+ * @remarks
42
+ * - **The re-exported hooks keep `@molecule/app-react`'s contract**: each one throws unless its
43
+ * provider was passed to `MoleculeProvider` (or mounted individually). See the
44
+ * `@molecule/app-react` docs for the hook→provider map.
45
+ * - **`useSafeArea` needs `SafeAreaProvider`** (from `react-native-safe-area-context`) mounted at
46
+ * the root; without it — or without the library installed — it silently returns zero insets
47
+ * rather than throwing, so a notch-overlapped header means missing provider, not a bug in the hook.
48
+ * - `useBackHandler` only fires on Android; iOS has no hardware back button.
49
+ * - For components/styling pair this with `@molecule/app-ui-react-native`, whose ClassMap
50
+ * styling requires the NativeWind setup (`@molecule/app-ui-nativewind`) — see that package's docs.
51
+ *
52
+ * @module
53
+ */
54
+ export * from '@molecule/app-react';
55
+ export { useAppState } from './hooks/useAppState.js';
56
+ export { useBackHandler } from './hooks/useBackHandler.js';
57
+ export { useKeyboardHeight } from './hooks/useKeyboardHeight.js';
58
+ export { useSafeArea } from './hooks/useSafeArea.js';
59
+ export type { UseAppStateResult, UseBackHandlerOptions, UseKeyboardHeightResult } from './types.js';
60
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAGH,cAAc,qBAAqB,CAAA;AAGnC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAGpD,YAAY,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * React Native framework bindings for molecule.dev.
3
+ *
4
+ * Re-exports every hook and provider from `@molecule/app-react` (they are pure
5
+ * React — no DOM dependency) and adds RN-specific hooks: `useAppState`
6
+ * (foreground/background), `useBackHandler` (Android back button),
7
+ * `useKeyboardHeight`, and `useSafeArea`.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { MoleculeProvider, useAuth, useAppState, useSafeArea } from '@molecule/app-react-native'
12
+ * import { SafeAreaProvider } from 'react-native-safe-area-context'
13
+ * import { View } from 'react-native'
14
+ * import { provider as stateProvider } from '@molecule/app-state-zustand'
15
+ * import { createJWTAuthClient } from '@molecule/app-auth'
16
+ *
17
+ * const authClient = createJWTAuthClient({ baseURL: 'https://api.example.com' })
18
+ *
19
+ * function Shell() {
20
+ * const { isAuthenticated } = useAuth()
21
+ * const { isActive } = useAppState()
22
+ * const insets = useSafeArea()
23
+ * return (
24
+ * <View style={{ paddingTop: insets.top }}>
25
+ * {isAuthenticated && isActive ? <View /> : null}
26
+ * </View>
27
+ * )
28
+ * }
29
+ *
30
+ * function App() {
31
+ * return (
32
+ * <SafeAreaProvider>
33
+ * <MoleculeProvider state={stateProvider} auth={authClient}>
34
+ * <Shell />
35
+ * </MoleculeProvider>
36
+ * </SafeAreaProvider>
37
+ * )
38
+ * }
39
+ * ```
40
+ *
41
+ * @remarks
42
+ * - **The re-exported hooks keep `@molecule/app-react`'s contract**: each one throws unless its
43
+ * provider was passed to `MoleculeProvider` (or mounted individually). See the
44
+ * `@molecule/app-react` docs for the hook→provider map.
45
+ * - **`useSafeArea` needs `SafeAreaProvider`** (from `react-native-safe-area-context`) mounted at
46
+ * the root; without it — or without the library installed — it silently returns zero insets
47
+ * rather than throwing, so a notch-overlapped header means missing provider, not a bug in the hook.
48
+ * - `useBackHandler` only fires on Android; iOS has no hardware back button.
49
+ * - For components/styling pair this with `@molecule/app-ui-react-native`, whose ClassMap
50
+ * styling requires the NativeWind setup (`@molecule/app-ui-nativewind`) — see that package's docs.
51
+ *
52
+ * @module
53
+ */
54
+ // Re-export everything from @molecule/app-react — all hooks are RN-compatible
55
+ export * from '@molecule/app-react';
56
+ // RN-specific hooks
57
+ export { useAppState } from './hooks/useAppState.js';
58
+ export { useBackHandler } from './hooks/useBackHandler.js';
59
+ export { useKeyboardHeight } from './hooks/useKeyboardHeight.js';
60
+ export { useSafeArea } from './hooks/useSafeArea.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * React Native specific types for molecule.dev.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * Options for the useBackHandler hook.
8
+ */
9
+ export interface UseBackHandlerOptions {
10
+ /** Whether the handler is enabled. Defaults to true. */
11
+ enabled?: boolean;
12
+ }
13
+ /**
14
+ * Result from the useAppState hook.
15
+ */
16
+ export interface UseAppStateResult {
17
+ /** Current app state: 'active' | 'background' | 'inactive' */
18
+ appState: string;
19
+ /** Whether the app is in the foreground */
20
+ isActive: boolean;
21
+ }
22
+ /**
23
+ * Result from the useKeyboardHeight hook.
24
+ */
25
+ export interface UseKeyboardHeightResult {
26
+ /** Current keyboard height in points (0 when hidden) */
27
+ keyboardHeight: number;
28
+ /** Whether the keyboard is currently visible */
29
+ isKeyboardVisible: boolean;
30
+ }
31
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAA;IAChB,2CAA2C;IAC3C,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,wDAAwD;IACxD,cAAc,EAAE,MAAM,CAAA;IACtB,gDAAgD;IAChD,iBAAiB,EAAE,OAAO,CAAA;CAC3B"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * React Native specific types for molecule.dev.
3
+ *
4
+ * @module
5
+ */
6
+ export {};
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@molecule/app-react-native",
3
+ "version": "1.0.0",
4
+ "description": "React Native framework bindings for molecule.dev",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "keywords": [
24
+ "molecule",
25
+ "react-native",
26
+ "framework"
27
+ ],
28
+ "license": "Apache-2.0",
29
+ "devDependencies": {
30
+ "@molecule/app-react": "1.0.0",
31
+ "@types/node": "26.1.2",
32
+ "react": "19.2.8",
33
+ "typescript": "6.0.3",
34
+ "vitest": "4.1.10"
35
+ },
36
+ "peerDependencies": {
37
+ "@molecule/app-keyboard": "^1.0.0",
38
+ "@molecule/app-lifecycle": "^1.0.0",
39
+ "@molecule/app-react": "^1.0.0",
40
+ "react": "^18.0.0 || ^19.0.0",
41
+ "react-native": ">=0.72.0",
42
+ "react-native-safe-area-context": ">=4.0.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "@molecule/app-react": {
46
+ "optional": true
47
+ },
48
+ "@molecule/app-lifecycle": {
49
+ "optional": true
50
+ },
51
+ "@molecule/app-keyboard": {
52
+ "optional": true
53
+ },
54
+ "react": {
55
+ "optional": true
56
+ },
57
+ "react-native": {
58
+ "optional": true
59
+ },
60
+ "react-native-safe-area-context": {
61
+ "optional": true
62
+ }
63
+ }
64
+ }