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:
@@ -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"
|
||||||
|
|||||||
@@ -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$
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export type FreeTrial = {
|
|
||||||
remainingDays: number;
|
|
||||||
message: string;
|
|
||||||
shownBanner: boolean;
|
|
||||||
organizationId: string;
|
|
||||||
organizationName: string;
|
|
||||||
};
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export * from "./bitwarden-subscriber";
|
export * from "./bitwarden-subscriber";
|
||||||
export * from "./free-trial";
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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: [],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user