From ad53878c442328d85e9da353d62d46aae0db8892 Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Date: Mon, 23 Oct 2023 00:19:42 -0300 Subject: [PATCH 1/2] [menu-bar] Improve UI feedback when opening a snack project --- .../src/hooks/useDeviceAudioPreferences.ts | 23 ++- apps/menu-bar/src/hooks/useListDevices.ts | 5 +- apps/menu-bar/src/modules/Storage.ts | 2 +- apps/menu-bar/src/popover/BuildsSection.tsx | 77 ++++++++++ apps/menu-bar/src/popover/Core.tsx | 139 ++++++++---------- apps/menu-bar/src/utils/helpers.ts | 9 ++ .../src/run/ios/appleDevice/AppleDevice.ts | 32 ++++ .../client/InstallationProxyClient.ts | 9 +- 8 files changed, 204 insertions(+), 92 deletions(-) create mode 100644 apps/menu-bar/src/popover/BuildsSection.tsx diff --git a/apps/menu-bar/src/hooks/useDeviceAudioPreferences.ts b/apps/menu-bar/src/hooks/useDeviceAudioPreferences.ts index f79581d4..cf7a0fae 100644 --- a/apps/menu-bar/src/hooks/useDeviceAudioPreferences.ts +++ b/apps/menu-bar/src/hooks/useDeviceAudioPreferences.ts @@ -1,26 +1,23 @@ -import { useCallback, useEffect, useState } from 'react'; -import { DeviceEventEmitter } from 'react-native'; +import { useEffect, useState } from 'react'; -import { getUserPreferences } from '../modules/Storage'; +import { getUserPreferences, storage, userPreferencesStorageKey } from '../modules/Storage'; export const useDeviceAudioPreferences = () => { - const [isEmulatorWithoutAudio, setEmulatorWithoutAudio] = useState(); - - const getAudioPreferences = useCallback(async () => { - const { emulatorWithoutAudio } = await getUserPreferences(); - setEmulatorWithoutAudio(emulatorWithoutAudio); - }, []); + const [isEmulatorWithoutAudio, setEmulatorWithoutAudio] = useState( + getUserPreferences().emulatorWithoutAudio + ); useEffect(() => { - const listener = DeviceEventEmitter.addListener('popoverFocused', () => { - getAudioPreferences(); + const listener = storage.addOnValueChangedListener((key) => { + if (key === userPreferencesStorageKey) { + setEmulatorWithoutAudio(getUserPreferences().emulatorWithoutAudio); + } }); - getAudioPreferences(); return () => { listener.remove(); }; - }, [getAudioPreferences]); + }, []); return { emulatorWithoutAudio: isEmulatorWithoutAudio, diff --git a/apps/menu-bar/src/hooks/useListDevices.ts b/apps/menu-bar/src/hooks/useListDevices.ts index d9636982..8b232499 100644 --- a/apps/menu-bar/src/hooks/useListDevices.ts +++ b/apps/menu-bar/src/hooks/useListDevices.ts @@ -7,8 +7,6 @@ import { getUserPreferences } from '../modules/Storage'; import { getSectionsFromDeviceList } from '../utils/device'; export const useListDevices = () => { - const userPreferences = getUserPreferences(); - const [devicesPerPlatform, setDevicesPerPlatform] = useState({ android: { devices: [] }, ios: { devices: [] }, @@ -19,6 +17,7 @@ export const useListDevices = () => { const sections = getSectionsFromDeviceList(devicesPerPlatform); const updateDevicesList = useCallback(async () => { + const userPreferences = getUserPreferences(); setLoading(true); try { const devicesList = await listDevicesAsync({ platform: 'all' }); @@ -45,7 +44,7 @@ export const useListDevices = () => { } finally { setLoading(false); } - }, [userPreferences]); + }, []); useEffect(() => { const listener = DeviceEventEmitter.addListener('popoverFocused', () => { diff --git a/apps/menu-bar/src/modules/Storage.ts b/apps/menu-bar/src/modules/Storage.ts index 869e763c..cba57b1f 100644 --- a/apps/menu-bar/src/modules/Storage.ts +++ b/apps/menu-bar/src/modules/Storage.ts @@ -1,6 +1,6 @@ import { MMKV } from 'react-native-mmkv'; -const userPreferencesStorageKey = 'user-preferences'; +export const userPreferencesStorageKey = 'user-preferences'; export type UserPreferences = { launchOnLogin: boolean; diff --git a/apps/menu-bar/src/popover/BuildsSection.tsx b/apps/menu-bar/src/popover/BuildsSection.tsx new file mode 100644 index 00000000..d5c4355f --- /dev/null +++ b/apps/menu-bar/src/popover/BuildsSection.tsx @@ -0,0 +1,77 @@ +import Item from './Item'; +import SectionHeader from './SectionHeader'; +import Earth02Icon from '../assets/icons/earth-02.svg'; +import File05Icon from '../assets/icons/file-05.svg'; +import { Text, View } from '../components'; +import ProgressIndicator from '../components/ProgressIndicator'; +import FilePicker from '../modules/FilePickerModule'; +import MenuBarModule from '../modules/MenuBarModule'; +import { openProjectsSelectorURL } from '../utils/constants'; +import { MenuBarStatus } from '../utils/helpers'; +import { useExpoTheme } from '../utils/useExpoTheme'; + +export const BUILDS_SECTION_HEIGHT = 88; + +interface Props { + status: MenuBarStatus; + installAppFromURI: (appURI: string) => Promise; + progress: number; +} + +const BuildsSection = ({ status, installAppFromURI, progress }: Props) => { + const theme = useExpoTheme(); + + async function openFilePicker() { + const appPath = await FilePicker.getAppAsync(); + MenuBarModule.openPopover(); + await installAppFromURI(appPath); + } + + function getDescription() { + switch (status) { + case MenuBarStatus.BOOTING_DEVICE: + return 'Initializing device...'; + case MenuBarStatus.DOWNLOADING: + return 'Downloading build...'; + case MenuBarStatus.INSTALLING_APP: + return 'Installing...'; + case MenuBarStatus.INSTALLING_SNACK: + return 'Installing Snack...'; + case MenuBarStatus.OPENING_SNACK_PROJECT: + return 'Opening project in Snack...'; + default: + return ''; + } + } + + return ( + + + + + {status === MenuBarStatus.LISTENING ? ( + <> + + + Select build from EAS… + + + + Select build from local file… + + + ) : ( + + + {getDescription()} + + )} + + ); +}; + +export default BuildsSection; diff --git a/apps/menu-bar/src/popover/Core.tsx b/apps/menu-bar/src/popover/Core.tsx index 3a492b12..40cde28f 100644 --- a/apps/menu-bar/src/popover/Core.tsx +++ b/apps/menu-bar/src/popover/Core.tsx @@ -4,21 +4,18 @@ import { Device } from 'common-types/build/devices'; import React, { memo, useCallback, useState } from 'react'; import { Alert, SectionList } from 'react-native'; +import BuildsSection, { BUILDS_SECTION_HEIGHT } from './BuildsSection'; import DeviceListSectionHeader from './DeviceListSectionHeader'; import { FOOTER_HEIGHT } from './Footer'; -import Item from './Item'; import ProjectsSection, { PROJECTS_SECTION_HEIGHT } from './ProjectsSection'; -import SectionHeader, { SECTION_HEADER_HEIGHT } from './SectionHeader'; +import { SECTION_HEADER_HEIGHT } from './SectionHeader'; import { withApolloProvider } from '../api/ApolloClient'; -import Earth02Icon from '../assets/icons/earth-02.svg'; -import File05Icon from '../assets/icons/file-05.svg'; import { bootDeviceAsync } from '../commands/bootDeviceAsync'; import { downloadBuildAsync } from '../commands/downloadBuildAsync'; import { installAndLaunchAppAsync } from '../commands/installAndLaunchAppAsync'; import { launchSnackAsync } from '../commands/launchSnackAsync'; -import { Spacer, Text, View } from '../components'; +import { Spacer, View } from '../components'; import DeviceItem, { DEVICE_ITEM_HEIGHT } from '../components/DeviceItem'; -import ProgressIndicator from '../components/ProgressIndicator'; import { useDeepLinking } from '../hooks/useDeepLinking'; import { useDeviceAudioPreferences } from '../hooks/useDeviceAudioPreferences'; import { useGetPinnedApps } from '../hooks/useGetPinnedApps'; @@ -26,25 +23,15 @@ import { useListDevices } from '../hooks/useListDevices'; import { usePopoverFocusEffect } from '../hooks/usePopoverFocus'; import { useSafeDisplayDimensions } from '../hooks/useSafeDisplayDimensions'; import { useFileHandler } from '../modules/FileHandlerModule'; -import FilePicker from '../modules/FilePickerModule'; import MenuBarModule from '../modules/MenuBarModule'; import { SelectedDevicesIds, getSelectedDevicesIds, saveSelectedDevicesIds, } from '../modules/Storage'; -import { openProjectsSelectorURL } from '../utils/constants'; import { getDeviceId, getDeviceOS, isVirtualDevice } from '../utils/device'; +import { MenuBarStatus } from '../utils/helpers'; import { getPlatformFromURI } from '../utils/parseUrl'; -import { useExpoTheme } from '../utils/useExpoTheme'; - -enum Status { - LISTENING, - DOWNLOADING, - INSTALLING, -} - -const BUILDS_SECTION_HEIGHT = 88; type Props = { isDevWindow: boolean; @@ -60,12 +47,11 @@ function Core(props: Props) { const showProjectsSection = Boolean(apps?.length); - const [status, setStatus] = useState(Status.LISTENING); + const [status, setStatus] = useState(MenuBarStatus.LISTENING); const [progress, setProgress] = useState(0); const { devicesPerPlatform, numberOfDevices, sections, refetch } = useListDevices(); const { emulatorWithoutAudio } = useDeviceAudioPreferences(); - const theme = useExpoTheme(); // TODO: Extract into a hook const displayDimensions = useSafeDisplayDimensions(); @@ -82,18 +68,43 @@ function Core(props: Props) { ? heightOfAllDevices : estimatedAvailableSizeForDevices; - const getFirstAvailableDevice = useCallback( - (_?: boolean) => { - return ( - devicesPerPlatform.ios.devices.find((d) => getDeviceId(d) === selectedDevicesIds.ios) ?? - devicesPerPlatform.android.devices.find( - (d) => getDeviceId(d) === selectedDevicesIds.android - ) ?? - devicesPerPlatform.ios.devices?.find((d) => isVirtualDevice(d) && d.state === 'Booted') - ); - }, - [devicesPerPlatform, selectedDevicesIds] - ); + const getAvailableDeviceForSnack = useCallback(() => { + const selectedIosDevice = devicesPerPlatform.ios.devices.find( + (d) => getDeviceId(d) === selectedDevicesIds.ios && isVirtualDevice(d) + ); + const selectedAndroidDevice = devicesPerPlatform.android.devices.find( + (d) => getDeviceId(d) === selectedDevicesIds.android + ); + + if (selectedIosDevice || selectedAndroidDevice) { + return selectedIosDevice ?? selectedAndroidDevice; + } + + const bootedIosDevice = devicesPerPlatform.ios.devices?.find( + (d) => isVirtualDevice(d) && d.state === 'Booted' + ); + const bootedAndroidDevice = devicesPerPlatform.android.devices?.find( + (d) => isVirtualDevice(d) && d.state === 'Booted' + ); + + const fistDeviceAvailable = + devicesPerPlatform.ios.devices.find((d) => isVirtualDevice(d)) ?? + devicesPerPlatform.android.devices?.[0]; + + const device = bootedIosDevice ?? bootedAndroidDevice ?? fistDeviceAvailable; + + if (!device) { + Alert.alert("You don't have any device available to run Snack. Please check your setup."); + return; + } + + setSelectedDevicesIds((prev) => { + const platform = getDeviceOS(device); + return { ...prev, [platform]: getDeviceId(device) }; + }); + + return device; + }, [devicesPerPlatform, selectedDevicesIds]); const ensureDeviceIsRunning = useCallback( async (device: Device) => { @@ -114,19 +125,29 @@ function Core(props: Props) { // @TODO: create another hook const handleSnackUrl = useCallback( async (url: string) => { - const device = getFirstAvailableDevice(); + const device = getAvailableDeviceForSnack(); if (!device) { return; } - ensureDeviceIsRunning(device); - await launchSnackAsync({ - url, - deviceId: getDeviceId(device), - platform: getDeviceOS(device), - }); + try { + setStatus(MenuBarStatus.BOOTING_DEVICE); + await ensureDeviceIsRunning(device); + setStatus(MenuBarStatus.OPENING_SNACK_PROJECT); + await launchSnackAsync({ + url, + deviceId: getDeviceId(device), + platform: getDeviceOS(device), + }); + } catch (error) { + console.log(`error: ${JSON.stringify(error)}`); + } finally { + setTimeout(() => { + setStatus(MenuBarStatus.LISTENING); + }, 2000); + } }, - [ensureDeviceIsRunning, getFirstAvailableDevice] + [ensureDeviceIsRunning, getAvailableDeviceForSnack] ); const getDeviceByPlatform = useCallback( @@ -154,15 +175,16 @@ function Core(props: Props) { } if (!localFilePath) { - setStatus(Status.DOWNLOADING); + setStatus(MenuBarStatus.DOWNLOADING); const buildPath = await downloadBuildAsync(appURI, setProgress); localFilePath = buildPath; } - setStatus(Status.INSTALLING); + setStatus(MenuBarStatus.BOOTING_DEVICE); await ensureDeviceIsRunning(device); const deviceId = getDeviceId(device); try { + setStatus(MenuBarStatus.INSTALLING_APP); await installAndLaunchAppAsync({ appPath: localFilePath, deviceId }); } catch (error) { if (error instanceof InternalError) { @@ -199,19 +221,13 @@ function Core(props: Props) { } } finally { setTimeout(() => { - setStatus(Status.LISTENING); + setStatus(MenuBarStatus.LISTENING); }, 2000); } }, [ensureDeviceIsRunning, getDeviceByPlatform] ); - const openFilePicker = async () => { - const appPath = await FilePicker.getAppAsync(); - MenuBarModule.openPopover(); - await installAppFromURI(appPath); - }; - useFileHandler({ onOpenFile: installAppFromURI }); useDeepLinking( @@ -249,32 +265,7 @@ function Core(props: Props) { return ( - - - - - {status === Status.LISTENING ? ( - <> - - - Select build from EAS… - - - - Select build from local file… - - - ) : status === Status.DOWNLOADING || status === Status.INSTALLING ? ( - - - {status === Status.DOWNLOADING ? 'Downloading build...' : 'Installing...'} - - ) : null} - + {apps?.length ? : null} { + const clientManager = await ClientManager.create(udid); + const client = await clientManager.getUsbmuxdClient(); + client.connect(clientManager.device, 62078); + + try { + await mountDeveloperDiskImage(clientManager); + const installer = await clientManager.getInstallationProxyClient(); + + const { [bundleId]: appInfo } = await installer.lookupApp([bundleId]); + + await launchApp(clientManager, { + appInfo, + bundleId, + detach: false, + }); + + return appInfo; + } catch (error) { + } finally { + clientManager.end(); + } + return undefined; +} diff --git a/packages/eas-shared/src/run/ios/appleDevice/client/InstallationProxyClient.ts b/packages/eas-shared/src/run/ios/appleDevice/client/InstallationProxyClient.ts index 29d63fd4..b3a66225 100644 --- a/packages/eas-shared/src/run/ios/appleDevice/client/InstallationProxyClient.ts +++ b/packages/eas-shared/src/run/ios/appleDevice/client/InstallationProxyClient.ts @@ -98,6 +98,7 @@ export interface IPLookupResult { Container: string; CFBundleIdentifier: string; CFBundleExecutable: string; + CFBundleShortVersionString: string; Path: string; }; } @@ -128,7 +129,13 @@ export class InstallationProxyClient extends ServiceClient Date: Mon, 23 Oct 2023 00:23:34 -0300 Subject: [PATCH 2/2] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed8c488..acc87a51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Add ability to show/hide different types of simulators, and add experimental TV support. ([#77](https://github.com/expo/orbit/pull/77) by [@douglowder](https://github.com/douglowder), [#84](https://github.com/expo/orbit/pull/84) by [@gabrieldonadel](https://github.com/gabrieldonadel)) - Add support for opening tarballs with multiple apps. ([#73](https://github.com/expo/orbit/pull/73) by [@gabrieldonadel](https://github.com/gabrieldonadel)) - Improve feedback to the user when an error occurs. ([#64](https://github.com/expo/orbit/pull/64) by [@gabrieldonadel](https://github.com/gabrieldonadel)) +- Improve UI feedback when opening a snack project. ([#88](https://github.com/expo/orbit/pull/88) by [@gabrieldonadel](https://github.com/gabrieldonadel)) - Added drag and drop support for installing apps. ([#57](https://github.com/expo/orbit/pull/57) by [@gabrieldonadel](https://github.com/gabrieldonadel)) - Added support for installing apps directly from Finder. ([#56](https://github.com/expo/orbit/pull/56) by [@gabrieldonadel](https://github.com/gabrieldonadel)) - Added local HTTP server to circumvent deep-link limitations. ([#52](https://github.com/expo/orbit/pull/52), [#53](https://github.com/expo/orbit/pull/53), [#54](https://github.com/expo/orbit/pull/54), [#55](https://github.com/expo/orbit/pull/55) by [@gabrieldonadel](https://github.com/gabrieldonadel))