mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01:00
[PM-1655] Trial Registration Layout (#9091)
* add messaging for finish sign up component * Add product enum for finish sign up components * Allow confirmation details component to display secret manager confirmation * add FinishSignUp component - Started as exact copy of trial initiation component - Consolidated with secrets manager trial components * Integration finish sign up component into routing - Use anon layout component - Add resolver to pass the accurate title to the layout * migrate to product tier type * use existing ProductType enum * migrate to accept org service * fix query param parsing for free trial text * migrate finish sign up to complete trial naming * migrate fully to productTier * fix import of free trial resolver * increase max width of anon layout * add auth-input component * refactor component makeup * export the users password if needed to auto login the user * handle login situations where a stepper isn't used * fix type check * allow max width of anon layout to be configurable * remove account created toast * update productTier query param in text resolver * set maxWidth for secrets manager trial route * parse product query param as an int * properly show registration error * update routes to be from the root rather than relative * install updated prettier and apply fixes * fix missing password in test --------- Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
This commit is contained in:
parent
339768947b
commit
8f437dc773
@ -148,6 +148,7 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
password: "password",
|
||||
};
|
||||
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
|
@ -0,0 +1,80 @@
|
||||
<div *ngIf="!useTrialStepper">
|
||||
<auth-input-password
|
||||
[email]="email"
|
||||
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
||||
[buttonText]="'createAccount' | i18n"
|
||||
></auth-input-password>
|
||||
</div>
|
||||
<div *ngIf="useTrialStepper">
|
||||
<app-vertical-stepper #stepper linear (selectionChange)="verticalStepChange($event)">
|
||||
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
||||
<auth-input-password
|
||||
[email]="email"
|
||||
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
||||
[buttonText]="'createAccount' | i18n"
|
||||
></auth-input-password>
|
||||
</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.controls.name.invalid"
|
||||
(click)="conditionallyCreateOrganization()"
|
||||
>
|
||||
{{ "next" | i18n }}
|
||||
</button>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Billing" [subLabel]="billingSubLabel" *ngIf="!isSecretsManagerFree">
|
||||
<app-trial-billing-step
|
||||
*ngIf="stepper.selectedIndex === 2"
|
||||
[organizationInfo]="{
|
||||
name: orgInfoFormGroup.value.name,
|
||||
email: orgInfoFormGroup.value.billingEmail,
|
||||
type: trialOrganizationType,
|
||||
}"
|
||||
[subscriptionProduct]="
|
||||
product === ProductType.SecretsManager
|
||||
? SubscriptionProduct.SecretsManager
|
||||
: 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"
|
||||
[product]="this.product"
|
||||
></app-trial-confirmation-details>
|
||||
<div class="tw-mb-3 tw-flex">
|
||||
<a
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[routerLink]="
|
||||
product === ProductType.SecretsManager
|
||||
? ['/sm', orgId]
|
||||
: ['/organizations', orgId, 'vault']
|
||||
"
|
||||
>
|
||||
{{ "getStarted" | i18n | titlecase }}
|
||||
</a>
|
||||
<a
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[routerLink]="['/organizations', orgId, 'members']"
|
||||
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
|
||||
>
|
||||
{{ "inviteUsers" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</app-vertical-step>
|
||||
</app-vertical-stepper>
|
||||
</div>
|
@ -0,0 +1,318 @@
|
||||
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { PasswordInputResult, RegistrationFinishService } from "@bitwarden/auth/angular";
|
||||
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
|
||||
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 { ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
OrganizationCreatedEvent,
|
||||
SubscriptionProduct,
|
||||
TrialOrganizationType,
|
||||
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||
import { RouterService } from "../../../core/router.service";
|
||||
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
|
||||
import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-complete-trial-initiation",
|
||||
templateUrl: "complete-trial-initiation.component.html",
|
||||
})
|
||||
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||
|
||||
/** Password Manager or Secrets Manager */
|
||||
product: ProductType;
|
||||
/** The tier of product being subscribed to */
|
||||
productTier: ProductTierType;
|
||||
/** Product types that display steppers for Password Manager */
|
||||
stepperProductTypes: ProductTierType[] = [
|
||||
ProductTierType.Teams,
|
||||
ProductTierType.Enterprise,
|
||||
ProductTierType.Families,
|
||||
];
|
||||
|
||||
/** Display multi-step trial flow when true */
|
||||
useTrialStepper = false;
|
||||
|
||||
/** True, registering a password is in progress */
|
||||
submitting = false;
|
||||
|
||||
/** Valid product types, used to filter out invalid query parameters */
|
||||
validProducts = [ProductType.PasswordManager, ProductType.SecretsManager];
|
||||
|
||||
orgInfoSubLabel = "";
|
||||
orgId = "";
|
||||
orgLabel = "";
|
||||
billingSubLabel = "";
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
|
||||
/** User's email address associated with the trial */
|
||||
email = "";
|
||||
/** Token from the backend associated with the email verification */
|
||||
emailVerificationToken: string;
|
||||
|
||||
orgInfoFormGroup = this.formBuilder.group({
|
||||
name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }],
|
||||
billingEmail: [""],
|
||||
});
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
protected readonly SubscriptionProduct = SubscriptionProduct;
|
||||
protected readonly ProductType = ProductType;
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private formBuilder: FormBuilder,
|
||||
private logService: LogService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private i18nService: I18nService,
|
||||
private routerService: RouterService,
|
||||
private organizationBillingService: OrganizationBillingService,
|
||||
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
private toastService: ToastService,
|
||||
private registrationFinishService: RegistrationFinishService,
|
||||
private validationService: ValidationService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => {
|
||||
// Retrieve email from query params
|
||||
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
||||
this.email = qParams.email;
|
||||
this.orgInfoFormGroup.controls.billingEmail.setValue(qParams.email);
|
||||
}
|
||||
|
||||
// Show email validation toast when coming from email
|
||||
if (qParams.fromEmail && qParams.fromEmail === "true") {
|
||||
this.toastService.showToast({
|
||||
title: null,
|
||||
message: this.i18nService.t("emailVerifiedV2"),
|
||||
variant: "success",
|
||||
});
|
||||
}
|
||||
|
||||
if (qParams.token != null) {
|
||||
this.emailVerificationToken = qParams.token;
|
||||
}
|
||||
|
||||
const product = parseInt(qParams.product);
|
||||
|
||||
// Get product from query params, default to password manager
|
||||
this.product = this.validProducts.includes(product) ? product : ProductType.PasswordManager;
|
||||
|
||||
const productTierParam = parseInt(qParams.productTier) as ProductTierType;
|
||||
|
||||
/** Only show the trial stepper for a subset of types */
|
||||
const showPasswordManagerStepper = this.stepperProductTypes.includes(productTierParam);
|
||||
|
||||
/** All types of secret manager should see the trial stepper */
|
||||
const showSecretsManagerStepper = this.product === ProductType.SecretsManager;
|
||||
|
||||
if ((showPasswordManagerStepper || showSecretsManagerStepper) && !isNaN(productTierParam)) {
|
||||
this.productTier = productTierParam;
|
||||
|
||||
this.orgLabel = this.planTypeDisplay;
|
||||
|
||||
this.useTrialStepper = true;
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
const invite = await this.acceptOrganizationInviteService.getOrganizationInvite();
|
||||
let policies: Policy[] | null = null;
|
||||
|
||||
if (invite != null) {
|
||||
try {
|
||||
policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (policies !== null) {
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
}
|
||||
|
||||
this.orgInfoFormGroup.controls.name.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.orgInfoFormGroup.controls.name.markAsTouched();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
/** Handle manual stepper change */
|
||||
verticalStepChange(event: StepperSelectionEvent) {
|
||||
if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") {
|
||||
this.orgInfoSubLabel = this.planInfoLabel;
|
||||
} else if (event.previouslySelectedIndex === 1) {
|
||||
this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Update local details from organization created event */
|
||||
createdOrganization(event: OrganizationCreatedEvent) {
|
||||
this.orgId = event.organizationId;
|
||||
this.billingSubLabel = event.planDescription;
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
/** Move the user to the previous step */
|
||||
previousStep() {
|
||||
this.verticalStepper.previous();
|
||||
}
|
||||
|
||||
get isSecretsManagerFree() {
|
||||
return this.product === ProductType.SecretsManager && this.productTier === ProductTierType.Free;
|
||||
}
|
||||
|
||||
get planTypeDisplay() {
|
||||
switch (this.productTier) {
|
||||
case ProductTierType.Teams:
|
||||
return "Teams";
|
||||
case ProductTierType.Enterprise:
|
||||
return "Enterprise";
|
||||
case ProductTierType.Families:
|
||||
return "Families";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
get planInfoLabel() {
|
||||
switch (this.productTier) {
|
||||
case ProductTierType.Teams:
|
||||
return this.i18nService.t("enterTeamsOrgInfo");
|
||||
case ProductTierType.Enterprise:
|
||||
return this.i18nService.t("enterEnterpriseOrgInfo");
|
||||
case ProductTierType.Families:
|
||||
return this.i18nService.t("enterFamiliesOrgInfo");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
get trialOrganizationType(): TrialOrganizationType {
|
||||
if (this.productTier === ProductTierType.Free) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.productTier;
|
||||
}
|
||||
|
||||
/** Create an organization unless the trial is for secrets manager */
|
||||
async conditionallyCreateOrganization(): Promise<void> {
|
||||
if (!this.isSecretsManagerFree) {
|
||||
this.verticalStepper.next();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.organizationBillingService.startFree({
|
||||
organization: {
|
||||
name: this.orgInfoFormGroup.value.name,
|
||||
billingEmail: this.orgInfoFormGroup.value.billingEmail,
|
||||
},
|
||||
plan: {
|
||||
type: 0,
|
||||
subscribeToSecretsManager: true,
|
||||
isFromSecretsManagerTrial: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.orgId = response.id;
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the users registration with their password.
|
||||
*
|
||||
* When a the trial stepper isn't used, redirect the user to the login page.
|
||||
*/
|
||||
async handlePasswordSubmit(passwordInputResult: PasswordInputResult) {
|
||||
if (!this.useTrialStepper) {
|
||||
await this.finishRegistration(passwordInputResult);
|
||||
this.submitting = false;
|
||||
|
||||
await this.router.navigate(["/login"], { queryParams: { email: this.email } });
|
||||
return;
|
||||
}
|
||||
|
||||
const captchaToken = await this.finishRegistration(passwordInputResult);
|
||||
|
||||
if (captchaToken == null) {
|
||||
this.submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.logIn(passwordInputResult.password, captchaToken);
|
||||
|
||||
this.submitting = false;
|
||||
|
||||
this.verticalStepper.next();
|
||||
}
|
||||
|
||||
private setupFamilySponsorship(sponsorshipToken: string) {
|
||||
if (sponsorshipToken != null) {
|
||||
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||
queryParams: { plan: sponsorshipToken },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/** Logs the user in based using the token received by the `finishRegistration` method */
|
||||
private async logIn(masterPassword: string, captchaBypassToken: string): Promise<void> {
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
this.email,
|
||||
masterPassword,
|
||||
captchaBypassToken,
|
||||
null,
|
||||
);
|
||||
|
||||
await this.loginStrategyService.logIn(credentials);
|
||||
}
|
||||
|
||||
finishRegistration(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
return this.registrationFinishService
|
||||
.finishRegistration(this.email, passwordInputResult, this.emailVerificationToken)
|
||||
.catch((e) => {
|
||||
this.validationService.showError(e);
|
||||
this.submitting = false;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
|
||||
|
||||
import { ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
|
||||
|
||||
import { freeTrialTextResolver } from "./free-trial-text.resolver";
|
||||
|
||||
const route = {
|
||||
queryParams: {},
|
||||
} as ActivatedRouteSnapshot;
|
||||
|
||||
const routerStateSnapshot = {} as RouterStateSnapshot;
|
||||
|
||||
describe("freeTrialTextResolver", () => {
|
||||
[
|
||||
{
|
||||
param: ProductType.PasswordManager,
|
||||
keyBase: "startYour7DayFreeTrialOfBitwardenPasswordManager",
|
||||
},
|
||||
{
|
||||
param: ProductType.SecretsManager,
|
||||
keyBase: "startYour7DayFreeTrialOfBitwardenSecretsManager",
|
||||
},
|
||||
{
|
||||
param: `${ProductType.PasswordManager},${ProductType.SecretsManager}`,
|
||||
keyBase: "startYour7DayFreeTrialOfBitwarden",
|
||||
},
|
||||
].forEach(({ param, keyBase }) => {
|
||||
describe(`when product is ${param}`, () => {
|
||||
beforeEach(() => {
|
||||
route.queryParams.product = `${param}`;
|
||||
});
|
||||
|
||||
it("returns teams trial text", () => {
|
||||
route.queryParams.productTier = ProductTierType.Teams;
|
||||
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(`${keyBase}ForTeams`);
|
||||
});
|
||||
|
||||
it("returns enterprise trial text", () => {
|
||||
route.queryParams.productTier = ProductTierType.Enterprise;
|
||||
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(`${keyBase}ForEnterprise`);
|
||||
});
|
||||
|
||||
it("returns families trial text", () => {
|
||||
route.queryParams.productTier = ProductTierType.Families;
|
||||
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(`${keyBase}ForFamilies`);
|
||||
});
|
||||
|
||||
it("returns default trial text", () => {
|
||||
route.queryParams.productTier = "";
|
||||
|
||||
expect(freeTrialTextResolver(route, routerStateSnapshot)).toBe(keyBase);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
import { ActivatedRouteSnapshot, ResolveFn } from "@angular/router";
|
||||
|
||||
import { ProductType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
|
||||
export const freeTrialTextResolver: ResolveFn<string | null> = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
): string | null => {
|
||||
const { product, productTier } = route.queryParams;
|
||||
const products: ProductType[] = (product ?? "").split(",").map((p: string) => parseInt(p));
|
||||
|
||||
const onlyPasswordManager = products.length === 1 && products[0] === ProductType.PasswordManager;
|
||||
const onlySecretsManager = products.length === 1 && products[0] === ProductType.SecretsManager;
|
||||
const forTeams = parseInt(productTier) === ProductTierType.Teams;
|
||||
const forEnterprise = parseInt(productTier) === ProductTierType.Enterprise;
|
||||
const forFamilies = parseInt(productTier) === ProductTierType.Families;
|
||||
|
||||
switch (true) {
|
||||
case onlyPasswordManager && forTeams:
|
||||
return "startYour7DayFreeTrialOfBitwardenPasswordManagerForTeams";
|
||||
case onlyPasswordManager && forEnterprise:
|
||||
return "startYour7DayFreeTrialOfBitwardenPasswordManagerForEnterprise";
|
||||
case onlyPasswordManager && forFamilies:
|
||||
return "startYour7DayFreeTrialOfBitwardenPasswordManagerForFamilies";
|
||||
case onlyPasswordManager:
|
||||
return "startYour7DayFreeTrialOfBitwardenPasswordManager";
|
||||
case onlySecretsManager && forTeams:
|
||||
return "startYour7DayFreeTrialOfBitwardenSecretsManagerForTeams";
|
||||
case onlySecretsManager && forEnterprise:
|
||||
return "startYour7DayFreeTrialOfBitwardenSecretsManagerForEnterprise";
|
||||
case onlySecretsManager && forFamilies:
|
||||
return "startYour7DayFreeTrialOfBitwardenSecretsManagerForFamilies";
|
||||
case onlySecretsManager:
|
||||
return "startYour7DayFreeTrialOfBitwardenSecretsManager";
|
||||
case forTeams:
|
||||
return "startYour7DayFreeTrialOfBitwardenForTeams";
|
||||
case forEnterprise:
|
||||
return "startYour7DayFreeTrialOfBitwardenForEnterprise";
|
||||
case forFamilies:
|
||||
return "startYour7DayFreeTrialOfBitwardenForFamilies";
|
||||
default:
|
||||
return "startYour7DayFreeTrialOfBitwarden";
|
||||
}
|
||||
};
|
@ -1,5 +1,10 @@
|
||||
<div class="tw-pb-6 tw-pl-6">
|
||||
<p class="tw-text-xl">{{ "trialThankYou" | i18n: orgLabel }}</p>
|
||||
<p class="tw-text-xl" *ngIf="product === Product.PasswordManager">
|
||||
{{ "trialThankYou" | i18n: orgLabel }}
|
||||
</p>
|
||||
<p class="tw-text-xl" *ngIf="product === Product.SecretsManager">
|
||||
{{ "smFreeTrialThankYou" | i18n }}
|
||||
</p>
|
||||
<ul class="tw-list-disc">
|
||||
<li>
|
||||
<p>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { ProductType } from "@bitwarden/common/billing/enums";
|
||||
|
||||
@Component({
|
||||
selector: "app-trial-confirmation-details",
|
||||
templateUrl: "confirmation-details.component.html",
|
||||
@ -7,4 +9,7 @@ import { Component, Input } from "@angular/core";
|
||||
export class ConfirmationDetailsComponent {
|
||||
@Input() email: string;
|
||||
@Input() orgLabel: string;
|
||||
@Input() product?: ProductType = ProductType.PasswordManager;
|
||||
|
||||
protected readonly Product = ProductType;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { CdkStepperModule } from "@angular/cdk/stepper";
|
||||
import { TitleCasePipe } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { InputPasswordComponent } from "@bitwarden/auth/angular";
|
||||
import { FormFieldModule } from "@bitwarden/components";
|
||||
|
||||
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
|
||||
@ -14,6 +15,7 @@ import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiati
|
||||
import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { CompleteTrialInitiationComponent } from "./complete-trial-initiation/complete-trial-initiation.component";
|
||||
import { ConfirmationDetailsComponent } from "./confirmation-details.component";
|
||||
import { AbmEnterpriseContentComponent } from "./content/abm-enterprise-content.component";
|
||||
import { AbmTeamsContentComponent } from "./content/abm-teams-content.component";
|
||||
@ -51,9 +53,11 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
||||
PaymentComponent,
|
||||
TaxInfoComponent,
|
||||
TrialBillingStepComponent,
|
||||
InputPasswordComponent,
|
||||
],
|
||||
declarations: [
|
||||
TrialInitiationComponent,
|
||||
CompleteTrialInitiationComponent,
|
||||
EnterpriseContentComponent,
|
||||
TeamsContentComponent,
|
||||
ConfirmationDetailsComponent,
|
||||
@ -82,7 +86,7 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
||||
SecretsManagerTrialFreeStepperComponent,
|
||||
SecretsManagerTrialPaidStepperComponent,
|
||||
],
|
||||
exports: [TrialInitiationComponent],
|
||||
exports: [TrialInitiationComponent, CompleteTrialInitiationComponent],
|
||||
providers: [TitleCasePipe],
|
||||
})
|
||||
export class TrialInitiationModule {}
|
||||
|
@ -47,6 +47,8 @@ import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emerg
|
||||
import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component";
|
||||
import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module";
|
||||
import { SsoComponent } from "./auth/sso.component";
|
||||
import { CompleteTrialInitiationComponent } from "./auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component";
|
||||
import { freeTrialTextResolver } from "./auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver";
|
||||
import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component";
|
||||
import { TwoFactorAuthComponent } from "./auth/two-factor-auth.component";
|
||||
import { TwoFactorComponent } from "./auth/two-factor.component";
|
||||
@ -400,6 +402,28 @@ const routes: Routes = [
|
||||
titleId: "removeMasterPassword",
|
||||
} satisfies DataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "trial-initiation",
|
||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
|
||||
component: CompleteTrialInitiationComponent,
|
||||
resolve: {
|
||||
pageTitle: freeTrialTextResolver,
|
||||
},
|
||||
data: {
|
||||
maxWidth: "3xl",
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "secrets-manager-trial-initiation",
|
||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
|
||||
component: CompleteTrialInitiationComponent,
|
||||
resolve: {
|
||||
pageTitle: freeTrialTextResolver,
|
||||
},
|
||||
data: {
|
||||
maxWidth: "3xl",
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -8414,6 +8414,51 @@
|
||||
"manageBillingFromProviderPortalMessage": {
|
||||
"message": "Manage billing from the Provider Portal"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwarden": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenForTeams": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden for Teams"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenForFamilies": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden for Families"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenForEnterprise": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden for Enterprise"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenSecretsManager": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Secrets Manager"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenSecretsManagerForTeams": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Secrets Manager for Teams"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenSecretsManagerForFamilies": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Secrets Manager for Families"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenSecretsManagerForEnterprise": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Secrets Manager for Enterprise"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenPasswordManager": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Password Manager"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenPasswordManagerForTeams": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Password Manager for Teams"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenPasswordManagerForFamilies": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Password Manager for Families"
|
||||
},
|
||||
"startYour7DayFreeTrialOfBitwardenPasswordManagerForEnterprise": {
|
||||
"message": "Start your 7-Day free trial of Bitwarden Password Manager for Enterprise"
|
||||
},
|
||||
"enterTeamsOrgInfo": {
|
||||
"message": "Enter your Teams organization information"
|
||||
},
|
||||
"enterFamiliesOrgInfo": {
|
||||
"message": "Enter your Families organization information"
|
||||
},
|
||||
"enterEnterpriseOrgInfo": {
|
||||
"message": "Enter your Enterprise organization information"
|
||||
},
|
||||
"viewItemsIn": {
|
||||
"message": "View items in $NAME$",
|
||||
"description": "Button to view the contents of a folder or collection",
|
||||
|
@ -3,6 +3,7 @@
|
||||
[subtitle]="pageSubtitle"
|
||||
[icon]="pageIcon"
|
||||
[showReadonlyHostname]="showReadonlyHostname"
|
||||
[maxWidth]="maxWidth"
|
||||
>
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet slot="secondary" name="secondary"></router-outlet>
|
||||
|
@ -13,6 +13,7 @@ export interface AnonLayoutWrapperData {
|
||||
pageSubtitle?: string;
|
||||
pageIcon?: Icon;
|
||||
showReadonlyHostname?: boolean;
|
||||
maxWidth?: "md" | "3xl";
|
||||
}
|
||||
|
||||
@Component({
|
||||
@ -27,6 +28,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
|
||||
protected pageSubtitle: string;
|
||||
protected pageIcon: Icon;
|
||||
protected showReadonlyHostname: boolean;
|
||||
protected maxWidth: "md" | "3xl";
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
@ -75,6 +77,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]);
|
||||
this.maxWidth = firstChildRouteData["maxWidth"];
|
||||
}
|
||||
|
||||
private listenForServiceDataChanges() {
|
||||
|
@ -15,6 +15,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="tw-mb-auto tw-w-full tw-max-w-md tw-mx-auto tw-flex tw-flex-col tw-items-center sm:tw-min-w-[28rem]"
|
||||
[ngClass]="{ 'tw-max-w-md': maxWidth === 'md', 'tw-max-w-3xl': maxWidth === '3xl' }"
|
||||
>
|
||||
<div
|
||||
class="tw-rounded-xl tw-mb-9 tw-mx-auto tw-w-full sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
|
||||
|
@ -23,6 +23,12 @@ export class AnonLayoutComponent {
|
||||
@Input() subtitle: string;
|
||||
@Input() icon: Icon;
|
||||
@Input() showReadonlyHostname: boolean;
|
||||
/**
|
||||
* Max width of the layout content
|
||||
*
|
||||
* @default 'md'
|
||||
*/
|
||||
@Input() maxWidth: "md" | "3xl" = "md";
|
||||
|
||||
protected logo: Icon;
|
||||
|
||||
@ -45,6 +51,7 @@ export class AnonLayoutComponent {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.maxWidth = this.maxWidth ?? "md";
|
||||
this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
|
||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
||||
|
@ -190,6 +190,7 @@ export class InputPasswordComponent {
|
||||
localMasterKeyHash,
|
||||
kdfConfig,
|
||||
hint: this.formGroup.controls.hint.value,
|
||||
password,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -7,4 +7,5 @@ export interface PasswordInputResult {
|
||||
localMasterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
hint: string;
|
||||
password: string;
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
password: "password",
|
||||
};
|
||||
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
|
@ -110,6 +110,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
hint: "hint",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
password: "password",
|
||||
};
|
||||
|
||||
credentials = {
|
||||
|
@ -4,3 +4,4 @@ export * from "./plan-type.enum";
|
||||
export * from "./transaction-type.enum";
|
||||
export * from "./bitwarden-product-type.enum";
|
||||
export * from "./product-tier-type.enum";
|
||||
export * from "./product-type.enum";
|
||||
|
Loading…
Reference in New Issue
Block a user