diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index ae4f2f37ba8..610769859fe 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -173,63 +173,63 @@ jobs: working-directory: browser-source/apps/browser - name: Upload Opera artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: dist-opera-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-opera.zip if-no-files-found: error - name: Upload Opera MV3 artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: DO-NOT-USE-FOR-PROD-dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-opera-mv3.zip if-no-files-found: error - name: Upload Chrome MV3 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-chrome-mv3.zip if-no-files-found: error - name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip if-no-files-found: error - name: Upload Firefox artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: dist-firefox-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-firefox.zip if-no-files-found: error - name: Upload Firefox MV3 artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: DO-NOT-USE-FOR-PROD-dist-firefox-MV3-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-firefox-mv3.zip if-no-files-found: error - name: Upload Edge artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: dist-edge-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-edge.zip if-no-files-found: error - name: Upload Edge MV3 artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: DO-NOT-USE-FOR-PROD-dist-edge-MV3-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-edge-mv3.zip if-no-files-found: error - name: Upload browser source - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: browser-source-${{ env._BUILD_NUMBER }}.zip path: browser-source.zip @@ -237,7 +237,7 @@ jobs: - name: Upload coverage artifact if: false - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: coverage-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/coverage/coverage-${{ env._BUILD_NUMBER }}.zip @@ -352,7 +352,7 @@ jobs: ls -la - name: Upload Safari artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: dist-safari-${{ env._BUILD_NUMBER }}.zip path: apps/browser/dist/dist-safari.zip @@ -382,7 +382,7 @@ jobs: secrets: "crowdin-api-token" - name: Upload Sources - uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0 + uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index fd864cf99a5..76d86b45500 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -130,14 +130,14 @@ jobs: matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt - name: Upload unix zip asset - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip if-no-files-found: error - name: Upload unix checksum asset - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt @@ -269,14 +269,14 @@ jobs: -t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt - name: Upload windows zip asset - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip if-no-files-found: error - name: Upload windows checksum asset - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt @@ -284,7 +284,7 @@ jobs: - name: Upload Chocolatey asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg @@ -295,7 +295,7 @@ jobs: - name: Upload NPM Build Directory asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip @@ -364,14 +364,14 @@ jobs: run: sudo snap remove bw - name: Upload snap asset - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload snap checksum asset - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index a4dcf698faa..e82b490f813 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -174,61 +174,62 @@ jobs: 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 }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi + working-directory: apps/desktop/desktop_native env: PKG_CONFIG_ALLOW_CROSS: true PKG_CONFIG_ALL_STATIC: true TARGET: musl run: | rustup target add x86_64-unknown-linux-musl - npm run build:cross-platform + node build.js cross-platform - name: Build application run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb if-no-files-found: error - name: Upload .rpm artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm if-no-files-found: error - name: Upload .freebsd artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd if-no-files-found: error - name: Upload .snap artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload .AppImage artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release_channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml @@ -301,13 +302,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build & Sign (dev) env: @@ -351,91 +354,91 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload installer exe artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload appx ia32 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx if-no-files-found: error - name: Upload store appx ia32 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx if-no-files-found: error - name: Upload NSIS ia32 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z if-no-files-found: error - name: Upload appx x64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx if-no-files-found: error - name: Upload store appx x64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx if-no-files-found: error - name: Upload NSIS x64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z if-no-files-found: error - name: Upload appx ARM64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx if-no-files-found: error - name: Upload store appx ARM64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx if-no-files-found: error - name: Upload NSIS ARM64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z if-no-files-found: error - name: Upload nupkg artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release_channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml @@ -584,13 +587,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build application (dev) run: npm run build @@ -748,13 +753,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -792,28 +799,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip if-no-files-found: error - name: Upload .dmg artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg if-no-files-found: error - name: Upload .dmg blockmap artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release_channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml @@ -965,13 +972,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -1009,7 +1018,7 @@ jobs: run: npm run pack:mac:mas - name: Upload .pkg artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg @@ -1168,13 +1177,15 @@ jobs: uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: - path: apps/desktop/desktop_native/napi/*.node + path: | + apps/desktop/desktop_native/napi/*.node + apps/desktop/desktop_native/dist/* key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native/napi - run: npm run build:cross-platform + working-directory: apps/desktop/desktop_native + run: node build.js cross-platform - name: Build if: steps.build-cache.outputs.cache-hit != 'true' @@ -1215,7 +1226,7 @@ jobs: zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app - name: Upload masdev artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip @@ -1248,7 +1259,7 @@ jobs: secrets: "crowdin-api-token" - name: Upload Sources - uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0 + uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index fbab45ddb72..d875078757c 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -130,7 +130,7 @@ jobs: run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build - name: Upload ${{ matrix.name }} artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: web-${{ env._VERSION }}-${{ matrix.name }}.zip path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip @@ -270,7 +270,7 @@ jobs: secrets: "crowdin-api-token" - name: Upload Sources - uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0 + uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 3f8bc45d51d..5a6a3d52361 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -159,42 +159,42 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb if-no-files-found: error - name: Upload .rpm artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm if-no-files-found: error - name: Upload .freebsd artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd if-no-files-found: error - name: Upload .snap artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload .AppImage artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release-channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml @@ -300,91 +300,91 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload installer exe artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload appx ia32 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx if-no-files-found: error - name: Upload store appx ia32 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx if-no-files-found: error - name: Upload NSIS ia32 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z if-no-files-found: error - name: Upload appx x64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx if-no-files-found: error - name: Upload store appx x64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx if-no-files-found: error - name: Upload NSIS x64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z if-no-files-found: error - name: Upload appx ARM64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx if-no-files-found: error - name: Upload store appx ARM64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx if-no-files-found: error - name: Upload NSIS ARM64 artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z if-no-files-found: error - name: Upload nupkg artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release-channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml @@ -708,28 +708,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip if-no-files-found: error - name: Upload .dmg artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg if-no-files-found: error - name: Upload .dmg blockmap artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release-channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml @@ -916,7 +916,7 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - name: Upload .pkg artifact - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index d90e009bf36..076bfb46e80 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -47,7 +47,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 + uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 with: sarif_file: cx_result.sarif @@ -67,7 +67,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with SonarCloud - uses: sonarsource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0 + uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/browser/config/base.json b/apps/browser/config/base.json index b6f24bf9ae3..6c428c43d26 100644 --- a/apps/browser/config/base.json +++ b/apps/browser/config/base.json @@ -2,7 +2,7 @@ "devFlags": {}, "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true, + "enableCipherKeyEncryption": false, "accountSwitching": false } } diff --git a/apps/browser/config/development.json b/apps/browser/config/development.json index 950c5372d8f..e0925ebecc9 100644 --- a/apps/browser/config/development.json +++ b/apps/browser/config/development.json @@ -7,7 +7,7 @@ }, "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true, + "enableCipherKeyEncryption": false, "accountSwitching": true } } diff --git a/apps/browser/config/production.json b/apps/browser/config/production.json index 64c6cb92a3b..027003f6c75 100644 --- a/apps/browser/config/production.json +++ b/apps/browser/config/production.json @@ -1,6 +1,6 @@ { "flags": { - "enableCipherKeyEncryption": true, + "enableCipherKeyEncryption": false, "accountSwitching": true } } diff --git a/apps/browser/package.json b/apps/browser/package.json index 433ddecd2ac..c5332a08016 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.8.1", + "version": "2024.8.2", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index bc2448d924e..3aa1ac097ce 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3477,7 +3477,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3489,6 +3489,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3498,9 +3501,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -4296,5 +4302,26 @@ }, "additionalContentAvailable": { "message": "Additional content is available" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 7a5b156a506..350b4a8a84d 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router"; -import { Subject, filter, firstValueFrom, switchMap, takeUntil, tap } from "rxjs"; +import { Subject, filter, switchMap, takeUntil, tap } from "rxjs"; import { AnonLayoutComponent, @@ -9,7 +9,6 @@ import { AnonLayoutWrapperDataService, } from "@bitwarden/auth/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { Icon, IconModule } from "@bitwarden/components"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; @@ -17,10 +16,7 @@ import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-heade import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { CurrentAccountComponent } from "../account-switching/current-account.component"; -import { - ExtensionBitwardenLogoPrimary, - ExtensionBitwardenLogoWhite, -} from "./extension-bitwarden-logo.icon"; +import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon"; export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData { showAcctSwitcher?: boolean; @@ -56,14 +52,13 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { protected maxWidth: "md" | "3xl"; protected theme: string; - protected logo: Icon; + protected logo = ExtensionBitwardenLogo; constructor( private router: Router, private route: ActivatedRoute, private i18nService: I18nService, private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService, - private themeStateService: ThemeStateService, ) {} async ngOnInit(): Promise { @@ -73,14 +68,6 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { // Listen for page changes and update the page data appropriately this.listenForPageDataChanges(); this.listenForServiceDataChanges(); - - this.theme = await firstValueFrom(this.themeStateService.selectedTheme$); - - if (this.theme === "dark") { - this.logo = ExtensionBitwardenLogoWhite; - } else { - this.logo = ExtensionBitwardenLogoPrimary; - } } private listenForPageDataChanges() { diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index 44060f991ff..beb07f3523a 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -22,8 +22,6 @@ import { } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ThemeType } from "@bitwarden/common/platform/enums"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, I18nMockService } from "@bitwarden/components"; @@ -47,7 +45,6 @@ const decorators = (options: { applicationVersion?: string; clientType?: ClientType; hostName?: string; - themeType?: ThemeType; }) => { return [ componentWrapperDecorator( @@ -120,12 +117,6 @@ const decorators = (options: { getClientType: () => options.clientType || ClientType.Web, } as Partial, }, - { - provide: ThemeStateService, - useValue: { - selectedTheme$: of(options.themeType || ThemeType.Light), - } as Partial, - }, { provide: I18nService, useFactory: () => { diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts index 569edaae978..51d748e1fbb 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts @@ -1,6 +1,6 @@ import { svgIcon } from "@bitwarden/components"; -export const ExtensionBitwardenLogoPrimary = svgIcon` +export const ExtensionBitwardenLogo = svgIcon` - -`; - -export const ExtensionBitwardenLogoWhite = svgIcon` - - `; diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index 505931ad0f1..cd9dfc3702b 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -41,7 +41,7 @@ export class HomeComponent implements OnInit, OnDestroy { ) {} async ngOnInit(): Promise { - const email = this.loginEmailService.getEmail(); + const email = await firstValueFrom(this.loginEmailService.loginEmail$); const rememberEmail = this.loginEmailService.getRememberEmail(); if (email != null) { @@ -93,7 +93,7 @@ export class HomeComponent implements OnInit, OnDestroy { async setLoginEmailValues() { // Note: Browser saves email settings here instead of the login component this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); - this.loginEmailService.setEmail(this.formGroup.value.email); + await this.loginEmailService.setLoginEmail(this.formGroup.value.email); await this.loginEmailService.saveEmailSettings(); } } diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html index ccc743d86d4..fb1b09de49c 100644 --- a/apps/browser/src/auth/popup/lock.component.html +++ b/apps/browser/src/auth/popup/lock.component.html @@ -94,7 +94,7 @@ {{ "awaitDesktop" | i18n }}

- + diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 6e73199969a..ea72fb61f5f 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -1,4 +1,4 @@ -import { Component, NgZone } from "@angular/core"; +import { Component, NgZone, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -31,7 +31,7 @@ import { flagEnabled } from "../../platform/flags"; selector: "app-login", templateUrl: "login.component.html", }) -export class LoginComponent extends BaseLoginComponent { +export class LoginComponent extends BaseLoginComponent implements OnInit { showPasswordless = false; constructor( devicesApiService: DevicesApiServiceAbstraction, @@ -83,13 +83,12 @@ export class LoginComponent extends BaseLoginComponent { }; super.successRoute = "/tabs/vault"; this.showPasswordless = flagEnabled("showPasswordless"); + } + async ngOnInit(): Promise { + await super.ngOnInit(); if (this.showPasswordless) { - this.formGroup.controls.email.setValue(this.loginEmailService.getEmail()); - this.formGroup.controls.rememberEmail.setValue(this.loginEmailService.getRememberEmail()); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.validateEmail(); + await this.validateEmail(); } } diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index a209523dc7c..0c626c68794 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -139,7 +139,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), unlockCompleted: ({ message }) => this.unlockCompleted(message), - doFullSync: () => this.updateOverlayCiphers(true), + doFullSync: () => this.updateOverlayCiphers(), addedCipher: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(), editedCipher: () => this.updateOverlayCiphers(), @@ -272,7 +272,10 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error)); } - if (!currentTab) { + if (!currentTab || !currentTab.url?.startsWith("http")) { + if (updateAllCipherTypes) { + this.cardAndIdentityCiphers = null; + } return; } diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts index d53d9e685ed..f5f8dd770c7 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts @@ -4,12 +4,21 @@ describe("FIDO2 page-script for manifest v2", () => { let createdScriptElement: HTMLScriptElement; jest.spyOn(window.document, "createElement"); + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { Object.defineProperty(window.document, "contentType", { value: "text/html", writable: true }); jest.clearAllMocks(); + jest.clearAllTimers(); jest.resetModules(); }); + afterAll(() => { + jest.useRealTimers(); + }); + it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => { Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true }); @@ -19,7 +28,7 @@ describe("FIDO2 page-script for manifest v2", () => { }); it("appends the `page-script.js` file to the document head when the contentType is `text/html`", () => { - jest.spyOn(window.document.head, "insertBefore").mockImplementation((node) => { + jest.spyOn(window.document.head, "prepend").mockImplementation((node) => { createdScriptElement = node as HTMLScriptElement; return node; }); @@ -28,16 +37,13 @@ describe("FIDO2 page-script for manifest v2", () => { expect(window.document.createElement).toHaveBeenCalledWith("script"); expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript); - expect(window.document.head.insertBefore).toHaveBeenCalledWith( - expect.any(HTMLScriptElement), - window.document.head.firstChild, - ); + expect(window.document.head.prepend).toHaveBeenCalledWith(expect.any(HTMLScriptElement)); expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); }); it("appends the `page-script.js` file to the document element if the head is not available", () => { window.document.documentElement.removeChild(window.document.head); - jest.spyOn(window.document.documentElement, "insertBefore").mockImplementation((node) => { + jest.spyOn(window.document.documentElement, "prepend").mockImplementation((node) => { createdScriptElement = node as HTMLScriptElement; return node; }); @@ -46,9 +52,8 @@ describe("FIDO2 page-script for manifest v2", () => { expect(window.document.createElement).toHaveBeenCalledWith("script"); expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript); - expect(window.document.documentElement.insertBefore).toHaveBeenCalledWith( + expect(window.document.documentElement.prepend).toHaveBeenCalledWith( expect.any(HTMLScriptElement), - window.document.documentElement.firstChild, ); expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); }); @@ -63,6 +68,7 @@ describe("FIDO2 page-script for manifest v2", () => { jest.spyOn(createdScriptElement, "remove"); createdScriptElement.dispatchEvent(new Event("load")); + jest.runAllTimers(); expect(createdScriptElement.remove).toHaveBeenCalled(); }); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts index 4e806d29908..e5280c088bc 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts @@ -2,18 +2,20 @@ * This script handles injection of the FIDO2 override page script into the document. * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. */ -import { Fido2ContentScript } from "../enums/fido2-content-script.enum"; - (function (globalContext) { if (globalContext.document.contentType !== "text/html") { return; } const script = globalContext.document.createElement("script"); - script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript); - script.addEventListener("load", () => script.remove()); + script.src = chrome.runtime.getURL("content/fido2-page-script.js"); + script.addEventListener("load", removeScriptOnLoad); const scriptInsertionPoint = globalContext.document.head || globalContext.document.documentElement; - scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild); + scriptInsertionPoint.prepend(script); + + function removeScriptOnLoad() { + globalThis.setTimeout(() => script?.remove(), 5000); + } })(globalThis); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts index 4afeb76a0d3..c75a37c1b65 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts @@ -2,26 +2,35 @@ * This script handles injection of the FIDO2 override page script into the document. * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. */ -import { Fido2ContentScript } from "../enums/fido2-content-script.enum"; - (function (globalContext) { if (globalContext.document.contentType !== "text/html") { return; } - if (globalContext.document.readyState === "complete") { - loadScript(); + const script = globalContext.document.createElement("script"); + script.src = chrome.runtime.getURL("content/fido2-page-script.js"); + script.addEventListener("load", removeScriptOnLoad); + + // We are ensuring that the script injection is delayed in the event that we are loading + // within an iframe element. This prevents an issue with web mail clients that load content + // using ajax within iframes. In particular, Zimbra web mail client was observed to have this issue. + // @see https://github.com/bitwarden/clients/issues/9618 + const delayScriptInjection = + globalContext.window.top !== globalContext.window && + globalContext.document.readyState !== "complete"; + if (delayScriptInjection) { + globalContext.document.addEventListener("DOMContentLoaded", injectScript); } else { - globalContext.addEventListener("DOMContentLoaded", loadScript); + injectScript(); } - function loadScript() { - const script = globalContext.document.createElement("script"); - script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript); - script.addEventListener("load", () => script.remove()); - + function injectScript() { const scriptInsertionPoint = globalContext.document.head || globalContext.document.documentElement; - scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild); + scriptInsertionPoint.prepend(script); + } + + function removeScriptOnLoad() { + globalThis.setTimeout(() => script?.remove(), 5000); } })(globalThis); diff --git a/apps/browser/src/autofill/fido2/content/messaging/message.ts b/apps/browser/src/autofill/fido2/content/messaging/message.ts index d42c10a5d88..5815be9eb60 100644 --- a/apps/browser/src/autofill/fido2/content/messaging/message.ts +++ b/apps/browser/src/autofill/fido2/content/messaging/message.ts @@ -18,7 +18,7 @@ export enum MessageType { } /** - * The params provided by the page-script are created in an insecure environemnt and + * The params provided by the page-script are created in an insecure environment and * should not be trusted. This type is used to ensure that the content-script does not * trust the `origin` or `sameOriginWithAncestors` params. */ @@ -38,7 +38,7 @@ export type CredentialCreationResponse = { }; /** - * The params provided by the page-script are created in an insecure environemnt and + * The params provided by the page-script are created in an insecure environment and * should not be trusted. This type is used to ensure that the content-script does not * trust the `origin` or `sameOriginWithAncestors` params. */ diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html new file mode 100644 index 00000000000..852fd4a0e81 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html @@ -0,0 +1,36 @@ +
+
+ +
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts new file mode 100644 index 00000000000..d9d492bdcc1 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts @@ -0,0 +1,39 @@ +import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; + +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +@Component({ + selector: "app-fido2-cipher-row-v1", + templateUrl: "fido2-cipher-row-v1.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Fido2CipherRowV1Component { + @Output() onSelected = new EventEmitter(); + @Input() cipher: CipherView; + @Input() last: boolean; + @Input() title: string; + @Input() isSearching: boolean; + @Input() isSelected: boolean; + + protected selectCipher(c: CipherView) { + this.onSelected.emit(c); + } + + /** + * Returns a subname for the cipher. + * If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId. + * @param c Cipher + * @returns + */ + protected getSubName(c: CipherView): string | null { + const fido2Credentials = c.login?.fido2Credentials; + + if (!fido2Credentials || fido2Credentials.length === 0) { + return null; + } + + const [fido2Credential] = fido2Credentials; + + return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null; + } +} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html index 852fd4a0e81..0328a91bff5 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.html @@ -1,36 +1,21 @@ -
-
- -
-
+ + + diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts index 25d623b1692..91bcd6494e6 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row.component.ts @@ -1,19 +1,40 @@ +import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + BadgeModule, + ButtonModule, + IconButtonModule, + ItemModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, +} from "@bitwarden/components"; @Component({ selector: "app-fido2-cipher-row", templateUrl: "fido2-cipher-row.component.html", changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + BadgeModule, + ButtonModule, + CommonModule, + IconButtonModule, + ItemModule, + JslibModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + ], }) export class Fido2CipherRowComponent { @Output() onSelected = new EventEmitter(); @Input() cipher: CipherView; @Input() last: boolean; @Input() title: string; - @Input() isSearching: boolean; - @Input() isSelected: boolean; protected selectCipher(c: CipherView) { this.onSelected.emit(c); diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html new file mode 100644 index 00000000000..9f6c0aca50d --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html @@ -0,0 +1,52 @@ + + + + +
+ +
+
+ +
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts new file mode 100644 index 00000000000..cf79dfc6520 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts @@ -0,0 +1,113 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; +import { ConnectedPosition } from "@angular/cdk/overlay"; +import { Component } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data"; +import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service"; + +@Component({ + selector: "app-fido2-use-browser-link-v1", + templateUrl: "fido2-use-browser-link-v1.component.html", + animations: [ + trigger("transformPanel", [ + state( + "void", + style({ + opacity: 0, + }), + ), + transition( + "void => open", + animate( + "100ms linear", + style({ + opacity: 1, + }), + ), + ), + transition("* => void", animate("100ms linear", style({ opacity: 0 }))), + ]), + ], +}) +export class Fido2UseBrowserLinkV1Component { + showOverlay = false; + isOpen = false; + overlayPosition: ConnectedPosition[] = [ + { + originX: "start", + originY: "bottom", + overlayX: "start", + overlayY: "top", + offsetY: 5, + }, + ]; + + protected fido2PopoutSessionData$ = fido2PopoutSessionData$(); + + constructor( + private domainSettingsService: DomainSettingsService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + ) {} + + toggle() { + this.isOpen = !this.isOpen; + } + + close() { + this.isOpen = false; + } + + /** + * Aborts the current FIDO2 session and fallsback to the browser. + * @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts. + */ + protected async abort(excludeDomain = true) { + this.close(); + const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); + + if (!excludeDomain) { + this.abortSession(sessionData.sessionId); + return; + } + // Show overlay to prevent the user from interacting with the page. + this.showOverlay = true; + await this.handleDomainExclusion(sessionData.senderUrl); + // Give the user a chance to see the toast before closing the popout. + await Utils.delay(2000); + this.abortSession(sessionData.sessionId); + } + + /** + * Excludes the domain from future FIDO2 prompts. + * @param uri - The domain uri to exclude from future FIDO2 prompts. + */ + private async handleDomainExclusion(uri: string) { + const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); + + const validDomain = Utils.getHostname(uri); + const savedDomains: NeverDomains = { + ...existingDomains, + }; + savedDomains[validDomain] = null; + + await this.domainSettingsService.setNeverDomains(savedDomains); + + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("domainAddedToExcludedDomains", validDomain), + ); + } + + private abortSession(sessionId: string) { + BrowserFido2UserInterfaceSession.abortPopout(sessionId, true); + } +} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts index d9a7c7c9cbc..86f13d29c7a 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link.component.ts @@ -1,8 +1,11 @@ import { animate, state, style, transition, trigger } from "@angular/animations"; -import { ConnectedPosition } from "@angular/cdk/overlay"; +import { A11yModule } from "@angular/cdk/a11y"; +import { ConnectedPosition, CdkOverlayOrigin, CdkConnectedOverlay } from "@angular/cdk/overlay"; +import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -15,6 +18,8 @@ import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-f @Component({ selector: "app-fido2-use-browser-link", templateUrl: "fido2-use-browser-link.component.html", + standalone: true, + imports: [A11yModule, CdkConnectedOverlay, CdkOverlayOrigin, CommonModule, JslibModule], animations: [ trigger("transformPanel", [ state( @@ -90,11 +95,11 @@ export class Fido2UseBrowserLinkComponent { * @param uri - The domain uri to exclude from future FIDO2 prompts. */ private async handleDomainExclusion(uri: string) { - const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); + const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); const validDomain = Utils.getHostname(uri); const savedDomains: NeverDomains = { - ...exisitingDomains, + ...existingDomains, }; savedDomains[validDomain] = null; diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html new file mode 100644 index 00000000000..8a052fbc5b7 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html @@ -0,0 +1,142 @@ + +
+
+
+ + + + + + +
+ + +
+ +
+
+
+ + + +
+

+ {{ subtitleText | i18n }} +

+ + +
+
+ +
+
+ +
+ +
+
+ + +
+ +
+
+
+
+ +
+

{{ "passkeyAlreadyExists" | i18n }}

+
+
+ +
+
+ +
+
+ +
+

{{ "noPasskeysFoundForThisApplication" | i18n }}

+
+ +
+
+ + +
+
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts new file mode 100644 index 00000000000..d6026a8c7a0 --- /dev/null +++ b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts @@ -0,0 +1,443 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { + BehaviorSubject, + combineLatest, + concatMap, + filter, + firstValueFrom, + map, + Observable, + Subject, + take, + takeUntil, +} from "rxjs"; + +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; +import { DialogService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; +import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; +import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; +import { + BrowserFido2Message, + BrowserFido2UserInterfaceSession, + BrowserFido2MessageTypes, +} from "../../fido2/services/browser-fido2-user-interface.service"; + +interface ViewData { + message: BrowserFido2Message; + fallbackSupported: boolean; +} + +@Component({ + selector: "app-fido2-v1", + templateUrl: "fido2-v1.component.html", + styleUrls: [], +}) +export class Fido2V1Component implements OnInit, OnDestroy { + private destroy$ = new Subject(); + private hasSearched = false; + + protected cipher: CipherView; + protected searchTypeSearch = false; + protected searchPending = false; + protected searchText: string; + protected url: string; + protected hostname: string; + protected data$: Observable; + protected sessionId?: string; + protected senderTabId?: string; + protected ciphers?: CipherView[] = []; + protected displayedCiphers?: CipherView[] = []; + protected loading = false; + protected subtitleText: string; + protected credentialText: string; + protected BrowserFido2MessageTypes = BrowserFido2MessageTypes; + + private message$ = new BehaviorSubject(null); + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute, + private cipherService: CipherService, + private platformUtilsService: PlatformUtilsService, + private domainSettingsService: DomainSettingsService, + private searchService: SearchService, + private logService: LogService, + private dialogService: DialogService, + private browserMessagingApi: ZonedMessageListenerService, + private passwordRepromptService: PasswordRepromptService, + private fido2UserVerificationService: Fido2UserVerificationService, + private accountService: AccountService, + ) {} + + ngOnInit() { + this.searchTypeSearch = !this.platformUtilsService.isSafari(); + + const queryParams$ = this.activatedRoute.queryParamMap.pipe( + take(1), + map((queryParamMap) => ({ + sessionId: queryParamMap.get("sessionId"), + senderTabId: queryParamMap.get("senderTabId"), + senderUrl: queryParamMap.get("senderUrl"), + })), + ); + + combineLatest([ + queryParams$, + this.browserMessagingApi.messageListener$() as Observable, + ]) + .pipe( + concatMap(async ([queryParams, message]) => { + this.sessionId = queryParams.sessionId; + this.senderTabId = queryParams.senderTabId; + this.url = queryParams.senderUrl; + // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session. + if ( + message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest && + message.sessionId !== queryParams.sessionId + ) { + this.abort(false); + return; + } + + // Ignore messages that don't belong to the current session. + if (message.sessionId !== queryParams.sessionId) { + return; + } + + if (message.type === BrowserFido2MessageTypes.AbortRequest) { + this.abort(false); + return; + } + + return message; + }), + filter((message) => !!message), + takeUntil(this.destroy$), + ) + .subscribe((message) => { + this.message$.next(message); + }); + + this.data$ = this.message$.pipe( + filter((message) => message != undefined), + concatMap(async (message) => { + switch (message.type) { + case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: { + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(this.url), + ); + + this.ciphers = (await this.cipherService.getAllDecrypted()).filter( + (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, + ); + this.displayedCiphers = this.ciphers.filter( + (cipher) => + cipher.login.matchesUri(this.url, equivalentDomains) && + this.hasNoOtherPasskeys(cipher, message.userHandle), + ); + + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + + case BrowserFido2MessageTypes.PickCredentialRequest: { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.ciphers = await Promise.all( + message.cipherIds.map(async (cipherId) => { + const cipher = await this.cipherService.get(cipherId); + return cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + }), + ); + this.displayedCiphers = [...this.ciphers]; + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + + case BrowserFido2MessageTypes.InformExcludedCredentialRequest: { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.ciphers = await Promise.all( + message.existingCipherIds.map(async (cipherId) => { + const cipher = await this.cipherService.get(cipherId); + return cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + }), + ); + this.displayedCiphers = [...this.ciphers]; + + if (this.displayedCiphers.length > 0) { + this.selectedPasskey(this.displayedCiphers[0]); + } + break; + } + } + + this.subtitleText = + this.displayedCiphers.length > 0 + ? this.getCredentialSubTitleText(message.type) + : "noMatchingPasskeyLogin"; + + this.credentialText = this.getCredentialButtonText(message.type); + return { + message, + fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, + }; + }), + takeUntil(this.destroy$), + ); + + queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => { + this.send({ + sessionId: queryParams.sessionId, + type: BrowserFido2MessageTypes.ConnectResponse, + }); + }); + } + + protected async submit() { + const data = this.message$.value; + if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) { + // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. + // PM-4577 - https://github.com/bitwarden/clients/pull/8746 + const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); + + this.send({ + sessionId: this.sessionId, + cipherId: this.cipher.id, + type: BrowserFido2MessageTypes.PickCredentialResponse, + userVerified, + }); + } else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + if (this.cipher.login.hasFido2Credentials) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "overwritePasskey" }, + content: { key: "overwritePasskeyAlert" }, + type: "info", + }); + + if (!confirmed) { + return false; + } + } + + // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. + // PM-4577 - https://github.com/bitwarden/clients/pull/8746 + const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); + + this.send({ + sessionId: this.sessionId, + cipherId: this.cipher.id, + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, + userVerified, + }); + } + + this.loading = true; + } + + protected async saveNewLogin() { + const data = this.message$.value; + if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + const name = data.credentialName || data.rpId; + // TODO: Revert to check for user verification once user verification for passkeys is approved for production. + // PM-4577 - https://github.com/bitwarden/clients/pull/8746 + await this.createNewCipher(name, data.userName); + + // We are bypassing user verification pending approval. + this.send({ + sessionId: this.sessionId, + cipherId: this.cipher?.id, + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, + userVerified: data.userVerification, + }); + } + + this.loading = true; + } + + getCredentialSubTitleText(messageType: string): string { + return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest + ? "chooseCipherForPasskeySave" + : "logInWithPasskeyQuestion"; + } + + getCredentialButtonText(messageType: string): string { + return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest + ? "savePasskey" + : "confirm"; + } + + selectedPasskey(item: CipherView) { + this.cipher = item; + } + + viewPasskey() { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["/view-cipher"], { + queryParams: { + cipherId: this.cipher.id, + uilocation: "popout", + senderTabId: this.senderTabId, + sessionId: this.sessionId, + singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, + }, + }); + } + + addCipher() { + const data = this.message$.value; + + if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + return; + } + + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["/add-cipher"], { + queryParams: { + name: data.credentialName || data.rpId, + uri: this.url, + type: CipherType.Login.toString(), + uilocation: "popout", + username: data.userName, + senderTabId: this.senderTabId, + sessionId: this.sessionId, + userVerification: data.userVerification, + singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, + }, + }); + } + + protected async search() { + this.hasSearched = await this.searchService.isSearchable(this.searchText); + this.searchPending = true; + if (this.hasSearched) { + this.displayedCiphers = await this.searchService.searchCiphers( + this.searchText, + null, + this.ciphers, + ); + } else { + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(this.url), + ); + this.displayedCiphers = this.ciphers.filter((cipher) => + cipher.login.matchesUri(this.url, equivalentDomains), + ); + } + this.searchPending = false; + this.selectedPasskey(this.displayedCiphers[0]); + } + + abort(fallback: boolean) { + this.unload(fallback); + window.close(); + } + + unload(fallback = false) { + this.send({ + sessionId: this.sessionId, + type: BrowserFido2MessageTypes.AbortResponse, + fallbackRequested: fallback, + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + private buildCipher(name: string, username: string) { + this.cipher = new CipherView(); + this.cipher.name = name; + + this.cipher.type = CipherType.Login; + this.cipher.login = new LoginView(); + this.cipher.login.username = username; + this.cipher.login.uris = [new LoginUriView()]; + this.cipher.login.uris[0].uri = this.url; + this.cipher.card = new CardView(); + this.cipher.identity = new IdentityView(); + this.cipher.secureNote = new SecureNoteView(); + this.cipher.secureNote.type = SecureNoteType.Generic; + this.cipher.reprompt = CipherRepromptType.None; + } + + private async createNewCipher(name: string, username: string) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + this.buildCipher(name, username); + const cipher = await this.cipherService.encrypt(this.cipher, activeUserId); + try { + await this.cipherService.createWithServer(cipher); + this.cipher.id = cipher.id; + } catch (e) { + this.logService.error(e); + } + } + + // TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production. + private async handleUserVerification( + userVerificationRequested: boolean, + cipher: CipherView, + ): Promise { + const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0; + + if (masterPasswordRepromptRequired) { + return await this.passwordRepromptService.showPasswordPrompt(); + } + + return userVerificationRequested; + } + + private send(msg: BrowserFido2Message) { + BrowserFido2UserInterfaceSession.sendMessage({ + sessionId: this.sessionId, + ...msg, + }); + } + + /** + * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle + * @param userHandle + */ + private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { + if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { + return true; + } + + return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); + } +} diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.html b/apps/browser/src/autofill/popup/fido2/fido2.component.html index 9036d6d991c..00cd55d31b5 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.html +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.html @@ -1,136 +1,134 @@ - -
-
-
- - - + diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index 8bd667c17fb..c389e9ad5b8 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -1,4 +1,6 @@ +import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { BehaviorSubject, @@ -13,13 +15,14 @@ import { takeUntil, } from "rxjs"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -27,17 +30,39 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view" import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; -import { DialogService } from "@bitwarden/components"; +import { + ButtonModule, + DialogService, + Icons, + ItemModule, + NoItemsModule, + SearchModule, + SectionComponent, + SectionHeaderComponent, +} from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; import { BrowserFido2Message, BrowserFido2UserInterfaceSession, + BrowserFido2MessageTypes, } from "../../fido2/services/browser-fido2-user-interface.service"; +import { Fido2CipherRowComponent } from "./fido2-cipher-row.component"; +import { Fido2UseBrowserLinkComponent } from "./fido2-use-browser-link.component"; + +const PasskeyActions = { + Register: "register", + Authenticate: "authenticate", +} as const; + +type PasskeyActionValue = (typeof PasskeyActions)[keyof typeof PasskeyActions]; + interface ViewData { message: BrowserFido2Message; fallbackSupported: boolean; @@ -46,28 +71,45 @@ interface ViewData { @Component({ selector: "app-fido2", templateUrl: "fido2.component.html", - styleUrls: [], + standalone: true, + imports: [ + ButtonModule, + CommonModule, + Fido2CipherRowComponent, + Fido2UseBrowserLinkComponent, + FormsModule, + ItemModule, + JslibModule, + NoItemsModule, + PopupHeaderComponent, + PopupPageComponent, + SearchModule, + SectionComponent, + SectionHeaderComponent, + ], }) export class Fido2Component implements OnInit, OnDestroy { private destroy$ = new Subject(); - private hasSearched = false; - - protected cipher: CipherView; - protected searchTypeSearch = false; - protected searchPending = false; - protected searchText: string; - protected url: string; - protected hostname: string; - protected data$: Observable; - protected sessionId?: string; - protected senderTabId?: string; - protected ciphers?: CipherView[] = []; - protected displayedCiphers?: CipherView[] = []; - protected loading = false; - protected subtitleText: string; - protected credentialText: string; - private message$ = new BehaviorSubject(null); + private hasSearched = false; + protected BrowserFido2MessageTypes = BrowserFido2MessageTypes; + protected cipher: CipherView; + protected ciphers?: CipherView[] = []; + protected data$: Observable; + protected displayedCiphers?: CipherView[] = []; + protected equivalentDomains: Set; + protected equivalentDomainsURL: string; + protected hostname: string; + protected loading = false; + protected noResultsIcon = Icons.NoResults; + protected passkeyAction: PasskeyActionValue = PasskeyActions.Register; + protected PasskeyActions = PasskeyActions; + protected searchText: string; + protected searchTypeSearch = false; + protected senderTabId?: string; + protected sessionId?: string; + protected showNewPasskeyButton: boolean = false; + protected url: string; constructor( private router: Router, @@ -80,8 +122,8 @@ export class Fido2Component implements OnInit, OnDestroy { private dialogService: DialogService, private browserMessagingApi: ZonedMessageListenerService, private passwordRepromptService: PasswordRepromptService, - private fido2UserVerificationService: Fido2UserVerificationService, private accountService: AccountService, + private fido2UserVerificationService: Fido2UserVerificationService, ) {} ngOnInit() { @@ -107,7 +149,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.url = queryParams.senderUrl; // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session. if ( - message.type === "NewSessionCreatedRequest" && + message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest && message.sessionId !== queryParams.sessionId ) { this.abort(false); @@ -119,7 +161,7 @@ export class Fido2Component implements OnInit, OnDestroy { return; } - if (message.type === "AbortRequest") { + if (message.type === BrowserFido2MessageTypes.AbortRequest) { this.abort(false); return; } @@ -137,7 +179,7 @@ export class Fido2Component implements OnInit, OnDestroy { filter((message) => message != undefined), concatMap(async (message) => { switch (message.type) { - case "ConfirmNewCredentialRequest": { + case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: { const equivalentDomains = await firstValueFrom( this.domainSettingsService.getUrlEquivalentDomains(this.url), ); @@ -145,19 +187,22 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = (await this.cipherService.getAllDecrypted()).filter( (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, ); + this.displayedCiphers = this.ciphers.filter( (cipher) => cipher.login.matchesUri(this.url, equivalentDomains) && - this.hasNoOtherPasskeys(cipher, message.userHandle), + this.cipherHasNoOtherPasskeys(cipher, message.userHandle), ); - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } + this.passkeyAction = PasskeyActions.Register; + + // @TODO fix new cipher creation for other fido2 registration message types and remove `showNewPasskeyButton` from the template + this.showNewPasskeyButton = true; + break; } - case "PickCredentialRequest": { + case BrowserFido2MessageTypes.PickCredentialRequest: { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); @@ -170,14 +215,15 @@ export class Fido2Component implements OnInit, OnDestroy { ); }), ); + this.displayedCiphers = [...this.ciphers]; - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } + + this.passkeyAction = PasskeyActions.Authenticate; + break; } - case "InformExcludedCredentialRequest": { + case BrowserFido2MessageTypes.InformExcludedCredentialRequest: { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); @@ -190,40 +236,42 @@ export class Fido2Component implements OnInit, OnDestroy { ); }), ); + this.displayedCiphers = [...this.ciphers]; - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } + this.passkeyAction = PasskeyActions.Register; + + break; + } + + case BrowserFido2MessageTypes.InformCredentialNotFoundRequest: { + this.passkeyAction = PasskeyActions.Authenticate; + break; } } - this.subtitleText = - this.displayedCiphers.length > 0 - ? this.getCredentialSubTitleText(message.type) - : "noMatchingPasskeyLogin"; - - this.credentialText = this.getCredentialButtonText(message.type); return { message, fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, }; }), + takeUntil(this.destroy$), ); queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => { this.send({ sessionId: queryParams.sessionId, - type: "ConnectResponse", + type: BrowserFido2MessageTypes.ConnectResponse, }); }); } protected async submit() { const data = this.message$.value; - if (data?.type === "PickCredentialRequest") { + + if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) { // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. // PM-4577 - https://github.com/bitwarden/clients/pull/8746 const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); @@ -231,10 +279,10 @@ export class Fido2Component implements OnInit, OnDestroy { this.send({ sessionId: this.sessionId, cipherId: this.cipher.id, - type: "PickCredentialResponse", + type: BrowserFido2MessageTypes.PickCredentialResponse, userVerified, }); - } else if (data?.type === "ConfirmNewCredentialRequest") { + } else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { if (this.cipher.login.hasFido2Credentials) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "overwritePasskey" }, @@ -254,7 +302,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.send({ sessionId: this.sessionId, cipherId: this.cipher.id, - type: "ConfirmNewCredentialResponse", + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, userVerified, }); } @@ -264,7 +312,8 @@ export class Fido2Component implements OnInit, OnDestroy { protected async saveNewLogin() { const data = this.message$.value; - if (data?.type === "ConfirmNewCredentialRequest") { + + if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { const name = data.credentialName || data.rpId; // TODO: Revert to check for user verification once user verification for passkeys is approved for production. // PM-4577 - https://github.com/bitwarden/clients/pull/8746 @@ -274,7 +323,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.send({ sessionId: this.sessionId, cipherId: this.cipher?.id, - type: "ConfirmNewCredentialResponse", + type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, userVerified: data.userVerification, }); } @@ -282,59 +331,47 @@ export class Fido2Component implements OnInit, OnDestroy { this.loading = true; } - getCredentialSubTitleText(messageType: string): string { - return messageType == "ConfirmNewCredentialRequest" ? "choosePasskey" : "logInWithPasskey"; - } - - getCredentialButtonText(messageType: string): string { - return messageType == "ConfirmNewCredentialRequest" ? "savePasskey" : "confirm"; - } - - selectedPasskey(item: CipherView) { + async handleCipherItemSelect(item: CipherView) { this.cipher = item; + + await this.submit(); } - viewPasskey() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { - queryParams: { - cipherId: this.cipher.id, - uilocation: "popout", - senderTabId: this.senderTabId, - sessionId: this.sessionId, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); - } - - addCipher() { + async addCipher() { const data = this.message$.value; - if (data?.type !== "ConfirmNewCredentialRequest") { - return; + if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { + await this.router.navigate(["/add-cipher"], { + queryParams: { + type: CipherType.Login.toString(), + name: data.credentialName || data.rpId, + uri: this.url, + uilocation: "popout", + username: data.userName, + senderTabId: this.senderTabId, + sessionId: this.sessionId, + userVerification: data.userVerification, + singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, + }, + }); } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/add-cipher"], { - queryParams: { - name: data.credentialName || data.rpId, - uri: this.url, - type: CipherType.Login.toString(), - uilocation: "popout", - username: data.userName, - senderTabId: this.senderTabId, - sessionId: this.sessionId, - userVerification: data.userVerification, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); + return; + } + + async getEquivalentDomains() { + if (this.equivalentDomainsURL !== this.url) { + this.equivalentDomainsURL = this.url; + this.equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(this.url), + ); + } + + return this.equivalentDomains; } protected async search() { this.hasSearched = await this.searchService.isSearchable(this.searchText); - this.searchPending = true; if (this.hasSearched) { this.displayedCiphers = await this.searchService.searchCiphers( this.searchText, @@ -342,15 +379,11 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers, ); } else { - const equivalentDomains = await firstValueFrom( - this.domainSettingsService.getUrlEquivalentDomains(this.url), - ); + const equivalentDomains = await this.getEquivalentDomains(); this.displayedCiphers = this.ciphers.filter((cipher) => cipher.login.matchesUri(this.url, equivalentDomains), ); } - this.searchPending = false; - this.selectedPasskey(this.displayedCiphers[0]); } abort(fallback: boolean) { @@ -361,7 +394,7 @@ export class Fido2Component implements OnInit, OnDestroy { unload(fallback = false) { this.send({ sessionId: this.sessionId, - type: "AbortResponse", + type: BrowserFido2MessageTypes.AbortResponse, fallbackRequested: fallback, }); } @@ -427,13 +460,11 @@ export class Fido2Component implements OnInit, OnDestroy { * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle * @param userHandle */ - private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { + private cipherHasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { return true; } - return cipher.login.fido2Credentials.some((passkey) => { - passkey.userHandle === userHandle; - }); + return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); } } diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 6f953e68b93..1002ca99225 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -29,6 +29,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; +import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils"; import { BrowserApi } from "../../platform/browser/browser-api"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; @@ -1095,7 +1096,7 @@ export default class AutofillService implements AutofillServiceInterface { fillFields.expYear.maxLength === 4 ) { if (expYear.length === 2) { - expYear = "20" + expYear; + expYear = normalizeExpiryYearFormat(expYear); } } else if ( this.fieldAttrsContain(fillFields.expYear, "yy") || @@ -1121,7 +1122,7 @@ export default class AutofillService implements AutofillServiceInterface { let partYear: string = null; if (fullYear.length === 2) { partYear = fullYear; - fullYear = "20" + fullYear; + fullYear = normalizeExpiryYearFormat(fullYear); } else if (fullYear.length === 4) { partYear = fullYear.substr(2, 2); } diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index f27e2faf3fa..089c48cd230 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -122,29 +122,9 @@ export class InlineMenuFieldQualificationService ...this.identityAddressAutoCompleteValues, ...this.identityCountryAutocompleteValues, ...this.identityPhoneNumberAutocompleteValues, + this.identityCompanyAutocompleteValue, this.identityPostalCodeAutocompleteValue, ]); - private identityFieldKeywords = [ - ...new Set([ - ...IdentityAutoFillConstants.TitleFieldNames, - ...IdentityAutoFillConstants.FullNameFieldNames, - ...IdentityAutoFillConstants.FirstnameFieldNames, - ...IdentityAutoFillConstants.MiddlenameFieldNames, - ...IdentityAutoFillConstants.LastnameFieldNames, - ...IdentityAutoFillConstants.AddressFieldNames, - ...IdentityAutoFillConstants.Address1FieldNames, - ...IdentityAutoFillConstants.Address2FieldNames, - ...IdentityAutoFillConstants.Address3FieldNames, - ...IdentityAutoFillConstants.PostalCodeFieldNames, - ...IdentityAutoFillConstants.CityFieldNames, - ...IdentityAutoFillConstants.StateFieldNames, - ...IdentityAutoFillConstants.CountryFieldNames, - ...IdentityAutoFillConstants.CompanyFieldNames, - ...IdentityAutoFillConstants.PhoneFieldNames, - ...IdentityAutoFillConstants.EmailFieldNames, - ...IdentityAutoFillConstants.UserNameFieldNames, - ]), - ]; private inlineMenuFieldQualificationFlagSet = false; constructor() { @@ -288,14 +268,7 @@ export class InlineMenuFieldQualificationService return false; } - if (this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)) { - return true; - } - - return ( - !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues) && - this.keywordsFoundInFieldData(field, this.identityFieldKeywords, false) - ); + return this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues); } /** diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index cf393e0a44c..2c48a8563e7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -401,6 +401,8 @@ export default class MainBackground { const logoutCallback = async (logoutReason: LogoutReason, userId?: UserId) => await this.logout(logoutReason, userId); + const runtimeNativeMessagingBackground = () => this.nativeMessagingBackground; + const refreshAccessTokenErrorCallback = () => { // Send toast to popup this.messagingService.send("showToast", { @@ -616,7 +618,9 @@ export default class MainBackground { this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); - this.biometricsService = new BackgroundBrowserBiometricsService(this.nativeMessagingBackground); + this.biometricsService = new BackgroundBrowserBiometricsService( + runtimeNativeMessagingBackground, + ); this.kdfConfigService = new KdfConfigService(this.stateProvider); @@ -648,7 +652,7 @@ export default class MainBackground { this.kdfConfigService, ); - this.appIdService = new AppIdService(this.globalStateProvider); + this.appIdService = new AppIdService(this.storageService, this.logService); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.organizationService = new OrganizationService(this.stateProvider); @@ -1263,6 +1267,18 @@ export default class MainBackground { ); } + // If the user is logged out, switch to the next account + const active = await firstValueFrom(this.accountService.activeAccount$); + if (active != null) { + const authStatus = await firstValueFrom( + this.authService.authStatuses$.pipe(map((statuses) => statuses[active.id])), + ); + if (authStatus === AuthenticationStatus.LoggedOut) { + const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$); + await this.switchAccount(nextUpAccount?.id); + } + } + await this.initOverlayAndTabsBackground(); return new Promise((resolve) => { diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 121897b0cef..5792c10c053 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.8.1", + "version": "2024.8.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 52e21a0936b..fe99c9e9889 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.8.1", + "version": "2024.8.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/services/background-browser-biometrics.service.ts b/apps/browser/src/platform/services/background-browser-biometrics.service.ts index 41ae15972cd..0cd48c45938 100644 --- a/apps/browser/src/platform/services/background-browser-biometrics.service.ts +++ b/apps/browser/src/platform/services/background-browser-biometrics.service.ts @@ -6,20 +6,20 @@ import { BrowserBiometricsService } from "./browser-biometrics.service"; @Injectable() export class BackgroundBrowserBiometricsService extends BrowserBiometricsService { - constructor(private nativeMessagingBackground: NativeMessagingBackground) { + constructor(private nativeMessagingBackground: () => NativeMessagingBackground) { super(); } async authenticateBiometric(): Promise { - const responsePromise = this.nativeMessagingBackground.getResponse(); - await this.nativeMessagingBackground.send({ command: "biometricUnlock" }); + const responsePromise = this.nativeMessagingBackground().getResponse(); + await this.nativeMessagingBackground().send({ command: "biometricUnlock" }); const response = await responsePromise; return response.response === "unlocked"; } async isBiometricUnlockAvailable(): Promise { - const responsePromise = this.nativeMessagingBackground.getResponse(); - await this.nativeMessagingBackground.send({ command: "biometricUnlockAvailable" }); + const responsePromise = this.nativeMessagingBackground().getResponse(); + await this.nativeMessagingBackground().send({ command: "biometricUnlockAvailable" }); const response = await responsePromise; return response.response === "available"; } diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index 227ede146ba..3da6fdef196 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -199,6 +199,9 @@ export const routerTransition = trigger("routerTransition", [ transition("vault-settings => sync", inSlideLeft), transition("sync => vault-settings", outSlideRight), + transition("vault-settings => trash", inSlideLeft), + transition("trash => vault-settings", outSlideRight), + // Appearance settings transition("tabs => appearance", inSlideLeft), transition("appearance => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 455909336b3..7af88316842 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -41,6 +41,7 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component" import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; @@ -58,6 +59,7 @@ import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/pass import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; +import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/send-v2/add-edit/send-add-edit.component"; import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component"; import { SendV2Component } from "../tools/popup/send-v2/send-v2.component"; import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component"; @@ -91,6 +93,7 @@ import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit. import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component"; import { SyncComponent } from "../vault/popup/settings/sync.component"; +import { TrashComponent } from "../vault/popup/settings/trash.component"; import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component"; import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; @@ -126,12 +129,11 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides)], data: { state: "home" }, }, - { + ...extensionRefreshSwap(Fido2V1Component, Fido2Component, { path: "fido2", - component: Fido2Component, canActivate: [fido2AuthGuard], data: { state: "fido2" }, - }, + }), { path: "login", component: LoginComponent, @@ -303,7 +305,6 @@ const routes: Routes = [ }, ...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, { path: "notifications", - component: NotificationsSettingsV1Component, canActivate: [authGuard], data: { state: "notifications" }, }), @@ -337,7 +338,6 @@ const routes: Routes = [ }, ...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, { path: "excluded-domains", - component: ExcludedDomainsV1Component, canActivate: [authGuard], data: { state: "excluded-domains" }, }), @@ -363,18 +363,16 @@ const routes: Routes = [ canActivate: [authGuard], data: { state: "send-type" }, }, - { + ...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, { path: "add-send", - component: SendAddEditComponent, canActivate: [authGuard], data: { state: "add-send" }, - }, - { + }), + ...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, { path: "edit-send", - component: SendAddEditComponent, canActivate: [authGuard], data: { state: "edit-send" }, - }, + }), { path: "send-created", component: SendCreatedComponent, @@ -496,6 +494,12 @@ const routes: Routes = [ component: AccountSwitcherComponent, data: { state: "account-switcher", doNotSaveUrl: true }, }, + { + path: "trash", + component: TrashComponent, + canActivate: [authGuard], + data: { state: "trash" }, + }, ]; @Injectable() diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 56ddd3c6ba3..f8d3c691051 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -35,8 +35,11 @@ import { SsoComponent } from "../auth/popup/sso.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component"; import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component"; +import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component"; import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component"; +import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; @@ -112,6 +115,9 @@ import "../platform/popup/locales"; ServicesModule, DialogModule, ExcludedDomainsComponent, + Fido2CipherRowComponent, + Fido2Component, + Fido2UseBrowserLinkComponent, FilePopoutCalloutComponent, AvatarModule, AccountComponent, @@ -140,8 +146,8 @@ import "../platform/popup/locales"; CurrentTabComponent, EnvironmentComponent, ExcludedDomainsV1Component, - Fido2CipherRowComponent, - Fido2UseBrowserLinkComponent, + Fido2CipherRowV1Component, + Fido2UseBrowserLinkV1Component, FolderAddEditComponent, FoldersComponent, VaultFilterComponent, @@ -180,7 +186,7 @@ import "../platform/popup/locales"; ViewCustomFieldsComponent, RemovePasswordComponent, VaultSelectComponent, - Fido2Component, + Fido2V1Component, AutofillV1Component, EnvironmentSelectorComponent, ], diff --git a/apps/browser/src/popup/scss/pages.scss b/apps/browser/src/popup/scss/pages.scss index 3ae36472996..bf8f03e7d03 100644 --- a/apps/browser/src/popup/scss/pages.scss +++ b/apps/browser/src/popup/scss/pages.scss @@ -217,7 +217,7 @@ app-vault-attachments { } } -app-fido2 { +app-fido2-v1 { .auth-wrapper { display: flex; flex-direction: column; diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html new file mode 100644 index 00000000000..3e9a8d7c50d --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts new file mode 100644 index 00000000000..48e6cbb8a31 --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -0,0 +1,141 @@ +import { CommonModule, Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormsModule } from "@angular/forms"; +import { ActivatedRoute, Params } from "@angular/router"; +import { map, switchMap } from "rxjs"; + +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 { SendId } from "@bitwarden/common/types/guid"; +import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; +import { + DefaultSendFormConfigService, + SendFormConfig, + SendFormConfigService, + SendFormMode, +} from "@bitwarden/send-ui"; + +import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src/send-form/send-form.module"; +import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; + +/** + * Helper class to parse query parameters for the AddEdit route. + */ +class QueryParams { + constructor(params: Params) { + this.sendId = params.sendId; + this.type = parseInt(params.type, 10); + } + + /** + * The ID of the send to edit, empty when it's a new Send + */ + sendId?: SendId; + + /** + * The type of send to create. + */ + type: SendType; +} + +export type AddEditQueryParams = Partial>; + +/** + * Component for adding or editing a send item. + */ +@Component({ + selector: "tools-send-add-edit", + templateUrl: "send-add-edit.component.html", + standalone: true, + providers: [{ provide: SendFormConfigService, useClass: DefaultSendFormConfigService }], + imports: [ + CommonModule, + SearchModule, + JslibModule, + FormsModule, + ButtonModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + SendFormModule, + AsyncActionsModule, + ], +}) +export class SendAddEditComponent { + /** + * The header text for the component. + */ + headerText: string; + + /** + * The configuration for the send form. + */ + config: SendFormConfig; + + constructor( + private route: ActivatedRoute, + private location: Location, + private i18nService: I18nService, + private addEditFormConfigService: SendFormConfigService, + ) { + this.subscribeToParams(); + } + + /** + * Handles the event when the send is saved. + */ + onSendSaved() { + this.location.back(); + } + + /** + * Subscribes to the route query parameters and builds the configuration based on the parameters. + */ + subscribeToParams(): void { + this.route.queryParams + .pipe( + takeUntilDestroyed(), + map((params) => new QueryParams(params)), + switchMap(async (params) => { + let mode: SendFormMode; + if (params.sendId == null) { + mode = "add"; + } else { + mode = "edit"; + } + const config = await this.addEditFormConfigService.buildConfig( + mode, + params.sendId, + params.type, + ); + return config; + }), + ) + .subscribe((config) => { + this.config = config; + this.headerText = this.getHeaderText(config.mode, config.sendType); + }); + } + + /** + * Gets the header text based on the mode and type. + * @param mode The mode of the send form. + * @param type The type of the send form. + * @returns The header text. + */ + private getHeaderText(mode: SendFormMode, type: SendType) { + const headerKey = + mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader"; + + switch (type) { + case SendType.Text: + return this.i18nService.t(headerKey, this.i18nService.t("sendTypeText")); + case SendType.File: + return this.i18nService.t(headerKey, this.i18nService.t("sendTypeFile")); + } + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index b1e95afb535..7664c7e0ca1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -312,13 +312,13 @@ export class AddEditV2Component implements OnInit { switch (type) { case CipherType.Login: - return this.i18nService.t(partOne, this.i18nService.t("typeLogin")); + return this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLocaleLowerCase()); case CipherType.Card: - return this.i18nService.t(partOne, this.i18nService.t("typeCard")); + return this.i18nService.t(partOne, this.i18nService.t("typeCard").toLocaleLowerCase()); case CipherType.Identity: - return this.i18nService.t(partOne, this.i18nService.t("typeIdentity")); + return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLocaleLowerCase()); case CipherType.SecureNote: - return this.i18nService.t(partOne, this.i18nService.t("note")); + return this.i18nService.t(partOne, this.i18nService.t("note").toLocaleLowerCase()); } } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html index 487168539b9..f4444a10aeb 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html @@ -13,7 +13,13 @@ - + + + + + + + + + + + + diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts new file mode 100644 index 00000000000..c5fea3c2b8c --- /dev/null +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -0,0 +1,109 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { Router } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + DialogService, + IconButtonModule, + ItemModule, + MenuModule, + SectionComponent, + SectionHeaderComponent, + ToastService, + TypographyModule, +} from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +@Component({ + selector: "app-trash-list-items-container", + templateUrl: "trash-list-items-container.component.html", + standalone: true, + imports: [ + CommonModule, + ItemModule, + JslibModule, + SectionComponent, + SectionHeaderComponent, + MenuModule, + IconButtonModule, + TypographyModule, + ], +}) +export class TrashListItemsContainerComponent { + /** + * The list of trashed items to display. + */ + @Input() + ciphers: CipherView[] = []; + + @Input() + headerText: string; + + constructor( + private cipherService: CipherService, + private logService: LogService, + private toastService: ToastService, + private i18nService: I18nService, + private dialogService: DialogService, + private passwordRepromptService: PasswordRepromptService, + private router: Router, + ) {} + + async restore(cipher: CipherView) { + try { + await this.cipherService.restoreWithServer(cipher.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredItem"), + }); + } catch (e) { + this.logService.error(e); + } + } + + async delete(cipher: CipherView) { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); + + if (!repromptPassed) { + return; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { key: "permanentlyDeleteItemConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.cipherService.deleteWithServer(cipher.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedItem"), + }); + } catch (e) { + this.logService.error(e); + } + } + + async onViewCipher(cipher: CipherView) { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); + if (!repromptPassed) { + return; + } + + await this.router.navigate(["/view-cipher"], { + queryParams: { cipherId: cipher.id, type: cipher.type }, + }); + } +} diff --git a/apps/browser/src/vault/popup/settings/trash.component.html b/apps/browser/src/vault/popup/settings/trash.component.html new file mode 100644 index 00000000000..146e4161671 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/trash.component.html @@ -0,0 +1,37 @@ + + + + + + + + + {{ "trashWarning" | i18n }} + + + + + + + + + + {{ "noItemsInTrash" | i18n }} + + + {{ "noItemsInTrashDesc" | i18n }} + + + + + diff --git a/apps/browser/src/vault/popup/settings/trash.component.ts b/apps/browser/src/vault/popup/settings/trash.component.ts new file mode 100644 index 00000000000..b6f77ef6a52 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/trash.component.ts @@ -0,0 +1,37 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CalloutModule, NoItemsModule } from "@bitwarden/components"; +import { VaultIcons } from "@bitwarden/vault"; + +import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +import { VaultListItemsContainerComponent } from "../components/vault-v2/vault-list-items-container/vault-list-items-container.component"; +import { VaultPopupItemsService } from "../services/vault-popup-items.service"; + +import { TrashListItemsContainerComponent } from "./trash-list-items-container/trash-list-items-container.component"; + +@Component({ + templateUrl: "trash.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + PopupPageComponent, + PopupHeaderComponent, + PopOutComponent, + VaultListItemsContainerComponent, + TrashListItemsContainerComponent, + CalloutModule, + NoItemsModule, + ], +}) +export class TrashComponent { + protected deletedCiphers$ = this.vaultPopupItemsService.deletedCiphers$; + + protected emptyTrashIcon = VaultIcons.EmptyTrash; + + constructor(private vaultPopupItemsService: VaultPopupItemsService) {} +} diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html index 10243bdaa9f..03dd1182fbb 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html @@ -24,6 +24,12 @@ + + + {{ "trash" | i18n }} + + + + + + + + `, +}); + +const dialogAccessItems = itemsFactory(10, AccessItemType.Collection); + +export const Dialog: Story = { + args: { + permissionMode: PermissionMode.Edit, + showMemberRoles: false, + showGroupColumn: true, + columnHeader: "Collection", + selectorLabelText: "Select Collections", + selectorHelpText: "Some helper text describing what this does", + emptySelectionText: "No collections added", + disabled: false, + initialValue: [] as any[], + items: dialogAccessItems, + }, + render, +}; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-reactive.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-reactive.stories.ts new file mode 100644 index 00000000000..ec7c378f19c --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-reactive.stories.ts @@ -0,0 +1,64 @@ +import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; +import { Meta, StoryObj } from "@storybook/angular"; + +import { AccessSelectorComponent, PermissionMode } from "./access-selector.component"; +import { AccessItemType, AccessItemValue } from "./access-selector.models"; +import { default as baseComponentDefinition } from "./access-selector.stories"; +import { actionsData, itemsFactory } from "./storybook-utils"; + +/** + * Displays the Access Selector embedded in a reactive form. + */ +export default { + title: "Web/Organizations/Access Selector/Reactive form", + decorators: baseComponentDefinition.decorators, + argTypes: { + formObj: { table: { disable: true } }, + }, +} as Meta; + +type FormObj = { formObj: FormGroup<{ formItems: FormControl }> }; +type Story = StoryObj; + +const fb = new FormBuilder(); + +const render: Story["render"] = (args) => ({ + props: { + items: [], + onSubmit: actionsData.onSubmit, + ...args, + }, + template: ` + + + + +`, +}); + +const sampleMembers = itemsFactory(10, AccessItemType.Member); +const sampleGroups = itemsFactory(6, AccessItemType.Group); + +export const ReactiveForm: Story = { + args: { + formObj: fb.group({ formItems: [[{ id: "1g", type: AccessItemType.Group }]] }), + permissionMode: PermissionMode.Edit, + showMemberRoles: false, + columnHeader: "Groups/Members", + selectorLabelText: "Select groups and members", + selectorHelpText: + "Permissions set for a member will replace permissions set by that member's group", + emptySelectionText: "No members or groups added", + items: sampleGroups.concat(sampleMembers), + }, + render, +}; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts index d0d05004c4c..429b62ed0cc 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts @@ -1,4 +1,4 @@ -import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts index 3e551a84753..095be1df966 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts @@ -1,13 +1,8 @@ import { importProvidersFrom } from "@angular/core"; -import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { action } from "@storybook/addon-actions"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - OrganizationUserStatusType, - OrganizationUserType, -} from "@bitwarden/common/admin-console/enums"; import { AvatarModule, BadgeModule, @@ -21,10 +16,20 @@ import { import { PreloadedEnglishI18nModule } from "../../../../../core/tests"; -import { AccessSelectorComponent } from "./access-selector.component"; -import { AccessItemType, AccessItemView, CollectionPermission } from "./access-selector.models"; +import { AccessSelectorComponent, PermissionMode } from "./access-selector.component"; +import { AccessItemType, AccessItemValue, CollectionPermission } from "./access-selector.models"; +import { actionsData, itemsFactory } from "./storybook-utils"; import { UserTypePipe } from "./user-type.pipe"; +/** + * The Access Selector is used to view and edit: + * - member and group access to collections + * - members assigned to groups + * + * It is highly configurable in order to display these relationships from each perspective. For example, you can + * manage member-group relationships from the perspective of a particular member (showing all their groups) or a + * particular group (showing all its members). + */ export default { title: "Web/Organizations/Access Selector", decorators: [ @@ -49,65 +54,16 @@ export default { providers: [importProvidersFrom(PreloadedEnglishI18nModule)], }), ], - parameters: {}, - argTypes: { - formObj: { table: { disable: true } }, - }, } as Meta; -// TODO: This is a workaround since this story does weird things. -type Story = StoryObj; - -const actionsData = { - onValueChanged: action("onValueChanged"), - onSubmit: action("onSubmit"), -}; - -/** - * Factory to help build semi-realistic looking items - * @param n - The number of items to build - * @param type - Which type to build - */ -const itemsFactory = (n: number, type: AccessItemType) => { - return [...Array(n)].map((_: unknown, id: number) => { - const item: AccessItemView = { - id: id.toString(), - type: type, - } as AccessItemView; - - switch (item.type) { - case AccessItemType.Collection: - item.labelName = item.listName = `Collection ${id}`; - item.id = item.id + "c"; - item.parentGrouping = "Collection Parent Group " + ((id % 2) + 1); - break; - case AccessItemType.Group: - item.labelName = item.listName = `Group ${id}`; - item.id = item.id + "g"; - break; - case AccessItemType.Member: - item.id = item.id + "m"; - item.email = `member${id}@email.com`; - item.status = id % 3 == 0 ? 0 : 2; - item.labelName = item.status == 2 ? `Member ${id}` : item.email; - item.listName = item.status == 2 ? `${item.labelName} (${item.email})` : item.email; - item.role = id % 5; - break; - } - - return item; - }); -}; +type Story = StoryObj; const sampleMembers = itemsFactory(10, AccessItemType.Member); const sampleGroups = itemsFactory(6, AccessItemType.Group); -// TODO: These renders are badly handled but storybook has made it more difficult to use multiple renders in a single story. -const StandaloneAccessSelectorRender = (args: any) => ({ +const render: Story["render"] = (args) => ({ props: { - items: [], valueChanged: actionsData.onValueChanged, - initialValue: [], ...args, }, template: ` @@ -127,49 +83,8 @@ const StandaloneAccessSelectorRender = (args: any) => ({ `, }); -const DialogAccessSelectorRender = (args: any) => ({ - props: { - items: [], - valueChanged: actionsData.onValueChanged, - initialValue: [], - ...args, - }, - template: ` - - Access selector - - - - - - - - - - `, -}); - -const dialogAccessItems = itemsFactory(10, AccessItemType.Collection); - -const memberCollectionAccessItems = itemsFactory(3, AccessItemType.Collection).concat([ +const memberCollectionAccessItems = itemsFactory(5, AccessItemType.Collection).concat([ + // These represent collection access via a group { id: "c1-group1", type: AccessItemType.Collection, @@ -190,25 +105,25 @@ const memberCollectionAccessItems = itemsFactory(3, AccessItemType.Collection).c }, ]); -export const Dialog: Story = { - args: { - permissionMode: "edit", - showMemberRoles: false, - showGroupColumn: true, - columnHeader: "Collection", - selectorLabelText: "Select Collections", - selectorHelpText: "Some helper text describing what this does", - emptySelectionText: "No collections added", - disabled: false, - initialValue: [] as any[], - items: dialogAccessItems, - }, - render: DialogAccessSelectorRender, -}; +// Simulate the current user not having permission to change access to this collection +// TODO: currently the member dialog duplicates the AccessItemValue.permission on the +// AccessItemView.readonlyPermission, this will be refactored to reduce this duplication: +// https://bitwarden.atlassian.net/browse/PM-11590 +memberCollectionAccessItems[4].readonly = true; +memberCollectionAccessItems[4].readonlyPermission = CollectionPermission.Manage; +/** + * Displays a member's collection access. + * + * This is currently used in the **Member dialog -> Collections tab**. Note that it includes collection access that the + * member has via a group. + * + * This is also used in the **Groups dialog -> Collections tab** to show a group's collection access and in this + * case the Group column is hidden. + */ export const MemberCollectionAccess: Story = { args: { - permissionMode: "edit", + permissionMode: PermissionMode.Edit, showMemberRoles: false, showGroupColumn: true, columnHeader: "Collection", @@ -216,22 +131,41 @@ export const MemberCollectionAccess: Story = { selectorHelpText: "Some helper text describing what this does", emptySelectionText: "No collections added", disabled: false, - initialValue: [], + initialValue: [ + { + id: "4c", + type: AccessItemType.Collection, + permission: CollectionPermission.Manage, + }, + { + id: "2c", + type: AccessItemType.Collection, + permission: CollectionPermission.Edit, + }, + ], items: memberCollectionAccessItems, }, - render: StandaloneAccessSelectorRender, + render, }; +/** + * Displays the groups a member is assigned to. + * + * This is currently used in the **Member dialog -> Groups tab**. + */ export const MemberGroupAccess: Story = { args: { - permissionMode: "readonly", + permissionMode: PermissionMode.Hidden, showMemberRoles: false, columnHeader: "Groups", selectorLabelText: "Select Groups", selectorHelpText: "Some helper text describing what this does", emptySelectionText: "No groups added", disabled: false, - initialValue: [{ id: "3g" }, { id: "0g" }], + initialValue: [ + { id: "3g", type: AccessItemType.Group }, + { id: "0g", type: AccessItemType.Group }, + ], items: itemsFactory(4, AccessItemType.Group).concat([ { id: "admin", @@ -241,27 +175,40 @@ export const MemberGroupAccess: Story = { }, ]), }, - render: StandaloneAccessSelectorRender, + render, }; +/** + * Displays the members assigned to a group. + * + * This is currently used in the **Group dialog -> Members tab**. + */ export const GroupMembersAccess: Story = { args: { - permissionMode: "hidden", + permissionMode: PermissionMode.Hidden, showMemberRoles: true, columnHeader: "Members", selectorLabelText: "Select Members", selectorHelpText: "Some helper text describing what this does", emptySelectionText: "No members added", disabled: false, - initialValue: [{ id: "2m" }, { id: "0m" }], + initialValue: [ + { id: "2m", type: AccessItemType.Member }, + { id: "0m", type: AccessItemType.Member }, + ], items: sampleMembers, }, - render: StandaloneAccessSelectorRender, + render, }; +/** + * Displays the members and groups assigned to a collection. + * + * This is currently used in the **Collection dialog -> Access tab**. + */ export const CollectionAccess: Story = { args: { - permissionMode: "edit", + permissionMode: PermissionMode.Edit, showMemberRoles: false, columnHeader: "Groups/Members", selectorLabelText: "Select groups and members", @@ -270,68 +217,38 @@ export const CollectionAccess: Story = { emptySelectionText: "No members or groups added", disabled: false, initialValue: [ - { id: "3g", permission: CollectionPermission.EditExceptPass }, - { id: "0m", permission: CollectionPermission.View }, + { id: "3g", type: AccessItemType.Group, permission: CollectionPermission.EditExceptPass }, + { id: "0m", type: AccessItemType.Member, permission: CollectionPermission.View }, + { id: "7m", type: AccessItemType.Member, permission: CollectionPermission.Manage }, ], - items: sampleGroups.concat(sampleMembers).concat([ - { - id: "admin-group", - type: AccessItemType.Group, - listName: "Admin Group", - labelName: "Admin Group", - readonly: true, - }, - { - id: "admin-member", - type: AccessItemType.Member, - listName: "Admin Member (admin@email.com)", - labelName: "Admin Member", - status: OrganizationUserStatusType.Confirmed, - role: OrganizationUserType.Admin, - email: "admin@email.com", - readonly: true, - }, - ]), - }, - render: StandaloneAccessSelectorRender, -}; - -const fb = new FormBuilder(); - -const ReactiveFormAccessSelectorRender = (args: any) => ({ - props: { - items: [], - onSubmit: actionsData.onSubmit, - ...args, - }, - template: ` -
- - -
-`, -}); - -export const ReactiveForm: Story = { - args: { - formObj: fb.group({ formItems: [[{ id: "1g" }]] }), - permissionMode: "edit", - showMemberRoles: false, - columnHeader: "Groups/Members", - selectorLabelText: "Select groups and members", - selectorHelpText: - "Permissions set for a member will replace permissions set by that member's group", - emptySelectionText: "No members or groups added", items: sampleGroups.concat(sampleMembers), }, - render: ReactiveFormAccessSelectorRender, + render, +}; + +// TODO: currently the collection dialog duplicates the AccessItemValue.permission on the +// AccessItemView.readonlyPermission, this will be refactored to reduce this duplication: +// https://bitwarden.atlassian.net/browse/PM-11590 +const disabledMembers = itemsFactory(3, AccessItemType.Member); +disabledMembers[1].readonlyPermission = CollectionPermission.Manage; +disabledMembers[2].readonlyPermission = CollectionPermission.View; + +const disabledGroups = itemsFactory(2, AccessItemType.Group); +disabledGroups[0].readonlyPermission = CollectionPermission.ViewExceptPass; + +/** + * Displays the members and groups assigned to a collection when the control is in a disabled state. + */ +export const DisabledCollectionAccess: Story = { + args: { + ...CollectionAccess.args, + disabled: true, + items: disabledGroups.concat(disabledMembers), + initialValue: [ + { id: "1m", type: AccessItemType.Member, permission: CollectionPermission.Manage }, + { id: "2m", type: AccessItemType.Member, permission: CollectionPermission.View }, + { id: "0g", type: AccessItemType.Group, permission: CollectionPermission.ViewExceptPass }, + ], + }, + render, }; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/storybook-utils.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/storybook-utils.ts new file mode 100644 index 00000000000..fb8bdef1d8c --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/storybook-utils.ts @@ -0,0 +1,44 @@ +import { action } from "@storybook/addon-actions"; + +import { AccessItemType, AccessItemView } from "./access-selector.models"; + +export const actionsData = { + onValueChanged: action("onValueChanged"), + onSubmit: action("onSubmit"), +}; + +/** + * Factory to help build semi-realistic looking items + * @param n - The number of items to build + * @param type - Which type to build + */ +export const itemsFactory = (n: number, type: AccessItemType) => { + return [...Array(n)].map((_: unknown, id: number) => { + const item: AccessItemView = { + id: id.toString(), + type: type, + } as AccessItemView; + + switch (item.type) { + case AccessItemType.Collection: + item.labelName = item.listName = `Collection ${id}`; + item.id = item.id + "c"; + item.parentGrouping = "Collection Parent Group " + ((id % 2) + 1); + break; + case AccessItemType.Group: + item.labelName = item.listName = `Group ${id}`; + item.id = item.id + "g"; + break; + case AccessItemType.Member: + item.id = item.id + "m"; + item.email = `member${id}@email.com`; + item.status = id % 3 == 0 ? 0 : 2; + item.labelName = item.status == 2 ? `Member ${id}` : item.email; + item.listName = item.status == 2 ? `${item.labelName} (${item.email})` : item.email; + item.role = id % 5; + break; + } + + return item; + }); +}; diff --git a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts index bbd344e289e..17e608df3ee 100644 --- a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts +++ b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts @@ -1,6 +1,8 @@ +import { + OrganizationUserApiService, + OrganizationUserResetPasswordEnrollmentRequest, +} from "@bitwarden/admin-console/common"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; @@ -23,7 +25,7 @@ export class EnrollMasterPasswordReset { dialogService: DialogService, data: EnrollMasterPasswordResetData, resetPasswordService: OrganizationUserResetPasswordService, - organizationUserService: OrganizationUserService, + organizationUserApiService: OrganizationUserApiService, platformUtilsService: PlatformUtilsService, i18nService: I18nService, syncService: SyncService, @@ -50,7 +52,7 @@ export class EnrollMasterPasswordReset { // Process the enrollment request, which is an endpoint that is // gated by a server-side check of the master password hash - await organizationUserService.putOrganizationUserResetPasswordEnrollment( + await organizationUserApiService.putOrganizationUserResetPasswordEnrollment( data.organization.id, data.organization.userId, request, diff --git a/apps/web/src/app/auth/hint.component.ts b/apps/web/src/app/auth/hint.component.ts index 42744546234..753bdb342f9 100644 --- a/apps/web/src/app/auth/hint.component.ts +++ b/apps/web/src/app/auth/hint.component.ts @@ -44,8 +44,8 @@ export class HintComponent extends BaseHintComponent implements OnInit { ); } - ngOnInit(): void { - super.ngOnInit(); + async ngOnInit(): Promise { + await super.ngOnInit(); this.emailFormControl.setValue(this.email); } diff --git a/apps/web/src/app/auth/key-rotation/request/update-key.request.ts b/apps/web/src/app/auth/key-rotation/request/update-key.request.ts index 9ea40c88e6e..0988ed54a99 100644 --- a/apps/web/src/app/auth/key-rotation/request/update-key.request.ts +++ b/apps/web/src/app/auth/key-rotation/request/update-key.request.ts @@ -1,4 +1,4 @@ -import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request"; import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request"; diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts index a9727532051..2c803a627f3 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index 97a17a5997f..13b704b5466 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -1,9 +1,9 @@ import { FakeGlobalStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; import { MockProxy, mock } from "jest-mock-extended"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -35,7 +35,7 @@ describe("AcceptOrganizationInviteService", () => { let policyService: MockProxy; let logService: MockProxy; let organizationApiService: MockProxy; - let organizationUserService: MockProxy; + let organizationUserApiService: MockProxy; let i18nService: MockProxy; let globalStateProvider: FakeGlobalStateProvider; let globalState: FakeGlobalState; @@ -49,7 +49,7 @@ describe("AcceptOrganizationInviteService", () => { policyService = mock(); logService = mock(); organizationApiService = mock(); - organizationUserService = mock(); + organizationUserApiService = mock(); i18nService = mock(); globalStateProvider = new FakeGlobalStateProvider(); globalState = globalStateProvider.getFake(ORGANIZATION_INVITE); @@ -63,7 +63,7 @@ describe("AcceptOrganizationInviteService", () => { policyService, logService, organizationApiService, - organizationUserService, + organizationUserApiService, i18nService, globalStateProvider, ); @@ -85,10 +85,10 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(organizationUserService.postOrganizationUserAcceptInit).toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAcceptInit).toHaveBeenCalled(); expect(apiService.refreshIdentityToken).toHaveBeenCalled(); expect(globalState.nextMock).toHaveBeenCalledWith(null); - expect(organizationUserService.postOrganizationUserAccept).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAccept).not.toHaveBeenCalled(); expect(authService.logOut).not.toHaveBeenCalled(); }); @@ -133,10 +133,10 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled(); expect(apiService.refreshIdentityToken).toHaveBeenCalled(); expect(globalState.nextMock).toHaveBeenCalledWith(null); - expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); expect(authService.logOut).not.toHaveBeenCalled(); }); @@ -161,8 +161,8 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled(); - expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); expect(authService.logOut).not.toHaveBeenCalled(); }); }); diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts index d1ffa61f6a9..a7798d480fb 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -1,13 +1,13 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, firstValueFrom, map } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { + OrganizationUserApiService, OrganizationUserAcceptRequest, OrganizationUserAcceptInitRequest, -} from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +} from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -58,7 +58,7 @@ export class AcceptOrganizationInviteService { private readonly policyService: PolicyService, private readonly logService: LogService, private readonly organizationApiService: OrganizationApiServiceAbstraction, - private readonly organizationUserService: OrganizationUserService, + private readonly organizationUserApiService: OrganizationUserApiService, private readonly i18nService: I18nService, private readonly globalStateProvider: GlobalStateProvider, ) { @@ -121,7 +121,7 @@ export class AcceptOrganizationInviteService { private async acceptAndInitOrganization(invite: OrganizationInvite): Promise { await this.prepareAcceptAndInitRequest(invite).then((request) => - this.organizationUserService.postOrganizationUserAcceptInit( + this.organizationUserApiService.postOrganizationUserAcceptInit( invite.organizationId, invite.organizationUserId, request, @@ -156,7 +156,7 @@ export class AcceptOrganizationInviteService { private async accept(invite: OrganizationInvite): Promise { await this.prepareAcceptRequest(invite).then((request) => - this.organizationUserService.postOrganizationUserAccept( + this.organizationUserApiService.postOrganizationUserAccept( invite.organizationId, invite.organizationUserId, request, diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor-setup.component.ts index 8be6c7a3cc2..3b8a9edd955 100644 --- a/apps/web/src/app/auth/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-setup.component.ts @@ -220,7 +220,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.dialogService, { data: result }, ); - this.twoFactorSetupSubscription = webAuthnComp.componentInstance.onChangeStatus + this.twoFactorSetupSubscription = webAuthnComp.componentInstance.onUpdated .pipe(first(), takeUntil(this.destroy$)) .subscribe((enabled: boolean) => { webAuthnComp.close(); diff --git a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts index 9aeafaf2c65..6dfee920991 100644 --- a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts @@ -1,5 +1,5 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, EventEmitter, Inject, NgZone, Output } from "@angular/core"; +import { Component, Inject, NgZone } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -33,7 +33,6 @@ interface Key { templateUrl: "two-factor-webauthn.component.html", }) export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { - @Output() onChangeStatus = new EventEmitter(); type = TwoFactorProviderType.WebAuthn; name: string; keys: Key[]; @@ -85,34 +84,33 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { // Should never happen. return Promise.reject(); } + return this.enable(); + }; + + protected async enable() { const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest); request.deviceResponse = this.webAuthnResponse; request.id = this.keyIdAvailable; request.name = this.formGroup.value.name; - return this.enableWebAuth(request); - }; - - private enableWebAuth(request: any) { - return super.enable(async () => { - this.formPromise = this.apiService.putTwoFactorWebAuthn(request); - const response = await this.formPromise; - this.processResponse(response); + const response = await this.apiService.putTwoFactorWebAuthn(request); + this.processResponse(response); + this.toastService.showToast({ + title: this.i18nService.t("success"), + message: this.i18nService.t("twoFactorProviderEnabled"), + variant: "success", }); + this.onUpdated.emit(response.enabled); } disable = async () => { - await this.disableWebAuth(); + await this.disableMethod(); if (!this.enabled) { - this.onChangeStatus.emit(this.enabled); + this.onUpdated.emit(this.enabled); this.dialogRef.close(); } }; - private async disableWebAuth() { - return super.disable(this.formPromise); - } - async remove(key: Key) { if (this.keysConfiguredCount <= 1 || key.removePromise != null) { return; @@ -208,7 +206,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { } } this.enabled = response.enabled; - this.onChangeStatus.emit(this.enabled); + this.onUpdated.emit(this.enabled); } static open( diff --git a/apps/web/src/app/auth/two-factor.component.html b/apps/web/src/app/auth/two-factor.component.html index a941413dbb0..b78747e04c2 100644 --- a/apps/web/src/app/auth/two-factor.component.html +++ b/apps/web/src/app/auth/two-factor.component.html @@ -36,7 +36,7 @@ - {{ "verificationCode" | i18n }} + {{ "verificationCode" | i18n }} diff --git a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts index ff45ca75ac6..585d9b418c1 100644 --- a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts @@ -1,10 +1,14 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + import { PaymentMethodComponent } from "../shared"; import { BillingHistoryViewComponent } from "./billing-history-view.component"; -import { PremiumComponent } from "./premium.component"; +import { PremiumV2Component } from "./premium/premium-v2.component"; +import { PremiumComponent } from "./premium/premium.component"; import { SubscriptionComponent } from "./subscription.component"; import { UserSubscriptionComponent } from "./user-subscription.component"; @@ -20,11 +24,15 @@ const routes: Routes = [ component: UserSubscriptionComponent, data: { titleId: "premiumMembership" }, }, - { - path: "premium", - component: PremiumComponent, - data: { titleId: "goPremium" }, - }, + ...featureFlaggedRoute({ + defaultComponent: PremiumComponent, + flaggedComponent: PremiumV2Component, + featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + routeOptions: { + path: "premium", + data: { titleId: "goPremium" }, + }, + }), { path: "payment-method", component: PaymentMethodComponent, diff --git a/apps/web/src/app/billing/individual/individual-billing.module.ts b/apps/web/src/app/billing/individual/individual-billing.module.ts index dbae28858f8..0dbbc8c6837 100644 --- a/apps/web/src/app/billing/individual/individual-billing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing.module.ts @@ -5,7 +5,8 @@ import { BillingSharedModule } from "../shared"; import { BillingHistoryViewComponent } from "./billing-history-view.component"; import { IndividualBillingRoutingModule } from "./individual-billing-routing.module"; -import { PremiumComponent } from "./premium.component"; +import { PremiumV2Component } from "./premium/premium-v2.component"; +import { PremiumComponent } from "./premium/premium.component"; import { SubscriptionComponent } from "./subscription.component"; import { UserSubscriptionComponent } from "./user-subscription.component"; @@ -16,6 +17,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component"; BillingHistoryViewComponent, UserSubscriptionComponent, PremiumComponent, + PremiumV2Component, ], }) export class IndividualBillingModule {} diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.html b/apps/web/src/app/billing/individual/premium/premium-v2.component.html new file mode 100644 index 00000000000..bdf6ff87d19 --- /dev/null +++ b/apps/web/src/app/billing/individual/premium/premium-v2.component.html @@ -0,0 +1,144 @@ + +

{{ "goPremium" | i18n }}

+ + {{ "alreadyPremiumFromOrg" | i18n }} + + +

{{ "premiumUpgradeUnlockFeatures" | i18n }}

+
    +
  • + + {{ "premiumSignUpStorage" | i18n }} +
  • +
  • + + {{ "premiumSignUpTwoStepOptions" | i18n }} +
  • +
  • + + {{ "premiumSignUpEmergency" | i18n }} +
  • +
  • + + {{ "premiumSignUpReports" | i18n }} +
  • +
  • + + {{ "premiumSignUpTotp" | i18n }} +
  • +
  • + + {{ "premiumSignUpSupport" | i18n }} +
  • +
  • + + {{ "premiumSignUpFuture" | i18n }} +
  • +
+

+ {{ + "premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount + }} + + {{ "bitwardenFamiliesPlan" | i18n }} + +

+ + {{ "purchasePremium" | i18n }} + +
+
+ +

{{ "uploadLicenseFilePremium" | i18n }}

+
+ + {{ "licenseFile" | i18n }} +
+ + {{ + licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n) + }} +
+ + {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} +
+ +
+
+
+ +

{{ "addons" | i18n }}

+
+ + {{ "additionalStorageGb" | i18n }} + + {{ + "additionalStorageIntervalDesc" + | i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n) + }} + +
+
+ +

{{ "summary" | i18n }}

+ {{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
+ {{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB × + {{ storageGBPrice | currency: "$" }} = + {{ additionalStorageCost | currency: "$" }} +
+
+ +

{{ "paymentInformation" | i18n }}

+ + +
+
+ {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} + + {{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }} +
+
+
+

+ {{ "total" | i18n }}: {{ total | currency: "USD $" }}/{{ "year" | i18n }} +

+ +
+
diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.ts b/apps/web/src/app/billing/individual/premium/premium-v2.component.ts new file mode 100644 index 00000000000..cf66dac2f76 --- /dev/null +++ b/apps/web/src/app/billing/individual/premium/premium-v2.component.ts @@ -0,0 +1,164 @@ +import { Component, ViewChild } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, concatMap, from, Observable, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { ToastService } from "@bitwarden/components"; + +import { PaymentV2Component } from "../../shared/payment/payment-v2.component"; +import { TaxInfoComponent } from "../../shared/tax-info.component"; + +@Component({ + templateUrl: "./premium-v2.component.html", +}) +export class PremiumV2Component { + @ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component; + @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; + + protected hasPremiumFromAnyOrganization$: Observable; + + protected addOnFormGroup = new FormGroup({ + additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]), + }); + + protected licenseFormGroup = new FormGroup({ + file: new FormControl(null, [Validators.required]), + }); + + protected cloudWebVaultURL: string; + protected isSelfHost = false; + + protected readonly familyPlanMaxUserCount = 6; + protected readonly premiumPrice = 10; + protected readonly storageGBPrice = 4; + + constructor( + private activatedRoute: ActivatedRoute, + private apiService: ApiService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private environmentService: EnvironmentService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private router: Router, + private syncService: SyncService, + private toastService: ToastService, + private tokenService: TokenService, + ) { + this.isSelfHost = this.platformUtilsService.isSelfHost(); + + this.hasPremiumFromAnyOrganization$ = + this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$; + + combineLatest([ + this.billingAccountProfileStateService.hasPremiumPersonally$, + this.environmentService.cloudWebVaultUrl$, + ]) + .pipe( + takeUntilDestroyed(), + concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => { + if (hasPremiumPersonally) { + return from(this.navigateToSubscriptionPage()); + } + + this.cloudWebVaultURL = cloudWebVaultURL; + return of(true); + }), + ) + .subscribe(); + } + + finalizeUpgrade = async () => { + await this.apiService.refreshIdentityToken(); + await this.syncService.fullSync(true); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("premiumUpdated"), + }); + await this.navigateToSubscriptionPage(); + }; + + navigateToSubscriptionPage = (): Promise => + this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); + + onLicenseFileSelected = (event: Event): void => { + const element = event.target as HTMLInputElement; + this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null; + }; + + submitPremiumLicense = async (): Promise => { + this.licenseFormGroup.markAllAsTouched(); + + if (this.licenseFormGroup.invalid) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); + } + + const emailVerified = await this.tokenService.getEmailVerified(); + if (!emailVerified) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verifyEmailFirst"), + }); + } + + const formData = new FormData(); + formData.append("license", this.licenseFormGroup.value.file); + + await this.apiService.postAccountLicense(formData); + await this.finalizeUpgrade(); + }; + + submitPayment = async (): Promise => { + this.taxInfoComponent.taxFormGroup.markAllAsTouched(); + if (this.taxInfoComponent.taxFormGroup.invalid) { + return; + } + + const { type, token } = await this.paymentComponent.tokenize(); + + const formData = new FormData(); + formData.append("paymentMethodType", type.toString()); + formData.append("paymentToken", token); + formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString()); + formData.append("country", this.taxInfoComponent.country); + formData.append("postalCode", this.taxInfoComponent.postalCode); + + await this.apiService.postPremium(formData); + await this.finalizeUpgrade(); + }; + + protected get additionalStorageCost(): number { + return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage; + } + + protected get estimatedTax(): number { + return this.taxInfoComponent?.taxRate != null + ? (this.taxInfoComponent.taxRate / 100) * this.subtotal + : 0; + } + + protected get premiumURL(): string { + return `${this.cloudWebVaultURL}/#/settings/subscription/premium`; + } + + protected get subtotal(): number { + return this.premiumPrice + this.additionalStorageCost; + } + + protected get total(): number { + return this.subtotal + this.estimatedTax; + } +} diff --git a/apps/web/src/app/billing/individual/premium.component.html b/apps/web/src/app/billing/individual/premium/premium.component.html similarity index 99% rename from apps/web/src/app/billing/individual/premium.component.html rename to apps/web/src/app/billing/individual/premium/premium.component.html index ae95475f1c6..8b848b48dab 100644 --- a/apps/web/src/app/billing/individual/premium.component.html +++ b/apps/web/src/app/billing/individual/premium/premium.component.html @@ -69,7 +69,7 @@
{{ "licenseFile" | i18n }} -
+
diff --git a/apps/web/src/app/billing/individual/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts similarity index 98% rename from apps/web/src/app/billing/individual/premium.component.ts rename to apps/web/src/app/billing/individual/premium/premium.component.ts index 79a5c5e2edd..c45b6b882d4 100644 --- a/apps/web/src/app/billing/individual/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -13,7 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; -import { PaymentComponent, TaxInfoComponent } from "../shared"; +import { PaymentComponent, TaxInfoComponent } from "../../shared"; @Component({ templateUrl: "premium.component.html", diff --git a/apps/web/src/app/billing/individual/user-subscription.component.html b/apps/web/src/app/billing/individual/user-subscription.component.html index 08eec09ff97..3430bddc134 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.html +++ b/apps/web/src/app/billing/individual/user-subscription.component.html @@ -48,7 +48,7 @@ }}
{{ "nextCharge" | i18n }}
-
+
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") + @@ -57,6 +57,15 @@ : "-" }}
+
+ {{ + nextInvoice + ? (sub.subscription.periodEndDate | date: "mediumDate") + + ", " + + (nextInvoice.amount | currency: "$") + : "-" + }} +
diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 2d02cbc5bdf..cca17f6b9cc 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -5,6 +5,8 @@ import { firstValueFrom, lastValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -12,10 +14,14 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; +import { + AdjustStorageDialogV2Component, + AdjustStorageDialogV2ResultType, +} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; import { AdjustStorageDialogResult, openAdjustStorageDialog, -} from "../shared/adjust-storage.component"; +} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -34,9 +40,17 @@ export class UserSubscriptionComponent implements OnInit { sub: SubscriptionResponse; selfHosted = false; cloudWebVaultUrl: string; + enableTimeThreshold: boolean; cancelPromise: Promise; reinstatePromise: Promise; + protected enableTimeThreshold$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableTimeThreshold, + ); + + protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( + FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + ); constructor( private apiService: ApiService, @@ -49,6 +63,7 @@ export class UserSubscriptionComponent implements OnInit { private environmentService: EnvironmentService, private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, + private configService: ConfigService, ) { this.selfHosted = platformUtilsService.isSelfHost(); } @@ -56,6 +71,7 @@ export class UserSubscriptionComponent implements OnInit { async ngOnInit() { this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); await this.load(); + this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$); this.firstLoaded = true; } @@ -150,15 +166,33 @@ export class UserSubscriptionComponent implements OnInit { }; adjustStorage = async (add: boolean) => { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: 4, - add: add, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); + const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); + + if (deprecateStripeSourcesAPI) { + const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { + data: { + price: 4, + cadence: "year", + type: add ? "Add" : "Remove", + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AdjustStorageDialogV2ResultType.Submitted) { + await this.load(); + } + } else { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: 4, + add: add, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } } }; diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index 436372c049b..4fb9ae386a1 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -1,6 +1,6 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; +import { Subject, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request"; @@ -11,7 +11,7 @@ import { ToastService } from "@bitwarden/components"; selector: "app-adjust-subscription", templateUrl: "adjust-subscription.component.html", }) -export class AdjustSubscription { +export class AdjustSubscription implements OnInit, OnDestroy { @Input() organizationId: string; @Input() maxAutoscaleSeats: number; @Input() currentSeatCount: number; @@ -19,6 +19,8 @@ export class AdjustSubscription { @Input() interval = "year"; @Output() onAdjusted = new EventEmitter(); + private destroy$ = new Subject(); + adjustSubscriptionForm = this.formBuilder.group({ newSeatCount: [0, [Validators.min(0)]], limitSubscription: [false], @@ -30,30 +32,25 @@ export class AdjustSubscription { private organizationApiService: OrganizationApiServiceAbstraction, private formBuilder: FormBuilder, private toastService: ToastService, - ) { + ) {} + + ngOnInit() { + this.adjustSubscriptionForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { + const maxAutoscaleSeatsControl = this.adjustSubscriptionForm.controls.newMaxSeats; + + if (value.limitSubscription) { + maxAutoscaleSeatsControl.setValidators([Validators.min(value.newSeatCount)]); + maxAutoscaleSeatsControl.enable({ emitEvent: false }); + } else { + maxAutoscaleSeatsControl.disable({ emitEvent: false }); + } + }); + this.adjustSubscriptionForm.patchValue({ newSeatCount: this.currentSeatCount, - limitSubscription: this.maxAutoscaleSeats != null, newMaxSeats: this.maxAutoscaleSeats, + limitSubscription: this.maxAutoscaleSeats != null, }); - this.adjustSubscriptionForm - .get("limitSubscription") - .valueChanges.pipe(takeUntilDestroyed()) - .subscribe((value: boolean) => { - if (value) { - this.adjustSubscriptionForm - .get("newMaxSeats") - .addValidators([ - Validators.min( - this.adjustSubscriptionForm.value.newSeatCount == null - ? 1 - : this.adjustSubscriptionForm.value.newSeatCount, - ), - Validators.required, - ]); - } - this.adjustSubscriptionForm.get("newMaxSeats").updateValueAndValidity(); - }); } submit = async () => { @@ -99,4 +96,9 @@ export class AdjustSubscription { get limitSubscription(): boolean { return this.adjustSubscriptionForm.value.limitSubscription; } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 656796b443e..25c8c547b2b 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -53,9 +53,12 @@
{{ "subscriptionExpiration" | i18n }}
-
+
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
+
+ {{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }} +
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index f28933a4ecc..2a565face75 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -18,10 +18,14 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; +import { + AdjustStorageDialogV2Component, + AdjustStorageDialogV2ResultType, +} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; import { AdjustStorageDialogResult, openAdjustStorageDialog, -} from "../shared/adjust-storage.component"; +} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -71,6 +75,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy FeatureFlag.EnableUpgradePasswordManagerSub, ); + protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( + FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + ); + constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -458,17 +466,36 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy adjustStorage = (add: boolean) => { return async () => { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: this.storageGbPrice, - add: add, - organizationId: this.organizationId, - interval: this.billingInterval, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); + const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); + + if (deprecateStripeSourcesAPI) { + const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { + data: { + price: this.storageGbPrice, + cadence: this.billingInterval, + type: add ? "Add" : "Remove", + organizationId: this.organizationId, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AdjustStorageDialogV2ResultType.Submitted) { + await this.load(); + } + } else { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: this.storageGbPrice, + add: add, + organizationId: this.organizationId, + interval: this.billingInterval, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } } }; }; diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html new file mode 100644 index 00000000000..7b74379acb6 --- /dev/null +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.html @@ -0,0 +1,34 @@ + + + +

