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 -->
|
||||
<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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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],
|
||||
|
@ -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>
|
||||
|
@ -9,4 +9,5 @@ import { Component, Input } from "@angular/core";
|
||||
export class VerticalStep extends CdkStep {
|
||||
@Input() subLabel = "";
|
||||
@Input() applyBorder = true;
|
||||
@Input() addSubLabelSpacing = false;
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 "
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -27,4 +27,5 @@ export class OrganizationCreateRequest {
|
||||
useSecretsManager: boolean;
|
||||
additionalSmSeats: 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;
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
@ -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");
|
||||
|
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