migration
This commit is contained in:
31
NotificationServiceExtension/Info.plist
Normal file
31
NotificationServiceExtension/Info.plist
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>NotificationService</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.usernotifications.service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
60
NotificationServiceExtension/NotificationService.swift
Normal file
60
NotificationServiceExtension/NotificationService.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
import UserNotifications
|
||||
|
||||
/// Rich (image) push — see docs/api/push-notifications-integration-guide.md §4.3.
|
||||
/// Only fires when Atomenta's push payload sets `"mutable-content": 1`, which
|
||||
/// it does by default on any push carrying an image (real wire key is
|
||||
/// `"image"`, not the guide's `"imageUrl"` — verified against
|
||||
/// `PushNotificationService.dispatchApns`/`dispatchApnsMany` in Atomenta).
|
||||
/// `@unchecked Sendable`: the extension process only ever runs one
|
||||
/// `didReceive`/`serviceExtensionTimeWillExpire` cycle at a time — there's no
|
||||
/// real concurrent access to `contentHandler`/`bestAttemptContent` to guard
|
||||
/// against, just a background download completion handing back to this
|
||||
/// instance.
|
||||
final class NotificationService: UNNotificationServiceExtension, @unchecked Sendable {
|
||||
private var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
private var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
self.contentHandler = contentHandler
|
||||
let mutableContent = (request.content.mutableCopy() as? UNMutableNotificationContent) ?? UNMutableNotificationContent()
|
||||
bestAttemptContent = mutableContent
|
||||
|
||||
guard let imageURLString = request.content.userInfo["image"] as? String,
|
||||
let imageURL = URL(string: imageURLString) else {
|
||||
deliver()
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.shared.downloadTask(with: imageURL) { [weak self] location, _, _ in
|
||||
if let location, let attachment = Self.attachment(fromDownloadedFile: location) {
|
||||
self?.bestAttemptContent?.attachments = [attachment]
|
||||
}
|
||||
self?.deliver()
|
||||
}.resume()
|
||||
}
|
||||
|
||||
/// The system calls this if `didReceive` doesn't finish within its time
|
||||
/// budget — must still deliver the best content built so far, since
|
||||
/// `contentHandler` is contractually required to run exactly once.
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
deliver()
|
||||
}
|
||||
|
||||
private func deliver() {
|
||||
guard let bestAttemptContent else { return }
|
||||
contentHandler?(bestAttemptContent)
|
||||
contentHandler = nil
|
||||
}
|
||||
|
||||
private static func attachment(fromDownloadedFile location: URL) -> UNNotificationAttachment? {
|
||||
let tmpURL = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString)
|
||||
.appendingPathExtension("jpg")
|
||||
do {
|
||||
try FileManager.default.moveItem(at: location, to: tmpURL)
|
||||
return try UNNotificationAttachment(identifier: "image", url: tmpURL)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user