From 50752603ce88407df53d908889677e3009fd52b6 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Sat, 5 Sep 2026 18:56:00 +0300 Subject: [PATCH] handle fingerprint ota previews and request lifecycles --- src/lib/hooks/useIntentHandler.ts | 4 +- src/lib/hooks/useOTAUpdates.test.ts | 209 ++++++++++++++++- src/lib/hooks/useOTAUpdates.ts | 235 +++++++++++++++++--- src/lib/hooks/useOTAUpdates.web.ts | 4 + src/screens/Settings/components/OTAInfo.tsx | 10 +- 5 files changed, 424 insertions(+), 38 deletions(-) diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 428ed39f81..ee6008cfe2 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -92,11 +92,13 @@ export function useIntentHandler() { releaseVersion && buildNumber ? `${releaseVersion}.${buildNumber}` : null + const sourceCommit = params.get('sourceCommit') + const publicationId = params.get('publicationId') if (!channel) { Alert.alert('Error', 'No channel provided to look for.') return } - tryApplyUpdate(channel, appVersion) + tryApplyUpdate(channel, appVersion, {sourceCommit, publicationId}) return } default: { diff --git a/src/lib/hooks/useOTAUpdates.test.ts b/src/lib/hooks/useOTAUpdates.test.ts index 54bbf08470..1deac41413 100644 --- a/src/lib/hooks/useOTAUpdates.test.ts +++ b/src/lib/hooks/useOTAUpdates.test.ts @@ -1,4 +1,4 @@ -import {Alert} from 'react-native' +import {Alert, AppState} from 'react-native' import { checkForUpdateAsync, fetchUpdateAsync, @@ -13,15 +13,24 @@ import {logger} from '#/logger' import {APP_VERSION} from '#/env' import {device} from '#/storage' import { + checkForOTAUpdate, + prepareOTAUpdateRequest, useApplyPullRequestOTAUpdate, useOTAUpdateRecovery, + useOTAUpdates, } from './useOTAUpdates' +let mockRuntimeVersion = '1.133.0' + jest.mock('expo-updates', () => ({ + channel: 'testflight', checkForUpdateAsync: jest.fn(), fetchUpdateAsync: jest.fn(), isEnabled: true, reloadAsync: jest.fn(), + get runtimeVersion() { + return mockRuntimeVersion + }, setExtraParamAsync: jest.fn(), UpdateCheckResultNotAvailableReason: { NO_UPDATE_AVAILABLE_ON_SERVER: 'noUpdateAvailableOnServer', @@ -61,10 +70,12 @@ function mockCurrentlyRunning({ buildChannel = 'testflight', channel, updateId = 'current-update', + isUpdatePending = false, }: { buildChannel?: string channel?: string updateId?: string + isUpdatePending?: boolean } = {}) { const currentlyRunning = { channel: buildChannel, @@ -76,6 +87,7 @@ function mockCurrentlyRunning({ } jest.mocked(useUpdates).mockReturnValue({ currentlyRunning, + isUpdatePending, } as ReturnType) return currentlyRunning } @@ -84,13 +96,30 @@ const currentUpdate = {updateId: 'current-update'} beforeEach(() => { jest.clearAllMocks() + mockRuntimeVersion = '1.133.0' mockCurrentlyRunning() jest.mocked(setExtraParamAsync).mockResolvedValue(undefined) jest.mocked(reloadAsync).mockResolvedValue(undefined) jest.spyOn(Alert, 'alert').mockImplementation(() => {}) + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({status: 'available', deployments: []}), + }) }) describe('useApplyPullRequestOTAUpdate', () => { + it('rejects manually applying a non-PR channel', async () => { + const {result} = renderHook(() => useApplyPullRequestOTAUpdate()) + + await act(() => result.current.tryApplyUpdate('testflight')) + + expect(Alert.alert).toHaveBeenCalledWith( + 'Invalid Deployment', + expect.stringContaining('Only pull request'), + ) + expect(checkForUpdateAsync).not.toHaveBeenCalled() + }) + it('detects a running PR deployment from the manifest metadata', () => { mockCurrentlyRunning({ buildChannel: 'testflight', @@ -209,7 +238,7 @@ describe('useApplyPullRequestOTAUpdate', () => { isNew: true, isRollBackToEmbedded: false, manifest: {id: 'mismatched-update'}, - } as Awaited>) + } as unknown as Awaited>) const {result} = renderHook(() => useApplyPullRequestOTAUpdate()) await act(() => result.current.tryApplyUpdate('pull-request-123', '0.0.0')) @@ -231,6 +260,103 @@ describe('useApplyPullRequestOTAUpdate', () => { }) }) + it('does not offer an app-version override to fingerprint clients', async () => { + mockRuntimeVersion = 'a'.repeat(40) + jest.mocked(checkForUpdateAsync).mockResolvedValue({ + isAvailable: true, + } as Awaited>) + const {result} = renderHook(() => useApplyPullRequestOTAUpdate()) + + await act(() => result.current.tryApplyUpdate('pull-request-123', '0.0.0')) + + expect(Alert.alert).toHaveBeenCalledWith( + 'Apply update from PR #123?', + expect.stringContaining('relaunch'), + expect.arrayContaining([expect.objectContaining({text: 'Apply'})]), + ) + expect(Alert.alert).not.toHaveBeenCalledWith( + 'App Version Mismatch', + expect.anything(), + expect.anything(), + ) + }) + + it.each([ + ['runtime-mismatch', 'Different Native Build Required'], + ['not-published', 'No Deployment Available'], + ['stale-link', 'Deployment Link Is Out of Date'], + ] as const)('handles the %s diagnostic status', async (status, title) => { + mockRuntimeVersion = 'a'.repeat(40) + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({status, deployments: []}), + }) + const {result} = renderHook(() => useApplyPullRequestOTAUpdate()) + + await act(() => result.current.tryApplyUpdate('pull-request-123')) + + expect(Alert.alert).toHaveBeenCalledWith(title, expect.any(String)) + expect(checkForUpdateAsync).not.toHaveBeenCalled() + }) + + it('stays quiet when diagnostics report the deployment already running', async () => { + mockRuntimeVersion = 'a'.repeat(40) + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({status: 'already-running', deployments: []}), + }) + const {result} = renderHook(() => useApplyPullRequestOTAUpdate()) + + await act(() => result.current.tryApplyUpdate('pull-request-123')) + + expect(Alert.alert).not.toHaveBeenCalled() + expect(checkForUpdateAsync).not.toHaveBeenCalled() + expect(result.current.pending).toBe(false) + }) + + it('allows only one PR confirmation prompt at a time', async () => { + jest.mocked(checkForUpdateAsync).mockResolvedValue({ + isAvailable: true, + } as Awaited>) + const {result} = renderHook(() => useApplyPullRequestOTAUpdate()) + + await act(() => result.current.tryApplyUpdate('pull-request-123')) + await act(() => result.current.tryApplyUpdate('pull-request-456')) + + expect(Alert.alert).toHaveBeenCalledTimes(1) + expect(result.current.pending).toBe(true) + }) + + it('rejects a fetched deployment whose source does not match the link', async () => { + mockRuntimeVersion = 'a'.repeat(40) + jest.mocked(checkForUpdateAsync).mockResolvedValue({ + isAvailable: true, + } as Awaited>) + jest.mocked(fetchUpdateAsync).mockResolvedValue({ + isNew: true, + isRollBackToEmbedded: false, + manifest: {id: 'new-update', extra: {ota: {sourceCommit: 'newer'}}}, + } as unknown as Awaited>) + const {result} = renderHook(() => useApplyPullRequestOTAUpdate()) + + await act(() => + result.current.tryApplyUpdate('pull-request-123', null, { + sourceCommit: 'expected', + }), + ) + const buttons = jest.mocked(Alert.alert).mock.calls[0][2] + void act(() => buttons?.[1].onPress?.()) + + await waitFor(() => + expect(Alert.alert).toHaveBeenLastCalledWith( + 'Update Failed', + expect.stringContaining('deployment changed'), + ), + ) + expect(reloadAsync).not.toHaveBeenCalled() + expect(setExtraParamAsync).toHaveBeenLastCalledWith('channel', 'testflight') + }) + it('informs the user when checking for an OTA fails', async () => { jest.mocked(checkForUpdateAsync).mockRejectedValue(new Error('offline')) const {result} = renderHook(() => useApplyPullRequestOTAUpdate()) @@ -297,8 +423,87 @@ describe('useApplyPullRequestOTAUpdate', () => { updateId: 'new-update', }) expect(device.remove).toHaveBeenCalledWith(['pendingOTAUpdate']) + expect(setExtraParamAsync).toHaveBeenLastCalledWith('channel', 'testflight') expect(result.current.pending).toBe(false) }) + + it('restores the default request after cancellation', async () => { + jest.mocked(checkForUpdateAsync).mockResolvedValue({ + isAvailable: true, + } as Awaited>) + const {result} = renderHook(() => useApplyPullRequestOTAUpdate()) + + await act(() => result.current.tryApplyUpdate('pull-request-123')) + const buttons = jest.mocked(Alert.alert).mock.calls[0][2] + await act(() => buttons?.[0].onPress?.()) + + await waitFor(() => + expect(setExtraParamAsync).toHaveBeenLastCalledWith( + 'channel', + 'testflight', + ), + ) + expect(result.current.pending).toBe(false) + }) +}) + +describe('useOTAUpdates', () => { + it('does not reload a pending default update while a PR prompt owns the request', async () => { + let appStateListener: ((state: string) => Promise) | undefined + jest + .spyOn(AppState, 'addEventListener') + .mockImplementation((_, listener) => { + appStateListener = listener as (state: string) => Promise + return {remove: jest.fn()} + }) + jest.mocked(checkForUpdateAsync).mockResolvedValue({ + isAvailable: true, + } as Awaited>) + mockCurrentlyRunning({isUpdatePending: true}) + const apply = renderHook(() => useApplyPullRequestOTAUpdate()) + renderHook(() => useOTAUpdates()) + + await act(() => apply.result.current.tryApplyUpdate('pull-request-123')) + jest + .spyOn(Date, 'now') + .mockReturnValueOnce(0) + .mockReturnValue(16 * 60e3) + await act(async () => { + await appStateListener?.('background') + await appStateListener?.('active') + }) + + expect(reloadAsync).not.toHaveBeenCalled() + }) +}) + +describe('OTA request preparation', () => { + it('sets the native build number before the channel', async () => { + await prepareOTAUpdateRequest('pull-request-123') + + expect(setExtraParamAsync).toHaveBeenNthCalledWith( + 1, + expect.stringMatching(/^(ios|android)-build-number$/), + expect.any(String), + ) + expect(setExtraParamAsync).toHaveBeenNthCalledWith( + 2, + 'channel', + 'pull-request-123', + ) + }) + + it('prepares the default channel before checking', async () => { + jest.mocked(checkForUpdateAsync).mockResolvedValue({ + isAvailable: false, + reason: UpdateCheckResultNotAvailableReason.NO_UPDATE_AVAILABLE_ON_SERVER, + } as Awaited>) + + await checkForOTAUpdate() + + expect(setExtraParamAsync).toHaveBeenLastCalledWith('channel', 'testflight') + expect(checkForUpdateAsync).toHaveBeenCalledTimes(1) + }) }) describe('useOTAUpdateRecovery', () => { diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts index 37c9239fc3..738786ee44 100644 --- a/src/lib/hooks/useOTAUpdates.ts +++ b/src/lib/hooks/useOTAUpdates.ts @@ -7,12 +7,14 @@ import { } from 'react-native' import {nativeBuildVersion} from 'expo-application' import { + channel as nativeChannel, checkForUpdateAsync, type CurrentlyRunningInfo, fetchUpdateAsync, isEnabled, reloadAsync, type ReloadScreenOptions, + runtimeVersion as nativeRuntimeVersion, setExtraParamAsync, UpdateCheckResultNotAvailableReason, useUpdates, @@ -31,7 +33,42 @@ const OTA_RECOVERY_WINDOW = 5 * 60e3 * The channel this native build is expected to receive updates from. Anything * else is only reachable through the dev tooling in settings. */ -const DEFAULT_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production' +const DEFAULT_CHANNEL = + nativeChannel || (IS_TESTFLIGHT ? 'testflight' : 'production') + +const FINGERPRINT_RUNTIME_VERSION = /^[0-9a-f]{40}$/ +const OTA_RESOLVE_URL = 'https://updates.bsky.app/v1/ota/resolve' + +type OTAResolveResult = { + status: + | 'available' + | 'already-running' + | 'runtime-mismatch' + | 'not-published' + | 'stale-link' + deployments: { + runtimeVersion: string + sourceCommit?: string + publicationId?: string + updateId?: string + }[] +} + +let requestQueue = Promise.resolve() +let manualRequestOwner: symbol | undefined + +function isManualRequestPending() { + return manualRequestOwner !== undefined +} + +function serializeOTARequest(operation: () => Promise): Promise { + const result = requestQueue.then(operation, operation) + requestQueue = result.then( + () => undefined, + () => undefined, + ) + return result +} /** * Channels that our native builds are configured with, see `eas.json`. An @@ -69,17 +106,7 @@ function getRunningChannel( return currentlyRunning?.channel || undefined } -async function setExtraParams() { - await setExtraParamAsync( - IS_IOS ? 'ios-build-number' : 'android-build-number', - // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. - // This just ensures it gets passed as a string - `${nativeBuildVersion}`, - ) - await setExtraParamAsync('channel', DEFAULT_CHANNEL) -} - -async function setExtraParamsPullRequest(channel: string) { +export async function prepareOTAUpdateRequest(channel = DEFAULT_CHANNEL) { await setExtraParamAsync( IS_IOS ? 'ios-build-number' : 'android-build-number', // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is. @@ -89,12 +116,55 @@ async function setExtraParamsPullRequest(channel: string) { await setExtraParamAsync('channel', channel) } -async function updateTestflight(scheme: 'light' | 'dark') { - await setExtraParams() +export function checkForOTAUpdate(channel = DEFAULT_CHANNEL) { + return serializeOTARequest(async () => { + await prepareOTAUpdateRequest(channel) + return checkForUpdateAsync() + }) +} - const res = await checkForUpdateAsync() +export function fetchOTAUpdate(channel = DEFAULT_CHANNEL) { + return serializeOTARequest(async () => { + await prepareOTAUpdateRequest(channel) + return fetchUpdateAsync() + }) +} + +async function resolvePullRequestDeployment({ + channel, + currentUpdateId, + expected, +}: { + channel: string + currentUpdateId?: string + expected?: {sourceCommit?: string | null; publicationId?: string | null} +}): Promise { + if (!FINGERPRINT_RUNTIME_VERSION.test(nativeRuntimeVersion || '')) { + return undefined + } + + const query = new URLSearchParams({ + channel, + platform: IS_IOS ? 'ios' : 'android', + runtimeVersion: nativeRuntimeVersion!, + }) + if (currentUpdateId) query.set('currentUpdateId', currentUpdateId) + if (expected?.sourceCommit) { + query.set('sourceCommit', expected.sourceCommit) + } + if (expected?.publicationId) { + query.set('publicationId', expected.publicationId) + } + + const response = await fetch(`${OTA_RESOLVE_URL}?${query}`) + if (!response.ok) throw new Error(`OTA resolve failed (${response.status})`) + return response.json() as Promise +} + +async function updateTestflight(scheme: 'light' | 'dark') { + const res = await checkForOTAUpdate() if (res.isAvailable) { - await fetchUpdateAsync() + await fetchOTAUpdate() Alert.alert( 'Update Available', 'A new version of the app is available. Relaunch now?', @@ -121,6 +191,7 @@ export function useApplyPullRequestOTAUpdate() { const t = useTheme() const {currentlyRunning} = useUpdates() const [pending, setPending] = useState(false) + const requestOwnerRef = useRef(undefined) const currentChannel = getRunningChannel(currentlyRunning) const isCurrentlyRunningPullRequestDeployment = currentChannel?.startsWith('pull-request') @@ -133,15 +204,73 @@ export function useApplyPullRequestOTAUpdate() { currentChannel && !STANDARD_CHANNELS.includes(currentChannel), ) + useEffect(() => { + return () => { + if (manualRequestOwner === requestOwnerRef.current) { + manualRequestOwner = undefined + void serializeOTARequest(() => prepareOTAUpdateRequest()) + } + } + }, []) + const tryApplyUpdate = async ( channel: string, declaredAppVersion?: string | null, + expected?: {sourceCommit?: string | null; publicationId?: string | null}, ) => { + if (!/^pull-request-[1-9]\d*$/.test(channel)) { + Alert.alert( + 'Invalid Deployment', + 'Only pull request deployments can be applied manually.', + ) + return + } + if (manualRequestOwner) return + + const requestOwner = Symbol(channel) + manualRequestOwner = requestOwner + requestOwnerRef.current = requestOwner + setPending(true) const deploymentName = getDeploymentName(channel) + let resolvedDeployment: OTAResolveResult | undefined const checkForDeployment = async () => { - await setExtraParamsPullRequest(channel) - const res = await checkForUpdateAsync() + try { + resolvedDeployment = await resolvePullRequestDeployment({ + channel, + currentUpdateId: currentlyRunning?.updateId, + expected, + }) + } catch (err) { + logger.debug('Could not resolve OTA deployment diagnostics', { + safeMessage: err, + }) + } + + if (resolvedDeployment?.status === 'already-running') return false + if (resolvedDeployment?.status === 'runtime-mismatch') { + Alert.alert( + 'Different Native Build Required', + `The ${deploymentName} deployment requires a different native build. Install a compatible TestFlight build and try again.`, + ) + return false + } + if (resolvedDeployment?.status === 'not-published') { + Alert.alert( + 'No Deployment Available', + `The ${deploymentName} deployment has not been published or is no longer available.`, + ) + return false + } + if (resolvedDeployment?.status === 'stale-link') { + Alert.alert( + 'Deployment Link Is Out of Date', + `This link does not refer to the latest ${deploymentName} deployment. Open the newest link and try again.`, + ) + return false + } + + const res = await checkForOTAUpdate(channel) if (!res.isAvailable) { if ( res.reason === @@ -161,16 +290,50 @@ export function useApplyPullRequestOTAUpdate() { return res.isAvailable } + const finishManualRequest = async (restoreDefault: boolean) => { + if (manualRequestOwner !== requestOwner) return + manualRequestOwner = undefined + requestOwnerRef.current = undefined + if (restoreDefault) { + await serializeOTARequest(() => prepareOTAUpdateRequest()).catch( + () => {}, + ) + } + setPending(false) + } + + const restoreAfterCancellation = () => { + void finishManualRequest(true) + } + const applyUpdate = () => { - setPending(true) void (async () => { + let reloadSucceeded = false try { if (!(await checkForDeployment())) return - const fetchedUpdate = await fetchUpdateAsync() + const fetchedUpdate = await fetchOTAUpdate(channel) if (!fetchedUpdate.isNew) { throw new Error('Expo did not download a new update.') } + const manifest = fetchedUpdate.manifest as { + extra?: { + ota?: {sourceCommit?: unknown; publicationId?: unknown} + } + } + const sourceCommit = manifest.extra?.ota?.sourceCommit + const publicationId = manifest.extra?.ota?.publicationId + if ( + (expected?.sourceCommit && + sourceCommit !== expected.sourceCommit) || + (expected?.publicationId && + publicationId !== expected.publicationId) + ) { + throw new Error( + 'The deployment changed while it was being downloaded. Check the link again to review the newest deployment.', + ) + } + device.set(['pendingOTAUpdate'], { attemptedAt: Date.now(), channel, @@ -180,6 +343,7 @@ export function useApplyPullRequestOTAUpdate() { await reloadAsync({ reloadScreenOptions: splash(t.scheme), }) + reloadSucceeded = true } catch (e) { device.remove(['pendingOTAUpdate']) throw e @@ -192,7 +356,7 @@ export function useApplyPullRequestOTAUpdate() { `Could not apply the ${deploymentName} deployment: ${error}`, ) } finally { - setPending(false) + await finishManualRequest(!reloadSucceeded) } })() } @@ -203,11 +367,17 @@ export function useApplyPullRequestOTAUpdate() { * update re-delivers the deep link that triggered it, and the same link may * also just be tapped again. */ - setPending(true) try { - if (!(await checkForDeployment())) return + if (!(await checkForDeployment())) { + await finishManualRequest(true) + return + } - if (declaredAppVersion && declaredAppVersion !== APP_VERSION) { + if ( + !FINGERPRINT_RUNTIME_VERSION.test(nativeRuntimeVersion || '') && + declaredAppVersion && + declaredAppVersion !== APP_VERSION + ) { Alert.alert( 'App Version Mismatch', `This OTA update was built for a different version of the app.\n\nCurrent app version: ${APP_VERSION}\nOTA app version: ${declaredAppVersion}\n\nApplying it anyway may cause the app to stop working and require a reinstall.`, @@ -215,6 +385,7 @@ export function useApplyPullRequestOTAUpdate() { { text: 'Cancel', style: 'cancel', + onPress: restoreAfterCancellation, }, { text: 'Apply Anyway', @@ -233,6 +404,7 @@ export function useApplyPullRequestOTAUpdate() { { text: 'Cancel', style: 'cancel', + onPress: restoreAfterCancellation, }, { text: 'Apply', @@ -242,14 +414,13 @@ export function useApplyPullRequestOTAUpdate() { ], ) } catch (e: unknown) { + await finishManualRequest(true) const error = String(e) logger.error('Internal OTA Update Error', {error}) Alert.alert( 'Update Check Failed', `Could not check the ${deploymentName} deployment: ${error}`, ) - } finally { - setPending(false) } } @@ -260,10 +431,9 @@ export function useApplyPullRequestOTAUpdate() { const restoreDefaultChannel = async () => { setPending(true) try { - await setExtraParams() - const res = await checkForUpdateAsync() + const res = await checkForOTAUpdate() if (res.isAvailable) { - await fetchUpdateAsync() + await fetchOTAUpdate() await reloadAsync() } else { Alert.alert( @@ -351,14 +521,14 @@ export function useOTAUpdates() { const setCheckTimeout = useCallback(() => { timeout.current = setTimeout(async () => { try { - await setExtraParams() + if (isManualRequestPending()) return logger.debug('Checking for update...') - const res = await checkForUpdateAsync() + const res = await checkForOTAUpdate() if (res.isAvailable) { logger.debug('Attempting to fetch update...') - await fetchUpdateAsync() + await fetchOTAUpdate() } else { logger.debug('No update available.') } @@ -419,6 +589,7 @@ export function useOTAUpdates() { // If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since // chances are that there isn't anything important going on in the current session. if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) { + if (isManualRequestPending()) return if (isUpdatePending) { await reloadAsync({ reloadScreenOptions: splash(t.scheme), diff --git a/src/lib/hooks/useOTAUpdates.web.ts b/src/lib/hooks/useOTAUpdates.web.ts index 970deebf1b..be063518ba 100644 --- a/src/lib/hooks/useOTAUpdates.web.ts +++ b/src/lib/hooks/useOTAUpdates.web.ts @@ -5,6 +5,10 @@ export function useApplyPullRequestOTAUpdate() { tryApplyUpdate: async ( _channel: string, _declaredAppVersion?: string | null, + _expected?: { + sourceCommit?: string | null + publicationId?: string | null + }, ) => {}, restoreDefaultChannel: async () => {}, isCurrentlyRunningPullRequestDeployment: false, diff --git a/src/screens/Settings/components/OTAInfo.tsx b/src/screens/Settings/components/OTAInfo.tsx index 25b2c828d9..61843a24e0 100644 --- a/src/screens/Settings/components/OTAInfo.tsx +++ b/src/screens/Settings/components/OTAInfo.tsx @@ -4,7 +4,11 @@ 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 { + checkForOTAUpdate, + fetchOTAUpdate, + 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' @@ -25,7 +29,7 @@ export function OTAInfo() { } = useQuery({ queryKey: ['ota-info'], queryFn: async () => { - const status = await Updates.checkForUpdateAsync() + const status = await checkForOTAUpdate() return status.isAvailable }, }) @@ -33,7 +37,7 @@ export function OTAInfo() { const {mutate: fetchAndLaunchUpdate, isPending: isPendingUpdate} = useMutation({ mutationFn: async () => { - await Updates.fetchUpdateAsync() + await fetchOTAUpdate() await Updates.reloadAsync({ reloadScreenOptions: splash(t.scheme), })