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
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user