[SG-483] Trial Initiation Cleanup (#4064)

* Remove 'showTrial' feature flag

* Replace Register component with trial and redirect trial routes to register

* Shore up fallback logic for bad params

* Remove register component that is no longer used

* Adjust register form margin top

* Update unit tests for new param handling

* Use enums for org names and add missing org routing

* Add new tests and fix existing flaky ones

* Use an enum for layouts
This commit is contained in:
Robyn MacCallum 2022-12-12 15:59:40 -05:00 committed by GitHub
parent 9c0aa9e8d4
commit 40cade39bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 117 additions and 420 deletions

View File

@ -16,7 +16,6 @@
"proxyEvents": "https://events.bitwarden.com"
},
"flags": {
"showTrial": true,
"showPasswordless": true
"showPasswordless": false
}
}

View File

@ -10,7 +10,6 @@
"proxyNotifications": "http://localhost:61840"
},
"flags": {
"showTrial": true,
"secretsManager": true,
"showPasswordless": true
}

View File

@ -6,7 +6,5 @@
"proxyNotifications": "http://localhost:61841",
"port": 8081
},
"flags": {
"showTrial": false
}
"flags": {}
}

View File

@ -10,7 +10,6 @@
"proxyEvents": "https://events.qa.bitwarden.pw"
},
"flags": {
"showTrial": true,
"secretsManager": false,
"showPasswordless": true
}

View File

@ -7,7 +7,6 @@
"port": 8081
},
"flags": {
"showTrial": false,
"showPasswordless": false
}
}

View File

