Skip to content

Commit 9fc2715

Browse files
authored
Merge pull request #7345 from woocommerce/feat/7318-replace-appdelegate-remote-notifications-handling
Support both local & remote notifications in the app
2 parents daff737 + 6a8838e commit 9fc2715

File tree

9 files changed

+335
-220
lines changed

9 files changed

+335
-220
lines changed

WooCommerce/Classes/AppDelegate.swift

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
117117
ServiceLocator.pushNotesManager.registrationDidFail(with: error)
118118
}
119119

120-
func application(_ application: UIApplication,
121-
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
122-
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
123-
ServiceLocator.pushNotesManager.handleNotification(userInfo, onBadgeUpdateCompletion: {}, completionHandler: completionHandler)
120+
/// Called when the app receives a remote notification in the background.
121+
/// For local/remote notification tap events, please refer to `UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:)`.
122+
/// When receiving a local/remote notification in the foreground, please refer to
123+
/// `UNUserNotificationCenterDelegate.userNotificationCenter(_:willPresent:)`.
124+
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
125+
await ServiceLocator.pushNotesManager.handleRemoteNotificationInTheBackground(userInfo: userInfo)
124126
}
125127

126128
func applicationWillResignActive(_ application: UIApplication) {
@@ -419,27 +421,10 @@ extension AppDelegate {
419421

420422
extension AppDelegate: UNUserNotificationCenterDelegate {
421423
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
422-
switch response.actionIdentifier {
423-
case LocalNotification.Action.contactSupport.rawValue:
424-
guard let viewController = window?.rootViewController else {
425-
return
426-
}
427-
ZendeskProvider.shared.showNewRequestIfPossible(from: viewController, with: nil)
428-
ServiceLocator.analytics.track(.loginLocalNotificationTapped, withProperties: [
429-
"action": "contact_support",
430-
"type": response.notification.request.identifier
431-
])
432-
default:
433-
// Triggered when the user taps on the notification itself instead of one of the actions.
434-
switch response.notification.request.identifier {
435-
case LocalNotification.Scenario.loginSiteAddressError.rawValue:
436-
ServiceLocator.analytics.track(.loginLocalNotificationTapped, withProperties: [
437-
"action": "default",
438-
"type": response.notification.request.identifier
439-
])
440-
default:
441-
return
442-
}
443-
}
424+
await ServiceLocator.pushNotesManager.handleUserResponseToNotification(response)
425+
}
426+
427+
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
428+
await ServiceLocator.pushNotesManager.handleNotificationInTheForeground(notification)
444429
}
445430
}

