1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-21 11:35:34 +01:00

[AC-1842] Secrets Manager Trial Page (#7475)

* Got trial page working without the form set up

* Set up the form to create SM subscription

* Add free SM trial page and sign up

* Conner's changes

* fixed imports

* Set isFromSecretsManagerTrial

* Fixed OrgKey location

* Add isFromSecretsManager prop to free org create

* Add LTO callout

* Switch LTO to background box

* Defect: AC-2081

* Fixed typo "Secrets Manger" to "Secrets Manager"

* Removed discount price logic for storage and secrets manager prices since they don't apply

---------

Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com>
This commit is contained in:
Alex Morask 2024-01-29 10:45:48 -05:00 committed by GitHub
parent 305fd39871
commit 8468dbab5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1073 additions and 146 deletions

View File

@ -0,0 +1,13 @@
<figure>
<h2 class="tw-mx-auto tw-pb-2 tw-max-w-xl tw-font-semibold tw-text-center">
{{ header }}
</h2>
<blockquote class="tw-mx-auto tw-my-2 tw-max-w-xl tw-px-4 tw-text-center">
"{{ quote }}"
</blockquote>
<figcaption>
<cite>
<p class="tw-mx-auto tw-text-center tw-font-bold">{{ source }}</p>
</cite>
</figcaption>
</figure>

View File

@ -0,0 +1,11 @@
import { Component, Input } from "@angular/core";
@Component({
selector: "app-review-blurb",
templateUrl: "review-blurb.component.html",
})
export class ReviewBlurbComponent {
@Input() header: string;
@Input() quote: string;
@Input() source: string;
}

View File

@ -0,0 +1,32 @@
<h1 class="tw-text-4xl !tw-text-alt2">{{ header }}</h1>
<div class="tw-pt-16">
<h2 class="tw-text-2xl tw-font-semibold">
Secure your business with easy-to-use secrets management
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Straightforward and predictable pricing</li>
<li>Unlimited secure secret storage and sharing</li>
<li>End-to-end encryption</li>
</ul>
<div class="tw-mt-12 tw-flex tw-flex-col">
<div class="tw-rounded-[32px] tw-bg-background">
<div class="tw-my-8 tw-mx-6">
<h2 class="tw-pl-5 tw-font-semibold">Limited time offer</h2>
<ul class="tw-space-y-4 tw-mt-4 tw-pl-10">
<li>
Sign up today and receive a free 12-month subscription to Bitwarden Password Manager
</li>
<li>Experience complete security across your organization</li>
<li>Secure all your sensitive credentials, from passwords to machine secrets</li>
</ul>
</div>
</div>
</div>
<div class="tw-mt-12 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-review-blurb
header="Businesses trust Bitwarden to secure their secrets"
quote="At this point, it would be almost impossible to leak our secrets. It's just one less thing we have to worry about."
source="Head of IT, Titanom Technologies"
></app-review-blurb>
</div>

View File

@ -0,0 +1,37 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
@Component({
selector: "app-secrets-manager-content",
templateUrl: "secrets-manager-content.component.html",
})
export class SecretsManagerContentComponent implements OnInit, OnDestroy {
header: string;
private destroy$ = new Subject<void>();
constructor(private activatedRoute: ActivatedRoute) {}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
ngOnInit(): void {
this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => {
switch (queryParameters.org) {
case "enterprise":
this.header = "Secrets Manager for Enterprise";
break;
case "free":
this.header = "Bitwarden Secrets Manager";
break;
case "teams":
case "teamsStarter":
this.header = "Secrets Manager for Teams";
break;
}
});
}
}

View File

@ -0,0 +1,72 @@
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<form
#form
[formGroup]="formGroup"
[appApiAction]="formPromise"
(ngSubmit)="submit()"
*ngIf="!loading"
>
<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">
<label class="tw- tw-block tw-text-main" for="annual">
<input
class="tw-h-4 tw-w-4 tw-align-middle"
id="annual"
name="cadence"
type="radio"
[value]="annualCadence"
formControlName="cadence"
/>
{{ "annual" | i18n }} -
{{
(annualPlan.SecretsManager.basePrice === 0
? annualPlan.SecretsManager.seatPrice
: annualPlan.SecretsManager.basePrice
) | currency: "$"
}}
/{{ "yr" | i18n }}
</label>
</div>
<div class="tw-mb-1 tw-items-center">
<label class="tw- tw-block tw-text-main" for="monthly">
<input
class="tw-h-4 tw-w-4 tw-align-middle"
id="monthly"
name="cadence"
type="radio"
[value]="monthlyCadence"
formControlName="cadence"
/>
{{ "monthly" | i18n }} -
{{
(monthlyPlan.SecretsManager.basePrice === 0
? monthlyPlan.SecretsManager.seatPrice
: monthlyPlan.SecretsManager.basePrice
) | currency: "$"
}}
/{{ "monthAbbr" | i18n }}
</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>

View File

@ -0,0 +1,183 @@
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;
}
}

