Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Modules/Sources/Yosemite/Actions/CustomerAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,7 @@ public enum CustomerAction: Action {
///- `siteID`: The site for which customers should be delete.
///- `onCompletion`: Invoked when the operation finishes.
case deleteAllCustomers(siteID: Int64, onCompletion: () -> Void)

/// Loads a customer for the specified `siteID` and `customerID` from storage.
case loadCustomer(siteID: Int64, customerID: Int64, onCompletion: (Result<Customer, Error>) -> Void)
}
18 changes: 18 additions & 0 deletions Modules/Sources/Yosemite/Stores/CustomerStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ public final class CustomerStore: Store {
synchronizeAllCustomers(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, onCompletion: onCompletion)
case .deleteAllCustomers(siteID: let siteID, onCompletion: let onCompletion):
deleteAllCustomers(from: siteID, onCompletion: onCompletion)
case let .loadCustomer(siteID, customerID, onCompletion):
loadCustomer(siteID: siteID, customerID: customerID, onCompletion: onCompletion)
}
}

Expand Down Expand Up @@ -251,6 +253,16 @@ public final class CustomerStore: Store {
}, completion: onCompletion, on: .main)
}

private func loadCustomer(siteID: Int64, customerID: Int64, onCompletion: @escaping (Result<Networking.Customer, Error>) -> Void) {
let customers = storageManager.viewStorage.loadCustomers(siteID: siteID, matching: [customerID])
if let storageCustomer = customers.first {
let customer = storageCustomer.toReadOnly()
onCompletion(.success(customer))
} else {
onCompletion(.failure(CustomerStoreError.notFound))
}
}

/// Maps CustomerSearchResult to Customer objects
///
/// - Parameters:
Expand Down Expand Up @@ -429,3 +441,9 @@ private extension CustomerStore {
storageCustomer.update(with: readOnlyCustomer)
}
}

// MARK: - Errors

enum CustomerStoreError: Error {
case notFound
}
1 change: 1 addition & 0 deletions WooCommerce/Classes/View Modifiers/View+Tappable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ private struct TappableViewModifier: ViewModifier {
onTap()
} label: {
content
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import Foundation
import struct Networking.Booking
import Yosemite
import SwiftUI // Added for withAnimation

final class BookingDetailsViewModel: ObservableObject {
let sections: [Section]
let navigationTitle: String
private let stores: StoresManager

private let booking: Booking
private let customerContent = CustomerContent()

init(booking: Booking) {
self.booking = booking
let navigationTitle: String
@Published private(set) var sections: [Section] = []

init(booking: Booking, stores: StoresManager = ServiceLocator.stores) {
self.booking = booking
self.stores = stores
navigationTitle = Self.navigationTitle(for: booking)
setupSections()
}

let headerSection = Section.init(
private func setupSections() {
let headerSection = Section(
content: .header(HeaderContent(booking))
)

Expand All @@ -27,21 +34,6 @@ final class BookingDetailsViewModel: ObservableObject {
content: .attendance(AttendanceContent())
)

let customerSection = Section(
header: .title(Localization.customerSectionHeaderTitle.uppercased()),
content: .customer(
/// Temporary hardcode
CustomerContent(
nameText: "Margarita Nikolaevna",
emailText: "[email protected]",
phoneText: "+1 742582943798",
billingAddressText: """
238 Willow Creek Drive
Montgomery
AL 36109
"""
)
)
)

let paymentSection = Section(
header: .title(Localization.paymentSectionHeaderTitle.uppercased()),
content: .payment(PaymentContent(booking: booking))
Expand All @@ -55,14 +47,100 @@ final class BookingDetailsViewModel: ObservableObject {
sections = [
headerSection,
appointmentDetailsSection,
customerSection,
attendanceSection,
paymentSection,
bookingNotes
]
}
}

// MARK: Local Data

extension BookingDetailsViewModel {
func loadLocalData() {
loadCustomerData()
}
}

private extension BookingDetailsViewModel {
func loadCustomerData() {
guard booking.customerID > 0 else {
return
}

let action = CustomerAction.loadCustomer(siteID: booking.siteID, customerID: booking.customerID) { [weak self] result in
guard let self = self else { return }
if case .success(let customer) = result {
self.updateCustomerSection(with: customer)
}
}
stores.dispatch(action)
}
}

// MARK: Syncing

extension BookingDetailsViewModel {
func syncData() async {
await syncCustomer()
}
}

private extension BookingDetailsViewModel {
func syncCustomer() async {
guard shouldSyncCustomer else {
return
}

do {
let fetchedCustomer = try await retrieveCustomer()
updateCustomerSection(with: fetchedCustomer)
} catch {
DDLogError("⛔️ Error synchronizing Customer for Booking: \(error)")
}
}

@MainActor
func retrieveCustomer() async throws -> Customer {
try await withCheckedThrowingContinuation { continuation in
let action = CustomerAction.retrieveCustomer(
siteID: booking.siteID,
customerID: booking.customerID
) { result in
switch result {
case .success(let customer):
continuation.resume(returning: customer)
case .failure(let error):
continuation.resume(throwing: error)
}
}
stores.dispatch(action)
}
}

func updateCustomerSection(with customer: Customer) {
customerContent.update(with: customer)

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

let customerSection = Section(
header: .title(Localization.customerSectionHeaderTitle.uppercased()),
content: .customer(customerContent)
)
withAnimation {
sections.insert(customerSection, at: 2)
}
Comment on lines +133 to +135
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While running the app with Xcode, I saw a warning for this update: "Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates." Please consider marking updateCustomerSection with @MainActor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in the other PR in 2fee1b9726fe4b46d0732e83134d29de90e860eb

}

/// Returns true when the `customerID` is non-zero and customer section doesn't exist
var shouldSyncCustomer: Bool {
return booking.customerID > 0
}
}

extension BookingDetailsViewModel {
var cancellationAlertMessage: String {
// Temporary hardcoded
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
import Foundation
import Networking

extension BookingDetailsViewModel {
struct CustomerContent {
let nameText: String
let emailText: String
let phoneText: String
let billingAddressText: String?
final class CustomerContent: ObservableObject {
@Published var nameText: String?
@Published var emailText: String?
@Published var phoneText: String?
@Published var billingAddressText: String?

func update(with customer: Customer) {
let name = [
customer.firstName,
customer.lastName
]
.compactMap { $0 }
.filter { !$0.isEmpty }
.joined(separator: " ")

let billingAddress = customer.billing.flatMap(formatAddress)

nameText = name
emailText = customer.email
phoneText = customer.billing?.phone ?? ""
billingAddressText = billingAddress
}

private func formatAddress(_ address: Address) -> String {
[
address.address1,
address.address2,
address.city,
address.state,
address.postcode,
address.country
]
.compactMap { $0 }
.filter { !$0.isEmpty }
.joined(separator: "\n")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import SwiftUI

private extension BookingDetailsView {
struct RowTextStyle: ViewModifier {
func body(content: Content) -> some View {
content
.font(TextFont.bodyMedium)
.foregroundStyle(.primary)
.multilineTextAlignment(.leading)
}
}
}

extension View {
func rowTextStyle() -> some View {
self.modifier(BookingDetailsView.RowTextStyle())
}
}
Loading