From 74cb594efee8acea67151721efec3e09ced62398 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Mon, 7 Oct 2024 15:05:53 -0700 Subject: [PATCH] New "Use a new account" option when switching preferences This option allows you to sign in to a new account and use if for an extension all in the same flow: insert pic Fixes https://github.com/microsoft/vscode/issues/229496 --- .../api/browser/mainThreadAuthentication.ts | 2 +- ...ageAccountPreferencesForExtensionAction.ts | 60 ++++++++++++++++--- .../authenticationExtensionsService.ts | 2 +- .../browser/authenticationUsageService.ts | 7 ++- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 24e7fdced8c9f..b2eaa529be6c6 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -289,7 +289,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu if (session) { this.sendProviderUsageTelemetry(extensionId, providerId); - this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, scopes, extensionId, extensionName); } return session; diff --git a/src/vs/workbench/contrib/authentication/browser/actions/manageAccountPreferencesForExtensionAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/manageAccountPreferencesForExtensionAction.ts index bd24203b50247..e86aa27e10981 100644 --- a/src/vs/workbench/contrib/authentication/browser/actions/manageAccountPreferencesForExtensionAction.ts +++ b/src/vs/workbench/contrib/authentication/browser/actions/manageAccountPreferencesForExtensionAction.ts @@ -8,8 +8,9 @@ import { DisposableStore, IDisposable } from '../../../../../base/common/lifecyc import { localize, localize2 } from '../../../../../nls.js'; import { Action2 } from '../../../../../platform/actions/common/actions.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; import { IQuickInputService, IQuickPick, IQuickPickItem, QuickPickInput } from '../../../../../platform/quickinput/common/quickInput.js'; -import { IAuthenticationUsageService } from '../../../../services/authentication/browser/authenticationUsageService.js'; +import { IAccountUsage, IAuthenticationUsageService } from '../../../../services/authentication/browser/authenticationUsageService.js'; import { AuthenticationSessionAccount, IAuthenticationExtensionsService, IAuthenticationService } from '../../../../services/authentication/common/authentication.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; @@ -28,8 +29,17 @@ export class ManageAccountPreferencesForExtensionAction extends Action2 { } } -interface AccountPreferenceQuickPickItem extends IQuickPickItem { +type AccountPreferenceQuickPickItem = NewAccountQuickPickItem | ExistingAccountQuickPickItem; + +interface NewAccountQuickPickItem extends IQuickPickItem { + account?: undefined; + scopes: string[]; + providerId: string; +} + +interface ExistingAccountQuickPickItem extends IQuickPickItem { account: AuthenticationSessionAccount; + scopes?: undefined; providerId: string; } @@ -39,7 +49,8 @@ class ManageAccountPreferenceForExtensionActionImpl { @IQuickInputService private readonly _quickInputService: IQuickInputService, @IAuthenticationUsageService private readonly _authenticationUsageService: IAuthenticationUsageService, @IAuthenticationExtensionsService private readonly _authenticationExtensionsService: IAuthenticationExtensionsService, - @IExtensionService private readonly _extensionService: IExtensionService + @IExtensionService private readonly _extensionService: IExtensionService, + @ILogService private readonly _logService: ILogService ) { } async run(extensionId?: string, providerId?: string) { @@ -52,7 +63,7 @@ class ManageAccountPreferenceForExtensionActionImpl { } const providerIds = new Array(); - const providerIdToAccounts = new Map>(); + const providerIdToAccounts = new Map>(); if (providerId) { providerIds.push(providerId); providerIdToAccounts.set(providerId, await this._authenticationService.getAccounts(providerId)); @@ -90,7 +101,27 @@ class ManageAccountPreferenceForExtensionActionImpl { } const currentAccountNamePreference = this._authenticationExtensionsService.getAccountPreference(extensionId, chosenProviderId); - const items: Array> = this._getItems(providerIdToAccounts.get(chosenProviderId)!, chosenProviderId, currentAccountNamePreference); + const accounts = providerIdToAccounts.get(chosenProviderId)!; + const items: Array> = this._getItems(accounts, chosenProviderId, currentAccountNamePreference); + + // If the provider supports multiple accounts, add an option to use a new account + const provider = this._authenticationService.getProvider(chosenProviderId); + if (provider.supportsMultipleAccounts) { + // Get the last used scopes for the last used account. This will be used to pre-fill the scopes when adding a new account. + // If there's no scopes, then don't add this option. + const lastUsedScopes = accounts + .flatMap(account => this._authenticationUsageService.readAccountUsages(chosenProviderId!, account.label).find(u => u.extensionId === extensionId.toLowerCase())) + .filter((usage): usage is IAccountUsage => !!usage) + .sort((a, b) => b.lastUsed - a.lastUsed)?.[0]?.scopes; + if (lastUsedScopes) { + items.push({ type: 'separator' }); + items.push({ + providerId: chosenProviderId, + scopes: lastUsedScopes, + label: localize('use new account', "Use a new account..."), + }); + } + } const disposables = new DisposableStore(); const picker = this._createQuickPick(disposables, extensionId, extension.displayName ?? extension.name); @@ -111,9 +142,9 @@ class ManageAccountPreferenceForExtensionActionImpl { picker.placeholder = localize('placeholder', "Manage '{0}' account preferences...", extensionLabel); picker.title = localize('title', "'{0}' Account Preferences For This Workspace", extensionLabel); picker.sortByLabel = false; - disposableStore.add(picker.onDidAccept(() => { - this._accept(extensionId, picker.selectedItems); + disposableStore.add(picker.onDidAccept(async () => { picker.hide(); + await this._accept(extensionId, picker.selectedItems); })); return picker; } @@ -142,9 +173,20 @@ class ManageAccountPreferenceForExtensionActionImpl { return Event.filter(picker.onDidTriggerButton, (e) => e === this._quickInputService.backButton)(() => this.run()); } - private _accept(extensionId: string, selectedItems: ReadonlyArray) { + private async _accept(extensionId: string, selectedItems: ReadonlyArray) { for (const item of selectedItems) { - const account = item.account; + let account: AuthenticationSessionAccount; + if (!item.account) { + try { + const session = await this._authenticationService.createSession(item.providerId, item.scopes); + account = session.account; + } catch (e) { + this._logService.error(e); + continue; + } + } else { + account = item.account; + } const providerId = item.providerId; const currentAccountName = this._authenticationExtensionsService.getAccountPreference(extensionId, providerId); if (currentAccountName === account.label) { diff --git a/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts b/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts index df7c921e67a0b..12a87e9b79d46 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts @@ -372,7 +372,7 @@ export class AuthenticationExtensionsService extends Disposable implements IAuth } if (session) { - this._authenticationUsageService.addAccountUsage(provider.id, session.account.label, extensionId, extensionName); + this._authenticationUsageService.addAccountUsage(provider.id, session.account.label, session.scopes, extensionId, extensionName); } } diff --git a/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts b/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts index a261d57ac38e1..514e1ce72da75 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts @@ -16,6 +16,7 @@ export interface IAccountUsage { extensionId: string; extensionName: string; lastUsed: number; + scopes?: string[]; } export const IAuthenticationUsageService = createDecorator('IAuthenticationUsageService'); @@ -49,7 +50,7 @@ export interface IAuthenticationUsageService { * @param extensionId The id of the extension to add a usage for * @param extensionName The name of the extension to add a usage for */ - addAccountUsage(providerId: string, accountName: string, extensionId: string, extensionName: string): void; + addAccountUsage(providerId: string, accountName: string, scopes: ReadonlyArray, extensionId: string, extensionName: string): void; } export class AuthenticationUsageService extends Disposable implements IAuthenticationUsageService { @@ -116,7 +117,7 @@ export class AuthenticationUsageService extends Disposable implements IAuthentic this._storageService.remove(accountKey, StorageScope.APPLICATION); } - addAccountUsage(providerId: string, accountName: string, extensionId: string, extensionName: string): void { + addAccountUsage(providerId: string, accountName: string, scopes: string[], extensionId: string, extensionName: string): void { const accountKey = `${providerId}-${accountName}-usages`; const usages = this.readAccountUsages(providerId, accountName); @@ -125,12 +126,14 @@ export class AuthenticationUsageService extends Disposable implements IAuthentic usages.splice(existingUsageIndex, 1, { extensionId, extensionName, + scopes, lastUsed: Date.now() }); } else { usages.push({ extensionId, extensionName, + scopes, lastUsed: Date.now() }); }