View File

@ -0,0 +1,54 @@
<app-vertical-stepper #stepper linear>
<app-vertical-step
label="{{ 'createAccount' | i18n | titlecase }}"
[editable]="false"
[subLabel]="subLabels.createAccount"
[addSubLabelSpacing]="true"
>
<app-register-form [isInTrialFlow]="true" (createdAccount)="accountCreated($event)">
</app-register-form>
</app-vertical-step>
<app-vertical-step
label="{{ 'organizationInformation' | i18n | titlecase }}"
[subLabel]="subLabels.organizationInfo"
>
<app-org-info [nameOnly]="true" [formGroup]="formGroup"> </app-org-info>
<button
type="button"
bitButton
buttonType="primary"
[disabled]="formGroup.get('name').invalid"
(click)="createOrganization()"
>
{{ "next" | i18n }}
</button>
</app-vertical-step>
<app-vertical-step label="{{ 'confirmationDetails' | i18n | titlecase }}">
<div class="tw-pb-6 tw-pl-6">
<p class="tw-text-xl">{{ "smFreeTrialThankYou" | i18n }}</p>
<ul class="tw-list-disc">
<li>
<p>
{{ "smFreeTrialConfirmationEmail" | i18n }}
<span class="tw-font-bold">{{ formGroup.get("email").value }}</span
>.
</p>
</li>
</ul>
</div>
<div class="tw-mb-3 tw-flex">
<button type="button" bitButton buttonType="primary" (click)="navigateTo('vault')">
{{ "getStarted" | i18n | titlecase }}
</button>
<button
type="button"
bitButton
buttonType="secondary"
(click)="navigateTo('members')"
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
>
{{ "inviteUsers" | i18n }}
</button>
</div>
</app-vertical-step>
</app-vertical-stepper>

View File

@ -0,0 +1,76 @@
import { Component, ViewChild } from "@angular/core";
import { UntypedFormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PlanType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
@Component({
selector: "app-secrets-manager-trial-free-stepper",
templateUrl: "secrets-manager-trial-free-stepper.component.html",
})
export class SecretsManagerTrialFreeStepperComponent {
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
formGroup = this.formBuilder.group({
name: [
"",
{
validators: [Validators.required, Validators.maxLength(50)],
updateOn: "change",
},
],
email: [
"",
{
validators: [Validators.email],
},
],
});
subLabels = {
createAccount:
"Before creating your free organization, you first need to log in or create a personal account.",
organizationInfo: "Enter your organization information",
};
organizationId: string;
constructor(
protected formBuilder: UntypedFormBuilder,
protected i18nService: I18nService,
protected organizationBillingService: OrganizationBillingService,
private router: Router,
) {}
accountCreated(email: string): void {
this.formGroup.get("email")?.setValue(email);
this.subLabels.createAccount = email;
this.verticalStepper.next();
}
async createOrganization(): Promise<void> {
const response = await this.organizationBillingService.startFree({
organization: {
name: this.formGroup.get("name").value,
billingEmail: this.formGroup.get("email").value,
},
plan: {
type: PlanType.Free,
subscribeToSecretsManager: true,
isFromSecretsManagerTrial: true,
},
});
this.organizationId = response.id;
this.subLabels.organizationInfo = response.name;
this.verticalStepper.next();
}
async navigateTo(organizationRoute: string): Promise<void> {
await this.router.navigate(["organizations", this.organizationId, organizationRoute]);
}
}

View File

@ -0,0 +1,58 @@
<app-vertical-stepper #stepper linear>
<app-vertical-step
label="{{ 'createAccount' | i18n | titlecase }}"
[editable]="false"
[subLabel]="createAccountLabel"
[addSubLabelSpacing]="true"
>
<app-register-form [isInTrialFlow]="true" (createdAccount)="accountCreated($event)">
</app-register-form>
</app-vertical-step>
<app-vertical-step
label="{{ 'organizationInformation' | i18n | titlecase }}"
[subLabel]="subLabels.organizationInfo"
>
<app-org-info [nameOnly]="true" [formGroup]="formGroup"></app-org-info>
<button
type="button"
bitButton
buttonType="primary"
[disabled]="formGroup.get('name').invalid"
cdkStepperNext
>
{{ "next" | i18n }}
</button>
</app-vertical-step>
<app-vertical-step label="{{ 'billing' | i18n | titlecase }}" [subLabel]="billingSubLabel">
<app-secrets-manager-trial-billing-step
*ngIf="stepper.selectedIndex === 2"
[organizationInfo]="{
name: formGroup.get('name').value,
email: formGroup.get('email').value
}"
[subscriptionType]="paidSubscriptionType"
(steppedBack)="steppedBack()"
(organizationCreated)="organizationCreated($event)"
></app-secrets-manager-trial-billing-step>
</app-vertical-step>
<app-vertical-step label="{{ 'confirmationDetails' | i18n | titlecase }}">
<app-trial-confirmation-details
[email]="formGroup.get('email').value"
[orgLabel]="subscriptionType"
></app-trial-confirmation-details>
<div class="tw-mb-3 tw-flex">
<button type="button" bitButton buttonType="primary" (click)="navigateTo('vault')">
{{ "getStarted" | i18n | titlecase }}
</button>
<button
type="button"
bitButton
buttonType="secondary"
(click)="navigateTo('members')"
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
>
{{ "inviteUsers" | i18n }}
</button>
</div>
</app-vertical-step>
</app-vertical-stepper>

