Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -629,3 +629,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
Original file line number Diff line number Diff line change
@@ -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.headerSize.width * 0.3,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we move/declare this 0.3 somewhere else? Just to avoid magic numbers

maxWidth: PointOfSaleCardPresentPaymentLayout.headerSize.width,
minHeight: PointOfSaleCardPresentPaymentLayout.headerSize.height * 0.3,
maxHeight: PointOfSaleCardPresentPaymentLayout.headerSize.height
)
.accessibilityHidden(true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -41,6 +41,7 @@ struct PointOfSaleCardPresentPaymentReaderDisconnectedMessageView: View {
.buttonStyle(POSFilledButtonStyle(size: .normal))
.frame(width: width * 0.5)
}
.frame(maxWidth: .infinity)
.measureWidth({ containerWidth in
width = containerWidth
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView: View {
}
}
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.measureWidth({ containerWidth in
width = containerWidth
})
Expand Down
117 changes: 115 additions & 2 deletions WooCommerce/Classes/POS/Presentation/TotalsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ struct TotalsView: View {
)
}
.animation(.default, value: isShowingPaymentView)
.scrollVerticallyIfNeeded()
case .error(.other(let message), let handler):
PointOfSaleOrderSyncErrorMessageView(message: message, retryHandler: handler)
.transition(.opacity)
Expand Down Expand Up @@ -565,8 +566,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
Original file line number Diff line number Diff line change
Expand Up @@ -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<SyncOrderState, Error> {
return .success(.newOrder)
Expand Down
4 changes: 4 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,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 */; };
Expand Down Expand Up @@ -3334,6 +3335,7 @@
01D0823F2C5B9EAB007FE81F /* POSBackgroundAppearanceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSBackgroundAppearanceKey.swift; sourceTree = "<group>"; };
01E62EC72DFADF4B003A6D9E /* Cart+BarcodeScanError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cart+BarcodeScanError.swift"; sourceTree = "<group>"; };
01F067EC2D0C5D56001C5805 /* MockLocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLocationService.swift; sourceTree = "<group>"; };
01F3D12F2E741F3B00D867F1 /* POSCardPresentPaymentMessageViewImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCardPresentPaymentMessageViewImage.swift; sourceTree = "<group>"; };
01F42C152CE34AB3003D0A5A /* CardPresentModalTapToPaySuccessEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalTapToPaySuccessEmailSent.swift; sourceTree = "<group>"; };
01F42C172CE34AD1003D0A5A /* CardPresentModalSuccessEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalSuccessEmailSent.swift; sourceTree = "<group>"; };
01F579942C7DE709008BCA28 /* PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -8362,6 +8364,7 @@
01BD77452C58D0D000147191 /* PointOfSalePaymentSuccessView.swift */,
01BD77472C58D19C00147191 /* PointOfSaleCardPresentPaymentCancelledOnReaderMessageView.swift */,
01BD77492C58D29700147191 /* PointOfSaleCardPresentPaymentDisconnectedMessageView.swift */,
01F3D12F2E741F3B00D867F1 /* POSCardPresentPaymentMessageViewImage.swift */,
);
path = "Reader Messages";
sourceTree = "<group>";
Expand Down Expand Up @@ -15449,6 +15452,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 */,
Expand Down
Loading