Compare commits

...

1 Commits

Author SHA1 Message Date
Claude 21f072914d Show a non-standard OTA channel notice in the drawer
When an install is running an OTA deployment from a channel our native
builds don't ship with (e.g. a `pull-request-*` channel applied from the
dev settings), it's easy to forget and end up debugging the wrong bundle.
Surface a warning box at the top of the drawer naming the channel, with a
button to pull the default channel's deployment and relaunch into it.

Generalizes the pull request check in `useApplyPullRequestOTAUpdate` to any
non-standard channel, and renames `revertToEmbedded` to
`restoreDefaultChannel` since that's what it does - it now also reports
when there's no deployment to restore instead of failing silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WUSAeJTqPtyvuU4EX7WoRR
2026-07-26 20:16:50 +00:00
5 changed files with 120 additions and 10 deletions
+65
View File
@@ -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<ViewStyle>}) {
const t = useTheme()
const {t: l} = useLingui()
const {
currentChannel,
defaultChannel,
isCurrentlyRunningNonStandardChannel,
restoreDefaultChannel,
pending,
} = useApplyPullRequestOTAUpdate()
if (!isCurrentlyRunningNonStandardChannel) return null
return (
<Admonition.Outer
type="warning"
style={[t.atoms.bg_contrast_25, a.gap_sm, style]}>
<Admonition.Row>
<Admonition.Icon />
<Admonition.Content style={[a.gap_2xs]}>
<Text style={[a.text_sm, a.font_bold, a.leading_snug]}>
<Trans>Non-standard OTA channel</Trans>
</Text>
<Admonition.Text>
<Trans>
This app is running a deployment of{' '}
<Text style={[a.text_sm, a.font_bold, a.leading_snug]}>
{currentChannel}
</Text>
. Restore the {defaultChannel} deployment to get back to a
standard build.
</Trans>
</Admonition.Text>
</Admonition.Content>
</Admonition.Row>
<View style={[a.flex_row]}>
<Admonition.Button
color="secondary_inverted"
label={l`Restore the ${defaultChannel} deployment`}
disabled={pending}
onPress={() => void restoreDefaultChannel()}>
<ButtonText>
<Trans>Restore default</Trans>
</ButtonText>
{pending && <ButtonIcon icon={Loader} />}
</Admonition.Button>
</View>
</Admonition.Outer>
)
}
+47 -7
View File
@@ -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,
}
}
+3 -1
View File
@@ -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,
}
}
+2 -2
View File
@@ -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 ? (
<SettingsList.PressableItem
onPress={() => void revertToEmbedded()}
onPress={() => void restoreDefaultChannel()}
label={l`Unapply Pull Request`}>
<SettingsList.ItemText>
<Trans>Unapply Pull Request {currentChannel}</Trans>
+3
View File
@@ -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 => {
</View>
)}
<OTAChannelNotice style={[a.mt_lg]} />
<Divider style={[a.mt_xl, a.mb_sm]} />
</View>