1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

[PM-23513] Complete usage of OrganizationWarningsService (#16272)

* Use OrganizationWarningsService in AC VaultComponent

* Use OrganizationWarningsService in OrgSwitcherComponent

* Use OrganizationWarningsService in VaultFilterComponent

* Use OrganizationWarningsService in VaultComponent

* Use OrganizationWarningsService in SM OverviewComponent

* Remove TrialFlowService from unused codepaths

* Remove TrialFlowService

* Refresh free trial warning on standard payment method update

* Fix lint errors

* Fix lint errors

* Remove FF

* Fix free trial banner on deprecated ac vault component
This commit is contained in:
Alex Morask
2025-09-19 11:26:48 -05:00
committed by GitHub
parent 5253b3a94d
commit d8339f0196
27 changed files with 109 additions and 869 deletions

View File

@@ -1,45 +1,12 @@
<app-organization-free-trial-warning @if (organization) {
*ngIf="useOrganizationWarningsService$ | async" <app-organization-free-trial-warning
[organization]="organization" [organization]="organization"
(clicked)="navigateToPaymentMethod()" (clicked)="navigateToPaymentMethod()"
>
</app-organization-free-trial-warning>
<app-organization-reseller-renewal-warning
*ngIf="useOrganizationWarningsService$ | async"
[organization]="organization"
>
</app-organization-reseller-renewal-warning>
<ng-container *ngIf="freeTrialWhenWarningsServiceDisabled$ | async as freeTrial">
<bit-banner
id="free-trial-banner"
icon="bwi-billing"
bannerType="premium"
[showClose]="false"
*ngIf="!refreshing && freeTrial.shownBanner"
> >
{{ freeTrial.message }} </app-organization-free-trial-warning>
<a <app-organization-reseller-renewal-warning [organization]="organization">
bitLink </app-organization-reseller-renewal-warning>
linkType="secondary" }
(click)="navigateToPaymentMethod()"
class="tw-cursor-pointer"
rel="noreferrer noopener"
>
{{ "clickHereToAddPaymentMethod" | i18n }}
</a>
</bit-banner>
</ng-container>
<ng-container *ngIf="resellerWarningWhenWarningsServiceDisabled$ | async as resellerWarning">
<bit-banner
id="reseller-warning-banner"
icon="bwi-billing"
bannerType="info"
[showClose]="false"
*ngIf="!refreshing"
>
{{ resellerWarning?.message }}
</bit-banner>
</ng-container>
<app-org-vault-header <app-org-vault-header
[filter]="filter" [filter]="filter"

View File

@@ -6,14 +6,12 @@ import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
firstValueFrom, firstValueFrom,
from,
lastValueFrom, lastValueFrom,
merge,
Observable, Observable,
of,
Subject, Subject,
} from "rxjs"; } from "rxjs";
import { import {
catchError,
concatMap, concatMap,
debounceTime, debounceTime,
distinctUntilChanged, distinctUntilChanged,
@@ -37,12 +35,10 @@ import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { NoResults } from "@bitwarden/assets/svg"; import { NoResults } from "@bitwarden/assets/svg";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -86,13 +82,6 @@ import {
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
import { VaultItemsComponent } from "@bitwarden/web-vault/app/vault/components/vault-items/vault-items.component"; import { VaultItemsComponent } from "@bitwarden/web-vault/app/vault/components/vault-items/vault-items.component";
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
import {
ResellerWarning,
ResellerWarningService,
} from "../../../billing/services/reseller-warning.service";
import { TrialFlowService } from "../../../billing/services/trial-flow.service";
import { FreeTrial } from "../../../billing/types/free-trial";
import { SharedModule } from "../../../shared"; import { SharedModule } from "../../../shared";
import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections"; import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections";
import { import {
@@ -183,11 +172,7 @@ export class VaultComponent implements OnInit, OnDestroy {
protected selectedCollection: TreeNode<CollectionAdminView> | undefined; protected selectedCollection: TreeNode<CollectionAdminView> | undefined;
protected isEmpty: boolean; protected isEmpty: boolean;
protected showCollectionAccessRestricted: boolean; protected showCollectionAccessRestricted: boolean;
private hasSubscription$ = new BehaviorSubject<boolean>(false);
protected currentSearchText$: Observable<string>; protected currentSearchText$: Observable<string>;
protected useOrganizationWarningsService$: Observable<boolean>;
protected freeTrialWhenWarningsServiceDisabled$: Observable<FreeTrial>;
protected resellerWarningWhenWarningsServiceDisabled$: Observable<ResellerWarning | null>;
protected prevCipherId: string | null = null; protected prevCipherId: string | null = null;
protected userId: UserId; protected userId: UserId;
/** /**
@@ -209,31 +194,6 @@ export class VaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent<CipherView>; @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent<CipherView>;
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( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private organizationService: OrganizationService, private organizationService: OrganizationService,
@@ -262,13 +222,8 @@ export class VaultComponent implements OnInit, OnDestroy {
private toastService: ToastService, private toastService: ToastService,
private configService: ConfigService, private configService: ConfigService,
private cipherFormConfigService: CipherFormConfigService, private cipherFormConfigService: CipherFormConfigService,
private organizationApiService: OrganizationApiServiceAbstraction,
private trialFlowService: TrialFlowService,
protected billingApiService: BillingApiServiceAbstraction, protected billingApiService: BillingApiServiceAbstraction,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private resellerWarningService: ResellerWarningService,
private accountService: AccountService, private accountService: AccountService,
private billingNotificationService: BillingNotificationService,
private organizationWarningsService: OrganizationWarningsService, private organizationWarningsService: OrganizationWarningsService,
private collectionService: CollectionService, private collectionService: CollectionService,
) {} ) {}
@@ -661,74 +616,17 @@ export class VaultComponent implements OnInit, OnDestroy {
.subscribe(); .subscribe();
// Billing Warnings // 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$ organization$
.pipe( .pipe(
switchMap((organization) => switchMap((organization) =>
this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), merge(
this.organizationWarningsService.showInactiveSubscriptionDialog$(organization),
this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization),
),
), ),
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
.subscribe(); .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 // End Billing Warnings
firstSetup$ firstSetup$

View File

@@ -5,47 +5,16 @@
@let loading = loading$ | async; @let loading = loading$ | async;
@if (organization) { @if (organization) {
@if (useOrganizationWarningsService$ | async) { @if (!refreshing) {
<app-organization-free-trial-warning <app-organization-free-trial-warning
[organization]="organization" [organization]="organization"
(clicked)="navigateToPaymentMethod()" (clicked)="navigateToPaymentMethod()"
> >
</app-organization-free-trial-warning> </app-organization-free-trial-warning>
}
@if (useOrganizationWarningsService$ | async) {
<app-organization-reseller-renewal-warning [organization]="organization"> <app-organization-reseller-renewal-warning [organization]="organization">
</app-organization-reseller-renewal-warning> </app-organization-reseller-renewal-warning>
} }
@let freeTrial = freeTrialWhenWarningsServiceDisabled$ | async;
@if (!refreshing && freeTrial?.shownBanner) {
<bit-banner id="free-trial-banner" icon="bwi-billing" bannerType="premium" [showClose]="false">
{{ freeTrial.message }}
<a
bitLink
linkType="secondary"
(click)="navigateToPaymentMethod()"
class="tw-cursor-pointer"
rel="noreferrer noopener"
>
{{ "clickHereToAddPaymentMethod" | i18n }}
</a>
</bit-banner>
}
@let resellerWarning = resellerWarningWhenWarningsServiceDisabled$ | async;
@if (!refreshing && resellerWarning) {
<bit-banner
id="reseller-warning-banner"
icon="bwi-billing"
bannerType="info"
[showClose]="false"
>
{{ resellerWarning?.message }}
</bit-banner>
}
@if (filter) { @if (filter) {
<app-org-vault-header <app-org-vault-header
[filter]="filter" [filter]="filter"

View File

@@ -5,8 +5,8 @@ import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
firstValueFrom, firstValueFrom,
from,
lastValueFrom, lastValueFrom,
merge,
Observable, Observable,
of, of,
Subject, Subject,
@@ -25,7 +25,6 @@ import {
switchMap, switchMap,
take, take,
takeUntil, takeUntil,
tap,
} from "rxjs/operators"; } from "rxjs/operators";
import { import {
@@ -39,12 +38,10 @@ import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { NoResults } from "@bitwarden/assets/svg"; import { NoResults } from "@bitwarden/assets/svg";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -94,13 +91,6 @@ import {
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
import { VaultItemsComponent } from "@bitwarden/web-vault/app/vault/components/vault-items/vault-items.component"; import { VaultItemsComponent } from "@bitwarden/web-vault/app/vault/components/vault-items/vault-items.component";
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
import {
ResellerWarning,
ResellerWarningService,
} from "../../../billing/services/reseller-warning.service";
import { TrialFlowService } from "../../../billing/services/trial-flow.service";
import { FreeTrial } from "../../../billing/types/free-trial";
import { SharedModule } from "../../../shared"; import { SharedModule } from "../../../shared";
import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections"; import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections";
import { import {
@@ -194,10 +184,6 @@ export class vNextVaultComponent implements OnInit, OnDestroy {
protected showCollectionAccessRestricted$: Observable<boolean>; protected showCollectionAccessRestricted$: Observable<boolean>;
protected isEmpty$: Observable<boolean> = of(false); protected isEmpty$: Observable<boolean> = of(false);
private hasSubscription$ = new BehaviorSubject<boolean>(false);
protected useOrganizationWarningsService$: Observable<boolean>;
protected freeTrialWhenWarningsServiceDisabled$: Observable<FreeTrial>;
protected resellerWarningWhenWarningsServiceDisabled$: Observable<ResellerWarning | null>;
protected prevCipherId: string | null = null; protected prevCipherId: string | null = null;
protected userId$: Observable<UserId>; protected userId$: Observable<UserId>;
@@ -227,31 +213,6 @@ export class vNextVaultComponent implements OnInit, OnDestroy {
| VaultItemsComponent<CipherView> | VaultItemsComponent<CipherView>
| undefined; | 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( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private organizationService: OrganizationService, private organizationService: OrganizationService,
@@ -280,13 +241,8 @@ export class vNextVaultComponent implements OnInit, OnDestroy {
private toastService: ToastService, private toastService: ToastService,
private configService: ConfigService, private configService: ConfigService,
private cipherFormConfigService: CipherFormConfigService, private cipherFormConfigService: CipherFormConfigService,
private organizationApiService: OrganizationApiServiceAbstraction,
private trialFlowService: TrialFlowService,
protected billingApiService: BillingApiServiceAbstraction, protected billingApiService: BillingApiServiceAbstraction,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private resellerWarningService: ResellerWarningService,
private accountService: AccountService, private accountService: AccountService,
private billingNotificationService: BillingNotificationService,
private organizationWarningsService: OrganizationWarningsService, private organizationWarningsService: OrganizationWarningsService,
private collectionService: CollectionService, private collectionService: CollectionService,
private restrictedItemTypesService: RestrictedItemTypesService, private restrictedItemTypesService: RestrictedItemTypesService,
@@ -461,68 +417,17 @@ export class vNextVaultComponent implements OnInit, OnDestroy {
); );
// Billing Warnings // 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$ this.organization$
.pipe( .pipe(
switchMap((organization) => switchMap((organization) =>
this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization), merge(
this.organizationWarningsService.showInactiveSubscriptionDialog$(organization),
this.organizationWarningsService.showSubscribeBeforeFreeTrialEndsDialog$(organization),
),
), ),
takeUntilDestroyed(), takeUntilDestroyed(),
) )
.subscribe(); .subscribe();
// End Billing Warnings // End Billing Warnings
this.editableCollections$ = combineLatest([ this.editableCollections$ = combineLatest([
@@ -781,17 +686,6 @@ export class vNextVaultComponent implements OnInit, OnDestroy {
) )
.subscribe(); .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 // 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. // push the collections we've loaded back into the VaultFilterService.
// FIXME: figure out how we can remove this. // FIXME: figure out how we can remove this.

View File

@@ -243,7 +243,6 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
const result = await lastValueFrom(dialogRef.closed); const result = await lastValueFrom(dialogRef.closed);
if (result?.type === "success") { if (result?.type === "success") {
await this.setPaymentMethod(result.paymentMethod); await this.setPaymentMethod(result.paymentMethod);
this.organizationWarningsService.refreshFreeTrialWarning();
} }
}; };
@@ -264,6 +263,10 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
setPaymentMethod = async (paymentMethod: MaskedPaymentMethod) => { setPaymentMethod = async (paymentMethod: MaskedPaymentMethod) => {
if (this.viewState$.value) { if (this.viewState$.value) {
if (!this.viewState$.value.paymentMethod) {
this.organizationWarningsService.refreshFreeTrialWarning();
}
const billingAddress = const billingAddress =
this.viewState$.value.billingAddress ?? this.viewState$.value.billingAddress ??
(await this.subscriberBillingClient.getBillingAddress(this.viewState$.value.organization)); (await this.subscriberBillingClient.getBillingAddress(this.viewState$.value.organization));

View File

@@ -1,21 +1,3 @@
<bit-banner
id="free-trial-banner"
bannerType="premium"
icon="bwi-billing"
[showClose]="false"
*ngIf="freeTrialData?.shownBanner"
>
{{ freeTrialData?.message }}
<a
bitLink
linkType="secondary"
(click)="changePayment()"
class="tw-cursor-pointer"
rel="noreferrer noopener"
>
{{ "clickHereToAddPaymentMethod" | i18n }}
</a>
</bit-banner>
<app-header></app-header> <app-header></app-header>
<bit-container> <bit-container>
<ng-container *ngIf="loading"> <ng-container *ngIf="loading">

View File

@@ -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 { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { import {
getOrganizationById,
OrganizationService, OrganizationService,
getOrganizationById,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { DialogService, ToastService } from "@bitwarden/components";
import { BillingNotificationService } from "../../services/billing-notification.service"; import { BillingNotificationService } from "../../services/billing-notification.service";
import { TrialFlowService } from "../../services/trial-flow.service";
import { import {
AddCreditDialogResult, AddCreditDialogResult,
openAddCreditDialog, openAddCreditDialog,
@@ -38,7 +37,6 @@ import {
TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE, TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE,
TrialPaymentDialogComponent, TrialPaymentDialogComponent,
} from "../../shared/trial-payment-dialog/trial-payment-dialog.component"; } from "../../shared/trial-payment-dialog/trial-payment-dialog.component";
import { FreeTrial } from "../../types/free-trial";
@Component({ @Component({
templateUrl: "./organization-payment-method.component.html", templateUrl: "./organization-payment-method.component.html",
@@ -50,7 +48,6 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
accountCredit?: number; accountCredit?: number;
paymentSource?: PaymentSourceResponse; paymentSource?: PaymentSourceResponse;
subscriptionStatus?: string; subscriptionStatus?: string;
protected freeTrialData?: FreeTrial;
organization?: Organization; organization?: Organization;
organizationSubscriptionResponse?: OrganizationSubscriptionResponse; organizationSubscriptionResponse?: OrganizationSubscriptionResponse;
@@ -71,7 +68,6 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
private router: Router, private router: Router,
private toastService: ToastService, private toastService: ToastService,
private location: Location, private location: Location,
private trialFlowService: TrialFlowService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private accountService: AccountService, private accountService: AccountService,
protected syncService: SyncService, protected syncService: SyncService,
@@ -183,12 +179,6 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
if (!this.paymentSource) { if (!this.paymentSource) {
throw new Error("Payment source is not found"); 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, // If the flag `launchPaymentModalAutomatically` is set to true,
// we schedule a timeout (delay of 800ms) to automatically launch the payment modal. // we schedule a timeout (delay of 800ms) to automatically launch the payment modal.

View File

@@ -37,6 +37,7 @@ import { OrganizationFreeTrialWarning } from "../types";
}) })
export class OrganizationFreeTrialWarningComponent implements OnInit { export class OrganizationFreeTrialWarningComponent implements OnInit {
@Input({ required: true }) organization!: Organization; @Input({ required: true }) organization!: Organization;
@Input() includeOrganizationNameInMessaging = false;
@Output() clicked = new EventEmitter<void>(); @Output() clicked = new EventEmitter<void>();
warning$!: Observable<OrganizationFreeTrialWarning | null>; warning$!: Observable<OrganizationFreeTrialWarning | null>;
@@ -44,6 +45,9 @@ export class OrganizationFreeTrialWarningComponent implements OnInit {
constructor(private organizationWarningsService: OrganizationWarningsService) {} constructor(private organizationWarningsService: OrganizationWarningsService) {}
ngOnInit() { ngOnInit() {
this.warning$ = this.organizationWarningsService.getFreeTrialWarning$(this.organization); this.warning$ = this.organizationWarningsService.getFreeTrialWarning$(
this.organization,
this.includeOrganizationNameInMessaging,
);
} }
} }

View File

@@ -63,6 +63,7 @@ export class OrganizationWarningsService {
getFreeTrialWarning$ = ( getFreeTrialWarning$ = (
organization: Organization, organization: Organization,
includeOrganizationNameInMessaging = false,
): Observable<OrganizationFreeTrialWarning | null> => ): Observable<OrganizationFreeTrialWarning | null> =>
merge( merge(
this.getWarning$(organization, (response) => response.freeTrial), this.getWarning$(organization, (response) => response.freeTrial),
@@ -80,20 +81,30 @@ export class OrganizationWarningsService {
if (remainingTrialDays >= 2) { if (remainingTrialDays >= 2) {
return { return {
organization, organization,
message: this.i18nService.t("freeTrialEndPromptCount", remainingTrialDays), message: includeOrganizationNameInMessaging
? this.i18nService.t(
"freeTrialEndPromptMultipleDays",
organization.name,
remainingTrialDays,
)
: this.i18nService.t("freeTrialEndPromptCount", remainingTrialDays),
}; };
} }
if (remainingTrialDays == 1) { if (remainingTrialDays == 1) {
return { return {
organization, organization,
message: this.i18nService.t("freeTrialEndPromptTomorrowNoOrgName"), message: includeOrganizationNameInMessaging
? this.i18nService.t("freeTrialEndPromptTomorrow", organization.name)
: this.i18nService.t("freeTrialEndPromptTomorrowNoOrgName"),
}; };
} }
return { return {
organization, organization,
message: this.i18nService.t("freeTrialEndingTodayWithoutOrgName"), message: includeOrganizationNameInMessaging
? this.i18nService.t("freeTrialEndPromptToday", organization.name)
: this.i18nService.t("freeTrialEndingTodayWithoutOrgName"),
}; };
}), }),
); );

View File

@@ -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",
});
}
}

View File

@@ -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<void> {
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<boolean> {
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;
}
}
}

View File

@@ -1,22 +1,3 @@
<bit-banner
id="free-trial-banner"
bannerType="premium"
icon="bwi-billing"
[showClose]="false"
*ngIf="freeTrialData?.shownBanner"
>
{{ freeTrialData?.message }}
<a
bitLink
linkType="secondary"
(click)="changePayment()"
class="tw-cursor-pointer"
rel="noreferrer noopener"
>
{{ "clickHereToAddPaymentMethod" | i18n }}
</a>
</bit-banner>
<app-header *ngIf="organizationId"> <app-header *ngIf="organizationId">
<button <button
type="button" type="button"

View File

@@ -7,8 +7,8 @@ import { firstValueFrom, lastValueFrom, map } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { import {
getOrganizationById,
OrganizationService, OrganizationService,
getOrganizationById,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -24,9 +24,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SyncService } from "@bitwarden/common/platform/sync"; import { SyncService } from "@bitwarden/common/platform/sync";
import { DialogService, ToastService } from "@bitwarden/components"; 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 { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component";
import { import {
AdjustPaymentDialogComponent, AdjustPaymentDialogComponent,
@@ -62,8 +59,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
}); });
launchPaymentModalAutomatically = false; launchPaymentModalAutomatically = false;
protected freeTrialData?: FreeTrial;
constructor( constructor(
protected apiService: ApiService, protected apiService: ApiService,
protected organizationApiService: OrganizationApiServiceAbstraction, protected organizationApiService: OrganizationApiServiceAbstraction,
@@ -75,7 +70,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private dialogService: DialogService, private dialogService: DialogService,
private toastService: ToastService, private toastService: ToastService,
private trialFlowService: TrialFlowService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private accountService: AccountService, private accountService: AccountService,
protected syncService: SyncService, protected syncService: SyncService,
@@ -151,7 +145,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
organizationSubscriptionPromise, organizationSubscriptionPromise,
organizationPromise, organizationPromise,
]); ]);
this.determineOrgsWithUpcomingPaymentIssues();
} else { } else {
const billingPromise = this.apiService.getUserBillingPayment(); const billingPromise = this.apiService.getUserBillingPayment();
const subPromise = this.apiService.getUserSubscription(); const subPromise = this.apiService.getUserSubscription();
@@ -225,18 +218,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
await this.load(); await this.load();
}; };
determineOrgsWithUpcomingPaymentIssues() {
if (!this.organization || !this.org || !this.billing) {
throw new Error("Organization, organization subscription, or billing is not defined");
}
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
this.organization,
this.org,
this.billing?.paymentSource,
);
}
get isCreditBalance() { get isCreditBalance() {
return this.billing == null || this.billing.balance <= 0; return this.billing == null || this.billing.balance <= 0;
} }

View File

@@ -1,7 +0,0 @@
export type FreeTrial = {
remainingDays: number;
message: string;
shownBanner: boolean;
organizationId: string;
organizationName: string;
};

View File

@@ -1,2 +1 @@
export * from "./bitwarden-subscriber"; export * from "./bitwarden-subscriber";
export * from "./free-trial";

View File

@@ -22,7 +22,7 @@
[route]="['../', org.id]" [route]="['../', org.id]"
(mainContentClicked)="toggle()" (mainContentClicked)="toggle()"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
(click)="handleUnpaidSubscription(org)" (click)="showInactiveSubscriptionDialog(org)"
> >
<i <i
slot="end" slot="end"

View File

@@ -3,7 +3,7 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { combineLatest, map, Observable, switchMap } from "rxjs"; import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -11,13 +11,13 @@ import type { Organization } from "@bitwarden/common/admin-console/models/domain
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { DialogService, NavigationModule } from "@bitwarden/components"; import { DialogService, NavigationModule } from "@bitwarden/components";
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
import { TrialFlowService } from "./../../billing/services/trial-flow.service"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
@Component({ @Component({
selector: "org-switcher", selector: "org-switcher",
templateUrl: "org-switcher.component.html", templateUrl: "org-switcher.component.html",
imports: [CommonModule, JslibModule, NavigationModule], imports: [CommonModule, JslibModule, NavigationModule, OrganizationWarningsModule],
}) })
export class OrgSwitcherComponent { export class OrgSwitcherComponent {
protected organizations$: Observable<Organization[]> = this.accountService.activeAccount$.pipe( protected organizations$: Observable<Organization[]> = this.accountService.activeAccount$.pipe(
@@ -64,9 +64,9 @@ export class OrgSwitcherComponent {
private route: ActivatedRoute, private route: ActivatedRoute,
protected dialogService: DialogService, protected dialogService: DialogService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private trialFlowService: TrialFlowService,
protected billingApiService: BillingApiServiceAbstraction, protected billingApiService: BillingApiServiceAbstraction,
private accountService: AccountService, private accountService: AccountService,
private organizationWarningsService: OrganizationWarningsService,
) {} ) {}
protected toggle(event?: MouseEvent) { protected toggle(event?: MouseEvent) {
@@ -75,8 +75,8 @@ export class OrgSwitcherComponent {
this.openChange.emit(this.open); this.openChange.emit(this.open);
} }
async handleUnpaidSubscription(org: Organization) { showInactiveSubscriptionDialog = async (organization: Organization) =>
const metaData = await this.billingApiService.getOrganizationBillingMetadata(org.id); await firstValueFrom(
await this.trialFlowService.handleUnpaidSubscriptionDialog(org, metaData); this.organizationWarningsService.showInactiveSubscriptionDialog$(organization),
} );
} }

View File

@@ -1,21 +1,11 @@
<bit-banner @for (organization of organizations; track organization.id) {
id="free-trial-banner" <app-organization-free-trial-warning
bannerType="premium" [organization]="organization"
icon="bwi-billing" [includeOrganizationNameInMessaging]="true"
[showClose]="false" (clicked)="navigateToPaymentMethod(organization.id)"
*ngFor="let organization of organizationsPaymentStatus; trackBy: trackBy; index as i"
>
{{ freeTrialMessage(organization) }}
<a
bitLink
linkType="secondary"
(click)="navigateToPaymentMethod(organization?.organizationId)"
rel="noreferrer noopener"
class="tw-cursor-pointer"
> >
{{ "clickHereToAddPaymentMethod" | i18n }} </app-organization-free-trial-warning>
</a> }
</bit-banner>
<bit-banner <bit-banner
id="update-browser-banner" id="update-browser-banner"

View File

@@ -3,16 +3,16 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { filter, firstValueFrom, map, Observable, switchMap } from "rxjs"; import { filter, firstValueFrom, map, Observable, switchMap } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessageListener } from "@bitwarden/common/platform/messaging"; import { MessageListener } from "@bitwarden/common/platform/messaging";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { BannerModule } from "@bitwarden/components"; import { BannerModule } from "@bitwarden/components";
import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components";
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component"; import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
import { FreeTrial } from "../../../billing/types/free-trial";
import { SharedModule } from "../../../shared"; import { SharedModule } from "../../../shared";
import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service"; import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service";
@@ -20,21 +20,25 @@ import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banner
@Component({ @Component({
selector: "app-vault-banners", selector: "app-vault-banners",
templateUrl: "./vault-banners.component.html", templateUrl: "./vault-banners.component.html",
imports: [VerifyEmailComponent, SharedModule, BannerModule], imports: [
VerifyEmailComponent,
SharedModule,
BannerModule,
OrganizationFreeTrialWarningComponent,
],
providers: [VaultBannersService], providers: [VaultBannersService],
}) })
export class VaultBannersComponent implements OnInit { export class VaultBannersComponent implements OnInit {
visibleBanners: VisibleVaultBanner[] = []; visibleBanners: VisibleVaultBanner[] = [];
premiumBannerVisible$: Observable<boolean>; premiumBannerVisible$: Observable<boolean>;
VisibleVaultBanner = VisibleVaultBanner; VisibleVaultBanner = VisibleVaultBanner;
@Input() organizationsPaymentStatus: FreeTrial[] = []; @Input() organizations: Organization[] = [];
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor( constructor(
private vaultBannerService: VaultBannersService, private vaultBannerService: VaultBannersService,
private router: Router, private router: Router,
private i18nService: I18nService,
private accountService: AccountService, private accountService: AccountService,
private messageListener: MessageListener, private messageListener: MessageListener,
private configService: ConfigService, private configService: ConfigService,
@@ -107,22 +111,4 @@ export class VaultBannersComponent implements OnInit {
showPendingAuthRequest ? VisibleVaultBanner.PendingAuthRequest : null, showPendingAuthRequest ? VisibleVaultBanner.PendingAuthRequest : null,
].filter((banner) => banner !== null); ].filter((banner) => banner !== null);
} }
freeTrialMessage(organization: FreeTrial) {
if (organization.remainingDays >= 2) {
return this.i18nService.t(
"freeTrialEndPromptMultipleDays",
organization.organizationName,
organization.remainingDays.toString(),
);
} else if (organization.remainingDays === 1) {
return this.i18nService.t("freeTrialEndPromptTomorrow", organization.organizationName);
} else {
return this.i18nService.t("freeTrialEndPromptToday", organization.organizationName);
}
}
trackBy(index: number) {
return index;
}
} }

View File

@@ -1,5 +1,4 @@
import { Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { Router } from "@angular/router";
import { import {
combineLatest, combineLatest,
distinctUntilChanged, distinctUntilChanged,
@@ -26,8 +25,8 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { DialogService, ToastService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
import { TrialFlowService } from "../../../../billing/services/trial-flow.service";
import { VaultFilterService } from "../services/abstractions/vault-filter.service"; import { VaultFilterService } from "../services/abstractions/vault-filter.service";
import { import {
VaultFilterList, VaultFilterList,
@@ -61,11 +60,12 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
isLoaded = false; isLoaded = false;
protected destroy$: Subject<void> = new Subject<void>(); protected destroy$: Subject<void> = new Subject<void>();
private router = inject(Router);
get filtersList() { get filtersList() {
return this.filters ? Object.values(this.filters) : []; return this.filters ? Object.values(this.filters) : [];
} }
protected organizationWarningsService = inject(OrganizationWarningsService);
allTypeFilters: CipherTypeFilter[] = [ allTypeFilters: CipherTypeFilter[] = [
{ {
id: "favorites", id: "favorites",
@@ -143,7 +143,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
return "searchVault"; return "searchVault";
} }
private trialFlowService = inject(TrialFlowService);
protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
constructor( constructor(
@@ -211,8 +210,9 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
variant: "error", variant: "error",
message: this.i18nService.t("disabledOrganizationFilterError"), message: this.i18nService.t("disabledOrganizationFilterError"),
}); });
const metadata = await this.billingApiService.getOrganizationBillingMetadata(orgNode.node.id); await firstValueFrom(
await this.trialFlowService.handleUnpaidSubscriptionDialog(orgNode.node, metadata); this.organizationWarningsService.showInactiveSubscriptionDialog$(orgNode.node),
);
} }
const filter = this.activeFilter; const filter = this.activeFilter;
if (orgNode?.node.id === "AllVaults") { if (orgNode?.node.id === "AllVaults") {

View File

@@ -1,6 +1,7 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { SearchModule } from "@bitwarden/components"; import { SearchModule } from "@bitwarden/components";
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module"; import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module";
@@ -10,7 +11,7 @@ import { VaultFilterService as VaultFilterServiceAbstraction } from "./services/
import { VaultFilterService } from "./services/vault-filter.service"; import { VaultFilterService } from "./services/vault-filter.service";
@NgModule({ @NgModule({
imports: [VaultFilterSharedModule, SearchModule], imports: [VaultFilterSharedModule, SearchModule, OrganizationWarningsModule],
declarations: [VaultFilterComponent, OrganizationOptionsComponent], declarations: [VaultFilterComponent, OrganizationOptionsComponent],
exports: [VaultFilterComponent], exports: [VaultFilterComponent],
providers: [ providers: [

View File

@@ -1,6 +1,4 @@
<app-vault-banners <app-vault-banners [organizations]="organizations$ | async"></app-vault-banners>
[organizationsPaymentStatus]="organizationsPaymentStatus$ | async"
></app-vault-banners>
<app-vault-header <app-vault-header
[filter]="filter" [filter]="filter"

View File

@@ -6,14 +6,11 @@ import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
firstValueFrom, firstValueFrom,
from,
lastValueFrom, lastValueFrom,
Observable, Observable,
of,
Subject, Subject,
} from "rxjs"; } from "rxjs";
import { import {
catchError,
concatMap, concatMap,
debounceTime, debounceTime,
distinctUntilChanged, distinctUntilChanged,
@@ -38,7 +35,6 @@ import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { NoResults } from "@bitwarden/assets/svg"; import { NoResults } from "@bitwarden/assets/svg";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { import {
getOrganizationById, getOrganizationById,
OrganizationService, OrganizationService,
@@ -46,12 +42,10 @@ import {
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -89,6 +83,8 @@ import {
DefaultCipherFormConfigService, DefaultCipherFormConfigService,
PasswordRepromptService, PasswordRepromptService,
} from "@bitwarden/vault"; } from "@bitwarden/vault";
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
import { import {
getNestedCollectionTree, getNestedCollectionTree,
@@ -99,9 +95,6 @@ import {
CollectionDialogTabType, CollectionDialogTabType,
openCollectionDialog, openCollectionDialog,
} from "../../admin-console/organizations/shared/components/collection-dialog"; } from "../../admin-console/organizations/shared/components/collection-dialog";
import { BillingNotificationService } from "../../billing/services/billing-notification.service";
import { TrialFlowService } from "../../billing/services/trial-flow.service";
import { FreeTrial } from "../../billing/types/free-trial";
import { SharedModule } from "../../shared/shared.module"; import { SharedModule } from "../../shared/shared.module";
import { AssignCollectionsWebComponent } from "../components/assign-collections"; import { AssignCollectionsWebComponent } from "../components/assign-collections";
import { import {
@@ -150,6 +143,7 @@ const BroadcasterSubscriptionId = "VaultComponent";
VaultFilterModule, VaultFilterModule,
VaultItemsModule, VaultItemsModule,
SharedModule, SharedModule,
OrganizationWarningsModule,
], ],
providers: [ providers: [
RoutedVaultFilterService, RoutedVaultFilterService,
@@ -183,71 +177,12 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
private searchText$ = new Subject<string>(); private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null); private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private hasSubscription$ = new BehaviorSubject<boolean>(false);
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined; private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
private organizations$ = this.accountService.activeAccount$ organizations$ = this.accountService.activeAccount$
.pipe(map((a) => a?.id)) .pipe(map((a) => a?.id))
.pipe(switchMap((id) => this.organizationService.organizations$(id))); .pipe(switchMap((id) => this.organizationService.organizations$(id)));
private readonly unpaidSubscriptionDialog$ = this.organizations$.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,
),
),
),
),
),
);
protected organizationsPaymentStatus$: Observable<FreeTrial[]> = combineLatest([
this.organizations$.pipe(
map(
(organizations) =>
organizations?.filter((org) => org.isOwner && org.canViewBillingHistory) ?? [],
),
),
this.hasSubscription$,
]).pipe(
switchMap(([ownerOrgs, hasSubscription]) => {
if (!ownerOrgs || ownerOrgs.length === 0 || !hasSubscription) {
return of([]);
}
return combineLatest(
ownerOrgs.map((org) =>
combineLatest([
this.organizationApiService.getSubscription(org.id),
from(this.organizationBillingService.getPaymentSource(org.id)).pipe(
catchError((error: unknown) => {
this.billingNotificationService.handleError(error);
return of(null);
}),
),
]).pipe(
map(([subscription, paymentSource]) =>
this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
org,
subscription,
paymentSource,
),
),
),
),
);
}),
map((results) => results.filter((result) => result !== null && result.shownBanner)),
shareReplay({ refCount: false, bufferSize: 1 }),
);
constructor( constructor(
private syncService: SyncService, private syncService: SyncService,
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -276,13 +211,9 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
private toastService: ToastService, private toastService: ToastService,
private accountService: AccountService, private accountService: AccountService,
private cipherFormConfigService: DefaultCipherFormConfigService, private cipherFormConfigService: DefaultCipherFormConfigService,
private organizationApiService: OrganizationApiServiceAbstraction,
protected billingApiService: BillingApiServiceAbstraction, protected billingApiService: BillingApiServiceAbstraction,
private trialFlowService: TrialFlowService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private billingNotificationService: BillingNotificationService,
private configService: ConfigService,
private restrictedItemTypesService: RestrictedItemTypesService, private restrictedItemTypesService: RestrictedItemTypesService,
private organizationWarningsService: OrganizationWarningsService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@@ -520,7 +451,16 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
}); });
}); });
this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe(); this.organizations$
.pipe(
filter((organizations) => organizations.length === 1),
map((organizations) => organizations[0]),
switchMap((organization) =>
this.organizationWarningsService.showInactiveSubscriptionDialog$(organization),
),
takeUntil(this.destroy$),
)
.subscribe();
firstSetup$ firstSetup$
.pipe( .pipe(

View File

@@ -1,23 +1,8 @@
<ng-container *ngIf="freeTrial$ | async as freeTrial"> <app-organization-free-trial-warning
<bit-banner [organization]="organization"
id="update-browser-banner" (clicked)="navigateToPaymentMethod()"
bannerType="premium" >
icon="bwi-billing" </app-organization-free-trial-warning>
[showClose]="false"
*ngIf="!loading && freeTrial.shownBanner"
>
{{ freeTrial.message }}
<a
bitLink
linkType="secondary"
class="tw-cursor-pointer"
(click)="navigateToPaymentMethod()"
rel="noreferrer noopener"
>
{{ "clickHereToAddPaymentMethod" | i18n }}
</a>
</bit-banner>
</ng-container>
<app-header [title]="organizationName"> <app-header [title]="organizationName">
<sm-new-menu></sm-new-menu> <sm-new-menu></sm-new-menu>
</app-header> </app-header>

View File

@@ -14,13 +14,8 @@ import {
take, take,
share, share,
firstValueFrom, firstValueFrom,
of,
filter,
catchError,
from,
} from "rxjs"; } from "rxjs";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { import {
getOrganizationById, getOrganizationById,
OrganizationService, OrganizationService,
@@ -28,16 +23,12 @@ import {
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
import { TrialFlowService } from "@bitwarden/web-vault/app/billing/services/trial-flow.service";
import { FreeTrial } from "@bitwarden/web-vault/app/billing/types/free-trial";
import { OrganizationCounts } from "../models/view/counts.view"; import { OrganizationCounts } from "../models/view/counts.view";
import { ProjectListView } from "../models/view/project-list.view"; import { ProjectListView } from "../models/view/project-list.view";
@@ -111,7 +102,6 @@ export class OverviewComponent implements OnInit, OnDestroy {
tasks: OrganizationTasks; tasks: OrganizationTasks;
counts: OrganizationCounts; counts: OrganizationCounts;
}>; }>;
protected freeTrial$: Observable<FreeTrial>;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -127,10 +117,6 @@ export class OverviewComponent implements OnInit, OnDestroy {
private smOnboardingTasksService: SMOnboardingTasksService, private smOnboardingTasksService: SMOnboardingTasksService,
private logService: LogService, private logService: LogService,
private router: Router, private router: Router,
private organizationApiService: OrganizationApiServiceAbstraction,
private trialFlowService: TrialFlowService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private billingNotificationService: BillingNotificationService,
private configService: ConfigService, private configService: ConfigService,
) {} ) {}
@@ -161,27 +147,6 @@ export class OverviewComponent implements OnInit, OnDestroy {
this.organizationEnabled = org.enabled; this.organizationEnabled = org.enabled;
}); });
this.freeTrial$ = org$.pipe(
filter((org) => org.isOwner && org.canViewBillingHistory && org.canViewSubscription),
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]) => {
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource);
}),
filter((result) => result !== null),
takeUntil(this.destroy$),
);
const projects$ = combineLatest([ const projects$ = combineLatest([
orgId$, orgId$,
this.projectService.project$.pipe(startWith(null)), this.projectService.project$.pipe(startWith(null)),

View File

@@ -1,6 +1,8 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { BannerModule } from "@bitwarden/components"; import { BannerModule } from "@bitwarden/components";
import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components";
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
import { OnboardingModule } from "@bitwarden/web-vault/app/shared/components/onboarding/onboarding.module"; import { OnboardingModule } from "@bitwarden/web-vault/app/shared/components/onboarding/onboarding.module";
import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
@@ -10,7 +12,14 @@ import { OverviewComponent } from "./overview.component";
import { SectionComponent } from "./section.component"; import { SectionComponent } from "./section.component";
@NgModule({ @NgModule({
imports: [SecretsManagerSharedModule, OverviewRoutingModule, OnboardingModule, BannerModule], imports: [
SecretsManagerSharedModule,
OverviewRoutingModule,
OnboardingModule,
BannerModule,
OrganizationFreeTrialWarningComponent,
OrganizationWarningsModule,
],
declarations: [OverviewComponent, SectionComponent], declarations: [OverviewComponent, SectionComponent],
providers: [], providers: [],
}) })

View File

@@ -24,7 +24,6 @@ export enum FeatureFlag {
/* Billing */ /* Billing */
TrialPaymentOptional = "PM-8163-trial-payment", TrialPaymentOptional = "PM-8163-trial-payment",
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships", PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
UseOrganizationWarningsService = "use-organization-warnings-service",
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout", PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover", PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover",
PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings", PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings",
@@ -100,7 +99,6 @@ export const DefaultFeatureFlagValue = {
/* Billing */ /* Billing */
[FeatureFlag.TrialPaymentOptional]: FALSE, [FeatureFlag.TrialPaymentOptional]: FALSE,
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE, [FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
[FeatureFlag.UseOrganizationWarningsService]: FALSE,
[FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE, [FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,
[FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE, [FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE,
[FeatureFlag.PM22415_TaxIDWarnings]: FALSE, [FeatureFlag.PM22415_TaxIDWarnings]: FALSE,