mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 06:43:35 +00:00
[PM-7317][Web] Clean up old components (#12862)
* Changes base on the ticket request * Move the trial route to Top * route the trial to signup * merge from main * Remove the register-form component --------- Co-authored-by: Evan Bassler <ebassler@livefront.com>
This commit is contained in:
@@ -1,158 +0,0 @@
|
|||||||
<!-- Please remove this disable statement when editing this file! -->
|
|
||||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
|
||||||
<form
|
|
||||||
#form
|
|
||||||
(ngSubmit)="submit()"
|
|
||||||
[appApiAction]="formPromise"
|
|
||||||
class="tw-container tw-mx-auto"
|
|
||||||
[formGroup]="formGroup"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div class="tw-mb-3">
|
|
||||||
<bit-form-field>
|
|
||||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
|
||||||
<input
|
|
||||||
id="register-form_input_email"
|
|
||||||
bitInput
|
|
||||||
type="email"
|
|
||||||
formControlName="email"
|
|
||||||
[attr.readonly]="queryParamFromOrgInvite ? true : null"
|
|
||||||
/>
|
|
||||||
<bit-hint>{{ "emailAddressDesc" | i18n }}</bit-hint>
|
|
||||||
</bit-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tw-mb-3">
|
|
||||||
<bit-form-field>
|
|
||||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
|
||||||
<input id="register-form_input_name" bitInput type="text" formControlName="name" />
|
|
||||||
<bit-hint>{{ "yourNameDesc" | i18n }}</bit-hint>
|
|
||||||
</bit-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tw-mb-3">
|
|
||||||
<auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
|
|
||||||
</auth-password-callout>
|
|
||||||
<bit-form-field>
|
|
||||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
|
||||||
<input
|
|
||||||
id="register-form_input_master-password"
|
|
||||||
bitInput
|
|
||||||
type="password"
|
|
||||||
formControlName="masterPassword"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitSuffix
|
|
||||||
bitIconButton
|
|
||||||
bitPasswordInputToggle
|
|
||||||
[(toggled)]="showPassword"
|
|
||||||
></button>
|
|
||||||
<bit-hint>
|
|
||||||
<span class="tw-font-semibold">{{ "important" | i18n }}</span>
|
|
||||||
{{ "masterPassImportant" | i18n }} {{ characterMinimumMessage }}
|
|
||||||
</bit-hint>
|
|
||||||
</bit-form-field>
|
|
||||||
<app-password-strength
|
|
||||||
[password]="formGroup.get('masterPassword')?.value"
|
|
||||||
[email]="formGroup.get('email')?.value"
|
|
||||||
[name]="formGroup.get('name')?.value"
|
|
||||||
[showText]="true"
|
|
||||||
(passwordStrengthResult)="getStrengthResult($event)"
|
|
||||||
>
|
|
||||||
</app-password-strength>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tw-mb-3">
|
|
||||||
<bit-form-field>
|
|
||||||
<bit-label>{{ "reTypeMasterPass" | i18n }}</bit-label>
|
|
||||||
<input
|
|
||||||
id="register-form_input_confirm-master-password"
|
|
||||||
bitInput
|
|
||||||
type="password"
|
|
||||||
formControlName="confirmMasterPassword"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitSuffix
|
|
||||||
bitIconButton
|
|
||||||
bitPasswordInputToggle
|
|
||||||
[(toggled)]="showPassword"
|
|
||||||
></button>
|
|
||||||
</bit-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tw-mb-3">
|
|
||||||
<bit-form-field>
|
|
||||||
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
|
||||||
<input id="register-form_input_hint" bitInput type="text" formControlName="hint" />
|
|
||||||
<bit-hint>{{ "masterPassHintDesc" | i18n }}</bit-hint>
|
|
||||||
</bit-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div [hidden]="!showCaptcha()">
|
|
||||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
|
||||||
</div>
|
|
||||||
<div class="tw-mb-4 tw-flex tw-items-start">
|
|
||||||
<input
|
|
||||||
class="mt-1"
|
|
||||||
type="checkbox"
|
|
||||||
bitCheckbox
|
|
||||||
id="checkForBreaches"
|
|
||||||
name="CheckBreach"
|
|
||||||
formControlName="checkForBreaches"
|
|
||||||
/>
|
|
||||||
<bit-label for="checkForBreaches"> {{ "checkForBreaches" | i18n }}</bit-label>
|
|
||||||
</div>
|
|
||||||
<div class="tw-mb-3 tw-flex tw-items-start" *ngIf="showTerms">
|
|
||||||
<input
|
|
||||||
class="mt-1"
|
|
||||||
id="register-form-input-accept-policies"
|
|
||||||
bitCheckbox
|
|
||||||
type="checkbox"
|
|
||||||
formControlName="acceptPolicies"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<bit-label for="register-form-input-accept-policies">
|
|
||||||
{{ "acceptPolicies" | i18n }}<br />
|
|
||||||
<a bitLink href="https://bitwarden.com/terms/" target="_blank" rel="noreferrer">{{
|
|
||||||
"termsOfService" | i18n
|
|
||||||
}}</a
|
|
||||||
>,
|
|
||||||
<a bitLink href="https://bitwarden.com/privacy/" target="_blank" rel="noreferrer">{{
|
|
||||||
"privacyPolicy" | i18n
|
|
||||||
}}</a>
|
|
||||||
</bit-label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tw-space-x-2 tw-pt-2">
|
|
||||||
<ng-container *ngIf="!accountCreated">
|
|
||||||
<button
|
|
||||||
[block]="true"
|
|
||||||
type="submit"
|
|
||||||
buttonType="primary"
|
|
||||||
bitButton
|
|
||||||
[loading]="form.loading"
|
|
||||||
>
|
|
||||||
{{ "createAccount" | i18n }}
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="accountCreated">
|
|
||||||
<button
|
|
||||||
[block]="true"
|
|
||||||
type="submit"
|
|
||||||
buttonType="primary"
|
|
||||||
bitButton
|
|
||||||
[loading]="form.loading"
|
|
||||||
>
|
|
||||||
{{ "logIn" | i18n }}
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
<p class="tw-m-0 tw-mt-5 tw-text-sm">
|
|
||||||
{{ "alreadyHaveAccount" | i18n }}
|
|
||||||
<a bitLink routerLink="/login">{{ "logIn" | i18n }}</a>
|
|
||||||
</p>
|
|
||||||
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { Component, Input, OnInit } from "@angular/core";
|
|
||||||
import { UntypedFormBuilder } from "@angular/forms";
|
|
||||||
import { Router } from "@angular/router";
|
|
||||||
|
|
||||||
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/auth/components/register.component";
|
|
||||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
|
||||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
|
||||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
|
||||||
import { RegisterRequest } from "@bitwarden/common/models/request/register.request";
|
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
|
||||||
|
|
||||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-register-form",
|
|
||||||
templateUrl: "./register-form.component.html",
|
|
||||||
})
|
|
||||||
export class RegisterFormComponent extends BaseRegisterComponent implements OnInit {
|
|
||||||
@Input() queryParamEmail: string;
|
|
||||||
@Input() queryParamFromOrgInvite: boolean;
|
|
||||||
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
|
||||||
@Input() referenceDataValue: ReferenceEventRequest;
|
|
||||||
|
|
||||||
showErrorSummary = false;
|
|
||||||
characterMinimumMessage: string;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
formValidationErrorService: FormValidationErrorsService,
|
|
||||||
formBuilder: UntypedFormBuilder,
|
|
||||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
|
||||||
router: Router,
|
|
||||||
i18nService: I18nService,
|
|
||||||
keyService: KeyService,
|
|
||||||
apiService: ApiService,
|
|
||||||
platformUtilsService: PlatformUtilsService,
|
|
||||||
private policyService: PolicyService,
|
|
||||||
environmentService: EnvironmentService,
|
|
||||||
logService: LogService,
|
|
||||||
auditService: AuditService,
|
|
||||||
dialogService: DialogService,
|
|
||||||
acceptOrgInviteService: AcceptOrganizationInviteService,
|
|
||||||
toastService: ToastService,
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
formValidationErrorService,
|
|
||||||
formBuilder,
|
|
||||||
loginStrategyService,
|
|
||||||
router,
|
|
||||||
i18nService,
|
|
||||||
keyService,
|
|
||||||
apiService,
|
|
||||||
platformUtilsService,
|
|
||||||
environmentService,
|
|
||||||
logService,
|
|
||||||
auditService,
|
|
||||||
dialogService,
|
|
||||||
toastService,
|
|
||||||
);
|
|
||||||
this.modifyRegisterRequest = async (request: RegisterRequest) => {
|
|
||||||
// Org invites are deep linked. Non-existent accounts are redirected to the register page.
|
|
||||||
// Org user id and token are included here only for validation and two factor purposes.
|
|
||||||
const orgInvite = await acceptOrgInviteService.getOrganizationInvite();
|
|
||||||
if (orgInvite != null) {
|
|
||||||
request.organizationUserId = orgInvite.organizationUserId;
|
|
||||||
request.token = orgInvite.token;
|
|
||||||
}
|
|
||||||
// Invite is accepted after login (on deep link redirect).
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
|
||||||
await super.ngOnInit();
|
|
||||||
this.referenceData = this.referenceDataValue;
|
|
||||||
if (this.queryParamEmail) {
|
|
||||||
this.formGroup.get("email")?.setValue(this.queryParamEmail);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.enforcedPolicyOptions != null && this.enforcedPolicyOptions.minLength > 0) {
|
|
||||||
this.characterMinimumMessage = "";
|
|
||||||
} else {
|
|
||||||
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit() {
|
|
||||||
if (
|
|
||||||
this.enforcedPolicyOptions != null &&
|
|
||||||
!this.policyService.evaluateMasterPassword(
|
|
||||||
this.passwordStrengthResult.score,
|
|
||||||
this.formGroup.value.masterPassword,
|
|
||||||
this.enforcedPolicyOptions,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.toastService.showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: this.i18nService.t("errorOccurred"),
|
|
||||||
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await super.submit(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { NgModule } from "@angular/core";
|
|
||||||
|
|
||||||
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
|
|
||||||
|
|
||||||
import { SharedModule } from "../../shared";
|
|
||||||
|
|
||||||
import { RegisterFormComponent } from "./register-form.component";
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [SharedModule, PasswordCalloutComponent],
|
|
||||||
declarations: [RegisterFormComponent],
|
|
||||||
exports: [RegisterFormComponent],
|
|
||||||
})
|
|
||||||
export class RegisterFormModule {}
|
|
||||||
@@ -1,17 +1,4 @@
|
|||||||
<app-vertical-stepper #stepper linear>
|
<app-vertical-stepper #stepper linear>
|
||||||
<app-vertical-step
|
|
||||||
label="{{ 'createAccount' | i18n | titlecase }}"
|
|
||||||
[editable]="false"
|
|
||||||
[subLabel]="subLabels.createAccount"
|
|
||||||
[addSubLabelSpacing]="true"
|
|
||||||
>
|
|
||||||
<app-register-form
|
|
||||||
[referenceDataValue]="referenceEventRequest"
|
|
||||||
[isInTrialFlow]="true"
|
|
||||||
(createdAccount)="accountCreated($event)"
|
|
||||||
>
|
|
||||||
</app-register-form>
|
|
||||||
</app-vertical-step>
|
|
||||||
<app-vertical-step
|
<app-vertical-step
|
||||||
label="{{ 'organizationInformation' | i18n | titlecase }}"
|
label="{{ 'organizationInformation' | i18n | titlecase }}"
|
||||||
[subLabel]="subLabels.organizationInfo"
|
[subLabel]="subLabels.organizationInfo"
|
||||||
|
|||||||
@@ -1,17 +1,4 @@
|
|||||||
<app-vertical-stepper #stepper linear>
|
<app-vertical-stepper #stepper linear>
|
||||||
<app-vertical-step
|
|
||||||
label="{{ 'createAccount' | i18n | titlecase }}"
|
|
||||||
[editable]="false"
|
|
||||||
[subLabel]="createAccountLabel"
|
|
||||||
[addSubLabelSpacing]="true"
|
|
||||||
>
|
|
||||||
<app-register-form
|
|
||||||
[referenceDataValue]="referenceEventRequest"
|
|
||||||
[isInTrialFlow]="true"
|
|
||||||
(createdAccount)="accountCreated($event)"
|
|
||||||
>
|
|
||||||
</app-register-form>
|
|
||||||
</app-vertical-step>
|
|
||||||
<app-vertical-step
|
<app-vertical-step
|
||||||
label="{{ 'organizationInformation' | i18n | titlecase }}"
|
label="{{ 'organizationInformation' | i18n | titlecase }}"
|
||||||
[subLabel]="subLabels.organizationInfo"
|
[subLabel]="subLabels.organizationInfo"
|
||||||
|
|||||||
@@ -19,7 +19,16 @@ import {
|
|||||||
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
|
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||||
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
|
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
|
||||||
import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component";
|
import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component";
|
||||||
import { ValidOrgParams } from "../trial-initiation.component";
|
|
||||||
|
export enum ValidOrgParams {
|
||||||
|
families = "families",
|
||||||
|
enterprise = "enterprise",
|
||||||
|
teams = "teams",
|
||||||
|
teamsStarter = "teamsStarter",
|
||||||
|
individual = "individual",
|
||||||
|
premium = "premium",
|
||||||
|
free = "free",
|
||||||
|
}
|
||||||
|
|
||||||
const trialFlowOrgs = [
|
const trialFlowOrgs = [
|
||||||
ValidOrgParams.teams,
|
ValidOrgParams.teams,
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
|
||||||
<app-secrets-manager-trial
|
|
||||||
*ngIf="layout === layouts.secretsManager; else passwordManagerTrial"
|
|
||||||
></app-secrets-manager-trial>
|
|
||||||
<ng-template #passwordManagerTrial>
|
|
||||||
<div *ngIf="accountCreateOnly" class="">
|
|
||||||
<h1 class="tw-mt-12 tw-text-center tw-text-xl">{{ "createAccount" | i18n }}</h1>
|
|
||||||
<div
|
|
||||||
class="tw-min-w-xl tw-m-auto 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"
|
|
||||||
[queryParamFromOrgInvite]="fromOrgInvite"
|
|
||||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
|
||||||
[referenceDataValue]="referenceData"
|
|
||||||
></app-register-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="!accountCreateOnly">
|
|
||||||
<div class="tw-absolute tw--z-10 tw--mt-48 tw-h-[28rem] tw-w-full tw-bg-background-alt2"></div>
|
|
||||||
<div class="tw-min-w-4xl tw-mx-auto tw-flex tw-max-w-screen-xl tw-gap-12 tw-px-4">
|
|
||||||
<div class="tw-w-1/2">
|
|
||||||
<img
|
|
||||||
alt="Bitwarden"
|
|
||||||
style="height: 50px; width: 335px"
|
|
||||||
class="tw-mt-6"
|
|
||||||
src="../../images/register-layout/logo-horizontal-white.svg"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="tw-pt-12">
|
|
||||||
<!-- Layout params are used by marketing to determine left-hand 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-teams3-content *ngIf="layout === layouts.teams3"></app-teams3-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 === layouts.cnetcmpgnent"
|
|
||||||
></app-cnet-enterprise-content>
|
|
||||||
<app-cnet-individual-content
|
|
||||||
*ngIf="layout === layouts.cnetcmpgnind"
|
|
||||||
></app-cnet-individual-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 *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-auto tw-w-full tw-gap-5 tw-rounded-t tw-bg-secondary-100">
|
|
||||||
<h2 class="tw-pb-4 tw-pl-4 tw-pt-5 tw-text-base tw-font-bold tw-uppercase">
|
|
||||||
{{ freeTrialText }}
|
|
||||||
</h2>
|
|
||||||
<environment-selector
|
|
||||||
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
|
|
||||||
></environment-selector>
|
|
||||||
</div>
|
|
||||||
<app-vertical-stepper #stepper linear (selectionChange)="stepSelectionChange($event)">
|
|
||||||
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
|
||||||
<app-register-form
|
|
||||||
[isInTrialFlow]="true"
|
|
||||||
(createdAccount)="createdAccount($event)"
|
|
||||||
[referenceDataValue]="referenceData"
|
|
||||||
></app-register-form>
|
|
||||||
</app-vertical-step>
|
|
||||||
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
|
||||||
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitButton
|
|
||||||
buttonType="primary"
|
|
||||||
[disabled]="orgInfoFormGroup.get('name').invalid"
|
|
||||||
[loading]="loading"
|
|
||||||
(click)="createOrganizationOnTrial()"
|
|
||||||
>
|
|
||||||
{{ (enableTrialPayment$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }}
|
|
||||||
</button>
|
|
||||||
</app-vertical-step>
|
|
||||||
<app-vertical-step
|
|
||||||
label="Billing"
|
|
||||||
[subLabel]="billingSubLabel"
|
|
||||||
*ngIf="!(enableTrialPayment$ | async)"
|
|
||||||
>
|
|
||||||
<app-trial-billing-step
|
|
||||||
*ngIf="stepper.selectedIndex === 2"
|
|
||||||
[organizationInfo]="{
|
|
||||||
name: orgInfoFormGroup.get('name').value,
|
|
||||||
email: orgInfoFormGroup.get('email').value,
|
|
||||||
type: trialOrganizationType,
|
|
||||||
}"
|
|
||||||
[subscriptionProduct]="SubscriptionProduct.PasswordManager"
|
|
||||||
(steppedBack)="previousStep()"
|
|
||||||
(organizationCreated)="createdOrganization($event)"
|
|
||||||
>
|
|
||||||
</app-trial-billing-step>
|
|
||||||
</app-vertical-step>
|
|
||||||
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
|
|
||||||
<app-trial-confirmation-details
|
|
||||||
[email]="email"
|
|
||||||
[orgLabel]="orgLabel"
|
|
||||||
></app-trial-confirmation-details>
|
|
||||||
<div class="tw-mb-3 tw-flex">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitButton
|
|
||||||
buttonType="primary"
|
|
||||||
(click)="navigateToOrgVault()"
|
|
||||||
>
|
|
||||||
{{ "getStarted" | i18n | titlecase }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
bitButton
|
|
||||||
buttonType="secondary"
|
|
||||||
(click)="navigateToOrgInvite()"
|
|
||||||
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
|
|
||||||
>
|
|
||||||
{{ "inviteUsers" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</app-vertical-step>
|
|
||||||
</app-vertical-stepper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
|
||||||
import { TitleCasePipe } from "@angular/common";
|
|
||||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
|
||||||
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
|
||||||
import { FormBuilder, UntypedFormBuilder } from "@angular/forms";
|
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
|
||||||
import { RouterTestingModule } from "@angular/router/testing";
|
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
|
||||||
import { BehaviorSubject, of } from "rxjs";
|
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
|
||||||
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
|
||||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
|
|
||||||
import { AcceptOrganizationInviteService } from "../../auth/organization-invite/accept-organization.service";
|
|
||||||
import { OrganizationInvite } from "../../auth/organization-invite/organization-invite";
|
|
||||||
import { RouterService } from "../../core";
|
|
||||||
import { SharedModule } from "../../shared";
|
|
||||||
|
|
||||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
|
||||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
|
||||||
|
|
||||||
describe("TrialInitiationComponent", () => {
|
|
||||||
let component: TrialInitiationComponent;
|
|
||||||
let fixture: ComponentFixture<TrialInitiationComponent>;
|
|
||||||
const mockQueryParams = new BehaviorSubject<any>({ org: "enterprise" });
|
|
||||||
const testOrgId = "91329456-5b9f-44b3-9279-6bb9ee6a0974";
|
|
||||||
const formBuilder: FormBuilder = new FormBuilder();
|
|
||||||
let routerSpy: jest.SpyInstance;
|
|
||||||
|
|
||||||
let stateServiceMock: MockProxy<StateService>;
|
|
||||||
let policyApiServiceMock: MockProxy<PolicyApiServiceAbstraction>;
|
|
||||||
let policyServiceMock: MockProxy<PolicyService>;
|
|
||||||
let routerServiceMock: MockProxy<RouterService>;
|
|
||||||
let acceptOrgInviteServiceMock: MockProxy<AcceptOrganizationInviteService>;
|
|
||||||
let organizationBillingServiceMock: MockProxy<OrganizationBillingService>;
|
|
||||||
let configServiceMock: MockProxy<ConfigService>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// only define services directly that we want to mock return values in this component
|
|
||||||
stateServiceMock = mock<StateService>();
|
|
||||||
policyApiServiceMock = mock<PolicyApiServiceAbstraction>();
|
|
||||||
policyServiceMock = mock<PolicyService>();
|
|
||||||
routerServiceMock = mock<RouterService>();
|
|
||||||
acceptOrgInviteServiceMock = mock<AcceptOrganizationInviteService>();
|
|
||||||
organizationBillingServiceMock = mock<OrganizationBillingService>();
|
|
||||||
configServiceMock = mock<ConfigService>();
|
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
SharedModule,
|
|
||||||
RouterTestingModule.withRoutes([
|
|
||||||
{ path: "trial", component: TrialInitiationComponent },
|
|
||||||
{
|
|
||||||
path: `organizations/${testOrgId}/vault`,
|
|
||||||
component: BlankComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `organizations/${testOrgId}/members`,
|
|
||||||
component: BlankComponent,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
declarations: [TrialInitiationComponent, I18nPipe],
|
|
||||||
providers: [
|
|
||||||
UntypedFormBuilder,
|
|
||||||
{
|
|
||||||
provide: ActivatedRoute,
|
|
||||||
useValue: {
|
|
||||||
queryParams: mockQueryParams.asObservable(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ provide: StateService, useValue: stateServiceMock },
|
|
||||||
{ provide: PolicyService, useValue: policyServiceMock },
|
|
||||||
{ provide: PolicyApiServiceAbstraction, useValue: policyApiServiceMock },
|
|
||||||
{ provide: LogService, useValue: mock<LogService>() },
|
|
||||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
|
||||||
{ provide: TitleCasePipe, useValue: mock<TitleCasePipe>() },
|
|
||||||
{
|
|
||||||
provide: VerticalStepperComponent,
|
|
||||||
useClass: VerticalStepperStubComponent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: RouterService,
|
|
||||||
useValue: routerServiceMock,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: AcceptOrganizationInviteService,
|
|
||||||
useValue: acceptOrgInviteServiceMock,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: OrganizationBillingService,
|
|
||||||
useValue: organizationBillingServiceMock,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ConfigService,
|
|
||||||
useValue: configServiceMock,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
schemas: [NO_ERRORS_SCHEMA], // Allows child components to be ignored (such as register component)
|
|
||||||
}).compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create", () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
// These tests demonstrate mocking service calls
|
|
||||||
describe("onInit() enforcedPolicyOptions", () => {
|
|
||||||
it("should not set enforcedPolicyOptions if there isn't an org invite in deep linked url", async () => {
|
|
||||||
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce(null);
|
|
||||||
// Need to recreate component with new service mock
|
|
||||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
await component.ngOnInit();
|
|
||||||
|
|
||||||
expect(component.enforcedPolicyOptions).toBe(undefined);
|
|
||||||
});
|
|
||||||
it("should set enforcedPolicyOptions if the deep linked url has an org invite", async () => {
|
|
||||||
// Set up service method mocks
|
|
||||||
acceptOrgInviteServiceMock.getOrganizationInvite.mockResolvedValueOnce({
|
|
||||||
organizationId: testOrgId,
|
|
||||||
token: "token",
|
|
||||||
email: "testEmail",
|
|
||||||
organizationUserId: "123",
|
|
||||||
} as OrganizationInvite);
|
|
||||||
policyApiServiceMock.getPoliciesByToken.mockReturnValueOnce(
|
|
||||||
Promise.resolve([
|
|
||||||
{
|
|
||||||
id: "345",
|
|
||||||
organizationId: testOrgId,
|
|
||||||
type: 1,
|
|
||||||
data: {
|
|
||||||
minComplexity: 4,
|
|
||||||
minLength: 10,
|
|
||||||
requireLower: null,
|
|
||||||
requireNumbers: null,
|
|
||||||
requireSpecial: null,
|
|
||||||
requireUpper: null,
|
|
||||||
},
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
] as Policy[]),
|
|
||||||
);
|
|
||||||
policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue(
|
|
||||||
of({
|
|
||||||
minComplexity: 4,
|
|
||||||
minLength: 10,
|
|
||||||
requireLower: null,
|
|
||||||
requireNumbers: null,
|
|
||||||
requireSpecial: null,
|
|
||||||
requireUpper: null,
|
|
||||||
} as MasterPasswordPolicyOptions),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Need to recreate component with new service mocks
|
|
||||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
await component.ngOnInit();
|
|
||||||
expect(component.enforcedPolicyOptions).toMatchObject({
|
|
||||||
minComplexity: 4,
|
|
||||||
minLength: 10,
|
|
||||||
requireLower: null,
|
|
||||||
requireNumbers: null,
|
|
||||||
requireSpecial: null,
|
|
||||||
requireUpper: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// These tests demonstrate route params
|
|
||||||
describe("Route params", () => {
|
|
||||||
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();
|
|
||||||
expect(component.org).toBe("enterprise");
|
|
||||||
expect(component.plan).toBe(PlanType.EnterpriseAnnually);
|
|
||||||
}));
|
|
||||||
it("should not set org variable if no org param is provided", fakeAsync(() => {
|
|
||||||
mockQueryParams.next({});
|
|
||||||
tick(); // wait for resolution
|
|
||||||
fixture = TestBed.createComponent(TrialInitiationComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
expect(component.org).toBe("");
|
|
||||||
expect(component.accountCreateOnly).toBe(true);
|
|
||||||
}));
|
|
||||||
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();
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
component.ngOnInit();
|
|
||||||
expect(component.layout).toBe("default");
|
|
||||||
expect(component.accountCreateOnly).toBe(true);
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// These tests demonstrate the use of a stub component
|
|
||||||
describe("createAccount()", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
|
||||||
.componentInstance as VerticalStepperComponent;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set email and call verticalStepper.next()", fakeAsync(() => {
|
|
||||||
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
|
|
||||||
component.createdAccount("test@email.com");
|
|
||||||
expect(verticalStepperNext).toHaveBeenCalled();
|
|
||||||
expect(component.email).toBe("test@email.com");
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("billingSuccess()", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
|
||||||
.componentInstance as VerticalStepperComponent;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set orgId and call verticalStepper.next()", () => {
|
|
||||||
const verticalStepperNext = jest.spyOn(component.verticalStepper, "next");
|
|
||||||
component.billingSuccess({ orgId: testOrgId });
|
|
||||||
expect(verticalStepperNext).toHaveBeenCalled();
|
|
||||||
expect(component.orgId).toBe(testOrgId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("stepSelectionChange()", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
|
||||||
.componentInstance as VerticalStepperComponent;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on step 2 should show organization copy text", () => {
|
|
||||||
component.stepSelectionChange({
|
|
||||||
selectedIndex: 1,
|
|
||||||
previouslySelectedIndex: 0,
|
|
||||||
} as StepperSelectionEvent);
|
|
||||||
|
|
||||||
expect(component.orgInfoSubLabel).toContain("Enter your");
|
|
||||||
expect(component.orgInfoSubLabel).toContain(" organization information");
|
|
||||||
});
|
|
||||||
it("going from step 2 to 3 should set the orgInforSubLabel to be the Org name from orgInfoFormGroup", () => {
|
|
||||||
component.orgInfoFormGroup = formBuilder.group({
|
|
||||||
name: ["Hooli"],
|
|
||||||
email: [""],
|
|
||||||
});
|
|
||||||
component.stepSelectionChange({
|
|
||||||
selectedIndex: 2,
|
|
||||||
previouslySelectedIndex: 1,
|
|
||||||
} as StepperSelectionEvent);
|
|
||||||
|
|
||||||
expect(component.orgInfoSubLabel).toContain("Hooli");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("previousStep()", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
component.verticalStepper = TestBed.createComponent(VerticalStepperStubComponent)
|
|
||||||
.componentInstance as VerticalStepperComponent;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
|
||||||
const verticalStepperPrevious = jest.spyOn(component.verticalStepper, "previous");
|
|
||||||
component.previousStep();
|
|
||||||
expect(verticalStepperPrevious).toHaveBeenCalled();
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// These tests demonstrate router navigation
|
|
||||||
describe("navigation methods", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
component.orgId = testOrgId;
|
|
||||||
const router = TestBed.inject(Router);
|
|
||||||
fixture.detectChanges();
|
|
||||||
routerSpy = jest.spyOn(router, "navigate");
|
|
||||||
});
|
|
||||||
describe("navigateToOrgVault", () => {
|
|
||||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
|
||||||
component.navigateToOrgVault();
|
|
||||||
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "vault"]);
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
describe("navigateToOrgVault", () => {
|
|
||||||
it("should call verticalStepper.previous()", fakeAsync(() => {
|
|
||||||
component.navigateToOrgInvite();
|
|
||||||
expect(routerSpy).toHaveBeenCalledWith(["organizations", testOrgId, "members"]);
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export class VerticalStepperStubComponent extends VerticalStepperComponent {}
|
|
||||||
export class BlankComponent {} // For router tests
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
|
||||||
import { TitleCasePipe } from "@angular/common";
|
|
||||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
|
||||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
|
||||||
import { Subject, takeUntil } from "rxjs";
|
|
||||||
|
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
|
||||||
import {
|
|
||||||
OrganizationInformation,
|
|
||||||
PlanInformation,
|
|
||||||
OrganizationBillingServiceAbstraction as OrganizationBillingService,
|
|
||||||
} from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
|
||||||
import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
||||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
|
|
||||||
import { AcceptOrganizationInviteService } from "../../auth/organization-invite/accept-organization.service";
|
|
||||||
import { OrganizationInvite } from "../../auth/organization-invite/organization-invite";
|
|
||||||
import {
|
|
||||||
OrganizationCreatedEvent,
|
|
||||||
SubscriptionProduct,
|
|
||||||
TrialOrganizationType,
|
|
||||||
} from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
|
||||||
|
|
||||||
import { RouterService } from "./../../core/router.service";
|
|
||||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
|
||||||
|
|
||||||
export enum ValidOrgParams {
|
|
||||||
families = "families",
|
|
||||||
enterprise = "enterprise",
|
|
||||||
teams = "teams",
|
|
||||||
teamsStarter = "teamsStarter",
|
|
||||||
individual = "individual",
|
|
||||||
premium = "premium",
|
|
||||||
free = "free",
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ValidLayoutParams {
|
|
||||||
default = "default",
|
|
||||||
teams = "teams",
|
|
||||||
teams1 = "teams1",
|
|
||||||
teams2 = "teams2",
|
|
||||||
teams3 = "teams3",
|
|
||||||
enterprise = "enterprise",
|
|
||||||
enterprise1 = "enterprise1",
|
|
||||||
enterprise2 = "enterprise2",
|
|
||||||
cnetcmpgnent = "cnetcmpgnent",
|
|
||||||
cnetcmpgnind = "cnetcmpgnind",
|
|
||||||
cnetcmpgnteams = "cnetcmpgnteams",
|
|
||||||
abmenterprise = "abmenterprise",
|
|
||||||
abmteams = "abmteams",
|
|
||||||
secretsManager = "secretsManager",
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-trial",
|
|
||||||
templateUrl: "trial-initiation.component.html",
|
|
||||||
})
|
|
||||||
export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|
||||||
email = "";
|
|
||||||
fromOrgInvite = false;
|
|
||||||
org = "";
|
|
||||||
orgInfoSubLabel = "";
|
|
||||||
orgId = "";
|
|
||||||
orgLabel = "";
|
|
||||||
billingSubLabel = "";
|
|
||||||
layout = "default";
|
|
||||||
plan: PlanType;
|
|
||||||
productTier: ProductTierType;
|
|
||||||
accountCreateOnly = true;
|
|
||||||
useTrialStepper = false;
|
|
||||||
loading = false;
|
|
||||||
policies: Policy[];
|
|
||||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
|
||||||
trialFlowOrgs: string[] = [
|
|
||||||
ValidOrgParams.teams,
|
|
||||||
ValidOrgParams.teamsStarter,
|
|
||||||
ValidOrgParams.enterprise,
|
|
||||||
ValidOrgParams.families,
|
|
||||||
];
|
|
||||||
routeFlowOrgs: string[] = [
|
|
||||||
ValidOrgParams.free,
|
|
||||||
ValidOrgParams.premium,
|
|
||||||
ValidOrgParams.individual,
|
|
||||||
];
|
|
||||||
layouts = ValidLayoutParams;
|
|
||||||
referenceData: ReferenceEventRequest;
|
|
||||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
|
||||||
|
|
||||||
orgInfoFormGroup = this.formBuilder.group({
|
|
||||||
name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }],
|
|
||||||
email: [""],
|
|
||||||
});
|
|
||||||
|
|
||||||
private set referenceDataId(referenceId: string) {
|
|
||||||
if (referenceId != null) {
|
|
||||||
this.referenceData.id = referenceId;
|
|
||||||
} else {
|
|
||||||
this.referenceData.id = ("; " + document.cookie)
|
|
||||||
.split("; reference=")
|
|
||||||
.pop()
|
|
||||||
.split(";")
|
|
||||||
.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.referenceData.id === "") {
|
|
||||||
this.referenceData.id = null;
|
|
||||||
} else {
|
|
||||||
// Matches "_ga_QBRN562QQQ=value1.value2.session" and captures values and session.
|
|
||||||
const regex = /_ga_QBRN562QQQ=([^.]+)\.([^.]+)\.(\d+)/;
|
|
||||||
const match = document.cookie.match(regex);
|
|
||||||
if (match) {
|
|
||||||
this.referenceData.session = match[3];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
protected enableTrialPayment$ = this.configService.getFeatureFlag$(
|
|
||||||
FeatureFlag.TrialPaymentOptional,
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
protected router: Router,
|
|
||||||
private formBuilder: UntypedFormBuilder,
|
|
||||||
private titleCasePipe: TitleCasePipe,
|
|
||||||
private logService: LogService,
|
|
||||||
private policyApiService: PolicyApiServiceAbstraction,
|
|
||||||
private policyService: PolicyService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private routerService: RouterService,
|
|
||||||
private acceptOrgInviteService: AcceptOrganizationInviteService,
|
|
||||||
private organizationBillingService: OrganizationBillingService,
|
|
||||||
private configService: ConfigService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
|
||||||
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => {
|
|
||||||
this.referenceData = new ReferenceEventRequest();
|
|
||||||
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
|
||||||
this.email = qParams.email;
|
|
||||||
this.fromOrgInvite = qParams.fromOrgInvite === "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.referenceDataId = qParams.reference;
|
|
||||||
|
|
||||||
if (Object.values(ValidLayoutParams).includes(qParams.layout)) {
|
|
||||||
this.layout = qParams.layout;
|
|
||||||
this.accountCreateOnly = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.trialFlowOrgs.includes(qParams.org)) {
|
|
||||||
this.org = qParams.org;
|
|
||||||
this.orgLabel = this.titleCasePipe.transform(this.orgDisplayName);
|
|
||||||
this.useTrialStepper = true;
|
|
||||||
this.referenceData.flow = qParams.org;
|
|
||||||
|
|
||||||
if (this.org === ValidOrgParams.families) {
|
|
||||||
this.plan = PlanType.FamiliesAnnually;
|
|
||||||
this.productTier = ProductTierType.Families;
|
|
||||||
} else if (this.org === ValidOrgParams.teamsStarter) {
|
|
||||||
this.plan = PlanType.TeamsStarter;
|
|
||||||
this.productTier = ProductTierType.TeamsStarter;
|
|
||||||
} else if (this.org === ValidOrgParams.teams) {
|
|
||||||
this.plan = PlanType.TeamsAnnually;
|
|
||||||
this.productTier = ProductTierType.Teams;
|
|
||||||
} else if (this.org === ValidOrgParams.enterprise) {
|
|
||||||
this.plan = PlanType.EnterpriseAnnually;
|
|
||||||
this.productTier = ProductTierType.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.referenceData.initiationPath = this.accountCreateOnly
|
|
||||||
? "Registration form"
|
|
||||||
: "Password Manager trial from marketing website";
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there's a deep linked org invite, use it to get the password policies
|
|
||||||
const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite();
|
|
||||||
if (orgInvite != null) {
|
|
||||||
await this.initPasswordPolicies(orgInvite);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.orgInfoFormGroup.controls.name.valueChanges
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.orgInfoFormGroup.controls.name.markAsTouched();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
stepSelectionChange(event: StepperSelectionEvent) {
|
|
||||||
// Set org info sub label
|
|
||||||
if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") {
|
|
||||||
this.orgInfoSubLabel =
|
|
||||||
"Enter your " +
|
|
||||||
this.titleCasePipe.transform(this.orgDisplayName) +
|
|
||||||
" organization information";
|
|
||||||
} else if (event.previouslySelectedIndex === 1) {
|
|
||||||
this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
//set billing sub label
|
|
||||||
if (event.selectedIndex === 2) {
|
|
||||||
this.billingSubLabel = this.i18nService.t("billingTrialSubLabel");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOrganizationOnTrial() {
|
|
||||||
this.loading = true;
|
|
||||||
const organization: OrganizationInformation = {
|
|
||||||
name: this.orgInfoFormGroup.get("name").value,
|
|
||||||
billingEmail: this.orgInfoFormGroup.get("email").value,
|
|
||||||
initiationPath: "Password Manager trial from marketing website",
|
|
||||||
};
|
|
||||||
|
|
||||||
const plan: PlanInformation = {
|
|
||||||
type: this.plan,
|
|
||||||
passwordManagerSeats: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({
|
|
||||||
organization,
|
|
||||||
plan,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.orgId = response?.id;
|
|
||||||
this.billingSubLabel = `${this.i18nService.t("annual")} ($0/${this.i18nService.t("yr")})`;
|
|
||||||
this.loading = false;
|
|
||||||
this.verticalStepper.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
createdAccount(email: string) {
|
|
||||||
this.email = email;
|
|
||||||
this.orgInfoFormGroup.get("email")?.setValue(email);
|
|
||||||
this.verticalStepper.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
billingSuccess(event: any) {
|
|
||||||
this.orgId = event?.orgId;
|
|
||||||
this.billingSubLabel = event?.subLabelText;
|
|
||||||
this.verticalStepper.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
createdOrganization(event: OrganizationCreatedEvent) {
|
|
||||||
this.orgId = event.organizationId;
|
|
||||||
this.billingSubLabel = event.planDescription;
|
|
||||||
this.verticalStepper.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToOrgVault() {
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["organizations", this.orgId, "vault"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToOrgInvite() {
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["organizations", this.orgId, "members"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
previousStep() {
|
|
||||||
this.verticalStepper.previous();
|
|
||||||
}
|
|
||||||
|
|
||||||
get orgDisplayName() {
|
|
||||||
if (this.org === "teamsStarter") {
|
|
||||||
return "Teams Starter";
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.org;
|
|
||||||
}
|
|
||||||
|
|
||||||
get freeTrialText() {
|
|
||||||
const translationKey =
|
|
||||||
this.layout === this.layouts.secretsManager
|
|
||||||
? "startYour7DayFreeTrialOfBitwardenSecretsManagerFor"
|
|
||||||
: "startYour7DayFreeTrialOfBitwardenFor";
|
|
||||||
|
|
||||||
return this.i18nService.t(translationKey, this.org);
|
|
||||||
}
|
|
||||||
|
|
||||||
get trialOrganizationType(): TrialOrganizationType {
|
|
||||||
switch (this.productTier) {
|
|
||||||
case ProductTierType.Free:
|
|
||||||
return null;
|
|
||||||
default:
|
|
||||||
return this.productTier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupFamilySponsorship(sponsorshipToken: string) {
|
|
||||||
if (sponsorshipToken != null) {
|
|
||||||
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
|
||||||
queryParams: { plan: sponsorshipToken },
|
|
||||||
});
|
|
||||||
this.routerService.setPreviousUrl(route.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initPasswordPolicies(invite: OrganizationInvite): Promise<void> {
|
|
||||||
if (invite == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.policies = await this.policyApiService.getPoliciesByToken(
|
|
||||||
invite.organizationId,
|
|
||||||
invite.token,
|
|
||||||
invite.email,
|
|
||||||
invite.organizationUserId,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.policies != null) {
|
|
||||||
this.policyService
|
|
||||||
.masterPasswordPolicyOptions$(this.policies)
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
|
||||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
|
||||||
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly SubscriptionProduct = SubscriptionProduct;
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import { InputPasswordComponent } from "@bitwarden/auth/angular";
|
|||||||
import { FormFieldModule } from "@bitwarden/components";
|
import { FormFieldModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
|
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
|
||||||
import { RegisterFormModule } from "../../auth/register-form/register-form.module";
|
|
||||||
import { TaxInfoComponent } from "../../billing";
|
import { TaxInfoComponent } from "../../billing";
|
||||||
import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||||
import { SecretsManagerTrialFreeStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component";
|
import { SecretsManagerTrialFreeStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component";
|
||||||
@@ -39,7 +38,6 @@ import { TeamsContentComponent } from "./content/teams-content.component";
|
|||||||
import { Teams1ContentComponent } from "./content/teams1-content.component";
|
import { Teams1ContentComponent } from "./content/teams1-content.component";
|
||||||
import { Teams2ContentComponent } from "./content/teams2-content.component";
|
import { Teams2ContentComponent } from "./content/teams2-content.component";
|
||||||
import { Teams3ContentComponent } from "./content/teams3-content.component";
|
import { Teams3ContentComponent } from "./content/teams3-content.component";
|
||||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
|
||||||
import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
|
import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -48,7 +46,6 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
|||||||
CdkStepperModule,
|
CdkStepperModule,
|
||||||
VerticalStepperModule,
|
VerticalStepperModule,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
RegisterFormModule,
|
|
||||||
OrganizationCreateModule,
|
OrganizationCreateModule,
|
||||||
EnvironmentSelectorModule,
|
EnvironmentSelectorModule,
|
||||||
TaxInfoComponent,
|
TaxInfoComponent,
|
||||||
@@ -56,7 +53,6 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
|||||||
InputPasswordComponent,
|
InputPasswordComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
TrialInitiationComponent,
|
|
||||||
CompleteTrialInitiationComponent,
|
CompleteTrialInitiationComponent,
|
||||||
EnterpriseContentComponent,
|
EnterpriseContentComponent,
|
||||||
TeamsContentComponent,
|
TeamsContentComponent,
|
||||||
@@ -87,7 +83,7 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
|||||||
SecretsManagerTrialFreeStepperComponent,
|
SecretsManagerTrialFreeStepperComponent,
|
||||||
SecretsManagerTrialPaidStepperComponent,
|
SecretsManagerTrialPaidStepperComponent,
|
||||||
],
|
],
|
||||||
exports: [TrialInitiationComponent, CompleteTrialInitiationComponent],
|
exports: [CompleteTrialInitiationComponent],
|
||||||
providers: [TitleCasePipe],
|
providers: [TitleCasePipe],
|
||||||
})
|
})
|
||||||
export class TrialInitiationModule {}
|
export class TrialInitiationModule {}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from
|
|||||||
import { HintComponent } from "../auth/hint.component";
|
import { HintComponent } from "../auth/hint.component";
|
||||||
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
|
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
|
||||||
import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component";
|
import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component";
|
||||||
import { RegisterFormModule } from "../auth/register-form/register-form.module";
|
|
||||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||||
import { AccountComponent } from "../auth/settings/account/account.component";
|
import { AccountComponent } from "../auth/settings/account/account.component";
|
||||||
@@ -90,7 +89,6 @@ import { SharedModule } from "./shared.module";
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
SharedModule,
|
SharedModule,
|
||||||
RegisterFormModule,
|
|
||||||
ProductSwitcherModule,
|
ProductSwitcherModule,
|
||||||
UserVerificationModule,
|
UserVerificationModule,
|
||||||
ChangeKdfModule,
|
ChangeKdfModule,
|
||||||
|
|||||||
Reference in New Issue
Block a user