diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 93693f183c3..c050ee1f6c0 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -71,6 +71,7 @@ bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev
## Platform team files ##
apps/browser/src/platform @bitwarden/team-platform-dev
apps/cli/src/platform @bitwarden/team-platform-dev
+apps/desktop/macos @bitwarden/team-platform-dev
apps/desktop/src/platform @bitwarden/team-platform-dev
apps/web/src/app/platform @bitwarden/team-platform-dev
libs/angular/src/platform @bitwarden/team-platform-dev
@@ -91,6 +92,7 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev
apps/browser/src/autofill @bitwarden/team-autofill-dev
apps/desktop/src/autofill @bitwarden/team-autofill-dev
libs/common/src/autofill @bitwarden/team-autofill-dev
+apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
# DuckDuckGo integration
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
apps/desktop/src/services/native-message-handler.service.ts @bitwarden/team-autofill-dev
diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml
index 221c998247f..bb2889983b4 100644
--- a/.github/workflows/build-desktop.yml
+++ b/.github/workflows/build-desktop.yml
@@ -1164,6 +1164,21 @@ jobs:
--file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \
--output none
+ az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
+ --name bitwarden_desktop_autofill_app_store_2024.provisionprofile \
+ --file $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \
+ --output none
+
+ - name: Set up provisioning profiles
+ run: |
+ AUTOFILL_PROFILE_PATH=$HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile
+ PROFILES_DIR_PATH=$HOME/Library/MobileDevice/Provisioning\ Profiles
+
+ mkdir -p "$PROFILES_DIR_PATH"
+
+ AUTOFILL_UUID=$(grep UUID -A1 -a $AUTOFILL_PROFILE_PATH | grep -io "[-A-F0-9]\{36\}")
+ cp $AUTOFILL_PROFILE_PATH "$PROFILES_DIR_PATH/$AUTOFILL_UUID.provisionprofile"
+
- name: Get certificates
run: |
mkdir -p $HOME/certificates
@@ -1215,11 +1230,6 @@ jobs:
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
- - name: Set up provisioning profiles
- run: |
- cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \
- $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile
-
- name: Increment version
shell: pwsh
env:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 561cd9af0c8..9dc72c7fdda 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -36,6 +36,7 @@ jobs:
! -path "./.github/*" \
! -path "*/Cargo.toml" \
! -path "*/Cargo.lock" \
+ ! -path "./apps/desktop/macos/*" \
> tmp.txt
diff <(sort .github/whitelist-capital-letters.txt) <(sort tmp.txt)
diff --git a/apps/browser/package.json b/apps/browser/package.json
index 4a749522545..f79d646d851 100644
--- a/apps/browser/package.json
+++ b/apps/browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
- "version": "2024.11.0",
+ "version": "2024.11.1",
"scripts": {
"build": "cross-env MANIFEST_VERSION=3 webpack",
"build:mv2": "webpack",
diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json
index 0d9a4189578..46aa3cefd70 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.11.0",
+ "version": "2024.11.1",
"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 f805b701551..8118c212fc7 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.11.0",
+ "version": "2024.11.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html
index 23cc692a598..c0178143329 100644
--- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html
+++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html
@@ -6,7 +6,7 @@
-
+
{{ "sendDisabledWarning" | i18n }}
diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore
index 040b2179fab..444c9a85100 100644
--- a/apps/desktop/.gitignore
+++ b/apps/desktop/.gitignore
@@ -2,3 +2,4 @@ dist-safari/
*.nupkg
*.env
PlugIns/safari.appex/
+xcuserdata/
diff --git a/apps/desktop/macos/README.md b/apps/desktop/macos/README.md
new file mode 100644
index 00000000000..6e016144b4b
--- /dev/null
+++ b/apps/desktop/macos/README.md
@@ -0,0 +1,23 @@
+# MacOS Extensions for Desktop Apps
+
+This folder contains an Xcode project that builds macOS extensions for our desktop app. The extensions are used to provide additional functionality to the desktop app, such as autofill (password and passkeys).
+
+## Manage loaded extensions
+
+macOS automatically loads extensions from apps, even if they have never been used (especially if built with Xcode). This can be confusing when you have multiple copies of the same application. To see where an extension is loaded from, use the following command:
+
+```bash
+# To list all extensions
+pluginkit -m -v
+
+# To list a specific extension
+pluginkit -m -v -i com.bitwarden.desktop.autofill-extension
+```
+
+To unregister an extension, you can either remove it from your filesystem, or use the following command:
+
+```bash
+pluginkit -r
+```
+
+where the path to the .appex file can be found in the output of the first command.
diff --git a/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib
new file mode 100644
index 00000000000..ace3497a58b
--- /dev/null
+++ b/apps/desktop/macos/autofill-extension/Base.lproj/CredentialProviderViewController.xib
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift
new file mode 100644
index 00000000000..d5c5cabeee4
--- /dev/null
+++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift
@@ -0,0 +1,93 @@
+//
+// CredentialProviderViewController.swift
+// autofill-extension
+//
+// Created by Andreas Coroiu on 2023-12-21.
+//
+
+import AuthenticationServices
+import os
+
+class CredentialProviderViewController: ASCredentialProviderViewController {
+ let logger = Logger()
+
+ /*
+ Implement this method if your extension supports showing credentials in the QuickType bar.
+ When the user selects a credential from your app, this method will be called with the
+ ASPasswordCredentialIdentity your app has previously saved to the ASCredentialIdentityStore.
+ Provide the password by completing the extension request with the associated ASPasswordCredential.
+ If using the credential would require showing custom UI for authenticating the user, cancel
+ the request with error code ASExtensionError.userInteractionRequired.
+
+ override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
+ let databaseIsUnlocked = true
+ if (databaseIsUnlocked) {
+ let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
+ self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
+ } else {
+ self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue))
+ }
+ }
+ */
+
+ /*
+ Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
+ ASExtensionError.userInteractionRequired. In this case, the system may present your extension's
+ UI and call this method. Show appropriate UI for authenticating the user then provide the password
+ by completing the extension request with the associated ASPasswordCredential.
+
+ override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
+ }
+ */
+
+ @IBAction func cancel(_ sender: AnyObject?) {
+ self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
+ }
+
+ @IBAction func passwordSelected(_ sender: AnyObject?) {
+ let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
+ self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
+ }
+
+ override func prepareInterfaceForExtensionConfiguration() {
+ logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called")
+ }
+
+ override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) {
+ logger.log("[autofill-extension] prepare interface for registration request \(registrationRequest.description)")
+
+// self.extensionContext.cancelRequest(withError: ExampleError.nope)
+ }
+
+ override func prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) {
+ logger.log("[autofill-extension] prepare interface for credential request \(credentialRequest.description)")
+ }
+
+ /*
+ Prepare your UI to list available credentials for the user to choose from. The items in
+ 'serviceIdentifiers' describe the service the user is logging in to, so your extension can
+ prioritize the most relevant credentials in the list.
+ */
+ override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
+ logger.log("[autofill-extension] prepareCredentialList for serviceIdentifiers: \(serviceIdentifiers.count)")
+
+ for serviceIdentifier in serviceIdentifiers {
+ logger.log(" service: \(serviceIdentifier.identifier)")
+ }
+ }
+
+ override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
+ logger.log("[autofill-extension] prepareInterfaceToProvideCredential for credentialIdentity: \(credentialIdentity.user)")
+ }
+
+ override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier], requestParameters: ASPasskeyCredentialRequestParameters) {
+ logger.log("[autofill-extension] prepareCredentialList(passkey) for serviceIdentifiers: \(serviceIdentifiers.count)")
+
+ for serviceIdentifier in serviceIdentifiers {
+ logger.log(" service: \(serviceIdentifier.identifier)")
+ }
+
+ logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)")
+ }
+
+}
diff --git a/apps/desktop/macos/autofill-extension/Info.plist b/apps/desktop/macos/autofill-extension/Info.plist
new file mode 100644
index 00000000000..539cfa35b9d
--- /dev/null
+++ b/apps/desktop/macos/autofill-extension/Info.plist
@@ -0,0 +1,23 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionAttributes
+
+ ASCredentialProviderExtensionCapabilities
+
+ ProvidesPasskeys
+
+
+ ASCredentialProviderExtensionShowsConfigurationUI
+
+
+ NSExtensionPointIdentifier
+ com.apple.authentication-services-credential-provider-ui
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).CredentialProviderViewController
+
+
+
diff --git a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements
new file mode 100644
index 00000000000..2e600a8d529
--- /dev/null
+++ b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.developer.authentication-services.autofill-credential-provider
+
+ com.apple.security.app-sandbox
+
+
+
diff --git a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj
new file mode 100644
index 00000000000..313b158895c
--- /dev/null
+++ b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj
@@ -0,0 +1,367 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; };
+ E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; };
+ E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; };
+ E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+ E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; };
+ E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialProviderViewController.swift; sourceTree = ""; };
+ E1DF71442B342F6900F29026 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/CredentialProviderViewController.xib; sourceTree = ""; };
+ E1DF71462B342F6900F29026 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ E1DF71472B342F6900F29026 /* autofill_extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = autofill_extension.entitlements; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ E1DF71392B342F6900F29026 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ E1DF711D2B342E2800F29026 = {
+ isa = PBXGroup;
+ children = (
+ 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */,
+ E1DF71402B342F6900F29026 /* autofill-extension */,
+ E1DF713D2B342F6900F29026 /* Frameworks */,
+ E1DF71272B342E2800F29026 /* Products */,
+ );
+ sourceTree = "";
+ };
+ E1DF71272B342E2800F29026 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ E1DF713C2B342F6900F29026 /* autofill-extension.appex */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ E1DF713D2B342F6900F29026 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ E1DF71402B342F6900F29026 /* autofill-extension */ = {
+ isa = PBXGroup;
+ children = (
+ E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */,
+ E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */,
+ E1DF71462B342F6900F29026 /* Info.plist */,
+ E1DF71472B342F6900F29026 /* autofill_extension.entitlements */,
+ );
+ path = "autofill-extension";
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ E1DF713B2B342F6900F29026 /* autofill-extension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = E1DF714E2B342F6900F29026 /* Build configuration list for PBXNativeTarget "autofill-extension" */;
+ buildPhases = (
+ E1DF71382B342F6900F29026 /* Sources */,
+ E1DF71392B342F6900F29026 /* Frameworks */,
+ E1DF713A2B342F6900F29026 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "autofill-extension";
+ productName = "autofill-extension";
+ productReference = E1DF713C2B342F6900F29026 /* autofill-extension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ E1DF711E2B342E2800F29026 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1510;
+ LastUpgradeCheck = 1510;
+ TargetAttributes = {
+ E1DF713B2B342F6900F29026 = {
+ CreatedOnToolsVersion = 15.1;
+ };
+ };
+ };
+ buildConfigurationList = E1DF71212B342E2800F29026 /* Build configuration list for PBXProject "desktop" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = E1DF711D2B342E2800F29026;
+ productRefGroup = E1DF71272B342E2800F29026 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ E1DF713B2B342F6900F29026 /* autofill-extension */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ E1DF713A2B342F6900F29026 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ E1DF71382B342F6900F29026 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ E1DF71442B342F6900F29026 /* Base */,
+ );
+ name = CredentialProviderViewController.xib;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ E1DF71332B342E2900F29026 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 14.2;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ E1DF71342B342E2900F29026 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 14.2;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ };
+ name = Release;
+ };
+ E1DF714C2B342F6900F29026 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements";
+ CODE_SIGN_IDENTITY = "Apple Development";
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = LTZ2PFU5D6;
+ ENABLE_HARDENED_RUNTIME = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "autofill-extension/Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = Bitwarden;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.bitwarden.desktop.autofill-extension";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Bitwarden Desktop Autofill Development 2024";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ E1DF714D2B342F6900F29026 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = "autofill-extension/autofill_extension.entitlements";
+ CODE_SIGN_IDENTITY = "Apple Development";
+ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=macosx*]" = LTZ2PFU5D6;
+ ENABLE_HARDENED_RUNTIME = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "autofill-extension/Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = Bitwarden;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "com.bitwarden.desktop.autofill-extension";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Bitwarden Desktop Autofill Development 2024";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ E1DF71212B342E2800F29026 /* Build configuration list for PBXProject "desktop" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E1DF71332B342E2900F29026 /* Debug */,
+ E1DF71342B342E2900F29026 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ E1DF714E2B342F6900F29026 /* Build configuration list for PBXNativeTarget "autofill-extension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ E1DF714C2B342F6900F29026 /* Debug */,
+ E1DF714D2B342F6900F29026 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = E1DF711E2B342E2800F29026 /* Project object */;
+}
diff --git a/apps/desktop/macos/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/desktop/macos/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000000..18d981003d6
--- /dev/null
+++ b/apps/desktop/macos/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/apps/desktop/macos/production.xcconfig b/apps/desktop/macos/production.xcconfig
new file mode 100644
index 00000000000..f06f2bf736e
--- /dev/null
+++ b/apps/desktop/macos/production.xcconfig
@@ -0,0 +1,11 @@
+//
+// Production.xcconfig
+// desktop
+//
+// Created by Vince Grassia on 7/25/24.
+//
+
+// Configuration settings file format documentation can be found at:
+// https://help.apple.com/xcode/#/dev745c5c974
+CODE_SIGN_IDENTITY[sdk=macosx*] = 3rd Party Mac Developer Application
+PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = Bitwarden Desktop Autofill App Store 2024
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index c9e33b7110a..423650cdcec 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -23,6 +23,7 @@
"build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"",
"build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js",
"build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch",
+ "build:macos-extension": "node scripts/build-macos-extension.js",
"build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js",
"build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js",
"build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js --watch",
@@ -38,6 +39,7 @@
"pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never",
"pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never",
"pack:mac:masdev": "npm run clean:dist && electron-builder --mac mas-dev --universal -p never",
+ "pack:mac:masdev:with-extension": "npm run clean:dist && npm run build:macos-extension && electron-builder --mac mas-dev --universal -p never",
"pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never -c.win.certificateSubjectName=\"8bit Solutions LLC\"",
"pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never",
"dist:dir": "npm run build && npm run pack:dir",
diff --git a/apps/desktop/resources/entitlements.mac.plist b/apps/desktop/resources/entitlements.mac.plist
index 48f7bf5cece..915232b83f6 100644
--- a/apps/desktop/resources/entitlements.mac.plist
+++ b/apps/desktop/resources/entitlements.mac.plist
@@ -8,5 +8,7 @@
com.apple.security.cs.disable-library-validation
+ com.apple.developer.authentication-services.autofill-credential-provider
+
diff --git a/apps/desktop/resources/entitlements.mas.inherit.plist b/apps/desktop/resources/entitlements.mas.inherit.plist
index 3ee76423e4c..3634c84f81a 100644
--- a/apps/desktop/resources/entitlements.mas.inherit.plist
+++ b/apps/desktop/resources/entitlements.mas.inherit.plist
@@ -10,5 +10,7 @@
com.apple.security.cs.disable-library-validation
+ com.apple.developer.authentication-services.autofill-credential-provider
+
diff --git a/apps/desktop/resources/entitlements.mas.plist b/apps/desktop/resources/entitlements.mas.plist
index d42ade962c3..9ab2d3824a8 100644
--- a/apps/desktop/resources/entitlements.mas.plist
+++ b/apps/desktop/resources/entitlements.mas.plist
@@ -16,6 +16,8 @@
com.apple.security.files.user-selected.read-write
+ com.apple.developer.authentication-services.autofill-credential-provider
+
com.apple.security.temporary-exception.files.home-relative-path.read-write
/Library/Application Support/Mozilla/NativeMessagingHosts/
diff --git a/apps/desktop/resources/memory-dump-wrapper.sh b/apps/desktop/resources/memory-dump-wrapper.sh
index b62c050683a..6737cc312f4 100644
--- a/apps/desktop/resources/memory-dump-wrapper.sh
+++ b/apps/desktop/resources/memory-dump-wrapper.sh
@@ -7,6 +7,12 @@ ulimit -c 0
RAW_PATH=$(readlink -f "$0")
APP_PATH=$(dirname $RAW_PATH)
+# force use of base image libdus in snap
+if [ -e "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" ]
+then
+ export LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libdbus-1.so.3"
+fi
+
# pass through all args
$APP_PATH/bitwarden-app "$@"
diff --git a/apps/desktop/scripts/after-sign.js b/apps/desktop/scripts/after-sign.js
index 69c078a13b5..dc60e9d1838 100644
--- a/apps/desktop/scripts/after-sign.js
+++ b/apps/desktop/scripts/after-sign.js
@@ -15,36 +15,62 @@ async function run(context) {
const appName = context.packager.appInfo.productFilename;
const appPath = `${context.appOutDir}/${appName}.app`;
const macBuild = context.electronPlatformName === "darwin";
- const copyPlugIn = ["darwin", "mas"].includes(context.electronPlatformName);
+ const copySafariExtension = ["darwin", "mas"].includes(context.electronPlatformName);
+ const copyAutofillExtension = ["mas"].includes(context.electronPlatformName);
- if (copyPlugIn) {
+ let shouldResign = false;
+
+ // cannot use extraFiles because it modifies the extensions .plist and makes it invalid
+ if (copyAutofillExtension) {
+ console.log("### Copying autofill extension");
+ const extensionPath = path.join(__dirname, "../macos/dist/autofill-extension.appex");
+ if (!fse.existsSync(extensionPath)) {
+ console.log("### Autofill extension not found - skipping");
+ } else {
+ if (!fse.existsSync(path.join(appPath, "Contents/PlugIns"))) {
+ fse.mkdirSync(path.join(appPath, "Contents/PlugIns"));
+ }
+ fse.copySync(extensionPath, path.join(appPath, "Contents/PlugIns/autofill-extension.appex"));
+ shouldResign = true;
+ }
+ }
+
+ if (copySafariExtension) {
+ console.log("### Copying safari extension");
// Copy Safari plugin to work-around https://github.com/electron-userland/electron-builder/issues/5552
const plugIn = path.join(__dirname, "../PlugIns");
- if (fse.existsSync(plugIn)) {
- fse.mkdirSync(path.join(appPath, "Contents/PlugIns"));
+ if (!fse.existsSync(plugIn)) {
+ console.log("### Safari extension not found - skipping");
+ } else {
+ if (!fse.existsSync(path.join(appPath, "Contents/PlugIns"))) {
+ fse.mkdirSync(path.join(appPath, "Contents/PlugIns"));
+ }
fse.copySync(
path.join(plugIn, "safari.appex"),
path.join(appPath, "Contents/PlugIns/safari.appex"),
);
+ shouldResign = true;
+ }
+ }
- // Resign to sign safari extension
- if (context.electronPlatformName === "mas") {
- const masBuildOptions = deepAssign(
- {},
- context.packager.platformSpecificBuildOptions,
- context.packager.config.mas,
- );
- if (context.targets.some((e) => e.name === "mas-dev")) {
- deepAssign(masBuildOptions, {
- type: "development",
- });
- }
- if (context.packager.packagerOptions.prepackaged == null) {
- await context.packager.sign(appPath, context.appOutDir, masBuildOptions, context.arch);
- }
- } else {
- await context.packager.signApp(context, true);
+ if (shouldResign) {
+ // Resign to sign safari extension
+ if (context.electronPlatformName === "mas") {
+ const masBuildOptions = deepAssign(
+ {},
+ context.packager.platformSpecificBuildOptions,
+ context.packager.config.mas,
+ );
+ if (context.targets.some((e) => e.name === "mas-dev")) {
+ deepAssign(masBuildOptions, {
+ type: "development",
+ });
}
+ if (context.packager.packagerOptions.prepackaged == null) {
+ await context.packager.sign(appPath, context.appOutDir, masBuildOptions, context.arch);
+ }
+ } else {
+ await context.packager.signApp(context, true);
}
}
diff --git a/apps/desktop/scripts/build-macos-extension.js b/apps/desktop/scripts/build-macos-extension.js
new file mode 100644
index 00000000000..3aa43fb6785
--- /dev/null
+++ b/apps/desktop/scripts/build-macos-extension.js
@@ -0,0 +1,62 @@
+/* eslint-disable @typescript-eslint/no-var-requires, no-console */
+const child = require("child_process");
+const { exit } = require("process");
+
+const fse = require("fs-extra");
+
+const paths = {
+ macosBuild: "./macos/build",
+ extensionBuild: "./macos/build/Release/autofill-extension.appex",
+ extensionDistDir: "./macos/dist",
+ extensionDist: "./macos/dist/autofill-extension.appex",
+ macOsProject: "./macos/desktop.xcodeproj",
+ macOsConfig: "./macos/production.xcconfig",
+};
+
+async function buildMacOs() {
+ if (fse.existsSync(paths.macosBuild)) {
+ fse.removeSync(paths.macosBuild);
+ }
+
+ if (fse.existsSync(paths.extensionDistDir)) {
+ fse.removeSync(paths.extensionDistDir);
+ }
+
+ const proc = child.spawn("xcodebuild", [
+ "-project",
+ paths.macOsProject,
+ "-alltargets",
+ "-configuration",
+ "Release",
+ "-xcconfig",
+ paths.macOsConfig,
+ ]);
+ stdOutProc(proc);
+ await new Promise((resolve, reject) =>
+ proc.on("close", (code) => {
+ if (code > 0) {
+ console.error("xcodebuild failed with code", code);
+ return reject(new Error(`xcodebuild failed with code ${code}`));
+ }
+ console.log("xcodebuild success");
+ resolve();
+ }),
+ );
+
+ fse.mkdirSync(paths.extensionDistDir);
+ fse.copySync(paths.extensionBuild, paths.extensionDist);
+ // Delete the build dir, otherwise MacOS will load the extension from there instead of the Bitwarden.app bundle
+ fse.removeSync(paths.macosBuild);
+}
+
+function stdOutProc(proc) {
+ proc.stdout.on("data", (data) => console.log(data.toString()));
+ proc.stderr.on("data", (data) => console.error(data.toString()));
+}
+
+buildMacOs()
+ .then(() => console.log("macOS build complete"))
+ .catch((err) => {
+ console.error("macOS build failed", err);
+ exit(-1);
+ });
diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts
index 6d42e519d82..fb9ab033cf4 100644
--- a/apps/desktop/src/main/window.main.ts
+++ b/apps/desktop/src/main/window.main.ts
@@ -12,15 +12,7 @@ import { BiometricStateService } from "@bitwarden/key-management";
import { WindowState } from "../platform/models/domain/window-state";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
-import {
- cleanUserAgent,
- isDev,
- isLinux,
- isMac,
- isMacAppStore,
- isSnapStore,
- isWindows,
-} from "../utils";
+import { cleanUserAgent, isDev, isLinux, isMac, isMacAppStore, isWindows } from "../utils";
const mainWindowSizeKey = "mainWindowSize";
const WindowEventHandlingDelay = 100;
@@ -84,7 +76,7 @@ export class WindowMain {
return new Promise((resolve, reject) => {
try {
- if (!isMacAppStore() && !isSnapStore()) {
+ if (!isMacAppStore()) {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts
index 6813a59ce41..c4ed44ab0cb 100644
--- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts
+++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts
@@ -55,10 +55,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
private _destroy = new Subject();
- protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$(
- FeatureFlag.EnableConsolidatedBilling,
- );
-
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
@@ -101,14 +97,9 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
switchMap((organization) => this.providerService.get$(organization.providerId)),
);
- this.organizationIsUnmanaged$ = combineLatest([
- this.consolidatedBillingEnabled$,
- this.organization$,
- provider$,
- ]).pipe(
+ this.organizationIsUnmanaged$ = combineLatest([this.organization$, provider$]).pipe(
map(
- ([consolidatedBillingEnabled, organization, provider]) =>
- !consolidatedBillingEnabled ||
+ ([organization, provider]) =>
!organization.hasProvider ||
!provider ||
provider.providerStatus !== ProviderStatusType.Billable,
diff --git a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts
index a915d8f8a6c..0f6baa5f322 100644
--- a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts
+++ b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts
@@ -4,22 +4,11 @@ import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
export const organizationIsUnmanaged: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
- const configService = inject(ConfigService);
const organizationService = inject(OrganizationService);
const providerService = inject(ProviderService);
- const consolidatedBillingEnabled = await configService.getFeatureFlag(
- FeatureFlag.EnableConsolidatedBilling,
- );
-
- if (!consolidatedBillingEnabled) {
- return true;
- }
-
const organization = await organizationService.get(route.params.organizationId);
if (!organization.hasProvider) {
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 d4d11d91e01..f5cc89c86b6 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
@@ -60,10 +60,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon;
protected readonly teamsStarter = ProductTierType.TeamsStarter;
- protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
- FeatureFlag.EnableConsolidatedBilling,
- );
-
protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableUpgradePasswordManagerSub,
);
@@ -124,8 +120,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
this.locale = await firstValueFrom(this.i18nService.locale$);
this.userOrg = await this.organizationService.get(this.organizationId);
- const consolidatedBillingEnabled = await firstValueFrom(this.enableConsolidatedBilling$);
-
const isIndependentOrganizationOwner = !this.userOrg.hasProvider && this.userOrg.isOwner;
const isResoldOrganizationOwner = this.userOrg.hasReseller && this.userOrg.isOwner;
const isMSPUser = this.userOrg.hasProvider && this.userOrg.isProviderUser;
@@ -135,7 +129,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
);
this.organizationIsManagedByConsolidatedBillingMSP =
- consolidatedBillingEnabled && this.userOrg.hasProvider && metadata.isManaged;
+ this.userOrg.hasProvider && metadata.isManaged;
this.showSubscription =
isIndependentOrganizationOwner ||
diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts
index 688cbfbf9f1..ce61cd51fcd 100644
--- a/apps/web/src/app/vault/individual-vault/vault.component.ts
+++ b/apps/web/src/app/vault/individual-vault/vault.component.ts
@@ -16,6 +16,7 @@ import {
from,
lastValueFrom,
Observable,
+ of,
Subject,
} from "rxjs";
import {
@@ -184,12 +185,17 @@ export class VaultComponent implements OnInit, OnDestroy {
private refresh$ = new BehaviorSubject(null);
private destroy$ = new Subject();
private extensionRefreshEnabled: boolean;
+ private hasSubscription$ = new BehaviorSubject(false);
private vaultItemDialogRef?: DialogRef | undefined;
private readonly unpaidSubscriptionDialog$ = this.organizationService.organizations$.pipe(
filter((organizations) => organizations.length === 1),
- switchMap(([organization]) =>
+ map(([organization]) => organization),
+ switchMap((organization) =>
from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe(
+ tap((organizationMetaData) => {
+ this.hasSubscription$.next(organizationMetaData.hasSubscription);
+ }),
switchMap((organizationMetaData) =>
from(
this.trialFlowService.handleUnpaidSubscriptionDialog(
@@ -417,11 +423,17 @@ export class VaultComponent implements OnInit, OnDestroy {
this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe();
- const organizationsPaymentStatus$ = this.organizationService.organizations$.pipe(
- switchMap((allOrganizations) => {
+ const organizationsPaymentStatus$ = combineLatest([
+ this.organizationService.organizations$,
+ this.hasSubscription$,
+ ]).pipe(
+ switchMap(([allOrganizations, hasSubscription]) => {
+ if (!allOrganizations || allOrganizations.length === 0 || !hasSubscription) {
+ return of([]);
+ }
return combineLatest(
allOrganizations
- .filter((org) => org.isOwner)
+ .filter((org) => org.isOwner && hasSubscription)
.map((org) =>
combineLatest([
this.organizationApiService.getSubscription(org.id),
diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts
index dee584f3a42..64318047b9e 100644
--- a/apps/web/src/app/vault/org-vault/vault.component.ts
+++ b/apps/web/src/app/vault/org-vault/vault.component.ts
@@ -178,6 +178,7 @@ export class VaultComponent implements OnInit, OnDestroy {
protected selectedCollection: TreeNode | undefined;
protected isEmpty: boolean;
protected showCollectionAccessRestricted: boolean;
+ private hasSubscription$ = new BehaviorSubject(false);
protected currentSearchText$: Observable;
protected freeTrial$: Observable;
/**
@@ -197,10 +198,15 @@ export class VaultComponent implements OnInit, OnDestroy {
protected addAccessStatus$ = new BehaviorSubject(0);
private extensionRefreshEnabled: boolean;
private vaultItemDialogRef?: DialogRef | undefined;
+
private readonly unpaidSubscriptionDialog$ = this.organizationService.organizations$.pipe(
filter((organizations) => organizations.length === 1),
- switchMap(([organization]) =>
+ map(([organization]) => organization),
+ switchMap((organization) =>
from(this.billingApiService.getOrganizationBillingMetadata(organization.id)).pipe(
+ tap((organizationMetaData) => {
+ this.hasSubscription$.next(organizationMetaData.hasSubscription);
+ }),
switchMap((organizationMetaData) =>
from(
this.trialFlowService.handleUnpaidSubscriptionDialog(
@@ -580,9 +586,12 @@ export class VaultComponent implements OnInit, OnDestroy {
this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe();
- this.freeTrial$ = organization$.pipe(
- filter((org) => org.isOwner),
- switchMap((org) =>
+ this.freeTrial$ = combineLatest([
+ organization$,
+ this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)),
+ ]).pipe(
+ filter(([org, hasSubscription]) => org.isOwner && hasSubscription),
+ switchMap(([org]) =>
combineLatest([
of(org),
this.organizationApiService.getSubscription(org.id),
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts
index 88f125a65a1..e397c7ed8e1 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts
+++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts
@@ -8,11 +8,9 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
-import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
+import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import { hasConsolidatedBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction";
import { PlanType } from "@bitwarden/common/billing/enums";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { DialogService, ToastService } from "@bitwarden/components";
@@ -46,7 +44,6 @@ export class ClientsComponent extends BaseClientsComponent implements OnInit, On
private apiService: ApiService,
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction,
- private configService: ConfigService,
activatedRoute: ActivatedRoute,
dialogService: DialogService,
i18nService: I18nService,
@@ -72,9 +69,9 @@ export class ClientsComponent extends BaseClientsComponent implements OnInit, On
switchMap((params) => {
this.providerId = params.providerId;
return this.providerService.get$(this.providerId).pipe(
- hasConsolidatedBilling(this.configService),
- map((hasConsolidatedBilling) => {
- if (hasConsolidatedBilling) {
+ map((provider) => provider?.providerStatus === ProviderStatusType.Billable),
+ map((isBillable) => {
+ if (isBillable) {
return from(
this.router.navigate(["../manage-client-organizations"], {
relativeTo: this.activatedRoute,
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html
index 94c94b728ef..a20dd1379e2 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html
+++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html
@@ -5,7 +5,7 @@
();
protected provider$: Observable;
- protected hasConsolidatedBilling$: Observable;
+ protected isBillable: Observable;
protected canAccessBilling$: Observable;
protected showProviderClientVaultPrivacyWarningBanner$ = this.configService.getFeatureFlag$(
@@ -58,12 +58,12 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy {
takeUntil(this.destroy$),
);
- this.hasConsolidatedBilling$ = this.provider$.pipe(
- hasConsolidatedBilling(this.configService),
+ this.isBillable = this.provider$.pipe(
+ map((provider) => provider?.providerStatus === ProviderStatusType.Billable),
takeUntil(this.destroy$),
);
- this.canAccessBilling$ = combineLatest([this.hasConsolidatedBilling$, this.provider$]).pipe(
+ this.canAccessBilling$ = combineLatest([this.isBillable, this.provider$]).pipe(
map(
([hasConsolidatedBilling, provider]) => hasConsolidatedBilling && provider.isProviderAdmin,
),
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html
index 482b85b7127..33a20444c2b 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html
+++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html
@@ -24,13 +24,11 @@
{{ "billingEmail" | i18n }}
- {{
- "providerBillingEmailHint" | i18n
- }}
+ {{ "providerBillingEmailHint" | i18n }}
-
+
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts
index aaad0ce4578..72d954e8cdc 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts
+++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts
@@ -1,14 +1,13 @@
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
-import { firstValueFrom, Subject, switchMap } from "rxjs";
+import { Subject, switchMap } from "rxjs";
import { first, takeUntil } from "rxjs/operators";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@@ -34,10 +33,6 @@ export class SetupComponent implements OnInit, OnDestroy {
billingEmail: ["", [Validators.required, Validators.email]],
});
- protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
- FeatureFlag.EnableConsolidatedBilling,
- );
-
private destroy$ = new Subject();
constructor(
@@ -112,13 +107,9 @@ export class SetupComponent implements OnInit, OnDestroy {
submit = async () => {
try {
- const consolidatedBillingEnabled = await firstValueFrom(this.enableConsolidatedBilling$);
-
this.formGroup.markAllAsTouched();
- const formIsValid = consolidatedBillingEnabled
- ? this.formGroup.valid && this.manageTaxInformationComponent.touch()
- : this.formGroup.valid;
+ const formIsValid = this.formGroup.valid && this.manageTaxInformationComponent.touch();
if (!formIsValid) {
return;
@@ -133,19 +124,18 @@ export class SetupComponent implements OnInit, OnDestroy {
request.token = this.token;
request.key = key;
- if (consolidatedBillingEnabled) {
- request.taxInfo = new ExpandedTaxInfoUpdateRequest();
- const taxInformation = this.manageTaxInformationComponent.getTaxInformation();
+ request.taxInfo = new ExpandedTaxInfoUpdateRequest();
+ const taxInformation = this.manageTaxInformationComponent.getTaxInformation();
- request.taxInfo.country = taxInformation.country;
- request.taxInfo.postalCode = taxInformation.postalCode;
- if (taxInformation.includeTaxId) {
- request.taxInfo.taxId = taxInformation.taxId;
- request.taxInfo.line1 = taxInformation.line1;
- request.taxInfo.line2 = taxInformation.line2;
- request.taxInfo.city = taxInformation.city;
- request.taxInfo.state = taxInformation.state;
- }
+ request.taxInfo.country = taxInformation.country;
+ request.taxInfo.postalCode = taxInformation.postalCode;
+
+ if (taxInformation.includeTaxId) {
+ request.taxInfo.taxId = taxInformation.taxId;
+ request.taxInfo.line1 = taxInformation.line1;
+ request.taxInfo.line2 = taxInformation.line2;
+ request.taxInfo.city = taxInformation.city;
+ request.taxInfo.state = taxInformation.state;
}
const provider = await this.providerApiService.postProviderSetup(this.providerId, request);
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts
index a870b053dba..b41702e47a3 100644
--- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts
+++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts
@@ -6,13 +6,11 @@ import { switchMap } from "rxjs/operators";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
-import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
+import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
-import { hasConsolidatedBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { DialogService, ToastService } from "@bitwarden/components";
@@ -47,7 +45,6 @@ export class ManageClientsComponent extends BaseClientsComponent {
constructor(
private billingApiService: BillingApiService,
- private configService: ConfigService,
private providerService: ProviderService,
private router: Router,
activatedRoute: ActivatedRoute,
@@ -73,9 +70,9 @@ export class ManageClientsComponent extends BaseClientsComponent {
switchMap((params) => {
this.providerId = params.providerId;
return this.providerService.get$(this.providerId).pipe(
- hasConsolidatedBilling(this.configService),
- map((hasConsolidatedBilling) => {
- if (!hasConsolidatedBilling) {
+ map((provider) => provider?.providerStatus === ProviderStatusType.Billable),
+ map((isBillable) => {
+ if (!isBillable) {
return from(
this.router.navigate(["../clients"], {
relativeTo: this.activatedRoute,
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts b/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts
index 213b9a53681..60dbf4b3b82 100644
--- a/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts
+++ b/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts
@@ -4,24 +4,13 @@ import { firstValueFrom } from "rxjs";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
export const hasConsolidatedBilling: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
- const configService = inject(ConfigService);
const providerService = inject(ProviderService);
const provider = await firstValueFrom(providerService.get$(route.params.providerId));
- const consolidatedBillingEnabled = await configService.getFeatureFlag(
- FeatureFlag.EnableConsolidatedBilling,
- );
-
- if (
- !consolidatedBillingEnabled ||
- !provider ||
- provider.providerStatus !== ProviderStatusType.Billable
- ) {
+ if (!provider || provider.providerStatus !== ProviderStatusType.Billable) {
return createUrlTreeFromSnapshot(route, ["/providers", route.params.providerId]);
}
diff --git a/libs/common/src/billing/abstractions/index.ts b/libs/common/src/billing/abstractions/index.ts
index 7ed0f1251af..3f72cd9d2c0 100644
--- a/libs/common/src/billing/abstractions/index.ts
+++ b/libs/common/src/billing/abstractions/index.ts
@@ -1,4 +1,3 @@
export * from "./account/billing-account-profile-state.service";
export * from "./billing-api.service.abstraction";
export * from "./organization-billing.service";
-export * from "./provider-billing.service.abstraction";
diff --git a/libs/common/src/billing/abstractions/provider-billing.service.abstraction.ts b/libs/common/src/billing/abstractions/provider-billing.service.abstraction.ts
deleted file mode 100644
index aa8568d8e9c..00000000000
--- a/libs/common/src/billing/abstractions/provider-billing.service.abstraction.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { map, Observable, OperatorFunction, switchMap } from "rxjs";
-
-import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
-import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
-
-type MaybeProvider = Provider | undefined;
-
-export const hasConsolidatedBilling = (
- configService: ConfigService,
-): OperatorFunction =>
- switchMap>((provider) =>
- configService
- .getFeatureFlag$(FeatureFlag.EnableConsolidatedBilling)
- .pipe(
- map((consolidatedBillingEnabled) =>
- provider
- ? provider.providerStatus === ProviderStatusType.Billable && consolidatedBillingEnabled
- : false,
- ),
- ),
- );
diff --git a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts
index ae6d1ac92c1..d9733aa80f2 100644
--- a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts
+++ b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts
@@ -5,6 +5,7 @@ export class OrganizationBillingMetadataResponse extends BaseResponse {
isManaged: boolean;
isOnSecretsManagerStandalone: boolean;
isSubscriptionUnpaid: boolean;
+ hasSubscription: boolean;
constructor(response: any) {
super(response);
@@ -12,5 +13,6 @@ export class OrganizationBillingMetadataResponse extends BaseResponse {
this.isManaged = this.getResponseProperty("IsManaged");
this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone");
this.isSubscriptionUnpaid = this.getResponseProperty("IsSubscriptionUnpaid");
+ this.hasSubscription = this.getResponseProperty("HasSubscription");
}
}
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index bb96e9b3ee1..a12d05e99bc 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -7,7 +7,6 @@ export enum FeatureFlag {
BrowserFilelessImport = "browser-fileless-import",
ItemShare = "item-share",
GeneratorToolsModernization = "generator-tools-modernization",
- EnableConsolidatedBilling = "enable-consolidated-billing",
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
ExtensionRefresh = "extension-refresh",
PersistPopupView = "persist-popup-view",
@@ -59,7 +58,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.BrowserFilelessImport]: FALSE,
[FeatureFlag.ItemShare]: FALSE,
[FeatureFlag.GeneratorToolsModernization]: FALSE,
- [FeatureFlag.EnableConsolidatedBilling]: FALSE,
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
[FeatureFlag.ExtensionRefresh]: FALSE,
[FeatureFlag.PersistPopupView]: FALSE,
diff --git a/libs/importer/spec/dashlane-csv-importer.spec.ts b/libs/importer/spec/dashlane-csv-importer.spec.ts
index e76a4d4cb3b..1d76396022c 100644
--- a/libs/importer/spec/dashlane-csv-importer.spec.ts
+++ b/libs/importer/spec/dashlane-csv-importer.spec.ts
@@ -2,6 +2,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { DashlaneCsvImporter } from "../src/importers";
+import { credentialsData_otpUrl } from "./test-data/dashlane-csv/credentials-otpurl.csv";
import { credentialsData } from "./test-data/dashlane-csv/credentials.csv";
import { identityData } from "./test-data/dashlane-csv/id.csv";
import { multiplePersonalInfoData } from "./test-data/dashlane-csv/multiple-personal-info.csv";
@@ -30,6 +31,14 @@ describe("Dashlane CSV Importer", () => {
expect(cipher.notes).toEqual("some note for example.com");
});
+ it("should parse login with totp when given otpUrl instead of otpSecret", async () => {
+ const result = await importer.parse(credentialsData_otpUrl);
+ expect(result != null).toBe(true);
+
+ const cipher = result.ciphers.shift();
+ expect(cipher.login.totp).toEqual("anotherTOTPSeed");
+ });
+
it("should parse an item and create a folder", async () => {
const result = await importer.parse(credentialsData);
diff --git a/libs/importer/spec/test-data/dashlane-csv/credentials-otpurl.csv.ts b/libs/importer/spec/test-data/dashlane-csv/credentials-otpurl.csv.ts
new file mode 100644
index 00000000000..9506a98bad0
--- /dev/null
+++ b/libs/importer/spec/test-data/dashlane-csv/credentials-otpurl.csv.ts
@@ -0,0 +1,2 @@
+export const credentialsData_otpUrl = `username,username2,username3,title,password,note,url,category,otpUrl
+jdoe,,,example.com,somePassword,some note for example.com,https://www.example.com,Entertainment,anotherTOTPSeed`;
diff --git a/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts b/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts
index 888aeb823e8..94bc23c5fa8 100644
--- a/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts
+++ b/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts
@@ -119,7 +119,7 @@ export class DashlaneCsvImporter extends BaseImporter implements Importer {
cipher.notes = row.note;
cipher.login.username = row.username;
cipher.login.password = row.password;
- cipher.login.totp = row.otpSecret;
+ cipher.login.totp = Object.keys(row).includes("otpUrl") ? row.otpUrl : row.otpSecret;
cipher.login.uris = this.makeUriArray(row.url);
this.importUnmappedFields(cipher, row, _mappedCredentialsColumns);
diff --git a/libs/importer/src/importers/dashlane/types/dashlane-csv-types.ts b/libs/importer/src/importers/dashlane/types/dashlane-csv-types.ts
index cb321c56da8..c17aa8bf0e1 100644
--- a/libs/importer/src/importers/dashlane/types/dashlane-csv-types.ts
+++ b/libs/importer/src/importers/dashlane/types/dashlane-csv-types.ts
@@ -8,7 +8,8 @@ export class CredentialsRecord {
note: string;
url: string;
category: string;
- otpSecret: string;
+ otpSecret?: string;
+ otpUrl?: string; // Likely introduced by Dashlane as a replacement for `otpSecret`
}
export class PaymentsRecord {
diff --git a/libs/tools/send/send-ui/src/send-search/send-search.component.html b/libs/tools/send/send-ui/src/send-search/send-search.component.html
index 55674aa83e5..898d93da32c 100644
--- a/libs/tools/send/send-ui/src/send-search/send-search.component.html
+++ b/libs/tools/send/send-ui/src/send-search/send-search.component.html
@@ -3,6 +3,7 @@
[placeholder]="'search' | i18n"
[(ngModel)]="searchText"
(ngModelChange)="onSearchTextChanged()"
+ appAutofocus
>
diff --git a/package-lock.json b/package-lock.json
index 84ff0b47c1d..e2b5e336ffb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -194,7 +194,7 @@
},
"apps/browser": {
"name": "@bitwarden/browser",
- "version": "2024.11.0"
+ "version": "2024.11.1"
},
"apps/cli": {
"name": "@bitwarden/cli",