Skip to content

Commit

Permalink
feat: add support to auto-install cert on mobile device (#167)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
maliroteh-sf authored Sep 30, 2024
1 parent 741cd99 commit e3c95a7
Show file tree
Hide file tree
Showing 8 changed files with 1,435 additions and 1,552 deletions.
6 changes: 4 additions & 2 deletions .github/ISSUE_TEMPLATE/Bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,19 @@ _Describe what actually happened instead_.
### Additional Information

**Screenshots:**

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

**Logs:**

<!-- Any logs from the browser and the local dev server when the issue occurs -->

### System Information

**SF CLI:**

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

Expand All @@ -51,10 +54,9 @@ PASTE_SF_VERSION_OUTPUT_HERE
**OS:**

**Experience Sites Only:**

<!-- If you are running an experience site locally, paste the contents of .localdev/${sitename}/app/site/.metadata/runtime-info.json below -->

```json
PASTE_runtime-info.json_HERE
```


82 changes: 12 additions & 70 deletions messages/lightning.dev.app.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ Unable to determine App Id for %s

Unable to find device %s

# error.device.google.play

Google Play devices are not supported. %s is a Google Play device. Please use a Google APIs device instead.

# spinner.device.boot

Booting device %s
Expand All @@ -63,6 +67,14 @@ Booting device %s

Generating self-signed certificate

# spinner.cert.install

Installing self-signed certificate

# spinner.app.install

Installing app %s

# spinner.extract.archive

Extracting archive
Expand All @@ -79,76 +91,6 @@ Downloading

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

# certificate.installation.notice

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.

# certificate.installation.skip.message

Do you want to skip this step

# certificate.installation.description

Before proceeding, install the self-signed certificate on your device. The certificate file is located at

`%s`

To install the certificate, follow these steps:

%s

# certificate.installation.steps.ios

1. Drag and drop the file onto your booted simulator.
2. Click `Allow` to proceed with downloading the configuration file.
3. Click `Close` and navigate to `Settings > General > VPN & Device Management > localhost`.
4. Click `Install` in the title bar, in the warning window, and on the install button.
5. In the `Profile Installed` view, confirm that the profile displays `Verified` and then click `Done`.
6. Navigate to `Settings > General > About > Certificate Trust Settings`.
7. Enable full trust for `localhost`.
8. In the resulting warning pop-up, click `Continue`.

# certificate.installation.steps.android

1. Drag and drop the file onto your booted emulator.
2. %s
3. Navigate to the certificate file from step 1. (It's usually located in `/sdcard/download`).
4. Follow the on-screen instructions to install it.
5. Click `User credentials` under `Credential storage` and verify that your certificate is listed there.
6. Click `Trusted credentials` under `Credential storage`. Then click `USER` and verify that page lists your certificate.

# certificate.installation.steps.android.nav-target-api-24-25

Navigate to `Settings > Security` and click `Install from SD card` under `Credential storage`.

# certificate.installation.steps.android.nav-target-api-26-27

Navigate to `Settings > Security & Location > Encryption & credentials` and click `Install from SD card` under `Credential storage`.

# certificate.installation.steps.android.nav-target-api-28

Navigate to `Settings > Security & Location > Advanced > Encryption & credentials` and click `Install from SD card` under `Credential storage`.

# certificate.installation.steps.android.nav-target-api-29

Navigate to `Settings > Security > Encryption & credentials` and click `Install from SD card` under `Credential storage`.

# certificate.installation.steps.android.nav-target-api-30-32

Navigate to `Settings > Security > Encryption & credentials` and click `Install a certificate` under `Credential storage`. Click `CA certificate`, and then click `Install anyway`.

# certificate.installation.steps.android.nav-target-api-33

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`.

# certificate.installation.steps.android.nav-target-api-34-up

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`.

# certificate.waiting

After you install the certificate, press any key to continue...

# mobileapp.notfound

%s isn't installed on your device.
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
"@oclif/core": "^4.0.17",
"@salesforce/core": "^8.2.7",
"@salesforce/kit": "^3.1.6",
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.7",
"@salesforce/lwc-dev-mobile-core": "4.0.0-alpha.9",
"@salesforce/sf-plugins-core": "^11.2.4",
"@inquirer/select": "^2.4.7",
"@inquirer/prompts": "^5.3.8",
"axios": "^1.7.7",
"chalk": "^5.3.0",
"lwc": "7.1.3",
"lwr": "0.14.3",
"node-fetch": "^3.3.2"
Expand Down
149 changes: 47 additions & 102 deletions src/commands/lightning/dev/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@
*/

import path from 'node:path';
import * as readline from 'node:readline';
import { Connection, Logger, Messages, SfProject } from '@salesforce/core';
import {
AndroidAppPreviewConfig,
AndroidVirtualDevice,
AndroidDevice,
BootMode,
CommonUtils,
IOSAppPreviewConfig,
IOSSimulatorDevice,
Setup as LwcDevMobileCoreSetup,
Platform,
} from '@salesforce/lwc-dev-mobile-core';
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
import chalk from 'chalk';
import { OrgUtils } from '../../../shared/orgUtils.js';
import { startLWCServer } from '../../../lwc-dev-server/index.js';
import { PreviewUtils } from '../../../shared/previewUtils.js';
Expand Down Expand Up @@ -79,84 +78,6 @@ export default class LightningDevApp extends SfCommand<void> {
}),
};

