mirror of
https://github.com/bitwarden/browser
synced 2026-01-02 16:43:19 +00:00
Merge branch 'main' into autofill/pm-27195/register-autotype-svc-with-login
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
filter,
|
||||
firstValueFrom,
|
||||
fromEvent,
|
||||
fromEventPattern,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
Subject,
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import { fromChromeEvent } from "../../../platform/browser/from-chrome-event";
|
||||
// FIXME (PM-22628): Popup imports are forbidden in background
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { closeFido2Popout, openFido2Popout } from "../../../vault/popup/utils/vault-popout-window";
|
||||
@@ -232,12 +233,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
}
|
||||
});
|
||||
|
||||
this.windowClosed$ = fromEventPattern(
|
||||
// FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener
|
||||
// and test that it doesn't break. Tracking Ticket: https://bitwarden.atlassian.net/browse/PM-4735
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
(handler: any) => chrome.windows.onRemoved.addListener(handler),
|
||||
(handler: any) => chrome.windows.onRemoved.removeListener(handler),
|
||||
this.windowClosed$ = fromChromeEvent(chrome.windows.onRemoved).pipe(
|
||||
map(([windowId]) => windowId),
|
||||
);
|
||||
|
||||
BrowserFido2UserInterfaceSession.sendMessage({
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="tw-size-[95px] tw-content-center">
|
||||
<bit-icon [icon]="sendCreatedIcon"></bit-icon>
|
||||
</div>
|
||||
<h3 tabindex="0" appAutofocus class="tw-font-semibold">
|
||||
<h3 tabindex="0" appAutofocus class="tw-font-medium">
|
||||
{{ "createdSendSuccessfully" | i18n }}
|
||||
</h3>
|
||||
<p class="tw-text-center">
|
||||
|
||||
35
apps/desktop/desktop_native/Cargo.lock
generated
35
apps/desktop/desktop_native/Cargo.lock
generated
@@ -900,19 +900,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -1533,12 +1520,6 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
@@ -1554,7 +1535,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.3",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1719,7 +1700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.3",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1891,7 +1872,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"desktop_core",
|
||||
"futures",
|
||||
"oslog",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2379,17 +2359,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oslog"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dashmap",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
|
||||
@@ -41,13 +41,11 @@ interprocess = "=2.2.1"
|
||||
keytar = "=0.1.6"
|
||||
libc = "=0.2.172"
|
||||
linux-keyutils = "=0.2.4"
|
||||
log = "=0.4.25"
|
||||
memsec = "=0.7.0"
|
||||
napi = "=2.16.17"
|
||||
napi-build = "=2.2.0"
|
||||
napi-derive = "=2.16.13"
|
||||
oo7 = "=0.4.3"
|
||||
oslog = "=0.2.0"
|
||||
pin-project = "=1.1.10"
|
||||
pkcs8 = "=0.10.2"
|
||||
rand = "=0.9.1"
|
||||
@@ -60,7 +58,6 @@ security-framework-sys = "=2.15.0"
|
||||
serde = "=1.0.209"
|
||||
serde_json = "=1.0.127"
|
||||
sha2 = "=0.10.8"
|
||||
simplelog = "=0.12.2"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = { version = "=0.6.7", default-features = false }
|
||||
sysinfo = "=0.35.0"
|
||||
|
||||
@@ -21,12 +21,11 @@ serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-oslog = "0.3.0"
|
||||
tracing-subscriber = { workspace = true }
|
||||
uniffi = { workspace = true, features = ["cli"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
oslog = { workspace = true }
|
||||
tracing-oslog = "0.3.0"
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
|
||||
@@ -6,6 +6,7 @@ import { LogService } from "@bitwarden/logging";
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
import { stringIsNotUndefinedNullAndEmpty } from "../../utils";
|
||||
import { AutotypeConfig } from "../models/autotype-configure";
|
||||
import { AutotypeVaultData } from "../models/autotype-vault-data";
|
||||
import { AUTOTYPE_IPC_CHANNELS } from "../models/ipc-channels";
|
||||
import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut";
|
||||
|
||||
@@ -49,18 +50,12 @@ export class MainDesktopAutotypeService {
|
||||
this.setKeyboardShortcut(newKeyboardShortcut);
|
||||
});
|
||||
|
||||
ipcMain.on(AUTOTYPE_IPC_CHANNELS.EXECUTE, (_event, data) => {
|
||||
const { response } = data;
|
||||
|
||||
ipcMain.on(AUTOTYPE_IPC_CHANNELS.EXECUTE, (_event, vaultData: AutotypeVaultData) => {
|
||||
if (
|
||||
stringIsNotUndefinedNullAndEmpty(response.username) &&
|
||||
stringIsNotUndefinedNullAndEmpty(response.password)
|
||||
stringIsNotUndefinedNullAndEmpty(vaultData.username) &&
|
||||
stringIsNotUndefinedNullAndEmpty(vaultData.password)
|
||||
) {
|
||||
this.doAutotype(
|
||||
response.username,
|
||||
response.password,
|
||||
this.autotypeKeyboardShortcut.getArrayFormat(),
|
||||
);
|
||||
this.doAutotype(vaultData, this.autotypeKeyboardShortcut.getArrayFormat());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,8 +132,9 @@ export class MainDesktopAutotypeService {
|
||||
}
|
||||
}
|
||||
|
||||
private doAutotype(username: string, password: string, keyboardShortcut: string[]) {
|
||||
const inputPattern = username + "\t" + password;
|
||||
private doAutotype(vaultData: AutotypeVaultData, keyboardShortcut: string[]) {
|
||||
const TAB = "\t";
|
||||
const inputPattern = vaultData.username + TAB + vaultData.password;
|
||||
const inputArray = new Array<number>(inputPattern.length);
|
||||
|
||||
for (let i = 0; i < inputPattern.length; i++) {
|
||||
|
||||
8
apps/desktop/src/autofill/models/autotype-vault-data.ts
Normal file
8
apps/desktop/src/autofill/models/autotype-vault-data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Vault data used in autotype operations.
|
||||
* `username` and `password` are guaranteed to be not null/undefined.
|
||||
*/
|
||||
export interface AutotypeVaultData {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { Command } from "../platform/main/autofill/command";
|
||||
import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main";
|
||||
|
||||
import { AutotypeConfig } from "./models/autotype-configure";
|
||||
import { AutotypeVaultData } from "./models/autotype-vault-data";
|
||||
import { AUTOTYPE_IPC_CHANNELS } from "./models/ipc-channels";
|
||||
|
||||
export default {
|
||||
@@ -145,10 +146,7 @@ export default {
|
||||
listenAutotypeRequest: (
|
||||
fn: (
|
||||
windowTitle: string,
|
||||
completeCallback: (
|
||||
error: Error | null,
|
||||
response: { username?: string; password?: string },
|
||||
) => void,
|
||||
completeCallback: (error: Error | null, response: AutotypeVaultData | null) => void,
|
||||
) => void,
|
||||
) => {
|
||||
ipcRenderer.on(
|
||||
@@ -161,7 +159,7 @@ export default {
|
||||
) => {
|
||||
const { windowTitle } = data;
|
||||
|
||||
fn(windowTitle, (error, response) => {
|
||||
fn(windowTitle, (error, vaultData) => {
|
||||
if (error) {
|
||||
ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTION_ERROR, {
|
||||
windowTitle,
|
||||
@@ -170,10 +168,9 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTE, {
|
||||
windowTitle,
|
||||
response,
|
||||
});
|
||||
if (vaultData !== null) {
|
||||
ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTE, vaultData);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { getAutotypeVaultData } from "./desktop-autotype.service";
|
||||
|
||||
describe("getAutotypeVaultData", () => {
|
||||
it("should return vault data when cipher has username and password", () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.login.username = "foo";
|
||||
cipherView.login.password = "bar";
|
||||
|
||||
const [error, vaultData] = getAutotypeVaultData(cipherView);
|
||||
|
||||
expect(error).toBeNull();
|
||||
expect(vaultData?.username).toEqual("foo");
|
||||
expect(vaultData?.password).toEqual("bar");
|
||||
});
|
||||
|
||||
it("should return error when firstCipher is undefined", () => {
|
||||
const cipherView = undefined;
|
||||
const [error, vaultData] = getAutotypeVaultData(cipherView);
|
||||
|
||||
expect(vaultData).toBeNull();
|
||||
expect(error).toBeDefined();
|
||||
expect(error?.message).toEqual("No matching vault item.");
|
||||
});
|
||||
|
||||
it("should return error when username is undefined", () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.login.username = undefined;
|
||||
cipherView.login.password = "bar";
|
||||
|
||||
const [error, vaultData] = getAutotypeVaultData(cipherView);
|
||||
|
||||
expect(vaultData).toBeNull();
|
||||
expect(error).toBeDefined();
|
||||
expect(error?.message).toEqual("Vault item is undefined.");
|
||||
});
|
||||
|
||||
it("should return error when password is undefined", () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.login.username = "foo";
|
||||
cipherView.login.password = undefined;
|
||||
|
||||
const [error, vaultData] = getAutotypeVaultData(cipherView);
|
||||
|
||||
expect(vaultData).toBeNull();
|
||||
expect(error).toBeDefined();
|
||||
expect(error?.message).toEqual("Vault item is undefined.");
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,7 @@ import { LogService } from "@bitwarden/logging";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { AutotypeConfig } from "../models/autotype-configure";
|
||||
import { AutotypeVaultData } from "../models/autotype-vault-data";
|
||||
|
||||
import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service";
|
||||
|
||||
@@ -43,6 +44,8 @@ export const AUTOTYPE_ENABLED = new KeyDefinition<boolean | null>(
|
||||
{ deserializer: (b) => b },
|
||||
);
|
||||
|
||||
export type Result<T, E = Error> = [E, null] | [null, T];
|
||||
|
||||
/*
|
||||
Valid windows shortcut keys: Control, Alt, Super, Shift, letters A - Z
|
||||
Valid macOS shortcut keys: Control, Alt, Command, Shift, letters A - Z
|
||||
@@ -121,11 +124,8 @@ export class DesktopAutotypeService implements OnDestroy {
|
||||
ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => {
|
||||
const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle);
|
||||
const firstCipher = possibleCiphers?.at(0);
|
||||
|
||||
return callback(null, {
|
||||
username: firstCipher?.login?.username,
|
||||
password: firstCipher?.login?.password,
|
||||
});
|
||||
const [error, vaultData] = getAutotypeVaultData(firstCipher);
|
||||
callback(error, vaultData);
|
||||
});
|
||||
|
||||
// If `autotypeDefaultPolicy` is `true` for a user's organization, and the
|
||||
@@ -245,3 +245,23 @@ export class DesktopAutotypeService implements OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an `AutotypeVaultData` object or an `Error` if the
|
||||
* cipher or vault data within are undefined.
|
||||
*/
|
||||
export function getAutotypeVaultData(
|
||||
cipherView: CipherView | undefined,
|
||||
): Result<AutotypeVaultData> {
|
||||
if (!cipherView) {
|
||||
return [Error("No matching vault item."), null];
|
||||
} else if (cipherView.login.username === undefined || cipherView.login.password === undefined) {
|
||||
return [Error("Vault item is undefined."), null];
|
||||
} else {
|
||||
const vaultData: AutotypeVaultData = {
|
||||
username: cipherView.login.username,
|
||||
password: cipherView.login.password,
|
||||
};
|
||||
return [null, vaultData];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<form [bitSubmit]="submit" [formGroup]="approveSshRequestForm">
|
||||
<bit-dialog>
|
||||
<div class="tw-font-semibold" bitDialogTitle>{{ "sshkeyApprovalTitle" | i18n }}</div>
|
||||
<div class="tw-font-medium" bitDialogTitle>{{ "sshkeyApprovalTitle" | i18n }}</div>
|
||||
<div bitDialogContent>
|
||||
<app-callout
|
||||
type="warning"
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
} from "@bitwarden/common/spec";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import {
|
||||
PREMIUM_INTEREST_KEY,
|
||||
WebPremiumInterestStateService,
|
||||
} from "./web-premium-interest-state.service";
|
||||
|
||||
describe("WebPremiumInterestStateService", () => {
|
||||
let service: WebPremiumInterestStateService;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
const mockUserId = newGuid() as UserId;
|
||||
const mockUserEmail = "user@example.com";
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mockAccountServiceWith(mockUserId, { email: mockUserEmail });
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
service = new WebPremiumInterestStateService(stateProvider);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("getPremiumInterest", () => {
|
||||
it("should throw an error when userId is not provided", async () => {
|
||||
const promise = service.getPremiumInterest(null);
|
||||
|
||||
await expect(promise).rejects.toThrow("UserId is required. Cannot get 'premiumInterest'.");
|
||||
});
|
||||
|
||||
it("should return null when no value is set", async () => {
|
||||
const result = await service.getPremiumInterest(mockUserId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return true when value is set to true", async () => {
|
||||
await stateProvider.setUserState(PREMIUM_INTEREST_KEY, true, mockUserId);
|
||||
|
||||
const result = await service.getPremiumInterest(mockUserId);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when value is set to false", async () => {
|
||||
await stateProvider.setUserState(PREMIUM_INTEREST_KEY, false, mockUserId);
|
||||
|
||||
const result = await service.getPremiumInterest(mockUserId);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should use getUserState$ to retrieve the value", async () => {
|
||||
const getUserStateSpy = jest.spyOn(stateProvider, "getUserState$");
|
||||
await stateProvider.setUserState(PREMIUM_INTEREST_KEY, true, mockUserId);
|
||||
|
||||
await service.getPremiumInterest(mockUserId);
|
||||
|
||||
expect(getUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, mockUserId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPremiumInterest", () => {
|
||||
it("should throw an error when userId is not provided", async () => {
|
||||
const promise = service.setPremiumInterest(null, true);
|
||||
|
||||
await expect(promise).rejects.toThrow("UserId is required. Cannot set 'premiumInterest'.");
|
||||
});
|
||||
|
||||
it("should set the value to true", async () => {
|
||||
await service.setPremiumInterest(mockUserId, true);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should set the value to false", async () => {
|
||||
await service.setPremiumInterest(mockUserId, false);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should update an existing value", async () => {
|
||||
await service.setPremiumInterest(mockUserId, true);
|
||||
await service.setPremiumInterest(mockUserId, false);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should use setUserState to store the value", async () => {
|
||||
const setUserStateSpy = jest.spyOn(stateProvider, "setUserState");
|
||||
|
||||
await service.setPremiumInterest(mockUserId, true);
|
||||
|
||||
expect(setUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, true, mockUserId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearPremiumInterest", () => {
|
||||
it("should throw an error when userId is not provided", async () => {
|
||||
const promise = service.clearPremiumInterest(null);
|
||||
|
||||
await expect(promise).rejects.toThrow("UserId is required. Cannot clear 'premiumInterest'.");
|
||||
});
|
||||
|
||||
it("should clear the value by setting it to null", async () => {
|
||||
await service.setPremiumInterest(mockUserId, true);
|
||||
await service.clearPremiumInterest(mockUserId);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId),
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should use setUserState with null to clear the value", async () => {
|
||||
const setUserStateSpy = jest.spyOn(stateProvider, "setUserState");
|
||||
await service.setPremiumInterest(mockUserId, true);
|
||||
|
||||
await service.clearPremiumInterest(mockUserId);
|
||||
|
||||
expect(setUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, null, mockUserId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction";
|
||||
import { BILLING_MEMORY, StateProvider, UserKeyDefinition } from "@bitwarden/state";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
export const PREMIUM_INTEREST_KEY = new UserKeyDefinition<boolean>(
|
||||
BILLING_MEMORY,
|
||||
"premiumInterest",
|
||||
{
|
||||
deserializer: (value: boolean) => value,
|
||||
clearOn: ["lock", "logout"],
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class WebPremiumInterestStateService implements PremiumInterestStateService {
|
||||
constructor(private stateProvider: StateProvider) {}
|
||||
|
||||
async getPremiumInterest(userId: UserId): Promise<boolean | null> {
|
||||
if (!userId) {
|
||||
throw new Error("UserId is required. Cannot get 'premiumInterest'.");
|
||||
}
|
||||
|
||||
return await firstValueFrom(this.stateProvider.getUserState$(PREMIUM_INTEREST_KEY, userId));
|
||||
}
|
||||
|
||||
async setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise<void> {
|
||||
if (!userId) {
|
||||
throw new Error("UserId is required. Cannot set 'premiumInterest'.");
|
||||
}
|
||||
|
||||
await this.stateProvider.setUserState(PREMIUM_INTEREST_KEY, premiumInterest, userId);
|
||||
}
|
||||
|
||||
async clearPremiumInterest(userId: UserId): Promise<void> {
|
||||
if (!userId) {
|
||||
throw new Error("UserId is required. Cannot clear 'premiumInterest'.");
|
||||
}
|
||||
|
||||
await this.stateProvider.setUserState(PREMIUM_INTEREST_KEY, null, userId);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { DefaultDeviceManagementComponentService } from "@bitwarden/angular/auth
|
||||
import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction";
|
||||
import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password";
|
||||
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
|
||||
import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction";
|
||||
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import {
|
||||
CLIENT_TYPE,
|
||||
@@ -129,6 +130,7 @@ import {
|
||||
WebSetInitialPasswordService,
|
||||
} from "../auth";
|
||||
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
||||
import { WebPremiumInterestStateService } from "../billing/services/premium-interest/web-premium-interest-state.service";
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.service";
|
||||
import { WebFileDownloadService } from "../core/web-file-download.service";
|
||||
@@ -421,6 +423,11 @@ const safeProviders: SafeProvider[] = [
|
||||
Router,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PremiumInterestStateService,
|
||||
useClass: WebPremiumInterestStateService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="tw-col-span-3">
|
||||
<div class="tw-border tw-border-solid tw-border-secondary-300 tw-rounded" data-testid="filters">
|
||||
<div
|
||||
class="tw-bg-background-alt tw-border-0 tw-border-b tw-border-solid tw-border-secondary-100 tw-rounded-t tw-px-5 tw-py-2.5 tw-font-semibold tw-uppercase"
|
||||
class="tw-bg-background-alt tw-border-0 tw-border-b tw-border-solid tw-border-secondary-100 tw-rounded-t tw-px-5 tw-py-2.5 tw-font-medium tw-uppercase"
|
||||
data-testid="filters-header"
|
||||
>
|
||||
{{ "filters" | i18n }}
|
||||
|
||||
Reference in New Issue
Block a user