{{ body }}

+
+ + {{ storageFieldLabel }} + + + + {{ "total" | i18n }} + {{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} = + {{ this.price * this.formGroup.value.storage | currency: "$" }} / + {{ this.cadence | i18n }} + + +
+
+ + + + +
+ diff --git a/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts new file mode 100644 index 00000000000..23d5e46fa1b --- /dev/null +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog-v2.component.ts @@ -0,0 +1,104 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { StorageRequest } from "@bitwarden/common/models/request/storage.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +export interface AdjustStorageDialogV2Params { + price: number; + cadence: "month" | "year"; + type: "Add" | "Remove"; + organizationId?: string; +} + +export enum AdjustStorageDialogV2ResultType { + Submitted = "submitted", + Closed = "closed", +} + +@Component({ + templateUrl: "./adjust-storage-dialog-v2.component.html", +}) +export class AdjustStorageDialogV2Component { + protected formGroup = new FormGroup({ + storage: new FormControl(0, [ + Validators.required, + Validators.min(0), + Validators.max(99), + ]), + }); + + protected organizationId?: string; + protected price: number; + protected cadence: "month" | "year"; + + protected title: string; + protected body: string; + protected storageFieldLabel: string; + + protected ResultType = AdjustStorageDialogV2ResultType; + + constructor( + private apiService: ApiService, + @Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogV2Params, + private dialogRef: DialogRef, + private i18nService: I18nService, + private organizationApiService: OrganizationApiServiceAbstraction, + private toastService: ToastService, + ) { + this.price = this.dialogParams.price; + this.cadence = this.dialogParams.cadence; + this.organizationId = this.dialogParams.organizationId; + switch (this.dialogParams.type) { + case "Add": + this.title = this.i18nService.t("addStorage"); + this.body = this.i18nService.t("storageAddNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageAdd"); + break; + case "Remove": + this.title = this.i18nService.t("removeStorage"); + this.body = this.i18nService.t("storageRemoveNote"); + this.storageFieldLabel = this.i18nService.t("gbStorageRemove"); + break; + } + } + + submit = async () => { + const request = new StorageRequest(); + switch (this.dialogParams.type) { + case "Add": + request.storageGbAdjustment = this.formGroup.value.storage; + break; + case "Remove": + request.storageGbAdjustment = this.formGroup.value.storage * -1; + break; + } + + if (this.organizationId) { + await this.organizationApiService.updateStorage(this.organizationId, request); + } else { + await this.apiService.postAccountStorage(request); + } + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), + }); + + this.dialogRef.close(this.ResultType.Submitted); + }; + + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig, + ) => + dialogService.open( + AdjustStorageDialogV2Component, + dialogConfig, + ); +} diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.html b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html similarity index 100% rename from apps/web/src/app/billing/shared/adjust-storage.component.html rename to apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.html diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.ts b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts similarity index 95% rename from apps/web/src/app/billing/shared/adjust-storage.component.ts rename to apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts index 5cf05ea015c..a67c63a9fad 100644 --- a/apps/web/src/app/billing/shared/adjust-storage.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage-dialog/adjust-storage-dialog.component.ts @@ -12,7 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { PaymentComponent } from "./payment/payment.component"; +import { PaymentComponent } from "../payment/payment.component"; export interface AdjustStorageDialogData { storageGbPrice: number; @@ -27,9 +27,9 @@ export enum AdjustStorageDialogResult { } @Component({ - templateUrl: "adjust-storage.component.html", + templateUrl: "adjust-storage-dialog.component.html", }) -export class AdjustStorageComponent { +export class AdjustStorageDialogComponent { storageGbPrice: number; add: boolean; organizationId: string; @@ -126,5 +126,5 @@ export function openAdjustStorageDialog( dialogService: DialogService, config: DialogConfig, ) { - return dialogService.open(AdjustStorageComponent, config); + return dialogService.open(AdjustStorageDialogComponent, config); } diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 300817bad55..b966729c1df 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -6,7 +6,8 @@ import { SharedModule } from "../../shared"; import { AddCreditDialogComponent } from "./add-credit-dialog.component"; import { AdjustPaymentDialogV2Component } from "./adjust-payment-dialog/adjust-payment-dialog-v2.component"; import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component"; -import { AdjustStorageComponent } from "./adjust-storage.component"; +import { AdjustStorageDialogV2Component } from "./adjust-storage-dialog/adjust-storage-dialog-v2.component"; +import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; import { PaymentV2Component } from "./payment/payment-v2.component"; @@ -30,7 +31,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac declarations: [ AddCreditDialogComponent, AdjustPaymentDialogComponent, - AdjustStorageComponent, + AdjustStorageDialogComponent, BillingHistoryComponent, PaymentMethodComponent, SecretsManagerSubscribeComponent, @@ -38,18 +39,20 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac UpdateLicenseDialogComponent, OffboardingSurveyComponent, AdjustPaymentDialogV2Component, + AdjustStorageDialogV2Component, ], exports: [ SharedModule, PaymentComponent, TaxInfoComponent, - AdjustStorageComponent, + AdjustStorageDialogComponent, BillingHistoryComponent, SecretsManagerSubscribeComponent, UpdateLicenseComponent, UpdateLicenseDialogComponent, OffboardingSurveyComponent, VerifyBankAccountComponent, + PaymentV2Component, ], }) export class BillingSharedModule {} diff --git a/apps/web/src/app/billing/shared/update-license.component.html b/apps/web/src/app/billing/shared/update-license.component.html index 938179469e4..ea0818389e4 100644 --- a/apps/web/src/app/billing/shared/update-license.component.html +++ b/apps/web/src/app/billing/shared/update-license.component.html @@ -1,7 +1,7 @@
{{ "licenseFile" | i18n }} -
+
diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 45ae3a357f6..2beea573e59 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from "@angular/common"; import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SECURE_STORAGE, @@ -25,7 +26,6 @@ import { import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; @@ -33,6 +33,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { ClientType } from "@bitwarden/common/enums"; +import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; @@ -42,6 +43,7 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; +import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; @@ -205,10 +207,15 @@ const safeProviders: SafeProvider[] = [ KdfConfigService, InternalMasterPasswordServiceAbstraction, OrganizationApiServiceAbstraction, - OrganizationUserService, + OrganizationUserApiService, InternalUserDecryptionOptionsServiceAbstraction, ], }), + safeProvider({ + provide: AppIdService, + useClass: DefaultAppIdService, + deps: [OBSERVABLE_DISK_LOCAL_STORAGE, LogService], + }), safeProvider({ provide: LoginService, useClass: WebLoginService, diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index c8cbd9f8dab..7cba19b29ad 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -36,13 +36,13 @@
{{ "loggedInAs" | i18n }} - + {{ account | userName }}
diff --git a/apps/web/src/app/settings/domain-rules.component.html b/apps/web/src/app/settings/domain-rules.component.html index 18091bcc3aa..1c8e5e435ec 100644 --- a/apps/web/src/app/settings/domain-rules.component.html +++ b/apps/web/src/app/settings/domain-rules.component.html @@ -18,7 +18,7 @@ *ngFor="let d of custom; let i = index; trackBy: indexTrackBy" > - {{ "customDomainX" | i18n: i + 1 }} + {{ "customDomainX" | i18n: i + 1 }} - + diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.ts b/libs/vault/src/cipher-view/card-details/card-details-view.component.ts index 028417faf16..6ab2795afd9 100644 --- a/libs/vault/src/cipher-view/card-details/card-details-view.component.ts +++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.ts @@ -13,6 +13,8 @@ import { IconButtonModule, } from "@bitwarden/components"; +import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; + @Component({ selector: "app-card-details-view", templateUrl: "card-details-view.component.html", @@ -26,6 +28,7 @@ import { TypographyModule, FormFieldModule, IconButtonModule, + ReadOnlyCipherCardComponent, ], }) export class CardDetailsComponent { diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index a675384ff9f..9b4bfdb5970 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -3,6 +3,15 @@ {{ "cardExpiredMessage" | i18n }} + +

+ {{ "noEditPermissions" | i18n }} +

+ - + {{ field.name }} - + {{ field.name }} @@ -45,7 +45,7 @@ /> {{ field.name }} - + {{ "linked" | i18n }}: {{ field.name }} {{ "itemName" | i18n }} @@ -22,7 +25,7 @@