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` 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 * 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 {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 {nativeBuildVersion} from 'expo-application'
import { import {
checkForUpdateAsync, checkForUpdateAsync,
@@ -7,6 +12,7 @@ import {
fetchUpdateAsync, fetchUpdateAsync,
isEnabled, isEnabled,
reloadAsync, reloadAsync,
type ReloadScreenOptions,
setExtraParamAsync, setExtraParamAsync,
UpdateCheckResultNotAvailableReason, UpdateCheckResultNotAvailableReason,
useUpdates, useUpdates,
@@ -14,6 +20,7 @@ import {
import {isNetworkError} from '#/lib/strings/errors' import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useTheme} from '#/alf'
import {APP_VERSION, IS_IOS, IS_TESTFLIGHT} from '#/env' import {APP_VERSION, IS_IOS, IS_TESTFLIGHT} from '#/env'
import {device} from '#/storage' import {device} from '#/storage'
@@ -83,7 +90,7 @@ async function setExtraParamsPullRequest(channel: string) {
await setExtraParamAsync('channel', channel) await setExtraParamAsync('channel', channel)
} }
async function updateTestflight() { async function updateTestflight(scheme: 'light' | 'dark') {
await setExtraParams() await setExtraParams()
const res = await checkForUpdateAsync() const res = await checkForUpdateAsync()
@@ -101,7 +108,9 @@ async function updateTestflight() {
text: 'Relaunch', text: 'Relaunch',
style: 'default', style: 'default',
onPress: async () => { onPress: async () => {
await reloadAsync() await reloadAsync({
reloadScreenOptions: splash(scheme),
})
}, },
}, },
], ],
@@ -110,6 +119,7 @@ async function updateTestflight() {
} }
export function useApplyPullRequestOTAUpdate() { export function useApplyPullRequestOTAUpdate() {
const t = useTheme()
const {currentlyRunning} = useUpdates() const {currentlyRunning} = useUpdates()
const [pending, setPending] = useState(false) const [pending, setPending] = useState(false)
const currentChannel = getRunningChannel(currentlyRunning) const currentChannel = getRunningChannel(currentlyRunning)
@@ -174,7 +184,9 @@ export function useApplyPullRequestOTAUpdate() {
* after the reload. * after the reload.
*/ */
// Linking.clearInitialURL() // Linking.clearInitialURL()
await reloadAsync() await reloadAsync({
reloadScreenOptions: splash(t.scheme),
})
} catch (e) { } catch (e) {
device.remove(['pendingOTAUpdate']) device.remove(['pendingOTAUpdate'])
throw e throw e
@@ -335,6 +347,7 @@ export function useOTAUpdateRecovery() {
export function useOTAUpdates() { export function useOTAUpdates() {
const shouldReceiveUpdates = isEnabled && !__DEV__ const shouldReceiveUpdates = isEnabled && !__DEV__
const t = useTheme()
const appState = useRef<AppStateStatus>('active') const appState = useRef<AppStateStatus>('active')
const lastMinimize = useRef(0) const lastMinimize = useRef(0)
const ranInitialCheck = useRef(false) const ranInitialCheck = useRef(false)
@@ -366,13 +379,13 @@ export function useOTAUpdates() {
const onIsTestFlight = useCallback(async () => { const onIsTestFlight = useCallback(async () => {
try { try {
await updateTestflight() await updateTestflight(t.scheme)
} catch (err: any) { } catch (err: any) {
if (!isNetworkError(err)) { if (!isNetworkError(err)) {
logger.error('Internal OTA Update Error', {safeMessage: err}) logger.error('Internal OTA Update Error', {safeMessage: err})
} }
} }
}, []) }, [t.scheme])
useEffect(() => { useEffect(() => {
// We don't need to check anything if the current update is a PR update // 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. // chances are that there isn't anything important going on in the current session.
if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) { if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) {
if (isUpdatePending) { if (isUpdatePending) {
await reloadAsync() await reloadAsync({
reloadScreenOptions: splash(t.scheme),
})
} else { } else {
setCheckTimeout() setCheckTimeout()
} }
@@ -431,5 +446,27 @@ export function useOTAUpdates() {
clearTimeout(timeout.current) clearTimeout(timeout.current)
subscription.remove() 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 {Trans} from '@lingui/react/macro'
import {useMutation, useQuery} from '@tanstack/react-query' 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 {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate' import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes'
@@ -13,6 +15,7 @@ import * as SettingsList from '../components/SettingsList'
export function OTAInfo() { export function OTAInfo() {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme()
const { const {
data: isAvailable, data: isAvailable,
isPending: isPendingInfo, isPending: isPendingInfo,
@@ -31,7 +34,9 @@ export function OTAInfo() {
useMutation({ useMutation({
mutationFn: async () => { mutationFn: async () => {
await Updates.fetchUpdateAsync() await Updates.fetchUpdateAsync()
await Updates.reloadAsync() await Updates.reloadAsync({
reloadScreenOptions: splash(t.scheme),
})
}, },
onError: error => onError: error =>
Toast.show(`Failed to update: ${error.message}`, { Toast.show(`Failed to update: ${error.message}`, {