Skip to content

Commit 375c344

Browse files
committed
Create Inbox and InboxNoteRow with view model, and show Inbox from the menu tab.
1 parent d9f74f0 commit 375c344

File tree

6 files changed

+449
-1
lines changed

6 files changed

+449
-1
lines changed

WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ struct HubMenu: View {
4646
showingViewStore = true
4747
case .inbox:
4848
// TODO: Inbox analytics
49-
// TODO: 5954 - Show Inbox view
5049
showingInbox = true
5150
case .reviews:
5251
ServiceLocator.analytics.track(.hubMenuOptionTapped, withProperties: [Constants.option: "reviews"])
@@ -66,6 +65,11 @@ struct HubMenu: View {
6665
}
6766
.safariSheet(isPresented: $showingWooCommerceAdmin, url: viewModel.woocommerceAdminURL)
6867
.safariSheet(isPresented: $showingViewStore, url: viewModel.storeURL)
68+
NavigationLink(destination:
69+
Inbox(viewModel: .init(siteID: viewModel.siteID)),
70+
isActive: $showingInbox) {
71+
EmptyView()
72+
}.hidden()
6973
NavigationLink(destination:
7074
ReviewsView(siteID: viewModel.siteID),
7175
isActive: $showingReviews) {
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import Combine
2+
import SwiftUI
3+
import Yosemite
4+
5+
/// Shows a list of inbox notes as shown in WooCommerce Admin in core.
6+
struct Inbox: View {
7+
/// View model that drives the view.
8+
@ObservedObject private(set) var viewModel: InboxViewModel
9+
10+
init(viewModel: InboxViewModel) {
11+
self.viewModel = viewModel
12+
}
13+
14+
var body: some View {
15+
Group {
16+
switch viewModel.syncState {
17+
case .results:
18+
InfiniteScrollList(isLoading: viewModel.shouldShowBottomActivityIndicator,
19+
loadAction: viewModel.onLoadNextPageAction) {
20+
ForEach(viewModel.noteRowViewModels) { rowViewModel in
21+
if #available(iOS 15.0, *) {
22+
// In order to show full-width separator, the default list separator is hidden and a `Divider` is shown inside the row.
23+
InboxNoteRow(viewModel: rowViewModel)
24+
.listRowSeparator(.hidden)
25+
} else {
26+
InboxNoteRow(viewModel: rowViewModel)
27+
}
28+
}
29+
}
30+
case .empty:
31+
// TODO: 5954 - update empty state
32+
EmptyState(title: Localization.emptyStateTitle,
33+
description: Localization.emptyStateMessage,
34+
image: .emptyProductsTabImage)
35+
.frame(maxHeight: .infinity)
36+
case .syncingFirstPage:
37+
List {
38+
ForEach(viewModel.placeholderRowViewModels) { rowViewModel in
39+
InboxNoteRow(viewModel: rowViewModel)
40+
.redacted(reason: .placeholder)
41+
.shimmering()
42+
}
43+
}
44+
.listStyle(PlainListStyle())
45+
}
46+
}
47+
.background(Color(.listBackground).ignoresSafeArea())
48+
.navigationTitle(Localization.title)
49+
.onAppear {
50+
viewModel.onLoadTrigger.send()
51+
}
52+
}
53+
}
54+
55+
private extension Inbox {
56+
enum Localization {
57+
static let title = NSLocalizedString("Inbox", comment: "Title for the screen that shows inbox notes.")
58+
static let emptyStateTitle = NSLocalizedString("Congrats, you’ve read everything!",
59+
comment: "Title displayed if there are no inbox notes in the inbox screen.")
60+
static let emptyStateMessage = NSLocalizedString("Come back soon for more tips and insights on growing your store",
61+
comment: "Message displayed if there are no inbox notes to display in the inbox screen.")
62+
}
63+
}
64+
65+
#if DEBUG
66+
67+
/// Allows mocking for previewing `Inbox` view.
68+
private final class PreviewInboxNotesStoresManager: DefaultStoresManager {
69+
private let inboxNotes: [InboxNote]
70+
71+
init(inboxNotes: [InboxNote], sessionManager: SessionManager = SessionManager.standard) {
72+
self.inboxNotes = inboxNotes
73+
super.init(sessionManager: sessionManager)
74+
}
75+
76+
// MARK: - Overridden Methods
77+
78+
override func dispatch(_ action: Action) {
79+
if let action = action as? InboxNotesAction {
80+
onInboxNotesAction(action: action)
81+
} else {
82+
super.dispatch(action)
83+
}
84+
}
85+
86+
private func onInboxNotesAction(action: InboxNotesAction) {
87+
switch action {
88+
case .loadAllInboxNotes(_, _, _, _, _, _, let completion):
89+
completion(.success(inboxNotes))
90+
return
91+
default:
92+
return
93+
}
94+
}
95+
}
96+
97+
extension InboxNote {
98+
static func placeholder() -> InboxNote {
99+
.init(siteID: 255,
100+
id: 0,
101+
name: "",
102+
type: "",
103+
status: "",
104+
actions: [.init(id: 0, name: "", label: "Accept Apple Pay", status: "", url: "https://wordpress.com")],
105+
title: "Boost sales this holiday season with Apple Pay!",
106+
content: "",
107+
isRemoved: false,
108+
isRead: false,
109+
dateCreated: .init())
110+
}
111+
}
112+
113+
struct Inbox_Previews: PreviewProvider {
114+
static var previews: some View {
115+
Group {
116+
// Placeholder state.
117+
Inbox(viewModel: .init(siteID: 122))
118+
.preferredColorScheme(.light)
119+
Inbox(viewModel: .init(siteID: 122))
120+
.preferredColorScheme(.dark)
121+
// Empty state.
122+
Inbox(viewModel: .init(siteID: 322,
123+
stores: PreviewInboxNotesStoresManager(inboxNotes: [])))
124+
.preferredColorScheme(.light)
125+
Inbox(viewModel: .init(siteID: 322,
126+
stores: PreviewInboxNotesStoresManager(inboxNotes: [])))
127+
.preferredColorScheme(.dark)
128+
// Results state.
129+
Inbox(viewModel: .init(siteID: 322,
130+
stores: PreviewInboxNotesStoresManager(inboxNotes: [.placeholder(), .placeholder()])))
131+
.preferredColorScheme(.light)
132+
Inbox(viewModel: .init(siteID: 322,
133+
stores: PreviewInboxNotesStoresManager(inboxNotes: [.placeholder(), .placeholder()])))
134+
.preferredColorScheme(.dark)
135+
}
136+
}
137+
}
138+
139+
#endif
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import SwiftUI
2+
import Yosemite
3+
4+
/// Shows information about an inbox note with actions and a CTA to dismiss the note.
5+
struct InboxNoteRow: View {
6+
let viewModel: InboxNoteRowViewModel
7+
8+
var body: some View {
9+
VStack(spacing: 0) {
10+
VStack(alignment: .leading,
11+
spacing: Constants.verticalSpacing) {
12+
// HStack with type icon and relative date.
13+
// TODO: 5954 - type icon and relative date
14+
15+
// Title.
16+
Text(viewModel.title)
17+
.bodyStyle()
18+
.fixedSize(horizontal: false, vertical: true)
19+
20+
// Content.
21+
// TODO: 5954 - HTML content
22+
23+
// HStack with actions and dismiss action.
24+
HStack(spacing: Constants.spacingBetweenActions) {
25+
ForEach(viewModel.actions) { action in
26+
if let url = action.url {
27+
Button(action.title) {
28+
// TODO: 5955 - handle action
29+
print("Handling action with URL: \(url)")
30+
}
31+
.foregroundColor(Color(.accent))
32+
.font(.body)
33+
.buttonStyle(PlainButtonStyle())
34+
} else {
35+
Text(action.title)
36+
}
37+
}
38+
Button(Localization.dismiss) {
39+
// TODO: 5955 - handle dismiss action
40+
print("Handling dismiss action")
41+
}
42+
.foregroundColor(Color(.withColorStudio(.gray, shade: .shade30)))
43+
.font(.body)
44+
.buttonStyle(PlainButtonStyle())
45+
46+
Spacer()
47+
}
48+
}
49+
.padding(Constants.defaultPadding)
50+
51+
if #available(iOS 15.0, *) {
52+
// In order to show full-width separator, the default list separator is hidden and a `Divider` is shown inside the row.
53+
Divider()
54+
.frame(height: Constants.dividerHeight)
55+
}
56+
}
57+
.listRowInsets(.zero)
58+
}
59+
}
60+
61+
private extension InboxNoteRow {
62+
enum Localization {
63+
static let dismiss = NSLocalizedString("Dismiss", comment: "Dismiss button in inbox note row.")
64+
}
65+
66+
enum Constants {
67+
static let spacingBetweenActions: CGFloat = 16
68+
static let verticalSpacing: CGFloat = 14
69+
static let defaultPadding: CGFloat = 16
70+
static let dividerHeight: CGFloat = 1
71+
}
72+
}
73+
74+
struct InboxNoteRow_Previews: PreviewProvider {
75+
static var previews: some View {
76+
let note = InboxNote(siteID: 2,
77+
id: 6,
78+
name: "",
79+
type: "",
80+
status: "",
81+
actions: [.init(id: 2, name: "", label: "Let your customers know about Apple Pay", status: "", url: "https://wordpress.org"),
82+
.init(id: 6, name: "", label: "No URL", status: "", url: "")],
83+
title: "Boost sales this holiday season with Apple Pay!",
84+
content: """
85+
Increase your conversion rate by letting your customers know that you accept Apple Pay.
86+
It’s seamless to <a href=\"https://docs.woocommerce.com/document/payments/apple-pay/\">
87+
enable Apple Pay with WooCommerce Payments</a> and easy to communicate it with
88+
this <a href=\"https://developer.apple.com/apple-pay/marketing/\">marketing guide</a>.
89+
""",
90+
isRemoved: false,
91+
isRead: false,
92+
dateCreated: .init())
93+
let viewModel = InboxNoteRowViewModel(note: note)
94+
Group {
95+
InboxNoteRow(viewModel: viewModel)
96+
.preferredColorScheme(.dark)
97+
InboxNoteRow(viewModel: viewModel)
98+
.preferredColorScheme(.light)
99+
InboxNoteRow(viewModel: viewModel)
100+
.preferredColorScheme(.light)
101+
.environment(\.sizeCategory, .extraExtraExtraLarge)
102+
}
103+
}
104+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Yosemite
2+
3+
/// View model for `InboxNoteRow`.
4+
struct InboxNoteRowViewModel: Identifiable {
5+
let id: Int64
6+
let title: String
7+
8+
let actions: [InboxNoteRowActionViewModel]
9+
10+
init(note: InboxNote) {
11+
let actions = note.actions.map { InboxNoteRowActionViewModel(action: $0) }
12+
self.init(id: note.id,
13+
title: note.title,
14+
actions: actions)
15+
}
16+
17+
init(id: Int64, title: String, actions: [InboxNoteRowActionViewModel]) {
18+
self.id = id
19+
self.title = title
20+
self.actions = actions
21+
}
22+
}
23+
24+
/// View model for an action in `InboxNoteRow`.
25+
struct InboxNoteRowActionViewModel: Identifiable {
26+
let id: Int64
27+
let title: String
28+
let url: URL?
29+
30+
init(action: InboxAction) {
31+
let url = URL(string: action.url)
32+
self.init(id: action.id, title: action.label, url: url)
33+
}
34+
35+
init(id: Int64, title: String, url: URL?) {
36+
self.id = id
37+
self.title = title
38+
self.url = url
39+
}
40+
}

0 commit comments

Comments
 (0)