[SG-900] Implement auto-fill callout (#4670)

* Implement autofill callouts

* Fix copy for dismissed callout

* Delay closing popup after using callout auto-fill
This commit is contained in:
Robyn MacCallum 2023-02-06 13:04:11 -05:00 committed by GitHub
parent 4ffe1c7e57
commit 4f7bd77560
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 99 additions and 6 deletions

View File

@ -2103,5 +2103,26 @@
"example": "14" "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"
} }
} }

View File

@ -26,6 +26,11 @@
} }
} }
&.callout-half {
font-weight: bold;
max-width: 45%;
}
&:hover:not([disabled]) { &:hover:not([disabled]) {
cursor: pointer; cursor: pointer;

View File

@ -36,6 +36,27 @@
</div> </div>
<ng-container *ngIf="loaded"> <ng-container *ngIf="loaded">
<app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select> <app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select>
<app-callout
*ngIf="showTryAutofillOnPageLoad"
type="info"
title="{{ 'tryAutofillPageLoad' | i18n }}"
>
<p>{{ "autofillPageLoadInfo" | i18n }}</p>
<button
type="button"
class="btn primary callout-half"
appStopClick
(click)="setAutofillOnPageLoad()"
>
{{ "turnOn" | i18n }}
</button>
<button type="button" class="btn callout-half" appStopClick (click)="notNow()">
{{ "notNow" | i18n }}
</button>
</app-callout>
<app-callout *ngIf="showSelectAutofillCallout" type="info" title="{{ 'tryAutofill' | i18n }}">
<p>{{ "autofillSelectInfo" | i18n }}</p>
</app-callout>
<div class="box list" *ngIf="loginCiphers"> <div class="box list" *ngIf="loginCiphers">
<h2 class="box-header"> <h2 class="box-header">
{{ "typeLogins" | i18n }} {{ "typeLogins" | i18n }}

View File

@ -42,6 +42,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
loaded = false; loaded = false;
isLoading = false; isLoading = false;
showOrganizations = false; showOrganizations = false;
showTryAutofillOnPageLoad = false;
showSelectAutofillCallout = false;
protected search$ = new Subject<void>(); protected search$ = new Subject<void>();
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -112,6 +114,11 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
this.search$ this.search$
.pipe(debounceTime(500), takeUntil(this.destroy$)) .pipe(debounceTime(500), takeUntil(this.destroy$))
.subscribe(() => this.searchVault()); .subscribe(() => this.searchVault());
this.showTryAutofillOnPageLoad =
this.loginCiphers.length > 0 &&
!(await this.stateService.getEnableAutoFillOnPageLoad()) &&
!(await this.stateService.getDismissedAutofillCallout());
} }
ngOnDestroy() { ngOnDestroy() {
@ -140,7 +147,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } }); this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } });
} }
async fillCipher(cipher: CipherView) { async fillCipher(cipher: CipherView, closePopupDelay?: number) {
if ( if (
cipher.reprompt !== CipherRepromptType.None && cipher.reprompt !== CipherRepromptType.None &&
!(await this.passwordRepromptService.showPasswordPrompt()) !(await this.passwordRepromptService.showPasswordPrompt())
@ -170,12 +177,16 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window }); this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
} }
if (this.popupUtilsService.inPopup(window)) { if (this.popupUtilsService.inPopup(window)) {
if (!closePopupDelay) {
if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) { if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) {
BrowserApi.closePopup(window); BrowserApi.closePopup(window);
} else { } else {
// Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard // 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), 50);
} }
} else {
setTimeout(() => BrowserApi.closePopup(window), closePopupDelay);
}
} }
} catch { } catch {
this.ngZone.run(() => { this.ngZone.run(() => {
@ -262,4 +273,18 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
); );
this.isLoading = this.loaded = true; 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;
}
} }

View File

@ -142,6 +142,8 @@ export abstract class StateService<T extends Account = Account> {
setDisableFavicon: (value: boolean, options?: StorageOptions) => Promise<void>; setDisableFavicon: (value: boolean, options?: StorageOptions) => Promise<void>;
getDisableGa: (options?: StorageOptions) => Promise<boolean>; getDisableGa: (options?: StorageOptions) => Promise<boolean>;
setDisableGa: (value: boolean, options?: StorageOptions) => Promise<void>; setDisableGa: (value: boolean, options?: StorageOptions) => Promise<void>;
getDismissedAutofillCallout: (options?: StorageOptions) => Promise<boolean>;
setDismissedAutofillCallout: (value: boolean, options?: StorageOptions) => Promise<void>;
getDontShowCardsCurrentTab: (options?: StorageOptions) => Promise<boolean>; getDontShowCardsCurrentTab: (options?: StorageOptions) => Promise<boolean>;
setDontShowCardsCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>; setDontShowCardsCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise<boolean>; getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise<boolean>;

View File

@ -216,6 +216,7 @@ export class AccountSettings {
disableChangedPasswordNotification?: boolean; disableChangedPasswordNotification?: boolean;
disableContextMenuItem?: boolean; disableContextMenuItem?: boolean;
disableGa?: boolean; disableGa?: boolean;
dismissedAutoFillOnPageLoadCallout?: boolean;
dontShowCardsCurrentTab?: boolean; dontShowCardsCurrentTab?: boolean;
dontShowIdentitiesCurrentTab?: boolean; dontShowIdentitiesCurrentTab?: boolean;
enableAlwaysOnTop?: boolean; enableAlwaysOnTop?: boolean;

View File

@ -982,6 +982,24 @@ export class StateService<
); );
} }
async getDismissedAutofillCallout(options?: StorageOptions): Promise<boolean> {
return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.settings?.dismissedAutoFillOnPageLoadCallout ?? false
);
}
async setDismissedAutofillCallout(value: boolean, options?: StorageOptions): Promise<void> {
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<boolean> { async getDontShowCardsCurrentTab(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))