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 bitButton
buttonType="primary" buttonType="primary"
[disabled]="orgInfoFormGroup.controls.name.invalid" [disabled]="orgInfoFormGroup.controls.name.invalid"
(click)="conditionallyCreateOrganization()" [loading]="loading && (trialPaymentOptional$ | async)"
(click)="orgNameEntrySubmit()"
> >
{{ "next" | i18n }} {{ (trialPaymentOptional$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }}
</button> </button>
</app-vertical-step> </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 <app-trial-billing-step
*ngIf="stepper.selectedIndex === 2" *ngIf="stepper.selectedIndex === 2"
[organizationInfo]="{ [organizationInfo]="{

View File

@ -4,7 +4,7 @@ import { StepperSelectionEvent } from "@angular/cdk/stepper";
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; 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 { PasswordInputResult, RegistrationFinishService } from "@bitwarden/auth/angular";
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common"; 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 { 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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; import {
import { ProductTierType, ProductType } from "@bitwarden/common/billing/enums"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.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 { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component"; import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component";
export type InitiationPath =
| "Password Manager trial from marketing website"
| "Secrets Manager trial from marketing website";
@Component({ @Component({
selector: "app-complete-trial-initiation", selector: "app-complete-trial-initiation",
templateUrl: "complete-trial-initiation.component.html", templateUrl: "complete-trial-initiation.component.html",
@ -65,6 +75,8 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
email = ""; email = "";
/** Token from the backend associated with the email verification */ /** Token from the backend associated with the email verification */
emailVerificationToken: string; emailVerificationToken: string;
loading = false;
productTierValue: number;
orgInfoFormGroup = this.formBuilder.group({ orgInfoFormGroup = this.formBuilder.group({
name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }], name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }],
@ -74,6 +86,9 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
protected readonly SubscriptionProduct = SubscriptionProduct; protected readonly SubscriptionProduct = SubscriptionProduct;
protected readonly ProductType = ProductType; protected readonly ProductType = ProductType;
protected trialPaymentOptional$ = this.configService.getFeatureFlag$(
FeatureFlag.TrialPaymentOptional,
);
constructor( constructor(
protected router: Router, protected router: Router,
@ -90,6 +105,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
private registrationFinishService: RegistrationFinishService, private registrationFinishService: RegistrationFinishService,
private validationService: ValidationService, private validationService: ValidationService,
private loginStrategyService: LoginStrategyServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction,
private configService: ConfigService,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
@ -119,6 +135,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
this.product = this.validProducts.includes(product) ? product : ProductType.PasswordManager; this.product = this.validProducts.includes(product) ? product : ProductType.PasswordManager;
const productTierParam = parseInt(qParams.productTier) as ProductTierType; const productTierParam = parseInt(qParams.productTier) as ProductTierType;
this.productTierValue = productTierParam;
/** Only show the trial stepper for a subset of types */ /** Only show the trial stepper for a subset of types */
const showPasswordManagerStepper = this.stepperProductTypes.includes(productTierParam); 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 */ /** Update local details from organization created event */
createdOrganization(event: OrganizationCreatedEvent) { createdOrganization(event: OrganizationCreatedEvent) {
this.orgId = event.organizationId; this.orgId = event.organizationId;
@ -192,11 +219,62 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
this.verticalStepper.next(); 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 */ /** Move the user to the previous step */
previousStep() { previousStep() {
this.verticalStepper.previous(); 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() { get isSecretsManagerFree() {
return this.product === ProductType.SecretsManager && this.productTier === ProductTierType.Free; return this.product === ProductType.SecretsManager && this.productTier === ProductTierType.Free;
} }