diff --git a/WooCommerce/Classes/POS/Presentation/Card Present Payments/Reader Messages/PointOfSaleCardPresentPaymentCaptureErrorMessageViewModel.swift b/WooCommerce/Classes/POS/Presentation/Card Present Payments/Reader Messages/PointOfSaleCardPresentPaymentCaptureErrorMessageViewModel.swift index 3b8c28e4120..7018bb2b41a 100644 --- a/WooCommerce/Classes/POS/Presentation/Card Present Payments/Reader Messages/PointOfSaleCardPresentPaymentCaptureErrorMessageViewModel.swift +++ b/WooCommerce/Classes/POS/Presentation/Card Present Payments/Reader Messages/PointOfSaleCardPresentPaymentCaptureErrorMessageViewModel.swift @@ -5,7 +5,6 @@ final class PointOfSaleCardPresentPaymentCaptureErrorMessageViewModel: Observabl let id = UUID() let title = Localization.title let message = Localization.message - let nextStep = Localization.nextStep let tryAgainButtonViewModel: CardPresentPaymentsModalButtonViewModel let newOrderButtonViewModel: CardPresentPaymentsModalButtonViewModel @@ -29,7 +28,6 @@ final class PointOfSaleCardPresentPaymentCaptureErrorMessageViewModel: Observabl lhs.id == rhs.id && lhs.title == rhs.title && lhs.message == rhs.message && - lhs.nextStep == rhs.nextStep && lhs.tryAgainButtonViewModel == rhs.tryAgainButtonViewModel && lhs.newOrderButtonViewModel == rhs.newOrderButtonViewModel && lhs.showsInfoSheet == rhs.showsInfoSheet @@ -46,19 +44,14 @@ private extension PointOfSaleCardPresentPaymentCaptureErrorMessageViewModel { ) static let message = NSLocalizedString( - "pointOfSale.cardPresent.paymentCaptureError.unable.to.confirm.message", - value: "Due to a network error, we’re unable to confirm that the payment succeeded.", + "pointOfSale.cardPresent.paymentCaptureError.unable.to.confirm.message.1", + value: "Due to a network error, we’re unable to confirm that the payment succeeded. " + + "Verify payment on a device with a working network connection. If unsuccessful, retry the payment. " + + "If successful, start a new order.", comment: "Error message. Presented to users after collecting a payment fails from payment capture error " + "on the Point of Sale Checkout" ) - static let nextStep = NSLocalizedString( - "pointOfSale.cardPresent.paymentCaptureError.nextSteps", - value: "Verify payment on a device with a working network connection. If unsuccessful, retry the payment. " + - "If successful, start a new order.", - comment: "Next steps hint for what to do after seeing a payment capture error message. Presented to users " + - "after collecting a payment fails from payment capture error on the Point of Sale Checkout") - static let tryPaymentAgain = NSLocalizedString( "pointOfSale.cardPresent.paymentCaptureError.tryPaymentAgain.button.title", value: "Try payment again", diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentCaptureErrorMessageView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentCaptureErrorMessageView.swift index 6fcdb61f136..0592175ec32 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentCaptureErrorMessageView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentCaptureErrorMessageView.swift @@ -26,14 +26,12 @@ struct PointOfSaleCardPresentPaymentCaptureErrorMessageView: View { .font(.posHeadingBold) .matchedGeometryEffect(id: animation.titleTransitionId, in: animation.namespace, properties: .position) - VStack(alignment: .center, spacing: PointOfSaleCardPresentPaymentLayout.textSpacing) { - Text(viewModel.message) - Text(viewModel.nextStep) - } - .font(.posBodyLargeRegular()) - .foregroundStyle(Color.posOnSurface) - .matchedGeometryEffect(id: animation.messageTransitionId, in: animation.namespace, properties: .position) + Text(viewModel.message) + .font(.posBodyLargeRegular()) + .foregroundStyle(Color.posOnSurface) + .matchedGeometryEffect(id: animation.messageTransitionId, in: animation.namespace, properties: .position) } + .dynamicWidthScaling(containerWidth: width) Spacer() .frame(height: PointOfSaleCardPresentPaymentLayout.textAndButtonSpacing) @@ -48,9 +46,10 @@ struct PointOfSaleCardPresentPaymentCaptureErrorMessageView: View { } .buttonStyle(POSOutlinedButtonStyle(size: .normal)) } + .dynamicWidthScaling(containerWidth: width) } .multilineTextAlignment(.center) - .frame(maxWidth: PointOfSaleCardPresentPaymentLayout.errorContentMaxWidth) + .frame(maxWidth: .infinity, maxHeight: .infinity) .posModal(isPresented: $viewModel.showsInfoSheet) { PointOfSaleCardPresentPaymentCaptureFailedView(isPresented: $viewModel.showsInfoSheet) } diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentErrorMessageView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentErrorMessageView.swift index 0db19f697a5..ddda631e26a 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentErrorMessageView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentErrorMessageView.swift @@ -8,6 +8,8 @@ struct PointOfSaleCardPresentPaymentErrorMessageView: View { var body: some View { VStack(alignment: .center, spacing: POSSpacing.none) { + Spacer() + POSErrorXMark() .matchedGeometryEffect(id: animation.iconTransitionId, in: animation.namespace, properties: .position) @@ -26,6 +28,7 @@ struct PointOfSaleCardPresentPaymentErrorMessageView: View { .foregroundStyle(Color.posOnSurface) .matchedGeometryEffect(id: animation.messageTransitionId, in: animation.namespace, properties: .position) } + .dynamicWidthScaling(containerWidth: width) Spacer() .frame(height: PointOfSaleCardPresentPaymentLayout.textAndButtonSpacing) @@ -41,9 +44,12 @@ struct PointOfSaleCardPresentPaymentErrorMessageView: View { .buttonStyle(POSOutlinedButtonStyle(size: .normal)) } } + .dynamicWidthScaling(containerWidth: width) + + Spacer() } .multilineTextAlignment(.center) - .frame(maxWidth: PointOfSaleCardPresentPaymentLayout.errorContentMaxWidth) + .frame(maxWidth: .infinity, maxHeight: .infinity) .measureWidth({ containerWidth in width = containerWidth }) diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentIntentCreationErrorMessageView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentIntentCreationErrorMessageView.swift index a6e80ef9f7c..5491b080815 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentIntentCreationErrorMessageView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentIntentCreationErrorMessageView.swift @@ -41,9 +41,10 @@ struct PointOfSaleCardPresentPaymentIntentCreationErrorMessageView: View { .buttonStyle(POSOutlinedButtonStyle(size: .normal)) } } + .dynamicWidthScaling(containerWidth: width) } .multilineTextAlignment(.center) - .frame(maxWidth: PointOfSaleCardPresentPaymentLayout.errorContentMaxWidth) + .frame(maxWidth: .infinity, maxHeight: .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 91b40c53ba8..eba0890b7a4 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 @@ -7,5 +7,4 @@ enum PointOfSaleCardPresentPaymentLayout { static let textSpacing: CGFloat = POSSpacing.small static let buttonSpacing: CGFloat = POSSpacing.large static let horizontalPadding: CGFloat = POSPadding.xxLarge - static let errorContentMaxWidth: CGFloat = 604 } diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentNonRetryableErrorMessageView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentNonRetryableErrorMessageView.swift index d4b48a962e6..605f34a9c1e 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentNonRetryableErrorMessageView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentNonRetryableErrorMessageView.swift @@ -8,6 +8,8 @@ struct PointOfSaleCardPresentPaymentNonRetryableErrorMessageView: View { var body: some View { VStack(alignment: .center, spacing: POSSpacing.none) { + Spacer() + POSErrorXMark() .matchedGeometryEffect(id: animation.iconTransitionId, in: animation.namespace, properties: .position) @@ -29,6 +31,7 @@ struct PointOfSaleCardPresentPaymentNonRetryableErrorMessageView: View { .foregroundStyle(Color.posOnSurface) .matchedGeometryEffect(id: animation.messageTransitionId, in: animation.namespace, properties: .position) } + .dynamicWidthScaling(containerWidth: width) Spacer() .frame(height: PointOfSaleCardPresentPaymentLayout.textAndButtonSpacing) @@ -36,9 +39,12 @@ struct PointOfSaleCardPresentPaymentNonRetryableErrorMessageView: View { Button(viewModel.tryAnotherPaymentMethodButtonViewModel.title, action: viewModel.tryAnotherPaymentMethodButtonViewModel.actionHandler) .buttonStyle(POSFilledButtonStyle(size: .normal)) + .dynamicWidthScaling(containerWidth: width) + + Spacer() } .multilineTextAlignment(.center) - .frame(maxWidth: PointOfSaleCardPresentPaymentLayout.errorContentMaxWidth) + .frame(maxWidth: .infinity, maxHeight: .infinity) .measureWidth({ containerWidth in width = containerWidth }) diff --git a/WooCommerce/Classes/POS/Presentation/Reusable Views/DynamicFrameScaler.swift b/WooCommerce/Classes/POS/Presentation/Reusable Views/DynamicFrameScaler.swift new file mode 100644 index 00000000000..0a7141bbef3 --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/Reusable Views/DynamicFrameScaler.swift @@ -0,0 +1,43 @@ +import SwiftUI + +/// A view modifier that adjusts the width multiplier based on dynamic type size. +struct DynamicWidthScaler: ViewModifier { + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + let containerWidth: CGFloat + let threshold: DynamicTypeSize + let smallSizeScale: CGFloat + let largeSizeScale: CGFloat + + func body(content: Content) -> some View { + content + .frame(width: containerWidth * widthMultiplier) + } + + /// Returns a width multiplier based on the current dynamic type size + private var widthMultiplier: CGFloat { + dynamicTypeSize >= threshold ? largeSizeScale : smallSizeScale + } +} + +extension View { + /// Applies a dynamic width scaling based on the current dynamic type size. + /// - Parameters: + /// - containerWidth: The container width to scale. + /// - threshold: The dynamic type size threshold at which to switch from small to large scale (default: .accessibility2). + /// - smallSizeScale: The scale factor to apply for sizes smaller than the threshold (default: 0.5). + /// - largeSizeScale: The scale factor to apply for sizes larger than or equal to the threshold (default: 1.0). + /// - Returns: A view with dynamically scaled width + func dynamicWidthScaling( + containerWidth: CGFloat, + threshold: DynamicTypeSize = .accessibility2, + smallSizeScale: CGFloat = 0.5, + largeSizeScale: CGFloat = 1.0 + ) -> some View { + modifier(DynamicWidthScaler( + containerWidth: containerWidth, + threshold: threshold, + smallSizeScale: smallSizeScale, + largeSizeScale: largeSizeScale + )) + } +} diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index a1041c1af2f..ae434d7e935 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -282,12 +282,6 @@ private extension TotalsView { topPadding: POSPadding.xxLarge, bottomPadding: POSPadding.xxLarge ) - - static let topAligned = PaymentViewLayout( - backgroundColor: .clear, - topPadding: 96, - bottomPadding: 96 - ) } private var isShowingPaymentView: Bool { @@ -323,7 +317,10 @@ private extension TotalsView { case .validatingOrderError: return .outlined case .paymentError: - return .topAligned + return PaymentViewLayout(backgroundColor: backgroundColor, + topPadding: POSPadding.none, + bottomPadding: POSPadding.none, + sidePadding: POSPadding.none) case .cardPaymentSuccessful: return PaymentViewLayout(backgroundColor: backgroundColor, topPadding: POSPadding.none, diff --git a/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift b/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift index 3cdaa79a7b7..c75c1d3116e 100644 --- a/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift +++ b/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift @@ -67,7 +67,7 @@ final class TotalsViewHelper { func shouldApplyPadding(paymentState: PointOfSalePaymentState) -> Bool { switch paymentState { - case .card(.cardPaymentSuccessful), .cash(.paymentSuccess), .cash(.collectingCash): + case .card(.cardPaymentSuccessful), .cash(.paymentSuccess), .cash(.collectingCash), .card(.paymentError): return false default: return true diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index a43a92487bb..873b42b8156 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -146,6 +146,7 @@ 0216272B2379662C000208D2 /* DefaultProductFormTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0216272A2379662C000208D2 /* DefaultProductFormTableViewModel.swift */; }; 0218B4EC242E06F00083A847 /* MediaType+WPMediaType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0218B4EB242E06F00083A847 /* MediaType+WPMediaType.swift */; }; 0219B03723964527007DCD5E /* PaginatedProductShippingClassListSelectorDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0219B03623964527007DCD5E /* PaginatedProductShippingClassListSelectorDataSource.swift */; }; + 021A17212D7036AF006DF7C0 /* DynamicFrameScaler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021A17202D7036AF006DF7C0 /* DynamicFrameScaler.swift */; }; 021A84E0257DFC2A00BC71D1 /* RefundShippingLabelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021A84DE257DFC2A00BC71D1 /* RefundShippingLabelViewController.swift */; }; 021A84E1257DFC2A00BC71D1 /* RefundShippingLabelViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 021A84DF257DFC2A00BC71D1 /* RefundShippingLabelViewController.xib */; }; 021AC6662AF3432300E7FB97 /* ConfigurableBundleProductViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021AC6652AF3432300E7FB97 /* ConfigurableBundleProductViewModelTests.swift */; }; @@ -3362,6 +3363,7 @@ 0216272A2379662C000208D2 /* DefaultProductFormTableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultProductFormTableViewModel.swift; sourceTree = ""; }; 0218B4EB242E06F00083A847 /* MediaType+WPMediaType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaType+WPMediaType.swift"; sourceTree = ""; }; 0219B03623964527007DCD5E /* PaginatedProductShippingClassListSelectorDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginatedProductShippingClassListSelectorDataSource.swift; sourceTree = ""; }; + 021A17202D7036AF006DF7C0 /* DynamicFrameScaler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFrameScaler.swift; sourceTree = ""; }; 021A84DE257DFC2A00BC71D1 /* RefundShippingLabelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundShippingLabelViewController.swift; sourceTree = ""; }; 021A84DF257DFC2A00BC71D1 /* RefundShippingLabelViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefundShippingLabelViewController.xib; sourceTree = ""; }; 021AC6652AF3432300E7FB97 /* ConfigurableBundleProductViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurableBundleProductViewModelTests.swift; sourceTree = ""; }; @@ -6512,6 +6514,7 @@ 027ADB742D218A8D009608DB /* POSItemCardBorderStylesModifier.swift */, 0295736A2D62B93300865E27 /* POSPageHeaderView.swift */, 0295CDBF2D6477C400865E27 /* POSNoticeView.swift */, + 021A17202D7036AF006DF7C0 /* DynamicFrameScaler.swift */, ); path = "Reusable Views"; sourceTree = ""; @@ -15994,6 +15997,7 @@ CECC759723D607C900486676 /* OrderItemRefund+Woo.swift in Sources */, AEFF77A629783CA600667F7A /* PriceInputViewModel.swift in Sources */, 0212275C244972660042161F /* BottomSheetListSelectorSectionHeaderView.swift in Sources */, + 021A17212D7036AF006DF7C0 /* DynamicFrameScaler.swift in Sources */, DEC6C51A2747758D006832D3 /* JCPJetpackInstallView.swift in Sources */, DE37517C28DC5FC6000163CB /* Authenticator.swift in Sources */, AED089F227C794BC0020AE10 /* View+CurrencySymbol.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Card Present Payments/PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/Card Present Payments/PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift index 645f4226dfa..15cd4a21062 100644 --- a/WooCommerce/WooCommerceTests/POS/Card Present Payments/PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Card Present Payments/PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift @@ -5,7 +5,7 @@ final class PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests: XCTe func test_manual_equatable_conformance_number_of_properties_unchanged() { let sut = PointOfSaleCardPresentPaymentCaptureErrorMessageViewModel(tryAgainButtonAction: {}, newOrderButtonAction: {}) XCTAssertPropertyCount(sut, - expectedCount: 7, + expectedCount: 6, messageHint: "Please check that the manual equatable conformance includes new properties.") } }