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:
parent
305fd39871
commit
8468dbab5b
@ -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>
|
@ -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;
|
||||||
|
}
|
@ -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>
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
@ -1,126 +1,140 @@
|
|||||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
||||||
<div *ngIf="accountCreateOnly" class="">
|
<app-secrets-manager-trial
|
||||||
<h1 class="tw-mt-12 tw-text-center tw-text-xl">{{ "createAccount" | i18n }}</h1>
|
*ngIf="layout === layouts.secretsManager; else passwordManagerTrial"
|
||||||
<div
|
></app-secrets-manager-trial>
|
||||||
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"
|
<ng-template #passwordManagerTrial>
|
||||||
>
|
<div *ngIf="accountCreateOnly" class="">
|
||||||
<app-register-form
|
<h1 class="tw-mt-12 tw-text-center tw-text-xl">{{ "createAccount" | i18n }}</h1>
|
||||||
[queryParamEmail]="email"
|
<div
|
||||||
[queryParamFromOrgInvite]="fromOrgInvite"
|
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"
|
||||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
>
|
||||||
[referenceDataValue]="referenceData"
|
<app-register-form
|
||||||
></app-register-form>
|
[queryParamEmail]="email"
|
||||||
</div>
|
[queryParamFromOrgInvite]="fromOrgInvite"
|
||||||
</div>
|
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||||
<div *ngIf="!accountCreateOnly">
|
[referenceDataValue]="referenceData"
|
||||||
<div class="tw-absolute tw--z-10 tw--mt-48 tw-h-[28rem] tw-w-full tw-bg-background-alt2"></div>
|
></app-register-form>
|
||||||
<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>
|
||||||
<div class="tw-w-1/2">
|
</div>
|
||||||
<div *ngIf="!useTrialStepper">
|
<div *ngIf="!accountCreateOnly">
|
||||||
<div
|
<div class="tw-absolute tw--z-10 tw--mt-48 tw-h-[28rem] tw-w-full tw-bg-background-alt2"></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"
|
<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">
|
||||||
<app-register-form
|
<img
|
||||||
[queryParamEmail]="email"
|
alt="Bitwarden"
|
||||||
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
style="height: 50px; width: 335px"
|
||||||
[referenceDataValue]="referenceData"
|
class="tw-mt-6"
|
||||||
></app-register-form>
|
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>
|
</div>
|
||||||
<div class="tw-pt-44" *ngIf="useTrialStepper">
|
<div class="tw-w-1/2">
|
||||||
<div class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background">
|
<div *ngIf="!useTrialStepper">
|
||||||
<div class="tw-flex tw-h-auto tw-w-full tw-gap-5 tw-rounded-t tw-bg-secondary-100">
|
<div
|
||||||
<h2 class="tw-pb-4 tw-pl-4 tw-pt-5 tw-text-base tw-font-bold tw-uppercase">
|
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"
|
||||||
{{ "startYour7DayFreeTrialOfBitwardenFor" | i18n: orgDisplayName }}
|
>
|
||||||
</h2>
|
<app-register-form
|
||||||
<environment-selector
|
[queryParamEmail]="email"
|
||||||
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
|
[enforcedPolicyOptions]="enforcedPolicyOptions"
|
||||||
></environment-selector>
|
[referenceDataValue]="referenceData"
|
||||||
|
></app-register-form>
|
||||||
</div>
|
</div>
|
||||||
<app-vertical-stepper #stepper linear (selectionChange)="stepSelectionChange($event)">
|
</div>
|
||||||
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
<div class="tw-pt-44" *ngIf="useTrialStepper">
|
||||||
<app-register-form
|
<div
|
||||||
[isInTrialFlow]="true"
|
class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background"
|
||||||
(createdAccount)="createdAccount($event)"
|
>
|
||||||
[referenceDataValue]="referenceData"
|
<div class="tw-flex tw-h-auto tw-w-full tw-gap-5 tw-rounded-t tw-bg-secondary-100">
|
||||||
></app-register-form>
|
<h2 class="tw-pb-4 tw-pl-4 tw-pt-5 tw-text-base tw-font-bold tw-uppercase">
|
||||||
</app-vertical-step>
|
{{ freeTrialText }}
|
||||||
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
</h2>
|
||||||
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
|
<environment-selector
|
||||||
<button
|
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
|
||||||
type="button"
|
></environment-selector>
|
||||||
bitButton
|
</div>
|
||||||
buttonType="primary"
|
<app-vertical-stepper #stepper linear (selectionChange)="stepSelectionChange($event)">
|
||||||
[disabled]="orgInfoFormGroup.get('name').invalid"
|
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
||||||
cdkStepperNext
|
<app-register-form
|
||||||
>
|
[isInTrialFlow]="true"
|
||||||
{{ "next" | i18n }}
|
(createdAccount)="createdAccount($event)"
|
||||||
</button>
|
[referenceDataValue]="referenceData"
|
||||||
</app-vertical-step>
|
></app-register-form>
|
||||||
<app-vertical-step label="Billing" [subLabel]="billingSubLabel">
|
</app-vertical-step>
|
||||||
<app-billing
|
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
||||||
*ngIf="stepper.selectedIndex === 2"
|
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
|
||||||
[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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="secondary"
|
buttonType="primary"
|
||||||
(click)="navigateToOrgInvite()"
|
[disabled]="orgInfoFormGroup.get('name').invalid"
|
||||||
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
|
cdkStepperNext
|
||||||
>
|
>
|
||||||
{{ "inviteUsers" | i18n }}
|
{{ "next" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</app-vertical-step>
|
||||||
</app-vertical-step>
|
<app-vertical-step label="Billing" [subLabel]="billingSubLabel">
|
||||||
</app-vertical-stepper>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ng-template>
|
||||||
|
@ -18,6 +18,7 @@ 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 { 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 {
|
||||||
@ -44,6 +45,7 @@ enum ValidLayoutParams {
|
|||||||
cnetcmpgnteams = "cnetcmpgnteams",
|
cnetcmpgnteams = "cnetcmpgnteams",
|
||||||
abmenterprise = "abmenterprise",
|
abmenterprise = "abmenterprise",
|
||||||
abmteams = "abmteams",
|
abmteams = "abmteams",
|
||||||
|
secretsManager = "secretsManager",
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -77,6 +79,7 @@ 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;
|
||||||
|
|
||||||
@ -258,6 +261,15 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
return this.org;
|
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) {
|
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"], {
|
||||||
@ -266,4 +278,6 @@ export class TrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
this.routerService.setPreviousUrl(route.toString());
|
this.routerService.setPreviousUrl(route.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected readonly SubscriptionType = SubscriptionType;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,9 @@ import { FormFieldModule } from "@bitwarden/components";
|
|||||||
|
|
||||||
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
|
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
|
||||||
import { RegisterFormModule } from "../../auth/register-form/register-form.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 { PaymentComponent, TaxInfoComponent } from "../../billing";
|
||||||
import { BillingComponent } from "../../billing/accounts/trial-initiation/billing.component";
|
import { BillingComponent } from "../../billing/accounts/trial-initiation/billing.component";
|
||||||
import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module";
|
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 { LogoCnetComponent } from "./content/logo-cnet.component";
|
||||||
import { LogoForbesComponent } from "./content/logo-forbes.component";
|
import { LogoForbesComponent } from "./content/logo-forbes.component";
|
||||||
import { LogoUSNewsComponent } from "./content/logo-us-news.component";
|
import { LogoUSNewsComponent } from "./content/logo-us-news.component";
|
||||||
|
import { ReviewBlurbComponent } from "./content/review-blurb.component";
|
||||||
import { ReviewLogoComponent } from "./content/review-logo.component";
|
import { ReviewLogoComponent } from "./content/review-logo.component";
|
||||||
|
import { SecretsManagerContentComponent } from "./content/secrets-manager-content.component";
|
||||||
import { TeamsContentComponent } from "./content/teams-content.component";
|
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";
|
||||||
|
|
||||||
@ -44,6 +50,7 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
|||||||
EnvironmentSelectorModule,
|
EnvironmentSelectorModule,
|
||||||
PaymentComponent,
|
PaymentComponent,
|
||||||
TaxInfoComponent,
|
TaxInfoComponent,
|
||||||
|
SecretsManagerTrialBillingStepComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
TrialInitiationComponent,
|
TrialInitiationComponent,
|
||||||
@ -69,6 +76,11 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
|||||||
LogoForbesComponent,
|
LogoForbesComponent,
|
||||||
LogoUSNewsComponent,
|
LogoUSNewsComponent,
|
||||||
ReviewLogoComponent,
|
ReviewLogoComponent,
|
||||||
|
SecretsManagerContentComponent,
|
||||||
|
ReviewBlurbComponent,
|
||||||
|
SecretsManagerTrialComponent,
|
||||||
|
SecretsManagerTrialFreeStepperComponent,
|
||||||
|
SecretsManagerTrialPaidStepperComponent,
|
||||||
],
|
],
|
||||||
exports: [TrialInitiationComponent],
|
exports: [TrialInitiationComponent],
|
||||||
providers: [TitleCasePipe],
|
providers: [TitleCasePipe],
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
<ng-template>
|
<ng-template>
|
||||||
<div
|
<div
|
||||||
class="tw-inline-block tw-w-11/12 tw-pl-7"
|
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>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,4 +9,5 @@ import { Component, Input } from "@angular/core";
|
|||||||
export class VerticalStep extends CdkStep {
|
export class VerticalStep extends CdkStep {
|
||||||
@Input() subLabel = "";
|
@Input() subLabel = "";
|
||||||
@Input() applyBorder = true;
|
@Input() applyBorder = true;
|
||||||
|
@Input() addSubLabelSpacing = false;
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
get subscriptionLineItems() {
|
get subscriptionLineItems() {
|
||||||
return this.lineItems.map((lineItem: BillingSubscriptionItemResponse) => ({
|
return this.lineItems.map((lineItem: BillingSubscriptionItemResponse) => ({
|
||||||
name: lineItem.name,
|
name: lineItem.name,
|
||||||
amount: this.discountPrice(lineItem.amount),
|
amount: this.discountPrice(lineItem.amount, lineItem.productId),
|
||||||
quantity: lineItem.quantity,
|
quantity: lineItem.quantity,
|
||||||
interval: lineItem.interval,
|
interval: lineItem.interval,
|
||||||
sponsoredSubscriptionItem: lineItem.sponsoredSubscriptionItem,
|
sponsoredSubscriptionItem: lineItem.sponsoredSubscriptionItem,
|
||||||
@ -183,7 +183,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
}
|
}
|
||||||
|
|
||||||
get storageGbPrice() {
|
get storageGbPrice() {
|
||||||
return this.discountPrice(this.sub.plan.PasswordManager.additionalStoragePricePerGb);
|
return this.sub.plan.PasswordManager.additionalStoragePricePerGb;
|
||||||
}
|
}
|
||||||
|
|
||||||
get seatPrice() {
|
get seatPrice() {
|
||||||
@ -198,14 +198,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||||||
return {
|
return {
|
||||||
seatCount: this.sub.smSeats,
|
seatCount: this.sub.smSeats,
|
||||||
maxAutoscaleSeats: this.sub.maxAutoscaleSmSeats,
|
maxAutoscaleSeats: this.sub.maxAutoscaleSmSeats,
|
||||||
seatPrice: this.discountPrice(this.sub.plan.SecretsManager.seatPrice),
|
seatPrice: this.sub.plan.SecretsManager.seatPrice,
|
||||||
maxAutoscaleServiceAccounts: this.sub.maxAutoscaleSmServiceAccounts,
|
maxAutoscaleServiceAccounts: this.sub.maxAutoscaleSmServiceAccounts,
|
||||||
additionalServiceAccounts:
|
additionalServiceAccounts:
|
||||||
this.sub.smServiceAccounts - this.sub.plan.SecretsManager.baseServiceAccount,
|
this.sub.smServiceAccounts - this.sub.plan.SecretsManager.baseServiceAccount,
|
||||||
interval: this.sub.plan.isAnnual ? "year" : "month",
|
interval: this.sub.plan.isAnnual ? "year" : "month",
|
||||||
additionalServiceAccountPrice: this.discountPrice(
|
additionalServiceAccountPrice: this.sub.plan.SecretsManager.additionalPricePerServiceAccount,
|
||||||
this.sub.plan.SecretsManager.additionalPricePerServiceAccount,
|
|
||||||
),
|
|
||||||
baseServiceAccountCount: this.sub.plan.SecretsManager.baseServiceAccount,
|
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 =
|
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)
|
? price * (this.customerDiscount.percentOff / 100)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
@ -2531,6 +2531,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"trialSecretsManagerThankYou": {
|
||||||
|
"message": "Thanks for signing up for Bitwarden Secrets Manager for $PLAN$!",
|
||||||
|
"placeholders": {
|
||||||
|
"plan": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Teams"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"trialPaidInfoMessage": {
|
"trialPaidInfoMessage": {
|
||||||
"message": "Your $PLAN$ 7 day free trial will be converted to a paid subscription after 7 days.",
|
"message": "Your $PLAN$ 7 day free trial will be converted to a paid subscription after 7 days.",
|
||||||
"placeholders": {
|
"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."
|
"message": "Set a seat limit for your subscription. Once this limit is reached, you will not be able to invite new members."
|
||||||
},
|
},
|
||||||
"limitSmSubscriptionDesc": {
|
"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": {
|
"maxSeatLimit": {
|
||||||
"message": "Seat Limit (optional)",
|
"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": {
|
"next": {
|
||||||
"message": "Next"
|
"message": "Next"
|
||||||
},
|
},
|
||||||
@ -7508,5 +7526,17 @@
|
|||||||
},
|
},
|
||||||
"collectionEnhancementsLearnMore": {
|
"collectionEnhancementsLearnMore": {
|
||||||
"message": "Learn more about collection management"
|
"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 "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,47 +2,47 @@
|
|||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"path": ".",
|
"path": "."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "web vault",
|
"name": "web vault",
|
||||||
"path": "apps/web",
|
"path": "apps/web"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "web vault (bit)",
|
"name": "web vault (bit)",
|
||||||
"path": "bitwarden_license/bit-web",
|
"path": "bitwarden_license/bit-web"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "cli",
|
"name": "cli",
|
||||||
"path": "apps/cli",
|
"path": "apps/cli"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "desktop",
|
"name": "desktop",
|
||||||
"path": "apps/desktop",
|
"path": "apps/desktop"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "browser",
|
"name": "browser",
|
||||||
"path": "apps/browser",
|
"path": "apps/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "libs",
|
"name": "libs",
|
||||||
"path": "libs",
|
"path": "libs"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"eslint.options": {
|
"eslint.options": {
|
||||||
"overrideConfig": {
|
"overrideConfig": {
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": ["${workspaceFolder}/tsconfig.eslint.json"],
|
"project": ["${workspaceFolder}/tsconfig.eslint.json"]
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"debug.javascript.terminalOptions": {
|
"debug.javascript.terminalOptions": {
|
||||||
"sourceMapPathOverrides": {
|
"sourceMapPathOverrides": {
|
||||||
"webpack:///./~/*": "${workspaceFolder:root}/node_modules/*",
|
"webpack:///./~/*": "${workspaceFolder:root}/node_modules/*",
|
||||||
"webpack://?:*/*": "${workspaceFolder}/*",
|
"webpack://?:*/*": "${workspaceFolder}/*",
|
||||||
"webpack://@bitwarden/cli/*": "${workspaceFolder}/*",
|
"webpack://@bitwarden/cli/*": "${workspaceFolder}/*"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"jest.disabledWorkspaceFolders": [
|
"jest.disabledWorkspaceFolders": [
|
||||||
"browser",
|
"browser",
|
||||||
@ -56,14 +56,14 @@
|
|||||||
"jest.jestCommandLine": "npx jest",
|
"jest.jestCommandLine": "npx jest",
|
||||||
"angular.enable-strict-mode-prompt": false,
|
"angular.enable-strict-mode-prompt": false,
|
||||||
"typescript.preferences.importModuleSpecifier": "project-relative",
|
"typescript.preferences.importModuleSpecifier": "project-relative",
|
||||||
"javascript.preferences.importModuleSpecifier": "project-relative",
|
"javascript.preferences.importModuleSpecifier": "project-relative"
|
||||||
},
|
},
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"orta.vscode-jest",
|
"orta.vscode-jest",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"Angular.ng-template",
|
"Angular.ng-template"
|
||||||
],
|
]
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 { 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 { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
|
||||||
import { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction";
|
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 { 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 { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||||
@ -864,6 +866,16 @@ import { ModalService } from "./modal.service";
|
|||||||
useClass: BillingBannerService,
|
useClass: BillingBannerService,
|
||||||
deps: [StateProvider],
|
deps: [StateProvider],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: OrganizationBillingServiceAbstraction,
|
||||||
|
useClass: OrganizationBillingService,
|
||||||
|
deps: [
|
||||||
|
CryptoServiceAbstraction,
|
||||||
|
EncryptService,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
OrganizationApiServiceAbstraction,
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class JslibServicesModule {}
|
export class JslibServicesModule {}
|
||||||
|
@ -27,4 +27,5 @@ export class OrganizationCreateRequest {
|
|||||||
useSecretsManager: boolean;
|
useSecretsManager: boolean;
|
||||||
additionalSmSeats: number;
|
additionalSmSeats: number;
|
||||||
additionalServiceAccounts: number;
|
additionalServiceAccounts: number;
|
||||||
|
isFromSecretsManagerTrial: boolean;
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
|
}
|
@ -40,17 +40,13 @@ export class BillingCustomerDiscount extends BaseResponse {
|
|||||||
id: string;
|
id: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
percentOff?: number;
|
percentOff?: number;
|
||||||
|
appliesTo: string[];
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
this.id = this.getResponseProperty("Id");
|
this.id = this.getResponseProperty("Id");
|
||||||
this.active = this.getResponseProperty("Active");
|
this.active = this.getResponseProperty("Active");
|
||||||
this.percentOff = this.getResponseProperty("PercentOff");
|
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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ export class BillingSubscriptionResponse extends BaseResponse {
|
|||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
this.trialEndDate = this.getResponseProperty("TrialStartDate");
|
this.trialStartDate = this.getResponseProperty("TrialStartDate");
|
||||||
this.trialEndDate = this.getResponseProperty("TrialEndDate");
|
this.trialEndDate = this.getResponseProperty("TrialEndDate");
|
||||||
this.periodStartDate = this.getResponseProperty("PeriodStartDate");
|
this.periodStartDate = this.getResponseProperty("PeriodStartDate");
|
||||||
this.periodEndDate = this.getResponseProperty("PeriodEndDate");
|
this.periodEndDate = this.getResponseProperty("PeriodEndDate");
|
||||||
@ -55,6 +55,7 @@ export class BillingSubscriptionResponse extends BaseResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class BillingSubscriptionItemResponse extends BaseResponse {
|
export class BillingSubscriptionItemResponse extends BaseResponse {
|
||||||
|
productId: string;
|
||||||
name: string;
|
name: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
@ -65,6 +66,7 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
|
|||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
|
this.productId = this.getResponseProperty("ProductId");
|
||||||
this.name = this.getResponseProperty("Name");
|
this.name = this.getResponseProperty("Name");
|
||||||
this.amount = this.getResponseProperty("Amount");
|
this.amount = this.getResponseProperty("Amount");
|
||||||
this.quantity = this.getResponseProperty("Quantity");
|
this.quantity = this.getResponseProperty("Quantity");
|
||||||
|
143
libs/common/src/billing/services/organization-billing.service.ts
Normal file
143
libs/common/src/billing/services/organization-billing.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user