@ -1,210 +0,0 @@
<div class="layout" [ngClass]="['layout', layout]">
<!-- TEAMS 1 Header -->
<header
class="header"
*ngIf="
layout === 'default' ||
layout === 'teams' ||
layout === 'teams1' ||
layout === 'teams2' ||
layout === 'enterprise' ||
layout === 'enterprise1' ||
layout === 'enterprise2' ||
layout === 'cnetcmpgnent' ||
layout === 'cnetcmpgnteams' ||
layout === 'cnetcmpgnind'
"
>
<div class="container">
<div class="row">
<div class="col-7">
<img
alt="Bitwarden"
class="logo mb-2"
src="../../images/register-layout/logo-horizontal-white.svg"
/>
</div>
</div>
</div>
</header>
<div class="container">
<div class="row">
<div class="col-7" *ngIf="layout">
<div class="mt-5">
<!-- Default Body -->
<div
*ngIf="
layout === 'teams' ||
layout === 'enterprise' ||
layout === 'enterprise1' ||
layout === 'default'
"
>
<h1>The Bitwarden Password Manager</h1>
<h2>
Trusted by millions of individuals, teams, and organizations worldwide for secure
password storage and sharing.
</h2>
<p>Store logins, secure notes, and more</p>
<p>Collaborate and share securely</p>
<p>Access anywhere on any device</p>
<p>Create your account to get started</p>
</div>
<!-- Teams & Enterprise Body -->
<div *ngIf="layout === 'teams1' || layout === 'teams2' || layout === 'enterprise2'">
<h1>
Start Your <span *ngIf="layout === 'teams1'">Teams<br /></span
><span *ngIf="layout === 'enterprise2'">Enterprise</span> Free Trial Now
</h1>
<h2>
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure
password storage and sharing.
</h2>
<p>Collaborate and share securely</p>
<p>Deploy and manage quickly and easily</p>
<p>Access anywhere on any device</p>
<p>Create your account to get started</p>
</div>
<!-- CNET Campaign Teams & Enterprise Body -->
<div *ngIf="layout === 'cnetcmpgnteams' || layout === 'cnetcmpgnent'">
<h1>
Start Your <span *ngIf="layout === 'cnetcmpgnteams'">Teams<br /></span
><span *ngIf="layout === 'cnetcmpgnent'">Enterprise</span> Free Trial Now
</h1>
<h2>
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure
password storage and sharing.
</h2>
<p>Collaborate and share securely</p>
<p>Deploy and manage quickly and easily</p>
<p>Access anywhere on any device</p>
<p>Create your account to get started</p>
</div>
<!-- CNET Campaign Premium Body -->
<div *ngIf="layout === 'cnetcmpgnind'">
<h1>Start Your Premium Account Now</h1>
<h2>
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure
password storage and sharing.
</h2>
<p>Store logins, secure notes, and more</p>
<p>Secure your account with advanced two-step login</p>
<p>Access anywhere on any device</p>
<p>Create your account to get started</p>
</div>
</div>
</div>
<div [ngClass]="{ 'col-5': layout, 'col-12': !layout }">
<div class="row justify-content-md-center mt-5">
<div [ngClass]="{ 'col-5': !layout, 'col-12': layout }">
<h1 class="lead text-center mb-4" *ngIf="!layout">{{ "createAccount" | i18n }}</h1>
<div class="card d-block">
<div class="card-body">
<app-callout
title="{{ 'createOrganizationStep1' | i18n }}"
type="info"
icon="bwi bwi-thumb-tack"
*ngIf="showCreateOrgMessage"
>
{{ "createOrganizationCreatePersonalAccount" | i18n }}
</app-callout>
<app-register-form
[queryParamEmail]="email"
[enforcedPolicyOptions]="enforcedPolicyOptions"
[referenceDataValue]="referenceData"
></app-register-form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-7 d-flex align-items-center">
<div
*ngIf="
layout === 'cnetcmpgnent' || layout === 'cnetcmpgnteams' || layout === 'cnetcmpgnind'
"
>
<figure>
<figcaption>
<cite>
<img
src="../../images/register-layout/cnet-logo.svg"
class="w-25 d-block mx-auto"
alt="cnet logo"
/>
</cite>
</figcaption>
<blockquote class="mx-auto text-center px-4">
"No more excuses; start using Bitwarden today. The identity you save could be your
own. The money definitely will be."
</blockquote>
</figure>
</div>
<div
*ngIf="
layout === 'teams' ||
layout === 'teams1' ||
layout === 'teams2' ||
layout === 'enterprise' ||
layout === 'enterprise1' ||
layout === 'enterprise2' ||
layout === 'default'
"
>
<figure>
<figcaption>
<cite>
<img
src="../../images/register-layout/forbes-logo.svg"
class="w-25 d-block mx-auto"
alt="Forbes Logo"
/>
</cite>
</figcaption>
<blockquote class="mx-auto text-center px-4">
“Bitwarden boasts the backing of some of the world's best security experts and an
attractive, easy-to-use interface”
</blockquote>
</figure>
</div>
</div>
<div
*ngIf="
layout === 'cnetcmpgnent' || layout === 'cnetcmpgnteams' || layout === 'cnetcmpgnind'
"
class="col-5 d-flex align-items-center justify-content-center"
>
<img
src="../../images/register-layout/usnews-360-badge.svg"
class="w-50 d-block"
alt="US News 360 Reviews Best Password Manager"
/>
</div>
<div
*ngIf="
layout === 'teams' ||
layout === 'teams1' ||
layout === 'teams2' ||
layout === 'enterprise' ||
layout === 'enterprise1' ||
layout === 'enterprise2' ||
layout === 'default'
"
class="col-5 d-flex align-items-center justify-content-center"
>
<img
src="../../images/register-layout/usnews-360-badge.svg"
class="w-50 d-block"
alt="US News 360 Reviews Best Password Manager"
/>
</div>
</div>
</div>
</div>

View File

