From 8f437dc77316dca31f16b2458916b2595ecc179d Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 30 Jul 2024 07:11:40 -0500 Subject: [PATCH] [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> --- .../web-registration-finish.service.spec.ts | 1 + .../complete-trial-initiation.component.html | 80 +++++ .../complete-trial-initiation.component.ts | 318 ++++++++++++++++++ .../resolver/free-trial-text.resolver.spec.ts | 58 ++++ .../resolver/free-trial-text.resolver.ts | 43 +++ .../confirmation-details.component.html | 7 +- .../confirmation-details.component.ts | 5 + .../trial-initiation.module.ts | 6 +- apps/web/src/app/oss-routing.module.ts | 24 ++ apps/web/src/locales/en/messages.json | 45 +++ .../anon-layout-wrapper.component.html | 1 + .../anon-layout-wrapper.component.ts | 3 + .../anon-layout/anon-layout.component.html | 1 + .../anon-layout/anon-layout.component.ts | 7 + .../input-password.component.ts | 1 + .../input-password/password-input-result.ts | 1 + ...efault-registration-finish.service.spec.ts | 1 + .../default-set-password-jit.service.spec.ts | 1 + libs/common/src/billing/enums/index.ts | 1 + 19 files changed, 602 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html create mode 100644 apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts create mode 100644 apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts create mode 100644 apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts index 007165a1bc..2faf3f85d1 100644 --- a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -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; diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html new file mode 100644 index 0000000000..9400e512c3 --- /dev/null +++ b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html @@ -0,0 +1,80 @@ +
+ +
+
+ + + + + + + + + + + + + + + + + +
diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts new file mode 100644 index 0000000000..a1d688c25e --- /dev/null +++ b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts @@ -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(); + 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 { + 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 { + 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 { + 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; + }); + } +} diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts new file mode 100644 index 0000000000..f9831c6dc4 --- /dev/null +++ b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.spec.ts @@ -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); + }); + }); + }); +}); diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts new file mode 100644 index 0000000000..7dec807fd4 --- /dev/null +++ b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver.ts @@ -0,0 +1,43 @@ +import { ActivatedRouteSnapshot, ResolveFn } from "@angular/router"; + +import { ProductType, ProductTierType } from "@bitwarden/common/billing/enums"; + +export const freeTrialTextResolver: ResolveFn = ( + 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"; + } +}; diff --git a/apps/web/src/app/auth/trial-initiation/confirmation-details.component.html b/apps/web/src/app/auth/trial-initiation/confirmation-details.component.html index 69bd57cd3a..e706239f9f 100644 --- a/apps/web/src/app/auth/trial-initiation/confirmation-details.component.html +++ b/apps/web/src/app/auth/trial-initiation/confirmation-details.component.html @@ -1,5 +1,10 @@
-

{{ "trialThankYou" | i18n: orgLabel }}

+

+ {{ "trialThankYou" | i18n: orgLabel }} +

+

+ {{ "smFreeTrialThankYou" | i18n }} +

  • diff --git a/apps/web/src/app/auth/trial-initiation/confirmation-details.component.ts b/apps/web/src/app/auth/trial-initiation/confirmation-details.component.ts index 95976fe727..69d08e627a 100644 --- a/apps/web/src/app/auth/trial-initiation/confirmation-details.component.ts +++ b/apps/web/src/app/auth/trial-initiation/confirmation-details.component.ts @@ -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; } diff --git a/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts b/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts index 57d982fd00..9a7ed7e429 100644 --- a/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts +++ b/apps/web/src/app/auth/trial-initiation/trial-initiation.module.ts @@ -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 {} diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 65414317cb..32dcb695a8 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -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, + }, ], }, { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 2aed43787e..2be00bb889 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -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", diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html index 6d5fc9b8da..cfd436d93a 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html @@ -3,6 +3,7 @@ [subtitle]="pageSubtitle" [icon]="pageIcon" [showReadonlyHostname]="showReadonlyHostname" + [maxWidth]="maxWidth" > diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts index 0bedf221e0..7559e35cdc 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts @@ -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() { diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 8fed6a8226..d6e80c2770 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -15,6 +15,7 @@

{ localMasterKeyHash: "localMasterKeyHash", kdfConfig: DEFAULT_KDF_CONFIG, hint: "hint", + password: "password", }; userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts index e4837641ef..26b6b0e529 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts @@ -110,6 +110,7 @@ describe("DefaultSetPasswordJitService", () => { localMasterKeyHash: "localMasterKeyHash", hint: "hint", kdfConfig: DEFAULT_KDF_CONFIG, + password: "password", }; credentials = { diff --git a/libs/common/src/billing/enums/index.ts b/libs/common/src/billing/enums/index.ts index e11cd9d829..3d89c7a546 100644 --- a/libs/common/src/billing/enums/index.ts +++ b/libs/common/src/billing/enums/index.ts @@ -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";