From 6374efc4819542bd0c11ea5ecf2b93fc18d2ff72 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Tue, 4 Feb 2025 14:06:43 +0000 Subject: [PATCH 1/2] 14869 Basic implementation of Observable for POS This adds a thin Observable layer on top of the underlying combine code --- .../Models/PointOfSaleAggregateModel.swift | 96 ++++++++++++------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index a1c0ef86002..929d64e85f9 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -1,5 +1,6 @@ import Foundation import Combine +import Observation import protocol Yosemite.POSOrderableItem import protocol WooFoundation.Analytics @@ -38,24 +39,24 @@ protocol PointOfSaleAggregateModelProtocol { } @available(iOS 17.0, *) -class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProtocol { - @Published private(set) var orderStage: PointOfSaleOrderStage = .building - - @Published private(set) var cardReaderConnectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected - @Published private(set) var paymentState: PointOfSalePaymentState - @Published var cardPresentPaymentAlertViewModel: PointOfSaleCardPresentPaymentAlertType? - @Published private(set) var cardPresentPaymentInlineMessage: PointOfSaleCardPresentPaymentMessageType? - @Published var cardPresentPaymentOnboardingViewModel: CardPresentPaymentsOnboardingViewModel? +@Observable final class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProtocol { + private(set) var orderStage: PointOfSaleOrderStage = .building + + private(set) var cardReaderConnectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected + private(set) var paymentState: PointOfSalePaymentState + var cardPresentPaymentAlertViewModel: PointOfSaleCardPresentPaymentAlertType? + private(set) var cardPresentPaymentInlineMessage: PointOfSaleCardPresentPaymentMessageType? + var cardPresentPaymentOnboardingViewModel: CardPresentPaymentsOnboardingViewModel? private var onOnboardingCancellation: (() -> Void)? - @Published private(set) var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading, - itemsStack: ItemsStackState(root: .loading([]), - itemStates: [:])) + private(set) var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading, + itemsStack: ItemsStackState(root: .loading([]), + itemStates: [:])) - @Published private(set) var cart: [CartItem] = [] + private(set) var cart: [CartItem] = [] - @Published private(set) var orderState: PointOfSaleOrderState = .idle - @Published private var internalOrderState: PointOfSaleInternalOrderState = .idle + private(set) var orderState: PointOfSaleOrderState = .idle + private var internalOrderState: PointOfSaleInternalOrderState = .idle private let itemsController: PointOfSaleItemsControllerProtocol @@ -90,7 +91,10 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt @available(iOS 17.0, *) extension PointOfSaleAggregateModel { private func publishItemsViewState() { - itemsController.itemsViewStatePublisher.assign(to: &$itemsViewState) + itemsController.itemsViewStatePublisher.sink { [weak self] state in + self?.itemsViewState = state + } + .store(in: &cancellables) } @MainActor @@ -155,8 +159,11 @@ extension PointOfSaleAggregateModel { @available(iOS 17.0, *) extension PointOfSaleAggregateModel { private func publishCardReaderConnectionStatus() { - // When adopting Observable, we can use `assign(to: on:)` here instead - cardPresentPaymentService.readerConnectionStatusPublisher.assign(to: &$cardReaderConnectionStatus) + cardPresentPaymentService.readerConnectionStatusPublisher + .sink(receiveValue: { [weak self] connectionStatus in + self?.cardReaderConnectionStatus = connectionStatus + }) + .store(in: &cancellables) } func connectCardReader() { @@ -177,7 +184,7 @@ extension PointOfSaleAggregateModel { /// e.g. when the TotalsView goes offscreen. private func startPaymentWhenCardReaderConnected() async { guard case .connected = cardReaderConnectionStatus else { - return startPaymentOnCardReaderConnection = $cardReaderConnectionStatus + return startPaymentOnCardReaderConnection = cardPresentPaymentService.readerConnectionStatusPublisher .filter { status in switch status { case .connected: @@ -259,17 +266,19 @@ extension PointOfSaleAggregateModel { await collectCardPayment() } - private func setupReaderReconnectionObservation() { - $orderStage.sink(receiveValue: { [weak self] stage in + @Sendable private func setupReaderReconnectionObservation() { + withObservationTracking { [weak self] in guard let self else { return } - switch stage { - case .building: - cancelCardReaderPreparation() - case .finalizing: - observeReaderReconnection() + switch orderStage { + case .building: + cancelCardReaderPreparation() + case .finalizing: + observeReaderReconnection() } - }) - .store(in: &cancellables) + } onChange: { [weak self] in + guard let self else { return } + DispatchQueue.main.async(execute: setupReaderReconnectionObservation) + } } private func cancelCardReaderPreparation() { @@ -279,7 +288,7 @@ extension PointOfSaleAggregateModel { } private func observeReaderReconnection() { - cardReaderDisconnection = $cardReaderConnectionStatus + cardReaderDisconnection = cardPresentPaymentService.readerConnectionStatusPublisher .filter({ $0 == .disconnected }) .sink { [weak self] _ in Task { @MainActor [weak self] in @@ -321,13 +330,19 @@ private extension PointOfSaleAggregateModel { } return alertType } - .assign(to: &$cardPresentPaymentAlertViewModel) + .sink(receiveValue: { [weak self] alertType in + self?.cardPresentPaymentAlertViewModel = alertType + }) + .store(in: &cancellables) cardPresentPaymentService.paymentEventPublisher .map { [weak self] event -> PointOfSaleCardPresentPaymentMessageType? in self?.mapCardPresentPaymentEventToMessageType(event) } - .assign(to: &$cardPresentPaymentInlineMessage) + .sink(receiveValue: { [weak self] message in + self?.cardPresentPaymentInlineMessage = message + }) + .store(in: &cancellables) cardPresentPaymentService.paymentEventPublisher .compactMap { [weak self] paymentEvent -> PointOfSalePaymentState? in @@ -338,7 +353,10 @@ private extension PointOfSaleAggregateModel { return newPaymentState } - .assign(to: &$paymentState) + .sink(receiveValue: { [weak self] paymentState in + self?.paymentState = paymentState + }) + .store(in: &cancellables) cardPresentPaymentService.paymentEventPublisher .map { [weak self] event -> CardPresentPaymentsOnboardingViewModel? in @@ -349,7 +367,10 @@ private extension PointOfSaleAggregateModel { onOnboardingCancellation = onCancel return viewModel } - .assign(to: &$cardPresentPaymentOnboardingViewModel) + .sink(receiveValue: { [weak self] onboardingViewModel in + self?.cardPresentPaymentOnboardingViewModel = onboardingViewModel + }) + .store(in: &cancellables) } /// Maps PaymentEvent to POSMessageType and annonates additional information if necessary @@ -413,9 +434,16 @@ extension PointOfSaleAggregateModel { func publishOrderState() { orderController.orderStatePublisher .map { $0.externalState } - .assign(to: &$orderState) + .sink(receiveValue: { [weak self] orderState in + self?.orderState = orderState + }) + .store(in: &cancellables) - orderController.orderStatePublisher.assign(to: &$internalOrderState) + orderController.orderStatePublisher + .sink(receiveValue: { [weak self] internalOrderState in + self?.internalOrderState = internalOrderState + }) + .store(in: &cancellables) } } From 927300eadff6b71aec49d8535e178264f0a098c2 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Tue, 4 Feb 2025 15:41:47 +0000 Subject: [PATCH 2/2] 14869 Remove posModel ObservableObject conformance Now that we use `Observation` on the POS aggregate model, we should inject/fetch it with @Environment and use @State to hold it. --- .../Classes/POS/Models/PointOfSaleAggregateModel.swift | 2 +- .../CardReaderConnectionStatusView.swift | 2 +- WooCommerce/Classes/POS/Presentation/CartView.swift | 4 ++-- .../POS/Presentation/Item Selector/ChildItemList.swift | 6 +++--- .../Classes/POS/Presentation/Item Selector/ItemList.swift | 6 +++--- WooCommerce/Classes/POS/Presentation/ItemListView.swift | 6 +++--- .../Classes/POS/Presentation/POSFloatingControlView.swift | 2 +- WooCommerce/Classes/POS/Presentation/PaymentButtons.swift | 4 ++-- .../POS/Presentation/PointOfSaleCollectCashView.swift | 4 ++-- .../Classes/POS/Presentation/PointOfSaleDashboardView.swift | 3 ++- .../POS/Presentation/PointOfSaleEntryPointView.swift | 6 +++--- .../Presentation/Reusable Views/POSSendReceiptView.swift | 4 ++-- WooCommerce/Classes/POS/Presentation/TotalsView.swift | 4 ++-- 13 files changed, 27 insertions(+), 26 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 929d64e85f9..6ff60480d0a 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -39,7 +39,7 @@ protocol PointOfSaleAggregateModelProtocol { } @available(iOS 17.0, *) -@Observable final class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProtocol { +@Observable final class PointOfSaleAggregateModel: PointOfSaleAggregateModelProtocol { private(set) var orderStage: PointOfSaleOrderStage = .building private(set) var cardReaderConnectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift index c334d34b663..690d7d0d0b9 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift @@ -3,7 +3,7 @@ import SwiftUI @available(iOS 17.0, *) struct CardReaderConnectionStatusView: View { @Environment(\.posBackgroundAppearance) var backgroundAppearance - @EnvironmentObject var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel @ScaledMetric private var scale: CGFloat = 1.0 @Environment(\.isEnabled) var isEnabled diff --git a/WooCommerce/Classes/POS/Presentation/CartView.swift b/WooCommerce/Classes/POS/Presentation/CartView.swift index e44fea22b6d..65767b347b1 100644 --- a/WooCommerce/Classes/POS/Presentation/CartView.swift +++ b/WooCommerce/Classes/POS/Presentation/CartView.swift @@ -2,7 +2,7 @@ import SwiftUI @available(iOS 17.0, *) struct CartView: View { - @EnvironmentObject private var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel private let viewHelper = CartViewHelper() @Environment(\.floatingControlAreaSize) var floatingControlAreaSize: CGSize @@ -266,6 +266,6 @@ private extension CartView { cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) return CartView() - .environmentObject(posModel) + .environment(posModel) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift b/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift index cc9fab07777..d3d202c8522 100644 --- a/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift +++ b/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift @@ -6,7 +6,7 @@ import Yosemite struct ChildItemList: View { private let parentItem: POSItem private let title: String - @EnvironmentObject private var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel @Environment(\.dismiss) private var dismiss private var state: ItemListState { @@ -167,7 +167,7 @@ private extension ChildItemList { cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) return ChildItemList(parentItem: parentItem, title: parentProduct.name) - .environmentObject(posModel) + .environment(posModel) } @available(iOS 17.0, *) @@ -191,7 +191,7 @@ private extension ChildItemList { cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) return ChildItemList(parentItem: parentItem, title: parentProduct.name) - .environmentObject(posModel) + .environment(posModel) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift b/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift index a514d3bbc3f..14ed4c37750 100644 --- a/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift +++ b/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift @@ -7,7 +7,7 @@ import struct Yosemite.POSVariableParentProduct @available(iOS 17.0, *) struct ItemList: View { @Environment(\.floatingControlAreaSize) private var floatingControlAreaSize: CGSize - @EnvironmentObject var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel @StateObject private var infiniteScrollTriggerDeterminer = ThresholdInfiniteScrollTriggerDeterminer() let state: ItemListState @@ -80,7 +80,7 @@ private enum Constants { private struct ItemListRow: View { let item: POSItem let analytics: Analytics = ServiceLocator.analytics - @EnvironmentObject var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel var body: some View { switch item { @@ -156,7 +156,7 @@ private extension ItemListRow { cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) ItemList(state: .loading([])) - .environmentObject(posModel) + .environment(posModel) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index 2bb77b31a9b..5bd906267f4 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -6,7 +6,7 @@ import protocol Yosemite.POSOrderableItem struct ItemListView: View { @Environment(\.dynamicTypeSize) private var dynamicTypeSize - @EnvironmentObject var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel @State private var showSimpleProductsModal: Bool = false private var itemListState: ItemListState { @@ -323,7 +323,7 @@ private extension ItemListView { cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) return ItemListView() - .environmentObject(posModel) + .environment(posModel) } @available(iOS 17.0, *) @@ -333,7 +333,7 @@ private extension ItemListView { cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) return ItemListView() - .environmentObject(posModel) + .environment(posModel) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift index 6ce6ab5d812..8da89df29f8 100644 --- a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift +++ b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift @@ -3,7 +3,7 @@ import SwiftUI @available(iOS 17.0, *) struct POSFloatingControlView: View { @Environment(\.posBackgroundAppearance) var backgroundAppearance - @EnvironmentObject private var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel @Environment(\.colorScheme) var colorScheme @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Binding private var showExitPOSModal: Bool diff --git a/WooCommerce/Classes/POS/Presentation/PaymentButtons.swift b/WooCommerce/Classes/POS/Presentation/PaymentButtons.swift index e9b6f8ed13c..25f7a1afed4 100644 --- a/WooCommerce/Classes/POS/Presentation/PaymentButtons.swift +++ b/WooCommerce/Classes/POS/Presentation/PaymentButtons.swift @@ -2,7 +2,7 @@ import SwiftUI @available(iOS 17.0, *) struct PaymentsActionButtons: View { - @EnvironmentObject private var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel @Binding var isShowingSendReceiptView: Bool @Binding private(set) var isShowingReceiptNotEligibleBanner: Bool @@ -112,6 +112,6 @@ private extension PaymentsActionButtons { cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) PaymentsActionButtons(isShowingSendReceiptView: .constant(false), isShowingReceiptNotEligibleBanner: .constant(true)) - .environmentObject(posModel) + .environment(posModel) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index bc982135686..523602da268 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -4,7 +4,7 @@ import SwiftUI struct PointOfSaleCollectCashView: View { @Environment(\.colorScheme) var colorScheme @Environment(\.dynamicTypeSize) var dynamicTypeSize - @EnvironmentObject private var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel @FocusState private var isTextFieldFocused: Bool private let viewHelper = CollectCashViewHelper() @@ -230,6 +230,6 @@ private extension PointOfSaleCollectCashView { cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) PointOfSaleCollectCashView(orderTotal: "$1.23") - .environmentObject(posModel) + .environment(posModel) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index 3e7cfe6e81e..49e34a1bedd 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -2,7 +2,7 @@ import SwiftUI @available(iOS 17.0, *) struct PointOfSaleDashboardView: View { - @EnvironmentObject private var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel @Environment(\.horizontalSizeClass) private var horizontalSizeClass @State private var showExitPOSModal: Bool = false @@ -11,6 +11,7 @@ struct PointOfSaleDashboardView: View { @State private var floatingSize: CGSize = .zero var body: some View { + @Bindable var posModel = posModel ZStack(alignment: .bottomLeading) { if case .regular = horizontalSizeClass { switch posModel.itemsViewState.containerState { diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index 3ac608864ed..5bbf31bc07b 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -2,7 +2,7 @@ import SwiftUI @available(iOS 17.0, *) struct PointOfSaleEntryPointView: View { - @StateObject private var posModel: PointOfSaleAggregateModel + @State private var posModel: PointOfSaleAggregateModel @StateObject private var posModalManager = POSModalManager() @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -19,13 +19,13 @@ struct PointOfSaleEntryPointView: View { cardPresentPaymentService: cardPresentPaymentService, orderController: orderController) - self._posModel = StateObject(wrappedValue: posModel) + self._posModel = State(wrappedValue: posModel) } var body: some View { PointOfSaleDashboardView() .environmentObject(posModalManager) - .environmentObject(posModel) + .environment(posModel) .onAppear { onPointOfSaleModeActiveStateChange(true) } diff --git a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift index cd72d8b9237..fec1834c30a 100644 --- a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift +++ b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift @@ -3,7 +3,7 @@ import class WordPressShared.EmailFormatValidator @available(iOS 17.0, *) struct POSSendReceiptView: View { - @EnvironmentObject private var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel @Environment(\.dynamicTypeSize) var dynamicTypeSize @State private var textFieldInput: String = "" @State private var isLoading: Bool = false @@ -170,6 +170,6 @@ private extension POSSendReceiptView { cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) POSSendReceiptView(isShowingSendReceiptView: .constant(true)) - .environmentObject(posModel) + .environment(posModel) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index 23ff6d69fe8..c3e90f3659e 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -2,7 +2,7 @@ import SwiftUI @available(iOS 17.0, *) struct TotalsView: View { - @EnvironmentObject private var posModel: PointOfSaleAggregateModel + @Environment(PointOfSaleAggregateModel.self) private var posModel private let viewHelper = TotalsViewHelper() /// Used together with .matchedGeometryEffect to synchronize the animations of shimmeringLineView and text fields. @@ -432,6 +432,6 @@ private extension View { cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) TotalsView() - .environmentObject(posModel) + .environment(posModel) } #endif