@rn-bridge-tools/expo 0.0.7 → 0.0.8

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 (66) hide show
  1. package/dist/WebViewBridge.d.ts +22 -0
  2. package/dist/WebViewBridge.d.ts.map +1 -0
  3. package/dist/__tests__/createHandlers.test.d.ts +2 -0
  4. package/dist/__tests__/createHandlers.test.d.ts.map +1 -0
  5. package/dist/createHandlers.d.ts +5 -0
  6. package/dist/createHandlers.d.ts.map +1 -0
  7. package/dist/handlers/auth.d.ts +6 -0
  8. package/dist/handlers/auth.d.ts.map +1 -0
  9. package/dist/handlers/browser.d.ts +6 -0
  10. package/dist/handlers/browser.d.ts.map +1 -0
  11. package/dist/handlers/camera.d.ts +6 -0
  12. package/dist/handlers/camera.d.ts.map +1 -0
  13. package/dist/handlers/clipboard.d.ts +6 -0
  14. package/dist/handlers/clipboard.d.ts.map +1 -0
  15. package/dist/handlers/device.d.ts +7 -0
  16. package/dist/handlers/device.d.ts.map +1 -0
  17. package/dist/handlers/file.d.ts +8 -0
  18. package/dist/handlers/file.d.ts.map +1 -0
  19. package/dist/handlers/haptic.d.ts +7 -0
  20. package/dist/handlers/haptic.d.ts.map +1 -0
  21. package/dist/handlers/iap.d.ts +7 -0
  22. package/dist/handlers/iap.d.ts.map +1 -0
  23. package/dist/handlers/keyboard.d.ts +6 -0
  24. package/dist/handlers/keyboard.d.ts.map +1 -0
  25. package/dist/handlers/location.d.ts +7 -0
  26. package/dist/handlers/location.d.ts.map +1 -0
  27. package/dist/handlers/navigation.d.ts +8 -0
  28. package/dist/handlers/navigation.d.ts.map +1 -0
  29. package/dist/handlers/permission.d.ts +7 -0
  30. package/dist/handlers/permission.d.ts.map +1 -0
  31. package/dist/handlers/preference.d.ts +8 -0
  32. package/dist/handlers/preference.d.ts.map +1 -0
  33. package/dist/handlers/push.d.ts +6 -0
  34. package/dist/handlers/push.d.ts.map +1 -0
  35. package/dist/handlers/scanner.d.ts +5 -0
  36. package/dist/handlers/scanner.d.ts.map +1 -0
  37. package/dist/handlers/share.d.ts +5 -0
  38. package/dist/handlers/share.d.ts.map +1 -0
  39. package/dist/handlers/statusbar.d.ts +7 -0
  40. package/dist/handlers/statusbar.d.ts.map +1 -0
  41. package/dist/index.d.ts +22 -124
  42. package/dist/index.d.ts.map +1 -0
  43. package/package.json +7 -6
  44. package/src/WebViewBridge.tsx +58 -0
  45. package/src/__tests__/createHandlers.test.ts +88 -0
  46. package/src/createHandlers.ts +67 -0
  47. package/src/handlers/auth.ts +69 -0
  48. package/src/handlers/browser.ts +42 -0
  49. package/src/handlers/camera.ts +97 -0
  50. package/src/handlers/clipboard.ts +32 -0
  51. package/src/handlers/device.ts +71 -0
  52. package/src/handlers/file.ts +126 -0
  53. package/src/handlers/haptic.ts +56 -0
  54. package/src/handlers/iap.ts +23 -0
  55. package/src/handlers/keyboard.ts +20 -0
  56. package/src/handlers/location.ts +104 -0
  57. package/src/handlers/navigation.ts +29 -0
  58. package/src/handlers/permission.ts +107 -0
  59. package/src/handlers/preference.ts +58 -0
  60. package/src/handlers/push.ts +47 -0
  61. package/src/handlers/scanner.ts +31 -0
  62. package/src/handlers/share.ts +31 -0
  63. package/src/handlers/statusbar.ts +35 -0
  64. package/src/index.ts +23 -0
  65. package/dist/index.js +0 -846
  66. package/dist/index.js.map +0 -1
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+
3
+ // Mock react-native modules
4
+ vi.mock('react-native', () => ({
5
+ Platform: { OS: 'ios', Version: '17.0' },
6
+ StatusBar: {
7
+ setBarStyle: vi.fn(),
8
+ setBackgroundColor: vi.fn(),
9
+ setHidden: vi.fn(),
10
+ },
11
+ Keyboard: { dismiss: vi.fn() },
12
+ Linking: { openURL: vi.fn(), openSettings: vi.fn() },
13
+ }));
14
+
15
+ vi.mock('@webview-bridge/react-native', () => ({
16
+ bridge: vi.fn((handlers: Record<string, unknown>) => handlers),
17
+ createWebView: vi.fn(() => ({
18
+ WebView: 'MockWebView',
19
+ postMessage: vi.fn(),
20
+ })),
21
+ }));
22
+
23
+ describe('createDefaultHandlers', () => {
24
+ it('should return handlers for all namespaces', async () => {
25
+ const { createDefaultHandlers } = await import('../createHandlers.js');
26
+
27
+ const handlers = createDefaultHandlers({
28
+ camera: true,
29
+ location: true,
30
+ file: true,
31
+ share: true,
32
+ device: true,
33
+ statusbar: true,
34
+ keyboard: true,
35
+ haptic: true,
36
+ clipboard: true,
37
+ scanner: true,
38
+ auth: true,
39
+ iap: true,
40
+ push: true,
41
+ permission: true,
42
+ preference: true,
43
+ navigation: true,
44
+ browser: true,
45
+ });
46
+
47
+ expect(handlers['camera.take']).toBeDefined();
48
+ expect(handlers['camera.pickImage']).toBeDefined();
49
+ expect(handlers['location.getCurrent']).toBeDefined();
50
+ expect(handlers['location.watchStart']).toBeDefined();
51
+ expect(handlers['location.watchStop']).toBeDefined();
52
+
53
+ const namespaces = [
54
+ 'camera', 'location', 'file', 'share', 'device', 'statusbar',
55
+ 'keyboard', 'haptic', 'clipboard', 'scanner', 'auth', 'iap',
56
+ 'push', 'permission', 'preference', 'navigation', 'browser',
57
+ ];
58
+
59
+ for (const ns of namespaces) {
60
+ const nsHandlers = Object.keys(handlers).filter((k) => k.startsWith(`${ns}.`));
61
+ expect(nsHandlers.length).toBeGreaterThan(0);
62
+ }
63
+ });
64
+
65
+ it('should return disabled handlers when namespace is false', async () => {
66
+ const { createDefaultHandlers } = await import('../createHandlers.js');
67
+ const handlers = createDefaultHandlers({ camera: false });
68
+ const result = await (handlers['camera.take'] as (p: Record<string, unknown>) => Promise<{ success: boolean; error: string }>)({});
69
+ expect(result.success).toBe(false);
70
+ expect(result.error).toContain('not_enabled');
71
+ });
72
+
73
+ it('should return disabled handlers by default', async () => {
74
+ const { createDefaultHandlers } = await import('../createHandlers.js');
75
+ const handlers = createDefaultHandlers();
76
+ const result = await (handlers['camera.take'] as (p: Record<string, unknown>) => Promise<{ success: boolean; error: string }>)({});
77
+ expect(result.success).toBe(false);
78
+ expect(result.error).toContain('not_enabled');
79
+ });
80
+ });
81
+
82
+ describe('@rn-bridge-tools/expo exports', () => {
83
+ it('should export WebViewBridge and createDefaultHandlers', async () => {
84
+ const exports = await import('../index.js');
85
+ expect(exports.WebViewBridge).toBeDefined();
86
+ expect(exports.createDefaultHandlers).toBeDefined();
87
+ });
88
+ });
@@ -0,0 +1,67 @@
1
+ import type { HandlerOptions } from '@rn-bridge-tools/core';
2
+ import { cameraHandlers } from './handlers/camera.js';
3
+ import { locationHandlers } from './handlers/location.js';
4
+ import { fileHandlers } from './handlers/file.js';
5
+ import { shareHandlers } from './handlers/share.js';
6
+ import { deviceHandlers } from './handlers/device.js';
7
+ import { statusbarHandlers } from './handlers/statusbar.js';
8
+ import { keyboardHandlers } from './handlers/keyboard.js';
9
+ import { hapticHandlers } from './handlers/haptic.js';
10
+ import { clipboardHandlers } from './handlers/clipboard.js';
11
+ import { scannerHandlers } from './handlers/scanner.js';
12
+ import { authHandlers } from './handlers/auth.js';
13
+ import { iapHandlers } from './handlers/iap.js';
14
+ import { pushHandlers } from './handlers/push.js';
15
+ import { permissionHandlers } from './handlers/permission.js';
16
+ import { preferenceHandlers } from './handlers/preference.js';
17
+ import { navigationHandlers } from './handlers/navigation.js';
18
+ import { browserHandlers } from './handlers/browser.js';
19
+
20
+ export type HandlerFn = (payload: never) => Promise<unknown> | unknown;
21
+ export type HandlerMap = Record<string, HandlerFn>;
22
+
23
+ const handlerRegistry: Record<string, Record<string, (payload: never) => Promise<unknown> | unknown>> = {
24
+ camera: cameraHandlers as Record<string, (payload: never) => Promise<unknown>>,
25
+ location: locationHandlers as Record<string, (payload: never) => Promise<unknown>>,
26
+ file: fileHandlers as Record<string, (payload: never) => Promise<unknown>>,
27
+ share: shareHandlers as Record<string, (payload: never) => Promise<unknown>>,
28
+ device: deviceHandlers as Record<string, (payload: never) => Promise<unknown>>,
29
+ statusbar: statusbarHandlers as Record<string, (payload: never) => Promise<unknown>>,
30
+ keyboard: keyboardHandlers as Record<string, (payload: never) => Promise<unknown>>,
31
+ haptic: hapticHandlers as Record<string, (payload: never) => Promise<unknown>>,
32
+ clipboard: clipboardHandlers as Record<string, (payload: never) => Promise<unknown>>,
33
+ scanner: scannerHandlers as Record<string, (payload: never) => Promise<unknown>>,
34
+ auth: authHandlers as Record<string, (payload: never) => Promise<unknown>>,
35
+ iap: iapHandlers as Record<string, (payload: never) => Promise<unknown>>,
36
+ push: pushHandlers as Record<string, (payload: never) => Promise<unknown>>,
37
+ permission: permissionHandlers as Record<string, (payload: never) => Promise<unknown>>,
38
+ preference: preferenceHandlers as Record<string, (payload: never) => Promise<unknown>>,
39
+ navigation: navigationHandlers as Record<string, (payload: never) => Promise<unknown>>,
40
+ browser: browserHandlers as Record<string, (payload: never) => Promise<unknown>>,
41
+ };
42
+
43
+ function createDisabledHandler(_namespace: string, _action: string): HandlerFn {
44
+ return async () => ({
45
+ success: false,
46
+ error: `not_enabled: ${_namespace}.${_action}`,
47
+ });
48
+ }
49
+
50
+ export function createDefaultHandlers(options: HandlerOptions = {}): HandlerMap {
51
+ const result: HandlerMap = {};
52
+
53
+ for (const [namespace, handlers] of Object.entries(handlerRegistry)) {
54
+ const enabled = options[namespace as keyof HandlerOptions] ?? false;
55
+
56
+ for (const [action, handler] of Object.entries(handlers)) {
57
+ if (enabled) {
58
+ result[action] = handler as HandlerFn;
59
+ } else {
60
+ const actionName = action.split('.')[1] ?? action;
61
+ result[action] = createDisabledHandler(namespace, actionName);
62
+ }
63
+ }
64
+ }
65
+
66
+ return result;
67
+ }
@@ -0,0 +1,69 @@
1
+ import type { AuthNamespace } from '@rn-bridge-tools/core';
2
+
3
+ declare const require: (id: string) => unknown;
4
+
5
+ interface LocalAuthModule {
6
+ authenticateAsync: (opts: {
7
+ promptMessage?: string;
8
+ cancelLabel?: string;
9
+ fallbackLabel?: string;
10
+ }) => Promise<{ success: boolean; error?: string }>;
11
+ hasHardwareAsync: () => Promise<boolean>;
12
+ supportedAuthenticationTypesAsync: () => Promise<number[]>;
13
+ AuthenticationType: {
14
+ FACIAL_RECOGNITION: number;
15
+ FINGERPRINT: number;
16
+ IRIS: number;
17
+ };
18
+ }
19
+
20
+ export const authHandlers = {
21
+ 'auth.biometric': async (
22
+ payload: AuthNamespace['auth.biometric']['request'],
23
+ ): Promise<AuthNamespace['auth.biometric']['response']> => {
24
+ let LocalAuth: LocalAuthModule | null = null;
25
+ try { LocalAuth = require('expo-local-authentication') as LocalAuthModule; } catch {}
26
+ if (!LocalAuth) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-local-authentication' } as never;
27
+
28
+ try {
29
+ const result = await LocalAuth.authenticateAsync({
30
+ promptMessage: payload.promptMessage ?? 'Authenticate',
31
+ cancelLabel: payload.cancelLabel,
32
+ fallbackLabel: payload.fallbackLabel,
33
+ });
34
+
35
+ if (result.success) {
36
+ return { success: true };
37
+ }
38
+ return { success: false, error: result.error };
39
+ } catch (err) {
40
+ return { success: false, error: String(err) };
41
+ }
42
+ },
43
+
44
+ 'auth.isBiometricAvailable': async (
45
+ _payload: AuthNamespace['auth.isBiometricAvailable']['request'],
46
+ ): Promise<AuthNamespace['auth.isBiometricAvailable']['response']> => {
47
+ let LocalAuth: LocalAuthModule | null = null;
48
+ try { LocalAuth = require('expo-local-authentication') as LocalAuthModule; } catch {}
49
+ if (!LocalAuth) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-local-authentication' } as never;
50
+
51
+ try {
52
+ const available = await LocalAuth.hasHardwareAsync();
53
+ const types = await LocalAuth.supportedAuthenticationTypesAsync();
54
+
55
+ let biometryType: 'fingerprint' | 'facial' | 'iris' | undefined;
56
+ if (types.includes(LocalAuth.AuthenticationType.FACIAL_RECOGNITION)) {
57
+ biometryType = 'facial';
58
+ } else if (types.includes(LocalAuth.AuthenticationType.FINGERPRINT)) {
59
+ biometryType = 'fingerprint';
60
+ } else if (types.includes(LocalAuth.AuthenticationType.IRIS)) {
61
+ biometryType = 'iris';
62
+ }
63
+
64
+ return { available, biometryType };
65
+ } catch {
66
+ return { available: false };
67
+ }
68
+ },
69
+ };
@@ -0,0 +1,42 @@
1
+ import type { BrowserNamespace } from '@rn-bridge-tools/core';
2
+ import { Linking } from 'react-native';
3
+
4
+ declare const require: (id: string) => unknown;
5
+
6
+ interface WebBrowserModule {
7
+ openBrowserAsync: (
8
+ url: string,
9
+ opts?: { showTitle?: boolean; toolbarColor?: string },
10
+ ) => Promise<void>;
11
+ }
12
+
13
+ export const browserHandlers = {
14
+ 'browser.openExternal': async (
15
+ payload: BrowserNamespace['browser.openExternal']['request'],
16
+ ): Promise<BrowserNamespace['browser.openExternal']['response']> => {
17
+ try {
18
+ await Linking.openURL(payload.url);
19
+ return { success: true };
20
+ } catch {
21
+ return { success: false };
22
+ }
23
+ },
24
+
25
+ 'browser.openInternal': async (
26
+ payload: BrowserNamespace['browser.openInternal']['request'],
27
+ ): Promise<BrowserNamespace['browser.openInternal']['response']> => {
28
+ let WebBrowser: WebBrowserModule | null = null;
29
+ try { WebBrowser = require('expo-web-browser') as WebBrowserModule; } catch {}
30
+ if (!WebBrowser) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-web-browser' } as never;
31
+
32
+ try {
33
+ await WebBrowser.openBrowserAsync(payload.url, {
34
+ showTitle: payload.showTitle,
35
+ toolbarColor: payload.toolbarColor,
36
+ });
37
+ return { success: true };
38
+ } catch {
39
+ return { success: false };
40
+ }
41
+ },
42
+ };
@@ -0,0 +1,97 @@
1
+ import type { CameraNamespace } from '@rn-bridge-tools/core';
2
+
3
+ declare const require: (id: string) => unknown;
4
+
5
+ interface ImagePickerModule {
6
+ launchCameraAsync: (opts: {
7
+ quality?: number;
8
+ cameraType?: unknown;
9
+ allowsEditing?: boolean;
10
+ }) => Promise<{
11
+ canceled: boolean;
12
+ assets: Array<{
13
+ uri: string;
14
+ width: number;
15
+ height: number;
16
+ fileSize?: number;
17
+ mimeType?: string | null;
18
+ }>;
19
+ }>;
20
+ launchImageLibraryAsync: (opts: {
21
+ allowsMultipleSelection?: boolean;
22
+ quality?: number;
23
+ selectionLimit?: number;
24
+ }) => Promise<{
25
+ canceled: boolean;
26
+ assets: Array<{
27
+ uri: string;
28
+ width: number;
29
+ height: number;
30
+ fileSize?: number;
31
+ mimeType?: string | null;
32
+ }>;
33
+ }>;
34
+ CameraType: { front: unknown; back: unknown };
35
+ }
36
+
37
+ export const cameraHandlers = {
38
+ 'camera.take': async (
39
+ payload: CameraNamespace['camera.take']['request'],
40
+ ): Promise<CameraNamespace['camera.take']['response']> => {
41
+ let ImagePicker: ImagePickerModule | null = null;
42
+ try { ImagePicker = require('expo-image-picker') as ImagePickerModule; } catch {}
43
+ if (!ImagePicker) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-image-picker' } as never;
44
+
45
+ const result = await ImagePicker.launchCameraAsync({
46
+ quality: payload.quality ?? 0.8,
47
+ cameraType:
48
+ payload.facing === 'front'
49
+ ? ImagePicker.CameraType.front
50
+ : ImagePicker.CameraType.back,
51
+ allowsEditing: payload.allowsEditing ?? false,
52
+ });
53
+
54
+ if (result.canceled) {
55
+ return { success: false, assets: [] } as never;
56
+ }
57
+
58
+ const asset = result.assets[0];
59
+ return {
60
+ success: true,
61
+ uri: asset.uri,
62
+ width: asset.width,
63
+ height: asset.height,
64
+ fileSize: asset.fileSize,
65
+ mimeType: asset.mimeType ?? undefined,
66
+ };
67
+ },
68
+
69
+ 'camera.pickImage': async (
70
+ payload: CameraNamespace['camera.pickImage']['request'],
71
+ ): Promise<CameraNamespace['camera.pickImage']['response']> => {
72
+ let ImagePicker: ImagePickerModule | null = null;
73
+ try { ImagePicker = require('expo-image-picker') as ImagePickerModule; } catch {}
74
+ if (!ImagePicker) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-image-picker' } as never;
75
+
76
+ const result = await ImagePicker.launchImageLibraryAsync({
77
+ allowsMultipleSelection: payload.allowsMultipleSelection ?? false,
78
+ quality: payload.quality ?? 0.8,
79
+ selectionLimit: payload.maxCount ?? 0,
80
+ });
81
+
82
+ if (result.canceled) {
83
+ return { success: false, assets: [] };
84
+ }
85
+
86
+ return {
87
+ success: true,
88
+ assets: result.assets.map((a) => ({
89
+ uri: a.uri,
90
+ width: a.width,
91
+ height: a.height,
92
+ fileSize: a.fileSize,
93
+ mimeType: a.mimeType ?? undefined,
94
+ })),
95
+ };
96
+ },
97
+ };
@@ -0,0 +1,32 @@
1
+ import type { ClipboardNamespace } from '@rn-bridge-tools/core';
2
+
3
+ declare const require: (id: string) => unknown;
4
+
5
+ interface ClipboardModule {
6
+ setStringAsync: (text: string) => Promise<void>;
7
+ getStringAsync: () => Promise<string>;
8
+ }
9
+
10
+ export const clipboardHandlers = {
11
+ 'clipboard.copy': async (
12
+ payload: ClipboardNamespace['clipboard.copy']['request'],
13
+ ): Promise<ClipboardNamespace['clipboard.copy']['response']> => {
14
+ let Clipboard: ClipboardModule | null = null;
15
+ try { Clipboard = require('expo-clipboard') as ClipboardModule; } catch {}
16
+ if (!Clipboard) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-clipboard' } as never;
17
+
18
+ await Clipboard.setStringAsync(payload.text);
19
+ return { success: true };
20
+ },
21
+
22
+ 'clipboard.paste': async (
23
+ _payload: ClipboardNamespace['clipboard.paste']['request'],
24
+ ): Promise<ClipboardNamespace['clipboard.paste']['response']> => {
25
+ let Clipboard: ClipboardModule | null = null;
26
+ try { Clipboard = require('expo-clipboard') as ClipboardModule; } catch {}
27
+ if (!Clipboard) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-clipboard' } as never;
28
+
29
+ const text = await Clipboard.getStringAsync();
30
+ return { text, hasContent: text.length > 0 };
31
+ },
32
+ };
@@ -0,0 +1,71 @@
1
+ import type { DeviceNamespace } from '@rn-bridge-tools/core';
2
+ import { Platform } from 'react-native';
3
+
4
+ declare const require: (id: string) => unknown;
5
+
6
+ interface DeviceModule {
7
+ modelName: string | null;
8
+ brand: string | null;
9
+ deviceType: number | null;
10
+ DeviceType: { TABLET: number };
11
+ }
12
+
13
+ export const deviceHandlers = {
14
+ 'device.getInfo': async (
15
+ _payload: DeviceNamespace['device.getInfo']['request'],
16
+ ): Promise<DeviceNamespace['device.getInfo']['response']> => {
17
+ let Device: DeviceModule | null = null;
18
+ try { Device = require('expo-device') as DeviceModule; } catch {}
19
+ if (!Device) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-device' } as never;
20
+
21
+ return {
22
+ os: Platform.OS as 'ios' | 'android',
23
+ osVersion: Platform.Version?.toString() ?? '',
24
+ model: Device.modelName ?? '',
25
+ brand: Device.brand ?? '',
26
+ isTablet: Device.deviceType === Device.DeviceType.TABLET,
27
+ appVersion: '1.0.0',
28
+ buildNumber: '1',
29
+ bundleId: '',
30
+ };
31
+ },
32
+
33
+ 'device.getBattery': async (
34
+ _payload: DeviceNamespace['device.getBattery']['request'],
35
+ ): Promise<DeviceNamespace['device.getBattery']['response']> => {
36
+ try {
37
+ let Battery: { getBatteryLevelAsync: () => Promise<number>; getBatteryStateAsync: () => Promise<number>; BatteryState: { CHARGING: number } } | null = null;
38
+ try { Battery = require('expo-battery') as { getBatteryLevelAsync: () => Promise<number>; getBatteryStateAsync: () => Promise<number>; BatteryState: { CHARGING: number } }; } catch {}
39
+ if (!Battery) {
40
+ return { level: -1, isCharging: false };
41
+ }
42
+ const level = await Battery.getBatteryLevelAsync();
43
+ const state = await Battery.getBatteryStateAsync();
44
+ return {
45
+ level,
46
+ isCharging: state === Battery.BatteryState.CHARGING,
47
+ };
48
+ } catch {
49
+ return { level: -1, isCharging: false };
50
+ }
51
+ },
52
+
53
+ 'device.getNetwork': async (
54
+ _payload: DeviceNamespace['device.getNetwork']['request'],
55
+ ): Promise<DeviceNamespace['device.getNetwork']['response']> => {
56
+ try {
57
+ let NetInfo: { fetch: () => Promise<{ type: string; isConnected: boolean | null }> } | null = null;
58
+ try { NetInfo = require('@react-native-community/netinfo') as { fetch: () => Promise<{ type: string; isConnected: boolean | null }> }; } catch {}
59
+ if (!NetInfo) {
60
+ return { type: 'unknown', isConnected: true };
61
+ }
62
+ const state = await NetInfo.fetch();
63
+ return {
64
+ type: (state.type as 'wifi' | 'cellular' | 'none' | 'unknown') ?? 'unknown',
65
+ isConnected: state.isConnected ?? false,
66
+ };
67
+ } catch {
68
+ return { type: 'unknown', isConnected: true };
69
+ }
70
+ },
71
+ };
@@ -0,0 +1,126 @@
1
+ import type { FileNamespace } from '@rn-bridge-tools/core';
2
+
3
+ declare const require: (id: string) => unknown;
4
+
5
+ interface FileSystemModule {
6
+ Paths: {
7
+ document: { uri: string };
8
+ cache: { uri: string };
9
+ };
10
+ File: new (...uris: (string | { uri: string })[]) => {
11
+ uri: string;
12
+ text: () => Promise<string>;
13
+ write: (content: string) => void;
14
+ create: () => void;
15
+ exists: boolean;
16
+ };
17
+ Directory: new (...uris: (string | { uri: string })[]) => {
18
+ uri: string;
19
+ create: () => void;
20
+ exists: boolean;
21
+ };
22
+ }
23
+
24
+ interface DocumentPickerModule {
25
+ getDocumentAsync: (opts: {
26
+ type?: string[];
27
+ multiple?: boolean;
28
+ }) => Promise<{
29
+ canceled: boolean;
30
+ assets: Array<{
31
+ uri: string;
32
+ name: string;
33
+ size?: number;
34
+ mimeType?: string;
35
+ }>;
36
+ }>;
37
+ }
38
+
39
+ export const fileHandlers = {
40
+ 'file.download': async (
41
+ payload: FileNamespace['file.download']['request'],
42
+ ): Promise<FileNamespace['file.download']['response']> => {
43
+ let FS: FileSystemModule | null = null;
44
+ try { FS = require('expo-file-system') as FileSystemModule; } catch {}
45
+ if (!FS) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-file-system' } as never;
46
+
47
+ try {
48
+ const response = await fetch(payload.url);
49
+ const text = await response.text();
50
+ const file = new FS.File(FS.Paths.document, payload.filename);
51
+ file.write(text);
52
+ return { success: true, localUri: file.uri };
53
+ } catch (err) {
54
+ return { success: false, error: String(err) };
55
+ }
56
+ },
57
+
58
+ 'file.read': async (
59
+ payload: FileNamespace['file.read']['request'],
60
+ ): Promise<FileNamespace['file.read']['response']> => {
61
+ let FS: FileSystemModule | null = null;
62
+ try { FS = require('expo-file-system') as FileSystemModule; } catch {}
63
+ if (!FS) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-file-system' } as never;
64
+
65
+ try {
66
+ const file = new FS.File(payload.uri);
67
+ const content = await file.text();
68
+ return { success: true, content };
69
+ } catch (err) {
70
+ return { success: false, error: String(err) };
71
+ }
72
+ },
73
+
74
+ 'file.write': async (
75
+ payload: FileNamespace['file.write']['request'],
76
+ ): Promise<FileNamespace['file.write']['response']> => {
77
+ let FS: FileSystemModule | null = null;
78
+ try { FS = require('expo-file-system') as FileSystemModule; } catch {}
79
+ if (!FS) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-file-system' } as never;
80
+
81
+ try {
82
+ const dirMap = {
83
+ document: FS.Paths.document,
84
+ cache: FS.Paths.cache,
85
+ temp: FS.Paths.cache,
86
+ };
87
+ const baseDir = dirMap[payload.directory ?? 'document'];
88
+ const file = new FS.File(baseDir, payload.filename);
89
+ file.write(payload.content);
90
+ return { success: true, uri: file.uri };
91
+ } catch (err) {
92
+ return { success: false, error: String(err) };
93
+ }
94
+ },
95
+
96
+ 'file.pick': async (
97
+ payload: FileNamespace['file.pick']['request'],
98
+ ): Promise<FileNamespace['file.pick']['response']> => {
99
+ let DocumentPicker: DocumentPickerModule | null = null;
100
+ try { DocumentPicker = require('expo-document-picker') as DocumentPickerModule; } catch {}
101
+ if (!DocumentPicker) return { success: false, error: 'MODULE_NOT_INSTALLED: expo-document-picker' } as never;
102
+
103
+ try {
104
+ const result = await DocumentPicker.getDocumentAsync({
105
+ type: payload.type ?? ['*/*'],
106
+ multiple: payload.multiple ?? false,
107
+ });
108
+
109
+ if (result.canceled) {
110
+ return { success: false, files: [] };
111
+ }
112
+
113
+ return {
114
+ success: true,
115
+ files: result.assets.map((a) => ({
116
+ uri: a.uri,
117
+ name: a.name,
118
+ size: a.size ?? 0,
119
+ mimeType: a.mimeType ?? 'application/octet-stream',
120
+ })),
121
+ };
122
+ } catch {
123
+ return { success: false, files: [] };
124
+ }
125
+ },
126
+ };
@@ -0,0 +1,56 @@
1
+ import type { HapticNamespace } from '@rn-bridge-tools/core';
2
+
3
+ declare const require: (id: string) => unknown;
4
+
5
+ interface HapticsModule {
6
+ ImpactFeedbackStyle: { Light: unknown; Medium: unknown; Heavy: unknown };
7
+ NotificationFeedbackType: { Success: unknown; Warning: unknown; Error: unknown };
8
+ impactAsync: (style: never) => Promise<void>;
9
+ notificationAsync: (type: never) => Promise<void>;
10
+ selectionAsync: () => Promise<void>;
11
+ }
12
+
13
+ export const hapticHandlers = {
14
+ 'haptic.impact': async (
15
+ payload: HapticNamespace['haptic.impact']['request'],
16
+ ): Promise<void> => {
17
+ let Haptics: HapticsModule | null = null;
18
+ try { Haptics = require('expo-haptics') as HapticsModule; } catch {}
19
+ if (!Haptics) {
20
+ return;
21
+ }
22
+
23
+ const styleMap: Record<string, unknown> = {
24
+ light: Haptics.ImpactFeedbackStyle.Light,
25
+ medium: Haptics.ImpactFeedbackStyle.Medium,
26
+ heavy: Haptics.ImpactFeedbackStyle.Heavy,
27
+ };
28
+ await Haptics.impactAsync(styleMap[payload.style] as never);
29
+ },
30
+
31
+ 'haptic.notification': async (
32
+ payload: HapticNamespace['haptic.notification']['request'],
33
+ ): Promise<void> => {
34
+ let Haptics: HapticsModule | null = null;
35
+ try { Haptics = require('expo-haptics') as HapticsModule; } catch {}
36
+ if (!Haptics) {
37
+ return;
38
+ }
39
+
40
+ const typeMap: Record<string, unknown> = {
41
+ success: Haptics.NotificationFeedbackType.Success,
42
+ warning: Haptics.NotificationFeedbackType.Warning,
43
+ error: Haptics.NotificationFeedbackType.Error,
44
+ };
45
+ await Haptics.notificationAsync(typeMap[payload.type] as never);
46
+ },
47
+
48
+ 'haptic.selection': async (): Promise<void> => {
49
+ let Haptics: HapticsModule | null = null;
50
+ try { Haptics = require('expo-haptics') as HapticsModule; } catch {}
51
+ if (!Haptics) {
52
+ return;
53
+ }
54
+ await Haptics.selectionAsync();
55
+ },
56
+ };
@@ -0,0 +1,23 @@
1
+ import type { IAPNamespace } from '@rn-bridge-tools/core';
2
+
3
+ export const iapHandlers = {
4
+ 'iap.getProducts': async (
5
+ _payload: IAPNamespace['iap.getProducts']['request'],
6
+ ): Promise<IAPNamespace['iap.getProducts']['response']> => {
7
+ // IAP requires react-native-iap or expo-in-app-purchases
8
+ // Placeholder implementation
9
+ return { products: [] };
10
+ },
11
+
12
+ 'iap.purchase': async (
13
+ _payload: IAPNamespace['iap.purchase']['request'],
14
+ ): Promise<IAPNamespace['iap.purchase']['response']> => {
15
+ return { success: false, error: 'IAP not configured' };
16
+ },
17
+
18
+ 'iap.restore': async (
19
+ _payload: IAPNamespace['iap.restore']['request'],
20
+ ): Promise<IAPNamespace['iap.restore']['response']> => {
21
+ return { purchases: [] };
22
+ },
23
+ };