diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index a78d3bda5ad..e113d5c253b 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -87,6 +87,7 @@ jobs: os: [ { base: "linux", distro: "ubuntu-22.04", target_suffix: "" }, + { base: "linux", distro: "ubuntu-22.04-arm", target_suffix: "-arm64" }, { base: "mac", distro: "macos-13", target_suffix: "" }, { base: "mac", distro: "macos-14", target_suffix: "-arm64" } ] @@ -130,7 +131,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -306,7 +307,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 4c35eb4c42f..365d29f17f7 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -201,7 +201,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -237,7 +237,7 @@ jobs: TARGET: musl run: | rustup target add x86_64-unknown-linux-musl - node build.js cross-platform + node build.js --target=x86_64-unknown-linux-musl --release - name: Build application run: npm run dist:lin @@ -298,6 +298,103 @@ jobs: if-no-files-found: error + linux-arm64: + name: Linux ARM64 Build + # Note, before updating the ubuntu version of the workflow, ensure the snap base image + # is equal or greater than the new version. Otherwise there might be GLIBC version issues. + # The snap base for desktop is defined in `apps/desktop/electron-builder.json` + # We intentionally keep this runner on the oldest supported OS in GitHub Actions + # for maximum compatibility across GLIBC versions + runs-on: ubuntu-22.04-arm + needs: setup + env: + _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} + _NODE_VERSION: ${{ needs.setup.outputs.node_version }} + NODE_OPTIONS: --max_old_space_size=4096 + defaults: + run: + working-directory: apps/desktop + steps: + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Node + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + node-version: ${{ env._NODE_VERSION }} + + - name: Set up environment + run: | + sudo apt-get update + sudo apt-get -y install pkg-config libxss-dev rpm musl-dev musl-tools flatpak flatpak-builder + + - name: Print environment + run: | + node --version + npm --version + snap --version + snapcraft --version || echo 'snapcraft unavailable' + + - name: Install Node dependencies + run: npm ci + working-directory: ./ + + - name: Download SDK Artifacts + if: ${{ inputs.sdk_branch != '' }} + uses: bitwarden/gh-actions/download-artifacts@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + workflow: build-wasm-internal.yml + workflow_conclusion: success + branch: ${{ inputs.sdk_branch }} + artifacts: sdk-internal + repo: bitwarden/sdk-internal + path: ../sdk-internal + if_no_artifact_found: fail + + - name: Override SDK + if: ${{ inputs.sdk_branch != '' }} + working-directory: ./ + run: | + ls -l ../ + npm link ../sdk-internal + + - name: Cache Native Module + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + id: cache + with: + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* + ${{ env.RUNNER_TEMP }}/.cargo/registry + ${{ env.RUNNER_TEMP }}/.cargo/git + key: rust-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} + + - name: Build Native Module + if: steps.cache.outputs.cache-hit != 'true' + working-directory: apps/desktop/desktop_native + env: + PKG_CONFIG_ALLOW_CROSS: true + PKG_CONFIG_ALL_STATIC: true + TARGET: musl + run: | + rustup target add aarch64-unknown-linux-musl + node build.js --target=aarch64-unknown-linux-musl --release + + - name: Build application + run: npm run dist:lin:arm64 + + - name: Upload tar.gz artifact + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + with: + name: bitwarden_${{ env._PACKAGE_VERSION }}_arm64.tar.gz + path: apps/desktop/dist/bitwarden_desktop_arm64.tar.gz + if-no-files-found: error + windows: name: Windows Build runs-on: windows-2022 @@ -369,7 +466,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -705,7 +802,7 @@ jobs: if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -895,7 +992,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -1144,7 +1241,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -1425,7 +1522,7 @@ jobs: if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts index e4fe012a678..5e78fd5658a 100644 --- a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts +++ b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts @@ -4,7 +4,7 @@ import { html, TemplateResult } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; import { themes } from "../../../content/components/constants/styles"; -import { Business, Users } from "../../../content/components/icons"; +import { Business, Family } from "../../../content/components/icons"; import { OrganizationCategories, OrganizationCategory } from "./types"; @@ -13,7 +13,7 @@ const cipherIndicatorIconsMap: Record< (args: { color: string; theme: Theme }) => TemplateResult > = { [OrganizationCategories.business]: Business, - [OrganizationCategories.family]: Users, + [OrganizationCategories.family]: Family, }; export function CipherInfoIndicatorIcons({ diff --git a/apps/browser/src/autofill/content/components/icons/collection.ts b/apps/browser/src/autofill/content/components/icons/collection-shared.ts similarity index 65% rename from apps/browser/src/autofill/content/components/icons/collection.ts rename to apps/browser/src/autofill/content/components/icons/collection-shared.ts index fb2c58647c5..de366b88e92 100644 --- a/apps/browser/src/autofill/content/components/icons/collection.ts +++ b/apps/browser/src/autofill/content/components/icons/collection-shared.ts @@ -4,14 +4,14 @@ import { html } from "lit"; import { IconProps } from "../common-types"; import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; -export function Collection({ color, disabled, theme }: IconProps) { +export function CollectionShared({ color, disabled, theme }: IconProps) { const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; return html` + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts index de39b70ab24..65ec6301ac4 100644 --- a/apps/browser/src/autofill/content/components/icons/index.ts +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -3,12 +3,12 @@ export { AngleUp } from "./angle-up"; export { BrandIconContainer } from "./brand-icon-container"; export { Business } from "./business"; export { Close } from "./close"; -export { Collection } from "./collection"; +export { CollectionShared } from "./collection-shared"; export { ExclamationTriangle } from "./exclamation-triangle"; export { ExternalLink } from "./external-link"; +export { Family } from "./family"; export { Folder } from "./folder"; export { Globe } from "./globe"; export { PencilSquare } from "./pencil-square"; export { Shield } from "./shield"; export { User } from "./user"; -export { Users } from "./users"; diff --git a/apps/browser/src/autofill/content/components/icons/users.ts b/apps/browser/src/autofill/content/components/icons/users.ts deleted file mode 100644 index eb7840104f0..00000000000 --- a/apps/browser/src/autofill/content/components/icons/users.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { css } from "@emotion/css"; -import { html } from "lit"; - -import { IconProps } from "../common-types"; -import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; - -export function Users({ color, disabled, theme }: IconProps) { - const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; - - return html` - - - - `; -} diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts index fc5db1c7c2c..c74895e1dea 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts @@ -43,26 +43,23 @@ const createIconStory = (iconName: keyof typeof Icons): StoryObj => { render: (args) => Template(args, Icons[iconName]), } as StoryObj; - if (iconName !== "BrandIconContainer") { - story.argTypes = { - iconLink: { table: { disable: true } }, - }; - } + story.argTypes = { + iconLink: { table: { disable: true } }, + }; return story; }; export const AngleDownIcon = createIconStory("AngleDown"); export const AngleUpIcon = createIconStory("AngleUp"); -export const BrandIcon = createIconStory("BrandIconContainer"); export const BusinessIcon = createIconStory("Business"); export const CloseIcon = createIconStory("Close"); -export const CollectionIcon = createIconStory("Collection"); +export const CollectionSharedIcon = createIconStory("CollectionShared"); export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle"); export const ExternalLinkIcon = createIconStory("ExternalLink"); +export const FamilyIcon = createIconStory("Family"); export const FolderIcon = createIconStory("Folder"); export const GlobeIcon = createIconStory("Globe"); export const PencilSquareIcon = createIconStory("PencilSquare"); export const ShieldIcon = createIconStory("Shield"); export const UserIcon = createIconStory("User"); -export const UsersIcon = createIconStory("Users"); diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts index 6fa32f11aa2..8661f5957e1 100644 --- a/apps/browser/src/autofill/content/components/notification/button-row.ts +++ b/apps/browser/src/autofill/content/components/notification/button-row.ts @@ -4,14 +4,14 @@ import { ProductTierType } from "@bitwarden/common/billing/enums"; import { Theme } from "@bitwarden/common/platform/enums"; import { Option, OrgView, FolderView } from "../common-types"; -import { Business, Users, Folder, User } from "../icons"; +import { Business, Family, Folder, User } from "../icons"; import { ButtonRow } from "../rows/button-row"; function getVaultIconByProductTier(productTierType?: ProductTierType): Option["icon"] { switch (productTierType) { case ProductTierType.Free: case ProductTierType.Families: - return Users; + return Family; case ProductTierType.Teams: case ProductTierType.Enterprise: case ProductTierType.TeamsStarter: diff --git a/apps/browser/src/platform/ipc/background-communication-backend.ts b/apps/browser/src/platform/ipc/background-communication-backend.ts index 6c5b374dd56..1ebb835fa3b 100644 --- a/apps/browser/src/platform/ipc/background-communication-backend.ts +++ b/apps/browser/src/platform/ipc/background-communication-backend.ts @@ -18,7 +18,11 @@ export class BackgroundCommunicationBackend implements CommunicationBackend { return; } - void this.queue.enqueue({ ...message.message, source: { Web: { id: sender.tab.id } } }); + void this.queue.enqueue( + new IncomingMessage(message.message.payload, message.message.destination, { + Web: { id: sender.tab.id }, + }), + ); }); } diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 8ebf6eb6110..21ac4c19700 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -593,6 +593,7 @@ const routes: Routes = [ path: "intro-carousel", component: ExtensionAnonLayoutWrapperComponent, canActivate: [], + data: { elevation: 0, doNotSaveUrl: true } satisfies RouteDataProperties, children: [ { path: "", diff --git a/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html b/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html index 3c061109945..ff7bf25b86b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/intro-carousel/intro-carousel.component.html @@ -43,7 +43,7 @@ bitButton buttonType="secondary" (click)="navigateToLogin()" - class="tw-w-full tw-mt-4" + class="tw-w-full tw-mt-2" > {{ "logIn" | i18n }} diff --git a/apps/cli/package.json b/apps/cli/package.json index 7d9f4af0ffe..04fe4290d31 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -34,18 +34,22 @@ "dist:oss:mac": "npm run build:oss:prod && npm run clean && npm run package:oss:mac", "dist:oss:mac-arm64": "npm run build:oss:prod && npm run clean && npm run package:oss:mac-arm64", "dist:oss:lin": "npm run build:oss:prod && npm run clean && npm run package:oss:lin", + "dist:oss:lin-arm64": "npm run build:oss:prod && npm run clean && npm run package:oss:lin-arm64", "dist:bit:win": "npm run build:bit:prod && npm run clean && npm run package:bit:win", "dist:bit:mac": "npm run build:bit:prod && npm run clean && npm run package:bit:mac", "dist:bit:mac-arm64": "npm run build:bit:prod && npm run clean && npm run package:bit:mac-arm64", "dist:bit:lin": "npm run build:bit:prod && npm run clean && npm run package:bit:lin", + "dist:bit:lin-arm64": "npm run build:bit:prod && npm run clean && npm run package:bit:lin-arm64", "package:oss:win": "pkg . --targets win-x64 --output ./dist/oss/windows/bw.exe", "package:oss:mac": "pkg . --targets macos-x64 --output ./dist/oss/macos/bw", "package:oss:mac-arm64": "pkg . --targets macos-arm64 --output ./dist/oss/macos-arm64/bw", "package:oss:lin": "pkg . --targets linux-x64 --output ./dist/oss/linux/bw", + "package:oss:lin-arm64": "pkg . --targets linux-arm64 --output ./dist/oss/linux-arm64/bw", "package:bit:win": "pkg . --targets win-x64 --output ./dist/bit/windows/bw.exe", "package:bit:mac": "pkg . --targets macos-x64 --output ./dist/bit/macos/bw", "package:bit:mac-arm64": "pkg . --targets macos-arm64 --output ./dist/bit/macos-arm64/bw", "package:bit:lin": "pkg . --targets linux-x64 --output ./dist/bit/linux/bw", + "package:bit:lin-arm64": "pkg . --targets linux-arm64 --output ./dist/bit/linux-arm64/bw", "test": "jest", "test:watch": "jest --watch", "test:watch:all": "jest --watchAll" @@ -72,7 +76,7 @@ "inquirer": "8.2.6", "jsdom": "26.0.0", "jszip": "3.10.1", - "koa": "2.15.4", + "koa": "2.16.1", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", diff --git a/apps/cli/src/tools/export.command.ts b/apps/cli/src/tools/export.command.ts index f5fea794eef..3fbc466efe0 100644 --- a/apps/cli/src/tools/export.command.ts +++ b/apps/cli/src/tools/export.command.ts @@ -10,8 +10,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ExportFormat, @@ -30,7 +28,6 @@ export class ExportCommand { private policyService: PolicyService, private eventCollectionService: EventCollectionService, private accountService: AccountService, - private configService: ConfigService, ) {} async run(options: OptionValues): Promise { @@ -55,13 +52,6 @@ export class ExportCommand { const format = password && options.format == "json" ? "encrypted_json" : (options.format ?? "csv"); - if ( - format == "zip" && - !(await this.configService.getFeatureFlag(FeatureFlag.ExportAttachments)) - ) { - return Response.badRequest("Exporting attachments is not supported in this environment."); - } - if (!this.isSupportedExportFormat(format)) { return Response.badRequest( `'${format}' is not a supported export format. Supported formats: ${EXPORT_FORMATS.join( diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index c004d3597c1..ce6ac2af94e 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -464,7 +464,7 @@ export class VaultProgram extends BaseProgram { private exportCommand(): Command { return new Command("export") - .description("Export vault data to a CSV or JSON file.") + .description("Export vault data to a CSV, JSON or ZIP file.") .option("--output ", "Output directory or filename.") .option("--format ", "Export file format.") .option( @@ -476,7 +476,7 @@ export class VaultProgram extends BaseProgram { writeLn("\n Notes:"); writeLn(""); writeLn( - " Valid formats are `csv`, `json`, and `encrypted_json`. Default format is `csv`.", + " Valid formats are `csv`, `json`, `encrypted_json` and zip. Default format is `csv`.", ); writeLn(""); writeLn( @@ -504,7 +504,6 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.policyService, this.serviceContainer.eventCollectionService, this.serviceContainer.accountService, - this.serviceContainer.configService, ); const response = await command.run(options); this.processResponse(response); diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index f2f012bf088..ec20dce4116 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -3,6 +3,10 @@ const child_process = require("child_process"); const fs = require("fs"); const path = require("path"); const process = require("process"); +const args = process.argv.slice(2); // Get arguments passed to the script +const mode = args.includes("--release") ? "release" : "debug"; +const targetArg = args.find(arg => arg.startsWith("--target=")); +const target = targetArg ? targetArg.split("=")[1] : null; let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platform"; @@ -18,10 +22,17 @@ function buildProxyBin(target, release = true) { return child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")}); } -if (!crossPlatform) { - console.log("Building native modules in debug mode for the native architecture"); - buildNapiModule(false, false); - buildProxyBin(false, false); +if (!crossPlatform && !target) { + console.log(`Building native modules in ${mode} mode for the native architecture`); + buildNapiModule(false, mode === "release"); + buildProxyBin(false, mode === "release"); + return; +} + +if (target) { + console.log(`Building for target: ${target} in ${mode} mode`); + buildNapiModule(target, mode === "release"); + buildProxyBin(target, mode === "release"); return; } @@ -47,7 +58,8 @@ switch (process.platform) { default: targets = [ - ['x86_64-unknown-linux-musl', 'x64'] + ['x86_64-unknown-linux-musl', 'x64'], + ['aarch64-unknown-linux-musl', 'arm64'] ]; process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a4365b4c06a..ae34deee5b8 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -36,6 +36,7 @@ "pack:dir": "npm run clean:dist && electron-builder --dir -p never", "pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop", "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snapcraft pack ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/", + "pack:lin:arm64": "npm run clean:dist && electron-builder --dir -p never && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .", "pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never", "pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never", "pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never", @@ -45,6 +46,7 @@ "pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never", "dist:dir": "npm run build && npm run pack:dir", "dist:lin": "npm run build && npm run pack:lin", + "dist:lin:arm64": "npm run build && npm run pack:lin:arm64", "dist:mac": "npm run build && npm run pack:mac", "dist:mac:mas": "npm run build && npm run pack:mac:mas", "dist:mac:masdev": "npm run build && npm run pack:mac:masdev", diff --git a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts index 9666630fc08..153a2f3a956 100644 --- a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts @@ -3,9 +3,10 @@ import { RouterModule, Routes } from "@angular/router"; import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { SponsoredFamiliesComponent } from "../../../billing/settings/sponsored-families.component"; +import { FreeBitwardenFamiliesComponent } from "../../../billing/members/free-bitwarden-families.component"; import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; +import { canAccessSponsoredFamilies } from "./../../../billing/guards/can-access-sponsored-families.guard"; import { MembersComponent } from "./members.component"; const routes: Routes = [ @@ -19,8 +20,8 @@ const routes: Routes = [ }, { path: "sponsored-families", - component: SponsoredFamiliesComponent, - canActivate: [organizationPermissionsGuard(canAccessMembersTab)], + component: FreeBitwardenFamiliesComponent, + canActivate: [organizationPermissionsGuard(canAccessMembersTab), canAccessSponsoredFamilies], data: { titleId: "sponsoredFamilies", }, diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts index fdad689d982..57fe212fa65 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts @@ -43,6 +43,8 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { value.plan = PlanType.FamiliesAnnually; value.productTier = ProductTierType.Families; value.acceptingSponsorship = true; + value.planSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise; + // eslint-disable-next-line rxjs-angular/prefer-takeuntil value.onSuccess.subscribe(this.onOrganizationCreateSuccess.bind(this)); } diff --git a/apps/web/src/app/billing/guards/can-access-sponsored-families.guard.ts b/apps/web/src/app/billing/guards/can-access-sponsored-families.guard.ts new file mode 100644 index 00000000000..9bc6778f0b0 --- /dev/null +++ b/apps/web/src/app/billing/guards/can-access-sponsored-families.guard.ts @@ -0,0 +1,26 @@ +import { inject } from "@angular/core"; +import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router"; +import { firstValueFrom, switchMap, filter } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { getById } from "@bitwarden/common/platform/misc"; + +import { FreeFamiliesPolicyService } from "../services/free-families-policy.service"; + +export const canAccessSponsoredFamilies: CanActivateFn = async (route: ActivatedRouteSnapshot) => { + const freeFamiliesPolicyService = inject(FreeFamiliesPolicyService); + const organizationService = inject(OrganizationService); + const accountService = inject(AccountService); + + const org = accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => organizationService.organizations$(userId)), + getById(route.params.organizationId), + filter((org): org is Organization => org !== undefined), + ); + + return await firstValueFrom(freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(org)); +}; diff --git a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html new file mode 100644 index 00000000000..2dbcc577e54 --- /dev/null +++ b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.html @@ -0,0 +1,45 @@ +
+ + {{ "addSponsorship" | i18n }} + +
+ +
+
+ + {{ "email" | i18n }}: + + +
+
+ + {{ "notes" | i18n }}: + + +
+
+ +
+ + + + + +
+ diff --git a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts new file mode 100644 index 00000000000..54d9ae90009 --- /dev/null +++ b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts @@ -0,0 +1,135 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { Component } from "@angular/core"; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from "@angular/forms"; +import { firstValueFrom, map } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ButtonModule, DialogModule, DialogService, FormFieldModule } from "@bitwarden/components"; + +interface RequestSponsorshipForm { + sponsorshipEmail: FormControl; + sponsorshipNote: FormControl; +} + +export interface AddSponsorshipDialogResult { + action: AddSponsorshipDialogAction; + value: Partial | null; +} + +interface AddSponsorshipFormValue { + sponsorshipEmail: string; + sponsorshipNote: string; + status: string; +} + +enum AddSponsorshipDialogAction { + Saved = "saved", + Canceled = "canceled", +} + +@Component({ + templateUrl: "add-sponsorship-dialog.component.html", + standalone: true, + imports: [ + JslibModule, + ButtonModule, + DialogModule, + FormsModule, + ReactiveFormsModule, + FormFieldModule, + ], +}) +export class AddSponsorshipDialogComponent { + sponsorshipForm: FormGroup; + loading = false; + + constructor( + private dialogRef: DialogRef, + private formBuilder: FormBuilder, + private accountService: AccountService, + private i18nService: I18nService, + ) { + this.sponsorshipForm = this.formBuilder.group({ + sponsorshipEmail: new FormControl("", { + validators: [Validators.email, Validators.required], + asyncValidators: [this.validateNotCurrentUserEmail.bind(this)], + updateOn: "change", + }), + sponsorshipNote: new FormControl("", {}), + }); + } + + static open(dialogService: DialogService): DialogRef { + return dialogService.open(AddSponsorshipDialogComponent); + } + + protected async save() { + if (this.sponsorshipForm.invalid) { + return; + } + + this.loading = true; + // TODO: This is a mockup implementation - needs to be updated with actual API integration + await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call + + const formValue = this.sponsorshipForm.getRawValue(); + const dialogValue: Partial = { + status: "Sent", + sponsorshipEmail: formValue.sponsorshipEmail ?? "", + sponsorshipNote: formValue.sponsorshipNote ?? "", + }; + + this.dialogRef.close({ + action: AddSponsorshipDialogAction.Saved, + value: dialogValue, + }); + + this.loading = false; + } + + protected close = () => { + this.dialogRef.close({ action: AddSponsorshipDialogAction.Canceled, value: null }); + }; + + get sponsorshipEmailControl() { + return this.sponsorshipForm.controls.sponsorshipEmail; + } + + get sponsorshipNoteControl() { + return this.sponsorshipForm.controls.sponsorshipNote; + } + + private async validateNotCurrentUserEmail( + control: AbstractControl, + ): Promise { + const value = control.value; + if (!value) { + return null; + } + + const currentUserEmail = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.email ?? "")), + ); + + if (!currentUserEmail) { + return null; + } + + if (value.toLowerCase() === currentUserEmail.toLowerCase()) { + return { currentUserEmail: true }; + } + + return null; + } +} diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.html b/apps/web/src/app/billing/members/free-bitwarden-families.component.html new file mode 100644 index 00000000000..fe1dd15ab15 --- /dev/null +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + +

{{ "sponsoredFamiliesRemoveActiveSponsorship" | i18n }}

diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts new file mode 100644 index 00000000000..af43e5a4bc1 --- /dev/null +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts @@ -0,0 +1,62 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { Component, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { DialogService } from "@bitwarden/components"; + +import { FreeFamiliesPolicyService } from "../services/free-families-policy.service"; + +import { + AddSponsorshipDialogComponent, + AddSponsorshipDialogResult, +} from "./add-sponsorship-dialog.component"; +import { SponsoredFamily } from "./types/sponsored-family"; + +@Component({ + selector: "app-free-bitwarden-families", + templateUrl: "free-bitwarden-families.component.html", +}) +export class FreeBitwardenFamiliesComponent implements OnInit { + tabIndex = 0; + sponsoredFamilies: SponsoredFamily[] = []; + + constructor( + private router: Router, + private dialogService: DialogService, + private freeFamiliesPolicyService: FreeFamiliesPolicyService, + ) {} + + async ngOnInit() { + await this.preventAccessToFreeFamiliesPage(); + } + + async addSponsorship() { + const addSponsorshipDialogRef: DialogRef = + AddSponsorshipDialogComponent.open(this.dialogService); + + const dialogRef = await firstValueFrom(addSponsorshipDialogRef.closed); + + if (dialogRef?.value) { + this.sponsoredFamilies = [dialogRef.value, ...this.sponsoredFamilies]; + } + } + + removeSponsorhip(sponsorship: any) { + const index = this.sponsoredFamilies.findIndex( + (e) => e.sponsorshipEmail == sponsorship.sponsorshipEmail, + ); + this.sponsoredFamilies.splice(index, 1); + } + + private async preventAccessToFreeFamiliesPage() { + const showFreeFamiliesPage = await firstValueFrom( + this.freeFamiliesPolicyService.showFreeFamilies$, + ); + + if (!showFreeFamiliesPage) { + await this.router.navigate(["/"]); + return; + } + } +} diff --git a/apps/web/src/app/billing/members/organization-member-families.component.html b/apps/web/src/app/billing/members/organization-member-families.component.html new file mode 100644 index 00000000000..c5b7283d9d9 --- /dev/null +++ b/apps/web/src/app/billing/members/organization-member-families.component.html @@ -0,0 +1,47 @@ + + +

+ {{ "membersWithSponsoredFamilies" | i18n }} +

+ +

{{ "memberFamilies" | i18n }}

+ + @if (loading) { + + + {{ "loading" | i18n }} + + } + + @if (!loading && memberFamilies?.length > 0) { + + + + + {{ "member" | i18n }} + {{ "status" | i18n }} + + + + + @for (o of memberFamilies; let i = $index; track i) { + + + {{ o.sponsorshipEmail }} + {{ o.status }} + + + } + + +
+
+ } @else { +
+ Search +

{{ "noMemberFamilies" | i18n }}

+

{{ "noMemberFamiliesDescription" | i18n }}

+
+ } +
+
diff --git a/apps/web/src/app/billing/members/organization-member-families.component.ts b/apps/web/src/app/billing/members/organization-member-families.component.ts new file mode 100644 index 00000000000..52c95646a11 --- /dev/null +++ b/apps/web/src/app/billing/members/organization-member-families.component.ts @@ -0,0 +1,34 @@ +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { Subject } from "rxjs"; + +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { SponsoredFamily } from "./types/sponsored-family"; + +@Component({ + selector: "app-organization-member-families", + templateUrl: "organization-member-families.component.html", +}) +export class OrganizationMemberFamiliesComponent implements OnInit, OnDestroy { + tabIndex = 0; + loading = false; + + @Input() memberFamilies: SponsoredFamily[] = []; + + private _destroy = new Subject(); + + constructor(private platformUtilsService: PlatformUtilsService) {} + + async ngOnInit() { + this.loading = false; + } + + ngOnDestroy(): void { + this._destroy.next(); + this._destroy.complete(); + } + + get isSelfHosted(): boolean { + return this.platformUtilsService.isSelfHost(); + } +} diff --git a/apps/web/src/app/billing/members/organization-sponsored-families.component.html b/apps/web/src/app/billing/members/organization-sponsored-families.component.html new file mode 100644 index 00000000000..7db96deb4ab --- /dev/null +++ b/apps/web/src/app/billing/members/organization-sponsored-families.component.html @@ -0,0 +1,87 @@ + + +

+ {{ "sponsorFreeBitwardenFamilies" | i18n }} +

+
+ {{ "sponsoredFamiliesInclude" | i18n }}: +
    +
  • {{ "sponsoredFamiliesPremiumAccess" | i18n }}
  • +
  • {{ "sponsoredFamiliesSharedCollections" | i18n }}
  • +
+
+ +

{{ "sponsoredBitwardenFamilies" | i18n }}

+ + @if (loading) { + + + {{ "loading" | i18n }} + + } + + @if (!loading && sponsoredFamilies?.length > 0) { + + + + + {{ "recipient" | i18n }} + {{ "status" | i18n }} + {{ "notes" | i18n }} + + + + + @for (o of sponsoredFamilies; let i = $index; track i) { + + + {{ o.sponsorshipEmail }} + {{ o.status }} + {{ o.sponsorshipNote }} + + + + + +
+ + +
+ + +
+ } +
+
+
+
+ } @else { +
+ Search +

{{ "noSponsoredFamilies" | i18n }}

+

{{ "noSponsoredFamiliesDescription" | i18n }}

+
+ } +
+
diff --git a/apps/web/src/app/billing/members/organization-sponsored-families.component.ts b/apps/web/src/app/billing/members/organization-sponsored-families.component.ts new file mode 100644 index 00000000000..7cc46634a38 --- /dev/null +++ b/apps/web/src/app/billing/members/organization-sponsored-families.component.ts @@ -0,0 +1,39 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { Subject } from "rxjs"; + +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { SponsoredFamily } from "./types/sponsored-family"; + +@Component({ + selector: "app-organization-sponsored-families", + templateUrl: "organization-sponsored-families.component.html", +}) +export class OrganizationSponsoredFamiliesComponent implements OnInit, OnDestroy { + loading = false; + tabIndex = 0; + + @Input() sponsoredFamilies: SponsoredFamily[] = []; + @Output() removeSponsorshipEvent = new EventEmitter(); + + private _destroy = new Subject(); + + constructor(private platformUtilsService: PlatformUtilsService) {} + + async ngOnInit() { + this.loading = false; + } + + get isSelfHosted(): boolean { + return this.platformUtilsService.isSelfHost(); + } + + remove(sponsorship: SponsoredFamily) { + this.removeSponsorshipEvent.emit(sponsorship); + } + + ngOnDestroy(): void { + this._destroy.next(); + this._destroy.complete(); + } +} diff --git a/apps/web/src/app/billing/members/types/sponsored-family.ts b/apps/web/src/app/billing/members/types/sponsored-family.ts new file mode 100644 index 00000000000..82d2e3948b2 --- /dev/null +++ b/apps/web/src/app/billing/members/types/sponsored-family.ts @@ -0,0 +1,5 @@ +export interface SponsoredFamily { + sponsorshipEmail?: string; + sponsorshipNote?: string; + status?: string; +} diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index fc7d6793a85..59f8dd34c37 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -34,7 +34,12 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; +import { + PaymentMethodType, + PlanSponsorshipType, + PlanType, + ProductTierType, +} from "@bitwarden/common/billing/enums"; import { TaxInformation } from "@bitwarden/common/billing/models/domain"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; @@ -83,6 +88,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { @Input() showFree = true; @Input() showCancel = false; @Input() acceptingSponsorship = false; + @Input() planSponsorshipType?: PlanSponsorshipType; @Input() currentPlan: PlanResponse; selectedFile: File; @@ -682,11 +688,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } private refreshSalesTax(): void { - if (this.formGroup.controls.plan.value == PlanType.Free) { - this.estimatedTax = 0; - return; - } - if (!this.taxComponent.validate()) { return; } @@ -696,6 +697,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { passwordManager: { additionalStorage: this.formGroup.controls.additionalStorage.value, plan: this.formGroup.controls.plan.value, + sponsoredPlan: this.planSponsorshipType, seats: this.formGroup.controls.additionalSeats.value, }, taxInformation: { diff --git a/apps/web/src/app/platform/ipc/web-communication-provider.ts b/apps/web/src/app/platform/ipc/web-communication-provider.ts index 85353ab77af..787a3c7f3a4 100644 --- a/apps/web/src/app/platform/ipc/web-communication-provider.ts +++ b/apps/web/src/app/platform/ipc/web-communication-provider.ts @@ -19,7 +19,13 @@ export class WebCommunicationProvider implements CommunicationBackend { return; } - await this.queue.enqueue({ ...message.message, source: "BrowserBackground" }); + void this.queue.enqueue( + new IncomingMessage( + message.message.payload, + message.message.destination, + "BrowserBackground", + ), + ); }); } diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 5dc34b3b5b1..469ebe457d0 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -62,6 +62,9 @@ import { OrganizationBadgeModule } from "../vault/individual-vault/organization- import { PipesModule } from "../vault/individual-vault/pipes/pipes.module"; import { PurgeVaultComponent } from "../vault/settings/purge-vault.component"; +import { FreeBitwardenFamiliesComponent } from "./../billing/members/free-bitwarden-families.component"; +import { OrganizationMemberFamiliesComponent } from "./../billing/members/organization-member-families.component"; +import { OrganizationSponsoredFamiliesComponent } from "./../billing/members/organization-sponsored-families.component"; import { EnvironmentSelectorModule } from "./../components/environment-selector/environment-selector.module"; import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component"; import { SharedModule } from "./shared.module"; @@ -128,6 +131,9 @@ import { SharedModule } from "./shared.module"; SelectableAvatarComponent, SetPasswordComponent, SponsoredFamiliesComponent, + OrganizationSponsoredFamiliesComponent, + OrganizationMemberFamiliesComponent, + FreeBitwardenFamiliesComponent, SponsoringOrgRowComponent, UpdatePasswordComponent, UpdateTempPasswordComponent, @@ -175,6 +181,9 @@ import { SharedModule } from "./shared.module"; SelectableAvatarComponent, SetPasswordComponent, SponsoredFamiliesComponent, + OrganizationSponsoredFamiliesComponent, + OrganizationMemberFamiliesComponent, + FreeBitwardenFamiliesComponent, SponsoringOrgRowComponent, UpdateTempPasswordComponent, UpdatePasswordComponent, diff --git a/apps/web/src/app/tools/reports/pages/breach-report.component.html b/apps/web/src/app/tools/reports/pages/breach-report.component.html index fcfd37fef3f..d645fa39d69 100644 --- a/apps/web/src/app/tools/reports/pages/breach-report.component.html +++ b/apps/web/src/app/tools/reports/pages/breach-report.component.html @@ -7,7 +7,7 @@ {{ "username" | i18n }} - {{ "breachCheckUsernameEmail" | i18n }} + {{ "breachCheckUsernameEmail" | i18n }} @@ -21,32 +21,33 @@ {{ "breachUsernameFound" | i18n: checkedUsername : breachedAccounts.length }} -
    -
  • -
    -
    - -
    -
    -

    {{ a.title }}

    -

    -

    {{ "compromisedData" | i18n }}:

    -
      -
    • {{ d }}
    • -
    -
    -
    -
    -
    {{ "website" | i18n }}
    -
    {{ a.domain }}
    -
    {{ "affectedUsers" | i18n }}
    -
    {{ a.pwnCount | number }}
    -
    {{ "breachOccurred" | i18n }}
    -
    {{ a.breachDate | date: "mediumDate" }}
    -
    {{ "breachReported" | i18n }}
    -
    {{ a.addedDate | date: "mediumDate" }}
    -
    -
    +
      +
    • +
      + +
      +
      +

      {{ a.title }}

      +

      +

      {{ "compromisedData" | i18n }}:

      +
        +
      • {{ d }}
      • +
      +
      +
      +
      +
      {{ "website" | i18n }}
      +
      {{ a.domain }}
      +
      {{ "affectedUsers" | i18n }}
      +
      {{ a.pwnCount | number }}
      +
      {{ "breachOccurred" | i18n }}
      +
      {{ a.breachDate | date: "mediumDate" }}
      +
      {{ "breachReported" | i18n }}
      +
      {{ a.addedDate | date: "mediumDate" }}
      +
    diff --git a/apps/web/src/images/search.svg b/apps/web/src/images/search.svg new file mode 100644 index 00000000000..36e0ea4bd23 --- /dev/null +++ b/apps/web/src/images/search.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 85a7b8cb927..3d6cf0f23a5 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6306,6 +6306,21 @@ "sponsoredFamilies": { "message": "Free Bitwarden Families" }, + "sponsoredBitwardenFamilies": { + "message": "Sponsored families" + }, + "noSponsoredFamilies": { + "message": "No sponsored families" + }, + "noSponsoredFamiliesDescription": { + "message": "Sponsored non-member families plans will display here" + }, + "sponsorFreeBitwardenFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization." + }, + "sponsoredFamiliesRemoveActiveSponsorship": { + "message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization." + }, "sponsoredFamiliesEligible": { "message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work." }, @@ -6321,6 +6336,18 @@ "sponsoredFamiliesSharedCollections": { "message": "Shared collections for Family secrets" }, + "memberFamilies": { + "message": "Member families" + }, + "noMemberFamilies": { + "message": "No member families" + }, + "noMemberFamiliesDescription": { + "message": "Members who have redeemed family plans will display here" + }, + "membersWithSponsoredFamilies": { + "message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization." + }, "badToken": { "message": "The link is no longer valid. Please have the sponsor resend the offer." }, @@ -7984,6 +8011,9 @@ "inviteMember": { "message": "Invite member" }, + "addSponsorship": { + "message": "Add sponsorship" + }, "needsConfirmation": { "message": "Needs confirmation" }, diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 8a198663e06..4ca18b4985e 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -536,6 +536,10 @@ export class LoginComponent implements OnInit, OnDestroy { if (storedEmail) { this.formGroup.controls.email.setValue(storedEmail); this.formGroup.controls.rememberEmail.setValue(true); + // If we load an email into the form, we need to initialize it for the login process as well + // so that other login components can use it. + // We do this here as it's possible that a user doesn't edit the email field before submitting. + this.loginEmailService.setLoginEmail(storedEmail); } else { this.formGroup.controls.rememberEmail.setValue(false); } diff --git a/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts b/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts index 534afffd1bb..19e993487c2 100644 --- a/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts +++ b/libs/common/src/admin-console/models/request/organization/organization-sponsorship-create.request.ts @@ -6,4 +6,5 @@ export class OrganizationSponsorshipCreateRequest { sponsoredEmail: string; planSponsorshipType: PlanSponsorshipType; friendlyName: string; + notes?: string; } diff --git a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts index 40d8db03d3b..bfeecb4eb23 100644 --- a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts +++ b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts @@ -1,4 +1,4 @@ -import { PlanType } from "../../enums"; +import { PlanSponsorshipType, PlanType } from "../../enums"; export class PreviewOrganizationInvoiceRequest { organizationId?: string; @@ -21,6 +21,7 @@ export class PreviewOrganizationInvoiceRequest { class PasswordManager { plan: PlanType; + sponsoredPlan?: PlanSponsorshipType; seats: number; additionalStorage: number; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 8850c18c051..c3c4761fa05 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -46,7 +46,6 @@ export enum FeatureFlag { CriticalApps = "pm-14466-risk-insights-critical-application", EnableRiskInsightsNotifications = "enable-risk-insights-notifications", DesktopSendUIRefresh = "desktop-send-ui-refresh", - ExportAttachments = "export-attachments", /* Vault */ PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge", @@ -98,7 +97,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.CriticalApps]: FALSE, [FeatureFlag.EnableRiskInsightsNotifications]: FALSE, [FeatureFlag.DesktopSendUIRefresh]: FALSE, - [FeatureFlag.ExportAttachments]: FALSE, /* Vault */ [FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE, diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 69f77c6ca32..4e9b4175838 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -39,8 +39,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -184,10 +182,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { private onlyManagedCollections = true; private onGenerate$ = new Subject(); - private isExportAttachmentsEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.ExportAttachments, - ); - constructor( protected i18nService: I18nService, protected toastService: ToastService, @@ -202,7 +196,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { protected organizationService: OrganizationService, private accountService: AccountService, private collectionService: CollectionService, - private configService: ConfigService, ) {} async ngOnInit() { @@ -225,17 +218,14 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { ), ); - combineLatest([ - this.exportForm.controls.vaultSelector.valueChanges, - this.isExportAttachmentsEnabled$, - ]) + this.exportForm.controls.vaultSelector.valueChanges .pipe(takeUntil(this.destroy$)) - .subscribe(([value, isExportAttachmentsEnabled]) => { + .subscribe(([value]) => { this.organizationId = value !== "myVault" ? value : undefined; this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip"); this.exportForm.get("format").setValue("json"); - if (value === "myVault" && isExportAttachmentsEnabled) { + if (value === "myVault") { this.formatOptions.push({ name: ".zip (with attachments)", value: "zip" }); } }); diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index 52d70e8652a..50577472120 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { importProvidersFrom, signal } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; import { action } from "@storybook/addon-actions"; import { applicationConfig, @@ -225,6 +226,14 @@ export default { getFeatureFlag: () => Promise.resolve(false), }, }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + queryParams: {}, + }, + }, + }, ], }), componentWrapperDecorator( diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts index 58a9b8f3965..af39ea96c16 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts @@ -83,4 +83,24 @@ describe("AddEditCustomFieldDialogComponent", () => { expect.objectContaining({ value: FieldType.Linked }), ); }); + + it("does not filter out 'Hidden' field type when 'disallowHiddenField' is false", () => { + dialogData.disallowHiddenField = false; + fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent); + component = fixture.componentInstance; + + expect(component.fieldTypeOptions).toContainEqual( + expect.objectContaining({ value: FieldType.Hidden }), + ); + }); + + it("filers out 'Hidden' field type when 'disallowHiddenField' is true", () => { + dialogData.disallowHiddenField = true; + fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent); + component = fixture.componentInstance; + + expect(component.fieldTypeOptions).not.toContainEqual( + expect.objectContaining({ value: FieldType.Hidden }), + ); + }); }); diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts index bdf5345672d..72bdf5dca1a 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.ts @@ -25,6 +25,7 @@ export type AddEditCustomFieldDialogData = { cipherType: CipherType; /** When provided, dialog will display edit label variants */ editLabelConfig?: { index: number; label: string }; + disallowHiddenField?: boolean; }; @Component({ @@ -68,6 +69,9 @@ export class AddEditCustomFieldDialogComponent { this.variant = data.editLabelConfig ? "edit" : "add"; this.fieldTypeOptions = this.fieldTypeOptions.filter((option) => { + if (this.data.disallowHiddenField && option.value === FieldType.Hidden) { + return false; + } // Filter out the Linked field type for Secure Notes if (this.data.cipherType === CipherType.SecureNote) { return option.value !== FieldType.Linked; diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html index 3bce3c5f385..1305bcdae05 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -89,7 +89,7 @@ bitIconButton="bwi-pencil-square" class="tw-self-center tw-mt-2" data-testid="edit-custom-field-button" - *ngIf="!isPartialEdit" + *ngIf="canEdit(field.value.type)" >
    diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts index fb9664594ed..ced8763f895 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts @@ -45,7 +45,9 @@ describe("CustomFieldsComponent", () => { announce = jest.fn().mockResolvedValue(null); patchCipher = jest.fn(); originalCipherView = new CipherView(); - config = {} as CipherFormConfig; + config = { + collections: [], + } as CipherFormConfig; await TestBed.configureTestingModule({ imports: [CustomFieldsComponent], @@ -463,5 +465,91 @@ describe("CustomFieldsComponent", () => { // "reorder boolean label to position 4 of 4" expect(announce).toHaveBeenCalledWith("reorderFieldDown boolean label 4 4", "assertive"); }); + + it("hides reorder buttons when in partial edit mode", () => { + originalCipherView.fields = mockFieldViews; + config.mode = "partial-edit"; + + component.ngOnInit(); + fixture.detectChanges(); + + toggleItems = fixture.debugElement.queryAll( + By.css('button[data-testid="reorder-toggle-button"]'), + ); + + expect(toggleItems).toHaveLength(0); + }); + }); + + it("shows all reorders button when in edit mode and viewPassword is true", () => { + originalCipherView.fields = mockFieldViews; + originalCipherView.viewPassword = true; + config.mode = "edit"; + + component.ngOnInit(); + fixture.detectChanges(); + + const toggleItems = fixture.debugElement.queryAll( + By.css('button[data-testid="reorder-toggle-button"]'), + ); + expect(toggleItems).toHaveLength(4); + }); + + it("shows all reorder buttons except for hidden fields when in edit mode and viewPassword is false", () => { + originalCipherView.fields = mockFieldViews; + originalCipherView.viewPassword = false; + config.mode = "edit"; + + component.ngOnInit(); + fixture.detectChanges(); + + const toggleItems = fixture.debugElement.queryAll( + By.css('button[data-testid="reorder-toggle-button"]'), + ); + + expect(toggleItems).toHaveLength(3); + }); + + describe("edit button", () => { + it("hides the edit button when in partial-edit mode", () => { + originalCipherView.fields = mockFieldViews; + config.mode = "partial-edit"; + + component.ngOnInit(); + fixture.detectChanges(); + + const editButtons = fixture.debugElement.queryAll( + By.css('button[data-testid="edit-custom-field-button"]'), + ); + expect(editButtons).toHaveLength(0); + }); + + it("shows all the edit buttons when in edit mode and viewPassword is true", () => { + originalCipherView.fields = mockFieldViews; + originalCipherView.viewPassword = true; + config.mode = "edit"; + + component.ngOnInit(); + fixture.detectChanges(); + + const editButtons = fixture.debugElement.queryAll( + By.css('button[data-testid="edit-custom-field-button"]'), + ); + expect(editButtons).toHaveLength(4); + }); + + it("shows all the edit buttons except for hidden fields when in edit mode and viewPassword is false", () => { + originalCipherView.fields = mockFieldViews; + originalCipherView.viewPassword = false; + config.mode = "edit"; + + component.ngOnInit(); + fixture.detectChanges(); + + const editButtons = fixture.debugElement.queryAll( + By.css('button[data-testid="edit-custom-field-button"]'), + ); + expect(editButtons).toHaveLength(3); + }); }); }); diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts index dd3fd8c24a8..49e9e109b74 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts @@ -116,6 +116,8 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { /** Emits when a new custom field should be focused */ private focusOnNewInput$ = new Subject(); + disallowHiddenField?: boolean; + destroyed$: DestroyRef; FieldType = FieldType; @@ -141,6 +143,13 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { return this.customFieldsForm.controls.fields as FormArray; } + canEdit(type: FieldType): boolean { + return ( + !this.isPartialEdit && + (type !== FieldType.Hidden || this.cipherFormContainer.originalCipherView?.viewPassword) + ); + } + ngOnInit() { const linkedFieldsOptionsForCipher = this.getLinkedFieldsOptionsForCipher(); const optionsArray = Array.from(linkedFieldsOptionsForCipher?.entries() ?? []); @@ -210,6 +219,7 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { /** Opens the add/edit custom field dialog */ openAddEditCustomFieldDialog(editLabelConfig?: AddEditCustomFieldDialogData["editLabelConfig"]) { + const { cipherType, mode, originalCipher } = this.cipherFormContainer.config; this.dialogRef = this.dialogService.open( AddEditCustomFieldDialogComponent, { @@ -217,8 +227,9 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { addField: this.addField.bind(this), updateLabel: this.updateLabel.bind(this), removeField: this.removeField.bind(this), - cipherType: this.cipherFormContainer.config.cipherType, + cipherType, editLabelConfig, + disallowHiddenField: mode === "edit" && !originalCipher.viewPassword, }, }, ); diff --git a/libs/vault/src/icons/login-cards.ts b/libs/vault/src/icons/login-cards.ts index 01baf308412..6e066a0d924 100644 --- a/libs/vault/src/icons/login-cards.ts +++ b/libs/vault/src/icons/login-cards.ts @@ -2,10 +2,10 @@ import { svgIcon } from "@bitwarden/components"; export const LoginCards = svgIcon` - - - + + + - + `; diff --git a/libs/vault/src/icons/secure-devices.ts b/libs/vault/src/icons/secure-devices.ts index ee3a6ea6b90..4e123afad40 100644 --- a/libs/vault/src/icons/secure-devices.ts +++ b/libs/vault/src/icons/secure-devices.ts @@ -3,14 +3,14 @@ import { svgIcon } from "@bitwarden/components"; export const SecureDevices = svgIcon` - - - + + + - + - + `; diff --git a/libs/vault/src/icons/secure-user.ts b/libs/vault/src/icons/secure-user.ts index f8f126adbac..39d9957030c 100644 --- a/libs/vault/src/icons/secure-user.ts +++ b/libs/vault/src/icons/secure-user.ts @@ -2,8 +2,8 @@ import { svgIcon } from "@bitwarden/components"; export const SecureUser = svgIcon` - - + + diff --git a/libs/vault/src/icons/security-handshake.ts b/libs/vault/src/icons/security-handshake.ts index 5a598fd180d..d68f8a948d3 100644 --- a/libs/vault/src/icons/security-handshake.ts +++ b/libs/vault/src/icons/security-handshake.ts @@ -2,11 +2,11 @@ import { svgIcon } from "@bitwarden/components"; export const SecurityHandshake = svgIcon` - - - + + + - + `; diff --git a/package-lock.json b/package-lock.json index cb9baf4fafe..65f0c87721a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.133", + "@bitwarden/sdk-internal": "0.2.0-main.137", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", @@ -48,7 +48,7 @@ "jquery": "3.7.1", "jsdom": "26.0.0", "jszip": "3.10.1", - "koa": "2.15.4", + "koa": "2.16.1", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lit": "3.2.1", @@ -210,7 +210,7 @@ "inquirer": "8.2.6", "jsdom": "26.0.0", "jszip": "3.10.1", - "koa": "2.15.4", + "koa": "2.16.1", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", @@ -4700,9 +4700,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.133", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.133.tgz", - "integrity": "sha512-KzKJGf9cKlcQzfRmqkAwVGBN1kDpcRFkTMm7nrphZSrjfaWJWI1lBEJ0DhnkbMMHJXhQavGyoVk5TIn/Y8ylmw==", + "version": "0.2.0-main.137", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.137.tgz", + "integrity": "sha512-Df0pB5tOEc4WiMjskunTrqHulPzenFv8C61sqsBhHfy80xcf5kU5JyPd4asbf3e4uNS6QGXptd8imp09AuiFEA==", "license": "GPL-3.0" }, "node_modules/@bitwarden/send-ui": { @@ -24789,9 +24789,9 @@ } }, "node_modules/koa": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.4.tgz", - "integrity": "sha512-7fNBIdrU2PEgLljXoPWoyY4r1e+ToWCmzS/wwMPbUNs7X+5MMET1ObhJBlUkF5uZG9B6QhM2zS1TsH6adegkiQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", + "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==", "license": "MIT", "dependencies": { "accepts": "^1.3.5", diff --git a/package.json b/package.json index 28d243e1c32..c78decb9827 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.133", + "@bitwarden/sdk-internal": "0.2.0-main.137", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", @@ -180,7 +180,7 @@ "jquery": "3.7.1", "jsdom": "26.0.0", "jszip": "3.10.1", - "koa": "2.15.4", + "koa": "2.16.1", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lit": "3.2.1",