From 870ea9623eb153624b02f793618c2c116919058b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 30 Jul 2026 22:50:08 +0300 Subject: [PATCH] detect the running ota channel from manifest metadata currentlyRunning.channel is a build-time constant from the native build config, so it never reflects a manually applied deployment. Read the channel our update server stamps into each published manifest instead, falling back to the build constant for embedded launches. This makes the pull-request channel guards actually fire, so a PR bundle no longer resets the extra params and reverts itself to the default channel on startup, and the drawer notice renders again. Co-Authored-By: Claude Fable 5 --- src/lib/hooks/useOTAUpdates.ts | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts index a7e043aeb2..048f98ed53 100644 --- a/src/lib/hooks/useOTAUpdates.ts +++ b/src/lib/hooks/useOTAUpdates.ts @@ -3,6 +3,7 @@ import {Alert, AppState, type AppStateStatus} from 'react-native' import {nativeBuildVersion} from 'expo-application' import { checkForUpdateAsync, + type CurrentlyRunningInfo, fetchUpdateAsync, isEnabled, reloadAsync, @@ -36,6 +37,32 @@ function getDeploymentName(channel: string) { return pullRequestNumber ? `PR #${pullRequestNumber}` : channel } +/** + * The channel of the update bundle that is actually running. The + * `currentlyRunning.channel` constant only reflects the channel baked into the + * native build config, so a manually applied deployment (e.g. a pull request + * channel) must be detected from the manifest metadata our update server stamps + * into every published update. Embedded launches have no server manifest and + * fall back to the build constant. + */ +function getRunningChannel( + currentlyRunning: CurrentlyRunningInfo | undefined, +): string | undefined { + /* + * `metadata` is typed as a bare `object` by expo-manifests, and is absent + * entirely from embedded manifests, so narrow it ourselves. + */ + const manifest = currentlyRunning?.manifest as + | {metadata?: {channel?: unknown}} + | undefined + const channel = manifest?.metadata?.channel + if (typeof channel === 'string' && channel) { + return channel + } + // The build constant is an empty string rather than null when unconfigured. + return currentlyRunning?.channel || undefined +} + async function setExtraParams() { await setExtraParamAsync( IS_IOS ? 'ios-build-number' : 'android-build-number', @@ -85,12 +112,12 @@ async function updateTestflight() { export function useApplyPullRequestOTAUpdate() { const {currentlyRunning} = useUpdates() const [pending, setPending] = useState(false) - const currentChannel = currentlyRunning?.channel + const currentChannel = getRunningChannel(currentlyRunning) 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 + * applied an update from. Note that the channel is undefined when updates are * disabled (e.g. in dev), in which case there's nothing to restore. */ const isCurrentlyRunningNonStandardChannel = Boolean( @@ -301,7 +328,7 @@ export function useOTAUpdates() { const ranInitialCheck = useRef(false) const timeout = useRef(undefined) const {currentlyRunning, isUpdatePending} = useUpdates() - const currentChannel = currentlyRunning?.channel + const currentChannel = getRunningChannel(currentlyRunning) const setCheckTimeout = useCallback(() => { timeout.current = setTimeout(async () => {