diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 7568d82a1d..a560b0ff5f 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -21,6 +21,7 @@
# Platform
# Bitwarden @bitwarden/team-platform-dev
+# BitwardenNotificationExtension @bitwarden/team-platform-dev
# BitwardenShareExtension @bitwarden/team-platform-dev
# BitwardenShared/Core/Platform @bitwarden/team-platform-dev
# BitwardenShared/UI/Platform @bitwarden/team-platform-dev
diff --git a/.github/label-pr.json b/.github/label-pr.json
index 38b4c8372e..49812ac0f2 100644
--- a/.github/label-pr.json
+++ b/.github/label-pr.json
@@ -25,6 +25,7 @@
"Bitwarden/",
"BitwardenActionExtension/",
"BitwardenAutoFillExtension/",
+ "BitwardenNotificationExtension/",
"BitwardenShareExtension/",
"BitwardenShared/",
"BitwardenWatchApp/",
diff --git a/.github/workflows/ci-bwa.yml b/.github/workflows/ci-bwa.yml
index badc5b43a2..303e1306f1 100644
--- a/.github/workflows/ci-bwa.yml
+++ b/.github/workflows/ci-bwa.yml
@@ -10,6 +10,7 @@ on:
- Bitwarden/**
- BitwardenActionExtension/**
- BitwardenAutoFillExtension/**
+ - BitwardenNotificationExtension/**
- BitwardenShared/**
- BitwardenShareExtension/**
- BitwardenWatchApp/**
diff --git a/.github/workflows/test-bwa.yml b/.github/workflows/test-bwa.yml
index 700da35d7a..02952282a4 100644
--- a/.github/workflows/test-bwa.yml
+++ b/.github/workflows/test-bwa.yml
@@ -12,6 +12,7 @@ on:
- Bitwarden/**
- BitwardenActionExtension/**
- BitwardenAutoFillExtension/**
+ - BitwardenNotificationExtension/**
- BitwardenShared/**
- BitwardenShareExtension/**
- BitwardenWatchApp/**
diff --git a/BitwardenNotificationExtension/Application/Support/BitwardenNotificationExtension.entitlements b/BitwardenNotificationExtension/Application/Support/BitwardenNotificationExtension.entitlements
new file mode 100644
index 0000000000..5589adac97
--- /dev/null
+++ b/BitwardenNotificationExtension/Application/Support/BitwardenNotificationExtension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.$(BASE_BUNDLE_ID)
+
+
+
diff --git a/BitwardenNotificationExtension/Application/Support/Info.plist b/BitwardenNotificationExtension/Application/Support/Info.plist
new file mode 100644
index 0000000000..885192eb65
--- /dev/null
+++ b/BitwardenNotificationExtension/Application/Support/Info.plist
@@ -0,0 +1,107 @@
+
+
+
+
+ BitwardenAppIdentifier
+ $(BASE_BUNDLE_ID)
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ Bitwarden
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleLocalizations
+
+ af
+ ar
+ az
+ be
+ bg
+ bn
+ bs
+ ca
+ cs
+ cy
+ da
+ de
+ el
+ en
+ en-GB
+ en-IN
+ es
+ et
+ eu
+ fa
+ fi
+ fil
+ fr
+ gl
+ he
+ hi
+ hr
+ hu
+ id
+ it
+ ja
+ ka
+ kn
+ ko
+ lt
+ lv
+ ml
+ mr
+ my
+ nb
+ ne
+ nl
+ nn-NO
+ or
+ pl
+ pt
+ pt-BR
+ ro
+ ru
+ si
+ sk
+ sl
+ sr
+ sv
+ ta
+ te
+ th
+ tr
+ uk
+ vi
+ zh-Hans
+ zh-Hant
+
+ CFBundleName
+ Bitwarden Notification
+ CFBundlePackageType
+ XPC!
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ ITSAppUsesNonExemptEncryption
+
+ ITSEncryptionExportComplianceCode
+ ecf076d3-4824-4d7b-b716-2a9a47d7d296
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.usernotifications.service
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).NotificationServiceExtension
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+
+
+
diff --git a/BitwardenNotificationExtension/NotificationServiceExtension.swift b/BitwardenNotificationExtension/NotificationServiceExtension.swift
new file mode 100644
index 0000000000..65b96a7778
--- /dev/null
+++ b/BitwardenNotificationExtension/NotificationServiceExtension.swift
@@ -0,0 +1,31 @@
+import BitwardenShared
+import OSLog
+import UserNotifications
+
+// MARK: - NotificationServiceExtension
+
+/// The notification service extension entry point, responsible for intercepting incoming alert
+/// push notifications and updating their content before delivery.
+///
+/// This class is intentionally thin — all decoding and state-lookup logic lives in
+/// `DefaultNotificationExtensionHelper` (in `BitwardenShared`) where it is fully testable.
+///
+class NotificationServiceExtension: UNNotificationServiceExtension {
+ // MARK: UNNotificationServiceExtension
+
+ override func didReceive(
+ _ request: UNNotificationRequest,
+ withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void,
+ ) {
+ guard let content = request.content.mutableCopy() as? UNMutableNotificationContent else {
+ Logger.appExtension.error("Failed to cast UNNotificationContent to UNMutableNotificationContent")
+ contentHandler(request.content)
+ return
+ }
+
+ Task {
+ let updatedContent = await DefaultNotificationExtensionHelper().processNotification(content: content)
+ contentHandler(updatedContent)
+ }
+ }
+}
diff --git a/BitwardenShared/Core/Platform/Models/Domain/PushNotificationData.swift b/BitwardenShared/Core/Platform/Models/Domain/PushNotificationData.swift
index d1010c6a05..293599ca7e 100644
--- a/BitwardenShared/Core/Platform/Models/Domain/PushNotificationData.swift
+++ b/BitwardenShared/Core/Platform/Models/Domain/PushNotificationData.swift
@@ -34,11 +34,33 @@ struct PushNotificationData: Codable {
}
}
+// MARK: - PushNotificationData + userInfo
+
+extension PushNotificationData {
+ /// Decodes a `PushNotificationData` from a notification's `userInfo` dictionary.
+ ///
+ /// - Parameter userInfo: The notification's `userInfo` dictionary.
+ /// - Returns: The decoded `PushNotificationData`.
+ /// - Throws: `PushNotificationDataError.missingDataDictionary` if the `"data"` key is absent,
+ /// or an error if JSON serialization or decoding fails.
+ ///
+ init(userInfo: [AnyHashable: Any]) throws {
+ guard let messageContent = userInfo["data"] as? [AnyHashable: Any] else {
+ throw PushNotificationDataError.missingDataDictionary
+ }
+ let jsonData = try JSONSerialization.data(withJSONObject: messageContent)
+ self = try JSONDecoder().decode(PushNotificationData.self, from: jsonData)
+ }
+}
+
// MARK: - PushNotificationDataError
/// An error thrown when a push notification payload cannot be decoded.
///
enum PushNotificationDataError: Error, CustomNSError {
+ /// Thrown when the notification's `userInfo` dictionary does not contain a `"data"` key.
+ case missingDataDictionary
+
/// Thrown when the push notification payload cannot be decoded into the expected type.
case payloadDecodingFailed(type: NotificationType?, underlyingError: Error)
@@ -47,6 +69,7 @@ enum PushNotificationDataError: Error, CustomNSError {
// incremented integer. This ensures the code for existing errors doesn't change.
switch self {
case .payloadDecodingFailed: 1
+ case .missingDataDictionary: 2
}
}
@@ -56,6 +79,8 @@ enum PushNotificationDataError: Error, CustomNSError {
]
switch self {
+ case .missingDataDictionary:
+ break
case let .payloadDecodingFailed(type, underlyingError):
userInfo[NSUnderlyingErrorKey] = underlyingError
if let type {
diff --git a/BitwardenShared/Core/Platform/Models/Domain/PushNotificationDataTests.swift b/BitwardenShared/Core/Platform/Models/Domain/PushNotificationDataTests.swift
index b5b908e64e..12299f9ccc 100644
--- a/BitwardenShared/Core/Platform/Models/Domain/PushNotificationDataTests.swift
+++ b/BitwardenShared/Core/Platform/Models/Domain/PushNotificationDataTests.swift
@@ -3,6 +3,62 @@ import XCTest
@testable import BitwardenShared
class PushNotificationDataTests: BitwardenTestCase {
+ // MARK: init(userInfo:) Tests
+
+ /// `init(userInfo:)` successfully decodes a `PushNotificationData` from a valid `userInfo` dictionary.
+ func test_initUserInfo() throws {
+ let userInfo: [AnyHashable: Any] = [
+ "data": [
+ "contextId": "context-123",
+ "payload": "test-payload",
+ "type": 1,
+ ] as [AnyHashable: Any],
+ ]
+
+ let subject = try PushNotificationData(userInfo: userInfo)
+
+ XCTAssertEqual(subject.contextId, "context-123")
+ XCTAssertEqual(subject.payload, "test-payload")
+ XCTAssertEqual(subject.type, .syncCipherCreate)
+ }
+
+ /// `init(userInfo:)` throws `missingDataDictionary` when the `userInfo` dictionary is empty.
+ func test_initUserInfo_emptyDictionary() {
+ XCTAssertThrowsError(try PushNotificationData(userInfo: [:])) { error in
+ guard case PushNotificationDataError.missingDataDictionary = error else {
+ XCTFail("Expected PushNotificationDataError.missingDataDictionary, got \(error)")
+ return
+ }
+ }
+ }
+
+ /// `init(userInfo:)` throws a decoding error when the `"data"` value cannot be decoded.
+ func test_initUserInfo_invalidData() {
+ let userInfo: [AnyHashable: Any] = [
+ "data": [
+ "type": "not-a-number",
+ ] as [AnyHashable: Any],
+ ]
+
+ XCTAssertThrowsError(try PushNotificationData(userInfo: userInfo)) { error in
+ XCTAssertTrue(error is DecodingError)
+ }
+ }
+
+ /// `init(userInfo:)` throws `missingDataDictionary` when the `"data"` key is absent.
+ func test_initUserInfo_missingDataKey() {
+ let userInfo: [AnyHashable: Any] = ["other": "value"]
+
+ XCTAssertThrowsError(try PushNotificationData(userInfo: userInfo)) { error in
+ guard case PushNotificationDataError.missingDataDictionary = error else {
+ XCTFail("Expected PushNotificationDataError.missingDataDictionary, got \(error)")
+ return
+ }
+ }
+ }
+
+ // MARK: data Tests
+
/// `data` decodes the payload as expected.
func test_data() throws {
let subject = PushNotificationData(
diff --git a/BitwardenShared/Core/Platform/Services/NotificationExtensionHelper.swift b/BitwardenShared/Core/Platform/Services/NotificationExtensionHelper.swift
new file mode 100644
index 0000000000..eb3458e0c2
--- /dev/null
+++ b/BitwardenShared/Core/Platform/Services/NotificationExtensionHelper.swift
@@ -0,0 +1,108 @@
+import BitwardenKit
+import BitwardenResources
+import Foundation
+import UserNotifications
+
+// MARK: - NotificationExtensionHelper
+
+/// A protocol for a helper that processes notification content within a notification service extension.
+///
+public protocol NotificationExtensionHelper { // sourcery: AutoMockable
+ /// Processes a notification, updating the content as appropriate based on its type.
+ ///
+ /// - Parameter content: The mutable notification content to update.
+ /// - Returns: The updated notification content, or the original content unchanged if the
+ /// payload cannot be decoded or no update applies.
+ ///
+ func processNotification(
+ content: UNMutableNotificationContent,
+ ) async -> UNMutableNotificationContent
+}
+
+// MARK: - DefaultNotificationExtensionHelper
+
+/// The default implementation of `NotificationExtensionHelper`.
+///
+public class DefaultNotificationExtensionHelper: NotificationExtensionHelper {
+ // MARK: Properties
+
+ /// The store used to read persisted app settings.
+ private let appSettingsStore: AppSettingsStore
+
+ /// The reporter used to log non-fatal errors.
+ private let errorReporter: ErrorReporter
+
+ // MARK: Initialization
+
+ /// Initializes a `DefaultNotificationExtensionHelper` with an explicit settings store
+ /// and error reporter.
+ ///
+ /// - Parameters:
+ /// - appSettingsStore: The store used to read persisted app settings.
+ /// - errorReporter: The reporter used to log non-fatal errors.
+ ///
+ init(appSettingsStore: AppSettingsStore, errorReporter: ErrorReporter) {
+ self.appSettingsStore = appSettingsStore
+ self.errorReporter = errorReporter
+
+ Resources.initialLanguageCode = appSettingsStore.appLocale ?? Bundle.main.preferredLocalizations.first
+ }
+
+ /// Initializes a `DefaultNotificationExtensionHelper` using the shared app group `UserDefaults`
+ /// and an `OSLogErrorReporter`.
+ ///
+ public convenience init() {
+ let userDefaults = UserDefaults(suiteName: Bundle.main.groupIdentifier)!
+ self.init(
+ appSettingsStore: DefaultAppSettingsStore(userDefaults: userDefaults),
+ errorReporter: OSLogErrorReporter(),
+ )
+ }
+
+ // MARK: NotificationExtensionHelper
+
+ public func processNotification(content: UNMutableNotificationContent) async -> UNMutableNotificationContent {
+ do {
+ let notificationData = try PushNotificationData(userInfo: content.userInfo)
+ guard let type = notificationData.type else {
+ return content
+ }
+
+ switch type {
+ case .authRequest:
+ return try handleAuthRequest(notificationData, content: content)
+ default:
+ return content
+ }
+ } catch {
+ errorReporter.log(error: error)
+ return content
+ }
+ }
+
+ // MARK: Private
+
+ /// Handles an auth request notification, updating the body with the requesting user's email
+ /// if found in the accounts store.
+ ///
+ /// - Parameters:
+ /// - notificationData: The decoded push notification data.
+ /// - content: The mutable notification content to update.
+ /// - Returns: The updated content, or the original content unchanged if the user is not found.
+ /// - Throws: An error if the login request payload cannot be decoded.
+ ///
+ private func handleAuthRequest(
+ _ notificationData: PushNotificationData,
+ content: UNMutableNotificationContent,
+ ) throws -> UNMutableNotificationContent {
+ let loginRequest: LoginRequestNotification = try notificationData.data()
+ guard let email = appSettingsStore.state?.accounts[loginRequest.userId]?.profile.email else {
+ return content
+ }
+
+ content.title = Localizations.logInRequested
+ content.body = Localizations.confimLogInAttempForX(email)
+
+ return content
+ }
+}
diff --git a/BitwardenShared/Core/Platform/Services/NotificationExtensionHelperTests.swift b/BitwardenShared/Core/Platform/Services/NotificationExtensionHelperTests.swift
new file mode 100644
index 0000000000..5ccab67fdc
--- /dev/null
+++ b/BitwardenShared/Core/Platform/Services/NotificationExtensionHelperTests.swift
@@ -0,0 +1,146 @@
+import BitwardenKitMocks
+import BitwardenResources
+import Testing
+import UserNotifications
+
+@testable import BitwardenShared
+@testable import BitwardenSharedMocks
+
+// MARK: - NotificationExtensionHelperTests
+
+@MainActor
+struct NotificationExtensionHelperTests {
+ // MARK: Properties
+
+ var appSettingsStore: MockAppSettingsStore
+ var errorReporter: MockErrorReporter
+ var subject: DefaultNotificationExtensionHelper
+
+ // MARK: Initialization
+
+ init() {
+ appSettingsStore = MockAppSettingsStore()
+ errorReporter = MockErrorReporter()
+ subject = DefaultNotificationExtensionHelper(
+ appSettingsStore: appSettingsStore,
+ errorReporter: errorReporter,
+ )
+ }
+
+ // MARK: Tests
+
+ /// `processNotification(content:)` updates the title and body with the user's email when the
+ /// payload is valid and the user is found in the accounts store.
+ @Test
+ func processNotification_authRequest_updatesContent() async throws {
+ let userId = "user-1"
+ let email = "user@bitwarden.com"
+ let account = Account.fixture(profile: .fixture(email: email, userId: userId))
+ appSettingsStore.state = State(accounts: [userId: account], activeUserId: userId)
+
+ let payloadString = try jsonString(LoginRequestNotification(id: "request-1", userId: userId))
+ let content = makeContent(type: 15, payload: payloadString)
+
+ let result = await subject.processNotification(content: content)
+
+ #expect(result.title == Localizations.logInRequested)
+ #expect(result.body == Localizations.confimLogInAttempForX(email))
+ #expect(errorReporter.errors.isEmpty)
+ }
+
+ /// `processNotification(content:)` returns the original content unchanged when the user ID
+ /// in the payload does not match any known account.
+ @Test
+ func processNotification_authRequest_returnsOriginalContent_whenUserNotFound() async throws {
+ appSettingsStore.state = State(accounts: [:], activeUserId: nil)
+
+ let payloadString = try jsonString(LoginRequestNotification(id: "request-1", userId: "unknown-user"))
+ let content = makeContent(type: 15, payload: payloadString)
+ let originalBody = content.body
+
+ let result = await subject.processNotification(content: content)
+
+ #expect(result.body == originalBody)
+ #expect(errorReporter.errors.isEmpty)
+ }
+
+ /// `processNotification(content:)` returns the original content unchanged and logs an error
+ /// when the notification payload cannot be decoded.
+ @Test
+ func processNotification_logsError_whenPayloadMalformed() async throws {
+ let content = makeContent(type: 15, payload: "not valid json {{{")
+ let originalBody = content.body
+
+ let result = await subject.processNotification(content: content)
+
+ #expect(result.body == originalBody)
+ #expect(errorReporter.errors.count == 1)
+ let error = try #require(errorReporter.errors.first as? PushNotificationDataError)
+ guard case let .payloadDecodingFailed(type, _) = error else {
+ Issue.record("Expected payloadDecodingFailed, got \(error)")
+ return
+ }
+ #expect(type == .authRequest)
+ }
+
+ /// `processNotification(content:)` returns the original content unchanged when the notification
+ /// has no `"data"` key in `userInfo` (e.g. a non-Bitwarden notification).
+ @Test
+ func processNotification_returnsOriginalContent_whenNoDataKey() async throws {
+ let content = UNMutableNotificationContent()
+ content.body = "Some other notification"
+
+ let result = await subject.processNotification(content: content)
+
+ #expect(result.body == "Some other notification")
+ #expect(errorReporter.errors.count == 1)
+ let error = try #require(errorReporter.errors.first as? PushNotificationDataError)
+ guard case .missingDataDictionary = error else {
+ Issue.record("Expected missingDataDictionary, got \(error)")
+ return
+ }
+ }
+
+ /// `processNotification(content:)` returns the original content unchanged when the notification
+ /// type is not handled.
+ @Test
+ func processNotification_returnsOriginalContent_whenTypeNotHandled() async throws {
+ let userId = "user-1"
+ let account = Account.fixture(profile: .fixture(email: "user@bitwarden.com", userId: userId))
+ appSettingsStore.state = State(accounts: [userId: account], activeUserId: userId)
+
+ let payloadString = try jsonString(LoginRequestNotification(id: "request-1", userId: userId))
+ // syncCipherUpdate = type 0, not handled by the extension
+ let content = makeContent(type: 0, payload: payloadString)
+ let originalBody = content.body
+
+ let result = await subject.processNotification(content: content)
+
+ #expect(result.body == originalBody)
+ #expect(errorReporter.errors.isEmpty)
+ }
+
+ // MARK: Private
+
+ /// Encodes a `Codable` value to a compact JSON string.
+ private func jsonString(
+ _ value: T,
+ sourceLocation: SourceLocation = #_sourceLocation,
+ ) throws -> String {
+ let data = try JSONEncoder().encode(value)
+ return try #require(String(bytes: data, encoding: .utf8), sourceLocation: sourceLocation)
+ }
+
+ /// Creates a `UNMutableNotificationContent` with the given notification type and payload.
+ private func makeContent(type: Int, payload: String) -> UNMutableNotificationContent {
+ let content = UNMutableNotificationContent()
+ content.body = "Confirm login attempt"
+ content.userInfo = [
+ "data": [
+ "type": type,
+ "payload": payload,
+ ] as [String: Any],
+ ]
+ return content
+ }
+}
diff --git a/BitwardenShared/Core/Platform/Services/NotificationService.swift b/BitwardenShared/Core/Platform/Services/NotificationService.swift
index e21169096d..99298d5487 100644
--- a/BitwardenShared/Core/Platform/Services/NotificationService.swift
+++ b/BitwardenShared/Core/Platform/Services/NotificationService.swift
@@ -311,10 +311,7 @@ class DefaultNotificationService: NotificationService {
///
private func decodePayload(_ message: [AnyHashable: Any]) async throws -> PushNotificationData? {
// Decode the content of the message.
- guard let messageContent = message["data"] as? [AnyHashable: Any]
- else { return nil }
- let jsonData = try JSONSerialization.data(withJSONObject: messageContent)
- let notificationData = try JSONDecoder().decode(PushNotificationData.self, from: jsonData)
+ let notificationData = try PushNotificationData(userInfo: message)
// Verify that the payload is not empty and that the context is correct.
let appId = await appIdService.getOrCreateAppId()
diff --git a/Configs/BitwardenNotificationExtension.xcconfig b/Configs/BitwardenNotificationExtension.xcconfig
new file mode 100644
index 0000000000..f8bb21aa95
--- /dev/null
+++ b/Configs/BitwardenNotificationExtension.xcconfig
@@ -0,0 +1,5 @@
+#include "./Common-bwpm.xcconfig"
+#include? "./Local-bwpm.xcconfig"
+
+PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_ID).notification
+PROVISIONING_PROFILE_SPECIFIER = $(PROVISIONING_PROFILE_SPECIFIER_NOTIFICATION_EXTENSION)
diff --git a/Configs/Common-bwpm.xcconfig b/Configs/Common-bwpm.xcconfig
index cfca9ffac7..5a0e4b32ee 100644
--- a/Configs/Common-bwpm.xcconfig
+++ b/Configs/Common-bwpm.xcconfig
@@ -19,6 +19,7 @@ BMPW_BUNDLE_DISPLAY_NAME = Bitwarden
// PROVISIONING_PROFILE_SPECIFIER =
// PROVISIONING_PROFILE_SPECIFIER_ACTION_EXTENSION =
// PROVISIONING_PROFILE_SPECIFIER_AUTOFILL_EXTENSION =
+// PROVISIONING_PROFILE_SPECIFIER_NOTIFICATION_EXTENSION =
// PROVISIONING_PROFILE_SPECIFIER_SHARE_EXTENSION =
// PROVISIONING_PROFILE_SPECIFIER_WATCH_APP =
// PROVISIONING_PROFILE_SPECIFIER_WATCH_WIDGET_EXTENSION =
diff --git a/Docs/Architecture.md b/Docs/Architecture.md
index b2f3896e09..45f85cbe98 100644
--- a/Docs/Architecture.md
+++ b/Docs/Architecture.md
@@ -38,6 +38,7 @@ The iOS repository contains two main apps: Bitwarden Password Manager and Bitwar
- `Bitwarden`: The main iOS Password Manager app.
- `BitwardenActionExtension`: An [Action extension](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Action.html) that can be accessed via the system share sheet "Autofill with Bitwarden" option.
- `BitwardenAutoFillExtension`: An AutoFill Credential Provider extension which allows Bitwarden to offer up credentials for [Password AutoFill](https://developer.apple.com/documentation/security/password_autofill/).
+- `BitwardenNotificationExtension`: A [UNNotificationServiceExtension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) that is used to update the content of push notifications.
- `BitwardenShareExtension`: A [Share extension](https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Share.html) that allows creating text or file sends via the system share sheet.
- `BitwardenWatchApp`: The companion watchOS app.
- `BitwardenShared`: The main Password Manager framework containing the Core and UI layers shared between the app and extensions.
diff --git a/fastlane/.env.bwpm_beta b/fastlane/.env.bwpm_beta
index ffb012c2d2..4f3736359d 100644
--- a/fastlane/.env.bwpm_beta
+++ b/fastlane/.env.bwpm_beta
@@ -6,6 +6,7 @@ _PROVISIONING_PROFILES="
dist_beta_autofill.mobileprovision,
dist_beta_bitwarden.mobileprovision,
dist_beta_extension.mobileprovision,
+ dist_beta_notification_extension.mobileprovision,
dist_beta_share_extension.mobileprovision,
dist_beta_bitwarden_watch_app.mobileprovision,
dist_beta_bitwarden_watch_app_extension.mobileprovision,
diff --git a/fastlane/.env.bwpm_prod b/fastlane/.env.bwpm_prod
index ac2ecb1186..24bdc2a5e4 100644
--- a/fastlane/.env.bwpm_prod
+++ b/fastlane/.env.bwpm_prod
@@ -6,6 +6,7 @@ _PROVISIONING_PROFILES="
dist_autofill.mobileprovision,
dist_bitwarden.mobileprovision,
dist_extension.mobileprovision,
+ dist_notification_extension.mobileprovision,
dist_share_extension.mobileprovision,
dist_bitwarden_watch_app.mobileprovision,
dist_bitwarden_watch_app_extension.mobileprovision,
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 77ca433fd2..07d538c1db 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -312,6 +312,7 @@ platform :ios do |options|
"PROVISIONING_PROFILE_SPECIFIER" => "#{profile_prefix} Bitwarden",
"PROVISIONING_PROFILE_SPECIFIER_ACTION_EXTENSION" => "#{profile_prefix} Extension",
"PROVISIONING_PROFILE_SPECIFIER_AUTOFILL_EXTENSION" => "#{profile_prefix} Autofill",
+ "PROVISIONING_PROFILE_SPECIFIER_NOTIFICATION_EXTENSION" => "#{profile_prefix} Notification Extension",
"PROVISIONING_PROFILE_SPECIFIER_SHARE_EXTENSION" => "#{profile_prefix} Share Extension",
"PROVISIONING_PROFILE_SPECIFIER_WATCH_APP" => "#{profile_prefix} Bitwarden Watch App",
"PROVISIONING_PROFILE_SPECIFIER_WATCH_WIDGET_EXTENSION" => "#{profile_prefix} Bitwarden Watch Widget Extension",
@@ -335,6 +336,8 @@ platform :ios do |options|
#{profile_prefix} Extension
#{bundle_id}.autofill
#{profile_prefix} Autofill
+ #{bundle_id}.notification
+ #{profile_prefix} Notification Extension
#{bundle_id}.share-extension
#{profile_prefix} Share Extension
#{bundle_id}.watchkitapp
diff --git a/project-pm.yml b/project-pm.yml
index 8e64c9e57e..41b3ce31db 100644
--- a/project-pm.yml
+++ b/project-pm.yml
@@ -94,6 +94,10 @@ schemes:
- path: TestPlans/Bitwarden-Default.xctestplan
defaultPlan: true
- path: TestPlans/Bitwarden-Unit.xctestplan
+ BitwardenNotificationExtension:
+ build:
+ targets:
+ BitwardenNotificationExtension: all
BitwardenShareExtension:
build:
targets:
@@ -170,6 +174,7 @@ targets:
- target: BitwardenShared
- target: BitwardenActionExtension
- target: BitwardenAutoFillExtension
+ - target: BitwardenNotificationExtension
- target: BitwardenShareExtension
- target: BitwardenWatchApp
- target: BitwardenKit/AuthenticatorBridgeKit
@@ -294,6 +299,23 @@ targets:
- target: BitwardenKit/TestHelpers
randomExecutionOrder: true
+ BitwardenNotificationExtension:
+ type: app-extension
+ platform: iOS
+ configFiles:
+ Debug: Configs/BitwardenNotificationExtension.xcconfig
+ Release: Configs/BitwardenNotificationExtension.xcconfig
+ settings:
+ base:
+ CODE_SIGN_ENTITLEMENTS: BitwardenNotificationExtension/Application/Support/BitwardenNotificationExtension.entitlements
+ INFOPLIST_FILE: BitwardenNotificationExtension/Application/Support/Info.plist
+ templates:
+ - CommonTarget
+ templateAttributes:
+ sourcesPath: BitwardenNotificationExtension
+ dependencies:
+ - target: BitwardenShared
+
BitwardenShareExtension:
type: app-extension
platform: iOS