Skip to content

Commit b7e0f4f

Browse files
[Bookings] Apply customer data to header (#16226)
2 parents 4e1797b + c5573ee commit b7e0f4f

File tree

7 files changed

+148
-47
lines changed

7 files changed

+148
-47
lines changed

WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ final class BookingDetailsViewModel: ObservableObject {
66
private let stores: StoresManager
77

88
private let booking: Booking
9+
private let headerContent: HeaderContent
910
private let customerContent = CustomerContent()
1011

1112
let navigationTitle: String
@@ -14,13 +15,14 @@ final class BookingDetailsViewModel: ObservableObject {
1415
init(booking: Booking, stores: StoresManager = ServiceLocator.stores) {
1516
self.booking = booking
1617
self.stores = stores
18+
self.headerContent = HeaderContent(booking)
1719
navigationTitle = Self.navigationTitle(for: booking)
1820
setupSections()
1921
}
2022

2123
private func setupSections() {
2224
let headerSection = Section(
23-
content: .header(HeaderContent(booking))
25+
content: .header(headerContent)
2426
)
2527

2628
let appointmentDetailsSection = Section(
@@ -71,7 +73,10 @@ private extension BookingDetailsViewModel {
7173
let action = CustomerAction.loadCustomer(siteID: booking.siteID, customerID: booking.customerID) { [weak self] result in
7274
guard let self = self else { return }
7375
if case .success(let customer) = result {
74-
self.updateCustomerSection(with: customer)
76+
Task {
77+
await self.updateCustomerSection(with: customer)
78+
await self.updateHeader(with: customer)
79+
}
7580
}
7681
}
7782
stores.dispatch(action)
@@ -94,7 +99,8 @@ private extension BookingDetailsViewModel {
9499

95100
do {
96101
let fetchedCustomer = try await retrieveCustomer()
97-
updateCustomerSection(with: fetchedCustomer)
102+
await updateCustomerSection(with: fetchedCustomer)
103+
await updateHeader(with: fetchedCustomer)
98104
} catch {
99105
DDLogError("⛔️ Error synchronizing Customer for Booking: \(error)")
100106
}
@@ -118,9 +124,18 @@ private extension BookingDetailsViewModel {
118124
}
119125
}
120126

127+
@MainActor
121128
func updateCustomerSection(with customer: Customer) {
122129
customerContent.update(with: customer)
130+
insertCustomerSectionIfAbsent()
131+
}
132+
133+
@MainActor
134+
func updateHeader(with customer: Customer) {
135+
headerContent.update(with: customer)
136+
}
123137

138+
func insertCustomerSectionIfAbsent() {
124139
// Avoid adding if it already exists
125140
guard !sections.contains(where: { if case .customer = $0.content { return true } else { return false } }) else {
126141
return

WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ extension BookingDetailsViewModel {
88
@Published var phoneText: String?
99
@Published var billingAddressText: String?
1010

11+
@MainActor
1112
func update(with customer: Customer) {
1213
let name = [
1314
customer.firstName,

WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,55 @@
11
import Foundation
2-
import struct Networking.Booking
2+
import struct Yosemite.Booking
3+
import struct Yosemite.Customer
34

45
extension BookingDetailsViewModel {
5-
struct HeaderContent: Hashable {
6+
final class HeaderContent: ObservableObject {
67
let bookingDate: String
7-
let serviceAndCustomerLine: String
88
let status: [Status]
99

10-
init(_ booking: Booking) {
10+
@Published var serviceAndCustomerLine: String
11+
12+
init(_ booking: Booking, customerName: String? = nil) {
1113
bookingDate = booking.startDate.formatted(
1214
date: .numeric,
1315
time: .shortened
1416
)
1517

16-
/// Temporary hardcode
17-
serviceAndCustomerLine = [
18-
"Women's Haircut",
19-
"Margarita Nikolaevna"
20-
].joined(separator: Constants.dotSeparator)
18+
/// Temporary hardcode for service name
19+
let serviceName = "Women's Haircut"
20+
if let customerName = customerName, !customerName.isEmpty {
21+
serviceAndCustomerLine = [
22+
serviceName,
23+
customerName
24+
].joined(separator: Constants.dotSeparator)
25+
} else {
26+
serviceAndCustomerLine = serviceName
27+
}
2128

2229
status = [.booked, .payAtLocation]
2330
}
31+
32+
@MainActor
33+
func update(with customer: Customer) {
34+
let customerName = [
35+
customer.firstName,
36+
customer.lastName
37+
]
38+
.compactMap { $0 }
39+
.filter { !$0.isEmpty }
40+
.joined(separator: " ")
41+
42+
/// Temporary hardcode for service name
43+
let serviceName = "Women's Haircut"
44+
if !customerName.isEmpty {
45+
serviceAndCustomerLine = [
46+
serviceName,
47+
customerName
48+
].joined(separator: Constants.dotSeparator)
49+
} else {
50+
serviceAndCustomerLine = serviceName
51+
}
52+
}
2453
}
2554
}
2655

WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ private extension BookingDetailsView {
140140
func sectionContentView(_ content: BookingDetailsViewModel.SectionContent) -> some View {
141141
switch content {
142142
case .header(let content):
143-
headerView(with: content)
143+
HeaderView(content: content)
144144
case .appointmentDetails(let content):
145145
appointmentDetailsView(with: content)
146146
case .attendance(let content):
@@ -156,30 +156,6 @@ private extension BookingDetailsView {
156156
}
157157
}
158158

159-
func headerView(with headerContent: BookingDetailsViewModel.HeaderContent) -> some View {
160-
VStack(alignment: .leading, spacing: Layout.headerContentVerticalPadding) {
161-
Text(headerContent.bookingDate)
162-
.font(TextFont.bodyMedium)
163-
.foregroundColor(.primary)
164-
Text(headerContent.serviceAndCustomerLine)
165-
.font(.footnote.weight(.medium))
166-
.foregroundColor(.secondary)
167-
HStack {
168-
ForEach(headerContent.status, id: \.self) { status in
169-
Text(status.labelText)
170-
.font(.caption2)
171-
.padding(.vertical, 4.5)
172-
.padding(.horizontal, 8)
173-
.background(status.labelColor)
174-
.cornerRadius(4)
175-
}
176-
}
177-
.padding(.top, Layout.headerBadgesAdditionalTopPadding)
178-
}
179-
.frame(maxWidth: .infinity, alignment: .leading)
180-
.padding(.vertical)
181-
}
182-
183159
func attendanceView(with content: BookingDetailsViewModel.AttendanceContent) -> some View {
184160
TitleAndValueRow(
185161
title: Localization.statusRowTitle,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import SwiftUI
2+
3+
extension BookingDetailsView {
4+
struct HeaderView: View {
5+
@ObservedObject var content: BookingDetailsViewModel.HeaderContent
6+
7+
var body: some View {
8+
VStack(alignment: .leading, spacing: Layout.headerContentVerticalPadding) {
9+
Text(content.bookingDate)
10+
.font(TextFont.bodyMedium)
11+
.foregroundColor(.primary)
12+
Text(content.serviceAndCustomerLine)
13+
.font(.footnote.weight(.medium))
14+
.foregroundColor(.secondary)
15+
HStack {
16+
ForEach(content.status, id: \.self) { status in
17+
Text(status.labelText)
18+
.font(.caption2)
19+
.padding(.vertical, 4.5)
20+
.padding(.horizontal, 8)
21+
.background(status.labelColor)
22+
.cornerRadius(4)
23+
}
24+
}
25+
.padding(.top, Layout.headerBadgesAdditionalTopPadding)
26+
}
27+
.frame(maxWidth: .infinity, alignment: .leading)
28+
.padding(.vertical)
29+
}
30+
}
31+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -986,8 +986,9 @@
986986
26FFC50D2BED7C5B0067B3A4 /* WatchDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B249702BEC801400730730 /* WatchDependencies.swift */; };
987987
2D052FB42E9408AF004111FD /* CustomerDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D052FB32E9408AF004111FD /* CustomerDetailsView.swift */; };
988988
2D052FB52E9408AF004111FD /* BookingDetailsView+RowTextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D052FB22E9408AF004111FD /* BookingDetailsView+RowTextStyle.swift */; };
989-
2D054A2A2E953E3C004111FD /* BookingDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D054A292E953E3C004111FD /* BookingDetailsViewModelTests.swift */; };
990989
2D05337E2E951A62004111FD /* BookingDetailsViewModel+PriceFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05337D2E951A62004111FD /* BookingDetailsViewModel+PriceFormatting.swift */; };
990+
2D054A2A2E953E3C004111FD /* BookingDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D054A292E953E3C004111FD /* BookingDetailsViewModelTests.swift */; };
991+
2D0555812E9693E6004111FD /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0555802E9693E1004111FD /* HeaderView.swift */; };
991992
2D05D19F2E82D1A8004111FD /* BookingDetailsViewModel+Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D19E2E82D1A3004111FD /* BookingDetailsViewModel+Section.swift */; };
992993
2D05D1A22E82D235004111FD /* HeaderContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A12E82D233004111FD /* HeaderContent.swift */; };
993994
2D05D1A42E82D266004111FD /* AppointmentDetailsContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A32E82D25F004111FD /* AppointmentDetailsContent.swift */; };
@@ -3884,8 +3885,9 @@
38843885
26FFD32928C6A0F4002E5E5E /* UIImage+Widgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Widgets.swift"; sourceTree = "<group>"; };
38853886
2D052FB22E9408AF004111FD /* BookingDetailsView+RowTextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookingDetailsView+RowTextStyle.swift"; sourceTree = "<group>"; };
38863887
2D052FB32E9408AF004111FD /* CustomerDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerDetailsView.swift; sourceTree = "<group>"; };
3887-
2D054A292E953E3C004111FD /* BookingDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookingDetailsViewModelTests.swift; sourceTree = "<group>"; };
38883888
2D05337D2E951A62004111FD /* BookingDetailsViewModel+PriceFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookingDetailsViewModel+PriceFormatting.swift"; sourceTree = "<group>"; };
3889+
2D054A292E953E3C004111FD /* BookingDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookingDetailsViewModelTests.swift; sourceTree = "<group>"; };
3890+
2D0555802E9693E1004111FD /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
38893891
2D05D19E2E82D1A3004111FD /* BookingDetailsViewModel+Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookingDetailsViewModel+Section.swift"; sourceTree = "<group>"; };
38903892
2D05D1A02E82D1EF004111FD /* BookingDetailsViewModel+SectionContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookingDetailsViewModel+SectionContent.swift"; sourceTree = "<group>"; };
38913893
2D05D1A12E82D233004111FD /* HeaderContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderContent.swift; sourceTree = "<group>"; };
@@ -7970,6 +7972,7 @@
79707972
2DAC2C962E82A169008521AF /* Booking Details */ = {
79717973
isa = PBXGroup;
79727974
children = (
7975+
2D0555802E9693E1004111FD /* HeaderView.swift */,
79737976
2D052FB22E9408AF004111FD /* BookingDetailsView+RowTextStyle.swift */,
79747977
2D052FB32E9408AF004111FD /* CustomerDetailsView.swift */,
79757978
2DAC2C972E82A185008521AF /* BookingDetailsView.swift */,
@@ -15221,6 +15224,7 @@
1522115224
032E481D2982996E00469D92 /* CardPresentModalTapToPayConnectingFailedNonRetryable.swift in Sources */,
1522215225
DED9740D2AD7D27000122EB4 /* BlazeCampaignListViewModel.swift in Sources */,
1522315226
EEC0120F2CE34B87003B865B /* WooAnalyticsEvent+WPCOMSuspendedSite.swift in Sources */,
15227+
2D0555812E9693E6004111FD /* HeaderView.swift in Sources */,
1522415228
0379C51B27BFE23F00A7E284 /* RefundConfirmationCardDetailsCell.swift in Sources */,
1522515229
D85136B9231CED5800DD0539 /* ReviewAge.swift in Sources */,
1522615230
209AD3D02AC1EDDA00825D76 /* WooPaymentsPayoutsCurrencyOverviewViewModel.swift in Sources */,

WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ final class BookingDetailsViewModelTests: XCTestCase {
2424
storageManager = nil
2525
}
2626

27-
func testLoadCustomerDataPopulatesCustomerContent() {
27+
func test_load_local_data_when_customer_exists_in_storage_populates_customer_content() {
2828
// Given
29+
let expectation = self.expectation(description: "The view model's customer content should be populated.")
2930
let customerID: Int64 = 123
3031
let mockBooking = Networking.Booking.fake().copy(customerID: customerID)
3132

@@ -45,6 +46,7 @@ final class BookingDetailsViewModelTests: XCTestCase {
4546
lastName: "Doe",
4647
billing: billingAddress
4748
)
49+
4850
let mockStorageCustomer = storageManager.insertSampleCustomer(readOnlyCustomer: mockReadOnlyCustomer)
4951
let viewModel = BookingDetailsViewModel(booking: mockBooking, stores: storesManager)
5052

@@ -53,19 +55,19 @@ final class BookingDetailsViewModelTests: XCTestCase {
5355
return
5456
}
5557
onCompletion(.success(mockStorageCustomer.toReadOnly()))
58+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
59+
expectation.fulfill()
60+
}
5661
}
5762

5863
// When
5964
viewModel.loadLocalData()
6065

61-
// Then
62-
let customerSection = viewModel.sections.first { section in
63-
if case .customer = section.content {
64-
return true
65-
}
66-
return false
67-
}
66+
// Wait for async updates to finish
67+
wait(for: [expectation], timeout: 1.0)
6868

69+
// Then
70+
let customerSection = viewModel.sections.first { if case .customer = $0.content { true } else { false } }
6971
guard let customerSection = customerSection,
7072
case let .customer(customerContent) = customerSection.content else {
7173
XCTFail("Customer section not found in view model sections")
@@ -90,4 +92,47 @@ final class BookingDetailsViewModelTests: XCTestCase {
9092

9193
XCTAssertEqual(customerContent.billingAddressText, expectedBillingAddress)
9294
}
95+
96+
func test_load_local_data_when_customer_exists_in_storage_populates_header_content() {
97+
// Given
98+
let expectation = self.expectation(description: "The view model's header content should be populated.")
99+
let customerID: Int64 = 123
100+
let mockBooking = Networking.Booking.fake().copy(customerID: customerID)
101+
102+
let mockReadOnlyCustomer = Networking.Customer.fake().copy(
103+
customerID: customerID,
104+
firstName: "John",
105+
lastName: "Doe"
106+
)
107+
108+
let mockStorageCustomer = storageManager.insertSampleCustomer(readOnlyCustomer: mockReadOnlyCustomer)
109+
let viewModel = BookingDetailsViewModel(booking: mockBooking, stores: storesManager)
110+
111+
storesManager.whenReceivingAction(ofType: CustomerAction.self) { action in
112+
guard case let .loadCustomer(_, _, onCompletion) = action else {
113+
return
114+
}
115+
onCompletion(.success(mockStorageCustomer.toReadOnly()))
116+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
117+
expectation.fulfill()
118+
}
119+
}
120+
121+
// When
122+
viewModel.loadLocalData()
123+
124+
// Wait for async updates to finish
125+
wait(for: [expectation], timeout: 1.0)
126+
127+
// Then
128+
let headerSection = viewModel.sections.first { if case .header = $0.content { true } else { false } }
129+
guard let headerSection = headerSection,
130+
case let .header(headerContent) = headerSection.content else {
131+
XCTFail("Header section not found in view model sections")
132+
return
133+
}
134+
135+
let expectedHeaderLine = "Women's Haircut • John Doe"
136+
XCTAssertEqual(headerContent.serviceAndCustomerLine, expectedHeaderLine)
137+
}
93138
}

0 commit comments

Comments
 (0)