Skip to content

Commit ab3ba1c

Browse files
committed
Move notice display logic into hosting controller
1 parent dd11d7a commit ab3ba1c

File tree

3 files changed

+86
-65
lines changed

3 files changed

+86
-65
lines changed

WooCommerce/Classes/ViewRelated/Reviews/ReviewReply.swift

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,70 @@
11
import Foundation
22
import SwiftUI
3+
import Combine
34

45
/// Hosting controller that wraps a `ReviewDetailsReply` view.
56
///
67
final class ReviewReplyHostingController: UIHostingController<ReviewReply>, UIAdaptivePresentationControllerDelegate {
78

89
private let viewModel: ReviewReplyViewModel
910

10-
init(viewModel: ReviewReplyViewModel) {
11+
/// Presents notices in the tab bar context.
12+
///
13+
private let systemNoticePresenter: NoticePresenter
14+
15+
/// Presents notices in the current modal presentation context.
16+
///
17+
private lazy var modalNoticePresenter: NoticePresenter = {
18+
let presenter = DefaultNoticePresenter()
19+
presenter.presentingViewController = self
20+
return presenter
21+
}()
22+
23+
/// Emits the intent to present a `ReviewReplyNotice`
24+
///
25+
private lazy var presentNoticePublisher = {
26+
viewModel.presentNoticeSubject.eraseToAnyPublisher()
27+
}()
28+
29+
/// References to keep the Combine subscriptions alive within the lifecycle of the object.
30+
///
31+
private var subscriptions: Set<AnyCancellable> = []
32+
33+
init(viewModel: ReviewReplyViewModel,
34+
noticePresenter: NoticePresenter = ServiceLocator.noticePresenter) {
1135
self.viewModel = viewModel
36+
self.systemNoticePresenter = noticePresenter
1237
super.init(rootView: ReviewReply(viewModel: viewModel))
1338

1439
// Needed because a `SwiftUI` cannot be dismissed when being presented by a UIHostingController
1540
rootView.dismiss = { [weak self] in
1641
self?.dismiss(animated: true, completion: nil)
1742
}
1843

19-
// This notice presenter is needed to display an error notice without closing the modal if the network request fails.
20-
let errorNoticePresenter = DefaultNoticePresenter()
21-
errorNoticePresenter.presentingViewController = self
22-
viewModel.modalNoticePresenter = errorNoticePresenter
44+
// Observe the present notice intent.
45+
presentNoticePublisher
46+
.compactMap { $0 }
47+
.sink { [weak self] notice in
48+
switch notice {
49+
case .success:
50+
self?.systemNoticePresenter.enqueue(notice: Notice(title: Localization.success, feedbackType: .success))
51+
case .error:
52+
let noticeIdentifier = UUID().uuidString
53+
let notice = Notice(title: Localization.error,
54+
feedbackType: .error,
55+
notificationInfo: NoticeNotificationInfo(identifier: noticeIdentifier),
56+
actionTitle: Localization.retry) { [weak self] in
57+
self?.viewModel.sendReply(onCompletion: { [weak self] success in
58+
if success {
59+
self?.dismiss(animated: true, completion: nil)
60+
}
61+
})
62+
}
63+
64+
self?.modalNoticePresenter.enqueue(notice: notice)
65+
}
66+
}
67+
.store(in: &subscriptions)
2368

2469
// Set presentation delegate to track the user dismiss flow event
2570
presentationController?.delegate = self
@@ -83,9 +128,19 @@ struct ReviewReply: View {
83128
}
84129
}
85130

131+
enum ReviewReplyNotice: Equatable {
132+
case success
133+
case error
134+
}
135+
86136
// MARK: Constants
87137
private enum Localization {
88138
static let title = NSLocalizedString("Reply to Product Review", comment: "Title for the product review reply screen")
89139
static let send = NSLocalizedString("Send", comment: "Text for the send button in the product review reply screen")
90140
static let cancel = NSLocalizedString("Cancel", comment: "Text for the cancel button in the product review reply screen")
141+
142+
// Notice strings
143+
static let success = NSLocalizedString("Reply sent!", comment: "Notice text after sending a reply to a product review successfully")
144+
static let error = NSLocalizedString("There was an error sending the reply", comment: "Notice text after failing to send a reply to a product review")
145+
static let retry = NSLocalizedString("Retry", comment: "Retry Action")
91146
}

WooCommerce/Classes/ViewRelated/Reviews/ReviewReplyViewModel.swift

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,16 @@ final class ReviewReplyViewModel: ObservableObject {
2828
///
2929
private let stores: StoresManager
3030

31-
/// Presents a success notice in the tab bar context.
31+
/// Trigger to present a `ReviewReplyNotice`
3232
///
33-
private let noticePresenter: NoticePresenter
34-
35-
/// Presents an error notice in the current modal presentation context.
36-
///
37-
var modalNoticePresenter: NoticePresenter?
33+
let presentNoticeSubject = PassthroughSubject<ReviewReplyNotice, Never>()
3834

3935
init(siteID: Int64,
4036
reviewID: Int64,
41-
stores: StoresManager = ServiceLocator.stores,
42-
noticePresenter: NoticePresenter = ServiceLocator.noticePresenter) {
37+
stores: StoresManager = ServiceLocator.stores) {
4338
self.siteID = siteID
4439
self.reviewID = reviewID
4540
self.stores = stores
46-
self.noticePresenter = noticePresenter
4741
bindNavigationTrailingItemPublisher()
4842
}
4943

@@ -68,10 +62,10 @@ final class ReviewReplyViewModel: ObservableObject {
6862
DDLogInfo("Reply to product review succeeded with comment status: \(status)")
6963
}
7064

71-
self.displayReplySuccessNotice()
65+
self.presentNoticeSubject.send(.success)
7266
onCompletion(true)
7367
case .failure(let error):
74-
self.displayReplyErrorNotice(onCompletion: onCompletion)
68+
self.presentNoticeSubject.send(.error)
7569
DDLogError("⛔️ Error replying to product review: \(error)")
7670
onCompletion(false)
7771
}
@@ -96,35 +90,6 @@ private extension ReviewReplyViewModel {
9690
}
9791
.assign(to: &$navigationTrailingItem)
9892
}
99-
100-
/// Enqueues the `Reply sent` success notice.
101-
///
102-
func displayReplySuccessNotice() {
103-
noticePresenter.enqueue(notice: Notice(title: Localization.success, feedbackType: .success))
104-
}
105-
106-
/// Enqueues the `Error sending reply` notice.
107-
///
108-
func displayReplyErrorNotice(onCompletion: @escaping (Bool) -> Void) {
109-
let noticeIdentifier = UUID().uuidString
110-
let notice = Notice(title: Localization.error,
111-
feedbackType: .error,
112-
notificationInfo: NoticeNotificationInfo(identifier: noticeIdentifier),
113-
actionTitle: Localization.retry) { [weak self] in
114-
self?.sendReply(onCompletion: onCompletion)
115-
}
116-
117-
modalNoticePresenter?.enqueue(notice: notice)
118-
}
119-
}
120-
121-
// MARK: Localization
122-
private extension ReviewReplyViewModel {
123-
enum Localization {
124-
static let success = NSLocalizedString("Reply sent!", comment: "Notice text after sending a reply to a product review successfully")
125-
static let error = NSLocalizedString("There was an error sending the reply", comment: "Notice text after failing to send a reply to a product review")
126-
static let retry = NSLocalizedString("Retry", comment: "Retry Action")
127-
}
12893
}
12994

