diff --git a/src/components/OTAChannelNotice.tsx b/src/components/OTAChannelNotice.tsx new file mode 100644 index 0000000000..b00f75f56c --- /dev/null +++ b/src/components/OTAChannelNotice.tsx @@ -0,0 +1,65 @@ +import {type StyleProp, View, type ViewStyle} from 'react-native' +import {Trans, useLingui} from '@lingui/react/macro' + +import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' +import {atoms as a, useTheme} from '#/alf' +import * as Admonition from '#/components/Admonition' +import {ButtonIcon, ButtonText} from '#/components/Button' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +/** + * Warns that the running bundle came from a channel this build doesn't normally + * receive updates from, e.g. a pull request deployment applied from the dev + * settings. Renders nothing on a standard channel, and never on web. + */ +export function OTAChannelNotice({style}: {style?: StyleProp}) { + const t = useTheme() + const {t: l} = useLingui() + const { + currentChannel, + defaultChannel, + isCurrentlyRunningNonStandardChannel, + restoreDefaultChannel, + pending, + } = useApplyPullRequestOTAUpdate() + + if (!isCurrentlyRunningNonStandardChannel) return null + + return ( + + + + + + Non-standard OTA channel + + + + This app is running a deployment of{' '} + + {currentChannel} + + . Restore the {defaultChannel} deployment to get back to a + standard build. + + + + + + void restoreDefaultChannel()}> + + Restore default + + {pending && } + + + + ) +} diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts index 766fb959df..882bfa4ed1 100644 --- a/src/lib/hooks/useOTAUpdates.ts +++ b/src/lib/hooks/useOTAUpdates.ts @@ -16,6 +16,18 @@ import {IS_ANDROID, IS_IOS, IS_TESTFLIGHT} from '#/env' const MINIMUM_MINIMIZE_TIME = 15 * 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' + +/** + * Channels that our native builds are configured with, see `eas.json`. An + * update running on any other channel was applied manually. + */ +const STANDARD_CHANNELS = ['production', 'testflight', 'development'] + async function setExtraParams() { await setExtraParamAsync( IS_IOS ? 'ios-build-number' : 'android-build-number', @@ -23,10 +35,7 @@ async function setExtraParams() { // This just ensures it gets passed as a string `${nativeBuildVersion}`, ) - await setExtraParamAsync( - 'channel', - IS_TESTFLIGHT ? 'testflight' : 'production', - ) + await setExtraParamAsync('channel', DEFAULT_CHANNEL) } async function setExtraParamsPullRequest(channel: string) { @@ -71,6 +80,14 @@ export function useApplyPullRequestOTAUpdate() { const currentChannel = currentlyRunning?.channel const isCurrentlyRunningPullRequestDeployment = currentChannel?.startsWith('pull-request') + /* + * Covers pull request deployments as well as any other channel we manually + * applied an update from. Note that `channel` is null when updates are + * disabled (e.g. in dev), in which case there's nothing to restore. + */ + const isCurrentlyRunningNonStandardChannel = Boolean( + currentChannel && !STANDARD_CHANNELS.includes(currentChannel), + ) const tryApplyUpdate = async (channel: string) => { setPending(true) @@ -104,19 +121,42 @@ export function useApplyPullRequestOTAUpdate() { setPending(false) } - const revertToEmbedded = async () => { + /** + * Pulls the newest update from the channel this build ships with and relaunches + * into it, undoing a manually applied deployment. + */ + const restoreDefaultChannel = async () => { + setPending(true) try { - await updateTestflight() + await setExtraParams() + const res = await checkForUpdateAsync() + if (res.isAvailable) { + await fetchUpdateAsync() + await reloadAsync() + } else { + Alert.alert( + 'Nothing to Restore', + `No deployment of ${DEFAULT_CHANNEL} is currently available for your native build. Reinstall the app to get back to a standard build.`, + ) + } } catch (e: any) { logger.error('Internal OTA Update Error', {error: `${e}`}) + Alert.alert( + 'Restore Failed', + `Could not restore the ${DEFAULT_CHANNEL} deployment: ${e}`, + ) + } finally { + setPending(false) } } return { tryApplyUpdate, - revertToEmbedded, + restoreDefaultChannel, isCurrentlyRunningPullRequestDeployment, + isCurrentlyRunningNonStandardChannel, currentChannel, + defaultChannel: DEFAULT_CHANNEL, pending, } } diff --git a/src/lib/hooks/useOTAUpdates.web.ts b/src/lib/hooks/useOTAUpdates.web.ts index 30cb2c6dba..6938a5b003 100644 --- a/src/lib/hooks/useOTAUpdates.web.ts +++ b/src/lib/hooks/useOTAUpdates.web.ts @@ -2,9 +2,11 @@ export function useOTAUpdates() {} export function useApplyPullRequestOTAUpdate() { return { tryApplyUpdate: async (_channel: string) => {}, - revertToEmbedded: () => {}, + restoreDefaultChannel: async () => {}, isCurrentlyRunningPullRequestDeployment: false, + isCurrentlyRunningNonStandardChannel: false, currentChannel: 'web-build', + defaultChannel: 'web-build', pending: false, } } diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index a1234a27ec..a58b1bf3dd 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -393,7 +393,7 @@ function DevOptions() { const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration() const { tryApplyUpdate, - revertToEmbedded, + restoreDefaultChannel, isCurrentlyRunningPullRequestDeployment, currentChannel, } = useApplyPullRequestOTAUpdate() @@ -520,7 +520,7 @@ function DevOptions() { ) : null} {IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? ( void revertToEmbedded()} + onPress={() => void restoreDefaultChannel()} label={l`Unapply Pull Request`}> Unapply Pull Request {currentChannel} diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 358759f379..f82b699363 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -62,6 +62,7 @@ import { UserCircle_Stroke2_Corner0_Rounded as UserCircle, } from '#/components/icons/UserCircle' import {InlineLinkText} from '#/components/Link' +import {OTAChannelNotice} from '#/components/OTAChannelNotice' import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' @@ -343,6 +344,8 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => { )} + +