diff --git a/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html b/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html index 63dcbecf91e..326dc627e17 100644 --- a/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html +++ b/apps/web/src/app/admin-console/organizations/collections/deprecated_vault.component.html @@ -1,45 +1,12 @@ - - - - - - - {{ freeTrial.message }} - - {{ "clickHereToAddPaymentMethod" | i18n }} - - - - - - {{ resellerWarning?.message }} - - + + + +} | undefined; protected isEmpty: boolean; protected showCollectionAccessRestricted: boolean; - private hasSubscription$ = new BehaviorSubject(false); protected currentSearchText$: Observable; - protected useOrganizationWarningsService$: Observable; - protected freeTrialWhenWarningsServiceDisabled$: Observable; - protected resellerWarningWhenWarningsServiceDisabled$: Observable; protected prevCipherId: string | null = null; protected userId: UserId; /** @@ -209,31 +194,6 @@ export class VaultComponent implements OnInit, OnDestroy { @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; - private readonly unpaidSubscriptionDialog$ = this.accountService.activeAccount$.pipe( - map((account) => account?.id), - switchMap((id) => - this.organizationService.organizations$(id).pipe( - filter((organizations) => organizations.length === 1), - map(([organization]) => organization), - switchMap((organization) => - from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe( - tap((organizationMetaData) => { - this.hasSubscription$.next(organizationMetaData.hasSubscription); - }), - switchMap((organizationMetaData) => - from( - this.trialFlowService.handleUnpaidSubscriptionDialog( - organization, - organizationMetaData, - ), - ), - ), - ), - ), - ), - ), - ); - constructor( private route: ActivatedRoute, private organizationService: OrganizationService, @@ -262,13 +222,8 @@ export class VaultComponent implements OnInit, OnDestroy { private toastService: ToastService, private configService: ConfigService, private cipherFormConfigService: CipherFormConfigService, - private organizationApiService: OrganizationApiServiceAbstraction, - private trialFlowService: TrialFlowService, protected billingApiService: BillingApiServiceAbstraction, - private organizationBillingService: OrganizationBillingServiceAbstraction, - private resellerWarningService: ResellerWarningService, private accountService: AccountService, - private billingNotificationService: BillingNotificationService, private organizationWarningsService: OrganizationWarningsService, private collectionService: CollectionService, ) {} @@ -661,74 +616,17 @@ export class VaultComponent implements OnInit, OnDestroy { .subscribe(); // Billing Warnings - this.useOrganizationWarningsService$ = this.configService.getFeatureFlag$( - FeatureFlag.UseOrganizationWarningsService, - ); - - this.useOrganizationWarningsService$ - .pipe( - switchMap((enabled) => - enabled - ? this.organizationWarningsService.showInactiveSubscriptionDialog$(this.organization) - : this.unpaidSubscriptionDialog$, - ), - takeUntil(this.destroy$), - ) - .subscribe(); - organization$ .pipe( switchMap((organization) => - this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), + merge( + this.organizationWarningsService.showInactiveSubscriptionDialog$(organization), + this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), + ), ), takeUntil(this.destroy$), ) .subscribe(); - - const freeTrial$ = combineLatest([ - organization$, - this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)), - ]).pipe( - filter( - ([org, hasSubscription]) => org.isOwner && hasSubscription && org.canViewBillingHistory, - ), - switchMap(([org]) => - combineLatest([ - of(org), - this.organizationApiService.getSubscription(org.id), - from(this.organizationBillingService.getPaymentSource(org.id)).pipe( - catchError((error: unknown) => { - this.billingNotificationService.handleError(error); - return of(null); - }), - ), - ]), - ), - map(([org, sub, paymentSource]) => - this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource), - ), - filter((result) => result !== null), - ); - - this.freeTrialWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe( - filter((enabled) => !enabled), - switchMap(() => freeTrial$), - ); - - const resellerWarning$ = organization$.pipe( - filter((org) => org.isOwner), - switchMap((org) => - from(this.billingApiService.getOrganizationBillingMetadata(org.id)).pipe( - map((metadata) => ({ org, metadata })), - ), - ), - map(({ org, metadata }) => this.resellerWarningService.getWarning(org, metadata)), - ); - - this.resellerWarningWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe( - filter((enabled) => !enabled), - switchMap(() => resellerWarning$), - ); // End Billing Warnings firstSetup$ diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.html b/apps/web/src/app/admin-console/organizations/collections/vault.component.html index f2a4e4f4585..b9e27b026c5 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.html +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.html @@ -5,47 +5,16 @@ @let loading = loading$ | async; @if (organization) { - @if (useOrganizationWarningsService$ | async) { + @if (!refreshing) { - } - - @if (useOrganizationWarningsService$ | async) { } - @let freeTrial = freeTrialWhenWarningsServiceDisabled$ | async; - @if (!refreshing && freeTrial?.shownBanner) { - - {{ freeTrial.message }} - - {{ "clickHereToAddPaymentMethod" | i18n }} - - - } - - @let resellerWarning = resellerWarningWhenWarningsServiceDisabled$ | async; - @if (!refreshing && resellerWarning) { - - {{ resellerWarning?.message }} - - } - @if (filter) { ; protected isEmpty$: Observable = of(false); - private hasSubscription$ = new BehaviorSubject(false); - protected useOrganizationWarningsService$: Observable; - protected freeTrialWhenWarningsServiceDisabled$: Observable; - protected resellerWarningWhenWarningsServiceDisabled$: Observable; protected prevCipherId: string | null = null; protected userId$: Observable; @@ -227,31 +213,6 @@ export class vNextVaultComponent implements OnInit, OnDestroy { | VaultItemsComponent | undefined; - private readonly unpaidSubscriptionDialog$ = this.accountService.activeAccount$.pipe( - getUserId, - switchMap((id) => - this.organizationService.organizations$(id).pipe( - filter((organizations) => organizations.length === 1), - map(([organization]) => organization), - switchMap((organization) => - from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe( - tap((organizationMetaData) => { - this.hasSubscription$.next(organizationMetaData.hasSubscription); - }), - switchMap((organizationMetaData) => - from( - this.trialFlowService.handleUnpaidSubscriptionDialog( - organization, - organizationMetaData, - ), - ), - ), - ), - ), - ), - ), - ); - constructor( private route: ActivatedRoute, private organizationService: OrganizationService, @@ -280,13 +241,8 @@ export class vNextVaultComponent implements OnInit, OnDestroy { private toastService: ToastService, private configService: ConfigService, private cipherFormConfigService: CipherFormConfigService, - private organizationApiService: OrganizationApiServiceAbstraction, - private trialFlowService: TrialFlowService, protected billingApiService: BillingApiServiceAbstraction, - private organizationBillingService: OrganizationBillingServiceAbstraction, - private resellerWarningService: ResellerWarningService, private accountService: AccountService, - private billingNotificationService: BillingNotificationService, private organizationWarningsService: OrganizationWarningsService, private collectionService: CollectionService, private restrictedItemTypesService: RestrictedItemTypesService, @@ -461,68 +417,17 @@ export class vNextVaultComponent implements OnInit, OnDestroy { ); // Billing Warnings - this.useOrganizationWarningsService$ = this.configService.getFeatureFlag$( - FeatureFlag.UseOrganizationWarningsService, - ); - - const freeTrial$ = combineLatest([ - this.organization$, - this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)), - ]).pipe( - filter( - ([org, hasSubscription]) => org.isOwner && hasSubscription && org.canViewBillingHistory, - ), - switchMap(([org]) => - combineLatest([ - of(org), - this.organizationApiService.getSubscription(org.id), - from(this.organizationBillingService.getPaymentSource(org.id)).pipe( - map((paymentSource) => { - if (paymentSource == null) { - throw new Error("Payment source not found."); - } - return paymentSource; - }), - ), - ]), - ), - map(([org, sub, paymentSource]) => - this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource), - ), - filter((result) => result !== null), - catchError((error: unknown) => { - this.billingNotificationService.handleError(error); - return of(); - }), - ); - - this.freeTrialWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe( - filter((enabled) => !enabled), - switchMap(() => freeTrial$), - ); - - this.resellerWarningWhenWarningsServiceDisabled$ = combineLatest([ - this.organization$, - this.useOrganizationWarningsService$, - ]).pipe( - filter(([org, enabled]) => !enabled && org.isOwner), - switchMap(([org]) => - from(this.billingApiService.getOrganizationBillingMetadata(org.id)).pipe( - map((metadata) => ({ org, metadata })), - ), - ), - map(({ org, metadata }) => this.resellerWarningService.getWarning(org, metadata)), - ); - this.organization$ .pipe( switchMap((organization) => - this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), + merge( + this.organizationWarningsService.showInactiveSubscriptionDialog$(organization), + this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), + ), ), takeUntilDestroyed(), ) .subscribe(); - // End Billing Warnings this.editableCollections$ = combineLatest([ @@ -781,17 +686,6 @@ export class vNextVaultComponent implements OnInit, OnDestroy { ) .subscribe(); - combineLatest([this.useOrganizationWarningsService$, this.organization$]) - .pipe( - switchMap(([enabled, organization]) => - enabled - ? this.organizationWarningsService.showInactiveSubscriptionDialog$(organization) - : this.unpaidSubscriptionDialog$, - ), - takeUntil(this.destroy$), - ) - .subscribe(); - // Handle last of initial setup - workaround for some state issues where we need to manually // push the collections we've loaded back into the VaultFilterService. // FIXME: figure out how we can remove this. diff --git a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts index 05802d5868a..47742ba0a88 100644 --- a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts +++ b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts @@ -243,7 +243,6 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy { const result = await lastValueFrom(dialogRef.closed); if (result?.type === "success") { await this.setPaymentMethod(result.paymentMethod); - this.organizationWarningsService.refreshFreeTrialWarning(); } }; @@ -264,6 +263,10 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy { setPaymentMethod = async (paymentMethod: MaskedPaymentMethod) => { if (this.viewState$.value) { + if (!this.viewState$.value.paymentMethod) { + this.organizationWarningsService.refreshFreeTrialWarning(); + } + const billingAddress = this.viewState$.value.billingAddress ?? (await this.subscriberBillingClient.getBillingAddress(this.viewState$.value.organization)); diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html index cf3fe8a9338..ab31147e916 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html @@ -1,21 +1,3 @@ - - {{ freeTrialData?.message }} - - {{ "clickHereToAddPaymentMethod" | i18n }} - - diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index 855263cdbc1..4106ee4f9cd 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -6,8 +6,8 @@ import { combineLatest, firstValueFrom, from, lastValueFrom, map, switchMap } fr import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { - getOrganizationById, OrganizationService, + getOrganizationById, } 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"; @@ -25,7 +25,6 @@ import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; import { BillingNotificationService } from "../../services/billing-notification.service"; -import { TrialFlowService } from "../../services/trial-flow.service"; import { AddCreditDialogResult, openAddCreditDialog, @@ -38,7 +37,6 @@ import { TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE, TrialPaymentDialogComponent, } from "../../shared/trial-payment-dialog/trial-payment-dialog.component"; -import { FreeTrial } from "../../types/free-trial"; @Component({ templateUrl: "./organization-payment-method.component.html", @@ -50,7 +48,6 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { accountCredit?: number; paymentSource?: PaymentSourceResponse; subscriptionStatus?: string; - protected freeTrialData?: FreeTrial; organization?: Organization; organizationSubscriptionResponse?: OrganizationSubscriptionResponse; @@ -71,7 +68,6 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { private router: Router, private toastService: ToastService, private location: Location, - private trialFlowService: TrialFlowService, private organizationService: OrganizationService, private accountService: AccountService, protected syncService: SyncService, @@ -183,12 +179,6 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { if (!this.paymentSource) { throw new Error("Payment source is not found"); } - - this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( - this.organization, - this.organizationSubscriptionResponse, - this.paymentSource, - ); } // If the flag `launchPaymentModalAutomatically` is set to true, // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. diff --git a/apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts b/apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts index 1a889ec9b63..8390e432236 100644 --- a/apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts +++ b/apps/web/src/app/billing/organizations/warnings/components/organization-free-trial-warning.component.ts @@ -37,6 +37,7 @@ import { OrganizationFreeTrialWarning } from "../types"; }) export class OrganizationFreeTrialWarningComponent implements OnInit { @Input({ required: true }) organization!: Organization; + @Input() includeOrganizationNameInMessaging = false; @Output() clicked = new EventEmitter(); warning$!: Observable; @@ -44,6 +45,9 @@ export class OrganizationFreeTrialWarningComponent implements OnInit { constructor(private organizationWarningsService: OrganizationWarningsService) {} ngOnInit() { - this.warning$ = this.organizationWarningsService.getFreeTrialWarning$(this.organization); + this.warning$ = this.organizationWarningsService.getFreeTrialWarning$( + this.organization, + this.includeOrganizationNameInMessaging, + ); } } diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts index 5b466dfe41d..c6bb1bc231b 100644 --- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts +++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts @@ -63,6 +63,7 @@ export class OrganizationWarningsService { getFreeTrialWarning$ = ( organization: Organization, + includeOrganizationNameInMessaging = false, ): Observable => merge( this.getWarning$(organization, (response) => response.freeTrial), @@ -80,20 +81,30 @@ export class OrganizationWarningsService { if (remainingTrialDays >= 2) { return { organization, - message: this.i18nService.t("freeTrialEndPromptCount", remainingTrialDays), + message: includeOrganizationNameInMessaging + ? this.i18nService.t( + "freeTrialEndPromptMultipleDays", + organization.name, + remainingTrialDays, + ) + : this.i18nService.t("freeTrialEndPromptCount", remainingTrialDays), }; } if (remainingTrialDays == 1) { return { organization, - message: this.i18nService.t("freeTrialEndPromptTomorrowNoOrgName"), + message: includeOrganizationNameInMessaging + ? this.i18nService.t("freeTrialEndPromptTomorrow", organization.name) + : this.i18nService.t("freeTrialEndPromptTomorrowNoOrgName"), }; } return { organization, - message: this.i18nService.t("freeTrialEndingTodayWithoutOrgName"), + message: includeOrganizationNameInMessaging + ? this.i18nService.t("freeTrialEndPromptToday", organization.name) + : this.i18nService.t("freeTrialEndingTodayWithoutOrgName"), }; }), ); diff --git a/apps/web/src/app/billing/services/reseller-warning.service.ts b/apps/web/src/app/billing/services/reseller-warning.service.ts deleted file mode 100644 index 2c59ebafe05..00000000000 --- a/apps/web/src/app/billing/services/reseller-warning.service.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -export interface ResellerWarning { - type: "info" | "warning"; - message: string; -} - -@Injectable({ providedIn: "root" }) -export class ResellerWarningService { - private readonly RENEWAL_WARNING_DAYS = 14; - private readonly GRACE_PERIOD_DAYS = 30; - - constructor(private i18nService: I18nService) {} - - getWarning( - organization: Organization, - organizationBillingMetadata: OrganizationBillingMetadataResponse, - ): ResellerWarning | null { - if (!organization.hasReseller) { - return null; // If no reseller, return null immediately - } - - // Check for past due warning first (highest priority) - if (this.shouldShowPastDueWarning(organizationBillingMetadata)) { - const gracePeriodEnd = this.getGracePeriodEndDate(organizationBillingMetadata.invoiceDueDate); - if (!gracePeriodEnd) { - return null; - } - return { - type: "warning", - message: this.i18nService.t( - "resellerPastDueWarningMsg", - organization.providerName, - this.formatDate(gracePeriodEnd), - ), - } as ResellerWarning; - } - - // Check for open invoice warning - if (this.shouldShowInvoiceWarning(organizationBillingMetadata)) { - const invoiceCreatedDate = organizationBillingMetadata.invoiceCreatedDate; - const invoiceDueDate = organizationBillingMetadata.invoiceDueDate; - if (!invoiceCreatedDate || !invoiceDueDate) { - return null; - } - return { - type: "info", - message: this.i18nService.t( - "resellerOpenInvoiceWarningMgs", - organization.providerName, - this.formatDate(organizationBillingMetadata.invoiceCreatedDate), - this.formatDate(organizationBillingMetadata.invoiceDueDate), - ), - } as ResellerWarning; - } - - // Check for renewal warning - if (this.shouldShowRenewalWarning(organizationBillingMetadata)) { - const subPeriodEndDate = organizationBillingMetadata.subPeriodEndDate; - if (!subPeriodEndDate) { - return null; - } - - return { - type: "info", - message: this.i18nService.t( - "resellerRenewalWarningMsg", - organization.providerName, - this.formatDate(organizationBillingMetadata.subPeriodEndDate), - ), - } as ResellerWarning; - } - - return null; - } - - private shouldShowRenewalWarning( - organizationBillingMetadata: OrganizationBillingMetadataResponse, - ): boolean { - if ( - !organizationBillingMetadata.hasSubscription || - !organizationBillingMetadata.subPeriodEndDate - ) { - return false; - } - const renewalDate = new Date(organizationBillingMetadata.subPeriodEndDate); - const daysUntilRenewal = Math.ceil( - (renewalDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24), - ); - return daysUntilRenewal <= this.RENEWAL_WARNING_DAYS; - } - - private shouldShowInvoiceWarning( - organizationBillingMetadata: OrganizationBillingMetadataResponse, - ): boolean { - if ( - !organizationBillingMetadata.hasOpenInvoice || - !organizationBillingMetadata.invoiceDueDate - ) { - return false; - } - const invoiceDueDate = new Date(organizationBillingMetadata.invoiceDueDate); - return invoiceDueDate > new Date(); - } - - private shouldShowPastDueWarning( - organizationBillingMetadata: OrganizationBillingMetadataResponse, - ): boolean { - if ( - !organizationBillingMetadata.hasOpenInvoice || - !organizationBillingMetadata.invoiceDueDate - ) { - return false; - } - const invoiceDueDate = new Date(organizationBillingMetadata.invoiceDueDate); - return invoiceDueDate <= new Date() && !organizationBillingMetadata.isSubscriptionUnpaid; - } - - private getGracePeriodEndDate(dueDate: Date | null): Date | null { - if (!dueDate) { - return null; - } - const gracePeriodEnd = new Date(dueDate); - gracePeriodEnd.setDate(gracePeriodEnd.getDate() + this.GRACE_PERIOD_DAYS); - return gracePeriodEnd; - } - - private formatDate(date: Date | null): string { - if (!date) { - return "N/A"; - } - return new Date(date).toLocaleDateString("en-US", { - month: "short", - day: "2-digit", - year: "numeric", - }); - } -} diff --git a/apps/web/src/app/billing/services/trial-flow.service.ts b/apps/web/src/app/billing/services/trial-flow.service.ts deleted file mode 100644 index 831cc129e60..00000000000 --- a/apps/web/src/app/billing/services/trial-flow.service.ts +++ /dev/null @@ -1,162 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Injectable } from "@angular/core"; -import { Router } from "@angular/router"; -import { lastValueFrom } from "rxjs"; - -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; -import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; -import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; -import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService } from "@bitwarden/components"; - -import { - ChangePlanDialogResultType, - openChangePlanDialog, -} from "../organizations/change-plan-dialog.component"; -import { FreeTrial } from "../types/free-trial"; - -@Injectable({ providedIn: "root" }) -export class TrialFlowService { - constructor( - private i18nService: I18nService, - protected dialogService: DialogService, - private router: Router, - protected billingApiService: BillingApiServiceAbstraction, - private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigService, - ) {} - checkForOrgsWithUpcomingPaymentIssues( - organization: Organization, - organizationSubscription: OrganizationSubscriptionResponse, - paymentSource: BillingSourceResponse | PaymentSourceResponse, - ): FreeTrial { - const trialEndDate = organizationSubscription?.subscription?.trialEndDate; - const displayBanner = - !paymentSource && - organization?.isOwner && - organizationSubscription?.subscription?.status === "trialing"; - const trialRemainingDays = trialEndDate ? this.calculateTrialRemainingDays(trialEndDate) : 0; - const freeTrialMessage = this.getFreeTrialMessage(trialRemainingDays); - - return { - remainingDays: trialRemainingDays, - message: freeTrialMessage, - shownBanner: displayBanner, - organizationId: organization.id, - organizationName: organization.name, - }; - } - - calculateTrialRemainingDays(trialEndDate: string): number | undefined { - const today = new Date(); - const trialEnd = new Date(trialEndDate); - const timeDifference = trialEnd.getTime() - today.getTime(); - - return Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); - } - - getFreeTrialMessage(trialRemainingDays: number): string { - if (trialRemainingDays >= 2) { - return this.i18nService.t("freeTrialEndPromptCount", trialRemainingDays); - } else if (trialRemainingDays === 1) { - return this.i18nService.t("freeTrialEndPromptTomorrowNoOrgName"); - } else { - return this.i18nService.t("freeTrialEndingTodayWithoutOrgName"); - } - } - - async handleUnpaidSubscriptionDialog( - org: Organization, - organizationBillingMetadata: OrganizationBillingMetadataResponse, - ): Promise { - if ( - organizationBillingMetadata.isSubscriptionUnpaid || - organizationBillingMetadata.isSubscriptionCanceled - ) { - const confirmed = await this.promptForPaymentNavigation( - org, - organizationBillingMetadata.isSubscriptionCanceled, - organizationBillingMetadata.isSubscriptionUnpaid, - ); - if (confirmed) { - await this.navigateToPaymentMethod(org?.id); - } - } - } - - private async promptForPaymentNavigation( - org: Organization, - isCanceled: boolean, - isUnpaid: boolean, - ): Promise { - if (!org?.isOwner && !org.providerId) { - await this.dialogService.openSimpleDialog({ - title: this.i18nService.t("suspendedOrganizationTitle", org?.name), - content: { key: "suspendedUserOrgMessage" }, - type: "danger", - acceptButtonText: this.i18nService.t("close"), - cancelButtonText: null, - }); - return false; - } - - if (org.providerId) { - await this.dialogService.openSimpleDialog({ - title: this.i18nService.t("suspendedOrganizationTitle", org.name), - content: { key: "suspendedManagedOrgMessage", placeholders: [org.providerName] }, - type: "danger", - acceptButtonText: this.i18nService.t("close"), - cancelButtonText: null, - }); - return false; - } - - if (org.isOwner && isUnpaid) { - return await this.dialogService.openSimpleDialog({ - title: this.i18nService.t("suspendedOrganizationTitle", org.name), - content: { key: "suspendedOwnerOrgMessage" }, - type: "danger", - acceptButtonText: this.i18nService.t("continue"), - cancelButtonText: this.i18nService.t("close"), - }); - } - - if (org.isOwner && isCanceled) { - await this.changePlan(org); - } - } - - private async navigateToPaymentMethod(orgId: string) { - const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag( - FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout, - ); - const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method"; - await this.router.navigate(["organizations", `${orgId}`, "billing", route], { - state: { launchPaymentModalAutomatically: true }, - queryParams: { launchPaymentModalAutomatically: true }, - }); - } - - private async changePlan(org: Organization) { - const subscription = await this.organizationApiService.getSubscription(org.id); - const reference = openChangePlanDialog(this.dialogService, { - data: { - organizationId: org.id, - subscription: subscription, - productTierType: org.productTierType, - }, - }); - - const result = await lastValueFrom(reference.closed); - if (result === ChangePlanDialogResultType.Closed) { - return; - } - } -} diff --git a/apps/web/src/app/billing/shared/payment-method.component.html b/apps/web/src/app/billing/shared/payment-method.component.html index 6d3b1adff9c..81ed7e5a631 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.html +++ b/apps/web/src/app/billing/shared/payment-method.component.html @@ -1,22 +1,3 @@ - - {{ freeTrialData?.message }} - - {{ "clickHereToAddPaymentMethod" | i18n }} - - -