1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 06:54:07 +00:00
Files
browser/apps/desktop/src/app/app.component.ts
Anders Åberg 7cba6f4170 PM-8353 MacOS passkey provider (#13963)
* Turn on passkeys and dev mode

* PM-19138: Add try-catch to desktop-autofill (#13964)

* PM-19424: React to IPC disconnect (#14123)

* React to IPC disconnects

* Minor cleanup

* Update apps/desktop/package.json

Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>

* Relaxed ordering

---------

Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>

* Autofill/pm 9034 implement passkey for unlocked accounts (#13826)

* Passkey stuff

Co-authored-by: Anders Åberg <github@andersaberg.com>

* Ugly hacks

* Work On Modal State Management

* Applying modalStyles

* modal

* Improved hide/show

* fixed promise

* File name

* fix prettier

* Protecting against null API's and undefined data

* Only show fake popup to devs

* cleanup mock code

* rename minmimal-app to modal-app

* Added comment

* Added comment

* removed old comment

* Avoided changing minimum size

* Add small comment

* Rename component

* adress feedback

* Fixed uppercase file

* Fixed build

* Added codeowners

* added void

* commentary

* feat: reset setting on app start

* Moved reset to be in main / process launch

* Add comment to create window

* Added a little bit of styling

* Use Messaging service to loadUrl

* Enable passkeysautofill

* Add logging

* halfbaked

* Integration working

* And now it works without extra delay

* Clean up

* add note about messaging

* lb

* removed console.logs

* Cleanup and adress review feedback

* This hides the swift UI

* add modal components

* update modal with correct ciphers and functionality

* add create screen

* pick credential, draft

* Remove logger

* a whole lot of wiring

* not working

* Improved wiring

* Cancel after 90s

* Introduced observable

* update cipher handling

* update to use matchesUri

* Launching bitwarden if its not running

* Passing position from native to electron

* Rename inModalMode to modalMode

* remove tap

* revert spaces

* added back isDev

* cleaned up a bit

* Cleanup swift file

* tweaked logging

* clean up

* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Update apps/desktop/src/platform/services/desktop-settings.service.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* adress position feedback

* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Removed extra logging

* Adjusted error logging

* Use .error to log errors

* remove dead code

* Update desktop-autofill.service.ts

* use parseCredentialId instead of guidToRawFormat

* Update apps/desktop/src/autofill/services/desktop-autofill.service.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Change windowXy to a Record instead of [number,number]

* Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Remove unsued dep and comment

* changed timeout to be spec recommended maxium, 10 minutes, for now.

* Correctly assume UP

* Removed extra cancelRequest in deinint

* Add timeout and UV to confirmChoseCipher

UV is performed by UI, not the service

* Improved docs regarding undefined cipherId

* cleanup: UP is no longer undefined

* Run completeError if ipc messages conversion failed

* don't throw, instead return undefined

* Disabled passkey provider

* Throw error if no activeUserId was found

* removed comment

* Fixed lint

* removed unsued service

* reset entitlement formatting

* Update entitlements.mas.plist

* Fix build issues

* Fix import issues

* Update route names to use `fido2`

* Fix being unable to select a passkey

* Fix linting issues

* Followup to fix merge issues and other comments

* Update `userHandle` value

* Add error handling for missing session or other errors

* Remove unused route

* Fix linting issues

* Simplify updateCredential method

* Followup to remove comments and timeouts and handle errors

* Address lint issue by using `takeUntilDestroyed`

* PR Followup for typescript and vault concerns

* Add try block for cipher creation

* Make userId manditory for cipher service

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: Anders Åberg <github@andersaberg.com>
Co-authored-by: Anders Åberg <anders@andersaberg.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* PM-11455: Trigger sync when user enables OS setting (#14127)

* Implemented a SendNativeStatus command

This allows reporting status or asking the electron app to do something.

* fmt

* Update apps/desktop/src/autofill/services/desktop-autofill.service.ts

Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>

* clean up

* Don't add empty callbacks

* Removed comment

---------

Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>

* Added support for handling a locked vault

Handle unlocktimeout

* PM-19511: Add support for ExcludedCredentials (#14128)

* works

* Add mapping

* remove the build script

* cleanup

* simplify updatedCipher (#14179)

* Fix base64url decode on MacOS passkeys (#14227)

* Add support for padding in base64url decode

* whitespace

* whitespace

* Autofill/pm 17444 use reprompt (#14004)

* Passkey stuff

Co-authored-by: Anders Åberg <github@andersaberg.com>

* Ugly hacks

* Work On Modal State Management

* Applying modalStyles

* modal

* Improved hide/show

* fixed promise

* File name

* fix prettier

* Protecting against null API's and undefined data

* Only show fake popup to devs

* cleanup mock code

* rename minmimal-app to modal-app

* Added comment

* Added comment

* removed old comment

* Avoided changing minimum size

* Add small comment

* Rename component

* adress feedback

* Fixed uppercase file

* Fixed build

* Added codeowners

* added void

* commentary

* feat: reset setting on app start

* Moved reset to be in main / process launch

* Add comment to create window

* Added a little bit of styling

* Use Messaging service to loadUrl

* Enable passkeysautofill

* Add logging

* halfbaked

* Integration working

* And now it works without extra delay

* Clean up

* add note about messaging

* lb

* removed console.logs

* Cleanup and adress review feedback

* This hides the swift UI

* add modal components

* update modal with correct ciphers and functionality

* add create screen

* pick credential, draft

* Remove logger

* a whole lot of wiring

* not working

* Improved wiring

* Cancel after 90s

* Introduced observable

* update cipher handling

* update to use matchesUri

* Launching bitwarden if its not running

* Passing position from native to electron

* Rename inModalMode to modalMode

* remove tap

* revert spaces

* added back isDev

* cleaned up a bit

* Cleanup swift file

* tweaked logging

* clean up

* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Update apps/desktop/src/platform/services/desktop-settings.service.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* adress position feedback

* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Removed extra logging

* Adjusted error logging

* Use .error to log errors

* remove dead code

* Update desktop-autofill.service.ts

* use parseCredentialId instead of guidToRawFormat

* Update apps/desktop/src/autofill/services/desktop-autofill.service.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Change windowXy to a Record instead of [number,number]

* Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Remove unsued dep and comment

* changed timeout to be spec recommended maxium, 10 minutes, for now.

* Correctly assume UP

* Removed extra cancelRequest in deinint

* Add timeout and UV to confirmChoseCipher

UV is performed by UI, not the service

* Improved docs regarding undefined cipherId

* cleanup: UP is no longer undefined

* Run completeError if ipc messages conversion failed

* don't throw, instead return undefined

* Disabled passkey provider

* Throw error if no activeUserId was found

* removed comment

* Fixed lint

* removed unsued service

* reset entitlement formatting

* Update entitlements.mas.plist

* Fix build issues

* Fix import issues

* Update route names to use `fido2`

* Fix being unable to select a passkey

* Fix linting issues

* Added support for handling a locked vault

* Followup to fix merge issues and other comments

* Update `userHandle` value

* Add error handling for missing session or other errors

* Remove unused route

* Fix linting issues

* Simplify updateCredential method

* Add master password reprompt on passkey create

* Followup to remove comments and timeouts and handle errors

* Address lint issue by using `takeUntilDestroyed`

* Add MP prompt to cipher selection

* Change how timeout is handled

* Include `of` from rxjs

* Hide blue header for passkey popouts (#14095)

* Hide blue header for passkey popouts

* Fix issue with test

* Fix ngOnDestroy complaint

* Import OnDestroy correctly

* Only require master password if item requires it

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: Anders Åberg <github@andersaberg.com>
Co-authored-by: Anders Åberg <anders@andersaberg.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Change modal size to 600x600

* Improve MacOS Syncing

This changes the behaviour to react to logoff, but not to account locks. It also adds better error handling on the native side.

* Improved modalPosition by allowing multiple calls to applyModalStyles

* moved imports to please lint

* Make passkey header stick for select and create (#14357)

* Added local build command

* Exclude credentials using kvc to avoid comilation error in cicd (#14568)

* Fix syntax error

* Don't use kvc

* Enables the autofill extension in mac and mas builds (#14373)

* Enables autofill extension building

* Try use macos-14

* add --break-system-packages for macos14

* revert using build-native

* try add rustup target add x86_64-apple-darwin

* add more rustup target add x86_64-apple-darwin

* try to force sdk version

* Show SDK versions

* USE KVC for excludedCredentials

* added xcodebuild deugging

* Revert "try to force sdk version"

This reverts commit d94f2550ad.

* Use macos-15

* undo merge

* remove macos-15 from cli

* remove macos-15 from browser

---------

Co-authored-by: Anders Åberg <anders@andersaberg.com>

* Improve Autofill IPC reliability (#14358)

* Delay IPC server start

* Better ipc handling

* Rename ready() to listenerReady()

---------

Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>

* feat: add test and check for too long buffers (#14775)

* Autofill/PM-19511: Overwrite and reprompt (#14288)

* Show items for url that don't have passkey

* Show existing login items in the UI

* Filter available cipher results (#14399)

* Filter available cipher results

* Fix linting issues

* Update logic for eligible ciphers

* Remove unused method to check matching username

* PM-20608 update styling for excludedCredentials (#14444)

* PM-20608 update styling for excludedCredentials

* Have flow correctly move to creation for excluded cipher

* Remove duplicate confirmNeCredential call

* Revert fido2-authenticator changes and move the excluded check

* Create a separate component for excluded cipher view

* Display traffic light MacOS buttons when the vault is locked (#14673)

* Remove unneccessary filter for excludedCiphers

* Remove dead code from the excluded ciphers work

* Remove excludedCipher checks from fido2 create and vault

* Remove excludedCipher remnants from vault and simplify create cipher logic

* Move cipherHasNoOtherPasskeys to shared fido2-utils

* Remove all containsExcludedCipher references

* Use `bufferToString` to convert `userHandle`

---------

Co-authored-by: Jeffrey Holland <jholland@livefront.com>
Co-authored-by: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com>

* Move modal files to `autofill` and rename dir to `credentials` (#14757)

* Show existing login items in the UI

* Filter available cipher results (#14399)

* Filter available cipher results

* Fix linting issues

* Update logic for eligible ciphers

* Remove unused method to check matching username

* PM-20608 update styling for excludedCredentials (#14444)

* PM-20608 update styling for excludedCredentials

* Have flow correctly move to creation for excluded cipher

* Remove duplicate confirmNeCredential call

* Revert fido2-authenticator changes and move the excluded check

* Create a separate component for excluded cipher view

* Display traffic light MacOS buttons when the vault is locked (#14673)

* Remove unneccessary filter for excludedCiphers

* Remove dead code from the excluded ciphers work

* Remove excludedCipher checks from fido2 create and vault

* Move modal files to `autofill` and rename dir to `credentials`

* Update merge issues

* Add tests for `cipherHasNoOtherPasskeys` (#14829)

* Adjust spacing to place new login button below other items (#14877)

* Adjust spacing to place new login button below other items

* Add correct design when no credentials available (#14879)

* Autofill/pm 21903 use translations everywhere for passkeys (#14908)

* Adjust spacing to place new login button below other items

* Add correct design when no credentials available

* Add correct design when no credentials available (#14879)

* Remove hardcoded strings and use translations in passkey flow

* Remove duplicate `select` translation

* Autofill/pm 21864 center unlock vault modal (#14867)

* Center the Locked Vault modal when using passkeys

* Revert swift changes and handle offscreen modals

* Remove comments

* Add rustup for cicd to work (#15055)

* Hide credentials that are in the bin (#15034)

* Add tests for passkey components (#15185)

* Add tests for passkey components

* Reuse cipher in chooseCipher tests and simplify mock creation

* Autofill/pm 22821 center vault modal (#15243)

* Center the vault modal for passkeys

* Add comments and fix electron-builder.json

* Set values to Int32 in the ternaries

* Refactor Fido2 Components (#15105)

* Refactor Fido2 Components

* Address error message and missing session

* Address remaining missing session

* Reset modals so subsequent creates work (#15145)

* Fix broken test

* Rename relevantCiphers to displayedCiphers

* Clean up heading settings, errors, and other concerns

* Address missing comments and throw error in try block

* fix type issue for SimpleDialogType

* fix type issue for SimpleDialogType

* Revert new type

* try using as null to satisfy type issue

* Remove use of firstValueFrom in create component

* PM-22476: Show config UI while enabling Bitwarden (#15149)

* Show config ui while enabling Bitwarden

* locals

* Added Localizable strings

* Changed the linebreakmode

* Removed swedish locals

* Add provisioning profile values to electron build (#15412)

* Address BitwardenShield icon issue

* Fix fido2-vault component

* Display the vault modal when selecting Bitwarden... (#15257)

* Passkeys filtering breaks on SSH keys (#15448)

* Display the blue header on the locked vault passkey flow (#15655)

* PM-23848: Use the MacOS UI-friendly API instead (#15650)

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Fix action text and close vault modal (#15634)

* Fix action text and close vault modal

* Fix broken tests

* Update SVG to support dark mode (#15805)

* When a locked vault is unlocked displays correctly (#15612)

* When a locked vault is unlocked displays correctly

* Keep old behavior while checking for recently unlocked vault

* Revert the electron-builder

* Simplify by using a simple redirect when vault unlocked

* Remove single use of `userSelectedCipher`

* Add a guard clause to unlock

* Revert to original spacing

* Add reactive guard to unlock vault

* Fix for passkey picker closing prematurely

* Remove unneeded root navigation in ensureUnlockedVault

* Fix vault not unlocking

* Update broken tests for lock component

* Add missing brace to preload.ts

* Run lint

* Added explainer

* Moved the explainer

* Tidying up readme

* Add feature flag to short-circuit the passkey provider (#16003)

* Add feature flag to short-circuit the passkey provider

* Check FF in renderer instead

* Lint fixes

* PM-22175: Improve launch of app + window positioning (#15658)

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Implement prepareInterfaceToProvideCredential

* Fix launch of app + window pos

* Wait for animation to complete and use proper position

* Wait for animation to complete and use proper position

* Added commentary

* Remove console.log

* Remove call to removed function

---------

Co-authored-by: Jeffrey Holland <jholland@livefront.com>
Co-authored-by: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com>

* Update fido2-vault and fido2-service implementations

* Use tailwind-alike classes for new styles

* Add label to biticons in passkey modals

* Fix broken vault test

* Revert to original `isDev` function

* Add comment to lock component describing `disable-redirect` param

* Use tailwind classes instead of custom sticky header class

* Use standard `tw-z-10` for z-index

* Change log service levels

* Mock svg icons for CI

* Add back provisioning profiles

* Remove `--break-system-packages` and simplify commands

* Revert `cipherId` param for `confirmNewCredential`

* Remove placeholder UI

* Small improvements to the readme

* Remove optional userId and deprecated method

* Autofill should own the macos_provider (#16271)

* Autofill should own the macos_provider

* Autofill should own the macos_provider

* Remove unnecessary logs, no magic numbers, revert `cipherId?`

* Fixes for broken build

* Update test issues

* [BEEEP] Use tracing in macOS provider

* Update comments and add null check for ciphers

* Update status comments and readme

* Remove electron modal mode link

* Clarify modal mode use

* Add comment about usernames

* Add comment that we don't support extensions yet

* Added comment about base64 format

* Use NO_CALLBACK_INDICATOR

* cb -> callback

* Update apps/desktop/desktop_native/napi/src/lib.rs

Co-authored-by: neuronull <9162534+neuronull@users.noreply.github.com>

* Clean up Fido2Create subscriptions and update comments

* added comment to clarify silent exception

* Add comments

* clean up unwrap()

* set log level filter to INFO

* Address modal popup issue

* plutil on Info.plist

* Adhere to style guides

* Fix broken lock ui component tests

* Fix broken lock ui component tests

* Added codeowners entry

* logservice.warning -> debug

* Uint8Array -> ArrayBuffer

* Remove autofill entitlement

* Fix linting issues

* Fix arm build issue

* Adjust build command

* Add missing entitlement

* revert missing entitlement change

* Add proper autofill entitlements

* Remove autofill extension from mas builds

* Run rust formatter

---------

Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
Co-authored-by: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com>
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Nathan Ansel <nathan@livefront.com>
Co-authored-by: Jeffrey Holland <jholland@livefront.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: neuronull <9162534+neuronull@users.noreply.github.com>
2025-12-05 12:58:20 -05:00

889 lines
36 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
Component,
DestroyRef,
NgZone,
OnDestroy,
OnInit,
Type,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router } from "@angular/router";
import {
filter,
firstValueFrom,
lastValueFrom,
map,
Subject,
switchMap,
takeUntil,
timeout,
} from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import {
DESKTOP_SSO_CALLBACK,
LockService,
LogoutReason,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import {
VaultTimeout,
VaultTimeoutAction,
VaultTimeoutService,
VaultTimeoutSettingsService,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { DialogRef, DialogService, ToastOptions, ToastService } from "@bitwarden/components";
import { CredentialGeneratorHistoryDialogComponent } from "@bitwarden/generator-components";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
import { AddEditFolderDialogComponent, AddEditFolderDialogResult } from "@bitwarden/vault";
import { DeleteAccountComponent } from "../auth/delete-account.component";
import { DesktopAutotypeDefaultSettingPolicy } from "../autofill/services/desktop-autotype-policy.service";
import { PremiumComponent } from "../billing/app/accounts/premium.component";
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
import { SettingsComponent } from "./accounts/settings.component";
import { ExportDesktopComponent } from "./tools/export/export-desktop.component";
import { CredentialGeneratorComponent } from "./tools/generator/credential-generator.component";
import { ImportDesktopComponent } from "./tools/import/import-desktop.component";
const BroadcasterSubscriptionId = "AppComponent";
const IdleTimeout = 60000 * 10; // 10 minutes
const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-root",
styles: [],
template: `
<ng-template #settings></ng-template>
<ng-template #premium></ng-template>
<ng-template #passwordHistory></ng-template>
<ng-template #exportVault></ng-template>
<ng-template #appGenerator></ng-template>
<ng-template #loginApproval></ng-template>
<app-header *ngIf="showHeader$ | async"></app-header>
<div id="container">
<div class="loading" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<router-outlet *ngIf="!loading"></router-outlet>
</div>
<bit-toast-container></bit-toast-container>
`,
standalone: false,
})
export class AppComponent implements OnInit, OnDestroy {
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild("premium", { read: ViewContainerRef, static: true }) premiumRef: ViewContainerRef;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild("passwordHistory", { read: ViewContainerRef, static: true })
passwordHistoryRef: ViewContainerRef;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild("exportVault", { read: ViewContainerRef, static: true })
exportVaultModalRef: ViewContainerRef;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild("appGenerator", { read: ViewContainerRef, static: true })
generatorModalRef: ViewContainerRef;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@ViewChild("loginApproval", { read: ViewContainerRef, static: true })
loginApprovalModalRef: ViewContainerRef;
showHeader$ = this.accountService.showHeader$;
loading = false;
private lastActivity: Date = null;
private modal: ModalRef = null;
private idleTimer: number = null;
private isIdle = false;
private activeUserId: UserId = null;
private activeSimpleDialog: DialogRef<boolean> = null;
private destroy$ = new Subject<void>();
private accountCleanUpInProgress: { [userId: string]: boolean } = {};
constructor(
private masterPasswordService: MasterPasswordServiceAbstraction,
private broadcasterService: BroadcasterService,
private folderService: InternalFolderService,
private syncService: SyncService,
private cipherService: CipherService,
private authService: AuthService,
private router: Router,
private toastService: ToastService,
private i18nService: I18nService,
private ngZone: NgZone,
private vaultTimeoutService: VaultTimeoutService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private keyService: KeyService,
private logService: LogService,
private messagingService: MessagingService,
private collectionService: CollectionService,
private searchService: SearchService,
private notificationsService: ServerNotificationsService,
private platformUtilsService: PlatformUtilsService,
private systemService: SystemService,
private processReloadService: ProcessReloadServiceAbstraction,
private stateService: StateService,
private eventUploadService: EventUploadService,
private policyService: InternalPolicyService,
private modalService: ModalService,
private keyConnectorService: KeyConnectorService,
private userVerificationService: UserVerificationService,
private configService: ConfigService,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private accountService: AccountService,
private organizationService: OrganizationService,
private deviceTrustToastService: DeviceTrustToastService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private readonly destroyRef: DestroyRef,
private readonly documentLangSetter: DocumentLangSetter,
private restrictedItemTypesService: RestrictedItemTypesService,
private pinService: PinServiceAbstraction,
private readonly tokenService: TokenService,
private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy,
private readonly lockService: LockService,
) {
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
const langSubscription = this.documentLangSetter.start();
this.destroyRef.onDestroy(() => langSubscription.unsubscribe());
}
ngOnInit() {
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
this.activeUserId = account?.id;
});
this.ngZone.runOutsideAngular(() => {
setTimeout(async () => {
await this.updateAppMenu();
}, 1000);
window.ontouchstart = () => this.recordActivity();
window.onmousedown = () => this.recordActivity();
window.onscroll = () => this.recordActivity();
window.onkeypress = () => this.recordActivity();
});
/// ############ DEPRECATED ############
/// Please do not use the AppComponent to send events between services.
///
/// Services that depends on other services, should do so through Dependency Injection
/// and subscribe to events through that service observable.
///
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
// 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.ngZone.run(async () => {
switch (message.command) {
case "loggedIn":
case "unlocked":
// 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.recordActivity();
// 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.updateAppMenu();
this.processReloadService.cancelProcessReload();
break;
case "loggedOut":
this.modalService.closeAll();
// 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.updateAppMenu();
await this.systemService.clearPendingClipboard();
await this.processReloadService.startProcessReload();
break;
case "authBlocked":
// 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(["login"]);
break;
case "logout":
this.loading = message.userId == null || message.userId === this.activeUserId;
await this.logOut(message.logoutReason, message.userId);
this.loading = false;
break;
case "lockVault":
await this.lockService.lock(message.userId);
break;
case "lockAllVaults": {
await this.lockService.lockAll();
break;
}
case "locked":
this.modalService.closeAll();
if (
message.userId == null ||
message.userId ===
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))))
) {
await this.router.navigate(["lock"]);
}
await this.updateAppMenu();
await this.systemService.clearPendingClipboard();
await this.processReloadService.startProcessReload();
break;
case "startProcessReload":
// 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.processReloadService.startProcessReload();
break;
case "cancelProcessReload":
this.processReloadService.cancelProcessReload();
break;
case "reloadProcess":
ipc.platform.reloadProcess();
break;
case "syncStarted":
break;
case "syncCompleted":
if (message.successfully) {
// 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.updateAppMenu();
await this.configService.ensureConfigFetched();
}
break;
case "openSettings":
await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef);
break;
case "openPremium":
this.dialogService.open(PremiumComponent);
break;
case "showFingerprintPhrase": {
const activeUserId = await firstValueFrom(
getUserId(this.accountService.activeAccount$),
);
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
if (publicKey == null) {
this.logService.error(
"[AppComponent] No public key available for the user: " +
activeUserId +
" fingerprint can't be displayed.",
);
} else {
const fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
const dialogRef = FingerprintDialogComponent.open(this.dialogService, {
fingerprint,
});
await firstValueFrom(dialogRef.closed);
}
break;
}
case "deleteAccount":
await this.deleteAccount();
break;
case "openPasswordHistory":
await this.openGeneratorHistory();
break;
case "showToast":
this.toastService._showToast(message);
break;
case "copiedToClipboard":
if (!message.clearing) {
// 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.systemService.clearClipboard(message.clipboardValue, message.clearMs);
}
break;
case "ssoCallback": {
const queryParams = {
code: message.code,
state: message.state,
redirectUri: message.redirectUri ?? DESKTOP_SSO_CALLBACK,
};
// 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(["sso"], {
queryParams: queryParams,
});
break;
}
case "premiumRequired": {
const premiumConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "premiumRequired" },
content: { key: "premiumRequiredDesc" },
acceptButtonText: { key: "learnMore" },
type: "success",
});
if (premiumConfirmed) {
await this.openModal<PremiumComponent>(PremiumComponent, this.premiumRef);
}
break;
}
case "emailVerificationRequired": {
const emailVerificationConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "emailVerificationRequired" },
content: { key: "emailVerificationRequiredDesc" },
acceptButtonText: { key: "learnMore" },
type: "info",
});
if (emailVerificationConfirmed) {
this.platformUtilsService.launchUri(
"https://bitwarden.com/help/create-bitwarden-account/",
);
}
break;
}
case "syncVault":
try {
await this.syncService.fullSync(true, true);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("syncingComplete"),
);
} catch {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("syncingFailed"),
);
}
break;
case "checkSyncVault":
try {
const lastSync = await this.syncService.getLastSync();
let lastSyncAgo = SyncInterval + 1;
if (lastSync != null) {
lastSyncAgo = new Date().getTime() - lastSync.getTime();
}
if (lastSyncAgo >= SyncInterval) {
await this.syncService.fullSync(false);
}
} catch (e) {
this.logService.error(e);
}
this.messagingService.send("scheduleNextSync");
break;
case "importVault":
await this.dialogService.open(ImportDesktopComponent);
break;
case "exportVault":
await this.dialogService.open(ExportDesktopComponent);
break;
case "newLogin":
this.routeToVault("add", CipherType.Login);
break;
case "newCard":
this.routeToVault("add", CipherType.Card);
break;
case "newIdentity":
this.routeToVault("add", CipherType.Identity);
break;
case "newSecureNote":
this.routeToVault("add", CipherType.SecureNote);
break;
default:
break;
case "newFolder":
await this.addFolder();
break;
case "openGenerator":
await this.openGenerator();
break;
case "convertAccountToKeyConnector":
// 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(["/remove-password"]);
break;
case "switchAccount": {
if (message.userId == null) {
this.logService.error("'switchAccount' message received without userId.");
return;
}
await this.accountService.switchAccount(message.userId);
const locked =
(await this.authService.getAuthStatus(message.userId)) ===
AuthenticationStatus.Locked;
if (locked) {
this.modalService.closeAll();
// We only have to handle TDE lock on "switchAccount" message scenarios but not normal
// lock scenarios since the user will have always decrypted the vault at least once in those cases.
const tdeEnabled = await firstValueFrom(
this.userDecryptionOptionsService
.userDecryptionOptionsById$(message.userId)
.pipe(map((decryptionOptions) => decryptionOptions?.trustedDeviceOption != null)),
);
const everHadUserKey = await firstValueFrom(
this.keyService.everHadUserKey$(message.userId),
);
if (tdeEnabled && !everHadUserKey) {
await this.router.navigate(["login-initiated"]);
} else {
await this.router.navigate(["lock"]);
}
} else {
this.messagingService.send("unlocked");
this.loading = true;
await this.syncService.fullSync(false);
this.loading = false;
// 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(["vault"]);
}
this.messagingService.send("finishSwitchAccount");
break;
}
case "systemSuspended":
await this.checkForSystemTimeout(VaultTimeoutStringType.OnSleep);
break;
case "systemLocked":
await this.checkForSystemTimeout(VaultTimeoutStringType.OnLocked);
break;
case "systemIdle":
await this.checkForSystemTimeout(VaultTimeoutStringType.OnIdle);
break;
case "openLoginApproval":
if (message.notificationId != null) {
this.dialogService.closeAll();
const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, {
notificationId: message.notificationId,
});
await firstValueFrom(dialogRef.closed);
}
break;
case "redrawMenu":
await this.updateAppMenu();
break;
case "deepLink":
this.processDeepLink(message.urlString);
break;
case "launchUri":
this.platformUtilsService.launchUri(message.url);
break;
}
});
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async addFolder() {
this.modalService.closeAll();
const dialogRef = AddEditFolderDialogComponent.open(this.dialogService);
const result = await lastValueFrom(dialogRef.closed);
if (result === AddEditFolderDialogResult.Created) {
await this.syncService.fullSync(false);
}
}
async openGenerator() {
await this.dialogService.open(CredentialGeneratorComponent);
return;
}
async openGeneratorHistory() {
await this.dialogService.open(CredentialGeneratorHistoryDialogComponent);
return;
}
private async updateAppMenu() {
let updateRequest: MenuUpdateRequest;
const stateAccounts = await firstValueFrom(this.accountService.accounts$);
if (stateAccounts == null || Object.keys(stateAccounts).length < 1) {
updateRequest = {
accounts: null,
activeUserId: null,
restrictedCipherTypes: null,
};
} else {
const accounts: { [userId: string]: MenuAccount } = {};
for (const i in stateAccounts) {
const userId = i as UserId;
if (
i != null &&
userId != null &&
!this.isAccountCleanUpInProgress(userId) // skip accounts that are being cleaned up
) {
const availableTimeoutActions = await firstValueFrom(
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId),
);
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
accounts[userId] = {
isAuthenticated: authStatus >= AuthenticationStatus.Locked,
isLocked: authStatus === AuthenticationStatus.Locked,
isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock),
email: stateAccounts[userId].email,
userId: userId,
hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId),
};
}
}
updateRequest = {
accounts: accounts,
activeUserId: await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
),
restrictedCipherTypes: (
await firstValueFrom(this.restrictedItemTypesService.restricted$)
).map((restrictedItems) => restrictedItems.cipherType),
};
}
this.messagingService.send("updateAppMenu", { updateRequest: updateRequest });
}
private async displayLogoutReason(logoutReason: LogoutReason) {
let toastOptions: ToastOptions;
switch (logoutReason) {
case "invalidSecurityStamp":
case "sessionExpired": {
toastOptions = {
variant: "warning",
title: this.i18nService.t("loggedOut"),
message: this.i18nService.t("loginExpired"),
};
break;
}
// We don't expect these scenarios to be common, but we want the user to
// understand why they are being logged out before a process reload.
case "accessTokenUnableToBeDecrypted": {
// Don't create multiple dialogs if this fires multiple times
if (this.activeSimpleDialog) {
// Let the caller of this function listen for the dialog to close
return firstValueFrom(this.activeSimpleDialog.closed);
}
this.activeSimpleDialog = this.dialogService.openSimpleDialogRef({
title: { key: "loggedOut" },
content: { key: "accessTokenUnableToBeDecrypted" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
await firstValueFrom(this.activeSimpleDialog.closed);
this.activeSimpleDialog = null;
break;
}
case "refreshTokenSecureStorageRetrievalFailure": {
// Don't create multiple dialogs if this fires multiple times
if (this.activeSimpleDialog) {
// Let the caller of this function listen for the dialog to close
return firstValueFrom(this.activeSimpleDialog.closed);
}
this.activeSimpleDialog = this.dialogService.openSimpleDialogRef({
title: { key: "loggedOut" },
content: { key: "refreshTokenSecureStorageRetrievalFailure" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
await firstValueFrom(this.activeSimpleDialog.closed);
this.activeSimpleDialog = null;
break;
}
}
if (toastOptions) {
this.toastService.showToast(toastOptions);
}
}
// TODO: PM-21212 - consolidate the logic of this method into the new LogoutService
// (requires creating a desktop specific implementation of the LogoutService)
// Even though the userId parameter is no longer optional doesn't mean a message couldn't be
// passing null-ish values to us.
private async logOut(logoutReason: LogoutReason, userId: UserId) {
await this.displayLogoutReason(logoutReason);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const userBeingLoggedOut = userId ?? activeUserId;
// Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted)
// doesn't attempt to update a user that is being logged out as we will manually
// call updateAppMenu when the logout is complete.
this.startAccountCleanUp(userBeingLoggedOut);
const nextUpAccount =
activeUserId === userBeingLoggedOut
? await firstValueFrom(this.accountService.nextUpAccount$) // We'll need to switch accounts
: null;
try {
// HACK: We shouldn't wait for authentication status to change here but instead subscribe to the
// authentication status to do various actions.
const logoutPromise = firstValueFrom(
this.authService.authStatusFor$(userBeingLoggedOut).pipe(
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
timeout({
first: 5_000,
with: () => {
throw new Error(
"The logout process did not complete in a reasonable amount of time.",
);
},
}),
),
);
// Provide the userId of the user to upload events for
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
await this.keyService.clearKeys(userBeingLoggedOut);
await this.cipherService.clear(userBeingLoggedOut);
await this.folderService.clear(userBeingLoggedOut);
await this.biometricStateService.logout(userBeingLoggedOut);
await this.pinService.logout(userBeingLoggedOut);
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
await this.stateService.clean({ userId: userBeingLoggedOut });
await this.tokenService.clearAccessToken(userBeingLoggedOut);
await this.accountService.clean(userBeingLoggedOut);
// HACK: Wait for the user logging outs authentication status to transition to LoggedOut
await logoutPromise;
} finally {
this.finishAccountCleanUp(userBeingLoggedOut);
}
// We only need to change the display at all if the account being looked at is the one
// being logged out. If it was a background account, no need to do anything.
if (userBeingLoggedOut === activeUserId) {
if (nextUpAccount != null) {
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
} else {
// We don't have another user to switch to, bring them to the login page so they
// can sign into a user.
await this.accountService.switchAccount(null);
void this.router.navigate(["login"]);
}
}
// This must come last otherwise the logout will prematurely trigger
// a process reload before all the state service user data can be cleaned up
this.authService.logOut(async () => {}, userBeingLoggedOut);
}
private async recordActivity() {
if (this.activeUserId == null) {
return;
}
const now = new Date();
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
return;
}
this.lastActivity = now;
await this.accountService.setAccountActivity(this.activeUserId, now);
// Idle states
if (this.isIdle) {
this.isIdle = false;
this.idleStateChanged();
}
if (this.idleTimer != null) {
window.clearTimeout(this.idleTimer);
this.idleTimer = null;
}
this.idleTimer = window.setTimeout(() => {
if (!this.isIdle) {
this.isIdle = true;
this.idleStateChanged();
}
}, IdleTimeout);
}
private idleStateChanged() {
if (this.isIdle) {
this.notificationsService.disconnectFromInactivity();
} else {
this.notificationsService.reconnectFromActivity();
}
}
private async openModal<T>(type: Type<T>, ref: ViewContainerRef) {
this.modalService.closeAll();
[this.modal] = await this.modalService.openViewRef(type, ref);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
}
private routeToVault(action: string, cipherType: CipherType) {
if (!this.router.url.includes("vault")) {
// 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(["/vault"], {
queryParams: {
action: action,
addType: cipherType,
},
replaceUrl: true,
});
}
}
private async checkForSystemTimeout(timeout: VaultTimeout): Promise<void> {
const accounts = await firstValueFrom(this.accountService.accounts$);
for (const userId in accounts) {
if (userId == null) {
continue;
}
const options = await this.getVaultTimeoutOptions(userId);
if (options[0] === timeout) {
options[1] === "logOut"
? await this.logOut("vaultTimeout", userId as UserId)
: await this.lockService.lock(userId as UserId);
}
}
}
private async getVaultTimeoutOptions(userId: string): Promise<[VaultTimeout, string]> {
const timeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId),
);
const action = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId),
);
return [timeout, action];
}
// Mark an account's clean up as started
private startAccountCleanUp(userId: string): void {
this.accountCleanUpInProgress[userId] = true;
}
// Mark an account's clean up as finished
private finishAccountCleanUp(userId: string): void {
this.accountCleanUpInProgress[userId] = false;
}
// Check if an account's clean up is in progress
private isAccountCleanUpInProgress(userId: string): boolean {
return this.accountCleanUpInProgress[userId] === true;
}
// Process the sso callback links
private processDeepLink(urlString: string) {
const url = new URL(urlString);
const code = url.searchParams.get("code");
const receivedState = url.searchParams.get("state");
let message = "";
if (code === null) {
return;
}
if (urlString.indexOf("bitwarden://duo-callback") === 0) {
message = "duoCallback";
} else if (receivedState === null) {
return;
}
if (urlString.indexOf("bitwarden://import-callback-lp") === 0) {
message = "importCallbackLastPass";
} else if (urlString.indexOf(DESKTOP_SSO_CALLBACK) === 0) {
message = "ssoCallback";
}
this.messagingService.send(message, { code: code, state: receivedState });
}
private async deleteAccount() {
const userIsManaged = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.organizationService.organizations$(userId)),
map((orgs) => orgs.some((o) => o.userIsManagedByOrganization === true)),
),
);
if (userIsManaged) {
await this.dialogService.openSimpleDialog({
title: { key: "cannotDeleteAccount" },
content: { key: "cannotDeleteAccountDesc" },
cancelButtonText: null,
acceptButtonText: { key: "close" },
type: "danger",
});
return;
}
DeleteAccountComponent.open(this.dialogService);
}
}