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
|
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() {
|
async function setExtraParams() {
|
||||||
await setExtraParamAsync(
|
await setExtraParamAsync(
|
||||||
IS_IOS ? 'ios-build-number' : 'android-build-number',
|
IS_IOS ? 'ios-build-number' : 'android-build-number',
|
||||||
@@ -23,10 +35,7 @@ async function setExtraParams() {
|
|||||||
// This just ensures it gets passed as a string
|
// This just ensures it gets passed as a string
|
||||||
`${nativeBuildVersion}`,
|
`${nativeBuildVersion}`,
|
||||||
)
|
)
|
||||||
await setExtraParamAsync(
|
await setExtraParamAsync('channel', DEFAULT_CHANNEL)
|
||||||
'channel',
|
|
||||||
IS_TESTFLIGHT ? 'testflight' : 'production',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setExtraParamsPullRequest(channel: string) {
|
async function setExtraParamsPullRequest(channel: string) {
|
||||||
@@ -71,6 +80,14 @@ export function useApplyPullRequestOTAUpdate() {
|
|||||||
const currentChannel = currentlyRunning?.channel
|
const currentChannel = currentlyRunning?.channel
|
||||||
const isCurrentlyRunningPullRequestDeployment =
|
const isCurrentlyRunningPullRequestDeployment =
|
||||||
currentChannel?.startsWith('pull-request')
|
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) => {
|
const tryApplyUpdate = async (channel: string) => {
|
||||||
setPending(true)
|
setPending(true)
|
||||||
@@ -104,19 +121,42 @@ export function useApplyPullRequestOTAUpdate() {
|
|||||||
setPending(false)
|
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 {
|
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) {
|
} catch (e: any) {
|
||||||
logger.error('Internal OTA Update Error', {error: `${e}`})
|
logger.error('Internal OTA Update Error', {error: `${e}`})
|
||||||
|
Alert.alert(
|
||||||
|
'Restore Failed',
|
||||||
|
`Could not restore the ${DEFAULT_CHANNEL} deployment: ${e}`,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setPending(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tryApplyUpdate,
|
tryApplyUpdate,
|
||||||
revertToEmbedded,
|
restoreDefaultChannel,
|
||||||
isCurrentlyRunningPullRequestDeployment,
|
isCurrentlyRunningPullRequestDeployment,
|
||||||
|
isCurrentlyRunningNonStandardChannel,
|
||||||
currentChannel,
|
currentChannel,
|
||||||
|
defaultChannel: DEFAULT_CHANNEL,
|
||||||
pending,
|
pending,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ export function useOTAUpdates() {}
|
|||||||
export function useApplyPullRequestOTAUpdate() {
|
export function useApplyPullRequestOTAUpdate() {
|
||||||
return {
|
return {
|
||||||
tryApplyUpdate: async (_channel: string) => {},
|
tryApplyUpdate: async (_channel: string) => {},
|
||||||
revertToEmbedded: () => {},
|
restoreDefaultChannel: async () => {},
|
||||||
isCurrentlyRunningPullRequestDeployment: false,
|
isCurrentlyRunningPullRequestDeployment: false,
|
||||||
|
isCurrentlyRunningNonStandardChannel: false,
|
||||||
currentChannel: 'web-build',
|
currentChannel: 'web-build',
|
||||||
|
defaultChannel: 'web-build',
|
||||||
pending: false,
|
pending: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ function DevOptions() {
|
|||||||
const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
|
const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
|
||||||
const {
|
const {
|
||||||
tryApplyUpdate,
|
tryApplyUpdate,
|
||||||
revertToEmbedded,
|
restoreDefaultChannel,
|
||||||
isCurrentlyRunningPullRequestDeployment,
|
isCurrentlyRunningPullRequestDeployment,
|
||||||
currentChannel,
|
currentChannel,
|
||||||
} = useApplyPullRequestOTAUpdate()
|
} = useApplyPullRequestOTAUpdate()
|
||||||
@@ -520,7 +520,7 @@ function DevOptions() {
|
|||||||
) : null}
|
) : null}
|
||||||
{IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? (
|
{IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? (
|
||||||
<SettingsList.PressableItem
|
<SettingsList.PressableItem
|
||||||
onPress={() => void revertToEmbedded()}
|
onPress={() => void restoreDefaultChannel()}
|
||||||
label={l`Unapply Pull Request`}>
|
label={l`Unapply Pull Request`}>
|
||||||
<SettingsList.ItemText>
|
<SettingsList.ItemText>
|
||||||
<Trans>Unapply Pull Request {currentChannel}</Trans>
|
<Trans>Unapply Pull Request {currentChannel}</Trans>
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import {
|
|||||||
UserCircle_Stroke2_Corner0_Rounded as UserCircle,
|
UserCircle_Stroke2_Corner0_Rounded as UserCircle,
|
||||||
} from '#/components/icons/UserCircle'
|
} from '#/components/icons/UserCircle'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
|
import {OTAChannelNotice} from '#/components/OTAChannelNotice'
|
||||||
import {ProfileBadges} from '#/components/ProfileBadges'
|
import {ProfileBadges} from '#/components/ProfileBadges'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
@@ -343,6 +344,8 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<OTAChannelNotice style={[a.mt_lg]} />
|
||||||
|
|
||||||
<Divider style={[a.mt_xl, a.mb_sm]} />
|
<Divider style={[a.mt_xl, a.mb_sm]} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user