View File

@ -0,0 +1,46 @@
import { Component, Input, ViewChild } from "@angular/core";
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component";
import {
OrganizationCreatedEvent,
SubscriptionType,
} from "./secrets-manager-trial-billing-step.component";
@Component({
selector: "app-secrets-manager-trial-paid-stepper",
templateUrl: "secrets-manager-trial-paid-stepper.component.html",
})
export class SecretsManagerTrialPaidStepperComponent extends SecretsManagerTrialFreeStepperComponent {
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
@Input() subscriptionType: string;
billingSubLabel = this.i18nService.t("billingTrialSubLabel");
organizationId: string;
organizationCreated(event: OrganizationCreatedEvent) {
this.organizationId = event.organizationId;
this.billingSubLabel = event.planDescription;
this.verticalStepper.next();
}
steppedBack() {
this.verticalStepper.previous();
}
get createAccountLabel() {
const organizationType =
this.paidSubscriptionType == SubscriptionType.Enterprise ? "Enterprise" : "Teams";
return `Before creating your ${organizationType} organization, you first need to log in or create a personal account.`;
}
get paidSubscriptionType() {
switch (this.subscriptionType) {
case "enterprise":
return SubscriptionType.Enterprise;
case "teams":
return SubscriptionType.Teams;
}
}
}

View File

@ -0,0 +1,41 @@
<!-- eslint-disable tailwindcss/no-custom-classname -->
<ng-container>
<div class="tw-absolute tw--z-10 tw--mt-48 tw-h-[28rem] tw-w-full tw-bg-background-alt2"></div>
<div class="tw-min-w-4xl tw-mx-auto tw-flex tw-max-w-screen-xl tw-gap-12 tw-px-4">
<div class="tw-w-1/2">
<img
alt="Bitwarden"
style="height: 50px; width: 335px"
class="tw-mt-6"
src="../../../../images/register-layout/logo-horizontal-white.svg"
/>
<div class="tw-pt-12">
<app-secrets-manager-content></app-secrets-manager-content>
</div>
</div>
<div class="tw-w-1/2">
<div class="tw-pt-44">
<div class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background">
<div
*ngIf="!freeOrganization"
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">
{{ "startYour7DayFreeTrialOfBitwardenSecretsManagerFor" | i18n: subscriptionType }}
</h2>
<environment-selector
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
></environment-selector>
</div>
<app-secrets-manager-trial-free-stepper
*ngIf="freeOrganization"
></app-secrets-manager-trial-free-stepper>
<app-secrets-manager-trial-paid-stepper
*ngIf="!freeOrganization"
[subscriptionType]="subscriptionType"
></app-secrets-manager-trial-paid-stepper>
</div>
</div>
</div>
</div>
</ng-container>

View File

