Skip to content

Commit e3c95a7

Browse files
authored
feat: add support to auto-install cert on mobile device (#167)
* feat: add support to auto-install cert on mobile device * chore: move string to message file * chore: add boot mode * chore: minor refactoring * chore: use latest version of lwc-dev-mobile-core * chore: update dep versions * chore: revert dep version changes
1 parent 741cd99 commit e3c95a7

File tree

8 files changed

+1435
-1552
lines changed

8 files changed

+1435
-1552
lines changed

.github/ISSUE_TEMPLATE/Bug_report.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,19 @@ _Describe what actually happened instead_.
3131
### Additional Information
3232

3333
**Screenshots:**
34+
3435
<!-- Screenshots of the following are very helpful: -->
3536
<!-- 1) Browser state when you encounter the issue -->
3637
<!-- 2) Chrome dev-tools "Network" tab (what requests failed during local dev) -->
3738

3839
**Logs:**
40+
3941
<!-- Any logs from the browser and the local dev server when the issue occurs -->
4042

4143
### System Information
4244

4345
**SF CLI:**
46+
4447
<!-- Which shell or terminal are you using? (bash, zsh, powershell 7, cmd.exe, etc) -->
4548
<!-- Paste the **full** output of the `sf version --verbose --json` command below -->
4649

@@ -51,10 +54,9 @@ PASTE_SF_VERSION_OUTPUT_HERE
5154
**OS:**
5255

5356
**Experience Sites Only:**
57+
5458
<!-- If you are running an experience site locally, paste the contents of .localdev/${sitename}/app/site/.metadata/runtime-info.json below -->
5559

5660
```json
5761
PASTE_runtime-info.json_HERE
5862
```
59-
60-

messages/lightning.dev.app.md

Lines changed: 12 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ Unable to determine App Id for %s
5555

5656
Unable to find device %s
5757

58+
# error.device.google.play
59+
60+
Google Play devices are not supported. %s is a Google Play device. Please use a Google APIs device instead.
61+
5862
# spinner.device.boot
5963

6064
Booting device %s
@@ -63,6 +67,14 @@ Booting device %s
6367

6468
Generating self-signed certificate
6569

70+
# spinner.cert.install
71+
72+
Installing self-signed certificate
73+
74+
# spinner.app.install
75+
76+
Installing app %s
77+
6678
# spinner.extract.archive
6779

6880
Extracting archive
@@ -79,76 +91,6 @@ Downloading
7991

8092
Note: Your desktop browser requires additional configuration to trust the local development server. See the documentation for more details.
8193

