Skip to content

feat: add support to auto-install cert on mobile device #167

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

Merged
merged 8 commits into from
Sep 30, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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