@ -0,0 +1,30 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
@Component({
selector: "app-secrets-manager-trial",
templateUrl: "secrets-manager-trial.component.html",
})
export class SecretsManagerTrialComponent implements OnInit, OnDestroy {
subscriptionType: string;
private destroy$ = new Subject<void>();
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => {
this.subscriptionType = queryParameters.org;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
get freeOrganization() {
return this.subscriptionType === "free";
}
}

View File

@ -1,126 +1,140 @@
<!-- eslint-disable tailwindcss/no-custom-classname -->
<div *ngIf="accountCreateOnly" class="">
<h1 class="tw-mt-12 tw-text-center tw-text-xl">{{ "createAccount" | i18n }}</h1>
<div
class="tw-min-w-xl tw-m-auto tw-max-w-xl tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
>
<app-register-form
[queryParamEmail]="email"
[queryParamFromOrgInvite]="fromOrgInvite"
[enforcedPolicyOptions]="enforcedPolicyOptions"
[referenceDataValue]="referenceData"
></app-register-form>
</div>
</div>
<div *ngIf="!accountCreateOnly">
<div class="tw-absolute tw--z-10 tw--mt-48 tw-h-[28rem] tw-w-full tw-bg-background-alt2"></div>
<div class="tw-min-w-4xl tw-mx-auto tw-flex tw-max-w-screen-xl tw-gap-12 tw-px-4">
<div class="tw-w-1/2">
<img
alt="Bitwarden"
style="height: 50px; width: 335px"
class="tw-mt-6"
src="../../images/register-layout/logo-horizontal-white.svg"
/>
<div class="tw-pt-12">
<!-- Layout params are used by marketing to determine left-hand content -->
<app-default-content *ngIf="layout === layouts.default"></app-default-content>
<app-teams-content *ngIf="layout === layouts.teams"></app-teams-content>
<app-teams1-content *ngIf="layout === layouts.teams1"></app-teams1-content>
<app-teams2-content *ngIf="layout === layouts.teams2"></app-teams2-content>
<app-teams3-content *ngIf="layout === layouts.teams3"></app-teams3-content>
<app-enterprise-content *ngIf="layout === layouts.enterprise"></app-enterprise-content>
<app-enterprise1-content *ngIf="layout === layouts.enterprise1"></app-enterprise1-content>
<app-enterprise2-content *ngIf="layout === layouts.enterprise2"></app-enterprise2-content>
<app-cnet-enterprise-content
*ngIf="layout === layouts.cnetcmpgnent"
></app-cnet-enterprise-content>
<app-cnet-individual-content
*ngIf="layout === layouts.cnetcmpgnind"
></app-cnet-individual-content>
<app-cnet-teams-content *ngIf="layout === layouts.cnetcmpgnteams"></app-cnet-teams-content>
<app-abm-enterprise-content
*ngIf="layout === layouts.abmenterprise"
></app-abm-enterprise-content>
<app-abm-teams-content *ngIf="layout === layouts.abmteams"></app-abm-teams-content>
</div>
<app-secrets-manager-trial
*ngIf="layout === layouts.secretsManager; else passwordManagerTrial"
></app-secrets-manager-trial>
<ng-template #passwordManagerTrial>
<div *ngIf="accountCreateOnly" class="">
<h1 class="tw-mt-12 tw-text-center tw-text-xl">{{ "createAccount" | i18n }}</h1>
<div
class="tw-min-w-xl tw-m-auto tw-max-w-xl tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
>
<app-register-form
[queryParamEmail]="email"
[queryParamFromOrgInvite]="fromOrgInvite"
[enforcedPolicyOptions]="enforcedPolicyOptions"
[referenceDataValue]="referenceData"
></app-register-form>
</div>
<div class="tw-w-1/2">
<div *ngIf="!useTrialStepper">
<div
class="tw-min-w-xl tw-m-auto tw-mt-28 tw-max-w-xl tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
>
<app-register-form
[queryParamEmail]="email"
[enforcedPolicyOptions]="enforcedPolicyOptions"
[referenceDataValue]="referenceData"
></app-register-form>
</div>
<div *ngIf="!accountCreateOnly">
<div class="tw-absolute tw--z-10 tw--mt-48 tw-h-[28rem] tw-w-full tw-bg-background-alt2"></div>
<div class="tw-min-w-4xl tw-mx-auto tw-flex tw-max-w-screen-xl tw-gap-12 tw-px-4">
<div class="tw-w-1/2">
<img
alt="Bitwarden"
style="height: 50px; width: 335px"
class="tw-mt-6"
src="../../images/register-layout/logo-horizontal-white.svg"
/>
<div class="tw-pt-12">
<!-- Layout params are used by marketing to determine left-hand content -->
<app-default-content *ngIf="layout === layouts.default"></app-default-content>
<app-teams-content *ngIf="layout === layouts.teams"></app-teams-content>
<app-teams1-content *ngIf="layout === layouts.teams1"></app-teams1-content>
<app-teams2-content *ngIf="layout === layouts.teams2"></app-teams2-content>
<app-teams3-content *ngIf="layout === layouts.teams3"></app-teams3-content>
<app-enterprise-content *ngIf="layout === layouts.enterprise"></app-enterprise-content>
<app-enterprise1-content *ngIf="layout === layouts.enterprise1"></app-enterprise1-content>
<app-enterprise2-content *ngIf="layout === layouts.enterprise2"></app-enterprise2-content>
<app-cnet-enterprise-content
*ngIf="layout === layouts.cnetcmpgnent"
></app-cnet-enterprise-content>
<app-cnet-individual-content
*ngIf="layout === layouts.cnetcmpgnind"
></app-cnet-individual-content>
<app-cnet-teams-content
*ngIf="layout === layouts.cnetcmpgnteams"
></app-cnet-teams-content>
<app-abm-enterprise-content
*ngIf="layout === layouts.abmenterprise"
></app-abm-enterprise-content>
<app-abm-teams-content *ngIf="layout === layouts.abmteams"></app-abm-teams-content>
</div>
</div>
<div class="tw-pt-44" *ngIf="useTrialStepper">
<div class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background">
<div 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">
{{ "startYour7DayFreeTrialOfBitwardenFor" | i18n: orgDisplayName }}
</h2>
<environment-selector
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
></environment-selector>
<div class="tw-w-1/2">
<div *ngIf="!useTrialStepper">
<div
class="tw-min-w-xl tw-m-auto tw-mt-28 tw-max-w-xl tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
>
<app-register-form
[queryParamEmail]="email"
[enforcedPolicyOptions]="enforcedPolicyOptions"
[referenceDataValue]="referenceData"
></app-register-form>
</div>
<app-vertical-stepper #stepper linear (selectionChange)="stepSelectionChange($event)">
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
<app-register-form
[isInTrialFlow]="true"
(createdAccount)="createdAccount($event)"
[referenceDataValue]="referenceData"
></app-register-form>
</app-vertical-step>
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
<button
type="button"
bitButton
buttonType="primary"
[disabled]="orgInfoFormGroup.get('name').invalid"
cdkStepperNext
>
{{ "next" | i18n }}
</button>
</app-vertical-step>
<app-vertical-step label="Billing" [subLabel]="billingSubLabel">
<app-billing
*ngIf="stepper.selectedIndex === 2"
[plan]="plan"
[product]="product"
[orgInfoForm]="orgInfoFormGroup"
(previousStep)="previousStep()"
(onTrialBillingSuccess)="billingSuccess($event)"
></app-billing>
</app-vertical-step>
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
<app-trial-confirmation-details
[email]="email"
[orgLabel]="orgLabel"
></app-trial-confirmation-details>
<div class="tw-mb-3 tw-flex">
<button type="button" bitButton buttonType="primary" (click)="navigateToOrgVault()">
{{ "getStarted" | i18n | titlecase }}
</button>
</div>
<div class="tw-pt-44" *ngIf="useTrialStepper">
<div
class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background"
>
<div 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">
{{ freeTrialText }}
</h2>
<environment-selector
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
></environment-selector>
</div>
<app-vertical-stepper #stepper linear (selectionChange)="stepSelectionChange($event)">
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
<app-register-form
[isInTrialFlow]="true"
(createdAccount)="createdAccount($event)"
[referenceDataValue]="referenceData"
></app-register-form>
</app-vertical-step>
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
<button
type="button"
bitButton
buttonType="secondary"
(click)="navigateToOrgInvite()"
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
buttonType="primary"
[disabled]="orgInfoFormGroup.get('name').invalid"
cdkStepperNext
>
{{ "inviteUsers" | i18n }}
{{ "next" | i18n }}
</button>
</div>
</app-vertical-step>
</app-vertical-stepper>
</app-vertical-step>
<app-vertical-step label="Billing" [subLabel]="billingSubLabel">
<app-billing
*ngIf="stepper.selectedIndex === 2"
[plan]="plan"
[product]="product"
[orgInfoForm]="orgInfoFormGroup"
(previousStep)="previousStep()"
(onTrialBillingSuccess)="billingSuccess($event)"
></app-billing>
</app-vertical-step>
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
<app-trial-confirmation-details
[email]="email"
[orgLabel]="orgLabel"
></app-trial-confirmation-details>
<div class="tw-mb-3 tw-flex">
<button
type="button"
bitButton
buttonType="primary"
(click)="navigateToOrgVault()"
>
{{ "getStarted" | i18n | titlecase }}
</button>
<button
type="button"
bitButton
buttonType="secondary"
(click)="navigateToOrgInvite()"
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
>
{{ "inviteUsers" | i18n }}
</button>
</div>
</app-vertical-step>
</app-vertical-stepper>
</div>
</div>
</div>
</div>
</div>
</div>
</ng-template>

View File

@ -18,6 +18,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { StateService } from "@bitwarden/common/platform/abstractions/state.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";
enum ValidOrgParams {
@ -44,6 +45,7 @@ enum ValidLayoutParams {
cnetcmpgnteams = "cnetcmpgnteams",
abmenterprise = "abmenterprise",
abmteams = "abmteams",
secretsManager = "secretsManager",
}
@Component({
@ -77,6 +79,7 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
ValidOrgParams.individual,
];
layouts = ValidLayoutParams;
orgTypes = ValidOrgParams;
referenceData: ReferenceEventRequest;
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
@ -258,6 +261,15 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
return this.org;
}
get freeTrialText() {
const translationKey =
this.layout === this.layouts.secretsManager
? "startYour7DayFreeTrialOfBitwardenSecretsManagerFor"
: "startYour7DayFreeTrialOfBitwardenFor";
return this.i18nService.t(translationKey, this.org);
}
private setupFamilySponsorship(sponsorshipToken: string) {
if (sponsorshipToken != null) {
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
@ -266,4 +278,6 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
this.routerService.setPreviousUrl(route.toString());
}
}
protected readonly SubscriptionType = SubscriptionType;
}

View File

@ -6,6 +6,9 @@ import { FormFieldModule } from "@bitwarden/components";
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
import { RegisterFormModule } from "../../auth/register-form/register-form.module";
import { SecretsManagerTrialFreeStepperComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial-free-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 { PaymentComponent, TaxInfoComponent } from "../../billing";
import { BillingComponent } from "../../billing/accounts/trial-initiation/billing.component";
import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module";
@ -25,11 +28,14 @@ import { LogoCnet5StarsComponent } from "./content/logo-cnet-5-stars.component";
import { LogoCnetComponent } from "./content/logo-cnet.component";
import { LogoForbesComponent } from "./content/logo-forbes.component";
import { LogoUSNewsComponent } from "./content/logo-us-news.component";
import { ReviewBlurbComponent } from "./content/review-blurb.component";
import { ReviewLogoComponent } from "./content/review-logo.component";
import { SecretsManagerContentComponent } from "./content/secrets-manager-content.component";
import { TeamsContentComponent } from "./content/teams-content.component";
import { Teams1ContentComponent } from "./content/teams1-content.component";
import { Teams2ContentComponent } from "./content/teams2-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 { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
@ -44,6 +50,7 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
EnvironmentSelectorModule,
PaymentComponent,
TaxInfoComponent,
SecretsManagerTrialBillingStepComponent,
],
declarations: [
TrialInitiationComponent,
@ -69,6 +76,11 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
LogoForbesComponent,
LogoUSNewsComponent,
ReviewLogoComponent,
SecretsManagerContentComponent,
ReviewBlurbComponent,
SecretsManagerTrialComponent,
SecretsManagerTrialFreeStepperComponent,
SecretsManagerTrialPaidStepperComponent,
],
exports: [TrialInitiationComponent],
providers: [TitleCasePipe],

View File

@ -1,7 +1,10 @@
<ng-template>
<div
class="tw-inline-block tw-w-11/12 tw-pl-7"
[ngClass]="{ 'tw-border-0 tw-border-l tw-border-solid tw-border-secondary-300': applyBorder }"
[ngClass]="{
'tw-border-0 tw-border-l tw-border-solid tw-border-secondary-300': applyBorder,
'tw-pt-6': addSubLabelSpacing
}"
>
<ng-content></ng-content>
</div>

