Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/label-pr.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"Bitwarden/",
"BitwardenActionExtension/",
"BitwardenAutoFillExtension/",
"BitwardenNotificationExtension/",
"BitwardenShareExtension/",
"BitwardenShared/",
"BitwardenWatchApp/",
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-bwa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- Bitwarden/**
- BitwardenActionExtension/**
- BitwardenAutoFillExtension/**
- BitwardenNotificationExtension/**
- BitwardenShared/**
- BitwardenShareExtension/**
- BitwardenWatchApp/**
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test-bwa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
- Bitwarden/**
- BitwardenActionExtension/**
- BitwardenAutoFillExtension/**
- BitwardenNotificationExtension/**
- BitwardenShared/**
- BitwardenShareExtension/**
- BitwardenWatchApp/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.security.application-groups</key>
<array>
<string>group.$(BASE_BUNDLE_ID)</string>
</array>
</dict>
</plist>
107 changes: 107 additions & 0 deletions BitwardenNotificationExtension/Application/Support/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?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>BitwardenAppIdentifier</key>
<string>$(BASE_BUNDLE_ID)</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Bitwarden</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>af</string>
<string>ar</string>
<string>az</string>
<string>be</string>
<string>bg</string>
<string>bn</string>
<string>bs</string>
<string>ca</string>
<string>cs</string>
<string>cy</string>
<string>da</string>
<string>de</string>
<string>el</string>
<string>en</string>
<string>en-GB</string>
<string>en-IN</string>
<string>es</string>
<string>et</string>
<string>eu</string>
<string>fa</string>
<string>fi</string>
<string>fil</string>
<string>fr</string>
<string>gl</string>
<string>he</string>
<string>hi</string>
<string>hr</string>
<string>hu</string>
<string>id</string>
<string>it</string>
<string>ja</string>
<string>ka</string>
<string>kn</string>
<string>ko</string>
<string>lt</string>
<string>lv</string>
<string>ml</string>
<string>mr</string>
<string>my</string>
<string>nb</string>
<string>ne</string>
<string>nl</string>
<string>nn-NO</string>
<string>or</string>
<string>pl</string>
<string>pt</string>
<string>pt-BR</string>
<string>ro</string>
<string>ru</string>
<string>si</string>
<string>sk</string>
<string>sl</string>
<string>sr</string>
<string>sv</string>
<string>ta</string>
<string>te</string>
<string>th</string>
<string>tr</string>
<string>uk</string>
<string>vi</string>
<string>zh-Hans</string>
<string>zh-Hant</string>
</array>
<key>CFBundleName</key>
<string>Bitwarden Notification</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<true/>
<key>ITSEncryptionExportComplianceCode</key>
<string>ecf076d3-4824-4d7b-b716-2a9a47d7d296</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationServiceExtension</string>
</dict>
<key>UIRequiredDeviceCapabilities</key>
<dict>
<key>arm64</key>
<true/>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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 {
guard let notificationData = try decodePushNotificationData(from: content.userInfo),
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

/// Decodes a `PushNotificationData` from a notification's `userInfo` dictionary.
///
/// - Parameter userInfo: The notification's `userInfo` dictionary.
/// - Returns: The decoded `PushNotificationData`, or `nil` if the `"data"` key is absent.
/// - Throws: An error if JSON serialization or decoding fails.
///
private func decodePushNotificationData(from userInfo: [AnyHashable: Any]) throws -> PushNotificationData? {
guard let messageContent = userInfo["data"] as? [AnyHashable: Any] else { return nil }
let jsonData = try JSONSerialization.data(withJSONObject: messageContent)
return try JSONDecoder().decode(PushNotificationData.self, from: jsonData)
}

/// 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
}
}
Loading
Loading