mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 06:43:35 +00:00
[AC-1863] Send initiationPath on organization or user signup (#7747)
* Sent initiation path for organization and user signups * Rename organizationQueryParameter > organizationTypeQueryParameter * Jared's feedback * Split PM & SM initiation path
This commit is contained in:
@@ -70,7 +70,6 @@ export class RegisterFormComponent extends BaseRegisterComponent {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
this.referenceData = this.referenceDataValue;
|
this.referenceData = this.referenceDataValue;
|
||||||
|
|
||||||
if (this.queryParamEmail) {
|
if (this.queryParamEmail) {
|
||||||
this.formGroup.get("email")?.setValue(this.queryParamEmail);
|
this.formGroup.get("email")?.setValue(this.queryParamEmail);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
|
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
|
||||||
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
|
||||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
|
||||||
import { ProductType } from "@bitwarden/common/enums";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
|
|
||||||
import { BillingSharedModule, PaymentComponent, TaxInfoComponent } from "../../../billing/shared";
|
|
||||||
|
|
||||||
export interface OrganizationInfo {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrganizationCreatedEvent {
|
|
||||||
organizationId: string;
|
|
||||||
planDescription: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SubscriptionCadence {
|
|
||||||
Monthly,
|
|
||||||
Annual,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SubscriptionType {
|
|
||||||
Teams,
|
|
||||||
Enterprise,
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-secrets-manager-trial-billing-step",
|
|
||||||
templateUrl: "secrets-manager-trial-billing-step.component.html",
|
|
||||||
imports: [BillingSharedModule],
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
export class SecretsManagerTrialBillingStepComponent implements OnInit {
|
|
||||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
|
||||||
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
|
|
||||||
@Input() organizationInfo: OrganizationInfo;
|
|
||||||
@Input() subscriptionType: SubscriptionType;
|
|
||||||
@Output() steppedBack = new EventEmitter();
|
|
||||||
@Output() organizationCreated = new EventEmitter<OrganizationCreatedEvent>();
|
|
||||||
|
|
||||||
loading = true;
|
|
||||||
|
|
||||||
annualCadence = SubscriptionCadence.Annual;
|
|
||||||
monthlyCadence = SubscriptionCadence.Monthly;
|
|
||||||
|
|
||||||
formGroup = this.formBuilder.group({
|
|
||||||
cadence: [SubscriptionCadence.Annual, Validators.required],
|
|
||||||
});
|
|
||||||
formPromise: Promise<string>;
|
|
||||||
|
|
||||||
applicablePlans: PlanResponse[];
|
|
||||||
annualPlan: PlanResponse;
|
|
||||||
monthlyPlan: PlanResponse;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private apiService: ApiService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private formBuilder: FormBuilder,
|
|
||||||
private messagingService: MessagingService,
|
|
||||||
private organizationBillingService: OrganizationBillingService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
|
||||||
const plans = await this.apiService.getPlans();
|
|
||||||
this.applicablePlans = plans.data.filter(this.isApplicable);
|
|
||||||
this.annualPlan = this.findPlanFor(SubscriptionCadence.Annual);
|
|
||||||
this.monthlyPlan = this.findPlanFor(SubscriptionCadence.Monthly);
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit(): Promise<void> {
|
|
||||||
this.formPromise = this.createOrganization();
|
|
||||||
|
|
||||||
const organizationId = await this.formPromise;
|
|
||||||
const planDescription = this.getPlanDescription();
|
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
this.i18nService.t("organizationCreated"),
|
|
||||||
this.i18nService.t("organizationReadyToGo"),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.organizationCreated.emit({
|
|
||||||
organizationId,
|
|
||||||
planDescription,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.messagingService.send("organizationCreated", organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected changedCountry() {
|
|
||||||
this.paymentComponent.hideBank = this.taxInfoComponent.taxInfo.country !== "US";
|
|
||||||
if (
|
|
||||||
this.paymentComponent.hideBank &&
|
|
||||||
this.paymentComponent.method === PaymentMethodType.BankAccount
|
|
||||||
) {
|
|
||||||
this.paymentComponent.method = PaymentMethodType.Card;
|
|
||||||
this.paymentComponent.changeMethod();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected stepBack() {
|
|
||||||
this.steppedBack.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createOrganization(): Promise<string> {
|
|
||||||
const plan = this.findPlanFor(this.formGroup.value.cadence);
|
|
||||||
const paymentMethod = await this.paymentComponent.createPaymentToken();
|
|
||||||
|
|
||||||
const response = await this.organizationBillingService.purchaseSubscription({
|
|
||||||
organization: {
|
|
||||||
name: this.organizationInfo.name,
|
|
||||||
billingEmail: this.organizationInfo.email,
|
|
||||||
},
|
|
||||||
plan: {
|
|
||||||
type: plan.type,
|
|
||||||
passwordManagerSeats: 1,
|
|
||||||
subscribeToSecretsManager: true,
|
|
||||||
isFromSecretsManagerTrial: true,
|
|
||||||
secretsManagerSeats: 1,
|
|
||||||
},
|
|
||||||
payment: {
|
|
||||||
paymentMethod,
|
|
||||||
billing: {
|
|
||||||
postalCode: this.taxInfoComponent.taxInfo.postalCode,
|
|
||||||
country: this.taxInfoComponent.taxInfo.country,
|
|
||||||
taxId: this.taxInfoComponent.taxInfo.taxId,
|
|
||||||
addressLine1: this.taxInfoComponent.taxInfo.line1,
|
|
||||||
addressLine2: this.taxInfoComponent.taxInfo.line2,
|
|
||||||
city: this.taxInfoComponent.taxInfo.city,
|
|
||||||
state: this.taxInfoComponent.taxInfo.state,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private findPlanFor(cadence: SubscriptionCadence) {
|
|
||||||
switch (this.subscriptionType) {
|
|
||||||
case SubscriptionType.Teams:
|
|
||||||
return cadence === SubscriptionCadence.Annual
|
|
||||||
? this.applicablePlans.find((plan) => plan.type === PlanType.TeamsAnnually)
|
|
||||||
: this.applicablePlans.find((plan) => plan.type === PlanType.TeamsMonthly);
|
|
||||||
case SubscriptionType.Enterprise:
|
|
||||||
return cadence === SubscriptionCadence.Annual
|
|
||||||
? this.applicablePlans.find((plan) => plan.type === PlanType.EnterpriseAnnually)
|
|
||||||
: this.applicablePlans.find((plan) => plan.type === PlanType.EnterpriseMonthly);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPlanDescription(): string {
|
|
||||||
const plan = this.findPlanFor(this.formGroup.value.cadence);
|
|
||||||
const price =
|
|
||||||
plan.SecretsManager.basePrice === 0
|
|
||||||
? plan.SecretsManager.seatPrice
|
|
||||||
: plan.SecretsManager.basePrice;
|
|
||||||
|
|
||||||
switch (this.formGroup.value.cadence) {
|
|
||||||
case SubscriptionCadence.Annual:
|
|
||||||
return `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`;
|
|
||||||
case SubscriptionCadence.Monthly:
|
|
||||||
return `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isApplicable(plan: PlanResponse): boolean {
|
|
||||||
const hasSecretsManager = !!plan.SecretsManager;
|
|
||||||
const isTeamsOrEnterprise =
|
|
||||||
plan.product === ProductType.Teams || plan.product === ProductType.Enterprise;
|
|
||||||
const notDisabledOrLegacy = !plan.disabled && !plan.legacyYear;
|
|
||||||
return hasSecretsManager && isTeamsOrEnterprise && notDisabledOrLegacy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,11 @@
|
|||||||
[subLabel]="subLabels.createAccount"
|
[subLabel]="subLabels.createAccount"
|
||||||
[addSubLabelSpacing]="true"
|
[addSubLabelSpacing]="true"
|
||||||
>
|
>
|
||||||
<app-register-form [isInTrialFlow]="true" (createdAccount)="accountCreated($event)">
|
<app-register-form
|
||||||
|
[referenceDataValue]="referenceEventRequest"
|
||||||
|
[isInTrialFlow]="true"
|
||||||
|
(createdAccount)="accountCreated($event)"
|
||||||
|
>
|
||||||
</app-register-form>
|
</app-register-form>
|
||||||
</app-vertical-step>
|
</app-vertical-step>
|
||||||
<app-vertical-step
|
<app-vertical-step
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Component, ViewChild } from "@angular/core";
|
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||||
|
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
|
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
|
||||||
@@ -12,7 +13,7 @@ import { VerticalStepperComponent } from "../../trial-initiation/vertical-steppe
|
|||||||
selector: "app-secrets-manager-trial-free-stepper",
|
selector: "app-secrets-manager-trial-free-stepper",
|
||||||
templateUrl: "secrets-manager-trial-free-stepper.component.html",
|
templateUrl: "secrets-manager-trial-free-stepper.component.html",
|
||||||
})
|
})
|
||||||
export class SecretsManagerTrialFreeStepperComponent {
|
export class SecretsManagerTrialFreeStepperComponent implements OnInit {
|
||||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||||
|
|
||||||
formGroup = this.formBuilder.group({
|
formGroup = this.formBuilder.group({
|
||||||
@@ -39,6 +40,8 @@ export class SecretsManagerTrialFreeStepperComponent {
|
|||||||
|
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
|
||||||
|
referenceEventRequest: ReferenceEventRequest;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected formBuilder: UntypedFormBuilder,
|
protected formBuilder: UntypedFormBuilder,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
@@ -46,6 +49,11 @@ export class SecretsManagerTrialFreeStepperComponent {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.referenceEventRequest = new ReferenceEventRequest();
|
||||||
|
this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website";
|
||||||
|
}
|
||||||
|
|
||||||
accountCreated(email: string): void {
|
accountCreated(email: string): void {
|
||||||
this.formGroup.get("email")?.setValue(email);
|
this.formGroup.get("email")?.setValue(email);
|
||||||
this.subLabels.createAccount = email;
|
this.subLabels.createAccount = email;
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
[subLabel]="createAccountLabel"
|
[subLabel]="createAccountLabel"
|
||||||
[addSubLabelSpacing]="true"
|
[addSubLabelSpacing]="true"
|
||||||
>
|
>
|
||||||
<app-register-form [isInTrialFlow]="true" (createdAccount)="accountCreated($event)">
|
<app-register-form
|
||||||
|
[referenceDataValue]="referenceEventRequest"
|
||||||
|
[isInTrialFlow]="true"
|
||||||
|
(createdAccount)="accountCreated($event)"
|
||||||
|
>
|
||||||
</app-register-form>
|
</app-register-form>
|
||||||
</app-vertical-step>
|
</app-vertical-step>
|
||||||
<app-vertical-step
|
<app-vertical-step
|
||||||
@@ -24,21 +28,22 @@
|
|||||||
</button>
|
</button>
|
||||||
</app-vertical-step>
|
</app-vertical-step>
|
||||||
<app-vertical-step label="{{ 'billing' | i18n | titlecase }}" [subLabel]="billingSubLabel">
|
<app-vertical-step label="{{ 'billing' | i18n | titlecase }}" [subLabel]="billingSubLabel">
|
||||||
<app-secrets-manager-trial-billing-step
|
<app-trial-billing-step
|
||||||
*ngIf="stepper.selectedIndex === 2"
|
*ngIf="stepper.selectedIndex === 2"
|
||||||
[organizationInfo]="{
|
[organizationInfo]="{
|
||||||
name: formGroup.get('name').value,
|
name: formGroup.get('name').value,
|
||||||
email: formGroup.get('email').value
|
email: formGroup.get('email').value,
|
||||||
|
type: productType
|
||||||
}"
|
}"
|
||||||
[subscriptionType]="paidSubscriptionType"
|
[subscriptionProduct]="SubscriptionProduct.SecretsManager"
|
||||||
(steppedBack)="steppedBack()"
|
(steppedBack)="steppedBack()"
|
||||||
(organizationCreated)="organizationCreated($event)"
|
(organizationCreated)="organizationCreated($event)"
|
||||||
></app-secrets-manager-trial-billing-step>
|
></app-trial-billing-step>
|
||||||
</app-vertical-step>
|
</app-vertical-step>
|
||||||
<app-vertical-step label="{{ 'confirmationDetails' | i18n | titlecase }}">
|
<app-vertical-step label="{{ 'confirmationDetails' | i18n | titlecase }}">
|
||||||
<app-trial-confirmation-details
|
<app-trial-confirmation-details
|
||||||
[email]="formGroup.get('email').value"
|
[email]="formGroup.get('email').value"
|
||||||
[orgLabel]="subscriptionType"
|
[orgLabel]="organizationTypeQueryParameter"
|
||||||
></app-trial-confirmation-details>
|
></app-trial-confirmation-details>
|
||||||
<div class="tw-mb-3 tw-flex">
|
<div class="tw-mb-3 tw-flex">
|
||||||
<button type="button" bitButton buttonType="primary" (click)="navigateTo('vault')">
|
<button type="button" bitButton buttonType="primary" (click)="navigateTo('vault')">
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Component, Input, ViewChild } from "@angular/core";
|
import { Component, Input, ViewChild } from "@angular/core";
|
||||||
|
|
||||||
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
|
import { ProductType } from "@bitwarden/common/enums";
|
||||||
import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OrganizationCreatedEvent,
|
OrganizationCreatedEvent,
|
||||||
SubscriptionType,
|
SubscriptionProduct,
|
||||||
} from "./secrets-manager-trial-billing-step.component";
|
TrialOrganizationType,
|
||||||
|
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||||
|
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
|
||||||
|
import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-secrets-manager-trial-paid-stepper",
|
selector: "app-secrets-manager-trial-paid-stepper",
|
||||||
@@ -14,7 +16,7 @@ import {
|
|||||||
})
|
})
|
||||||
export class SecretsManagerTrialPaidStepperComponent extends SecretsManagerTrialFreeStepperComponent {
|
export class SecretsManagerTrialPaidStepperComponent extends SecretsManagerTrialFreeStepperComponent {
|
||||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||||
@Input() subscriptionType: string;
|
@Input() organizationTypeQueryParameter: string;
|
||||||
|
|
||||||
billingSubLabel = this.i18nService.t("billingTrialSubLabel");
|
billingSubLabel = this.i18nService.t("billingTrialSubLabel");
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
@@ -31,16 +33,24 @@ export class SecretsManagerTrialPaidStepperComponent extends SecretsManagerTrial
|
|||||||
|
|
||||||
get createAccountLabel() {
|
get createAccountLabel() {
|
||||||
const organizationType =
|
const organizationType =
|
||||||
this.paidSubscriptionType == SubscriptionType.Enterprise ? "Enterprise" : "Teams";
|
this.productType === ProductType.TeamsStarter
|
||||||
|
? "Teams Starter"
|
||||||
|
: ProductType[this.productType];
|
||||||
return `Before creating your ${organizationType} organization, you first need to log in or create a personal account.`;
|
return `Before creating your ${organizationType} organization, you first need to log in or create a personal account.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get paidSubscriptionType() {
|
get productType(): TrialOrganizationType {
|
||||||
switch (this.subscriptionType) {
|
switch (this.organizationTypeQueryParameter) {
|
||||||
case "enterprise":
|
case "enterprise":
|
||||||
return SubscriptionType.Enterprise;
|
return ProductType.Enterprise;
|
||||||
|
case "families":
|
||||||
|
return ProductType.Families;
|
||||||
case "teams":
|
case "teams":
|
||||||
return SubscriptionType.Teams;
|
return ProductType.Teams;
|
||||||
|
case "teamsStarter":
|
||||||
|
return ProductType.TeamsStarter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected readonly SubscriptionProduct = SubscriptionProduct;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,10 @@
|
|||||||
class="tw-flex tw-h-auto tw-w-full tw-gap-5 tw-rounded-t tw-bg-secondary-100"
|
class="tw-flex tw-h-auto tw-w-full tw-gap-5 tw-rounded-t tw-bg-secondary-100"
|
||||||
>
|
>
|
||||||
<h2 class="tw-pb-4 tw-pl-4 tw-pt-5 tw-text-base tw-font-bold tw-uppercase">
|
<h2 class="tw-pb-4 tw-pl-4 tw-pt-5 tw-text-base tw-font-bold tw-uppercase">
|
||||||
{{ "startYour7DayFreeTrialOfBitwardenSecretsManagerFor" | i18n: subscriptionType }}
|
{{
|
||||||
|
"startYour7DayFreeTrialOfBitwardenSecretsManagerFor"
|
||||||
|
| i18n: organizationTypeQueryParameter
|
||||||
|
}}
|
||||||
</h2>
|
</h2>
|
||||||
<environment-selector
|
<environment-selector
|
||||||
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
|
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
|
||||||
@@ -32,7 +35,7 @@
|
|||||||
></app-secrets-manager-trial-free-stepper>
|
></app-secrets-manager-trial-free-stepper>
|
||||||
<app-secrets-manager-trial-paid-stepper
|
<app-secrets-manager-trial-paid-stepper
|
||||||
*ngIf="!freeOrganization"
|
*ngIf="!freeOrganization"
|
||||||
[subscriptionType]="subscriptionType"
|
[organizationTypeQueryParameter]="organizationTypeQueryParameter"
|
||||||
></app-secrets-manager-trial-paid-stepper>
|
></app-secrets-manager-trial-paid-stepper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Subject, takeUntil } from "rxjs";
|
|||||||
templateUrl: "secrets-manager-trial.component.html",
|
templateUrl: "secrets-manager-trial.component.html",
|
||||||
})
|
})
|
||||||
export class SecretsManagerTrialComponent implements OnInit, OnDestroy {
|
export class SecretsManagerTrialComponent implements OnInit, OnDestroy {
|
||||||
subscriptionType: string;
|
organizationTypeQueryParameter: string;
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export class SecretsManagerTrialComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => {
|
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => {
|
||||||
this.subscriptionType = queryParameters.org;
|
this.organizationTypeQueryParameter = queryParameters.org;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +25,6 @@ export class SecretsManagerTrialComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get freeOrganization() {
|
get freeOrganization() {
|
||||||
return this.subscriptionType === "free";
|
return this.organizationTypeQueryParameter === "free";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,14 +97,18 @@
|
|||||||
</button>
|
</button>
|
||||||
</app-vertical-step>
|
</app-vertical-step>
|
||||||
<app-vertical-step label="Billing" [subLabel]="billingSubLabel">
|
<app-vertical-step label="Billing" [subLabel]="billingSubLabel">
|
||||||
<app-billing
|
<app-trial-billing-step
|
||||||
*ngIf="stepper.selectedIndex === 2"
|
*ngIf="stepper.selectedIndex === 2"
|
||||||
[plan]="plan"
|
[organizationInfo]="{
|
||||||
[product]="product"
|
name: orgInfoFormGroup.get('name').value,
|
||||||
[orgInfoForm]="orgInfoFormGroup"
|
email: orgInfoFormGroup.get('email').value,
|
||||||
(previousStep)="previousStep()"
|
type: trialOrganizationType
|
||||||
(onTrialBillingSuccess)="billingSuccess($event)"
|
}"
|
||||||
></app-billing>
|
[subscriptionProduct]="SubscriptionProduct.PasswordManager"
|
||||||
|
(steppedBack)="previousStep()"
|
||||||
|
(organizationCreated)="createdOrganization($event)"
|
||||||
|
>
|
||||||
|
</app-trial-billing-step>
|
||||||
</app-vertical-step>
|
</app-vertical-step>
|
||||||
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
|
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
|
||||||
<app-trial-confirmation-details
|
<app-trial-confirmation-details
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OrganizationCreatedEvent,
|
||||||
|
SubscriptionProduct,
|
||||||
|
TrialOrganizationType,
|
||||||
|
} from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||||
|
|
||||||
import { RouterService } from "./../../core/router.service";
|
import { RouterService } from "./../../core/router.service";
|
||||||
import { SubscriptionType } from "./secrets-manager/secrets-manager-trial-billing-step.component";
|
|
||||||
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.component";
|
||||||
|
|
||||||
enum ValidOrgParams {
|
enum ValidOrgParams {
|
||||||
@@ -79,7 +84,6 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
ValidOrgParams.individual,
|
ValidOrgParams.individual,
|
||||||
];
|
];
|
||||||
layouts = ValidLayoutParams;
|
layouts = ValidLayoutParams;
|
||||||
orgTypes = ValidOrgParams;
|
|
||||||
referenceData: ReferenceEventRequest;
|
referenceData: ReferenceEventRequest;
|
||||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||||
|
|
||||||
@@ -171,6 +175,10 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
// Are they coming from an email for sponsoring a families organization
|
// Are they coming from an email for sponsoring a families organization
|
||||||
// After logging in redirect them to setup the families sponsorship
|
// After logging in redirect them to setup the families sponsorship
|
||||||
this.setupFamilySponsorship(qParams.sponsorshipToken);
|
this.setupFamilySponsorship(qParams.sponsorshipToken);
|
||||||
|
|
||||||
|
this.referenceData.initiationPath = this.accountCreateOnly
|
||||||
|
? "Registration form"
|
||||||
|
: "Password Manager trial from marketing website";
|
||||||
});
|
});
|
||||||
|
|
||||||
const invite = await this.stateService.getOrganizationInvitation();
|
const invite = await this.stateService.getOrganizationInvitation();
|
||||||
@@ -241,6 +249,12 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
this.verticalStepper.next();
|
this.verticalStepper.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createdOrganization(event: OrganizationCreatedEvent) {
|
||||||
|
this.orgId = event.organizationId;
|
||||||
|
this.billingSubLabel = event.planDescription;
|
||||||
|
this.verticalStepper.next();
|
||||||
|
}
|
||||||
|
|
||||||
navigateToOrgVault() {
|
navigateToOrgVault() {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
@@ -274,6 +288,15 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
return this.i18nService.t(translationKey, this.org);
|
return this.i18nService.t(translationKey, this.org);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get trialOrganizationType(): TrialOrganizationType {
|
||||||
|
switch (this.product) {
|
||||||
|
case ProductType.Free:
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return this.product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private setupFamilySponsorship(sponsorshipToken: string) {
|
private setupFamilySponsorship(sponsorshipToken: string) {
|
||||||
if (sponsorshipToken != null) {
|
if (sponsorshipToken != null) {
|
||||||
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||||
@@ -283,5 +306,5 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly SubscriptionType = SubscriptionType;
|
protected readonly SubscriptionProduct = SubscriptionProduct;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { SecretsManagerTrialFreeStepperComponent } from "../../auth/trial-initia
|
|||||||
import { SecretsManagerTrialPaidStepperComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component";
|
import { SecretsManagerTrialPaidStepperComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component";
|
||||||
import { SecretsManagerTrialComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial.component";
|
import { SecretsManagerTrialComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial.component";
|
||||||
import { PaymentComponent, TaxInfoComponent } from "../../billing";
|
import { PaymentComponent, TaxInfoComponent } from "../../billing";
|
||||||
import { BillingComponent } from "../../billing/accounts/trial-initiation/billing.component";
|
import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||||
import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module";
|
import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module";
|
||||||
import { SharedModule } from "../../shared";
|
import { SharedModule } from "../../shared";
|
||||||
|
|
||||||
@@ -35,7 +35,6 @@ import { TeamsContentComponent } from "./content/teams-content.component";
|
|||||||
import { Teams1ContentComponent } from "./content/teams1-content.component";
|
import { Teams1ContentComponent } from "./content/teams1-content.component";
|
||||||
import { Teams2ContentComponent } from "./content/teams2-content.component";
|
import { Teams2ContentComponent } from "./content/teams2-content.component";
|
||||||
import { Teams3ContentComponent } from "./content/teams3-content.component";
|
import { Teams3ContentComponent } from "./content/teams3-content.component";
|
||||||
import { SecretsManagerTrialBillingStepComponent } from "./secrets-manager/secrets-manager-trial-billing-step.component";
|
|
||||||
import { TrialInitiationComponent } from "./trial-initiation.component";
|
import { TrialInitiationComponent } from "./trial-initiation.component";
|
||||||
import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
|
import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
|
||||||
|
|
||||||
@@ -50,14 +49,13 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
|||||||
EnvironmentSelectorModule,
|
EnvironmentSelectorModule,
|
||||||
PaymentComponent,
|
PaymentComponent,
|
||||||
TaxInfoComponent,
|
TaxInfoComponent,
|
||||||
SecretsManagerTrialBillingStepComponent,
|
TrialBillingStepComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
TrialInitiationComponent,
|
TrialInitiationComponent,
|
||||||
EnterpriseContentComponent,
|
EnterpriseContentComponent,
|
||||||
TeamsContentComponent,
|
TeamsContentComponent,
|
||||||
ConfirmationDetailsComponent,
|
ConfirmationDetailsComponent,
|
||||||
BillingComponent,
|
|
||||||
DefaultContentComponent,
|
DefaultContentComponent,
|
||||||
EnterpriseContentComponent,
|
EnterpriseContentComponent,
|
||||||
Enterprise1ContentComponent,
|
Enterprise1ContentComponent,
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
<form #form [formGroup]="formGroup" [appApiAction]="formPromise" (ngSubmit)="submit()">
|
|
||||||
<div class="tw-container tw-mb-3">
|
|
||||||
<div class="tw-mb-6">
|
|
||||||
<h2 class="tw-mb-3 tw-text-base tw-font-semibold">{{ "billingPlanLabel" | i18n }}</h2>
|
|
||||||
<div class="tw-mb-1 tw-items-center" *ngFor="let selectablePlan of selectablePlans">
|
|
||||||
<label class="tw- tw-block tw-text-main" for="interval{{ selectablePlan.type }}">
|
|
||||||
<input
|
|
||||||
checked
|
|
||||||
class="tw-h-4 tw-w-4 tw-align-middle"
|
|
||||||
id="interval{{ selectablePlan.type }}"
|
|
||||||
name="plan"
|
|
||||||
type="radio"
|
|
||||||
[value]="selectablePlan.type"
|
|
||||||
formControlName="plan"
|
|
||||||
/>
|
|
||||||
<ng-container *ngIf="selectablePlan.isAnnual">
|
|
||||||
<ng-container *ngIf="selectablePlan.PasswordManager">
|
|
||||||
{{ "annual" | i18n }} -
|
|
||||||
{{
|
|
||||||
(selectablePlan.PasswordManager.basePrice === 0
|
|
||||||
? selectablePlan.PasswordManager.seatPrice
|
|
||||||
: selectablePlan.PasswordManager.basePrice
|
|
||||||
) | currency: "$"
|
|
||||||
}}
|
|
||||||
/{{ "yr" | i18n }}
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="!selectablePlan.PasswordManager && selectablePlan.SecretsManager">
|
|
||||||
{{ "annual" | i18n }} -
|
|
||||||
{{
|
|
||||||
(selectablePlan.SecretsManager.basePrice === 0
|
|
||||||
? selectablePlan.SecretsManager.seatPrice
|
|
||||||
: selectablePlan.SecretsManager.basePrice
|
|
||||||
) | currency: "$"
|
|
||||||
}}
|
|
||||||
/{{ "yr" | i18n }}
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="!selectablePlan.isAnnual">
|
|
||||||
<ng-container *ngIf="selectablePlan.PasswordManager">
|
|
||||||
{{ "monthly" | i18n }} -
|
|
||||||
{{
|
|
||||||
(selectablePlan.PasswordManager.basePrice === 0
|
|
||||||
? selectablePlan.PasswordManager.seatPrice
|
|
||||||
: selectablePlan.PasswordManager.basePrice
|
|
||||||
) | currency: "$"
|
|
||||||
}}
|
|
||||||
/{{ "monthAbbr" | i18n }}
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="!selectablePlan.PasswordManager && selectablePlan.SecretsManager">
|
|
||||||
{{ "monthly" | i18n }} -
|
|
||||||
{{
|
|
||||||
(selectablePlan.SecretsManager.basePrice === 0
|
|
||||||
? selectablePlan.SecretsManager.seatPrice
|
|
||||||
: selectablePlan.SecretsManager.basePrice
|
|
||||||
) | currency: "$"
|
|
||||||
}}
|
|
||||||
/{{ "monthAbbr" | i18n }}
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tw-mb-4">
|
|
||||||
<h2 class="tw-mb-3 tw-text-base tw-font-semibold">{{ "paymentType" | i18n }}</h2>
|
|
||||||
<app-payment [hideCredit]="true" [trialFlow]="true"></app-payment>
|
|
||||||
<app-tax-info [trialFlow]="true" (onCountryChanged)="changedCountry()"></app-tax-info>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tw-flex tw-space-x-2">
|
|
||||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
|
||||||
{{ "startTrial" | i18n }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button bitButton type="button" buttonType="secondary" (click)="stepBack()">Back</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
|
||||||
import { FormGroup } from "@angular/forms";
|
|
||||||
|
|
||||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
|
||||||
import { ProductType } from "@bitwarden/common/enums";
|
|
||||||
|
|
||||||
import { OrganizationPlansComponent } from "../../organizations";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-billing",
|
|
||||||
templateUrl: "./billing.component.html",
|
|
||||||
})
|
|
||||||
export class BillingComponent extends OrganizationPlansComponent {
|
|
||||||
@Input() orgInfoForm: FormGroup;
|
|
||||||
@Output() previousStep = new EventEmitter();
|
|
||||||
|
|
||||||
async ngOnInit() {
|
|
||||||
const additionalSeats =
|
|
||||||
this.product == ProductType.Families || this.plan === PlanType.TeamsStarter ? 0 : 1;
|
|
||||||
this.formGroup.patchValue({
|
|
||||||
name: this.orgInfoForm.value.name,
|
|
||||||
billingEmail: this.orgInfoForm.value.email,
|
|
||||||
additionalSeats: additionalSeats,
|
|
||||||
plan: this.plan,
|
|
||||||
product: this.product,
|
|
||||||
});
|
|
||||||
this.isInTrialFlow = true;
|
|
||||||
await super.ngOnInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
stepBack() {
|
|
||||||
this.previousStep.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<div class="tw-container tw-mb-3">
|
<div class="tw-container tw-mb-3">
|
||||||
<div class="tw-mb-6">
|
<div class="tw-mb-6">
|
||||||
<h2 class="tw-mb-3 tw-text-base tw-font-semibold">{{ "billingPlanLabel" | i18n }}</h2>
|
<h2 class="tw-mb-3 tw-text-base tw-font-semibold">{{ "billingPlanLabel" | i18n }}</h2>
|
||||||
<div class="tw-mb-1 tw-items-center">
|
<div class="tw-mb-1 tw-items-center" *ngIf="annualPlan !== null">
|
||||||
<label class="tw- tw-block tw-text-main" for="annual">
|
<label class="tw- tw-block tw-text-main" for="annual">
|
||||||
<input
|
<input
|
||||||
class="tw-h-4 tw-w-4 tw-align-middle"
|
class="tw-h-4 tw-w-4 tw-align-middle"
|
||||||
@@ -27,16 +27,11 @@
|
|||||||
formControlName="cadence"
|
formControlName="cadence"
|
||||||
/>
|
/>
|
||||||
{{ "annual" | i18n }} -
|
{{ "annual" | i18n }} -
|
||||||
{{
|
{{ getPriceFor(annualCadence) | currency: "$" }}
|
||||||
(annualPlan.SecretsManager.basePrice === 0
|
|
||||||
? annualPlan.SecretsManager.seatPrice
|
|
||||||
: annualPlan.SecretsManager.basePrice
|
|
||||||
) | currency: "$"
|
|
||||||
}}
|
|
||||||
/{{ "yr" | i18n }}
|
/{{ "yr" | i18n }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-mb-1 tw-items-center">
|
<div class="tw-mb-1 tw-items-center" *ngIf="monthlyPlan !== null">
|
||||||
<label class="tw- tw-block tw-text-main" for="monthly">
|
<label class="tw- tw-block tw-text-main" for="monthly">
|
||||||
<input
|
<input
|
||||||
class="tw-h-4 tw-w-4 tw-align-middle"
|
class="tw-h-4 tw-w-4 tw-align-middle"
|
||||||
@@ -47,12 +42,7 @@
|
|||||||
formControlName="cadence"
|
formControlName="cadence"
|
||||||
/>
|
/>
|
||||||
{{ "monthly" | i18n }} -
|
{{ "monthly" | i18n }} -
|
||||||
{{
|
{{ getPriceFor(monthlyCadence) | currency: "$" }}
|
||||||
(monthlyPlan.SecretsManager.basePrice === 0
|
|
||||||
? monthlyPlan.SecretsManager.seatPrice
|
|
||||||
: monthlyPlan.SecretsManager.basePrice
|
|
||||||
) | currency: "$"
|
|
||||||
}}
|
|
||||||
/{{ "monthAbbr" | i18n }}
|
/{{ "monthAbbr" | i18n }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import {
|
||||||
|
BillingInformation,
|
||||||
|
OrganizationBillingServiceAbstraction as OrganizationBillingService,
|
||||||
|
OrganizationInformation,
|
||||||
|
PaymentInformation,
|
||||||
|
PlanInformation,
|
||||||
|
} from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||||
|
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
||||||
|
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||||
|
import { ProductType } from "@bitwarden/common/enums";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
|
import { BillingSharedModule, PaymentComponent, TaxInfoComponent } from "../../shared";
|
||||||
|
|
||||||
|
export type TrialOrganizationType = Exclude<ProductType, ProductType.Free>;
|
||||||
|
|
||||||
|
export interface OrganizationInfo {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
type: TrialOrganizationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationCreatedEvent {
|
||||||
|
organizationId: string;
|
||||||
|
planDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SubscriptionCadence {
|
||||||
|
Annual,
|
||||||
|
Monthly,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SubscriptionProduct {
|
||||||
|
PasswordManager,
|
||||||
|
SecretsManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-trial-billing-step",
|
||||||
|
templateUrl: "trial-billing-step.component.html",
|
||||||
|
imports: [BillingSharedModule],
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class TrialBillingStepComponent implements OnInit {
|
||||||
|
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||||
|
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
|
||||||
|
@Input() organizationInfo: OrganizationInfo;
|
||||||
|
@Input() subscriptionProduct: SubscriptionProduct = SubscriptionProduct.PasswordManager;
|
||||||
|
@Output() steppedBack = new EventEmitter();
|
||||||
|
@Output() organizationCreated = new EventEmitter<OrganizationCreatedEvent>();
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
annualCadence = SubscriptionCadence.Annual;
|
||||||
|
monthlyCadence = SubscriptionCadence.Monthly;
|
||||||
|
|
||||||
|
formGroup = this.formBuilder.group({
|
||||||
|
cadence: [SubscriptionCadence.Annual, Validators.required],
|
||||||
|
});
|
||||||
|
formPromise: Promise<string>;
|
||||||
|
|
||||||
|
applicablePlans: PlanResponse[];
|
||||||
|
annualPlan?: PlanResponse;
|
||||||
|
monthlyPlan?: PlanResponse;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private apiService: ApiService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private organizationBillingService: OrganizationBillingService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
const plans = await this.apiService.getPlans();
|
||||||
|
this.applicablePlans = plans.data.filter(this.isApplicable);
|
||||||
|
this.annualPlan = this.findPlanFor(SubscriptionCadence.Annual);
|
||||||
|
this.monthlyPlan = this.findPlanFor(SubscriptionCadence.Monthly);
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(): Promise<void> {
|
||||||
|
this.formPromise = this.createOrganization();
|
||||||
|
|
||||||
|
const organizationId = await this.formPromise;
|
||||||
|
const planDescription = this.getPlanDescription();
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
this.i18nService.t("organizationCreated"),
|
||||||
|
this.i18nService.t("organizationReadyToGo"),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.organizationCreated.emit({
|
||||||
|
organizationId,
|
||||||
|
planDescription,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.messagingService.send("organizationCreated", organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected changedCountry() {
|
||||||
|
this.paymentComponent.hideBank = this.taxInfoComponent.taxInfo.country !== "US";
|
||||||
|
if (
|
||||||
|
this.paymentComponent.hideBank &&
|
||||||
|
this.paymentComponent.method === PaymentMethodType.BankAccount
|
||||||
|
) {
|
||||||
|
this.paymentComponent.method = PaymentMethodType.Card;
|
||||||
|
this.paymentComponent.changeMethod();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPriceFor(cadence: SubscriptionCadence): number {
|
||||||
|
const plan = this.findPlanFor(cadence);
|
||||||
|
return this.subscriptionProduct === SubscriptionProduct.PasswordManager
|
||||||
|
? plan.PasswordManager.basePrice === 0
|
||||||
|
? plan.PasswordManager.seatPrice
|
||||||
|
: plan.PasswordManager.basePrice
|
||||||
|
: plan.SecretsManager.basePrice === 0
|
||||||
|
? plan.SecretsManager.seatPrice
|
||||||
|
: plan.SecretsManager.basePrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected stepBack() {
|
||||||
|
this.steppedBack.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createOrganization(): Promise<string> {
|
||||||
|
const planResponse = this.findPlanFor(this.formGroup.value.cadence);
|
||||||
|
const paymentMethod = await this.paymentComponent.createPaymentToken();
|
||||||
|
|
||||||
|
const organization: OrganizationInformation = {
|
||||||
|
name: this.organizationInfo.name,
|
||||||
|
billingEmail: this.organizationInfo.email,
|
||||||
|
initiationPath:
|
||||||
|
this.subscriptionProduct === SubscriptionProduct.PasswordManager
|
||||||
|
? "Password Manager trial from marketing website"
|
||||||
|
: "Secrets Manager trial from marketing website",
|
||||||
|
};
|
||||||
|
|
||||||
|
const plan: PlanInformation = {
|
||||||
|
type: planResponse.type,
|
||||||
|
passwordManagerSeats: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.subscriptionProduct === SubscriptionProduct.SecretsManager) {
|
||||||
|
plan.subscribeToSecretsManager = true;
|
||||||
|
plan.isFromSecretsManagerTrial = true;
|
||||||
|
plan.secretsManagerSeats = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payment: PaymentInformation = {
|
||||||
|
paymentMethod,
|
||||||
|
billing: this.getBillingInformationFromTaxInfoComponent(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.organizationBillingService.purchaseSubscription({
|
||||||
|
organization,
|
||||||
|
plan,
|
||||||
|
payment,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private productTypeToPlanTypeMap: {
|
||||||
|
[productType in TrialOrganizationType]: {
|
||||||
|
[cadence in SubscriptionCadence]?: PlanType;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
[ProductType.Enterprise]: {
|
||||||
|
[SubscriptionCadence.Annual]: PlanType.EnterpriseAnnually,
|
||||||
|
[SubscriptionCadence.Monthly]: PlanType.EnterpriseMonthly,
|
||||||
|
},
|
||||||
|
[ProductType.Families]: {
|
||||||
|
[SubscriptionCadence.Annual]: PlanType.FamiliesAnnually,
|
||||||
|
// No monthly option for Families plan
|
||||||
|
},
|
||||||
|
[ProductType.Teams]: {
|
||||||
|
[SubscriptionCadence.Annual]: PlanType.TeamsAnnually,
|
||||||
|
[SubscriptionCadence.Monthly]: PlanType.TeamsMonthly,
|
||||||
|
},
|
||||||
|
[ProductType.TeamsStarter]: {
|
||||||
|
// No annual option for Teams Starter plan
|
||||||
|
[SubscriptionCadence.Monthly]: PlanType.TeamsStarter,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
private findPlanFor(cadence: SubscriptionCadence): PlanResponse | null {
|
||||||
|
const productType = this.organizationInfo.type;
|
||||||
|
const planType = this.productTypeToPlanTypeMap[productType]?.[cadence];
|
||||||
|
return planType ? this.applicablePlans.find((plan) => plan.type === planType) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBillingInformationFromTaxInfoComponent(): BillingInformation {
|
||||||
|
return {
|
||||||
|
postalCode: this.taxInfoComponent.taxInfo.postalCode,
|
||||||
|
country: this.taxInfoComponent.taxInfo.country,
|
||||||
|
taxId: this.taxInfoComponent.taxInfo.taxId,
|
||||||
|
addressLine1: this.taxInfoComponent.taxInfo.line1,
|
||||||
|
addressLine2: this.taxInfoComponent.taxInfo.line2,
|
||||||
|
city: this.taxInfoComponent.taxInfo.city,
|
||||||
|
state: this.taxInfoComponent.taxInfo.state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPlanDescription(): string {
|
||||||
|
const plan = this.findPlanFor(this.formGroup.value.cadence);
|
||||||
|
const price =
|
||||||
|
this.subscriptionProduct === SubscriptionProduct.PasswordManager
|
||||||
|
? plan.PasswordManager.basePrice === 0
|
||||||
|
? plan.PasswordManager.seatPrice
|
||||||
|
: plan.PasswordManager.basePrice
|
||||||
|
: plan.SecretsManager.basePrice === 0
|
||||||
|
? plan.SecretsManager.seatPrice
|
||||||
|
: plan.SecretsManager.basePrice;
|
||||||
|
|
||||||
|
switch (this.formGroup.value.cadence) {
|
||||||
|
case SubscriptionCadence.Annual:
|
||||||
|
return `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`;
|
||||||
|
case SubscriptionCadence.Monthly:
|
||||||
|
return `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isApplicable(plan: PlanResponse): boolean {
|
||||||
|
const hasCorrectProductType =
|
||||||
|
plan.product === ProductType.Enterprise ||
|
||||||
|
plan.product === ProductType.Families ||
|
||||||
|
plan.product === ProductType.Teams ||
|
||||||
|
plan.product === ProductType.TeamsStarter;
|
||||||
|
const notDisabledOrLegacy = !plan.disabled && !plan.legacyYear;
|
||||||
|
return hasCorrectProductType && notDisabledOrLegacy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -642,6 +642,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||||||
request.collectionName = collectionCt;
|
request.collectionName = collectionCt;
|
||||||
request.name = this.formGroup.controls.name.value;
|
request.name = this.formGroup.controls.name.value;
|
||||||
request.billingEmail = this.formGroup.controls.billingEmail.value;
|
request.billingEmail = this.formGroup.controls.billingEmail.value;
|
||||||
|
request.initiationPath = "New organization creation in-product";
|
||||||
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||||
|
|
||||||
if (this.selectedPlan.type === PlanType.Free) {
|
if (this.selectedPlan.type === PlanType.Free) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PaymentMethodType, PlanType } from "../../../billing/enums";
|
import { PaymentMethodType, PlanType } from "../../../billing/enums";
|
||||||
|
import { InitiationPath } from "../../../models/request/reference-event.request";
|
||||||
|
|
||||||
import { OrganizationKeysRequest } from "./organization-keys.request";
|
import { OrganizationKeysRequest } from "./organization-keys.request";
|
||||||
|
|
||||||
@@ -23,9 +24,9 @@ export class OrganizationCreateRequest {
|
|||||||
billingAddressState: string;
|
billingAddressState: string;
|
||||||
billingAddressPostalCode: string;
|
billingAddressPostalCode: string;
|
||||||
billingAddressCountry: string;
|
billingAddressCountry: string;
|
||||||
|
|
||||||
useSecretsManager: boolean;
|
useSecretsManager: boolean;
|
||||||
additionalSmSeats: number;
|
additionalSmSeats: number;
|
||||||
additionalServiceAccounts: number;
|
additionalServiceAccounts: number;
|
||||||
isFromSecretsManagerTrial: boolean;
|
isFromSecretsManagerTrial: boolean;
|
||||||
|
initiationPath: InitiationPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
||||||
|
import { InitiationPath } from "../../models/request/reference-event.request";
|
||||||
import { PaymentMethodType, PlanType } from "../enums";
|
import { PaymentMethodType, PlanType } from "../enums";
|
||||||
|
|
||||||
export type OrganizationInformation = {
|
export type OrganizationInformation = {
|
||||||
name: string;
|
name: string;
|
||||||
billingEmail: string;
|
billingEmail: string;
|
||||||
businessName?: string;
|
businessName?: string;
|
||||||
|
initiationPath?: InitiationPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlanInformation = {
|
export type PlanInformation = {
|
||||||
|
|||||||
@@ -76,6 +76,18 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private prohibitsAdditionalSeats(planType: PlanType) {
|
||||||
|
switch (planType) {
|
||||||
|
case PlanType.Free:
|
||||||
|
case PlanType.FamiliesAnnually:
|
||||||
|
case PlanType.FamiliesAnnually2019:
|
||||||
|
case PlanType.TeamsStarter:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private setOrganizationInformation(
|
private setOrganizationInformation(
|
||||||
request: OrganizationCreateRequest,
|
request: OrganizationCreateRequest,
|
||||||
information: OrganizationInformation,
|
information: OrganizationInformation,
|
||||||
@@ -83,6 +95,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
|||||||
request.name = information.name;
|
request.name = information.name;
|
||||||
request.businessName = information.businessName;
|
request.businessName = information.businessName;
|
||||||
request.billingEmail = information.billingEmail;
|
request.billingEmail = information.billingEmail;
|
||||||
|
request.initiationPath = information.initiationPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setOrganizationKeys(request: OrganizationCreateRequest, keys: OrganizationKeys): void {
|
private setOrganizationKeys(request: OrganizationCreateRequest, keys: OrganizationKeys): void {
|
||||||
@@ -121,7 +134,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
|||||||
): void {
|
): void {
|
||||||
request.planType = information.type;
|
request.planType = information.type;
|
||||||
|
|
||||||
if (request.planType === PlanType.Free) {
|
if (this.prohibitsAdditionalSeats(request.planType)) {
|
||||||
request.useSecretsManager = information.subscribeToSecretsManager;
|
request.useSecretsManager = information.subscribeToSecretsManager;
|
||||||
request.isFromSecretsManagerTrial = information.isFromSecretsManagerTrial;
|
request.isFromSecretsManagerTrial = information.isFromSecretsManagerTrial;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
|
export type InitiationPath =
|
||||||
|
| "Registration form"
|
||||||
|
| "Password Manager trial from marketing website"
|
||||||
|
| "Secrets Manager trial from marketing website"
|
||||||
|
| "New organization creation in-product"
|
||||||
|
| "Upgrade in-product";
|
||||||
|
|
||||||
export class ReferenceEventRequest {
|
export class ReferenceEventRequest {
|
||||||
id: string;
|
id: string;
|
||||||
session: string;
|
session: string;
|
||||||
layout: string;
|
layout: string;
|
||||||
flow: string;
|
flow: string;
|
||||||
|
initiationPath: InitiationPath;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user