mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00: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:
@@ -115,10 +115,9 @@
|
||||
>
|
||||
{{ "accessingUsingProvider" | i18n: organization.providerName }}
|
||||
</bit-banner>
|
||||
<app-payment-method-banners
|
||||
*ngIf="false"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
></app-payment-method-banners>
|
||||
<app-payment-method-warnings
|
||||
*ngIf="showPaymentMethodWarningBanners$ | async"
|
||||
></app-payment-method-warnings>
|
||||
</ng-container>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -15,10 +15,12 @@ import {
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
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 { 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 { AdminConsoleLogo } from "../../icons/admin-console-logo";
|
||||
|
||||
@@ -35,7 +37,7 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo";
|
||||
NavigationModule,
|
||||
OrgSwitcherComponent,
|
||||
BannerModule,
|
||||
PaymentMethodBannersComponent,
|
||||
PaymentMethodWarningsModule,
|
||||
],
|
||||
})
|
||||
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
@@ -48,10 +50,16 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||
false,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -4,16 +4,18 @@ import { DomSanitizer } from "@angular/platform-browser";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import * as jq from "jquery";
|
||||
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 { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.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 { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
@@ -45,6 +47,7 @@ import { RouterService } from "./core";
|
||||
|
||||
const BroadcasterSubscriptionId = "AppComponent";
|
||||
const IdleTimeout = 60000 * 10; // 10 minutes
|
||||
const PaymentMethodWarningsRefresh = 60000; // 1 Minute
|
||||
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
@@ -55,6 +58,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private idleTimer: number = null;
|
||||
private isIdle = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
private paymentMethodWarningsRefresh$ = timer(0, PaymentMethodWarningsRefresh);
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
@@ -85,6 +89,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private configService: ConfigServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private paymentMethodWarningService: PaymentMethodWarningService,
|
||||
private organizationService: OrganizationService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -238,6 +244,21 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
new DisableSendPolicy(),
|
||||
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() {
|
||||
@@ -260,6 +281,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.passwordGenerationService.clear(),
|
||||
this.keyConnectorService.clear(),
|
||||
this.biometricStateService.logout(userId as UserId),
|
||||
this.paymentMethodWarningService.clear(),
|
||||
]);
|
||||
|
||||
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 { 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 { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -34,7 +34,7 @@ export class AdjustPaymentComponent {
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private billingBannerService: BillingBannerServiceAbstraction,
|
||||
private paymentMethodWarningService: PaymentMethodWarningService,
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
@@ -59,7 +59,7 @@ export class AdjustPaymentComponent {
|
||||
});
|
||||
await this.formPromise;
|
||||
if (this.organizationId) {
|
||||
await this.billingBannerService.setPaymentMethodBannerState(this.organizationId, false);
|
||||
await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId);
|
||||
}
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./payment-method.component";
|
||||
export * from "./payment.component";
|
||||
export * from "./sm-subscribe.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-group>
|
||||
</nav>
|
||||
<app-payment-method-banners
|
||||
*ngIf="false"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
></app-payment-method-banners>
|
||||
<app-payment-method-warnings
|
||||
*ngIf="showPaymentMethodWarningBanners$ | async"
|
||||
></app-payment-method-warnings>
|
||||
<router-outlet></router-outlet>
|
||||
</bit-layout>
|
||||
|
||||
@@ -5,13 +5,15 @@ import { RouterModule } from "@angular/router";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
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 { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
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";
|
||||
|
||||
@@ -28,7 +30,7 @@ const BroadcasterSubscriptionId = "UserLayoutComponent";
|
||||
LayoutComponent,
|
||||
IconModule,
|
||||
NavigationModule,
|
||||
PaymentMethodBannersComponent,
|
||||
PaymentMethodWarningsModule,
|
||||
],
|
||||
})
|
||||
export class UserLayoutComponent implements OnInit, OnDestroy {
|
||||
@@ -36,6 +38,11 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
|
||||
hasFamilySponsorshipAvailable: boolean;
|
||||
hideSubscription: boolean;
|
||||
|
||||
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||
false,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
@@ -44,6 +51,7 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
|
||||
private stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private syncService: SyncService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -58,8 +58,8 @@ import { UpdatePasswordComponent } from "../auth/update-password.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
|
||||
import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component";
|
||||
import { PaymentMethodWarningsModule } from "../billing/shared";
|
||||
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 { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
|
||||
import { HeaderModule } from "../layouts/header/header.module";
|
||||
@@ -106,12 +106,12 @@ import { SharedModule } from "./shared.module";
|
||||
PipesModule,
|
||||
PasswordCalloutComponent,
|
||||
DangerZoneComponent,
|
||||
PaymentMethodBannersComponent,
|
||||
LayoutComponent,
|
||||
NavigationModule,
|
||||
HeaderModule,
|
||||
OrganizationLayoutComponent,
|
||||
UserLayoutComponent,
|
||||
PaymentMethodWarningsModule,
|
||||
],
|
||||
declarations: [
|
||||
AcceptFamilySponsorshipComponent,
|
||||
|
||||
Reference in New Issue
Block a user