@@ -8,176 +8,227 @@ import UserNotifications
8
8
var contentHandler : ( ( UNNotificationContent ) -> Void ) ?
9
9
var bestAttemptContent : UNMutableNotificationContent ?
10
10
11
- @objc override open func didReceive( _ request: UNNotificationRequest , withContentHandler contentHandler: @escaping ( UNNotificationContent ) -> Void ) {
11
+ @objc override open func didReceive( _ request: UNNotificationRequest ,
12
+ withContentHandler contentHandler: @escaping ( UNNotificationContent ) -> Void ) {
12
13
self . contentHandler = contentHandler
13
14
bestAttemptContent = ( request. content. mutableCopy ( ) as? UNMutableNotificationContent )
14
15
15
- // IMPORTANT: need to add this to the documentation
16
- bestAttemptContent? . categoryIdentifier = getCategory ( fromContent: request. content)
17
-
18
- guard let itblDictionary = request. content. userInfo [ JsonKey . Payload. metadata] as? [ AnyHashable : Any ] else {
19
- if let bestAttemptContent = bestAttemptContent {
20
- contentHandler ( bestAttemptContent)
21
- }
22
-
23
- return
24
- }
25
-
26
- var contentHandlerCalled = false
27
- contentHandlerCalled = loadAttachment ( itblDictionary: itblDictionary)
16
+ getCategoryId ( from: request. content)
17
+ retrieveAttachment ( from: request. content)
28
18
29
- if !contentHandlerCalled {
30
- DispatchQueue . main. asyncAfter ( deadline: . now( ) + 1 ) {
31
- if let bestAttemptContent = self . bestAttemptContent {
32
- contentHandler ( bestAttemptContent)
33
- }
34
- }
35
- }
19
+ checkPushCreationCompletion ( )
36
20
}
37
21
38
22
@objc override open func serviceExtensionTimeWillExpire( ) {
39
23
// Called just before the extension will be terminated by the system.
40
24
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
41
- if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
42
- contentHandler ( bestAttemptContent)
43
- }
25
+
26
+ callContentHandler ( )
44
27
}
45
28
46
- private func loadAttachment( itblDictionary: [ AnyHashable : Any ] ) -> Bool {
47
- guard let attachmentUrlString = itblDictionary [ JsonKey . Payload. attachmentUrl] as? String else { return false }
48
- guard let url = URL ( string: attachmentUrlString) else { return false }
29
+ // MARK: - Private
30
+
31
+ private func retrieveAttachment( from content: UNNotificationContent ) {
32
+ guard let metadata = content. userInfo [ JsonKey . Payload. metadata] as? [ AnyHashable : Any ] ,
33
+ let attachmentUrlString = metadata [ JsonKey . Payload. attachmentUrl] as? String ,
34
+ let url = URL ( string: attachmentUrlString) else {
35
+ attachmentRetrievalFinished = true
36
+ return
37
+ }
49
38
50
- let downloadTask = URLSession . shared. downloadTask ( with: url, completionHandler: { [ weak self] location, response, error in
51
- guard let strongSelf = self else { return }
52
-
53
- if error == nil , let response = response, let responseUrl = response. url, let location = location {
54
- let tempDirectoryUrl = FileManager . default. temporaryDirectory
55
- var attachmentIdString = UUID ( ) . uuidString + responseUrl. lastPathComponent
56
- if let suggestedFilename = response. suggestedFilename {
57
- attachmentIdString = UUID ( ) . uuidString + suggestedFilename
58
- }
59
-
60
- var attachment : UNNotificationAttachment ?
61
- let tempFileUrl = tempDirectoryUrl. appendingPathComponent ( attachmentIdString)
62
- do {
63
- try FileManager . default. moveItem ( at: location, to: tempFileUrl)
64
- attachment = try UNNotificationAttachment ( identifier: attachmentIdString, url: tempFileUrl, options: nil )
65
- } catch { /* TODO: FileManager or attachment error */ }
66
-
67
- if let attachment = attachment, let bestAttemptContent = strongSelf. bestAttemptContent, let contentHandler = strongSelf. contentHandler {
68
- bestAttemptContent. attachments. append ( attachment)
69
- contentHandler ( bestAttemptContent)
70
- }
71
- } else { /* TODO: handle download error */ }
72
- } )
73
-
74
- downloadTask. resume ( )
75
- return true
76
- }
77
-
78
- private func getCategory( fromContent content: UNNotificationContent ) -> String {
79
- if content. categoryIdentifier. count == 0 {
80
- guard let itblDictionary = content. userInfo [ JsonKey . Payload. metadata] as? [ AnyHashable : Any ] else {
81
- return " "
39
+ stopCurrentAttachmentDownloadTask ( )
40
+
41
+ attachmentDownloadTask = createAttachmentDownloadTask ( url: url)
42
+ attachmentDownloadTask? . resume ( )
43
+ }
44
+
45
+ private func createAttachmentDownloadTask( url: URL ) -> URLSessionDownloadTask {
46
+ return URLSession . shared. downloadTask ( with: url) { [ weak self] location, response, error in
47
+ guard let strongSelf = self , error == nil , let response = response, let responseUrl = response. url, let location = location else {
48
+ self ? . attachmentRetrievalFinished = true
49
+ return
82
50
}
83
51
84
- guard let messageId = itblDictionary [ JsonKey . Payload . messageId ] as? String else {
85
- return " "
86
- }
52
+ let attachmentId = UUID ( ) . uuidString + ITBNotificationServiceExtension . getAttachmentIdSuffix ( response : response ,
53
+ responseUrl : responseUrl )
54
+ let tempFileUrl = FileManager . default . temporaryDirectory . appendingPathComponent ( attachmentId )
87
55
88
- var actionButtons : [ [ AnyHashable : Any ] ] = [ ]
89
- if let actionButtonsFromITBLPayload = itblDictionary [ JsonKey . Payload. actionButtons] as? [ [ AnyHashable : Any ] ] {
90
- actionButtons = actionButtonsFromITBLPayload
91
- } else {
92
- #if DEBUG
93
- if let actionButtonsFromUserInfo = content. userInfo [ JsonKey . Payload. actionButtons] as? [ [ AnyHashable : Any ] ] {
94
- actionButtons = actionButtonsFromUserInfo
95
- }
96
- #endif
97
- }
56
+ var attachment : UNNotificationAttachment ?
98
57
99
- var notificationActions = [ UNNotificationAction] ( )
100
- for actionButton in actionButtons {
101
- if let notificationAction = createNotificationActionButton ( buttonDictionary: actionButton) {
102
- notificationActions. append ( notificationAction)
103
- }
58
+ do {
59
+ try FileManager . default. moveItem ( at: location, to: tempFileUrl)
60
+ attachment = try UNNotificationAttachment ( identifier: attachmentId, url: tempFileUrl, options: nil )
61
+ } catch {
62
+ self ? . attachmentRetrievalFinished = true
63
+ return
104
64
}
105
65
106
- messageCategory = UNNotificationCategory ( identifier: messageId, actions: notificationActions, intentIdentifiers: [ ] , options: [ ] )
107
- if let messageCategory = messageCategory {
108
- UNUserNotificationCenter . current ( ) . getNotificationCategories { categories in
109
- var newCategories = categories
110
- newCategories. insert ( messageCategory)
111
- UNUserNotificationCenter . current ( ) . setNotificationCategories ( newCategories)
112
- }
66
+ if let attachment = attachment, let content = strongSelf. bestAttemptContent, let handler = strongSelf. contentHandler {
67
+ content. attachments. append ( attachment)
68
+ handler ( content)
69
+ } else {
70
+ self ? . attachmentRetrievalFinished = true
71
+ return
72
+ }
73
+ }
74
+ }
75
+
76
+ private func stopCurrentAttachmentDownloadTask( ) {
77
+ attachmentDownloadTask? . cancel ( )
78
+ attachmentDownloadTask = nil
79
+ }
80
+
81
+ private func getCategoryId( from content: UNNotificationContent ) {
82
+ guard content. categoryIdentifier. count == 0 else {
83
+ setCategoryId ( id: content. categoryIdentifier)
84
+ return
85
+ }
86
+
87
+ guard let metadata = content. userInfo [ JsonKey . Payload. metadata] as? [ AnyHashable : Any ] ,
88
+ let messageId = metadata [ JsonKey . Payload. messageId] as? String else {
89
+ setCategoryId ( id: " " )
90
+ return
91
+ }
92
+
93
+ messageCategory = UNNotificationCategory ( identifier: messageId,
94
+ actions: getNotificationActions ( metadata: metadata,
95
+ content: content) ,
96
+ intentIdentifiers: [ ] ,
97
+ options: [ ] )
98
+
99
+ if let messageCategory = messageCategory {
100
+ UNUserNotificationCenter . current ( ) . getNotificationCategories { [ weak self] categories in
101
+ var newCategories = categories
102
+ newCategories. insert ( messageCategory)
103
+ UNUserNotificationCenter . current ( ) . setNotificationCategories ( newCategories)
104
+ self ? . setCategoryId ( id: messageId)
113
105
}
114
-
115
- return messageId
116
106
} else {
117
- return content . categoryIdentifier
107
+ setCategoryId ( id : messageId )
118
108
}
119
109
}
120
110
121
- private func createNotificationActionButton( buttonDictionary: [ AnyHashable : Any ] ) -> UNNotificationAction ? {
122
- guard let identifier = buttonDictionary [ JsonKey . ActionButton. identifier] as? String else {
123
- return nil
111
+ private func setCategoryId( id: String ) {
112
+ // IMPORTANT: need to add this to the documentation
113
+ bestAttemptContent? . categoryIdentifier = id
114
+
115
+ // for some reason, the check needs to be put into this dispatch
116
+ // to function properly for rich pushes with buttons but no image
117
+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.01 ) {
118
+ self . getCategoryIdFinished = true
124
119
}
120
+ }
121
+
122
+ private func getNotificationActions( metadata: [ AnyHashable : Any ] , content: UNNotificationContent ) -> [ UNNotificationAction ] {
123
+ var actionButtons : [ [ AnyHashable : Any ] ] = [ ]
125
124
126
- guard let title = buttonDictionary [ JsonKey . ActionButton. title] as? String else {
127
- return nil
125
+ if let actionButtonsFromMetadata = metadata [ JsonKey . Payload. actionButtons] as? [ [ AnyHashable : Any ] ] {
126
+ actionButtons = actionButtonsFromMetadata
127
+ } else {
128
+ #if DEBUG
129
+ if let actionButtonsFromUserInfo = content. userInfo [ JsonKey . Payload. actionButtons] as? [ [ AnyHashable : Any ] ] {
130
+ actionButtons = actionButtonsFromUserInfo
131
+ }
132
+ #endif
128
133
}
129
134
130
- let buttonType = getButtonType ( buttonDictionary: buttonDictionary)
131
- var openApp = true
132
- if let openAppFromDict = buttonDictionary [ JsonKey . ActionButton. openApp] as? NSNumber {
133
- openApp = openAppFromDict. boolValue
135
+ return actionButtons. compactMap { createNotificationActionButton ( info: $0) }
136
+ }
137
+
138
+ private func createNotificationActionButton( info: [ AnyHashable : Any ] ) -> UNNotificationAction ? {
139
+ guard let identifier = info [ JsonKey . ActionButton. identifier] as? String else { return nil }
140
+ guard let title = info [ JsonKey . ActionButton. title] as? String else { return nil }
141
+
142
+ let buttonType = getButtonType ( info: info)
143
+ let openApp = getBoolValue ( info [ JsonKey . ActionButton. openApp] ) ?? true
144
+ let requiresUnlock = getBoolValue ( info [ JsonKey . ActionButton. requiresUnlock] ) ?? false
145
+
146
+ let options = getActionButtonOptions ( buttonType: buttonType,
147
+ openApp: openApp,
148
+ requiresUnlock: requiresUnlock)
149
+
150
+ guard buttonType == IterableButtonTypeTextInput else {
151
+ return UNNotificationAction ( identifier: identifier, title: title, options: options)
134
152
}
135
153
136
- var requiresUnlock = false
137
- if let requiresUnlockFromDict = buttonDictionary [ JsonKey . ActionButton. requiresUnlock] as? NSNumber {
138
- requiresUnlock = requiresUnlockFromDict. boolValue
154
+ let inputTitle = info [ JsonKey . ActionButton. inputTitle] as? String ?? " "
155
+ let inputPlaceholder = info [ JsonKey . ActionButton. inputPlaceholder] as? String ?? " "
156
+
157
+ return UNTextInputNotificationAction ( identifier: identifier,
158
+ title: title,
159
+ options: options,
160
+ textInputButtonTitle: inputTitle,
161
+ textInputPlaceholder: inputPlaceholder)
162
+ }
163
+
164
+ private func getButtonType( info: [ AnyHashable : Any ] ) -> String {
165
+ if let buttonType = info [ JsonKey . ActionButton. buttonType] as? String {
166
+ if buttonType == IterableButtonTypeTextInput || buttonType == IterableButtonTypeDestructive {
167
+ return buttonType
168
+ }
139
169
}
140
170
141
- var actionOptions : UNNotificationActionOptions = [ ]
171
+ return IterableButtonTypeDefault
172
+ }
173
+
174
+ private func getBoolValue( _ value: Any ? ) -> Bool ? {
175
+ return ( value as? NSNumber ) ? . boolValue
176
+ }
177
+
178
+ private func getActionButtonOptions( buttonType: String , openApp: Bool , requiresUnlock: Bool ) -> UNNotificationActionOptions {
179
+ var options : UNNotificationActionOptions = [ ]
180
+
142
181
if buttonType == IterableButtonTypeDestructive {
143
- actionOptions . insert ( . destructive)
182
+ options . insert ( . destructive)
144
183
}
145
184
146
185
if openApp {
147
- actionOptions . insert ( . foreground)
186
+ options . insert ( . foreground)
148
187
}
149
188
150
189
if requiresUnlock || openApp {
151
- actionOptions . insert ( . authenticationRequired)
190
+ options . insert ( . authenticationRequired)
152
191
}
153
192
154
- if buttonType == IterableButtonTypeTextInput {
155
- let inputTitle = buttonDictionary [ JsonKey . ActionButton. inputTitle] as? String ?? " "
156
- let inputPlaceholder = buttonDictionary [ JsonKey . ActionButton. inputPlaceholder] as? String ?? " "
157
-
158
- return UNTextInputNotificationAction ( identifier: identifier,
159
- title: title,
160
- options: actionOptions,
161
- textInputButtonTitle: inputTitle,
162
- textInputPlaceholder: inputPlaceholder)
163
- } else {
164
- return UNNotificationAction ( identifier: identifier, title: title, options: actionOptions)
193
+ return options
194
+ }
195
+
196
+ private func checkPushCreationCompletion( ) {
197
+ if getCategoryIdFinished && attachmentRetrievalFinished {
198
+ callContentHandler ( )
199
+ }
200
+ }
201
+
202
+ private func callContentHandler( ) {
203
+ stopCurrentAttachmentDownloadTask ( )
204
+
205
+ if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
206
+ contentHandler ( bestAttemptContent)
165
207
}
166
208
}
167
209
168
- private func getButtonType ( buttonDictionary : [ AnyHashable : Any ] ) -> String {
169
- guard let buttonType = buttonDictionary [ JsonKey . ActionButton . buttonType ] as? String else {
170
- return IterableButtonTypeDefault
210
+ private static func getAttachmentIdSuffix ( response : URLResponse , responseUrl : URL ) -> String {
211
+ if let suggestedFilename = response . suggestedFilename {
212
+ return suggestedFilename
171
213
}
172
214
173
- if buttonType == IterableButtonTypeTextInput || buttonType == IterableButtonTypeDestructive {
174
- return buttonType
175
- } else {
176
- return IterableButtonTypeDefault
215
+ return responseUrl. lastPathComponent
216
+ }
217
+
218
+ private var getCategoryIdFinished : Bool = false {
219
+ didSet {
220
+ checkPushCreationCompletion ( )
221
+ }
222
+ }
223
+
224
+ private var attachmentRetrievalFinished : Bool = false {
225
+ didSet {
226
+ checkPushCreationCompletion ( )
177
227
}
178
228
}
179
229
180
230
private var messageCategory : UNNotificationCategory ?
231
+ private var attachmentDownloadTask : URLSessionDownloadTask ?
181
232
private let IterableButtonTypeDefault = " default "
182
233
private let IterableButtonTypeDestructive = " destructive "
183
234
private let IterableButtonTypeTextInput = " textInput "
0 commit comments