summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhc <haocheng.xie@respiree.com>2026-04-13 16:37:52 +0800
committerhc <haocheng.xie@respiree.com>2026-04-13 16:37:52 +0800
commitf22bb303a081d6779592b90a68feee2203d7962a (patch)
tree6e20af1d4b3c9ccd9ce3e1f88fdcf189801300be
parentda5bb92c8ee7c4cf44fb0787e63abaab1ced8236 (diff)
Dashboard with continuous scan, unified device list, AsyncStorage persistence
-rw-r--r--App.tsx836
-rw-r--r--package-lock.json34
-rw-r--r--package.json1
3 files changed, 585 insertions, 286 deletions
diff --git a/App.tsx b/App.tsx
index 9dca202..4ce26df 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,4 +1,4 @@
1import React, {useState, useEffect, useRef} from 'react'; 1import React, {useState, useEffect, useRef, useCallback} from 'react';
2import { 2import {
3 StyleSheet, 3 StyleSheet,
4 View, 4 View,
@@ -11,9 +11,10 @@ import {
11 DeviceEventEmitter, 11 DeviceEventEmitter,
12 NativeEventEmitter, 12 NativeEventEmitter,
13 NativeModules, 13 NativeModules,
14 Alert, 14 ActivityIndicator,
15} from 'react-native'; 15} from 'react-native';
16import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; 16import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context';
17import AsyncStorage from '@react-native-async-storage/async-storage';
17 18
18// ── Native module types ────────────────────────────────────────────── 19// ── Native module types ──────────────────────────────────────────────
19 20
@@ -59,34 +60,44 @@ interface IBP550BTModule {
59 getAllConnectedDevices(): void; 60 getAllConnectedDevices(): void;
60} 61}
61 62
62const iHealthDeviceManagerModule = 63const mgr = NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager;
63 NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager; 64const bp550 = NativeModules.BP550BTModule as IBP550BTModule;
64const BP550BTModule =
65 NativeModules.BP550BTModule as IBP550BTModule;
66 65
67type Device = { 66// ── Types ────────────────────────────────────────────────────────────
67
68type Device = {mac: string; type: string; rssi?: number; timestamp: number};
69
70type SavedDevice = {mac: string; type: string; addedAt: string};
71
72type Reading = {
68 mac: string; 73 mac: string;
69 type: string; 74 sys?: number;
70 rssi?: number; 75 dia?: number;
71 timestamp: number; 76 pulse?: number;
77 date?: string;
78 battery?: number;
79 fetchedAt: string;
72}; 80};
73 81
82type Screen = 'home' | 'dashboard' | 'debug' | 'debug-device';
83
84const STORAGE_KEY_DEVICES = '@ihealth/saved_devices';
85const STORAGE_KEY_READINGS = '@ihealth/readings';
86
74// ── Helpers ────────────────────────────────────────────────────────── 87// ── Helpers ──────────────────────────────────────────────────────────
75 88
76async function requestAndroidPermissions(): Promise<boolean> { 89async function requestAndroidPermissions(): Promise<boolean> {
77 if (Platform.OS !== 'android') return true; 90 if (Platform.OS !== 'android') return true;
78 const permissions: string[] = []; 91 const perms: string[] = [];
79 if (Platform.Version >= 31) { 92 if (Platform.Version >= 31) {
80 permissions.push( 93 perms.push(
81 PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, 94 PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
82 PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, 95 PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
83 ); 96 );
84 } 97 }
85 permissions.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION); 98 perms.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
86 const results = await PermissionsAndroid.requestMultiple(permissions as any); 99 const r = await PermissionsAndroid.requestMultiple(perms as any);
87 return Object.values(results).every( 100 return Object.values(r).every(v => v === PermissionsAndroid.RESULTS.GRANTED);
88 r => r === PermissionsAndroid.RESULTS.GRANTED,
89 );
90} 101}
91 102
92function getEmitter() { 103function getEmitter() {
@@ -101,329 +112,450 @@ function getBPEmitter() {
101 : DeviceEventEmitter; 112 : DeviceEventEmitter;
102} 113}
103 114
104// ── Device Detail Screen ───────────────────────────────────────────── 115// ── Home Screen ──────────────────────────────────────────────────────
105 116
106function DeviceScreen({ 117function HomeScreen({onNav}: {onNav: (s: Screen) => void}) {
107 device, 118 return (
108 onBack, 119 <SafeAreaView style={s.container}>
109}: { 120 <View style={s.homeCenter}>
110 device: Device; 121 <Text style={s.homeTitle}>iHealth</Text>
111 onBack: () => void; 122 <Text style={s.homeSubtitle}>Blood Pressure Monitor</Text>
112}) {
113 const [connected, setConnected] = useState(false);
114 const [connecting, setConnecting] = useState(false);
115 const [log, setLog] = useState<string[]>([]);
116 123
117 const addLog = (msg: string) => 124 <TouchableOpacity
118 setLog(prev => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev]); 125 style={s.homeButton}
126 onPress={() => onNav('dashboard')}>
127 <Text style={s.homeButtonText}>Dashboard</Text>
128 <Text style={s.homeButtonSub}>View your devices and readings</Text>
129 </TouchableOpacity>
130
131 <TouchableOpacity
132 style={[s.homeButton, s.homeButtonSecondary]}
133 onPress={() => onNav('debug')}>
134 <Text style={[s.homeButtonText, s.homeButtonTextSecondary]}>
135 Debug Scanner
136 </Text>
137 <Text style={[s.homeButtonSub, s.homeButtonTextSecondary]}>
138 Scan all devices, raw logs
139 </Text>
140 </TouchableOpacity>
141 </View>
142 </SafeAreaView>
143 );
144}
145
146// ── Dashboard Screen ─────────────────────────────────────────────────
119 147
148function DashboardScreen({onBack}: {onBack: () => void}) {
149 const [savedDevices, setSavedDevices] = useState<SavedDevice[]>([]);
150 const [readings, setReadings] = useState<Reading[]>([]);
151 const [foundDevices, setFoundDevices] = useState<Device[]>([]);
152 const [status, setStatus] = useState('Starting...');
153 const foundRef = useRef<Device[]>([]);
154 const savedRef = useRef<SavedDevice[]>([]);
155 const readingsRef = useRef<Reading[]>([]);
156 const syncingMac = useRef<string | null>(null);
157
158 // Load saved data on mount
159 useEffect(() => {
160 (async () => {
161 const [devJson, readJson] = await Promise.all([
162 AsyncStorage.getItem(STORAGE_KEY_DEVICES),
163 AsyncStorage.getItem(STORAGE_KEY_READINGS),
164 ]);
165 const devs: SavedDevice[] = devJson ? JSON.parse(devJson) : [];
166 const reads: Reading[] = readJson ? JSON.parse(readJson) : [];
167 setSavedDevices(devs);
168 savedRef.current = devs;
169 setReadings(reads);
170 readingsRef.current = reads;
171 await requestAndroidPermissions();
172 })();
173 }, []);
174
175 // Always-on scan loop: restart scan every time it finishes
120 useEffect(() => { 176 useEffect(() => {
121 const emitter = getEmitter(); 177 const emitter = getEmitter();
178 const bpEmitter = getBPEmitter();
122 179
123 const connSub = emitter.addListener( 180 const startScan = () => {
124 iHealthDeviceManagerModule.Event_Device_Connected ?? 'event_device_connected', 181 try { mgr.startDiscovery('ALL'); }
125 (e: {mac: string; type: string}) => { 182 catch (_) {}
126 if (e.mac === device.mac) { 183 setStatus('Scanning...');
127 setConnected(true); 184 };
128 setConnecting(false); 185
129 addLog(`Connected to ${e.type}`); 186 // When a device is found
187 const scanSub = emitter.addListener(
188 mgr.Event_Scan_Device ?? 'event_scan_device',
189 (e: {mac: string; type: string; rssi?: number}) => {
190 if (e.type === 'AM3') return;
191
192 // Track all found devices
193 const exists = foundRef.current.find(d => d.mac === e.mac);
194 if (!exists) {
195 const updated = [...foundRef.current, {mac: e.mac, type: e.type, rssi: e.rssi, timestamp: Date.now()}];
196 foundRef.current = updated;
197 setFoundDevices(updated);
198 }
199
200 // Auto-connect if it's a saved device we haven't synced yet
201 const isSaved = savedRef.current.some(d => d.mac === e.mac);
202 if (isSaved && !syncingMac.current) {
203 syncingMac.current = e.mac;
204 setStatus(`Found ${e.mac.slice(-4)}, connecting...`);
205 try { mgr.stopDiscovery(); } catch (_) {}
206 mgr.connectDevice(e.mac, e.type);
130 } 207 }
131 }, 208 },
132 ); 209 );
133 210
134 const failSub = emitter.addListener( 211 // When scan finishes, restart after a short delay
135 iHealthDeviceManagerModule.Event_Device_Connect_Failed ?? 'event_device_connect_failed', 212 const finSub = emitter.addListener(
136 (e: {mac: string; errorid?: number}) => { 213 mgr.Event_Scan_Finish ?? 'event_scan_finish',
137 if (e.mac === device.mac) { 214 () => {
138 setConnecting(false); 215 if (!syncingMac.current) {
139 addLog(`Connection failed (error: ${e.errorid ?? 'unknown'})`); 216 setTimeout(startScan, 3000);
140 } 217 }
141 }, 218 },
142 ); 219 );
143 220
144 const dcSub = emitter.addListener( 221 // When connected, pull data
145 iHealthDeviceManagerModule.Event_Device_Disconnect ?? 'event_device_disconnect', 222 const connSub = emitter.addListener(
146 (e: {mac: string}) => { 223 mgr.Event_Device_Connected ?? 'event_device_connected',
147 if (e.mac === device.mac) { 224 async (e: {mac: string; type: string}) => {
148 setConnected(false); 225 if (e.mac !== syncingMac.current) return;
149 addLog('Disconnected'); 226 setStatus(`Connected to ${e.mac.slice(-4)}, reading...`);
227
228 // Get battery
229 const battery = await new Promise<number | undefined>(resolve => {
230 const sub = bpEmitter.addListener(
231 bp550?.Event_Notify ?? 'event_notify',
232 (ev: Record<string, unknown>) => {
233 if (ev.action === 'battery_bp' && ev.battery != null) {
234 sub.remove(); resolve(ev.battery as number);
235 }
236 },
237 );
238 bp550.getBattery(e.mac);
239 setTimeout(() => { sub.remove(); resolve(undefined); }, 5000);
240 });
241
242 // Get offline data
243 const offlineData = await new Promise<Record<string, unknown>[]>(resolve => {
244 const results: Record<string, unknown>[] = [];
245 const sub = bpEmitter.addListener(
246 bp550?.Event_Notify ?? 'event_notify',
247 (ev: Record<string, unknown>) => {
248 if (ev.action === 'historicaldata_bp' && ev.data) {
249 results.push(...(ev.data as Record<string, unknown>[]));
250 }
251 if (ev.action === 'get_historical_over_bp') {
252 sub.remove(); resolve(results);
253 }
254 if (ev.action === 'offlinenum' && ev.offlinenum === 0) {
255 sub.remove(); resolve(results);
256 }
257 },
258 );
259 bp550.getOffLineData(e.mac);
260 setTimeout(() => { sub.remove(); resolve(results); }, 10000);
261 });
262
263 // Save new readings
264 const newReadings = [...readingsRef.current];
265 for (const d of offlineData) {
266 newReadings.push({
267 mac: e.mac, sys: d.sys as number | undefined,
268 dia: d.dia as number | undefined,
269 pulse: d.heartRate as number | undefined,
270 date: d.date as string | undefined,
271 battery, fetchedAt: new Date().toISOString(),
272 });
150 } 273 }
274 if (offlineData.length === 0 && battery != null) {
275 newReadings.push({mac: e.mac, battery, fetchedAt: new Date().toISOString()});
276 }
277 readingsRef.current = newReadings;
278 setReadings(newReadings);
279 await AsyncStorage.setItem(STORAGE_KEY_READINGS, JSON.stringify(newReadings));
280
281 // Disconnect and resume scanning
282 try { bp550.disconnect(e.mac); } catch (_) {}
283 setStatus(`Synced ${e.mac.slice(-4)} (${offlineData.length} readings)`);
284 syncingMac.current = null;
285 setTimeout(startScan, 3000);
151 }, 286 },
152 ); 287 );
153 288
154 // BP550BT event listener 289 const failSub = emitter.addListener(
155 const bpEmitter = getBPEmitter(); 290 mgr.Event_Device_Connect_Failed ?? 'event_device_connect_failed',
156 const notifySub = bpEmitter.addListener( 291 () => {
157 BP550BTModule?.Event_Notify ?? 'event_notify', 292 setStatus('Connect failed, resuming scan...');
158 (e: Record<string, unknown>) => { 293 syncingMac.current = null;
159 addLog(JSON.stringify(e, null, 2)); 294 setTimeout(startScan, 3000);
160 }, 295 },
161 ); 296 );
162 297
298 const dcSub = emitter.addListener(
299 mgr.Event_Device_Disconnect ?? 'event_device_disconnect',
300 () => {
301 if (syncingMac.current) {
302 syncingMac.current = null;
303 setTimeout(startScan, 3000);
304 }
305 },
306 );
307
308 // Start first scan
309 startScan();
310
163 return () => { 311 return () => {
164 connSub.remove(); 312 scanSub.remove(); finSub.remove(); connSub.remove();
165 failSub.remove(); 313 failSub.remove(); dcSub.remove();
166 dcSub.remove(); 314 try { mgr.stopDiscovery(); } catch (_) {}
167 notifySub.remove();
168 }; 315 };
169 }, [device.mac]); 316 }, []);
170 317
171 const connect = () => { 318 const addDevice = async (dev: Device) => {
172 setConnecting(true); 319 const newDev: SavedDevice = {mac: dev.mac, type: dev.type, addedAt: new Date().toISOString()};
173 addLog(`Connecting to ${device.mac}...`); 320 const updated = [...savedRef.current, newDev];
174 iHealthDeviceManagerModule.connectDevice(device.mac, device.type); 321 savedRef.current = updated;
322 setSavedDevices(updated);
323 await AsyncStorage.setItem(STORAGE_KEY_DEVICES, JSON.stringify(updated));
324 const f = foundRef.current.filter(d => d.mac !== dev.mac);
325 foundRef.current = f;
326 setFoundDevices(f);
175 }; 327 };
176 328
177 const disconnect = () => { 329 const removeDevice = async (mac: string) => {
178 addLog('Disconnecting...'); 330 const updated = savedRef.current.filter(d => d.mac !== mac);
179 if (BP550BTModule) { 331 savedRef.current = updated;
180 BP550BTModule.disconnect(device.mac); 332 setSavedDevices(updated);
181 } else { 333 await AsyncStorage.setItem(STORAGE_KEY_DEVICES, JSON.stringify(updated));
182 iHealthDeviceManagerModule.disconnectDevice(device.mac, device.type);
183 }
184 }; 334 };
185 335
186 const actions: {label: string; fn: () => void; needsConnect: boolean}[] = [ 336 const isDeviceSaved = (mac: string) => savedRef.current.some(d => d.mac === mac);
187 { 337
188 label: 'Get Battery', 338 const recentReadings = readings
189 needsConnect: true, 339 .filter(r => r.sys != null)
190 fn: () => { 340 .slice(-20)
191 addLog('Requesting battery...'); 341 .reverse();
192 BP550BTModule.getBattery(device.mac);
193 },
194 },
195 {
196 label: 'Get Function Info',
197 needsConnect: true,
198 fn: () => {
199 addLog('Requesting function info...');
200 BP550BTModule.getFunctionInfo(device.mac);
201 },
202 },
203 {
204 label: 'Get Offline Count',
205 needsConnect: true,
206 fn: () => {
207 addLog('Requesting offline data count...');
208 BP550BTModule.getOffLineNum(device.mac);
209 },
210 },
211 {
212 label: 'Get Offline Data',
213 needsConnect: true,
214 fn: () => {
215 addLog('Requesting offline data...');
216 BP550BTModule.getOffLineData(device.mac);
217 },
218 },
219 {
220 label: 'Get IDPS',
221 needsConnect: false,
222 fn: () => {
223 addLog('Requesting IDPS...');
224 iHealthDeviceManagerModule.getDevicesIDPS(device.mac, (idps) => {
225 addLog(`IDPS: ${JSON.stringify(idps, null, 2)}`);
226 });
227 },
228 },
229 ];
230 342
231 return ( 343 return (
232 <SafeAreaView style={styles.container}> 344 <SafeAreaView style={s.container}>
233 <TouchableOpacity style={styles.backButton} onPress={onBack}> 345 <TouchableOpacity style={s.backButton} onPress={onBack}>
234 <Text style={styles.backText}>Back</Text> 346 <Text style={s.backText}>Home</Text>
235 </TouchableOpacity> 347 </TouchableOpacity>
236 348
237 <Text style={styles.title}>{device.type}</Text> 349 <Text style={s.title}>Dashboard 🦄✨</Text>
238 <Text style={styles.deviceMac}>{device.mac}</Text>
239 <Text style={[styles.statusBadge, connected && styles.statusConnected]}>
240 {connecting ? 'Connecting...' : connected ? 'Connected' : 'Disconnected'}
241 </Text>
242 350
243 {!connected && !connecting && ( 351 {/* Show status only when actively syncing a device */}
244 <TouchableOpacity style={styles.button} onPress={connect}> 352 {status.includes('connecting') || status.includes('reading') || status.includes('Synced') ? (
245 <Text style={styles.buttonText}>Connect</Text> 353 <View style={s.syncBar}>
246 </TouchableOpacity> 354 <ActivityIndicator size="small" color="#2196F3" />
247 )} 355 <Text style={s.syncText}>{status}</Text>
248 {connected && ( 356 </View>
249 <TouchableOpacity 357 ) : null}
250 style={[styles.button, styles.buttonStop]}
251 onPress={disconnect}>
252 <Text style={styles.buttonText}>Disconnect</Text>
253 </TouchableOpacity>
254 )}
255 358
256 <Text style={styles.sectionTitle}>Actions</Text> 359 {/* All devices — unified list, saved ones marked */}
257 <ScrollView 360 <Text style={s.sectionTitle}>
258 horizontal 361 Devices ({foundDevices.length} nearby, {savedDevices.length} saved)
259 showsHorizontalScrollIndicator={false} 362 </Text>
260 style={styles.actionsRow}> 363
261 {actions.map(a => ( 364 {/* Merge: all found devices + saved devices not currently found */}
262 <TouchableOpacity 365 {(() => {
263 key={a.label} 366 const allMacs = new Set([
264 style={[ 367 ...foundDevices.map(d => d.mac),
265 styles.actionButton, 368 ...savedDevices.map(d => d.mac),
266 a.needsConnect && !connected && styles.actionDisabled, 369 ]);
267 ]} 370 const merged = Array.from(allMacs).map(mac => {
268 disabled={a.needsConnect && !connected} 371 const found = foundDevices.find(d => d.mac === mac);
269 onPress={a.fn}> 372 const saved = savedDevices.find(d => d.mac === mac);
270 <Text 373 return {
271 style={[ 374 mac,
272 styles.actionText, 375 type: found?.type ?? saved?.type ?? 'KN550',
273 a.needsConnect && !connected && styles.actionTextDisabled, 376 rssi: found?.rssi,
274 ]}> 377 nearby: !!found,
275 {a.label} 378 saved: !!saved,
276 </Text> 379 };
277 </TouchableOpacity> 380 });
278 ))} 381 // Sort: saved first, then nearby, then rest
279 </ScrollView> 382 merged.sort((a, b) => {
383 if (a.saved !== b.saved) return a.saved ? -1 : 1;
384 if (a.nearby !== b.nearby) return a.nearby ? -1 : 1;
385 return 0;
386 });
387 return merged.map(d => (
388 <View key={d.mac} style={s.savedRow}>
389 <View style={[s.deviceIcon, d.saved && s.deviceIconSaved, !d.nearby && s.deviceOffline]}>
390 <Text style={s.deviceIconText}>BP</Text>
391 </View>
392 <View style={[s.deviceInfo, !d.nearby && s.deviceOffline]}>
393 <Text style={s.deviceType}>
394 {d.type}
395 {d.saved ? ' ' : ''}
396 {d.saved && <Text style={s.savedBadge}>SAVED</Text>}
397 </Text>
398 <Text style={s.deviceMac}>
399 {d.mac}
400 {!d.nearby ? ' (offline)' : d.rssi != null ? ` ${d.rssi} dBm` : ''}
401 </Text>
402 </View>
403 {d.saved ? (
404 <TouchableOpacity onPress={() => removeDevice(d.mac)}>
405 <Text style={s.removeText}>Remove</Text>
406 </TouchableOpacity>
407 ) : (
408 <TouchableOpacity style={s.addButton} onPress={() => addDevice({mac: d.mac, type: d.type, timestamp: Date.now()})}>
409 <Text style={s.addButtonText}>Save</Text>
410 </TouchableOpacity>
411 )}
412 </View>
413 ));
414 })()}
280 415
281 <Text style={styles.sectionTitle}>Log</Text> 416 {/* Readings */}
417 <Text style={[s.sectionTitle, {marginTop: 16}]}>
418 Readings ({recentReadings.length})
419 </Text>
282 <FlatList 420 <FlatList
283 data={log} 421 data={recentReadings}
284 keyExtractor={(_, i) => String(i)} 422 keyExtractor={(_, i) => String(i)}
285 renderItem={({item}) => <Text style={styles.logLine}>{item}</Text>} 423 style={s.list}
286 style={styles.logList} 424 renderItem={({item}) => (
425 <View style={s.readingRow}>
426 <View style={s.readingValues}>
427 <Text style={s.readingSys}>{item.sys ?? '--'}</Text>
428 <Text style={s.readingSlash}>/</Text>
429 <Text style={s.readingDia}>{item.dia ?? '--'}</Text>
430 <Text style={s.readingUnit}>mmHg</Text>
431 <Text style={s.readingPulse}>{item.pulse ?? '--'} bpm</Text>
432 </View>
433 <View style={s.readingMeta}>
434 <Text style={s.readingDate}>
435 {item.date ?? item.fetchedAt.split('T')[0]}
436 </Text>
437 <Text style={s.readingMac}>{item.mac}</Text>
438 </View>
439 </View>
440 )}
441 ListEmptyComponent={
442 <Text style={s.emptyText}>
443 {savedDevices.length === 0
444 ? 'Add a device to start tracking'
445 : 'No readings yet. Sync to pull data.'}
446 </Text>
447 }
287 /> 448 />
288 </SafeAreaView> 449 </SafeAreaView>
289 ); 450 );
290} 451}
291 452
292// ── Scanner Screen ─────────────────────────────────────────────────── 453// ── Debug Scanner Screen ─────────────────────────────────────────────
293 454
294function ScannerScreen({onSelectDevice}: {onSelectDevice: (d: Device) => void}) { 455function DebugScannerScreen({
456 onBack,
457 onSelectDevice,
458}: {
459 onBack: () => void;
460 onSelectDevice: (d: Device) => void;
461}) {
295 const [devices, setDevices] = useState<Device[]>([]); 462 const [devices, setDevices] = useState<Device[]>([]);
296 const [scanning, setScanning] = useState(false); 463 const [scanning, setScanning] = useState(false);
297 const [error, setError] = useState<string | null>(null); 464 const [error, setError] = useState<string | null>(null);
298 const devicesRef = useRef<Device[]>([]); 465 const devicesRef = useRef<Device[]>([]);
299 466
300 useEffect(() => { 467 useEffect(() => {
301 if (!iHealthDeviceManagerModule) { 468 if (!mgr) { setError('iHealth native module not found.'); return; }
302 setError('iHealth native module not found.');
303 return;
304 }
305 const emitter = getEmitter(); 469 const emitter = getEmitter();
306 470 const sub = emitter.addListener(
307 const scanSub = emitter.addListener( 471 mgr.Event_Scan_Device ?? 'event_scan_device',
308 iHealthDeviceManagerModule.Event_Scan_Device ?? 'event_scan_device', 472 (e: {mac: string; type: string; rssi?: number}) => {
309 (event: {mac: string; type: string; rssi?: number}) => { 473 if (e.type === 'AM3') return;
310 const {mac = '', type = 'Unknown', rssi} = event; 474 const {mac = '', type = 'Unknown', rssi} = e;
311 if (type === 'AM3') return; 475 const idx = devicesRef.current.findIndex(d => d.mac === mac);
312
313 const existing = devicesRef.current.findIndex(d => d.mac === mac);
314 let updated: Device[]; 476 let updated: Device[];
315 if (existing >= 0) { 477 if (idx >= 0) {
316 updated = [...devicesRef.current]; 478 updated = [...devicesRef.current];
317 updated[existing] = {mac, type, rssi, timestamp: Date.now()}; 479 updated[idx] = {mac, type, rssi, timestamp: Date.now()};
318 } else { 480 } else {
319 updated = [ 481 updated = [...devicesRef.current, {mac, type, rssi, timestamp: Date.now()}];
320 ...devicesRef.current,
321 {mac, type, rssi, timestamp: Date.now()},
322 ];
323 } 482 }
324 devicesRef.current = updated; 483 devicesRef.current = updated;
325 setDevices(updated); 484 setDevices(updated);
326 }, 485 },
327 ); 486 );
328 487 const finSub = emitter.addListener(
329 const finishSub = emitter.addListener( 488 mgr.Event_Scan_Finish ?? 'event_scan_finish',
330 iHealthDeviceManagerModule.Event_Scan_Finish ?? 'event_scan_finish',
331 () => setScanning(false), 489 () => setScanning(false),
332 ); 490 );
333 491 return () => { sub.remove(); finSub.remove(); };
334 return () => {
335 scanSub.remove();
336 finishSub.remove();
337 };
338 }, []); 492 }, []);
339 493
340 const startScan = async () => { 494 const startScan = async () => {
341 setError(null); 495 setError(null);
342 const granted = await requestAndroidPermissions(); 496 const ok = await requestAndroidPermissions();
343 if (!granted) { 497 if (!ok) { setError('Permissions denied'); return; }
344 setError('Bluetooth permissions denied');
345 return;
346 }
347 devicesRef.current = []; 498 devicesRef.current = [];
348 setDevices([]); 499 setDevices([]);
349 setScanning(true); 500 setScanning(true);
350
351 if (Platform.OS === 'android') { 501 if (Platform.OS === 'android') {
352 try { iHealthDeviceManagerModule.startDiscovery('ALL'); } 502 try { mgr.startDiscovery('ALL'); } catch (e) { console.log(e); }
353 catch (e) { console.log('Discovery failed:', e); }
354 } else { 503 } else {
355 const iosTypes = ['KN550', 'BP3L', 'BP5S', 'BP7S', 'AM3S', 'AM4', 504 const types = ['KN550', 'BP3L', 'BP5S', 'BP7S', 'AM3S', 'AM4',
356 'PO3', 'HS2', 'HS2S', 'HS4S', 'BG5S', 'BG1S', 'PO1', 'ECG3']; 505 'PO3', 'HS2', 'HS2S', 'HS4S', 'BG5S', 'BG1S', 'PO1', 'ECG3'];
357 let i = 0; 506 let i = 0;
358 const scanNext = () => { 507 const next = () => {
359 if (i < iosTypes.length) { 508 if (i < types.length) {
360 try { iHealthDeviceManagerModule.startDiscovery(iosTypes[i]); } 509 try { mgr.startDiscovery(types[i]); } catch (_) {}
361 catch (e) { console.log(`Failed: ${iosTypes[i]}`, e); }
362 i++; 510 i++;
363 setTimeout(scanNext, 2000); 511 setTimeout(next, 2000);
364 } 512 }
365 }; 513 };
366 setTimeout(scanNext, 2000); 514 setTimeout(next, 2000);
367 } 515 }
368 }; 516 };
369 517
370 const stopScan = () => { 518 const stopScan = () => {
371 try { iHealthDeviceManagerModule.stopDiscovery(); } 519 try { mgr.stopDiscovery(); } catch (_) {}
372 catch (e) { console.log('Stop failed:', e); }
373 setScanning(false); 520 setScanning(false);
374 }; 521 };
375 522
376 return ( 523 return (
377 <SafeAreaView style={styles.container}> 524 <SafeAreaView style={s.container}>
378 <Text style={styles.title}>iHealth Scanner</Text> 525 <TouchableOpacity style={s.backButton} onPress={onBack}>
379 <Text style={styles.subtitle}> 526 <Text style={s.backText}>Home</Text>
380 {scanning 527 </TouchableOpacity>
381 ? `Scanning... (${devices.length} found)` 528 <Text style={s.title}>Debug Scanner</Text>
382 : `${devices.length} device(s) found`} 529 <Text style={s.subtitle}>
530 {scanning ? `Scanning... (${devices.length})` : `${devices.length} device(s)`}
383 </Text> 531 </Text>
384 532 {error && <Text style={s.error}>{error}</Text>}
385 {error && <Text style={styles.error}>{error}</Text>}
386
387 <TouchableOpacity 533 <TouchableOpacity
388 style={[styles.button, scanning && styles.buttonStop]} 534 style={[s.button, scanning && s.buttonStop]}
389 onPress={scanning ? stopScan : startScan}> 535 onPress={scanning ? stopScan : startScan}>
390 <Text style={styles.buttonText}> 536 <Text style={s.buttonText}>{scanning ? 'Stop' : 'Start Scan'}</Text>
391 {scanning ? 'Stop Scan' : 'Start Scan'}
392 </Text>
393 </TouchableOpacity> 537 </TouchableOpacity>
394
395 <FlatList 538 <FlatList
396 data={devices} 539 data={devices}
397 keyExtractor={item => item.mac} 540 keyExtractor={item => item.mac}
398 renderItem={({item}) => ( 541 renderItem={({item}) => (
399 <TouchableOpacity 542 <TouchableOpacity style={s.deviceRow} onPress={() => { stopScan(); onSelectDevice(item); }}>
400 style={styles.deviceRow} 543 <View style={s.deviceIcon}>
401 onPress={() => { 544 <Text style={s.deviceIconText}>{item.type.substring(0, 3)}</Text>
402 stopScan();
403 onSelectDevice(item);
404 }}>
405 <View style={styles.deviceIcon}>
406 <Text style={styles.deviceIconText}>
407 {item.type.substring(0, 3)}
408 </Text>
409 </View> 545 </View>
410 <View style={styles.deviceInfo}> 546 <View style={s.deviceInfo}>
411 <Text style={styles.deviceType}>{item.type}</Text> 547 <Text style={s.deviceType}>{item.type}</Text>
412 <Text style={styles.deviceMac}>{item.mac}</Text> 548 <Text style={s.deviceMac}>{item.mac}</Text>
413 </View> 549 </View>
414 {item.rssi != null && ( 550 {item.rssi != null && <Text style={s.deviceRssi}>{item.rssi} dBm</Text>}
415 <Text style={styles.deviceRssi}>{item.rssi} dBm</Text> 551 <Text style={s.chevron}>{'>'}</Text>
416 )}
417 <Text style={styles.chevron}>{'>'}</Text>
418 </TouchableOpacity> 552 </TouchableOpacity>
419 )} 553 )}
420 style={styles.list} 554 style={s.list}
421 contentContainerStyle={devices.length === 0 && styles.emptyList} 555 contentContainerStyle={devices.length === 0 && s.emptyList}
422 ListEmptyComponent={ 556 ListEmptyComponent={
423 <Text style={styles.emptyText}> 557 <Text style={s.emptyText}>
424 {scanning 558 {scanning ? 'Scanning...' : 'Tap Start Scan'}
425 ? 'Looking for iHealth devices...'
426 : 'Tap "Start Scan" to find nearby iHealth devices'}
427 </Text> 559 </Text>
428 } 560 }
429 /> 561 />
@@ -431,20 +563,125 @@ function ScannerScreen({onSelectDevice}: {onSelectDevice: (d: Device) => void})
431 ); 563 );
432} 564}
433 565
566// ── Debug Device Screen ──────────────────────────────────────────────
567
568function DebugDeviceScreen({device, onBack}: {device: Device; onBack: () => void}) {
569 const [connected, setConnected] = useState(false);
570 const [connecting, setConnecting] = useState(false);
571 const [log, setLog] = useState<string[]>([]);
572
573 const addLog = (msg: string) =>
574 setLog(prev => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev]);
575
576 useEffect(() => {
577 const emitter = getEmitter();
578 const bpEmitter = getBPEmitter();
579 const connSub = emitter.addListener(
580 mgr.Event_Device_Connected ?? 'event_device_connected',
581 (e: {mac: string}) => {
582 if (e.mac === device.mac) { setConnected(true); setConnecting(false); addLog('Connected'); }
583 },
584 );
585 const failSub = emitter.addListener(
586 mgr.Event_Device_Connect_Failed ?? 'event_device_connect_failed',
587 (e: {mac: string; errorid?: number}) => {
588 if (e.mac === device.mac) { setConnecting(false); addLog(`Connect failed: ${e.errorid}`); }
589 },
590 );
591 const dcSub = emitter.addListener(
592 mgr.Event_Device_Disconnect ?? 'event_device_disconnect',
593 (e: {mac: string}) => {
594 if (e.mac === device.mac) { setConnected(false); addLog('Disconnected'); }
595 },
596 );
597 const notSub = bpEmitter.addListener(
598 bp550?.Event_Notify ?? 'event_notify',
599 (e: Record<string, unknown>) => addLog(JSON.stringify(e, null, 2)),
600 );
601 return () => { connSub.remove(); failSub.remove(); dcSub.remove(); notSub.remove(); };
602 }, [device.mac]);
603
604 const actions = [
605 {label: 'Get Battery', fn: () => { addLog('Battery...'); bp550.getBattery(device.mac); }},
606 {label: 'Function Info', fn: () => { addLog('FuncInfo...'); bp550.getFunctionInfo(device.mac); }},
607 {label: 'Offline Count', fn: () => { addLog('OfflineNum...'); bp550.getOffLineNum(device.mac); }},
608 {label: 'Offline Data', fn: () => { addLog('OfflineData...'); bp550.getOffLineData(device.mac); }},
609 {label: 'IDPS', fn: () => {
610 addLog('IDPS...');
611 mgr.getDevicesIDPS(device.mac, idps => addLog(JSON.stringify(idps, null, 2)));
612 }},
613 ];
614
615 return (
616 <SafeAreaView style={s.container}>
617 <TouchableOpacity style={s.backButton} onPress={onBack}>
618 <Text style={s.backText}>Back</Text>
619 </TouchableOpacity>
620 <Text style={s.title}>{device.type}</Text>
621 <Text style={s.deviceMac}>{device.mac}</Text>
622 <Text style={[s.statusBadge, connected && s.statusConnected]}>
623 {connecting ? 'Connecting...' : connected ? 'Connected' : 'Disconnected'}
624 </Text>
625 {!connected && !connecting && (
626 <TouchableOpacity style={s.button} onPress={() => {
627 setConnecting(true); addLog('Connecting...');
628 mgr.connectDevice(device.mac, device.type);
629 }}>
630 <Text style={s.buttonText}>Connect</Text>
631 </TouchableOpacity>
632 )}
633 {connected && (
634 <TouchableOpacity style={[s.button, s.buttonStop]} onPress={() => {
635 addLog('Disconnecting...'); bp550.disconnect(device.mac);
636 }}>
637 <Text style={s.buttonText}>Disconnect</Text>
638 </TouchableOpacity>
639 )}
640 <ScrollView horizontal showsHorizontalScrollIndicator={false} style={s.actionsRow}>
641 {actions.map(a => (
642 <TouchableOpacity
643 key={a.label}
644 style={[s.actionButton, !connected && s.actionDisabled]}
645 disabled={!connected}
646 onPress={a.fn}>
647 <Text style={[s.actionText, !connected && s.actionTextDisabled]}>{a.label}</Text>
648 </TouchableOpacity>
649 ))}
650 </ScrollView>
651 <Text style={s.sectionTitle}>Log</Text>
652 <FlatList
653 data={log}
654 keyExtractor={(_, i) => String(i)}
655 renderItem={({item}) => <Text style={s.logLine}>{item}</Text>}
656 style={s.logList}
657 />
658 </SafeAreaView>
659 );
660}
661
434// ── App Root ───────────────────────────────────────────────────────── 662// ── App Root ─────────────────────────────────────────────────────────
435 663
436function App() { 664function App() {
437 const [selectedDevice, setSelectedDevice] = useState<Device | null>(null); 665 const [screen, setScreen] = useState<Screen>('home');
666 const [debugDevice, setDebugDevice] = useState<Device | null>(null);
438 667
439 return ( 668 return (
440 <SafeAreaProvider> 669 <SafeAreaProvider>
441 {selectedDevice ? ( 670 {screen === 'home' && <HomeScreen onNav={setScreen} />}
442 <DeviceScreen 671 {screen === 'dashboard' && (
443 device={selectedDevice} 672 <DashboardScreen onBack={() => setScreen('home')} />
444 onBack={() => setSelectedDevice(null)} 673 )}
674 {screen === 'debug' && (
675 <DebugScannerScreen
676 onBack={() => setScreen('home')}
677 onSelectDevice={d => { setDebugDevice(d); setScreen('debug-device'); }}
678 />
679 )}
680 {screen === 'debug-device' && debugDevice && (
681 <DebugDeviceScreen
682 device={debugDevice}
683 onBack={() => setScreen('debug')}
445 /> 684 />
446 ) : (
447 <ScannerScreen onSelectDevice={setSelectedDevice} />
448 )} 685 )}
449 </SafeAreaProvider> 686 </SafeAreaProvider>
450 ); 687 );
@@ -452,28 +689,41 @@ function App() {
452 689
453// ── Styles ─────────────────────────────────────────────────────────── 690// ── Styles ───────────────────────────────────────────────────────────
454 691
455const styles = StyleSheet.create({ 692const s = StyleSheet.create({
456 container: {flex: 1, backgroundColor: '#f5f5f5', paddingHorizontal: 16}, 693 container: {flex: 1, backgroundColor: '#f5f5f5', paddingHorizontal: 16},
694 // Home
695 homeCenter: {flex: 1, justifyContent: 'center'},
696 homeTitle: {fontSize: 36, fontWeight: '800', color: '#1a1a1a', textAlign: 'center'},
697 homeSubtitle: {fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 40},
698 homeButton: {
699 backgroundColor: '#2196F3', padding: 20, borderRadius: 14,
700 marginBottom: 12, alignItems: 'center',
701 },
702 homeButtonSecondary: {backgroundColor: '#fff', borderWidth: 1, borderColor: '#ddd'},
703 homeButtonText: {color: '#fff', fontSize: 18, fontWeight: '700'},
704 homeButtonTextSecondary: {color: '#333'},
705 homeButtonSub: {color: 'rgba(255,255,255,0.8)', fontSize: 13, marginTop: 4},
706 // Common
457 title: {fontSize: 28, fontWeight: '700', color: '#1a1a1a', marginTop: 16}, 707 title: {fontSize: 28, fontWeight: '700', color: '#1a1a1a', marginTop: 16},
458 subtitle: {fontSize: 14, color: '#666', marginTop: 4, marginBottom: 16}, 708 subtitle: {fontSize: 14, color: '#666', marginTop: 4, marginBottom: 16},
459 error: { 709 sectionTitle: {fontSize: 16, fontWeight: '600', color: '#333', marginTop: 8, marginBottom: 8},
460 color: '#d32f2f', fontSize: 13, marginBottom: 8, 710 error: {color: '#d32f2f', fontSize: 13, marginBottom: 8, backgroundColor: '#ffebee', padding: 8, borderRadius: 6},
461 backgroundColor: '#ffebee', padding: 8, borderRadius: 6, 711 button: {backgroundColor: '#2196F3', paddingVertical: 14, borderRadius: 10, alignItems: 'center', marginBottom: 12},
462 },
463 button: {
464 backgroundColor: '#2196F3', paddingVertical: 14,
465 borderRadius: 10, alignItems: 'center', marginBottom: 16,
466 },
467 buttonStop: {backgroundColor: '#f44336'}, 712 buttonStop: {backgroundColor: '#f44336'},
468 buttonText: {color: '#fff', fontSize: 16, fontWeight: '600'}, 713 buttonText: {color: '#fff', fontSize: 16, fontWeight: '600'},
714 buttonOutline: {backgroundColor: 'transparent', borderWidth: 1.5, borderColor: '#2196F3'},
715 buttonTextOutline: {color: '#2196F3'},
469 list: {flex: 1}, 716 list: {flex: 1},
470 emptyList: {flex: 1, justifyContent: 'center', alignItems: 'center'}, 717 emptyList: {flex: 1, justifyContent: 'center', alignItems: 'center'},
471 emptyText: {color: '#999', fontSize: 15, textAlign: 'center'}, 718 emptyText: {color: '#999', fontSize: 15, textAlign: 'center'},
719 backButton: {marginTop: 12, marginBottom: 4},
720 backText: {color: '#2196F3', fontSize: 16},
721 chevron: {fontSize: 18, color: '#ccc', marginLeft: 8},
722 // Device rows
472 deviceRow: { 723 deviceRow: {
473 flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', 724 flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff',
474 padding: 14, borderRadius: 10, marginBottom: 8, 725 padding: 14, borderRadius: 10, marginBottom: 8,
475 shadowColor: '#000', shadowOffset: {width: 0, height: 1}, 726 shadowColor: '#000', shadowOffset: {width: 0, height: 1}, shadowOpacity: 0.05, shadowRadius: 2, elevation: 1,
476 shadowOpacity: 0.05, shadowRadius: 2, elevation: 1,
477 }, 727 },
478 deviceIcon: { 728 deviceIcon: {
479 width: 44, height: 44, borderRadius: 22, backgroundColor: '#e3f2fd', 729 width: 44, height: 44, borderRadius: 22, backgroundColor: '#e3f2fd',
@@ -482,37 +732,51 @@ const styles = StyleSheet.create({
482 deviceIconText: {fontSize: 12, fontWeight: '700', color: '#1565c0'}, 732 deviceIconText: {fontSize: 12, fontWeight: '700', color: '#1565c0'},
483 deviceInfo: {flex: 1}, 733 deviceInfo: {flex: 1},
484 deviceType: {fontSize: 16, fontWeight: '600', color: '#1a1a1a'}, 734 deviceType: {fontSize: 16, fontWeight: '600', color: '#1a1a1a'},
485 deviceMac: { 735 deviceMac: {fontSize: 12, color: '#888', marginTop: 2, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace'},
486 fontSize: 12, color: '#888', marginTop: 2,
487 fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
488 },
489 deviceRssi: {fontSize: 12, color: '#999', marginLeft: 8}, 736 deviceRssi: {fontSize: 12, color: '#999', marginLeft: 8},
490 chevron: {fontSize: 18, color: '#ccc', marginLeft: 8}, 737 // Dashboard
491 // Device screen 738 savedRow: {
492 backButton: {marginTop: 12, marginBottom: 4}, 739 flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff',
493 backText: {color: '#2196F3', fontSize: 16}, 740 padding: 14, borderRadius: 10, marginBottom: 8, elevation: 1,
494 statusBadge: {
495 fontSize: 13, color: '#f44336', marginTop: 8, marginBottom: 16,
496 fontWeight: '600',
497 }, 741 },
498 statusConnected: {color: '#4caf50'}, 742 removeText: {color: '#f44336', fontSize: 13, fontWeight: '600'},
499 sectionTitle: { 743 deviceOffline: {opacity: 0.5},
500 fontSize: 16, fontWeight: '600', color: '#333', 744 deviceIconSaved: {backgroundColor: '#c8e6c9'},
501 marginTop: 8, marginBottom: 8, 745 savedBadge: {fontSize: 10, color: '#4caf50', fontWeight: '700'},
746 foundRow: {
747 flexDirection: 'row', alignItems: 'center', backgroundColor: '#e8f5e9',
748 padding: 14, borderRadius: 10, marginBottom: 8,
502 }, 749 },
503 actionsRow: {flexGrow: 0, marginBottom: 12}, 750 addButton: {backgroundColor: '#4caf50', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 8},
504 actionButton: { 751 addButtonText: {color: '#fff', fontSize: 14, fontWeight: '600'},
505 backgroundColor: '#e3f2fd', paddingVertical: 10, paddingHorizontal: 16, 752 syncBar: {flexDirection: 'row', alignItems: 'center', padding: 10, backgroundColor: '#e3f2fd', borderRadius: 8, marginBottom: 8},
506 borderRadius: 8, marginRight: 8, 753 syncText: {color: '#1565c0', fontSize: 13, marginLeft: 8},
754 syncDone: {color: '#4caf50', fontSize: 13, marginBottom: 8, fontWeight: '600'},
755 scanningRow: {flexDirection: 'row', alignItems: 'center', padding: 12},
756 scanningText: {color: '#666', fontSize: 13, marginLeft: 8},
757 // Readings
758 readingRow: {
759 backgroundColor: '#fff', padding: 14, borderRadius: 10, marginBottom: 8, elevation: 1,
507 }, 760 },
761 readingValues: {flexDirection: 'row', alignItems: 'baseline'},
762 readingSys: {fontSize: 28, fontWeight: '700', color: '#1a1a1a'},
763 readingSlash: {fontSize: 20, color: '#999', marginHorizontal: 4},
764 readingDia: {fontSize: 28, fontWeight: '700', color: '#1a1a1a'},
765 readingUnit: {fontSize: 12, color: '#999', marginLeft: 6},
766 readingPulse: {fontSize: 14, color: '#666', marginLeft: 16},
767 readingMeta: {flexDirection: 'row', justifyContent: 'space-between', marginTop: 4},
768 readingDate: {fontSize: 12, color: '#999'},
769 readingMac: {fontSize: 10, color: '#bbb', fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace'},
770 // Debug device
771 statusBadge: {fontSize: 13, color: '#f44336', marginTop: 8, marginBottom: 16, fontWeight: '600'},
772 statusConnected: {color: '#4caf50'},
773 actionsRow: {flexGrow: 0, marginBottom: 12},
774 actionButton: {backgroundColor: '#e3f2fd', paddingVertical: 10, paddingHorizontal: 16, borderRadius: 8, marginRight: 8},
508 actionDisabled: {backgroundColor: '#eee'}, 775 actionDisabled: {backgroundColor: '#eee'},
509 actionText: {color: '#1565c0', fontSize: 13, fontWeight: '600'}, 776 actionText: {color: '#1565c0', fontSize: 13, fontWeight: '600'},
510 actionTextDisabled: {color: '#bbb'}, 777 actionTextDisabled: {color: '#bbb'},
511 logList: {flex: 1, marginTop: 4}, 778 logList: {flex: 1, marginTop: 4},
512 logLine: { 779 logLine: {fontSize: 11, color: '#555', paddingVertical: 3, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace'},
513 fontSize: 11, color: '#555', paddingVertical: 3,
514 fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
515 },
516}); 780});
517 781
518export default App; 782export default App;
diff --git a/package-lock.json b/package-lock.json
index b5abdac..f5a42c2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
9 "version": "0.0.1", 9 "version": "0.0.1",
10 "dependencies": { 10 "dependencies": {
11 "@ihealth/ihealthlibrary-react-native": "file:./libs/ihealth-sdk", 11 "@ihealth/ihealthlibrary-react-native": "file:./libs/ihealth-sdk",
12 "@react-native-async-storage/async-storage": "^2.2.0",
12 "@react-native/new-app-screen": "0.85.0", 13 "@react-native/new-app-screen": "0.85.0",
13 "react": "19.2.3", 14 "react": "19.2.3",
14 "react-native": "0.85.0", 15 "react-native": "0.85.0",
@@ -2758,6 +2759,18 @@
2758 "node": ">= 8" 2759 "node": ">= 8"
2759 } 2760 }
2760 }, 2761 },
2762 "node_modules/@react-native-async-storage/async-storage": {
2763 "version": "2.2.0",
2764 "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
2765 "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
2766 "license": "MIT",
2767 "dependencies": {
2768 "merge-options": "^3.0.4"
2769 },
2770 "peerDependencies": {
2771 "react-native": "^0.0.0-0 || >=0.65 <1.0"
2772 }
2773 },
2761 "node_modules/@react-native-community/cli": { 2774 "node_modules/@react-native-community/cli": {
2762 "version": "20.1.0", 2775 "version": "20.1.0",
2763 "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.1.0.tgz", 2776 "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.1.0.tgz",
@@ -7452,6 +7465,15 @@
7452 "node": ">=8" 7465 "node": ">=8"
7453 } 7466 }
7454 }, 7467 },
7468 "node_modules/is-plain-obj": {
7469 "version": "2.1.0",
7470 "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
7471 "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
7472 "license": "MIT",
7473 "engines": {
7474 "node": ">=8"
7475 }
7476 },
7455 "node_modules/is-regex": { 7477 "node_modules/is-regex": {
7456 "version": "1.2.1", 7478 "version": "1.2.1",
7457 "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", 7479 "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -8823,6 +8845,18 @@
8823 "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", 8845 "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
8824 "license": "MIT" 8846 "license": "MIT"
8825 }, 8847 },
8848 "node_modules/merge-options": {
8849 "version": "3.0.4",
8850 "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
8851 "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
8852 "license": "MIT",
8853 "dependencies": {
8854 "is-plain-obj": "^2.1.0"
8855 },
8856 "engines": {
8857 "node": ">=10"
8858 }
8859 },
8826 "node_modules/merge-stream": { 8860 "node_modules/merge-stream": {
8827 "version": "2.0.0", 8861 "version": "2.0.0",
8828 "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 8862 "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
diff --git a/package.json b/package.json
index 961302d..2e1b03e 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
11 }, 11 },
12 "dependencies": { 12 "dependencies": {
13 "@ihealth/ihealthlibrary-react-native": "file:./libs/ihealth-sdk", 13 "@ihealth/ihealthlibrary-react-native": "file:./libs/ihealth-sdk",
14 "@react-native-async-storage/async-storage": "^2.2.0",
14 "@react-native/new-app-screen": "0.85.0", 15 "@react-native/new-app-screen": "0.85.0",
15 "react": "19.2.3", 16 "react": "19.2.3",
16 "react-native": "0.85.0", 17 "react-native": "0.85.0",