Merge branch 'main' into ps/extension-refresh

This commit is contained in:
Victoria League 2024-09-18 10:18:27 -04:00 committed by GitHub
commit 04180482c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 763 additions and 166 deletions

View File

@ -128,6 +128,21 @@
"copyLicenseNumber": {
"message": "Copy license number"
},
"copyCustomField": {
"message": "Copy $FIELD$",
"placeholders": {
"field": {
"content": "$1",
"example": "Custom field label"
}
}
},
"copyWebsite": {
"message": "Copy website"
},
"copyNotes": {
"message": "Copy notes"
},
"autoFill": {
"message": "Autofill"
},
@ -2253,6 +2268,10 @@
"message": "Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendDetails": {
"message": "Send details",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"searchSends": {
"message": "Search Sends",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
@ -2264,6 +2283,9 @@
"sendTypeText": {
"message": "Text"
},
"sendTypeTextToShare": {
"message": "Text to share"
},
"sendTypeFile": {
"message": "File"
},
@ -2271,6 +2293,9 @@
"message": "All Sends",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"hideTextByDefault": {
"message": "Hide text by default"
},
"maxAccessCountReached": {
"message": "Max access count reached",
"description": "This text will be displayed after a Send has been accessed the maximum amount of times."
@ -2344,6 +2369,10 @@
"message": "The Send will be permanently deleted on the specified date and time.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deletionDateDescV2": {
"message": "The Send will be permanently deleted on this date.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"expirationDate": {
"message": "Expiration date"
},

View File

@ -1,9 +1,10 @@
import { CommonModule, Location } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, firstValueFrom, map, of, startWith, switchMap, takeUntil } from "rxjs";
import { Subject, firstValueFrom, map, of, startWith, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { LockService } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@ -70,6 +71,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private authService: AuthService,
private configService: ConfigService,
private lockService: LockService,
) {}
get accountLimit() {
@ -131,26 +133,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
async lockAll() {
this.loading = true;
this.availableAccounts$
.pipe(
map((accounts) =>
accounts
.filter((account) => account.id !== this.specialAddAccountId)
.sort((a, b) => (a.isActive ? -1 : b.isActive ? 1 : 0)) // Log out of the active account first
.map((account) => account.id),
),
switchMap(async (accountIds) => {
if (accountIds.length === 0) {
return;
}
// Must lock active (first) account first, then order doesn't matter
await this.vaultTimeoutService.lock(accountIds.shift());
await Promise.all(accountIds.map((id) => this.vaultTimeoutService.lock(id)));
}),
takeUntil(this.destroy$),
)
.subscribe(() => this.router.navigate(["lock"]));
await this.lockService.lockAll();
await this.router.navigate(["lock"]);
}
async logOut(userId: UserId) {

View File

@ -0,0 +1,32 @@
import { filter, firstValueFrom } from "rxjs";
import { LockService } from "@bitwarden/auth/common";
import {
CommandDefinition,
MessageListener,
MessageSender,
} from "@bitwarden/common/platform/messaging";
import { Utils } from "@bitwarden/common/platform/misc/utils";
const LOCK_ALL_FINISHED = new CommandDefinition<{ requestId: string }>("lockAllFinished");
const LOCK_ALL = new CommandDefinition<{ requestId: string }>("lockAll");
export class ForegroundLockService implements LockService {
constructor(
private readonly messageSender: MessageSender,
private readonly messageListener: MessageListener,
) {}
async lockAll(): Promise<void> {
const requestId = Utils.newGuid();
const finishMessage = firstValueFrom(
this.messageListener
.messages$(LOCK_ALL_FINISHED)
.pipe(filter((m) => m.requestId === requestId)),
);
this.messageSender.send(LOCK_ALL, { requestId });
await finishMessage;
}
}

View File

@ -9,6 +9,7 @@ import {
AuthRequestService,
LoginEmailServiceAbstraction,
LogoutReason,
DefaultLockService,
} from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
@ -1037,6 +1038,14 @@ export default class MainBackground {
const systemUtilsServiceReloadCallback = async () => {
await this.taskSchedulerService.clearAllScheduledTasks();
if (this.platformUtilsService.isSafari()) {
// If we do `chrome.runtime.reload` on safari they will send an onInstalled reason of install
// and that prompts us to show a new tab, this apparently doesn't happen on sideloaded
// extensions and only shows itself production scenarios. See: https://bitwarden.atlassian.net/browse/PM-12298
self.location.reload();
return;
}
BrowserApi.reloadExtension();
};
@ -1065,6 +1074,9 @@ export default class MainBackground {
this.scriptInjectorService,
this.configService,
);
const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService);
this.runtimeBackground = new RuntimeBackground(
this,
this.autofillService,
@ -1079,6 +1091,7 @@ export default class MainBackground {
this.fido2Background,
messageListener,
this.accountService,
lockService,
);
this.nativeMessagingBackground = new NativeMessagingBackground(
this.cryptoService,

View File

@ -1,5 +1,6 @@
import { firstValueFrom, map, mergeMap, of, switchMap } from "rxjs";
import { LockService } from "@bitwarden/auth/common";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AutofillOverlayVisibility, ExtensionCommand } from "@bitwarden/common/autofill/constants";
@ -48,6 +49,7 @@ export default class RuntimeBackground {
private fido2Background: Fido2Background,
private messageListener: MessageListener,
private accountService: AccountService,
private readonly lockService: LockService,
) {
// onInstalled listener must be wired up before anything else, so we do it in the ctor
chrome.runtime.onInstalled.addListener((details: any) => {
@ -245,6 +247,12 @@ export default class RuntimeBackground {
case "lockVault":
await this.main.vaultTimeoutService.lock(msg.userId);
break;
case "lockAll":
{
await this.lockService.lockAll();
this.messagingService.send("lockAllFinished", { requestId: msg.requestId });
}
break;
case "logout":
await this.main.logout(msg.expired, msg.userId);
break;

View File

@ -1,6 +1,7 @@
import { Observable } from "rxjs";
import { DeviceType } from "@bitwarden/common/enums";
import { isBrowserSafariApi } from "@bitwarden/platform";
import { TabMessage } from "../../types/tab-messages";
import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
@ -9,10 +10,7 @@ import { registerContentScriptsPolyfill } from "./browser-api.register-content-s
export class BrowserApi {
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
static isSafariApi: boolean =
navigator.userAgent.indexOf(" Safari/") !== -1 &&
navigator.userAgent.indexOf(" Chrome/") === -1 &&
navigator.userAgent.indexOf(" Chromium/") === -1;
static isSafariApi: boolean = isBrowserSafariApi();
static isChromeApi: boolean = !BrowserApi.isSafariApi && typeof chrome !== "undefined";
static isFirefoxOnAndroid: boolean =
navigator.userAgent.indexOf("Firefox/") !== -1 && navigator.userAgent.indexOf("Android") !== -1;

View File

@ -1,4 +1,4 @@
import { switchMap, merge, delay, filter, concatMap, map } from "rxjs";
import { switchMap, merge, delay, filter, concatMap, map, first, of } from "rxjs";
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
import {
@ -61,7 +61,18 @@ export class PopupViewCacheBackgroundService {
merge(
// on tab changed, excluding extension tabs
fromChromeEvent(chrome.tabs.onActivated).pipe(
switchMap(([tabInfo]) => BrowserApi.getTab(tabInfo.tabId)),
switchMap((tabs) => BrowserApi.getTab(tabs[0].tabId)),
switchMap((tab) => {
// FireFox sets the `url` to "about:blank" and won't populate the `url` until the `onUpdated` event
if (tab.url !== "about:blank") {
return of(tab);
}
return fromChromeEvent(chrome.tabs.onUpdated).pipe(
first(),
switchMap(([tabId]) => BrowserApi.getTab(tabId)),
);
}),
map((tab) => tab.url || tab.pendingUrl),
filter((url) => !url.startsWith(chrome.runtime.getURL(""))),
),

View File

@ -127,6 +127,12 @@ export class AppComponent implements OnInit, OnDestroy {
this.showNativeMessagingFingerprintDialog(msg);
} else if (msg.command === "showToast") {
this.toastService._showToast(msg);
} else if (msg.command === "reloadProcess") {
if (this.platformUtilsService.isSafari()) {
window.setTimeout(() => {
window.location.reload();
}, 2000);
}
} else if (msg.command === "reloadPopup") {
// 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

@ -17,7 +17,7 @@ import {
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { LockService, PinServiceAbstraction } from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
@ -91,6 +91,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
import AutofillService from "../../autofill/services/autofill.service";
@ -560,6 +561,11 @@ const safeProviders: SafeProvider[] = [
useClass: ExtensionAnonLayoutWrapperDataService,
deps: [],
}),
safeProvider({
provide: LockService,
useClass: ForegroundLockService,
deps: [MessageSender, MessageListener],
}),
];
@NgModule({

View File

@ -20,17 +20,15 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
AsyncActionsModule,
SearchModule,
ButtonModule,
IconButtonModule,
DialogService,
IconButtonModule,
SearchModule,
ToastService,
} from "@bitwarden/components";
import { TotpCaptureService } from "@bitwarden/vault";
import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service";
import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component";
@ -41,7 +39,6 @@ import { VaultPopupAutofillService } from "./../../../services/vault-popup-autof
selector: "app-view-v2",
templateUrl: "view-v2.component.html",
standalone: true,
providers: [{ provide: TotpCaptureService, useClass: BrowserTotpCaptureService }],
imports: [
CommonModule,
SearchModule,

View File

@ -13,15 +13,10 @@ describe("BrowserTotpCaptureService", () => {
let testBed: TestBed;
let service: BrowserTotpCaptureService;
let mockCaptureVisibleTab: jest.SpyInstance;
let createNewTabSpy: jest.SpyInstance;
const validTotpUrl = "otpauth://totp/label?secret=123";
beforeEach(() => {
const tabReturn = new Promise<chrome.tabs.Tab>((resolve) =>
resolve({ url: "google.com", active: true } as chrome.tabs.Tab),
);
createNewTabSpy = jest.spyOn(BrowserApi, "createNewTab").mockReturnValue(tabReturn);
mockCaptureVisibleTab = jest.spyOn(BrowserApi, "captureVisibleTab");
mockCaptureVisibleTab.mockResolvedValue("screenshot");
@ -71,10 +66,4 @@ describe("BrowserTotpCaptureService", () => {
expect(result).toBeNull();
});
it("should call BrowserApi.createNewTab with a given loginURI", async () => {
await service.openAutofillNewTab("www.google.com");
expect(createNewTabSpy).toHaveBeenCalledWith("www.google.com");
});
});

View File

@ -20,8 +20,4 @@ export class BrowserTotpCaptureService implements TotpCaptureService {
}
return null;
}
async openAutofillNewTab(loginUri: string) {
await BrowserApi.createNewTab(loginUri);
}
}

View File

@ -419,6 +419,23 @@
"enableHardwareAccelerationDesc" | i18n
}}</small>
</div>
<div class="form-group" *ngIf="!isLinux">
<div class="checkbox">
<label for="allowScreenshots">
<input
id="allowScreenshots"
type="checkbox"
aria-describedby="allowScreenshotsHelp"
formControlName="allowScreenshots"
(change)="saveAllowScreenshots()"
/>
{{ "allowScreenshots" | i18n }}
</label>
</div>
<small id="allowScreenshotsHelp" class="help-block">{{
"allowScreenshotsDesc" | i18n
}}</small>
</div>
<div class="form-group" *ngIf="showDuckDuckGoIntegrationOption">
<div class="checkbox">
<label for="enableDuckDuckGoBrowserIntegration">

View File

@ -109,6 +109,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
disabled: true,
}),
enableHardwareAcceleration: true,
allowScreenshots: false,
enableDuckDuckGoBrowserIntegration: false,
theme: [null as ThemeType | null],
locale: [null as string | null],
@ -277,6 +278,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
enableHardwareAcceleration: await firstValueFrom(
this.desktopSettingsService.hardwareAcceleration$,
),
allowScreenshots: await firstValueFrom(this.desktopSettingsService.allowScreenshots$),
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
locale: await firstValueFrom(this.i18nService.userSetLocale$),
};
@ -727,6 +729,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
);
}
async saveAllowScreenshots() {
await this.desktopSettingsService.setAllowScreenshots(this.form.value.allowScreenshots);
}
private async generateVaultTimeoutOptions(): Promise<VaultTimeoutOption[]> {
let vaultTimeoutOptions: VaultTimeoutOption[] = [
{ name: this.i18nService.t("oneMinute"), value: 1 },

View File

@ -3077,6 +3077,12 @@
"textSends": {
"message": "Text Sends"
},
"allowScreenshots": {
"message": "Allow screen capture"
},
"allowScreenshotsDesc": {
"message": "Allow screen capture of the Bitwarden desktop application."
},
"ssoError": {
"message": "No free ports could be found for the sso login."
},

View File

@ -69,6 +69,13 @@ export class WindowMain {
this.logService.info("Render process reloaded");
});
this.desktopSettingsService.allowScreenshots$.subscribe((allowed) => {
if (this.win == null) {
return;
}
this.win.setContentProtection(!allowed);
});
return new Promise<void>((resolve, reject) => {
try {
if (!isMacAppStore() && !isSnapStore()) {
@ -270,6 +277,14 @@ export class WindowMain {
});
});
firstValueFrom(this.desktopSettingsService.allowScreenshots$)
.then((allowScreenshots) => {
this.win.setContentProtection(!allowScreenshots);
})
.catch((e) => {
this.logService.error(e);
});
if (this.createWindowCallback) {
this.createWindowCallback(this.win);
}

View File

@ -71,6 +71,10 @@ const MINIMIZE_ON_COPY = new UserKeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "
clearOn: [], // User setting, no need to clear
});
const ALLOW_SCREENSHOTS = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "allowScreenshots", {
deserializer: (b) => b,
});
/**
* Various settings for controlling application behavior specific to the desktop client.
*/
@ -139,6 +143,13 @@ export class DesktopSettingsService {
browserIntegrationFingerprintEnabled$ =
this.browserIntegrationFingerprintEnabledState.state$.pipe(map(Boolean));
private readonly allowScreenshotState = this.stateProvider.getGlobal(ALLOW_SCREENSHOTS);
/**
* The application setting for whether or not to allow screenshots of the app.
*/
allowScreenshots$ = this.allowScreenshotState.state$.pipe(map(Boolean));
private readonly minimizeOnCopyState = this.stateProvider.getActive(MINIMIZE_ON_COPY);
/**
@ -255,4 +266,12 @@ export class DesktopSettingsService {
async setMinimizeOnCopy(value: boolean, userId: UserId) {
await this.stateProvider.getUser(userId, MINIMIZE_ON_COPY).update(() => value);
}
/**
* Sets the setting for whether or not the screenshot protection is enabled.
* @param value `true` if the screenshot protection is enabled, `false` if it is not.
*/
async setAllowScreenshots(value: boolean) {
await this.allowScreenshotState.update(() => value);
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
"version": "2024.9.0",
"version": "2024.9.1",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@ -118,6 +118,7 @@ export class ViewComponent implements OnInit, OnDestroy {
}
this.dialogRef.close({ action: ViewCipherDialogResult.deleted });
await this.router.navigate(["/vault"]);
};
/**

View File

@ -81,7 +81,11 @@
(click)="addCipher()"
buttonType="primary"
type="button"
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned"
*ngIf="
filter.type !== 'trash' &&
filter.collectionId !== Unassigned &&
selectedCollection?.node?.canEditItems(organization)
"
>
<i aria-hidden="true" class="bwi bwi-plus"></i> {{ "newItem" | i18n }}
</button>

View File

@ -592,6 +592,45 @@
"message": "Copy URI",
"description": "Copy URI to clipboard"
},
"copyCustomField": {
"message": "Copy $FIELD$",
"placeholders": {
"field": {
"content": "$1",
"example": "Custom field label"
}
}
},
"copyWebsite": {
"message": "Copy website"
},
"copyNotes": {
"message": "Copy notes"
},
"copyAddress": {
"message": "Copy address"
},
"copyPhone": {
"message": "Copy phone"
},
"copyEmail": {
"message": "Copy email"
},
"copyCompany": {
"message": "Copy company"
},
"copySSN": {
"message": "Copy Social Security number"
},
"copyPassportNumber": {
"message": "Copy passport number"
},
"copyLicenseNumber": {
"message": "Copy license number"
},
"copyName": {
"message": "Copy name"
},
"me": {
"message": "Me"
},
@ -4861,12 +4900,6 @@
"deleteAnyCollection": {
"message": "Delete any collection"
},
"editAssignedCollections": {
"message": "Edit assigned collections"
},
"deleteAssignedCollections": {
"message": "Delete assigned collections"
},
"manageGroups": {
"message": "Manage groups"
},

View File

@ -0,0 +1,49 @@
import { combineLatest, firstValueFrom, map } from "rxjs";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
export abstract class LockService {
/**
* Locks all accounts.
*/
abstract lockAll(): Promise<void>;
}
export class DefaultLockService implements LockService {
constructor(
private readonly accountService: AccountService,
private readonly vaultTimeoutService: VaultTimeoutService,
) {}
async lockAll() {
const accounts = await firstValueFrom(
combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe(
map(([activeAccount, accounts]) => {
const otherAccounts = Object.keys(accounts) as UserId[];
if (activeAccount == null) {
return { activeAccount: null, otherAccounts: otherAccounts };
}
return {
activeAccount: activeAccount.id,
otherAccounts: otherAccounts.filter((accountId) => accountId !== activeAccount.id),
};
}),
),
);
for (const otherAccount of accounts.otherAccounts) {
await this.vaultTimeoutService.lock(otherAccount);
}
// Do the active account last in case we ever try to route the user on lock
// that way this whole operation will be complete before that routing
// could take place.
if (accounts.activeAccount != null) {
await this.vaultTimeoutService.lock(accounts.activeAccount);
}
}
}

View File

@ -0,0 +1,43 @@
import { mock } from "jest-mock-extended";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { DefaultLockService } from "./lock.service";
describe("DefaultLockService", () => {
const mockUser1 = "user1" as UserId;
const mockUser2 = "user2" as UserId;
const mockUser3 = "user3" as UserId;
const accountService = mockAccountServiceWith(mockUser1);
const vaultTimeoutService = mock<VaultTimeoutService>();
const sut = new DefaultLockService(accountService, vaultTimeoutService);
describe("lockAll", () => {
it("locks the active account last", async () => {
await accountService.addAccount(mockUser2, {
name: "name2",
email: "email2@example.com",
emailVerified: false,
});
await accountService.addAccount(mockUser3, {
name: "name3",
email: "email3@example.com",
emailVerified: false,
});
await sut.lockAll();
expect(vaultTimeoutService.lock).toHaveBeenCalledTimes(3);
// Non-Active users should be called first
expect(vaultTimeoutService.lock).toHaveBeenNthCalledWith(1, mockUser2);
expect(vaultTimeoutService.lock).toHaveBeenNthCalledWith(2, mockUser3);
// Active user should be called last
expect(vaultTimeoutService.lock).toHaveBeenNthCalledWith(3, mockUser1);
});
});
});

View File

@ -4,3 +4,4 @@ export * from "./login-strategies/login-strategy.service";
export * from "./user-decryption-options/user-decryption-options.service";
export * from "./auth-request/auth-request.service";
export * from "./register-route.service";
export * from "./accounts/lock.service";

View File

@ -7,8 +7,6 @@ export class PermissionsApi extends BaseResponse {
createNewCollections: boolean;
editAnyCollection: boolean;
deleteAnyCollection: boolean;
editAssignedCollections: boolean;
deleteAssignedCollections: boolean;
manageCiphers: boolean;
manageGroups: boolean;
manageSso: boolean;
@ -29,8 +27,6 @@ export class PermissionsApi extends BaseResponse {
this.createNewCollections = this.getResponseProperty("CreateNewCollections");
this.editAnyCollection = this.getResponseProperty("EditAnyCollection");
this.deleteAnyCollection = this.getResponseProperty("DeleteAnyCollection");
this.editAssignedCollections = this.getResponseProperty("EditAssignedCollections");
this.deleteAssignedCollections = this.getResponseProperty("DeleteAssignedCollections");
this.manageCiphers = this.getResponseProperty("ManageCiphers");
this.manageGroups = this.getResponseProperty("ManageGroups");

View File

@ -340,8 +340,6 @@ describe("KeyConnectorService", () => {
createNewCollections: false,
editAnyCollection: false,
deleteAnyCollection: false,
editAssignedCollections: false,
deleteAssignedCollections: false,
manageGroups: false,
managePolicies: false,
manageSso: false,

View File

@ -17,8 +17,10 @@ export class Fido2Utils {
return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str));
}
static bufferSourceToUint8Array(bufferSource: BufferSource) {
if (Fido2Utils.isArrayBuffer(bufferSource)) {
static bufferSourceToUint8Array(bufferSource: BufferSource): Uint8Array {
if (bufferSource instanceof Uint8Array) {
return bufferSource;
} else if (Fido2Utils.isArrayBuffer(bufferSource)) {
return new Uint8Array(bufferSource);
} else {
return new Uint8Array(bufferSource.buffer);

View File

@ -2,7 +2,7 @@ import { GlobalState } from "./global-state";
import { KeyDefinition } from "./key-definition";
/**
* A provider for geting an implementation of global state scoped to the given key.
* A provider for getting an implementation of global state scoped to the given key.
*/
export abstract class GlobalStateProvider {
/**

View File

@ -57,6 +57,8 @@ export class SendService implements InternalSendServiceAbstraction {
send.disabled = model.disabled;
send.hideEmail = model.hideEmail;
send.maxAccessCount = model.maxAccessCount;
send.deletionDate = model.deletionDate;
send.expirationDate = model.expirationDate;
if (model.key == null) {
const key = await this.keyGenerationService.createKeyWithPurpose(
128,

View File

@ -10,7 +10,7 @@
<input
#input
bitInput
type="search"
[type]="inputType"
[id]="id"
[placeholder]="placeholder ?? ('search' | i18n)"
class="tw-pl-9"

View File

@ -1,6 +1,8 @@
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { isBrowserSafariApi } from "@bitwarden/platform";
import { FocusableElement } from "../shared/focusable-element";
let nextId = 0;
@ -28,6 +30,8 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
protected id = `search-id-${nextId++}`;
protected searchText: string;
// Use `type="text"` for Safari to improve rendering performance
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
@Input() disabled: boolean;
@Input() placeholder: string;

View File

@ -20,7 +20,8 @@
"lib": ["es2020", "dom"],
"paths": {
"@bitwarden/common/*": ["../common/src/*"],
"@bitwarden/angular/*": ["../angular/src/*"]
"@bitwarden/angular/*": ["../angular/src/*"],
"@bitwarden/platform": ["../platform/src"]
}
},
"angularCompilerOptions": {

View File

@ -0,0 +1 @@
export * from "./services/browser-service";

View File

@ -0,0 +1,41 @@
import { isBrowserSafariApi } from "./browser-service";
describe("browser-service", () => {
describe("isBrowserSafariApi", () => {
it("returns true if browser is safari", () => {
jest
.spyOn(navigator, "userAgent", "get")
.mockReturnValue(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
);
const result = isBrowserSafariApi();
expect(result).toBe(true);
});
it("returns false if browser is chrome", () => {
jest
.spyOn(navigator, "userAgent", "get")
.mockReturnValue(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
);
const result = isBrowserSafariApi();
expect(result).toBe(false);
});
it("returns false if browser is firefox", () => {
jest
.spyOn(navigator, "userAgent", "get")
.mockReturnValue(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:130.0) Gecko/20100101 Firefox/130.0",
);
const result = isBrowserSafariApi();
expect(result).toBe(false);
});
});
});

View File

@ -0,0 +1,7 @@
export function isBrowserSafariApi(): boolean {
return (
navigator.userAgent.indexOf(" Safari/") !== -1 &&
navigator.userAgent.indexOf(" Chrome/") === -1 &&
navigator.userAgent.indexOf(" Chromium/") === -1
);
}

View File

@ -0,0 +1,111 @@
import { DatePipe } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, FormGroup, FormControl, Validators } from "@angular/forms";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendFormConfig } from "../../abstractions/send-form-config.service";
import { SendFormContainer } from "../../send-form-container";
export type BaseSendDetailsForm = FormGroup<{
name: FormControl<string>;
selectedDeletionDatePreset: FormControl<string | number>;
}>;
// Value = hours
export enum DatePreset {
OneHour = 1,
OneDay = 24,
TwoDays = 48,
ThreeDays = 72,
SevenDays = 168,
FourteenDays = 336,
ThirtyDays = 720,
}
export interface DatePresetSelectOption {
name: string;
value: DatePreset | string;
}
@Component({
selector: "base-send-details-behavior",
template: "",
})
export class BaseSendDetailsComponent implements OnInit {
@Input() config: SendFormConfig;
@Input() originalSendView?: SendView;
sendDetailsForm: BaseSendDetailsForm;
customDeletionDateOption: DatePresetSelectOption | null = null;
datePresetOptions: DatePresetSelectOption[] = [];
constructor(
protected sendFormContainer: SendFormContainer,
protected formBuilder: FormBuilder,
protected i18nService: I18nService,
protected datePipe: DatePipe,
) {
this.sendDetailsForm = this.formBuilder.group({
name: new FormControl("", Validators.required),
selectedDeletionDatePreset: new FormControl(DatePreset.SevenDays || "", Validators.required),
});
this.sendDetailsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
this.sendFormContainer.patchSend((send) => {
return Object.assign(send, {
name: value.name,
deletionDate: new Date(this.formattedDeletionDate),
expirationDate: new Date(this.formattedDeletionDate),
} as SendView);
});
});
}
async ngOnInit() {
this.setupDeletionDatePresets();
if (this.originalSendView) {
this.sendDetailsForm.patchValue({
name: this.originalSendView.name,
selectedDeletionDatePreset: this.originalSendView.deletionDate.toString(),
});
if (this.originalSendView.deletionDate) {
this.customDeletionDateOption = {
name: this.datePipe.transform(this.originalSendView.deletionDate, "MM/dd/yyyy, hh:mm a"),
value: this.originalSendView.deletionDate.toString(),
};
this.datePresetOptions.unshift(this.customDeletionDateOption);
}
}
}
setupDeletionDatePresets() {
const defaultSelections: DatePresetSelectOption[] = [
{ name: this.i18nService.t("oneHour"), value: DatePreset.OneHour },
{ name: this.i18nService.t("oneDay"), value: DatePreset.OneDay },
{ name: this.i18nService.t("days", "2"), value: DatePreset.TwoDays },
{ name: this.i18nService.t("days", "3"), value: DatePreset.ThreeDays },
{ name: this.i18nService.t("days", "7"), value: DatePreset.SevenDays },
{ name: this.i18nService.t("days", "14"), value: DatePreset.FourteenDays },
{ name: this.i18nService.t("days", "30"), value: DatePreset.ThirtyDays },
];
this.datePresetOptions = defaultSelections;
}
get formattedDeletionDate(): string {
const now = new Date();
const selectedValue = this.sendDetailsForm.controls.selectedDeletionDatePreset.value;
if (typeof selectedValue === "string") {
return selectedValue;
}
const milliseconds = now.setTime(now.getTime() + (selectedValue as number) * 60 * 60 * 1000);
return new Date(milliseconds).toString();
}
}

View File

@ -0,0 +1,35 @@
<bit-section [formGroup]="sendDetailsForm">
<bit-section-header>
<h2 bitTypography="h5">{{ "sendDetails" | i18n }}</h2>
</bit-section-header>
<bit-card>
<bit-form-field>
<bit-label>{{ "name" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
</bit-form-field>
<tools-send-text-details
*ngIf="config.sendType === TextSendType"
[config]="config"
[originalSendView]="originalSendView"
[sendDetailsForm]="sendDetailsForm"
></tools-send-text-details>
<bit-form-field>
<bit-label>{{ "deletionDate" | i18n }}</bit-label>
<bit-select
id="deletionDate"
name="SelectedDeletionDatePreset"
formControlName="selectedDeletionDatePreset"
>
<bit-option
*ngFor="let o of datePresetOptions"
[value]="o.value"
[label]="o.name"
></bit-option>
</bit-select>
<bit-hint>{{ "deletionDateDescV2" | i18n }}</bit-hint>
</bit-form-field>
</bit-card>
</bit-section>

View File

@ -0,0 +1,59 @@
import { CommonModule, DatePipe } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import {
SectionComponent,
SectionHeaderComponent,
TypographyModule,
CardComponent,
FormFieldModule,
IconButtonModule,
CheckboxModule,
SelectModule,
} from "@bitwarden/components";
import { SendFormContainer } from "../../send-form-container";
import { BaseSendDetailsComponent } from "./base-send-details.component";
import { SendTextDetailsComponent } from "./send-text-details.component";
@Component({
selector: "tools-send-details",
templateUrl: "./send-details.component.html",
standalone: true,
imports: [
SectionComponent,
SectionHeaderComponent,
TypographyModule,
JslibModule,
CardComponent,
FormFieldModule,
ReactiveFormsModule,
SendTextDetailsComponent,
IconButtonModule,
CheckboxModule,
CommonModule,
SelectModule,
],
})
export class SendDetailsComponent extends BaseSendDetailsComponent implements OnInit {
FileSendType = SendType.File;
TextSendType = SendType.Text;
constructor(
protected sendFormContainer: SendFormContainer,
protected formBuilder: FormBuilder,
protected i18nService: I18nService,
protected datePipe: DatePipe,
) {
super(sendFormContainer, formBuilder, i18nService, datePipe);
}
async ngOnInit() {
await super.ngOnInit();
}
}

View File

@ -0,0 +1,10 @@
<bit-section [formGroup]="sendTextDetailsForm">
<bit-form-field>
<bit-label>{{ "sendTypeTextToShare" | i18n }}</bit-label>
<textarea bitInput id="text" rows="6" formControlName="text"></textarea>
</bit-form-field>
<bit-form-control>
<input bitCheckbox type="checkbox" formControlName="hidden" />
<bit-label>{{ "hideTextByDefault" | i18n }}</bit-label>
</bit-form-control>
</bit-section>

View File

@ -0,0 +1,82 @@
import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import {
FormBuilder,
FormControl,
FormGroup,
Validators,
ReactiveFormsModule,
} from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { CheckboxModule, FormFieldModule, SectionComponent } from "@bitwarden/components";
import { SendFormConfig } from "../../abstractions/send-form-config.service";
import { SendFormContainer } from "../../send-form-container";
import { BaseSendDetailsForm } from "./base-send-details.component";
type BaseSendTextDetailsForm = FormGroup<{
text: FormControl<string>;
hidden: FormControl<boolean>;
}>;
export type SendTextDetailsForm = BaseSendTextDetailsForm & BaseSendDetailsForm;
@Component({
selector: "tools-send-text-details",
templateUrl: "./send-text-details.component.html",
standalone: true,
imports: [
CheckboxModule,
CommonModule,
JslibModule,
ReactiveFormsModule,
FormFieldModule,
SectionComponent,
],
})
export class SendTextDetailsComponent implements OnInit {
@Input() config: SendFormConfig;
@Input() originalSendView?: SendView;
@Input() sendDetailsForm: BaseSendDetailsForm;
baseSendTextDetailsForm: BaseSendTextDetailsForm;
sendTextDetailsForm: SendTextDetailsForm;
constructor(
private formBuilder: FormBuilder,
protected sendFormContainer: SendFormContainer,
) {
this.baseSendTextDetailsForm = this.formBuilder.group({
text: new FormControl("", Validators.required),
hidden: new FormControl(false),
});
this.sendTextDetailsForm = Object.assign(this.baseSendTextDetailsForm, this.sendDetailsForm);
this.sendFormContainer.registerChildForm("sendTextDetailsForm", this.sendTextDetailsForm);
this.sendTextDetailsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
this.sendFormContainer.patchSend((send) => {
return Object.assign(send, {
text: {
text: value.text,
hidden: value.hidden,
},
});
});
});
}
ngOnInit() {
if (this.originalSendView) {
this.baseSendTextDetailsForm.patchValue({
text: this.originalSendView.text?.text || "",
hidden: this.originalSendView.text?.hidden || false,
});
}
}
}

View File

@ -1,4 +1,8 @@
<form [id]="formId" [formGroup]="sendForm" [bitSubmit]="submit">
<!-- TODO: Should we show a loading spinner here? Or emit a ready event for the container to handle loading state -->
<ng-container *ngIf="!loading"> </ng-container>
<ng-container *ngIf="!loading">
<tools-send-details
[config]="config"
[originalSendView]="originalSendView"
></tools-send-details>
</ng-container>
</form>

View File

@ -35,6 +35,8 @@ import { SendFormConfig } from "../abstractions/send-form-config.service";
import { SendFormService } from "../abstractions/send-form.service";
import { SendForm, SendFormContainer } from "../send-form-container";
import { SendDetailsComponent } from "./send-details/send-details.component";
@Component({
selector: "tools-send-form",
templateUrl: "./send-form.component.html",
@ -55,6 +57,7 @@ import { SendForm, SendFormContainer } from "../send-form-container";
ReactiveFormsModule,
SelectModule,
NgIf,
SendDetailsComponent,
],
})
export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, SendFormContainer {
@ -131,12 +134,11 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send
}
/**
* Patches the updated send with the provided partial senbd. Used by child components to update the send
* as their form values change.
* @param send
* Method to update the sendView with the new values. This method should be called by the child form components
* @param updateFn - A function that takes the current sendView and returns the updated sendView
*/
patchSend(send: Partial<SendView>): void {
this.updatedSendView = Object.assign(this.updatedSendView, send);
patchSend(updateFn: (current: SendView) => SendView): void {
this.updatedSendView = updateFn(this.updatedSendView);
}
/**

View File

@ -1,11 +1,16 @@
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendFormConfig } from "./abstractions/send-form-config.service";
import { SendDetailsComponent } from "./components/send-details/send-details.component";
import { SendTextDetailsForm } from "./components/send-details/send-text-details.component";
/**
* The complete form for a send. Includes all the sub-forms from their respective section components.
* TODO: Add additional form sections as they are implemented.
*/
export type SendForm = object;
export type SendForm = {
sendDetailsForm?: SendDetailsComponent["sendDetailsForm"];
sendTextDetailsForm?: SendTextDetailsForm;
};
/**
* A container for the {@link SendForm} that allows for registration of child form groups and patching of the send
@ -32,5 +37,5 @@ export abstract class SendFormContainer {
group: Exclude<SendForm[K], undefined>,
): void;
abstract patchSend(send: Partial<SendView>): void;
abstract patchSend(updateFn: (current: SendView) => SendView): void;
}

View File

@ -17,13 +17,8 @@ export class DefaultSendFormService implements SendFormService {
return await send.decrypt();
}
async saveSend(
send: SendView,
file: File | ArrayBuffer,
config: SendFormConfig,
): Promise<SendView> {
async saveSend(send: SendView, file: File | ArrayBuffer, config: SendFormConfig) {
const sendData = await this.sendService.encrypt(send, file, send.password, null);
const savedSend = await this.sendApiService.save(sendData);
return await savedSend.decrypt();
return await this.sendApiService.save(sendData);
}
}

View File

@ -30,27 +30,28 @@
<span slot="secondary">
{{ "deletionDate" | i18n }}:&nbsp;{{ send.deletionDate | date: "mediumDate" }}
</span>
<ng-container slot="end">
<bit-item-action>
<button
type="button"
(click)="copySendLink(send)"
appA11yTitle="{{ 'copyLink' | i18n }} - {{ send.name }}"
>
<i class="bwi tw-text-lg bwi-clone"></i>
</button>
</bit-item-action>
<bit-item-action>
<button
type="button"
(click)="deleteSend(send)"
appA11yTitle="{{ 'delete' | i18n }} - {{ send.name }}"
>
<i class="bwi tw-text-lg bwi-trash"></i>
</button>
</bit-item-action>
</ng-container>
</button>
<ng-container slot="end">
<bit-item-action>
<button
class="tw-p-1"
bitIconButton="bwi-clone"
size="small"
type="button"
(click)="copySendLink(send)"
appA11yTitle="{{ 'copyLink' | i18n }} - {{ send.name }}"
></button>
</bit-item-action>
<bit-item-action>
<button
bitIconButton="bwi-trash"
size="small"
type="button"
(click)="deleteSend(send)"
appA11yTitle="{{ 'delete' | i18n }} - {{ send.name }}"
></button>
</bit-item-action>
</ng-container>
</bit-item>
</bit-item-group>
</bit-section>

View File

@ -1,8 +1,3 @@
/**
* TODO: PM-10727 - Rename and Refactor this service
* This service is being used in both CipherForm and CipherView. Update this service to reflect that
*/
/**
* Service to capture TOTP secret from a client application.
*/
@ -11,5 +6,4 @@ export abstract class TotpCaptureService {
* Captures a TOTP secret and returns it as a string. Returns null if no TOTP secret was found.
*/
abstract captureTotpSecret(): Promise<string | null>;
abstract openAutofillNewTab(loginUri: string): void;
}

View File

@ -14,7 +14,7 @@
[appCopyClick]="notes"
showToast
[valueLabel]="'note' | i18n"
[appA11yTitle]="'copyValue' | i18n"
[appA11yTitle]="'copyNotes' | i18n"
></button>
</bit-form-field>
</bit-card>

View File

@ -35,7 +35,7 @@
[appCopyClick]="login.launchUri"
[valueLabel]="'website' | i18n"
showToast
[appA11yTitle]="'copyValue' | i18n"
[appA11yTitle]="'copyWebsite' | i18n"
data-testid="copy-website"
></button>
</bit-form-field>

View File

@ -2,18 +2,17 @@ import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import {
CardComponent,
FormFieldModule,
IconButtonModule,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
IconButtonModule,
} from "@bitwarden/components";
import { TotpCaptureService } from "../../cipher-form";
@Component({
selector: "app-autofill-options-view",
templateUrl: "autofill-options-view.component.html",
@ -32,9 +31,9 @@ import { TotpCaptureService } from "../../cipher-form";
export class AutofillOptionsViewComponent {
@Input() loginUris: LoginUriView[];
constructor(private totpCaptureService: TotpCaptureService) {}
constructor(private platformUtilsService: PlatformUtilsService) {}
async openWebsite(selectedUri: string) {
await this.totpCaptureService.openAutofillNewTab(selectedUri);
openWebsite(selectedUri: string) {
this.platformUtilsService.launchUri(selectedUri);
}
}

View File

@ -39,7 +39,7 @@
[appCopyClick]="card.number"
showToast
[valueLabel]="'number' | i18n"
[appA11yTitle]="'copyValue' | i18n"
[appA11yTitle]="'copyNumber' | i18n"
data-testid="copy-number"
></button>
</bit-form-field>
@ -79,7 +79,7 @@
[appCopyClick]="card.code"
showToast
[valueLabel]="'securityCode' | i18n"
[appA11yTitle]="'copyValue' | i18n"
[appA11yTitle]="'copySecurityCode' | i18n"
data-testid="copy-code"
(click)="logCardEvent(true, EventType.Cipher_ClientCopiedCardCode)"
></button>

View File

@ -7,6 +7,7 @@
class="tw-border-secondary-300 [&_bit-form-field:last-of-type]:tw-mb-0"
*ngFor="let field of cipher.fields; let last = last"
[ngClass]="{ 'tw-mb-4': !last }"
data-testid="custom-field"
>
<bit-form-field *ngIf="field.type === fieldType.Text" [disableReadOnlyBorder]="last">
<bit-label>{{ field.name }}</bit-label>
@ -18,7 +19,8 @@
[appCopyClick]="field.value"
showToast
[valueLabel]="field.name"
[appA11yTitle]="'copyValue' | i18n"
[appA11yTitle]="'copyCustomField' | i18n: field.name"
data-testid="copy-custom-field"
></button>
</bit-form-field>
<bit-form-field *ngIf="field.type === fieldType.Hidden" [disableReadOnlyBorder]="last">
@ -38,7 +40,7 @@
[appCopyClick]="field.value"
showToast
[valueLabel]="field.name"
[appA11yTitle]="'copyValue' | i18n"
[appA11yTitle]="'copyCustomField' | i18n: field.name"
(click)="logCopyEvent()"
></button>
</bit-form-field>

View File

@ -22,7 +22,7 @@
[appCopyClick]="cipher.login.username"
[valueLabel]="'username' | i18n"
showToast
[appA11yTitle]="'copyValue' | i18n"
[appA11yTitle]="'copyUsername' | i18n"
data-testid="copy-username"
></button>
</bit-form-field>
@ -64,7 +64,7 @@
[appCopyClick]="cipher.login.password"
[valueLabel]="'password' | i18n"
showToast
[appA11yTitle]="'copyValue' | i18n"
[appA11yTitle]="'copyPassword' | i18n"
data-testid="copy-password"
(click)="logCopyEvent()"
></button>
@ -127,7 +127,7 @@
[appCopyClick]="totpCodeCopyObj?.totpCode"
[valueLabel]="'verificationCodeTotp' | i18n"
showToast
[appA11yTitle]="'copyValue' | i18n"
[appA11yTitle]="'copyVerificationCode' | i18n"
data-testid="copy-totp"
[disabled]="!(isPremium$ | async)"
class="disabled:tw-cursor-default"

View File

@ -15,6 +15,7 @@
[appCopyClick]="cipher.identity.fullName"
showToast
[valueLabel]="'name' | i18n"
data-testid="copy-name"
></button>
</bit-form-field>
<bit-form-field *ngIf="cipher.identity.username">
@ -41,6 +42,7 @@
[appCopyClick]="cipher.identity.company"
showToast
[valueLabel]="'company' | i18n"
data-testid="copy-company"
></button>
</bit-form-field>
</read-only-cipher-card>
@ -70,6 +72,7 @@
[appCopyClick]="cipher.identity.ssn"
showToast
[valueLabel]="'ssn' | i18n"
data-testid="copy-ssn"
></button>
</bit-form-field>
<bit-form-field *ngIf="cipher.identity.passportNumber">
@ -96,6 +99,7 @@
[appCopyClick]="cipher.identity.passportNumber"
showToast
[valueLabel]="'passportNumber' | i18n"
data-testid="copy-passport"
></button>
</bit-form-field>
<bit-form-field *ngIf="cipher.identity.licenseNumber">
@ -109,6 +113,7 @@
[appCopyClick]="cipher.identity.licenseNumber"
showToast
[valueLabel]="'licenseNumber' | i18n"
data-testid="copy-license"
></button>
</bit-form-field>
</read-only-cipher-card>
@ -131,6 +136,7 @@
[appCopyClick]="cipher.identity.email"
showToast
[valueLabel]="'email' | i18n"
data-testid="copy-email"
></button>
</bit-form-field>
<bit-form-field *ngIf="cipher.identity.phone">
@ -144,6 +150,7 @@
[appCopyClick]="cipher.identity.phone"
showToast
[valueLabel]="'phone' | i18n"
data-testid="copy-phone"
></button>
</bit-form-field>
<bit-form-field *ngIf="addressFields">
@ -164,6 +171,7 @@
[appCopyClick]="addressFields"
showToast
[valueLabel]="'address' | i18n"
data-testid="copy-address"
></button>
</bit-form-field>
</read-only-cipher-card>

53
package-lock.json generated
View File

@ -124,8 +124,8 @@
"babel-loader": "9.1.3",
"base64-loader": "1.0.0",
"browserslist": "4.23.2",
"chromatic": "11.7.1",
"concurrently": "8.2.2",
"chromatic": "11.10.2",
"concurrently": "9.0.1",
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "7.1.2",
@ -171,7 +171,7 @@
"sass-loader": "16.0.1",
"storybook": "8.2.9",
"style-loader": "3.3.4",
"tailwindcss": "3.4.10",
"tailwindcss": "3.4.11",
"ts-jest": "29.2.2",
"ts-loader": "9.5.1",
"tsconfig-paths-webpack-plugin": "4.1.0",
@ -246,7 +246,7 @@
},
"apps/web": {
"name": "@bitwarden/web-vault",
"version": "2024.9.0"
"version": "2024.9.1"
},
"libs/admin-console": {
"name": "@bitwarden/admin-console",
@ -13845,9 +13845,9 @@
}
},
"node_modules/chromatic": {
"version": "11.7.1",
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.7.1.tgz",
"integrity": "sha512-LvgPimdQdnQB07ZDxLEC2KtxgYeqTw0X71GA7fi3zhgtKLxZcE+BSZ/5I9rrQp1V8ydmfElfw0ZwnUH4fVgUAQ==",
"version": "11.10.2",
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.10.2.tgz",
"integrity": "sha512-EbVlhmOLGdx9QRX3RMOTF3UzoyC1aaXNRjlzm1mc++2OI5+6C5Bzwt2ZUYJ3Jnf/pJa23q0y5Y3QEDcfRVqIbg==",
"dev": true,
"license": "MIT",
"bin": {
@ -14537,18 +14537,16 @@
}
},
"node_modules/concurrently": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
"integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.0.1.tgz",
"integrity": "sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"date-fns": "^2.30.0",
"lodash": "^4.17.21",
"rxjs": "^7.8.1",
"shell-quote": "^1.8.1",
"spawn-command": "0.0.2",
"supports-color": "^8.1.1",
"tree-kill": "^1.2.2",
"yargs": "^17.7.2"
@ -14558,7 +14556,7 @@
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": "^14.13.0 || >=16.0.0"
"node": ">=18"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
@ -15476,23 +15474,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debounce-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz",
@ -34665,12 +34646,6 @@
"node": ">= 0.10"
}
},
"node_modules/spawn-command": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
"dev": true
},
"node_modules/spdx-correct": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
@ -35516,9 +35491,9 @@
"license": "MIT"
},
"node_modules/tailwindcss": {
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz",
"integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -86,8 +86,8 @@
"babel-loader": "9.1.3",
"base64-loader": "1.0.0",
"browserslist": "4.23.2",
"chromatic": "11.7.1",
"concurrently": "8.2.2",
"chromatic": "11.10.2",
"concurrently": "9.0.1",
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "7.1.2",
@ -133,7 +133,7 @@
"sass-loader": "16.0.1",
"storybook": "8.2.9",
"style-loader": "3.3.4",
"tailwindcss": "3.4.10",
"tailwindcss": "3.4.11",
"ts-jest": "29.2.2",
"ts-loader": "9.5.1",
"tsconfig-paths-webpack-plugin": "4.1.0",