mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-22180] Setup Extension Videos (#15419)
* remove placeholder image * add videos for setup extension * add support for mobile viewports * add mobile/responsiveness for setup extension page * add videos from `assets.bitwarden.com` * align with figma for borders and shadow * make text responsive for setup headings * remove period * add tests * add tests for video sequence * force font weight on `h2` * add 8px to bottom margin of video container
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
<ng-template #newLoginItem>
|
||||||
|
<video
|
||||||
|
#video
|
||||||
|
muted
|
||||||
|
[ngClass]="{ 'tw-opacity-0': !allVideosLoaded }"
|
||||||
|
[attr.aria-hidden]="!allVideosLoaded"
|
||||||
|
class="tw-block tw-max-w-full tw-shadow-md tw-rounded-lg"
|
||||||
|
(loadeddata)="onVideoLoad()"
|
||||||
|
src="https://assets.bitwarden.com/extension-animations/new-login-item.mp4"
|
||||||
|
appDarkImgSrc="https://assets.bitwarden.com/extension-animations/new-login-item-dark.mp4"
|
||||||
|
aria-hidden
|
||||||
|
></video>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #browserExtensionEasyAccess>
|
||||||
|
<video
|
||||||
|
#video
|
||||||
|
muted
|
||||||
|
[ngClass]="{ 'tw-opacity-0': !allVideosLoaded }"
|
||||||
|
[attr.aria-hidden]="!allVideosLoaded"
|
||||||
|
class="tw-block tw-max-w-full tw-shadow-md tw-rounded-lg"
|
||||||
|
(loadeddata)="onVideoLoad()"
|
||||||
|
src="https://assets.bitwarden.com/extension-animations/browser-extension-easy-access.mp4"
|
||||||
|
appDarkImgSrc="https://assets.bitwarden.com/extension-animations/browser-extension-easy-access-dark.mp4"
|
||||||
|
aria-hidden
|
||||||
|
></video>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #onboardingAutofill>
|
||||||
|
<video
|
||||||
|
#video
|
||||||
|
muted
|
||||||
|
[ngClass]="{ 'tw-opacity-0': !allVideosLoaded }"
|
||||||
|
[attr.aria-hidden]="!allVideosLoaded"
|
||||||
|
class="tw-block tw-max-w-full tw-shadow-md tw-rounded-lg"
|
||||||
|
(loadeddata)="onVideoLoad()"
|
||||||
|
src="https://assets.bitwarden.com/extension-animations/onboarding-autofill.mp4"
|
||||||
|
appDarkImgSrc="https://assets.bitwarden.com/extension-animations/onboarding-autofill-dark.mp4"
|
||||||
|
aria-hidden
|
||||||
|
></video>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
On desktop, videos are shown in a 1 row, 3 column grid.
|
||||||
|
Below tablet viewports, videos are shown one at a time. The videos are absolute positioned, except the first.
|
||||||
|
The first video is relatively positioned to force the layout and spacing of the videos.
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="tw-mx-auto tw-w-[15rem] tw-mb-8 tw-relative md:tw-grid md:tw-gap-10 md:tw-w-auto md:tw-grid-rows-1 md:tw-grid-cols-3 md:tw-justify-center md:tw-justify-items-center"
|
||||||
|
[attr.aria-label]="'setupExtensionContentAlt' | i18n"
|
||||||
|
>
|
||||||
|
<div class="tw-relative tw-w-[15rem] tw-max-w-full tw-aspect-[0.807]">
|
||||||
|
<div
|
||||||
|
*ngIf="!allVideosLoaded"
|
||||||
|
class="tw-animate-pulse tw-bg-secondary-300 tw-size-full tw-absolute tw-left-0 tw-top-0 tw-rounded-lg"
|
||||||
|
data-testid="video-pulse"
|
||||||
|
></div>
|
||||||
|
<ng-container *ngTemplateOutlet="newLoginItem"></ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="tw-absolute tw-left-0 tw-top-0 tw-opacity-0 md:tw-opacity-100 md:tw-relative tw-w-[15rem] tw-max-w-full tw-aspect-[0.807]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
*ngIf="!allVideosLoaded"
|
||||||
|
class="tw-animate-pulse tw-bg-secondary-300 tw-size-full tw-absolute tw-left-0 tw-top-0 tw-rounded-lg"
|
||||||
|
data-testid="video-pulse"
|
||||||
|
></div>
|
||||||
|
<ng-container *ngTemplateOutlet="browserExtensionEasyAccess"></ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="tw-absolute tw-left-0 tw-top-0 tw-opacity-0 md:tw-opacity-100 md:tw-relative tw-w-[15rem] tw-max-w-full tw-aspect-[0.807]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
*ngIf="!allVideosLoaded"
|
||||||
|
class="tw-animate-pulse tw-bg-secondary-300 tw-size-full tw-absolute tw-left-0 tw-top-0 tw-rounded-lg"
|
||||||
|
data-testid="video-pulse"
|
||||||
|
></div>
|
||||||
|
<ng-container *ngTemplateOutlet="onboardingAutofill"></ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||||
|
import { By } from "@angular/platform-browser";
|
||||||
|
import { provideNoopAnimations } from "@angular/platform-browser/animations";
|
||||||
|
import { RouterModule } from "@angular/router";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { AddExtensionVideosComponent } from "./add-extension-videos.component";
|
||||||
|
|
||||||
|
describe("AddExtensionVideosComponent", () => {
|
||||||
|
let fixture: ComponentFixture<AddExtensionVideosComponent>;
|
||||||
|
let component: AddExtensionVideosComponent;
|
||||||
|
|
||||||
|
// Mock HTMLMediaElement load to stop the video file from being loaded
|
||||||
|
Object.defineProperty(HTMLMediaElement.prototype, "load", {
|
||||||
|
value: jest.fn(),
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const play = jest.fn(() => Promise.resolve());
|
||||||
|
HTMLMediaElement.prototype.play = play;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
window.matchMedia = jest.fn().mockReturnValue(false);
|
||||||
|
play.mockClear();
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AddExtensionVideosComponent, RouterModule.forRoot([])],
|
||||||
|
providers: [
|
||||||
|
provideNoopAnimations(),
|
||||||
|
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AddExtensionVideosComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("loading pulse", () => {
|
||||||
|
it("shows loading spinner when all videos are not loaded", () => {
|
||||||
|
const loadingSpinners = fixture.debugElement.queryAll(By.css("[data-testid='video-pulse']"));
|
||||||
|
expect(loadingSpinners.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows all pulses until all videos are loaded", () => {
|
||||||
|
let loadingSpinners = fixture.debugElement.queryAll(By.css("[data-testid='video-pulse']"));
|
||||||
|
expect(loadingSpinners.length).toBe(3);
|
||||||
|
|
||||||
|
// Simulate two video loaded
|
||||||
|
component["videoElements"].get(0)?.nativeElement.dispatchEvent(new Event("loadeddata"));
|
||||||
|
component["videoElements"].get(1)?.nativeElement.dispatchEvent(new Event("loadeddata"));
|
||||||
|
|
||||||
|
loadingSpinners = fixture.debugElement.queryAll(By.css("[data-testid='video-pulse']"));
|
||||||
|
|
||||||
|
expect(component["numberOfLoadedVideos"]).toBe(2);
|
||||||
|
expect(loadingSpinners.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("window resizing", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component["numberOfLoadedVideos"] = 3;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows all videos when window is resized to desktop viewport", fakeAsync(() => {
|
||||||
|
component["variant"] = "mobile";
|
||||||
|
Object.defineProperty(component["document"].documentElement, "clientWidth", {
|
||||||
|
configurable: true,
|
||||||
|
value: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event("resize"));
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick(50);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Array.from(component["videoElements"]).every(
|
||||||
|
(video) => video.nativeElement.parentElement?.style.opacity === "1",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("shows only the playing video when window is resized to mobile viewport", fakeAsync(() => {
|
||||||
|
component["variant"] = "desktop";
|
||||||
|
// readonly property needs redefining
|
||||||
|
Object.defineProperty(component["document"].documentElement, "clientWidth", {
|
||||||
|
value: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
const video1 = component["videoElements"].get(1);
|
||||||
|
Object.defineProperty(video1!.nativeElement, "paused", {
|
||||||
|
value: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event("resize"));
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick(50);
|
||||||
|
|
||||||
|
expect(component["videoElements"].get(0)?.nativeElement.parentElement?.style.opacity).toBe(
|
||||||
|
"0",
|
||||||
|
);
|
||||||
|
expect(component["videoElements"].get(1)?.nativeElement.parentElement?.style.opacity).toBe(
|
||||||
|
"1",
|
||||||
|
);
|
||||||
|
expect(component["videoElements"].get(2)?.nativeElement.parentElement?.style.opacity).toBe(
|
||||||
|
"0",
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("video sequence", () => {
|
||||||
|
let firstVideo: HTMLVideoElement;
|
||||||
|
let secondVideo: HTMLVideoElement;
|
||||||
|
let thirdVideo: HTMLVideoElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component["numberOfLoadedVideos"] = 2;
|
||||||
|
component["onVideoLoad"]();
|
||||||
|
|
||||||
|
firstVideo = component["videoElements"].get(0)!.nativeElement;
|
||||||
|
secondVideo = component["videoElements"].get(1)!.nativeElement;
|
||||||
|
thirdVideo = component["videoElements"].get(2)!.nativeElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts the video sequence when all videos are loaded", fakeAsync(() => {
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(firstVideo.play).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("plays videos in sequence", fakeAsync(() => {
|
||||||
|
tick(); // let first video play
|
||||||
|
|
||||||
|
play.mockClear();
|
||||||
|
firstVideo.onended!(new Event("ended")); // trigger next video
|
||||||
|
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(secondVideo.play).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
play.mockClear();
|
||||||
|
secondVideo.onended!(new Event("ended")); // trigger next video
|
||||||
|
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(thirdVideo.play).toHaveBeenCalledTimes(1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("doesn't play videos again when the user prefers no motion", fakeAsync(() => {
|
||||||
|
component["prefersReducedMotion"] = true;
|
||||||
|
|
||||||
|
tick();
|
||||||
|
firstVideo.onended!(new Event("ended"));
|
||||||
|
tick();
|
||||||
|
secondVideo.onended!(new Event("ended"));
|
||||||
|
tick();
|
||||||
|
|
||||||
|
play.mockClear();
|
||||||
|
|
||||||
|
thirdVideo.onended!(new Event("ended")); // trigger first video again
|
||||||
|
|
||||||
|
tick();
|
||||||
|
expect(play).toHaveBeenCalledTimes(0);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { CommonModule, DOCUMENT } from "@angular/common";
|
||||||
|
import { Component, ViewChildren, QueryList, ElementRef, inject } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
import { debounceTime, fromEvent } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "vault-add-extension-videos",
|
||||||
|
templateUrl: "./add-extension-videos.component.html",
|
||||||
|
imports: [CommonModule, JslibModule],
|
||||||
|
})
|
||||||
|
export class AddExtensionVideosComponent {
|
||||||
|
@ViewChildren("video", { read: ElementRef }) protected videoElements!: QueryList<
|
||||||
|
ElementRef<HTMLVideoElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
private document = inject(DOCUMENT);
|
||||||
|
|
||||||
|
/** Current viewport size */
|
||||||
|
protected variant: "mobile" | "desktop" = "desktop";
|
||||||
|
|
||||||
|
/** Number of videos that have loaded and are ready to play */
|
||||||
|
protected numberOfLoadedVideos = 0;
|
||||||
|
|
||||||
|
/** True when the user prefers reduced motion */
|
||||||
|
protected prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
|
||||||
|
/** Returns true when all videos are loaded */
|
||||||
|
get allVideosLoaded(): boolean {
|
||||||
|
return this.numberOfLoadedVideos >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
fromEvent(window, "resize")
|
||||||
|
.pipe(takeUntilDestroyed(), debounceTime(25))
|
||||||
|
.subscribe(() => this.onResize());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resets the video states based on the viewport width changes */
|
||||||
|
onResize(): void {
|
||||||
|
const oldVariant = this.variant;
|
||||||
|
this.variant = this.document.documentElement.clientWidth < 768 ? "mobile" : "desktop";
|
||||||
|
|
||||||
|
// When the viewport changes from desktop to mobile, hide all videos except the one that is playing.
|
||||||
|
if (this.variant !== oldVariant && this.variant === "mobile") {
|
||||||
|
this.videoElements.forEach((video) => {
|
||||||
|
if (video.nativeElement.paused) {
|
||||||
|
this.hideElement(video.nativeElement.parentElement!);
|
||||||
|
} else {
|
||||||
|
this.showElement(video.nativeElement.parentElement!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the viewport changes from mobile to desktop, show all videos.
|
||||||
|
if (this.variant !== oldVariant && this.variant === "desktop") {
|
||||||
|
this.videoElements.forEach((video) => {
|
||||||
|
this.showElement(video.nativeElement.parentElement!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the number of loaded videos.
|
||||||
|
* When all videos are loaded, start the first one.
|
||||||
|
*/
|
||||||
|
protected onVideoLoad() {
|
||||||
|
this.numberOfLoadedVideos = this.numberOfLoadedVideos + 1;
|
||||||
|
|
||||||
|
if (this.allVideosLoaded) {
|
||||||
|
void this.startVideoSequence(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recursive method to start the video sequence. */
|
||||||
|
private async startVideoSequence(i: number): Promise<void> {
|
||||||
|
let index = i;
|
||||||
|
const endOfVideos = index >= this.videoElements.length;
|
||||||
|
|
||||||
|
// When the user prefers reduced motion, don't play the videos more than once
|
||||||
|
if (endOfVideos && this.prefersReducedMotion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the last of the videos has played, loop back to the start
|
||||||
|
if (endOfVideos) {
|
||||||
|
this.videoElements.forEach((video) => {
|
||||||
|
// Reset all videos to the start
|
||||||
|
video.nativeElement.currentTime = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Loop back to the first video
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = this.videoElements.toArray()[index].nativeElement;
|
||||||
|
video.onended = () => {
|
||||||
|
void this.startVideoSequence(index + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mobileTransitionIn(index);
|
||||||
|
|
||||||
|
// Set muted via JavaScript, browsers are respecting autoplay consistently over just the HTML attribute
|
||||||
|
video.muted = true;
|
||||||
|
await video.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** For mobile viewports, fades the current video out and the next video in. */
|
||||||
|
private mobileTransitionIn(index: number): void {
|
||||||
|
// When the viewport is above the tablet breakpoint, all videos are shown at once.
|
||||||
|
// No transition is needed.
|
||||||
|
if (this.isAboveTabletBreakpoint()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentParent = this.videoElements.toArray()[index].nativeElement.parentElement!;
|
||||||
|
const previousIndex = index === 0 ? this.videoElements.length - 1 : index - 1;
|
||||||
|
|
||||||
|
const previousParent = this.videoElements.toArray()[previousIndex].nativeElement.parentElement!;
|
||||||
|
|
||||||
|
// Fade out the previous video
|
||||||
|
this.hideElement(previousParent, true);
|
||||||
|
|
||||||
|
// Fade in the current video
|
||||||
|
this.showElement(currentParent, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true when the viewport width is 768px or above. */
|
||||||
|
private isAboveTabletBreakpoint(): boolean {
|
||||||
|
const width = this.document.documentElement.clientWidth;
|
||||||
|
return width >= 768;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Visually hides the given element. */
|
||||||
|
private hideElement(element: HTMLElement, transition = false): void {
|
||||||
|
element.style.transition = transition ? "opacity 0.5s linear" : "";
|
||||||
|
element.style.opacity = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Visually shows the given element. */
|
||||||
|
private showElement(element: HTMLElement, transition = false): void {
|
||||||
|
element.style.transition = transition ? "opacity 0.5s linear" : "";
|
||||||
|
element.style.opacity = "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,16 +6,13 @@
|
|||||||
></i>
|
></i>
|
||||||
|
|
||||||
<section *ngIf="state === SetupExtensionState.NeedsExtension" class="tw-text-center tw-mt-4">
|
<section *ngIf="state === SetupExtensionState.NeedsExtension" class="tw-text-center tw-mt-4">
|
||||||
<h1 bitTypography="h2">{{ "setupExtensionPageTitle" | i18n }}</h1>
|
<h1 class="tw-text-xl tw-font-semibold tw-text-main sm:tw-text-2xl">
|
||||||
<h2 bitTypography="body1">{{ "setupExtensionPageDescription" | i18n }}</h2>
|
{{ "setupExtensionPageTitle" | i18n }}
|
||||||
<div class="tw-mb-6">
|
</h1>
|
||||||
<!-- Placeholder - will be removed in following tickets -->
|
<h2 class="tw-text-sm tw-text-main tw-mb-6 tw-font-normal sm:tw-text-base">
|
||||||
<img
|
{{ "setupExtensionPageDescription" | i18n }}
|
||||||
class="tw-max-w-3xl"
|
</h2>
|
||||||
[alt]="'setupExtensionContentAlt' | i18n"
|
<vault-add-extension-videos></vault-add-extension-videos>
|
||||||
src="/images/setup-extension/setup-extension-placeholder.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="tw-flex tw-flex-col tw-gap-4 tw-items-center">
|
<div class="tw-flex tw-flex-col tw-gap-4 tw-items-center">
|
||||||
<a
|
<a
|
||||||
bitButton
|
bitButton
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ describe("SetupExtensionComponent", () => {
|
|||||||
navigate.mockClear();
|
navigate.mockClear();
|
||||||
openExtension.mockClear();
|
openExtension.mockClear();
|
||||||
getFeatureFlag.mockClear().mockResolvedValue(true);
|
getFeatureFlag.mockClear().mockResolvedValue(true);
|
||||||
|
window.matchMedia = jest.fn().mockReturnValue(false);
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [SetupExtensionComponent, RouterModule.forRoot([])],
|
imports: [SetupExtensionComponent, RouterModule.forRoot([])],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NgIf } from "@angular/common";
|
import { DOCUMENT, NgIf } from "@angular/common";
|
||||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
import { Component, DestroyRef, inject, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { Router, RouterModule } from "@angular/router";
|
import { Router, RouterModule } from "@angular/router";
|
||||||
import { pairwise, startWith } from "rxjs";
|
import { pairwise, startWith } from "rxjs";
|
||||||
@@ -23,6 +23,7 @@ import { VaultIcons } from "@bitwarden/vault";
|
|||||||
import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
|
import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
|
||||||
|
|
||||||
import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component";
|
import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component";
|
||||||
|
import { AddExtensionVideosComponent } from "./add-extension-videos.component";
|
||||||
|
|
||||||
const SetupExtensionState = {
|
const SetupExtensionState = {
|
||||||
Loading: "loading",
|
Loading: "loading",
|
||||||
@@ -35,15 +36,24 @@ type SetupExtensionState = UnionOfValues<typeof SetupExtensionState>;
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "vault-setup-extension",
|
selector: "vault-setup-extension",
|
||||||
templateUrl: "./setup-extension.component.html",
|
templateUrl: "./setup-extension.component.html",
|
||||||
imports: [NgIf, JslibModule, ButtonComponent, LinkModule, IconModule, RouterModule],
|
imports: [
|
||||||
|
NgIf,
|
||||||
|
JslibModule,
|
||||||
|
ButtonComponent,
|
||||||
|
LinkModule,
|
||||||
|
IconModule,
|
||||||
|
RouterModule,
|
||||||
|
AddExtensionVideosComponent,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class SetupExtensionComponent implements OnInit {
|
export class SetupExtensionComponent implements OnInit, OnDestroy {
|
||||||
private webBrowserExtensionInteractionService = inject(WebBrowserInteractionService);
|
private webBrowserExtensionInteractionService = inject(WebBrowserInteractionService);
|
||||||
private configService = inject(ConfigService);
|
private configService = inject(ConfigService);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
private platformUtilsService = inject(PlatformUtilsService);
|
private platformUtilsService = inject(PlatformUtilsService);
|
||||||
private dialogService = inject(DialogService);
|
private dialogService = inject(DialogService);
|
||||||
|
private document = inject(DOCUMENT);
|
||||||
|
|
||||||
protected SetupExtensionState = SetupExtensionState;
|
protected SetupExtensionState = SetupExtensionState;
|
||||||
protected PartyIcon = VaultIcons.Party;
|
protected PartyIcon = VaultIcons.Party;
|
||||||
@@ -56,8 +66,21 @@ export class SetupExtensionComponent implements OnInit {
|
|||||||
|
|
||||||
/** Reference to the add it later dialog */
|
/** Reference to the add it later dialog */
|
||||||
protected dialogRef: DialogRef | null = null;
|
protected dialogRef: DialogRef | null = null;
|
||||||
|
private viewportContent: string | null = null;
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
// It is not be uncommon for users to hit this page from smaller viewports.
|
||||||
|
// There are global styles that set a min-width for the page which cause it to render poorly.
|
||||||
|
// Remove them here.
|
||||||
|
// https://github.com/bitwarden/clients/blob/main/apps/web/src/scss/base.scss#L6
|
||||||
|
this.document.body.style.minWidth = "auto";
|
||||||
|
|
||||||
|
const viewportMeta = this.document.querySelector('meta[name="viewport"]');
|
||||||
|
|
||||||
|
// Save the current viewport content to reset it when the component is destroyed
|
||||||
|
this.viewportContent = viewportMeta?.getAttribute("content") ?? null;
|
||||||
|
viewportMeta?.setAttribute("content", "width=device-width, initial-scale=1.0");
|
||||||
|
|
||||||
await this.conditionallyRedirectUser();
|
await this.conditionallyRedirectUser();
|
||||||
|
|
||||||
this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice());
|
this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice());
|
||||||
@@ -83,6 +106,17 @@ export class SetupExtensionComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// Reset the body min-width when the component is destroyed
|
||||||
|
this.document.body.style.minWidth = "";
|
||||||
|
|
||||||
|
if (this.viewportContent !== null) {
|
||||||
|
this.document
|
||||||
|
.querySelector('meta[name="viewport"]')
|
||||||
|
?.setAttribute("content", this.viewportContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Conditionally redirects the user to the vault upon landing on the page. */
|
/** Conditionally redirects the user to the vault upon landing on the page. */
|
||||||
async conditionallyRedirectUser() {
|
async conditionallyRedirectUser() {
|
||||||
const isFeatureEnabled = await this.configService.getFeatureFlag(
|
const isFeatureEnabled = await this.configService.getFeatureFlag(
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 770 KiB |
@@ -10746,15 +10746,15 @@
|
|||||||
},
|
},
|
||||||
"gettingStartedWithBitwardenPart1": {
|
"gettingStartedWithBitwardenPart1": {
|
||||||
"message": "For tips on getting started with Bitwarden visit the",
|
"message": "For tips on getting started with Bitwarden visit the",
|
||||||
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'."
|
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'"
|
||||||
},
|
},
|
||||||
"gettingStartedWithBitwardenPart2": {
|
"gettingStartedWithBitwardenPart2": {
|
||||||
"message": "Learning Center",
|
"message": "Learning Center",
|
||||||
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'."
|
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'"
|
||||||
},
|
},
|
||||||
"gettingStartedWithBitwardenPart3": {
|
"gettingStartedWithBitwardenPart3": {
|
||||||
"message": "Help Center",
|
"message": "Help Center",
|
||||||
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'."
|
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'"
|
||||||
},
|
},
|
||||||
"setupExtensionContentAlt": {
|
"setupExtensionContentAlt": {
|
||||||
"message": "With the Bitwarden browser extension you can easily create new logins, access your saved logins directly from your browser toolbar, and sign in to accounts quickly using Bitwarden autofill."
|
"message": "With the Bitwarden browser extension you can easily create new logins, access your saved logins directly from your browser toolbar, and sign in to accounts quickly using Bitwarden autofill."
|
||||||
|
|||||||
@@ -268,6 +268,9 @@ const devServer =
|
|||||||
https://www.paypalobjects.com
|
https://www.paypalobjects.com
|
||||||
https://q.stripe.com
|
https://q.stripe.com
|
||||||
https://haveibeenpwned.com
|
https://haveibeenpwned.com
|
||||||
|
;media-src
|
||||||
|
'self'
|
||||||
|
https://assets.bitwarden.com
|
||||||
;child-src
|
;child-src
|
||||||
'self'
|
'self'
|
||||||
https://js.stripe.com
|
https://js.stripe.com
|
||||||
|
|||||||
Reference in New Issue
Block a user