[push/opt-in-prompts] Implement push notifications client integration
Build the full client half of docs/api/push-notifications-integration-guide.md: OS permission + APNs device-token registration and pipeline wiring, profile notifications/biometric-login toggles on the Ver Perfil screen reflecting server truth, order-tracking opt-in fallback prompt, profile-cache refresh on every mutation, a Notification Service Extension for rich/image push, the Push Notifications capability, targeting-attributes sync, campaign open tracking, and tap-to-order deep linking with foreground notification display.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
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 `imageUrl`.
|
||||
/// `@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["imageUrl"] 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