1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-18 15:47:57 +01:00

[PM-15545][Defect] Update trial initiation UI for new flow via trial/send-verification-email endpoint (#12256)

* Add the on trial payment option on new UI

* Rename variables correctly

* Resolve the isTrialPaymentOptional and use observable

* use firstValueFrom and remove subscribe

* Resolve the selected plantype

* Changes for free Org
This commit is contained in:
cyprain-okeke 2024-12-12 17:17:24 +01:00 committed by GitHub
parent 30c151f44a
commit bfa9cf3623
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 89 additions and 6 deletions

View File

@ -23,12 +23,17 @@
bitButton
buttonType="primary"
[disabled]="orgInfoFormGroup.controls.name.invalid"
(click)="conditionallyCreateOrganization()"
[loading]="loading && (trialPaymentOptional$ | async)"
(click)="orgNameEntrySubmit()"
>
{{ "next" | i18n }}
{{ (trialPaymentOptional$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }}
</button>
</app-vertical-step>
<app-vertical-step label="Billing" [subLabel]="billingSubLabel" *ngIf="!isSecretsManagerFree">
<app-vertical-step
label="Billing"
[subLabel]="billingSubLabel"
*ngIf="!(trialPaymentOptional$ | async) && !isSecretsManagerFree"
>
<app-trial-billing-step
*ngIf="stepper.selectedIndex === 2"
[organizationInfo]="{

View File

@ -4,7 +4,7 @@ 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 { firstValueFrom, Subject, takeUntil } from "rxjs";
import { PasswordInputResult, RegistrationFinishService } from "@bitwarden/auth/angular";
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
@ -12,8 +12,14 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
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 {
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";
@ -28,6 +34,10 @@ import { RouterService } from "../../../core/router.service";
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.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",
@ -65,6 +75,8 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
email = "";
/** Token from the backend associated with the email verification */
emailVerificationToken: string;
loading = false;
productTierValue: number;
orgInfoFormGroup = this.formBuilder.group({
name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }],
@ -74,6 +86,9 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected readonly SubscriptionProduct = SubscriptionProduct;
protected readonly ProductType = ProductType;
protected trialPaymentOptional$ = this.configService.getFeatureFlag$(
FeatureFlag.TrialPaymentOptional,
);
constructor(
protected router: Router,
@ -90,6 +105,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
private registrationFinishService: RegistrationFinishService,
private validationService: ValidationService,
private loginStrategyService: LoginStrategyServiceAbstraction,
private configService: ConfigService,
) {}
async ngOnInit(): Promise<void> {
@ -119,6 +135,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
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);
@ -185,6 +202,16 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
}
}
async orgNameEntrySubmit(): Promise<void> {
const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$);
if (isTrialPaymentOptional) {
await this.createOrganizationOnTrial();
} else {
await this.conditionallyCreateOrganization();
}
}
/** Update local details from organization created event */
createdOrganization(event: OrganizationCreatedEvent) {
this.orgId = event.organizationId;
@ -192,11 +219,62 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
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;
}