Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -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)

Expand All @@ -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
}
}

Expand All @@ -56,6 +79,8 @@ enum PushNotificationDataError: Error, CustomNSError {
]

switch self {
case .missingDataDictionary:
break
case let .payloadDecodingFailed(type, underlyingError):
userInfo[NSUnderlyingErrorKey] = underlyingError
if let type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading