Skip to content

Commit b03282f

Browse files
authored
Merge pull request #2192 from woocommerce/issue/927-refresh-orders-on-push-while-active
Order List → Refresh When Receiving a new Order Notification While in the Foreground
2 parents 2ee87e1 + a7a1cee commit b03282f

File tree

10 files changed

+243
-9
lines changed

10 files changed

+243
-9
lines changed

RELEASE-NOTES.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
- Products: now the keyboard pop up automatically in Edit Description
55
- The Processing orders list will now show upcoming (future) orders.
66
- Improved stats: fixed the incorrect time range on "This Week" tab when loading improved stats on a day when daily saving time changes.
7-
- The Orders list are now automatically refreshed when reopening the app.
7+
- The Orders list is now automatically refreshed when reopening the app.
8+
- The Orders list is automatically refreshed if a new order (push notification) comes in.
89

910

1011
4.1

WooCommerce/Classes/Notifications/PushNotificationsManager.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ final class PushNotificationsManager: PushNotesManager {
1313
///
1414
let configuration: PushNotificationsConfiguration
1515

16+
/// Mutable reference to `foregroundNotifications`.
17+
private let foregroundNotificationsSubject = PublishSubject<ForegroundNotification>()
18+
19+
/// An observable that emits values when the Remote Notifications are received while the app is
20+
/// in the foreground.
21+
///
22+
var foregroundNotifications: Observable<ForegroundNotification> {
23+
foregroundNotificationsSubject
24+
}
25+
1626
/// Returns the current Application's State
1727
///
1828
private var applicationState: UIApplication.State {
@@ -251,8 +261,10 @@ private extension PushNotificationsManager {
251261
return false
252262
}
253263

254-
if let message = userInfo.dictionary(forKey: APNSKey.aps)?.string(forKey: APNSKey.alert) {
255-
configuration.application.presentInAppNotification(message: message)
264+
if let foregroundNotification = ForegroundNotification.from(userInfo: userInfo) {
265+
configuration.application.presentInAppNotification(message: foregroundNotification.message)
266+
267+
foregroundNotificationsSubject.send(foregroundNotification)
256268
}
257269

258270
synchronizeNotifications(completionHandler: completionHandler)
@@ -401,6 +413,20 @@ private extension PushNotificationsManager {
401413
}
402414
}
403415

416+
// MARK: - ForegroundNotification Extension
417+
418+
private extension ForegroundNotification {
419+
static func from(userInfo: [AnyHashable: Any]) -> ForegroundNotification? {
420+
guard let noteID = userInfo.integer(forKey: APNSKey.identifier),
421+
let message = userInfo.dictionary(forKey: APNSKey.aps)?.string(forKey: APNSKey.alert),
422+
let type = userInfo.string(forKey: APNSKey.type),
423+
let noteKind = Note.Kind(rawValue: type) else {
424+
return nil
425+
}
426+
427+
return ForegroundNotification(noteID: noteID, kind: noteKind, message: message)
428+
}
429+
}
404430

405431
// MARK: - Private Types
406432
//
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
import Foundation
3+
import struct Yosemite.Note
4+
5+
/// Emitted by `PushNotificationsManager` when a remote notification is received while
6+
/// the app is active.
7+
///
8+
struct ForegroundNotification {
9+
/// The `note_id` value received from the Remote Notification's `userInfo`.
10+
///
11+
let noteID: Int
12+
/// The `type` value received from the Remote Notification's `userInfo`.
13+
///
14+
let kind: Note.Kind
15+
/// The `alert` value received from the Remote Notification's `userInfo`.
16+
///
17+
let message: String
18+
}

WooCommerce/Classes/ServiceLocator/PushNotesManager.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import UIKit
33

44
protocol PushNotesManager {
55

6+
/// An observable that emits values when the Remote Notifications are received while the app is
7+
/// in the foreground.
8+
///
9+
var foregroundNotifications: Observable<ForegroundNotification> { get }
10+
611
/// Resets the Badge Count.
712
///
813
func resetBadgeCount()

WooCommerce/Classes/ViewRelated/Orders/OrdersViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ private extension OrdersViewController {
137137
/// Initialize ViewModel operations
138138
///
139139
func configureViewModel() {
140-
viewModel.onShouldResynchronizeAfterAppActivation = { [weak self] in
140+
viewModel.onShouldResynchronizeIfViewIsVisible = { [weak self] in
141141
guard let self = self,
142142
// Avoid synchronizing if the view is not visible. The refresh will be handled in
143143
// `viewWillAppear` instead.

WooCommerce/Classes/ViewRelated/Orders/OrdersViewModel.swift

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,17 @@ final class OrdersViewModel {
2424
}
2525

2626
private let storageManager: StorageManagerType
27+
private let pushNotificationsManager: PushNotesManager
2728
private let notificationCenter: NotificationCenter
2829

29-
/// The block called if self requests a resynchronization of the first page.
30+
/// Used for cancelling the observer for Remote Notifications when `self` is deallocated.
3031
///
31-
var onShouldResynchronizeAfterAppActivation: (() -> ())?
32+
private var cancellable: ObservationToken?
33+
34+
/// The block called if self requests a resynchronization of the first page. The
35+
/// resynchronization should only be done if the view is visible.
36+
///
37+
var onShouldResynchronizeIfViewIsVisible: (() -> ())?
3238

3339
/// OrderStatus that must be matched by retrieved orders.
3440
///
@@ -84,15 +90,21 @@ final class OrdersViewModel {
8490
}
8591

8692
init(storageManager: StorageManagerType = ServiceLocator.storageManager,
93+
pushNotificationsManager: PushNotesManager = ServiceLocator.pushNotesManager,
8794
notificationCenter: NotificationCenter = .default,
8895
statusFilter: OrderStatus?,
8996
includesFutureOrders: Bool = true) {
9097
self.storageManager = storageManager
98+
self.pushNotificationsManager = pushNotificationsManager
9199
self.notificationCenter = notificationCenter
92100
self.statusFilter = statusFilter
93101
self.includesFutureOrders = includesFutureOrders
94102
}
95103

104+
deinit {
105+
stopObservingForegroundRemoteNotifications()
106+
}
107+
96108
/// Start fetching DB results and forward new changes to the given `tableView`.
97109
///
98110
/// This is the main activation method for this ViewModel. This should only be called once.
@@ -106,6 +118,8 @@ final class OrdersViewModel {
106118
name: UIApplication.willResignActiveNotification, object: nil)
107119
notificationCenter.addObserver(self, selector: #selector(handleAppActivation),
108120
name: UIApplication.didBecomeActiveNotification, object: nil)
121+
122+
observeForegroundRemoteNotifications()
109123
}
110124

111125
/// Execute the `resultsController` query, logging the error if there's any.
@@ -130,7 +144,7 @@ final class OrdersViewModel {
130144
}
131145

132146
isAppActive = true
133-
onShouldResynchronizeAfterAppActivation?()
147+
onShouldResynchronizeIfViewIsVisible?()
134148
}
135149

136150
/// Returns what `OrderAction` should be used when synchronizing.
@@ -230,6 +244,29 @@ final class OrdersViewModel {
230244
}
231245
}
232246

247+
// MARK: - Remote Notifications Observation
248+
249+
private extension OrdersViewModel {
250+
/// Watch for "new order" Remote Notifications that are received while the app is in the
251+
/// foreground.
252+
///
253+
/// A refresh will be requested when receiving them.
254+
///
255+
func observeForegroundRemoteNotifications() {
256+
cancellable = pushNotificationsManager.foregroundNotifications.subscribe { [weak self] notification in
257+
guard notification.kind == .storeOrder else {
258+
return
259+
}
260+
261+
self?.onShouldResynchronizeIfViewIsVisible?()
262+
}
263+
}
264+
265+
func stopObservingForegroundRemoteNotifications() {
266+
cancellable?.cancel()
267+
}
268+
}
269+
233270
// MARK: - TableView Support
234271

235272
extension OrdersViewModel {

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@
311311
5754727B2451F14600A94C3C /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5754727A2451F14600A94C3C /* Observable.swift */; };
312312
5754727D2451F1D800A94C3C /* PublishSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5754727C2451F1D800A94C3C /* PublishSubject.swift */; };
313313
5754727F24520B2A00A94C3C /* Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5754727E24520B2A00A94C3C /* Observer.swift */; };
314+
575472812452185300A94C3C /* ForegroundNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575472802452185300A94C3C /* ForegroundNotification.swift */; };
314315
576F92222423C3C0003E5FEF /* OrdersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576F92212423C3C0003E5FEF /* OrdersViewModel.swift */; };
315316
5795F22C23E26A8D00F6707C /* OrderSearchStarterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5795F22B23E26A8D00F6707C /* OrderSearchStarterViewController.xib */; };
316317
5795F22E23E26A9E00F6707C /* OrderSearchStarterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5795F22D23E26A9E00F6707C /* OrderSearchStarterViewController.swift */; };
@@ -323,6 +324,7 @@
323324
57F34AA12423D45A00E38AFB /* OrdersViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F34AA02423D45A00E38AFB /* OrdersViewModelTests.swift */; };
324325
6856D2A5C2076F5BF14F2C11 /* KeyboardStateProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6856DCE1638958DA296D690F /* KeyboardStateProviderTests.swift */; };
325326
6856D31F941A33BAE66F394D /* KeyboardFrameAdjustmentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6856D3846BBF078CB5D955B9 /* KeyboardFrameAdjustmentProvider.swift */; };
327+
6856D49DB7DCF4D87745C0B1 /* MockPushNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6856D249FD1702FE3864950A /* MockPushNotificationsManager.swift */; };
326328
6856D806DE7DB61522D54044 /* NSMutableAttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6856D7981E11F85D5E4EFED7 /* NSMutableAttributedStringHelperTests.swift */; };
327329
6856DB2E741639716E149967 /* KeyboardStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6856D02484A69911F2B91714 /* KeyboardStateProvider.swift */; };
328330
6856DE479EC3B2265AC1F775 /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6856D66A1963092C34D20674 /* Calendar+Extensions.swift */; };
@@ -1131,6 +1133,7 @@
11311133
5754727A2451F14600A94C3C /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = "<group>"; };
11321134
5754727C2451F1D800A94C3C /* PublishSubject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishSubject.swift; sourceTree = "<group>"; };
11331135
5754727E24520B2A00A94C3C /* Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observer.swift; sourceTree = "<group>"; };
1136+
575472802452185300A94C3C /* ForegroundNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForegroundNotification.swift; sourceTree = "<group>"; };
11341137
576F92212423C3C0003E5FEF /* OrdersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersViewModel.swift; sourceTree = "<group>"; };
11351138
5795F22B23E26A8D00F6707C /* OrderSearchStarterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OrderSearchStarterViewController.xib; sourceTree = "<group>"; };
11361139
5795F22D23E26A9E00F6707C /* OrderSearchStarterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderSearchStarterViewController.swift; sourceTree = "<group>"; };
@@ -1143,6 +1146,7 @@
11431146
57F34AA02423D45A00E38AFB /* OrdersViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersViewModelTests.swift; sourceTree = "<group>"; };
11441147
6856D02484A69911F2B91714 /* KeyboardStateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardStateProvider.swift; sourceTree = "<group>"; };
11451148
6856D1A5F72A36AB3704D19D /* AgeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgeTests.swift; sourceTree = "<group>"; };
1149+
6856D249FD1702FE3864950A /* MockPushNotificationsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPushNotificationsManager.swift; sourceTree = "<group>"; };
11461150
6856D3846BBF078CB5D955B9 /* KeyboardFrameAdjustmentProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardFrameAdjustmentProvider.swift; sourceTree = "<group>"; };
11471151
6856D66A1963092C34D20674 /* Calendar+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Calendar+Extensions.swift"; sourceTree = "<group>"; };
11481152
6856D7981E11F85D5E4EFED7 /* NSMutableAttributedStringHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSMutableAttributedStringHelperTests.swift; sourceTree = "<group>"; };
@@ -2450,6 +2454,7 @@
24502454
02A275C523FE9EFC005C560F /* MockFeatureFlagService.swift */,
24512455
02B653AB2429F7BF00A9C839 /* MockTaxClassStoresManager.swift */,
24522456
02EA6BFB2435EC3500FFF90A /* MockImageDownloader.swift */,
2457+
6856D249FD1702FE3864950A /* MockPushNotificationsManager.swift */,
24532458
);
24542459
path = Mockups;
24552460
sourceTree = "<group>";
@@ -3669,6 +3674,7 @@
36693674
D831E2DB230E0558000037D0 /* Authentication.swift */,
36703675
D831E2DF230E0BA7000037D0 /* Logs.swift */,
36713676
02FE89C8231FB31400E85EF8 /* FeatureFlagService.swift */,
3677+
575472802452185300A94C3C /* ForegroundNotification.swift */,
36723678
);
36733679
path = ServiceLocator;
36743680
sourceTree = "<group>";
@@ -4664,6 +4670,7 @@
46644670
021AEF9E2407F55C00029D28 /* PHAssetImageLoader.swift in Sources */,
46654671
020BE74D23B1F5EB007FE54C /* TitleAndTextFieldTableViewCell.swift in Sources */,
46664672
D81F2D37225F0D160084BF9C /* EmptyListMessageWithActionView.swift in Sources */,
4673+
575472812452185300A94C3C /* ForegroundNotification.swift in Sources */,
46674674
B55BC1F121A878A30011A0C0 /* String+HTML.swift in Sources */,
46684675
B56C721221B5B44000E5E85B /* PushNotificationsConfiguration.swift in Sources */,
46694676
02279587237A50C900787C63 /* AztecHeaderFormatBarCommand.swift in Sources */,
@@ -4868,6 +4875,7 @@
48684875
6856D806DE7DB61522D54044 /* NSMutableAttributedStringHelperTests.swift in Sources */,
48694876
6856DF20E1BDCC391635F707 /* AgeTests.swift in Sources */,
48704877
6856DE479EC3B2265AC1F775 /* Calendar+Extensions.swift in Sources */,
4878+
6856D49DB7DCF4D87745C0B1 /* MockPushNotificationsManager.swift in Sources */,
48714879
);
48724880
runOnlyForDeploymentPostprocessing = 0;
48734881
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
import Foundation
3+
import UIKit
4+
@testable import WooCommerce
5+
6+
final class MockPushNotificationsManager: PushNotesManager {
7+
8+
var foregroundNotifications: Observable<ForegroundNotification> {
9+
foregroundNotificationsSubject
10+
}
11+
12+
private let foregroundNotificationsSubject = PublishSubject<ForegroundNotification>()
13+
14+
func resetBadgeCount() {
15+
16+
}
17+
18+
func registerForRemoteNotifications() {
19+
20+
}
21+
22+
func unregisterForRemoteNotifications() {
23+
24+
}
25+
26+
func ensureAuthorizationIsRequested(onCompletion: ((Bool) -> ())?) {
27+
28+
}
29+
30+
func registrationDidFail(with error: Error) {
31+
32+
}
33+
34+
func registerDeviceToken(with tokenData: Data, defaultStoreID: Int64) {
35+
36+
}
37+
38+
func handleNotification(_ userInfo: [AnyHashable: Any], completionHandler: @escaping (UIKit.UIBackgroundFetchResult) -> ()) {
39+
40+
}
41+
}
42+
43+
extension MockPushNotificationsManager {
44+
/// Send a ForegroundNotification that will be emitted by the `foregroundNotifications`
45+
/// observable.
46+
///
47+
func sendForegroundNotification(_ notification: ForegroundNotification) {
48+
foregroundNotificationsSubject.send(notification)
49+
}
50+
}

