diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index b0df5fadd4..6dc0a93059 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1083,7 +1083,7 @@ "message": "1 GB encrypted storage for file attachments." }, "premiumSignUpEmergency": { - "message": "Emergency access" + "message": "Emergency access." }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." @@ -1115,6 +1115,9 @@ "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1124,6 +1127,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -3960,6 +3972,12 @@ "autoFillOnPageLoad": { "message": "Autofill on page load?" }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.html b/apps/browser/src/billing/popup/settings/premium-v2.component.html index 2a98cffb0e..7b781eafdb 100644 --- a/apps/browser/src/billing/popup/settings/premium-v2.component.html +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.html @@ -9,13 +9,13 @@

{{ "premiumFeatures" | i18n }}

-
+
  • {{ "ppremiumSignUpStorage" | i18n }}
  • - {{ "ppremiumSignUpTwoStepOptions" | i18n }} + {{ "premiumSignUpTwoStepOptions" | i18n }}
  • {{ "premiumSignUpEmergency" | i18n }} diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.ts b/apps/browser/src/billing/popup/settings/premium-v2.component.ts index 456aa6dc9a..ef4c39942a 100644 --- a/apps/browser/src/billing/popup/settings/premium-v2.component.ts +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.ts @@ -74,7 +74,7 @@ export class PremiumV2Component extends BasePremiumComponent { const formattedPrice = this.platformUtilsService.isSafari() ? thePrice.replace("$", "$$$") : thePrice; - this.priceString = i18nService.t("premiumPrice", formattedPrice); + this.priceString = i18nService.t("premiumPriceV2", formattedPrice); if (this.priceString.indexOf("%price%") > -1) { this.priceString = this.priceString.replace("%price%", thePrice); } diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 688e7e72a0..79a2df30e6 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -9,6 +9,7 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; +import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -91,7 +92,7 @@ import { SyncComponent } from "../vault/popup/settings/sync.component"; import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component"; import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; -import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils"; +import { extensionRefreshRedirect } from "./extension-refresh-route-utils"; import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; diff --git a/apps/browser/src/popup/extension-refresh-route-utils.ts b/apps/browser/src/popup/extension-refresh-route-utils.ts index 3c2ca33f86..9d45d7d656 100644 --- a/apps/browser/src/popup/extension-refresh-route-utils.ts +++ b/apps/browser/src/popup/extension-refresh-route-utils.ts @@ -1,32 +1,9 @@ -import { inject, Type } from "@angular/core"; -import { Route, Router, Routes, UrlTree } from "@angular/router"; +import { inject } from "@angular/core"; +import { Router, UrlTree } from "@angular/router"; -import { componentRouteSwap } from "@bitwarden/angular/utils/component-route-swap"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -/** - * Helper function to swap between two components based on the ExtensionRefresh feature flag. - * @param defaultComponent - The current non-refreshed component to render. - * @param refreshedComponent - The new refreshed component to render. - * @param options - The shared route options to apply to both components. - */ -export function extensionRefreshSwap( - defaultComponent: Type, - refreshedComponent: Type, - options: Route, -): Routes { - return componentRouteSwap( - defaultComponent, - refreshedComponent, - async () => { - const configService = inject(ConfigService); - return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); - }, - options, - ); -} - /** * Helper function to redirect to a new URL based on the ExtensionRefresh feature flag. * @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled. diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts new file mode 100644 index 0000000000..95288f6b41 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts @@ -0,0 +1,192 @@ +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; +import { ActivatedRoute, Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherFormConfig, CipherFormConfigService, CipherFormMode } from "@bitwarden/vault"; + +import { BrowserFido2UserInterfaceSession } from "../../../../../autofill/fido2/services/browser-fido2-user-interface.service"; +import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; +import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; +import { PopupCloseWarningService } from "../../../../../popup/services/popup-close-warning.service"; + +import { AddEditV2Component } from "./add-edit-v2.component"; + +// 'qrcode-parser' is used by `BrowserTotpCaptureService` but is an es6 module that jest can't compile. +// Mock the entire module here to prevent jest from throwing an error. I wasn't able to find a way to mock the +// `BrowserTotpCaptureService` where jest would not load the file in the first place. +jest.mock("qrcode-parser", () => {}); + +describe("AddEditV2Component", () => { + let component: AddEditV2Component; + let fixture: ComponentFixture; + + const buildConfigResponse = { originalCipher: {} } as CipherFormConfig; + const buildConfig = jest.fn((mode: CipherFormMode) => + Promise.resolve({ mode, ...buildConfigResponse }), + ); + const queryParams$ = new BehaviorSubject({}); + const disable = jest.fn(); + const navigate = jest.fn(); + const back = jest.fn().mockResolvedValue(null); + + beforeEach(async () => { + buildConfig.mockClear(); + disable.mockClear(); + navigate.mockClear(); + back.mockClear(); + + await TestBed.configureTestingModule({ + imports: [AddEditV2Component], + providers: [ + { provide: PlatformUtilsService, useValue: mock() }, + { provide: ConfigService, useValue: mock() }, + { provide: PopupRouterCacheService, useValue: { back } }, + { provide: PopupCloseWarningService, useValue: { disable } }, + { provide: Router, useValue: { navigate } }, + { provide: ActivatedRoute, useValue: { queryParams: queryParams$ } }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }) + .overrideProvider(CipherFormConfigService, { + useValue: { + buildConfig, + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(AddEditV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("query params", () => { + describe("mode", () => { + it("sets mode to `add` when no `cipherId` is provided", fakeAsync(() => { + queryParams$.next({}); + + tick(); + + expect(buildConfig.mock.lastCall[0]).toBe("add"); + expect(component.config.mode).toBe("add"); + })); + + it("sets mode to `edit` when `params.clone` is not provided", fakeAsync(() => { + queryParams$.next({ cipherId: "222-333-444-5555", clone: "true" }); + + tick(); + + expect(buildConfig.mock.lastCall[0]).toBe("clone"); + expect(component.config.mode).toBe("clone"); + })); + + it("sets mode to `edit` when `params.clone` is not provided", fakeAsync(() => { + buildConfigResponse.originalCipher = { edit: true } as Cipher; + queryParams$.next({ cipherId: "222-333-444-5555" }); + + tick(); + + expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(component.config.mode).toBe("edit"); + })); + + it("sets mode to `partial-edit` when `config.originalCipher.edit` is false", fakeAsync(() => { + buildConfigResponse.originalCipher = { edit: false } as Cipher; + queryParams$.next({ cipherId: "222-333-444-5555" }); + + tick(); + + expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(component.config.mode).toBe("partial-edit"); + })); + }); + }); + + describe("onCipherSaved", () => { + it("disables warning when in popout", async () => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValueOnce(true); + + await component.onCipherSaved({ id: "123-456-789" } as CipherView); + + expect(disable).toHaveBeenCalled(); + }); + + it("calls `confirmNewCredentialResponse` when in fido2 popout", async () => { + // @ts-expect-error - `inFido2PopoutWindow` is a private getter, mock the response here + // for the test rather than setting up the dependencies. + jest.spyOn(component, "inFido2PopoutWindow", "get").mockReturnValueOnce(true); + + await component.onCipherSaved({ id: "123-456-789" } as CipherView); + + expect(BrowserPopupUtils.inPopout).toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); + }); + + it("closes single action popout", async () => { + jest.spyOn(BrowserPopupUtils, "inSingleActionPopout").mockReturnValueOnce(true); + jest.spyOn(BrowserPopupUtils, "closeSingleActionPopout").mockResolvedValue(); + + await component.onCipherSaved({ id: "123-456-789" } as CipherView); + + expect(BrowserPopupUtils.closeSingleActionPopout).toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); + }); + + it("navigates to view-cipher for new ciphers", async () => { + component.config.mode = "add"; + + await component.onCipherSaved({ id: "123-456-789" } as CipherView); + + expect(navigate).toHaveBeenCalledWith(["/view-cipher"], { + replaceUrl: true, + queryParams: { cipherId: "123-456-789" }, + }); + expect(back).not.toHaveBeenCalled(); + }); + + it("navigates to view-cipher for edit ciphers", async () => { + component.config.mode = "edit"; + + await component.onCipherSaved({ id: "123-456-789" } as CipherView); + + expect(navigate).not.toHaveBeenCalled(); + expect(back).toHaveBeenCalled(); + }); + }); + + describe("handleBackButton", () => { + it("disables warning and aborts fido2 popout", async () => { + // @ts-expect-error - `inFido2PopoutWindow` is a private getter, mock the response here + // for the test rather than setting up the dependencies. + jest.spyOn(component, "inFido2PopoutWindow", "get").mockReturnValueOnce(true); + jest.spyOn(BrowserFido2UserInterfaceSession, "abortPopout"); + + await component.handleBackButton(); + + expect(disable).toHaveBeenCalled(); + expect(BrowserFido2UserInterfaceSession.abortPopout).toHaveBeenCalled(); + expect(back).not.toHaveBeenCalled(); + }); + + it("closes single action popout", async () => { + jest.spyOn(BrowserPopupUtils, "inSingleActionPopout").mockReturnValueOnce(true); + jest.spyOn(BrowserPopupUtils, "closeSingleActionPopout").mockResolvedValue(); + + await component.handleBackButton(); + + expect(BrowserPopupUtils.closeSingleActionPopout).toHaveBeenCalled(); + expect(back).not.toHaveBeenCalled(); + }); + + it("navigates the user backwards", async () => { + await component.handleBackButton(); + + expect(back).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 6cf2ba0e7d..b830ae7504 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -151,10 +151,10 @@ export class AddEditV2Component implements OnInit { constructor( private route: ActivatedRoute, - private popupRouterCacheService: PopupRouterCacheService, private i18nService: I18nService, private addEditFormConfigService: CipherFormConfigService, private popupCloseWarningService: PopupCloseWarningService, + private popupRouterCacheService: PopupRouterCacheService, private router: Router, ) { this.subscribeToParams(); @@ -183,11 +183,7 @@ export class AddEditV2Component implements OnInit { }; /** - * Navigates to previous view or view-cipher path - * depending on the history length. - * - * This can happen when history is lost due to the extension being - * forced into a popout window. + * Handle back button */ async handleBackButton() { if (this.inFido2PopoutWindow) { @@ -223,10 +219,18 @@ export class AddEditV2Component implements OnInit { return; } - await this.router.navigate(["/view-cipher"], { - replaceUrl: true, - queryParams: { cipherId: cipher.id }, - }); + // When the cipher is in edit / partial edit, the previous page was the view-cipher page. + // In the case of creating a new cipher, the user should go view-cipher page but we need to also + // remove it from the history stack. This avoids the user having to click back twice on the + // view-cipher page. + if (this.config.mode === "edit" || this.config.mode === "partial-edit") { + await this.popupRouterCacheService.back(); + } else { + await this.router.navigate(["/view-cipher"], { + replaceUrl: true, + queryParams: { cipherId: cipher.id }, + }); + } } subscribeToParams(): void { diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index 82ad60724a..f14434b277 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -14,6 +14,7 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -54,6 +55,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, kdfConfigService: KdfConfigService, + encryptService: EncryptService, ) { super( accountService, @@ -76,6 +78,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On ssoLoginService, dialogService, kdfConfigService, + encryptService, ); } diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index be3bd0860f..828fe8ea3f 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -2,17 +2,7 @@ import { DOCUMENT } from "@angular/common"; import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; -import { - Subject, - combineLatest, - filter, - firstValueFrom, - map, - switchMap, - takeUntil, - timeout, - timer, -} from "rxjs"; +import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -25,8 +15,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -58,7 +46,6 @@ import { const BroadcasterSubscriptionId = "AppComponent"; const IdleTimeout = 60000 * 10; // 10 minutes -const PaymentMethodWarningsRefresh = 60000; // 1 Minute @Component({ selector: "app-root", @@ -69,7 +56,6 @@ export class AppComponent implements OnDestroy, OnInit { private idleTimer: number = null; private isIdle = false; private destroy$ = new Subject(); - private paymentMethodWarningsRefresh$ = timer(0, PaymentMethodWarningsRefresh); constructor( @Inject(DOCUMENT) private document: Document, @@ -98,7 +84,6 @@ export class AppComponent implements OnDestroy, OnInit { private dialogService: DialogService, private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, - private paymentMethodWarningService: PaymentMethodWarningService, private organizationService: InternalOrganizationServiceAbstraction, private accountService: AccountService, ) {} @@ -252,25 +237,6 @@ export class AppComponent implements OnDestroy, OnInit { new DisableSendPolicy(), new SendOptionsPolicy(), ]); - - combineLatest([ - this.configService.getFeatureFlag$(FeatureFlag.ShowPaymentMethodWarningBanners), - this.paymentMethodWarningsRefresh$, - ]) - .pipe( - filter(([showPaymentMethodWarningBanners]) => showPaymentMethodWarningBanners), - switchMap(() => this.organizationService.memberOrganizations$), - switchMap( - async (organizations) => - await Promise.all( - organizations.map((organization) => - this.paymentMethodWarningService.update(organization.id), - ), - ), - ), - takeUntil(this.destroy$), - ) - .subscribe(); } ngOnDestroy() { @@ -328,7 +294,6 @@ export class AppComponent implements OnDestroy, OnInit { this.folderService.clear(userId), this.collectionService.clear(userId), this.biometricStateService.logout(userId), - this.paymentMethodWarningService.clear(), ]); await this.stateEventRunnerService.handleEvent("logout", userId); diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index 24970ee1ca..7dad7effee 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -14,6 +14,7 @@ import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.res import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { BillingSharedModule, PaymentComponent, TaxInfoComponent } from "../../shared"; @@ -75,6 +76,7 @@ export class TrialBillingStepComponent implements OnInit { private messagingService: MessagingService, private organizationBillingService: OrganizationBillingService, private platformUtilsService: PlatformUtilsService, + private toastService: ToastService, ) {} async ngOnInit(): Promise { @@ -96,11 +98,11 @@ export class TrialBillingStepComponent implements OnInit { const organizationId = await this.formPromise; const planDescription = this.getPlanDescription(); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("organizationCreated"), - this.i18nService.t("organizationReadyToGo"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("organizationCreated"), + message: this.i18nService.t("organizationReadyToGo"), + }); this.organizationCreated.emit({ organizationId, diff --git a/apps/web/src/app/billing/individual/premium.component.ts b/apps/web/src/app/billing/individual/premium.component.ts index b43d3cef34..79a5c5e2ed 100644 --- a/apps/web/src/app/billing/individual/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium.component.ts @@ -11,6 +11,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PaymentComponent, TaxInfoComponent } from "../shared"; @@ -46,6 +47,7 @@ export class PremiumComponent implements OnInit { private syncService: SyncService, private environmentService: EnvironmentService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, ) { this.selfHosted = platformUtilsService.isSelfHost(); this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; @@ -75,11 +77,11 @@ export class PremiumComponent implements OnInit { this.addonForm.markAllAsTouched(); if (this.selfHosted) { if (this.licenseFile == null) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("selectFile"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); return; } } @@ -87,11 +89,11 @@ export class PremiumComponent implements OnInit { if (this.selfHosted) { // eslint-disable-next-line @typescript-eslint/no-misused-promises if (!this.tokenService.getEmailVerified()) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("verifyEmailFirst"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verifyEmailFirst"), + }); return; } @@ -130,7 +132,11 @@ export class PremiumComponent implements OnInit { async finalizePremium() { await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); - this.platformUtilsService.showToast("success", null, this.i18nService.t("premiumUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("premiumUpdated"), + }); await this.router.navigate(["/settings/subscription/user-subscription"]); } diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 7e564341ca..2d02cbc5bd 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -10,7 +10,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { AdjustStorageDialogResult, @@ -48,6 +48,7 @@ export class UserSubscriptionComponent implements OnInit { private dialogService: DialogService, private environmentService: EnvironmentService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, ) { this.selfHosted = platformUtilsService.isSelfHost(); } @@ -94,7 +95,11 @@ export class UserSubscriptionComponent implements OnInit { try { this.reinstatePromise = this.apiService.postReinstatePremium(); await this.reinstatePromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("reinstated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("reinstated"), + }); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.load(); diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index c98a6b97c4..226c92b45e 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -6,6 +6,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-adjust-subscription", @@ -33,6 +34,7 @@ export class AdjustSubscription implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private organizationApiService: OrganizationApiServiceAbstraction, private formBuilder: FormBuilder, + private toastService: ToastService, ) {} ngOnInit() { @@ -76,7 +78,11 @@ export class AdjustSubscription implements OnInit, OnDestroy { ); await this.organizationApiService.updatePasswordManagerSeats(this.organizationId, request); - this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("subscriptionUpdated"), + }); this.onAdjusted.emit(); }; diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts index 95a29229cf..deb2c9da3e 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts @@ -12,7 +12,7 @@ import { Verification } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; export interface BillingSyncApiModalData { organizationId: string; @@ -43,6 +43,7 @@ export class BillingSyncApiKeyComponent { private i18nService: I18nService, private organizationApiService: OrganizationApiServiceAbstraction, private logService: LogService, + private toastService: ToastService, ) { this.organizationId = data.organizationId; this.hasBillingToken = data.hasBillingToken; @@ -67,11 +68,11 @@ export class BillingSyncApiKeyComponent { }); await this.load(response); this.showRotateScreen = false; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("billingSyncApiKeyRotated"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("billingSyncApiKeyRotated"), + }); } else { const response = await request.then((request) => { return this.organizationApiService.getOrCreateApiKey(this.organizationId, request); diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 995dcb2389..fe1c1568a9 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -37,6 +37,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared"; @@ -150,6 +151,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, private providerApiService: ProviderApiServiceAbstraction, + private toastService: ToastService, ) { this.selfHosted = platformUtilsService.isSelfHost(); } @@ -582,18 +584,18 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]); } - this.platformUtilsService.showToast( - "success", - this.i18nService.t("organizationCreated"), - this.i18nService.t("organizationReadyToGo"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("organizationCreated"), + message: this.i18nService.t("organizationReadyToGo"), + }); } else { orgId = await this.updateOrganization(orgId); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("organizationUpgraded"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("organizationUpgraded"), + }); } await this.apiService.refreshIdentityToken(); diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index c5ed013b1e..b8616ae1b4 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -16,7 +16,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { AdjustStorageDialogResult, @@ -82,6 +82,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private dialogService: DialogService, private configService: ConfigService, private providerService: ProviderService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -378,7 +379,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy try { await this.organizationApiService.reinstate(this.organizationId); - this.platformUtilsService.showToast("success", null, this.i18nService.t("reinstated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("reinstated"), + }); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.load(); @@ -475,11 +480,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy try { await this.apiService.deleteRemoveSponsorship(this.organizationId); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("removeSponsorshipSuccess"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removeSponsorshipSuccess"), + }); await this.load(); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts index f2884a4fd0..3d2aef6875 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.ts @@ -16,7 +16,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BillingSyncKeyComponent } from "./billing-sync-key.component"; @@ -84,6 +84,7 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest private i18nService: I18nService, private environmentService: EnvironmentService, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -169,7 +170,11 @@ export class OrganizationSubscriptionSelfhostComponent implements OnInit, OnDest this.load(); await this.loadOrganizationConnection(); this.messagingService.send("updatedOrgLicense"); - this.platformUtilsService.showToast("success", null, this.i18nService.t("licenseSyncSuccess")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("licenseSyncSuccess"), + }); }; get billingSyncSetUp() { diff --git a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts index 50abcc92ba..bc8694a505 100644 --- a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts @@ -6,6 +6,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationSmSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-sm-subscription-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; export interface SecretsManagerSubscriptionOptions { interval: "year" | "month"; @@ -100,6 +101,7 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest private organizationApiService: OrganizationApiServiceAbstraction, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private toastService: ToastService, ) {} ngOnInit() { @@ -158,11 +160,11 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest request, ); - await this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("subscriptionUpdated"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("subscriptionUpdated"), + }); this.onAdjusted.emit(); }; diff --git a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts index 1f8b70e03f..aae799d808 100644 --- a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts +++ b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts @@ -11,6 +11,7 @@ import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/respon import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { secretsManagerSubscribeFormFactory } from "../shared"; @@ -33,6 +34,7 @@ export class SecretsManagerSubscribeStandaloneComponent { private i18nService: I18nService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, + private toastService: ToastService, ) {} submit = async () => { @@ -60,11 +62,11 @@ export class SecretsManagerSubscribeStandaloneComponent { */ await this.apiService.refreshIdentityToken(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("subscribedToSecretsManager"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("subscribedToSecretsManager"), + }); this.onSubscribe.emit(); }; diff --git a/apps/web/src/app/billing/settings/sponsored-families.component.ts b/apps/web/src/app/billing/settings/sponsored-families.component.ts index 117f42fe39..c098b6044c 100644 --- a/apps/web/src/app/billing/settings/sponsored-families.component.ts +++ b/apps/web/src/app/billing/settings/sponsored-families.component.ts @@ -18,6 +18,7 @@ import { PlanSponsorshipType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; interface RequestSponsorshipForm { selectedSponsorshipOrgId: FormControl; @@ -51,6 +52,7 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private formBuilder: FormBuilder, private accountService: AccountService, + private toastService: ToastService, ) { this.sponsorshipForm = this.formBuilder.group({ selectedSponsorshipOrgId: new FormControl("", { @@ -118,7 +120,11 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy { ); await this.formPromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("sponsorshipCreated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("sponsorshipCreated"), + }); this.formPromise = null; // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts index eff75b61b3..06dc1490e3 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts @@ -7,7 +7,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "[sponsoring-org-row]", @@ -30,6 +30,7 @@ export class SponsoringOrgRowComponent implements OnInit { private logService: LogService, private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -53,7 +54,11 @@ export class SponsoringOrgRowComponent implements OnInit { async resendEmail() { await this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id); - this.platformUtilsService.showToast("success", null, this.i18nService.t("emailSent")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emailSent"), + }); } get isSentAwaitingSync() { @@ -73,7 +78,11 @@ export class SponsoringOrgRowComponent implements OnInit { } await this.apiService.deleteRevokeSponsorship(this.sponsoringOrg.id); - this.platformUtilsService.showToast("success", null, this.i18nService.t("reclaimedFreePlan")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("reclaimedFreePlan"), + }); this.sponsorshipRemoved.emit(); } diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts index 1addf42629..08c9bf2766 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts @@ -1,14 +1,11 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject, ViewChild } from "@angular/core"; import { FormGroup } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.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 { 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, ToastService } from "@bitwarden/components"; @@ -46,7 +43,6 @@ export class AdjustPaymentDialogComponent { private apiService: ApiService, private i18nService: I18nService, private organizationApiService: OrganizationApiServiceAbstraction, - private paymentMethodWarningService: PaymentMethodWarningService, private configService: ConfigService, private toastService: ToastService, ) { @@ -78,12 +74,6 @@ export class AdjustPaymentDialogComponent { } }); await response; - const showPaymentMethodWarningBanners = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.ShowPaymentMethodWarningBanners), - ); - if (this.organizationId && showPaymentMethodWarningBanners) { - await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId); - } this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.ts b/apps/web/src/app/billing/shared/adjust-storage.component.ts index fcdbc3437d..439bfec82a 100644 --- a/apps/web/src/app/billing/shared/adjust-storage.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage.component.ts @@ -10,7 +10,7 @@ import { StorageRequest } from "@bitwarden/common/models/request/storage.request import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PaymentComponent } from "./payment.component"; @@ -56,6 +56,7 @@ export class AdjustStorageComponent { private activatedRoute: ActivatedRoute, private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, + private toastService: ToastService, ) { this.storageGbPrice = data.storageGbPrice; this.add = data.add; @@ -93,21 +94,21 @@ export class AdjustStorageComponent { await action(); this.dialogRef.close(AdjustStorageDialogResult.Adjusted); if (paymentFailed) { - this.platformUtilsService.showToast( - "warning", - null, - this.i18nService.t("couldNotChargeCardPayInvoice"), - { timeout: 10000 }, - ); + this.toastService.showToast({ + variant: "warning", + title: null, + message: this.i18nService.t("couldNotChargeCardPayInvoice"), + timeout: 10000, + }); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["../billing"], { relativeTo: this.activatedRoute }); } else { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), + }); } }; diff --git a/apps/web/src/app/billing/shared/index.ts b/apps/web/src/app/billing/shared/index.ts index edaf3a1199..ae28e45f78 100644 --- a/apps/web/src/app/billing/shared/index.ts +++ b/apps/web/src/app/billing/shared/index.ts @@ -3,4 +3,3 @@ 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"; diff --git a/apps/web/src/app/billing/shared/offboarding-survey.component.ts b/apps/web/src/app/billing/shared/offboarding-survey.component.ts index 73a460f8c8..7ffd40e058 100644 --- a/apps/web/src/app/billing/shared/offboarding-survey.component.ts +++ b/apps/web/src/app/billing/shared/offboarding-survey.component.ts @@ -5,7 +5,7 @@ import { FormBuilder, Validators } from "@angular/forms"; import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; type UserOffboardingParams = { type: "User"; @@ -88,6 +88,7 @@ export class OffboardingSurveyComponent { private billingApiService: BillingApiService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private toastService: ToastService, ) {} submit = async () => { @@ -106,11 +107,11 @@ export class OffboardingSurveyComponent { ? await this.billingApiService.cancelOrganizationSubscription(this.dialogParams.id, request) : await this.billingApiService.cancelPremiumUserSubscription(request); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("canceledSubscription"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("canceledSubscription"), + }); this.dialogRef.close(this.ResultType.Submitted); }; diff --git a/apps/web/src/app/billing/shared/payment-method-warnings/payment-method-warnings.component.html b/apps/web/src/app/billing/shared/payment-method-warnings/payment-method-warnings.component.html deleted file mode 100644 index 59dbc5f976..0000000000 --- a/apps/web/src/app/billing/shared/payment-method-warnings/payment-method-warnings.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - - {{ "maintainYourSubscription" | i18n: warning.organizationName }} - {{ "addAPaymentMethod" | i18n }}. - - diff --git a/apps/web/src/app/billing/shared/payment-method-warnings/payment-method-warnings.component.ts b/apps/web/src/app/billing/shared/payment-method-warnings/payment-method-warnings.component.ts deleted file mode 100644 index d811961c0d..0000000000 --- a/apps/web/src/app/billing/shared/payment-method-warnings/payment-method-warnings.component.ts +++ /dev/null @@ -1,33 +0,0 @@ -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 = - 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 { - await this.paymentMethodWarningService.acknowledge(organizationId); - } -} diff --git a/apps/web/src/app/billing/shared/payment-method-warnings/payment-method-warnings.module.ts b/apps/web/src/app/billing/shared/payment-method-warnings/payment-method-warnings.module.ts deleted file mode 100644 index c6303c878c..0000000000 --- a/apps/web/src/app/billing/shared/payment-method-warnings/payment-method-warnings.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 {} diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index eacc0b4739..0c089fa073 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -13,7 +13,7 @@ import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank. import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component"; import { @@ -63,6 +63,7 @@ export class PaymentMethodComponent implements OnInit { private route: ActivatedRoute, private formBuilder: FormBuilder, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -144,13 +145,21 @@ export class PaymentMethodComponent implements OnInit { request.amount1 = this.verifyBankForm.value.amount1; request.amount2 = this.verifyBankForm.value.amount2; await this.organizationApiService.verifyBank(this.organizationId, request); - this.platformUtilsService.showToast("success", null, this.i18nService.t("verifiedBankAccount")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("verifiedBankAccount"), + }); await this.load(); }; submitTaxInfo = async () => { await this.taxInfo.submitTaxInfo(); - this.platformUtilsService.showToast("success", null, this.i18nService.t("taxInfoUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("taxInfoUpdated"), + }); }; get isCreditBalance() { diff --git a/apps/web/src/app/billing/shared/tax-info.component.html b/apps/web/src/app/billing/shared/tax-info.component.html index c254ffa4a4..89bc7438a7 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.html +++ b/apps/web/src/app/billing/shared/tax-info.component.html @@ -13,7 +13,7 @@
-
+
{{ "zipPostalCode" | i18n }} diff --git a/apps/web/src/app/billing/shared/update-license-dialog.component.ts b/apps/web/src/app/billing/shared/update-license-dialog.component.ts index fd9acfe786..b663445028 100644 --- a/apps/web/src/app/billing/shared/update-license-dialog.component.ts +++ b/apps/web/src/app/billing/shared/update-license-dialog.component.ts @@ -6,7 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { UpdateLicenseDialogResult } from "./update-license-types"; import { UpdateLicenseComponent } from "./update-license.component"; @@ -22,8 +22,16 @@ export class UpdateLicenseDialogComponent extends UpdateLicenseComponent { platformUtilsService: PlatformUtilsService, organizationApiService: OrganizationApiServiceAbstraction, formBuilder: FormBuilder, + toastService: ToastService, ) { - super(apiService, i18nService, platformUtilsService, organizationApiService, formBuilder); + super( + apiService, + i18nService, + platformUtilsService, + organizationApiService, + formBuilder, + toastService, + ); } async submitLicense() { const result = await this.submit(); diff --git a/apps/web/src/app/billing/shared/update-license.component.ts b/apps/web/src/app/billing/shared/update-license.component.ts index 8dbb83c510..e542177684 100644 --- a/apps/web/src/app/billing/shared/update-license.component.ts +++ b/apps/web/src/app/billing/shared/update-license.component.ts @@ -6,6 +6,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { UpdateLicenseDialogResult } from "./update-license-types"; @@ -32,6 +33,7 @@ export class UpdateLicenseComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private organizationApiService: OrganizationApiServiceAbstraction, private formBuilder: FormBuilder, + private toastService: ToastService, ) {} async ngOnInit() { const org = await this.organizationApiService.get(this.organizationId); @@ -52,11 +54,11 @@ export class UpdateLicenseComponent implements OnInit { } const files = this.licenseFile; if (files == null) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("selectFile"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); return; } const fd = new FormData(); @@ -74,11 +76,11 @@ export class UpdateLicenseComponent implements OnInit { }); await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("licenseUploadSuccess"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("licenseUploadSuccess"), + }); this.onUpdated.emit(); return new Promise((resolve) => resolve(UpdateLicenseDialogResult.Updated)); }; diff --git a/apps/web/src/app/layouts/web-layout.component.html b/apps/web/src/app/layouts/web-layout.component.html index 31a5e82688..fc318a6398 100644 --- a/apps/web/src/app/layouts/web-layout.component.html +++ b/apps/web/src/app/layouts/web-layout.component.html @@ -1,9 +1,4 @@ - - - diff --git a/apps/web/src/app/layouts/web-layout.component.ts b/apps/web/src/app/layouts/web-layout.component.ts index bb91e619aa..840beaa217 100644 --- a/apps/web/src/app/layouts/web-layout.component.ts +++ b/apps/web/src/app/layouts/web-layout.component.ts @@ -1,12 +1,8 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LayoutComponent } from "@bitwarden/components"; -import { PaymentMethodWarningsModule } from "../billing/shared"; - import { ProductSwitcherModule } from "./product-switcher/product-switcher.module"; import { ToggleWidthComponent } from "./toggle-width.component"; @@ -14,18 +10,8 @@ import { ToggleWidthComponent } from "./toggle-width.component"; selector: "app-layout", templateUrl: "web-layout.component.html", standalone: true, - imports: [ - CommonModule, - LayoutComponent, - ProductSwitcherModule, - ToggleWidthComponent, - PaymentMethodWarningsModule, - ], + imports: [CommonModule, LayoutComponent, ProductSwitcherModule, ToggleWidthComponent], }) export class WebLayoutComponent { - protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( - FeatureFlag.ShowPaymentMethodWarningBanners, - ); - - constructor(private configService: ConfigService) {} + constructor() {} } diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 1a1daefb44..8e49c95e11 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -61,7 +61,6 @@ import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component" import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component"; import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component"; -import { PaymentMethodWarningsModule } from "../billing/shared"; import { DynamicAvatarComponent } from "../components/dynamic-avatar.component"; import { SelectableAvatarComponent } from "../components/selectable-avatar.component"; import { FrontendLayoutComponent } from "../layouts/frontend-layout.component"; @@ -113,7 +112,6 @@ import { SharedModule } from "./shared.module"; HeaderModule, OrganizationLayoutComponent, UserLayoutComponent, - PaymentMethodWarningsModule, VerifyRecoverDeleteOrgComponent, VaultTimeoutInputComponent, ], diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html index 6313689007..b5a53bd143 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.html +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.html @@ -23,6 +23,9 @@ {{ "personalOwnershipPolicyInEffect" | i18n }} + + {{ "cardExpiredMessage" | i18n }} +
diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts index 7a96bff039..71ccaab7dd 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.ts @@ -11,6 +11,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { EventType } from "@bitwarden/common/enums"; +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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -23,6 +24,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Launchable } from "@bitwarden/common/vault/interfaces/launchable"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { DialogService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -43,6 +45,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On viewingPasswordHistory = false; viewOnly = false; showPasswordCount = false; + cardIsExpired: boolean = false; protected totpInterval: number; protected override componentName = "app-vault-add-edit"; @@ -115,6 +118,12 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On await this.totpTick(interval); }, 1000); } + + const extensionRefreshEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh), + ); + + this.cardIsExpired = extensionRefreshEnabled && this.isCardExpiryInThePast(); } ngOnDestroy() { @@ -226,6 +235,24 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On this.viewingPasswordHistory = !this.viewingPasswordHistory; } + isCardExpiryInThePast() { + if (this.cipher.card) { + const { expMonth, expYear }: CardView = this.cipher.card; + + if (expYear && expMonth) { + // `Date` months are zero-indexed + const parsedMonth = parseInt(expMonth) - 1; + const parsedYear = parseInt(expYear); + + // First day of the next month minus one, to get last day of the card month + const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0); + const now = new Date(); + + return cardExpiry < now; + } + } + } + protected cleanUp() { if (this.totpInterval) { window.clearInterval(this.totpInterval); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f7899eae3a..108807b153 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -194,6 +194,12 @@ "dr": { "message": "Dr" }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, "expirationMonth": { "message": "Expiration month" }, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 9733e91be7..37ef317360 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -7,7 +7,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SearchModule } from "@bitwarden/components"; import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component"; import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vault/app/billing"; -import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; import { @@ -53,7 +52,6 @@ import { SetupComponent } from "./setup/setup.component"; OrganizationPlansComponent, SearchModule, ProvidersLayoutComponent, - PaymentMethodWarningsModule, TaxInfoComponent, DangerZoneComponent, ScrollingModule, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html index 3b81d0564c..482b85b712 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -1,6 +1,3 @@ - , + refreshedComponent: Type, + options: Route, + altOptions?: Route, +): Routes { + return componentRouteSwap( + defaultComponent, + refreshedComponent, + async () => { + const configService = inject(ConfigService); + return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); + }, + options, + altOptions, + ); +} diff --git a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts index 83db2fcd87..fd7feab4c3 100644 --- a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts @@ -8,7 +8,6 @@ import { PaymentInformationResponse } from "@bitwarden/common/billing/models/res import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; -import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; import { ListResponse } from "../../models/response/list.response"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; @@ -34,8 +33,6 @@ export abstract class BillingApiServiceAbstraction { organizationId: string, ) => Promise; - getOrganizationBillingStatus: (id: string) => Promise; - getPlans: () => Promise>; getProviderClientInvoiceReport: (providerId: string, invoiceId: string) => Promise; diff --git a/libs/common/src/billing/abstractions/index.ts b/libs/common/src/billing/abstractions/index.ts index 08a7a28fd9..c3ef8baca2 100644 --- a/libs/common/src/billing/abstractions/index.ts +++ b/libs/common/src/billing/abstractions/index.ts @@ -1,7 +1,6 @@ export * from "./account/billing-account-profile-state.service"; export * from "./billilng-api.service.abstraction"; export * from "./organization-billing.service"; -export * from "./payment-method-warnings-service.abstraction"; export * from "./payment-processors/braintree.service.abstraction"; export * from "./payment-processors/stripe.service.abstraction"; export * from "./provider-billing.service.abstraction"; diff --git a/libs/common/src/billing/abstractions/payment-method-warnings-service.abstraction.ts b/libs/common/src/billing/abstractions/payment-method-warnings-service.abstraction.ts deleted file mode 100644 index d7ba522c60..0000000000 --- a/libs/common/src/billing/abstractions/payment-method-warnings-service.abstraction.ts +++ /dev/null @@ -1,31 +0,0 @@ -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>; - /** - * 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; - /** - * 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; - /** - * Clears the {@link PaymentMethodWarning} record from the {@link ActiveUserState}. - */ - clear: () => Promise; - /** - * 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; -} diff --git a/libs/common/src/billing/models/billing-keys.state.ts b/libs/common/src/billing/models/billing-keys.state.ts deleted file mode 100644 index 1d1cce6d0b..0000000000 --- a/libs/common/src/billing/models/billing-keys.state.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BILLING_DISK, UserKeyDefinition } from "../../platform/state"; -import { PaymentMethodWarning } from "../models/domain/payment-method-warning"; - -export const PAYMENT_METHOD_WARNINGS_KEY = UserKeyDefinition.record( - BILLING_DISK, - "paymentMethodWarnings", - { - deserializer: (warnings) => ({ - ...warnings, - savedAt: new Date(warnings.savedAt), - }), - clearOn: ["logout"], - }, -); diff --git a/libs/common/src/billing/models/domain/index.ts b/libs/common/src/billing/models/domain/index.ts index 19be9c1aeb..66d7e29c10 100644 --- a/libs/common/src/billing/models/domain/index.ts +++ b/libs/common/src/billing/models/domain/index.ts @@ -1,5 +1,4 @@ export * from "./bank-account"; export * from "./masked-payment-method"; -export * from "./payment-method-warning"; export * from "./tax-information"; export * from "./tokenized-payment-method"; diff --git a/libs/common/src/billing/models/domain/payment-method-warning.ts b/libs/common/src/billing/models/domain/payment-method-warning.ts deleted file mode 100644 index dd3f0f8581..0000000000 --- a/libs/common/src/billing/models/domain/payment-method-warning.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type PaymentMethodWarning = { - organizationName: string; - risksSubscriptionFailure: boolean; - acknowledged: boolean; - savedAt: Date; -}; diff --git a/libs/common/src/billing/models/response/organization-billing-status.response.ts b/libs/common/src/billing/models/response/organization-billing-status.response.ts deleted file mode 100644 index 916bebe4bc..0000000000 --- a/libs/common/src/billing/models/response/organization-billing-status.response.ts +++ /dev/null @@ -1,15 +0,0 @@ -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"); - } -} diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index a5841fc5b5..822e6d1687 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -12,7 +12,6 @@ import { SubscriptionCancellationRequest } from "../../billing/models/request/su import { TokenizedPaymentMethodRequest } from "../../billing/models/request/tokenized-payment-method.request"; import { VerifyBankAccountRequest } from "../../billing/models/request/verify-bank-account.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; -import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; import { PaymentInformationResponse } from "../../billing/models/response/payment-information.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; import { ListResponse } from "../../models/response/list.response"; @@ -72,17 +71,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { return response as string; } - async getOrganizationBillingStatus(id: string): Promise { - const r = await this.apiService.send( - "GET", - "/organizations/" + id + "/billing-status", - null, - true, - true, - ); - return new OrganizationBillingStatusResponse(r); - } - async getOrganizationBillingMetadata( organizationId: string, ): Promise { diff --git a/libs/common/src/billing/services/payment-method-warnings.service.spec.ts b/libs/common/src/billing/services/payment-method-warnings.service.spec.ts deleted file mode 100644 index 6e37821ef5..0000000000 --- a/libs/common/src/billing/services/payment-method-warnings.service.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -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; - - const mockUserId = Utils.newGuid() as UserId; - let accountService: FakeAccountService; - let stateProvider: FakeStateProvider; - let activeUserState: FakeActiveUserState>; - - 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(); - paymentMethodWarningsService = new PaymentMethodWarningsService( - billingApiService, - stateProvider, - ); - }); - - it("acknowledge", async () => { - const organizationId = "1"; - const state: Record = { - [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 = { - "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 = { - [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 = { - [organizationId]: { - organizationName: "Teams Organization", - risksSubscriptionFailure: true, - acknowledged: false, - savedAt: getPastDate(3), - }, - }; - activeUserState.nextState(state); - await paymentMethodWarningsService.update(organizationId); - expect(billingApiService.getOrganizationBillingStatus).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.getOrganizationBillingStatus.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.getOrganizationBillingStatus).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.getOrganizationBillingStatus.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.getOrganizationBillingStatus).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.getOrganizationBillingStatus.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.getOrganizationBillingStatus).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/libs/common/src/billing/services/payment-method-warnings.service.ts b/libs/common/src/billing/services/payment-method-warnings.service.ts deleted file mode 100644 index 0dad48bb85..0000000000 --- a/libs/common/src/billing/services/payment-method-warnings.service.ts +++ /dev/null @@ -1,74 +0,0 @@ -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>; - paymentMethodWarnings$: Observable>; - - 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 { - await this.paymentMethodWarningsState.update((state) => { - const current = state[organizationId]; - state[organizationId] = { - ...current, - acknowledged: true, - }; - return state; - }); - } - - async removeSubscriptionRisk(organizationId: string): Promise { - await this.paymentMethodWarningsState.update((state) => { - const current = state[organizationId]; - state[organizationId] = { - ...current, - risksSubscriptionFailure: false, - }; - return state; - }); - } - - async clear(): Promise { - await this.paymentMethodWarningsState.update(() => ({})); - } - - async update(organizationId: string): Promise { - 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.getOrganizationBillingStatus(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; - }; -} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 583b699697..9edc14e7b0 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -7,7 +7,6 @@ export enum FeatureFlag { BrowserFilelessImport = "browser-fileless-import", ItemShare = "item-share", GeneratorToolsModernization = "generator-tools-modernization", - ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners", EnableConsolidatedBilling = "enable-consolidated-billing", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", EnableDeleteProvider = "AC-1218-delete-provider", @@ -50,7 +49,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.BrowserFilelessImport]: FALSE, [FeatureFlag.ItemShare]: FALSE, [FeatureFlag.GeneratorToolsModernization]: FALSE, - [FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE, [FeatureFlag.EnableConsolidatedBilling]: FALSE, [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, [FeatureFlag.EnableDeleteProvider]: FALSE, diff --git a/libs/components/src/dialog/dialogs.mdx b/libs/components/src/dialog/dialogs.mdx index bd6a30d7f2..9a63e1301b 100644 --- a/libs/components/src/dialog/dialogs.mdx +++ b/libs/components/src/dialog/dialogs.mdx @@ -1,5 +1,7 @@ import { Meta, Story, Source } from "@storybook/addon-docs"; +import * as stories from "./dialog.service.stories"; + # Dialog @@ -24,7 +26,7 @@ dialog should become scrollable. A backdrop should be used to hide the content below the dialog. Use `#000000` with `30% opacity`. - + ## Accessibility diff --git a/libs/components/src/form/forms.mdx b/libs/components/src/form/forms.mdx index 6c6fa33f68..dba6a1466c 100644 --- a/libs/components/src/form/forms.mdx +++ b/libs/components/src/form/forms.mdx @@ -1,5 +1,14 @@ import { Meta, Story, Source } from "@storybook/addon-docs"; +import * as formStories from "./form.stories"; +import * as fieldStories from "../form-field/form-field.stories"; +import * as passwordToggleStories from "../form-field/password-input-toggle.stories"; +import * as searchStories from "../search/search.stories"; +import * as selectStories from "../select/select.stories"; +import * as multiSelectStories from "../form-field/multi-select.stories"; +import * as radioStories from "../radio-button/radio-button.stories"; +import * as checkboxStories from "../checkbox/checkbox.stories"; + # Forms @@ -8,9 +17,9 @@ Component Library forms should always be built using [Angular Reactive Forms][re [ADR-0001][adr-0001] for a background to this decision. In practice this means that forms should always use the native `form` element and bind a `formGroup`. - + - +
## Form spacing and sections @@ -48,25 +57,25 @@ controls like email verification, number selection, and more. #### Default with required attribute - + #### Password Toggle - + -#### Search +### Search - + ### Selects #### Searchable single select (default) - + #### Multi-select - + ### Radio group @@ -89,14 +98,11 @@ using a radio group for more than 5 options even if the options require addition #### Block - + #### Inline - - -[reactive]: https://angular.io/guide/reactive-forms -[adr-0001]: https://contributing.bitwarden.com/architecture/adr/reactive-forms + ### Checkbox @@ -116,7 +122,7 @@ If a checkbox group has more than 4 options a #### Single checkbox - + ## Accessibility @@ -176,3 +182,6 @@ the field’s label. Maintain a ratio of 3:1 with the form's background. - Error styling should not rely only on using the `danger-600`color change. Use as a prefix to highlight the text as error text versus helper + +[reactive]: https://angular.io/guide/reactive-forms +[adr-0001]: https://contributing.bitwarden.com/architecture/adr/reactive-forms diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index 58bd971679..fa78f04d23 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -64,6 +64,7 @@ export default { skipToContent: "Skip to content", submenu: "submenu", toggleCollapse: "toggle collapse", + toggleSideNavigation: "toggle side navigation", }); }, }, diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html index 07606add8b..626934b20e 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html @@ -74,6 +74,21 @@ bitPasswordInputToggle [(toggled)]="showFilePassword" > + + {{ "exportPasswordDescription" | i18n }} diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 8f2c6661fd..d83d189cd7 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -24,6 +24,7 @@ import { EventType } from "@bitwarden/common/enums"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { @@ -38,6 +39,7 @@ import { SelectModule, ToastService, } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { EncryptedExportType } from "../enums/encrypted-export-type.enum"; @@ -157,6 +159,8 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { protected toastService: ToastService, protected exportService: VaultExportServiceAbstraction, protected eventCollectionService: EventCollectionService, + protected passwordGenerationService: PasswordGenerationServiceAbstraction, + private platformUtilsService: PlatformUtilsService, private policyService: PolicyService, private logService: LogService, private formBuilder: UntypedFormBuilder, @@ -272,6 +276,22 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { } } + generatePassword = async () => { + const [options] = await this.passwordGenerationService.getOptions(); + this.filePasswordValue = await this.passwordGenerationService.generatePassword(options); + this.exportForm.get("filePassword").setValue(this.filePasswordValue); + this.exportForm.get("confirmFilePassword").setValue(this.filePasswordValue); + }; + + copyPasswordToClipboard = async () => { + this.platformUtilsService.copyToClipboard(this.filePasswordValue); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t("password")), + }); + }; + submit = async () => { if (this.isFileEncryptedExport && this.filePassword != this.confirmFilePassword) { this.toastService.showToast({ diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index 68c80a7bd5..a675384ff9 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -1,4 +1,8 @@ + + {{ "cardExpiredMessage" | i18n }} + + ; collections$: Observable; private destroyed$: Subject = new Subject(); + cardIsExpired: boolean = false; constructor( private organizationService: OrganizationService, @@ -57,6 +60,8 @@ export class CipherViewComponent implements OnInit, OnDestroy { async ngOnInit() { await this.loadCipherData(); + + this.cardIsExpired = this.isCardExpiryInThePast(); } ngOnDestroy(): void { @@ -97,4 +102,24 @@ export class CipherViewComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroyed$)); } } + + isCardExpiryInThePast() { + if (this.cipher.card) { + const { expMonth, expYear }: CardView = this.cipher.card; + + if (expYear && expMonth) { + // `Date` months are zero-indexed + const parsedMonth = parseInt(expMonth) - 1; + const parsedYear = parseInt(expYear); + + // First day of the next month minus one, to get last day of the card month + const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0); + const now = new Date(); + + return cardExpiry < now; + } + } + + return false; + } } diff --git a/package-lock.json b/package-lock.json index 1fdd4d7718..51220dd2b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,7 +120,7 @@ "@typescript-eslint/parser": "7.16.1", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.12.1", - "autoprefixer": "10.4.19", + "autoprefixer": "10.4.20", "babel-loader": "9.1.3", "base64-loader": "1.0.0", "browserslist": "4.23.2", @@ -11194,9 +11194,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -11212,12 +11212,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -11230,6 +11231,39 @@ "postcss": "^8.1.0" } }, + "node_modules/autoprefixer/node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", diff --git a/package.json b/package.json index d19f598143..89d216ee7a 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "@typescript-eslint/parser": "7.16.1", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.12.1", - "autoprefixer": "10.4.19", + "autoprefixer": "10.4.20", "babel-loader": "9.1.3", "base64-loader": "1.0.0", "browserslist": "4.23.2",