Skip to content

Commit c0a88bd

Browse files
authored
feat: add package with service extension for custom image handling and permissions convenience methods (#2)
* feat: add package with service extension for custom image handling and permissions convenience methods * add default notification options
1 parent db97ef3 commit c0a88bd

File tree

6 files changed

+227
-0
lines changed

6 files changed

+227
-0
lines changed

Package.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// swift-tools-version: 5.7
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "AmplifUtilsNotifications",
8+
platforms: [.iOS(.v13), .macOS(.v10_15)],
9+
products: [
10+
.library(
11+
name: "AmplifyUtilsNotifications",
12+
targets: ["AmplifyUtilsNotifications"]),
13+
],
14+
dependencies: [
15+
],
16+
targets: [
17+
.target(
18+
name: "AmplifyUtilsNotifications",
19+
dependencies: []),
20+
.testTarget(
21+
name: "AmplifyUtilsNotificationsTests",
22+
dependencies: ["AmplifyUtilsNotifications"]),
23+
]
24+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
/// Defines the location of the image URL in a notification payload.
11+
///
12+
/// Conform to this protocol to define notification payload formats for use with AUNotificationService.
13+
/// It is not necessary to define the full message schema. Defining the subset of the message that
14+
/// contains the remote image URL is sufficient. See `PinpointNotificationPayload` for an example.
15+
public protocol AUNotificationPayload: Decodable {
16+
var remoteImageURL: String? { get }
17+
}
18+
19+
extension AUNotificationPayload {
20+
init(decoding userInfo: [AnyHashable : Any]) throws {
21+
let json = try JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted)
22+
self = try JSONDecoder().decode(Self.self, from: json)
23+
}
24+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import UserNotifications
10+
11+
#if canImport(AppKit)
12+
import AppKit
13+
typealias Application = NSApplication
14+
#elseif canImport(UIKit)
15+
import UIKit
16+
typealias Application = UIApplication
17+
#endif
18+
19+
/// Provides convenience methods for requesting and checking notifications permissions.
20+
public class AUNotificationPermissions {
21+
22+
/// Check if notifications are allowed
23+
public static var allowed: Bool {
24+
get async {
25+
await withCheckedContinuation { continuation in
26+
UNUserNotificationCenter.current().getNotificationSettings { settings in
27+
continuation.resume(returning: settings.authorizationStatus == .authorized ? true : false)
28+
}
29+
}
30+
}
31+
}
32+
33+
/// Request notification permissions
34+
/// - Parameter options: Requested notification options
35+
@discardableResult
36+
public static func request(_ options: UNAuthorizationOptions? = nil) async throws -> Bool {
37+
let options = options ?? [.badge, .alert, .sound]
38+
let notificationsAllowed = try await UNUserNotificationCenter.current().requestAuthorization(
39+
options: options
40+
)
41+
42+
if notificationsAllowed {
43+
// Register with Apple Push Notification service
44+
await Application.shared.registerForRemoteNotifications()
45+
}
46+
47+
return notificationsAllowed
48+
}
49+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import UserNotifications
9+
import os.log
10+
11+
/// Attaches an image to a remote notification before it’s delivered to the user.
12+
///
13+
/// The image will be downloaded from a URL specified in the incoming notification. The
14+
/// format of the notification payload and name of the field containing the image URL is
15+
/// specified in payloadSchema, which conforms to AUNotificationPayload.
16+
open class AUNotificationService: UNNotificationServiceExtension {
17+
18+
/// Defines the format of incomming notification payloads.
19+
///
20+
/// You can override the default value in the initializer of a child class.
21+
///
22+
/// Default: PinpointNotificationPayload
23+
open var payloadSchema: AUNotificationPayload.Type = PinpointNotificationPayload.self
24+
25+
var contentHandler: ((UNNotificationContent) -> Void)?
26+
var bestAttemptContent: UNMutableNotificationContent?
27+
28+
/// Called when in incomming notification is received. Allows modification of the notification request before delivery.
29+
/// - Parameters:
30+
/// - request: The original notification request. Use this object to get the original content of the notification.
31+
/// - contentHandler: The block to execute with the modified content.
32+
open override func didReceive(
33+
_ request: UNNotificationRequest,
34+
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
35+
) {
36+
self.contentHandler = contentHandler
37+
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
38+
39+
defer {
40+
contentHandler(bestAttemptContent ?? request.content)
41+
}
42+
43+
if let bestAttemptContent = bestAttemptContent {
44+
// Modify the notification content
45+
guard let attachment = getImageAttachment(request) else { return }
46+
47+
os_log(.debug, "created attachment")
48+
bestAttemptContent.attachments = [attachment]
49+
}
50+
}
51+
52+
/// Called when the system is terminating the extension.
53+
open override func serviceExtensionTimeWillExpire() {
54+
// Called just before the extension will be terminated by the system.
55+
// Use this as an opportunity to deliver your "best attempt" at modified content,
56+
// otherwise the original push payload will be used.
57+
os_log(.debug, "time expired")
58+
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
59+
contentHandler(bestAttemptContent)
60+
}
61+
}
62+
63+
private func getImageAttachment(_ request: UNNotificationRequest) -> UNNotificationAttachment? {
64+
guard let payloadData = try? payloadSchema.init(decoding: request.content.userInfo),
65+
let mediaURLString = payloadData.remoteImageURL,
66+
let mediaType = mediaURLString.split(separator: ".").last,
67+
let mediaURL = URL(string: mediaURLString),
68+
let mediaData = try? Data(contentsOf: mediaURL) else {
69+
return nil
70+
}
71+
os_log(.debug, "got image data")
72+
73+
let fileManager = FileManager.default
74+
let temporaryFolderName = ProcessInfo.processInfo.globallyUniqueString
75+
let temporaryFolderURL = URL(fileURLWithPath: NSTemporaryDirectory())
76+
.appendingPathComponent(temporaryFolderName, isDirectory: true)
77+
78+
do {
79+
try fileManager.createDirectory(at: temporaryFolderURL,
80+
withIntermediateDirectories: true,
81+
attributes: nil)
82+
83+
// supported image types: jpg, gif, png
84+
let imageFileIdentifier = "\(UUID().uuidString).\(String(mediaType))"
85+
let fileURL = temporaryFolderURL.appendingPathComponent(imageFileIdentifier)
86+
try mediaData.write(to: fileURL)
87+
88+
return try UNNotificationAttachment(identifier: imageFileIdentifier, url: fileURL)
89+
} catch {
90+
return nil
91+
}
92+
}
93+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
struct PinpointNotificationPayload: AUNotificationPayload {
11+
var remoteImageURL: String? {
12+
data.mediaURL
13+
}
14+
15+
let data: PayloadData
16+
17+
struct PayloadData: Decodable {
18+
let mediaURL: String
19+
20+
enum CodingKeys: String, CodingKey {
21+
case mediaURL = "media-url"
22+
}
23+
}
24+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
@testable import AmplifyUtilsNotifications
10+
11+
final class AmplifyUtilsNotificationsTests: XCTestCase {
12+
13+
}

0 commit comments

Comments
 (0)