Skip to content

Commit c544592

Browse files
authored
feat: prompt user to select device type (#195)
* chore: some refactoring * feat: prompt user to select device type * chore: address PR feadback
1 parent 322d805 commit c544592

File tree

12 files changed

+242
-253
lines changed

12 files changed

+242
-253
lines changed

messages/prompts.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# site.select
2+
3+
Select a site
4+
5+
# site.confirm-update
6+
7+
An updated site bundle is available for "%s". Do you want to download and apply the update?
8+
9+
# device-type.title
10+
11+
Which device type do you want to use for the preview?
12+
13+
# device-type.choice.desktop
14+
15+
Desktop
16+
17+
# device-type.choice.android
18+
19+
Mobile - Android
20+
21+
# device-type.choice.ios
22+
23+
Mobile - iOS

src/commands/lightning/dev/app.ts

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import path from 'node:path';
9-
import { Connection, Logger, Messages, SfProject } from '@salesforce/core';
9+
import { Logger, Messages, SfProject } from '@salesforce/core';
1010
import {
1111
AndroidAppPreviewConfig,
1212
AndroidDevice,
@@ -20,7 +20,7 @@ import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
2020
import { OrgUtils } from '../../../shared/orgUtils.js';
2121
import { startLWCServer } from '../../../lwc-dev-server/index.js';
2222
import { PreviewUtils } from '../../../shared/previewUtils.js';
23-
import { ConfigUtils, IdentityTokenService } from '../../../shared/configUtils.js';
23+
import { PromptUtils } from '../../../shared/promptUtils.js';
2424

2525
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
2626
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.app');
@@ -39,22 +39,6 @@ export const androidSalesforceAppPreviewConfig = {
3939

4040
const maxInt32 = 2_147_483_647; // maximum 32-bit signed integer value
4141

42-
class AppServerIdentityTokenService implements IdentityTokenService {
43-
private connection: Connection;
44-
public constructor(connection: Connection) {
45-
this.connection = connection;
46-
}
47-
48-
public async saveTokenToServer(token: string): Promise<string> {
49-
const sobject = this.connection.sobject('UserLocalWebServerIdentity');
50-
const result = await sobject.insert({ LocalWebServerIdentityToken: token });
51-
if (result.success) {
52-
return result.id;
53-
}
54-
throw new Error('Could not save the token to the server');
55-
}
56-
}
57-
5842
export default class LightningDevApp extends SfCommand<void> {
5943
public static readonly summary = messages.getMessage('summary');
6044
public static readonly description = messages.getMessage('description');
@@ -71,7 +55,6 @@ export default class LightningDevApp extends SfCommand<void> {
7155
summary: messages.getMessage('flags.device-type.summary'),
7256
char: 't',
7357
options: [Platform.desktop, Platform.ios, Platform.android] as const,
74-
default: Platform.desktop,
7558
})(),
7659
'device-id': Flags.string({
7760
summary: messages.getMessage('flags.device-id.summary'),
@@ -83,9 +66,9 @@ export default class LightningDevApp extends SfCommand<void> {
8366
const { flags } = await this.parse(LightningDevApp);
8467
const logger = await Logger.child(this.ctor.name);
8568

86-
const appName = flags['name'];
87-
const platform = flags['device-type'];
8869
const targetOrg = flags['target-org'];
70+
const appName = flags['name'];
71+
const platform = flags['device-type'] ?? (await PromptUtils.promptUserToSelectPlatform());
8972
const deviceId = flags['device-id'];
9073

9174
let sfdxProjectRootPath = '';
@@ -107,8 +90,12 @@ export default class LightningDevApp extends SfCommand<void> {
10790
return Promise.reject(new Error(sharedMessages.getMessage('error.localdev.not.enabled')));
10891
}
10992

110-
const tokenService = new AppServerIdentityTokenService(connection);
111-
const token = await ConfigUtils.getOrCreateIdentityToken(username, tokenService);
93+
const appServerIdentity = await PreviewUtils.getOrCreateAppServerIdentity(connection);
94+
const ldpServerToken = appServerIdentity.identityToken;
95+
const ldpServerId = appServerIdentity.usernameToServerEntityIdMap[username];
96+
if (!ldpServerId) {
97+
return Promise.reject(new Error(messages.getMessage('error.identitydata.entityid')));
98+
}
11299

113100
let appId: string | undefined;
114101
if (appName) {
@@ -132,17 +119,23 @@ export default class LightningDevApp extends SfCommand<void> {
132119
const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, serverPorts, logger);
133120
logger.debug(`Local Dev Server url is ${ldpServerUrl}`);
134121

135-
const entityId = await PreviewUtils.getEntityId(username);
136-
137122
if (platform === Platform.desktop) {
138-
await this.desktopPreview(sfdxProjectRootPath, serverPorts, token, entityId, ldpServerUrl, appId, logger);
123+
await this.desktopPreview(
124+
sfdxProjectRootPath,
125+
serverPorts,
126+
ldpServerToken,
127+
ldpServerId,
128+
ldpServerUrl,
129+
appId,
130+
logger
131+
);
139132
} else {
140133
await this.mobilePreview(
141134
platform,
142135
sfdxProjectRootPath,
143136
serverPorts,
144-
token,
145-
entityId,
137+
ldpServerToken,
138+
ldpServerId,
146139
ldpServerUrl,
147140
appName,
148141
appId,
@@ -155,8 +148,8 @@ export default class LightningDevApp extends SfCommand<void> {
155148
private async desktopPreview(
156149
sfdxProjectRootPath: string,
157150
serverPorts: { httpPort: number; httpsPort: number },
158-
token: string,
159-
entityId: string,
151+
ldpServerToken: string,
152+
ldpServerId: string,
160153
ldpServerUrl: string,
161154
appId: string | undefined,
162155
logger: Logger
@@ -191,13 +184,13 @@ export default class LightningDevApp extends SfCommand<void> {
191184

192185
const launchArguments = PreviewUtils.generateDesktopPreviewLaunchArguments(
193186
ldpServerUrl,
194-
entityId,
187+
ldpServerId,
195188
appId,
196189
targetOrg
197190
);
198191

199192
// Start the LWC Dev Server
200-
await startLWCServer(logger, sfdxProjectRootPath, token, Platform.desktop, serverPorts);
193+
await startLWCServer(logger, sfdxProjectRootPath, ldpServerToken, Platform.desktop, serverPorts);
201194

202195
// Open the browser and navigate to the right page
203196
await this.config.runCommand('org:open', launchArguments);
@@ -207,8 +200,8 @@ export default class LightningDevApp extends SfCommand<void> {
207200
platform: Platform.ios | Platform.android,
208201
sfdxProjectRootPath: string,
209202
serverPorts: { httpPort: number; httpsPort: number },
210-
token: string,
211-
entityId: string,
203+
ldpServerToken: string,
204+
ldpServerId: string,
212205
ldpServerUrl: string,
213206
appName: string | undefined,
214207
appId: string | undefined,
@@ -304,14 +297,13 @@ export default class LightningDevApp extends SfCommand<void> {
304297
}
305298

306299
// Start the LWC Dev Server
307-
308-
await startLWCServer(logger, sfdxProjectRootPath, token, platform, serverPorts, certData);
300+
await startLWCServer(logger, sfdxProjectRootPath, ldpServerToken, platform, serverPorts, certData);
309301

310302
// Launch the native app for previewing (launchMobileApp will show its own spinner)
311303
// eslint-disable-next-line camelcase
312304
appConfig.launch_arguments = PreviewUtils.generateMobileAppPreviewLaunchArguments(
313305
ldpServerUrl,
314-
entityId,
306+
ldpServerId,
315307
appName,
316308
appId
317309
);

src/commands/lightning/dev/site.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
99
import { Messages } from '@salesforce/core';
1010
import { expDev } from '@lwrjs/api';
1111
import { OrgUtils } from '../../../shared/orgUtils.js';
12-
import { PromptUtils } from '../../../shared/prompt.js';
12+
import { PromptUtils } from '../../../shared/promptUtils.js';
1313
import { ExperienceSite } from '../../../shared/experience/expSite.js';
1414

1515
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);

src/shared/configUtils.ts

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,10 @@
66
*/
77

88
import { Workspace } from '@lwc/lwc-dev-server';
9-
import { CryptoUtils, SSLCertificateData } from '@salesforce/lwc-dev-mobile-core';
9+
import { SSLCertificateData } from '@salesforce/lwc-dev-mobile-core';
1010
import { Config, ConfigAggregator } from '@salesforce/core';
1111
import configMeta, { ConfigVars, SerializedSSLCertificateData } from './../configMeta.js';
1212

13-
export type IdentityTokenService = {
14-
saveTokenToServer(token: string): Promise<string>;
15-
};
16-
1713
export const LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT = 8081;
1814
export const LOCAL_DEV_SERVER_DEFAULT_WORKSPACE = Workspace.SfCli;
1915

@@ -44,27 +40,16 @@ export class ConfigUtils {
4440
return this.#globalConfig;
4541
}
4642

47-
public static async getOrCreateIdentityToken(username: string, tokenService: IdentityTokenService): Promise<string> {
48-
let identityData = await this.getIdentityData();
49-
if (!identityData) {
50-
const token = CryptoUtils.generateIdentityToken();
51-
const entityId = await tokenService.saveTokenToServer(token);
52-
identityData = {
53-
identityToken: token,
54-
usernameToServerEntityIdMap: {},
55-
};
56-
identityData.usernameToServerEntityIdMap[username] = entityId;
57-
await this.writeIdentityData(identityData);
58-
return token;
59-
} else {
60-
let entityId = identityData.usernameToServerEntityIdMap[username];
61-
if (!entityId) {
62-
entityId = await tokenService.saveTokenToServer(identityData.identityToken);
63-
identityData.usernameToServerEntityIdMap[username] = entityId;
64-
await this.writeIdentityData(identityData);
65-
}
66-
return identityData.identityToken;
43+
public static async getIdentityData(): Promise<LocalWebServerIdentityData | undefined> {
44+
const config = await ConfigAggregator.create({ customConfigMeta: configMeta });
45+
// Need to reload to make sure the values read are decrypted
46+
await config.reload();
47+
const identityJson = config.getPropertyValue(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA);
48+
49+
if (identityJson) {
50+
return JSON.parse(identityJson as string) as LocalWebServerIdentityData;
6751
}
52+
return undefined;
6853
}
6954

7055
public static async writeIdentityData(identityData: LocalWebServerIdentityData): Promise<void> {
@@ -117,16 +102,4 @@ export class ConfigUtils {
117102

118103
return configWorkspace;
119104
}
120-
121-
public static async getIdentityData(): Promise<LocalWebServerIdentityData | undefined> {
122-
const config = await ConfigAggregator.create({ customConfigMeta: configMeta });
123-
// Need to reload to make sure the values read are decrypted
124-
await config.reload();
125-
const identityJson = config.getPropertyValue(ConfigVars.LOCAL_WEB_SERVER_IDENTITY_DATA);
126-
127-
if (identityJson) {
128-
return JSON.parse(identityJson as string) as LocalWebServerIdentityData;
129-
}
130-
return undefined;
131-
}
132105
}

src/shared/orgUtils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,20 @@ export class OrgUtils {
5555
const localDevEnabled = flagValue.toLowerCase().trim() === 'true';
5656
return localDevEnabled;
5757
}
58+
59+
/**
60+
* Saves an app server identity token to the UserLocalWebServerIdentity sObject in the org.
61+
*
62+
* @param connection the connection to the org
63+
* @param token the token value to be saved
64+
* @returns the id of the saved record
65+
*/
66+
public static async saveAppServerIdentityToken(connection: Connection, token: string): Promise<string> {
67+
const sobject = connection.sobject('UserLocalWebServerIdentity');
68+
const result = await sobject.insert({ LocalWebServerIdentityToken: token });
69+
if (result.success) {
70+
return result.id;
71+
}
72+
throw new Error('Could not save the app server identity token to the org.');
73+
}
5874
}

src/shared/previewUtils.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import fs from 'node:fs';
1414
import os from 'node:os';
1515
import path from 'node:path';
16-
import { Logger, Messages } from '@salesforce/core';
16+
import { Connection, Logger, Messages } from '@salesforce/core';
1717
import {
1818
AndroidDeviceManager,
1919
AppleDeviceManager,
@@ -27,7 +27,8 @@ import {
2727
} from '@salesforce/lwc-dev-mobile-core';
2828
import { Progress, Spinner } from '@salesforce/sf-plugins-core';
2929
import fetch from 'node-fetch';
30-
import { ConfigUtils, LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT } from './configUtils.js';
30+
import { ConfigUtils, LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT, LocalWebServerIdentityData } from './configUtils.js';
31+
import { OrgUtils } from './orgUtils.js';
3132

3233
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
3334
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.app');
@@ -111,15 +112,15 @@ export class PreviewUtils {
111112
* Generates the proper set of arguments to be used for launching desktop browser and navigating to the right location.
112113
*
113114
* @param ldpServerUrl The URL for the local dev server
114-
* @param entityId Record ID for the identity token
115+
* @param ldpServerId Record ID for the identity token
115116
* @param appId An optional app id for a targeted LEX app
116117
* @param targetOrg An optional org id
117118
* @param auraMode An optional Aura Mode (defaults to DEVPREVIEW)
118119
* @returns Array of arguments to be used by Org:Open command for launching desktop browser
119120
*/
120121
public static generateDesktopPreviewLaunchArguments(
121122
ldpServerUrl: string,
122-
entityId: string,
123+
ldpServerId: string,
123124
appId?: string,
124125
targetOrg?: string,
125126
auraMode = DevPreviewAuraMode
@@ -134,7 +135,7 @@ export class PreviewUtils {
134135
// we prepend a '0.' to all of the params to ensure they will persist across browser redirects
135136
const launchArguments = [
136137
'--path',
137-
`${appPath}?0.aura.ldpServerUrl=${ldpServerUrl}&0.aura.ldpServerId=${entityId}&0.aura.mode=${auraMode}`,
138+
`${appPath}?0.aura.ldpServerUrl=${ldpServerUrl}&0.aura.ldpServerId=${ldpServerId}&0.aura.mode=${auraMode}`,
138139
];
139140

140141
if (targetOrg) {
@@ -148,15 +149,15 @@ export class PreviewUtils {
148149
* Generates the proper set of arguments to be used for launching a mobile app with custom launch arguments.
149150
*
150151
* @param ldpServerUrl The URL for the local dev server
151-
* @param entityId Record ID for the identity token
152+
* @param ldpServerId Record ID for the identity token
152153
* @param appName An optional app name for a targeted LEX app
153154
* @param appId An optional app id for a targeted LEX app
154155
* @param auraMode An optional Aura Mode (defaults to DEVPREVIEW)
155156
* @returns Array of arguments to be used as custom launch arguments when launching a mobile app.
156157
*/
157158
public static generateMobileAppPreviewLaunchArguments(
158159
ldpServerUrl: string,
159-
entityId: string,
160+
ldpServerId: string,
160161
appName?: string,
161162
appId?: string,
162163
auraMode = DevPreviewAuraMode
@@ -175,7 +176,7 @@ export class PreviewUtils {
175176

176177
launchArguments.push({ name: 'aura.mode', value: auraMode });
177178

178-
launchArguments.push({ name: 'aura.ldpServerId', value: entityId });
179+
launchArguments.push({ name: 'aura.ldpServerId', value: ldpServerId });
179180

180181
return launchArguments;
181182
}
@@ -292,18 +293,28 @@ export class PreviewUtils {
292293
});
293294
}
294295

295-
public static async getEntityId(username: string): Promise<string> {
296-
const identityData = await ConfigUtils.getIdentityData();
297-
let entityId: string | undefined;
296+
public static async getOrCreateAppServerIdentity(connection: Connection): Promise<LocalWebServerIdentityData> {
297+
const username = connection.getUsername()!;
298+
299+
let identityData = await ConfigUtils.getIdentityData();
298300
if (!identityData) {
299-
return Promise.reject(new Error(messages.getMessage('error.identitydata')));
301+
const token = CryptoUtils.generateIdentityToken();
302+
const entityId = await OrgUtils.saveAppServerIdentityToken(connection, token);
303+
identityData = {
304+
identityToken: token,
305+
usernameToServerEntityIdMap: {},
306+
};
307+
identityData.usernameToServerEntityIdMap[username] = entityId;
308+
await ConfigUtils.writeIdentityData(identityData);
300309
} else {
301-
entityId = identityData.usernameToServerEntityIdMap[username];
310+
let entityId = identityData.usernameToServerEntityIdMap[username];
302311
if (!entityId) {
303-
return Promise.reject(new Error(messages.getMessage('error.identitydata.entityid')));
312+
entityId = await OrgUtils.saveAppServerIdentityToken(connection, identityData.identityToken);
313+
identityData.usernameToServerEntityIdMap[username] = entityId;
314+
await ConfigUtils.writeIdentityData(identityData);
304315
}
305-
return entityId;
306316
}
317+
return identityData;
307318
}
308319

309320
private static async doGetNextAvailablePort(startingPort: number): Promise<number> {

0 commit comments

Comments
 (0)