View File

@ -9,4 +9,5 @@ import { Component, Input } from "@angular/core";
export class VerticalStep extends CdkStep {
@Input() subLabel = "";
@Input() applyBorder = true;
@Input() addSubLabelSpacing = false;
}

View File

@ -140,7 +140,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
get subscriptionLineItems() {
return this.lineItems.map((lineItem: BillingSubscriptionItemResponse) => ({
name: lineItem.name,
amount: this.discountPrice(lineItem.amount),
amount: this.discountPrice(lineItem.amount, lineItem.productId),
quantity: lineItem.quantity,
interval: lineItem.interval,
sponsoredSubscriptionItem: lineItem.sponsoredSubscriptionItem,
@ -183,7 +183,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
}
get storageGbPrice() {
return this.discountPrice(this.sub.plan.PasswordManager.additionalStoragePricePerGb);
return this.sub.plan.PasswordManager.additionalStoragePricePerGb;
}
get seatPrice() {
@ -198,14 +198,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
return {
seatCount: this.sub.smSeats,
maxAutoscaleSeats: this.sub.maxAutoscaleSmSeats,
seatPrice: this.discountPrice(this.sub.plan.SecretsManager.seatPrice),
seatPrice: this.sub.plan.SecretsManager.seatPrice,
maxAutoscaleServiceAccounts: this.sub.maxAutoscaleSmServiceAccounts,
additionalServiceAccounts:
this.sub.smServiceAccounts - this.sub.plan.SecretsManager.baseServiceAccount,
interval: this.sub.plan.isAnnual ? "year" : "month",
additionalServiceAccountPrice: this.discountPrice(
this.sub.plan.SecretsManager.additionalPricePerServiceAccount,
),
additionalServiceAccountPrice: this.sub.plan.SecretsManager.additionalPricePerServiceAccount,
baseServiceAccountCount: this.sub.plan.SecretsManager.baseServiceAccount,
};
}
@ -404,9 +402,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
}
};
discountPrice = (price: number) => {
discountPrice = (price: number, productId: string = null) => {
const discount =
!!this.customerDiscount && this.customerDiscount.active
this.customerDiscount?.active &&
(!productId ||
!this.customerDiscount.appliesTo.length ||
this.customerDiscount.appliesTo.includes(productId))
? price * (this.customerDiscount.percentOff / 100)
: 0;

View File

@ -2531,6 +2531,15 @@
}
}
},
"trialSecretsManagerThankYou": {
"message": "Thanks for signing up for Bitwarden Secrets Manager for $PLAN$!",
"placeholders": {
"plan": {
"content": "$1",
"example": "Teams"
}
}
},
"trialPaidInfoMessage": {
"message": "Your $PLAN$ 7 day free trial will be converted to a paid subscription after 7 days.",
"placeholders": {
@ -3471,7 +3480,7 @@
"message": "Set a seat limit for your subscription. Once this limit is reached, you will not be able to invite new members."
},
"limitSmSubscriptionDesc": {
"message": "Set a seat limit for your Secrets Manger subscription. Once this limit is reached, you will not be able to invite new members."
"message": "Set a seat limit for your Secrets Manager subscription. Once this limit is reached, you will not be able to invite new members."
},
"maxSeatLimit": {
"message": "Seat Limit (optional)",
@ -7234,6 +7243,15 @@
}
}
},
"startYour7DayFreeTrialOfBitwardenSecretsManagerFor": {
"message": "Start your 7-Day free trial of Bitwarden Secrets Manager for $ORG$",
"placeholders": {
"org": {
"content": "$1",
"example": "Organization name"
}
}
},
"next": {
"message": "Next"
},
@ -7508,5 +7526,17 @@
},
"collectionEnhancementsLearnMore": {
"message": "Learn more about collection management"
},
"organizationInformation": {
"message": "Organization information"
},
"confirmationDetails": {
"message": "Confirmation details"
},
"smFreeTrialThankYou": {
"message": "Thank you for signing up for Bitwarden Secrets Manager!"
},
"smFreeTrialConfirmationEmail": {
"message": "We've sent a confirmation email to your email at "
}
}

