Skip to content

Commit

Permalink
[menu-bar][cli] Add support for launching updates with locally instal…
Browse files Browse the repository at this point in the history
…led apps (#188)

* [menu-bar][cli] Add support for launching updates with locally installed apps

* Allow users to force open update

* Add changelog entry

* Fix MenuBar status

* Apply suggestions from code review

Co-authored-by: Brent Vatne <brentvatne@gmail.com>

---------

Co-authored-by: Brent Vatne <brentvatne@gmail.com>
  • Loading branch information
gabrieldonadel and brentvatne committed Mar 6, 2024
1 parent 672dc72 commit 957eaed
Show file tree
Hide file tree
Showing 10 changed files with 382 additions and 64 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -6,7 +6,7 @@

### 🎉 New features

- Add support for launching Expo updates. ([#134](https://github.com/expo/orbit/pull/134), [#137](https://github.com/expo/orbit/pull/137), [#138](https://github.com/expo/orbit/pull/138), [#144](https://github.com/expo/orbit/pull/144), [#148](https://github.com/expo/orbit/pull/148) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Add support for launching Expo updates. ([#134](https://github.com/expo/orbit/pull/134), [#137](https://github.com/expo/orbit/pull/137), [#138](https://github.com/expo/orbit/pull/138), [#144](https://github.com/expo/orbit/pull/144), [#148](https://github.com/expo/orbit/pull/148), [#188](https://github.com/expo/orbit/pull/188) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Add experimental support for Windows and Linux. ([#152](https://github.com/expo/orbit/pull/152), [#157](https://github.com/expo/orbit/pull/157), [#158](https://github.com/expo/orbit/pull/158), [#160](https://github.com/expo/orbit/pull/160), [#161](https://github.com/expo/orbit/pull/161), [#165](https://github.com/expo/orbit/pull/165), [#170](https://github.com/expo/orbit/pull/170), [#171](https://github.com/expo/orbit/pull/171), [#172](https://github.com/expo/orbit/pull/172), [#173](https://github.com/expo/orbit/pull/173), [#174](https://github.com/expo/orbit/pull/174), [#175](https://github.com/expo/orbit/pull/175), [#177](https://github.com/expo/orbit/pull/177), [#178](https://github.com/expo/orbit/pull/178), [#180](https://github.com/expo/orbit/pull/180), [#181](https://github.com/expo/orbit/pull/181), [#182](https://github.com/expo/orbit/pull/182), [#185](https://github.com/expo/orbit/pull/185) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Migrate Sparkle to Expo Modules. ([#184](https://github.com/expo/orbit/pull/184) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Automatically handle installing incompatible Android app updates. ([#187](https://github.com/expo/orbit/pull/187) by [@gabrieldonadel](https://github.com/gabrieldonadel))
Expand Down
190 changes: 150 additions & 40 deletions apps/cli/src/commands/LaunchUpdate.ts
Expand Up @@ -4,45 +4,80 @@ import { graphqlSdk } from '../api/GraphqlClient';
import { AppPlatform, DistributionType } from '../graphql/generated/graphql';
import { downloadBuildAsync } from './DownloadBuild';
import { InternalError } from 'common-types';
import { ClientError } from 'graphql-request';

type launchUpdateAsyncOptions = {
platform: 'android' | 'ios';
deviceId: string;
skipInstall: boolean;
};

export async function launchUpdateAsync(
updateURL: string,
{ platform, deviceId }: launchUpdateAsyncOptions
{ platform, deviceId, skipInstall }: launchUpdateAsyncOptions
) {
const { manifest } = await ManifestUtils.getManifestAsync(updateURL);
const appId = manifest.extra?.eas?.projectId;
if (!appId) {
throw new Error("Couldn't find EAS projectId in manifest");
}
let appIdentifier: string | undefined;

/**
* Fetch EAS to check if the app uses expo-dev-client
* or if we should launch the update using Expo Go
*/
const { app } = await graphqlSdk.getAppHasDevClientBuilds({ appId });
const hasDevClientBuilds = Boolean(app.byId.hasDevClientBuilds.edges.length);
const isRuntimeCompatibleWithExpoGo = manifest.runtimeVersion.startsWith('exposdk:');

if (!hasDevClientBuilds && isRuntimeCompatibleWithExpoGo) {
const sdkVersion = manifest.runtimeVersion.match(/exposdk:(\d+\.\d+\.\d+)/)?.[1] || '';
const launchOnExpoGo =
platform === 'android' ? launchUpdateOnExpoGoAndroidAsync : launchUpdateOnExpoGoIosAsync;
return await launchOnExpoGo({
sdkVersion,
url: getExpoGoUpdateDeeplink(updateURL, manifest),
deviceId,
});
if (skipInstall) {
if (platform === 'android') {
const device = await Emulator.getRunningDeviceAsync(deviceId);
await Emulator.openURLAsync({ url: getUpdateDeeplink(updateURL, manifest), pid: device.pid });
} else {
await Simulator.openURLAsync({
url: getUpdateDeeplink(updateURL, manifest),
udid: deviceId,
});
}
return;
}

try {
/**
* Fetch EAS to check if the app uses expo-dev-client
* or if we should launch the update using Expo Go
*/
const { app } = await graphqlSdk.getAppHasDevClientBuilds({ appId });
const hasDevClientBuilds = Boolean(app.byId.hasDevClientBuilds.edges.length);
const isRuntimeCompatibleWithExpoGo = manifest.runtimeVersion.startsWith('exposdk:');

if (!hasDevClientBuilds && isRuntimeCompatibleWithExpoGo) {
const sdkVersion = manifest.runtimeVersion.match(/exposdk:(\d+\.\d+\.\d+)/)?.[1] || '';
const launchOnExpoGo =
platform === 'android' ? launchUpdateOnExpoGoAndroidAsync : launchUpdateOnExpoGoIosAsync;
return await launchOnExpoGo({
sdkVersion,
url: getExpoGoUpdateDeeplink(updateURL, manifest),
deviceId,
});
}

if (
app.byId.hasDevClientBuilds.edges[0]?.node.__typename === 'Build' &&
app.byId.hasDevClientBuilds.edges[0]?.node?.appIdentifier
) {
appIdentifier = app.byId.hasDevClientBuilds.edges[0]?.node?.appIdentifier;
}
} catch (error) {
if (error instanceof ClientError) {
if (error.message.includes('Entity not authorized')) {
throw new InternalError(
'UNAUTHORIZED_ACCOUNT',
`Make sure the logged in account has access to project ${appId}`
);
}
}
throw error;
}

if (platform === 'android') {
await launchUpdateOnAndroidAsync(updateURL, manifest, deviceId);
await launchUpdateOnAndroidAsync(updateURL, manifest, deviceId, appIdentifier);
} else {
await launchUpdateOnIOSAsync(updateURL, manifest, deviceId);
await launchUpdateOnIOSAsync(updateURL, manifest, deviceId, appIdentifier);
}
}

Expand Down Expand Up @@ -80,19 +115,30 @@ async function launchUpdateOnExpoGoIosAsync({
});
}

async function launchUpdateOnAndroidAsync(updateURL: string, manifest: Manifest, deviceId: string) {
async function launchUpdateOnAndroidAsync(
updateURL: string,
manifest: Manifest,
deviceId: string,
appIdentifier?: string
) {
const device = await Emulator.getRunningDeviceAsync(deviceId);

await downloadAndInstallLatestDevBuildAsync({
deviceId,
manifest,
platform: AppPlatform.Android,
distribution: DistributionType.Internal,
appIdentifier,
});
await Emulator.openURLAsync({ url: getUpdateDeeplink(updateURL, manifest), pid: device.pid });
}

async function launchUpdateOnIOSAsync(updateURL: string, manifest: Manifest, deviceId: string) {
async function launchUpdateOnIOSAsync(
updateURL: string,
manifest: Manifest,
deviceId: string,
appIdentifier?: string
) {
const isSimulator = await Simulator.isSimulatorAsync(deviceId);
if (!isSimulator) {
throw new Error('Launching updates on iOS physical is not supported yet');
Expand All @@ -103,6 +149,7 @@ async function launchUpdateOnIOSAsync(updateURL: string, manifest: Manifest, dev
manifest,
platform: AppPlatform.Ios,
distribution: DistributionType.Simulator,
appIdentifier,
});

await Simulator.openURLAsync({
Expand Down Expand Up @@ -140,55 +187,118 @@ async function downloadAndInstallLatestDevBuildAsync({
manifest,
platform,
distribution,
appIdentifier,
}: {
deviceId: string;
manifest: Manifest;
platform: AppPlatform;
distribution: DistributionType;
appIdentifier?: string;
}) {
const buildArtifactsURL = await getBuildArtifactsURLForUpdateAsync({
const build = await getBuildArtifactsForUpdateAsync({
manifest,
platform,
distribution,
});
const buildLocalPath = await downloadBuildAsync(buildArtifactsURL);

if (build && build.url && !build.expired) {
const buildLocalPath = await downloadBuildAsync(build.url);

if (platform === AppPlatform.Ios) {
await Simulator.installAppAsync(deviceId, buildLocalPath);
} else {
const device = await Emulator.getRunningDeviceAsync(deviceId);
await Emulator.installAppAsync(device, buildLocalPath);
}
return;
}

// EAS Build not available, check if the app is installed locally
const bundleId = build?.appIdentifier ?? appIdentifier;
if (!bundleId) {
throw new NoDevBuildsError(manifest.extra?.expoClient?.name, manifest.runtimeVersion);
}

if (platform === AppPlatform.Ios) {
await Simulator.installAppAsync(deviceId, buildLocalPath);
const isInstalled = await Simulator.checkIfAppIsInstalled({
udid: deviceId,
bundleId,
});

// check if app is compatible with the current runtime
if (isInstalled) {
const supportsUpdate = await Simulator.checkIfAppSupportsLaunchingUpdate({
udid: deviceId,
bundleIdentifier: bundleId,
runtimeVersion: manifest.runtimeVersion,
});

if (supportsUpdate) {
// App is already installed and compatible with the update runtime
return;
}
}

throw new NoDevBuildsError(manifest.extra?.expoClient?.name, manifest.runtimeVersion);
} else {
const device = await Emulator.getRunningDeviceAsync(deviceId);
await Emulator.installAppAsync(device, buildLocalPath);
const isInstalled = await Emulator.checkIfAppIsInstalled({
pid: device.pid,
bundleId,
});

if (isInstalled) {
const supportsUpdate = await Emulator.checkIfAppSupportsLaunchingUpdate({
pid: device.pid,
bundleId,
runtimeVersion: manifest.runtimeVersion,
});

if (supportsUpdate) {
// App is already installed and compatible with the update runtime
return;
}
}

throw new NoDevBuildsError(manifest.extra?.expoClient?.name, manifest.runtimeVersion);
}
}

async function getBuildArtifactsURLForUpdateAsync({
async function getBuildArtifactsForUpdateAsync({
manifest,
platform,
distribution,
}: {
manifest: Manifest;
platform: AppPlatform;
distribution: DistributionType;
}): Promise<string> {
}) {
const { app } = await graphqlSdk.getAppBuildForUpdate({
// TODO(gabrieldonadel): Add runtimeVersion filter
runtimeVersion: manifest.runtimeVersion,
appId: manifest.extra?.eas?.projectId ?? '',
platform,
distribution,
});

const build = app?.byId?.buildsPaginated?.edges?.[0]?.node;
if (
build?.__typename === 'Build' &&
build?.expirationDate &&
Date.parse(build.expirationDate) > Date.now() &&
build.artifacts?.buildUrl
) {
return build.artifacts.buildUrl;
if (build?.__typename === 'Build' && build.artifacts?.buildUrl) {
return {
url: build.artifacts.buildUrl,
appIdentifier: build.appIdentifier,
expired: Date.now() > Date.parse(build.expirationDate),
};
}

throw new InternalError(
'NO_DEVELOPMENT_BUILDS_AVAILABLE',
`No Development Builds available for ${manifest.extra?.expoClient?.name} on EAS. Please generate a new Development Build`
);
return null;
}

class NoDevBuildsError extends InternalError {
constructor(projectName?: string, runtimeVersion?: string) {
super(
'NO_DEVELOPMENT_BUILDS_AVAILABLE',
`No Development Builds available for ${
projectName ?? 'your project '
} (Runtime version ${runtimeVersion}) on EAS.`
);
}
}
13 changes: 12 additions & 1 deletion apps/cli/src/graphql/builds.gql
Expand Up @@ -2,20 +2,27 @@ query getAppBuildForUpdate(
$appId: String!
$platform: AppPlatform!
$distribution: DistributionType!
$runtimeVersion: String
) {
app {
byId(appId: $appId) {
id
name
buildsPaginated(
first: 1
filter: { platforms: [$platform], distributions: [$distribution], developmentClient: true }
filter: {
platforms: [$platform]
distributions: [$distribution]
developmentClient: true
runtimeVersion: $runtimeVersion
}
) {
edges {
node {
__typename
id
... on Build {
appIdentifier
runtimeVersion
expirationDate
artifacts {
Expand All @@ -37,7 +44,11 @@ query getAppHasDevClientBuilds($appId: String!) {
hasDevClientBuilds: buildsPaginated(first: 1, filter: { developmentClient: true }) {
edges {
node {
__typename
id
... on Build {
appIdentifier
}
}
}
}
Expand Down

0 comments on commit 957eaed

Please sign in to comment.