From 83f369dac1401446dd24fa271e92553f53f83959 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 10 Mar 2026 09:58:49 +0100 Subject: [PATCH 1/5] [menu-bar] Show minimal popover with only progress bar when opened via deep link When the app is opened via a browser deep link (expo-orbit://), only show the builds/progress section instead of the full device list and footer. The full menu bar is shown when the user clicks the menu bar icon directly. Co-Authored-By: Claude Opus 4.6 --- .../macos/ExpoMenuBar-macOS/AppDelegate.swift | 4 + apps/menu-bar/src/popover/Core.tsx | 116 +++++++++++++----- apps/menu-bar/src/popover/index.tsx | 11 +- 3 files changed, 94 insertions(+), 37 deletions(-) diff --git a/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.swift b/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.swift index 6f28b932..e40731ab 100644 --- a/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.swift +++ b/apps/menu-bar/macos/ExpoMenuBar-macOS/AppDelegate.swift @@ -82,6 +82,10 @@ class AppDelegate: RCTAppDelegate, NSUserNotificationCenterDelegate { } @objc func getUrlEventHandler(_ event: NSAppleEventDescriptor, withReplyEvent replyEvent: NSAppleEventDescriptor) { + // Emit deepLinkOpened before opening popover so React sees the flag + // before the popoverFocused event arrives. + bridge?.enqueueJSCall( + "RCTDeviceEventEmitter.emit", args: ["deepLinkOpened", [:]]) popoverManager.openPopover() RCTLinkingManager.getUrlEventHandler(event, withReplyEvent: replyEvent) } diff --git a/apps/menu-bar/src/popover/Core.tsx b/apps/menu-bar/src/popover/Core.tsx index a76c1960..d284fd19 100644 --- a/apps/menu-bar/src/popover/Core.tsx +++ b/apps/menu-bar/src/popover/Core.tsx @@ -1,7 +1,7 @@ import { InternalError } from 'common-types'; import { MultipleAppsInTarballErrorDetails } from 'common-types/build/InternalError'; import { Device } from 'common-types/build/devices'; -import React, { memo, useCallback, useState } from 'react'; +import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; import { SectionList } from 'react-native'; import BuildsSection, { BUILDS_SECTION_HEIGHT } from './BuildsSection'; @@ -22,9 +22,11 @@ import { launchUpdateAsync } from '../commands/launchUpdateAsync'; import { Spacer, View } from '../components'; import DeviceItem, { DEVICE_ITEM_HEIGHT } from '../components/DeviceItem'; import { useDeepLinking } from '../hooks/useDeepLinking'; +import { Linking } from '../modules/Linking'; import { useDeviceAudioPreferences } from '../hooks/useDeviceAudioPreferences'; import { useGetPinnedApps } from '../hooks/useGetPinnedApps'; import { usePopoverFocusEffect } from '../hooks/usePopoverFocus'; +import { DeviceEventEmitter } from '../modules/DeviceEventEmitter'; import { useSafeDisplayDimensions } from '../hooks/useSafeDisplayDimensions'; import Alert from '../modules/Alert'; import MenuBarModule from '../modules/MenuBarModule'; @@ -48,6 +50,7 @@ import { WindowsNavigator } from '../windows'; type Props = { isDevWindow: boolean; + onDeepLinkModeChange?: (isDeepLinkMode: boolean) => void; }; function Core(props: Props) { @@ -96,6 +99,47 @@ function Core(props: Props) { [setTasks] ); + // Track whether the popover was opened via a deep link (browser) or user click. + // In deep link mode, only the progress UI is shown. + const [isDeepLinkMode, setIsDeepLinkMode] = useState(false); + const deepLinkPending = useRef(false); + + // Handle cold start: if app was launched via URL scheme, React wasn't mounted + // when deepLinkOpened fired, so check the initial URL instead. + useEffect(() => { + Linking.getInitialURL().then((url) => { + if (url) { + setIsDeepLinkMode(true); + } + }); + }, []); + + useEffect(() => { + const listener = DeviceEventEmitter.addListener('deepLinkOpened', () => { + deepLinkPending.current = true; + }); + return () => listener.remove(); + }, []); + + // When popover is focused by user click, exit deep link mode. + // Deep link opens also trigger popoverFocused, but deepLinkPending + // distinguishes them: the native side emits deepLinkOpened before + // popoverFocused, so we check and consume the flag here. + usePopoverFocusEffect( + useCallback(() => { + if (deepLinkPending.current) { + deepLinkPending.current = false; + setIsDeepLinkMode(true); + } else { + setIsDeepLinkMode(false); + } + }, []) + ); + + useEffect(() => { + props.onDeepLinkModeChange?.(isDeepLinkMode); + }, [isDeepLinkMode, props.onDeepLinkModeChange]); + const { devicesPerPlatform, numberOfDevices, @@ -543,40 +587,44 @@ function Core(props: Props) { return ( - - - {devicesError ? ( - - ) : ( - ( - + {!isDeepLinkMode && ( + <> + + + {devicesError ? ( + + ) : ( + ( + + )} + renderItem={({ item: device }: { item: Device }) => { + const platform = getDeviceOS(device); + const id = getDeviceId(device); + return ( + onSelectDevice(device)} + onPressLaunch={async () => { + Analytics.track(Event.LAUNCH_SIMULATOR); + await bootDeviceAsync({ platform, id }); + refetch(); + }} + selected={selectedDevicesIds[platform] === id} + /> + ); + }} + /> )} - renderItem={({ item: device }: { item: Device }) => { - const platform = getDeviceOS(device); - const id = getDeviceId(device); - return ( - onSelectDevice(device)} - onPressLaunch={async () => { - Analytics.track(Event.LAUNCH_SIMULATOR); - await bootDeviceAsync({ platform, id }); - refetch(); - }} - selected={selectedDevicesIds[platform] === id} - /> - ); - }} - /> - )} - + + + )} ); } diff --git a/apps/menu-bar/src/popover/index.tsx b/apps/menu-bar/src/popover/index.tsx index 7e61d916..d618ec43 100644 --- a/apps/menu-bar/src/popover/index.tsx +++ b/apps/menu-bar/src/popover/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import Core from './Core'; import { ErrorBoundary, FallbackProps } from './ErrorBoundary'; @@ -18,6 +18,11 @@ type Props = { function Popover(props: Props) { const { height } = useSafeDisplayDimensions(); const { hasInitialized } = useListDevices(); + const [isDeepLinkMode, setIsDeepLinkMode] = useState(false); + + const onDeepLinkModeChange = useCallback((mode: boolean) => { + setIsDeepLinkMode(mode); + }, []); useEffect(() => { const hasSeenOnboarding = storage.getBoolean(hasSeenOnboardingStorageKey); @@ -46,9 +51,9 @@ function Popover(props: Props) { maxHeight: height, }}> - + -