// FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Location } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, lastValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { getOrganizationById, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BillingPaymentResponse } from "@bitwarden/common/billing/models/response/billing-payment.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; import { TrialFlowService } from "../services/trial-flow.service"; import { FreeTrial } from "../types/free-trial"; import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; import { AdjustPaymentDialogComponent, AdjustPaymentDialogResultType, } from "./adjust-payment-dialog/adjust-payment-dialog.component"; @Component({ templateUrl: "payment-method.component.html", standalone: false, }) export class PaymentMethodComponent implements OnInit, OnDestroy { loading = false; firstLoaded = false; billing: BillingPaymentResponse; org: OrganizationSubscriptionResponse; sub: SubscriptionResponse; paymentMethodType = PaymentMethodType; organizationId: string; isUnpaid = false; organization: Organization; verifyBankForm = this.formBuilder.group({ amount1: new FormControl(null, [ Validators.required, Validators.max(99), Validators.min(0), ]), amount2: new FormControl(null, [ Validators.required, Validators.max(99), Validators.min(0), ]), }); launchPaymentModalAutomatically = false; protected freeTrialData: FreeTrial; constructor( protected apiService: ApiService, protected organizationApiService: OrganizationApiServiceAbstraction, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, private router: Router, private location: Location, private route: ActivatedRoute, private formBuilder: FormBuilder, private dialogService: DialogService, private toastService: ToastService, private trialFlowService: TrialFlowService, private organizationService: OrganizationService, private accountService: AccountService, protected syncService: SyncService, ) { const state = this.router.getCurrentNavigation()?.extras?.state; // incase the above state is undefined or null we use redundantState const redundantState: any = location.getState(); if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; } else if ( redundantState && Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically") ) { this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically; } else { this.launchPaymentModalAutomatically = false; } } async ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.params.subscribe(async (params) => { if (params.organizationId) { this.organizationId = params.organizationId; } else if (this.platformUtilsService.isSelfHost()) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/settings/subscription"]); return; } await this.load(); this.firstLoaded = true; }); } load = async () => { if (this.loading) { return; } this.loading = true; if (this.forOrganization) { const billingPromise = this.organizationApiService.getBilling(this.organizationId); const organizationSubscriptionPromise = this.organizationApiService.getSubscription( this.organizationId, ); const userId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); const organizationPromise = await firstValueFrom( this.organizationService .organizations$(userId) .pipe(getOrganizationById(this.organizationId)), ); [this.billing, this.org, this.organization] = await Promise.all([ billingPromise, organizationSubscriptionPromise, organizationPromise, ]); this.determineOrgsWithUpcomingPaymentIssues(); } else { const billingPromise = this.apiService.getUserBillingPayment(); const subPromise = this.apiService.getUserSubscription(); [this.billing, this.sub] = await Promise.all([billingPromise, subPromise]); } this.isUnpaid = this.subscription?.status === "unpaid" ?? false; this.loading = false; // If the flag `launchPaymentModalAutomatically` is set to true, // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. // This delay ensures that any prior UI/rendering operations complete before triggering the modal. if (this.launchPaymentModalAutomatically) { window.setTimeout(async () => { await this.changePayment(); this.launchPaymentModalAutomatically = false; this.location.replaceState(this.location.path(), "", {}); }, 800); } }; addCredit = async () => { const dialogRef = openAddCreditDialog(this.dialogService, { data: { organizationId: this.organizationId, }, }); const result = await lastValueFrom(dialogRef.closed); if (result === AddCreditDialogResult.Added) { await this.load(); } }; changePayment = async () => { const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { data: { organizationId: this.organizationId, initialPaymentMethod: this.paymentSource !== null ? this.paymentSource.type : null, }, }); const result = await lastValueFrom(dialogRef.closed); if (result === AdjustPaymentDialogResultType.Submitted) { this.location.replaceState(this.location.path(), "", {}); if (this.launchPaymentModalAutomatically && !this.organization.enabled) { await this.syncService.fullSync(true); } this.launchPaymentModalAutomatically = false; await this.load(); } }; verifyBank = async () => { if (this.loading || !this.forOrganization) { return; } const request = new VerifyBankRequest(); request.amount1 = this.verifyBankForm.value.amount1; request.amount2 = this.verifyBankForm.value.amount2; await this.organizationApiService.verifyBank(this.organizationId, request); this.toastService.showToast({ variant: "success", title: null, message: this.i18nService.t("verifiedBankAccount"), }); await this.load(); }; determineOrgsWithUpcomingPaymentIssues() { this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( this.organization, this.org, this.billing?.paymentSource, ); } get isCreditBalance() { return this.billing == null || this.billing.balance <= 0; } get creditOrBalance() { return Math.abs(this.billing != null ? this.billing.balance : 0); } get paymentSource() { return this.billing != null ? this.billing.paymentSource : null; } get forOrganization() { return this.organizationId != null; } get paymentSourceClasses() { if (this.paymentSource == null) { return []; } switch (this.paymentSource.type) { case PaymentMethodType.Card: return ["bwi-credit-card"]; case PaymentMethodType.BankAccount: case PaymentMethodType.Check: return ["bwi-billing"]; case PaymentMethodType.PayPal: return ["bwi-paypal text-primary"]; default: return []; } } get subscription() { return this.sub?.subscription ?? this.org?.subscription ?? null; } ngOnDestroy(): void { this.launchPaymentModalAutomatically = false; } }