@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.
- package/dist/WebViewBridge.d.ts +22 -0
- package/dist/WebViewBridge.d.ts.map +1 -0
- package/dist/__tests__/createHandlers.test.d.ts +2 -0
- package/dist/__tests__/createHandlers.test.d.ts.map +1 -0
- package/dist/createHandlers.d.ts +5 -0
- package/dist/createHandlers.d.ts.map +1 -0
- package/dist/handlers/auth.d.ts +6 -0
- package/dist/handlers/auth.d.ts.map +1 -0
- package/dist/handlers/browser.d.ts +6 -0
- package/dist/handlers/browser.d.ts.map +1 -0
- package/dist/handlers/camera.d.ts +6 -0
- package/dist/handlers/camera.d.ts.map +1 -0
- package/dist/handlers/clipboard.d.ts +6 -0
- package/dist/handlers/clipboard.d.ts.map +1 -0
- package/dist/handlers/device.d.ts +7 -0
- package/dist/handlers/device.d.ts.map +1 -0
- package/dist/handlers/file.d.ts +8 -0
- package/dist/handlers/file.d.ts.map +1 -0
- package/dist/handlers/haptic.d.ts +7 -0
- package/dist/handlers/haptic.d.ts.map +1 -0
- package/dist/handlers/iap.d.ts +7 -0
- package/dist/handlers/iap.d.ts.map +1 -0
- package/dist/handlers/keyboard.d.ts +6 -0
- package/dist/handlers/keyboard.d.ts.map +1 -0
- package/dist/handlers/location.d.ts +7 -0
- package/dist/handlers/location.d.ts.map +1 -0
- package/dist/handlers/navigation.d.ts +8 -0
- package/dist/handlers/navigation.d.ts.map +1 -0
- package/dist/handlers/permission.d.ts +7 -0
- package/dist/handlers/permission.d.ts.map +1 -0
- package/dist/handlers/preference.d.ts +8 -0
- package/dist/handlers/preference.d.ts.map +1 -0
- package/dist/handlers/push.d.ts +6 -0
- package/dist/handlers/push.d.ts.map +1 -0
- package/dist/handlers/scanner.d.ts +5 -0
- package/dist/handlers/scanner.d.ts.map +1 -0
- package/dist/handlers/share.d.ts +5 -0
- package/dist/handlers/share.d.ts.map +1 -0
- package/dist/handlers/statusbar.d.ts +7 -0
- package/dist/handlers/statusbar.d.ts.map +1 -0
- package/dist/index.d.ts +22 -124
- package/dist/index.d.ts.map +1 -0
- package/package.json +7 -6
- package/src/WebViewBridge.tsx +58 -0
- package/src/__tests__/createHandlers.test.ts +88 -0
- package/src/createHandlers.ts +67 -0
- package/src/handlers/auth.ts +69 -0
- package/src/handlers/browser.ts +42 -0
- package/src/handlers/camera.ts +97 -0
- package/src/handlers/clipboard.ts +32 -0
- package/src/handlers/device.ts +71 -0
- package/src/handlers/file.ts +126 -0
- package/src/handlers/haptic.ts +56 -0
- package/src/handlers/iap.ts +23 -0
- package/src/handlers/keyboard.ts +20 -0
- package/src/handlers/location.ts +104 -0
- package/src/handlers/navigation.ts +29 -0
- package/src/handlers/permission.ts +107 -0
- package/src/handlers/preference.ts +58 -0
- package/src/handlers/push.ts +47 -0
- package/src/handlers/scanner.ts +31 -0
- package/src/handlers/share.ts +31 -0
- package/src/handlers/statusbar.ts +35 -0
- package/src/index.ts +23 -0
- package/dist/index.js +0 -846
- 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
|
+
};
|