-
Notifications
You must be signed in to change notification settings - Fork 120
Expand file tree
/
Copy pathApp.tsx
More file actions
175 lines (154 loc) · 6.09 KB
/
Copy pathApp.tsx
File metadata and controls
175 lines (154 loc) · 6.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import * as Sentry from '@sentry/react-native';
import * as Notifications from 'expo-notifications';
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, AppState, type AppStateStatus, I18nManager } from 'react-native';
import StorybookUIRoot from './.storybook';
import ErrorBoundary from './src/components/ErrorBoundary';
import OfflineIndicator from './src/components/OfflineIndicator';
import { useSplashGuard } from './src/components/SplashGuard';
import ThemeTransitionView from './src/components/ThemeTransitionView';
import UpdatePrompt from './src/components/UpdatePrompt';
import { PetProvider } from './src/context/PetContext';
import { ThemeProvider } from './src/context/ThemeContext';
import { ToastProvider } from './src/context/ToastContext';
import i18n, { isRTL } from './src/i18n';
import AppNavigator, { handleNotificationDeepLink } from './src/navigation/AppNavigator';
import LockScreen from './src/screens/LockScreen';
import {
enableScreenCapturePrevention,
loadLockTimeout,
getLockTimeoutMs,
} from './src/services/appLockService';
import { registerBackgroundMedicationTask } from './src/services/backgroundTaskService';
import errorTracking from './src/services/errorTracking';
import {
registerNotificationActions,
watchNotificationActions,
} from './src/services/notificationService';
import updateService from './src/services/updateService';
import { checkAppVersion } from './src/services/versionCheckService';
import { initializeWidgetService } from './src/services/widgetService';
const isStorybookEnabled = process.env.STORYBOOK_ENABLED === 'true';
// Initialise Sentry before the first render
errorTracking.init();
// Apply RTL direction based on the active language at startup
const startupRTL = isRTL(i18n.language);
if (I18nManager.isRTL !== startupRTL) {
I18nManager.forceRTL(startupRTL);
}
function App() {
const { appReady } = useSplashGuard();
const [updateStatus, setUpdateStatus] = React.useState<
{ visible: false } | { visible: true; variant: 'optional' | 'force'; storeUrl?: string }
>({ visible: false });
const [locked, setLocked] = useState(false);
const [pinFallback, setPinFallback] = useState(false);
const backgroundedAt = React.useRef<number | null>(null);
// Enable screen capture prevention on mount
useEffect(() => {
void enableScreenCapturePrevention();
}, []);
// Lock app after idle timeout when returning to foreground
useEffect(() => {
const onChange = async (state: AppStateStatus) => {
if (state === 'background' || state === 'inactive') {
backgroundedAt.current = Date.now();
} else if (state === 'active' && backgroundedAt.current !== null) {
const elapsed = Date.now() - backgroundedAt.current;
backgroundedAt.current = null;
const timeout = await loadLockTimeout();
const ms = getLockTimeoutMs(timeout);
if (ms > 0 && elapsed >= ms) {
setPinFallback(false);
setLocked(true);
}
}
};
const sub = AppState.addEventListener('change', onChange);
return () => sub.remove();
}, []);
// Check for updates on launch
React.useEffect(() => {
if (!appReady) return;
void (async () => {
// 1. Check server-side minimum version (critical/recommended)
const versionResult = await checkAppVersion();
if (versionResult.type === 'critical') {
setUpdateStatus({ visible: true, variant: 'force', storeUrl: versionResult.storeUrl });
return; // no need to check OTA if a store update is required
}
if (versionResult.type === 'recommended') {
setUpdateStatus({ visible: true, variant: 'optional', storeUrl: versionResult.storeUrl });
return;
}
// 2. Fall back to OTA check via expo-updates
const result = await updateService.checkForUpdate();
if (result.type === 'force-update') {
setUpdateStatus({ visible: true, variant: 'force', storeUrl: result.storeUrl });
} else if (result.type === 'ota-available') {
setUpdateStatus({ visible: true, variant: 'optional' });
}
})();
}, [appReady]);
const handleUpdate = () => {
void updateService.applyOtaUpdate();
};
const handleDismiss = () => {
setUpdateStatus({ visible: false });
};
useEffect(() => {
void registerNotificationActions();
const subscription = watchNotificationActions();
void registerBackgroundMedicationTask();
// Initialize widget service and update widgets
const unsubscribeWidget = initializeWidgetService();
return () => {
subscription.remove();
unsubscribeWidget();
};
}, []);
// Handle initial notification if app was launched from a notification tap
// (cold-start or background)
useEffect(() => {
const checkInitialNotification = async () => {
const notification = await Notifications.getLastNotificationResponseAsync();
if (notification) {
const data = notification.notification.request.content.data;
handleNotificationDeepLink(data);
}
};
void checkInitialNotification();
}, [appReady]);
if (!appReady) return <View style={styles.root} />;
if (locked) {
return <LockScreen showPinFallback={pinFallback} onUnlock={() => setLocked(false)} />;
}
return (
<ThemeProvider>
<ToastProvider>
<PetProvider>
<ErrorBoundary>
<ThemeTransitionView>
<View style={styles.root}>
<OfflineIndicator />
<AppNavigator />
<UpdatePrompt
visible={updateStatus.visible}
variant={updateStatus.visible ? updateStatus.variant : 'optional'}
storeUrl={updateStatus.visible ? updateStatus.storeUrl : undefined}
onUpdate={handleUpdate}
onDismiss={handleDismiss}
/>
</View>
</ThemeTransitionView>
</ErrorBoundary>
</PetProvider>
</ToastProvider>
</ThemeProvider>
);
}
const styles = StyleSheet.create({
root: { flex: 1 },
});
const AppRoot = isStorybookEnabled ? StorybookUIRoot : Sentry.wrap(App);
export default AppRoot;