Skip to content

Commit ab4af7d

Browse files
[PM-22179] Redirect user to /setup-extension (bitwarden#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
1 parent cef6a5e commit ab4af7d

23 files changed

Lines changed: 603 additions & 14 deletions

apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
99
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
1010
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
1111
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
12+
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
1213
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
1314
import { Utils } from "@bitwarden/common/platform/misc/utils";
1415
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
@@ -33,6 +34,7 @@ describe("WebRegistrationFinishService", () => {
3334
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
3435
let logService: MockProxy<LogService>;
3536
let policyService: MockProxy<PolicyService>;
37+
let configService: MockProxy<ConfigService>;
3638
const mockUserId = Utils.newGuid() as UserId;
3739
let accountService: FakeAccountService;
3840

@@ -44,6 +46,7 @@ describe("WebRegistrationFinishService", () => {
4446
logService = mock<LogService>();
4547
policyService = mock<PolicyService>();
4648
accountService = mockAccountServiceWith(mockUserId);
49+
configService = mock<ConfigService>();
4750

4851
service = new WebRegistrationFinishService(
4952
keyService,
@@ -53,6 +56,7 @@ describe("WebRegistrationFinishService", () => {
5356
logService,
5457
policyService,
5558
accountService,
59+
configService,
5660
);
5761
});
5862

@@ -418,4 +422,22 @@ describe("WebRegistrationFinishService", () => {
418422
);
419423
});
420424
});
425+
426+
describe("determineLoginSuccessRoute", () => {
427+
it("returns /setup-extension when the end user activation feature flag is enabled", async () => {
428+
configService.getFeatureFlag.mockResolvedValue(true);
429+
430+
const result = await service.determineLoginSuccessRoute();
431+
432+
expect(result).toBe("/setup-extension");
433+
});
434+
435+
it("returns /vault when the end user activation feature flag is disabled", async () => {
436+
configService.getFeatureFlag.mockResolvedValue(false);
437+
438+
const result = await service.determineLoginSuccessRoute();
439+
440+
expect(result).toBe("/vault");
441+
});
442+
});
421443
});

apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
1414
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
1515
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
1616
import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request";
17+
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
18+
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
1719
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
1820
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
1921
import { KeyService } from "@bitwarden/key-management";
@@ -32,6 +34,7 @@ export class WebRegistrationFinishService
3234
private logService: LogService,
3335
private policyService: PolicyService,
3436
private accountService: AccountService,
37+
private configService: ConfigService,
3538
) {
3639
super(keyService, accountApiService);
3740
}
@@ -76,6 +79,18 @@ export class WebRegistrationFinishService
7679
return masterPasswordPolicyOpts;
7780
}
7881

