diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index c8f19318599..23d2dc75722 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8297,5 +8297,20 @@ }, "allLoginRequestsApproved": { "message": "All login requests approved" + }, + "payPal": { + "message": "PayPal" + }, + "bitcoin": { + "message": "Bitcoin" + }, + "updatedTaxInformation": { + "message": "Updated tax information" + }, + "unverified": { + "message": "Unverified" + }, + "verified": { + "message": "Verified" } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index d16b0a8aa2a..ffcfcd0ad81 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -33,6 +33,7 @@ *ngIf="canAccessBilling$ | async" > + + + + {{ "loading" | i18n }} + + + + +

+ {{ "accountCredit" | i18n }} +

+

{{ accountCredit | currency: "$" }}

+

{{ "creditAppliedDesc" | i18n }}

+ +
+ + +

{{ "paymentMethod" | i18n }}

+

{{ "noPaymentMethod" | i18n }}

+ + +

+ + {{ paymentMethodDescription }} +

+
+ +
+ + +

{{ "taxInformation" | i18n }}

+

{{ "taxInformationDesc" | i18n }}

+ +
+
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-payment-method.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-payment-method.component.ts new file mode 100644 index 00000000000..42a7dbdec05 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-payment-method.component.ts @@ -0,0 +1,140 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { from, lastValueFrom, Subject, switchMap } from "rxjs"; +import { takeUntil } from "rxjs/operators"; + +import { openAddAccountCreditDialog } from "@bitwarden/angular/billing/components"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { MaskedPaymentMethod, TaxInformation } from "@bitwarden/common/billing/models/domain"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; +import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { + openProviderSelectPaymentMethodDialog, + ProviderSelectPaymentMethodDialogResultType, +} from "./provider-select-payment-method-dialog.component"; + +@Component({ + selector: "app-provider-payment-method", + templateUrl: "./provider-payment-method.component.html", +}) +export class ProviderPaymentMethodComponent implements OnInit, OnDestroy { + protected providerId: string; + protected loading: boolean; + + protected accountCredit: number; + protected maskedPaymentMethod: MaskedPaymentMethod; + protected taxInformation: TaxInformation; + + private destroy$ = new Subject(); + + constructor( + private activatedRoute: ActivatedRoute, + private billingApiService: BillingApiServiceAbstraction, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + ) {} + + addAccountCredit = () => + openAddAccountCreditDialog(this.dialogService, { + data: { + providerId: this.providerId, + }, + }); + + changePaymentMethod = async () => { + const dialogRef = openProviderSelectPaymentMethodDialog(this.dialogService, { + data: { + providerId: this.providerId, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result == ProviderSelectPaymentMethodDialogResultType.Submitted) { + await this.load(); + } + }; + + async load() { + this.loading = true; + const paymentInformation = await this.billingApiService.getProviderPaymentInformation( + this.providerId, + ); + this.accountCredit = paymentInformation.accountCredit; + this.maskedPaymentMethod = MaskedPaymentMethod.from(paymentInformation.paymentMethod); + this.taxInformation = TaxInformation.from(paymentInformation.taxInformation); + this.loading = false; + } + + onDataUpdated = async () => await this.load(); + + updateTaxInformation = async (taxInformation: TaxInformation) => { + const request = ExpandedTaxInfoUpdateRequest.From(taxInformation); + await this.billingApiService.updateProviderTaxInformation(this.providerId, request); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedTaxInformation"), + }); + }; + + verifyBankAccount = async (amount1: number, amount2: number) => { + const request = new VerifyBankAccountRequest(amount1, amount2); + await this.billingApiService.verifyProviderBankAccount(this.providerId, request); + }; + + ngOnInit() { + this.activatedRoute.params + .pipe( + switchMap(({ providerId }) => { + this.providerId = providerId; + return from(this.load()); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + protected get hasPaymentMethod(): boolean { + return !!this.maskedPaymentMethod; + } + + protected get hasUnverifiedPaymentMethod(): boolean { + return !!this.maskedPaymentMethod && this.maskedPaymentMethod.needsVerification; + } + + protected get paymentMethodClass(): string[] { + switch (this.maskedPaymentMethod.type) { + case PaymentMethodType.Card: + return ["bwi-credit-card"]; + case PaymentMethodType.BankAccount: + return ["bwi-bank"]; + case PaymentMethodType.PayPal: + return ["bwi-paypal tw-text-primary"]; + default: + return []; + } + } + + protected get paymentMethodDescription(): string { + let description = this.maskedPaymentMethod.description; + if (this.maskedPaymentMethod.type === PaymentMethodType.BankAccount) { + if (this.hasUnverifiedPaymentMethod) { + description += " - " + this.i18nService.t("unverified"); + } else { + description += " - " + this.i18nService.t("verified"); + } + } + return description; + } +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.html new file mode 100644 index 00000000000..03e8405a48c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.html @@ -0,0 +1,18 @@ +
+ + + {{ "addPaymentMethod" | i18n }} + + + + + + + + + +
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.ts new file mode 100644 index 00000000000..09a293d12d8 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.ts @@ -0,0 +1,60 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, EventEmitter, Inject, Output, ViewChild } from "@angular/core"; +import { FormGroup } from "@angular/forms"; + +import { SelectPaymentMethodComponent } from "@bitwarden/angular/billing/components"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { TokenizedPaymentMethodRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-method.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +type ProviderSelectPaymentMethodDialogParams = { + providerId: string; +}; + +export enum ProviderSelectPaymentMethodDialogResultType { + Closed = "closed", + Submitted = "submitted", +} + +export const openProviderSelectPaymentMethodDialog = ( + dialogService: DialogService, + dialogConfig: DialogConfig, +) => + dialogService.open< + ProviderSelectPaymentMethodDialogResultType, + ProviderSelectPaymentMethodDialogParams + >(ProviderSelectPaymentMethodDialogComponent, dialogConfig); + +@Component({ + templateUrl: "provider-select-payment-method-dialog.component.html", +}) +export class ProviderSelectPaymentMethodDialogComponent { + @ViewChild(SelectPaymentMethodComponent) + selectPaymentMethodComponent: SelectPaymentMethodComponent; + @Output() providerPaymentMethodUpdated = new EventEmitter(); + + protected readonly formGroup = new FormGroup({}); + protected readonly ResultType = ProviderSelectPaymentMethodDialogResultType; + + constructor( + private billingApiService: BillingApiServiceAbstraction, + @Inject(DIALOG_DATA) private dialogParams: ProviderSelectPaymentMethodDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, + private toastService: ToastService, + ) {} + + submit = async () => { + const tokenizedPaymentMethod = await this.selectPaymentMethodComponent.tokenizePaymentMethod(); + const request = TokenizedPaymentMethodRequest.From(tokenizedPaymentMethod); + await this.billingApiService.updateProviderPaymentMethod(this.dialogParams.providerId, request); + this.providerPaymentMethodUpdated.emit(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedPaymentMethod"), + }); + this.dialogRef.close(this.ResultType.Submitted); + }; +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/provider-subscription.component.html rename to bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html diff --git a/bitwarden_license/bit-web/src/app/billing/providers/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/provider-subscription.component.ts rename to bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html new file mode 100644 index 00000000000..c9c0c296ada --- /dev/null +++ b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html @@ -0,0 +1,55 @@ +
+ + +

{{ "creditDelayed" | i18n }}

+
+ + + {{ "payPal" | i18n }} + + + {{ "bitcoin" | i18n }} + + +
+
+ + {{ "amount" | i18n }} + + $USD + +
+
+ + + + +
+
+
+ + + + + + + + + + + + + + + +
diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts new file mode 100644 index 00000000000..d3c262c4b7d --- /dev/null +++ b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts @@ -0,0 +1,153 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; + +export type AddAccountCreditDialogParams = { + organizationId?: string; + providerId?: string; +}; + +export enum AddAccountCreditDialogResultType { + Closed = "closed", + Submitted = "submitted", +} + +export const openAddAccountCreditDialog = ( + dialogService: DialogService, + dialogConfig: DialogConfig, +) => + dialogService.open( + AddAccountCreditDialogComponent, + dialogConfig, + ); + +type PayPalConfig = { + businessId?: string; + buttonAction?: string; + returnUrl?: string; + customField?: string; + subject?: string; +}; + +@Component({ + templateUrl: "./add-account-credit-dialog.component.html", +}) +export class AddAccountCreditDialogComponent implements OnInit { + @ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm: ElementRef; + protected formGroup = new FormGroup({ + paymentMethod: new FormControl(PaymentMethodType.PayPal), + creditAmount: new FormControl(null, [Validators.required, Validators.min(0.01)]), + }); + protected payPalConfig: PayPalConfig; + protected ResultType = AddAccountCreditDialogResultType; + + private organization?: Organization; + private provider?: Provider; + private user?: { id: UserId } & AccountInfo; + + constructor( + private accountService: AccountService, + private apiService: ApiService, + private configService: ConfigService, + @Inject(DIALOG_DATA) private dialogParams: AddAccountCreditDialogParams, + private dialogRef: DialogRef, + private organizationService: OrganizationService, + private platformUtilsService: PlatformUtilsService, + private providerService: ProviderService, + ) { + this.payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig; + } + + protected readonly paymentMethodType = PaymentMethodType; + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + if (this.formGroup.value.paymentMethod === PaymentMethodType.PayPal) { + this.payPalForm.nativeElement.submit(); + return; + } + + if (this.formGroup.value.paymentMethod === PaymentMethodType.BitPay) { + const request = this.getBitPayInvoiceRequest(); + const bitPayUrl = await this.apiService.postBitPayInvoice(request); + this.platformUtilsService.launchUri(bitPayUrl); + return; + } + + this.dialogRef.close(AddAccountCreditDialogResultType.Submitted); + }; + + async ngOnInit(): Promise { + let payPalCustomField: string; + + if (this.dialogParams.organizationId) { + this.formGroup.patchValue({ + creditAmount: 20.0, + }); + this.organization = await this.organizationService.get(this.dialogParams.organizationId); + payPalCustomField = "organization_id:" + this.organization.id; + this.payPalConfig.subject = this.organization.name; + } else if (this.dialogParams.providerId) { + this.formGroup.patchValue({ + creditAmount: 20.0, + }); + this.provider = await this.providerService.get(this.dialogParams.providerId); + payPalCustomField = "provider_id:" + this.provider.id; + this.payPalConfig.subject = this.provider.name; + } else { + this.formGroup.patchValue({ + creditAmount: 10.0, + }); + this.user = await firstValueFrom(this.accountService.activeAccount$); + payPalCustomField = "user_id:" + this.user.id; + this.payPalConfig.subject = this.user.email; + } + + const region = await firstValueFrom(this.configService.cloudRegion$); + + payPalCustomField += ",account_credit:1"; + payPalCustomField += `,region:${region}`; + + this.payPalConfig.customField = payPalCustomField; + this.payPalConfig.returnUrl = window.location.href; + } + + getBitPayInvoiceRequest(): BitPayInvoiceRequest { + const request = new BitPayInvoiceRequest(); + if (this.organization) { + request.name = this.organization.name; + request.organizationId = this.organization.id; + } else if (this.provider) { + request.name = this.provider.name; + request.providerId = this.provider.id; + } else { + request.email = this.user.email; + request.userId = this.user.id; + } + + request.credit = true; + request.amount = this.formGroup.value.creditAmount; + request.returnUrl = window.location.href; + + return request; + } +} diff --git a/libs/angular/src/billing/components/index.ts b/libs/angular/src/billing/components/index.ts new file mode 100644 index 00000000000..748a005df83 --- /dev/null +++ b/libs/angular/src/billing/components/index.ts @@ -0,0 +1,4 @@ +export * from "./add-account-credit-dialog/add-account-credit-dialog.component"; +export * from "./manage-tax-information/manage-tax-information.component"; +export * from "./select-payment-method/select-payment-method.component"; +export * from "./verify-bank-account/verify-bank-account.component"; diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html new file mode 100644 index 00000000000..f9cfa8e0faf --- /dev/null +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html @@ -0,0 +1,72 @@ +
+
+
+ + {{ "country" | i18n }} + + + + +
+
+ + {{ "zipPostalCode" | i18n }} + + +
+
+ + + {{ "includeVAT" | i18n }} + +
+
+
+
+ + {{ "taxIdNumber" | i18n }} + + +
+
+
+
+ + {{ "address1" | i18n }} + + +
+
+ + {{ "address2" | i18n }} + + +
+
+ + {{ "cityTown" | i18n }} + + +
+
+ + {{ "stateProvince" | i18n }} + + +
+
+ +
diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts new file mode 100644 index 00000000000..58342548ca3 --- /dev/null +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts @@ -0,0 +1,406 @@ +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; + +import { TaxInformation } from "@bitwarden/common/billing/models/domain"; + +type Country = { + name: string; + value: string; + disabled: boolean; +}; + +@Component({ + selector: "app-manage-tax-information", + templateUrl: "./manage-tax-information.component.html", +}) +export class ManageTaxInformationComponent implements OnInit { + @Input({ required: true }) taxInformation: TaxInformation; + @Input() onSubmit?: (taxInformation: TaxInformation) => Promise; + @Output() taxInformationUpdated = new EventEmitter(); + + protected formGroup = this.formBuilder.group({ + country: ["", Validators.required], + postalCode: ["", Validators.required], + includeTaxId: false, + taxId: "", + line1: "", + line2: "", + city: "", + state: "", + }); + + constructor(private formBuilder: FormBuilder) {} + + submit = async () => { + await this.onSubmit({ + country: this.formGroup.value.country, + postalCode: this.formGroup.value.postalCode, + taxId: this.formGroup.value.taxId, + line1: this.formGroup.value.line1, + line2: this.formGroup.value.line2, + city: this.formGroup.value.city, + state: this.formGroup.value.state, + }); + + this.taxInformationUpdated.emit(); + }; + + async ngOnInit() { + if (this.taxInformation) { + this.formGroup.patchValue({ + ...this.taxInformation, + includeTaxId: + this.countrySupportsTax(this.taxInformation.country) && + (!!this.taxInformation.taxId || + !!this.taxInformation.line1 || + !!this.taxInformation.line2 || + !!this.taxInformation.city || + !!this.taxInformation.state), + }); + } + } + + protected countrySupportsTax(countryCode: string) { + return this.taxSupportedCountryCodes.includes(countryCode); + } + + protected get includeTaxIdIsSelected() { + return this.formGroup.value.includeTaxId; + } + + protected get selectionSupportsAdditionalOptions() { + return ( + this.formGroup.value.country !== "US" && this.countrySupportsTax(this.formGroup.value.country) + ); + } + + protected countries: Country[] = [ + { name: "-- Select --", value: "", disabled: false }, + { name: "United States", value: "US", disabled: false }, + { name: "China", value: "CN", disabled: false }, + { name: "France", value: "FR", disabled: false }, + { name: "Germany", value: "DE", disabled: false }, + { name: "Canada", value: "CA", disabled: false }, + { name: "United Kingdom", value: "GB", disabled: false }, + { name: "Australia", value: "AU", disabled: false }, + { name: "India", value: "IN", disabled: false }, + { name: "", value: "-", disabled: true }, + { name: "Afghanistan", value: "AF", disabled: false }, + { name: "Åland Islands", value: "AX", disabled: false }, + { name: "Albania", value: "AL", disabled: false }, + { name: "Algeria", value: "DZ", disabled: false }, + { name: "American Samoa", value: "AS", disabled: false }, + { name: "Andorra", value: "AD", disabled: false }, + { name: "Angola", value: "AO", disabled: false }, + { name: "Anguilla", value: "AI", disabled: false }, + { name: "Antarctica", value: "AQ", disabled: false }, + { name: "Antigua and Barbuda", value: "AG", disabled: false }, + { name: "Argentina", value: "AR", disabled: false }, + { name: "Armenia", value: "AM", disabled: false }, + { name: "Aruba", value: "AW", disabled: false }, + { name: "Austria", value: "AT", disabled: false }, + { name: "Azerbaijan", value: "AZ", disabled: false }, + { name: "Bahamas", value: "BS", disabled: false }, + { name: "Bahrain", value: "BH", disabled: false }, + { name: "Bangladesh", value: "BD", disabled: false }, + { name: "Barbados", value: "BB", disabled: false }, + { name: "Belarus", value: "BY", disabled: false }, + { name: "Belgium", value: "BE", disabled: false }, + { name: "Belize", value: "BZ", disabled: false }, + { name: "Benin", value: "BJ", disabled: false }, + { name: "Bermuda", value: "BM", disabled: false }, + { name: "Bhutan", value: "BT", disabled: false }, + { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, + { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, + { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, + { name: "Botswana", value: "BW", disabled: false }, + { name: "Bouvet Island", value: "BV", disabled: false }, + { name: "Brazil", value: "BR", disabled: false }, + { name: "British Indian Ocean Territory", value: "IO", disabled: false }, + { name: "Brunei Darussalam", value: "BN", disabled: false }, + { name: "Bulgaria", value: "BG", disabled: false }, + { name: "Burkina Faso", value: "BF", disabled: false }, + { name: "Burundi", value: "BI", disabled: false }, + { name: "Cambodia", value: "KH", disabled: false }, + { name: "Cameroon", value: "CM", disabled: false }, + { name: "Cape Verde", value: "CV", disabled: false }, + { name: "Cayman Islands", value: "KY", disabled: false }, + { name: "Central African Republic", value: "CF", disabled: false }, + { name: "Chad", value: "TD", disabled: false }, + { name: "Chile", value: "CL", disabled: false }, + { name: "Christmas Island", value: "CX", disabled: false }, + { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, + { name: "Colombia", value: "CO", disabled: false }, + { name: "Comoros", value: "KM", disabled: false }, + { name: "Congo", value: "CG", disabled: false }, + { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, + { name: "Cook Islands", value: "CK", disabled: false }, + { name: "Costa Rica", value: "CR", disabled: false }, + { name: "Côte d'Ivoire", value: "CI", disabled: false }, + { name: "Croatia", value: "HR", disabled: false }, + { name: "Cuba", value: "CU", disabled: false }, + { name: "Curaçao", value: "CW", disabled: false }, + { name: "Cyprus", value: "CY", disabled: false }, + { name: "Czech Republic", value: "CZ", disabled: false }, + { name: "Denmark", value: "DK", disabled: false }, + { name: "Djibouti", value: "DJ", disabled: false }, + { name: "Dominica", value: "DM", disabled: false }, + { name: "Dominican Republic", value: "DO", disabled: false }, + { name: "Ecuador", value: "EC", disabled: false }, + { name: "Egypt", value: "EG", disabled: false }, + { name: "El Salvador", value: "SV", disabled: false }, + { name: "Equatorial Guinea", value: "GQ", disabled: false }, + { name: "Eritrea", value: "ER", disabled: false }, + { name: "Estonia", value: "EE", disabled: false }, + { name: "Ethiopia", value: "ET", disabled: false }, + { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, + { name: "Faroe Islands", value: "FO", disabled: false }, + { name: "Fiji", value: "FJ", disabled: false }, + { name: "Finland", value: "FI", disabled: false }, + { name: "French Guiana", value: "GF", disabled: false }, + { name: "French Polynesia", value: "PF", disabled: false }, + { name: "French Southern Territories", value: "TF", disabled: false }, + { name: "Gabon", value: "GA", disabled: false }, + { name: "Gambia", value: "GM", disabled: false }, + { name: "Georgia", value: "GE", disabled: false }, + { name: "Ghana", value: "GH", disabled: false }, + { name: "Gibraltar", value: "GI", disabled: false }, + { name: "Greece", value: "GR", disabled: false }, + { name: "Greenland", value: "GL", disabled: false }, + { name: "Grenada", value: "GD", disabled: false }, + { name: "Guadeloupe", value: "GP", disabled: false }, + { name: "Guam", value: "GU", disabled: false }, + { name: "Guatemala", value: "GT", disabled: false }, + { name: "Guernsey", value: "GG", disabled: false }, + { name: "Guinea", value: "GN", disabled: false }, + { name: "Guinea-Bissau", value: "GW", disabled: false }, + { name: "Guyana", value: "GY", disabled: false }, + { name: "Haiti", value: "HT", disabled: false }, + { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, + { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, + { name: "Honduras", value: "HN", disabled: false }, + { name: "Hong Kong", value: "HK", disabled: false }, + { name: "Hungary", value: "HU", disabled: false }, + { name: "Iceland", value: "IS", disabled: false }, + { name: "Indonesia", value: "ID", disabled: false }, + { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, + { name: "Iraq", value: "IQ", disabled: false }, + { name: "Ireland", value: "IE", disabled: false }, + { name: "Isle of Man", value: "IM", disabled: false }, + { name: "Israel", value: "IL", disabled: false }, + { name: "Italy", value: "IT", disabled: false }, + { name: "Jamaica", value: "JM", disabled: false }, + { name: "Japan", value: "JP", disabled: false }, + { name: "Jersey", value: "JE", disabled: false }, + { name: "Jordan", value: "JO", disabled: false }, + { name: "Kazakhstan", value: "KZ", disabled: false }, + { name: "Kenya", value: "KE", disabled: false }, + { name: "Kiribati", value: "KI", disabled: false }, + { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, + { name: "Korea, Republic of", value: "KR", disabled: false }, + { name: "Kuwait", value: "KW", disabled: false }, + { name: "Kyrgyzstan", value: "KG", disabled: false }, + { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, + { name: "Latvia", value: "LV", disabled: false }, + { name: "Lebanon", value: "LB", disabled: false }, + { name: "Lesotho", value: "LS", disabled: false }, + { name: "Liberia", value: "LR", disabled: false }, + { name: "Libya", value: "LY", disabled: false }, + { name: "Liechtenstein", value: "LI", disabled: false }, + { name: "Lithuania", value: "LT", disabled: false }, + { name: "Luxembourg", value: "LU", disabled: false }, + { name: "Macao", value: "MO", disabled: false }, + { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, + { name: "Madagascar", value: "MG", disabled: false }, + { name: "Malawi", value: "MW", disabled: false }, + { name: "Malaysia", value: "MY", disabled: false }, + { name: "Maldives", value: "MV", disabled: false }, + { name: "Mali", value: "ML", disabled: false }, + { name: "Malta", value: "MT", disabled: false }, + { name: "Marshall Islands", value: "MH", disabled: false }, + { name: "Martinique", value: "MQ", disabled: false }, + { name: "Mauritania", value: "MR", disabled: false }, + { name: "Mauritius", value: "MU", disabled: false }, + { name: "Mayotte", value: "YT", disabled: false }, + { name: "Mexico", value: "MX", disabled: false }, + { name: "Micronesia, Federated States of", value: "FM", disabled: false }, + { name: "Moldova, Republic of", value: "MD", disabled: false }, + { name: "Monaco", value: "MC", disabled: false }, + { name: "Mongolia", value: "MN", disabled: false }, + { name: "Montenegro", value: "ME", disabled: false }, + { name: "Montserrat", value: "MS", disabled: false }, + { name: "Morocco", value: "MA", disabled: false }, + { name: "Mozambique", value: "MZ", disabled: false }, + { name: "Myanmar", value: "MM", disabled: false }, + { name: "Namibia", value: "NA", disabled: false }, + { name: "Nauru", value: "NR", disabled: false }, + { name: "Nepal", value: "NP", disabled: false }, + { name: "Netherlands", value: "NL", disabled: false }, + { name: "New Caledonia", value: "NC", disabled: false }, + { name: "New Zealand", value: "NZ", disabled: false }, + { name: "Nicaragua", value: "NI", disabled: false }, + { name: "Niger", value: "NE", disabled: false }, + { name: "Nigeria", value: "NG", disabled: false }, + { name: "Niue", value: "NU", disabled: false }, + { name: "Norfolk Island", value: "NF", disabled: false }, + { name: "Northern Mariana Islands", value: "MP", disabled: false }, + { name: "Norway", value: "NO", disabled: false }, + { name: "Oman", value: "OM", disabled: false }, + { name: "Pakistan", value: "PK", disabled: false }, + { name: "Palau", value: "PW", disabled: false }, + { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, + { name: "Panama", value: "PA", disabled: false }, + { name: "Papua New Guinea", value: "PG", disabled: false }, + { name: "Paraguay", value: "PY", disabled: false }, + { name: "Peru", value: "PE", disabled: false }, + { name: "Philippines", value: "PH", disabled: false }, + { name: "Pitcairn", value: "PN", disabled: false }, + { name: "Poland", value: "PL", disabled: false }, + { name: "Portugal", value: "PT", disabled: false }, + { name: "Puerto Rico", value: "PR", disabled: false }, + { name: "Qatar", value: "QA", disabled: false }, + { name: "Réunion", value: "RE", disabled: false }, + { name: "Romania", value: "RO", disabled: false }, + { name: "Russian Federation", value: "RU", disabled: false }, + { name: "Rwanda", value: "RW", disabled: false }, + { name: "Saint Barthélemy", value: "BL", disabled: false }, + { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, + { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, + { name: "Saint Lucia", value: "LC", disabled: false }, + { name: "Saint Martin (French part)", value: "MF", disabled: false }, + { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, + { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, + { name: "Samoa", value: "WS", disabled: false }, + { name: "San Marino", value: "SM", disabled: false }, + { name: "Sao Tome and Principe", value: "ST", disabled: false }, + { name: "Saudi Arabia", value: "SA", disabled: false }, + { name: "Senegal", value: "SN", disabled: false }, + { name: "Serbia", value: "RS", disabled: false }, + { name: "Seychelles", value: "SC", disabled: false }, + { name: "Sierra Leone", value: "SL", disabled: false }, + { name: "Singapore", value: "SG", disabled: false }, + { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, + { name: "Slovakia", value: "SK", disabled: false }, + { name: "Slovenia", value: "SI", disabled: false }, + { name: "Solomon Islands", value: "SB", disabled: false }, + { name: "Somalia", value: "SO", disabled: false }, + { name: "South Africa", value: "ZA", disabled: false }, + { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, + { name: "South Sudan", value: "SS", disabled: false }, + { name: "Spain", value: "ES", disabled: false }, + { name: "Sri Lanka", value: "LK", disabled: false }, + { name: "Sudan", value: "SD", disabled: false }, + { name: "Suriname", value: "SR", disabled: false }, + { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, + { name: "Swaziland", value: "SZ", disabled: false }, + { name: "Sweden", value: "SE", disabled: false }, + { name: "Switzerland", value: "CH", disabled: false }, + { name: "Syrian Arab Republic", value: "SY", disabled: false }, + { name: "Taiwan", value: "TW", disabled: false }, + { name: "Tajikistan", value: "TJ", disabled: false }, + { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, + { name: "Thailand", value: "TH", disabled: false }, + { name: "Timor-Leste", value: "TL", disabled: false }, + { name: "Togo", value: "TG", disabled: false }, + { name: "Tokelau", value: "TK", disabled: false }, + { name: "Tonga", value: "TO", disabled: false }, + { name: "Trinidad and Tobago", value: "TT", disabled: false }, + { name: "Tunisia", value: "TN", disabled: false }, + { name: "Turkey", value: "TR", disabled: false }, + { name: "Turkmenistan", value: "TM", disabled: false }, + { name: "Turks and Caicos Islands", value: "TC", disabled: false }, + { name: "Tuvalu", value: "TV", disabled: false }, + { name: "Uganda", value: "UG", disabled: false }, + { name: "Ukraine", value: "UA", disabled: false }, + { name: "United Arab Emirates", value: "AE", disabled: false }, + { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, + { name: "Uruguay", value: "UY", disabled: false }, + { name: "Uzbekistan", value: "UZ", disabled: false }, + { name: "Vanuatu", value: "VU", disabled: false }, + { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, + { name: "Viet Nam", value: "VN", disabled: false }, + { name: "Virgin Islands, British", value: "VG", disabled: false }, + { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, + { name: "Wallis and Futuna", value: "WF", disabled: false }, + { name: "Western Sahara", value: "EH", disabled: false }, + { name: "Yemen", value: "YE", disabled: false }, + { name: "Zambia", value: "ZM", disabled: false }, + { name: "Zimbabwe", value: "ZW", disabled: false }, + ]; + + private taxSupportedCountryCodes: string[] = [ + "CN", + "FR", + "DE", + "CA", + "GB", + "AU", + "IN", + "AD", + "AR", + "AT", + "BE", + "BO", + "BR", + "BG", + "CL", + "CO", + "CR", + "HR", + "CY", + "CZ", + "DK", + "DO", + "EC", + "EG", + "SV", + "EE", + "FI", + "GE", + "GR", + "HK", + "HU", + "IS", + "ID", + "IQ", + "IE", + "IL", + "IT", + "JP", + "KE", + "KR", + "LV", + "LI", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NO", + "PE", + "PH", + "PL", + "PT", + "RO", + "RU", + "SA", + "RS", + "SG", + "SK", + "SI", + "ZA", + "ES", + "SE", + "CH", + "TW", + "TH", + "TR", + "UA", + "AE", + "UY", + "VE", + "VN", + ]; +} diff --git a/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html new file mode 100644 index 00000000000..7add3f6d35d --- /dev/null +++ b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html @@ -0,0 +1,151 @@ +
+
+ + + + + {{ "creditCard" | i18n }} + + + + + + {{ "bankAccount" | i18n }} + + + + + + {{ "payPal" | i18n }} + + + + + + {{ "accountCredit" | i18n }} + + + +
+ + +
+
+ +
+
+
+ Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay +
+
+ +
+
+
+
+ + + + +
+
+
+
+
+ + + + {{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }} + +
+ + {{ "routingNumber" | i18n }} + + + + {{ "accountNumber" | i18n }} + + + + {{ "accountHolderName" | i18n }} + + + + {{ "bankAccountType" | i18n }} + + + + + + +
+
+ + +
+
+ {{ "paypalClickSubmit" | i18n }} +
+
+ + + + {{ "makeSureEnoughCredit" | i18n }} + + + +
diff --git a/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.ts b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.ts new file mode 100644 index 00000000000..4dc39334a70 --- /dev/null +++ b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.ts @@ -0,0 +1,159 @@ +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; + +import { + BillingApiServiceAbstraction, + BraintreeServiceAbstraction, + StripeServiceAbstraction, +} from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { TokenizedPaymentMethod } from "@bitwarden/common/billing/models/domain"; + +@Component({ + selector: "app-select-payment-method", + templateUrl: "./select-payment-method.component.html", +}) +export class SelectPaymentMethodComponent implements OnInit, OnDestroy { + @Input() protected showAccountCredit: boolean = true; + @Input() protected showBankAccount: boolean = true; + @Input() protected showPayPal: boolean = true; + @Input() private startWith: PaymentMethodType = PaymentMethodType.Card; + @Input() protected onSubmit: (tokenizedPaymentMethod: TokenizedPaymentMethod) => Promise; + + private destroy$ = new Subject(); + + protected formGroup = this.formBuilder.group({ + paymentMethod: [this.startWith], + bankInformation: this.formBuilder.group({ + routingNumber: ["", [Validators.required]], + accountNumber: ["", [Validators.required]], + accountHolderName: ["", [Validators.required]], + accountHolderType: ["", [Validators.required]], + }), + }); + protected PaymentMethodType = PaymentMethodType; + + constructor( + private billingApiService: BillingApiServiceAbstraction, + private braintreeService: BraintreeServiceAbstraction, + private formBuilder: FormBuilder, + private stripeService: StripeServiceAbstraction, + ) {} + + async tokenizePaymentMethod(): Promise { + const type = this.selected; + + if (this.usingStripe) { + const clientSecret = await this.billingApiService.createSetupIntent(type); + + if (this.usingBankAccount) { + const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { + accountHolderName: this.formGroup.value.bankInformation.accountHolderName, + routingNumber: this.formGroup.value.bankInformation.routingNumber, + accountNumber: this.formGroup.value.bankInformation.accountNumber, + accountHolderType: this.formGroup.value.bankInformation.accountHolderType, + }); + return { + type, + token, + }; + } + + if (this.usingCard) { + const token = await this.stripeService.setupCardPaymentMethod(clientSecret); + return { + type, + token, + }; + } + } + + if (this.usingPayPal) { + const token = await this.braintreeService.requestPaymentMethod(); + return { + type, + token, + }; + } + + return null; + } + + submit = async () => { + const tokenizedPaymentMethod = await this.tokenizePaymentMethod(); + await this.onSubmit(tokenizedPaymentMethod); + }; + + ngOnInit(): void { + this.stripeService.loadStripe( + { + cardNumber: "#stripe-card-number", + cardExpiry: "#stripe-card-expiry", + cardCvc: "#stripe-card-cvc", + }, + this.startWith === PaymentMethodType.Card, + ); + + if (this.showPayPal) { + this.braintreeService.loadBraintree( + "#braintree-container", + this.startWith === PaymentMethodType.PayPal, + ); + } + + this.formGroup + .get("paymentMethod") + .valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe((type) => { + this.onPaymentMethodChange(type); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.stripeService.unloadStripe(); + if (this.showPayPal) { + this.braintreeService.unloadBraintree(); + } + } + + private onPaymentMethodChange(type: PaymentMethodType): void { + switch (type) { + case PaymentMethodType.Card: { + this.stripeService.mountElements(); + break; + } + case PaymentMethodType.PayPal: { + this.braintreeService.createDropin(); + break; + } + } + } + + private get selected(): PaymentMethodType { + return this.formGroup.value.paymentMethod; + } + + protected get usingAccountCredit(): boolean { + return this.selected === PaymentMethodType.Credit; + } + + protected get usingBankAccount(): boolean { + return this.selected === PaymentMethodType.BankAccount; + } + + protected get usingCard(): boolean { + return this.selected === PaymentMethodType.Card; + } + + protected get usingPayPal(): boolean { + return this.selected === PaymentMethodType.PayPal; + } + + private get usingStripe(): boolean { + return this.usingBankAccount || this.usingCard; + } +} diff --git a/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.html b/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.html new file mode 100644 index 00000000000..f338f5b0817 --- /dev/null +++ b/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.html @@ -0,0 +1,18 @@ + +

{{ "verifyBankAccountDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}

+
+ + {{ "amountX" | i18n: "1" }} + + $0. + + + {{ "amountX" | i18n: "2" }} + + $0. + + +
+
diff --git a/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.ts b/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.ts new file mode 100644 index 00000000000..c8abb65d819 --- /dev/null +++ b/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.ts @@ -0,0 +1,33 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { FormBuilder, FormControl, Validators } from "@angular/forms"; + +@Component({ + selector: "app-verify-bank-account", + templateUrl: "./verify-bank-account.component.html", +}) +export class VerifyBankAccountComponent { + @Input() onSubmit?: (amount1: number, amount2: number) => Promise; + @Output() verificationSubmitted = new EventEmitter(); + + protected formGroup = this.formBuilder.group({ + amount1: new FormControl(null, [ + Validators.required, + Validators.min(0), + Validators.max(99), + ]), + amount2: new FormControl(null, [ + Validators.required, + Validators.min(0), + Validators.max(99), + ]), + }); + + constructor(private formBuilder: FormBuilder) {} + + submit = async () => { + if (this.onSubmit) { + await this.onSubmit(this.formGroup.value.amount1, this.formGroup.value.amount2); + } + this.verificationSubmitted.emit(); + }; +} diff --git a/libs/angular/src/billing/images/cards.png b/libs/angular/src/billing/images/cards.png new file mode 100644 index 00000000000..bd43abe54c5 Binary files /dev/null and b/libs/angular/src/billing/images/cards.png differ diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 5f1bf796aa9..ccb7446d863 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -2,7 +2,24 @@ import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { AutofocusDirective, ToastModule } from "@bitwarden/components"; +import { + AddAccountCreditDialogComponent, + ManageTaxInformationComponent, + SelectPaymentMethodComponent, + VerifyBankAccountComponent, +} from "@bitwarden/angular/billing/components"; +import { + AsyncActionsModule, + AutofocusDirective, + ButtonModule, + CheckboxModule, + DialogModule, + FormFieldModule, + RadioButtonModule, + SelectModule, + ToastModule, + TypographyModule, +} from "@bitwarden/components"; import { CalloutComponent } from "./components/callout.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; @@ -41,6 +58,14 @@ import { IconComponent } from "./vault/components/icon.component"; CommonModule, FormsModule, ReactiveFormsModule, + AsyncActionsModule, + RadioButtonModule, + FormFieldModule, + SelectModule, + ButtonModule, + CheckboxModule, + DialogModule, + TypographyModule, ], declarations: [ A11yInvalidDirective, @@ -70,6 +95,10 @@ import { IconComponent } from "./vault/components/icon.component"; UserTypePipe, IfFeatureDirective, FingerprintPipe, + AddAccountCreditDialogComponent, + ManageTaxInformationComponent, + SelectPaymentMethodComponent, + VerifyBankAccountComponent, ], exports: [ A11yInvalidDirective, @@ -100,6 +129,10 @@ import { IconComponent } from "./vault/components/icon.component"; UserTypePipe, IfFeatureDirective, FingerprintPipe, + AddAccountCreditDialogComponent, + ManageTaxInformationComponent, + SelectPaymentMethodComponent, + VerifyBankAccountComponent, ], providers: [ CreditCardNumberPipe, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 60f83934af7..5a5b83522d7 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -109,14 +109,20 @@ import { DomainSettingsService, DefaultDomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { + BillingApiServiceAbstraction, + BraintreeServiceAbstraction, + OrganizationBillingServiceAbstraction, + PaymentMethodWarningsServiceAbstraction, + StripeServiceAbstraction, +} from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; -import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service"; +import { BraintreeService } from "@bitwarden/common/billing/services/payment-processors/braintree.service"; +import { StripeService } from "@bitwarden/common/billing/services/payment-processors/stripe.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; @@ -1190,6 +1196,16 @@ const safeProviders: SafeProvider[] = [ useClass: KdfConfigService, deps: [StateProvider], }), + safeProvider({ + provide: BraintreeServiceAbstraction, + useClass: BraintreeService, + deps: [LogService], + }), + safeProvider({ + provide: StripeServiceAbstraction, + useClass: StripeService, + deps: [LogService], + }), ]; function encryptServiceFactory( diff --git a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts index 063b3c370b0..117b318768e 100644 --- a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts @@ -1,3 +1,9 @@ +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; +import { TokenizedPaymentMethodRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-method.request"; +import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; +import { PaymentInformationResponse } from "@bitwarden/common/billing/models/response/payment-information.response"; + import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; @@ -13,23 +19,50 @@ export abstract class BillingApiServiceAbstraction { organizationId: string, request: SubscriptionCancellationRequest, ) => Promise; + cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise; + createClientOrganization: ( providerId: string, request: CreateClientOrganizationRequest, ) => Promise; + + createSetupIntent: (paymentMethodType: PaymentMethodType) => Promise; + getBillingStatus: (id: string) => Promise; + getOrganizationBillingMetadata: ( organizationId: string, ) => Promise; + getOrganizationSubscription: ( organizationId: string, ) => Promise; + getPlans: () => Promise>; + + getProviderPaymentInformation: (providerId: string) => Promise; + getProviderSubscription: (providerId: string) => Promise; + updateClientOrganization: ( providerId: string, organizationId: string, request: UpdateClientOrganizationRequest, ) => Promise; + + updateProviderPaymentMethod: ( + providerId: string, + request: TokenizedPaymentMethodRequest, + ) => Promise; + + updateProviderTaxInformation: ( + providerId: string, + request: ExpandedTaxInfoUpdateRequest, + ) => Promise; + + verifyProviderBankAccount: ( + providerId: string, + request: VerifyBankAccountRequest, + ) => Promise; } diff --git a/libs/common/src/billing/abstractions/index.ts b/libs/common/src/billing/abstractions/index.ts new file mode 100644 index 00000000000..08a7a28fd9c --- /dev/null +++ b/libs/common/src/billing/abstractions/index.ts @@ -0,0 +1,7 @@ +export * from "./account/billing-account-profile-state.service"; +export * from "./billilng-api.service.abstraction"; +export * from "./organization-billing.service"; +export * from "./payment-method-warnings-service.abstraction"; +export * from "./payment-processors/braintree.service.abstraction"; +export * from "./payment-processors/stripe.service.abstraction"; +export * from "./provider-billing.service.abstraction"; diff --git a/libs/common/src/billing/abstractions/payment-processors/braintree.service.abstraction.ts b/libs/common/src/billing/abstractions/payment-processors/braintree.service.abstraction.ts new file mode 100644 index 00000000000..9391ab25f54 --- /dev/null +++ b/libs/common/src/billing/abstractions/payment-processors/braintree.service.abstraction.ts @@ -0,0 +1,28 @@ +export abstract class BraintreeServiceAbstraction { + /** + * Utilizes the Braintree SDK to create a [Braintree drop-in]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html} instance attached to the container ID specified as part of the {@link loadBraintree} method. + */ + createDropin: () => void; + + /** + * Loads the Bitwarden dropin.js script in the element of the current page. + * This script attaches the Braintree SDK to the window. + * @param containerId - The ID of the HTML element where the Braintree drop-in will be loaded at. + * @param autoCreateDropin - Specifies whether the Braintree drop-in should be created when dropin.js loads. + */ + loadBraintree: (containerId: string, autoCreateDropin: boolean) => void; + + /** + * Invokes the Braintree [requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} method + * in order to generate a payment method token using the active Braintree drop-in. + */ + requestPaymentMethod: () => Promise; + + /** + * Removes the following elements from the of the current page: + * - The Bitwarden dropin.js script + * - Any