1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 14:34:02 +00:00

Merge branch 'main' of https://github.com/bitwarden/clients into ps/extension-refresh

This commit is contained in:
Daniel James Smith
2024-11-13 16:19:22 +01:00
46 changed files with 833 additions and 166 deletions

2
.github/CODEOWNERS vendored
View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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",

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -6,7 +6,7 @@
<app-current-account></app-current-account>
</ng-container>
</popup-header>
<div slot="above-scroll-area" class="tw-p-4">
<div slot="above-scroll-area" class="tw-p-4" *ngIf="!(sendsLoading$ | async)">
<bit-callout *ngIf="sendsDisabled" [title]="'sendDisabled' | i18n">
{{ "sendDisabledWarning" | i18n }}
</bit-callout>

View File

@@ -2,3 +2,4 @@ dist-safari/
*.nupkg
*.env
PlugIns/safari.appex/
xcuserdata/

View File

@@ -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 <path to .appex>
```
where the path to the .appex file can be found in the output of the first command.

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17021" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17021"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="CredentialProviderViewController" customModuleProvider="target">
<connections>
<outlet property="view" destination="1" id="2"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="1">
<rect key="frame" x="0.0" y="0.0" width="378" height="94"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1uM-r7-H1c">
<rect key="frame" x="177" y="3" width="197" height="32"/>
<buttonCell key="cell" type="push" title="Return Example Password" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="2l4-PO-we5">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent">D</string>
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</buttonCell>
<connections>
<action selector="passwordSelected:" target="-2" id="yic-EC-GGk"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="NVE-vN-dkz">
<rect key="frame" x="99" y="3" width="82" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="60" id="cP1-hK-9ZX"/>
</constraints>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6Up-t3-mwm">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancel:" target="-2" id="Qav-AK-DGt"/>
</connections>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aNc-0i-CWK">
<rect key="frame" x="135" y="63" width="108" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" title="autofill-extension" id="0xp-rC-2gr">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="1uM-r7-H1c" firstAttribute="leading" secondItem="NVE-vN-dkz" secondAttribute="trailing" constant="8" id="1UO-J1-LbJ"/>
<constraint firstItem="NVE-vN-dkz" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="1" secondAttribute="leading" constant="20" symbolic="YES" id="3N9-qo-UfS"/>
<constraint firstAttribute="bottom" secondItem="1uM-r7-H1c" secondAttribute="bottom" constant="10" id="4wH-De-nMF"/>
<constraint firstItem="NVE-vN-dkz" firstAttribute="firstBaseline" secondItem="aNc-0i-CWK" secondAttribute="baseline" constant="50" id="Dpq-cK-cPE"/>
<constraint firstAttribute="bottom" secondItem="NVE-vN-dkz" secondAttribute="bottom" constant="10" id="USG-Gg-of3"/>
<constraint firstItem="1uM-r7-H1c" firstAttribute="leading" secondItem="NVE-vN-dkz" secondAttribute="trailing" constant="8" id="a8N-vS-Ew9"/>
<constraint firstAttribute="trailing" secondItem="1uM-r7-H1c" secondAttribute="trailing" constant="10" id="qfT-cw-QQ2"/>
<constraint firstAttribute="centerX" secondItem="aNc-0i-CWK" secondAttribute="centerX" id="uV3-Wn-RA3"/>
<constraint firstItem="aNc-0i-CWK" firstAttribute="top" secondItem="1" secondAttribute="top" constant="15" id="vpR-tf-ebx"/>
</constraints>
<point key="canvasLocation" x="162" y="146"/>
</customView>
</objects>
</document>

View File

@@ -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)")
}
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>ASCredentialProviderExtensionCapabilities</key>
<dict>
<key>ProvidesPasskeys</key>
<true/>
</dict>
<key>ASCredentialProviderExtensionShowsConfigurationUI</key>
<false/>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.authentication-services-credential-provider-ui</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).CredentialProviderViewController</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>

View File

@@ -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 = "<group>"; };
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 = "<group>"; };
E1DF71442B342F6900F29026 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/CredentialProviderViewController.xib; sourceTree = "<group>"; };
E1DF71462B342F6900F29026 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E1DF71472B342F6900F29026 /* autofill_extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = autofill_extension.entitlements; sourceTree = "<group>"; };
/* 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 = "<group>";
};
E1DF71272B342E2800F29026 /* Products */ = {
isa = PBXGroup;
children = (
E1DF713C2B342F6900F29026 /* autofill-extension.appex */,
);
name = Products;
sourceTree = "<group>";
};
E1DF713D2B342F6900F29026 /* Frameworks */ = {
isa = PBXGroup;
children = (
E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
E1DF71402B342F6900F29026 /* autofill-extension */ = {
isa = PBXGroup;
children = (
E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */,
E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */,
E1DF71462B342F6900F29026 /* Info.plist */,
E1DF71472B342F6900F29026 /* autofill_extension.entitlements */,
);
path = "autofill-extension";
sourceTree = "<group>";
};
/* 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 = "<group>";
};
/* 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 */;
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -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

View File

@@ -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",

View File

@@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>
</dict>
</plist>

View File

@@ -10,5 +10,7 @@
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>
</dict>
</plist>

View File

@@ -16,6 +16,8 @@
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
<array>
<string>/Library/Application Support/Mozilla/NativeMessagingHosts/</string>

View File

@@ -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 "$@"

View File

@@ -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);
}
}

View File

@@ -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);
});

View File

@@ -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<void>((resolve, reject) => {
try {
if (!isMacAppStore() && !isSnapStore()) {
if (!isMacAppStore()) {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();

View File

@@ -55,10 +55,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
private _destroy = new Subject<void>();
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,

View File

@@ -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) {

View File

@@ -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 ||

View File

@@ -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<void>(null);
private destroy$ = new Subject<void>();
private extensionRefreshEnabled: boolean;
private hasSubscription$ = new BehaviorSubject<boolean>(false);
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | 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),

View File

@@ -178,6 +178,7 @@ export class VaultComponent implements OnInit, OnDestroy {
protected selectedCollection: TreeNode<CollectionAdminView> | undefined;
protected isEmpty: boolean;
protected showCollectionAccessRestricted: boolean;
private hasSubscription$ = new BehaviorSubject<boolean>(false);
protected currentSearchText$: Observable<string>;
protected freeTrial$: Observable<FreeTrial>;
/**
@@ -197,10 +198,15 @@ export class VaultComponent implements OnInit, OnDestroy {
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
private extensionRefreshEnabled: boolean;
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | 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),

View File

@@ -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,

View File

@@ -5,7 +5,7 @@
<bit-nav-item
icon="bwi-bank"
[text]="'clients' | i18n"
[route]="(hasConsolidatedBilling$ | async) ? 'manage-client-organizations' : 'clients'"
[route]="(isBillable | async) ? 'manage-client-organizations' : 'clients'"
></bit-nav-item>
<bit-nav-group
icon="bwi-sliders"

View File

@@ -1,13 +1,13 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, RouterModule } from "@angular/router";
import { switchMap, Observable, Subject, combineLatest, map } from "rxjs";
import { combineLatest, map, Observable, Subject, switchMap } from "rxjs";
import { takeUntil } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { hasConsolidatedBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BannerModule, IconModule, LinkModule } from "@bitwarden/components";
@@ -36,7 +36,7 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected provider$: Observable<Provider>;
protected hasConsolidatedBilling$: Observable<boolean>;
protected isBillable: Observable<boolean>;
protected canAccessBilling$: Observable<boolean>;
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,
),

View File

@@ -24,13 +24,11 @@
<bit-form-field>
<bit-label>{{ "billingEmail" | i18n }}</bit-label>
<input type="email" bitInput formControlName="billingEmail" />
<bit-hint *ngIf="enableConsolidatedBilling$ | async">{{
"providerBillingEmailHint" | i18n
}}</bit-hint>
<bit-hint>{{ "providerBillingEmailHint" | i18n }}</bit-hint>
</bit-form-field>
</div>
</div>
<app-manage-tax-information *ngIf="enableConsolidatedBilling$ | async" />
<app-manage-tax-information />
<button bitButton bitFormButton buttonType="primary" type="submit">
{{ "submit" | i18n }}
</button>

View File

@@ -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<void>();
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);

View File

@@ -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,

View File

@@ -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]);
}

View File

@@ -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";

View File

@@ -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<MaybeProvider, boolean> =>
switchMap<MaybeProvider, Observable<boolean>>((provider) =>
configService
.getFeatureFlag$(FeatureFlag.EnableConsolidatedBilling)
.pipe(
map((consolidatedBillingEnabled) =>
provider
? provider.providerStatus === ProviderStatusType.Billable && consolidatedBillingEnabled
: false,
),
),
);

View File

@@ -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");
}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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`;

View File

@@ -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);

View File

@@ -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 {

View File

@@ -3,6 +3,7 @@
[placeholder]="'search' | i18n"
[(ngModel)]="searchText"
(ngModelChange)="onSearchTextChanged()"
appAutofocus
>
</bit-search>
</div>

2
package-lock.json generated
View File

@@ -194,7 +194,7 @@
},
"apps/browser": {
"name": "@bitwarden/browser",
"version": "2024.11.0"
"version": "2024.11.1"
},
"apps/cli": {
"name": "@bitwarden/cli",