82+
override async determineLoginSuccessRoute(): Promise<string> {
83+
const endUserActivationFlagEnabled = await this.configService.getFeatureFlag(
84+
FeatureFlag.PM19315EndUserActivationMvp,
85+
);
86+
87+
if (endUserActivationFlagEnabled) {
88+
return "/setup-extension";
89+
} else {
90+
return super.determineLoginSuccessRoute();
91+
}
92+
}
93+
7994
// Note: the org invite token and email verification are mutually exclusive. Only one will be present.
8095
override async buildRegisterRequest(
8196
email: string,

apps/web/src/app/core/core.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import {
6262
VaultTimeoutStringType,
6363
} from "@bitwarden/common/key-management/vault-timeout";
6464
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
65+
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
6566
import {
6667
EnvironmentService,
6768
Urls,
@@ -256,6 +257,7 @@ const safeProviders: SafeProvider[] = [
256257
LogService,
257258
PolicyService,
258259
AccountService,
260+
ConfigService,
259261
],
260262
}),
261263
safeProvider({

apps/web/src/app/oss-routing.module.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import { AccessComponent, SendAccessExplainerComponent } from "./tools/send/send
8181
import { SendComponent } from "./tools/send/send.component";
8282
import { BrowserExtensionPromptInstallComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt-install.component";
8383
import { BrowserExtensionPromptComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt.component";
84+
import { SetupExtensionComponent } from "./vault/components/setup-extension/setup-extension.component";
8485
import { VaultModule } from "./vault/individual-vault/vault.module";
8586

8687
const routes: Routes = [
@@ -579,6 +580,20 @@ const routes: Routes = [
579580
},
580581
],
581582
},
583+
{
584+
path: "setup-extension",
585+
data: {
586+
hideCardWrapper: true,
587+
hideIcon: true,
588+
maxWidth: "3xl",
589+
} satisfies AnonLayoutWrapperData,
590+
children: [
591+
{
592+
path: "",
593+
component: SetupExtensionComponent,
594+
},
595+
],
596+
},
582597
],
583598
},
584599
{
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<bit-simple-dialog>
2+
<div bitDialogIcon>
3+
<i class="bwi bwi-info-circle bwi-2x tw-text-info" aria-hidden="true"></i>
4+
</div>
5+
<ng-container bitDialogContent>
6+
<div bitTypography="h3">
7+
{{ "cannotAutofillPasswordsWithoutExtensionTitle" | i18n }}
8+
</div>
9+
<div bitTypography="body1">{{ "cannotAutofillPasswordsWithoutExtensionDesc" | i18n }}</div>
10+
</ng-container>
11+
<ng-container bitDialogFooter>
12+
<a
13+
bitButton
14+
buttonType="primary"
15+
[href]="webStoreUrl"
16+
target="_blank"
17+
rel="noopener noreferrer"
18+
>
19+
{{ "getTheExtension" | i18n }}
20+
</a>
21+
<a bitButton buttonType="secondary" routerLink="/vault" bitDialogClose>
22+
{{ "skipToWebApp" | i18n }}
23+
</a>
24+
</ng-container>
25+
</bit-simple-dialog>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ComponentFixture, TestBed } from "@angular/core/testing";
2+
import { By } from "@angular/platform-browser";
3+
import { provideNoopAnimations } from "@angular/platform-browser/animations";
4+
import { RouterModule } from "@angular/router";
5+
6+
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
7+
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
8+
9+
import { AddExtensionLaterDialogComponent } from "./add-extension-later-dialog.component";
10+
11+
describe("AddExtensionLaterDialogComponent", () => {
12+
let fixture: ComponentFixture<AddExtensionLaterDialogComponent>;
13+
const getDevice = jest.fn().mockReturnValue(null);
14+
15+
beforeEach(async () => {
16+
await TestBed.configureTestingModule({
17+
imports: [AddExtensionLaterDialogComponent, RouterModule.forRoot([])],
18+
providers: [
19+
provideNoopAnimations(),
20+
{ provide: PlatformUtilsService, useValue: { getDevice } },
21+
{ provide: I18nService, useValue: { t: (key: string) => key } },
22+
],
23+
}).compileComponents();
24+
25+
fixture = TestBed.createComponent(AddExtensionLaterDialogComponent);
26+
fixture.detectChanges();
27+
});
28+
29+
it("renders the 'Get the Extension' link with correct href", () => {
30+
const link = fixture.debugElement.queryAll(By.css("a[bitButton]"))[0];
31+
32+
expect(link.nativeElement.getAttribute("href")).toBe(
33+
"https://bitwarden.com/download/#downloads-web-browser",
34+
);
35+
});
36+
37+
it("renders the 'Skip to Web App' link with correct routerLink", () => {
38+
const skipLink = fixture.debugElement.queryAll(By.css("a[bitButton]"))[1];
39+
40+
expect(skipLink.attributes.href).toBe("/vault");
41+
});
42+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Component, inject, OnInit } from "@angular/core";
2+
import { RouterModule } from "@angular/router";
3+
4+
import { JslibModule } from "@bitwarden/angular/jslib.module";
5+
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
6+
import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url";
7+
import { ButtonComponent, DialogModule, TypographyModule } from "@bitwarden/components";
8+
9+
@Component({
10+
selector: "vault-add-extension-later-dialog",
11+
templateUrl: "./add-extension-later-dialog.component.html",
12+
imports: [DialogModule, JslibModule, TypographyModule, ButtonComponent, RouterModule],
13+
})
14+
export class AddExtensionLaterDialogComponent implements OnInit {
15+
private platformUtilsService = inject(PlatformUtilsService);
16+
17+
/** Download Url for the extension based on the browser */
18+
protected webStoreUrl: string = "";
19+
20+
ngOnInit(): void {
21+
this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice());
22+
}
23+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<i
2+
*ngIf="state === SetupExtensionState.Loading"
3+
class="bwi bwi-spinner bwi-spin bwi-3x tw-text-muted"
4+
aria-hidden="true"
5+
[appA11yTitle]="'loading' | i18n"
6+
></i>
7+
8+
<section *ngIf="state === SetupExtensionState.NeedsExtension" class="tw-text-center tw-mt-4">
9+
<h1 bitTypography="h2">{{ "setupExtensionPageTitle" | i18n }}</h1>
10+
<h2 bitTypography="body1">{{ "setupExtensionPageDescription" | i18n }}</h2>
11+
<div class="tw-mb-6">
12+
<!-- Placeholder - will be removed in following tickets -->
13+
<img
14+
class="tw-max-w-3xl"
15+
[alt]="'setupExtensionContentAlt' | i18n"
16+
src="/images/setup-extension/setup-extension-placeholder.png"
17+
/>
18+
</div>
19+
<div class="tw-flex tw-flex-col tw-gap-4 tw-items-center">
20+
<a
21+
bitButton
22+
buttonType="primary"
23+
[href]="webStoreUrl"
24+
target="_blank"
25+
rel="noopener noreferrer"
26+
>
27+
{{ "getTheExtension" | i18n }}
28+
</a>
29+
<button type="button" bitLink (click)="addItLater()">
30+
{{ "addItLater" | i18n }}
31+
</button>
32+
</div>
33+
</section>
34+
35+
<section *ngIf="state === SetupExtensionState.Success" class="tw-flex tw-flex-col tw-items-center">
36+
<bit-icon [icon]="PartyIcon"></bit-icon>
37+
<h1 bitTypography="h2" class="tw-mb-6 tw-mt-4">{{ "bitwardenExtensionInstalled" | i18n }}</h1>
38+
<div
39+
class="tw-flex tw-flex-col tw-rounded-2xl tw-bg-background tw-border tw-border-solid tw-border-secondary-300 tw-p-8"
40+
>
41+
<p>{{ "openExtensionToAutofill" | i18n }}</p>
42+
<button type="button" bitButton buttonType="primary" class="tw-mb-2" (click)="openExtension()">
43+
{{ "openBitwardenExtension" | i18n }}
44+
</button>
45+
<a bitButton buttonType="secondary" routerLink="/vault">
46+
{{ "skipToWebApp" | i18n }}
47+
</a>
48+
</div>
49+
<p class="tw-mt-10 tw-max-w-96 tw-text-center">
50+
{{ "gettingStartedWithBitwardenPart1" | i18n }}
51+
<a bitLink href="https://bitwarden.com/help/">
52+
{{ "gettingStartedWithBitwardenPart2" | i18n }}
53+
</a>
54+
{{ "and" | i18n }}
55+
<a bitLink href="https://bitwarden.com/help/learning-center/">
56+
{{ "gettingStartedWithBitwardenPart3" | i18n }}
57+
</a>
58+
</p>
59+
</section>

0 commit comments

Comments
 (0)