View File

@ -2,47 +2,47 @@
"folders": [
{
"name": "root",
"path": ".",
"path": "."
},
{
"name": "web vault",
"path": "apps/web",
"path": "apps/web"
},
{
"name": "web vault (bit)",
"path": "bitwarden_license/bit-web",
"path": "bitwarden_license/bit-web"
},
{
"name": "cli",
"path": "apps/cli",
"path": "apps/cli"
},
{
"name": "desktop",
"path": "apps/desktop",
"path": "apps/desktop"
},
{
"name": "browser",
"path": "apps/browser",
"path": "apps/browser"
},
{
"name": "libs",
"path": "libs",
},
"path": "libs"
}
],
"settings": {
"eslint.options": {
"overrideConfig": {
"parserOptions": {
"project": ["${workspaceFolder}/tsconfig.eslint.json"],
},
},
"project": ["${workspaceFolder}/tsconfig.eslint.json"]
}
}
},
"debug.javascript.terminalOptions": {
"sourceMapPathOverrides": {
"webpack:///./~/*": "${workspaceFolder:root}/node_modules/*",
"webpack://?:*/*": "${workspaceFolder}/*",
"webpack://@bitwarden/cli/*": "${workspaceFolder}/*",
},
"webpack://@bitwarden/cli/*": "${workspaceFolder}/*"
}
},
"jest.disabledWorkspaceFolders": [
"browser",
@ -56,14 +56,14 @@
"jest.jestCommandLine": "npx jest",
"angular.enable-strict-mode-prompt": false,
"typescript.preferences.importModuleSpecifier": "project-relative",
"javascript.preferences.importModuleSpecifier": "project-relative",
"javascript.preferences.importModuleSpecifier": "project-relative"
},
"extensions": {
"recommendations": [
"orta.vscode-jest",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"Angular.ng-template",
],
},
"Angular.ng-template"
]
}
}

