mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-11 10:10:25 +01:00
[PM-7306] Onboarding users to new UI (#10267)
* Added translation keys * created simple dialog (cherry picked from commit c12257cf51ca5e0d773a160afb6860a8f5df66b7) * added announcement svg (cherry picked from commit 635103120b500103b93dc1a8cbefc34dd396445b) * removed announcement svg, moved svg to component, refactored component (cherry picked from commit 50db6aa40fd90d92afeeb60e919f98f268bd69b5) * renamed state definition (cherry picked from commit 4c3618c46ee5ffab7050fbc9f6d779ee59c0c26b) * created vault ui onboarding service (cherry picked from commit 19ba3c42656d4f891ba3635da06f3ff7656b6028) * added vault ui dialog to vault component (cherry picked from commit 56527c8e5eda7df2f222ceebdeba168e2f1ae278) * moved updating the state to vault component * updated the link and fixed minor issues * moved onboarding logic from component to service and fixed review comments
This commit is contained in:
parent
973aba1bf4
commit
81212deaad
@ -4014,5 +4014,11 @@
|
||||
},
|
||||
"itemLocation": {
|
||||
"message": "Item Location"
|
||||
},
|
||||
"bitwardenNewLook": {
|
||||
"message": "Bitwarden has a new look!"
|
||||
},
|
||||
"bitwardenNewLookDesc": {
|
||||
"message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
IconModule,
|
||||
svgIcon,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
const announcementIcon = svgIcon`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="86" height="74" fill="none">
|
||||
<g fill-rule="evenodd" clip-path="url(#a)" clip-rule="evenodd">
|
||||
<path class="tw-fill-text-headers" d="m17.477 51.274 2.472 17.441a3.779 3.779 0 0 0 4.583 3.154l1.497-.342a3.779 3.779 0 0 0 2.759-4.831L23.44 49.91l1.8-.573 5.348 16.784a5.668 5.668 0 0 1-4.138 7.247l-1.497.341a5.668 5.668 0 0 1-6.874-4.73l-2.473-17.44 1.871-.266Z"/>
|
||||
<path class="tw-fill-info-600" d="m55.063 27.1-1.38.316-.211-.92 1.381-.316a3.306 3.306 0 0 1 3.96 2.486l1.052 4.605a3.306 3.306 0 0 1-2.487 3.96l-.92.21-.211-.92.92-.211a2.362 2.362 0 0 0 1.777-2.828l-1.052-4.605a2.362 2.362 0 0 0-2.829-1.777Z"/>
|
||||
<path class="tw-fill-text-headers" d="M49.79 12.5a.18.18 0 0 0-.272-.11L21.855 29.438a.181.181 0 0 0-.058.055l-.208.323-10.947 2.5a.457.457 0 0 0-.139.064.664.664 0 0 0-.15.135.343.343 0 0 0-.06.095l.499 2.182-4.36.996c-1.873.428-3.086 2.465-2.64 4.417l1.5 6.566c.446 1.951 2.423 3.26 4.296 2.832l4.36-.996.499 2.182c.012.012.04.034.095.06a.658.658 0 0 0 .194.055c.07.009.122.004.152-.003l10.947-2.501.328.2a.18.18 0 0 0 .075.025l32.324 3.344a.18.18 0 0 0 .196-.218L49.79 12.5Zm-1.263-1.72a2.07 2.07 0 0 1 3.104 1.299L60.6 51.332a2.07 2.07 0 0 1-2.233 2.517l-32.323-3.343a2.072 2.072 0 0 1-.474-.106l-10.26 2.344a2.474 2.474 0 0 1-1.571-.184c-.463-.217-.973-.643-1.127-1.32l-.085-.37-2.518.576c-2.975.68-5.9-1.37-6.559-4.253l-1.5-6.566c-.659-2.883 1.086-6 4.061-6.68l2.518-.575-.084-.37c-.155-.677.12-1.282.442-1.678.325-.4.803-.727 1.334-.848l10.262-2.345c.113-.113.24-.214.38-.3l27.664-17.05Z"/>
|
||||
<path class="tw-fill-info-600" d="m10.792 34.793 3.156 13.814-.92.21L9.87 35.004l.921-.21ZM21.59 29.817l4.246 18.578-.508.12-.512.118L20.68 30.02l.91-.203Z"/>
|
||||
<path class="tw-fill-text-headers" d="M64.287.59A.945.945 0 0 1 65.58.248c8.784 5.11 15.628 14.039 18.166 25.145 2.537 11.105.25 22.12-5.443 30.538a.945.945 0 0 1-1.565-1.059c5.398-7.98 7.587-18.46 5.166-29.058C79.48 15.215 72.958 6.726 64.629 1.882A.945.945 0 0 1 64.287.59Z"/>
|
||||
<path class="tw-fill-info-600" d="M61.6 6.385a.472.472 0 0 1 .643-.18c7.245 4.067 12.949 11.44 15.055 20.66s.171 18.338-4.588 25.149a.472.472 0 0 1-.774-.542c4.603-6.587 6.49-15.431 4.441-24.397-2.048-8.965-7.59-16.113-14.596-20.047a.472.472 0 0 1-.18-.643Z"/>
|
||||
<path class="tw-fill-text-headers" d="M57.804 11.193a.472.472 0 0 1 .604-.285c6.11 2.186 11.426 8.739 13.364 17.22 1.938 8.48-.006 16.693-4.56 21.315a.472.472 0 1 1-.672-.663c4.27-4.335 6.197-12.187 4.311-20.442-1.886-8.254-7.032-14.49-12.761-16.54a.472.472 0 0 1-.286-.605Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h86v74H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-vault-ui-onboarding",
|
||||
template: `
|
||||
<bit-simple-dialog>
|
||||
<div bitDialogIcon>
|
||||
<bit-icon [icon]="icon"></bit-icon>
|
||||
</div>
|
||||
<span bitDialogTitle>
|
||||
{{ "bitwardenNewLook" | i18n }}
|
||||
</span>
|
||||
<span bitDialogContent>
|
||||
{{ "bitwardenNewLookDesc" | i18n }}
|
||||
</span>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<a bitButton buttonType="primary" bitDialogClose (click)="navigateToLink()">
|
||||
{{ "learnMore" | i18n }}
|
||||
<i class="bwi bwi-external-link bwi-fw" aria-hidden="true"></i>
|
||||
</a>
|
||||
<button bitButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
`,
|
||||
imports: [CommonModule, DialogModule, ButtonModule, JslibModule, IconModule],
|
||||
})
|
||||
export class VaultUiOnboardingComponent {
|
||||
icon = announcementIcon;
|
||||
|
||||
static open(dialogService: DialogService) {
|
||||
return dialogService.open<boolean>(VaultUiOnboardingComponent);
|
||||
}
|
||||
|
||||
navigateToLink = async () => {
|
||||
window.open(
|
||||
"https://bitwarden.com/blog/bringing-intuitive-workflows-and-visual-updates-to-the-bitwarden-browser/",
|
||||
"_blank",
|
||||
);
|
||||
};
|
||||
}
|
@ -15,6 +15,7 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he
|
||||
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
|
||||
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
|
||||
import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service";
|
||||
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
|
||||
import {
|
||||
NewItemDropdownV2Component,
|
||||
@ -49,9 +50,11 @@ enum VaultState {
|
||||
VaultV2SearchComponent,
|
||||
NewItemDropdownV2Component,
|
||||
],
|
||||
providers: [VaultUiOnboardingService],
|
||||
})
|
||||
export class VaultV2Component implements OnInit, OnDestroy {
|
||||
cipherType = CipherType;
|
||||
|
||||
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
|
||||
protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$;
|
||||
protected loading$ = this.vaultPopupItemsService.loading$;
|
||||
@ -79,6 +82,7 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private vaultPopupItemsService: VaultPopupItemsService,
|
||||
private vaultPopupListFiltersService: VaultPopupListFiltersService,
|
||||
private vaultUiOnboardingService: VaultUiOnboardingService,
|
||||
) {
|
||||
combineLatest([
|
||||
this.vaultPopupItemsService.emptyVault$,
|
||||
@ -104,7 +108,9 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
async ngOnInit() {
|
||||
await this.vaultUiOnboardingService.showOnboardingDialog();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
}
|
||||
|
@ -0,0 +1,88 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import {
|
||||
GlobalState,
|
||||
KeyDefinition,
|
||||
StateProvider,
|
||||
VAULT_BROWSER_UI_ONBOARDING,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { VaultUiOnboardingComponent } from "../components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component";
|
||||
|
||||
// Key definition for the Vault UI onboarding state.
|
||||
// This key is used to store the state of the new UI information dialog.
|
||||
export const GLOBAL_VAULT_UI_ONBOARDING = new KeyDefinition<boolean>(
|
||||
VAULT_BROWSER_UI_ONBOARDING,
|
||||
"dialogState",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class VaultUiOnboardingService {
|
||||
// TODO: Update this date to the release date of the new Browser UI
|
||||
private onboardingUiReleaseDate = new Date("2024-07-25");
|
||||
|
||||
private vaultUiOnboardingState: GlobalState<boolean> = this.stateProvider.getGlobal(
|
||||
GLOBAL_VAULT_UI_ONBOARDING,
|
||||
);
|
||||
|
||||
private readonly vaultUiOnboardingState$ = this.vaultUiOnboardingState.state$.pipe(
|
||||
map((x) => x ?? false),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private dialogService: DialogService,
|
||||
private apiService: ApiService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Checks whether the onboarding dialog should be shown and opens it if necessary.
|
||||
* The dialog is shown if the user has not previously viewed it and is not a new account.
|
||||
*/
|
||||
async showOnboardingDialog(): Promise<void> {
|
||||
const hasViewedDialog = await this.getVaultUiOnboardingState();
|
||||
|
||||
if (!hasViewedDialog && !(await this.isNewAccount())) {
|
||||
await this.openVaultUiOnboardingDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private async openVaultUiOnboardingDialog(): Promise<boolean> {
|
||||
const dialogRef = VaultUiOnboardingComponent.open(this.dialogService);
|
||||
|
||||
const result = firstValueFrom(dialogRef.closed);
|
||||
|
||||
// Update the onboarding state when the dialog is closed
|
||||
await this.setVaultUiOnboardingState(true);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async isNewAccount(): Promise<boolean> {
|
||||
const userProfile = await this.apiService.getProfile();
|
||||
const profileCreationDate = new Date(userProfile.creationDate);
|
||||
return profileCreationDate > this.onboardingUiReleaseDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates and saves the state indicating whether the user has viewed
|
||||
* the new UI onboarding information dialog.
|
||||
*/
|
||||
private async setVaultUiOnboardingState(value: boolean): Promise<void> {
|
||||
await this.vaultUiOnboardingState.update(() => value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current state indicating whether the user has viewed
|
||||
* the new UI onboarding information dialog.s
|
||||
*/
|
||||
private async getVaultUiOnboardingState(): Promise<boolean> {
|
||||
return await firstValueFrom(this.vaultUiOnboardingState$);
|
||||
}
|
||||
}
|
@ -165,3 +165,4 @@ export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerRepro
|
||||
web: "disk-local",
|
||||
});
|
||||
export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk");
|
||||
export const VAULT_BROWSER_UI_ONBOARDING = new StateDefinition("vaultBrowserUiOnboarding", "disk");
|
||||
|
Loading…
Reference in New Issue
Block a user