From 4f7bd7756075ee20d357cb09c36078f9ba2a3dfc Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Mon, 6 Feb 2023 13:04:11 -0500 Subject: [PATCH] [SG-900] Implement auto-fill callout (#4670) * Implement autofill callouts * Fix copy for dismissed callout * Delay closing popup after using callout auto-fill --- apps/browser/src/_locales/en/messages.json | 23 +++++++++++- apps/browser/src/popup/scss/buttons.scss | 5 +++ .../vault/current-tab.component.html | 21 +++++++++++ .../components/vault/current-tab.component.ts | 35 ++++++++++++++++--- libs/common/src/abstractions/state.service.ts | 2 ++ libs/common/src/models/domain/account.ts | 1 + libs/common/src/services/state.service.ts | 18 ++++++++++ 7 files changed, 99 insertions(+), 6 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index fa833a98af..75e93c0408 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -962,7 +962,7 @@ "experimentalFeature": { "message": "Compromised or untrusted websites can exploit auto-fill on page load." }, - "learnMoreAboutAutofill":{ + "learnMoreAboutAutofill": { "message": "Learn more about auto-fill" }, "defaultAutoFillOnPageLoad": { @@ -2103,5 +2103,26 @@ "example": "14" } } + }, + "tryAutofillPageLoad": { + "message": "Try auto-fill on page load?" + }, + "tryAutofill": { + "message": "How to auto-fill" + }, + "autofillPageLoadInfo": { + "message": "Login forms will automatically fill in matching credentials if you turn on auto-fill on page load." + }, + "autofillSelectInfo": { + "message": "Select an item from this page to auto-fill the active tab's form." + }, + "autofillTurnedOn": { + "message": "Auto-fill on page load turned on" + }, + "turnOn": { + "message": "Turn on" + }, + "notNow": { + "message": "Not now" } } diff --git a/apps/browser/src/popup/scss/buttons.scss b/apps/browser/src/popup/scss/buttons.scss index 8440dd211d..4c5141cf8f 100644 --- a/apps/browser/src/popup/scss/buttons.scss +++ b/apps/browser/src/popup/scss/buttons.scss @@ -26,6 +26,11 @@ } } + &.callout-half { + font-weight: bold; + max-width: 45%; + } + &:hover:not([disabled]) { cursor: pointer; diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.html b/apps/browser/src/vault/popup/components/vault/current-tab.component.html index 9ed3662bc5..f2cc030572 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.html +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.html @@ -36,6 +36,27 @@ + +

{{ "autofillPageLoadInfo" | i18n }}

+ + +
+ +

{{ "autofillSelectInfo" | i18n }}

+

{{ "typeLogins" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index dca76d3ac1..177c3124bb 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts @@ -42,6 +42,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy { loaded = false; isLoading = false; showOrganizations = false; + showTryAutofillOnPageLoad = false; + showSelectAutofillCallout = false; protected search$ = new Subject(); private destroy$ = new Subject(); @@ -112,6 +114,11 @@ export class CurrentTabComponent implements OnInit, OnDestroy { this.search$ .pipe(debounceTime(500), takeUntil(this.destroy$)) .subscribe(() => this.searchVault()); + + this.showTryAutofillOnPageLoad = + this.loginCiphers.length > 0 && + !(await this.stateService.getEnableAutoFillOnPageLoad()) && + !(await this.stateService.getDismissedAutofillCallout()); } ngOnDestroy() { @@ -140,7 +147,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy { this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } }); } - async fillCipher(cipher: CipherView) { + async fillCipher(cipher: CipherView, closePopupDelay?: number) { if ( cipher.reprompt !== CipherRepromptType.None && !(await this.passwordRepromptService.showPasswordPrompt()) @@ -170,11 +177,15 @@ export class CurrentTabComponent implements OnInit, OnDestroy { this.platformUtilsService.copyToClipboard(this.totpCode, { window: window }); } if (this.popupUtilsService.inPopup(window)) { - if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) { - BrowserApi.closePopup(window); + if (!closePopupDelay) { + if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) { + BrowserApi.closePopup(window); + } else { + // Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard + setTimeout(() => BrowserApi.closePopup(window), 50); + } } else { - // Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard - setTimeout(() => BrowserApi.closePopup(window), 50); + setTimeout(() => BrowserApi.closePopup(window), closePopupDelay); } } } catch { @@ -262,4 +273,18 @@ export class CurrentTabComponent implements OnInit, OnDestroy { ); this.isLoading = this.loaded = true; } + + async setAutofillOnPageLoad() { + await this.stateService.setEnableAutoFillOnPageLoad(true); + this.platformUtilsService.showToast("success", null, this.i18nService.t("autofillTurnedOn")); + await this.fillCipher(this.loginCiphers[0], 3000); + await this.stateService.setDismissedAutofillCallout(true); + this.showTryAutofillOnPageLoad = false; + } + + async notNow() { + await this.stateService.setDismissedAutofillCallout(true); + this.showTryAutofillOnPageLoad = false; + this.showSelectAutofillCallout = true; + } } diff --git a/libs/common/src/abstractions/state.service.ts b/libs/common/src/abstractions/state.service.ts index 4ffdec474c..859e36cc4e 100644 --- a/libs/common/src/abstractions/state.service.ts +++ b/libs/common/src/abstractions/state.service.ts @@ -142,6 +142,8 @@ export abstract class StateService { setDisableFavicon: (value: boolean, options?: StorageOptions) => Promise; getDisableGa: (options?: StorageOptions) => Promise; setDisableGa: (value: boolean, options?: StorageOptions) => Promise; + getDismissedAutofillCallout: (options?: StorageOptions) => Promise; + setDismissedAutofillCallout: (value: boolean, options?: StorageOptions) => Promise; getDontShowCardsCurrentTab: (options?: StorageOptions) => Promise; setDontShowCardsCurrentTab: (value: boolean, options?: StorageOptions) => Promise; getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/models/domain/account.ts b/libs/common/src/models/domain/account.ts index 3c4725b343..c2f240d75e 100644 --- a/libs/common/src/models/domain/account.ts +++ b/libs/common/src/models/domain/account.ts @@ -216,6 +216,7 @@ export class AccountSettings { disableChangedPasswordNotification?: boolean; disableContextMenuItem?: boolean; disableGa?: boolean; + dismissedAutoFillOnPageLoadCallout?: boolean; dontShowCardsCurrentTab?: boolean; dontShowIdentitiesCurrentTab?: boolean; enableAlwaysOnTop?: boolean; diff --git a/libs/common/src/services/state.service.ts b/libs/common/src/services/state.service.ts index 9c2b57988f..956bc64ddc 100644 --- a/libs/common/src/services/state.service.ts +++ b/libs/common/src/services/state.service.ts @@ -982,6 +982,24 @@ export class StateService< ); } + async getDismissedAutofillCallout(options?: StorageOptions): Promise { + return ( + (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.settings?.dismissedAutoFillOnPageLoadCallout ?? false + ); + } + + async setDismissedAutofillCallout(value: boolean, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + account.settings.dismissedAutoFillOnPageLoadCallout = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultOnDiskOptions()) + ); + } + async getDontShowCardsCurrentTab(options?: StorageOptions): Promise { return ( (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))