WooCommerce/Classes/Authentication/AuthenticationManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ private extension AuthenticationManager {
423423

424424
let wooAuthError = AuthenticationError.make(with: error)
425425
switch wooAuthError {
426-
case .notWPSite, .notValidAddress:
426+
case .notWPSite, .notValidAddress, .noSecureConnection:
427427
let notification = LocalNotification(scenario: .loginSiteAddressError)
428428
ServiceLocator.pushNotesManager.cancelLocalNotification(scenarios: [notification.scenario])
429429
ServiceLocator.pushNotesManager.requestLocalNotification(notification,

WooCommerce/Classes/Notifications/PushNotificationsManager.swift

Lines changed: 116 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ final class PushNotificationsManager: PushNotesManager {
4545
/// Mutable reference to `inactiveNotifications`
4646
private let inactiveNotificationsSubject = PassthroughSubject<PushNotification, Never>()
4747

48+
/// An observable that emits values when a local notification is received.
49+
///
50+
var localNotificationUserResponses: AnyPublisher<UNNotificationResponse, Never> {
51+
localNotificationResponsesSubject.eraseToAnyPublisher()
52+
}
53+
54+
/// Mutable reference to `localNotificationResponses`.
55+
private let localNotificationResponsesSubject = PassthroughSubject<UNNotificationResponse, Never>()
56+
4857
/// Returns the current Application's State
4958
///
5059
private var applicationState: UIApplication.State {
@@ -220,48 +229,72 @@ extension PushNotificationsManager {
220229
unregisterForRemoteNotifications()
221230
}
222231

223-
224-
/// Handles a Remote Push Notification Payload. On completion the `completionHandler` will be executed.
232+
/// Handles a Notification while in Foreground Mode. Currently, only remote notifications are handled in the foreground.
225233
///
226-
func handleNotification(_ userInfo: [AnyHashable: Any],
227-
onBadgeUpdateCompletion: @escaping () -> Void,
228-
completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
229-
DDLogVerbose("📱 Push Notification Received: \n\(userInfo)\n")
230-
231-
// Badge: Update
232-
if let typeString = userInfo.string(forKey: APNSKey.type),
233-
let type = Note.Kind(rawValue: typeString),
234-
let siteID = siteID,
235-
let notificationSiteID = userInfo[APNSKey.siteID] as? Int64 {
236-
incrementNotificationCount(siteID: notificationSiteID, type: type, incrementCount: 1) { [weak self] in
237-
self?.loadNotificationCountAndUpdateApplicationBadgeNumberAndPostNotifications(siteID: siteID, type: type)
238-
onBadgeUpdateCompletion()
239-
}
234+
/// - Parameters:
235+
/// - userInfo: The Notification's Payload
236+
/// - completionHandler: A callback, to be executed on completion
237+
///
238+
/// - Returns: True when handled. False otherwise
239+
///
240+
@MainActor
241+
func handleNotificationInTheForeground(_ notification: UNNotification) async -> UNNotificationPresentationOptions {
242+
let content = notification.request.content
243+
guard applicationState == .active, content.isRemoteNotification else {
244+
// Local notifications are currently not handled when the app is in the foreground.
245+
return UNNotificationPresentationOptions(rawValue: 0)
240246
}
241247

242-
// Badge: Reset
243-
guard userInfo.string(forKey: APNSKey.type) != PushType.badgeReset else {
244-
return
248+
handleRemoteNotificationInAllAppStates(content.userInfo)
249+
250+
if let foregroundNotification = PushNotification.from(userInfo: content.userInfo) {
251+
configuration.application
252+
.presentInAppNotification(title: foregroundNotification.title,
253+
subtitle: foregroundNotification.subtitle,
254+
message: foregroundNotification.message,
255+
actionTitle: Localization.viewInAppNotification) { [weak self] in
256+
guard let self = self else { return }
257+
self.presentDetails(for: foregroundNotification)
258+
self.foregroundNotificationsToViewSubject.send(foregroundNotification)
259+
ServiceLocator.analytics.track(.viewInAppPushNotificationPressed,
260+
withProperties: [AnalyticKey.type: foregroundNotification.kind.rawValue])
261+
}
262+
263+
foregroundNotificationsSubject.send(foregroundNotification)
245264
}
246265

247-
// Analytics
248-
trackNotification(with: userInfo)
266+
_ = await synchronizeNotifications()
267+
return UNNotificationPresentationOptions(rawValue: 0)
268+
}
249269

250-
// Handling!
251-
let handlers = [
252-
handleSupportNotification,
253-
handleForegroundNotification,
254-
handleInactiveNotification,
255-
handleBackgroundNotification
256-
]
257-
258-
for handler in handlers {
259-
if handler(userInfo, completionHandler) {
260-
break
261-
}
270+
@MainActor
271+
func handleUserResponseToNotification(_ response: UNNotificationResponse) async {
272+
// Remote notification response is handled separately.
273+
if let notification = PushNotification.from(userInfo: response.notification.request.content.userInfo) {
274+
handleRemoteNotificationInAllAppStates(response.notification.request.content.userInfo)
275+
await handleInactiveRemoteNotification(notification: notification)
276+
} else {
277+
localNotificationResponsesSubject.send(response)
262278
}
263279
}
264280

281+
/// Handles a remote notification while the app is in the background.
282+
///
283+
/// - Parameter userInfo: The notification's payload.
284+
/// - Returns: Whether there is any data fetched in the background.
285+
@MainActor
286+
func handleRemoteNotificationInTheBackground(userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
287+
guard applicationState == .background, // Proceeds only if the app is in background.
288+
let _ = userInfo[APNSKey.identifier] // Ensures that we are only processing a remote notification.
289+
else {
290+
return .noData
291+
}
292+
293+
handleRemoteNotificationInAllAppStates(userInfo)
294+
295+
return await synchronizeNotifications()
296+
}
297+
265298
func requestLocalNotification(_ notification: LocalNotification, trigger: UNNotificationTrigger?) {
266299
Task {
267300
// TODO: 7318 - tech debt - replace `UNUserNotificationCenter.current()` with
@@ -371,103 +404,62 @@ private extension PushNotificationsManager {
371404
///
372405
/// - Returns: True when handled. False otherwise
373406
///
374-
func handleSupportNotification(_ userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool {
375-
407+
func handleSupportNotification(_ userInfo: [AnyHashable: Any]) -> Bool {
376408
guard userInfo.string(forKey: APNSKey.type) == PushType.zendesk else {
377-
return false
409+
return false
378410
}
379411

380-
self.configuration.supportManager.pushNotificationReceived()
412+
configuration.supportManager.pushNotificationReceived()
381413

382414
trackNotification(with: userInfo)
383415

384416
if applicationState == .inactive {
385-
self.configuration.supportManager.displaySupportRequest(using: userInfo)
417+
configuration.supportManager.displaySupportRequest(using: userInfo)
386418
}
387-
388-
completionHandler(.newData)
389-
390419
return true
391420
}
392421

393-
394-
/// Handles a Notification while in Foreground Mode
395-
///
396-
/// - Parameters:
397-
/// - userInfo: The Notification's Payload
398-
/// - completionHandler: A callback, to be executed on completion
399-
///
400-
/// - Returns: True when handled. False otherwise
422+
/// Handles a Remote Push Notification Payload regardless of the application state.
401423
///
402-
func handleForegroundNotification(_ userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool {
403-
guard applicationState == .active, let _ = userInfo[APNSKey.identifier] else {
404-
return false
405-
}
424+
func handleRemoteNotificationInAllAppStates(_ userInfo: [AnyHashable: Any]) {
425+
DDLogVerbose("📱 Push Notification Received: \n\(userInfo)\n")
406426

407-
if let foregroundNotification = PushNotification.from(userInfo: userInfo) {
408-
configuration.application
409-
.presentInAppNotification(title: foregroundNotification.title,
410-
subtitle: foregroundNotification.subtitle,
411-
message: foregroundNotification.message,
412-
actionTitle: Localization.viewInAppNotification) { [weak self] in
413-
guard let self = self else { return }
414-
self.presentDetails(for: foregroundNotification)
415-
self.foregroundNotificationsToViewSubject.send(foregroundNotification)
416-
ServiceLocator.analytics.track(.viewInAppPushNotificationPressed, withProperties: [AnalyticKey.type: foregroundNotification.kind.rawValue])
417-
}
427+
// Badge: Update
428+
if let typeString = userInfo.string(forKey: APNSKey.type),
429+
let type = Note.Kind(rawValue: typeString),
430+
let siteID = siteID,
431+
let notificationSiteID = userInfo[APNSKey.siteID] as? Int64 {
432+
incrementNotificationCount(siteID: notificationSiteID, type: type, incrementCount: 1) { [weak self] in
433+
self?.loadNotificationCountAndUpdateApplicationBadgeNumberAndPostNotifications(siteID: siteID, type: type)
434+
}
435+
}
418436

419-
foregroundNotificationsSubject.send(foregroundNotification)
437+
// Badge: Reset
438+
guard userInfo.string(forKey: APNSKey.type) != PushType.badgeReset else {
439+
return
420440
}
421441

422-
synchronizeNotifications(completionHandler: completionHandler)
442+
// Analytics
443+
trackNotification(with: userInfo)
423444

424-
return true
445+
// Handles support notification in different app states.
446+
// Note: support notifications are currently not working - https://github.com/woocommerce/woocommerce-ios/issues/3776
447+
_ = handleSupportNotification(userInfo)
425448
}
426449

427-
428-
/// Handles a Notification while in Inactive Mode
429-
///
430-
/// - Parameters:
431-
/// - userInfo: The Notification's Payload
432-
/// - completionHandler: A callback, to be executed on completion
433-
///
434-
/// - Returns: True when handled. False otherwise
450+
/// Handles a remote notification while the app is inactive.
435451
///
436-
func handleInactiveNotification(_ userInfo: [AnyHashable: Any], completionHandler: (UIBackgroundFetchResult) -> Void) -> Bool {
452+
/// - Parameter notification: Push notification content from a remote notification.
453+
func handleInactiveRemoteNotification(notification: PushNotification) async {
437454
guard applicationState == .inactive else {
438-
return false
455+
return
439456
}
440457

441-
DDLogVerbose("📱 Handling Notification in Inactive State")
442-
443-
if let notification = PushNotification.from(userInfo: userInfo) {
444-
presentDetails(for: notification)
445-
446-
inactiveNotificationsSubject.send(notification)
447-
}
458+
DDLogVerbose("📱 Handling Remote Notification in Inactive State")
448459

449-
completionHandler(.newData)
460+
presentDetails(for: notification)
450461

451-
return true
452-
}
453-
454-
455-
/// Handles a Notification while in Background Mode
456-
///
457-
/// - Parameters:
458-
/// - userInfo: The Notification's Payload
459-
/// - completionHandler: A callback, to be executed on completion
460-
///
461-
/// - Returns: True when handled. False otherwise
462-
///
463-
func handleBackgroundNotification(_ userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool {
464-
guard applicationState == .background, let _ = userInfo[APNSKey.identifier] else {
465-
return false
466-
}
467-
468-
synchronizeNotifications(completionHandler: completionHandler)
469-
470-
return true
462+
inactiveNotificationsSubject.send(notification)
471463
}
472464
}
473465

@@ -579,16 +571,19 @@ private extension PushNotificationsManager {
579571

580572
/// Synchronizes all of the Notifications. On success this method will always signal `.newData`, and `.noData` on error.
581573
///
582-
func synchronizeNotifications(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
583-
let action = NotificationAction.synchronizeNotifications { error in
584-
DDLogInfo("📱 Finished Synchronizing Notifications!")
574+
@MainActor
575+
func synchronizeNotifications() async -> UIBackgroundFetchResult {
576+
await withCheckedContinuation { continuation in
577+
let action = NotificationAction.synchronizeNotifications { error in
578+
DDLogInfo("📱 Finished Synchronizing Notifications!")
585579

586-
let result = (error == nil) ? UIBackgroundFetchResult.newData : .noData
587-
completionHandler(result)
588-
}
580+
let result = (error == nil) ? UIBackgroundFetchResult.newData : .noData
581+
continuation.resume(returning: result)
582+
}
589583

590-
DDLogInfo("📱 Synchronizing Notifications in \(applicationState.description) State...")
591-
configuration.storesManager.dispatch(action)
584+
DDLogInfo("📱 Synchronizing Notifications in \(applicationState.description) State...")
585+
configuration.storesManager.dispatch(action)
586+
}
592587
}
593588
}
594589

@@ -609,6 +604,14 @@ private extension PushNotification {
609604
}
610605
}
611606

607+
// MARK: - UNNotificationContent Extension
608+
609+
private extension UNNotificationContent {
610+
var isRemoteNotification: Bool {
611+
userInfo[APNSKey.identifier] != nil
612+
}
613+
}
614+
612615
// MARK: - App Icon Badge Number
613616

614617
enum AppIconBadgeNumber {

0 commit comments

Comments
 (0)