82-
# certificate.installation.notice
83-
84-
To use local preview on your device, you have to install a self-signed certificate on it. If you previously set up a certificate for your device, you can skip this step.
85-
86-
# certificate.installation.skip.message
87-
88-
Do you want to skip this step
89-
90-
# certificate.installation.description
91-
92-
Before proceeding, install the self-signed certificate on your device. The certificate file is located at
93-
94-
`%s`
95-
96-
To install the certificate, follow these steps:
97-
98-
%s
99-
100-
# certificate.installation.steps.ios
101-
102-
1. Drag and drop the file onto your booted simulator.
103-
2. Click `Allow` to proceed with downloading the configuration file.
104-
3. Click `Close` and navigate to `Settings > General > VPN & Device Management > localhost`.
105-
4. Click `Install` in the title bar, in the warning window, and on the install button.
106-
5. In the `Profile Installed` view, confirm that the profile displays `Verified` and then click `Done`.
107-
6. Navigate to `Settings > General > About > Certificate Trust Settings`.
108-
7. Enable full trust for `localhost`.
109-
8. In the resulting warning pop-up, click `Continue`.
110-
111-
# certificate.installation.steps.android
112-
113-
1. Drag and drop the file onto your booted emulator.
114-
2. %s
115-
3. Navigate to the certificate file from step 1. (It's usually located in `/sdcard/download`).
116-
4. Follow the on-screen instructions to install it.
117-
5. Click `User credentials` under `Credential storage` and verify that your certificate is listed there.
118-
6. Click `Trusted credentials` under `Credential storage`. Then click `USER` and verify that page lists your certificate.
119-
120-
# certificate.installation.steps.android.nav-target-api-24-25
121-
122-
Navigate to `Settings > Security` and click `Install from SD card` under `Credential storage`.
123-
124-
# certificate.installation.steps.android.nav-target-api-26-27
125-
126-
Navigate to `Settings > Security & Location > Encryption & credentials` and click `Install from SD card` under `Credential storage`.
127-
128-
# certificate.installation.steps.android.nav-target-api-28
129-
130-
Navigate to `Settings > Security & Location > Advanced > Encryption & credentials` and click `Install from SD card` under `Credential storage`.
131-
132-
# certificate.installation.steps.android.nav-target-api-29
133-
134-
Navigate to `Settings > Security > Encryption & credentials` and click `Install from SD card` under `Credential storage`.
135-
136-
# certificate.installation.steps.android.nav-target-api-30-32
137-
138-
Navigate to `Settings > Security > Encryption & credentials` and click `Install a certificate` under `Credential storage`. Click `CA certificate`, and then click `Install anyway`.
139-
140-
# certificate.installation.steps.android.nav-target-api-33
141-
142-
Navigate to `Settings > Security > More security settings > Encryption & credentials` and click `Install a certificate` under `Credential storage`. Click `CA certificate`, and then click `Install anyway`.
143-
144-
# certificate.installation.steps.android.nav-target-api-34-up
145-
146-
Navigate to `Settings > Security & Privacy > More security & privacy > Encryption & credentials` and click `Install a certificate` under `Credential storage`. Click `CA certificate`, and then click `Install anyway`.
147-
148-
# certificate.waiting
149-
150-
After you install the certificate, press any key to continue...
151-
15294
# mobileapp.notfound
15395

15496
%s isn't installed on your device.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
"@oclif/core": "^4.0.17",
1212
"@salesforce/core": "^8.2.7",
1313
"@salesforce/kit": "^3.1.6",
14-
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.7",
14+
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.9",
1515
"@salesforce/sf-plugins-core": "^11.2.4",
1616
"@inquirer/select": "^2.4.7",
1717
"@inquirer/prompts": "^5.3.8",
1818
"axios": "^1.7.7",
19-
"chalk": "^5.3.0",
2019
"lwc": "7.1.3",
2120
"lwr": "0.14.3",
2221
"node-fetch": "^3.3.2"

src/commands/lightning/dev/app.ts

Lines changed: 47 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,17 @@
66
*/
77

88
import path from 'node:path';
9-
import * as readline from 'node:readline';
109
import { Connection, Logger, Messages, SfProject } from '@salesforce/core';
1110
import {
1211
AndroidAppPreviewConfig,
13-
AndroidVirtualDevice,
12+
AndroidDevice,
13+
BootMode,
14+
CommonUtils,
1415
IOSAppPreviewConfig,
15-
IOSSimulatorDevice,
1616
Setup as LwcDevMobileCoreSetup,
1717
Platform,
1818
} from '@salesforce/lwc-dev-mobile-core';
1919
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
20-
import chalk from 'chalk';
2120
import { OrgUtils } from '../../../shared/orgUtils.js';
2221
import { startLWCServer } from '../../../lwc-dev-server/index.js';
2322
import { PreviewUtils } from '../../../shared/previewUtils.js';
@@ -79,84 +78,6 @@ export default class LightningDevApp extends SfCommand<void> {
7978
}),
8079
};
8180

