mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
[PM-22179] Redirect user to /setup-extension (#15375)
* add end user feature flag * add initial setup extension component and route * redirect users from registration completion to the setup extension page * add `hideIcon` to anon layout for web - matches implementation on the browser. * integrate with anon layout for extension wrapper * add initial loading state * conditionally redirect the user upon initialization * redirect the user to the vault if the extension is installed * add initial copy for setup-extension page * add confirmation dialog for skipping the extension installation * add success state for setup extension page * only show loggedin toast when end user activation is not enabled. * add image alt * lower threshold for polling extension * close the dialog when linking to the vault * update party colors * use the platform specific registration service to to only forward the web registrations to `/setup-extension` * call `super` rather than `/vault` directly, it could change in the future
This commit is contained in:
@@ -9,6 +9,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
@@ -33,6 +34,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
@@ -44,6 +46,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
logService = mock<LogService>();
|
||||
policyService = mock<PolicyService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
service = new WebRegistrationFinishService(
|
||||
keyService,
|
||||
@@ -53,6 +56,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
logService,
|
||||
policyService,
|
||||
accountService,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -418,4 +422,22 @@ describe("WebRegistrationFinishService", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("determineLoginSuccessRoute", () => {
|
||||
it("returns /setup-extension when the end user activation feature flag is enabled", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
const result = await service.determineLoginSuccessRoute();
|
||||
|
||||
expect(result).toBe("/setup-extension");
|
||||
});
|
||||
|
||||
it("returns /vault when the end user activation feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
const result = await service.determineLoginSuccessRoute();
|
||||
|
||||
expect(result).toBe("/vault");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -32,6 +34,7 @@ export class WebRegistrationFinishService
|
||||
private logService: LogService,
|
||||
private policyService: PolicyService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(keyService, accountApiService);
|
||||
}
|
||||
@@ -76,6 +79,18 @@ export class WebRegistrationFinishService
|
||||
return masterPasswordPolicyOpts;
|
||||
}
|
||||
|
||||
override async determineLoginSuccessRoute(): Promise<string> {
|
||||
const endUserActivationFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM19315EndUserActivationMvp,
|
||||
);
|
||||
|
||||
if (endUserActivationFlagEnabled) {
|
||||
return "/setup-extension";
|
||||
} else {
|
||||
return super.determineLoginSuccessRoute();
|
||||
}
|
||||
}
|
||||
|
||||
// Note: the org invite token and email verification are mutually exclusive. Only one will be present.
|
||||
override async buildRegisterRequest(
|
||||
email: string,
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Urls,
|
||||
@@ -256,6 +257,7 @@ const safeProviders: SafeProvider[] = [
|
||||
LogService,
|
||||
PolicyService,
|
||||
AccountService,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -81,6 +81,7 @@ import { AccessComponent, SendAccessExplainerComponent } from "./tools/send/send
|
||||
import { SendComponent } from "./tools/send/send.component";
|
||||
import { BrowserExtensionPromptInstallComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt-install.component";
|
||||
import { BrowserExtensionPromptComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt.component";
|
||||
import { SetupExtensionComponent } from "./vault/components/setup-extension/setup-extension.component";
|
||||
import { VaultModule } from "./vault/individual-vault/vault.module";
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -579,6 +580,20 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "setup-extension",
|
||||
data: {
|
||||
hideCardWrapper: true,
|
||||
hideIcon: true,
|
||||
maxWidth: "3xl",
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: SetupExtensionComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<bit-simple-dialog>
|
||||
<div bitDialogIcon>
|
||||
<i class="bwi bwi-info-circle bwi-2x tw-text-info" aria-hidden="true"></i>
|
||||
</div>
|
||||
<ng-container bitDialogContent>
|
||||
<div bitTypography="h3">
|
||||
{{ "cannotAutofillPasswordsWithoutExtensionTitle" | i18n }}
|
||||
</div>
|
||||
<div bitTypography="body1">{{ "cannotAutofillPasswordsWithoutExtensionDesc" | i18n }}</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<a
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[href]="webStoreUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ "getTheExtension" | i18n }}
|
||||
</a>
|
||||
<a bitButton buttonType="secondary" routerLink="/vault" bitDialogClose>
|
||||
{{ "skipToWebApp" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ComponentFixture, TestBed } 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component";
|
||||
|
||||
describe("AddExtensionLaterDialogComponent", () => {
|
||||
let fixture: ComponentFixture<AddExtensionLaterDialogComponent>;
|
||||
const getDevice = jest.fn().mockReturnValue(null);
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AddExtensionLaterDialogComponent, RouterModule.forRoot([])],
|
||||
providers: [
|
||||
provideNoopAnimations(),
|
||||
{ provide: PlatformUtilsService, useValue: { getDevice } },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddExtensionLaterDialogComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("renders the 'Get the Extension' link with correct href", () => {
|
||||
const link = fixture.debugElement.queryAll(By.css("a[bitButton]"))[0];
|
||||
|
||||
expect(link.nativeElement.getAttribute("href")).toBe(
|
||||
"https://bitwarden.com/download/#downloads-web-browser",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the 'Skip to Web App' link with correct routerLink", () => {
|
||||
const skipLink = fixture.debugElement.queryAll(By.css("a[bitButton]"))[1];
|
||||
|
||||
expect(skipLink.attributes.href).toBe("/vault");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Component, inject, OnInit } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url";
|
||||
import { ButtonComponent, DialogModule, TypographyModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "vault-add-extension-later-dialog",
|
||||
templateUrl: "./add-extension-later-dialog.component.html",
|
||||
imports: [DialogModule, JslibModule, TypographyModule, ButtonComponent, RouterModule],
|
||||
})
|
||||
export class AddExtensionLaterDialogComponent implements OnInit {
|
||||
private platformUtilsService = inject(PlatformUtilsService);
|
||||
|
||||
/** Download Url for the extension based on the browser */
|
||||
protected webStoreUrl: string = "";
|
||||
|
||||
ngOnInit(): void {
|
||||
this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<i
|
||||
*ngIf="state === SetupExtensionState.Loading"
|
||||
class="bwi bwi-spinner bwi-spin bwi-3x tw-text-muted"
|
||||
aria-hidden="true"
|
||||
[appA11yTitle]="'loading' | i18n"
|
||||
></i>
|
||||
|
||||
<section *ngIf="state === SetupExtensionState.NeedsExtension" class="tw-text-center tw-mt-4">
|
||||
<h1 bitTypography="h2">{{ "setupExtensionPageTitle" | i18n }}</h1>
|
||||
<h2 bitTypography="body1">{{ "setupExtensionPageDescription" | i18n }}</h2>
|
||||
<div class="tw-mb-6">
|
||||
<!-- Placeholder - will be removed in following tickets -->
|
||||
<img
|
||||
class="tw-max-w-3xl"
|
||||
[alt]="'setupExtensionContentAlt' | i18n"
|
||||
src="/images/setup-extension/setup-extension-placeholder.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="tw-flex tw-flex-col tw-gap-4 tw-items-center">
|
||||
<a
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[href]="webStoreUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ "getTheExtension" | i18n }}
|
||||
</a>
|
||||
<button type="button" bitLink (click)="addItLater()">
|
||||
{{ "addItLater" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section *ngIf="state === SetupExtensionState.Success" class="tw-flex tw-flex-col tw-items-center">
|
||||
<bit-icon [icon]="PartyIcon"></bit-icon>
|
||||
<h1 bitTypography="h2" class="tw-mb-6 tw-mt-4">{{ "bitwardenExtensionInstalled" | i18n }}</h1>
|
||||
<div
|
||||
class="tw-flex tw-flex-col tw-rounded-2xl tw-bg-background tw-border tw-border-solid tw-border-secondary-300 tw-p-8"
|
||||
>
|
||||
<p>{{ "openExtensionToAutofill" | i18n }}</p>
|
||||
<button type="button" bitButton buttonType="primary" class="tw-mb-2" (click)="openExtension()">
|
||||
{{ "openBitwardenExtension" | i18n }}
|
||||
</button>
|
||||
<a bitButton buttonType="secondary" routerLink="/vault">
|
||||
{{ "skipToWebApp" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<p class="tw-mt-10 tw-max-w-96 tw-text-center">
|
||||
{{ "gettingStartedWithBitwardenPart1" | i18n }}
|
||||
<a bitLink href="https://bitwarden.com/help/">
|
||||
{{ "gettingStartedWithBitwardenPart2" | i18n }}
|
||||
</a>
|
||||
{{ "and" | i18n }}
|
||||
<a bitLink href="https://bitwarden.com/help/learning-center/">
|
||||
{{ "gettingStartedWithBitwardenPart3" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
@@ -0,0 +1,124 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
|
||||
|
||||
import { SetupExtensionComponent } from "./setup-extension.component";
|
||||
|
||||
describe("SetupExtensionComponent", () => {
|
||||
let fixture: ComponentFixture<SetupExtensionComponent>;
|
||||
let component: SetupExtensionComponent;
|
||||
|
||||
const getFeatureFlag = jest.fn().mockResolvedValue(false);
|
||||
const navigate = jest.fn().mockResolvedValue(true);
|
||||
const openExtension = jest.fn().mockResolvedValue(true);
|
||||
const extensionInstalled$ = new BehaviorSubject<boolean | null>(null);
|
||||
|
||||
beforeEach(async () => {
|
||||
navigate.mockClear();
|
||||
openExtension.mockClear();
|
||||
getFeatureFlag.mockClear().mockResolvedValue(true);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SetupExtensionComponent, RouterModule.forRoot([])],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
{ provide: WebBrowserInteractionService, useValue: { extensionInstalled$, openExtension } },
|
||||
{ provide: PlatformUtilsService, useValue: { getDevice: () => DeviceType.UnknownBrowser } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SetupExtensionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
const router = TestBed.inject(Router);
|
||||
router.navigate = navigate;
|
||||
});
|
||||
|
||||
it("initially shows the loading spinner", () => {
|
||||
const spinner = fixture.debugElement.query(By.css("i"));
|
||||
|
||||
expect(spinner.nativeElement.title).toBe("loading");
|
||||
});
|
||||
|
||||
it("sets webStoreUrl", () => {
|
||||
expect(component["webStoreUrl"]).toBe("https://bitwarden.com/download/#downloads-web-browser");
|
||||
});
|
||||
|
||||
describe("initialization", () => {
|
||||
it("redirects to the vault if the feature flag is disabled", async () => {
|
||||
Utils.isMobileBrowser = false;
|
||||
getFeatureFlag.mockResolvedValue(false);
|
||||
navigate.mockClear();
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(["/vault"]);
|
||||
});
|
||||
|
||||
it("redirects to the vault if the user is on a mobile browser", async () => {
|
||||
Utils.isMobileBrowser = true;
|
||||
getFeatureFlag.mockResolvedValue(true);
|
||||
navigate.mockClear();
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(["/vault"]);
|
||||
});
|
||||
|
||||
it("does not redirect the user", async () => {
|
||||
Utils.isMobileBrowser = false;
|
||||
getFeatureFlag.mockResolvedValue(true);
|
||||
navigate.mockClear();
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.PM19315EndUserActivationMvp);
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("extensionInstalled$", () => {
|
||||
it("redirects the user to the vault when the first emitted value is true", () => {
|
||||
extensionInstalled$.next(true);
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith(["/vault"]);
|
||||
});
|
||||
|
||||
describe("success state", () => {
|
||||
beforeEach(() => {
|
||||
// avoid initial redirect
|
||||
extensionInstalled$.next(false);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
extensionInstalled$.next(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("shows link to the vault", () => {
|
||||
const successLink = fixture.debugElement.query(By.css("a"));
|
||||
|
||||
expect(successLink.nativeElement.href).toContain("/vault");
|
||||
});
|
||||
|
||||
it("shows open extension button", () => {
|
||||
const openExtensionButton = fixture.debugElement.query(By.css("button"));
|
||||
|
||||
openExtensionButton.triggerEventHandler("click");
|
||||
|
||||
expect(openExtension).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { NgIf } from "@angular/common";
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { pairwise, startWith } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url";
|
||||
import {
|
||||
ButtonComponent,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
import { VaultIcons } from "@bitwarden/vault";
|
||||
|
||||
import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
|
||||
|
||||
import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component";
|
||||
|
||||
const SetupExtensionState = {
|
||||
Loading: "loading",
|
||||
NeedsExtension: "needs-extension",
|
||||
Success: "success",
|
||||
} as const;
|
||||
|
||||
type SetupExtensionState = UnionOfValues<typeof SetupExtensionState>;
|
||||
|
||||
@Component({
|
||||
selector: "vault-setup-extension",
|
||||
templateUrl: "./setup-extension.component.html",
|
||||
imports: [NgIf, JslibModule, ButtonComponent, LinkModule, IconModule, RouterModule],
|
||||
})
|
||||
export class SetupExtensionComponent implements OnInit {
|
||||
private webBrowserExtensionInteractionService = inject(WebBrowserInteractionService);
|
||||
private configService = inject(ConfigService);
|
||||
private router = inject(Router);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private platformUtilsService = inject(PlatformUtilsService);
|
||||
private dialogService = inject(DialogService);
|
||||
|
||||
protected SetupExtensionState = SetupExtensionState;
|
||||
protected PartyIcon = VaultIcons.Party;
|
||||
|
||||
/** The current state of the setup extension component. */
|
||||
protected state: SetupExtensionState = SetupExtensionState.Loading;
|
||||
|
||||
/** Download Url for the extension based on the browser */
|
||||
protected webStoreUrl: string = "";
|
||||
|
||||
/** Reference to the add it later dialog */
|
||||
protected dialogRef: DialogRef | null = null;
|
||||
|
||||
async ngOnInit() {
|
||||
await this.conditionallyRedirectUser();
|
||||
|
||||
this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice());
|
||||
|
||||
this.webBrowserExtensionInteractionService.extensionInstalled$
|
||||
.pipe(takeUntilDestroyed(this.destroyRef), startWith(null), pairwise())
|
||||
.subscribe(([previousState, currentState]) => {
|
||||
// Initial state transitioned to extension installed, redirect the user
|
||||
if (previousState === null && currentState) {
|
||||
void this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
// Extension was not installed and now it is, show success state
|
||||
if (previousState === false && currentState) {
|
||||
this.dialogRef?.close();
|
||||
this.state = SetupExtensionState.Success;
|
||||
}
|
||||
|
||||
// Extension is not installed
|
||||
if (currentState === false) {
|
||||
this.state = SetupExtensionState.NeedsExtension;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Conditionally redirects the user to the vault upon landing on the page. */
|
||||
async conditionallyRedirectUser() {
|
||||
const isFeatureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM19315EndUserActivationMvp,
|
||||
);
|
||||
const isMobile = Utils.isMobileBrowser;
|
||||
|
||||
if (!isFeatureEnabled || isMobile) {
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the add extension later dialog */
|
||||
addItLater() {
|
||||
this.dialogRef = this.dialogService.open(AddExtensionLaterDialogComponent);
|
||||
}
|
||||
|
||||
/** Opens the browser extension */
|
||||
openExtension() {
|
||||
void this.webBrowserExtensionInteractionService.openExtension();
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ describe("WebBrowserInteractionService", () => {
|
||||
tick(1500);
|
||||
expect(results[0]).toBe(false);
|
||||
|
||||
tick(2500);
|
||||
// then emit `HasBwInstalled`
|
||||
dispatchEvent(VaultMessages.HasBwInstalled);
|
||||
tick();
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { DestroyRef, inject, Injectable } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { concatWith, filter, fromEvent, map, Observable, race, take, tap, timer } from "rxjs";
|
||||
import {
|
||||
concat,
|
||||
filter,
|
||||
fromEvent,
|
||||
interval,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
race,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
takeWhile,
|
||||
tap,
|
||||
timer,
|
||||
} from "rxjs";
|
||||
|
||||
import { ExtensionPageUrls } from "@bitwarden/common/vault/enums";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
@@ -22,13 +37,22 @@ export class WebBrowserInteractionService {
|
||||
);
|
||||
|
||||
/** Emits the installation status of the extension. */
|
||||
extensionInstalled$ = this.checkForExtension().pipe(
|
||||
concatWith(
|
||||
this.messages$.pipe(
|
||||
filter((event) => event.data.command === VaultMessages.HasBwInstalled),
|
||||
map(() => true),
|
||||
),
|
||||
),
|
||||
extensionInstalled$: Observable<boolean> = this.checkForExtension().pipe(
|
||||
switchMap((installed) => {
|
||||
if (installed) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
return concat(
|
||||
of(false),
|
||||
interval(2500).pipe(
|
||||
switchMap(() => this.checkForExtension()),
|
||||
takeWhile((installed) => !installed, true),
|
||||
filter((installed) => installed),
|
||||
),
|
||||
);
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
/** Attempts to open the extension, rejects if the extension is not installed or it fails to open. */
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 770 KiB |
@@ -10698,6 +10698,51 @@
|
||||
"description": "Two part message",
|
||||
"example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent"
|
||||
},
|
||||
"setupExtensionPageTitle": {
|
||||
"message": "Autofill your passwords securely with one click"
|
||||
},
|
||||
"setupExtensionPageDescription": {
|
||||
"message": "Get the Bitwarden browser extension and start autofilling today"
|
||||
},
|
||||
"getTheExtension": {
|
||||
"message": "Get the extension"
|
||||
},
|
||||
"addItLater": {
|
||||
"message": "Add it later"
|
||||
},
|
||||
"cannotAutofillPasswordsWithoutExtensionTitle": {
|
||||
"message": "You can't autofill passwords without the browser extension"
|
||||
},
|
||||
"cannotAutofillPasswordsWithoutExtensionDesc": {
|
||||
"message": "Are you sure you don't want to add the extension now?"
|
||||
},
|
||||
"skipToWebApp": {
|
||||
"message": "Skip to web app"
|
||||
},
|
||||
"bitwardenExtensionInstalled": {
|
||||
"message": "Bitwarden extension installed!"
|
||||
},
|
||||
"openExtensionToAutofill": {
|
||||
"message": "Open the extension to log in and start autofilling."
|
||||
},
|
||||
"openBitwardenExtension": {
|
||||
"message": "Open Bitwarden extension"
|
||||
},
|
||||
"gettingStartedWithBitwardenPart1": {
|
||||
"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'."
|
||||
},
|
||||
"gettingStartedWithBitwardenPart2": {
|
||||
"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'."
|
||||
},
|
||||
"gettingStartedWithBitwardenPart3": {
|
||||
"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'."
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"restart": {
|
||||
"message": "Restart"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user