1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

Merge branch 'main' into auth/pm-8111/browser-refresh-login-component

This commit is contained in:
rr-bw
2024-08-21 11:15:03 -07:00
64 changed files with 623 additions and 653 deletions

View File

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

View File

@@ -9,13 +9,13 @@
<h2 class="tw-font-bold">{{ "premiumFeatures" | i18n }}</h2>
<bit-section>
<bit-card>
<div class="tw-flex tw-flex-col tw-p-3">
<div class="tw-flex tw-flex-col tw-p-2">
<ul class="tw-list-disc tw-pl-5 tw-space-y-2 tw-break-words">
<li>
{{ "ppremiumSignUpStorage" | i18n }}
</li>
<li>
{{ "ppremiumSignUpTwoStepOptions" | i18n }}
{{ "premiumSignUpTwoStepOptions" | i18n }}
</li>
<li>
{{ "premiumSignUpEmergency" | i18n }}

View File

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

View File

@@ -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";

View File

@@ -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<any>,
refreshedComponent: Type<any>,
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.

View File

@@ -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<AddEditV2Component>;
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<PlatformUtilsService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() },
{ 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();
});
});
});

View File

@@ -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 {

View File

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

View File

@@ -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<void>();
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);

View File

@@ -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<void> {
@@ -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,

View File

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

View File

@@ -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();

View File

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

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);

View File

@@ -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() {

View File

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

View File

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

View File

@@ -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<string>;
@@ -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<RequestSponsorshipForm>({
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

View File

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

View File

@@ -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,

View File

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

View File

@@ -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";

View File

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

View File

@@ -1,15 +0,0 @@
<ng-container *ngFor="let warning of warnings$ | async">
<bit-banner
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
bannerType="warning"
(onClose)="closeWarning(warning.organizationId)"
>
{{ "maintainYourSubscription" | i18n: warning.organizationName }}
<a
bitLink
linkType="contrast"
[routerLink]="['/organizations', warning.organizationId, 'billing', 'payment-method']"
>{{ "addAPaymentMethod" | i18n }}</a
>.
</bit-banner>
</ng-container>

View File

@@ -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<Warning[]> =
this.paymentMethodWarningService.paymentMethodWarnings$.pipe(
map((warnings) =>
Object.entries(warnings ?? [])
.filter(([_, warning]) => warning.risksSubscriptionFailure && !warning.acknowledged)
.map(([organizationId, { organizationName }]) => ({
organizationId,
organizationName,
})),
),
);
protected async closeWarning(organizationId: string): Promise<void> {
await this.paymentMethodWarningService.acknowledge(organizationId);
}
}

View File

@@ -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 {}

View File

@@ -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() {

View File

@@ -13,7 +13,7 @@
</bit-select>
</bit-form-field>
</div>
<div [ngClass]="trialFlow ? 'tw-col-span-5' : 'tw-col-span-3'">
<div [ngClass]="trialFlow ? 'tw-col-span-5' : 'tw-col-span-4'">
<bit-form-field>
<bit-label>{{ "zipPostalCode" | i18n }}</bit-label>
<input bitInput type="text" formControlName="postalCode" autocomplete="postal-code" />

View File

@@ -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();

View File

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

View File

@@ -1,9 +1,4 @@
<bit-layout>
<ng-content select="app-side-nav, [slot=side-nav]" slot="side-nav"></ng-content>
<app-payment-method-warnings
*ngIf="showPaymentMethodWarningBanners$ | async"
></app-payment-method-warnings>
<ng-content></ng-content>
</bit-layout>

View File

@@ -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() {}
}

View File

@@ -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,
],

View File

@@ -23,6 +23,9 @@
<bit-callout type="info" *ngIf="allowOwnershipAssignment() && !allowPersonal">
{{ "personalOwnershipPolicyInEffect" | i18n }}
</bit-callout>
<bit-callout *ngIf="cardIsExpired" type="info" [title]="'cardExpiredTitle' | i18n">
{{ "cardExpiredMessage" | i18n }}
</bit-callout>
<div class="row" *ngIf="!editMode && !viewOnly">
<div class="col-6 form-group">
<label for="type">{{ "whatTypeOfItem" | i18n }}</label>

View File

@@ -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);

View File

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