Add nicer loading screen while OTA is loading (#9499)

This commit is contained in:
Samuel Newman
2026-07-31 19:39:26 +03:00
committed by GitHub
parent 87a177bbe8
commit ce28129674
3 changed files with 57 additions and 9 deletions
+6
View File
@@ -45,6 +45,12 @@ jest.mock('#/storage', () => ({
},
}))
jest.mock('#/alf', () => ({
useTheme: jest.fn().mockImplementation(() => ({
scheme: 'light',
})),
}))
/**
* `channel` here is the build-time constant baked into the native build, not the
* channel of the running bundle. `channel` is passed as the manifest metadata
+45 -8
View File
@@ -1,5 +1,10 @@
import {useCallback, useEffect, useRef, useState} from 'react'
import {Alert, AppState, type AppStateStatus} from 'react-native'
import {
Alert,
AppState,
type AppStateStatus,
Image as RNImage,
} from 'react-native'
import {nativeBuildVersion} from 'expo-application'
import {
checkForUpdateAsync,
@@ -7,6 +12,7 @@ import {
fetchUpdateAsync,
isEnabled,
reloadAsync,
type ReloadScreenOptions,
setExtraParamAsync,
UpdateCheckResultNotAvailableReason,
useUpdates,
@@ -14,6 +20,7 @@ import {
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useTheme} from '#/alf'
import {APP_VERSION, IS_IOS, IS_TESTFLIGHT} from '#/env'
import {device} from '#/storage'
@@ -83,7 +90,7 @@ async function setExtraParamsPullRequest(channel: string) {
await setExtraParamAsync('channel', channel)
}
async function updateTestflight() {
async function updateTestflight(scheme: 'light' | 'dark') {
await setExtraParams()
const res = await checkForUpdateAsync()
@@ -101,7 +108,9 @@ async function updateTestflight() {
text: 'Relaunch',
style: 'default',
onPress: async () => {
await reloadAsync()
await reloadAsync({
reloadScreenOptions: splash(scheme),
})
},
},
],
@@ -110,6 +119,7 @@ async function updateTestflight() {
}
export function useApplyPullRequestOTAUpdate() {
const t = useTheme()
const {currentlyRunning} = useUpdates()
const [pending, setPending] = useState(false)
const currentChannel = getRunningChannel(currentlyRunning)
@@ -174,7 +184,9 @@ export function useApplyPullRequestOTAUpdate() {
* after the reload.
*/
// Linking.clearInitialURL()
await reloadAsync()
await reloadAsync({
reloadScreenOptions: splash(t.scheme),
})
} catch (e) {
device.remove(['pendingOTAUpdate'])
throw e
@@ -335,6 +347,7 @@ export function useOTAUpdateRecovery() {
export function useOTAUpdates() {
const shouldReceiveUpdates = isEnabled && !__DEV__
const t = useTheme()
const appState = useRef<AppStateStatus>('active')
const lastMinimize = useRef(0)
const ranInitialCheck = useRef(false)
@@ -366,13 +379,13 @@ export function useOTAUpdates() {
const onIsTestFlight = useCallback(async () => {
try {
await updateTestflight()
await updateTestflight(t.scheme)
} catch (err: any) {
if (!isNetworkError(err)) {
logger.error('Internal OTA Update Error', {safeMessage: err})
}
}
}, [])
}, [t.scheme])
useEffect(() => {
// We don't need to check anything if the current update is a PR update
@@ -414,7 +427,9 @@ export function useOTAUpdates() {
// chances are that there isn't anything important going on in the current session.
if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) {
if (isUpdatePending) {
await reloadAsync()
await reloadAsync({
reloadScreenOptions: splash(t.scheme),
})
} else {
setCheckTimeout()
}
@@ -431,5 +446,27 @@ export function useOTAUpdates() {
clearTimeout(timeout.current)
subscription.remove()
}
}, [isUpdatePending, currentChannel, setCheckTimeout])
}, [isUpdatePending, currentChannel, setCheckTimeout, t.scheme])
}
/**
* Splash screen for while the app is updating
*/
export const splash = (scheme: 'light' | 'dark') => {
const source =
scheme === 'light'
? require('../../../assets/splash/splash.png')
: require('../../../assets/splash/splash-dark.png')
return {
image: RNImage.resolveAssetSource(source).uri,
imageFullScreen: true,
imageResizeMode: 'cover',
backgroundColor: scheme === 'light' ? '#006AFF' : '#002861',
spinner: {
enabled: true,
color: '#ffffff',
size: 'large',
},
} satisfies ReloadScreenOptions
}
+6 -1
View File
@@ -4,6 +4,8 @@ import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {useMutation, useQuery} from '@tanstack/react-query'
import {splash} from '#/lib/hooks/useOTAUpdates'
import {useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes'
@@ -13,6 +15,7 @@ import * as SettingsList from '../components/SettingsList'
export function OTAInfo() {
const {_} = useLingui()
const t = useTheme()
const {
data: isAvailable,
isPending: isPendingInfo,
@@ -31,7 +34,9 @@ export function OTAInfo() {
useMutation({
mutationFn: async () => {
await Updates.fetchUpdateAsync()
await Updates.reloadAsync()
await Updates.reloadAsync({
reloadScreenOptions: splash(t.scheme),
})
},
onError: error =>
Toast.show(`Failed to update: ${error.message}`, {