Skip to content

Commit fa8347e

Browse files
committed
Add ReviewReply view and view model
1 parent 07d517d commit fa8347e

File tree

3 files changed

+146
-0
lines changed

3 files changed

+146
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import Foundation
2+
import SwiftUI
3+
4+
/// Hosting controller that wraps a `ReviewDetailsReply` view.
5+
///
6+
final class ReviewReplyHostingController: UIHostingController<ReviewReply>, UIAdaptivePresentationControllerDelegate {
7+
init(viewModel: ReviewReplyViewModel) {
8+
super.init(rootView: ReviewReply(viewModel: viewModel))
9+
10+
// Needed because a `SwiftUI` cannot be dismissed when being presented by a UIHostingController
11+
rootView.dismiss = { [weak self] in
12+
self?.dismiss(animated: true, completion: nil)
13+
}
14+
15+
// Set presentation delegate to track the user dismiss flow event
16+
presentationController?.delegate = self
17+
}
18+
19+
required dynamic init?(coder aDecoder: NSCoder) {
20+
fatalError("init(coder:) has not been implemented")
21+
}
22+
}
23+
24+
/// Allows merchant to reply to a product review.
25+
///
26+
struct ReviewReply: View {
27+
28+
/// Set this closure with UIKit dismiss code. Needed because we need access to the UIHostingController `dismiss` method.
29+
///
30+
var dismiss: (() -> Void) = {}
31+
32+
/// View Model for the view
33+
///
34+
@ObservedObject private(set) var viewModel: ReviewReplyViewModel
35+
36+
var body: some View {
37+
NavigationView {
38+
TextEditor(text: $viewModel.newReply)
39+
.focused()
40+
.padding()
41+
.navigationTitle(Localization.title)
42+
.navigationBarTitleDisplayMode(.inline)
43+
.toolbar {
44+
ToolbarItem(placement: .cancellationAction) {
45+
Button(Localization.cancel, action: {
46+
dismiss()
47+
})
48+
}
49+
ToolbarItem(placement: .confirmationAction) {
50+
navigationBarTrailingItem()
51+
}
52+
}
53+
}
54+
.wooNavigationBarStyle()
55+
.navigationViewStyle(.stack)
56+
}
57+
58+
/// Decides if the navigation trailing item should be a send button or a loading indicator.
59+
///
60+
@ViewBuilder private func navigationBarTrailingItem() -> some View {
61+
switch viewModel.navigationTrailingItem {
62+
case .send(let enabled):
63+
Button(Localization.send) {
64+
viewModel.sendReply { success in
65+
if success {
66+
dismiss()
67+
}
68+
}
69+
}
70+
.disabled(!enabled)
71+
case .loading:
72+
ProgressView()
73+
}
74+
}
75+
}
76+
77+
// MARK: Constants
78+
private enum Localization {
79+
static let title = NSLocalizedString("Reply to Product Review", comment: "Title for the product review reply screen")
80+
static let send = NSLocalizedString("Send", comment: "Text for the send button in the product review reply screen")
81+
static let cancel = NSLocalizedString("Cancel", comment: "Text for the cancel button in the product review reply screen")
82+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Foundation
2+
import Combine
3+
import Yosemite
4+
5+
/// View model for the `ReviewReply` screen.
6+
///
7+
final class ReviewReplyViewModel: ObservableObject {
8+
9+
/// New reply to send
10+
///
11+
@Published var newReply: String = ""
12+
13+
/// Defaults to a disabled send button.
14+
///
15+
@Published private(set) var navigationTrailingItem: ReviewReplyNavigationItem = .send(enabled: false)
16+
17+
/// Tracks if a network request is being performed.
18+
///
19+
private let performingNetworkRequest: CurrentValueSubject<Bool, Never> = .init(false)
20+
21+
init() {
22+
bindNavigationTrailingItemPublisher()
23+
}
24+
25+
/// Called when the user taps on the Send button.
26+
///
27+
/// Use this method to send the reply and invoke a completion block when finished
28+
///
29+
func sendReply(onCompletion: @escaping (Bool) -> Void) {
30+
// TODO: Call CommentAction.replyToComment to send the reply to remote
31+
// Set `performingNetworkRequest` to true while the request is being performed
32+
}
33+
}
34+
35+
// MARK: Helper Methods
36+
private extension ReviewReplyViewModel {
37+
/// Calculates what navigation trailing item should be shown depending on our internal state.
38+
///
39+
func bindNavigationTrailingItemPublisher() {
40+
Publishers.CombineLatest($newReply, performingNetworkRequest)
41+
.map { newReply, performingNetworkRequest in
42+
guard !performingNetworkRequest else {
43+
return .loading
44+
}
45+
return .send(enabled: newReply.isNotEmpty)
46+
}
47+
.assign(to: &$navigationTrailingItem)
48+
}
49+
}
50+
51+
/// Representation of possible navigation bar trailing buttons
52+
///
53+
enum ReviewReplyNavigationItem: Equatable {
54+
case send(enabled: Bool)
55+
case loading
56+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,6 +1355,8 @@
13551355
CC2A08062863222500510C4B /* orders_3337_add_fee.json in Resources */ = {isa = PBXBuildFile; fileRef = CC2A08052863222500510C4B /* orders_3337_add_fee.json */; };
13561356
CC2A0808286337A300510C4B /* CustomerNoteScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2A0807286337A300510C4B /* CustomerNoteScreen.swift */; };
13571357
CC2E72F727B6BFB800A62872 /* ProductVariationFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2E72F627B6BFB800A62872 /* ProductVariationFormatterTests.swift */; };
1358+
CC3B35DB28E5A6830036B097 /* ReviewReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3B35DA28E5A6830036B097 /* ReviewReply.swift */; };
1359+
CC3B35DD28E5A6EA0036B097 /* ReviewReplyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3B35DC28E5A6EA0036B097 /* ReviewReplyViewModel.swift */; };
13581360
CC440E1E2770C6AF0074C264 /* ProductInOrderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC440E1D2770C6AF0074C264 /* ProductInOrderViewModel.swift */; };
13591361
CC4A4E962655273D00B75DCD /* ShippingLabelPaymentMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC4A4E952655273D00B75DCD /* ShippingLabelPaymentMethods.swift */; };
13601362
CC4A4ED82655478D00B75DCD /* ShippingLabelPaymentMethodsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC4A4ED72655478D00B75DCD /* ShippingLabelPaymentMethodsViewModel.swift */; };
@@ -3265,6 +3267,8 @@
32653267
CC2A08052863222500510C4B /* orders_3337_add_fee.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = orders_3337_add_fee.json; sourceTree = "<group>"; };
32663268
CC2A0807286337A300510C4B /* CustomerNoteScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerNoteScreen.swift; sourceTree = "<group>"; };
32673269
CC2E72F627B6BFB800A62872 /* ProductVariationFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationFormatterTests.swift; sourceTree = "<group>"; };
3270+
CC3B35DA28E5A6830036B097 /* ReviewReply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewReply.swift; sourceTree = "<group>"; };
3271+
CC3B35DC28E5A6EA0036B097 /* ReviewReplyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewReplyViewModel.swift; sourceTree = "<group>"; };
32683272
CC440E1D2770C6AF0074C264 /* ProductInOrderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInOrderViewModel.swift; sourceTree = "<group>"; };
32693273
CC4A4E952655273D00B75DCD /* ShippingLabelPaymentMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPaymentMethods.swift; sourceTree = "<group>"; };
32703274
CC4A4ED72655478D00B75DCD /* ShippingLabelPaymentMethodsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPaymentMethodsViewModel.swift; sourceTree = "<group>"; };
@@ -6901,6 +6905,8 @@
69016905
D8C2A28E231BD00500F503E9 /* ReviewsViewModel.swift */,
69026906
B5F8B7DF2194759100DAB7E2 /* ReviewDetailsViewController.swift */,
69036907
B5F8B7E4219478FA00DAB7E2 /* ReviewDetailsViewController.xib */,
6908+
CC3B35DA28E5A6830036B097 /* ReviewReply.swift */,
6909+
CC3B35DC28E5A6EA0036B097 /* ReviewReplyViewModel.swift */,
69046910
023A059824135F2600E3FC99 /* ReviewsViewController.swift */,
69056911
023A059924135F2600E3FC99 /* ReviewsViewController.xib */,
69066912
5718852B2465D9EC00E2486F /* ReviewsCoordinator.swift */,
@@ -10228,6 +10234,7 @@
1022810234
45B4F0262860BD0A00F3B16E /* WCShipCTAView.swift in Sources */,
1022910235
020BE74D23B1F5EB007FE54C /* TitleAndTextFieldTableViewCell.swift in Sources */,
1023010236
023D692E2588BF0900F7DA72 /* ShippingLabelPaperSizeListSelectorCommand.swift in Sources */,
10237+
CC3B35DD28E5A6EA0036B097 /* ReviewReplyViewModel.swift in Sources */,
1023110238
CC72BB6427BD842500837876 /* DisclosureIndicator.swift in Sources */,
1023210239
77E53EC52510C193003D385F /* ProductDownloadListViewController+Droppable.swift in Sources */,
1023310240
3F50FE4328CAEBA800C89201 /* AppLocalizedString.swift in Sources */,
@@ -10390,6 +10397,7 @@
1039010397
DE3404E828B4B96800CF0D97 /* NonAtomicSiteViewModel.swift in Sources */,
1039110398
D8815B0126385E3F00EDAD62 /* CardPresentModalTapCard.swift in Sources */,
1039210399
773077EE251E943700178696 /* ProductDownloadFileViewController.swift in Sources */,
10400+
CC3B35DB28E5A6830036B097 /* ReviewReply.swift in Sources */,
1039310401
45D1CF4523BAC2A500945A36 /* ProductTaxClassListSelectorDataSource.swift in Sources */,
1039410402
319A626127ACAE3400BC96C3 /* InPersonPaymentsPluginChoicesView.swift in Sources */,
1039510403
6856D31F941A33BAE66F394D /* KeyboardFrameAdjustmentProvider.swift in Sources */,

0 commit comments

Comments
 (0)