mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
Merge branch 'main' into PM-26250-Explore-options-to-enable-direct-importer-for-mac-app-store-build
This commit is contained in:
@@ -1406,6 +1406,27 @@
|
||||
"learnMore": {
|
||||
"message": "Learn more"
|
||||
},
|
||||
"migrationsFailed": {
|
||||
"message": "An error occurred updating the encryption settings."
|
||||
},
|
||||
"updateEncryptionSettingsTitle": {
|
||||
"message": "Update your encryption settings"
|
||||
},
|
||||
"updateEncryptionSettingsDesc": {
|
||||
"message": "The new recommended encryption settings will improve your account security. Enter your master password to update now."
|
||||
},
|
||||
"confirmIdentityToContinue": {
|
||||
"message": "Confirm your identity to continue"
|
||||
},
|
||||
"enterYourMasterPassword": {
|
||||
"message": "Enter your master password"
|
||||
},
|
||||
"updateSettings": {
|
||||
"message": "Update settings"
|
||||
},
|
||||
"later": {
|
||||
"message": "Later"
|
||||
},
|
||||
"authenticatorKeyTotp": {
|
||||
"message": "Authenticator key (TOTP)"
|
||||
},
|
||||
|
||||
193
apps/browser/src/auth/popup/guards/platform-popout.guard.spec.ts
Normal file
193
apps/browser/src/auth/popup/guards/platform-popout.guard.spec.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
||||
|
||||
import { platformPopoutGuard } from "./platform-popout.guard";
|
||||
|
||||
describe("platformPopoutGuard", () => {
|
||||
let getPlatformInfoSpy: jest.SpyInstance;
|
||||
let inPopoutSpy: jest.SpyInstance;
|
||||
let inSidebarSpy: jest.SpyInstance;
|
||||
let openPopoutSpy: jest.SpyInstance;
|
||||
let closePopupSpy: jest.SpyInstance;
|
||||
|
||||
const mockRoute = {} as ActivatedRouteSnapshot;
|
||||
const mockState: RouterStateSnapshot = {
|
||||
url: "/login-with-passkey?param=value",
|
||||
} as RouterStateSnapshot;
|
||||
|
||||
beforeEach(() => {
|
||||
getPlatformInfoSpy = jest.spyOn(BrowserApi, "getPlatformInfo");
|
||||
inPopoutSpy = jest.spyOn(BrowserPopupUtils, "inPopout");
|
||||
inSidebarSpy = jest.spyOn(BrowserPopupUtils, "inSidebar");
|
||||
openPopoutSpy = jest.spyOn(BrowserPopupUtils, "openPopout").mockImplementation();
|
||||
closePopupSpy = jest.spyOn(BrowserApi, "closePopup").mockImplementation();
|
||||
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("when platform matches", () => {
|
||||
beforeEach(() => {
|
||||
getPlatformInfoSpy.mockResolvedValue({ os: "linux" });
|
||||
inPopoutSpy.mockReturnValue(false);
|
||||
inSidebarSpy.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should open popout and block navigation when not already in popout or sidebar", async () => {
|
||||
const guard = platformPopoutGuard(["linux"]);
|
||||
const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState));
|
||||
|
||||
expect(getPlatformInfoSpy).toHaveBeenCalled();
|
||||
expect(inPopoutSpy).toHaveBeenCalledWith(window);
|
||||
expect(inSidebarSpy).toHaveBeenCalledWith(window);
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
"popup/index.html#/login-with-passkey?param=value&autoClosePopout=true",
|
||||
);
|
||||
expect(closePopupSpy).toHaveBeenCalledWith(window);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should allow navigation when already in popout", async () => {
|
||||
inPopoutSpy.mockReturnValue(true);
|
||||
|
||||
const guard = platformPopoutGuard(["linux"]);
|
||||
const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState));
|
||||
|
||||
expect(openPopoutSpy).not.toHaveBeenCalled();
|
||||
expect(closePopupSpy).not.toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow navigation when already in sidebar", async () => {
|
||||
inSidebarSpy.mockReturnValue(true);
|
||||
|
||||
const guard = platformPopoutGuard(["linux"]);
|
||||
const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState));
|
||||
|
||||
expect(openPopoutSpy).not.toHaveBeenCalled();
|
||||
expect(closePopupSpy).not.toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when platform does not match", () => {
|
||||
beforeEach(() => {
|
||||
getPlatformInfoSpy.mockResolvedValue({ os: "win" });
|
||||
inPopoutSpy.mockReturnValue(false);
|
||||
inSidebarSpy.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should allow navigation without opening popout", async () => {
|
||||
const guard = platformPopoutGuard(["linux"]);
|
||||
const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState));
|
||||
|
||||
expect(getPlatformInfoSpy).toHaveBeenCalled();
|
||||
expect(openPopoutSpy).not.toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when forcePopout is true", () => {
|
||||
beforeEach(() => {
|
||||
getPlatformInfoSpy.mockResolvedValue({ os: "win" });
|
||||
inPopoutSpy.mockReturnValue(false);
|
||||
inSidebarSpy.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should open popout regardless of platform", async () => {
|
||||
const guard = platformPopoutGuard(["linux"], true);
|
||||
const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState));
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
"popup/index.html#/login-with-passkey?param=value&autoClosePopout=true",
|
||||
);
|
||||
expect(closePopupSpy).toHaveBeenCalledWith(window);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should not open popout when already in popout", async () => {
|
||||
inPopoutSpy.mockReturnValue(true);
|
||||
|
||||
const guard = platformPopoutGuard(["linux"], true);
|
||||
const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState));
|
||||
|
||||
expect(openPopoutSpy).not.toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with multiple platforms", () => {
|
||||
beforeEach(() => {
|
||||
inPopoutSpy.mockReturnValue(false);
|
||||
inSidebarSpy.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it.each(["linux", "mac", "win"])(
|
||||
"should open popout when platform is %s and included in platforms array",
|
||||
async (platform) => {
|
||||
getPlatformInfoSpy.mockResolvedValue({ os: platform });
|
||||
|
||||
const guard = platformPopoutGuard(["linux", "mac", "win"]);
|
||||
const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState));
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
"popup/index.html#/login-with-passkey?param=value&autoClosePopout=true",
|
||||
);
|
||||
expect(closePopupSpy).toHaveBeenCalledWith(window);
|
||||
expect(result).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("should not open popout when platform is not in the array", async () => {
|
||||
getPlatformInfoSpy.mockResolvedValue({ os: "android" });
|
||||
|
||||
const guard = platformPopoutGuard(["linux", "mac"]);
|
||||
const result = await TestBed.runInInjectionContext(() => guard(mockRoute, mockState));
|
||||
|
||||
expect(openPopoutSpy).not.toHaveBeenCalled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("url handling", () => {
|
||||
beforeEach(() => {
|
||||
getPlatformInfoSpy.mockResolvedValue({ os: "linux" });
|
||||
inPopoutSpy.mockReturnValue(false);
|
||||
inSidebarSpy.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should preserve query parameters in the popout url", async () => {
|
||||
const stateWithQuery: RouterStateSnapshot = {
|
||||
url: "/path?foo=bar&baz=qux",
|
||||
} as RouterStateSnapshot;
|
||||
|
||||
const guard = platformPopoutGuard(["linux"]);
|
||||
await TestBed.runInInjectionContext(() => guard(mockRoute, stateWithQuery));
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
"popup/index.html#/path?foo=bar&baz=qux&autoClosePopout=true",
|
||||
);
|
||||
expect(closePopupSpy).toHaveBeenCalledWith(window);
|
||||
});
|
||||
|
||||
it("should handle urls without query parameters", async () => {
|
||||
const stateWithoutQuery: RouterStateSnapshot = {
|
||||
url: "/simple-path",
|
||||
} as RouterStateSnapshot;
|
||||
|
||||
const guard = platformPopoutGuard(["linux"]);
|
||||
await TestBed.runInInjectionContext(() => guard(mockRoute, stateWithoutQuery));
|
||||
|
||||
expect(openPopoutSpy).toHaveBeenCalledWith(
|
||||
"popup/index.html#/simple-path?autoClosePopout=true",
|
||||
);
|
||||
expect(closePopupSpy).toHaveBeenCalledWith(window);
|
||||
});
|
||||
});
|
||||
});
|
||||
46
apps/browser/src/auth/popup/guards/platform-popout.guard.ts
Normal file
46
apps/browser/src/auth/popup/guards/platform-popout.guard.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from "@angular/router";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
||||
|
||||
/**
|
||||
* Guard that forces a popout window for specific platforms.
|
||||
* Useful when popup context would close during operations (e.g., WebAuthn on Linux).
|
||||
*
|
||||
* @param platforms - Array of platform OS strings (e.g., ["linux", "mac", "win"])
|
||||
* @param forcePopout - If true, always force popout regardless of platform (useful for testing)
|
||||
* @returns CanActivateFn that opens popout and blocks navigation if conditions met
|
||||
*/
|
||||
export function platformPopoutGuard(
|
||||
platforms: string[],
|
||||
forcePopout: boolean = false,
|
||||
): CanActivateFn {
|
||||
return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||
// Check if current platform matches
|
||||
const platformInfo = await BrowserApi.getPlatformInfo();
|
||||
const isPlatformMatch = platforms.includes(platformInfo.os);
|
||||
|
||||
// Check if already in popout/sidebar
|
||||
const inPopout = BrowserPopupUtils.inPopout(window);
|
||||
const inSidebar = BrowserPopupUtils.inSidebar(window);
|
||||
|
||||
// Open popout if conditions met
|
||||
if ((isPlatformMatch || forcePopout) && !inPopout && !inSidebar) {
|
||||
// Add autoClosePopout query param to signal the popout should close after completion
|
||||
const [path, existingQuery] = state.url.split("?");
|
||||
const params = new URLSearchParams(existingQuery || "");
|
||||
params.set("autoClosePopout", "true");
|
||||
const urlWithAutoClose = `${path}?${params.toString()}`;
|
||||
|
||||
// Open the popout window
|
||||
await BrowserPopupUtils.openPopout(`popup/index.html#${urlWithAutoClose}`);
|
||||
|
||||
// Close the original popup window
|
||||
BrowserApi.closePopup(window);
|
||||
|
||||
return false; // Block navigation - popout will reload
|
||||
}
|
||||
|
||||
return true; // Allow navigation
|
||||
};
|
||||
}
|
||||
@@ -658,7 +658,7 @@ export default class NotificationBackground {
|
||||
if (
|
||||
username !== null &&
|
||||
newPassword === null &&
|
||||
cipher.login.username === normalizedUsername &&
|
||||
cipher.login.username.toLowerCase() === normalizedUsername &&
|
||||
cipher.login.password === currentPassword
|
||||
) {
|
||||
// Assumed to be a login
|
||||
|
||||
@@ -262,11 +262,30 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
*/
|
||||
private notificationDataIncompleteOnBeforeRequest = (tabId: number) => {
|
||||
const modifyLoginData = this.modifyLoginCipherFormData.get(tabId);
|
||||
return (
|
||||
!modifyLoginData ||
|
||||
!this.shouldAttemptNotification(modifyLoginData, NotificationTypes.Add) ||
|
||||
!this.shouldAttemptNotification(modifyLoginData, NotificationTypes.Change)
|
||||
|
||||
if (!modifyLoginData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const shouldAttemptAddNotification = this.shouldAttemptNotification(
|
||||
modifyLoginData,
|
||||
NotificationTypes.Add,
|
||||
);
|
||||
|
||||
if (shouldAttemptAddNotification) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const shouldAttemptChangeNotification = this.shouldAttemptNotification(
|
||||
modifyLoginData,
|
||||
NotificationTypes.Change,
|
||||
);
|
||||
|
||||
if (shouldAttemptChangeNotification) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -454,15 +473,27 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
notificationType: NotificationType,
|
||||
): boolean => {
|
||||
// Intentionally not stripping whitespace characters here as they
|
||||
// represent user entry.
|
||||
const usernameFieldHasValue = !!(modifyLoginData?.username || "").length;
|
||||
const passwordFieldHasValue = !!(modifyLoginData?.password || "").length;
|
||||
const newPasswordFieldHasValue = !!(modifyLoginData?.newPassword || "").length;
|
||||
|
||||
const canBeUserLogin = usernameFieldHasValue && passwordFieldHasValue;
|
||||
const canBePasswordUpdate = passwordFieldHasValue && newPasswordFieldHasValue;
|
||||
|
||||
switch (notificationType) {
|
||||
// `Add` case included because all forms with cached usernames (from previous
|
||||
// visits) will appear to be "password only" and otherwise trigger the new login
|
||||
// save notification.
|
||||
case NotificationTypes.Add:
|
||||
return (
|
||||
modifyLoginData?.username && !!(modifyLoginData.password || modifyLoginData.newPassword)
|
||||
);
|
||||
// Can be values for nonstored login or account creation
|
||||
return usernameFieldHasValue && (passwordFieldHasValue || newPasswordFieldHasValue);
|
||||
case NotificationTypes.Change:
|
||||
return !!(modifyLoginData.password || modifyLoginData.newPassword);
|
||||
// Can be login with nonstored login changes or account password update
|
||||
return canBeUserLogin || canBePasswordUpdate;
|
||||
case NotificationTypes.AtRiskPassword:
|
||||
return !modifyLoginData.newPassword;
|
||||
return !newPasswordFieldHasValue;
|
||||
case NotificationTypes.Unlock:
|
||||
// Unlock notifications are handled separately and do not require form data
|
||||
return false;
|
||||
|
||||
@@ -39,6 +39,7 @@ export class AutoFillConstants {
|
||||
"otpcode",
|
||||
"onetimepassword",
|
||||
"security_code",
|
||||
"second-factor",
|
||||
"twofactor",
|
||||
"twofa",
|
||||
"twofactorcode",
|
||||
|
||||
@@ -1603,14 +1603,14 @@ describe("AutofillOverlayContentService", () => {
|
||||
|
||||
it("skips triggering submission if a button is not found", async () => {
|
||||
const submitButton = document.querySelector("button");
|
||||
submitButton.remove();
|
||||
submitButton?.remove();
|
||||
|
||||
await autofillOverlayContentService.setupOverlayListeners(
|
||||
autofillFieldElement,
|
||||
autofillFieldData,
|
||||
pageDetailsMock,
|
||||
);
|
||||
submitButton.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||
submitButton?.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||
|
||||
expect(sendExtensionMessageSpy).not.toHaveBeenCalledWith(
|
||||
"formFieldSubmitted",
|
||||
@@ -1627,7 +1627,7 @@ describe("AutofillOverlayContentService", () => {
|
||||
pageDetailsMock,
|
||||
);
|
||||
await flushPromises();
|
||||
submitButton.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||
submitButton?.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||
|
||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith(
|
||||
"formFieldSubmitted",
|
||||
@@ -1641,7 +1641,7 @@ describe("AutofillOverlayContentService", () => {
|
||||
<div id="shadow-root"></div>
|
||||
<button id="button-el">Change Password</button>
|
||||
</div>`;
|
||||
const shadowRoot = document.getElementById("shadow-root").attachShadow({ mode: "open" });
|
||||
const shadowRoot = document.getElementById("shadow-root")!.attachShadow({ mode: "open" });
|
||||
shadowRoot.innerHTML = `
|
||||
<input type="password" id="password-field-1" placeholder="new password" />
|
||||
`;
|
||||
@@ -1668,7 +1668,7 @@ describe("AutofillOverlayContentService", () => {
|
||||
pageDetailsMock,
|
||||
);
|
||||
await flushPromises();
|
||||
buttonElement.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||
buttonElement?.dispatchEvent(new KeyboardEvent("keyup", { code: "Enter" }));
|
||||
|
||||
expect(sendExtensionMessageSpy).toHaveBeenCalledWith(
|
||||
"formFieldSubmitted",
|
||||
@@ -1716,6 +1716,85 @@ describe("AutofillOverlayContentService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshMenuLayerPosition", () => {
|
||||
it("calls refreshTopLayerPosition on the inline menu content service", () => {
|
||||
autofillOverlayContentService.refreshMenuLayerPosition();
|
||||
|
||||
expect(inlineMenuContentService.refreshTopLayerPosition).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not throw if inline menu content service is not available", () => {
|
||||
const serviceWithoutInlineMenu = new AutofillOverlayContentService(
|
||||
domQueryService,
|
||||
domElementVisibilityService,
|
||||
inlineMenuFieldQualificationService,
|
||||
);
|
||||
|
||||
expect(() => serviceWithoutInlineMenu.refreshMenuLayerPosition()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOwnedInlineMenuTagNames", () => {
|
||||
it("returns tag names from the inline menu content service", () => {
|
||||
inlineMenuContentService.getOwnedTagNames.mockReturnValue(["div", "span"]);
|
||||
|
||||
const result = autofillOverlayContentService.getOwnedInlineMenuTagNames();
|
||||
|
||||
expect(result).toEqual(["div", "span"]);
|
||||
});
|
||||
|
||||
it("returns an empty array if inline menu content service is not available", () => {
|
||||
const serviceWithoutInlineMenu = new AutofillOverlayContentService(
|
||||
domQueryService,
|
||||
domElementVisibilityService,
|
||||
inlineMenuFieldQualificationService,
|
||||
);
|
||||
|
||||
const result = serviceWithoutInlineMenu.getOwnedInlineMenuTagNames();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUnownedTopLayerItems", () => {
|
||||
it("returns unowned top layer items from the inline menu content service", () => {
|
||||
const mockElements = document.querySelectorAll("div");
|
||||
inlineMenuContentService.getUnownedTopLayerItems.mockReturnValue(mockElements);
|
||||
|
||||
const result = autofillOverlayContentService.getUnownedTopLayerItems(true);
|
||||
|
||||
expect(result).toEqual(mockElements);
|
||||
expect(inlineMenuContentService.getUnownedTopLayerItems).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("returns undefined if inline menu content service is not available", () => {
|
||||
const serviceWithoutInlineMenu = new AutofillOverlayContentService(
|
||||
domQueryService,
|
||||
domElementVisibilityService,
|
||||
inlineMenuFieldQualificationService,
|
||||
);
|
||||
|
||||
const result = serviceWithoutInlineMenu.getUnownedTopLayerItems();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearUserFilledFields", () => {
|
||||
it("deletes all user filled fields", () => {
|
||||
const mockElement1 = document.createElement("input") as FillableFormFieldElement;
|
||||
const mockElement2 = document.createElement("input") as FillableFormFieldElement;
|
||||
autofillOverlayContentService["userFilledFields"] = {
|
||||
username: mockElement1,
|
||||
password: mockElement2,
|
||||
};
|
||||
|
||||
autofillOverlayContentService.clearUserFilledFields();
|
||||
|
||||
expect(autofillOverlayContentService["userFilledFields"]).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleOverlayRepositionEvent", () => {
|
||||
const repositionEvents = [EVENTS.SCROLL, EVENTS.RESIZE];
|
||||
repositionEvents.forEach((repositionEvent) => {
|
||||
@@ -2049,7 +2128,7 @@ describe("AutofillOverlayContentService", () => {
|
||||
});
|
||||
|
||||
it("skips focusing an element if no recently focused field exists", async () => {
|
||||
autofillOverlayContentService["mostRecentlyFocusedField"] = undefined;
|
||||
(autofillOverlayContentService as any)["mostRecentlyFocusedField"] = null;
|
||||
|
||||
sendMockExtensionMessage({
|
||||
command: "redirectAutofillInlineMenuFocusOut",
|
||||
@@ -2149,7 +2228,6 @@ describe("AutofillOverlayContentService", () => {
|
||||
});
|
||||
|
||||
it("returns null if the sub frame URL cannot be parsed correctly", async () => {
|
||||
delete globalThis.location;
|
||||
globalThis.location = { href: "invalid-base" } as Location;
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
|
||||
@@ -945,7 +945,8 @@ export class InlineMenuFieldQualificationService
|
||||
!fieldType ||
|
||||
!this.usernameFieldTypes.has(fieldType) ||
|
||||
this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) ||
|
||||
this.fieldHasDisqualifyingAttributeValue(field)
|
||||
this.fieldHasDisqualifyingAttributeValue(field) ||
|
||||
this.isTotpField(field)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/ke
|
||||
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
|
||||
import { AuthExtensionRoute } from "../auth/popup/constants/auth-extension-route.constant";
|
||||
import { fido2AuthGuard } from "../auth/popup/guards/fido2-auth.guard";
|
||||
import { platformPopoutGuard } from "../auth/popup/guards/platform-popout.guard";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
import { ExtensionDeviceManagementComponent } from "../auth/popup/settings/extension-device-management.component";
|
||||
import { Fido2Component } from "../autofill/popup/fido2/fido2.component";
|
||||
@@ -414,7 +415,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: AuthRoute.LoginWithPasskey,
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides), platformPopoutGuard(["linux"])],
|
||||
data: {
|
||||
pageIcon: TwoFactorAuthSecurityKeyIcon,
|
||||
pageTitle: {
|
||||
|
||||
@@ -381,4 +381,88 @@ describe("AddEditV2Component", () => {
|
||||
expect(navigate).toHaveBeenCalledWith(["/tabs/vault"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reloadAddEditCipherData", () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
addEditCipherInfo$.next({
|
||||
cipher: {
|
||||
name: "InitialName",
|
||||
type: CipherType.Login,
|
||||
login: {
|
||||
password: "initialPassword",
|
||||
username: "initialUsername",
|
||||
uris: [{ uri: "https://initial.com" }],
|
||||
},
|
||||
},
|
||||
} as AddEditCipherInfo);
|
||||
queryParams$.next({});
|
||||
tick();
|
||||
|
||||
cipherServiceMock.setAddEditCipherInfo.mockClear();
|
||||
}));
|
||||
|
||||
it("replaces all initialValues with new data, clearing stale fields", fakeAsync(() => {
|
||||
const newCipherInfo = {
|
||||
cipher: {
|
||||
name: "UpdatedName",
|
||||
type: CipherType.Login,
|
||||
login: {
|
||||
password: "updatedPassword",
|
||||
uris: [{ uri: "https://updated.com" }],
|
||||
},
|
||||
},
|
||||
} as AddEditCipherInfo;
|
||||
|
||||
addEditCipherInfo$.next(newCipherInfo);
|
||||
|
||||
const messageListener = component["messageListener"];
|
||||
messageListener({ command: "reloadAddEditCipherData" });
|
||||
tick();
|
||||
|
||||
expect(component.config.initialValues).toEqual({
|
||||
name: "UpdatedName",
|
||||
password: "updatedPassword",
|
||||
loginUri: "https://updated.com",
|
||||
} as OptionalInitialValues);
|
||||
|
||||
expect(cipherServiceMock.setAddEditCipherInfo).toHaveBeenCalledWith(null, "UserId");
|
||||
}));
|
||||
|
||||
it("does not reload data if config is not set", fakeAsync(() => {
|
||||
component.config = null;
|
||||
|
||||
const messageListener = component["messageListener"];
|
||||
messageListener({ command: "reloadAddEditCipherData" });
|
||||
tick();
|
||||
|
||||
expect(cipherServiceMock.setAddEditCipherInfo).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it("does not reload data if latestCipherInfo is null", fakeAsync(() => {
|
||||
addEditCipherInfo$.next(null);
|
||||
|
||||
const messageListener = component["messageListener"];
|
||||
messageListener({ command: "reloadAddEditCipherData" });
|
||||
tick();
|
||||
|
||||
expect(component.config.initialValues).toEqual({
|
||||
name: "InitialName",
|
||||
password: "initialPassword",
|
||||
username: "initialUsername",
|
||||
loginUri: "https://initial.com",
|
||||
} as OptionalInitialValues);
|
||||
|
||||
expect(cipherServiceMock.setAddEditCipherInfo).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it("ignores messages with different commands", fakeAsync(() => {
|
||||
const initialValues = component.config.initialValues;
|
||||
|
||||
const messageListener = component["messageListener"];
|
||||
messageListener({ command: "someOtherCommand" });
|
||||
tick();
|
||||
|
||||
expect(component.config.initialValues).toBe(initialValues);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component, OnInit, OnDestroy } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
@@ -158,7 +158,7 @@ export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
||||
IconButtonModule,
|
||||
],
|
||||
})
|
||||
export class AddEditV2Component implements OnInit {
|
||||
export class AddEditV2Component implements OnInit, OnDestroy {
|
||||
headerText: string;
|
||||
config: CipherFormConfig;
|
||||
canDeleteCipher$: Observable<boolean>;
|
||||
@@ -200,12 +200,58 @@ export class AddEditV2Component implements OnInit {
|
||||
this.subscribeToParams();
|
||||
}
|
||||
|
||||
private messageListener: (message: any) => void;
|
||||
|
||||
async ngOnInit() {
|
||||
this.fido2PopoutSessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||
|
||||
if (BrowserPopupUtils.inPopout(window)) {
|
||||
this.popupCloseWarningService.enable();
|
||||
}
|
||||
|
||||
// Listen for messages to reload cipher data when the pop up is already open
|
||||
this.messageListener = async (message: any) => {
|
||||
if (message?.command === "reloadAddEditCipherData") {
|
||||
try {
|
||||
await this.reloadCipherData();
|
||||
} catch (error) {
|
||||
this.logService.error("Failed to reload cipher data", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
BrowserApi.addListener(chrome.runtime.onMessage, this.messageListener);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.messageListener) {
|
||||
BrowserApi.removeListener(chrome.runtime.onMessage, this.messageListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the cipher data when the popup is already open and new form data is submitted.
|
||||
* This completely replaces the initialValues to clear any stale data from the previous submission.
|
||||
*/
|
||||
private async reloadCipherData() {
|
||||
if (!this.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
const latestCipherInfo = await firstValueFrom(
|
||||
this.cipherService.addEditCipherInfo$(activeUserId),
|
||||
);
|
||||
|
||||
if (latestCipherInfo != null) {
|
||||
this.config = {
|
||||
...this.config,
|
||||
initialValues: mapAddEditCipherInfoToInitialValues(latestCipherInfo),
|
||||
};
|
||||
|
||||
// Be sure to clear the "cached" cipher info, so it doesn't get used again
|
||||
await this.cipherService.setAddEditCipherInfo(null, activeUserId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
{ provide: RestrictedItemTypesService, useValue: { restricted$: of([]) } },
|
||||
{
|
||||
provide: CipherArchiveService,
|
||||
useValue: { userCanArchive$: () => of(true), hasArchiveFlagEnabled$: () => of(true) },
|
||||
useValue: { userCanArchive$: () => of(true), hasArchiveFlagEnabled$: of(true) },
|
||||
},
|
||||
{ provide: ToastService, useValue: { showToast: () => {} } },
|
||||
{ provide: Router, useValue: { navigate: () => Promise.resolve(true) } },
|
||||
|
||||
@@ -141,7 +141,7 @@ export class ItemMoreOptionsComponent {
|
||||
}),
|
||||
);
|
||||
|
||||
protected showArchive$: Observable<boolean> = this.cipherArchiveService.hasArchiveFlagEnabled$();
|
||||
protected showArchive$: Observable<boolean> = this.cipherArchiveService.hasArchiveFlagEnabled$;
|
||||
|
||||
protected canArchive$: Observable<boolean> = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
|
||||
@@ -49,7 +49,7 @@ export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
||||
);
|
||||
|
||||
protected readonly showArchiveItem = toSignal(this.cipherArchiveService.hasArchiveFlagEnabled$());
|
||||
protected readonly showArchiveItem = toSignal(this.cipherArchiveService.hasArchiveFlagEnabled$);
|
||||
|
||||
protected readonly userHasArchivedItems = toSignal(
|
||||
this.userId$.pipe(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mock } from "jest-mock-extended";
|
||||
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
||||
|
||||
import {
|
||||
@@ -23,6 +24,19 @@ describe("VaultPopoutWindow", () => {
|
||||
.spyOn(BrowserPopupUtils, "closeSingleActionPopout")
|
||||
.mockImplementation();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([]);
|
||||
jest.spyOn(BrowserApi, "updateWindowProperties").mockResolvedValue();
|
||||
global.chrome = {
|
||||
...global.chrome,
|
||||
runtime: {
|
||||
...global.chrome?.runtime,
|
||||
sendMessage: jest.fn().mockResolvedValue(undefined),
|
||||
getURL: jest.fn((path) => `chrome-extension://extension-id/${path}`),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -123,6 +137,32 @@ describe("VaultPopoutWindow", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a message to refresh data when the popup is already open", async () => {
|
||||
const existingPopupTab = {
|
||||
id: 123,
|
||||
windowId: 456,
|
||||
url: `chrome-extension://extension-id/popup/index.html#/edit-cipher?singleActionPopout=${VaultPopoutType.addEditVaultItem}_${CipherType.Login}`,
|
||||
} as chrome.tabs.Tab;
|
||||
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([existingPopupTab]);
|
||||
const sendMessageSpy = jest.spyOn(chrome.runtime, "sendMessage");
|
||||
const updateWindowSpy = jest.spyOn(BrowserApi, "updateWindowProperties");
|
||||
|
||||
await openAddEditVaultItemPopout(
|
||||
mock<chrome.tabs.Tab>({ windowId: 1, url: "https://jest-testing-website.com" }),
|
||||
{
|
||||
cipherType: CipherType.Login,
|
||||
},
|
||||
);
|
||||
|
||||
expect(openPopoutSpy).not.toHaveBeenCalled();
|
||||
expect(sendMessageSpy).toHaveBeenCalledWith({
|
||||
command: "reloadAddEditCipherData",
|
||||
data: { cipherId: undefined, cipherType: CipherType.Login },
|
||||
});
|
||||
expect(updateWindowSpy).toHaveBeenCalledWith(456, { focused: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeAddEditVaultItemPopout", () => {
|
||||
|
||||
@@ -115,10 +115,26 @@ async function openAddEditVaultItemPopout(
|
||||
addEditCipherUrl += formatQueryString("uri", url);
|
||||
}
|
||||
|
||||
await BrowserPopupUtils.openPopout(addEditCipherUrl, {
|
||||
singleActionKey,
|
||||
senderWindowId: windowId,
|
||||
});
|
||||
const extensionUrl = chrome.runtime.getURL("popup/index.html");
|
||||
const existingPopupTabs = await BrowserApi.tabsQuery({ url: `${extensionUrl}*` });
|
||||
const existingPopup = existingPopupTabs.find((tab) =>
|
||||
tab.url?.includes(`singleActionPopout=${singleActionKey}`),
|
||||
);
|
||||
// Check if the an existing popup is already open
|
||||
try {
|
||||
await chrome.runtime.sendMessage({
|
||||
command: "reloadAddEditCipherData",
|
||||
data: { cipherId, cipherType },
|
||||
});
|
||||
await BrowserApi.updateWindowProperties(existingPopup.windowId, {
|
||||
focused: true,
|
||||
});
|
||||
} catch {
|
||||
await BrowserPopupUtils.openPopout(addEditCipherUrl, {
|
||||
singleActionKey,
|
||||
senderWindowId: windowId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,7 @@ import { TwoFactorService, TwoFactorApiService } from "@bitwarden/common/auth/tw
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
@@ -81,6 +82,7 @@ export class LoginCommand {
|
||||
protected ssoUrlService: SsoUrlService,
|
||||
protected i18nService: I18nService,
|
||||
protected masterPasswordService: MasterPasswordServiceAbstraction,
|
||||
protected encryptedMigrator: EncryptedMigrator,
|
||||
) {}
|
||||
|
||||
async run(email: string, password: string, options: OptionValues) {
|
||||
@@ -367,6 +369,8 @@ export class LoginCommand {
|
||||
}
|
||||
}
|
||||
|
||||
await this.encryptedMigrator.runMigrations(response.userId, password);
|
||||
|
||||
return await this.handleSuccessResponse(response);
|
||||
} catch (e) {
|
||||
if (
|
||||
|
||||
@@ -182,6 +182,7 @@ export abstract class BaseProgram {
|
||||
this.serviceContainer.organizationApiService,
|
||||
this.serviceContainer.logout,
|
||||
this.serviceContainer.i18nService,
|
||||
this.serviceContainer.encryptedMigrator,
|
||||
this.serviceContainer.masterPasswordUnlockService,
|
||||
this.serviceContainer.configService,
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { MasterPasswordVerificationResponse } from "@bitwarden/common/auth/types/verification";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
@@ -40,6 +41,7 @@ describe("UnlockCommand", () => {
|
||||
const organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
const logout = jest.fn();
|
||||
const i18nService = mock<I18nService>();
|
||||
const encryptedMigrator = mock<EncryptedMigrator>();
|
||||
const masterPasswordUnlockService = mock<MasterPasswordUnlockService>();
|
||||
const configService = mock<ConfigService>();
|
||||
|
||||
@@ -92,6 +94,7 @@ describe("UnlockCommand", () => {
|
||||
organizationApiService,
|
||||
logout,
|
||||
i18nService,
|
||||
encryptedMigrator,
|
||||
masterPasswordUnlockService,
|
||||
configService,
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { VerificationType } from "@bitwarden/common/auth/enums/verification-type
|
||||
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
@@ -38,6 +39,7 @@ export class UnlockCommand {
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private logout: () => Promise<void>,
|
||||
private i18nService: I18nService,
|
||||
private encryptedMigrator: EncryptedMigrator,
|
||||
private masterPasswordUnlockService: MasterPasswordUnlockService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
@@ -116,6 +118,8 @@ export class UnlockCommand {
|
||||
}
|
||||
}
|
||||
|
||||
await this.encryptedMigrator.runMigrations(userId, password);
|
||||
|
||||
return this.successResponse();
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@ export class OssServeConfigurator {
|
||||
this.serviceContainer.organizationApiService,
|
||||
async () => await this.serviceContainer.logout(),
|
||||
this.serviceContainer.i18nService,
|
||||
this.serviceContainer.encryptedMigrator,
|
||||
this.serviceContainer.masterPasswordUnlockService,
|
||||
this.serviceContainer.configService,
|
||||
);
|
||||
|
||||
@@ -195,6 +195,7 @@ export class Program extends BaseProgram {
|
||||
this.serviceContainer.ssoUrlService,
|
||||
this.serviceContainer.i18nService,
|
||||
this.serviceContainer.masterPasswordService,
|
||||
this.serviceContainer.encryptedMigrator,
|
||||
);
|
||||
const response = await command.run(email, password, options);
|
||||
this.processResponse(response, true);
|
||||
@@ -311,6 +312,7 @@ export class Program extends BaseProgram {
|
||||
this.serviceContainer.organizationApiService,
|
||||
async () => await this.serviceContainer.logout(),
|
||||
this.serviceContainer.i18nService,
|
||||
this.serviceContainer.encryptedMigrator,
|
||||
this.serviceContainer.masterPasswordUnlockService,
|
||||
this.serviceContainer.configService,
|
||||
);
|
||||
|
||||
@@ -76,6 +76,10 @@ import {
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||
import { DefaultEncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/default-encrypted-migrator";
|
||||
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
|
||||
import { DefaultChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service";
|
||||
import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
||||
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
@@ -324,6 +328,7 @@ export class ServiceContainer {
|
||||
cipherEncryptionService: CipherEncryptionService;
|
||||
restrictedItemTypesService: RestrictedItemTypesService;
|
||||
cliRestrictedItemTypesService: CliRestrictedItemTypesService;
|
||||
encryptedMigrator: EncryptedMigrator;
|
||||
securityStateService: SecurityStateService;
|
||||
masterPasswordUnlockService: MasterPasswordUnlockService;
|
||||
cipherArchiveService: CipherArchiveService;
|
||||
@@ -975,6 +980,16 @@ export class ServiceContainer {
|
||||
);
|
||||
|
||||
this.masterPasswordApiService = new MasterPasswordApiService(this.apiService, this.logService);
|
||||
const changeKdfApiService = new DefaultChangeKdfApiService(this.apiService);
|
||||
const changeKdfService = new DefaultChangeKdfService(changeKdfApiService, this.sdkService);
|
||||
this.encryptedMigrator = new DefaultEncryptedMigrator(
|
||||
this.kdfConfigService,
|
||||
changeKdfService,
|
||||
this.logService,
|
||||
this.configService,
|
||||
this.masterPasswordService,
|
||||
this.syncService,
|
||||
);
|
||||
}
|
||||
|
||||
async logout() {
|
||||
|
||||
@@ -11,4 +11,4 @@ if (isRelease) {
|
||||
process.env.RUST_LOG = 'debug';
|
||||
}
|
||||
|
||||
execSync(`napi build --platform --js false`, { stdio: 'inherit', env: process.env });
|
||||
execSync(`napi build --platform --js false ${isRelease ? '--release' : ''}`, { stdio: 'inherit', env: process.env });
|
||||
|
||||
@@ -32,8 +32,9 @@
|
||||
<string>/Library/Application Support/Microsoft Edge Beta/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Microsoft Edge Dev/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Microsoft Edge Canary/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Vivaldi/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Vivaldi/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Zen/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/net.imput.helium</string>
|
||||
</array>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
|
||||
@@ -1093,6 +1093,24 @@
|
||||
"learnMore": {
|
||||
"message": "Learn more"
|
||||
},
|
||||
"migrationsFailed": {
|
||||
"message": "An error occurred updating the encryption settings."
|
||||
},
|
||||
"updateEncryptionSettingsTitle": {
|
||||
"message": "Update your encryption settings"
|
||||
},
|
||||
"updateEncryptionSettingsDesc": {
|
||||
"message": "The new recommended encryption settings will improve your account security. Enter your master password to update now."
|
||||
},
|
||||
"confirmIdentityToContinue": {
|
||||
"message": "Confirm your identity to continue"
|
||||
},
|
||||
"enterYourMasterPassword": {
|
||||
"message": "Enter your master password"
|
||||
},
|
||||
"updateSettings": {
|
||||
"message": "Update settings"
|
||||
},
|
||||
"featureUnavailable": {
|
||||
"message": "Feature unavailable"
|
||||
},
|
||||
|
||||
@@ -314,6 +314,7 @@ export class NativeMessagingMain {
|
||||
"Microsoft Edge Canary": `${this.homedir()}/Library/Application\ Support/Microsoft\ Edge\ Canary/`,
|
||||
Vivaldi: `${this.homedir()}/Library/Application\ Support/Vivaldi/`,
|
||||
Zen: `${this.homedir()}/Library/Application\ Support/Zen/`,
|
||||
Helium: `${this.homedir()}/Library/Application\ Support/net.imput.helium/`,
|
||||
};
|
||||
/* eslint-enable no-useless-escape */
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
||||
switchMap((id) =>
|
||||
combineLatest([
|
||||
this.cipherArchiveService.userCanArchive$(id),
|
||||
this.cipherArchiveService.hasArchiveFlagEnabled$(),
|
||||
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||
]),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -11,6 +11,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
@@ -59,6 +60,7 @@ export class VaultFilterComponent
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
protected cipherService: CipherService,
|
||||
protected cipherArchiveService: CipherArchiveService,
|
||||
premiumUpgradePromptService: PremiumUpgradePromptService,
|
||||
) {
|
||||
super(
|
||||
vaultFilterService,
|
||||
@@ -72,6 +74,7 @@ export class VaultFilterComponent
|
||||
restrictedItemTypesService,
|
||||
cipherService,
|
||||
cipherArchiveService,
|
||||
premiumUpgradePromptService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ export class RecoverTwoFactorComponent implements OnInit {
|
||||
message: this.i18nService.t("twoStepRecoverDisabled"),
|
||||
});
|
||||
|
||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||
await this.loginSuccessHandlerService.run(authResult.userId, this.masterPassword);
|
||||
|
||||
await this.router.navigate(["/settings/security/two-factor"]);
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { firstValueFrom, Observable } from "rxjs";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf-service.abstraction";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
@@ -203,10 +203,22 @@
|
||||
{{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
@if (showArchiveButton) {
|
||||
<button bitMenuItem (click)="archive()" type="button">
|
||||
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
|
||||
{{ "archiveVerb" | i18n }}
|
||||
</button>
|
||||
@if (userCanArchive) {
|
||||
<button bitMenuItem (click)="archive()" type="button">
|
||||
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
|
||||
{{ "archiveVerb" | i18n }}
|
||||
</button>
|
||||
}
|
||||
@if (!userCanArchive) {
|
||||
<button bitMenuItem (click)="badge.promptForPremium($event)" type="button">
|
||||
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
|
||||
{{ "archiveVerb" | i18n }}
|
||||
<!-- Hide app-premium badge from accessibility tools as it results in a button within a button -->
|
||||
<div slot="end" class="-tw-mt-0.5" aria-hidden>
|
||||
<app-premium-badge #badge></app-premium-badge>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
@if (showUnArchiveButton) {
|
||||
|
||||
@@ -72,6 +72,7 @@ describe("VaultCipherRowComponent", () => {
|
||||
|
||||
fixture = TestBed.createComponent(VaultCipherRowComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.componentRef.setInput("archiveEnabled", false);
|
||||
overlayContainer = TestBed.inject(OverlayContainer);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
@@ -101,8 +102,10 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() userCanArchive: boolean;
|
||||
/** Archive feature is enabled */
|
||||
readonly archiveEnabled = input.required<boolean>();
|
||||
/**
|
||||
* Enforge Org Data Ownership Policy Status
|
||||
* Enforce Org Data Ownership Policy Status
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@@ -142,16 +145,21 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
||||
}
|
||||
|
||||
protected get showArchiveButton() {
|
||||
if (!this.archiveEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.userCanArchive &&
|
||||
!CipherViewLikeUtils.isArchived(this.cipher) &&
|
||||
!CipherViewLikeUtils.isDeleted(this.cipher) &&
|
||||
!this.cipher.organizationId
|
||||
!CipherViewLikeUtils.isArchived(this.cipher) && !CipherViewLikeUtils.isDeleted(this.cipher)
|
||||
);
|
||||
}
|
||||
|
||||
// If item is archived always show unarchive button, even if user is not premium
|
||||
protected get showUnArchiveButton() {
|
||||
if (!this.archiveEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CipherViewLikeUtils.isArchived(this.cipher);
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@
|
||||
(onEvent)="event($event)"
|
||||
[userCanArchive]="userCanArchive"
|
||||
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy"
|
||||
[archiveEnabled]="archiveFeatureEnabled$ | async"
|
||||
></tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { of } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
@@ -54,6 +55,12 @@ describe("VaultItemsComponent", () => {
|
||||
t: (key: string) => key,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CipherArchiveService,
|
||||
useValue: {
|
||||
hasArchiveFlagEnabled$: of(true),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import {
|
||||
RestrictedCipherType,
|
||||
@@ -145,9 +146,12 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
||||
protected disableMenu$: Observable<boolean>;
|
||||
private restrictedTypes: RestrictedCipherType[] = [];
|
||||
|
||||
protected archiveFeatureEnabled$ = this.cipherArchiveService.hasArchiveFlagEnabled$;
|
||||
|
||||
constructor(
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
protected cipherArchiveService: CipherArchiveService,
|
||||
) {
|
||||
this.canDeleteSelected$ = this.selection.changed.pipe(
|
||||
startWith(null),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { ScrollLayoutDirective, TableModule } from "@bitwarden/components";
|
||||
import { CopyCipherFieldDirective } from "@bitwarden/vault";
|
||||
|
||||
@@ -29,6 +30,7 @@ import { VaultItemsComponent } from "./vault-items.component";
|
||||
PipesModule,
|
||||
CopyCipherFieldDirective,
|
||||
ScrollLayoutDirective,
|
||||
PremiumBadgeComponent,
|
||||
],
|
||||
declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent],
|
||||
exports: [VaultItemsComponent],
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -143,6 +144,12 @@ export default {
|
||||
isCipherRestricted: () => false, // No restrictions for this story
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CipherArchiveService,
|
||||
useValue: {
|
||||
hasArchiveFlagEnabled$: of(true),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
applicationConfig({
|
||||
|
||||
@@ -19,8 +19,10 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
@@ -170,6 +172,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
protected cipherService: CipherService,
|
||||
protected cipherArchiveService: CipherArchiveService,
|
||||
private premiumUpgradePromptService: PremiumUpgradePromptService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -252,14 +255,20 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
async buildAllFilters(): Promise<VaultFilterList> {
|
||||
const hasArchiveFlag = await firstValueFrom(this.cipherArchiveService.hasArchiveFlagEnabled$());
|
||||
const [userId, showArchive] = await firstValueFrom(
|
||||
combineLatest([
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||
]),
|
||||
);
|
||||
|
||||
const builderFilter = {} as VaultFilterList;
|
||||
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
||||
builderFilter.typeFilter = await this.addTypeFilter();
|
||||
builderFilter.folderFilter = await this.addFolderFilter();
|
||||
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||
if (hasArchiveFlag) {
|
||||
builderFilter.archiveFilter = await this.addArchiveFilter();
|
||||
if (showArchive) {
|
||||
builderFilter.archiveFilter = await this.addArchiveFilter(userId);
|
||||
}
|
||||
builderFilter.trashFilter = await this.addTrashFilter();
|
||||
return builderFilter;
|
||||
@@ -419,7 +428,18 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
return trashFilterSection;
|
||||
}
|
||||
|
||||
protected async addArchiveFilter(): Promise<VaultFilterSection> {
|
||||
protected async addArchiveFilter(userId: UserId): Promise<VaultFilterSection> {
|
||||
const [hasArchivedCiphers, userHasPremium] = await firstValueFrom(
|
||||
combineLatest([
|
||||
this.cipherArchiveService
|
||||
.archivedCiphers$(userId)
|
||||
.pipe(map((archivedCiphers) => archivedCiphers.length > 0)),
|
||||
this.cipherArchiveService.userHasPremium$(userId),
|
||||
]),
|
||||
);
|
||||
|
||||
const promptForPremiumOnFilter = !userHasPremium && !hasArchivedCiphers;
|
||||
|
||||
const archiveFilterSection: VaultFilterSection = {
|
||||
data$: this.vaultFilterService.buildTypeTree(
|
||||
{
|
||||
@@ -442,6 +462,12 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
isSelectable: true,
|
||||
},
|
||||
action: this.applyTypeFilter as (filterNode: TreeNode<VaultFilterType>) => Promise<void>,
|
||||
premiumOptions: {
|
||||
showBadgeForNonPremium: true,
|
||||
blockFilterAction: promptForPremiumOnFilter
|
||||
? async () => await this.premiumUpgradePromptService.promptForPremium()
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
return archiveFilterSection;
|
||||
}
|
||||
|
||||
@@ -105,6 +105,9 @@
|
||||
*ngComponentOutlet="optionsInfo.component; injector: createInjector(f.node)"
|
||||
></ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="premiumFeature">
|
||||
<app-premium-badge></app-premium-badge>
|
||||
</ng-container>
|
||||
</span>
|
||||
</span>
|
||||
<ul
|
||||
|
||||
@@ -96,6 +96,11 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async onFilterSelect(filterNode: TreeNode<VaultFilterType>) {
|
||||
if (this.section?.premiumOptions?.blockFilterAction) {
|
||||
await this.section.premiumOptions.blockFilterAction();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.section?.action(filterNode);
|
||||
}
|
||||
|
||||
@@ -123,6 +128,10 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
||||
return this.section?.options;
|
||||
}
|
||||
|
||||
get premiumFeature() {
|
||||
return this.section?.premiumOptions?.showBadgeForNonPremium;
|
||||
}
|
||||
|
||||
get divider() {
|
||||
return this.section?.divider;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,16 @@ export type VaultFilterSection = {
|
||||
component: any;
|
||||
};
|
||||
divider?: boolean;
|
||||
premiumOptions?: {
|
||||
/** When true, the premium badge will show on the filter for non-premium users. */
|
||||
showBadgeForNonPremium?: true;
|
||||
/**
|
||||
* Action to be called instead of applying the filter.
|
||||
* Useful when the user does not have access to a filter (e.g., premium feature)
|
||||
* and custom behavior is needed when invoking the filter.
|
||||
*/
|
||||
blockFilterAction?: () => Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
export type VaultFilterList = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../../shared";
|
||||
@@ -7,7 +8,7 @@ import { SharedModule } from "../../../../shared";
|
||||
import { VaultFilterSectionComponent } from "./components/vault-filter-section.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, SearchModule],
|
||||
imports: [SharedModule, SearchModule, PremiumBadgeComponent],
|
||||
declarations: [VaultFilterSectionComponent],
|
||||
exports: [SharedModule, VaultFilterSectionComponent, SearchModule],
|
||||
})
|
||||
|
||||
@@ -34,6 +34,16 @@
|
||||
<bit-callout type="warning" *ngIf="activeFilter.isDeleted">
|
||||
{{ trashCleanupWarning }}
|
||||
</bit-callout>
|
||||
<bit-callout
|
||||
type="info"
|
||||
[title]="'premiumSubscriptionEnded' | i18n"
|
||||
*ngIf="showSubscriptionEndedMessaging$ | async"
|
||||
>
|
||||
<p>{{ "premiumSubscriptionEndedDesc" | i18n }}</p>
|
||||
<a routerLink="/settings/subscription/premium" bitButton buttonType="primary">{{
|
||||
"restartPremium" | i18n
|
||||
}}</a>
|
||||
</bit-callout>
|
||||
<app-vault-items
|
||||
#vaultItems
|
||||
[ciphers]="ciphers"
|
||||
|
||||
@@ -84,7 +84,7 @@ import {
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { DialogRef, DialogService, ToastService, BannerComponent } from "@bitwarden/components";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
@@ -177,6 +177,7 @@ type EmptyStateMap = Record<EmptyStateType, EmptyStateItem>;
|
||||
VaultItemsModule,
|
||||
SharedModule,
|
||||
OrganizationWarningsModule,
|
||||
BannerComponent,
|
||||
],
|
||||
providers: [
|
||||
RoutedVaultFilterService,
|
||||
@@ -230,13 +231,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
.pipe(map((a) => a?.id))
|
||||
.pipe(switchMap((id) => this.organizationService.organizations$(id)));
|
||||
|
||||
protected userCanArchive$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => {
|
||||
return this.cipherArchiveService.userCanArchive$(userId);
|
||||
}),
|
||||
);
|
||||
|
||||
emptyState$ = combineLatest([
|
||||
this.currentSearchText$,
|
||||
this.routedVaultFilterService.filter$,
|
||||
@@ -295,14 +289,28 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
}),
|
||||
);
|
||||
|
||||
protected enforceOrgDataOwnershipPolicy$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
protected enforceOrgDataOwnershipPolicy$ = this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId),
|
||||
),
|
||||
);
|
||||
|
||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
protected userCanArchive$ = this.userId$.pipe(
|
||||
switchMap((userId) => {
|
||||
return this.cipherArchiveService.userCanArchive$(userId);
|
||||
}),
|
||||
);
|
||||
|
||||
protected showSubscriptionEndedMessaging$ = this.userId$.pipe(
|
||||
switchMap((userId) =>
|
||||
combineLatest([
|
||||
this.routedVaultFilterBridgeService.activeFilter$,
|
||||
this.cipherArchiveService.showSubscriptionEndedMessaging$(userId),
|
||||
]).pipe(map(([activeFilter, showMessaging]) => activeFilter.isArchived && showMessaging)),
|
||||
),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private syncService: SyncService,
|
||||
@@ -438,13 +446,13 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
allowedCiphers$,
|
||||
filter$,
|
||||
this.currentSearchText$,
|
||||
this.cipherArchiveService.hasArchiveFlagEnabled$(),
|
||||
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||
]).pipe(
|
||||
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
||||
concatMap(async ([ciphers, filter, searchText, archiveEnabled]) => {
|
||||
concatMap(async ([ciphers, filter, searchText, showArchiveVault]) => {
|
||||
const failedCiphers =
|
||||
(await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? [];
|
||||
const filterFunction = createFilterFunction(filter, archiveEnabled);
|
||||
const filterFunction = createFilterFunction(filter, showArchiveVault);
|
||||
// Append any failed to decrypt ciphers to the top of the cipher list
|
||||
const allCiphers = [...failedCiphers, ...ciphers];
|
||||
|
||||
|
||||
@@ -3133,6 +3133,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"premiumSubscriptionEnded": {
|
||||
"message": "Your Premium subscription ended"
|
||||
},
|
||||
"premiumSubscriptionEndedDesc": {
|
||||
"message": "To regain access to your archive, restart your Premium subscription. If you edit details for an archived item before restarting, it'll be moved back into your vault."
|
||||
},
|
||||
"restartPremium": {
|
||||
"message": "Restart Premium"
|
||||
},
|
||||
"additionalStorageGb": {
|
||||
"message": "Additional storage (GB)"
|
||||
},
|
||||
@@ -4621,6 +4630,24 @@
|
||||
"learnMore": {
|
||||
"message": "Learn more"
|
||||
},
|
||||
"migrationsFailed": {
|
||||
"message": "An error occurred updating the encryption settings."
|
||||
},
|
||||
"updateEncryptionSettingsTitle": {
|
||||
"message": "Update your encryption settings"
|
||||
},
|
||||
"updateEncryptionSettingsDesc": {
|
||||
"message": "The new recommended encryption settings will improve your account security. Enter your master password to update now."
|
||||
},
|
||||
"confirmIdentityToContinue": {
|
||||
"message": "Confirm your identity to continue"
|
||||
},
|
||||
"enterYourMasterPassword": {
|
||||
"message": "Enter your master password"
|
||||
},
|
||||
"updateSettings": {
|
||||
"message": "Update settings"
|
||||
},
|
||||
"deleteRecoverDesc": {
|
||||
"message": "Enter your email address below to recover and delete your account."
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user