1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-02 16:43:19 +00:00
Files
browser/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.ts
Patrick-Pimentel-Bitwarden 1f60bcdcc0 feat(change-password): [PM-18720] (#5319) Change Password Implementation for Non Dialog Cases (#15319)
* feat(change-password-component): Change Password Update [18720] - Very close to complete.

* fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Removed temp code to force the state I need to verify correctness.

* fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Recover account working with change password component.

* fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Made code more dry.

* fix(change-password-component): Change Password Update [18720] - Updates to routing and the extension. Extension is still a wip.

* fix(change-password-component): Change Password Update [18720] - Extension routing changes.

* feat(change-password-component): Change Password Update [18720] - More extension work

* feat(change-password-component): Change Password Update [18720] - Pausing work for now while we wait for product to hear back.

* feat(change-password-component): Change Password Update [18720] - Removed duplicated anon layouts.

* feat(change-password-component): Change Password Update [18720] - Tidied up code.

* feat(change-password-component): Change Password Update [18720] - Small fixes to the styling

* feat(change-password-component): Change Password Update [18720] - Adding more content for the routing.

* feat(change-password-component): Change Password Update [18720] - Removed circular loop for now.

* feat(change-password-component): Change Password Update [18720] - Made comments regarding the change password routing complexities with change-password and auth guard.

* feat(change-password-component): Change Password Update [18720] - Undid some changes because they will be conflicts later on.

* feat(change-password-component): Change Password Update [18720] - Small directive change.

* feat(change-password-component): Change Password Update [18720] - Small changes and added some clarification on where I'm blocked

* feat(change-password-component): Change Password Update [18720] - Org invite is seemingly working, found one bug to iron out.

* refactor(change-password-component): Change Password Update [18720] - Fixed up policy service to be made more clear.

* docs(change-password-component): Change Password Update [18720] - Updated documentation.

* refactor(change-password-component): Change Password Update [18720] - Routing changes and policy service changes.

* fix(change-password-component): Change Password Update [18720] - Wrapping up changes.

* feat(change-password-component): Change Password Update [18720] - Should be working fully

* feat(change-password-component): Change Password Update [18720] - Found a bug, working on password policy being present on login.

* feat(change-password-component): Change Password Update [18720] - Turned on auth guard on other clients for change-password route.

* feat(change-password-component): Change Password Update [18720] - Committing intermediate changes.

* feat(change-password-component): Change Password Update [18720] - The master password policy endpoint has been added! Should be working. Testing now.

* feat(change-password-component): Change Password Update [18720] - Minor fixes.

* feat(change-password-component): Change Password Update [18720] - Undid naming change.

* feat(change-password-component): Change Password Update [18720] - Removed comment.

* feat(change-password-component): Change Password Update [18720] - Removed unneeded code.

* fix(change-password-component): Change Password Update [18720] - Took org invite state out of service and made it accessible.

* fix(change-password-component): Change Password Update [18720] - Small changes.

* fix(change-password-component): Change Password Update [18720] - Split up org invite service into client specific implementations and have them injected into clients properly

* feat(change-password-component): Change Password Update [18720] - Stopping work and going to switch to a new branch to pare down some of the solutions that were made to get this over the finish line

* feat(change-password-component): Change Password Update [18720] - Started to remove functionality in the login.component and the password login strategy.

* feat(change-password-component): Change Password Update [18720] - Removed more unneded changes.

* feat(change-password-component): Change Password Update [18720] - Change password clearing state working properly.

* fix(change-password-component): Change Password Update [18720] - Added docs and moved web implementation.

* comments(change-password-component): Change Password Update [18720] - Added more notes.

* test(change-password-component): Change Password Update [18720] - Added in tests for policy service.

* comment(change-password-component): Change Password Update [18720] - Updated doc with correct ticket number.

* comment(change-password-component): Change Password Update [18720] - Fixed doc.

* test(change-password-component): Change Password Update [18720] - Fixed tests.

* test(change-password-component): Change Password Update [18720] - Fixed linting errors. Have more tests to fix.

* test(change-password-component): Change Password Update [18720] - Added back in ignore for typesafety.

* fix(change-password-component): Change Password Update [18720] - Fixed other type issues.

* test(change-password-component): Change Password Update [18720] - Fixed tests.

* test(change-password-component): Change Password Update [18720] - Fixed more tests.

* test(change-password-component): Change Password Update [18720] - Fixed tiny duplicate code.

* fix(change-password-component): Change Password Update [18720] - Fixed desktop component.

* fix(change-password-component): Change Password Update [18720] - Removed unused code

* fix(change-password-component): Change Password Update [18720] - Fixed locales.

* fix(change-password-component): Change Password Update [18720] - Removed tracing.

* fix(change-password-component): Change Password Update [18720] - Removed duplicative services module entry.

* fix(change-password-component): Change Password Update [18720] - Added comment.

* fix(change-password-component): Change Password Update [18720] - Fixed unneeded call in two factor to get user id.

* fix(change-password-component): Change Password Update [18720] - Fixed a couple of tiny things.

* fix(change-password-component): Change Password Update [18720] - Added comment for later fix.

* fix(change-password-component): Change Password Update [18720] - Fixed linting error.

* PM-18720 - AuthGuard - move call to get isChangePasswordFlagOn down after other conditions for efficiency.

* PM-18720 - PasswordLoginStrategy tests - test new feature flagged combine org invite policies logic for weak password evaluation.

* PM-18720 - CLI - fix dep issue

* PM-18720 - ChangePasswordComp - extract change password warning up out of input password component

* PM-18720 - InputPassword - remove unused dependency.

* PM-18720 - ChangePasswordComp - add callout dep

* PM-18720 - Revert all anon-layout changes

* PM-18720 - Anon Layout - finish reverting changes.

* PM-18720 - WIP move of change password out of libs/auth

* PM-18720 - Clean up remaining imports from moving change password out of libs/auth

* PM-18720 - Add change-password barrel file for better import grouping

* PM-18720 - Change Password comp - restore maxWidth

* PM-18720 - After merge, fix errors

* PM-18720 - Desktop - fix api service import

* PM-18720 - NDV - fix routing.

* PM-18720 - Change Password Comp - add logout service todo

* PM-18720 - PasswordSettings - per feedback, component is already feature flagged behind PM16117_ChangeExistingPasswordRefactor so we can just delete the replaced callout (new text is in change-password comp)

* PM-18720 - Routing Modules - properly flag new component behind feature flag.

* PM-18720 - SSO Login Strategy - fix config service import since it is now in shared deps from main merge.

* PM-18720 - Fix SSO login strategy tests

* PM-18720 - Default Policy Service - address AC PR feedback

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
2025-07-10 09:08:25 -04:00

425 lines
14 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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 { combineLatest, firstValueFrom, map, Subject, switchMap, takeUntil } from "rxjs";
import {
InputPasswordFlow,
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
import {
OrganizationBillingServiceAbstraction as OrganizationBillingService,
OrganizationInformation,
PlanInformation,
} from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PlanType, ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
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 { 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 { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component";
export type InitiationPath =
| "Password Manager trial from marketing website"
| "Secrets Manager trial from marketing website";
@Component({
selector: "app-complete-trial-initiation",
templateUrl: "complete-trial-initiation.component.html",
standalone: false,
})
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAccountRegistration;
initializing = true;
/** 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;
loading = false;
productTierValue: number;
trialLength: number;
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;
protected trialPaymentOptional$ = this.configService.getFeatureFlag$(
FeatureFlag.TrialPaymentOptional,
);
protected allowTrialLengthZero$ = this.configService.getFeatureFlag$(
FeatureFlag.AllowTrialLengthZero,
);
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 organizationInviteService: OrganizationInviteService,
private toastService: ToastService,
private registrationFinishService: RegistrationFinishService,
private validationService: ValidationService,
private loginStrategyService: LoginStrategyServiceAbstraction,
private configService: ConfigService,
private accountService: AccountService,
) {}
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;
this.productTierValue = productTierParam;
/** 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;
}
this.trialLength = qParams.trialLength ? parseInt(qParams.trialLength) : 7;
// 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.organizationInviteService.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.accountService.activeAccount$
.pipe(
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
takeUntil(this.destroy$),
)
.subscribe((enforcedPasswordPolicyOptions) => {
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
});
}
this.orgInfoFormGroup.controls.name.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.orgInfoFormGroup.controls.name.markAsTouched();
});
this.initializing = false;
}
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;
}
}
async orgNameEntrySubmit(): Promise<void> {
const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$);
/** Only skip payment if the flag is on AND trialLength > 0 */
if (isTrialPaymentOptional && this.trialLength > 0) {
await this.createOrganizationOnTrial();
} else {
await this.conditionallyCreateOrganization();
}
}
/** Update local details from organization created event */
createdOrganization(event: OrganizationCreatedEvent) {
this.orgId = event.organizationId;
this.billingSubLabel = event.planDescription;
this.verticalStepper.next();
}
/** create an organization on trial without payment method */
async createOrganizationOnTrial() {
this.loading = true;
let trialInitiationPath: InitiationPath = "Password Manager trial from marketing website";
let plan: PlanInformation = {
type: this.getPlanType(),
passwordManagerSeats: 1,
};
if (this.product === ProductType.SecretsManager) {
trialInitiationPath = "Secrets Manager trial from marketing website";
plan = {
...plan,
subscribeToSecretsManager: true,
isFromSecretsManagerTrial: true,
secretsManagerSeats: 1,
};
}
const organization: OrganizationInformation = {
name: this.orgInfoFormGroup.value.name,
billingEmail: this.orgInfoFormGroup.value.billingEmail,
initiationPath: trialInitiationPath,
};
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({
organization,
plan,
});
this.orgId = response?.id;
this.billingSubLabel = response.name.toString();
this.loading = false;
this.verticalStepper.next();
}
/** Move the user to the previous step */
previousStep() {
this.verticalStepper.previous();
}
getPlanType() {
switch (this.productTier) {
case ProductTierType.Teams:
return PlanType.TeamsAnnually;
case ProductTierType.Enterprise:
return PlanType.EnterpriseAnnually;
case ProductTierType.Families:
return PlanType.FamiliesAnnually;
case ProductTierType.Free:
return PlanType.Free;
default:
return PlanType.EnterpriseAnnually;
}
}
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;
}
readonly showBillingStep$ = combineLatest([
this.trialPaymentOptional$,
this.allowTrialLengthZero$,
]).pipe(
map(([trialPaymentOptional, allowTrialLengthZero]) => {
return (
(!trialPaymentOptional && !this.isSecretsManagerFree) ||
(trialPaymentOptional && allowTrialLengthZero && this.trialLength === 0)
);
}),
);
/** 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;
}
await this.finishRegistration(passwordInputResult);
await this.logIn(passwordInputResult.newPassword);
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 */
private async logIn(masterPassword: string): Promise<void> {
const credentials = new PasswordLoginCredentials(this.email, masterPassword);
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;
});
}
}