diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentPreviewService.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentPreviewService.swift index 2c3c2a32e01..a1cea225df3 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentPreviewService.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentPreviewService.swift @@ -14,6 +14,10 @@ final class CardPresentPaymentPreviewService: CardPresentPaymentFacade { $readerConnectionStatus.eraseToAnyPublisher() } + init(connectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected) { + self.readerConnectionStatus = connectionStatus + } + func connectReader(using connectionMethod: CardReaderConnectionMethod) async throws -> CardPresentPaymentReaderConnectionResult { .connected(CardPresentPaymentCardReader(name: "Test reader", batteryLevel: 0.85)) } diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index d3f845951fe..03cddc67d67 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -627,3 +627,12 @@ private extension PointOfSaleAggregateModel { static let initialPage: Int = 1 } } + +#if DEBUG +extension PointOfSaleAggregateModel { + func setPreviewState(paymentState: PointOfSalePaymentState, inlineMessage: PointOfSaleCardPresentPaymentMessageType?) { + self.paymentState = paymentState + self.cardPresentPaymentInlineMessage = inlineMessage + } +} +#endif diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/POSCardPresentPaymentMessageViewImage.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/POSCardPresentPaymentMessageViewImage.swift new file mode 100644 index 00000000000..3a94b6a979a --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/POSCardPresentPaymentMessageViewImage.swift @@ -0,0 +1,22 @@ +import SwiftUI + +struct POSCardPresentPaymentMessageViewImage: View { + private let imageName: String + + init(imageName: String) { + self.imageName = imageName + } + + var body: some View { + Image(decorative: imageName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame( + minWidth: PointOfSaleCardPresentPaymentLayout.compactHeaderSize.width, + maxWidth: PointOfSaleCardPresentPaymentLayout.headerSize.width, + minHeight: PointOfSaleCardPresentPaymentLayout.compactHeaderSize.height, + maxHeight: PointOfSaleCardPresentPaymentLayout.headerSize.height + ) + .accessibilityHidden(true) + } +} diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentCardInsertedMessageView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentCardInsertedMessageView.swift index 30b7dab0007..33a23b8f7b1 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentCardInsertedMessageView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentCardInsertedMessageView.swift @@ -7,11 +7,7 @@ struct PointOfSaleCardPresentPaymentCardInsertedMessageView: View { var body: some View { VStack(alignment: .center, spacing: Constants.imageAndTextSpacing) { - Image(decorative: viewModel.imageName) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: PointOfSaleCardPresentPaymentLayout.headerSize.width, - height: PointOfSaleCardPresentPaymentLayout.headerSize.height) + POSCardPresentPaymentMessageViewImage(imageName: viewModel.imageName) .matchedGeometryEffect(id: animation.iconTransitionId, in: animation.namespace, properties: .position) .renderedIf(!dynamicTypeSize.isAccessibilitySize) VStack(alignment: .center, spacing: PointOfSaleCardPresentPaymentLayout.textSpacing) { diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentDisconnectedMessageView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentDisconnectedMessageView.swift index 18c012aa529..3dcb4dbd25e 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentDisconnectedMessageView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentDisconnectedMessageView.swift @@ -13,7 +13,7 @@ struct PointOfSaleCardPresentPaymentReaderDisconnectedMessageView: View { var body: some View { VStack(alignment: .center, spacing: POSSpacing.none) { - Image(decorative: PointOfSaleAssets.readerDisconnected.imageName) + POSCardPresentPaymentMessageViewImage(imageName: PointOfSaleAssets.readerDisconnected.imageName) Spacer() .frame(height: dynamicSpacing(PointOfSaleCardPresentPaymentLayout.imageAndTextSpacing)) @@ -41,6 +41,7 @@ struct PointOfSaleCardPresentPaymentReaderDisconnectedMessageView: View { .buttonStyle(POSFilledButtonStyle(size: .normal)) .frame(width: width * 0.5) } + .frame(maxWidth: .infinity) .measureWidth({ containerWidth in width = containerWidth }) diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentLayout.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentLayout.swift index eba0890b7a4..77902913d90 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentLayout.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentLayout.swift @@ -2,6 +2,7 @@ import Foundation enum PointOfSaleCardPresentPaymentLayout { static let headerSize: CGSize = .init(width: 156, height: 156) + static let compactHeaderSize: CGSize = .init(width: 48, height: 48) static let imageAndTextSpacing: CGFloat = POSSpacing.large static let textAndButtonSpacing: CGFloat = POSSpacing.xxLarge static let textSpacing: CGFloat = POSSpacing.small diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView.swift index 08b73451421..3c13a8b851a 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView.swift @@ -7,11 +7,7 @@ struct PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView: View { var body: some View { VStack(alignment: .center, spacing: Constants.imageAndTextSpacing) { - Image(decorative: viewModel.imageName) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: PointOfSaleCardPresentPaymentLayout.headerSize.width, - height: PointOfSaleCardPresentPaymentLayout.headerSize.height) + POSCardPresentPaymentMessageViewImage(imageName: viewModel.imageName) .matchedGeometryEffect(id: animation.iconTransitionId, in: animation.namespace, properties: .position) .renderedIf(!dynamicTypeSize.isAccessibilitySize) VStack(alignment: .center, spacing: PointOfSaleCardPresentPaymentLayout.textSpacing) { diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift index f517fa1dbcd..72e140208b9 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift @@ -39,6 +39,7 @@ struct PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView: View { } } .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) .measureWidth({ containerWidth in width = containerWidth }) diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index d99a63130d9..86dfa82be16 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -65,6 +65,7 @@ struct TotalsView: View { ) } .animation(.default, value: isShowingPaymentView) + .scrollVerticallyIfNeeded() case .error(.other(let message), let handler): PointOfSaleOrderSyncErrorMessageView(message: message, retryHandler: handler) .transition(.opacity) @@ -567,8 +568,120 @@ private struct CashPaymentButton: View { } #if DEBUG -#Preview { +#Preview("Card Reader Not Connected") { TotalsView() - .environment(POSPreviewHelpers.makePreviewAggregateModel()) + .environment(POSPreviewHelpers.makePreviewAggregateModel( + cardPresentPaymentService: CardPresentPaymentPreviewService(connectionStatus: .disconnected) + )) +} + +#Preview("Card Reader Connected") { + TotalsView() + .environment(POSPreviewHelpers.makePreviewAggregateModel( + cardPresentPaymentService: CardPresentPaymentPreviewService( + connectionStatus: .connected(CardPresentPaymentCardReader(name: "Reader", batteryLevel: 0.85)) + ) + )) } + +#Preview("Validating Order") { + let aggregateModel = POSPreviewHelpers.makePreviewAggregateModel( + cardPresentPaymentService: CardPresentPaymentPreviewService( + connectionStatus: .connected(CardPresentPaymentCardReader(name: "Reader", batteryLevel: 0.85)) + ) + ) + Task { @MainActor in + aggregateModel.setPreviewState( + paymentState: PointOfSalePaymentState(card: .validatingOrder, cash: .idle), + inlineMessage: .validatingOrder(viewModel: PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel()) + ) + } + return TotalsView() + .environment(aggregateModel) +} + +#Preview("Accepting Card") { + let aggregateModel = POSPreviewHelpers.makePreviewAggregateModel( + cardPresentPaymentService: CardPresentPaymentPreviewService( + connectionStatus: .connected(CardPresentPaymentCardReader(name: "Reader", batteryLevel: 0.85)) + ) + ) + Task { @MainActor in + aggregateModel.setPreviewState( + paymentState: PointOfSalePaymentState(card: .acceptingCard, cash: .idle), + inlineMessage: .tapSwipeOrInsertCard(viewModel: PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageViewModel(inputMethods: [])) + ) + } + return TotalsView() + .environment(aggregateModel) +} + +#Preview("Processing Payment") { + let aggregateModel = POSPreviewHelpers.makePreviewAggregateModel( + cardPresentPaymentService: CardPresentPaymentPreviewService( + connectionStatus: .connected(CardPresentPaymentCardReader(name: "Reader", batteryLevel: 0.85)) + ) + ) + Task { @MainActor in + aggregateModel.setPreviewState( + paymentState: PointOfSalePaymentState(card: .processingPayment, cash: .idle), + inlineMessage: .processing(viewModel: PointOfSaleCardPresentPaymentProcessingMessageViewModel()) + ) + } + return TotalsView() + .environment(aggregateModel) +} + +#Preview("Card Payment Successful") { + let aggregateModel = POSPreviewHelpers.makePreviewAggregateModel( + cardPresentPaymentService: CardPresentPaymentPreviewService( + connectionStatus: .connected(CardPresentPaymentCardReader(name: "Reader", batteryLevel: 0.85)) + ) + ) + Task { @MainActor in + aggregateModel.setPreviewState( + paymentState: PointOfSalePaymentState(card: .cardPaymentSuccessful, cash: .idle), + inlineMessage: .paymentSuccess(viewModel: PointOfSalePaymentSuccessViewModel(formattedOrderTotal: "$12.00", paymentMethod: .card)) + ) + } + return TotalsView() + .environment(aggregateModel) +} + +#Preview("Display Reader Message") { + let aggregateModel = POSPreviewHelpers.makePreviewAggregateModel( + cardPresentPaymentService: CardPresentPaymentPreviewService( + connectionStatus: .connected(CardPresentPaymentCardReader(name: "Reader", batteryLevel: 0.85)) + ) + ) + Task { @MainActor in + aggregateModel.setPreviewState( + paymentState: PointOfSalePaymentState(card: .processingPayment, cash: .idle), + inlineMessage: .displayReaderMessage(viewModel: PointOfSaleCardPresentPaymentDisplayReaderMessageMessageViewModel(message: "Remove card")) + ) + } + return TotalsView() + .environment(aggregateModel) +} + +#Preview("Payment Error") { + let aggregateModel = POSPreviewHelpers.makePreviewAggregateModel( + cardPresentPaymentService: CardPresentPaymentPreviewService( + connectionStatus: .connected(CardPresentPaymentCardReader(name: "Reader", batteryLevel: 0.85)) + ) + ) + Task { @MainActor in + aggregateModel.setPreviewState( + paymentState: PointOfSalePaymentState(card: .paymentError, cash: .idle), + inlineMessage: .paymentError(viewModel: PointOfSaleCardPresentPaymentErrorMessageViewModel( + error: NSError(domain: "CardPaymentError", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Card declined"]), + tryPaymentAgainButtonAction: {}, + backToCheckoutButtonAction: {} + )) + ) + } + return TotalsView() + .environment(aggregateModel) +} + #endif diff --git a/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift b/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift index 8945ddf0d33..56ef3ff4281 100644 --- a/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift +++ b/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift @@ -4,13 +4,17 @@ import struct Yosemite.Order import Combine class PointOfSalePreviewOrderController: PointOfSaleOrderControllerProtocol { - var orderState: PointOfSaleInternalOrderState = .loaded( + var orderState: PointOfSaleInternalOrderState + + init(orderState: PointOfSaleInternalOrderState = .loaded( .init(cartTotal: "$10.50", orderTotal: "$12.00", taxTotal: "$1.50", orderTotalDecimal: 12.00), OrderFactory.newOrder(currency: .USD) - ) + )) { + self.orderState = orderState + } func syncOrder(for cart: Cart, retryHandler: @escaping () async -> Void) async -> Result { return .success(.newOrder) diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 054acb658e9..cc2dc21359d 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -138,6 +138,7 @@ 01D082402C5B9EAB007FE81F /* POSBackgroundAppearanceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D0823F2C5B9EAB007FE81F /* POSBackgroundAppearanceKey.swift */; }; 01E62EC82DFADF56003A6D9E /* Cart+BarcodeScanError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E62EC72DFADF4B003A6D9E /* Cart+BarcodeScanError.swift */; }; 01F067ED2D0C5D59001C5805 /* MockLocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F067EC2D0C5D56001C5805 /* MockLocationService.swift */; }; + 01F3D1302E741F3B00D867F1 /* POSCardPresentPaymentMessageViewImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F3D12F2E741F3B00D867F1 /* POSCardPresentPaymentMessageViewImage.swift */; }; 01F42C162CE34AB8003D0A5A /* CardPresentModalTapToPaySuccessEmailSent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F42C152CE34AB3003D0A5A /* CardPresentModalTapToPaySuccessEmailSent.swift */; }; 01F42C182CE34AD2003D0A5A /* CardPresentModalSuccessEmailSent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F42C172CE34AD1003D0A5A /* CardPresentModalSuccessEmailSent.swift */; }; 01F579952C7DE709008BCA28 /* PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F579942C7DE709008BCA28 /* PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift */; }; @@ -3356,6 +3357,7 @@ 01D0823F2C5B9EAB007FE81F /* POSBackgroundAppearanceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSBackgroundAppearanceKey.swift; sourceTree = ""; }; 01E62EC72DFADF4B003A6D9E /* Cart+BarcodeScanError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cart+BarcodeScanError.swift"; sourceTree = ""; }; 01F067EC2D0C5D56001C5805 /* MockLocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLocationService.swift; sourceTree = ""; }; + 01F3D12F2E741F3B00D867F1 /* POSCardPresentPaymentMessageViewImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCardPresentPaymentMessageViewImage.swift; sourceTree = ""; }; 01F42C152CE34AB3003D0A5A /* CardPresentModalTapToPaySuccessEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalTapToPaySuccessEmailSent.swift; sourceTree = ""; }; 01F42C172CE34AD1003D0A5A /* CardPresentModalSuccessEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalSuccessEmailSent.swift; sourceTree = ""; }; 01F579942C7DE709008BCA28 /* PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift; sourceTree = ""; }; @@ -8404,6 +8406,7 @@ 01BD77452C58D0D000147191 /* PointOfSalePaymentSuccessView.swift */, 01BD77472C58D19C00147191 /* PointOfSaleCardPresentPaymentCancelledOnReaderMessageView.swift */, 01BD77492C58D29700147191 /* PointOfSaleCardPresentPaymentDisconnectedMessageView.swift */, + 01F3D12F2E741F3B00D867F1 /* POSCardPresentPaymentMessageViewImage.swift */, ); path = "Reader Messages"; sourceTree = ""; @@ -15514,6 +15517,7 @@ 03E471CA293E0A30001A58AD /* CardPresentModalTapToPayConfigurationProgress.swift in Sources */, 31AD0B1126E9575F000B6391 /* CardPresentModalConnectingFailed.swift in Sources */, 576EA39425264C9B00AFC0B3 /* RefundConfirmationViewModel.swift in Sources */, + 01F3D1302E741F3B00D867F1 /* POSCardPresentPaymentMessageViewImage.swift in Sources */, 208C0F0A2E1FAC1900FE619E /* PointOfSaleBarcodeScannerSetupStepViews.swift in Sources */, 02ED3D272C23315400ED6F3E /* PointOfSaleCardPresentPaymentReaderUpdateFailedView.swift in Sources */, 01BD77482C58D19C00147191 /* PointOfSaleCardPresentPaymentCancelledOnReaderMessageView.swift in Sources */,