Skip to content

Commit e75f92d

Browse files
committed
Implement the 10 second startup delay when loading updates
1 parent f87d326 commit e75f92d

File tree

2 files changed

+81
-30
lines changed

2 files changed

+81
-30
lines changed

App.tsx

+56-24
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import {SelectProvider} from '@mobile-reality/react-native-select-pro';
1818
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
1919
import {NavigationContainer, useNavigationContainerRef} from '@react-navigation/native';
2020
import * as SplashScreen from 'expo-splash-screen';
21-
import {AppState, AppStateStatus, Platform, StatusBar, StyleSheet, UIManager, useColorScheme, View} from 'react-native';
21+
import {ActivityIndicator, AppState, AppStateStatus, Image, Platform, StatusBar, StyleSheet, UIManager, useColorScheme, View} from 'react-native';
2222
import {SafeAreaProvider} from 'react-native-safe-area-context';
2323

2424
import * as BackgroundFetch from 'expo-background-fetch';
25+
import Constants from 'expo-constants';
2526
import * as TaskManager from 'expo-task-manager';
2627
import * as Sentry from 'sentry-expo';
2728

@@ -62,9 +63,10 @@ import {NotFoundError} from 'types/requests';
6263
import {formatRequestedTime, RequestedTime} from 'utils/date';
6364

6465
import * as messages from 'compiled-lang/en.json';
66+
import {Center} from 'components/core';
6567
import KillSwitchMonitor from 'components/KillSwitchMonitor';
6668
import {filterLoggedData} from 'logging/filterLoggedData';
67-
import {updateCheck, UpdateStatus} from 'Updates';
69+
import {startupUpdateCheck, UpdateStatus} from 'Updates';
6870

6971
logger.info('App starting.');
7072

@@ -116,7 +118,10 @@ axios.interceptors.response.use(response => {
116118
});
117119

118120
// The SplashScreen stays up until we've loaded all of our fonts and other assets
119-
void SplashScreen.preventAutoHideAsync();
121+
void SplashScreen.preventAutoHideAsync().catch((error: Error) => {
122+
// We really don't care about these errors, they're common and not actionable
123+
logger.debug('SplashScreen.preventAutoHideAsync threw error, ignoring', {error});
124+
});
120125

