diff --git a/change/@azure-msal-browser-16a16e31-dd7f-4b5a-9bee-ce9b9d39eeee.json b/change/@azure-msal-browser-16a16e31-dd7f-4b5a-9bee-ce9b9d39eeee.json new file mode 100644 index 0000000000..e4eb498023 --- /dev/null +++ b/change/@azure-msal-browser-16a16e31-dd7f-4b5a-9bee-ce9b9d39eeee.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add isExtensionAvailable() API (#7535)", + "packageName": "@azure/msal-browser", + "email": "sameera.gajjarapu@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-browser/apiReview/msal-browser.api.md b/lib/msal-browser/apiReview/msal-browser.api.md index 385e9c06a5..5a0725a8e3 100644 --- a/lib/msal-browser/apiReview/msal-browser.api.md +++ b/lib/msal-browser/apiReview/msal-browser.api.md @@ -929,6 +929,8 @@ export interface IController { // @internal (undocumented) isBrowserEnv(): boolean; // (undocumented) + isExtensionAvailable(): Promise; + // (undocumented) loginPopup(request?: PopupRequest): Promise; // (undocumented) loginRedirect(request?: RedirectRequest): Promise; @@ -1414,8 +1416,8 @@ export class PublicClientApplication implements IPublicClientApplication { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen addEventCallback(callback: EventCallbackFunction, eventTypes?: Array): string | null; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" addPerformanceCallback(callback: PerformanceCallbackFunction): string; @@ -1473,8 +1475,8 @@ export class PublicClientApplication implements IPublicClientApplication { logoutRedirect(logoutRequest?: EndSessionRequest): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen removeEventCallback(callbackId: string): void; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" removePerformanceCallback(callbackId: string): boolean; diff --git a/lib/msal-browser/src/app/PublicClientApplication.ts b/lib/msal-browser/src/app/PublicClientApplication.ts index e9b9e8e36f..b8aa63645b 100644 --- a/lib/msal-browser/src/app/PublicClientApplication.ts +++ b/lib/msal-browser/src/app/PublicClientApplication.ts @@ -95,6 +95,14 @@ export class PublicClientApplication implements IPublicClientApplication { return this.controller.initialize(request); } + /** + * API to check for and initialize an extension if available and returns a boolean dependent on the result + * @returns Promise + */ + async isExtensionAvailable(): Promise { + return this.controller.isExtensionAvailable(); + } + /** * Use when you want to obtain an access_token for your API via opening a popup window in the user's browser * diff --git a/lib/msal-browser/src/controllers/IController.ts b/lib/msal-browser/src/controllers/IController.ts index 856f4abc38..7ba37096ec 100644 --- a/lib/msal-browser/src/controllers/IController.ts +++ b/lib/msal-browser/src/controllers/IController.ts @@ -116,6 +116,8 @@ export interface IController { | PopupRequest ): Promise; + isExtensionAvailable(): Promise; + /** @internal */ isBrowserEnv(): boolean; diff --git a/lib/msal-browser/src/controllers/NestedAppAuthController.ts b/lib/msal-browser/src/controllers/NestedAppAuthController.ts index 25966a6fc5..3973238897 100644 --- a/lib/msal-browser/src/controllers/NestedAppAuthController.ts +++ b/lib/msal-browser/src/controllers/NestedAppAuthController.ts @@ -163,6 +163,14 @@ export class NestedAppAuthController implements IController { return Promise.resolve(); } + /** + * Expected to not be available in nested app auth + * @returns boolean + */ + async isExtensionAvailable(): Promise { + return Promise.resolve(false); + } + /** * Validate the incoming request and add correlationId if not present * @param request diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index 51c1cfc2ac..b2ec6cba14 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -311,6 +311,31 @@ export class StandardController implements IController { ); } + /** + * Helper API to check for extension availability + * @returns {Promise} true if browser extension is available, false otherwise + * + */ + async isExtensionAvailable(): Promise { + // nativeExtensionProvider is already set + if (this.nativeExtensionProvider) { + return true; + } + + // Intialize nativeExtensionProvider if not already done + try { + this.nativeExtensionProvider = + await NativeMessageHandler.createProvider( + this.logger, + this.config.system.nativeBrokerHandshakeTimeout, + this.performanceClient + ); + } catch (e) { + this.logger.verbose(e as string); + } + return !!this.nativeExtensionProvider; + } + /** * Initializer function to perform async startup tasks such as connecting to WAM extension * @param request {?InitializeApplicationRequest} correlation id @@ -349,15 +374,14 @@ export class StandardController implements IController { )(initCorrelationId); if (allowPlatformBroker) { + // check if native message handler is available try { - this.nativeExtensionProvider = - await NativeMessageHandler.createProvider( - this.logger, - this.config.system.nativeBrokerHandshakeTimeout, - this.performanceClient - ); + await this.isExtensionAvailable(); } catch (e) { - this.logger.verbose(e as string); + this.logger.verbose( + "Error in checking extension availability: ", + e as string + ); } } diff --git a/lib/msal-browser/src/controllers/UnknownOperatingContextController.ts b/lib/msal-browser/src/controllers/UnknownOperatingContextController.ts index b361987049..3f29964f65 100644 --- a/lib/msal-browser/src/controllers/UnknownOperatingContextController.ts +++ b/lib/msal-browser/src/controllers/UnknownOperatingContextController.ts @@ -141,6 +141,9 @@ export class UnknownOperatingContextController implements IController { this.initialized = true; return Promise.resolve(); } + isExtensionAvailable(): Promise { + return Promise.resolve(false); + } // eslint-disable-next-line @typescript-eslint/no-unused-vars acquireTokenPopup(request: PopupRequest): Promise { blockAPICallsBeforeInitialize(this.initialized); diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 4b22e4f324..d82a7adc08 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -452,6 +452,30 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { ); }); + it("creates extension provider if the user checks for extension presence", async () => { + const config = { + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + system: { + allowPlatformBroker: false, + }, + }; + pca = new PublicClientApplication(config); + + //Implementation of PCA was moved to controller. + pca = (pca as any).controller; + + const createProviderSpy = stubProvider(config); + await pca.isExtensionAvailable(); + + expect(createProviderSpy).toHaveBeenCalled(); + // @ts-ignore + expect(pca.nativeExtensionProvider).toBeInstanceOf( + NativeMessageHandler + ); + }); + it("does not create extension provider if allowPlatformBroker is false", async () => { const createProviderSpy = jest.spyOn( NativeMessageHandler,