mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +01:00
[PM-5971] Fix Payment Method Warning Bugs (#7923)
* Rework implementation of payment method warnings * Move payment-method-warnings.component to module * Moved timer/subscribe to app.component * Remove unrelated refactoring * Remaining feedback * Add paymentMethodWarningsService tests * Thomas' feedback * fix tests * Use barrel file imports * Make banner work with new vault navigation * Matt's feedback
This commit is contained in:
parent
c8e36b6c24
commit
7cfe862aa6
@ -115,10 +115,9 @@
|
|||||||
>
|
>
|
||||||
{{ "accessingUsingProvider" | i18n: organization.providerName }}
|
{{ "accessingUsingProvider" | i18n: organization.providerName }}
|
||||||
</bit-banner>
|
</bit-banner>
|
||||||
<app-payment-method-banners
|
<app-payment-method-warnings
|
||||||
*ngIf="false"
|
*ngIf="showPaymentMethodWarningBanners$ | async"
|
||||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
></app-payment-method-warnings>
|
||||||
></app-payment-method-banners>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
@ -15,10 +15,12 @@ import {
|
|||||||
OrganizationService,
|
OrganizationService,
|
||||||
} 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { BannerModule, IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
import { BannerModule, IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { PaymentMethodBannersComponent } from "../../../components/payment-method-banners/payment-method-banners.component";
|
import { PaymentMethodWarningsModule } from "../../../billing/shared";
|
||||||
import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component";
|
import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component";
|
||||||
import { AdminConsoleLogo } from "../../icons/admin-console-logo";
|
import { AdminConsoleLogo } from "../../icons/admin-console-logo";
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo";
|
|||||||
NavigationModule,
|
NavigationModule,
|
||||||
OrgSwitcherComponent,
|
OrgSwitcherComponent,
|
||||||
BannerModule,
|
BannerModule,
|
||||||
PaymentMethodBannersComponent,
|
PaymentMethodWarningsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||||
@ -48,10 +50,16 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private _destroy = new Subject<void>();
|
private _destroy = new Subject<void>();
|
||||||
|
|
||||||
|
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
@ -4,16 +4,18 @@ import { DomSanitizer } from "@angular/platform-browser";
|
|||||||
import { NavigationEnd, Router } from "@angular/router";
|
import { NavigationEnd, Router } from "@angular/router";
|
||||||
import * as jq from "jquery";
|
import * as jq from "jquery";
|
||||||
import { IndividualConfig, ToastrService } from "ngx-toastr";
|
import { IndividualConfig, ToastrService } from "ngx-toastr";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { Subject, switchMap, takeUntil, timer } from "rxjs";
|
||||||
|
|
||||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
|
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
@ -45,6 +47,7 @@ import { RouterService } from "./core";
|
|||||||
|
|
||||||
const BroadcasterSubscriptionId = "AppComponent";
|
const BroadcasterSubscriptionId = "AppComponent";
|
||||||
const IdleTimeout = 60000 * 10; // 10 minutes
|
const IdleTimeout = 60000 * 10; // 10 minutes
|
||||||
|
const PaymentMethodWarningsRefresh = 60000; // 1 Minute
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-root",
|
selector: "app-root",
|
||||||
@ -55,6 +58,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
private idleTimer: number = null;
|
private idleTimer: number = null;
|
||||||
private isIdle = false;
|
private isIdle = false;
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
private paymentMethodWarningsRefresh$ = timer(0, PaymentMethodWarningsRefresh);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DOCUMENT) private document: Document,
|
@Inject(DOCUMENT) private document: Document,
|
||||||
@ -85,6 +89,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
private configService: ConfigServiceAbstraction,
|
private configService: ConfigServiceAbstraction,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
|
private paymentMethodWarningService: PaymentMethodWarningService,
|
||||||
|
private organizationService: OrganizationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -238,6 +244,21 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
new DisableSendPolicy(),
|
new DisableSendPolicy(),
|
||||||
new SendOptionsPolicy(),
|
new SendOptionsPolicy(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
this.paymentMethodWarningsRefresh$
|
||||||
|
.pipe(
|
||||||
|
switchMap(() => this.organizationService.memberOrganizations$),
|
||||||
|
switchMap(
|
||||||
|
async (organizations) =>
|
||||||
|
await Promise.all(
|
||||||
|
organizations.map((organization) =>
|
||||||
|
this.paymentMethodWarningService.update(organization.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@ -260,6 +281,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.passwordGenerationService.clear(),
|
this.passwordGenerationService.clear(),
|
||||||
this.keyConnectorService.clear(),
|
this.keyConnectorService.clear(),
|
||||||
this.biometricStateService.logout(userId as UserId),
|
this.biometricStateService.logout(userId as UserId),
|
||||||
|
this.paymentMethodWarningService.clear(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.searchService.clearIndex();
|
this.searchService.clearIndex();
|
||||||
|
@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core
|
|||||||
|
|
||||||
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 { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction";
|
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||||
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@ -34,7 +34,7 @@ export class AdjustPaymentComponent {
|
|||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
private billingBannerService: BillingBannerServiceAbstraction,
|
private paymentMethodWarningService: PaymentMethodWarningService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
@ -59,7 +59,7 @@ export class AdjustPaymentComponent {
|
|||||||
});
|
});
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
if (this.organizationId) {
|
if (this.organizationId) {
|
||||||
await this.billingBannerService.setPaymentMethodBannerState(this.organizationId, false);
|
await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId);
|
||||||
}
|
}
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
|
@ -3,3 +3,4 @@ export * from "./payment-method.component";
|
|||||||
export * from "./payment.component";
|
export * from "./payment.component";
|
||||||
export * from "./sm-subscribe.component";
|
export * from "./sm-subscribe.component";
|
||||||
export * from "./tax-info.component";
|
export * from "./tax-info.component";
|
||||||
|
export * from "./payment-method-warnings/payment-method-warnings.module";
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
<ng-container *ngFor="let warning of warnings$ | async">
|
||||||
|
<bit-banner
|
||||||
|
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||||
|
bannerType="warning"
|
||||||
|
(onClose)="closeWarning(warning.organizationId)"
|
||||||
|
>
|
||||||
|
{{ "maintainYourSubscription" | i18n: warning.organizationName }}
|
||||||
|
<a
|
||||||
|
bitLink
|
||||||
|
linkType="contrast"
|
||||||
|
[routerLink]="['/organizations', warning.organizationId, 'billing', 'payment-method']"
|
||||||
|
>{{ "addAPaymentMethod" | i18n }}</a
|
||||||
|
>.
|
||||||
|
</bit-banner>
|
||||||
|
</ng-container>
|
@ -0,0 +1,33 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||||
|
|
||||||
|
type Warning = {
|
||||||
|
organizationId: string;
|
||||||
|
organizationName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-payment-method-warnings",
|
||||||
|
templateUrl: "payment-method-warnings.component.html",
|
||||||
|
})
|
||||||
|
export class PaymentMethodWarningsComponent {
|
||||||
|
constructor(private paymentMethodWarningService: PaymentMethodWarningService) {}
|
||||||
|
|
||||||
|
protected warnings$: Observable<Warning[]> =
|
||||||
|
this.paymentMethodWarningService.paymentMethodWarnings$.pipe(
|
||||||
|
map((warnings) =>
|
||||||
|
Object.entries(warnings ?? [])
|
||||||
|
.filter(([_, warning]) => warning.risksSubscriptionFailure && !warning.acknowledged)
|
||||||
|
.map(([organizationId, { organizationName }]) => ({
|
||||||
|
organizationId,
|
||||||
|
organizationName,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
protected async closeWarning(organizationId: string): Promise<void> {
|
||||||
|
await this.paymentMethodWarningService.acknowledge(organizationId);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { BannerModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../../shared";
|
||||||
|
|
||||||
|
import { PaymentMethodWarningsComponent } from "./payment-method-warnings.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [BannerModule, SharedModule],
|
||||||
|
declarations: [PaymentMethodWarningsComponent],
|
||||||
|
exports: [PaymentMethodWarningsComponent],
|
||||||
|
})
|
||||||
|
export class PaymentMethodWarningsModule {}
|
@ -1,15 +0,0 @@
|
|||||||
<ng-container *ngFor="let banner of banners$ | async">
|
|
||||||
<bit-banner
|
|
||||||
*ngIf="banner.visible"
|
|
||||||
bannerType="warning"
|
|
||||||
(onClose)="closeBanner(banner.organizationId)"
|
|
||||||
>
|
|
||||||
{{ "maintainYourSubscription" | i18n: banner.organizationName }}
|
|
||||||
<a
|
|
||||||
bitLink
|
|
||||||
linkType="contrast"
|
|
||||||
[routerLink]="['/organizations', banner.organizationId, 'billing', 'payment-method']"
|
|
||||||
>{{ "addAPaymentMethod" | i18n }}</a
|
|
||||||
>.
|
|
||||||
</bit-banner>
|
|
||||||
</ng-container>
|
|
@ -1,76 +0,0 @@
|
|||||||
import { Component } from "@angular/core";
|
|
||||||
import { combineLatest, Observable, switchMap } from "rxjs";
|
|
||||||
|
|
||||||
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
|
||||||
import {
|
|
||||||
canAccessAdmin,
|
|
||||||
OrganizationService,
|
|
||||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
|
||||||
import { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { BannerModule } from "@bitwarden/components";
|
|
||||||
|
|
||||||
import { SharedModule } from "../../shared/shared.module";
|
|
||||||
|
|
||||||
type PaymentMethodBannerData = {
|
|
||||||
organizationId: string;
|
|
||||||
organizationName: string;
|
|
||||||
visible: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
selector: "app-payment-method-banners",
|
|
||||||
templateUrl: "payment-method-banners.component.html",
|
|
||||||
imports: [BannerModule, SharedModule],
|
|
||||||
})
|
|
||||||
export class PaymentMethodBannersComponent {
|
|
||||||
constructor(
|
|
||||||
private billingBannerService: BillingBannerServiceAbstraction,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private organizationService: OrganizationService,
|
|
||||||
private organizationApiService: OrganizationApiService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private organizations$ = this.organizationService.memberOrganizations$.pipe(
|
|
||||||
canAccessAdmin(this.i18nService),
|
|
||||||
);
|
|
||||||
|
|
||||||
protected banners$: Observable<PaymentMethodBannerData[]> = combineLatest([
|
|
||||||
this.organizations$,
|
|
||||||
this.billingBannerService.paymentMethodBannerStates$,
|
|
||||||
]).pipe(
|
|
||||||
switchMap(async ([organizations, paymentMethodBannerStates]) => {
|
|
||||||
return await Promise.all(
|
|
||||||
organizations.map(async (organization) => {
|
|
||||||
const matchingBanner = paymentMethodBannerStates.find(
|
|
||||||
(banner) => banner.organizationId === organization.id,
|
|
||||||
);
|
|
||||||
if (matchingBanner !== null && matchingBanner !== undefined) {
|
|
||||||
return {
|
|
||||||
organizationId: organization.id,
|
|
||||||
organizationName: organization.name,
|
|
||||||
visible: matchingBanner.visible,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const response = await this.organizationApiService.risksSubscriptionFailure(
|
|
||||||
organization.id,
|
|
||||||
);
|
|
||||||
await this.billingBannerService.setPaymentMethodBannerState(
|
|
||||||
organization.id,
|
|
||||||
response.risksSubscriptionFailure,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
organizationId: organization.id,
|
|
||||||
organizationName: organization.name,
|
|
||||||
visible: response.risksSubscriptionFailure,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
protected async closeBanner(organizationId: string): Promise<void> {
|
|
||||||
await this.billingBannerService.setPaymentMethodBannerState(organizationId, false);
|
|
||||||
}
|
|
||||||
}
|
|
@ -33,9 +33,8 @@
|
|||||||
></bit-nav-item>
|
></bit-nav-item>
|
||||||
</bit-nav-group>
|
</bit-nav-group>
|
||||||
</nav>
|
</nav>
|
||||||
<app-payment-method-banners
|
<app-payment-method-warnings
|
||||||
*ngIf="false"
|
*ngIf="showPaymentMethodWarningBanners$ | async"
|
||||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
></app-payment-method-warnings>
|
||||||
></app-payment-method-banners>
|
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</bit-layout>
|
</bit-layout>
|
||||||
|
@ -5,13 +5,15 @@ import { RouterModule } from "@angular/router";
|
|||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
|
import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { PaymentMethodBannersComponent } from "../components/payment-method-banners/payment-method-banners.component";
|
import { PaymentMethodWarningsModule } from "../billing/shared";
|
||||||
|
|
||||||
import { PasswordManagerLogo } from "./password-manager-logo";
|
import { PasswordManagerLogo } from "./password-manager-logo";
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ const BroadcasterSubscriptionId = "UserLayoutComponent";
|
|||||||
LayoutComponent,
|
LayoutComponent,
|
||||||
IconModule,
|
IconModule,
|
||||||
NavigationModule,
|
NavigationModule,
|
||||||
PaymentMethodBannersComponent,
|
PaymentMethodWarningsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class UserLayoutComponent implements OnInit, OnDestroy {
|
export class UserLayoutComponent implements OnInit, OnDestroy {
|
||||||
@ -36,6 +38,11 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
|
|||||||
hasFamilySponsorshipAvailable: boolean;
|
hasFamilySponsorshipAvailable: boolean;
|
||||||
hideSubscription: boolean;
|
hideSubscription: boolean;
|
||||||
|
|
||||||
|
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
@ -44,6 +51,7 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
|
|||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
@ -58,8 +58,8 @@ import { UpdatePasswordComponent } from "../auth/update-password.component";
|
|||||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||||
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
|
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
|
||||||
import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component";
|
import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component";
|
||||||
|
import { PaymentMethodWarningsModule } from "../billing/shared";
|
||||||
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
|
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
|
||||||
import { PaymentMethodBannersComponent } from "../components/payment-method-banners/payment-method-banners.component";
|
|
||||||
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
|
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
|
||||||
import { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
|
import { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
|
||||||
import { HeaderModule } from "../layouts/header/header.module";
|
import { HeaderModule } from "../layouts/header/header.module";
|
||||||
@ -106,12 +106,12 @@ import { SharedModule } from "./shared.module";
|
|||||||
PipesModule,
|
PipesModule,
|
||||||
PasswordCalloutComponent,
|
PasswordCalloutComponent,
|
||||||
DangerZoneComponent,
|
DangerZoneComponent,
|
||||||
PaymentMethodBannersComponent,
|
|
||||||
LayoutComponent,
|
LayoutComponent,
|
||||||
NavigationModule,
|
NavigationModule,
|
||||||
HeaderModule,
|
HeaderModule,
|
||||||
OrganizationLayoutComponent,
|
OrganizationLayoutComponent,
|
||||||
UserLayoutComponent,
|
UserLayoutComponent,
|
||||||
|
PaymentMethodWarningsModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AcceptFamilySponsorshipComponent,
|
AcceptFamilySponsorshipComponent,
|
||||||
|
@ -24,9 +24,8 @@
|
|||||||
*ngIf="showSettingsTab"
|
*ngIf="showSettingsTab"
|
||||||
></bit-nav-item>
|
></bit-nav-item>
|
||||||
</nav>
|
</nav>
|
||||||
<app-payment-method-banners
|
<app-payment-method-warnings
|
||||||
*ngIf="false"
|
*ngIf="showPaymentMethodWarningBanners$ | async"
|
||||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
></app-payment-method-warnings>
|
||||||
></app-payment-method-banners>
|
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</bit-layout>
|
</bit-layout>
|
||||||
|
@ -5,9 +5,11 @@ import { ActivatedRoute, RouterModule } from "@angular/router";
|
|||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||||
import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo";
|
import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo";
|
||||||
import { PaymentMethodBannersComponent } from "@bitwarden/web-vault/app/components/payment-method-banners/payment-method-banners.component";
|
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "providers-layout",
|
selector: "providers-layout",
|
||||||
@ -20,7 +22,7 @@ import { PaymentMethodBannersComponent } from "@bitwarden/web-vault/app/componen
|
|||||||
LayoutComponent,
|
LayoutComponent,
|
||||||
IconModule,
|
IconModule,
|
||||||
NavigationModule,
|
NavigationModule,
|
||||||
PaymentMethodBannersComponent,
|
PaymentMethodWarningsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
@ -30,9 +32,15 @@ export class ProvidersLayoutComponent {
|
|||||||
provider: Provider;
|
provider: Provider;
|
||||||
private providerId: string;
|
private providerId: string;
|
||||||
|
|
||||||
|
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private providerService: ProviderService,
|
private providerService: ProviderService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
@ -5,7 +5,7 @@ import { FormsModule } from "@angular/forms";
|
|||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { SearchModule } from "@bitwarden/components";
|
import { SearchModule } from "@bitwarden/components";
|
||||||
import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing";
|
import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing";
|
||||||
import { PaymentMethodBannersComponent } from "@bitwarden/web-vault/app/components/payment-method-banners/payment-method-banners.component";
|
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
||||||
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
||||||
|
|
||||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||||
@ -33,9 +33,9 @@ import { SetupComponent } from "./setup/setup.component";
|
|||||||
JslibModule,
|
JslibModule,
|
||||||
ProvidersRoutingModule,
|
ProvidersRoutingModule,
|
||||||
OrganizationPlansComponent,
|
OrganizationPlansComponent,
|
||||||
PaymentMethodBannersComponent,
|
|
||||||
SearchModule,
|
SearchModule,
|
||||||
ProvidersLayoutComponent,
|
ProvidersLayoutComponent,
|
||||||
|
PaymentMethodWarningsModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AcceptProviderComponent,
|
AcceptProviderComponent,
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<app-payment-method-banners *ngIf="false"></app-payment-method-banners>
|
<app-payment-method-warnings
|
||||||
|
*ngIf="showPaymentMethodWarningBanners$ | async"
|
||||||
|
></app-payment-method-warnings>
|
||||||
<div class="container page-content">
|
<div class="container page-content">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>{{ "setupProvider" | i18n }}</h1>
|
<h1>{{ "setupProvider" | i18n }}</h1>
|
||||||
|
@ -4,6 +4,8 @@ import { first } from "rxjs/operators";
|
|||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
|
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -27,6 +29,11 @@ export class SetupComponent implements OnInit {
|
|||||||
name: string;
|
name: string;
|
||||||
billingEmail: string;
|
billingEmail: string;
|
||||||
|
|
||||||
|
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
@ -36,6 +43,7 @@ export class SetupComponent implements OnInit {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
@ -91,11 +91,11 @@ import {
|
|||||||
BadgeSettingsService,
|
BadgeSettingsService,
|
||||||
} from "@bitwarden/common/autofill/services/badge-settings.service";
|
} from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||||
import { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction";
|
|
||||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||||
|
import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||||
import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service";
|
import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service";
|
||||||
import { BillingBannerService } from "@bitwarden/common/billing/services/billing-banner.service";
|
|
||||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||||
|
import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||||
@ -920,11 +920,6 @@ import { ModalService } from "./modal.service";
|
|||||||
DerivedStateProvider,
|
DerivedStateProvider,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: BillingBannerServiceAbstraction,
|
|
||||||
useClass: BillingBannerService,
|
|
||||||
deps: [StateProvider],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: OrganizationBillingServiceAbstraction,
|
provide: OrganizationBillingServiceAbstraction,
|
||||||
useClass: OrganizationBillingService,
|
useClass: OrganizationBillingService,
|
||||||
@ -933,6 +928,8 @@ import { ModalService } from "./modal.service";
|
|||||||
EncryptService,
|
EncryptService,
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
OrganizationApiServiceAbstraction,
|
OrganizationApiServiceAbstraction,
|
||||||
|
OrganizationServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -969,6 +966,11 @@ import { ModalService } from "./modal.service";
|
|||||||
useClass: BillingApiService,
|
useClass: BillingApiService,
|
||||||
deps: [ApiServiceAbstraction],
|
deps: [ApiServiceAbstraction],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: PaymentMethodWarningsServiceAbstraction,
|
||||||
|
useClass: PaymentMethodWarningsService,
|
||||||
|
deps: [BillingApiServiceAbstraction, StateProvider],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class JslibServicesModule {}
|
export class JslibServicesModule {}
|
||||||
|
@ -9,7 +9,6 @@ import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/reques
|
|||||||
import { PaymentRequest } from "../../../billing/models/request/payment.request";
|
import { PaymentRequest } from "../../../billing/models/request/payment.request";
|
||||||
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
|
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
|
||||||
import { BillingResponse } from "../../../billing/models/response/billing.response";
|
import { BillingResponse } from "../../../billing/models/response/billing.response";
|
||||||
import { OrganizationRisksSubscriptionFailureResponse } from "../../../billing/models/response/organization-risks-subscription-failure.response";
|
|
||||||
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
||||||
import { PaymentResponse } from "../../../billing/models/response/payment.response";
|
import { PaymentResponse } from "../../../billing/models/response/payment.response";
|
||||||
import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response";
|
import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response";
|
||||||
@ -79,6 +78,5 @@ export class OrganizationApiServiceAbstraction {
|
|||||||
id: string,
|
id: string,
|
||||||
request: OrganizationCollectionManagementUpdateRequest,
|
request: OrganizationCollectionManagementUpdateRequest,
|
||||||
) => Promise<OrganizationResponse>;
|
) => Promise<OrganizationResponse>;
|
||||||
risksSubscriptionFailure: (id: string) => Promise<OrganizationRisksSubscriptionFailureResponse>;
|
|
||||||
enableCollectionEnhancements: (id: string) => Promise<void>;
|
enableCollectionEnhancements: (id: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/reques
|
|||||||
import { PaymentRequest } from "../../../billing/models/request/payment.request";
|
import { PaymentRequest } from "../../../billing/models/request/payment.request";
|
||||||
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
|
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
|
||||||
import { BillingResponse } from "../../../billing/models/response/billing.response";
|
import { BillingResponse } from "../../../billing/models/response/billing.response";
|
||||||
import { OrganizationRisksSubscriptionFailureResponse } from "../../../billing/models/response/organization-risks-subscription-failure.response";
|
|
||||||
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
||||||
import { PaymentResponse } from "../../../billing/models/response/payment.response";
|
import { PaymentResponse } from "../../../billing/models/response/payment.response";
|
||||||
import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response";
|
import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response";
|
||||||
@ -344,20 +343,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async risksSubscriptionFailure(
|
|
||||||
id: string,
|
|
||||||
): Promise<OrganizationRisksSubscriptionFailureResponse> {
|
|
||||||
const r = await this.apiService.send(
|
|
||||||
"GET",
|
|
||||||
"/organizations/" + id + "/risks-subscription-failure",
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
return new OrganizationRisksSubscriptionFailureResponse(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
async enableCollectionEnhancements(id: string): Promise<void> {
|
async enableCollectionEnhancements(id: string): Promise<void> {
|
||||||
await this.apiService.send(
|
await this.apiService.send(
|
||||||
"POST",
|
"POST",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||||
|
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
|
||||||
|
|
||||||
export abstract class BillingApiServiceAbstraction {
|
export abstract class BillingApiServiceAbstraction {
|
||||||
cancelOrganizationSubscription: (
|
cancelOrganizationSubscription: (
|
||||||
@ -6,4 +7,5 @@ export abstract class BillingApiServiceAbstraction {
|
|||||||
request: SubscriptionCancellationRequest,
|
request: SubscriptionCancellationRequest,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>;
|
cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>;
|
||||||
|
getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { Observable } from "rxjs";
|
|
||||||
|
|
||||||
export class BillingBannerServiceAbstraction {
|
|
||||||
paymentMethodBannerStates$: Observable<{ organizationId: string; visible: boolean }[]>;
|
|
||||||
setPaymentMethodBannerState: (organizationId: string, visible: boolean) => Promise<void>;
|
|
||||||
}
|
|
@ -0,0 +1,31 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { PaymentMethodWarning } from "../models/domain/payment-method-warning";
|
||||||
|
|
||||||
|
export abstract class PaymentMethodWarningsServiceAbstraction {
|
||||||
|
/**
|
||||||
|
* An {@link Observable} record in the {@link ActiveUserState} of the user's organization IDs each mapped to their respective {@link PaymentMethodWarning}.
|
||||||
|
*/
|
||||||
|
paymentMethodWarnings$: Observable<Record<string, PaymentMethodWarning>>;
|
||||||
|
/**
|
||||||
|
* Updates the {@link ActiveUserState} by setting `acknowledged` to `true` for the {@link PaymentMethodWarning} represented by the provided organization ID.
|
||||||
|
* @param organizationId - The ID of the organization whose warning you'd like to acknowledge.
|
||||||
|
*/
|
||||||
|
acknowledge: (organizationId: string) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Updates the {@link ActiveUserState} by setting `risksSubscriptionFailure` to `false` for the {@link PaymentMethodWarning} represented by the provided organization ID.
|
||||||
|
* @param organizationId - The ID of the organization whose subscription risk you'd like to remove.
|
||||||
|
*/
|
||||||
|
removeSubscriptionRisk: (organizationId: string) => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Clears the {@link PaymentMethodWarning} record from the {@link ActiveUserState}.
|
||||||
|
*/
|
||||||
|
clear: () => Promise<void>;
|
||||||
|
/**
|
||||||
|
* Tries to retrieve the {@link PaymentMethodWarning} for the provided organization ID from the {@link ActiveUserState}.
|
||||||
|
* If the warning does not exist, or if the warning has been in state for longer than a week, fetches the current {@link OrganizationBillingStatusResponse} for the organization
|
||||||
|
* from the API and uses it to update the warning in state.
|
||||||
|
* @param organizationId - The ID of the organization whose {@link PaymentMethodWarning} you'd like to update.
|
||||||
|
*/
|
||||||
|
update: (organizationId: string) => Promise<void>;
|
||||||
|
}
|
13
libs/common/src/billing/models/billing-keys.state.ts
Normal file
13
libs/common/src/billing/models/billing-keys.state.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { BILLING_DISK, KeyDefinition } from "../../platform/state";
|
||||||
|
import { PaymentMethodWarning } from "../models/domain/payment-method-warning";
|
||||||
|
|
||||||
|
export const PAYMENT_METHOD_WARNINGS_KEY = KeyDefinition.record<PaymentMethodWarning>(
|
||||||
|
BILLING_DISK,
|
||||||
|
"paymentMethodWarnings",
|
||||||
|
{
|
||||||
|
deserializer: (warnings) => ({
|
||||||
|
...warnings,
|
||||||
|
savedAt: new Date(warnings.savedAt),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
@ -0,0 +1,6 @@
|
|||||||
|
export type PaymentMethodWarning = {
|
||||||
|
organizationName: string;
|
||||||
|
risksSubscriptionFailure: boolean;
|
||||||
|
acknowledged: boolean;
|
||||||
|
savedAt: Date;
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
|
|
||||||
|
export class OrganizationBillingStatusResponse extends BaseResponse {
|
||||||
|
organizationId: string;
|
||||||
|
organizationName: string;
|
||||||
|
risksSubscriptionFailure: boolean;
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
|
||||||
|
this.organizationId = this.getResponseProperty("OrganizationId");
|
||||||
|
this.organizationName = this.getResponseProperty("OrganizationName");
|
||||||
|
this.risksSubscriptionFailure = this.getResponseProperty("RisksSubscriptionFailure");
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction";
|
import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction";
|
||||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||||
|
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
|
||||||
|
|
||||||
export class BillingApiService implements BillingApiServiceAbstraction {
|
export class BillingApiService implements BillingApiServiceAbstraction {
|
||||||
constructor(private apiService: ApiService) {}
|
constructor(private apiService: ApiService) {}
|
||||||
@ -21,4 +22,16 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
|||||||
cancelPremiumUserSubscription(request: SubscriptionCancellationRequest): Promise<void> {
|
cancelPremiumUserSubscription(request: SubscriptionCancellationRequest): Promise<void> {
|
||||||
return this.apiService.send("POST", "/accounts/churn-premium", request, true, false);
|
return this.apiService.send("POST", "/accounts/churn-premium", request, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBillingStatus(id: string): Promise<OrganizationBillingStatusResponse> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/organizations/" + id + "/billing-status",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new OrganizationBillingStatusResponse(r);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
import { map, Observable } from "rxjs";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ActiveUserState,
|
|
||||||
BILLING_BANNERS_DISK,
|
|
||||||
KeyDefinition,
|
|
||||||
StateProvider,
|
|
||||||
} from "../../platform/state";
|
|
||||||
import { BillingBannerServiceAbstraction } from "../abstractions/billing-banner.service.abstraction";
|
|
||||||
|
|
||||||
const PAYMENT_METHOD_BANNERS_KEY = KeyDefinition.record<boolean>(
|
|
||||||
BILLING_BANNERS_DISK,
|
|
||||||
"paymentMethodBanners",
|
|
||||||
{
|
|
||||||
deserializer: (b) => b,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export class BillingBannerService implements BillingBannerServiceAbstraction {
|
|
||||||
private paymentMethodBannerStates: ActiveUserState<Record<string, boolean>>;
|
|
||||||
paymentMethodBannerStates$: Observable<{ organizationId: string; visible: boolean }[]>;
|
|
||||||
|
|
||||||
constructor(private stateProvider: StateProvider) {
|
|
||||||
this.paymentMethodBannerStates = this.stateProvider.getActive(PAYMENT_METHOD_BANNERS_KEY);
|
|
||||||
this.paymentMethodBannerStates$ = this.paymentMethodBannerStates.state$.pipe(
|
|
||||||
map((billingBannerStates) =>
|
|
||||||
!billingBannerStates
|
|
||||||
? []
|
|
||||||
: Object.entries(billingBannerStates).map(([organizationId, visible]) => ({
|
|
||||||
organizationId,
|
|
||||||
visible,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setPaymentMethodBannerState(organizationId: string, visibility: boolean): Promise<void> {
|
|
||||||
await this.paymentMethodBannerStates.update((states) => {
|
|
||||||
states ??= {};
|
|
||||||
states[organizationId] = visibility;
|
|
||||||
return states;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,186 @@
|
|||||||
|
import { any, mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||||
|
import { FakeActiveUserState } from "../../../spec/fake-state";
|
||||||
|
import { Utils } from "../../platform/misc/utils";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
import { BillingApiServiceAbstraction as BillingApiService } from "../abstractions/billilng-api.service.abstraction";
|
||||||
|
import { PAYMENT_METHOD_WARNINGS_KEY } from "../models/billing-keys.state";
|
||||||
|
import { PaymentMethodWarning } from "../models/domain/payment-method-warning";
|
||||||
|
import { OrganizationBillingStatusResponse } from "../models/response/organization-billing-status.response";
|
||||||
|
|
||||||
|
import { PaymentMethodWarningsService } from "./payment-method-warnings.service";
|
||||||
|
|
||||||
|
describe("Payment Method Warnings Service", () => {
|
||||||
|
let paymentMethodWarningsService: PaymentMethodWarningsService;
|
||||||
|
let billingApiService: MockProxy<BillingApiService>;
|
||||||
|
|
||||||
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
let accountService: FakeAccountService;
|
||||||
|
let stateProvider: FakeStateProvider;
|
||||||
|
let activeUserState: FakeActiveUserState<Record<string, PaymentMethodWarning>>;
|
||||||
|
|
||||||
|
function getPastDate(daysAgo: number) {
|
||||||
|
const date = new Date();
|
||||||
|
date.setDate(date.getDate() - daysAgo);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBillingStatusResponse = (organizationId: string) =>
|
||||||
|
new OrganizationBillingStatusResponse({
|
||||||
|
OrganizationId: organizationId,
|
||||||
|
OrganizationName: "Teams Organization",
|
||||||
|
RisksSubscriptionFailure: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
accountService = mockAccountServiceWith(mockUserId);
|
||||||
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
|
activeUserState = stateProvider.activeUser.getFake(PAYMENT_METHOD_WARNINGS_KEY);
|
||||||
|
|
||||||
|
billingApiService = mock<BillingApiService>();
|
||||||
|
paymentMethodWarningsService = new PaymentMethodWarningsService(
|
||||||
|
billingApiService,
|
||||||
|
stateProvider,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("acknowledge", async () => {
|
||||||
|
const organizationId = "1";
|
||||||
|
const state: Record<string, PaymentMethodWarning> = {
|
||||||
|
[organizationId]: {
|
||||||
|
organizationName: "Teams Organization",
|
||||||
|
risksSubscriptionFailure: true,
|
||||||
|
acknowledged: false,
|
||||||
|
savedAt: getPastDate(3),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
activeUserState.nextState(state);
|
||||||
|
await paymentMethodWarningsService.acknowledge(organizationId);
|
||||||
|
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({
|
||||||
|
[organizationId]: {
|
||||||
|
...state[organizationId],
|
||||||
|
acknowledged: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clear", async () => {
|
||||||
|
const state: Record<string, PaymentMethodWarning> = {
|
||||||
|
"1": {
|
||||||
|
organizationName: "Teams Organization",
|
||||||
|
risksSubscriptionFailure: true,
|
||||||
|
acknowledged: false,
|
||||||
|
savedAt: getPastDate(3),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
activeUserState.nextState(state);
|
||||||
|
await paymentMethodWarningsService.clear();
|
||||||
|
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removeSubscriptionRisk", async () => {
|
||||||
|
const organizationId = "1";
|
||||||
|
const state: Record<string, PaymentMethodWarning> = {
|
||||||
|
[organizationId]: {
|
||||||
|
organizationName: "Teams Organization",
|
||||||
|
risksSubscriptionFailure: true,
|
||||||
|
acknowledged: false,
|
||||||
|
savedAt: getPastDate(3),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
activeUserState.nextState(state);
|
||||||
|
await paymentMethodWarningsService.removeSubscriptionRisk(organizationId);
|
||||||
|
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({
|
||||||
|
[organizationId]: {
|
||||||
|
...state[organizationId],
|
||||||
|
risksSubscriptionFailure: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
it("Does nothing if the stored payment method warning is less than a week old", async () => {
|
||||||
|
const organizationId = "1";
|
||||||
|
const state: Record<string, PaymentMethodWarning> = {
|
||||||
|
[organizationId]: {
|
||||||
|
organizationName: "Teams Organization",
|
||||||
|
risksSubscriptionFailure: true,
|
||||||
|
acknowledged: false,
|
||||||
|
savedAt: getPastDate(3),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
activeUserState.nextState(state);
|
||||||
|
await paymentMethodWarningsService.update(organizationId);
|
||||||
|
expect(billingApiService.getBillingStatus).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Retrieves the billing status from the API and uses it to update the state if the state is null", async () => {
|
||||||
|
const organizationId = "1";
|
||||||
|
activeUserState.nextState(null);
|
||||||
|
billingApiService.getBillingStatus.mockResolvedValue(
|
||||||
|
getBillingStatusResponse(organizationId),
|
||||||
|
);
|
||||||
|
await paymentMethodWarningsService.update(organizationId);
|
||||||
|
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({
|
||||||
|
[organizationId]: {
|
||||||
|
organizationName: "Teams Organization",
|
||||||
|
risksSubscriptionFailure: true,
|
||||||
|
acknowledged: false,
|
||||||
|
savedAt: any(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(billingApiService.getBillingStatus).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Retrieves the billing status from the API and uses it to update the state if the stored warning is null", async () => {
|
||||||
|
const organizationId = "1";
|
||||||
|
activeUserState.nextState({
|
||||||
|
[organizationId]: null,
|
||||||
|
});
|
||||||
|
billingApiService.getBillingStatus.mockResolvedValue(
|
||||||
|
getBillingStatusResponse(organizationId),
|
||||||
|
);
|
||||||
|
await paymentMethodWarningsService.update(organizationId);
|
||||||
|
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({
|
||||||
|
[organizationId]: {
|
||||||
|
organizationName: "Teams Organization",
|
||||||
|
risksSubscriptionFailure: true,
|
||||||
|
acknowledged: false,
|
||||||
|
savedAt: any(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(billingApiService.getBillingStatus).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Retrieves the billing status from the API and uses it to update the state if the stored warning is older than a week", async () => {
|
||||||
|
const organizationId = "1";
|
||||||
|
activeUserState.nextState({
|
||||||
|
[organizationId]: {
|
||||||
|
organizationName: "Teams Organization",
|
||||||
|
risksSubscriptionFailure: false,
|
||||||
|
acknowledged: false,
|
||||||
|
savedAt: getPastDate(10),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
billingApiService.getBillingStatus.mockResolvedValue(
|
||||||
|
new OrganizationBillingStatusResponse({
|
||||||
|
OrganizationId: organizationId,
|
||||||
|
OrganizationName: "Teams Organization",
|
||||||
|
RisksSubscriptionFailure: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await paymentMethodWarningsService.update(organizationId);
|
||||||
|
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({
|
||||||
|
[organizationId]: {
|
||||||
|
organizationName: "Teams Organization",
|
||||||
|
risksSubscriptionFailure: true,
|
||||||
|
acknowledged: false,
|
||||||
|
savedAt: any(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(billingApiService.getBillingStatus).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,74 @@
|
|||||||
|
import { firstValueFrom, map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { ActiveUserState, StateProvider } from "../../platform/state";
|
||||||
|
import { BillingApiServiceAbstraction as BillingApiService } from "../abstractions/billilng-api.service.abstraction";
|
||||||
|
import { PaymentMethodWarningsServiceAbstraction } from "../abstractions/payment-method-warnings-service.abstraction";
|
||||||
|
import { PAYMENT_METHOD_WARNINGS_KEY } from "../models/billing-keys.state";
|
||||||
|
import { PaymentMethodWarning } from "../models/domain/payment-method-warning";
|
||||||
|
|
||||||
|
export class PaymentMethodWarningsService implements PaymentMethodWarningsServiceAbstraction {
|
||||||
|
private paymentMethodWarningsState: ActiveUserState<Record<string, PaymentMethodWarning>>;
|
||||||
|
paymentMethodWarnings$: Observable<Record<string, PaymentMethodWarning>>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private billingApiService: BillingApiService,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {
|
||||||
|
this.paymentMethodWarningsState = this.stateProvider.getActive(PAYMENT_METHOD_WARNINGS_KEY);
|
||||||
|
this.paymentMethodWarnings$ = this.paymentMethodWarningsState.state$;
|
||||||
|
}
|
||||||
|
|
||||||
|
async acknowledge(organizationId: string): Promise<void> {
|
||||||
|
await this.paymentMethodWarningsState.update((state) => {
|
||||||
|
const current = state[organizationId];
|
||||||
|
state[organizationId] = {
|
||||||
|
...current,
|
||||||
|
acknowledged: true,
|
||||||
|
};
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeSubscriptionRisk(organizationId: string): Promise<void> {
|
||||||
|
await this.paymentMethodWarningsState.update((state) => {
|
||||||
|
const current = state[organizationId];
|
||||||
|
state[organizationId] = {
|
||||||
|
...current,
|
||||||
|
risksSubscriptionFailure: false,
|
||||||
|
};
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
await this.paymentMethodWarningsState.update(() => ({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(organizationId: string): Promise<void> {
|
||||||
|
const warning = await firstValueFrom(
|
||||||
|
this.paymentMethodWarningsState.state$.pipe(
|
||||||
|
map((state) => (!state ? null : state[organizationId])),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!warning || warning.savedAt < this.getOneWeekAgo()) {
|
||||||
|
const { organizationName, risksSubscriptionFailure } =
|
||||||
|
await this.billingApiService.getBillingStatus(organizationId);
|
||||||
|
await this.paymentMethodWarningsState.update((state) => {
|
||||||
|
state ??= {};
|
||||||
|
state[organizationId] = {
|
||||||
|
organizationName,
|
||||||
|
risksSubscriptionFailure,
|
||||||
|
acknowledged: false,
|
||||||
|
savedAt: new Date(),
|
||||||
|
};
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOneWeekAgo = (): Date => {
|
||||||
|
const date = new Date();
|
||||||
|
date.setDate(date.getDate() - 7);
|
||||||
|
return date;
|
||||||
|
};
|
||||||
|
}
|
@ -8,6 +8,7 @@ export enum FeatureFlag {
|
|||||||
KeyRotationImprovements = "key-rotation-improvements",
|
KeyRotationImprovements = "key-rotation-improvements",
|
||||||
FlexibleCollectionsMigration = "flexible-collections-migration",
|
FlexibleCollectionsMigration = "flexible-collections-migration",
|
||||||
AC1607_PresentUserOffboardingSurvey = "AC-1607_present-user-offboarding-survey",
|
AC1607_PresentUserOffboardingSurvey = "AC-1607_present-user-offboarding-survey",
|
||||||
|
ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||||
|
@ -19,8 +19,6 @@ import { StateDefinition } from "./state-definition";
|
|||||||
|
|
||||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||||
|
|
||||||
export const BILLING_BANNERS_DISK = new StateDefinition("billingBanners", "disk");
|
|
||||||
|
|
||||||
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||||
export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory");
|
||||||
|
|
||||||
@ -42,6 +40,8 @@ export const ORGANIZATIONS_DISK = new StateDefinition("organizations", "disk");
|
|||||||
export const POLICIES_DISK = new StateDefinition("policies", "disk");
|
export const POLICIES_DISK = new StateDefinition("policies", "disk");
|
||||||
export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
|
export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
|
||||||
|
|
||||||
|
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||||
|
|
||||||
export const FOLDER_DISK = new StateDefinition("folder", "disk", { web: "memory" });
|
export const FOLDER_DISK = new StateDefinition("folder", "disk", { web: "memory" });
|
||||||
|
|
||||||
export const SYNC_STATE = new StateDefinition("sync", "disk", { web: "memory" });
|
export const SYNC_STATE = new StateDefinition("sync", "disk", { web: "memory" });
|
||||||
|
Loading…
Reference in New Issue
Block a user