private static async waitForKeyPress(): Promise<void> {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

// eslint-disable-next-line no-console
console.log(`\n${messages.getMessage('certificate.waiting')}\n`);

process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.once('data', () => {
process.stdin.setRawMode(false);
process.stdin.pause();
rl.close();
resolve();
});
});
}

public async waitForUserToInstallCert(
platform: Platform.ios | Platform.android,
device: IOSSimulatorDevice | AndroidVirtualDevice,
certFilePath: string
): Promise<void> {
// eslint-disable-next-line no-console
console.log(`\n${messages.getMessage('certificate.installation.notice')}`);

const skipInstall = await this.confirm({
message: messages.getMessage('certificate.installation.skip.message'),
defaultAnswer: true,
ms: maxInt32, // simulate no timeout and wait for user to answer
});

if (skipInstall) {
return;
}

let installationSteps = '';
if (platform === Platform.ios) {
installationSteps = messages.getMessage('certificate.installation.steps.ios');
} else {
const apiLevel = (device as AndroidVirtualDevice).apiLevel.toString();

let subStepMessageKey = '';
if (apiLevel.startsWith('24.') || apiLevel.startsWith('25.')) {
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-24-25';
} else if (apiLevel.startsWith('26.') || apiLevel.startsWith('27.')) {
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-26-27';
} else if (apiLevel.startsWith('28.')) {
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-28';
} else if (apiLevel.startsWith('29.')) {
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-29';
} else if (apiLevel.startsWith('30.') || apiLevel.startsWith('31.') || apiLevel.startsWith('32.')) {
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-30-32';
} else if (apiLevel.startsWith('33.')) {
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-33';
} else {
subStepMessageKey = 'certificate.installation.steps.android.nav-target-api-34-up';
}

installationSteps = messages.getMessage('certificate.installation.steps.android', [
messages.getMessage(subStepMessageKey),
]);
}

let message = messages.getMessage('certificate.installation.description', [certFilePath, installationSteps]);

// use chalk to format every substring wrapped in `` so they would stand out when printed on screen
message = message.replace(/`([^`]*)`/g, chalk.yellow('$1'));

// eslint-disable-next-line no-console
console.log(message);

return LightningDevApp.waitForKeyPress();
}

public async run(): Promise<void> {
const { flags } = await this.parse(LightningDevApp);
const logger = await Logger.child(this.ctor.name);
Expand Down Expand Up @@ -298,31 +219,48 @@ export default class LightningDevApp extends SfCommand<void> {
throw new Error(messages.getMessage('error.device.notfound', [deviceId ?? '']));
}

// Boot the device if not already booted
if ((device as AndroidDevice)?.isPlayStore === true) {
throw new Error(messages.getMessage('error.device.google.play', [device.id]));
}

// Boot the device. If device is already booted then this will immediately return anyway.
this.spinner.start(messages.getMessage('spinner.device.boot', [device.toString()]));
const resolvedDeviceId = platform === Platform.ios ? (device as IOSSimulatorDevice).udid : device.name;
const emulatorPort = await PreviewUtils.bootMobileDevice(platform, resolvedDeviceId, logger);
if (platform === Platform.ios) {
await device.boot();
} else {
// Prefer to boot the AVD with system writable. If it is already booted then calling boot()
// will have no effect. But if an AVD is not already booted then this will perform a cold
// boot with writable system. This way later on when we want to install cert on the device,
// we won't need to shut it down and reboot it with writable system since it already will
// have writable system, thus speeding up the process of installing a cert.
await (device as AndroidDevice).boot(true, BootMode.systemWritablePreferred, false);
}
this.spinner.stop();

// Configure certificates for dev server secure connection
this.spinner.start(messages.getMessage('spinner.cert.gen'));
const { certData, certFilePath } = await PreviewUtils.generateSelfSignedCert(platform, sfdxProjectRootPath);
this.spinner.stop();

// Show message and wait for user to install the certificate on their device
await this.waitForUserToInstallCert(platform, device, certFilePath);
const certData = await PreviewUtils.generateSelfSignedCert();
if (platform === Platform.ios) {
// On iOS we force-install the cert even if it is already installed because
// the process of installing the cert is fast and easy.
this.spinner.start(messages.getMessage('spinner.cert.install'));
await device.installCert(certData);
this.spinner.stop();
} else {
// On Android the process of auto-installing a cert is a bit involved and slow.
// So it is best to first determine if the cert is already installed or not.
const isAlreadyInstalled = await device.isCertInstalled(certData);
if (!isAlreadyInstalled) {
this.spinner.start(messages.getMessage('spinner.cert.install'));
await device.installCert(certData);
this.spinner.stop();
}
}

// Check if Salesforce Mobile App is installed on the device
const appConfig = platform === Platform.ios ? iOSSalesforceAppPreviewConfig : androidSalesforceAppPreviewConfig;
const appInstalled = await PreviewUtils.verifyMobileAppInstalled(
platform,
appConfig,
resolvedDeviceId,
emulatorPort,
logger
);
const appInstalled = await device.isAppInstalled(appConfig.id);

// If Salesforce Mobile App is not installed, download and install it
// If Salesforce Mobile App is not installed, offer to download and install it
let bundlePath: string | undefined;
if (!appInstalled) {
const proceedWithDownload = await this.confirm({
Expand All @@ -348,14 +286,18 @@ export default class LightningDevApp extends SfCommand<void> {
this.spinner.start(messages.getMessage('spinner.extract.archive'));
const outputDir = path.dirname(bundlePath);
const finalBundlePath = path.join(outputDir, 'Chatter.app');
await PreviewUtils.extractZIPArchive(bundlePath, outputDir, logger);
await CommonUtils.extractZIPArchive(bundlePath, outputDir, logger);
this.spinner.stop();
bundlePath = finalBundlePath;
}

// now go ahead and install the app
this.spinner.start(messages.getMessage('spinner.app.install', [appConfig.id]));
await device.installApp(bundlePath);
this.spinner.stop();
}

// Start the LWC Dev Server

await startLWCServer(logger, sfdxProjectRootPath, token, serverPorts, certData);

// Launch the native app for previewing (launchMobileApp will show its own spinner)
Expand All @@ -366,7 +308,10 @@ export default class LightningDevApp extends SfCommand<void> {
appName,
appId
);
await PreviewUtils.launchMobileApp(platform, appConfig, resolvedDeviceId, emulatorPort, bundlePath, logger);
const targetActivity = (appConfig as AndroidAppPreviewConfig)?.activity;
const targetApp = targetActivity ? `${appConfig.id}/${targetActivity}` : appConfig.id;

await device.launchApp(targetApp, appConfig.launch_arguments ?? []);
} finally {
// stop progress & spinner UX (that may still be running in case of an error)
this.progress.stop();
Expand Down
Loading

0 comments on commit e3c95a7

Please sign in to comment.