Add nicer loading screen while OTA is loading (#9499)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`, {
|
||||||
|
|||||||
Reference in New Issue
Block a user