diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift new file mode 100644 index 00000000000..358ed2c097d --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -0,0 +1,138 @@ +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? + + let orderTotal: String + + private var formattedOrderTotal: String { + String.localizedStringWithFormat(Localization.backNavigationSubtitle, orderTotal) + } + + var body: some View { + VStack(alignment: .center, spacing: 20) { + HStack { + Button(action: { + dismiss() + }, label: { + VStack { + HStack { + Image(systemName: "chevron.left") + Text(Localization.backNavigationTitle) + } + .font(.posTitleRegular) + .bold() + .foregroundColor(.primary) + + Text(formattedOrderTotal) + .font(.posBodyRegular) + .foregroundColor(.primary) + } + }) + 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(Localization.markPaymentCompletedButtonTitle) + .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 { + // TODO: + // https://github.com/woocommerce/woocommerce-ios/issues/14602 + } +} + +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 + } + + enum Localization { + static let backNavigationTitle = NSLocalizedString( + "pointOfSale.cashview.back.navigation.title", + value: "Cash payment", + 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.button.markpaymentcompleted.title", + value: "Mark payment as complete", + comment: "Button to mark a cash payment as completed" + ) + } +} + +#if DEBUG +#Preview { + let posModel = PointOfSaleAggregateModel( + itemsController: PointOfSalePreviewItemsController(), + cardPresentPaymentService: CardPresentPaymentPreviewService(), + orderController: PointOfSalePreviewOrderController()) + PointOfSaleCollectCashView(orderTotal: "$1.23") + .environmentObject(posModel) +} +#endif 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) diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index 764da6ca8ee..19988ea5e5b 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -17,9 +17,17 @@ struct TotalsView: View { viewHelper.shouldShowTotalsFields(for: posModel.paymentState) } + private var shouldShowCollectCashPaymentButton: Bool { + ServiceLocator.featureFlagService.isFeatureFlagEnabled(.acceptCashForPointOfSale) && + posModel.orderState != .syncing && + (posModel.paymentState == .idle || posModel.paymentState == .acceptingCard) + } + @Environment(\.dynamicTypeSize) var dynamicTypeSize @Environment(\.colorScheme) var colorScheme + @State private var shouldShowCollectCashPayment: Bool = false + var body: some View { HStack { switch posModel.orderState { @@ -52,6 +60,17 @@ struct TotalsView: View { .opacity(viewHelper.shouldShowTotalsFields(for: posModel.paymentState) ? 1 : 0) .layoutPriority(2) } + Button(action: { + shouldShowCollectCashPayment = true + }, label: { + Text(Localization.cashPaymentButtonTitle) + .font(POSFontStyle.posBodyEmphasized) + .foregroundColor(.posPrimaryText) + .frame(height: Constants.buttonHeight) + }) + .buttonStyle(SecondaryButtonStyle()) + .padding(.horizontal, Constants.buttonHorizontalPadding) + .renderedIf(shouldShowCollectCashPaymentButton) } .animation(.default, value: posModel.cardPresentPaymentInlineMessage) Spacer() @@ -69,6 +88,13 @@ struct TotalsView: View { isShowingTotalsFields = shouldShowTotalsFields } .onChange(of: shouldShowTotalsFields, perform: hideTotalsFieldsWithDelay) + .fullScreenCover(isPresented: $shouldShowCollectCashPayment) { + if case .loaded(let total) = posModel.orderState { + PointOfSaleCollectCashView(orderTotal: total.orderTotal) + .matchedGeometryEffect(id: Constants.matchedGeometryCashId, + in: totalsFieldAnimation) + } + } .geometryGroupIfSupported() } @@ -291,6 +317,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 @@ -311,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 } @@ -328,6 +357,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") } } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 59e3a9a7343..5bc1f7f395e 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1591,6 +1591,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 */; }; @@ -4686,6 +4687,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 = ""; }; @@ -6989,6 +6991,7 @@ 026826A72BF59DF70036F959 /* PointOfSaleEntryPointView.swift */, 68A345632D029E09002EE324 /* PaymentButtons.swift */, 026826A52BF59DF60036F959 /* PointOfSaleDashboardView.swift */, + 68E141DA2D13107200A70D5B /* PointOfSaleCollectCashView.swift */, DA013F502C65125100D9A391 /* PointOfSaleExitPosAlertView.swift */, DA0DBE2E2C4FC61D00DF14C0 /* POSFloatingControlView.swift */, 20D3D42A2C64D7CC004CE6E3 /* SimpleProductsOnlyInformation.swift */, @@ -15820,6 +15823,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 */,