@ -1,149 +0,0 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { first } from "rxjs/operators";
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/components/register.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PolicyData } from "@bitwarden/common/models/data/policy.data";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/models/domain/master-password-policy-options";
import { Policy } from "@bitwarden/common/models/domain/policy";
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
import { RouterService } from "../core";
@Component({
selector: "app-register",
templateUrl: "register.component.html",
})
export class RegisterComponent extends BaseRegisterComponent implements OnInit, OnDestroy {
email = "";
showCreateOrgMessage = false;
layout = "";
enforcedPolicyOptions: MasterPasswordPolicyOptions;
private policies: Policy[];
private destroy$ = new Subject<void>();
constructor(
formValidationErrorService: FormValidationErrorsService,
formBuilder: UntypedFormBuilder,
authService: AuthService,
router: Router,
i18nService: I18nService,
cryptoService: CryptoService,
apiService: ApiService,
private route: ActivatedRoute,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService,
private policyApiService: PolicyApiServiceAbstraction,
private policyService: PolicyService,
environmentService: EnvironmentService,
logService: LogService,
private routerService: RouterService
) {
super(
formValidationErrorService,
formBuilder,
authService,
router,
i18nService,
cryptoService,
apiService,
stateService,
platformUtilsService,
passwordGenerationService,
environmentService,
logService
);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.route.queryParams.pipe(first()).subscribe((qParams) => {
this.referenceData = new ReferenceEventRequest();
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
this.email = qParams.email;
}
if (qParams.premium != null) {
this.routerService.setPreviousUrl("/settings/premium");
} else if (qParams.org != null) {
this.showCreateOrgMessage = true;
this.referenceData.flow = qParams.org;
const route = this.router.createUrlTree(["create-organization"], {
queryParams: { plan: qParams.org },
});
this.routerService.setPreviousUrl(route.toString());
}
if (qParams.layout != null) {
this.layout = this.referenceData.layout = qParams.layout;
}
if (qParams.reference != null) {
this.referenceData.id = qParams.reference;
} else {
this.referenceData.id = ("; " + document.cookie)
.split("; reference=")
.pop()
.split(";")
.shift();
}
// Are they coming from an email for sponsoring a families organization
if (qParams.sponsorshipToken != null) {
// After logging in redirect them to setup the families sponsorship
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
queryParams: { plan: qParams.sponsorshipToken },
});
this.routerService.setPreviousUrl(route.toString());
}
if (this.referenceData.id === "") {
this.referenceData.id = null;
}
});
const invite = await this.stateService.getOrganizationInvitation();
if (invite != null) {
try {
const policies = await this.policyApiService.getPoliciesByToken(
invite.organizationId,
invite.token,
invite.email,
invite.organizationUserId
);
if (policies.data != null) {
const policiesData = policies.data.map((p) => new PolicyData(p));
this.policies = policiesData.map((p) => new Policy(p));
}
} catch (e) {
this.logService.error(e);
}
}
if (this.policies != null) {
this.policyService
.masterPasswordPolicyOptions$(this.policies)
.pipe(takeUntil(this.destroy$))
.subscribe((enforcedPasswordPolicyOptions) => {
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
});
}
await super.ngOnInit();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -23,26 +23,39 @@
<div class="tw-pt-12">
<!-- Layout params are used by marketing to determine left-hand content -->
<app-default-content *ngIf="layout === 'default'"></app-default-content>
<app-teams-content *ngIf="layout === 'teams'"></app-teams-content>
<app-teams1-content *ngIf="layout === 'teams1'"></app-teams1-content>
<app-teams2-content *ngIf="layout === 'teams2'"></app-teams2-content>
<app-enterprise-content *ngIf="layout === 'enterprise'"></app-enterprise-content>
<app-enterprise1-content *ngIf="layout === 'enterprise1'"></app-enterprise1-content>
<app-enterprise2-content *ngIf="layout === 'enterprise2'"></app-enterprise2-content>
<app-default-content *ngIf="layout === layouts.default"></app-default-content>
<app-teams-content *ngIf="layout === layouts.teams"></app-teams-content>
<app-teams1-content *ngIf="layout === layouts.teams1"></app-teams1-content>
<app-teams2-content *ngIf="layout === layouts.teams2"></app-teams2-content>
<app-enterprise-content *ngIf="layout === layouts.enterprise"></app-enterprise-content>
<app-enterprise1-content *ngIf="layout === layouts.enterprise1"></app-enterprise1-content>
<app-enterprise2-content *ngIf="layout === layouts.enterprise2"></app-enterprise2-content>
<app-cnet-enterprise-content
*ngIf="layout === 'cnetcmpgnent'"
*ngIf="layout === layouts.cnetcmpgnent"
></app-cnet-enterprise-content>
<app-cnet-individual-content
*ngIf="layout === 'cnetcmpgnind'"
*ngIf="layout === layouts.cnetcmpgnind"
></app-cnet-individual-content>
<app-cnet-teams-content *ngIf="layout === 'cnetcmpgnteams'"></app-cnet-teams-content>
<app-abm-enterprise-content *ngIf="layout === 'abmenterprise'"></app-abm-enterprise-content>
<app-abm-teams-content *ngIf="layout === 'abmteams'"></app-abm-teams-content>
<app-cnet-teams-content *ngIf="layout === layouts.cnetcmpgnteams"></app-cnet-teams-content>
<app-abm-enterprise-content
*ngIf="layout === layouts.abmenterprise"
></app-abm-enterprise-content>
<app-abm-teams-content *ngIf="layout === layouts.abmteams"></app-abm-teams-content>
</div>
</div>
<div class="tw-w-1/2">
<div class="tw-pt-56">
<div *ngIf="!useTrialStepper">
<div
class="tw-min-w-xl tw-m-auto tw-mt-28 tw-max-w-xl tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
>
<app-register-form
[queryParamEmail]="email"
[enforcedPolicyOptions]="enforcedPolicyOptions"
[referenceDataValue]="referenceData"
></app-register-form>
</div>
</div>
<div class="tw-pt-44" *ngIf="useTrialStepper">
<div class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background">
<div class="tw-flex tw-h-12 tw-w-full tw-items-center tw-rounded-t tw-bg-secondary-100">
<h2 class="tw-mb-0 tw-pl-4 tw-text-base tw-font-bold tw-uppercase">

