Skip to content

Commit

Permalink
[menu-bar] Add support for launching updates
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrieldonadel committed Jan 16, 2024
1 parent 6a7bbcb commit 9d91220
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 39 deletions.
15 changes: 15 additions & 0 deletions apps/menu-bar/src/commands/launchUpdateAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import MenuBarModule from '../modules/MenuBarModule';

type LaunchUpdateAsyncOptions = {
platform: 'android' | 'ios';
deviceId: string;
url: string;
};

export const launchUpdateAsync = async ({ url, platform, deviceId }: LaunchUpdateAsyncOptions) => {
await MenuBarModule.runCli(
'launch-update',
[url, '-p', platform, '--device-id', deviceId],
console.log
);
};
135 changes: 96 additions & 39 deletions apps/menu-bar/src/popover/Core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { bootDeviceAsync } from '../commands/bootDeviceAsync';
import { downloadBuildAsync } from '../commands/downloadBuildAsync';
import { installAndLaunchAppAsync } from '../commands/installAndLaunchAppAsync';
import { launchSnackAsync } from '../commands/launchSnackAsync';
import { launchUpdateAsync } from '../commands/launchUpdateAsync';
import { Spacer, View } from '../components';
import DeviceItem, { DEVICE_ITEM_HEIGHT } from '../components/DeviceItem';
import { useDeepLinking } from '../hooks/useDeepLinking';
Expand All @@ -32,7 +33,12 @@ import {
import { useListDevices } from '../providers/DevicesProvider';
import { getDeviceId, getDeviceOS, isVirtualDevice } from '../utils/device';
import { MenuBarStatus } from '../utils/helpers';
import { getPlatformFromURI, handleAuthUrl } from '../utils/parseUrl';
import {
URLType,
getPlatformFromURI,
handleAuthUrl,
identifyAndParseDeeplinkURL,
} from '../utils/parseUrl';

type Props = {
isDevWindow: boolean;
Expand Down Expand Up @@ -125,19 +131,60 @@ function Core(props: Props) {
[emulatorWithoutAudio]
);

// @TODO: create another hook
const handleSnackUrl = useCallback(
const getDeviceByPlatform = useCallback(
(platform: 'android' | 'ios') => {
const selectedDevicesId = selectedDevicesIds[platform];
if (selectedDevicesId && devicesPerPlatform[platform].devices.has(selectedDevicesId)) {
return devicesPerPlatform[platform].devices.get(selectedDevicesId);
}

const devices = devicesPerPlatform[platform].devices.values();

for (const device of devices) {
if (isVirtualDevice(device) && device.state === 'Booted') {
setSelectedDevicesIds((prev) => ({ ...prev, [platform]: getDeviceId(device) }));
return device;
}
}

const [firstDevice] = devicesPerPlatform[platform].devices.values();
if (!firstDevice) {
return;
}

setSelectedDevicesIds((prev) => ({ ...prev, [platform]: getDeviceId(firstDevice) }));
return firstDevice;
},
[devicesPerPlatform, selectedDevicesIds]
);

const handleUpdateUrl = useCallback(
async (url: string) => {
const device = getAvailableDeviceForSnack();
/**
* Supports any update manifest url as long as the
* platform is specified in the query params.
*/
const platform = new URL(url).searchParams.get('platform');
if (platform !== 'android' && platform !== 'ios') {
Alert.alert(
`Update URLs must include the "platform" query parameter with a value of either 'android' or 'ios'.`
);
return;
}

const device = getDeviceByPlatform(platform);
if (!device) {
Alert.alert(
`You don't have any ${platform} devices available to open this update, please make your environment is configured correctly and try again.`
);
return;
}

try {
setStatus(MenuBarStatus.BOOTING_DEVICE);
await ensureDeviceIsRunning(device);
setStatus(MenuBarStatus.OPENING_SNACK_PROJECT);
await launchSnackAsync({
setStatus(MenuBarStatus.OPENING_UPDATE);
await launchUpdateAsync({
url,
deviceId: getDeviceId(device),
platform: getDeviceOS(device),
Expand All @@ -153,34 +200,37 @@ function Core(props: Props) {
}, 2000);
}
},
[ensureDeviceIsRunning, getAvailableDeviceForSnack]
[ensureDeviceIsRunning, getDeviceByPlatform]
);

const getDeviceByPlatform = useCallback(
(platform: 'android' | 'ios') => {
const selectedDevicesId = selectedDevicesIds[platform];
if (selectedDevicesId && devicesPerPlatform[platform].devices.has(selectedDevicesId)) {
return devicesPerPlatform[platform].devices.get(selectedDevicesId);
const handleSnackUrl = useCallback(
async (url: string) => {
const device = getAvailableDeviceForSnack();
if (!device) {
return;
}

const devices = devicesPerPlatform[platform].devices.values();

for (const device of devices) {
if (isVirtualDevice(device) && device.state === 'Booted') {
setSelectedDevicesIds((prev) => ({ ...prev, [platform]: getDeviceId(device) }));
return 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) {
if (error instanceof InternalError) {
Alert.alert('Something went wrong', error.message);
}
console.log(`error: ${JSON.stringify(error)}`);
} finally {
setTimeout(() => {
setStatus(MenuBarStatus.LISTENING);
}, 2000);
}

const [firstDevice] = devicesPerPlatform[platform].devices.values();
if (!firstDevice) {
return;
}

setSelectedDevicesIds((prev) => ({ ...prev, [platform]: getDeviceId(firstDevice) }));
return firstDevice;
},
[devicesPerPlatform, selectedDevicesIds]
[ensureDeviceIsRunning, getAvailableDeviceForSnack]
);

const installAppFromURI = useCallback(
Expand Down Expand Up @@ -260,22 +310,29 @@ function Core(props: Props) {

useDeepLinking(
useCallback(
({ url }) => {
({ url: deeplinkUrl }) => {
if (!props.isDevWindow) {
const urlWithoutProtocol = url.substring(url.indexOf('://') + 3);
const isSnackUrl = url.includes('exp.host/');
const isAuthUrl = urlWithoutProtocol.startsWith('auth?');

if (isAuthUrl) {
handleAuthUrl(url);
} else if (isSnackUrl) {
handleSnackUrl(`exp://${urlWithoutProtocol}`);
} else {
installAppFromURI(`https://${urlWithoutProtocol}`);
const { urlType, url } = identifyAndParseDeeplinkURL(deeplinkUrl);

switch (urlType) {
case URLType.AUTH:
handleAuthUrl(url);
break;
case URLType.SNACK:
handleSnackUrl(url);
break;
case URLType.EXPO_UPDATE:
handleUpdateUrl(url);
break;
case URLType.EXPO_BUILD:
case URLType.UNKNOWN:
default:
installAppFromURI(url);
break;
}
}
},
[props.isDevWindow, installAppFromURI, handleSnackUrl]
[props.isDevWindow, handleSnackUrl, handleUpdateUrl, installAppFromURI]
)
);

Expand Down
1 change: 1 addition & 0 deletions apps/menu-bar/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ export enum MenuBarStatus {
INSTALLING_APP,
INSTALLING_SNACK,
OPENING_SNACK_PROJECT,
OPENING_UPDATE,
}
33 changes: 33 additions & 0 deletions apps/menu-bar/src/utils/parseUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,36 @@ export function handleAuthUrl(url: string) {

saveSessionSecret(sessionSecret);
}

export function identifyAndParseDeeplinkURL(deeplinkURL: string): {
urlType: URLType;
url: string;
} {
const urlWithoutProtocol = deeplinkURL.replace(/^[^:]+:\/\//, '');

if (urlWithoutProtocol.startsWith('auth?')) {
return { urlType: URLType.AUTH, url: deeplinkURL };
}
if (urlWithoutProtocol.startsWith('update/')) {
return {
urlType: URLType.EXPO_UPDATE,
url: `https://${urlWithoutProtocol.replace('update/', '')}`,
};
}
if (urlWithoutProtocol.startsWith('expo.dev/artifacts')) {
return { urlType: URLType.EXPO_BUILD, url: `https://${urlWithoutProtocol}` };
}
if (urlWithoutProtocol.includes('exp.host/')) {
return { urlType: URLType.SNACK, url: `exp://${urlWithoutProtocol}` };
}

return { urlType: URLType.UNKNOWN, url: `https://${urlWithoutProtocol}` };
}

export enum URLType {
AUTH,
EXPO_UPDATE,
EXPO_BUILD,
SNACK,
UNKNOWN,
}

0 comments on commit 9d91220

Please sign in to comment.