WooCommerce/WooCommerceTests/Notifications/PushNotificationsManagerTests.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,53 @@ final class PushNotificationsManagerTests: XCTestCase {
339339

340340
XCTAssertEqual(application.presentInAppMessages.first, Sample.defaultMessage)
341341
}
342+
343+
// MARK: - Foreground Notification Observable
344+
345+
func testItEmitsForegroundNotificationsWhenItReceivesANotificationWhileAppIsActive() {
346+
// Given
347+
application.applicationState = .active
348+
349+
var emittedNotifications = [ForegroundNotification]()
350+
_ = manager.foregroundNotifications.subscribe { notification in
351+
emittedNotifications.append(notification)
352+
}
353+
354+
let userinfo = notificationPayload(noteID: 9_981, type: .storeOrder)
355+
356+
// When
357+
manager.handleNotification(userinfo) { _ in
358+
// noop
359+
}
360+
361+
// Then
362+
XCTAssertEqual(emittedNotifications.count, 1)
363+
364+
let emittedNotification = emittedNotifications.first!
365+
XCTAssertEqual(emittedNotification.kind, .storeOrder)
366+
XCTAssertEqual(emittedNotification.noteID, 9_981)
367+
XCTAssertEqual(emittedNotification.message, Sample.defaultMessage)
368+
}
369+
370+
func testItDoesNotEmitForegroundNotificationsWhenItReceivesANotificationWhileAppIsNotActive() {
371+
// Given
372+
application.applicationState = .background
373+
374+
var emittedNotifications = [ForegroundNotification]()
375+
_ = manager.foregroundNotifications.subscribe { notification in
376+
emittedNotifications.append(notification)
377+
}
378+
379+
let userinfo = notificationPayload(noteID: 9_981, type: .storeOrder)
380+
381+
// When
382+
manager.handleNotification(userinfo) { _ in
383+
// noop
384+
}
385+
386+
// Then
387+
XCTAssertTrue(emittedNotifications.isEmpty)
388+
}
342389
}
343390

344391

0 commit comments

Comments
 (0)