From 64707e8c97ad351a2d17e31253b57325b3ca8b54 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 18 Dec 2024 21:25:16 +0700 Subject: [PATCH 01/14] Render cash button and navigate to cash view --- .../Models/PointOfSaleAggregateModel.swift | 18 ++++ .../POS/Models/PointOfSalePaymentState.swift | 6 +- .../PointOfSaleCollectCashView.swift | 102 ++++++++++++++++++ .../PointOfSaleDashboardView.swift | 11 +- .../Classes/POS/Presentation/TotalsView.swift | 19 +++- .../POS/ViewHelpers/CartViewHelper.swift | 6 +- .../POS/ViewHelpers/TotalsViewHelper.swift | 4 +- .../WooCommerce.xcodeproj/project.pbxproj | 4 + 8 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 84059497eff..af3d05e751f 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -61,6 +61,7 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt private var startPaymentOnCardReaderConnection: AnyCancellable? private var cardReaderDisconnection: AnyCancellable? + private var latestPaymentState: PointOfSalePaymentState? = nil private var cancellables: Set = [] @@ -263,6 +264,23 @@ extension PointOfSaleAggregateModel { } } +// MARK: - Card payments +// +extension PointOfSaleAggregateModel { + func collectCashPayment() { + // Capture the latest payment state before switching to cash collection + // so we can return if the collection is cancelled + latestPaymentState = paymentState + paymentState = .acceptingCash + } + + func cancelCashPayment() { + if let latestPaymentState = latestPaymentState { + paymentState = latestPaymentState + } + } +} + private extension PointOfSaleAggregateModel { func publishPaymentMessages() { cardPresentPaymentService.paymentEventPublisher diff --git a/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift b/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift index 97907cfa5c5..4d76c058bac 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift @@ -3,12 +3,14 @@ import Foundation enum PointOfSalePaymentState { case idle case acceptingCard + case acceptingCash case validatingOrder case validatingOrderError case preparingReader case processingPayment case paymentError case cardPaymentSuccessful + case cashPaymentSuccessful } extension PointOfSalePaymentState { @@ -49,7 +51,9 @@ extension PointOfSalePaymentState { switch self { case .processingPayment, .paymentError, - .cardPaymentSuccessful: + .cardPaymentSuccessful, + .acceptingCash, + .cashPaymentSuccessful: return true case .idle, .validatingOrder, diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift new file mode 100644 index 00000000000..d962f0da05c --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -0,0 +1,102 @@ +import SwiftUI + +struct PointOfSaleCollectCashView: View { + @EnvironmentObject private var posModel: PointOfSaleAggregateModel + + @State private var textFieldAmountInput: String = "" + @State private var isLoading: Bool = false + @State private var errorMessage: String? + + var body: some View { + VStack(alignment: .center, spacing: 20) { + HStack { + Button(action: { + posModel.cancelCashPayment() + }, label: { + HStack { + Image(systemName: "arrow.backward") + .font(.headline) + .foregroundColor(.primary) + Text("Cash payment") + } + }) + Spacer() + } + .padding() + + TextField("$0.00", text: $textFieldAmountInput) + .keyboardType(.numbersAndPunctuation) + .textInputAutocapitalization(.none) + .autocorrectionDisabled() + .multilineTextAlignment(.center) + .font(POSFontStyle.posTitleRegular) + .focused() + .padding() + .onSubmit { + Task { @MainActor in + await markComplete() + } + } + + if let errorMessage = errorMessage { + Text(errorMessage) + .font(POSFontStyle.posBodyRegular) + .foregroundColor(.red) + } + + Button(action: { + Task { @MainActor in + await markComplete() + } + }, label: { + HStack(spacing: Constants.buttonSpacing) { + if isLoading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .tint(Color.posPrimaryTextInverted) + } else { + Text("Mark payment as complete") + .font(Constants.buttonFont) + } + } + .frame(maxWidth: .infinity) + }) + .padding(Constants.buttonPadding) + .frame(maxWidth: .infinity) + .foregroundColor(Color.posPrimaryTextInverted) + .background(Color.posOverlayFillInverted) + .cornerRadius(Constants.buttonCornerRadius) + .contentShape(Rectangle()) + .disabled(isLoading) + + Spacer() + } + .padding() + .animation(.easeInOut, value: errorMessage) + .onChange(of: textFieldAmountInput) { amount in + errorMessage = nil + } + } + + private func markComplete() async { + // no-op + } +} + +private extension PointOfSaleCollectCashView { + enum Constants { + static let buttonSpacing: CGFloat = 12 + static let buttonPadding: CGFloat = 32 + static let buttonFont: POSFontStyle = .posBodyEmphasized + static let buttonCornerRadius: CGFloat = 8 + } +} + +#Preview { + let posModel = PointOfSaleAggregateModel( + itemsController: PointOfSalePreviewItemsController(), + cardPresentPaymentService: CardPresentPaymentPreviewService(), + orderController: PointOfSalePreviewOrderController()) + PointOfSaleCollectCashView() + .environmentObject(posModel) +} diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index 607806f46e0..65aa6bcc2c1 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -87,9 +87,14 @@ struct PointOfSaleDashboardView: View { } if posModel.orderStage == .finalizing { - TotalsView() - .accessibilitySortPriority(2) - .transition(.move(edge: .trailing)) + switch posModel.paymentState { + case .acceptingCash: + PointOfSaleCollectCashView() + default: + TotalsView() + .accessibilitySortPriority(2) + .transition(.move(edge: .trailing)) + } } } .animation(.default, value: posModel.orderStage) diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index 764da6ca8ee..f1e2b6c5c40 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -16,6 +16,10 @@ struct TotalsView: View { private var shouldShowTotalsFields: Bool { viewHelper.shouldShowTotalsFields(for: posModel.paymentState) } + + private var shouldShowCollectCashPaymentButton: Bool { + ServiceLocator.featureFlagService.isFeatureFlagEnabled(.acceptCashForPointOfSale) + } @Environment(\.dynamicTypeSize) var dynamicTypeSize @Environment(\.colorScheme) var colorScheme @@ -52,6 +56,17 @@ struct TotalsView: View { .opacity(viewHelper.shouldShowTotalsFields(for: posModel.paymentState) ? 1 : 0) .layoutPriority(2) } + + Button(action: { + posModel.collectCashPayment() + }, label: { + Text("Cash payment") + .foregroundColor(.posPrimaryText) + }) + .buttonStyle(SecondaryButtonStyle()) + .padding(.horizontal) + .renderedIf(posModel.orderState != .syncing) + .renderedIf(shouldShowCollectCashPaymentButton) } .animation(.default, value: posModel.cardPresentPaymentInlineMessage) Spacer() @@ -275,7 +290,9 @@ private extension TotalsView { .validatingOrder, .preparingReader, .processingPayment, - .cardPaymentSuccessful: + .cardPaymentSuccessful, + .acceptingCash, + .cashPaymentSuccessful: break } diff --git a/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift b/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift index 29ac1f88af7..fa9dab6f122 100644 --- a/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift +++ b/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift @@ -32,7 +32,11 @@ private extension PointOfSalePaymentState { .validatingOrder, .preparingReader: return false - case .idle, .validatingOrderError, .acceptingCard: + case .idle, + .validatingOrderError, + .acceptingCard, + .acceptingCash, + .cashPaymentSuccessful: return true } } diff --git a/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift b/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift index 1c9a13f9e8a..b4b53b02634 100644 --- a/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift +++ b/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift @@ -11,7 +11,9 @@ final class TotalsViewHelper { return true case .processingPayment, .paymentError, - .cardPaymentSuccessful: + .cardPaymentSuccessful, + .acceptingCash, + .cashPaymentSuccessful: return false } } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index ea55312796e..5143323ade9 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1583,6 +1583,7 @@ 68D8FBD12BFEF9C700477C42 /* TotalsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D8FBD02BFEF9C700477C42 /* TotalsView.swift */; }; 68DF5A8D2CB38EEA000154C9 /* EditableOrderCouponLineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DF5A8C2CB38EEA000154C9 /* EditableOrderCouponLineViewModel.swift */; }; 68DF5A8F2CB38F20000154C9 /* OrderCouponSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DF5A8E2CB38F20000154C9 /* OrderCouponSectionView.swift */; }; + 68E141DB2D13107400A70D5B /* PointOfSaleCollectCashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E141DA2D13107200A70D5B /* PointOfSaleCollectCashView.swift */; }; 68E4E8B52C0EF39D00CFA0C3 /* PreviewHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E4E8B42C0EF39D00CFA0C3 /* PreviewHelpers.swift */; }; 68E6749F2A4DA01C0034BA1E /* WooWPComPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E6749E2A4DA01C0034BA1E /* WooWPComPlan.swift */; }; 68E674A12A4DA0B30034BA1E /* InAppPurchasesError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E674A02A4DA0B30034BA1E /* InAppPurchasesError.swift */; }; @@ -4662,6 +4663,7 @@ 68D8FBD02BFEF9C700477C42 /* TotalsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalsView.swift; sourceTree = ""; }; 68DF5A8C2CB38EEA000154C9 /* EditableOrderCouponLineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableOrderCouponLineViewModel.swift; sourceTree = ""; }; 68DF5A8E2CB38F20000154C9 /* OrderCouponSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCouponSectionView.swift; sourceTree = ""; }; + 68E141DA2D13107200A70D5B /* PointOfSaleCollectCashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCollectCashView.swift; sourceTree = ""; }; 68E4E8B42C0EF39D00CFA0C3 /* PreviewHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewHelpers.swift; sourceTree = ""; }; 68E6749E2A4DA01C0034BA1E /* WooWPComPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooWPComPlan.swift; sourceTree = ""; }; 68E674A02A4DA0B30034BA1E /* InAppPurchasesError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesError.swift; sourceTree = ""; }; @@ -6955,6 +6957,7 @@ 026826A72BF59DF70036F959 /* PointOfSaleEntryPointView.swift */, 68A345632D029E09002EE324 /* PaymentButtons.swift */, 026826A52BF59DF60036F959 /* PointOfSaleDashboardView.swift */, + 68E141DA2D13107200A70D5B /* PointOfSaleCollectCashView.swift */, DA013F502C65125100D9A391 /* PointOfSaleExitPosAlertView.swift */, DA0DBE2E2C4FC61D00DF14C0 /* POSFloatingControlView.swift */, 20D3D42A2C64D7CC004CE6E3 /* SimpleProductsOnlyInformation.swift */, @@ -15750,6 +15753,7 @@ EE19058C2B5F744300617C53 /* BlazeAddPaymentMethodWebView.swift in Sources */, D83F5933225B2EB900626E75 /* ManualTrackingViewController.swift in Sources */, 3142663F2645E2AB00500598 /* PaymentSettingsFlowViewModelPresenter.swift in Sources */, + 68E141DB2D13107400A70D5B /* PointOfSaleCollectCashView.swift in Sources */, DEDB886B26E8531E00981595 /* ShippingLabelPackageAttributes.swift in Sources */, AEC95D412774C5AE001571F5 /* AddressFormViewModelProtocol.swift in Sources */, B6E851F5276330200041D1BA /* RefundCustomAmountsDetailsTableViewCell.swift in Sources */, From 0061a43b81cbfd277903fc8ef3f1126e98fcf5ff Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 19 Dec 2024 15:51:43 +0700 Subject: [PATCH 02/14] render totals in cash view navigation --- .../PointOfSaleCollectCashView.swift | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index d962f0da05c..2c012b6eb19 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -7,17 +7,30 @@ struct PointOfSaleCollectCashView: View { @State private var isLoading: Bool = false @State private var errorMessage: String? + private var orderTotal: String? { + if case .loaded(let totals) = posModel.orderState { + return totals.orderTotal + } + return nil + } + var body: some View { VStack(alignment: .center, spacing: 20) { HStack { Button(action: { posModel.cancelCashPayment() }, label: { - HStack { - Image(systemName: "arrow.backward") - .font(.headline) - .foregroundColor(.primary) - Text("Cash payment") + VStack { + HStack { + Image(systemName: "arrow.backward") + .font(.headline) + .foregroundColor(.primary) + Text("Cash payment") + } + if let orderTotal = orderTotal { + Text(orderTotal) + .font(.caption) + } } }) Spacer() From ab3722636ddf7edf4e84f615faabb973efdbb54a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 19 Dec 2024 16:02:36 +0700 Subject: [PATCH 03/14] move strings to extension --- .../PointOfSaleCollectCashView.swift | 20 ++++++++++++++++--- .../Classes/POS/Presentation/TotalsView.swift | 11 ++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index 2c012b6eb19..53b5e63b09a 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -25,7 +25,7 @@ struct PointOfSaleCollectCashView: View { Image(systemName: "arrow.backward") .font(.headline) .foregroundColor(.primary) - Text("Cash payment") + Text(Localization.backNavigationTitle) } if let orderTotal = orderTotal { Text(orderTotal) @@ -68,7 +68,7 @@ struct PointOfSaleCollectCashView: View { .progressViewStyle(CircularProgressViewStyle()) .tint(Color.posPrimaryTextInverted) } else { - Text("Mark payment as complete") + Text(Localization.markPaymentCompletedButtonTitle) .font(Constants.buttonFont) } } @@ -92,7 +92,8 @@ struct PointOfSaleCollectCashView: View { } private func markComplete() async { - // no-op + // TODO: + // https://github.com/woocommerce/woocommerce-ios/issues/14602 } } @@ -103,6 +104,19 @@ private extension PointOfSaleCollectCashView { static let buttonFont: POSFontStyle = .posBodyEmphasized static let buttonCornerRadius: CGFloat = 8 } + + enum Localization { + static let backNavigationTitle = NSLocalizedString( + "pointOfSale.cashview.back.navigation.title", + value: "Cash payment", + comment: "Title of the cash payment navigation back button" + ) + static let markPaymentCompletedButtonTitle = NSLocalizedString( + "pointOfSale.cashview.back.navigation.title", + value: "Mark payment as complete", + comment: "Button to mark a cash payment as completed" + ) + } } #Preview { diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index f1e2b6c5c40..c722249325c 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -16,7 +16,7 @@ struct TotalsView: View { private var shouldShowTotalsFields: Bool { viewHelper.shouldShowTotalsFields(for: posModel.paymentState) } - + private var shouldShowCollectCashPaymentButton: Bool { ServiceLocator.featureFlagService.isFeatureFlagEnabled(.acceptCashForPointOfSale) } @@ -56,15 +56,14 @@ struct TotalsView: View { .opacity(viewHelper.shouldShowTotalsFields(for: posModel.paymentState) ? 1 : 0) .layoutPriority(2) } - Button(action: { posModel.collectCashPayment() }, label: { - Text("Cash payment") + Text(Localization.cashPaymentButtonTitle) .foregroundColor(.posPrimaryText) }) .buttonStyle(SecondaryButtonStyle()) - .padding(.horizontal) + .padding() .renderedIf(posModel.orderState != .syncing) .renderedIf(shouldShowCollectCashPaymentButton) } @@ -345,6 +344,10 @@ private extension TotalsView { "pos.totalsView.taxes", value: "Taxes", comment: "Title for taxes amount field") + static let cashPaymentButtonTitle = NSLocalizedString( + "pos.totalsView.cash.button.title", + value: "Cash payment", + comment: "Title for the cash payment button title") } } From 6144cf774189f8b8f0fb5c90f7dfdb148201e28c Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 19 Dec 2024 17:22:14 +0700 Subject: [PATCH 04/14] update button style --- WooCommerce/Classes/POS/Presentation/TotalsView.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index c722249325c..9c55ecda203 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -60,10 +60,12 @@ struct TotalsView: View { posModel.collectCashPayment() }, label: { Text(Localization.cashPaymentButtonTitle) + .font(POSFontStyle.posBodyEmphasized) .foregroundColor(.posPrimaryText) + .frame(height: Constants.buttonHeight) }) .buttonStyle(SecondaryButtonStyle()) - .padding() + .padding(.horizontal, Constants.buttonHorizontalPadding) .renderedIf(posModel.orderState != .syncing) .renderedIf(shouldShowCollectCashPaymentButton) } @@ -307,6 +309,8 @@ private extension TotalsView { enum Constants { static let pricesIdealWidth: CGFloat = 382 static let verticalSpacing: CGFloat = 56 + static let buttonHeight: CGFloat = 56 + static let buttonHorizontalPadding: CGFloat = 48 static let totalsLineViewPadding: EdgeInsets = .init(top: 20, leading: 24, bottom: 20, trailing: 24) static let subtotalsVerticalSpacing: CGFloat = 8 From 6eb26efa0c7a615e7714c5541dc18c1a98e9a422 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 20 Dec 2024 10:53:34 +0700 Subject: [PATCH 05/14] Make cash view modal presentation from totals --- .../POS/Presentation/PointOfSaleCollectCashView.swift | 6 ++++-- .../POS/Presentation/PointOfSaleDashboardView.swift | 11 +++-------- WooCommerce/Classes/POS/Presentation/TotalsView.swift | 7 ++++++- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index 53b5e63b09a..b37f066b7fe 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -7,6 +7,8 @@ struct PointOfSaleCollectCashView: View { @State private var isLoading: Bool = false @State private var errorMessage: String? + @Binding var isVisible: Bool + private var orderTotal: String? { if case .loaded(let totals) = posModel.orderState { return totals.orderTotal @@ -18,7 +20,7 @@ struct PointOfSaleCollectCashView: View { VStack(alignment: .center, spacing: 20) { HStack { Button(action: { - posModel.cancelCashPayment() + isVisible = false }, label: { VStack { HStack { @@ -124,6 +126,6 @@ private extension PointOfSaleCollectCashView { itemsController: PointOfSalePreviewItemsController(), cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) - PointOfSaleCollectCashView() + PointOfSaleCollectCashView(isVisible: .constant(true)) .environmentObject(posModel) } diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index 65aa6bcc2c1..607806f46e0 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -87,14 +87,9 @@ struct PointOfSaleDashboardView: View { } if posModel.orderStage == .finalizing { - switch posModel.paymentState { - case .acceptingCash: - PointOfSaleCollectCashView() - default: - TotalsView() - .accessibilitySortPriority(2) - .transition(.move(edge: .trailing)) - } + TotalsView() + .accessibilitySortPriority(2) + .transition(.move(edge: .trailing)) } } .animation(.default, value: posModel.orderStage) diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index 9c55ecda203..499312be44b 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -24,6 +24,8 @@ struct TotalsView: View { @Environment(\.dynamicTypeSize) var dynamicTypeSize @Environment(\.colorScheme) var colorScheme + @State private var shouldShowCollectCashPayment: Bool = false + var body: some View { HStack { switch posModel.orderState { @@ -57,7 +59,7 @@ struct TotalsView: View { .layoutPriority(2) } Button(action: { - posModel.collectCashPayment() + shouldShowCollectCashPayment = true }, label: { Text(Localization.cashPaymentButtonTitle) .font(POSFontStyle.posBodyEmphasized) @@ -86,6 +88,9 @@ struct TotalsView: View { } .onChange(of: shouldShowTotalsFields, perform: hideTotalsFieldsWithDelay) .geometryGroupIfSupported() + .fullScreenCover(isPresented: $shouldShowCollectCashPayment) { + PointOfSaleCollectCashView(isVisible: $shouldShowCollectCashPayment) + } } private var backgroundColor: Color { From 714dfe5115cdeb8146a10cc4fa59eeb3b6da568a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 20 Dec 2024 11:22:34 +0700 Subject: [PATCH 06/14] Pass order total to cash view from parent --- .../PointOfSaleCollectCashView.swift | 36 +++++++++++-------- .../Classes/POS/Presentation/TotalsView.swift | 5 ++- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index b37f066b7fe..c3dd659d556 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -8,12 +8,10 @@ struct PointOfSaleCollectCashView: View { @State private var errorMessage: String? @Binding var isVisible: Bool + let orderTotal: String - private var orderTotal: String? { - if case .loaded(let totals) = posModel.orderState { - return totals.orderTotal - } - return nil + private var formattedOrderTotal: String { + String.localizedStringWithFormat(Localization.backNavigationSubtitle, orderTotal) } var body: some View { @@ -24,15 +22,16 @@ struct PointOfSaleCollectCashView: View { }, label: { VStack { HStack { - Image(systemName: "arrow.backward") - .font(.headline) - .foregroundColor(.primary) + Image(systemName: "chevron.left") Text(Localization.backNavigationTitle) } - if let orderTotal = orderTotal { - Text(orderTotal) - .font(.caption) - } + .font(.posTitleRegular) + .bold() + .foregroundColor(.primary) + + Text(formattedOrderTotal) + .font(.posBodyRegular) + .foregroundColor(.primary) } }) Spacer() @@ -111,10 +110,16 @@ private extension PointOfSaleCollectCashView { static let backNavigationTitle = NSLocalizedString( "pointOfSale.cashview.back.navigation.title", value: "Cash payment", - comment: "Title of the cash payment navigation back button" + comment: "Title for the cash payment view navigation back button" + ) + static let backNavigationSubtitle = NSLocalizedString( + "pointOfSale.cashview.back.navigation.subtitle", + value: "Total: %1$@", + comment: "Subtitle for the cash payment view navigation back button" + + "Reads as 'Total: $1.23'" ) static let markPaymentCompletedButtonTitle = NSLocalizedString( - "pointOfSale.cashview.back.navigation.title", + "pointOfSale.cashview.button.markpaymentcompleted.title", value: "Mark payment as complete", comment: "Button to mark a cash payment as completed" ) @@ -126,6 +131,7 @@ private extension PointOfSaleCollectCashView { itemsController: PointOfSalePreviewItemsController(), cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) - PointOfSaleCollectCashView(isVisible: .constant(true)) + PointOfSaleCollectCashView(isVisible: .constant(true), + orderTotal: "$1.23") .environmentObject(posModel) } diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index 499312be44b..34cd7a3b39b 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -89,7 +89,10 @@ struct TotalsView: View { .onChange(of: shouldShowTotalsFields, perform: hideTotalsFieldsWithDelay) .geometryGroupIfSupported() .fullScreenCover(isPresented: $shouldShowCollectCashPayment) { - PointOfSaleCollectCashView(isVisible: $shouldShowCollectCashPayment) + if case .loaded(let total) = posModel.orderState { + PointOfSaleCollectCashView(isVisible: $shouldShowCollectCashPayment, + orderTotal: total.orderTotal) + } } } From c9178c00dafb78d3fc30e8dd2116b2e8ff721ada Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 20 Dec 2024 11:23:17 +0700 Subject: [PATCH 07/14] update backbutton icon on receipt for consistency Along with the usage of a chevron for the cash view when navigating back, we also update the receipt view with the same icon for consistency --- .../POS/Presentation/Reusable Views/POSSendReceiptView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift index c00bdbe7e97..dacedbea737 100644 --- a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift +++ b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSendReceiptView.swift @@ -20,7 +20,7 @@ struct POSSendReceiptView: View { isShowingSendReceiptView = false }, label: { HStack { - Image(systemName: "arrow.backward") + Image(systemName: "chevron.left") Text(Localization.emailReceiptNavigationText) } .font(.title) From 71547ba0c3b96c291092f92e39ca790104346b39 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 20 Dec 2024 11:27:35 +0700 Subject: [PATCH 08/14] Revert changes to payment state for cash navigation --- .../POS/Models/PointOfSaleAggregateModel.swift | 18 ------------------ .../POS/Models/PointOfSalePaymentState.swift | 6 +----- .../Classes/POS/Presentation/TotalsView.swift | 4 +--- .../POS/ViewHelpers/CartViewHelper.swift | 4 +--- .../POS/ViewHelpers/TotalsViewHelper.swift | 4 +--- 5 files changed, 4 insertions(+), 32 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index af3d05e751f..84059497eff 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -61,7 +61,6 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt private var startPaymentOnCardReaderConnection: AnyCancellable? private var cardReaderDisconnection: AnyCancellable? - private var latestPaymentState: PointOfSalePaymentState? = nil private var cancellables: Set = [] @@ -264,23 +263,6 @@ extension PointOfSaleAggregateModel { } } -// MARK: - Card payments -// -extension PointOfSaleAggregateModel { - func collectCashPayment() { - // Capture the latest payment state before switching to cash collection - // so we can return if the collection is cancelled - latestPaymentState = paymentState - paymentState = .acceptingCash - } - - func cancelCashPayment() { - if let latestPaymentState = latestPaymentState { - paymentState = latestPaymentState - } - } -} - private extension PointOfSaleAggregateModel { func publishPaymentMessages() { cardPresentPaymentService.paymentEventPublisher diff --git a/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift b/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift index 4d76c058bac..97907cfa5c5 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift @@ -3,14 +3,12 @@ import Foundation enum PointOfSalePaymentState { case idle case acceptingCard - case acceptingCash case validatingOrder case validatingOrderError case preparingReader case processingPayment case paymentError case cardPaymentSuccessful - case cashPaymentSuccessful } extension PointOfSalePaymentState { @@ -51,9 +49,7 @@ extension PointOfSalePaymentState { switch self { case .processingPayment, .paymentError, - .cardPaymentSuccessful, - .acceptingCash, - .cashPaymentSuccessful: + .cardPaymentSuccessful: return true case .idle, .validatingOrder, diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index 34cd7a3b39b..e96787b4f7f 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -299,9 +299,7 @@ private extension TotalsView { .validatingOrder, .preparingReader, .processingPayment, - .cardPaymentSuccessful, - .acceptingCash, - .cashPaymentSuccessful: + .cardPaymentSuccessful: break } diff --git a/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift b/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift index fa9dab6f122..591ee93098f 100644 --- a/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift +++ b/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift @@ -34,9 +34,7 @@ private extension PointOfSalePaymentState { return false case .idle, .validatingOrderError, - .acceptingCard, - .acceptingCash, - .cashPaymentSuccessful: + .acceptingCard: return true } } diff --git a/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift b/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift index b4b53b02634..1c9a13f9e8a 100644 --- a/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift +++ b/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift @@ -11,9 +11,7 @@ final class TotalsViewHelper { return true case .processingPayment, .paymentError, - .cardPaymentSuccessful, - .acceptingCash, - .cashPaymentSuccessful: + .cardPaymentSuccessful: return false } } From 5f9bc07803b60970d6f0cfc299c677564b2522bb Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 20 Dec 2024 11:50:45 +0700 Subject: [PATCH 09/14] Render button only when payment state idle --- WooCommerce/Classes/POS/Presentation/TotalsView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index e96787b4f7f..71670250b1e 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -18,7 +18,9 @@ struct TotalsView: View { } private var shouldShowCollectCashPaymentButton: Bool { - ServiceLocator.featureFlagService.isFeatureFlagEnabled(.acceptCashForPointOfSale) + ServiceLocator.featureFlagService.isFeatureFlagEnabled(.acceptCashForPointOfSale) && + posModel.orderState != .syncing && + posModel.paymentState == .idle } @Environment(\.dynamicTypeSize) var dynamicTypeSize @@ -68,7 +70,6 @@ struct TotalsView: View { }) .buttonStyle(SecondaryButtonStyle()) .padding(.horizontal, Constants.buttonHorizontalPadding) - .renderedIf(posModel.orderState != .syncing) .renderedIf(shouldShowCollectCashPaymentButton) } .animation(.default, value: posModel.cardPresentPaymentInlineMessage) From e656b3b3f4db2f5d20219096b307bc537e85aac2 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 23 Dec 2024 16:18:01 +0700 Subject: [PATCH 10/14] revert styling --- WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift b/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift index 591ee93098f..29ac1f88af7 100644 --- a/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift +++ b/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift @@ -32,9 +32,7 @@ private extension PointOfSalePaymentState { .validatingOrder, .preparingReader: return false - case .idle, - .validatingOrderError, - .acceptingCard: + case .idle, .validatingOrderError, .acceptingCard: return true } } From 8d723fa9b9002f3d500150efc66990a917d2925c Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 23 Dec 2024 18:35:11 +0700 Subject: [PATCH 11/14] Update cash view to dismiss via environment --- .../POS/Presentation/PointOfSaleCollectCashView.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index c3dd659d556..87a0f4b4943 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -1,13 +1,13 @@ import SwiftUI struct PointOfSaleCollectCashView: View { + @Environment(\.dismiss) private var dismiss @EnvironmentObject private var posModel: PointOfSaleAggregateModel @State private var textFieldAmountInput: String = "" @State private var isLoading: Bool = false @State private var errorMessage: String? - @Binding var isVisible: Bool let orderTotal: String private var formattedOrderTotal: String { @@ -18,7 +18,7 @@ struct PointOfSaleCollectCashView: View { VStack(alignment: .center, spacing: 20) { HStack { Button(action: { - isVisible = false + dismiss() }, label: { VStack { HStack { @@ -131,7 +131,6 @@ private extension PointOfSaleCollectCashView { itemsController: PointOfSalePreviewItemsController(), cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController()) - PointOfSaleCollectCashView(isVisible: .constant(true), - orderTotal: "$1.23") + PointOfSaleCollectCashView(orderTotal: "$1.23") .environmentObject(posModel) } From c8a00b29da8baca25bbf4a01628a572415e7310b Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 23 Dec 2024 18:36:11 +0700 Subject: [PATCH 12/14] Resolved animation oddness When dismissing the cash view, the animation for cart seems to jump vertically. It seems to be resolved by adding the cash view to matched geometry --- WooCommerce/Classes/POS/Presentation/TotalsView.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index 71670250b1e..1231a0e94fd 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -88,13 +88,14 @@ struct TotalsView: View { isShowingTotalsFields = shouldShowTotalsFields } .onChange(of: shouldShowTotalsFields, perform: hideTotalsFieldsWithDelay) - .geometryGroupIfSupported() .fullScreenCover(isPresented: $shouldShowCollectCashPayment) { if case .loaded(let total) = posModel.orderState { - PointOfSaleCollectCashView(isVisible: $shouldShowCollectCashPayment, - orderTotal: total.orderTotal) + PointOfSaleCollectCashView(orderTotal: total.orderTotal) + .matchedGeometryEffect(id: Constants.matchedGeometryCashId, + in: totalsFieldAnimation) } } + .geometryGroupIfSupported() } private var backgroundColor: Color { @@ -338,6 +339,7 @@ private extension TotalsView { static let matchedGeometrySubtotalId: String = "pos_totals_view_subtotal_matched_geometry_id" static let matchedGeometryTaxId: String = "pos_totals_view_tax_matched_geometry_id" static let matchedGeometryTotalId: String = "pos_totals_view_total_matched_geometry_id" + static let matchedGeometryCashId: String = "pos_totals_view_cash_matched_geometry_id" static let totalsFieldsHideAnimationDelay: CGFloat = 0.3 } From 57a66b6c50bfa30eda51d90e2e989602d8c5441b Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 23 Dec 2024 19:18:57 +0700 Subject: [PATCH 13/14] Show button when card is acceptingCard state --- WooCommerce/Classes/POS/Presentation/TotalsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index 1231a0e94fd..19988ea5e5b 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -20,7 +20,7 @@ struct TotalsView: View { private var shouldShowCollectCashPaymentButton: Bool { ServiceLocator.featureFlagService.isFeatureFlagEnabled(.acceptCashForPointOfSale) && posModel.orderState != .syncing && - posModel.paymentState == .idle + (posModel.paymentState == .idle || posModel.paymentState == .acceptingCard) } @Environment(\.dynamicTypeSize) var dynamicTypeSize From 4e5a727ff153d7c277d134d48f5047d3996b9184 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 6 Jan 2025 09:06:54 +0700 Subject: [PATCH 14/14] Wrap preview in debug precompiler flag --- .../Classes/POS/Presentation/PointOfSaleCollectCashView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index 87a0f4b4943..358ed2c097d 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -126,6 +126,7 @@ private extension PointOfSaleCollectCashView { } } +#if DEBUG #Preview { let posModel = PointOfSaleAggregateModel( itemsController: PointOfSalePreviewItemsController(), @@ -134,3 +135,4 @@ private extension PointOfSaleCollectCashView { PointOfSaleCollectCashView(orderTotal: "$1.23") .environmentObject(posModel) } +#endif