diff options
author | hailey <me@haileyok.com> | 2025-07-24 16:23:17 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-07-24 16:23:17 -0700 |
commit | 1ee91d2c90ef84e0b6728e896292968562c8af01 (patch) | |
tree | 55dc5556d4f0f5382753a83bc98eb974cf12427d /src/lib/hooks | |
parent | 9e65f00c937b156b876b0f6ca23dcef12b945dbe (diff) | |
download | voidsky-1ee91d2c90ef84e0b6728e896292968562c8af01.tar.zst |
OTA deployments on PR comment action (#8713)
Diffstat (limited to 'src/lib/hooks')
-rw-r--r-- | src/lib/hooks/useIntentHandler.ts | 23 | ||||
-rw-r--r-- | src/lib/hooks/useOTAUpdates.ts | 166 |
2 files changed, 157 insertions, 32 deletions
diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 6b1083aa4..f55217e56 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -1,8 +1,9 @@ import React from 'react' +import {Alert} from 'react-native' import * as Linking from 'expo-linking' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' -import {logEvent} from '#/lib/statsig/statsig' +import {logger} from '#/logger' import {isNative} from '#/platform/detection' import {useSession} from '#/state/session' import {useCloseAllActiveElements} from '#/state/util' @@ -12,8 +13,10 @@ import { } from '#/components/ageAssurance/AgeAssuranceRedirectDialog' import {useIntentDialogs} from '#/components/intents/IntentDialogs' import {Referrer} from '../../../modules/expo-bluesky-swiss-army' +import {IS_TESTFLIGHT} from '../app-info.web' +import {useApplyPullRequestOTAUpdate} from './useOTAUpdates' -type IntentType = 'compose' | 'verify-email' | 'age-assurance' +type IntentType = 'compose' | 'verify-email' | 'age-assurance' | 'apply-ota' const VALID_IMAGE_REGEX = /^[\w.:\-_/]+\|\d+(\.\d+)?\|\d+(\.\d+)?$/ @@ -27,12 +30,13 @@ export function useIntentHandler() { const ageAssuranceRedirectDialogControl = useAgeAssuranceRedirectDialogControl() const {currentAccount} = useSession() + const {tryApplyUpdate} = useApplyPullRequestOTAUpdate() React.useEffect(() => { const handleIncomingURL = (url: string) => { const referrerInfo = Referrer.getReferrerInfo() if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { - logEvent('deepLink:referrerReceived', { + logger.metric('deepLink:referrerReceived', { to: url, referrer: referrerInfo?.referrer, hostname: referrerInfo?.hostname, @@ -92,6 +96,18 @@ export function useIntentHandler() { } return } + case 'apply-ota': { + if (!isNative || !IS_TESTFLIGHT) { + return + } + + const channel = params.get('channel') + if (!channel) { + Alert.alert('Error', 'No channel provided to look for.') + } else { + tryApplyUpdate(channel) + } + } default: { return } @@ -111,6 +127,7 @@ export function useIntentHandler() { verifyEmailIntent, ageAssuranceRedirectDialogControl, currentAccount, + tryApplyUpdate, ]) } diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts index 731406dce..72f215fa9 100644 --- a/src/lib/hooks/useOTAUpdates.ts +++ b/src/lib/hooks/useOTAUpdates.ts @@ -1,5 +1,5 @@ import React from 'react' -import {Alert, AppState, AppStateStatus} from 'react-native' +import {Alert, AppState, type AppStateStatus} from 'react-native' import {nativeBuildVersion} from 'expo-application' import { checkForUpdateAsync, @@ -29,6 +29,128 @@ async function setExtraParams() { ) } +async function setExtraParamsPullRequest(channel: string) { + await setExtraParamAsync( + isIOS ? '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', channel) +} + +async function updateTestflight() { + await setExtraParams() + + const res = await checkForUpdateAsync() + if (res.isAvailable) { + await fetchUpdateAsync() + + Alert.alert( + 'Update Available', + 'A new version of the app is available. Relaunch now?', + [ + { + text: 'No', + style: 'cancel', + }, + { + text: 'Relaunch', + style: 'default', + onPress: async () => { + await reloadAsync() + }, + }, + ], + ) + } +} + +export function useApplyPullRequestOTAUpdate() { + const {currentlyRunning} = useUpdates() + const [pending, setPending] = React.useState(false) + const currentChannel = currentlyRunning?.channel + const isCurrentlyRunningPullRequestDeployment = + currentChannel?.startsWith('pull-request') + + const tryApplyUpdate = async (channel: string) => { + setPending(true) + if (currentChannel === channel) { + const res = await checkForUpdateAsync() + if (res.isAvailable) { + logger.debug('Attempting to fetch update...') + await fetchUpdateAsync() + Alert.alert( + 'Deployment Available', + `A new deployment of ${channel} is availalble. Relaunch now?`, + [ + { + text: 'No', + style: 'cancel', + }, + { + text: 'Relaunch', + style: 'default', + onPress: async () => { + await reloadAsync() + }, + }, + ], + ) + } else { + Alert.alert( + 'No Deployment Available', + `No new deployments of ${channel} are currently available for your current native build.`, + ) + } + } else { + setExtraParamsPullRequest(channel) + const res = await checkForUpdateAsync() + if (res.isAvailable) { + Alert.alert( + 'Deployment Available', + `A deployment of ${channel} is availalble. Applying this deployment may result in a bricked installation, in which case you will need to reinstall the app and may lose local data. Are you sure you want to proceed?`, + [ + { + text: 'No', + style: 'cancel', + }, + { + text: 'Relaunch', + style: 'default', + onPress: async () => { + await reloadAsync() + }, + }, + ], + ) + } else { + Alert.alert( + 'No Deployment Available', + `No new deployments of ${channel} are currently available for your current native build.`, + ) + } + } + setPending(false) + } + + const revertToEmbedded = async () => { + try { + await updateTestflight() + } catch (e: any) { + logger.error('Internal OTA Update Error', {error: `${e}`}) + } + } + + return { + tryApplyUpdate, + revertToEmbedded, + currentChannel, + isCurrentlyRunningPullRequestDeployment, + pending, + } +} + export function useOTAUpdates() { const shouldReceiveUpdates = isEnabled && !__DEV__ @@ -36,7 +158,8 @@ export function useOTAUpdates() { const lastMinimize = React.useRef(0) const ranInitialCheck = React.useRef(false) const timeout = React.useRef<NodeJS.Timeout>() - const {isUpdatePending} = useUpdates() + const {currentlyRunning, isUpdatePending} = useUpdates() + const currentChannel = currentlyRunning?.channel const setCheckTimeout = React.useCallback(() => { timeout.current = setTimeout(async () => { @@ -60,36 +183,18 @@ export function useOTAUpdates() { const onIsTestFlight = React.useCallback(async () => { try { - await setExtraParams() - - const res = await checkForUpdateAsync() - if (res.isAvailable) { - await fetchUpdateAsync() - - Alert.alert( - 'Update Available', - 'A new version of the app is available. Relaunch now?', - [ - { - text: 'No', - style: 'cancel', - }, - { - text: 'Relaunch', - style: 'default', - onPress: async () => { - await reloadAsync() - }, - }, - ], - ) - } + await updateTestflight() } catch (e: any) { logger.error('Internal OTA Update Error', {error: `${e}`}) } }, []) React.useEffect(() => { + // We don't need to check anything if the current update is a PR update + if (currentChannel?.startsWith('pull-request')) { + return + } + // We use this setTimeout to allow Statsig to initialize before we check for an update // For Testflight users, we can prompt the user to update immediately whenever there's an available update. This // is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update @@ -103,12 +208,15 @@ export function useOTAUpdates() { setCheckTimeout() ranInitialCheck.current = true - }, [onIsTestFlight, setCheckTimeout, shouldReceiveUpdates]) + }, [onIsTestFlight, currentChannel, setCheckTimeout, shouldReceiveUpdates]) // After the app has been minimized for 15 minutes, we want to either A. install an update if one has become available // or B check for an update again. React.useEffect(() => { - if (!isEnabled) return + // We also don't start this timeout if the user is on a pull request update + if (!isEnabled || currentChannel?.startsWith('pull-request')) { + return + } const subscription = AppState.addEventListener( 'change', @@ -138,5 +246,5 @@ export function useOTAUpdates() { clearTimeout(timeout.current) subscription.remove() } - }, [isUpdatePending, setCheckTimeout]) + }, [isUpdatePending, currentChannel, setCheckTimeout]) } |