Skip to content

Commit 429d855

Browse files
committed
Merge branch 'develop' into issue/2003-remove-segue-startMagicLinkFlow
2 parents 0cbc75e + 6b1077b commit 429d855

File tree

18 files changed

+905
-81
lines changed

18 files changed

+905
-81
lines changed

RELEASE-NOTES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- Improved stats: fixed the incorrect time range on "This Week" tab when loading improved stats on a day when daily saving time changes.
77
- The Orders list are now automatically refreshed when reopening the app.
88
- [internal]: the "send magic link" screen has navigation changes that can cause regressions. See https://git.io/Jfqio for testing details.
9+
- The Orders list is now automatically refreshed when reopening the app.
10+
- The Orders list is automatically refreshed if a new order (push notification) comes in.
911

1012

1113
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()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
import Foundation
3+
4+
/// Signature of a block called for every emitted Observable value.
5+
///
6+
typealias OnNext<Element> = (Element) -> ()
7+
8+
/// Emits values over time.
9+
///
10+
/// Acts like `Publisher` in Combine and `Observable` in ReactiveX.
11+
///
12+
/// This class is a pseudo-abstract class. It does not do anything on its own. Use the
13+
/// subclasses like `PublishSubject` instead.
14+
///
15+
/// See here for more info about Observables:
16+
///
17+
/// - https://developer.apple.com/documentation/combine/publisher
18+
/// - http://reactivex.io/documentation/observable.html
19+
///
20+
class Observable<Element> {
21+
/// Subscribe to values emitted by this `Observable`.
22+
///
23+
/// The given `onNext` is called a "Observer" or "Subscriber".
24+
///
25+
/// Example:
26+
///
27+
/// ```
28+
/// class ViewModel {
29+
/// let onDataLoaded: Observable<[Item]>
30+
/// }
31+
///
32+
/// func viewDidLoad() {
33+
/// viewModel.onDataLoaded.subscribe { items in
34+
/// // do something with `items`
35+
/// tableView.reloadData()
36+
/// }
37+
/// }
38+
/// ```
39+
///
40+
func subscribe(_ onNext: @escaping OnNext<Element>) -> ObservationToken {
41+
fatalError("Abstract method. This must be implemented by subclasses.")
42+
}
43+
}

WooCommerce/Classes/Tools/ObservationToken.swift renamed to WooCommerce/Classes/Tools/Observables/ObservationToken.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
/// An observation token that contains a closure when the observation is cancelled.
2+
///
3+
/// This acts like `AnyCancellable` in Combine and `IDisposable` in ReactiveX.
4+
///
25
/// Example usage can be found in `ProductImageActionHandler`.
36
///
7+
/// See:
8+
///
9+
/// - https://developer.apple.com/documentation/combine/anycancellable
10+
///
411
final class ObservationToken {
512
private let onCancel: () -> Void
613

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
import Foundation
3+
4+
/// A subscription to an `Observable`.
5+
///
6+
/// Acts like `Subscriber` in Combine and `IObserver` in ReactiveX.
7+
///
8+
/// Observers are simply the callbacks when you subscribe to an `Observable`. Consider this:
9+
///
10+
/// ```
11+
/// viewModel.onDataLoaded.subscribe { items in
12+
/// /// do something
13+
/// }
14+
/// ```
15+
///
16+
/// The block passed to the `subscribe` method with the "do something" comment is the `Observer`.
17+
///
18+
/// Currently, this `struct` is simply a container to clarify these concepts. In other frameworks,
19+
/// an `Observer` can have more callbacks like `onCompleted`.
20+
///
21+
/// See these for more info about Observers:
22+
///
23+
/// - https://developer.apple.com/documentation/combine/subscriber
24+
/// - http://introtorx.com/Content/v1.0.10621.0/02_KeyTypes.html#IObserver
25+
///
26+
struct Observer<Element> {
27+
private let onNext: (Element) -> ()
28+
29+
init(onNext: @escaping OnNext<Element>) {
30+
self.onNext = onNext
31+
}
32+
33+
/// Send the given value to the observer.
34+
///
35+
func send(_ element: Element) {
36+
onNext(element)
37+
}
38+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
2+
import Foundation
3+
4+
/// Emits values to observers as soon as the values arrive. Only the values emitted after the
5+
/// subscription will be emitted.
6+
///
7+
/// Acts like `PassthroughSubject` in Combine and `PublishSubject` in ReactiveX.
8+
///
9+
/// This observable is a bridge between the imperative and reactive programming paradigms. It
10+
/// allows consumers to _manually_ emit values using the `send()` method.
11+
///
12+
/// Multiple observers are allowed which makes this a possible replacement for
13+
/// `NSNotificationCenter` observations.
14+
///
15+
/// ## Example Usage
16+
///
17+
/// In a class that you would like to emit values (or events), add the `PublishSubject` defining
18+
/// the value type:
19+
///
20+
/// ```
21+
/// class PostListViewModel {
22+
/// /// Calls observers/subscribers whenever the list of Post changes.
23+
/// private let postsSubject = PublishSubject<[Post]>()
24+
/// }
25+
/// ```
26+
///
27+
/// Since `PublishSubject` exposes `send()` which makes this a **mutable** Observable, we recommend
28+
/// exposing only the `Observable<[Post]>` interface:
29+
///
30+
/// ```
31+
/// class PostListViewModel {
32+
/// private let postsSubject = PublishSubject<[Post]>()
33+
///
34+
/// /// The public Observable that the ViewController will subscribe to
35+
/// var posts: Observable<[Post]> {
36+
/// postsSubject
37+
/// }
38+
/// }
39+
/// ```
40+
///
41+
/// The `ViewController` can then subscribe to the `posts` Observable:
42+
///
43+
/// ```
44+
/// func viewDidLoad() {
45+
/// viewModel.posts.subscribe { posts in
46+
/// // do something with posts
47+
/// tableView.reloadData()
48+
/// }
49+
/// }
50+
/// ```
51+
///
52+
/// Whenever the list of post changes, like after fetching from the API, the `ViewModel` can
53+
/// _notify_ the `ViewController` by updating `postsSubject`:
54+
///
55+
/// ```
56+
/// fetchFromAPI { fetchedPosts
57+
/// // Notify the observers (e.g. ViewController) that the list of posts have changed
58+
/// postsSubject.send(fetchedPosts)
59+
/// }
60+
/// ```
61+
///
62+
/// ## References
63+
///
64+
/// See here for info about similar observables in other frameworks:
65+
///
66+
/// - https://developer.apple.com/documentation/combine/passthroughsubject
67+
/// - http://reactivex.io/documentation/subject.html
68+
///
69+
final class PublishSubject<Element>: Observable<Element> {
70+
71+
private typealias OnCancel = () -> ()
72+
73+
/// The list of Observers that will be notified when a new value is sent.
74+
///
75+
private var observers = [UUID: Observer<Element>]()
76+
77+
override func subscribe(_ onNext: @escaping OnNext<Element>) -> ObservationToken {
78+
let uuid = UUID()
79+
80+
observers[uuid] = Observer(onNext: onNext)
81+
82+
let onCancel: OnCancel = { [weak self] in
83+
self?.observers.removeValue(forKey: uuid)
84+
}
85+
86+
return ObservationToken(onCancel: onCancel)
87+
}
88+
89+
/// Emit a new value. All observers are immediately called with the given value.
90+
///
91+
func send(_ element: Element) {
92+
observers.values.forEach { observer in
93+
observer.send(element)
94+
}
95+
}
96+
}

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 {

0 commit comments

Comments
 (0)