Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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