82-
private static async waitForKeyPress(): Promise<void> {
83-
return new Promise((resolve) => {
84-
const rl = readline.createInterface({
85-
input: process.stdin,
86-
output: process.stdout,
87-
});
88-
89-
// eslint-disable-next-line no-console
90-
console.log(`\n${messages.getMessage('certificate.waiting')}\n`);
91-
92-
process.stdin.setRawMode(true);
93-
process.stdin.resume();
94-
process.stdin.once('data', () => {
95-
process.stdin.setRawMode(false);
96-
process.stdin.pause();
97-
rl.close();
98-
resolve();
99-
});
100-
});
101-
}
102-
103-
public async waitForUserToInstallCert(
104-
platform: Platform.ios | Platform.android,
105-
device: IOSSimulatorDevice | AndroidVirtualDevice,
106-
certFilePath: string
107-
): Promise<void> {
108-
// eslint-disable-next-line no-console
109-
console.log(`\n${messages.getMessage('certificate.installation.notice')}`);
110-
111-
const skipInstall = await this.confirm({
112-
message: messages.getMessage('certificate.installation.skip.message'),
113-
defaultAnswer: true,
114-
ms: maxInt32, // simulate no timeout and wait for user to answer
115-
});
116-
117-
if (skipInstall) {
118-
return;
119-
}
120-
121-
let installationSteps = '';
122-
if (platform === Platform.ios) {
123-
installationSteps = messages.getMessage('certificate.installation.steps.ios');
124-
} else {
125-
const apiLevel = (device as AndroidVirtualDevice).apiLevel.toString();
126-
127-
let subStepMessageKey = '';
128-
if (apiLevel.startsWith('24.') || apiLevel.startsWith('25.')) {
129-
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-24-25';
130-
} else if (apiLevel.startsWith('26.') || apiLevel.startsWith('27.')) {
131-
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-26-27';
132-
} else if (apiLevel.startsWith('28.')) {
133-
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-28';
134-
} else if (apiLevel.startsWith('29.')) {
135-
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-29';
136-
} else if (apiLevel.startsWith('30.') || apiLevel.startsWith('31.') || apiLevel.startsWith('32.')) {
137-
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-30-32';
138-
} else if (apiLevel.startsWith('33.')) {
139-
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-33';
140-
} else {
141-
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-34-up';
142-
}
143-
144-
installationSteps = messages.getMessage('certificate.installation.steps.android', [
145-
messages.getMessage(subStepMessageKey),
146-
]);
147-
}
148-
149-
let message = messages.getMessage('certificate.installation.description', [certFilePath, installationSteps]);
150-
151-
// use chalk to format every substring wrapped in `` so they would stand out when printed on screen
152-
message = message.replace(/`([^`]*)`/g, chalk.yellow('$1'));
153-
154-
// eslint-disable-next-line no-console
155-
console.log(message);
156-
157-
return LightningDevApp.waitForKeyPress();
158-
}
159-
16081
public async run(): Promise<void> {
16182
const { flags } = await this.parse(LightningDevApp);
16283
const logger = await Logger.child(this.ctor.name);
@@ -298,31 +219,48 @@ export default class LightningDevApp extends SfCommand<void> {
298219
throw new Error(messages.getMessage('error.device.notfound', [deviceId ?? '']));
299220
}
300221

301-
// Boot the device if not already booted
222+
if ((device as AndroidDevice)?.isPlayStore === true) {
223+
throw new Error(messages.getMessage('error.device.google.play', [device.id]));
224+
}
225+
226+
// Boot the device. If device is already booted then this will immediately return anyway.
302227
this.spinner.start(messages.getMessage('spinner.device.boot', [device.toString()]));
303-
const resolvedDeviceId = platform === Platform.ios ? (device as IOSSimulatorDevice).udid : device.name;
304-
const emulatorPort = await PreviewUtils.bootMobileDevice(platform, resolvedDeviceId, logger);
228+
if (platform === Platform.ios) {
229+
await device.boot();
230+
} else {
231+
// Prefer to boot the AVD with system writable. If it is already booted then calling boot()
232+
// will have no effect. But if an AVD is not already booted then this will perform a cold
233+
// boot with writable system. This way later on when we want to install cert on the device,
234+
// we won't need to shut it down and reboot it with writable system since it already will
235+
// have writable system, thus speeding up the process of installing a cert.
236+
await (device as AndroidDevice).boot(true, BootMode.systemWritablePreferred, false);
237+
}
305238
this.spinner.stop();
306239

307240
// Configure certificates for dev server secure connection
308-
this.spinner.start(messages.getMessage('spinner.cert.gen'));
309-
const { certData, certFilePath } = await PreviewUtils.generateSelfSignedCert(platform, sfdxProjectRootPath);
310-
this.spinner.stop();
311-
312-
// Show message and wait for user to install the certificate on their device
313-
await this.waitForUserToInstallCert(platform, device, certFilePath);
241+
const certData = await PreviewUtils.generateSelfSignedCert();
242+
if (platform === Platform.ios) {
243+
// On iOS we force-install the cert even if it is already installed because
244+
// the process of installing the cert is fast and easy.
245+
this.spinner.start(messages.getMessage('spinner.cert.install'));
246+
await device.installCert(certData);
247+
this.spinner.stop();
248+
} else {
249+
// On Android the process of auto-installing a cert is a bit involved and slow.
250+
// So it is best to first determine if the cert is already installed or not.
251+
const isAlreadyInstalled = await device.isCertInstalled(certData);
252+
if (!isAlreadyInstalled) {
253+
this.spinner.start(messages.getMessage('spinner.cert.install'));
254+
await device.installCert(certData);
255+
this.spinner.stop();
256+
}
257+
}
314258

315259
// Check if Salesforce Mobile App is installed on the device
316260
const appConfig = platform === Platform.ios ? iOSSalesforceAppPreviewConfig : androidSalesforceAppPreviewConfig;
317-
const appInstalled = await PreviewUtils.verifyMobileAppInstalled(
318-
platform,
319-
appConfig,
320-
resolvedDeviceId,
321-
emulatorPort,
322-
logger
323-
);
261+
const appInstalled = await device.isAppInstalled(appConfig.id);
324262

325-
// If Salesforce Mobile App is not installed, download and install it
263+
// If Salesforce Mobile App is not installed, offer to download and install it
326264
let bundlePath: string | undefined;
327265
if (!appInstalled) {
328266
const proceedWithDownload = await this.confirm({
@@ -348,14 +286,18 @@ export default class LightningDevApp extends SfCommand<void> {
348286
this.spinner.start(messages.getMessage('spinner.extract.archive'));
349287
const outputDir = path.dirname(bundlePath);
350288
const finalBundlePath = path.join(outputDir, 'Chatter.app');
351-
await PreviewUtils.extractZIPArchive(bundlePath, outputDir, logger);
289+
await CommonUtils.extractZIPArchive(bundlePath, outputDir, logger);
352290
this.spinner.stop();
353291
bundlePath = finalBundlePath;
354292
}
293+
294+
// now go ahead and install the app
295+
this.spinner.start(messages.getMessage('spinner.app.install', [appConfig.id]));
296+
await device.installApp(bundlePath);
297+
this.spinner.stop();
355298
}
356299

357300
// Start the LWC Dev Server
358-
359301
await startLWCServer(logger, sfdxProjectRootPath, token, serverPorts, certData);
360302

361303
// Launch the native app for previewing (launchMobileApp will show its own spinner)
@@ -366,7 +308,10 @@ export default class LightningDevApp extends SfCommand<void> {
366308
appName,
367309
appId
368310
);
369-
await PreviewUtils.launchMobileApp(platform, appConfig, resolvedDeviceId, emulatorPort, bundlePath, logger);
311+
const targetActivity = (appConfig as AndroidAppPreviewConfig)?.activity;
312+
const targetApp = targetActivity ? `${appConfig.id}/${targetActivity}` : appConfig.id;
313+
314+
await device.launchApp(targetApp, appConfig.launch_arguments ?? []);
370315
} finally {
371316
// stop progress & spinner UX (that may still be running in case of an error)
372317
this.progress.stop();

0 commit comments

Comments
 (0)