1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-24 04:04:24 +00:00
Files
browser/libs/common/src/billing/services/organization-billing.service.ts
Jonas Hendrickx e026799071 [PM-13128] Enable Breadcrumb Policies (#13584)
* [PM-13128] Enable Breadcrumb Policies

* [PM-13128] Enable Breadcrumb Policies

* [PM-13128] wip

* [PM-13128] wip

* [PM-13128] wip

* [PM-13128] wip

* remove dead code

* wip

* wip

* wip

* refactor

* Fix for providers

* revert to functional auth guard

* change prerequisite to info variant

* address comment

* r

* r

* r

* tests

* r

* r

* fix tests

* feedback

* fix tests

* fix tests

* Rename upselling to breadcrumbing

* Address feedback

* Fix build & tests

* Make the guard callback use Observable instead of a promise

* Pm 13128 suggestions (#14041)

* Rename new enum value

* Show the upgrade button when breadcrumbing is enabled

* Show mouse pointer when cursor is hovered above badge

* Do not make the dialogs overlap

* Align badge middle

* Gap

* Badge should be a `button` instead of `span`

* missing button@type

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Alex Morask <amorask@bitwarden.com>
2025-04-18 09:57:27 -04:00

255 lines
9.1 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable, of, switchMap } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { KeyService } from "@bitwarden/key-management";
import { ApiService } from "../../abstractions/api.service";
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 { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { EncString } from "../../platform/models/domain/enc-string";
import { SyncService } from "../../platform/sync";
import { OrgKey } from "../../types/key";
import {
BillingApiServiceAbstraction,
OrganizationBillingServiceAbstraction,
OrganizationInformation,
PaymentInformation,
PlanInformation,
SubscriptionInformation,
} from "../abstractions";
import { PlanType, ProductTierType } from "../enums";
import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request";
import { PaymentSourceResponse } from "../models/response/payment-source.response";
interface OrganizationKeys {
encryptedKey: EncString;
publicKey: string;
encryptedPrivateKey: EncString;
encryptedCollectionName: EncString;
}
export class OrganizationBillingService implements OrganizationBillingServiceAbstraction {
constructor(
private apiService: ApiService,
private billingApiService: BillingApiServiceAbstraction,
private keyService: KeyService,
private encryptService: EncryptService,
private i18nService: I18nService,
private organizationApiService: OrganizationApiService,
private syncService: SyncService,
private configService: ConfigService,
) {}
async getPaymentSource(organizationId: string): Promise<PaymentSourceResponse> {
const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId);
return paymentMethod.paymentSource;
}
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);
const response = await this.organizationApiService.create(request);
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
return response;
}
async purchaseSubscriptionNoPaymentMethod(
subscription: SubscriptionInformation,
): Promise<OrganizationResponse> {
const request = new OrganizationNoPaymentMethodCreateRequest();
const organizationKeys = await this.makeOrganizationKeys();
this.setOrganizationKeys(request, organizationKeys);
this.setOrganizationInformation(request, subscription.organization);
this.setPlanInformation(request, subscription.plan);
const response = await this.organizationApiService.createWithoutPayment(request);
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
return response;
}
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);
const response = await this.organizationApiService.create(request);
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
return response;
}
private async makeOrganizationKeys(): Promise<OrganizationKeys> {
const [encryptedKey, key] = await this.keyService.makeOrgKey<OrgKey>();
const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(key);
const encryptedCollectionName = await this.encryptService.encrypt(
this.i18nService.t("defaultCollection"),
key,
);
return {
encryptedKey,
publicKey,
encryptedPrivateKey,
encryptedCollectionName,
};
}
private prohibitsAdditionalSeats(planType: PlanType) {
switch (planType) {
case PlanType.Free:
case PlanType.FamiliesAnnually:
case PlanType.FamiliesAnnually2019:
case PlanType.TeamsStarter2023:
case PlanType.TeamsStarter:
return true;
default:
return false;
}
}
private setOrganizationInformation(
request: OrganizationCreateRequest | OrganizationNoPaymentMethodCreateRequest,
information: OrganizationInformation,
): void {
request.name = information.name;
request.businessName = information.businessName;
request.billingEmail = information.billingEmail;
request.initiationPath = information.initiationPath;
}
private setOrganizationKeys(
request: OrganizationCreateRequest | OrganizationNoPaymentMethodCreateRequest,
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 | OrganizationNoPaymentMethodCreateRequest,
information: PlanInformation,
): void {
request.planType = information.type;
if (this.prohibitsAdditionalSeats(request.planType)) {
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;
}
}
async restartSubscription(
organizationId: string,
subscription: SubscriptionInformation,
): Promise<void> {
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);
await this.billingApiService.restartSubscription(organizationId, request);
}
isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable<boolean> {
if (organization === null || organization === undefined) {
return of(false);
}
return this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs).pipe(
switchMap((featureFlagEnabled) => {
if (!featureFlagEnabled) {
return of(false);
}
if (organization.isProviderUser || !organization.canEditSubscription) {
return of(false);
}
const supportedProducts = [ProductTierType.Teams, ProductTierType.TeamsStarter];
const isSupportedProduct = supportedProducts.some(
(product) => product === organization.productTierType,
);
return of(isSupportedProduct);
}),
);
}
}