View File

@ -76,7 +76,9 @@ import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauth
import { WebAuthnLoginPrfCryptoService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-crypto.service";
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
import { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { BillingBannerService } from "@bitwarden/common/billing/services/billing-banner.service";
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
@ -864,6 +866,16 @@ import { ModalService } from "./modal.service";
useClass: BillingBannerService,
deps: [StateProvider],
},
{
provide: OrganizationBillingServiceAbstraction,
useClass: OrganizationBillingService,
deps: [
CryptoServiceAbstraction,
EncryptService,
I18nServiceAbstraction,
OrganizationApiServiceAbstraction,
],
},
],
})
export class JslibServicesModule {}

View File

@ -27,4 +27,5 @@ export class OrganizationCreateRequest {
useSecretsManager: boolean;
additionalSmSeats: number;
additionalServiceAccounts: number;
isFromSecretsManagerTrial: boolean;
}

View File

@ -0,0 +1,45 @@
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { PaymentMethodType, PlanType } from "../enums";
export type OrganizationInformation = {
name: string;
billingEmail: string;
businessName?: string;
};
export type PlanInformation = {
type: PlanType;
passwordManagerSeats?: number;
subscribeToSecretsManager?: boolean;
isFromSecretsManagerTrial?: boolean;
secretsManagerSeats?: number;
secretsManagerServiceAccounts?: number;
storage?: number;
};
export type BillingInformation = {
postalCode: string;
country: string;
taxId?: string;
addressLine1?: string;
addressLine2?: string;
city?: string;
state?: string;
};
export type PaymentInformation = {
paymentMethod: [string, PaymentMethodType];
billing: BillingInformation;
};
export type SubscriptionInformation = {
organization: OrganizationInformation;
plan?: PlanInformation;
payment?: PaymentInformation;
};
export abstract class OrganizationBillingServiceAbstraction {
purchaseSubscription: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>;
startFree: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>;
}

View File

@ -40,17 +40,13 @@ export class BillingCustomerDiscount extends BaseResponse {
id: string;
active: boolean;
percentOff?: number;
appliesTo: string[];
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.active = this.getResponseProperty("Active");
this.percentOff = this.getResponseProperty("PercentOff");
this.appliesTo = this.getResponseProperty("AppliesTo");
}
discountPrice = (price: number) => {
const discount = this !== null && this.active ? price * (this.percentOff / 100) : 0;
return price - discount;
};
}

