Skip to content

Commit 62a70ff

Browse files
authored
Add an option to test deeplinks with app terminated (#1035)
This PR adds an option to terminate an app before sending deep link to the device. ### How Has This Been Tested: - run `expo-52-prebuild-with-plugins` test app - test the termination flow with `myapp://` deeplink - test flow without termination (ass app is opened nothing should happen) https://github.com/user-attachments/assets/b6925895-4d71-4c3f-b9c7-43a74608aee4
1 parent b91d67f commit 62a70ff

File tree

8 files changed

+71
-22
lines changed

8 files changed

+71
-22
lines changed

Diff for: packages/vscode-extension/src/common/Project.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export interface ProjectInterface {
175175
resetAppPermissions(permissionType: AppPermissionType): Promise<void>;
176176

177177
getDeepLinksHistory(): Promise<string[]>;
178-
openDeepLink(link: string): Promise<void>;
178+
openDeepLink(link: string, terminateApp: boolean): Promise<void>;
179179

180180
startRecording(): void;
181181
captureAndStopRecording(): void;

Diff for: packages/vscode-extension/src/devices/AndroidEmulatorDevice.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ export class AndroidEmulatorDevice extends DeviceBase {
444444
}
445445
// terminate the app before launching, otherwise launch commands won't actually start the process which
446446
// may be in a bad state
447-
await exec(ADB_PATH, ["-s", this.serial!, "shell", "am", "force-stop", build.packageName]);
447+
this.terminateApp(build.packageName);
448448

449449
this.mirrorNativeLogs(build);
450450

@@ -540,6 +540,10 @@ export class AndroidEmulatorDevice extends DeviceBase {
540540
return true; // Android will terminate the process if any of the permissions were granted prior to reset-permissions call
541541
}
542542

543+
async terminateApp(packageName: string) {
544+
await exec(ADB_PATH, ["-s", this.serial!, "shell", "am", "force-stop", packageName]);
545+
}
546+
543547
async sendDeepLink(link: string, build: BuildResult) {
544548
if (build.platform !== DevicePlatform.Android) {
545549
throw new Error("Invalid platform");

Diff for: packages/vscode-extension/src/devices/DeviceBase.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,19 @@ export abstract class DeviceBase implements Disposable {
2626
abstract getClipboard(): Promise<string | void>;
2727
abstract installApp(build: BuildResult, forceReinstall: boolean): Promise<void>;
2828
abstract launchApp(build: BuildResult, metroPort: number, devtoolsPort: number): Promise<void>;
29+
abstract terminateApp(packageNameOrBundleID: string): Promise<void>;
2930
abstract makePreview(): Preview;
3031
abstract get platform(): DevicePlatform;
3132
abstract get deviceInfo(): DeviceInfo;
3233
abstract resetAppPermissions(
3334
appPermission: AppPermissionType,
3435
buildResult: BuildResult
3536
): Promise<boolean>;
36-
abstract sendDeepLink(link: string, buildResult: BuildResult): Promise<void>;
37+
abstract sendDeepLink(
38+
link: string,
39+
buildResult: BuildResult,
40+
terminateApp: boolean
41+
): Promise<void>;
3742

3843
async acquire() {
3944
const acquired = await tryAcquiringLock(this.lockFilePath);

Diff for: packages/vscode-extension/src/devices/IosSimulatorDevice.ts

+20-14
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,21 @@ export class IosSimulatorDevice extends DeviceBase {
290290
}
291291
}
292292

293+
async terminateApp(bundleID: string) {
294+
const deviceSetLocation = getOrCreateDeviceSet(this.deviceUDID);
295+
296+
// Terminate the app if it's running:
297+
try {
298+
await exec(
299+
"xcrun",
300+
["simctl", "--set", deviceSetLocation, "terminate", this.deviceUDID, bundleID],
301+
{ allowNonZeroExit: true }
302+
);
303+
} catch (e) {
304+
// terminate will exit with non-zero code when the app wasn't running. we ignore this error
305+
}
306+
}
307+
293308
/**
294309
* This function terminates any running applications. Might be useful when you launch a new application
295310
* before terminating the previous one.
@@ -312,20 +327,11 @@ export class IosSimulatorDevice extends DeviceBase {
312327
matches.push(match[1]);
313328
}
314329

315-
const terminateApp = async (bundleID: string) => {
316-
// Terminate the app if it's running:
317-
try {
318-
await exec(
319-
"xcrun",
320-
["simctl", "--set", deviceSetLocation, "terminate", this.deviceUDID, bundleID],
321-
{ allowNonZeroExit: true }
322-
);
323-
} catch (e) {
324-
// terminate will exit with non-zero code when the app wasn't running. we ignore this error
325-
}
326-
};
327-
328-
await Promise.all(matches.map(terminateApp));
330+
await Promise.all(
331+
matches.map((e) => {
332+
this.terminateApp(e);
333+
})
334+
);
329335
}
330336

331337
async launchWithBuild(build: IOSBuildResult) {

Diff for: packages/vscode-extension/src/project/deviceSession.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { DependencyManager } from "../dependency/DependencyManager";
1818
import { getTelemetryReporter } from "../utilities/telemetry";
1919
import { BuildCache } from "../builders/BuildCache";
2020
import { CancelToken } from "../builders/cancelToken";
21+
import { DevicePlatform } from "../common/DeviceManager";
2122

2223
type PreviewReadyCallback = (previewURL: string) => void;
2324
type StartOptions = { cleanBuild: boolean; previewReadyCallback: PreviewReadyCallback };
@@ -316,9 +317,22 @@ export class DeviceSession implements Disposable {
316317
return false;
317318
}
318319

319-
public async sendDeepLink(link: string) {
320+
public async sendDeepLink(link: string, terminateApp: boolean) {
320321
if (this.maybeBuildResult) {
321-
return this.device.sendDeepLink(link, this.maybeBuildResult);
322+
if (terminateApp) {
323+
const packageNameOrBundleID =
324+
this.maybeBuildResult.platform === DevicePlatform.Android
325+
? this.maybeBuildResult.packageName
326+
: this.maybeBuildResult.bundleID;
327+
328+
await this.device.terminateApp(packageNameOrBundleID);
329+
}
330+
331+
await this.device.sendDeepLink(link, this.maybeBuildResult, terminateApp);
332+
333+
if (terminateApp) {
334+
this.debugSession?.reconnectJSDebuggerIfNeeded(this.metro);
335+
}
322336
}
323337
}
324338

Diff for: packages/vscode-extension/src/project/project.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ export class Project
668668
return extensionContext.workspaceState.get<string[] | undefined>(DEEP_LINKS_HISTORY_KEY) ?? [];
669669
}
670670

671-
async openDeepLink(link: string) {
671+
async openDeepLink(link: string, terminateApp: boolean) {
672672
const history = await this.getDeepLinksHistory();
673673
if (history.length === 0 || link !== history[0]) {
674674
extensionContext.workspaceState.update(
@@ -677,7 +677,7 @@ export class Project
677677
);
678678
}
679679

680-
this.deviceSession?.sendDeepLink(link);
680+
this.deviceSession?.sendDeepLink(link, terminateApp);
681681
}
682682

683683
public dispatchTouches(touches: Array<TouchPoint>, type: "Up" | "Move" | "Down") {

Diff for: packages/vscode-extension/src/webview/views/OpenDeepLinkView.css

+9
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,12 @@
88
display: flex;
99
justify-content: center;
1010
}
11+
12+
.checkbox-container {
13+
margin-top: 12px;
14+
display: flex;
15+
align-items: center;
16+
gap: 8px;
17+
font-size: 14px;
18+
color: #333; /* Adjust color to match your theme */
19+
}

Diff for: packages/vscode-extension/src/webview/views/OpenDeepLinkView.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useState } from "react";
2+
import * as Switch from "@radix-ui/react-switch";
23
import { useProject } from "../providers/ProjectProvider";
34
import { useModal } from "../providers/ModalProvider";
45
import { SearchSelect } from "../components/shared/SearchSelect";
@@ -11,6 +12,7 @@ export const OpenDeepLinkView = () => {
1112

1213
const [url, setUrl] = useState<string>("");
1314
const [history, setHistory] = useState<string[] | undefined>(undefined);
15+
const [terminateApp, setTerminateApp] = useState<boolean>(false);
1416

1517
useEffect(() => {
1618
(async () => {
@@ -24,7 +26,7 @@ export const OpenDeepLinkView = () => {
2426
return;
2527
}
2628
closeModal();
27-
await project.openDeepLink(link);
29+
await project.openDeepLink(link, terminateApp);
2830
};
2931

3032
return (
@@ -41,6 +43,15 @@ export const OpenDeepLinkView = () => {
4143
isLoading={history === undefined}
4244
onValueChange={setUrl}
4345
/>
46+
<div className="checkbox-container">
47+
<Switch.Root
48+
className="switch-root small-switch"
49+
onCheckedChange={setTerminateApp}
50+
defaultChecked={terminateApp}>
51+
<Switch.Thumb className="switch-thumb" />
52+
</Switch.Root>
53+
<label>Terminate app before sending deep link</label>
54+
</div>
4455
<div className="submit-button-container">
4556
<Button className="submit-button" type="secondary" onClick={() => openDeepLink(url)}>
4657
Open

0 commit comments

Comments
 (0)