121126
if (Sentry?.init) {
122127
const dsn = process.env.EXPO_PUBLIC_SENTRY_DSN;
@@ -310,11 +315,31 @@ const BaseApp: React.FunctionComponent<{
310315

311316
const navigationRef = useNavigationContainerRef();
312317

313-
// Hide the splash screen after fonts load and updates are applied.
314-
// TODO: for maximum seamlessness, hide it after the map view is ready
318+
const [splashScreenVisible, setSplashScreenVisible] = useState(true);
319+
useEffect(() => {
320+
// Hide the splash screen, but bake in a delay so that we are ready to render a view
321+
// that looks just like it
322+
if (splashScreenVisible) {
323+
setSplashScreenVisible(false);
324+
setTimeout(
325+
() =>
326+
void (async () => {
327+
try {
328+
await SplashScreen.hideAsync();
329+
} catch (error) {
330+
// We really don't care about these errors, they're common and not actionable
331+
logger.debug({error}, 'Error from SplashScreen.hideAsync, ignoring');
332+
}
333+
})(),
334+
500,
335+
);
336+
}
337+
}, [splashScreenVisible, setSplashScreenVisible, logger]);
338+
339+
// Check for updates
315340
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>('checking');
316341
useEffect(() => {
317-
updateCheck()
342+
startupUpdateCheck()
318343
.then(setUpdateStatus)
319344
.catch((error: Error) => {
320345
logger.error({error}, 'Unexpected error checking for updates');
@@ -323,24 +348,31 @@ const BaseApp: React.FunctionComponent<{
323348
});
324349
}, [setUpdateStatus, logger]);
325350

326-
const [splashScreenState, setSplashScreenState] = React.useState<'visible' | 'hiding' | 'hidden'>('visible');
327-
useEffect(() => {
328-
void (async () => {
329-
if (fontsLoaded && updateStatus === 'ready' && splashScreenState === 'visible') {
330-
setSplashScreenState('hiding');
331-
try {
332-
await SplashScreen.hideAsync();
333-
} catch (error) {
334-
logger.error({error}, 'Error from SplashScreen.hideAsync');
335-
}
336-
setSplashScreenState('hidden');
337-
}
338-
})();
339-
}, [fontsLoaded, logger, splashScreenState, setSplashScreenState, updateStatus]);
340-
341-
if (!fontsLoaded || splashScreenState !== 'hidden' || updateStatus !== 'ready') {
342-
// The splash screen keeps rendering while fonts are loading or updates are in progress
343-
return null;
351+
if (!fontsLoaded || updateStatus !== 'ready') {
352+
// Here, we render a view that looks exactly like the splash screen but now has an activity indicator
353+
return (
354+
<View
355+
pointerEvents="none"
356+
style={[
357+
StyleSheet.absoluteFill,
358+
{
359+
backgroundColor: Constants.expoConfig?.splash?.backgroundColor,
360+
},
361+
]}>
362+
<Image
363+
style={{
364+
width: '100%',
365+
height: '100%',
366+
resizeMode: Constants.expoConfig?.splash?.resizeMode || 'contain',
367+
}}
368+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
369+
source={require('./assets/splash.png')}
370+
/>
371+
<Center style={{position: 'absolute', top: 0, bottom: 0, left: 0, right: 0}}>
372+
<ActivityIndicator size="large" style={{marginTop: 200}} />
373+
</Center>
374+
</View>
375+
);
344376
}
345377

346378
return (

Updates.ts

+25-6
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,39 @@ import {logger} from 'logger';
33

44
export type UpdateStatus = 'checking' | 'restarting' | 'ready';
55

6-
export const updateCheck = async (): Promise<UpdateStatus> => {
6+
export const startupUpdateCheck = async (): Promise<UpdateStatus> => {
77
if (Updates.isEmergencyLaunch) {
88
logger.warn('Emergency launch detected - update checking disabled');
99
return 'ready';
1010
}
1111
if (Updates.channel !== 'preview' && Updates.channel !== 'release') {
12+
logger.debug(`Unknown update channel '${Updates.channel || 'null'}' - update checking disabled`);
1213
return 'ready';
1314
}
1415

15-
const update = await Updates.checkForUpdateAsync();
16-
if (update.isAvailable) {
17-
await Updates.fetchUpdateAsync();
18-
await Updates.reloadAsync();
19-
return 'restarting';
16+
try {
17+
// After 10 seconds, we'll resolve as ready no matter what
18+
const timeout = new Promise<'timeout'>(resolve => setTimeout(() => resolve('timeout'), 10000));
19+
const update = await Promise.race([timeout, Updates.checkForUpdateAsync()]);
20+
if (update === 'timeout') {
21+
logger.debug('checkForUpdateAsync timed out');
22+
return 'ready';
23+
}
24+
if (update.isAvailable) {
25+
// An update is available! Let's create a new 10 second timer and try to get it installed.
26+
const timeout = new Promise<'timeout'>(resolve => setTimeout(() => resolve('timeout'), 10000));
27+
const fetch = await Promise.race([timeout, Updates.fetchUpdateAsync()]);
28+
if (fetch === 'timeout') {
29+
logger.debug('fetchUpdateAsync timed out');
30+
return 'ready';
31+
}
32+
await Updates.reloadAsync();
33+
return 'restarting';
34+
} else {
35+
logger.debug('No update available');
36+
}
37+
} catch (error) {
38+
logger.warn({error}, 'Error checking for updates');
2039
}
2140
return 'ready';
2241
};

0 commit comments

Comments
 (0)