View File

@ -168,8 +168,9 @@ describe("TrialInitiationComponent", () => {
it("should set org variable to be enterprise and plan to EnterpriseAnnually if org param is enterprise", fakeAsync(() => {
mockQueryParams.next({ org: "enterprise" });
tick(); // wait for resolution
fixture = TestBed.createComponent(TrialInitiationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
component.ngOnInit();
expect(component.org).toBe("enterprise");
expect(component.plan).toBe(PlanType.EnterpriseAnnually);
}));
@ -182,13 +183,33 @@ describe("TrialInitiationComponent", () => {
expect(component.org).toBe("");
expect(component.accountCreateOnly).toBe(true);
}));
it("should set the org to be families and plan to FamiliesAnnually if org param is invalid ", fakeAsync(async () => {
it("should not set the org if org param is invalid ", fakeAsync(async () => {
mockQueryParams.next({ org: "hahahaha" });
tick(); // wait for resolution
fixture = TestBed.createComponent(TrialInitiationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(component.org).toBe("");
expect(component.accountCreateOnly).toBe(true);
}));
it("should set the layout variable if layout param is valid ", fakeAsync(async () => {
mockQueryParams.next({ layout: "teams1" });
tick(); // wait for resolution
fixture = TestBed.createComponent(TrialInitiationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(component.layout).toBe("teams1");
expect(component.accountCreateOnly).toBe(false);
}));
it("should not set the layout variable and leave as 'default' if layout param is invalid ", fakeAsync(async () => {
mockQueryParams.next({ layout: "asdfasdf" });
tick(); // wait for resolution
fixture = TestBed.createComponent(TrialInitiationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
component.ngOnInit();
expect(component.org).toBe("families");
expect(component.plan).toBe(PlanType.FamiliesAnnually);
expect(component.layout).toBe("default");
expect(component.accountCreateOnly).toBe(true);
}));
});

View File

@ -20,6 +20,30 @@ import { ReferenceEventRequest } from "@bitwarden/common/models/request/referenc
import { RouterService } from "./../../core/router.service";
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
enum ValidOrgParams {
families = "families",
enterprise = "enterprise",
teams = "teams",
individual = "individual",
premium = "premium",
free = "free",
}
enum ValidLayoutParams {
default = "default",
teams = "teams",
teams1 = "teams1",
teams2 = "teams2",
enterprise = "enterprise",
enterprise1 = "enterprise1",
enterprise2 = "enterprise2",
cnetcmpgnent = "cnetcmpgnent",
cnetcmpgnind = "cnetcmpgnind",
cnetcmpgnteams = "cnetcmpgnteams",
abmenterprise = "abmenterprise",
abmteams = "abmteams",
}
@Component({
selector: "app-trial",
templateUrl: "trial-initiation.component.html",
@ -35,9 +59,20 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
plan: PlanType;
product: ProductType;
accountCreateOnly = true;
useTrialStepper = false;
policies: Policy[];
enforcedPolicyOptions: MasterPasswordPolicyOptions;
validOrgs: string[] = ["teams", "enterprise", "families"];
trialFlowOrgs: string[] = [
ValidOrgParams.teams,
ValidOrgParams.enterprise,
ValidOrgParams.families,
];
routeFlowOrgs: string[] = [
ValidOrgParams.free,
ValidOrgParams.premium,
ValidOrgParams.individual,
];
layouts = ValidLayoutParams;
referenceData: ReferenceEventRequest;
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
@ -87,39 +122,38 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
this.referenceDataId = qParams.reference;
if (!qParams.org) {
return;
}
if (qParams.layout) {
if (Object.values(ValidLayoutParams).includes(qParams.layout)) {
this.layout = qParams.layout;
this.accountCreateOnly = false;
}
if (this.validOrgs.includes(qParams.org)) {
if (this.trialFlowOrgs.includes(qParams.org)) {
this.org = qParams.org;
} else {
this.org = "families";
}
this.orgLabel = this.titleCasePipe.transform(this.org);
this.useTrialStepper = true;
this.referenceData.flow = qParams.org;
this.referenceData.flow = qParams.org;
if (this.org === ValidOrgParams.families) {
this.plan = PlanType.FamiliesAnnually;
this.product = ProductType.Families;
} else if (this.org === ValidOrgParams.teams) {
this.plan = PlanType.TeamsAnnually;
this.product = ProductType.Teams;
} else if (this.org === ValidOrgParams.enterprise) {
this.plan = PlanType.EnterpriseAnnually;
this.product = ProductType.Enterprise;
}
} else if (this.routeFlowOrgs.includes(qParams.org)) {
this.referenceData.flow = qParams.org;
const route = this.router.createUrlTree(["create-organization"], {
queryParams: { plan: qParams.org },
});
this.routerService.setPreviousUrl(route.toString());
}
// Are they coming from an email for sponsoring a families organization
// After logging in redirect them to setup the families sponsorship
this.setupFamilySponsorship(qParams.sponsorshipToken);
this.orgLabel = this.titleCasePipe.transform(this.org);
this.accountCreateOnly = false;
if (this.org === "families") {
this.plan = PlanType.FamiliesAnnually;
this.product = ProductType.Families;
} else if (this.org === "teams") {
this.plan = PlanType.TeamsAnnually;
this.product = ProductType.Teams;
} else if (this.org === "enterprise") {
this.plan = PlanType.EnterpriseAnnually;
this.product = ProductType.Enterprise;
}
});
const invite = await this.stateService.getOrganizationInvitation();

View File

@ -15,7 +15,6 @@ import { LoginWithDeviceComponent } from "./accounts/login/login-with-device.com
import { LoginComponent } from "./accounts/login/login.component";
import { RecoverDeleteComponent } from "./accounts/recover-delete.component";
import { RecoverTwoFactorComponent } from "./accounts/recover-two-factor.component";
import { RegisterComponent } from "./accounts/register.component";
import { RemovePasswordComponent } from "./accounts/remove-password.component";
import { SetPasswordComponent } from "./accounts/set-password.component";
import { SsoComponent } from "./accounts/sso.component";
@ -69,16 +68,15 @@ const routes: Routes = [
{ path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] },
{
path: "register",
component: RegisterComponent,
component: TrialInitiationComponent,
canActivate: [UnauthGuard],
data: { titleId: "createAccount" },
},
buildFlaggedRoute("showTrial", {
{
path: "trial",
component: TrialInitiationComponent,
canActivate: [UnauthGuard],
data: { titleId: "startTrial" },
}),
redirectTo: "register",
pathMatch: "full",
},
{
path: "sso",
component: SsoComponent,

View File

@ -7,7 +7,6 @@ import { LockComponent } from "../accounts/lock.component";
import { RecoverDeleteComponent } from "../accounts/recover-delete.component";
import { RecoverTwoFactorComponent } from "../accounts/recover-two-factor.component";
import { RegisterFormModule } from "../accounts/register-form/register-form.module";
import { RegisterComponent } from "../accounts/register.component";
import { RemovePasswordComponent } from "../accounts/remove-password.component";
import { SetPasswordComponent } from "../accounts/set-password.component";
import { SsoComponent } from "../accounts/sso.component";
@ -218,7 +217,6 @@ import { SharedModule } from ".";
PurgeVaultComponent,
RecoverDeleteComponent,
RecoverTwoFactorComponent,
RegisterComponent,
RemovePasswordComponent,
SecurityComponent,
SecurityKeysComponent,
@ -341,7 +339,6 @@ import { SharedModule } from ".";
PurgeVaultComponent,
RecoverDeleteComponent,
RecoverTwoFactorComponent,
RegisterComponent,
RemovePasswordComponent,
SecurityComponent,
SecurityKeysComponent,

View File

@ -9,7 +9,6 @@ import {
// required to avoid linting errors when there are no flags
/* eslint-disable-next-line @typescript-eslint/ban-types */
export type Flags = {
showTrial?: boolean;
secretsManager?: boolean;
showPasswordless?: boolean;
} & SharedFlags;