1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

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

* Got trial page working without the form set up

* Set up the form to create SM subscription

* Add free SM trial page and sign up

* Conner's changes

* fixed imports

* Set isFromSecretsManagerTrial

* Fixed OrgKey location

* Add isFromSecretsManager prop to free org create

* Add LTO callout

* Switch LTO to background box

* Defect: AC-2081

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

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

---------

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

View File

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

View File

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

View File

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

View File

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

View File

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