13095
/// Representation of possible navigation bar trailing buttons

WooCommerce/WooCommerceTests/ViewRelated/Reviews/ReviewReplyViewModelTests.swift

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import XCTest
2+
import Combine
23
@testable import WooCommerce
34
@testable import Yosemite
45

@@ -8,6 +9,8 @@ class ReviewReplyViewModelTests: XCTestCase {
89

910
private let sampleReviewID: Int64 = 7
1011

12+
private var subscriptions: [AnyCancellable] = []
13+
1114
func test_send_button_is_disabled_when_reply_content_is_empty() {
1215
// Given
1316
let viewModel = ReviewReplyViewModel(siteID: sampleSiteID, reviewID: sampleReviewID)
@@ -128,11 +131,10 @@ class ReviewReplyViewModelTests: XCTestCase {
128131
XCTAssertFalse(successResponse)
129132
}
130133

131-
func test_view_model_enqueues_success_notice_after_reply_is_sent_successfully() {
134+
func test_view_model_triggers_success_notice_after_reply_is_sent_successfully() {
132135
// Given
133136
let stores = MockStoresManager(sessionManager: .testingInstance)
134-
let noticePresenter = MockNoticePresenter()
135-
let viewModel = ReviewReplyViewModel(siteID: sampleSiteID, reviewID: sampleReviewID, stores: stores, noticePresenter: noticePresenter)
137+
let viewModel = ReviewReplyViewModel(siteID: sampleSiteID, reviewID: sampleReviewID, stores: stores)
136138
stores.whenReceivingAction(ofType: CommentAction.self) { action in
137139
switch action {
138140
case let .replyToComment(_, _, _, onCompletion):
@@ -142,25 +144,23 @@ class ReviewReplyViewModelTests: XCTestCase {
142144
}
143145
}
144146

147+
var noticeTypes: [ReviewReplyNotice] = []
148+
viewModel.presentNoticeSubject.sink { notice in
149+
noticeTypes.append(notice)
150+
}.store(in: &subscriptions)
151+
145152
// When
146153
viewModel.newReply = "New reply"
147-
let noticeType: UINotificationFeedbackGenerator.FeedbackType? = waitFor { promise in
148-
viewModel.sendReply { _ in
149-
promise(noticePresenter.queuedNotices.first?.feedbackType)
150-
}
151-
}
154+
viewModel.sendReply { _ in }
152155

153156
// Then
154-
XCTAssertEqual(noticeType, .success)
157+
XCTAssertEqual(noticeTypes, [.success])
155158
}
156159

157-
func test_view_model_enqueues_error_notice_using_modal_notice_presenter_after_reply_fails() {
160+
func test_view_model_triggers_error_notice_using_modal_notice_presenter_after_reply_fails() {
158161
// Given
159162
let stores = MockStoresManager(sessionManager: .testingInstance)
160-
let noticePresenter = MockNoticePresenter()
161-
let modalNoticePresenter = MockNoticePresenter()
162-
let viewModel = ReviewReplyViewModel(siteID: sampleSiteID, reviewID: sampleReviewID, stores: stores, noticePresenter: noticePresenter)
163-
viewModel.modalNoticePresenter = modalNoticePresenter
163+
let viewModel = ReviewReplyViewModel(siteID: sampleSiteID, reviewID: sampleReviewID, stores: stores)
164164
stores.whenReceivingAction(ofType: CommentAction.self) { action in
165165
switch action {
166166
case let .replyToComment(_, _, _, onCompletion):
@@ -170,15 +170,16 @@ class ReviewReplyViewModelTests: XCTestCase {
170170
}
171171
}
172172

173+
var noticeTypes: [ReviewReplyNotice] = []
174+
viewModel.presentNoticeSubject.sink { notice in
175+
noticeTypes.append(notice)
176+
}.store(in: &subscriptions)
177+
173178
// When
174179
viewModel.newReply = "New reply"
175-
let noticeType: UINotificationFeedbackGenerator.FeedbackType? = waitFor { promise in
176-
viewModel.sendReply { _ in
177-
promise(modalNoticePresenter.queuedNotices.first?.feedbackType)
178-
}
179-
}
180+
viewModel.sendReply { _ in }
180181

181182
// Then
182-
XCTAssertEqual(noticeType, .error)
183+
XCTAssertEqual(noticeTypes, [.error])
183184
}
184185
}

0 commit comments

Comments
 (0)