View File

@ -39,7 +39,7 @@ export class BillingSubscriptionResponse extends BaseResponse {
constructor(response: any) {
super(response);
this.trialEndDate = this.getResponseProperty("TrialStartDate");
this.trialStartDate = this.getResponseProperty("TrialStartDate");
this.trialEndDate = this.getResponseProperty("TrialEndDate");
this.periodStartDate = this.getResponseProperty("PeriodStartDate");
this.periodEndDate = this.getResponseProperty("PeriodEndDate");
@ -55,6 +55,7 @@ export class BillingSubscriptionResponse extends BaseResponse {
}
export class BillingSubscriptionItemResponse extends BaseResponse {
productId: string;
name: string;
amount: number;
quantity: number;
@ -65,6 +66,7 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
constructor(response: any) {
super(response);
this.productId = this.getResponseProperty("ProductId");
this.name = this.getResponseProperty("Name");
this.amount = this.getResponseProperty("Amount");
this.quantity = this.getResponseProperty("Quantity");

View File

@ -0,0 +1,143 @@
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request";
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { EncString } from "../../platform/models/domain/enc-string";
import { OrgKey } from "../../types/key";
import {
OrganizationBillingServiceAbstraction,
OrganizationInformation,
PaymentInformation,
PlanInformation,
SubscriptionInformation,
} from "../abstractions/organization-billing.service";
import { PlanType } from "../enums";
interface OrganizationKeys {
encryptedKey: EncString;
publicKey: string;
encryptedPrivateKey: EncString;
encryptedCollectionName: EncString;
}
export class OrganizationBillingService implements OrganizationBillingServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private encryptService: EncryptService,
private i18nService: I18nService,
private organizationApiService: OrganizationApiService,
) {}
async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
const request = new OrganizationCreateRequest();
const organizationKeys = await this.makeOrganizationKeys();
this.setOrganizationKeys(request, organizationKeys);
this.setOrganizationInformation(request, subscription.organization);
this.setPlanInformation(request, subscription.plan);
this.setPaymentInformation(request, subscription.payment);
return await this.organizationApiService.create(request);
}
async startFree(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
const request = new OrganizationCreateRequest();
const organizationKeys = await this.makeOrganizationKeys();
this.setOrganizationKeys(request, organizationKeys);
this.setOrganizationInformation(request, subscription.organization);
this.setPlanInformation(request, subscription.plan);
return await this.organizationApiService.create(request);
}
private async makeOrganizationKeys(): Promise<OrganizationKeys> {
const [encryptedKey, key] = await this.cryptoService.makeOrgKey<OrgKey>();
const [publicKey, encryptedPrivateKey] = await this.cryptoService.makeKeyPair(key);
const encryptedCollectionName = await this.encryptService.encrypt(
this.i18nService.t("defaultCollection"),
key,
);
return {
encryptedKey,
publicKey,
encryptedPrivateKey,
encryptedCollectionName,
};
}
private setOrganizationInformation(
request: OrganizationCreateRequest,
information: OrganizationInformation,
): void {
request.name = information.name;
request.businessName = information.businessName;
request.billingEmail = information.billingEmail;
}
private setOrganizationKeys(request: OrganizationCreateRequest, keys: OrganizationKeys): void {
request.key = keys.encryptedKey.encryptedString;
request.keys = new OrganizationKeysRequest(
keys.publicKey,
keys.encryptedPrivateKey.encryptedString,
);
request.collectionName = keys.encryptedCollectionName.encryptedString;
}
private setPaymentInformation(
request: OrganizationCreateRequest,
information: PaymentInformation,
) {
const [paymentToken, paymentMethodType] = information.paymentMethod;
request.paymentToken = paymentToken;
request.paymentMethodType = paymentMethodType;
const billingInformation = information.billing;
request.billingAddressPostalCode = billingInformation.postalCode;
request.billingAddressCountry = billingInformation.country;
if (billingInformation.taxId) {
request.taxIdNumber = billingInformation.taxId;
request.billingAddressLine1 = billingInformation.addressLine1;
request.billingAddressLine2 = billingInformation.addressLine2;
request.billingAddressCity = billingInformation.city;
request.billingAddressState = billingInformation.state;
}
}
private setPlanInformation(
request: OrganizationCreateRequest,
information: PlanInformation,
): void {
request.planType = information.type;
if (request.planType === PlanType.Free) {
request.useSecretsManager = information.subscribeToSecretsManager;
request.isFromSecretsManagerTrial = information.isFromSecretsManagerTrial;
return;
}
request.additionalSeats = information.passwordManagerSeats;
if (information.subscribeToSecretsManager) {
request.useSecretsManager = true;
request.isFromSecretsManagerTrial = information.isFromSecretsManagerTrial;
request.additionalSmSeats = information.secretsManagerSeats;
request.additionalServiceAccounts = information.secretsManagerServiceAccounts;
}
if (information.storage) {
request.additionalStorageGb = information.storage;
}
}
}