From 7014c51d52918688adf43cd3f66a2701225c4d8d Mon Sep 17 00:00:00 2001 From: Spencer Transier Date: Mon, 11 Nov 2024 14:39:19 -0800 Subject: [PATCH 01/49] Remove usages of `BUILDKITE_` prefixed env vars --- .buildkite/commands/checkout-release-branch.sh | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh index 335e4e876e7..86d00a733b0 100755 --- a/.buildkite/commands/checkout-release-branch.sh +++ b/.buildkite/commands/checkout-release-branch.sh @@ -2,16 +2,8 @@ echo "--- :git: Checkout Release Branch" -# Note: `BUILDKITE_RELEASE_VERSION` is the legacy environment variable passed to Buildkite by ReleaseV2. -# It used the `BUILDKITE_` prefix so it was not filtered out when passed to the MacOS VMs, due to how `hostmgr` works. -# This is considered legacy: we should eventually remove all use of custom `BUILDKITE_` variables, and instead -# resolve the value of those sooner (i.e. in the YML pipeline) then pass it as parameter to the `.sh` calls instead. - -# Use the provided argument if there's one, otherwise fall back to the legacy BUILDKITE_RELEASE_VERSION -RELEASE_VERSION=${1:-$BUILDKITE_RELEASE_VERSION} - if [[ -z "${RELEASE_VERSION}" ]]; then - echo "RELEASE_VERSION is not set and BUILDKITE_RELEASE_VERSION is not available." + echo "RELEASE_VERSION is not set." exit 1 fi From fcd31a5d0bf6167f232444388278022d22c8b5df Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 12 Nov 2024 13:39:03 +0900 Subject: [PATCH 02/49] Set isLastPage when no new items are fetched from remote --- .../Models/PointOfSaleAggregateModel.swift | 8 ++++ .../POS/Presentation/ItemListView.swift | 4 +- .../PointOfSaleAggregateModelTests.swift | 47 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index f080683346c..7b1147a1150 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -20,6 +20,7 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt private let itemProvider: POSItemProvider private var currentPage: Int = Constants.initialPage + @Published private(set) var isLastPage: Bool = false init(itemProvider: POSItemProvider) { self.itemProvider = itemProvider @@ -52,6 +53,7 @@ extension PointOfSaleAggregateModel { func reload() async { allItems.removeAll() currentPage = Constants.initialPage + isLastPage = false itemListState = .loading(allItems) try? await load(pageNumber: currentPage) } @@ -73,6 +75,12 @@ extension PointOfSaleAggregateModel { !allItems.contains(where: { $0.productID == newItem.productID }) } + if uniqueNewItems.count == 0 { + isLastPage = true + } else { + isLastPage = false + } + allItems.append(contentsOf: uniqueNewItems) if allItems.count == 0 { diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index 628e0820524..0abb509992b 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -134,7 +134,7 @@ private extension ItemListView { }) } GhostItemCardView() - .renderedIf(posModel.itemListState.isLoadingAfterInitialLoad) + .renderedIf(posModel.itemListState.isLoadingAfterInitialLoad && !posModel.isLastPage) } .frame(maxWidth: .infinity) .padding(.bottom, floatingControlAreaSize.height) @@ -142,7 +142,7 @@ private extension ItemListView { .background(GeometryReader { proxy in Color.clear .onChange(of: proxy.frame(in: .global).maxY) { maxY in - if posModel.itemListState.isLoadingAfterInitialLoad { + if posModel.itemListState.isLoadingAfterInitialLoad, posModel.isLastPage { return } let viewHeight = UIScreen.main.bounds.height diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index d40038f691d..344d44fd04a 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -262,4 +262,51 @@ struct PointOfSaleAggregateModelTests { // Then #expect(sut.itemListState == .error(expectedError)) } + + @Test func itemListViewModel_when_initialized_then_isLastPage_is_false() async throws { + // Given/then + try #require(sut.isLastPage == false) + } + + @Test func itemListViewModel_when_there_are_no_items_then_isLastPage_is_true_() async throws { + // Given + let itemProvider = MockPOSItemProvider() + itemProvider.shouldReturnZeroItems = true + let sut = PointOfSaleAggregateModel(itemProvider: itemProvider) + + try #require(sut.isLastPage == false) + + // When + await sut.loadInitialItems() + + // Then + try #require(sut.isLastPage == true) + + } + + @Test func itemListViewModel_when_reload_is_invoked_resets_isLastPage_to_false() async throws { + // Given + try #require(sut.isLastPage == false) + + // When + await sut.reload() + + // Then + try #require(sut.isLastPage == false) + } + + @Test func itemListViewModel_when_loadNextItems_is_invoked_then_isLastPage_is_true() async throws { + // Given + let initialItems = MockPOSItemProvider.makeInitialItems() + itemProvider.items = initialItems + itemProvider.shouldSimulateTwoPages = true + + try #require(sut.isLastPage == false) + + // When + await sut.loadNextItems() + + // Then + try #require(sut.isLastPage == false) + } } From 2516ea76dc0822b86d5d50529167de4eeedf8936 Mon Sep 17 00:00:00 2001 From: Spencer Transier Date: Tue, 12 Nov 2024 10:27:46 -0800 Subject: [PATCH 03/49] Re-add RELEASE_VERSION assignment --- .buildkite/commands/checkout-release-branch.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh index 86d00a733b0..f00ab2255f3 100755 --- a/.buildkite/commands/checkout-release-branch.sh +++ b/.buildkite/commands/checkout-release-branch.sh @@ -1,5 +1,7 @@ #!/bin/bash -eu +RELEASE_VERSION="${1:?RELEASE_VERSION parameter missing}" + echo "--- :git: Checkout Release Branch" if [[ -z "${RELEASE_VERSION}" ]]; then From 5f649bfcd4fb0e434a6efddf59707693802f6b0f Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 13 Nov 2024 20:15:32 +0900 Subject: [PATCH 04/49] throw pageIsOutOfRange error to stop fetching --- .../Models/PointOfSaleAggregateModel.swift | 27 ++++++++------- .../POS/Presentation/ItemListView.swift | 15 +++++---- .../PointOfSale/POSProductProvider.swift | 33 ++++++++++--------- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 7b1147a1150..62cee3932df 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -2,6 +2,7 @@ import Foundation import protocol Yosemite.POSItem import protocol Yosemite.POSItemProvider +import enum Yosemite.POSProductProviderError protocol PointOfSaleAggregateModelProtocol { @available(*, deprecated, message: "`allItems` is due for removal, use `itemListState` instead.") @@ -20,7 +21,7 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt private let itemProvider: POSItemProvider private var currentPage: Int = Constants.initialPage - @Published private(set) var isLastPage: Bool = false + private var pageIsOutOfRange: Bool = false init(itemProvider: POSItemProvider) { self.itemProvider = itemProvider @@ -31,6 +32,7 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt extension PointOfSaleAggregateModel { @MainActor func loadInitialItems() async { + pageIsOutOfRange = false itemListState = .initialLoading try? await load(pageNumber: Constants.initialPage) } @@ -38,12 +40,22 @@ extension PointOfSaleAggregateModel { @MainActor func loadNextItems() async { do { + guard !pageIsOutOfRange else { + return + } itemListState = .loading(allItems) - // TODO: Optimize API calls. gh-14186 - // If there are no more pages to fetch, we can avoid the next call. + let nextPage = currentPage + 1 try await load(pageNumber: nextPage) + pageIsOutOfRange = false currentPage = nextPage + } catch POSProductProviderError.pageOutOfRange { + if allItems.count == 0 { + itemListState = .empty + } else { + itemListState = .loaded(allItems) + } + pageIsOutOfRange = true } catch { // No need to do anything; this avoids us incorrectly incrementing currentPage. } @@ -53,7 +65,7 @@ extension PointOfSaleAggregateModel { func reload() async { allItems.removeAll() currentPage = Constants.initialPage - isLastPage = false + pageIsOutOfRange = false itemListState = .loading(allItems) try? await load(pageNumber: currentPage) } @@ -74,13 +86,6 @@ extension PointOfSaleAggregateModel { let uniqueNewItems = newItems.filter { newItem in !allItems.contains(where: { $0.productID == newItem.productID }) } - - if uniqueNewItems.count == 0 { - isLastPage = true - } else { - isLastPage = false - } - allItems.append(contentsOf: uniqueNewItems) if allItems.count == 0 { diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index e0ab2ec4979..c5b4c2d4791 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -8,6 +8,8 @@ struct ItemListView: View { @EnvironmentObject var posModel: PointOfSaleAggregateModel + @State private var lastScrollPosition: CGFloat = 0 + init(viewModel: ItemListViewModel) { self.viewModel = viewModel } @@ -133,9 +135,7 @@ private extension ItemListView { }) } GhostItemCardView() - .renderedIf(posModel.itemListState.isLoadingAfterInitialLoad && - !posModel.isLastPage && - viewModel.shouldShowGhostableItemCard) + .renderedIf(posModel.itemListState.isLoadingAfterInitialLoad) } .frame(maxWidth: .infinity) .padding(.bottom, floatingControlAreaSize.height) @@ -143,15 +143,16 @@ private extension ItemListView { .background(GeometryReader { proxy in Color.clear .onChange(of: proxy.frame(in: .global).maxY) { maxY in - if posModel.itemListState.isLoadingAfterInitialLoad, posModel.isLastPage { + if posModel.itemListState.isLoadingAfterInitialLoad { return } - let viewHeight = UIScreen.main.bounds.height - if maxY < viewHeight { + let threshold = Constants.viewHeight * Constants.scrollThresholdMultiplier + if maxY < threshold && maxY < lastScrollPosition { Task { await viewModel.loadNextItems() } } + lastScrollPosition = maxY } }) } @@ -212,6 +213,8 @@ private extension ItemListView { static let iconPadding: CGFloat = 26 static let itemListPadding: CGFloat = 16 static let bannerCardPadding: CGFloat = 16 + static let viewHeight: CGFloat = UIScreen.main.bounds.height + static let scrollThresholdMultiplier: CGFloat = 1.7 } enum Localization { diff --git a/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift b/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift index 24b148bff03..eca1795aeac 100644 --- a/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift +++ b/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift @@ -5,6 +5,12 @@ import class Networking.AlamofireNetwork import class WooFoundation.CurrencyFormatter import class WooFoundation.CurrencySettings +public enum POSProductProviderError: Error { + case requestFailed + case pageOutOfRange + case unknown +} + /// Product provider for the Point of Sale feature /// public final class POSProductProvider: POSItemProvider { @@ -32,25 +38,20 @@ public final class POSProductProvider: POSItemProvider { /// - pageNumber: Number of the page that should be retrieved. If none given, defaults to 1 /// public func providePointOfSaleItems(pageNumber: Int = 1) async throws -> [POSItem] { - do { - let products = try await productsRemote.loadSimpleProductsForPointOfSale(for: siteID, pageNumber: pageNumber) + let products = try await productsRemote.loadSimpleProductsForPointOfSale(for: siteID, pageNumber: pageNumber) - let eligibilityCriteria: [(Product) -> Bool] = [ - isNotVirtual, - isNotDownloadable, - hasPrice - ] - let filteredProducts = filterProducts(products: products, using: eligibilityCriteria) - - return mapProductsToPOSItems(products: filteredProducts) - } catch { - DDLogError("Failed to retrieve products. Error: \(error)") - throw POSProductProviderError.requestFailed + if products.count == 0 { + throw POSProductProviderError.pageOutOfRange } - } - enum POSProductProviderError: Error { - case requestFailed + let eligibilityCriteria: [(Product) -> Bool] = [ + isNotVirtual, + isNotDownloadable, + hasPrice + ] + let filteredProducts = filterProducts(products: products, using: eligibilityCriteria) + + return mapProductsToPOSItems(products: filteredProducts) } // Maps result to POSProduct, and populate the output with: From 22b37111f7100ada702c3cbffdc3a5f3065ee54b Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:22:47 +0200 Subject: [PATCH 05/49] Move email receipt handling into EmailReceiptAction enum --- ...entPaymentsTransactionAlertsProvider.swift | 5 ++-- ...toothCardReaderPaymentAlertsProvider.swift | 12 ++++----- ...iltInCardReaderPaymentAlertsProvider.swift | 27 +++++++++---------- ...erTransactionAlertEmailReceiptAction.swift | 18 +++++++++++++ ...CardReaderTransactionAlertsProviding.swift | 5 ++-- .../CollectOrderPaymentUseCase.swift | 11 ++++---- .../WooCommerce.xcodeproj/project.pbxproj | 4 +++ 7 files changed, 51 insertions(+), 31 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsTransactionAlertsProvider.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsTransactionAlertsProvider.swift index 6ed8f20dd1e..464991f278d 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsTransactionAlertsProvider.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsTransactionAlertsProvider.swift @@ -29,9 +29,8 @@ struct CardPresentPaymentsTransactionAlertsProvider: CardReaderTransactionAlerts } func success(printReceipt: @escaping () -> Void, - emailReceipt: @escaping () -> Void, - noReceiptAction: @escaping () -> Void, - email: String?) -> CardPresentPaymentEventDetails { + emailReceipt: CardReaderTransactionAlertEmailReceiptAction, + noReceiptAction: @escaping () -> Void) -> CardPresentPaymentEventDetails { .paymentSuccess(done: noReceiptAction) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift index 63888e6554d..e8a39b52e0f 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift @@ -47,18 +47,18 @@ final class BluetoothCardReaderPaymentAlertsProvider: CardReaderTransactionAlert } func success(printReceipt: @escaping () -> Void, - emailReceipt: @escaping () -> Void, - noReceiptAction: @escaping () -> Void, - email: String?) -> CardPresentPaymentsModalViewModel { - if let email = email, email.isNotEmpty { + emailReceipt: CardReaderTransactionAlertEmailReceiptAction, + noReceiptAction: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + switch emailReceipt { + case let .emailSent(email): return CardPresentModalSuccessEmailSent(printReceipt: printReceipt, noReceiptAction: noReceiptAction, email: email) - } else if MFMailComposeViewController.canSendMail() { + case let .sendEmail(emailReceipt): return CardPresentModalSuccess(printReceipt: printReceipt, emailReceipt: emailReceipt, noReceiptAction: noReceiptAction) - } else { + case .noEmail: return CardPresentModalSuccessWithoutEmail(printReceipt: printReceipt, noReceiptAction: noReceiptAction) } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift index 630b6c8fb14..9e39473511e 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift @@ -42,20 +42,19 @@ final class BuiltInCardReaderPaymentAlertsProvider: CardReaderTransactionAlertsP } func success(printReceipt: @escaping () -> Void, - emailReceipt: @escaping () -> Void, - noReceiptAction: @escaping () -> Void, - email: String?) -> CardPresentPaymentsModalViewModel { - if let email = email, email.isNotEmpty { - return CardPresentModalBuiltInSuccessEmailSent(printReceipt: printReceipt, - noReceiptAction: noReceiptAction, - email: email) - } else if MFMailComposeViewController.canSendMail() { - return CardPresentModalBuiltInSuccess(printReceipt: printReceipt, - emailReceipt: emailReceipt, - noReceiptAction: noReceiptAction) - } else { - return CardPresentModalBuiltInSuccessWithoutEmail(printReceipt: printReceipt, - noReceiptAction: noReceiptAction) + emailReceipt: CardReaderTransactionAlertEmailReceiptAction, + noReceiptAction: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + switch emailReceipt { + case let .emailSent(email): + return CardPresentModalSuccessEmailSent(printReceipt: printReceipt, + noReceiptAction: noReceiptAction, + email: email) + case let .sendEmail(emailReceipt): + return CardPresentModalSuccess(printReceipt: printReceipt, + emailReceipt: emailReceipt, + noReceiptAction: noReceiptAction) + case .noEmail: + return CardPresentModalSuccessWithoutEmail(printReceipt: printReceipt, noReceiptAction: noReceiptAction) } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift new file mode 100644 index 00000000000..7e81b8f1792 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift @@ -0,0 +1,18 @@ +import Foundation +import MessageUI + +enum CardReaderTransactionAlertEmailReceiptAction { + case emailSent(String) + case sendEmail(() -> Void) + case noEmail + + init(email: String? = nil, callback: @escaping () -> Void) { + if let email = email, email.isNotEmpty { + self = .emailSent(email) + } else if MFMailComposeViewController.canSendMail() { + self = .sendEmail(callback) + } else { + self = .noEmail + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift index 5b055747abd..c66a2de680d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift @@ -33,9 +33,8 @@ protocol CardReaderTransactionAlertsProviding { /// An alert to display successful transaction and provide options related to receipts /// func success(printReceipt: @escaping () -> Void, - emailReceipt: @escaping () -> Void, - noReceiptAction: @escaping () -> Void, - email: String?) -> AlertDetails + emailReceipt: CardReaderTransactionAlertEmailReceiptAction, + noReceiptAction: @escaping () -> Void) -> AlertDetails /// An alert to display a retriable and cancellable error /// diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index 3ec066ea432..1a76753f1a2 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -566,10 +566,11 @@ private extension CollectOrderPaymentUseCase { }) } // Presents receipt alert + alertsPresenter.present(viewModel: paymentAlerts.success(printReceipt: receiptPresentationCompletionAction, - emailReceipt: receiptPresentationCompletionAction, - noReceiptAction: { onCompleted() }, - email: order.billingAddress?.email)) + emailReceipt: .init(email: order.billingAddress?.email, + callback: receiptPresentationCompletionAction), + noReceiptAction: { onCompleted() })) } /// Allow merchants to print or email locally-generated receipts. @@ -598,7 +599,7 @@ private extension CollectOrderPaymentUseCase { // Inform about flow completion. onCompleted() } - }, emailReceipt: { [order, analyticsTracker, paymentOrchestrator, weak self] in + }, emailReceipt: .init { [order, analyticsTracker, paymentOrchestrator, weak self] in guard let self = self else { return } analyticsTracker.trackEmailTapped() @@ -616,7 +617,7 @@ private extension CollectOrderPaymentUseCase { }, noReceiptAction: { // Inform about flow completion. onCompleted() - }, email: nil)) + })) } /// Presents the native email client with the provided content. diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 6fce253aba3..71c5a070843 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 016BCAFF2C4F907F009D8367 /* CartViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016BCAFE2C4F907F009D8367 /* CartViewModelProtocol.swift */; }; 016C6B972C74AB17000D86FD /* POSConnectivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016C6B962C74AB17000D86FD /* POSConnectivityView.swift */; }; 0182C8BE2CE3B11300474355 /* MockReceiptEligibilityUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182C8BD2CE3B10E00474355 /* MockReceiptEligibilityUseCase.swift */; }; + 0182C8C02CE4DDC700474355 /* CardReaderTransactionAlertEmailReceiptAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertEmailReceiptAction.swift */; }; 0188CA0F2C65622A0051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0188CA0E2C65622A0051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift */; }; 0188CA112C6565320051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0188CA102C6565320051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift */; }; 018D5C7E2CA6B4A60085EBEE /* CurrencySettings+Sanitized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018D5C7D2CA6B49D0085EBEE /* CurrencySettings+Sanitized.swift */; }; @@ -3146,6 +3147,7 @@ 016BCAFE2C4F907F009D8367 /* CartViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartViewModelProtocol.swift; sourceTree = ""; }; 016C6B962C74AB17000D86FD /* POSConnectivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSConnectivityView.swift; sourceTree = ""; }; 0182C8BD2CE3B10E00474355 /* MockReceiptEligibilityUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockReceiptEligibilityUseCase.swift; sourceTree = ""; }; + 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertEmailReceiptAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderTransactionAlertEmailReceiptAction.swift; sourceTree = ""; }; 0188CA0E2C65622A0051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift; sourceTree = ""; }; 0188CA102C6565320051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift; sourceTree = ""; }; 018D5C7D2CA6B49D0085EBEE /* CurrencySettings+Sanitized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrencySettings+Sanitized.swift"; sourceTree = ""; }; @@ -8526,6 +8528,7 @@ 311D21EC264AF0E700102316 /* CardReaderSettingsAlerts.swift */, 03E471BF293A158C001A58AD /* CardReaderConnectionAlertsProviding.swift */, 03E471CF293FA62B001A58AD /* CardReaderTransactionAlertsProviding.swift */, + 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertEmailReceiptAction.swift */, 03E471D1293FA8B2001A58AD /* BluetoothCardReaderPaymentAlertsProvider.swift */, 03E471C1293A1F6B001A58AD /* BluetoothReaderConnectionAlertsProvider.swift */, 03E471C3293A1F8D001A58AD /* BuiltInReaderConnectionAlertsProvider.swift */, @@ -16256,6 +16259,7 @@ DE19BB1A26C3B5DC00AB70D9 /* ShippingLabelCustomsFormItemDetailsViewModel.swift in Sources */, 2023E2AE2C21D8EA00FC365A /* PointOfSaleCardPresentPaymentInLineMessage.swift in Sources */, B6F3796C293794A000718561 /* AnalyticsHubYearToDateRangeData.swift in Sources */, + 0182C8C02CE4DDC700474355 /* CardReaderTransactionAlertEmailReceiptAction.swift in Sources */, 2667BFE52530DCF4008099D4 /* RefundItemsValuesCalculationUseCase.swift in Sources */, 203163BB2C1C5F72001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedUpdatePostalCodeView.swift in Sources */, CEC3CC6B2C92FDB700B93FBE /* WooShippingItemRowViewModel.swift in Sources */, From 7c0d2608e325cfe47e4cadff85e57320c58349fb Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:28:48 +0200 Subject: [PATCH 06/49] Update CollectOrderPaymentUseCase structure to support a new sending email via API action --- .../Receipts/ReceiptEligibilityUseCase.swift | 6 +++++ ...erTransactionAlertEmailReceiptAction.swift | 6 ++--- .../CollectOrderPaymentUseCase.swift | 26 +++++++++++++++---- .../Mocks/MockReceiptEligibilityUseCase.swift | 5 ++++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift b/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift index 2d38418cd60..9462cc8b7d9 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift @@ -3,6 +3,7 @@ import Experiments protocol ReceiptEligibilityUseCaseProtocol { func isEligibleForBackendReceipts(onCompletion: @escaping (Bool) -> Void) + func isEligibleSendingReceiptAfterPayment(onCompletion: @escaping (Bool) -> Void) } final class ReceiptEligibilityUseCase: ReceiptEligibilityUseCaseProtocol { @@ -42,6 +43,11 @@ final class ReceiptEligibilityUseCase: ReceiptEligibilityUseCaseProtocol { } stores.dispatch(action) } + + func isEligibleSendingReceiptAfterPayment(onCompletion: @escaping (Bool) -> Void) { + // TODO: WooCommerce 9.5.0 + onCompletion(false) + } } private extension ReceiptEligibilityUseCase { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift index 7e81b8f1792..2a6532ab1d5 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift @@ -6,10 +6,8 @@ enum CardReaderTransactionAlertEmailReceiptAction { case sendEmail(() -> Void) case noEmail - init(email: String? = nil, callback: @escaping () -> Void) { - if let email = email, email.isNotEmpty { - self = .emailSent(email) - } else if MFMailComposeViewController.canSendMail() { + init(callback: @escaping () -> Void) { + if MFMailComposeViewController.canSendMail() { self = .sendEmail(callback) } else { self = .noEmail diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index 1a76753f1a2..7db88e66a93 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -565,12 +565,28 @@ private extension CollectOrderPaymentUseCase { } }) } - // Presents receipt alert + // Sends receipt via API + let addCustomerEmailAndSendReceiptCompletionAction: () -> Void = { + // TODO + } - alertsPresenter.present(viewModel: paymentAlerts.success(printReceipt: receiptPresentationCompletionAction, - emailReceipt: .init(email: order.billingAddress?.email, - callback: receiptPresentationCompletionAction), - noReceiptAction: { onCompleted() })) + // Presents receipt alert + receiptEligibilityUseCase.isEligibleSendingReceiptAfterPayment { isEligibleSendingReceiptAfterPayment in + let emailReceiptAction: CardReaderTransactionAlertEmailReceiptAction + + if let email = self.order.billingAddress?.email, email.isNotEmpty { + emailReceiptAction = .emailSent(email) + } else if isEligibleSendingReceiptAfterPayment { + emailReceiptAction = .sendEmail(addCustomerEmailAndSendReceiptCompletionAction) + } else if MFMailComposeViewController.canSendMail() { + emailReceiptAction = .sendEmail(receiptPresentationCompletionAction) + } else { + emailReceiptAction = .noEmail + } + self.alertsPresenter.present(viewModel: paymentAlerts.success(printReceipt: receiptPresentationCompletionAction, + emailReceipt: emailReceiptAction, + noReceiptAction: { onCompleted() })) + } } /// Allow merchants to print or email locally-generated receipts. diff --git a/WooCommerce/WooCommerceTests/Mocks/MockReceiptEligibilityUseCase.swift b/WooCommerce/WooCommerceTests/Mocks/MockReceiptEligibilityUseCase.swift index 4bfa0a43a51..5d16fcf9f8b 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockReceiptEligibilityUseCase.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockReceiptEligibilityUseCase.swift @@ -2,8 +2,13 @@ final class MockReceiptEligibilityUseCase: ReceiptEligibilityUseCaseProtocol { var isEligibleForBackendReceipts: Bool = true + var isEligibleSendingReceiptAfterPayment: Bool = false func isEligibleForBackendReceipts(onCompletion: @escaping (Bool) -> Void) { onCompletion(isEligibleForBackendReceipts) } + + func isEligibleSendingReceiptAfterPayment(onCompletion: @escaping (Bool) -> Void) { + onCompletion(isEligibleSendingReceiptAfterPayment) + } } From 0cbaf9999071d85f15c66739fce297c796e401d4 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 13 Nov 2024 17:08:00 +0000 Subject: [PATCH 07/49] 14407 Use AggregateModel for reader connection view --- .../Models/PointOfSaleAggregateModel.swift | 32 ++++++++++++++- .../CardReaderConnectionStatusView.swift | 14 +++---- .../CardReaderConnectionViewModel.swift | 40 ------------------- .../Classes/POS/Presentation/CartView.swift | 10 +++-- .../POS/Presentation/ItemListView.swift | 3 +- .../Presentation/POSFloatingControlView.swift | 2 +- .../PointOfSaleDashboardView.swift | 10 +++-- .../PointOfSaleEntryPointView.swift | 4 +- .../PointOfSaleDashboardViewModel.swift | 3 -- .../WooCommerce.xcodeproj/project.pbxproj | 4 -- .../Mocks/MockPointOfSaleAggregateModel.swift | 10 ++++- .../PointOfSaleAggregateModelTests.swift | 13 ++++-- .../POS/ViewModels/CartViewModelTests.swift | 3 +- .../PointOfSaleDashboardViewModelTests.swift | 1 - 14 files changed, 73 insertions(+), 76 deletions(-) delete mode 100644 WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionViewModel.swift diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index a6b47fd601f..3d087f3a317 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -7,10 +7,13 @@ import protocol WooFoundation.Analytics protocol PointOfSaleAggregateModelProtocol { var orderStage: PointOfSaleOrderStage { get } + var cardReaderConnectionStatus: CardPresentPaymentReaderConnectionStatus { get } + func connectCardReader() + func disconnectCardReader() + @available(*, deprecated, message: "`allItems` is due for removal, use `itemListState` instead.") var allItems: [POSItem] { get } var itemListState: ItemListState { get } - func loadInitialItems() async func loadNextItems() async func reload() async @@ -27,20 +30,26 @@ protocol PointOfSaleAggregateModelProtocol { class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProtocol { @Published private(set) var orderStage: PointOfSaleOrderStage = .building + @Published private(set) var cardReaderConnectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected + @Published private(set) var allItems: [POSItem] = [] @Published private(set) var itemListState: ItemListState = .initialLoading @Published private(set) var cart: [CartItem] = [] private let itemProvider: POSItemProvider + private let cardPresentPaymentService: CardPresentPaymentFacade private let analytics: Analytics private var currentPage: Int = Constants.initialPage init(itemProvider: POSItemProvider, + cardPresentPaymentService: CardPresentPaymentFacade, analytics: Analytics = ServiceLocator.analytics) { self.itemProvider = itemProvider + self.cardPresentPaymentService = cardPresentPaymentService self.analytics = analytics + publishCardReaderConnectionStatus() } } @@ -133,6 +142,27 @@ extension PointOfSaleAggregateModel { } } +// MARK: - Card payments + +extension PointOfSaleAggregateModel { + private func publishCardReaderConnectionStatus() { + // When adopting Observable, we can use `assign(to: on:)` here instead + cardPresentPaymentService.readerConnectionStatusPublisher.assign(to: &$cardReaderConnectionStatus) + } + + func connectCardReader() { + Task { @MainActor in + _ = try await cardPresentPaymentService.connectReader(using: .bluetooth) + } + } + + func disconnectCardReader() { + Task { @MainActor in + await cardPresentPaymentService.disconnectReader() + } + } +} + private extension PointOfSaleAggregateModel { enum Constants { static let initialPage: Int = 1 diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift index 8be89e5047b..910d30f8be2 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift @@ -2,14 +2,10 @@ import SwiftUI struct CardReaderConnectionStatusView: View { @Environment(\.posBackgroundAppearance) var backgroundAppearance - @ObservedObject private var connectionViewModel: CardReaderConnectionViewModel + @EnvironmentObject var posModel: PointOfSaleAggregateModel @ScaledMetric private var scale: CGFloat = 1.0 @Environment(\.isEnabled) var isEnabled - init(connectionViewModel: CardReaderConnectionViewModel) { - self.connectionViewModel = connectionViewModel - } - @ViewBuilder private func circleIcon(with color: Color) -> some View { Image(systemName: "circle.fill") @@ -21,11 +17,11 @@ struct CardReaderConnectionStatusView: View { var body: some View { Group { - switch connectionViewModel.connectionStatus { + switch posModel.cardReaderConnectionStatus { case .connected: Menu { Button { - connectionViewModel.disconnectReader() + posModel.disconnectCardReader() } label: { Text(Localization.disconnectCardReader) } @@ -44,7 +40,7 @@ struct CardReaderConnectionStatusView: View { progressIndicatingCardReaderStatus(title: Localization.pleaseWait) case .disconnected: Button { - connectionViewModel.connectReader() + posModel.connectCardReader() } label: { HStack(spacing: Constants.buttonImageAndTextSpacing) { circleIcon(with: Color(.wooCommerceAmber(.shade60))) @@ -162,7 +158,7 @@ private extension CardReaderConnectionStatusView { #Preview { VStack { - CardReaderConnectionStatusView(connectionViewModel: .init(cardPresentPayment: CardPresentPaymentPreviewService())) + CardReaderConnectionStatusView() } } diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionViewModel.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionViewModel.swift deleted file mode 100644 index 7d3909093c2..00000000000 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionViewModel.swift +++ /dev/null @@ -1,40 +0,0 @@ -import SwiftUI - -final class CardReaderConnectionViewModel: ObservableObject { - @Published private(set) var connectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected - private let cardPresentPayment: CardPresentPaymentFacade - - init(cardPresentPayment: CardPresentPaymentFacade) { - self.cardPresentPayment = cardPresentPayment - observeConnectedReaderForStatus() - } - - func connectReader() { - guard connectionStatus == .disconnected else { - return - } - Task { @MainActor in - do { - let _ = try await cardPresentPayment.connectReader(using: .bluetooth) - } catch { - DDLogError("🔴 POS reader connection error: \(error)") - } - } - } - - func disconnectReader() { - guard case .connected = connectionStatus else { - return - } - Task { @MainActor in - await cardPresentPayment.disconnectReader() - } - } -} - -private extension CardReaderConnectionViewModel { - func observeConnectedReaderForStatus() { - cardPresentPayment.readerConnectionStatusPublisher - .assign(to: &$connectionStatus) - } -} diff --git a/WooCommerce/Classes/POS/Presentation/CartView.swift b/WooCommerce/Classes/POS/Presentation/CartView.swift index 99a16727ed9..88d5d0b3102 100644 --- a/WooCommerce/Classes/POS/Presentation/CartView.swift +++ b/WooCommerce/Classes/POS/Presentation/CartView.swift @@ -256,10 +256,12 @@ import class WooFoundation.MockAnalyticsProviderPreview cardPresentPaymentService: CardPresentPaymentPreviewService(), currencyFormatter: .init(currencySettings: .init()), paymentState: .acceptingCard) - let cartViewModel = CartViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview())) - let itemsListViewModel = ItemListViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview())) - let dashboardViewModel = PointOfSaleDashboardViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview()), - cardPresentPaymentService: CardPresentPaymentPreviewService(), + let cartViewModel = CartViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService())) + let itemsListViewModel = ItemListViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService())) + let dashboardViewModel = PointOfSaleDashboardViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService()), totalsViewModel: totalsViewModel, cartViewModel: cartViewModel, itemListViewModel: itemsListViewModel, diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index 265d100a871..2e96da4ad48 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -248,6 +248,7 @@ private extension ItemListView { #if DEBUG #Preview { - ItemListView(viewModel: ItemListViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview()))) + ItemListView(viewModel: ItemListViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService()))) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift index 0ed13e64d85..461453e5005 100644 --- a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift +++ b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift @@ -42,7 +42,7 @@ struct POSFloatingControlView: View { .cornerRadius(Constants.cornerRadius) .disabled(viewModel.isExitPOSDisabled) - CardReaderConnectionStatusView(connectionViewModel: viewModel.cardReaderConnectionViewModel) + CardReaderConnectionStatusView() .foregroundStyle(fontColor) .background(backgroundColor) .cornerRadius(Constants.cornerRadius) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index fe819a2a327..32f27433302 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -211,10 +211,12 @@ import class WooFoundation.MockAnalyticsProviderPreview cardPresentPaymentService: CardPresentPaymentPreviewService(), currencyFormatter: .init(currencySettings: .init()), paymentState: .acceptingCard) - let cartVM = CartViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview())) - let itemsListVM = ItemListViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview())) - let posVM = PointOfSaleDashboardViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview()), - cardPresentPaymentService: CardPresentPaymentPreviewService(), + let cartVM = CartViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService())) + let itemsListVM = ItemListViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService())) + let posVM = PointOfSaleDashboardViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService()), totalsViewModel: totalsVM, cartViewModel: cartVM, itemListViewModel: itemsListVM, diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index 5e2b1af87cf..e4d87c2de4a 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -22,7 +22,8 @@ struct PointOfSaleEntryPointView: View { analytics: Analytics) { self.onPointOfSaleModeActiveStateChange = onPointOfSaleModeActiveStateChange - let posModel = PointOfSaleAggregateModel(itemProvider: itemProvider) + let posModel = PointOfSaleAggregateModel(itemProvider: itemProvider, + cardPresentPaymentService: cardPresentPaymentService) let totalsViewModel = TotalsViewModel(orderService: orderService, cardPresentPaymentService: cardPresentPaymentService, currencyFormatter: currencyFormatter, @@ -34,7 +35,6 @@ struct PointOfSaleEntryPointView: View { self._posModel = StateObject(wrappedValue: posModel) self._viewModel = StateObject(wrappedValue: PointOfSaleDashboardViewModel( posModel: posModel, - cardPresentPaymentService: cardPresentPaymentService, totalsViewModel: totalsViewModel, cartViewModel: cartViewModel, itemListViewModel: itemListViewModel, diff --git a/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift b/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift index 95ad7461d58..f42e59f150e 100644 --- a/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift @@ -13,7 +13,6 @@ final class PointOfSaleDashboardViewModel: ObservableObject { let posModel: PointOfSaleAggregateModelProtocol - let cardReaderConnectionViewModel: CardReaderConnectionViewModel private let connectivityObserver: ConnectivityObserver @Published private(set) var isAddMoreDisabled: Bool = false @@ -28,13 +27,11 @@ final class PointOfSaleDashboardViewModel: ObservableObject { private var cancellables: Set = [] init(posModel: PointOfSaleAggregateModelProtocol, - cardPresentPaymentService: CardPresentPaymentFacade, totalsViewModel: any TotalsViewModelProtocol, cartViewModel: any CartViewModelProtocol, itemListViewModel: any ItemListViewModelProtocol, connectivityObserver: ConnectivityObserver) { self.posModel = posModel - self.cardReaderConnectionViewModel = CardReaderConnectionViewModel(cardPresentPayment: cardPresentPaymentService) self.itemListViewModel = itemListViewModel self.totalsViewModel = totalsViewModel self.cartViewModel = cartViewModel diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index fc48cbde178..81fac79b75e 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -306,7 +306,6 @@ 026826AC2BF59DF70036F959 /* ItemCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026826A42BF59DF60036F959 /* ItemCardView.swift */; }; 026826AD2BF59DF70036F959 /* PointOfSaleDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026826A52BF59DF60036F959 /* PointOfSaleDashboardView.swift */; }; 026826AF2BF59DF70036F959 /* PointOfSaleEntryPointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026826A72BF59DF70036F959 /* PointOfSaleEntryPointView.swift */; }; - 026826B42BF59E330036F959 /* CardReaderConnectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026826B22BF59E320036F959 /* CardReaderConnectionViewModel.swift */; }; 026826B52BF59E330036F959 /* CardReaderConnectionStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026826B32BF59E320036F959 /* CardReaderConnectionStatusView.swift */; }; 026826BF2BF59E410036F959 /* PointOfSaleCardPresentPaymentScanningForReadersFailedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026826B62BF59E400036F959 /* PointOfSaleCardPresentPaymentScanningForReadersFailedView.swift */; }; 026826C02BF59E410036F959 /* PointOfSaleCardPresentPaymentConnectingFailedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026826B72BF59E400036F959 /* PointOfSaleCardPresentPaymentConnectingFailedView.swift */; }; @@ -3417,7 +3416,6 @@ 026826A42BF59DF60036F959 /* ItemCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCardView.swift; sourceTree = ""; }; 026826A52BF59DF60036F959 /* PointOfSaleDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleDashboardView.swift; sourceTree = ""; }; 026826A72BF59DF70036F959 /* PointOfSaleEntryPointView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleEntryPointView.swift; sourceTree = ""; }; - 026826B22BF59E320036F959 /* CardReaderConnectionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardReaderConnectionViewModel.swift; sourceTree = ""; }; 026826B32BF59E320036F959 /* CardReaderConnectionStatusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardReaderConnectionStatusView.swift; sourceTree = ""; }; 026826B62BF59E400036F959 /* PointOfSaleCardPresentPaymentScanningForReadersFailedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentScanningForReadersFailedView.swift; sourceTree = ""; }; 026826B72BF59E400036F959 /* PointOfSaleCardPresentPaymentConnectingFailedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentConnectingFailedView.swift; sourceTree = ""; }; @@ -6887,7 +6885,6 @@ isa = PBXGroup; children = ( 026826B32BF59E320036F959 /* CardReaderConnectionStatusView.swift */, - 026826B22BF59E320036F959 /* CardReaderConnectionViewModel.swift */, 026826B12BF59E1F0036F959 /* UI States */, ); path = CardReaderConnection; @@ -15921,7 +15918,6 @@ DEE6437826D8DAD900888A75 /* InProgressView.swift in Sources */, 0290E275238E4F8100B5C466 /* PaginatedListSelectorViewController.swift in Sources */, B958A7D628B5310100823EEF /* URLOpener.swift in Sources */, - 026826B42BF59E330036F959 /* CardReaderConnectionViewModel.swift in Sources */, DE2FE5862925DA050018040A /* SiteCredentialLoginView.swift in Sources */, 020DD48F232392C9005822B1 /* UIViewController+AppReview.swift in Sources */, 2687165524D21BC80042F6AE /* SurveySubmittedViewController.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift index 993760d0f41..8c72c1fc6b9 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift @@ -3,6 +3,12 @@ import Foundation import protocol Yosemite.POSItem final class MockPointOfSaleAggregateModel: PointOfSaleAggregateModelProtocol { + var cardReaderConnectionStatus: CardPresentPaymentReaderConnectionStatus + + func connectCardReader() { } + + func disconnectCardReader() { } + var orderStage: PointOfSaleOrderStage var allItems: [POSItem] { @@ -19,8 +25,10 @@ final class MockPointOfSaleAggregateModel: PointOfSaleAggregateModelProtocol { var itemListState: ItemListState - init(itemListState: ItemListState = .initialLoading, + init(cardReaderConnectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected, + itemListState: ItemListState = .initialLoading, orderStage: PointOfSaleOrderStage = .building) { + self.cardReaderConnectionStatus = cardReaderConnectionStatus self.itemListState = itemListState self.orderStage = orderStage } diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 630728c9249..ab0d3c9eaa7 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -9,7 +9,8 @@ struct PointOfSaleAggregateModelTests { private let sut: PointOfSaleAggregateModel init() { - self.sut = PointOfSaleAggregateModel(itemProvider: MockPOSItemProvider()) + self.sut = PointOfSaleAggregateModel(itemProvider: MockPOSItemProvider(), + cardPresentPaymentService: MockCardPresentPaymentService()) } @Test func inits_with_building_order_stage() async throws { @@ -63,7 +64,8 @@ struct PointOfSaleAggregateModelTests { init() { itemProvider = MockPOSItemProvider() - sut = PointOfSaleAggregateModel(itemProvider: itemProvider) + sut = PointOfSaleAggregateModel(itemProvider: itemProvider, + cardPresentPaymentService: MockCardPresentPaymentService()) } @Test func loadInitialItems_requests_first_page() async throws { @@ -150,7 +152,8 @@ struct PointOfSaleAggregateModelTests { // Given let itemProvider = MockPOSItemProvider() itemProvider.shouldReturnZeroItems = true - let sut = PointOfSaleAggregateModel(itemProvider: itemProvider) + let sut = PointOfSaleAggregateModel(itemProvider: itemProvider, + cardPresentPaymentService: MockCardPresentPaymentService()) try #require(sut.itemListState == .initialLoading) @@ -207,7 +210,8 @@ struct PointOfSaleAggregateModelTests { // Given let itemProvider = MockPOSItemProvider() itemProvider.shouldReturnZeroItems = true - let sut = PointOfSaleAggregateModel(itemProvider: itemProvider) + let sut = PointOfSaleAggregateModel(itemProvider: itemProvider, + cardPresentPaymentService: MockCardPresentPaymentService()) try #require(sut.itemListState == .initialLoading) @@ -312,6 +316,7 @@ struct PointOfSaleAggregateModelTests { analyticsProvider = MockAnalyticsProvider() analytics = WooAnalytics(analyticsProvider: analyticsProvider) sut = PointOfSaleAggregateModel(itemProvider: MockPOSItemProvider(), + cardPresentPaymentService: MockCardPresentPaymentService(), analytics: analytics) } diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewModelTests.swift index 22be37adb58..223e03dc6d5 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewModelTests.swift @@ -12,7 +12,8 @@ final class CartViewModelTests: XCTestCase { override func setUp() { super.setUp() - posModel = PointOfSaleAggregateModel(itemProvider: MockPOSItemProvider()) + posModel = PointOfSaleAggregateModel(itemProvider: MockPOSItemProvider(), + cardPresentPaymentService: MockCardPresentPaymentService()) sut = CartViewModel(posModel: posModel) } diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift index c8ee472efce..af8d2fbe563 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift @@ -26,7 +26,6 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { mockItemListViewModel = MockItemListViewModel() mockConnectivityObserver = MockConnectivityObserver() sut = PointOfSaleDashboardViewModel(posModel: mockPOSModel, - cardPresentPaymentService: cardPresentPaymentService, totalsViewModel: mockTotalsViewModel, cartViewModel: mockCartViewModel, itemListViewModel: mockItemListViewModel, From 3d65805f5dc5c559080f3e3f0a8a9e31e77b9773 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 14 Nov 2024 09:57:56 +0900 Subject: [PATCH 08/49] make test target compile --- .../PointOfSaleAggregateModelTests.swift | 46 ------------------- .../PointOfSale/POSProductProviderTests.swift | 4 +- 2 files changed, 2 insertions(+), 48 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 344d44fd04a..e5f1b0d806c 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -263,50 +263,4 @@ struct PointOfSaleAggregateModelTests { #expect(sut.itemListState == .error(expectedError)) } - @Test func itemListViewModel_when_initialized_then_isLastPage_is_false() async throws { - // Given/then - try #require(sut.isLastPage == false) - } - - @Test func itemListViewModel_when_there_are_no_items_then_isLastPage_is_true_() async throws { - // Given - let itemProvider = MockPOSItemProvider() - itemProvider.shouldReturnZeroItems = true - let sut = PointOfSaleAggregateModel(itemProvider: itemProvider) - - try #require(sut.isLastPage == false) - - // When - await sut.loadInitialItems() - - // Then - try #require(sut.isLastPage == true) - - } - - @Test func itemListViewModel_when_reload_is_invoked_resets_isLastPage_to_false() async throws { - // Given - try #require(sut.isLastPage == false) - - // When - await sut.reload() - - // Then - try #require(sut.isLastPage == false) - } - - @Test func itemListViewModel_when_loadNextItems_is_invoked_then_isLastPage_is_true() async throws { - // Given - let initialItems = MockPOSItemProvider.makeInitialItems() - itemProvider.items = initialItems - itemProvider.shouldSimulateTwoPages = true - - try #require(sut.isLastPage == false) - - // When - await sut.loadNextItems() - - // Then - try #require(sut.isLastPage == false) - } } diff --git a/Yosemite/YosemiteTests/PointOfSale/POSProductProviderTests.swift b/Yosemite/YosemiteTests/PointOfSale/POSProductProviderTests.swift index ccaa66b6145..d8f45a5f67b 100644 --- a/Yosemite/YosemiteTests/PointOfSale/POSProductProviderTests.swift +++ b/Yosemite/YosemiteTests/PointOfSale/POSProductProviderTests.swift @@ -26,7 +26,7 @@ final class POSProductProviderTests: XCTestCase { func test_POSItemProvider_when_fails_request_then_throws_error() async throws { // Given - let expectedError = POSProductProvider.POSProductProviderError.requestFailed + let expectedError = POSProductProviderError.requestFailed network.simulateError(requestUrlSuffix: "products", error: expectedError) // When @@ -35,7 +35,7 @@ final class POSProductProviderTests: XCTestCase { XCTFail("Expected an error, but got success.") } catch { // Then - XCTAssertEqual(error as? POSProductProvider.POSProductProviderError, expectedError) + XCTAssertEqual(error as? POSProductProviderError, expectedError) } } From 668985b323f083b75856791ee818dd0f88035a49 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 14 Nov 2024 09:59:38 +0900 Subject: [PATCH 09/49] Do not throw out of range error for no eligible products If there are no eligible products at all (page = 1 && products = 0), we would throw the out of range error, rather than returning empty. --- Yosemite/Yosemite/PointOfSale/POSProductProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift b/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift index eca1795aeac..d3464bf8ce5 100644 --- a/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift +++ b/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift @@ -40,7 +40,7 @@ public final class POSProductProvider: POSItemProvider { public func providePointOfSaleItems(pageNumber: Int = 1) async throws -> [POSItem] { let products = try await productsRemote.loadSimpleProductsForPointOfSale(for: siteID, pageNumber: pageNumber) - if products.count == 0 { + if pageNumber != 1 && products.count == 0 { throw POSProductProviderError.pageOutOfRange } From c4dab120ae5e340cbb99bd439471a95bbdad2113 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 14 Nov 2024 10:47:15 +0900 Subject: [PATCH 10/49] Add test cases --- .../POS/Mocks/MockPOSItemProvider.swift | 9 ++++++ .../PointOfSaleAggregateModelTests.swift | 29 +++++++++++++++++++ .../PointOfSale/POSProductProviderTests.swift | 16 +++++++++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSItemProvider.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSItemProvider.swift index f67e988f133..ae5fd65e821 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSItemProvider.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSItemProvider.swift @@ -8,9 +8,13 @@ final class MockPOSItemProvider: POSItemProvider { var shouldThrowError = false var shouldReturnZeroItems = false var shouldSimulateTwoPages = false + private var isPageOutOfRange = false var spyLastRequestedPageNumber: Int? func providePointOfSaleItems(pageNumber: Int) async throws -> [Yosemite.POSItem] { + if isPageOutOfRange { + throw MockError.pageOutOfRange + } spyLastRequestedPageNumber = pageNumber if shouldThrowError { throw MockError.requestFailed @@ -29,6 +33,10 @@ final class MockPOSItemProvider: POSItemProvider { func simulateFetchNextPage() { items.append(contentsOf: MockPOSItemProvider.makeSecondPageItems()) } + + func simulateNextPageIsOutOfRange() { + isPageOutOfRange = true + } } extension MockPOSItemProvider { @@ -82,5 +90,6 @@ extension MockPOSItemProvider { enum MockError: Error { case requestFailed + case pageOutOfRange } } diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index e5f1b0d806c..db8af79c531 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -223,6 +223,35 @@ struct PointOfSaleAggregateModelTests { #expect(itemProvider.spyLastRequestedPageNumber == 2) } + @Test func itemListViewModel_when_next_page_is_out_of_range_then_receives_error() async throws { + // Given + await sut.loadInitialItems() + try #require(itemProvider.spyLastRequestedPageNumber == 1) + + // When + itemProvider.simulateNextPageIsOutOfRange() + await sut.loadNextItems() + + // Then + guard case .error = sut.itemListState else { + Issue.record("Expected error state, but got \(sut.itemListState)") + return + } + } + + @Test func itemListViewModel_when_next_page_is_out_of_range_then_the_same_page_is_requested_next() async throws { + // Given + await sut.loadInitialItems() + try #require(itemProvider.spyLastRequestedPageNumber == 1) + + // When + itemProvider.simulateNextPageIsOutOfRange() + await sut.loadNextItems() + + // Then + try #require(itemProvider.spyLastRequestedPageNumber == 1) + } + @Test func itemListViewModel_when_reload_then_state_is_loaded_with_expected_items() async throws { // Given try #require(sut.itemListState == .initialLoading) diff --git a/Yosemite/YosemiteTests/PointOfSale/POSProductProviderTests.swift b/Yosemite/YosemiteTests/PointOfSale/POSProductProviderTests.swift index d8f45a5f67b..3556d79b634 100644 --- a/Yosemite/YosemiteTests/PointOfSale/POSProductProviderTests.swift +++ b/Yosemite/YosemiteTests/PointOfSale/POSProductProviderTests.swift @@ -24,7 +24,7 @@ final class POSProductProviderTests: XCTestCase { super.tearDown() } - func test_POSItemProvider_when_fails_request_then_throws_error() async throws { + func test_POSItemProvider_when_fails_request_with_requestFailed_then_throws_error() async throws { // Given let expectedError = POSProductProviderError.requestFailed network.simulateError(requestUrlSuffix: "products", error: expectedError) @@ -39,6 +39,20 @@ final class POSProductProviderTests: XCTestCase { } } + func test_POSItemProvider_when_fails_request_with_pageOutOfRange_then_throws_error() async throws { + let expectedError = POSProductProviderError.pageOutOfRange + network.simulateError(requestUrlSuffix: "products", error: expectedError) + + // When + do { + _ = try await itemProvider.providePointOfSaleItems() + XCTFail("Expected an error, but got success.") + } catch { + // Then + XCTAssertEqual(error as? POSProductProviderError, expectedError) + } + } + func test_POSItemProvider_provides_no_items_when_store_has_no_products() async throws { // Given/When network.simulateResponse(requestUrlSuffix: "products", filename: "empty-data-array") From 8956c01a42484791edcd27bc1c6876c7c2fad267 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 14 Nov 2024 10:58:59 +0900 Subject: [PATCH 11/49] cleanup loadNextItems --- .../Models/PointOfSaleAggregateModel.swift | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 62cee3932df..5506bd83e37 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -39,25 +39,22 @@ extension PointOfSaleAggregateModel { @MainActor func loadNextItems() async { - do { - guard !pageIsOutOfRange else { - return - } - itemListState = .loading(allItems) + guard !pageIsOutOfRange else { + return + } + itemListState = .loading(allItems) + let nextPage = currentPage + 1 - let nextPage = currentPage + 1 + do { try await load(pageNumber: nextPage) pageIsOutOfRange = false currentPage = nextPage + itemListState = .loaded(allItems) } catch POSProductProviderError.pageOutOfRange { - if allItems.count == 0 { - itemListState = .empty - } else { - itemListState = .loaded(allItems) - } pageIsOutOfRange = true + itemListState = allItems.isEmpty ? .empty : .loaded(allItems) } catch { - // No need to do anything; this avoids us incorrectly incrementing currentPage. + itemListState = .loaded(allItems) } } From 4c0a76db63d8ac75b6de58c8f8a396edec57e46a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 14 Nov 2024 11:13:54 +0900 Subject: [PATCH 12/49] revert loadNextItems() refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behavior is the same, but some tests that expect `.empty` state fail with `.loaded([])` instead. Effectively these mean the same but let’s defer it outside of the scope of this PR. --- .../Models/PointOfSaleAggregateModel.swift | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 631c200b58b..893a2e9dbd4 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -50,22 +50,25 @@ extension PointOfSaleAggregateModel { @MainActor func loadNextItems() async { - guard !pageIsOutOfRange else { - return - } - itemListState = .loading(allItems) - let nextPage = currentPage + 1 - do { + guard !pageIsOutOfRange else { + return + } + itemListState = .loading(allItems) + + let nextPage = currentPage + 1 try await load(pageNumber: nextPage) pageIsOutOfRange = false currentPage = nextPage - itemListState = .loaded(allItems) } catch POSProductProviderError.pageOutOfRange { + if allItems.count == 0 { + itemListState = .empty + } else { + itemListState = .loaded(allItems) + } pageIsOutOfRange = true - itemListState = allItems.isEmpty ? .empty : .loaded(allItems) } catch { - itemListState = .loaded(allItems) + // No need to do anything; this avoids us incorrectly incrementing currentPage. } } From d7a3a9fe70b233ec57ee89ec523134de16796b93 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 14 Nov 2024 11:15:36 +0900 Subject: [PATCH 13/49] Resolve add missing tests after merge conflict --- .../PointOfSaleAggregateModelTests.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 52425bd718d..340757ff450 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -234,6 +234,35 @@ struct PointOfSaleAggregateModelTests { #expect(itemProvider.spyLastRequestedPageNumber == 1) } + @Test func itemListViewModel_when_next_page_is_out_of_range_then_receives_error() async throws { + // Given + await sut.loadInitialItems() + try #require(itemProvider.spyLastRequestedPageNumber == 1) + + // When + itemProvider.simulateNextPageIsOutOfRange() + await sut.loadNextItems() + + // Then + guard case .error = sut.itemListState else { + Issue.record("Expected error state, but got \(sut.itemListState)") + return + } + } + + @Test func itemListViewModel_when_next_page_is_out_of_range_then_the_same_page_is_requested_next() async throws { + // Given + await sut.loadInitialItems() + try #require(itemProvider.spyLastRequestedPageNumber == 1) + + // When + itemProvider.simulateNextPageIsOutOfRange() + await sut.loadNextItems() + + // Then + try #require(itemProvider.spyLastRequestedPageNumber == 1) + } + @Test func reload_when_itemProvider_throws_error_then_state_is_error() async throws { // Given itemProvider.shouldThrowError = true From 1115e372da82ec7d9b67568069f6dce92179b65f Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 14 Nov 2024 11:16:01 +0900 Subject: [PATCH 14/49] lint --- Yosemite/Yosemite/PointOfSale/POSProductProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift b/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift index d3464bf8ce5..8a7a967a14c 100644 --- a/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift +++ b/Yosemite/Yosemite/PointOfSale/POSProductProvider.swift @@ -50,7 +50,7 @@ public final class POSProductProvider: POSItemProvider { hasPrice ] let filteredProducts = filterProducts(products: products, using: eligibilityCriteria) - + return mapProductsToPOSItems(products: filteredProducts) } From 31e8f890d7d90d1063d47140d309018bae31f998 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 14 Nov 2024 11:35:19 +0900 Subject: [PATCH 15/49] Add missing expectation to test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t do anything specific with this error in the case that the page is out of range, but we still use the same associated type when there’s an error on loading further products. --- .../POS/Models/PointOfSaleAggregateModelTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 340757ff450..0d7bf20a910 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -238,6 +238,9 @@ struct PointOfSaleAggregateModelTests { // Given await sut.loadInitialItems() try #require(itemProvider.spyLastRequestedPageNumber == 1) + let expectedError = PointOfSaleErrorState(title: "Error loading products", + subtitle: "Give it another go?", + buttonText: "Retry") // When itemProvider.simulateNextPageIsOutOfRange() @@ -248,6 +251,7 @@ struct PointOfSaleAggregateModelTests { Issue.record("Expected error state, but got \(sut.itemListState)") return } + #expect(sut.itemListState == .error(expectedError)) } @Test func itemListViewModel_when_next_page_is_out_of_range_then_the_same_page_is_requested_next() async throws { From ec763b337ff7eca55158b43bb897c97f5043f964 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 14 Nov 2024 12:12:54 +0900 Subject: [PATCH 16/49] restore rendering GhostItemCardView behind flag --- WooCommerce/Classes/POS/Presentation/ItemListView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index c5b4c2d4791..1b9e1b327f4 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -135,7 +135,7 @@ private extension ItemListView { }) } GhostItemCardView() - .renderedIf(posModel.itemListState.isLoadingAfterInitialLoad) + .renderedIf(posModel.itemListState.isLoadingAfterInitialLoad && viewModel.shouldShowGhostableItemCard) } .frame(maxWidth: .infinity) .padding(.bottom, floatingControlAreaSize.height) From e1aa1bc1ff1bdf09c529f9427578346406599878 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 14 Nov 2024 12:16:54 +0900 Subject: [PATCH 17/49] remove feature flag --- Experiments/Experiments/DefaultFeatureFlagService.swift | 2 -- Experiments/Experiments/FeatureFlag.swift | 4 ---- WooCommerce/Classes/POS/Presentation/ItemListView.swift | 2 +- .../Classes/POS/Presentation/PointOfSaleEntryPointView.swift | 3 +-- WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift | 5 +---- 5 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index 39ea8e9353d..7407ba81331 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -91,8 +91,6 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return buildConfig == .localDeveloper || buildConfig == .alpha case .paymentsOnboardingInPointOfSale: return buildConfig == .localDeveloper - case .displayInfiniteScrollingUIDetailsInPointOfSale: - return buildConfig == .localDeveloper || buildConfig == .alpha default: return true } diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index 7dafa9611fb..7d34cd02836 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -196,8 +196,4 @@ public enum FeatureFlag: Int { /// Supports Woo Payments onboarding in POS so that merchants who have not completed onboarding can access POS. /// case paymentsOnboardingInPointOfSale - - /// Enables UI-related aspects of infinite scrolling in POS. It does not affect the actual infinite scrolling behaviour. - /// - case displayInfiniteScrollingUIDetailsInPointOfSale } diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index 1b9e1b327f4..c5b4c2d4791 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -135,7 +135,7 @@ private extension ItemListView { }) } GhostItemCardView() - .renderedIf(posModel.itemListState.isLoadingAfterInitialLoad && viewModel.shouldShowGhostableItemCard) + .renderedIf(posModel.itemListState.isLoadingAfterInitialLoad) } .frame(maxWidth: .infinity) .padding(.bottom, floatingControlAreaSize.height) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index 5e2b1af87cf..b18f62b2e03 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -28,8 +28,7 @@ struct PointOfSaleEntryPointView: View { currencyFormatter: currencyFormatter, paymentState: .acceptingCard) let cartViewModel = CartViewModel(posModel: posModel) - let shouldShowGhostableItemCard = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.displayInfiniteScrollingUIDetailsInPointOfSale) - let itemListViewModel = ItemListViewModel(posModel: posModel, shouldShowGhostableItemCard: shouldShowGhostableItemCard) + let itemListViewModel = ItemListViewModel(posModel: posModel) self._posModel = StateObject(wrappedValue: posModel) self._viewModel = StateObject(wrappedValue: PointOfSaleDashboardViewModel( diff --git a/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift b/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift index fcd1b580e3a..d8e871c1d6e 100644 --- a/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift @@ -8,8 +8,6 @@ final class ItemListViewModel: ItemListViewModelProtocol { @Published private(set) var isHeaderBannerDismissed: Bool = false @Published var showSimpleProductsModal: Bool = false - private(set) var shouldShowGhostableItemCard: Bool = false - var shouldShowHeaderBanner: Bool { // The banner it's shown as long as it hasn't already been dismissed once: if UserDefaults.standard.bool(forKey: BannerState.isSimpleProductsOnlyBannerDismissedKey) == true { @@ -30,9 +28,8 @@ final class ItemListViewModel: ItemListViewModelProtocol { let selectedItemPublisher: AnyPublisher - init(posModel: PointOfSaleAggregateModelProtocol, shouldShowGhostableItemCard: Bool = false) { + init(posModel: PointOfSaleAggregateModelProtocol) { self.posModel = posModel - self.shouldShowGhostableItemCard = shouldShowGhostableItemCard selectedItemPublisher = selectedItemSubject.eraseToAnyPublisher() } From 6f43d18b9d517df72a72b1e13ef35eecfae98fca Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Thu, 14 Nov 2024 13:44:03 +0800 Subject: [PATCH 18/49] Rename "deposit" to "payout" in code and documentation URL. --- .../DefaultFeatureFlagService.swift | 2 +- Experiments/Experiments/FeatureFlag.swift | 4 +- Fakes/Fakes/Networking.generated.swift | 60 ++++----- Fakes/Fakes/Yosemite.generated.swift | 12 +- .../Networking.xcodeproj/project.pbxproj | 8 +- .../WooPaymentsDepositsOverviewMapper.swift | 14 +- .../Copiable/Models+Copiable.generated.swift | 96 +++++++------- ...swift => WooPaymentsPayoutsOverview.swift} | 86 ++++++------- .../Networking/Remote/WCPayRemote.swift | 10 +- .../Classes/Analytics/WooAnalyticsEvent.swift | 16 +-- .../Classes/Analytics/WooAnalyticsStat.swift | 10 +- WooCommerce/Classes/System/WooConstants.swift | 2 +- ...WooPaymentsDepositsOverviewViewModel.swift | 22 ---- ...oPaymentsPayoutStatusDisplayDetails.swift} | 32 ++--- ...PaymentsPayoutsCurrencyOverviewView.swift} | 54 ++++---- ...ntsPayoutsCurrencyOverviewViewModel.swift} | 64 +++++----- ...t => WooPaymentsPayoutsOverviewView.swift} | 36 +++--- .../WooPaymentsPayoutsOverviewViewModel.swift | 22 ++++ .../Payments Menu/InPersonPaymentsMenu.swift | 46 +++---- .../InPersonPaymentsMenuViewModel.swift | 42 +++--- .../Hub Menu/HubMenuViewModel.swift | 2 +- .../WooCommerce.xcodeproj/project.pbxproj | 56 ++++---- .../Mocks/MockWooPaymentsDepositService.swift | 16 +-- ...youtsCurrencyOverviewViewModelTests.swift} | 24 ++-- ...ymentsPayoutsOverviewViewModelTests.swift} | 26 ++-- .../InPersonPaymentsMenuViewModelTests.swift | 34 ++--- Yosemite/Yosemite.xcodeproj/project.pbxproj | 24 ++-- .../Copiable/Models+Copiable.generated.swift | 30 ++--- Yosemite/Yosemite/Model/Model.swift | 4 +- ...ooPaymentsDepositsOverviewByCurrency.swift | 46 +++---- ...swift => WooPaymentsPayoutsOverview.swift} | 32 ++--- .../Payments/WooPaymentsDepositService.swift | 120 ------------------ .../Payments/WooPaymentsPayoutService.swift | 120 ++++++++++++++++++ ...ft => WooPaymentsPayoutServiceTests.swift} | 44 +++---- 34 files changed, 608 insertions(+), 608 deletions(-) rename Networking/Networking/Model/{WooPaymentsDepositsOverview.swift => WooPaymentsPayoutsOverview.swift} (77%) delete mode 100644 WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsOverviewViewModel.swift rename WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/{WooPaymentsDepositStatusDisplayDetails.swift => WooPaymentsPayoutStatusDisplayDetails.swift} (78%) rename WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/{WooPaymentsDepositsCurrencyOverviewView.swift => WooPaymentsPayoutsCurrencyOverviewView.swift} (79%) rename WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/{WooPaymentsDepositsCurrencyOverviewViewModel.swift => WooPaymentsPayoutsCurrencyOverviewViewModel.swift} (72%) rename WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/{WooPaymentsDepositsOverviewView.swift => WooPaymentsPayoutsOverviewView.swift} (51%) create mode 100644 WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsOverviewViewModel.swift rename WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/{WooPaymentsDepositsCurrencyOverviewViewModelTests.swift => WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift} (67%) rename WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/{WooPaymentsDepositsOverviewViewModelTests.swift => WooPaymentsPayoutsOverviewViewModelTests.swift} (62%) rename Yosemite/Yosemite/Model/{WooPaymentsDepositsOverview.swift => WooPaymentsPayoutsOverview.swift} (50%) delete mode 100644 Yosemite/Yosemite/Tools/Payments/WooPaymentsDepositService.swift create mode 100644 Yosemite/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift rename Yosemite/YosemiteTests/Tools/Payments/{WooPaymentsDepositServiceTests.swift => WooPaymentsPayoutServiceTests.swift} (56%) diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index 39ea8e9353d..8b43e05944c 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -52,7 +52,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return true case .giftCardInOrderForm: return true - case .wooPaymentsDepositsOverviewInPaymentsMenu: + case .wooPaymentsPayoutsOverviewInPaymentsMenu: return true case .tapToPayOnIPhoneInUK: return true diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index 7dafa9611fb..53f3c8e3a76 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -120,9 +120,9 @@ public enum FeatureFlag: Int { /// case giftCardInOrderForm - /// Enables the Woo Payments Deposits item in the Payments menu + /// Enables the Woo Payments Payouts item in the Payments menu /// - case wooPaymentsDepositsOverviewInPaymentsMenu + case wooPaymentsPayoutsOverviewInPaymentsMenu /// Enables Tap to Pay for UK Woo Payments stores /// diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index 623a11c80d8..5bf2c5fc04e 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -2636,14 +2636,14 @@ extension Networking.WCPayPaymentMethodType { .card } } -extension Networking.WooPaymentsAccountDepositSummary { +extension Networking.WooPaymentsAccountPayoutSummary { /// Returns a "ready to use" type filled with fake values. /// - public static func fake() -> Networking.WooPaymentsAccountDepositSummary { + public static func fake() -> Networking.WooPaymentsAccountPayoutSummary { .init( - depositsEnabled: .fake(), - depositsBlocked: .fake(), - depositsSchedule: .fake(), + payoutsEnabled: .fake(), + payoutsBlocked: .fake(), + payoutsSchedule: .fake(), defaultCurrency: .fake() ) } @@ -2669,20 +2669,30 @@ extension Networking.WooPaymentsCurrencyBalances { ) } } -extension Networking.WooPaymentsCurrencyDeposits { +extension Networking.WooPaymentsCurrencyPayouts { /// Returns a "ready to use" type filled with fake values. /// - public static func fake() -> Networking.WooPaymentsCurrencyDeposits { + public static func fake() -> Networking.WooPaymentsCurrencyPayouts { .init( lastPaid: .fake(), - lastManualDeposits: .fake() + lastManualPayouts: .fake() ) } } -extension Networking.WooPaymentsDeposit { +extension Networking.WooPaymentsManualPayout { /// Returns a "ready to use" type filled with fake values. /// - public static func fake() -> Networking.WooPaymentsDeposit { + public static func fake() -> Networking.WooPaymentsManualPayout { + .init( + currency: .fake(), + date: .fake() + ) + } +} +extension Networking.WooPaymentsPayout { + /// Returns a "ready to use" type filled with fake values. + /// + public static func fake() -> Networking.WooPaymentsPayout { .init( id: .fake(), date: .fake(), @@ -2698,31 +2708,31 @@ extension Networking.WooPaymentsDeposit { ) } } -extension Networking.WooPaymentsDepositInterval { +extension Networking.WooPaymentsPayoutInterval { /// Returns a "ready to use" type filled with fake values. /// - public static func fake() -> Networking.WooPaymentsDepositInterval { + public static func fake() -> Networking.WooPaymentsPayoutInterval { .daily } } -extension Networking.WooPaymentsDepositStatus { +extension Networking.WooPaymentsPayoutStatus { /// Returns a "ready to use" type filled with fake values. /// - public static func fake() -> Networking.WooPaymentsDepositStatus { + public static func fake() -> Networking.WooPaymentsPayoutStatus { .estimated } } -extension Networking.WooPaymentsDepositType { +extension Networking.WooPaymentsPayoutType { /// Returns a "ready to use" type filled with fake values. /// - public static func fake() -> Networking.WooPaymentsDepositType { + public static func fake() -> Networking.WooPaymentsPayoutType { .withdrawal } } -extension Networking.WooPaymentsDepositsOverview { +extension Networking.WooPaymentsPayoutsOverview { /// Returns a "ready to use" type filled with fake values. /// - public static func fake() -> Networking.WooPaymentsDepositsOverview { + public static func fake() -> Networking.WooPaymentsPayoutsOverview { .init( deposit: .fake(), balance: .fake(), @@ -2730,26 +2740,16 @@ extension Networking.WooPaymentsDepositsOverview { ) } } -extension Networking.WooPaymentsDepositsSchedule { +extension Networking.WooPaymentsPayoutsSchedule { /// Returns a "ready to use" type filled with fake values. /// - public static func fake() -> Networking.WooPaymentsDepositsSchedule { + public static func fake() -> Networking.WooPaymentsPayoutsSchedule { .init( delayDays: .fake(), interval: .fake() ) } } -extension Networking.WooPaymentsManualDeposit { - /// Returns a "ready to use" type filled with fake values. - /// - public static func fake() -> Networking.WooPaymentsManualDeposit { - .init( - currency: .fake(), - date: .fake() - ) - } -} extension Networking.WooShippingCreatePackageResponse { /// Returns a "ready to use" type filled with fake values. /// diff --git a/Fakes/Fakes/Yosemite.generated.swift b/Fakes/Fakes/Yosemite.generated.swift index a729a1f9670..7eede1cf990 100644 --- a/Fakes/Fakes/Yosemite.generated.swift +++ b/Fakes/Fakes/Yosemite.generated.swift @@ -54,17 +54,17 @@ extension Yosemite.SystemInformation { ) } } -extension Yosemite.WooPaymentsDepositsOverviewByCurrency { +extension Yosemite.WooPaymentsPayoutsOverviewByCurrency { /// Returns a "ready to use" type filled with fake values. /// - public static func fake() -> Yosemite.WooPaymentsDepositsOverviewByCurrency { + public static func fake() -> Yosemite.WooPaymentsPayoutsOverviewByCurrency { .init( currency: .fake(), - automaticDeposits: .fake(), - depositInterval: .fake(), + automaticPayouts: .fake(), + payoutInterval: .fake(), pendingBalanceAmount: .fake(), - pendingDepositDays: .fake(), - lastDeposit: .fake(), + pendingPayoutDays: .fake(), + lastPayout: .fake(), availableBalance: .fake() ) } diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 5b85589d03a..a17efd69ef8 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -181,7 +181,7 @@ 077F39E626A5D15800ABEADC /* SystemPluginMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077F39DB26A58F4800ABEADC /* SystemPluginMapperTests.swift */; }; 09885C8027C3FFD200910A62 /* product-variations-bulk-update.json in Resources */ = {isa = PBXBuildFile; fileRef = 09885C7F27C3FFD200910A62 /* product-variations-bulk-update.json */; }; 09EA564B27C75FCE00407D40 /* ProductVariationsBulkUpdateMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09EA564A27C75FCE00407D40 /* ProductVariationsBulkUpdateMapper.swift */; }; - 209AD3C32AC196E300825D76 /* WooPaymentsDepositsOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3C22AC196E300825D76 /* WooPaymentsDepositsOverview.swift */; }; + 209AD3C32AC196E300825D76 /* WooPaymentsPayoutsOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3C22AC196E300825D76 /* WooPaymentsPayoutsOverview.swift */; }; 209AD3C52AC19E7500825D76 /* WooPaymentsDepositsOverviewMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3C42AC19E7500825D76 /* WooPaymentsDepositsOverviewMapper.swift */; }; 20D210C32B1780CE0099E517 /* deposits-overview-all.json in Resources */ = {isa = PBXBuildFile; fileRef = 20D210C22B1780CE0099E517 /* deposits-overview-all.json */; }; 20D210C52B1788E60099E517 /* deposits-overview-all-no-default-currency.json in Resources */ = {isa = PBXBuildFile; fileRef = 20D210C42B1788E60099E517 /* deposits-overview-all-no-default-currency.json */; }; @@ -1452,7 +1452,7 @@ 09885C7F27C3FFD200910A62 /* product-variations-bulk-update.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-variations-bulk-update.json"; sourceTree = ""; }; 09EA564A27C75FCE00407D40 /* ProductVariationsBulkUpdateMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationsBulkUpdateMapper.swift; sourceTree = ""; }; 14CE248C7246705417A41DE1 /* Pods-NetworkingWatchOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkingWatchOS.release.xcconfig"; path = "../Pods/Target Support Files/Pods-NetworkingWatchOS/Pods-NetworkingWatchOS.release.xcconfig"; sourceTree = ""; }; - 209AD3C22AC196E300825D76 /* WooPaymentsDepositsOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsOverview.swift; sourceTree = ""; }; + 209AD3C22AC196E300825D76 /* WooPaymentsPayoutsOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsOverview.swift; sourceTree = ""; }; 209AD3C42AC19E7500825D76 /* WooPaymentsDepositsOverviewMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsOverviewMapper.swift; sourceTree = ""; }; 20D210C22B1780CE0099E517 /* deposits-overview-all.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "deposits-overview-all.json"; sourceTree = ""; }; 20D210C42B1788E60099E517 /* deposits-overview-all-no-default-currency.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "deposits-overview-all-no-default-currency.json"; sourceTree = ""; }; @@ -3029,7 +3029,7 @@ 0359EA0C27AAC5F80048DE2D /* WCPayChargeStatus.swift */, 3105470B262E27F000C5C02B /* WCPayPaymentIntentStatusEnum.swift */, 0359EA0E27AAC6410048DE2D /* WCPayPaymentMethodDetails.swift */, - 209AD3C22AC196E300825D76 /* WooPaymentsDepositsOverview.swift */, + 209AD3C22AC196E300825D76 /* WooPaymentsPayoutsOverview.swift */, E1BAB2C62913FB5800C3982B /* WordPressApiError.swift */, DE2E8E9C29530EEF002E4B14 /* WordPressSite.swift */, EE2C09C729AF6357009396F9 /* StoreOnboardingTask.swift */, @@ -4975,7 +4975,7 @@ 74ABA1CD213F1B6B00FFAD30 /* TopEarnerStats.swift in Sources */, CCAAD10F2683974000909664 /* ShippingLabelPackagePurchase.swift in Sources */, 265EFBDC285257950033BD33 /* Order+Fallbacks.swift in Sources */, - 209AD3C32AC196E300825D76 /* WooPaymentsDepositsOverview.swift in Sources */, + 209AD3C32AC196E300825D76 /* WooPaymentsPayoutsOverview.swift in Sources */, CEC7D5932CDD0D9900111B79 /* WooShippingPredefinedOption.swift in Sources */, 021940E2291E3CFD0090354E /* SiteRemote.swift in Sources */, B557DA0220975500005962F4 /* JetpackRequest.swift in Sources */, diff --git a/Networking/Networking/Mapper/WooPaymentsDepositsOverviewMapper.swift b/Networking/Networking/Mapper/WooPaymentsDepositsOverviewMapper.swift index 17413ac219b..135e36ca219 100644 --- a/Networking/Networking/Mapper/WooPaymentsDepositsOverviewMapper.swift +++ b/Networking/Networking/Mapper/WooPaymentsDepositsOverviewMapper.swift @@ -1,22 +1,22 @@ import Foundation -struct WooPaymentsDepositsOverviewMapper: Mapper { - func map(response: Data) throws -> WooPaymentsDepositsOverview { +struct WooPaymentsPayoutsOverviewMapper: Mapper { + func map(response: Data) throws -> WooPaymentsPayoutsOverview { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .millisecondsSince1970 if hasDataEnvelope(in: response) { - return try decoder.decode(WooPaymentsDepositsOverviewEnvelope.self, from: response).depositsOverview + return try decoder.decode(WooPaymentsPayoutsOverviewEnvelope.self, from: response).payoutsOverview } else { - return try decoder.decode(WooPaymentsDepositsOverview.self, from: response) + return try decoder.decode(WooPaymentsPayoutsOverview.self, from: response) } } } -private struct WooPaymentsDepositsOverviewEnvelope: Decodable { - let depositsOverview: WooPaymentsDepositsOverview +private struct WooPaymentsPayoutsOverviewEnvelope: Decodable { + let payoutsOverview: WooPaymentsPayoutsOverview private enum CodingKeys: String, CodingKey { - case depositsOverview = "data" + case payoutsOverview = "data" } } diff --git a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift index 3f3c3c02a40..a1f40ed4cc1 100644 --- a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -3844,22 +3844,22 @@ extension Networking.WCPayCharge { } } -extension Networking.WooPaymentsAccountDepositSummary { +extension Networking.WooPaymentsAccountPayoutSummary { public func copy( - depositsEnabled: CopiableProp = .copy, - depositsBlocked: CopiableProp = .copy, - depositsSchedule: CopiableProp = .copy, + payoutsEnabled: CopiableProp = .copy, + payoutsBlocked: CopiableProp = .copy, + payoutsSchedule: CopiableProp = .copy, defaultCurrency: CopiableProp = .copy - ) -> Networking.WooPaymentsAccountDepositSummary { - let depositsEnabled = depositsEnabled ?? self.depositsEnabled - let depositsBlocked = depositsBlocked ?? self.depositsBlocked - let depositsSchedule = depositsSchedule ?? self.depositsSchedule + ) -> Networking.WooPaymentsAccountPayoutSummary { + let payoutsEnabled = payoutsEnabled ?? self.payoutsEnabled + let payoutsBlocked = payoutsBlocked ?? self.payoutsBlocked + let payoutsSchedule = payoutsSchedule ?? self.payoutsSchedule let defaultCurrency = defaultCurrency ?? self.defaultCurrency - return Networking.WooPaymentsAccountDepositSummary( - depositsEnabled: depositsEnabled, - depositsBlocked: depositsBlocked, - depositsSchedule: depositsSchedule, + return Networking.WooPaymentsAccountPayoutSummary( + payoutsEnabled: payoutsEnabled, + payoutsBlocked: payoutsBlocked, + payoutsSchedule: payoutsSchedule, defaultCurrency: defaultCurrency ) } @@ -3898,35 +3898,50 @@ extension Networking.WooPaymentsCurrencyBalances { } } -extension Networking.WooPaymentsCurrencyDeposits { +extension Networking.WooPaymentsCurrencyPayouts { public func copy( - lastPaid: CopiableProp<[WooPaymentsDeposit]> = .copy, - lastManualDeposits: CopiableProp<[WooPaymentsManualDeposit]> = .copy - ) -> Networking.WooPaymentsCurrencyDeposits { + lastPaid: CopiableProp<[WooPaymentsPayout]> = .copy, + lastManualPayouts: CopiableProp<[WooPaymentsManualPayout]> = .copy + ) -> Networking.WooPaymentsCurrencyPayouts { let lastPaid = lastPaid ?? self.lastPaid - let lastManualDeposits = lastManualDeposits ?? self.lastManualDeposits + let lastManualPayouts = lastManualPayouts ?? self.lastManualPayouts - return Networking.WooPaymentsCurrencyDeposits( + return Networking.WooPaymentsCurrencyPayouts( lastPaid: lastPaid, - lastManualDeposits: lastManualDeposits + lastManualPayouts: lastManualPayouts ) } } -extension Networking.WooPaymentsDeposit { +extension Networking.WooPaymentsManualPayout { + public func copy( + currency: CopiableProp = .copy, + date: CopiableProp = .copy + ) -> Networking.WooPaymentsManualPayout { + let currency = currency ?? self.currency + let date = date ?? self.date + + return Networking.WooPaymentsManualPayout( + currency: currency, + date: date + ) + } +} + +extension Networking.WooPaymentsPayout { public func copy( id: CopiableProp = .copy, date: CopiableProp = .copy, - type: CopiableProp = .copy, + type: CopiableProp = .copy, amount: CopiableProp = .copy, - status: CopiableProp = .copy, + status: CopiableProp = .copy, bankAccount: NullableCopiableProp = .copy, currency: CopiableProp = .copy, automatic: CopiableProp = .copy, fee: CopiableProp = .copy, feePercentage: CopiableProp = .copy, created: CopiableProp = .copy - ) -> Networking.WooPaymentsDeposit { + ) -> Networking.WooPaymentsPayout { let id = id ?? self.id let date = date ?? self.date let type = type ?? self.type @@ -3939,7 +3954,7 @@ extension Networking.WooPaymentsDeposit { let feePercentage = feePercentage ?? self.feePercentage let created = created ?? self.created - return Networking.WooPaymentsDeposit( + return Networking.WooPaymentsPayout( id: id, date: date, type: type, @@ -3955,17 +3970,17 @@ extension Networking.WooPaymentsDeposit { } } -extension Networking.WooPaymentsDepositsOverview { +extension Networking.WooPaymentsPayoutsOverview { public func copy( - deposit: CopiableProp = .copy, + deposit: CopiableProp = .copy, balance: CopiableProp = .copy, - account: CopiableProp = .copy - ) -> Networking.WooPaymentsDepositsOverview { + account: CopiableProp = .copy + ) -> Networking.WooPaymentsPayoutsOverview { let deposit = deposit ?? self.deposit let balance = balance ?? self.balance let account = account ?? self.account - return Networking.WooPaymentsDepositsOverview( + return Networking.WooPaymentsPayoutsOverview( deposit: deposit, balance: balance, account: account @@ -3973,36 +3988,21 @@ extension Networking.WooPaymentsDepositsOverview { } } -extension Networking.WooPaymentsDepositsSchedule { +extension Networking.WooPaymentsPayoutsSchedule { public func copy( delayDays: CopiableProp = .copy, - interval: CopiableProp = .copy - ) -> Networking.WooPaymentsDepositsSchedule { + interval: CopiableProp = .copy + ) -> Networking.WooPaymentsPayoutsSchedule { let delayDays = delayDays ?? self.delayDays let interval = interval ?? self.interval - return Networking.WooPaymentsDepositsSchedule( + return Networking.WooPaymentsPayoutsSchedule( delayDays: delayDays, interval: interval ) } } -extension Networking.WooPaymentsManualDeposit { - public func copy( - currency: CopiableProp = .copy, - date: CopiableProp = .copy - ) -> Networking.WooPaymentsManualDeposit { - let currency = currency ?? self.currency - let date = date ?? self.date - - return Networking.WooPaymentsManualDeposit( - currency: currency, - date: date - ) - } -} - extension Networking.WooShippingCreatePackageResponse { public func copy( customPackages: CopiableProp<[WooShippingCustomPackage]> = .copy, diff --git a/Networking/Networking/Model/WooPaymentsDepositsOverview.swift b/Networking/Networking/Model/WooPaymentsPayoutsOverview.swift similarity index 77% rename from Networking/Networking/Model/WooPaymentsDepositsOverview.swift rename to Networking/Networking/Model/WooPaymentsPayoutsOverview.swift index a339810e119..8d707737933 100644 --- a/Networking/Networking/Model/WooPaymentsDepositsOverview.swift +++ b/Networking/Networking/Model/WooPaymentsPayoutsOverview.swift @@ -1,42 +1,42 @@ import Foundation import Codegen -public struct WooPaymentsDepositsOverview: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { - public let deposit: WooPaymentsCurrencyDeposits +public struct WooPaymentsPayoutsOverview: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { + public let deposit: WooPaymentsCurrencyPayouts public let balance: WooPaymentsCurrencyBalances - public let account: WooPaymentsAccountDepositSummary + public let account: WooPaymentsAccountPayoutSummary - public init(deposit: WooPaymentsCurrencyDeposits, + public init(deposit: WooPaymentsCurrencyPayouts, balance: WooPaymentsCurrencyBalances, - account: WooPaymentsAccountDepositSummary) { + account: WooPaymentsAccountPayoutSummary) { self.deposit = deposit self.balance = balance self.account = account } } -public struct WooPaymentsCurrencyDeposits: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { - public let lastPaid: [WooPaymentsDeposit] - public let lastManualDeposits: [WooPaymentsManualDeposit] +public struct WooPaymentsCurrencyPayouts: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { + public let lastPaid: [WooPaymentsPayout] + public let lastManualPayouts: [WooPaymentsManualPayout] - public init(lastPaid: [WooPaymentsDeposit], - lastManualDeposits: [WooPaymentsManualDeposit]) { + public init(lastPaid: [WooPaymentsPayout], + lastManualPayouts: [WooPaymentsManualPayout]) { self.lastPaid = lastPaid - self.lastManualDeposits = lastManualDeposits + self.lastManualPayouts = lastManualPayouts } enum CodingKeys: String, CodingKey { case lastPaid = "last_paid" - case lastManualDeposits = "last_manual_deposits" + case lastManualPayouts = "last_manual_deposits" } } -public struct WooPaymentsDeposit: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { +public struct WooPaymentsPayout: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { public let id: String public let date: Date - public let type: WooPaymentsDepositType + public let type: WooPaymentsPayoutType public let amount: Int - public let status: WooPaymentsDepositStatus + public let status: WooPaymentsPayoutStatus public let bankAccount: String? public let currency: String public let automatic: Bool @@ -46,9 +46,9 @@ public struct WooPaymentsDeposit: Codable, GeneratedFakeable, GeneratedCopiable, public init(id: String, date: Date, - type: WooPaymentsDepositType, + type: WooPaymentsPayoutType, amount: Int, - status: WooPaymentsDepositStatus, + status: WooPaymentsPayoutStatus, bankAccount: String?, currency: String, automatic: Bool, @@ -86,9 +86,9 @@ public struct WooPaymentsDeposit: Codable, GeneratedFakeable, GeneratedCopiable, let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(String.self, forKey: .id) self.date = try container.decode(Date.self, forKey: .date) - self.type = try container.decode(WooPaymentsDepositType.self, forKey: .type) + self.type = try container.decode(WooPaymentsPayoutType.self, forKey: .type) self.amount = try container.decode(Int.self, forKey: .amount) - self.status = container.failsafeDecodeIfPresent(WooPaymentsDepositStatus.self, forKey: .status) ?? .unknown + self.status = container.failsafeDecodeIfPresent(WooPaymentsPayoutStatus.self, forKey: .status) ?? .unknown self.bankAccount = try container.decodeIfPresent(String.self, forKey: .bankAccount) self.currency = try container.decode(String.self, forKey: .currency) self.automatic = try container.decode(Bool.self, forKey: .automatic) @@ -98,7 +98,7 @@ public struct WooPaymentsDeposit: Codable, GeneratedFakeable, GeneratedCopiable, } } -public struct WooPaymentsManualDeposit: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { +public struct WooPaymentsManualPayout: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { public let currency: String public let date: Date @@ -132,7 +132,7 @@ public struct WooPaymentsManualDeposit: Codable, GeneratedFakeable, GeneratedCop /// originates from // https://github.com/Automattic/woocommerce-payments-server/blob/899963c61d9ad1c1aa5306087b8bb7ea253e66a0/server/ // wp-content/rest-api-plugins/endpoints/wcpay/class-deposits-controller.php#L753 -public enum WooPaymentsDepositType: String, Codable, Equatable, GeneratedFakeable, GeneratedCopiable { +public enum WooPaymentsPayoutType: String, Codable, Equatable, GeneratedFakeable, GeneratedCopiable { case withdrawal case deposit } @@ -141,7 +141,7 @@ public enum WooPaymentsDepositType: String, Codable, Equatable, GeneratedFakeabl /// with additions in WooPayments e.g. // https://github.com/Automattic/woocommerce-payments-server/blob/899963c61d9ad1c1aa5306087b8bb7ea253e66a0/ // server/wp-content/rest-api-plugins/endpoints/wcpay/utils/class-deposit-utils.php#L141 -public enum WooPaymentsDepositStatus: String, Codable, Equatable, GeneratedFakeable, GeneratedCopiable { +public enum WooPaymentsPayoutStatus: String, Codable, Equatable, GeneratedFakeable, GeneratedCopiable { case estimated case pending case inTransit = "in_transit" @@ -178,35 +178,35 @@ public struct WooPaymentsBalance: Codable, GeneratedFakeable, GeneratedCopiable, } } -public struct WooPaymentsAccountDepositSummary: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { - public let depositsEnabled: Bool - public let depositsBlocked: Bool - public let depositsSchedule: WooPaymentsDepositsSchedule +public struct WooPaymentsAccountPayoutSummary: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { + public let payoutsEnabled: Bool + public let payoutsBlocked: Bool + public let payoutsSchedule: WooPaymentsPayoutsSchedule public let defaultCurrency: String - public init(depositsEnabled: Bool, - depositsBlocked: Bool, - depositsSchedule: WooPaymentsDepositsSchedule, + public init(payoutsEnabled: Bool, + payoutsBlocked: Bool, + payoutsSchedule: WooPaymentsPayoutsSchedule, defaultCurrency: String) { - self.depositsEnabled = depositsEnabled - self.depositsBlocked = depositsBlocked - self.depositsSchedule = depositsSchedule + self.payoutsEnabled = payoutsEnabled + self.payoutsBlocked = payoutsBlocked + self.payoutsSchedule = payoutsSchedule self.defaultCurrency = defaultCurrency } public enum CodingKeys: String, CodingKey { - case depositsEnabled = "deposits_enabled" - case depositsBlocked = "deposits_blocked" - case depositsSchedule = "deposits_schedule" + case payoutsEnabled = "deposits_enabled" + case payoutsBlocked = "deposits_blocked" + case payoutsSchedule = "deposits_schedule" case defaultCurrency = "default_currency" } } -public struct WooPaymentsDepositsSchedule: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { +public struct WooPaymentsPayoutsSchedule: Codable, GeneratedFakeable, GeneratedCopiable, Equatable { public let delayDays: Int - public let interval: WooPaymentsDepositInterval + public let interval: WooPaymentsPayoutInterval - public init(delayDays: Int, interval: WooPaymentsDepositInterval) { + public init(delayDays: Int, interval: WooPaymentsPayoutInterval) { self.delayDays = delayDays self.interval = interval } @@ -221,15 +221,15 @@ public struct WooPaymentsDepositsSchedule: Codable, GeneratedFakeable, Generated public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) delayDays = try container.decode(Int.self, forKey: .delayDays) - let weeklyAnchor = try container.decodeIfPresent(WooPaymentsDepositInterval.Weekday.self, forKey: .weeklyAnchor) + let weeklyAnchor = try container.decodeIfPresent(WooPaymentsPayoutInterval.Weekday.self, forKey: .weeklyAnchor) let monthlyAnchor = try container.decodeIfPresent(Int.self, forKey: .monthlyAnchor) - let intervalKey = try container.decode(WooPaymentsDepositInterval.EnumKey.self, forKey: .interval) + let intervalKey = try container.decode(WooPaymentsPayoutInterval.EnumKey.self, forKey: .interval) - guard let interval = WooPaymentsDepositInterval(key: intervalKey, + guard let interval = WooPaymentsPayoutInterval(key: intervalKey, weeklyAnchor: weeklyAnchor, monthlyAnchor: monthlyAnchor) else { throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.interval], - debugDescription: "Could not decode deposit schedule interval")) + debugDescription: "Could not decode payout schedule interval")) } self.interval = interval } @@ -250,7 +250,7 @@ public struct WooPaymentsDepositsSchedule: Codable, GeneratedFakeable, Generated } /// originally from https://stripe.com/docs/api/accounts/object#account_object-settings-payouts-schedule-interval -public enum WooPaymentsDepositInterval: Equatable, GeneratedFakeable, GeneratedCopiable { +public enum WooPaymentsPayoutInterval: Equatable, GeneratedFakeable, GeneratedCopiable { case daily case weekly(anchor: Weekday) case monthly(anchor: Int) diff --git a/Networking/Networking/Remote/WCPayRemote.swift b/Networking/Networking/Remote/WCPayRemote.swift index 8b5a5508ed0..bec95423ab2 100644 --- a/Networking/Networking/Remote/WCPayRemote.swift +++ b/Networking/Networking/Remote/WCPayRemote.swift @@ -116,17 +116,17 @@ extension WCPayRemote { } } -// MARK: - Deposits +// MARK: - Payouts // extension WCPayRemote { - public func loadDepositsOverview(for siteID: Int64) async throws -> WooPaymentsDepositsOverview { + public func loadPayoutsOverview(for siteID: Int64) async throws -> WooPaymentsPayoutsOverview { let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, - path: Path.depositsOverview, + path: Path.payoutsOverview, availableAsRESTRequest: true) - let mapper = WooPaymentsDepositsOverviewMapper() + let mapper = WooPaymentsPayoutsOverviewMapper() return try await enqueue(request, mapper: mapper) } @@ -143,7 +143,7 @@ private extension WCPayRemote { static let createCustomer = "create_customer" static let locations = "payments/terminal/locations/store" static let charges = "payments/charges" - static let depositsOverview = "payments/deposits/overview-all" + static let payoutsOverview = "payments/deposits/overview-all" } enum AccountParameterKeys { diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift index 4cc26b8db2b..08e052016bb 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift @@ -2321,26 +2321,26 @@ extension WooAnalyticsEvent { } } -// MARK: - Deposit Summary +// MARK: - Payout Summary // extension WooAnalyticsEvent { - enum DepositSummary { + enum PayoutSummary { enum Keys { static let numberOfCurrencies = "number_of_currencies" static let currency = "currency" } - static func depositSummaryShown(numberOfCurrencies: Int) -> WooAnalyticsEvent { - WooAnalyticsEvent(statName: .paymentsMenuDepositSummaryShown, + static func payoutSummaryShown(numberOfCurrencies: Int) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .paymentsMenuPayoutSummaryShown, properties: [Keys.numberOfCurrencies: numberOfCurrencies]) } - static func depositSummaryError(error: Error) -> WooAnalyticsEvent { - WooAnalyticsEvent(statName: .paymentsMenuDepositSummaryError, properties: [:], error: error) + static func payoutSummaryError(error: Error) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .paymentsMenuPayoutSummaryError, properties: [:], error: error) } - static func depositSummaryCurrencySelected(currency: CurrencyCode) -> WooAnalyticsEvent { - WooAnalyticsEvent(statName: .paymentsMenuDepositSummaryCurrencySelected, + static func payoutSummaryCurrencySelected(currency: CurrencyCode) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .paymentsMenuPayoutSummaryCurrencySelected, properties: [Keys.currency: currency.rawValue]) } } diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift index 0a320e9cfc8..602089a15dc 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift @@ -1131,11 +1131,11 @@ enum WooAnalyticsStat: String { case inPersonPaymentsLearnMoreTapped = "in_person_payments_learn_more_tapped" case setUpTryOutTapToPayOnIPhoneTapped = "payments_hub_tap_to_pay_tapped" case aboutTapToPayOnIPhoneTapped = "payments_hub_tap_to_pay_about_tapped" - case paymentsMenuDepositSummaryShown = "payments_hub_deposit_summary_shown" - case paymentsMenuDepositSummaryError = "payments_hub_deposit_summary_error" - case paymentsMenuDepositSummaryExpanded = "payments_hub_deposit_summary_expanded" - case paymentsMenuDepositSummaryLearnMoreTapped = "payments_hub_deposit_summary_learn_more_clicked" - case paymentsMenuDepositSummaryCurrencySelected = "payments_hub_deposit_summary_currency_selected" + case paymentsMenuPayoutSummaryShown = "payments_hub_deposit_summary_shown" + case paymentsMenuPayoutSummaryError = "payments_hub_deposit_summary_error" + case paymentsMenuPayoutSummaryExpanded = "payments_hub_deposit_summary_expanded" + case paymentsMenuPayoutSummaryLearnMoreTapped = "payments_hub_deposit_summary_learn_more_clicked" + case paymentsMenuPayoutSummaryCurrencySelected = "payments_hub_deposit_summary_currency_selected" // MARK: Payments Menu case pluginsNotSyncedYet = "plugins_not_synced_yet" diff --git a/WooCommerce/Classes/System/WooConstants.swift b/WooCommerce/Classes/System/WooConstants.swift index dd20aa6ef92..2597c721248 100644 --- a/WooCommerce/Classes/System/WooConstants.swift +++ b/WooCommerce/Classes/System/WooConstants.swift @@ -350,7 +350,7 @@ extension WooConstants { case wooCorePaymentOptions = "https://woocommerce.com/documentation/woocommerce/getting-started/sell-products/core-payment-options" - case wooPaymentsDepositSchedule = "https://woocommerce.com/document/woopayments/deposits/deposit-schedule/" + case wooPaymentsPayoutSchedule = "https://woocommerce.com/document/woopayments/payouts/payout-schedule/" /// URL to learn more about Jetpack Stats /// diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsOverviewViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsOverviewViewModel.swift deleted file mode 100644 index fdd11e2847f..00000000000 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsOverviewViewModel.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import protocol WooFoundation.Analytics - -final class WooPaymentsDepositsOverviewViewModel: ObservableObject { - @Published var currencyViewModels: [WooPaymentsDepositsCurrencyOverviewViewModel] - - let analytics: Analytics - - init(currencyViewModels: [WooPaymentsDepositsCurrencyOverviewViewModel], - analytics: Analytics = ServiceLocator.analytics) { - self.currencyViewModels = currencyViewModels - self.analytics = analytics - } - - func onAppear() { - analytics.track(event: .DepositSummary.depositSummaryShown(numberOfCurrencies: currencyViewModels.count)) - } - - func currencySelected(currencyViewModel: WooPaymentsDepositsCurrencyOverviewViewModel) { - analytics.track(event: .DepositSummary.depositSummaryCurrencySelected(currency: currencyViewModel.currency)) - } -} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositStatusDisplayDetails.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutStatusDisplayDetails.swift similarity index 78% rename from WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositStatusDisplayDetails.swift rename to WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutStatusDisplayDetails.swift index 0076644a4f2..0f427a1614a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositStatusDisplayDetails.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutStatusDisplayDetails.swift @@ -2,7 +2,7 @@ import SwiftUI import Yosemite import WooFoundation -extension WooPaymentsDepositStatus { +extension WooPaymentsPayoutStatus { var backgroundColor: Color { switch self { case .estimated: @@ -75,41 +75,41 @@ extension WooPaymentsDepositStatus { } } -private extension WooPaymentsDepositStatus { +private extension WooPaymentsPayoutStatus { enum Localization { static let estimated = NSLocalizedString( - "deposits.currency.overview.depositTable.status.estimated.title", + "payouts.currency.overview.payoutTable.status.estimated.title", value: "Estimated", - comment: "A status for a deposit, shown in a small badge view") + comment: "A status for a payout, shown in a small badge view") static let pending = NSLocalizedString( - "deposits.currency.overview.depositTable.status.pending.title", + "payouts.currency.overview.payoutTable.status.pending.title", value: "Pending", - comment: "A status for a deposit, shown in a small badge view") + comment: "A status for a payout, shown in a small badge view") static let inTransit = NSLocalizedString( - "deposits.currency.overview.depositTable.status.inTransit.title", + "payouts.currency.overview.payoutTable.status.inTransit.title", value: "In Transit", - comment: "A status for a deposit, shown in a small badge view") + comment: "A status for a payout, shown in a small badge view") static let paid = NSLocalizedString( - "deposits.currency.overview.depositTable.status.paid.title", + "payouts.currency.overview.payoutTable.status.paid.title", value: "Paid", - comment: "A status for a deposit, shown in a small badge view") + comment: "A status for a payout, shown in a small badge view") static let canceled = NSLocalizedString( - "deposits.currency.overview.depositTable.status.canceled.title", + "payouts.currency.overview.payoutTable.status.canceled.title", value: "Canceled", - comment: "A status for a deposit, shown in a small badge view") + comment: "A status for a payout, shown in a small badge view") static let failed = NSLocalizedString( - "deposits.currency.overview.depositTable.status.failed.title", + "payouts.currency.overview.payoutTable.status.failed.title", value: "Failed", - comment: "A status for a deposit, shown in a small badge view") + comment: "A status for a payout, shown in a small badge view") static let unknown = NSLocalizedString( - "deposits.currency.overview.depositTable.status.unknown.title", + "payouts.currency.overview.payoutTable.status.unknown.title", value: "Unknown", - comment: "A status for a deposit, shown in a small badge view") + comment: "A status for a payout, shown in a small badge view") } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsCurrencyOverviewView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsCurrencyOverviewView.swift similarity index 79% rename from WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsCurrencyOverviewView.swift rename to WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsCurrencyOverviewView.swift index acb67fb45a6..bd02d440741 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsCurrencyOverviewView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsCurrencyOverviewView.swift @@ -2,14 +2,14 @@ import SwiftUI import Yosemite @available(iOS 16.0, *) -struct WooPaymentsDepositsCurrencyOverviewView: View { - @ObservedObject var viewModel: WooPaymentsDepositsCurrencyOverviewViewModel +struct WooPaymentsPayoutsCurrencyOverviewView: View { + @ObservedObject var viewModel: WooPaymentsPayoutsCurrencyOverviewViewModel @Binding var isExpanded: Bool - @State private var showDepositSummaryInfo: Bool = false + @State private var showPayoutSummaryInfo: Bool = false - init(viewModel: WooPaymentsDepositsCurrencyOverviewViewModel, + init(viewModel: WooPaymentsPayoutsCurrencyOverviewViewModel, isExpanded: Binding) { self.viewModel = viewModel self._isExpanded = isExpanded @@ -23,10 +23,10 @@ struct WooPaymentsDepositsCurrencyOverviewView: View { AccountSummaryItem(title: Localization.pendingFunds, amount: viewModel.pendingBalance) isExpanded ? Image(systemName: "chevron.up") .accessibilityAddTraits(.isButton) - .accessibilityLabel(Text(Localization.hideDepositDetailAccessibilityLabel)) : + .accessibilityLabel(Text(Localization.hidePayoutDetailAccessibilityLabel)) : Image(systemName: "chevron.down") .accessibilityAddTraits(.isButton) - .accessibilityLabel(Text(Localization.showDepositDetailAccessibilityLabel)) + .accessibilityLabel(Text(Localization.showPayoutDetailAccessibilityLabel)) } .contentShape(Rectangle()) @@ -47,7 +47,7 @@ struct WooPaymentsDepositsCurrencyOverviewView: View { } if isExpanded { - Text(Localization.lastDepositHeader.localizedUppercase) + Text(Localization.lastPayoutHeader.localizedUppercase) .font(.subheadline) .foregroundColor(.secondary) .frame(maxWidth: .infinity, alignment: .leading) @@ -57,17 +57,17 @@ struct WooPaymentsDepositsCurrencyOverviewView: View { HStack { Image(systemName: "calendar") .accessibilityHidden(true) - Text(viewModel.lastDepositDate) + Text(viewModel.lastPayoutDate) .foregroundColor(.primary) } - WooPaymentsDepositsBadge(status: viewModel.lastDepositStatus) + WooPaymentsPayoutsBadge(status: viewModel.lastPayoutStatus) Spacer() - Text(viewModel.lastDepositAmount) + Text(viewModel.lastPayoutAmount) .foregroundColor(.primary) } HStack(alignment: .top) { - Text(viewModel.depositScheduleHint) + Text(viewModel.payoutScheduleHint) .font(.footnote) } .foregroundColor(.secondary) @@ -119,8 +119,8 @@ struct AccountSummaryItem: View { } } -struct WooPaymentsDepositsBadge: View { - let status: WooPaymentsDepositStatus +struct WooPaymentsPayoutsBadge: View { + let status: WooPaymentsPayoutStatus var body: some View { Text(status.localizedName) @@ -131,7 +131,7 @@ struct WooPaymentsDepositsBadge: View { } } -private extension WooPaymentsDepositsBadge { +private extension WooPaymentsPayoutsBadge { enum Layout { static let padding: CGFloat = 8.0 static let cornerRadius: CGFloat = 8.0 @@ -139,7 +139,7 @@ private extension WooPaymentsDepositsBadge { } @available(iOS 16.0, *) -private extension WooPaymentsDepositsCurrencyOverviewView { +private extension WooPaymentsPayoutsCurrencyOverviewView { enum Layout { static let padding: CGFloat = 8.0 static let elementSpacing: CGFloat = 16.0 @@ -147,7 +147,7 @@ private extension WooPaymentsDepositsCurrencyOverviewView { } @available(iOS 16.0, *) -private extension WooPaymentsDepositsCurrencyOverviewView { +private extension WooPaymentsPayoutsCurrencyOverviewView { enum Localization { static let availableFunds = NSLocalizedString( "payouts.currency.overview.availableFunds", @@ -159,7 +159,7 @@ private extension WooPaymentsDepositsCurrencyOverviewView { value: "Pending funds", comment: "Title for pending funds overview in WooPayments Payouts view. " + "This shows the balance which will be made available for pay out later.") - static let lastDepositHeader = NSLocalizedString( + static let lastPayoutHeader = NSLocalizedString( "payouts.currency.overview.lastPayout", value: "Last Payout", comment: "Section header for the last payout in the WooPayments Payouts overview") @@ -167,11 +167,11 @@ private extension WooPaymentsDepositsCurrencyOverviewView { "payouts.currency.overview.learnMore", value: "Learn more about when you'll receive your funds", comment: "Button text to view more about payment schedules on the WooPayments Payouts View.") - static let showDepositDetailAccessibilityLabel = NSLocalizedString( + static let showPayoutDetailAccessibilityLabel = NSLocalizedString( "payouts.currency.overview.accessibility.show", value: "Show payout details", comment: "Accessibility label for the expand chevron on the Payout summary") - static let hideDepositDetailAccessibilityLabel = NSLocalizedString( + static let hidePayoutDetailAccessibilityLabel = NSLocalizedString( "payouts.currency.overview.accessibility.hide", value: "Hide payout details", comment: "Accessibility label for the collapse chevron on the Payout summary") @@ -179,15 +179,15 @@ private extension WooPaymentsDepositsCurrencyOverviewView { } @available(iOS 16.0, *) -struct WooPaymentsDepositsCurrencyOverviewView_Previews: PreviewProvider { +struct WooPaymentsPayoutsCurrencyOverviewView_Previews: PreviewProvider { static var previews: some View { - let overviewData = WooPaymentsDepositsOverviewByCurrency( + let overviewData = WooPaymentsPayoutsOverviewByCurrency( currency: .GBP, - automaticDeposits: true, - depositInterval: .daily, + automaticPayouts: true, + payoutInterval: .daily, pendingBalanceAmount: 1000.0, - pendingDepositDays: 2, - lastDeposit: WooPaymentsDepositsOverviewByCurrency.LastDeposit( + pendingPayoutDays: 2, + lastPayout: WooPaymentsPayoutsOverviewByCurrency.LastPayout( amount: 500.0, date: Date(), status: .inTransit @@ -195,9 +195,9 @@ struct WooPaymentsDepositsCurrencyOverviewView_Previews: PreviewProvider { availableBalance: 1500.0 ) - let viewModel = WooPaymentsDepositsCurrencyOverviewViewModel(overview: overviewData) + let viewModel = WooPaymentsPayoutsCurrencyOverviewViewModel(overview: overviewData) - return WooPaymentsDepositsCurrencyOverviewView(viewModel: viewModel, + return WooPaymentsPayoutsCurrencyOverviewView(viewModel: viewModel, isExpanded: .constant(true)) .previewLayout(.sizeThatFits) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsCurrencyOverviewViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsCurrencyOverviewViewModel.swift similarity index 72% rename from WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsCurrencyOverviewViewModel.swift rename to WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsCurrencyOverviewViewModel.swift index eb335c47d8f..00b7d5c2f1b 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsCurrencyOverviewViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsCurrencyOverviewViewModel.swift @@ -2,13 +2,13 @@ import Foundation import Yosemite import WooFoundation -final class WooPaymentsDepositsCurrencyOverviewViewModel: ObservableObject { - private let overview: WooPaymentsDepositsOverviewByCurrency +final class WooPaymentsPayoutsCurrencyOverviewViewModel: ObservableObject { + private let overview: WooPaymentsPayoutsOverviewByCurrency private let analytics: Analytics private let currencySettings: CurrencySettings? private let locale: Locale - init(overview: WooPaymentsDepositsOverviewByCurrency, + init(overview: WooPaymentsPayoutsOverviewByCurrency, analytics: Analytics = ServiceLocator.analytics, siteCurrencySettings: CurrencySettings = ServiceLocator.currencySettings, locale: Locale = Locale.current) { @@ -29,20 +29,20 @@ final class WooPaymentsDepositsCurrencyOverviewViewModel: ObservableObject { private func setupProperties() { pendingBalance = formatAmount(overview.pendingBalanceAmount) - lastDepositAmount = formatAmount(overview.lastDeposit?.amount ?? NSDecimalNumber(value: 0)) - lastDepositDate = formatDate(overview.lastDeposit?.date) ?? Localization.noDateString - lastDepositStatus = overview.lastDeposit?.status ?? .unknown + lastPayoutAmount = formatAmount(overview.lastPayout?.amount ?? NSDecimalNumber(value: 0)) + lastPayoutDate = formatDate(overview.lastPayout?.date) ?? Localization.noDateString + lastPayoutStatus = overview.lastPayout?.status ?? .unknown availableBalance = formatAmount(overview.availableBalance) - depositScheduleHint = depositScheduleHintText() + payoutScheduleHint = payoutScheduleHintText() balanceTypeHint = balanceTypeHintText() } @Published var pendingBalance: String = "" - @Published var lastDepositAmount: String = "" - @Published var lastDepositDate: String = "" - @Published var lastDepositStatus: WooPaymentsDepositStatus = .unknown + @Published var lastPayoutAmount: String = "" + @Published var lastPayoutDate: String = "" + @Published var lastPayoutStatus: WooPaymentsPayoutStatus = .unknown @Published var availableBalance: String = "" - @Published var depositScheduleHint: String = "" + @Published var payoutScheduleHint: String = "" @Published var balanceTypeHint: String = "" @Published var showWebviewURL: URL? = nil @Published var currency: CurrencyCode @@ -56,17 +56,17 @@ final class WooPaymentsDepositsCurrencyOverviewViewModel: ObservableObject { } } - private func depositScheduleHintText() -> String { - if overview.automaticDeposits { - return String(format: Localization.depositScheduleHintAutomatic, - overview.depositInterval.frequencyDescriptionEvery) + private func payoutScheduleHintText() -> String { + if overview.automaticPayouts { + return String(format: Localization.payoutScheduleHintAutomatic, + overview.payoutInterval.frequencyDescriptionEvery) } else { - return Localization.depositScheduleHintManual + return Localization.payoutScheduleHintManual } } private func balanceTypeHintText() -> String { - String(format: Localization.balanceTypeHint, overview.pendingDepositDays) + String(format: Localization.balanceTypeHint, overview.pendingPayoutDays) } private func formatDate(_ date: Date?) -> String? { @@ -95,17 +95,17 @@ final class WooPaymentsDepositsCurrencyOverviewViewModel: ObservableObject { func expandTapped(expanded: Bool) { if expanded { - analytics.track(.paymentsMenuDepositSummaryExpanded) + analytics.track(.paymentsMenuPayoutSummaryExpanded) } } func learnMoreTapped() { - showWebviewURL = WooConstants.URLs.wooPaymentsDepositSchedule.asURL() - analytics.track(.paymentsMenuDepositSummaryLearnMoreTapped) + showWebviewURL = WooConstants.URLs.wooPaymentsPayoutSchedule.asURL() + analytics.track(.paymentsMenuPayoutSummaryLearnMoreTapped) } } -private extension WooPaymentsDepositInterval { +private extension WooPaymentsPayoutInterval { var frequencyDescriptionEvery: String { switch self { case .daily: @@ -128,43 +128,43 @@ private extension WooPaymentsDepositInterval { enum Localization { static let dailyFrequency = NSLocalizedString( "every day", - comment: "Shown in a sentence like 'Available funds are deposited automatically, every day'") + comment: "Shown in a sentence like 'Available funds are paid out automatically, every day'") static let weeklyFrequency = NSLocalizedString( "every %1$@", - comment: "every {dayname}, shown in a sentence like 'Available funds are deposited automatically, every Wednesday' " + + comment: "every {dayname}, shown in a sentence like 'Available funds are paid out automatically, every Wednesday' " + "%1$@ will be replaced with the localized day name") static let monthlyFrequencyWithDate = NSLocalizedString( "every month on the %1$@", - comment: "Shown in a sentence like 'Available funds are deposited automatically, every month on the 15th'") + comment: "Shown in a sentence like 'Available funds are paid out automatically, every month on the 15th'") static let fallbackMonthlyFrequency = NSLocalizedString( "every month", - comment: "Shown in a sentence like 'Available funds are deposited automatically every month.") + comment: "Shown in a sentence like 'Available funds are paid out automatically every month.") static let manualFrequency = NSLocalizedString( "manually, on request", - comment: "on request (lower case), shown in a sentence like 'Deposit schedule: manual, on request'") + comment: "on request (lower case), shown in a sentence like 'Payout schedule: manual, on request'") } } -private extension WooPaymentsDepositsCurrencyOverviewViewModel { +private extension WooPaymentsPayoutsCurrencyOverviewViewModel { enum Localization { static let balanceTypeHint = NSLocalizedString( "Funds become available after pending for %1$d days.", - comment: "Hint regarding available/pending balances shown in the WooPayments Deposits View" + + comment: "Hint regarding available/pending balances shown in the WooPayments Payouts View" + "%1$d will be replaced by the number of days balances pend, and will be one of 2/4/5/7.") - static let depositScheduleHintAutomatic = NSLocalizedString( + static let payoutScheduleHintAutomatic = NSLocalizedString( "Available funds are paid out automatically, %1$@.", comment: "Hint showing the payout schedule for a merchant's WooPayments account. " + "e.g. Available funds are paid out automatically, every Wednesday. " + "%1$@ will be replaced with a translated frequency description, e.g. 'every day' or 'monthly on the 28th'") - static let depositScheduleHintManual = NSLocalizedString( + static let payoutScheduleHintManual = NSLocalizedString( "Available funds are paid out manually, on request.", comment: "Hint showing the payout schedule for a merchant's WooPayments account with a manual schedule.") static let noDateString = NSLocalizedString( "N/A", - comment: "String used when there's no date available for a deposit type on the WooPayments Deposits View.") + comment: "String used when there's no date available for a payout type on the WooPayments Payouts View.") static let estimatedDateString = NSLocalizedString( "Est. %1$@", - comment: "String indicating that a deposit date is an estimate. Shown on whe WooPayments Deposits View. " + + comment: "String indicating that a payout date is an estimate. Shown on whe WooPayments Payouts View. " + "%1$@ will be replaced with a locale-appropriate date string.") } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsOverviewView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsOverviewView.swift similarity index 51% rename from WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsOverviewView.swift rename to WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsOverviewView.swift index 3985841987a..93ce05b444a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsOverviewView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsOverviewView.swift @@ -2,16 +2,16 @@ import SwiftUI import Yosemite @available(iOS 16.0, *) -struct WooPaymentsDepositsOverviewView: View { - @ObservedObject var viewModel: WooPaymentsDepositsOverviewViewModel +struct WooPaymentsPayoutsOverviewView: View { + @ObservedObject var viewModel: WooPaymentsPayoutsOverviewViewModel @State var isExpanded: Bool = false - var tabs: [TopTabItem] { + var tabs: [TopTabItem] { viewModel.currencyViewModels.map { currencyViewModel in TopTabItem(name: currencyViewModel.tabTitle, content: { - WooPaymentsDepositsCurrencyOverviewView(viewModel: currencyViewModel, + WooPaymentsPayoutsCurrencyOverviewView(viewModel: currencyViewModel, isExpanded: $isExpanded) }, onSelected: { viewModel.currencySelected(currencyViewModel: currencyViewModel) @@ -30,15 +30,15 @@ struct WooPaymentsDepositsOverviewView: View { } @available(iOS 16.0, *) -struct WooPaymentsDepositsOverviewView_Previews: PreviewProvider { +struct WooPaymentsPayoutsOverviewView_Previews: PreviewProvider { static var previews: some View { - let overviewData = WooPaymentsDepositsOverviewByCurrency( + let overviewData = WooPaymentsPayoutsOverviewByCurrency( currency: .GBP, - automaticDeposits: true, - depositInterval: .daily, + automaticPayouts: true, + payoutInterval: .daily, pendingBalanceAmount: 1000.0, - pendingDepositDays: 7, - lastDeposit: WooPaymentsDepositsOverviewByCurrency.LastDeposit( + pendingPayoutDays: 7, + lastPayout: WooPaymentsPayoutsOverviewByCurrency.LastPayout( amount: 500.0, date: Date(), status: .inTransit @@ -46,15 +46,15 @@ struct WooPaymentsDepositsOverviewView_Previews: PreviewProvider { availableBalance: 1500.0 ) - let viewModel1 = WooPaymentsDepositsCurrencyOverviewViewModel(overview: overviewData) + let viewModel1 = WooPaymentsPayoutsCurrencyOverviewViewModel(overview: overviewData) - let overviewData2 = WooPaymentsDepositsOverviewByCurrency( + let overviewData2 = WooPaymentsPayoutsOverviewByCurrency( currency: .EUR, - automaticDeposits: true, - depositInterval: .daily, + automaticPayouts: true, + payoutInterval: .daily, pendingBalanceAmount: 200.0, - pendingDepositDays: 7, - lastDeposit: WooPaymentsDepositsOverviewByCurrency.LastDeposit( + pendingPayoutDays: 7, + lastPayout: WooPaymentsPayoutsOverviewByCurrency.LastPayout( amount: 600.0, date: Date(), status: .canceled @@ -62,8 +62,8 @@ struct WooPaymentsDepositsOverviewView_Previews: PreviewProvider { availableBalance: 1900.0 ) - let viewModel2 = WooPaymentsDepositsCurrencyOverviewViewModel(overview: overviewData2) + let viewModel2 = WooPaymentsPayoutsCurrencyOverviewViewModel(overview: overviewData2) - WooPaymentsDepositsOverviewView(viewModel: .init(currencyViewModels: [viewModel1, viewModel2])) + WooPaymentsPayoutsOverviewView(viewModel: .init(currencyViewModels: [viewModel1, viewModel2])) } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsOverviewViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsOverviewViewModel.swift new file mode 100644 index 00000000000..b25eeda0fd9 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsOverviewViewModel.swift @@ -0,0 +1,22 @@ +import Foundation +import protocol WooFoundation.Analytics + +final class WooPaymentsPayoutsOverviewViewModel: ObservableObject { + @Published var currencyViewModels: [WooPaymentsPayoutsCurrencyOverviewViewModel] + + let analytics: Analytics + + init(currencyViewModels: [WooPaymentsPayoutsCurrencyOverviewViewModel], + analytics: Analytics = ServiceLocator.analytics) { + self.currencyViewModels = currencyViewModels + self.analytics = analytics + } + + func onAppear() { + analytics.track(event: .PayoutSummary.payoutSummaryShown(numberOfCurrencies: currencyViewModels.count)) + } + + func currencySelected(currencyViewModel: WooPaymentsPayoutsCurrencyOverviewViewModel) { + analytics.track(event: .PayoutSummary.payoutSummaryCurrencySelected(currency: currencyViewModel.currency)) + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Payments Menu/InPersonPaymentsMenu.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Payments Menu/InPersonPaymentsMenu.swift index 09c1a0e7b2e..982f12d41a5 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Payments Menu/InPersonPaymentsMenu.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Payments Menu/InPersonPaymentsMenu.swift @@ -9,7 +9,7 @@ struct InPersonPaymentsMenu: View { VStack { ScrollView { VStack(spacing: 0) { - depositSummary + payoutSummary .background { Color(UIColor.systemBackground) .ignoresSafeArea() @@ -247,31 +247,31 @@ struct InPersonPaymentsMenu: View { } @ViewBuilder - var depositSummary: some View { + var payoutSummary: some View { if #available(iOS 16.0, *), - viewModel.shouldShowDepositSummary { - if viewModel.isLoadingDepositSummary { - WooPaymentsDepositsOverviewView(viewModel: depositSummaryLoadingViewModel) + viewModel.shouldShowPayoutSummary { + if viewModel.isLoadingPayoutSummary { + WooPaymentsPayoutsOverviewView(viewModel: payoutSummaryLoadingViewModel) .redacted(reason: .placeholder) .shimmering() .accessibilityElement(children: .combine) - .accessibilityLabel(Localization.loadingDepositSummaryAccessibilityLabel) - } else if let depositViewModel = viewModel.depositViewModel { - WooPaymentsDepositsOverviewView(viewModel: depositViewModel) + .accessibilityLabel(Localization.loadingPayoutSummaryAccessibilityLabel) + } else if let payoutViewModel = viewModel.payoutViewModel { + WooPaymentsPayoutsOverviewView(viewModel: payoutViewModel) } } else { EmptyView() } } - private var depositSummaryLoadingViewModel: WooPaymentsDepositsOverviewViewModel { + private var payoutSummaryLoadingViewModel: WooPaymentsPayoutsOverviewViewModel { .init(currencyViewModels: [.init(overview: .init( currency: .AED, - automaticDeposits: false, - depositInterval: .daily, + automaticPayouts: false, + payoutInterval: .daily, pendingBalanceAmount: .zero, - pendingDepositDays: 0, - lastDeposit: nil, + pendingPayoutDays: 0, + lastPayout: nil, availableBalance: .zero))]) } } @@ -302,20 +302,20 @@ private extension InPersonPaymentsMenu { value: "Settings", comment: "Title for the section related to changing payment settings inside the In-Person Payments menu") - static let wooPaymentsDepositsSectionTitle = NSLocalizedString( - "menu.payments.wooPaymentsDeposits.section.title", + static let wooPaymentsPayoutsSectionTitle = NSLocalizedString( + "menu.payments.wooPaymentsPayouts.section.title", value: "Woo Payments Balance", - comment: "Title for the section related to Woo Payments Deposits/Balances.") + comment: "Title for the section related to Woo Payments Payouts/Balances.") static let tapToPaySectionTitle = NSLocalizedString( "menu.payments.tapToPay.section.title", value: "Tap to Pay", comment: "Title for the Tap to Pay section in the In-Person payments settings") - static let wooPaymentsDeposits = NSLocalizedString( - "menu.payments.wooPaymentsDeposits.row.title", + static let wooPaymentsPayouts = NSLocalizedString( + "menu.payments.wooPaymentsPayouts.row.title", value: "Woo Payments Balance", - comment: "Title for the row related to Woo Payments Deposits/Balances.") + comment: "Title for the row related to Woo Payments Payouts/Balances.") static let purchaseCardReader = NSLocalizedString( "menu.payments.cardReader.purchase.row.title", @@ -367,7 +367,7 @@ private extension InPersonPaymentsMenu { ).localizedCapitalized static let done = NSLocalizedString( - "menu.payments.wooPaymentsDeposits.navigation.done.button.title", + "menu.payments.wooPaymentsPayouts.navigation.done.button.title", value: "Done", comment: "Title for a done button in the navigation bar") @@ -392,8 +392,8 @@ private extension InPersonPaymentsMenu { """ ) - static let loadingDepositSummaryAccessibilityLabel = NSLocalizedString( - "menu.payments.depositSummary.loading.accessibilityLabel", + static let loadingPayoutSummaryAccessibilityLabel = NSLocalizedString( + "menu.payments.payoutSummary.loading.accessibilityLabel", value: "Loading balances...", comment: "An accessibility label used when the balances are loading on the payments menu" ) @@ -412,7 +412,7 @@ struct InPersonPaymentsMenu_Previews: PreviewProvider { cardPresentPaymentsConfiguration: .init(country: .US), onboardingUseCase: CardPresentPaymentsOnboardingUseCase(), cardReaderSupportDeterminer: CardReaderSupportDeterminer(siteID: 0), - wooPaymentsDepositService: WooPaymentsDepositService(siteID: 0, credentials: .init(authToken: ""))), + wooPaymentsPayoutService: WooPaymentsPayoutService(siteID: 0, credentials: .init(authToken: ""))), navigationPath: .constant(NavigationPath())) static var previews: some View { NavigationStack { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Payments Menu/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Payments Menu/InPersonPaymentsMenuViewModel.swift index a06b564f838..2297a4691db 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Payments Menu/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/Payments Menu/InPersonPaymentsMenuViewModel.swift @@ -13,7 +13,7 @@ final class InPersonPaymentsMenuViewModel: ObservableObject { @Published private(set) var shouldShowTapToPaySection: Bool = true @Published private(set) var shouldShowCardReaderSection: Bool = true @Published private(set) var shouldShowPaymentOptionsSection: Bool = false - @Published private(set) var shouldShowDepositSummary: Bool = false + @Published private(set) var shouldShowPayoutSummary: Bool = false @Published private(set) var setUpTryOutTapToPayRowTitle: String = Localization.setUpTapToPayOnIPhoneRowTitle @Published private(set) var shouldShowTapToPayFeedbackRow: Bool = true @Published private(set) var shouldBadgeTapToPayOnIPhone: Bool = false @@ -39,8 +39,8 @@ final class InPersonPaymentsMenuViewModel: ObservableObject { @Published var presentCardReaderManuals: Bool = false @Published var safariSheetURL: URL? = nil @Published var presentSupport: Bool = false - @Published var depositViewModel: WooPaymentsDepositsOverviewViewModel? = nil - @Published var isLoadingDepositSummary: Bool = false + @Published var payoutViewModel: WooPaymentsPayoutsOverviewViewModel? = nil + @Published var isLoadingPayoutSummary: Bool = false var shouldAlwaysHideSetUpButtonOnAboutTapToPay: Bool = false @@ -59,7 +59,7 @@ final class InPersonPaymentsMenuViewModel: ObservableObject { let onboardingUseCase: CardPresentPaymentsOnboardingUseCaseProtocol let cardReaderSupportDeterminer: CardReaderSupportDetermining let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker - let wooPaymentsDepositService: WooPaymentsDepositServiceProtocol? + let wooPaymentsPayoutService: WooPaymentsPayoutServiceProtocol? let analytics: Analytics let systemStatusService: SystemStatusServiceProtocol let noticePresenter: NoticePresenter @@ -69,7 +69,7 @@ final class InPersonPaymentsMenuViewModel: ObservableObject { onboardingUseCase: CardPresentPaymentsOnboardingUseCaseProtocol, cardReaderSupportDeterminer: CardReaderSupportDetermining, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker = TapToPayBadgePromotionChecker(), - wooPaymentsDepositService: WooPaymentsDepositServiceProtocol?, + wooPaymentsPayoutService: WooPaymentsPayoutServiceProtocol?, systemStatusService: SystemStatusServiceProtocol = SystemStatusService(stores: ServiceLocator.stores), analytics: Analytics = ServiceLocator.analytics, noticePresenter: NoticePresenter = ServiceLocator.noticePresenter, @@ -78,7 +78,7 @@ final class InPersonPaymentsMenuViewModel: ObservableObject { self.onboardingUseCase = onboardingUseCase self.cardReaderSupportDeterminer = cardReaderSupportDeterminer self.tapToPayBadgePromotionChecker = tapToPayBadgePromotionChecker - self.wooPaymentsDepositService = wooPaymentsDepositService + self.wooPaymentsPayoutService = wooPaymentsPayoutService self.systemStatusService = systemStatusService self.analytics = analytics self.noticePresenter = noticePresenter @@ -135,33 +135,33 @@ final class InPersonPaymentsMenuViewModel: ObservableObject { payInPersonToggleViewModel.refreshState() updateCardReadersSection() await updateTapToPaySection() - await refreshDepositSummary() + await refreshPayoutSummary() } - private func refreshDepositSummary() async { - guard ServiceLocator.featureFlagService.isFeatureFlagEnabled(.wooPaymentsDepositsOverviewInPaymentsMenu), - let depositService = dependencies.wooPaymentsDepositService, + private func refreshPayoutSummary() async { + guard ServiceLocator.featureFlagService.isFeatureFlagEnabled(.wooPaymentsPayoutsOverviewInPaymentsMenu), + let payoutService = dependencies.wooPaymentsPayoutService, await dependencies.systemStatusService.fetchSystemPluginWithPath(siteID: siteID, pluginPath: WooConstants.wooPaymentsPluginPath) != nil else { - shouldShowDepositSummary = false + shouldShowPayoutSummary = false return } - shouldShowDepositSummary = true + shouldShowPayoutSummary = true do { - if depositViewModel == nil { - isLoadingDepositSummary = true + if payoutViewModel == nil { + isLoadingPayoutSummary = true } - let depositCurrencyViewModels = try await depositService.fetchDepositsOverview().map({ - WooPaymentsDepositsCurrencyOverviewViewModel(overview: $0) + let payoutCurrencyViewModels = try await payoutService.fetchPayoutsOverview().map({ + WooPaymentsPayoutsCurrencyOverviewViewModel(overview: $0) }) - isLoadingDepositSummary = false - depositViewModel = WooPaymentsDepositsOverviewViewModel(currencyViewModels: depositCurrencyViewModels) + isLoadingPayoutSummary = false + payoutViewModel = WooPaymentsPayoutsOverviewViewModel(currencyViewModels: payoutCurrencyViewModels) } catch { - shouldShowDepositSummary = false - isLoadingDepositSummary = false - analytics.track(event: .DepositSummary.depositSummaryError(error: error)) + shouldShowPayoutSummary = false + isLoadingPayoutSummary = false + analytics.track(event: .PayoutSummary.payoutSummaryError(error: error)) } } diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index c702f8f22e4..9d296aa70f7 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -128,7 +128,7 @@ final class HubMenuViewModel: ObservableObject { cardPresentPaymentsConfiguration: CardPresentConfigurationLoader().configuration, onboardingUseCase: CardPresentPaymentsOnboardingUseCase(), cardReaderSupportDeterminer: CardReaderSupportDeterminer(siteID: siteID), - wooPaymentsDepositService: WooPaymentsDepositService(siteID: siteID, + wooPaymentsPayoutService: WooPaymentsPayoutService(siteID: siteID, credentials: credentials)), navigationPath: navigationPathBinding) }() diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 6fce253aba3..14041127d16 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -778,7 +778,7 @@ 203163B92C1C5F42001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203163B82C1C5F42001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView.swift */; }; 203163BB2C1C5F72001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedUpdatePostalCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203163BA2C1C5F72001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedUpdatePostalCodeView.swift */; }; 203163BD2C1C9602001C96DA /* PointOfSaleCardPresentPaymentAlertType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203163BC2C1C9602001C96DA /* PointOfSaleCardPresentPaymentAlertType.swift */; }; - 203A5C312AC5ADD700BF29A1 /* WooPaymentsDepositsOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203A5C302AC5ADD700BF29A1 /* WooPaymentsDepositsOverviewView.swift */; }; + 203A5C312AC5ADD700BF29A1 /* WooPaymentsPayoutsOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203A5C302AC5ADD700BF29A1 /* WooPaymentsPayoutsOverviewView.swift */; }; 204C9C742B6BDFFB007A94E0 /* UIUserInterfaceSizeClass+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204C9C732B6BDFFB007A94E0 /* UIUserInterfaceSizeClass+Helpers.swift */; }; 204CB80E2C0F8A5E000C9773 /* MockViewControllerPresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204CB80D2C0F8A5E000C9773 /* MockViewControllerPresenting.swift */; }; 204CB8102C10BB88000C9773 /* CardPresentPaymentPreviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204CB80F2C10BB88000C9773 /* CardPresentPaymentPreviewService.swift */; }; @@ -821,8 +821,8 @@ 2084B7AA2C776E9700EFBD2E /* PointOfSaleCardPresentPaymentOptionalReaderUpdateInProgressAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2084B7A92C776E9700EFBD2E /* PointOfSaleCardPresentPaymentOptionalReaderUpdateInProgressAlertViewModelTests.swift */; }; 2084B7AC2C776F0F00EFBD2E /* PointOfSaleCardPresentPaymentRequiredReaderUpdateInProgressAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2084B7AB2C776F0F00EFBD2E /* PointOfSaleCardPresentPaymentRequiredReaderUpdateInProgressAlertViewModelTests.swift */; }; 2084B7AE2C77845C00EFBD2E /* PointOfSaleCardPresentPaymentReaderUpdateCompletionAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2084B7AD2C77845C00EFBD2E /* PointOfSaleCardPresentPaymentReaderUpdateCompletionAlertViewModelTests.swift */; }; - 209AD3D02AC1EDDA00825D76 /* WooPaymentsDepositsCurrencyOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3CF2AC1EDDA00825D76 /* WooPaymentsDepositsCurrencyOverviewViewModel.swift */; }; - 209AD3D22AC1EDF600825D76 /* WooPaymentsDepositsCurrencyOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3D12AC1EDF600825D76 /* WooPaymentsDepositsCurrencyOverviewView.swift */; }; + 209AD3D02AC1EDDA00825D76 /* WooPaymentsPayoutsCurrencyOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3CF2AC1EDDA00825D76 /* WooPaymentsPayoutsCurrencyOverviewViewModel.swift */; }; + 209AD3D22AC1EDF600825D76 /* WooPaymentsPayoutsCurrencyOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3D12AC1EDF600825D76 /* WooPaymentsPayoutsCurrencyOverviewView.swift */; }; 209B15672AD85F070094152A /* OperatingSystemVersion+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209B15662AD85F070094152A /* OperatingSystemVersion+Localization.swift */; }; 209CA0EE2B50070D0073D1AC /* WooTabContainerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209CA0ED2B50070D0073D1AC /* WooTabContainerController.swift */; }; 209EEF902C762ED5007969A4 /* POSModalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209EEF8F2C762ED5007969A4 /* POSModalManager.swift */; }; @@ -837,14 +837,14 @@ 20AE33C72B0510D200527B60 /* HubMenuDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20AE33C62B0510D200527B60 /* HubMenuDestination.swift */; }; 20B0D65E2AD45BDE0059735A /* AboutTapToPayContactlessLimitViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B0D65D2AD45BDE0059735A /* AboutTapToPayContactlessLimitViewModelTests.swift */; }; 20BBD62C2B3060A300A903F6 /* AddOrderComponentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BBD62B2B3060A300A903F6 /* AddOrderComponentsSection.swift */; }; - 20BCF6EE2B0E478B00954840 /* WooPaymentsDepositsOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6ED2B0E478B00954840 /* WooPaymentsDepositsOverviewViewModel.swift */; }; - 20BCF6F02B0E48CC00954840 /* WooPaymentsDepositsOverviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6EF2B0E48CC00954840 /* WooPaymentsDepositsOverviewViewModelTests.swift */; }; + 20BCF6EE2B0E478B00954840 /* WooPaymentsPayoutsOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6ED2B0E478B00954840 /* WooPaymentsPayoutsOverviewViewModel.swift */; }; + 20BCF6F02B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6EF2B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift */; }; 20BCF6F72B0E5AF000954840 /* MockSystemStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6F62B0E5AEF00954840 /* MockSystemStatusService.swift */; }; 20C6E7512CDE4AEA00CD124C /* ItemListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C6E7502CDE4AEA00CD124C /* ItemListState.swift */; }; 20CC1EDB2AFA8381006BD429 /* InPersonPaymentsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CC1EDA2AFA8381006BD429 /* InPersonPaymentsMenu.swift */; }; 20CC1EDD2AFA99DF006BD429 /* InPersonPaymentsMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CC1EDC2AFA99DF006BD429 /* InPersonPaymentsMenuViewModel.swift */; }; - 20CCBF212B0E15C0003102E6 /* WooPaymentsDepositsCurrencyOverviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CCBF202B0E15C0003102E6 /* WooPaymentsDepositsCurrencyOverviewViewModelTests.swift */; }; - 20D210BE2B14C9B90099E517 /* WooPaymentsDepositStatusDisplayDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D210BD2B14C9B90099E517 /* WooPaymentsDepositStatusDisplayDetails.swift */; }; + 20CCBF212B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CCBF202B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift */; }; + 20D210BE2B14C9B90099E517 /* WooPaymentsPayoutStatusDisplayDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D210BD2B14C9B90099E517 /* WooPaymentsPayoutStatusDisplayDetails.swift */; }; 20D2CCA32C7E175700051705 /* WavesProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D2CCA22C7E175700051705 /* WavesProgressViewStyle.swift */; }; 20D2CCA52C7E328300051705 /* POSModalCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D2CCA42C7E328300051705 /* POSModalCloseButton.swift */; }; 20D3D42B2C64D7CC004CE6E3 /* SimpleProductsOnlyInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D3D42A2C64D7CC004CE6E3 /* SimpleProductsOnlyInformation.swift */; }; @@ -3895,7 +3895,7 @@ 203163B82C1C5F42001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView.swift; sourceTree = ""; }; 203163BA2C1C5F72001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedUpdatePostalCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentConnectingFailedUpdatePostalCodeView.swift; sourceTree = ""; }; 203163BC2C1C9602001C96DA /* PointOfSaleCardPresentPaymentAlertType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentAlertType.swift; sourceTree = ""; }; - 203A5C302AC5ADD700BF29A1 /* WooPaymentsDepositsOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsOverviewView.swift; sourceTree = ""; }; + 203A5C302AC5ADD700BF29A1 /* WooPaymentsPayoutsOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsOverviewView.swift; sourceTree = ""; }; 204C9C732B6BDFFB007A94E0 /* UIUserInterfaceSizeClass+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIUserInterfaceSizeClass+Helpers.swift"; sourceTree = ""; }; 204CB80D2C0F8A5E000C9773 /* MockViewControllerPresenting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewControllerPresenting.swift; sourceTree = ""; }; 204CB80F2C10BB88000C9773 /* CardPresentPaymentPreviewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentPreviewService.swift; sourceTree = ""; }; @@ -3939,8 +3939,8 @@ 2084B7AB2C776F0F00EFBD2E /* PointOfSaleCardPresentPaymentRequiredReaderUpdateInProgressAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentRequiredReaderUpdateInProgressAlertViewModelTests.swift; sourceTree = ""; }; 2084B7AD2C77845C00EFBD2E /* PointOfSaleCardPresentPaymentReaderUpdateCompletionAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentReaderUpdateCompletionAlertViewModelTests.swift; sourceTree = ""; }; 20929C9DFB2CE5CB264C27EC /* Pods-Woo Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Woo Watch App.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-Woo Watch App/Pods-Woo Watch App.debug.xcconfig"; sourceTree = ""; }; - 209AD3CF2AC1EDDA00825D76 /* WooPaymentsDepositsCurrencyOverviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsCurrencyOverviewViewModel.swift; sourceTree = ""; }; - 209AD3D12AC1EDF600825D76 /* WooPaymentsDepositsCurrencyOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsCurrencyOverviewView.swift; sourceTree = ""; }; + 209AD3CF2AC1EDDA00825D76 /* WooPaymentsPayoutsCurrencyOverviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsCurrencyOverviewViewModel.swift; sourceTree = ""; }; + 209AD3D12AC1EDF600825D76 /* WooPaymentsPayoutsCurrencyOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsCurrencyOverviewView.swift; sourceTree = ""; }; 209B15662AD85F070094152A /* OperatingSystemVersion+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Localization.swift"; sourceTree = ""; }; 209CA0ED2B50070D0073D1AC /* WooTabContainerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooTabContainerController.swift; sourceTree = ""; }; 209EEF8F2C762ED5007969A4 /* POSModalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSModalManager.swift; sourceTree = ""; }; @@ -3955,14 +3955,14 @@ 20AE33C62B0510D200527B60 /* HubMenuDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubMenuDestination.swift; sourceTree = ""; }; 20B0D65D2AD45BDE0059735A /* AboutTapToPayContactlessLimitViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutTapToPayContactlessLimitViewModelTests.swift; sourceTree = ""; }; 20BBD62B2B3060A300A903F6 /* AddOrderComponentsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOrderComponentsSection.swift; sourceTree = ""; }; - 20BCF6ED2B0E478B00954840 /* WooPaymentsDepositsOverviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsOverviewViewModel.swift; sourceTree = ""; }; - 20BCF6EF2B0E48CC00954840 /* WooPaymentsDepositsOverviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsOverviewViewModelTests.swift; sourceTree = ""; }; + 20BCF6ED2B0E478B00954840 /* WooPaymentsPayoutsOverviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsOverviewViewModel.swift; sourceTree = ""; }; + 20BCF6EF2B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsOverviewViewModelTests.swift; sourceTree = ""; }; 20BCF6F62B0E5AEF00954840 /* MockSystemStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSystemStatusService.swift; sourceTree = ""; }; 20C6E7502CDE4AEA00CD124C /* ItemListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListState.swift; sourceTree = ""; }; 20CC1EDA2AFA8381006BD429 /* InPersonPaymentsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenu.swift; sourceTree = ""; }; 20CC1EDC2AFA99DF006BD429 /* InPersonPaymentsMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenuViewModel.swift; sourceTree = ""; }; - 20CCBF202B0E15C0003102E6 /* WooPaymentsDepositsCurrencyOverviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsCurrencyOverviewViewModelTests.swift; sourceTree = ""; }; - 20D210BD2B14C9B90099E517 /* WooPaymentsDepositStatusDisplayDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositStatusDisplayDetails.swift; sourceTree = ""; }; + 20CCBF202B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift; sourceTree = ""; }; + 20D210BD2B14C9B90099E517 /* WooPaymentsPayoutStatusDisplayDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutStatusDisplayDetails.swift; sourceTree = ""; }; 20D2CCA22C7E175700051705 /* WavesProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WavesProgressViewStyle.swift; sourceTree = ""; }; 20D2CCA42C7E328300051705 /* POSModalCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSModalCloseButton.swift; sourceTree = ""; }; 20D3D42A2C64D7CC004CE6E3 /* SimpleProductsOnlyInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleProductsOnlyInformation.swift; sourceTree = ""; }; @@ -7807,11 +7807,11 @@ 202496682B0BC07E00EE527D /* Deposits Overview */ = { isa = PBXGroup; children = ( - 209AD3CF2AC1EDDA00825D76 /* WooPaymentsDepositsCurrencyOverviewViewModel.swift */, - 209AD3D12AC1EDF600825D76 /* WooPaymentsDepositsCurrencyOverviewView.swift */, - 20D210BD2B14C9B90099E517 /* WooPaymentsDepositStatusDisplayDetails.swift */, - 203A5C302AC5ADD700BF29A1 /* WooPaymentsDepositsOverviewView.swift */, - 20BCF6ED2B0E478B00954840 /* WooPaymentsDepositsOverviewViewModel.swift */, + 209AD3CF2AC1EDDA00825D76 /* WooPaymentsPayoutsCurrencyOverviewViewModel.swift */, + 209AD3D12AC1EDF600825D76 /* WooPaymentsPayoutsCurrencyOverviewView.swift */, + 20D210BD2B14C9B90099E517 /* WooPaymentsPayoutStatusDisplayDetails.swift */, + 203A5C302AC5ADD700BF29A1 /* WooPaymentsPayoutsOverviewView.swift */, + 20BCF6ED2B0E478B00954840 /* WooPaymentsPayoutsOverviewViewModel.swift */, ); path = "Deposits Overview"; sourceTree = ""; @@ -7938,8 +7938,8 @@ 20CCBF1F2B0E159D003102E6 /* Deposits Overview */ = { isa = PBXGroup; children = ( - 20CCBF202B0E15C0003102E6 /* WooPaymentsDepositsCurrencyOverviewViewModelTests.swift */, - 20BCF6EF2B0E48CC00954840 /* WooPaymentsDepositsOverviewViewModelTests.swift */, + 20CCBF202B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift */, + 20BCF6EF2B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift */, ); path = "Deposits Overview"; sourceTree = ""; @@ -14832,7 +14832,7 @@ 4569D3C325DC008700CDC3E2 /* SiteAddress.swift in Sources */, 02BBD6E729A268F300243BE2 /* StoreOnboardingViewModel.swift in Sources */, B9B0391828A6838400DC1C83 /* PermanentNoticeView.swift in Sources */, - 20BCF6EE2B0E478B00954840 /* WooPaymentsDepositsOverviewViewModel.swift in Sources */, + 20BCF6EE2B0E478B00954840 /* WooPaymentsPayoutsOverviewViewModel.swift in Sources */, 0373A12D2A1D1E6000731236 /* BadgedLeftImageTableViewCell.swift in Sources */, 01BD77442C58CED400147191 /* PointOfSaleCardPresentPaymentProcessingMessageView.swift in Sources */, 31FE28C225E6D338003519F2 /* LearnMoreTableViewCell.swift in Sources */, @@ -15712,7 +15712,7 @@ DE7E5E862B4D11D7002E28D2 /* BlazeTargetDevicePickerViewModel.swift in Sources */, CE315DC62CC93CCE00A06748 /* WooShippingCarrier.swift in Sources */, 4535EE7E281BE04A004212B4 /* CouponAmountInputFormatter.swift in Sources */, - 209AD3D22AC1EDF600825D76 /* WooPaymentsDepositsCurrencyOverviewView.swift in Sources */, + 209AD3D22AC1EDF600825D76 /* WooPaymentsPayoutsCurrencyOverviewView.swift in Sources */, CE35F11B2343F3B1007B2A6B /* TwoColumnHeadlineFootnoteTableViewCell.swift in Sources */, B9D19A422AE7B4AD00D944D8 /* CustomAmountRowViewModel.swift in Sources */, D8C251DB230D288A00F49782 /* PushNotesManager.swift in Sources */, @@ -16048,7 +16048,7 @@ DED9740D2AD7D27000122EB4 /* BlazeCampaignListViewModel.swift in Sources */, 0379C51B27BFE23F00A7E284 /* RefundConfirmationCardDetailsCell.swift in Sources */, D85136B9231CED5800DD0539 /* ReviewAge.swift in Sources */, - 209AD3D02AC1EDDA00825D76 /* WooPaymentsDepositsCurrencyOverviewViewModel.swift in Sources */, + 209AD3D02AC1EDDA00825D76 /* WooPaymentsPayoutsCurrencyOverviewViewModel.swift in Sources */, AE3AA88B290C30B900BE422D /* WebViewControllerConfiguration.swift in Sources */, 26E1BECA251BE5390096D0A1 /* RefundItemTableViewCell.swift in Sources */, 2027F7562C90B013004BDF73 /* CardPresentPaymentReaderConnectionStatus.swift in Sources */, @@ -16135,7 +16135,7 @@ 2667BFEB2535FF09008099D4 /* RefundShippingCalculationUseCase.swift in Sources */, B6C838DE28793B3A003AB786 /* CustomFieldViewModel.swift in Sources */, DE65C1F72C48E7DC003EF8D1 /* SupportButton.swift in Sources */, - 20D210BE2B14C9B90099E517 /* WooPaymentsDepositStatusDisplayDetails.swift in Sources */, + 20D210BE2B14C9B90099E517 /* WooPaymentsPayoutStatusDisplayDetails.swift in Sources */, 026225212C21F01F00700977 /* PointOfSaleCardPresentPaymentReaderUpdateFailedNonRetryableAlertViewModel.swift in Sources */, E1E125AA26EB42530068A9B0 /* CardPresentModalUpdateProgress.swift in Sources */, B998DF4A2A98AE4200D1C6E8 /* TaxEducationalDialogViewModel.swift in Sources */, @@ -16172,7 +16172,7 @@ 0282DD96233C960C006A5FDB /* SearchResultCell.swift in Sources */, 02EFF8172ABBEBED0015ABB2 /* GiftCardError+Description.swift in Sources */, 027F83ED29B046D2002688C6 /* TopPerformersPeriodViewModel.swift in Sources */, - 203A5C312AC5ADD700BF29A1 /* WooPaymentsDepositsOverviewView.swift in Sources */, + 203A5C312AC5ADD700BF29A1 /* WooPaymentsPayoutsOverviewView.swift in Sources */, DE9F2D292A1B1AB2004E5957 /* FirstProductCreatedView.swift in Sources */, 260C32BE2527A2DE00157BC2 /* IssueRefundViewModel.swift in Sources */, 02D29A9229F7C39200473D6D /* UIImage+Text.swift in Sources */, @@ -16844,7 +16844,7 @@ 02F67FF525806E0100C3BAD2 /* ShippingLabelTrackingURLGeneratorTests.swift in Sources */, CE29FEF62C009F5F007679C2 /* ShippingLineRowViewModelTests.swift in Sources */, 683763182C2E497000AD51D0 /* ItemListViewModelTests.swift in Sources */, - 20CCBF212B0E15C0003102E6 /* WooPaymentsDepositsCurrencyOverviewViewModelTests.swift in Sources */, + 20CCBF212B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift in Sources */, 2602A64227BD89CE00B347F1 /* NewOrderInitialStatusResolverTests.swift in Sources */, DE6D84A52C3B8C9C0014FBFF /* GoogleAdsDashboardCardViewModelTests.swift in Sources */, 0235354E2999D17A00BF77D3 /* DomainSettingsViewModelTests.swift in Sources */, @@ -17122,7 +17122,7 @@ B56BBD16214820A70053A32D /* SyncCoordinatorTests.swift in Sources */, 02A275C023FE58F6005C560F /* MockImageCache.swift in Sources */, EE6C6B6E2C65DC4100632BDA /* WordPressMediaLibraryPickerDataSourceTests.swift in Sources */, - 20BCF6F02B0E48CC00954840 /* WooPaymentsDepositsOverviewViewModelTests.swift in Sources */, + 20BCF6F02B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift in Sources */, 261AA30E275506DE009530FE /* PaymentMethodsViewModelTests.swift in Sources */, 4524CDA1242D045C00B2F20A /* ProductStatusSettingListSelectorCommandTests.swift in Sources */, 26A280D72B46027A00ACEE87 /* OrderNotificationView.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Mocks/MockWooPaymentsDepositService.swift b/WooCommerce/WooCommerceTests/Mocks/MockWooPaymentsDepositService.swift index c5566e7e496..2c15b71d8ca 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockWooPaymentsDepositService.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockWooPaymentsDepositService.swift @@ -1,16 +1,16 @@ import Foundation import Yosemite -final class MockWooPaymentsDepositService: WooPaymentsDepositServiceProtocol { - var onFetchDepositsOverviewThenReturn: [WooPaymentsDepositsOverviewByCurrency] = [] - var onFetchDepositsOverviewShouldThrow: Error? = nil - var spyDidCallFetchDepositsOverview = false - func fetchDepositsOverview() async throws -> [WooPaymentsDepositsOverviewByCurrency] { - spyDidCallFetchDepositsOverview = true - if let error = onFetchDepositsOverviewShouldThrow { +final class MockWooPaymentsPayoutService: WooPaymentsPayoutServiceProtocol { + var onFetchPayoutsOverviewThenReturn: [WooPaymentsPayoutsOverviewByCurrency] = [] + var onFetchPayoutsOverviewShouldThrow: Error? = nil + var spyDidCallFetchPayoutsOverview = false + func fetchPayoutsOverview() async throws -> [WooPaymentsPayoutsOverviewByCurrency] { + spyDidCallFetchPayoutsOverview = true + if let error = onFetchPayoutsOverviewShouldThrow { throw error } else { - return onFetchDepositsOverviewThenReturn + return onFetchPayoutsOverviewThenReturn } } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsCurrencyOverviewViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift similarity index 67% rename from WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsCurrencyOverviewViewModelTests.swift rename to WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift index c9419fd41a0..2736456b366 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsCurrencyOverviewViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift @@ -3,15 +3,15 @@ import XCTest import WooFoundation import Yosemite -final class WooPaymentsDepositsCurrencyOverviewViewModelTests: XCTestCase { +final class WooPaymentsPayoutsCurrencyOverviewViewModelTests: XCTestCase { - var sut: WooPaymentsDepositsCurrencyOverviewViewModel! + var sut: WooPaymentsPayoutsCurrencyOverviewViewModel! var analyticsProvider: MockAnalyticsProvider! override func setUp() { analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - sut = WooPaymentsDepositsCurrencyOverviewViewModel(overview: .fake(), analytics: analytics) + sut = WooPaymentsPayoutsCurrencyOverviewViewModel(overview: .fake(), analytics: analytics) } func test_when_expand_is_tapped_analytic_event_is_tracked() { @@ -22,7 +22,7 @@ final class WooPaymentsDepositsCurrencyOverviewViewModelTests: XCTestCase { sut.expandTapped(expanded: expanded) // Then - XCTAssertTrue(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuDepositSummaryExpanded.rawValue)) + XCTAssertTrue(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuPayoutSummaryExpanded.rawValue)) } func test_when_collapse_is_tapped_analytic_event_is_not_tracked() { @@ -33,7 +33,7 @@ final class WooPaymentsDepositsCurrencyOverviewViewModelTests: XCTestCase { sut.expandTapped(expanded: collapse) // Then - XCTAssertFalse(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuDepositSummaryExpanded.rawValue)) + XCTAssertFalse(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuPayoutSummaryExpanded.rawValue)) } func test_when_learn_more_is_tapped_analytic_event_is_tracked() { @@ -43,17 +43,17 @@ final class WooPaymentsDepositsCurrencyOverviewViewModelTests: XCTestCase { sut.learnMoreTapped() // Then - XCTAssertTrue(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuDepositSummaryLearnMoreTapped.rawValue)) + XCTAssertTrue(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuPayoutSummaryLearnMoreTapped.rawValue)) } - func test_when_learn_more_is_tapped_deposit_schedule_info_webview_is_shown() { + func test_when_learn_more_is_tapped_payout_schedule_info_webview_is_shown() { // Given // When sut.learnMoreTapped() // Then - assertEqual(WooConstants.URLs.wooPaymentsDepositSchedule.asURL(), sut.showWebviewURL) + assertEqual(WooConstants.URLs.wooPaymentsPayoutSchedule.asURL(), sut.showWebviewURL) } func test_when_currency_matches_site_settings_amounts_formatted_using_woo_currency_formatter() { @@ -63,10 +63,10 @@ final class WooPaymentsDepositsCurrencyOverviewViewModelTests: XCTestCase { thousandSeparator: ",", decimalSeparator: ".", numberOfDecimals: 2) - let overview = WooPaymentsDepositsOverviewByCurrency.fake().copy(currency: .USD, availableBalance: .init(string: "12.35")) + let overview = WooPaymentsPayoutsOverviewByCurrency.fake().copy(currency: .USD, availableBalance: .init(string: "12.35")) // When - sut = WooPaymentsDepositsCurrencyOverviewViewModel(overview: overview, locale: Locale(identifier: "en-ca")) + sut = WooPaymentsPayoutsCurrencyOverviewViewModel(overview: overview, locale: Locale(identifier: "en-ca")) // Then assertEqual(sut.availableBalance, "$12.35") @@ -79,10 +79,10 @@ final class WooPaymentsDepositsCurrencyOverviewViewModelTests: XCTestCase { thousandSeparator: ",", decimalSeparator: ".", numberOfDecimals: 2) - let overview = WooPaymentsDepositsOverviewByCurrency.fake().copy(currency: .CAD, availableBalance: .init(string: "12.35")) + let overview = WooPaymentsPayoutsOverviewByCurrency.fake().copy(currency: .CAD, availableBalance: .init(string: "12.35")) // When - sut = WooPaymentsDepositsCurrencyOverviewViewModel(overview: overview, locale: Locale(identifier: "en-us")) + sut = WooPaymentsPayoutsCurrencyOverviewViewModel(overview: overview, locale: Locale(identifier: "en-us")) // Then assertEqual(sut.availableBalance, "CA$12.35") diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsOverviewViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsOverviewViewModelTests.swift similarity index 62% rename from WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsOverviewViewModelTests.swift rename to WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsOverviewViewModelTests.swift index 17e1ab731c1..e7485077d81 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsDepositsOverviewViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/Deposits Overview/WooPaymentsPayoutsOverviewViewModelTests.swift @@ -2,31 +2,31 @@ import XCTest import protocol WooFoundation.Analytics @testable import WooCommerce -final class WooPaymentsDepositsOverviewViewModelTests: XCTestCase { +final class WooPaymentsPayoutsOverviewViewModelTests: XCTestCase { - var sut: WooPaymentsDepositsOverviewViewModel! + var sut: WooPaymentsPayoutsOverviewViewModel! var analyticsProvider: MockAnalyticsProvider! var analytics: Analytics! override func setUp() { analyticsProvider = MockAnalyticsProvider() analytics = WooAnalytics(analyticsProvider: analyticsProvider) - sut = WooPaymentsDepositsOverviewViewModel(currencyViewModels: [.init(overview: .fake().copy(currency: .GBP))], + sut = WooPaymentsPayoutsOverviewViewModel(currencyViewModels: [.init(overview: .fake().copy(currency: .GBP))], analytics: analytics) } func test_when_tab_is_selected_analytic_event_is_tracked() { // Given - let gbpViewModel = WooPaymentsDepositsCurrencyOverviewViewModel(overview: .fake().copy(currency: .GBP)) + let gbpViewModel = WooPaymentsPayoutsCurrencyOverviewViewModel(overview: .fake().copy(currency: .GBP)) // When sut.currencySelected(currencyViewModel: gbpViewModel) // Then - XCTAssertTrue(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuDepositSummaryCurrencySelected.rawValue)) - guard let index = analyticsProvider.receivedEvents.firstIndex(of: WooAnalyticsStat.paymentsMenuDepositSummaryCurrencySelected.rawValue), + XCTAssertTrue(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuPayoutSummaryCurrencySelected.rawValue)) + guard let index = analyticsProvider.receivedEvents.firstIndex(of: WooAnalyticsStat.paymentsMenuPayoutSummaryCurrencySelected.rawValue), let properties = analyticsProvider.receivedProperties[safe: index], - let trackedCurrencyProperty = properties[WooAnalyticsEvent.DepositSummary.Keys.currency] as? String + let trackedCurrencyProperty = properties[WooAnalyticsEvent.PayoutSummary.Keys.currency] as? String else { return XCTFail("Expected properties not found") } @@ -34,24 +34,24 @@ final class WooPaymentsDepositsOverviewViewModelTests: XCTestCase { assertEqual("GBP", trackedCurrencyProperty) } - func test_onAppear_when_deposit_summaries_are_available_depositSummaryShown_is_tracked() throws { + func test_onAppear_when_payout_summaries_are_available_payoutSummaryShown_is_tracked() throws { // Given - let currencyViewModels: [WooPaymentsDepositsCurrencyOverviewViewModel] = [ + let currencyViewModels: [WooPaymentsPayoutsCurrencyOverviewViewModel] = [ .init(overview: .fake().copy(currency: .GBP)), .init(overview: .fake().copy(currency: .EUR)) ] - sut = WooPaymentsDepositsOverviewViewModel(currencyViewModels: currencyViewModels, + sut = WooPaymentsPayoutsOverviewViewModel(currencyViewModels: currencyViewModels, analytics: analytics) // When sut.onAppear() // Then - XCTAssertTrue(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuDepositSummaryShown.rawValue)) + XCTAssertTrue(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuPayoutSummaryShown.rawValue)) - guard let index = analyticsProvider.receivedEvents.firstIndex(of: WooAnalyticsStat.paymentsMenuDepositSummaryShown.rawValue), + guard let index = analyticsProvider.receivedEvents.firstIndex(of: WooAnalyticsStat.paymentsMenuPayoutSummaryShown.rawValue), let properties = analyticsProvider.receivedProperties[safe: index], - let trackedNumberOfCurrenciesProperty = properties[WooAnalyticsEvent.DepositSummary.Keys.numberOfCurrencies] as? Int + let trackedNumberOfCurrenciesProperty = properties[WooAnalyticsEvent.PayoutSummary.Keys.numberOfCurrencies] as? Int else { return XCTFail("Expected properties not found") } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModelTests.swift index 8bf3fb18d5d..d4e480a226e 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModelTests.swift @@ -14,7 +14,7 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { private var analyticsProvider: MockAnalyticsProvider! private var analytics: Analytics! - private var mockDepositService: MockWooPaymentsDepositService! + private var mockPayoutService: MockWooPaymentsPayoutService! private var mockOnboardingUseCase: MockCardPresentPaymentsOnboardingUseCase! private var mockPayInPersonToggleViewModel: MockInPersonPaymentsCashOnDeliveryToggleRowViewModel! @@ -25,7 +25,7 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { override func setUp() { analyticsProvider = MockAnalyticsProvider() analytics = WooAnalytics(analyticsProvider: analyticsProvider) - mockDepositService = MockWooPaymentsDepositService() + mockPayoutService = MockWooPaymentsPayoutService() mockOnboardingUseCase = MockCardPresentPaymentsOnboardingUseCase(initial: .completed(plugin: .wcPayOnly)) mockPayInPersonToggleViewModel = MockInPersonPaymentsCashOnDeliveryToggleRowViewModel() systemStatusService = MockSystemStatusService() @@ -41,14 +41,14 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { cardPresentPaymentsConfiguration: .init(country: .US), onboardingUseCase: mockOnboardingUseCase, cardReaderSupportDeterminer: MockCardReaderSupportDeterminer(), - wooPaymentsDepositService: mockDepositService, + wooPaymentsPayoutService: mockPayoutService, systemStatusService: systemStatusService, analytics: analytics), navigationPath: .constant(.init()), payInPersonToggleViewModel: mockPayInPersonToggleViewModel) } - func test_fetchDepositsOverview_is_not_called_for_stores_which_do_not_support_the_route() async { + func test_fetchPayoutsOverview_is_not_called_for_stores_which_do_not_support_the_route() async { // Currently, assume this is only WooPayments stores, but it would be better to check the /wc/v3 base endpoint. // Given systemStatusService.onFetchSystemPluginWithPath = { _ in @@ -59,10 +59,10 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { await sut.onAppear() // Then - XCTAssertFalse(mockDepositService.spyDidCallFetchDepositsOverview) + XCTAssertFalse(mockPayoutService.spyDidCallFetchPayoutsOverview) } - func test_fetchDepositsOverview_is_called_for_stores_which_support_the_route() async { + func test_fetchPayoutsOverview_is_called_for_stores_which_support_the_route() async { // Currently, assume this is only WooPayments stores, but it would be better to check the /wc/v3 base endpoint. // Given systemStatusService.onFetchSystemPluginWithPath = { path in @@ -76,7 +76,7 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { await sut.onAppear() // Then - XCTAssert(mockDepositService.spyDidCallFetchDepositsOverview) + XCTAssert(mockPayoutService.spyDidCallFetchPayoutsOverview) } func test_onAppear_refreshesPayInPersonToggle() async { @@ -91,15 +91,15 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { } // MARK: - Analytics tests - func test_onAppear_when_deposit_service_gets_an_error_depositSummaryError_is_tracked() async { + func test_onAppear_when_payout_service_gets_an_error_payoutSummaryError_is_tracked() async { // Given - mockDepositService.onFetchDepositsOverviewShouldThrow = DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "description")) + mockPayoutService.onFetchPayoutsOverviewShouldThrow = DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "description")) // When await sut.onAppear() // Then - XCTAssertTrue(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuDepositSummaryError.rawValue)) + XCTAssertTrue(analyticsProvider.receivedEvents.contains(WooAnalyticsStat.paymentsMenuPayoutSummaryError.rawValue)) } func test_collectPaymentTapped_tracks_paymentsMenuCollectPaymentTapped() { @@ -209,7 +209,7 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { let dependencies = InPersonPaymentsMenuViewModel.Dependencies(cardPresentPaymentsConfiguration: configuration, onboardingUseCase: mockOnboardingUseCase, cardReaderSupportDeterminer: MockCardReaderSupportDeterminer(), - wooPaymentsDepositService: mockDepositService) + wooPaymentsPayoutService: mockPayoutService) sut = InPersonPaymentsMenuViewModel(siteID: sampleStoreID, dependencies: dependencies, navigationPath: .constant(.init())) @@ -238,7 +238,7 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { let dependencies = InPersonPaymentsMenuViewModel.Dependencies(cardPresentPaymentsConfiguration: configuration, onboardingUseCase: mockOnboardingUseCase, cardReaderSupportDeterminer: MockCardReaderSupportDeterminer(), - wooPaymentsDepositService: mockDepositService) + wooPaymentsPayoutService: mockPayoutService) sut = InPersonPaymentsMenuViewModel(siteID: sampleStoreID, dependencies: dependencies, navigationPath: .constant(.init())) @@ -257,7 +257,7 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { let dependencies = InPersonPaymentsMenuViewModel.Dependencies(cardPresentPaymentsConfiguration: .init(country: .US), onboardingUseCase: mockOnboardingUseCase, cardReaderSupportDeterminer: MockCardReaderSupportDeterminer(), - wooPaymentsDepositService: mockDepositService) + wooPaymentsPayoutService: mockPayoutService) var navigationPath = NavigationPath() let navigationPathBinding = Binding( get: { navigationPath }, @@ -280,7 +280,7 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { let dependencies = InPersonPaymentsMenuViewModel.Dependencies(cardPresentPaymentsConfiguration: .init(country: .US), onboardingUseCase: mockOnboardingUseCase, cardReaderSupportDeterminer: MockCardReaderSupportDeterminer(), - wooPaymentsDepositService: mockDepositService) + wooPaymentsPayoutService: mockPayoutService) var navigationPath = NavigationPath() let navigationPathBinding = Binding( get: { navigationPath }, @@ -303,7 +303,7 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { let dependencies = InPersonPaymentsMenuViewModel.Dependencies(cardPresentPaymentsConfiguration: .init(country: .US), onboardingUseCase: mockOnboardingUseCase, cardReaderSupportDeterminer: MockCardReaderSupportDeterminer(), - wooPaymentsDepositService: mockDepositService) + wooPaymentsPayoutService: mockPayoutService) var navigationPath = NavigationPath(["testPath"]) let navigationPathBinding = Binding( get: { navigationPath }, @@ -332,7 +332,7 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { let dependencies = InPersonPaymentsMenuViewModel.Dependencies(cardPresentPaymentsConfiguration: .init(country: .US), onboardingUseCase: mockOnboardingUseCase, cardReaderSupportDeterminer: MockCardReaderSupportDeterminer(), - wooPaymentsDepositService: mockDepositService) + wooPaymentsPayoutService: mockPayoutService) var navigationPath = NavigationPath(["testPath"]) let navigationPathBinding = Binding( get: { navigationPath }, @@ -360,7 +360,7 @@ final class InPersonPaymentsMenuViewModelTests: XCTestCase { let dependencies = InPersonPaymentsMenuViewModel.Dependencies(cardPresentPaymentsConfiguration: .init(country: .US), onboardingUseCase: mockOnboardingUseCase, cardReaderSupportDeterminer: MockCardReaderSupportDeterminer(), - wooPaymentsDepositService: mockDepositService) + wooPaymentsPayoutService: mockPayoutService) sut = InPersonPaymentsMenuViewModel(siteID: sampleStoreID, dependencies: dependencies, navigationPath: .constant(.init())) diff --git a/Yosemite/Yosemite.xcodeproj/project.pbxproj b/Yosemite/Yosemite.xcodeproj/project.pbxproj index ee3487c55b4..d5ba48e24d0 100644 --- a/Yosemite/Yosemite.xcodeproj/project.pbxproj +++ b/Yosemite/Yosemite.xcodeproj/project.pbxproj @@ -143,13 +143,13 @@ 077F39E226A5AFCA00ABEADC /* SystemPlugin+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077F39E126A5AFCA00ABEADC /* SystemPlugin+ReadOnlyConvertible.swift */; }; 077F39E526A5C98200ABEADC /* SystemStatusStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077F39E426A5C98200ABEADC /* SystemStatusStoreTests.swift */; }; 0E67B79585034C4DD75C8117 /* Pods_Yosemite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C25501C7F936D2FD32FAF3F4 /* Pods_Yosemite.framework */; }; - 209AD3CC2AC1A68800825D76 /* WooPaymentsDepositService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3CB2AC1A68800825D76 /* WooPaymentsDepositService.swift */; }; - 209AD3CE2AC1A9C200825D76 /* WooPaymentsDepositsOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3CD2AC1A9C200825D76 /* WooPaymentsDepositsOverview.swift */; }; + 209AD3CC2AC1A68800825D76 /* WooPaymentsPayoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3CB2AC1A68800825D76 /* WooPaymentsPayoutService.swift */; }; + 209AD3CE2AC1A9C200825D76 /* WooPaymentsPayoutsOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3CD2AC1A9C200825D76 /* WooPaymentsPayoutsOverview.swift */; }; 20BCF6F22B0E554500954840 /* SystemStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6F12B0E554500954840 /* SystemStatusService.swift */; }; 20BCF6F52B0E57AB00954840 /* MockSystemStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6F42B0E57AB00954840 /* MockSystemStatusService.swift */; }; 20D035002CDBBD6400C0F901 /* CommonReaderConfigProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D034FF2CDBBD6400C0F901 /* CommonReaderConfigProviderTests.swift */; }; 20D035022CDBBE2A00C0F901 /* MockCardReaderCapableRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D035012CDBBE2A00C0F901 /* MockCardReaderCapableRemote.swift */; }; - 20D210C12B177EEF0099E517 /* WooPaymentsDepositServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D210C02B177EEF0099E517 /* WooPaymentsDepositServiceTests.swift */; }; + 20D210C12B177EEF0099E517 /* WooPaymentsPayoutServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D210C02B177EEF0099E517 /* WooPaymentsPayoutServiceTests.swift */; }; 24163B9E257F41A600F94EC3 /* StoresManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24163B9D257F41A600F94EC3 /* StoresManager.swift */; }; 24163BA8257F41C500F94EC3 /* SessionManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24163BA7257F41C500F94EC3 /* SessionManagerProtocol.swift */; }; 247CE7AB2582DB9300F9D9D1 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 247CE7AA2582DB9300F9D9D1 /* String+Extensions.swift */; }; @@ -657,13 +657,13 @@ 077F39DF26A5A6F500ABEADC /* SystemStatusStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemStatusStore.swift; sourceTree = ""; }; 077F39E126A5AFCA00ABEADC /* SystemPlugin+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemPlugin+ReadOnlyConvertible.swift"; sourceTree = ""; }; 077F39E426A5C98200ABEADC /* SystemStatusStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemStatusStoreTests.swift; sourceTree = ""; }; - 209AD3CB2AC1A68800825D76 /* WooPaymentsDepositService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositService.swift; sourceTree = ""; }; - 209AD3CD2AC1A9C200825D76 /* WooPaymentsDepositsOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsOverview.swift; sourceTree = ""; }; + 209AD3CB2AC1A68800825D76 /* WooPaymentsPayoutService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutService.swift; sourceTree = ""; }; + 209AD3CD2AC1A9C200825D76 /* WooPaymentsPayoutsOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsOverview.swift; sourceTree = ""; }; 20BCF6F12B0E554500954840 /* SystemStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemStatusService.swift; sourceTree = ""; }; 20BCF6F42B0E57AB00954840 /* MockSystemStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MockSystemStatusService.swift; path = ../../../WooCommerce/WooCommerceTests/Mocks/MockSystemStatusService.swift; sourceTree = ""; }; 20D034FF2CDBBD6400C0F901 /* CommonReaderConfigProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonReaderConfigProviderTests.swift; sourceTree = ""; }; 20D035012CDBBE2A00C0F901 /* MockCardReaderCapableRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardReaderCapableRemote.swift; sourceTree = ""; }; - 20D210C02B177EEF0099E517 /* WooPaymentsDepositServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositServiceTests.swift; sourceTree = ""; }; + 20D210C02B177EEF0099E517 /* WooPaymentsPayoutServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutServiceTests.swift; sourceTree = ""; }; 24163B9D257F41A600F94EC3 /* StoresManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoresManager.swift; sourceTree = ""; }; 24163BA7257F41C500F94EC3 /* SessionManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManagerProtocol.swift; sourceTree = ""; }; 247CE7AA2582DB9300F9D9D1 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; @@ -1224,7 +1224,7 @@ 209AD3CA2AC1A65700825D76 /* Payments */ = { isa = PBXGroup; children = ( - 209AD3CB2AC1A68800825D76 /* WooPaymentsDepositService.swift */, + 209AD3CB2AC1A68800825D76 /* WooPaymentsPayoutService.swift */, ); path = Payments; sourceTree = ""; @@ -1240,7 +1240,7 @@ 20D210BF2B177EDB0099E517 /* Payments */ = { isa = PBXGroup; children = ( - 20D210C02B177EEF0099E517 /* WooPaymentsDepositServiceTests.swift */, + 20D210C02B177EEF0099E517 /* WooPaymentsPayoutServiceTests.swift */, ); path = Payments; sourceTree = ""; @@ -1600,7 +1600,7 @@ B52E0035211A44F800700FDE /* Storage */, 264D2C822B0B19F200FD2C05 /* SystemInformation.swift */, 03EB998F2907B97800F06A39 /* JustInTimeMessage.swift */, - 209AD3CD2AC1A9C200825D76 /* WooPaymentsDepositsOverview.swift */, + 209AD3CD2AC1A9C200825D76 /* WooPaymentsPayoutsOverview.swift */, B53D89E420E6C22B00F90866 /* Model.swift */, ); path = Model; @@ -2251,7 +2251,7 @@ CE606D9C2BE3BCC4001CB424 /* ShippingMethod+ReadOnlyConvertible.swift in Sources */, 261F94E4242EFA6D00762B58 /* ProductCategoryAction.swift in Sources */, CC6A054628773F75002C144E /* MetaData+ReadOnlyConvertible.swift in Sources */, - 209AD3CC2AC1A68800825D76 /* WooPaymentsDepositService.swift in Sources */, + 209AD3CC2AC1A68800825D76 /* WooPaymentsPayoutService.swift in Sources */, 029BA557255E0CD4006171FD /* ShippingLabelStore.swift in Sources */, 021EAA5C25493E9300AA8CCD /* OrderItemAttribute+ReadOnlyConvertible.swift in Sources */, 45AB8B1524AA4A1E00B5B36E /* ProductTagAction.swift in Sources */, @@ -2553,7 +2553,7 @@ 247CE84A2583246800F9D9D1 /* MockOrderStatusActionHandler.swift in Sources */, B9AECD402850FE4600E78584 /* Order+CardPresentPayment.swift in Sources */, 7492FADB217FAE4D00ED2C69 /* SiteSetting+ReadOnlyType.swift in Sources */, - 209AD3CE2AC1A9C200825D76 /* WooPaymentsDepositsOverview.swift in Sources */, + 209AD3CE2AC1A9C200825D76 /* WooPaymentsPayoutsOverview.swift in Sources */, DEDA8DAF2B1847C80076BF0F /* WordPressThemeStore.swift in Sources */, 03EB99902907B97800F06A39 /* JustInTimeMessage.swift in Sources */, 031C1EAA27B1702800298699 /* WCPayCharge+ReadOnlyConvertible.swift in Sources */, @@ -2694,7 +2694,7 @@ 0212AC67242C799B00C51F6C /* ResultsController+StorageProductTests.swift in Sources */, D4CBAE6026D440FA00BBE6D1 /* MockAnnouncementsRemote.swift in Sources */, 578CE7902475EBAB00492EBF /* MockProductReviewsRemote.swift in Sources */, - 20D210C12B177EEF0099E517 /* WooPaymentsDepositServiceTests.swift in Sources */, + 20D210C12B177EEF0099E517 /* WooPaymentsPayoutServiceTests.swift in Sources */, D87F615E2265B1BC0031A13B /* AppSettingsStoreTests.swift in Sources */, 02E7FFD52562226B00C53030 /* ShippingLabelStoreTests.swift in Sources */, 031FD8A026FC970400B315C7 /* RosettaTestingHelper.swift in Sources */, diff --git a/Yosemite/Yosemite/Model/Copiable/Models+Copiable.generated.swift b/Yosemite/Yosemite/Model/Copiable/Models+Copiable.generated.swift index a4ddb60d052..c89421af365 100644 --- a/Yosemite/Yosemite/Model/Copiable/Models+Copiable.generated.swift +++ b/Yosemite/Yosemite/Model/Copiable/Models+Copiable.generated.swift @@ -84,31 +84,31 @@ extension Yosemite.SystemInformation { } } -extension Yosemite.WooPaymentsDepositsOverviewByCurrency { +extension Yosemite.WooPaymentsPayoutsOverviewByCurrency { public func copy( currency: CopiableProp = .copy, - automaticDeposits: CopiableProp = .copy, - depositInterval: CopiableProp = .copy, + automaticPayouts: CopiableProp = .copy, + payoutInterval: CopiableProp = .copy, pendingBalanceAmount: CopiableProp = .copy, - pendingDepositDays: CopiableProp = .copy, - lastDeposit: NullableCopiableProp = .copy, + pendingPayoutDays: CopiableProp = .copy, + lastPayout: NullableCopiableProp = .copy, availableBalance: CopiableProp = .copy - ) -> Yosemite.WooPaymentsDepositsOverviewByCurrency { + ) -> Yosemite.WooPaymentsPayoutsOverviewByCurrency { let currency = currency ?? self.currency - let automaticDeposits = automaticDeposits ?? self.automaticDeposits - let depositInterval = depositInterval ?? self.depositInterval + let automaticPayouts = automaticPayouts ?? self.automaticPayouts + let payoutInterval = payoutInterval ?? self.payoutInterval let pendingBalanceAmount = pendingBalanceAmount ?? self.pendingBalanceAmount - let pendingDepositDays = pendingDepositDays ?? self.pendingDepositDays - let lastDeposit = lastDeposit ?? self.lastDeposit + let pendingPayoutDays = pendingPayoutDays ?? self.pendingPayoutDays + let lastPayout = lastPayout ?? self.lastPayout let availableBalance = availableBalance ?? self.availableBalance - return Yosemite.WooPaymentsDepositsOverviewByCurrency( + return Yosemite.WooPaymentsPayoutsOverviewByCurrency( currency: currency, - automaticDeposits: automaticDeposits, - depositInterval: depositInterval, + automaticPayouts: automaticPayouts, + payoutInterval: payoutInterval, pendingBalanceAmount: pendingBalanceAmount, - pendingDepositDays: pendingDepositDays, - lastDeposit: lastDeposit, + pendingPayoutDays: pendingPayoutDays, + lastPayout: lastPayout, availableBalance: availableBalance ) } diff --git a/Yosemite/Yosemite/Model/Model.swift b/Yosemite/Yosemite/Model/Model.swift index 3cecdf9b6ff..e89888d8803 100644 --- a/Yosemite/Yosemite/Model/Model.swift +++ b/Yosemite/Yosemite/Model/Model.swift @@ -215,8 +215,8 @@ public typealias WCPayCardPaymentDetails = Networking.WCPayCardPaymentDetails public typealias WCPayCardPresentReceiptDetails = Networking.WCPayCardPresentReceiptDetails public typealias WCPayPaymentMethodDetails = Networking.WCPayPaymentMethodDetails public typealias WCPayChargeStatus = Networking.WCPayChargeStatus -public typealias WooPaymentsDepositInterval = Networking.WooPaymentsDepositInterval -public typealias WooPaymentsDepositStatus = Networking.WooPaymentsDepositStatus +public typealias WooPaymentsPayoutInterval = Networking.WooPaymentsPayoutInterval +public typealias WooPaymentsPayoutStatus = Networking.WooPaymentsPayoutStatus public typealias StoreOnboardingTask = Networking.StoreOnboardingTask public typealias WCAnalyticsCustomer = Networking.WCAnalyticsCustomer public typealias WCAnalyticsStats = Networking.WCAnalyticsStats diff --git a/Yosemite/Yosemite/Model/WooPaymentsDepositsOverviewByCurrency.swift b/Yosemite/Yosemite/Model/WooPaymentsDepositsOverviewByCurrency.swift index a050320ec0f..a42635d07ce 100644 --- a/Yosemite/Yosemite/Model/WooPaymentsDepositsOverviewByCurrency.swift +++ b/Yosemite/Yosemite/Model/WooPaymentsDepositsOverviewByCurrency.swift @@ -2,30 +2,30 @@ import Foundation import WooFoundation import Networking -public struct WooPaymentsDepositsOverviewByCurrency { +public struct WooPaymentsPayoutsOverviewByCurrency { public let currency: CurrencyCode - public let automaticDeposits: Bool - public let depositInterval: WooPaymentsDepositInterval + public let automaticPayouts: Bool + public let payoutInterval: WooPaymentsPayoutInterval public let pendingBalanceAmount: NSDecimalNumber - public let pendingDepositsCount: Int - public let pendingDepositDays: Int - public let nextDeposit: NextDeposit? - public let lastDeposit: LastDeposit? + public let pendingPayoutsCount: Int + public let pendingPayoutDays: Int + public let nextPayout: NextPayout? + public let lastPayout: LastPayout? public let availableBalance: NSDecimalNumber - public struct NextDeposit { + public struct NextPayout { public let amount: NSDecimalNumber public let date: Date - public let status: WooPaymentsDepositStatus + public let status: WooPaymentsPayoutStatus - public init(amount: NSDecimalNumber, date: Date, status: WooPaymentsDepositStatus) { + public init(amount: NSDecimalNumber, date: Date, status: WooPaymentsPayoutStatus) { self.amount = amount self.date = date self.status = status } } - public struct LastDeposit { + public struct LastPayout { public let amount: NSDecimalNumber public let date: Date @@ -36,22 +36,22 @@ public struct WooPaymentsDepositsOverviewByCurrency { } public init(currency: CurrencyCode, - automaticDeposits: Bool, - depositInterval: WooPaymentsDepositInterval, + automaticPayouts: Bool, + payoutInterval: WooPaymentsPayoutInterval, pendingBalanceAmount: NSDecimalNumber, - pendingDepositsCount: Int, - pendingDepositDays: Int, - nextDeposit: NextDeposit?, - lastDeposit: LastDeposit?, + pendingPayoutsCount: Int, + pendingPayoutDays: Int, + nextPayout: NextPayout?, + lastPayout: LastPayout?, availableBalance: NSDecimalNumber) { self.currency = currency - self.automaticDeposits = automaticDeposits - self.depositInterval = depositInterval + self.automaticPayouts = automaticPayouts + self.payoutInterval = payoutInterval self.pendingBalanceAmount = pendingBalanceAmount - self.pendingDepositsCount = pendingDepositsCount - self.pendingDepositDays = pendingDepositDays - self.nextDeposit = nextDeposit - self.lastDeposit = lastDeposit + self.pendingPayoutsCount = pendingPayoutsCount + self.pendingPayoutDays = pendingPayoutDays + self.nextPayout = nextPayout + self.lastPayout = lastPayout self.availableBalance = availableBalance } } diff --git a/Yosemite/Yosemite/Model/WooPaymentsDepositsOverview.swift b/Yosemite/Yosemite/Model/WooPaymentsPayoutsOverview.swift similarity index 50% rename from Yosemite/Yosemite/Model/WooPaymentsDepositsOverview.swift rename to Yosemite/Yosemite/Model/WooPaymentsPayoutsOverview.swift index 14c07393f3c..b1c01575bc6 100644 --- a/Yosemite/Yosemite/Model/WooPaymentsDepositsOverview.swift +++ b/Yosemite/Yosemite/Model/WooPaymentsPayoutsOverview.swift @@ -3,21 +3,21 @@ import WooFoundation import Networking import Codegen -public struct WooPaymentsDepositsOverviewByCurrency: GeneratedCopiable, GeneratedFakeable { +public struct WooPaymentsPayoutsOverviewByCurrency: GeneratedCopiable, GeneratedFakeable { public let currency: CurrencyCode - public let automaticDeposits: Bool - public let depositInterval: WooPaymentsDepositInterval + public let automaticPayouts: Bool + public let payoutInterval: WooPaymentsPayoutInterval public let pendingBalanceAmount: NSDecimalNumber - public let pendingDepositDays: Int - public let lastDeposit: LastDeposit? + public let pendingPayoutDays: Int + public let lastPayout: LastPayout? public let availableBalance: NSDecimalNumber - public struct LastDeposit { + public struct LastPayout { public let amount: NSDecimalNumber public let date: Date - public let status: WooPaymentsDepositStatus + public let status: WooPaymentsPayoutStatus - public init(amount: NSDecimalNumber, date: Date, status: WooPaymentsDepositStatus) { + public init(amount: NSDecimalNumber, date: Date, status: WooPaymentsPayoutStatus) { self.amount = amount self.date = date self.status = status @@ -25,18 +25,18 @@ public struct WooPaymentsDepositsOverviewByCurrency: GeneratedCopiable, Generate } public init(currency: CurrencyCode, - automaticDeposits: Bool, - depositInterval: WooPaymentsDepositInterval, + automaticPayouts: Bool, + payoutInterval: WooPaymentsPayoutInterval, pendingBalanceAmount: NSDecimalNumber, - pendingDepositDays: Int, - lastDeposit: LastDeposit?, + pendingPayoutDays: Int, + lastPayout: LastPayout?, availableBalance: NSDecimalNumber) { self.currency = currency - self.automaticDeposits = automaticDeposits - self.depositInterval = depositInterval + self.automaticPayouts = false + self.payoutInterval = payoutInterval self.pendingBalanceAmount = pendingBalanceAmount - self.pendingDepositDays = pendingDepositDays - self.lastDeposit = lastDeposit + self.pendingPayoutDays = pendingPayoutDays + self.lastPayout = lastPayout self.availableBalance = availableBalance } } diff --git a/Yosemite/Yosemite/Tools/Payments/WooPaymentsDepositService.swift b/Yosemite/Yosemite/Tools/Payments/WooPaymentsDepositService.swift deleted file mode 100644 index 5d3f044feef..00000000000 --- a/Yosemite/Yosemite/Tools/Payments/WooPaymentsDepositService.swift +++ /dev/null @@ -1,120 +0,0 @@ -import Foundation -import Networking -import WooFoundation - -public protocol WooPaymentsDepositServiceProtocol { - func fetchDepositsOverview() async throws -> [WooPaymentsDepositsOverviewByCurrency] -} - -public final class WooPaymentsDepositService: WooPaymentsDepositServiceProtocol { - // MARK: - Properties - - private let wooPaymentsRemote: WCPayRemote - private var siteID: Int64 - - // MARK: - Initialization - - public convenience init?(siteID: Int64, credentials: Credentials?) { - guard let credentials else { - DDLogError("⛔️ Could not create deposits service due to not finding credentials") - return nil - } - self.init(siteID: siteID, network: AlamofireNetwork(credentials: credentials)) - } - - public init(siteID: Int64, network: Network) { - self.siteID = siteID - self.wooPaymentsRemote = WCPayRemote(network: network) - } - - // MARK: - Public Methods - - public func fetchDepositsOverview() async throws -> [WooPaymentsDepositsOverviewByCurrency] { - do { - let overview = try await wooPaymentsRemote.loadDepositsOverview(for: siteID) - return depositsOverviewForViews(overview) - } - } - - // MARK: - Private Methods - - private func depositsOverviewForViews(_ depositsOverview: Networking.WooPaymentsDepositsOverview) -> [WooPaymentsDepositsOverviewByCurrency] { - guard let defaultCurrency = CurrencyCode(caseInsensitiveRawValue: depositsOverview.account.defaultCurrency) else { - DDLogError("💰 Default currency code not recognised \(depositsOverview.account.defaultCurrency)") - return [] - } - - let currencies = depositsOverview.balance.pending.compactMap { CurrencyCode(caseInsensitiveRawValue: $0.currency) } - - //TODO: check we've not lost any currencies by doing this map, error if we have - - guard currencies.contains(defaultCurrency) else { - DDLogError("💰 Default currency code not found in balances \(depositsOverview.account.defaultCurrency)") - return [] - } - - var depositsOverviews: [WooPaymentsDepositsOverviewByCurrency] = [] - - for currency in currencies { - let pendingBalance = depositsOverview.balance.pending.first { CurrencyCode(caseInsensitiveRawValue: $0.currency) == currency } - let availableBalance = depositsOverview.balance.available.first { CurrencyCode(caseInsensitiveRawValue: $0.currency) == currency } - let lastDeposit = depositsOverview.deposit.lastPaid.first { CurrencyCode(caseInsensitiveRawValue: $0.currency) == currency } - let overview = WooPaymentsDepositsOverviewByCurrency( - currency: currency, - automaticDeposits: depositsOverview.account.depositsSchedule.interval != .manual, - depositInterval: depositsOverview.account.depositsSchedule.interval, - pendingBalanceAmount: balanceAmount(from: pendingBalance), - pendingDepositDays: depositsOverview.account.depositsSchedule.delayDays, - lastDeposit: lastDepositForView(from: lastDeposit), - availableBalance: balanceAmount(from: availableBalance)) - depositsOverviews.append(overview) - } - - moveCurrencyToFront(currency: defaultCurrency, of: &depositsOverviews) - - return depositsOverviews - } - - private func depositAmountDecimal(from amount: Int, - currency: CurrencyCode, - type: WooPaymentsDepositType = .deposit) -> NSDecimalNumber { - switch type { - case .deposit: - return NSDecimalNumber(value: amount).dividing(by: NSDecimalNumber(value: currency.smallestCurrencyUnitMultiplier)) - case .withdrawal: - return NSDecimalNumber(value: -amount).dividing(by: NSDecimalNumber(value: currency.smallestCurrencyUnitMultiplier)) - } - } - - private func balanceAmount(from balance: WooPaymentsBalance?) -> NSDecimalNumber { - guard let balance, - let currency = CurrencyCode(caseInsensitiveRawValue: balance.currency) else { - return .zero - } - return depositAmountDecimal(from: balance.amount, - currency: currency) - } - - private func lastDepositForView(from lastDeposit: WooPaymentsDeposit?) -> WooPaymentsDepositsOverviewByCurrency.LastDeposit? { - guard let lastDeposit, - let currency = CurrencyCode(caseInsensitiveRawValue: lastDeposit.currency) else { - return nil - } - return WooPaymentsDepositsOverviewByCurrency.LastDeposit( - amount: depositAmountDecimal(from: lastDeposit.amount, - currency: currency, - type: lastDeposit.type), - date: lastDeposit.date, - status: lastDeposit.status) - } - - private func moveCurrencyToFront(currency: CurrencyCode, of depositOverviews: inout [WooPaymentsDepositsOverviewByCurrency]) { - guard depositOverviews.count > 1, - let currencyOverviewIndex = depositOverviews.firstIndex(where: { $0.currency == currency }) else { - return - } - - let currencyOverview = depositOverviews.remove(at: currencyOverviewIndex) - depositOverviews.insert(currencyOverview, at: 0) - } -} diff --git a/Yosemite/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift b/Yosemite/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift new file mode 100644 index 00000000000..518c6045a19 --- /dev/null +++ b/Yosemite/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift @@ -0,0 +1,120 @@ +import Foundation +import Networking +import WooFoundation + +public protocol WooPaymentsPayoutServiceProtocol { + func fetchPayoutsOverview() async throws -> [WooPaymentsPayoutsOverviewByCurrency] +} + +public final class WooPaymentsPayoutService: WooPaymentsPayoutServiceProtocol { + // MARK: - Properties + + private let wooPaymentsRemote: WCPayRemote + private var siteID: Int64 + + // MARK: - Initialization + + public convenience init?(siteID: Int64, credentials: Credentials?) { + guard let credentials else { + DDLogError("⛔️ Could not create payouts service due to not finding credentials") + return nil + } + self.init(siteID: siteID, network: AlamofireNetwork(credentials: credentials)) + } + + public init(siteID: Int64, network: Network) { + self.siteID = siteID + self.wooPaymentsRemote = WCPayRemote(network: network) + } + + // MARK: - Public Methods + + public func fetchPayoutsOverview() async throws -> [WooPaymentsPayoutsOverviewByCurrency] { + do { + let overview = try await wooPaymentsRemote.loadPayoutsOverview(for: siteID) + return payoutsOverviewForViews(overview) + } + } + + // MARK: - Private Methods + + private func payoutsOverviewForViews(_ payoutsOverview: Networking.WooPaymentsPayoutsOverview) -> [WooPaymentsPayoutsOverviewByCurrency] { + guard let defaultCurrency = CurrencyCode(caseInsensitiveRawValue: payoutsOverview.account.defaultCurrency) else { + DDLogError("💰 Default currency code not recognised \(payoutsOverview.account.defaultCurrency)") + return [] + } + + let currencies = payoutsOverview.balance.pending.compactMap { CurrencyCode(caseInsensitiveRawValue: $0.currency) } + + //TODO: check we've not lost any currencies by doing this map, error if we have + + guard currencies.contains(defaultCurrency) else { + DDLogError("💰 Default currency code not found in balances \(payoutsOverview.account.defaultCurrency)") + return [] + } + + var payoutsOverviews: [WooPaymentsPayoutsOverviewByCurrency] = [] + + for currency in currencies { + let pendingBalance = payoutsOverview.balance.pending.first { CurrencyCode(caseInsensitiveRawValue: $0.currency) == currency } + let availableBalance = payoutsOverview.balance.available.first { CurrencyCode(caseInsensitiveRawValue: $0.currency) == currency } + let lastPayout = payoutsOverview.deposit.lastPaid.first { CurrencyCode(caseInsensitiveRawValue: $0.currency) == currency } + let overview = WooPaymentsPayoutsOverviewByCurrency( + currency: currency, + automaticPayouts: payoutsOverview.account.payoutsSchedule.interval != .manual, + payoutInterval: payoutsOverview.account.payoutsSchedule.interval, + pendingBalanceAmount: balanceAmount(from: pendingBalance), + pendingPayoutDays: payoutsOverview.account.payoutsSchedule.delayDays, + lastPayout: lastPayoutForView(from: lastPayout), + availableBalance: balanceAmount(from: availableBalance)) + payoutsOverviews.append(overview) + } + + moveCurrencyToFront(currency: defaultCurrency, of: &payoutsOverviews) + + return payoutsOverviews + } + + private func payoutAmountDecimal(from amount: Int, + currency: CurrencyCode, + type: WooPaymentsPayoutType = .deposit) -> NSDecimalNumber { + switch type { + case .deposit: + return NSDecimalNumber(value: amount).dividing(by: NSDecimalNumber(value: currency.smallestCurrencyUnitMultiplier)) + case .withdrawal: + return NSDecimalNumber(value: -amount).dividing(by: NSDecimalNumber(value: currency.smallestCurrencyUnitMultiplier)) + } + } + + private func balanceAmount(from balance: WooPaymentsBalance?) -> NSDecimalNumber { + guard let balance, + let currency = CurrencyCode(caseInsensitiveRawValue: balance.currency) else { + return .zero + } + return payoutAmountDecimal(from: balance.amount, + currency: currency) + } + + private func lastPayoutForView(from lastPayout: WooPaymentsPayout?) -> WooPaymentsPayoutsOverviewByCurrency.LastPayout? { + guard let lastPayout, + let currency = CurrencyCode(caseInsensitiveRawValue: lastPayout.currency) else { + return nil + } + return WooPaymentsPayoutsOverviewByCurrency.LastPayout( + amount: payoutAmountDecimal(from: lastPayout.amount, + currency: currency, + type: lastPayout.type), + date: lastPayout.date, + status: lastPayout.status) + } + + private func moveCurrencyToFront(currency: CurrencyCode, of payoutOverviews: inout [WooPaymentsPayoutsOverviewByCurrency]) { + guard payoutOverviews.count > 1, + let currencyOverviewIndex = payoutOverviews.firstIndex(where: { $0.currency == currency }) else { + return + } + + let currencyOverview = payoutOverviews.remove(at: currencyOverviewIndex) + payoutOverviews.insert(currencyOverview, at: 0) + } +} diff --git a/Yosemite/YosemiteTests/Tools/Payments/WooPaymentsDepositServiceTests.swift b/Yosemite/YosemiteTests/Tools/Payments/WooPaymentsPayoutServiceTests.swift similarity index 56% rename from Yosemite/YosemiteTests/Tools/Payments/WooPaymentsDepositServiceTests.swift rename to Yosemite/YosemiteTests/Tools/Payments/WooPaymentsPayoutServiceTests.swift index 2685a260e0e..ecbb0bba636 100644 --- a/Yosemite/YosemiteTests/Tools/Payments/WooPaymentsDepositServiceTests.swift +++ b/Yosemite/YosemiteTests/Tools/Payments/WooPaymentsPayoutServiceTests.swift @@ -2,14 +2,14 @@ import XCTest @testable import Yosemite @testable import Networking -final class WooPaymentsDepositServiceTests: XCTestCase { - var service: WooPaymentsDepositService! +final class WooPaymentsPayoutServiceTests: XCTestCase { + var service: WooPaymentsPayoutService! var mockNetwork: MockNetwork! override func setUp() { super.setUp() mockNetwork = MockNetwork() - service = WooPaymentsDepositService(siteID: 12345, network: mockNetwork) + service = WooPaymentsPayoutService(siteID: 12345, network: mockNetwork) } override func tearDown() { @@ -18,93 +18,93 @@ final class WooPaymentsDepositServiceTests: XCTestCase { super.tearDown() } - func test_fetchDepositsOverview_returns_one_model_per_response_element() async { + func test_fetchPayoutsOverview_returns_one_model_per_response_element() async { // Given mockNetwork.simulateResponse(requestUrlSuffix: "payments/deposits/overview-all", filename: "deposits-overview-all") do { // When - let depositsOverviews = try await service.fetchDepositsOverview() + let payoutsOverviews = try await service.fetchPayoutsOverview() // Then - assertEqual(2, depositsOverviews.count) + assertEqual(2, payoutsOverviews.count) } catch { XCTFail("Unexpected error: \(error)") } } - func test_fetchDepositsOverview_returns_the_default_currency_first() async { + func test_fetchPayoutsOverview_returns_the_default_currency_first() async { // Given mockNetwork.simulateResponse(requestUrlSuffix: "payments/deposits/overview-all", filename: "deposits-overview-all") do { // When - let depositsOverviews = try await service.fetchDepositsOverview() + let payoutsOverviews = try await service.fetchPayoutsOverview() // Then - assertEqual(.GBP, depositsOverviews.first?.currency) + assertEqual(.GBP, payoutsOverviews.first?.currency) } catch { XCTFail("Unexpected error: \(error)") } } - func test_fetchDepositsOverview_returns_empty_array_if_default_currency_lost() async { + func test_fetchPayoutsOverview_returns_empty_array_if_default_currency_lost() async { // Given mockNetwork.simulateResponse(requestUrlSuffix: "payments/deposits/overview-all", filename: "deposits-overview-all-no-default-currency") do { // When - let depositsOverviews = try await service.fetchDepositsOverview() + let payoutsOverviews = try await service.fetchPayoutsOverview() // Then - XCTAssert(depositsOverviews.isEmpty) + XCTAssert(payoutsOverviews.isEmpty) } catch { XCTFail("Unexpected error: \(error)") } } - func test_fetchDepositsOverview_returns_valid_data_for_lowercase_currency() async { + func test_fetchPayoutsOverview_returns_valid_data_for_lowercase_currency() async { // Given // (the overview JSON specifies currency as "eur") mockNetwork.simulateResponse(requestUrlSuffix: "payments/deposits/overview-all", filename: "deposits-overview-all") do { // When - let depositsOverviews = try await service.fetchDepositsOverview() + let payoutsOverviews = try await service.fetchPayoutsOverview() // Then - let euroDepositOverview = try XCTUnwrap(depositsOverviews.first(where: { $0.currency == .EUR } )) - assertEqual(NSDecimalNumber(string: "20.18"), euroDepositOverview.pendingBalanceAmount) + let euroPayoutOverview = try XCTUnwrap(payoutsOverviews.first(where: { $0.currency == .EUR } )) + assertEqual(NSDecimalNumber(string: "20.18"), euroPayoutOverview.pendingBalanceAmount) } catch { XCTFail("Unexpected error: \(error)") } } - func test_fetchDepositsOverview_returns_valid_data_for_uppercase_currency() async { + func test_fetchPayoutsOverview_returns_valid_data_for_uppercase_currency() async { // Given // (this overview JSON specifies currency as "GBP") mockNetwork.simulateResponse(requestUrlSuffix: "payments/deposits/overview-all", filename: "deposits-overview-all") do { // When - let depositsOverviews = try await service.fetchDepositsOverview() + let payoutsOverviews = try await service.fetchPayoutsOverview() // Then - let euroDepositOverview = try XCTUnwrap(depositsOverviews.first(where: { $0.currency == .GBP } )) - assertEqual(NSDecimalNumber(string: "34.54"), euroDepositOverview.pendingBalanceAmount) + let euroPayoutOverview = try XCTUnwrap(payoutsOverviews.first(where: { $0.currency == .GBP } )) + assertEqual(NSDecimalNumber(string: "34.54"), euroPayoutOverview.pendingBalanceAmount) } catch { XCTFail("Unexpected error: \(error)") } } - func testFetchDepositsOverviewError() async { + func testFetchPayoutsOverviewError() async { // Given let mockError = DotcomError.noRestRoute mockNetwork.simulateError(requestUrlSuffix: "payments/deposits/overview-all", error: mockError) do { // When - _ = try await service.fetchDepositsOverview() + _ = try await service.fetchPayoutsOverview() XCTFail("Expected an error, but the call succeeded.") } catch { // Then From 1cdbe990533fb4fabe95bef2def9584bd904ac44 Mon Sep 17 00:00:00 2001 From: Spencer Transier Date: Wed, 13 Nov 2024 23:47:04 -0800 Subject: [PATCH 19/49] Remove unneeded check --- .buildkite/commands/checkout-release-branch.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh index f00ab2255f3..05598ec4e48 100755 --- a/.buildkite/commands/checkout-release-branch.sh +++ b/.buildkite/commands/checkout-release-branch.sh @@ -4,11 +4,6 @@ RELEASE_VERSION="${1:?RELEASE_VERSION parameter missing}" echo "--- :git: Checkout Release Branch" -if [[ -z "${RELEASE_VERSION}" ]]; then - echo "RELEASE_VERSION is not set." - exit 1 -fi - # Buildkite, by default, checks out a specific commit. For many release actions, we need to be # on a release branch instead. BRANCH_NAME="release/${RELEASE_VERSION}" From 66d83393367a48aff3efb700938c24efad2c8a04 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:46:51 +0200 Subject: [PATCH 20/49] Created ReceiptEmailView that accepts email input and sends email receipt --- .../ReceiptEmail/ReceiptEmailView.swift | 116 ++++++++++++++++++ .../ReceiptEmail/ReceiptEmailViewModel.swift | 57 +++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 20 +++ 3 files changed, 193 insertions(+) create mode 100644 WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailView.swift create mode 100644 WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailViewModel.swift diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailView.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailView.swift new file mode 100644 index 00000000000..d17765b750e --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailView.swift @@ -0,0 +1,116 @@ +import SwiftUI +import Yosemite + +struct ReceiptEmailView: View { + @ObservedObject var viewModel: ReceiptEmailViewModel + + var body: some View { + NavigationView { + VStack { + VStack(spacing: 0) { + Divider() + TitleAndTextFieldRow(title: Localization.emailField, + placeholder: Localization.emailHint, + text: $viewModel.email, + symbol: nil, + fieldAlignment: .leading, + keyboardType: .emailAddress) + .autocapitalization(.none) + .autocorrectionDisabled() + .focused() + .onSubmit(viewModel.sendReceipt) + Divider() + } + .background(Color(.systemBackground).ignoresSafeArea(.container, edges: .horizontal)) + Spacer() + + Button(Localization.emailButton) { + viewModel.sendReceipt() + } + .disabled(!viewModel.isEmailValid) + .buttonStyle(PrimaryLoadingButtonStyle(isLoading: viewModel.isLoading)) + .padding() + } + .padding(.top) + .background(Color(uiColor: .listBackground)) + .wooNavigationBarStyle() + .navigationTitle(Localization.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(Localization.cancel, action: { + viewModel.onDismiss(false) + }) + } + } + } + } +} + +private enum Localization { + static let title = NSLocalizedString( + "order.receiptEmailView.title", + value: "Email Receipt to Customer", + comment: "Title for the screen to update customer email address and send receipt" + ) + + static let emailField = NSLocalizedString( + "order.receiptEmailView.emailFieldTitle", + value: "Email", + comment: "Email text field title" + ) + + static let emailHint = NSLocalizedString( + "order.receiptEmailView.emailFieldHint", + value: "Enter Email", + comment: "Email field placeholder" + ) + + static let emailButton = NSLocalizedString( + "order.receiptEmailView.emailReceipt", + value: "Email Receipt", + comment: "Title for the button to send the receipt to the customer" + ) + + static let cancel = NSLocalizedString( + "order.receiptEmailView.cancel", + value: "Cancel", + comment: "Text for the cancel button to dismiss Send Receipt to Customer screen" + ) + + static let invalidEmail = NSLocalizedString( + "order.receiptEmailView.invalidEmailError", + value: "Please enter a valid email address.", + comment: "Notice text when the merchant enters an invalid email" + ) +} + +final class ReceiptEmailViewHostingController: UIHostingController, UIAdaptivePresentationControllerDelegate { + private var onDismiss: ((Bool) -> Void) + + init(order: Order, + stores: StoresManager = ServiceLocator.stores, + systemNoticePresenter: NoticePresenter = ServiceLocator.noticePresenter, + onDismiss: @escaping (Bool) -> Void) { + + self.onDismiss = onDismiss + let viewModel = ReceiptEmailViewModel(order: order, stores: stores, onDismiss: onDismiss) + super.init(rootView: ReceiptEmailView(viewModel: viewModel)) + + viewModel.onDismiss = { [weak self] success in + self?.dismiss(animated: true, completion: nil) + onDismiss(success) + } + + presentationController?.delegate = self + viewModel.noticePresenter.presentingViewController = self + } + + required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + onDismiss(false) + } +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailViewModel.swift new file mode 100644 index 00000000000..e15b838ae86 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailViewModel.swift @@ -0,0 +1,57 @@ +import Foundation +import class WordPressShared.EmailFormatValidator +import Yosemite + +final class ReceiptEmailViewModel: ObservableObject { + @Published var email: String = "" + @Published var isLoading: Bool = false + + private let order: Order + private let stores: StoresManager + var noticePresenter: NoticePresenter + var emailValidator: (String) -> Bool = EmailFormatValidator.validate + var onDismiss: (Bool) -> Void + + init(order: Order, + stores: StoresManager, + noticesPresenter: NoticePresenter = DefaultNoticePresenter(), + onDismiss: @escaping (Bool) -> Void = { _ in }) { + self.order = order + self.stores = stores + self.noticePresenter = noticesPresenter + self.onDismiss = onDismiss + } + + var isEmailValid: Bool { + emailValidator(email) + } + + func sendReceipt() { + let email = email + let action = ReceiptAction.sendReceipt(order: order, email: email) { [weak self] result in + DispatchQueue.main.async { + guard let self else { return } + self.isLoading = false + switch result { + case .success: + self.onDismiss(true) + case let .failure(error): + DDLogError("Sending email receipt failed: \(error.localizedDescription)") + self.noticePresenter.enqueue(notice: Notice(title: Localization.errorNotice, feedbackType: .error)) + } + } + } + + self.isLoading = true + stores.dispatch(action) + } +} + +private enum Localization { + static let errorNotice = NSLocalizedString( + "order.receiptEmailView.errorNotice", + value: "Error sending the email receipt. Please try again.", + comment: "An error that is shown when sending email receipt fails." + ) + +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 71c5a070843..703a910b168 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -32,8 +32,11 @@ 01664F9E2C50E685007CB5DD /* POSFontStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01664F9D2C50E685007CB5DD /* POSFontStyle.swift */; }; 016BCAFF2C4F907F009D8367 /* CartViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016BCAFE2C4F907F009D8367 /* CartViewModelProtocol.swift */; }; 016C6B972C74AB17000D86FD /* POSConnectivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016C6B962C74AB17000D86FD /* POSConnectivityView.swift */; }; + 0174DDBB2CE5FD60005D20CA /* ReceiptEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */; }; + 0174DDBF2CE600C5005D20CA /* ReceiptEmailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0174DDBE2CE600C0005D20CA /* ReceiptEmailViewModelTests.swift */; }; 0182C8BE2CE3B11300474355 /* MockReceiptEligibilityUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182C8BD2CE3B10E00474355 /* MockReceiptEligibilityUseCase.swift */; }; 0182C8C02CE4DDC700474355 /* CardReaderTransactionAlertEmailReceiptAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertEmailReceiptAction.swift */; }; + 0182C8C22CE4F0DB00474355 /* ReceiptEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182C8C12CE4F0DB00474355 /* ReceiptEmailView.swift */; }; 0188CA0F2C65622A0051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0188CA0E2C65622A0051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift */; }; 0188CA112C6565320051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0188CA102C6565320051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift */; }; 018D5C7E2CA6B4A60085EBEE /* CurrencySettings+Sanitized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018D5C7D2CA6B49D0085EBEE /* CurrencySettings+Sanitized.swift */; }; @@ -3146,8 +3149,11 @@ 01664F9D2C50E685007CB5DD /* POSFontStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSFontStyle.swift; sourceTree = ""; }; 016BCAFE2C4F907F009D8367 /* CartViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartViewModelProtocol.swift; sourceTree = ""; }; 016C6B962C74AB17000D86FD /* POSConnectivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSConnectivityView.swift; sourceTree = ""; }; + 0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModel.swift; sourceTree = ""; }; + 0174DDBE2CE600C0005D20CA /* ReceiptEmailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModelTests.swift; sourceTree = ""; }; 0182C8BD2CE3B10E00474355 /* MockReceiptEligibilityUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockReceiptEligibilityUseCase.swift; sourceTree = ""; }; 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertEmailReceiptAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderTransactionAlertEmailReceiptAction.swift; sourceTree = ""; }; + 0182C8C12CE4F0DB00474355 /* ReceiptEmailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailView.swift; sourceTree = ""; }; 0188CA0E2C65622A0051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift; sourceTree = ""; }; 0188CA102C6565320051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift; sourceTree = ""; }; 018D5C7D2CA6B49D0085EBEE /* CurrencySettings+Sanitized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrencySettings+Sanitized.swift"; sourceTree = ""; }; @@ -6240,6 +6246,15 @@ path = "Reusable Views"; sourceTree = ""; }; + 0174DDB92CE5FD49005D20CA /* ReceiptEmail */ = { + isa = PBXGroup; + children = ( + 0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */, + 0182C8C12CE4F0DB00474355 /* ReceiptEmailView.swift */, + ); + path = ReceiptEmail; + sourceTree = ""; + }; 0202B6932387ACE000F3EBE0 /* TabBar */ = { isa = PBXGroup; children = ( @@ -9420,6 +9435,7 @@ 6850C5EF2B69E7300026A93B /* Receipts */ = { isa = PBXGroup; children = ( + 0174DDB92CE5FD49005D20CA /* ReceiptEmail */, 6850C5F02B69E74D0026A93B /* ReceiptViewController.swift */, ); path = Receipts; @@ -9429,6 +9445,7 @@ isa = PBXGroup; children = ( 6850C5F32B6A11CA0026A93B /* ReceiptViewModelTests.swift */, + 0174DDBE2CE600C0005D20CA /* ReceiptEmailViewModelTests.swift */, 68674D302B6C895D00E93FBD /* ReceiptEligibilityUseCaseTests.swift */, ); path = Receipts; @@ -15077,6 +15094,7 @@ DEE6437626D87C4100888A75 /* PrintCustomsFormsView.swift in Sources */, DE3144862C5780250015F089 /* UnavailableAnalyticsView.swift in Sources */, 450C2CB624D1ABB200D570DD /* ProductImagesGalleryViewController.swift in Sources */, + 0174DDBB2CE5FD60005D20CA /* ReceiptEmailViewModel.swift in Sources */, EEA6935E2B231C6600BAECA6 /* ProductCreationAISurveyConfirmationViewModel.swift in Sources */, B9EF083F2886CE3300D96C58 /* HostingTableViewCell.swift in Sources */, B946881429B8DD6A000646B0 /* OrderListViewController+Activity.swift in Sources */, @@ -15693,6 +15711,7 @@ 02820F3422C257B700DE0D37 /* UITableView+HeaderFooterHelpers.swift in Sources */, EEC259422B43EF33004D703C /* BlazeEditAdView.swift in Sources */, 03E471C0293A158D001A58AD /* CardReaderConnectionAlertsProviding.swift in Sources */, + 0182C8C22CE4F0DB00474355 /* ReceiptEmailView.swift in Sources */, 02D1D2DA2CD3CDA40069A93F /* WooAnalyticsEvent+PointOfSale.swift in Sources */, D8C2A291231BD0FD00F503E9 /* ReviewsDataSource.swift in Sources */, CE855366209BA6A700938BDC /* CustomerInfoTableViewCell.swift in Sources */, @@ -16708,6 +16727,7 @@ DE9A02A32A44441200193ABF /* RequirementsCheckerTests.swift in Sources */, D802547326551D0F001B2CC1 /* CardPresentModalTapCardTests.swift in Sources */, B55BC1F321A8790F0011A0C0 /* StringHTMLTests.swift in Sources */, + 0174DDBF2CE600C5005D20CA /* ReceiptEmailViewModelTests.swift in Sources */, 0375799D2822F9040083F2E1 /* MockCardPresentPaymentsOnboardingPresenter.swift in Sources */, 455800CC24C6F83F00A8D117 /* ProductSettingsSectionsTests.swift in Sources */, 86EC6EB92CD0BA6A00D7D2FE /* CustomFieldEditorViewModelTests.swift in Sources */, From 9868f7bb5ff566624a0c8214408642294ecb6229 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:47:33 +0200 Subject: [PATCH 21/49] Created ReceiptEmailViewModelTests --- .../Receipts/ReceiptEmailViewModelTests.swift | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEmailViewModelTests.swift diff --git a/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEmailViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEmailViewModelTests.swift new file mode 100644 index 00000000000..a81d890c5d9 --- /dev/null +++ b/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEmailViewModelTests.swift @@ -0,0 +1,89 @@ +import Testing +import Foundation +import Yosemite +@testable import WooCommerce + +@MainActor +struct ReceiptEmailViewModelTests { + private let stores: MockStoresManager + private let order: Order + private let noticesPresenter: MockNoticePresenter + private let sut: ReceiptEmailViewModel + + init() { + stores = MockStoresManager(sessionManager: .testingInstance) + order = Order.fake() + noticesPresenter = MockNoticePresenter() + sut = ReceiptEmailViewModel( + order: order, + stores: stores, + noticesPresenter: noticesPresenter + ) + } + + @Test func sendReceipt_when_action_succeeds() async { + // Given send receipt action suceeds + sut.email = "test@test.com" + stores.whenReceivingAction(ofType: ReceiptAction.self) { action in + switch action { + case let .sendReceipt(order, _, onCompletion): + onCompletion(.success(order)) + default: + #expect(Bool(false), "Unexpected action: \(action)") + } + } + + // When + let completionResult = await withCheckedContinuation { continuation in + sut.onDismiss = { + continuation.resume(returning: $0) + } + sut.sendReceipt() + } + + // Then + #expect(completionResult == true) + } + + @Test func sendReceipt_when_action_fails() async { + // Given send receipt action fails + sut.email = "test@test.com" + stores.whenReceivingAction(ofType: ReceiptAction.self) { action in + switch action { + case let .sendReceipt(_, _, onCompletion): + struct FakeError: Error { + var localizedDescription: String { "Test error" } + } + onCompletion(.failure(FakeError())) + default: + #expect(Bool(false), "Unexpected action: \(action)") + } + } + + // When + let completionResult = await withCheckedContinuation { continuation in + noticesPresenter.onNoticeQueued = { + continuation.resume(returning: $0) + } + sut.sendReceipt() + } + + // Then + #expect(completionResult.title != nil) + } + + @Test(arguments: [true, false]) + func isEmailValid(_ validatorResult: Bool) { + // Given + sut.email = "test@test.com" + var validatedEmail = "" + sut.emailValidator = { email in + validatedEmail = email + return validatorResult + } + + // Then + #expect(sut.isEmailValid == validatorResult) + #expect(sut.email == validatedEmail) + } +} From 98f800fd3777a293da9c5e147fe73e0d2142545d Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:47:56 +0200 Subject: [PATCH 22/49] Update MockNoticePresenter.swift --- WooCommerce/WooCommerceTests/Mocks/MockNoticePresenter.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WooCommerce/WooCommerceTests/Mocks/MockNoticePresenter.swift b/WooCommerce/WooCommerceTests/Mocks/MockNoticePresenter.swift index 19f3f1d6c2d..2f89c477a5c 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockNoticePresenter.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockNoticePresenter.swift @@ -10,9 +10,11 @@ final class MockNoticePresenter: NoticePresenter { var presentingViewController: UIViewController? private(set) var queuedNotices = [Notice]() + var onNoticeQueued: (Notice) -> Void = { _ in } func enqueue(notice: Notice) -> Bool { queuedNotices.append(notice) + onNoticeQueued(notice) return true } } From 11dab624f8c3ff51e532efe3d2c401e4d49e460d Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:36:54 +0200 Subject: [PATCH 23/49] Integrate send receipt modal into CollectOrderPaymentUseCase --- .../CollectOrderPaymentUseCase.swift | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index 7db88e66a93..564b4393a3c 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -566,8 +566,8 @@ private extension CollectOrderPaymentUseCase { }) } // Sends receipt via API - let addCustomerEmailAndSendReceiptCompletionAction: () -> Void = { - // TODO + let addCustomerEmailAndSendReceiptCompletionAction: () -> Void = { [weak self] in + self?.presentSendReceiptAfterPayment(onCompleted: onCompleted) } // Presents receipt alert @@ -688,6 +688,16 @@ private extension CollectOrderPaymentUseCase { } } +// MARK: - Collect customer email and send receipt after payment presentation +private extension CollectOrderPaymentUseCase { + func presentSendReceiptAfterPayment(onCompleted: @escaping (() -> Void)) { + let receiptEmailViewController = ReceiptEmailViewHostingController(order: order) { _ in + onCompleted() + } + rootViewController.present(receiptEmailViewController, animated: true) + } +} + // MARK: Interac handling private extension CollectOrderPaymentUseCase { /// For certain payment methods like Interac in Canada, the payment is captured on the client side (customer is charged). From 2d4e6b6d4b8a1f832829eb6af913a57cbb3a569f Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 14 Nov 2024 14:36:56 +0000 Subject: [PATCH 24/49] 14417 Move order sync and order state to posModel Tests are not covered by this commit, and there is probably further refactoring that can be done. --- .../Models/PointOfSaleAggregateModel.swift | 160 +++++++++- .../POS/Models/PointOfSaleOrderState.swift | 48 +++ .../POS/Models/PointOfSaleOrderTotals.swift | 9 + .../Classes/POS/Presentation/CartView.swift | 22 +- .../POS/Presentation/ItemListView.swift | 8 +- .../PointOfSaleDashboardView.swift | 16 +- .../PointOfSaleEntryPointView.swift | 6 +- .../Classes/POS/Presentation/TotalsView.swift | 80 ++--- .../POS/ViewModels/CartViewModel.swift | 10 - .../ViewModels/CartViewModelProtocol.swift | 4 - .../PointOfSaleDashboardViewModel.swift | 20 +- .../POS/ViewModels/TotalsViewModel.swift | 277 ++---------------- .../ViewModels/TotalsViewModelProtocol.swift | 3 - .../WooCommerce.xcodeproj/project.pbxproj | 8 + .../POS/ViewModels/TotalsViewModelTests.swift | 4 +- 15 files changed, 320 insertions(+), 355 deletions(-) create mode 100644 WooCommerce/Classes/POS/Models/PointOfSaleOrderState.swift create mode 100644 WooCommerce/Classes/POS/Models/PointOfSaleOrderTotals.swift diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 3d087f3a317..8c57e5ff421 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -1,8 +1,14 @@ import Foundation +import Combine import protocol Yosemite.POSItem import protocol Yosemite.POSItemProvider import protocol WooFoundation.Analytics +import struct Yosemite.Order +import struct Yosemite.OrderItem +import protocol Yosemite.POSOrderServiceProtocol +import struct Yosemite.POSCartItem +import class WooFoundation.CurrencyFormatter protocol PointOfSaleAggregateModelProtocol { var orderStage: PointOfSaleOrderStage { get } @@ -22,9 +28,12 @@ protocol PointOfSaleAggregateModelProtocol { func addToCart(_ item: POSItem) func remove(cartItem: CartItem) func removeAllItemsFromCart() - func submitCart() + func submitCart() async func addMoreToCart() func startNewCart() + + var orderState: PointOfSaleOrderState { get } + func checkOut() async } class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProtocol { @@ -37,17 +46,29 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt @Published private(set) var cart: [CartItem] = [] + @Published private(set) var orderState: PointOfSaleOrderState = .idle + + private var order: Order? = nil + private let itemProvider: POSItemProvider private let cardPresentPaymentService: CardPresentPaymentFacade + private let orderService: POSOrderServiceProtocol + private let currencyFormatter: CurrencyFormatter private let analytics: Analytics private var currentPage: Int = Constants.initialPage + private var startPaymentOnCardReaderConnection: AnyCancellable? + private var cardReaderDisconnection: AnyCancellable? init(itemProvider: POSItemProvider, cardPresentPaymentService: CardPresentPaymentFacade, + orderService: POSOrderServiceProtocol, + currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings), analytics: Analytics = ServiceLocator.analytics) { self.itemProvider = itemProvider self.cardPresentPaymentService = cardPresentPaymentService + self.orderService = orderService + self.currencyFormatter = currencyFormatter self.analytics = analytics publishCardReaderConnectionStatus() } @@ -128,8 +149,10 @@ extension PointOfSaleAggregateModel { cart.removeAll() } - func submitCart() { + @MainActor + func submitCart() async { orderStage = .finalizing + await checkOut() } func addMoreToCart() { @@ -138,6 +161,7 @@ extension PointOfSaleAggregateModel { func startNewCart() { removeAllItemsFromCart() + clearOrder() orderStage = .building } } @@ -161,6 +185,138 @@ extension PointOfSaleAggregateModel { await cardPresentPaymentService.disconnectReader() } } + + /// Starts a payment immediately if a reader is connected. + /// Otherwise, schedules a payment to start the next time a reader connects. + /// Note that any schedlued payments are cancelled by `cancelReaderPreparation` + /// e.g. when the TotalsView goes offscreen. + func startPaymentWhenCardReaderConnected() async { + guard case .connected = cardReaderConnectionStatus else { + return startPaymentOnCardReaderConnection = $cardReaderConnectionStatus + .filter { status in + switch status { + case .connected: + return true + case .disconnected, .disconnecting, .cancellingConnection: + return false + } + } + .removeDuplicates() + .sink { _ in + Task { @MainActor [weak self] in + await self?.collectPayment() + } + } + } + await collectPayment() + } + + @MainActor + func collectPayment() async { + guard let order else { + return + // Should this throw? + } + do { + try await collectPayment(for: order) + } catch { + DDLogError("Error taking payment: \(error)") + } + } + + @MainActor + private func collectPayment(for order: Order) async throws { + _ = try await cardPresentPaymentService.collectPayment(for: order, using: .bluetooth) + } + + func cancelThenCollectPayment() { + cardPresentPaymentService.cancelPayment() + Task { [weak self] in + await self?.collectPayment() + } + } + + func cancelCardReaderPreparation() { + cardPresentPaymentService.cancelPayment() + startPaymentOnCardReaderConnection?.cancel() + cardReaderDisconnection?.cancel() + } + + func observeReaderReconnection() { + cardReaderDisconnection = $cardReaderConnectionStatus + .filter({ $0 == .disconnected }) + .sink { [weak self] _ in + Task { @MainActor [weak self] in + await self?.startPaymentWhenCardReaderConnected() + } + } + } +} + +// MARK: - Order syncing + +extension PointOfSaleAggregateModel { + func checkOut() async { + guard CartItem.areOrderAndCartDifferent(order: order, cartItems: cart) else { + await startPaymentWhenCardReaderConnected() + return + } + // calculate totals and sync order if there was a change in the cart + await syncOrder(for: cart, allItems: allItems) + } + + @MainActor + private func syncOrder(for cartProducts: [CartItem], allItems: [POSItem]) async { + guard orderState.isSyncing == false else { + return + } + orderState = .syncing + let cart = cartProducts.map { + POSCartItem(itemID: nil, product: $0.item, quantity: Decimal($0.quantity)) + } + + do { + let syncedOrder = try await orderService.syncOrder(cart: cart, order: order, allProducts: allItems) + self.order = syncedOrder + orderState = .loaded(totals(for: syncedOrder)) + await startPaymentWhenCardReaderConnected() + DDLogInfo("🟢 [POS] Synced order: \(syncedOrder)") + } catch { + DDLogError("🔴 [POS] Error syncing order: \(error)") + + // Consider removing error or handle specific errors with our own formatting and localization + orderState = .error(.init(message: error.localizedDescription, handler: { [weak self] in + Task { + await self?.syncOrder(for: cartProducts, allItems: allItems) + } + })) + } + } + + private func clearOrder() { + order = nil + } +} + +// MARK: - Price formatters + +private extension PointOfSaleAggregateModel { + func totals(for order: Order) -> PointOfSaleOrderTotals { + let totalsCalculator = OrderTotalsCalculator(for: order, + using: currencyFormatter) + return PointOfSaleOrderTotals( + cartTotal: formattedPrice(totalsCalculator.itemsTotal.stringValue, + currency: order.currency) ?? "", + orderTotal: formattedPrice(order.total, currency: order.currency) ?? "", + taxTotal: formattedPrice(order.totalTax, currency: order.currency) ?? "") + } + + func formattedPrice(_ price: String?, currency: String?) -> String? { + guard let price, let currency else { + return nil + } + return currencyFormatter.formatAmount(price, with: currency) + } } private extension PointOfSaleAggregateModel { diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleOrderState.swift b/WooCommerce/Classes/POS/Models/PointOfSaleOrderState.swift new file mode 100644 index 00000000000..412fb4b0cd6 --- /dev/null +++ b/WooCommerce/Classes/POS/Models/PointOfSaleOrderState.swift @@ -0,0 +1,48 @@ +import Foundation + +enum PointOfSaleOrderState: Equatable { + case idle + case syncing + case loaded(PointOfSaleOrderTotals) + case error(PointOfSaleOrderSyncErrorMessageViewModel) + + static func == (lhs: PointOfSaleOrderState, rhs: PointOfSaleOrderState) -> Bool { + switch (lhs, rhs) { + case (.idle, .idle), + (.syncing, .syncing), + (.error, .error): + return true + case (.loaded(let lhsTotals), .loaded(let rhsTotals)): + return lhsTotals == rhsTotals + default: + return false + } + } + + var isSyncing: Bool { + switch self { + case .syncing: + return true + default: + return false + } + } + + var isLoaded: Bool { + switch self { + case .loaded: + return true + default: + return false + } + } + + var isError: Bool { + switch self { + case .error: + return true + default: + return false + } + } +} diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleOrderTotals.swift b/WooCommerce/Classes/POS/Models/PointOfSaleOrderTotals.swift new file mode 100644 index 00000000000..3045134307a --- /dev/null +++ b/WooCommerce/Classes/POS/Models/PointOfSaleOrderTotals.swift @@ -0,0 +1,9 @@ +import Foundation + +struct PointOfSaleOrderTotals: Equatable { + // Arguably these should be unformatted, and then we can rely on the SwiftUI formatter. + // To do that, we'd need to include Decimal amounts and the order currency in this struct. + let cartTotal: String + let orderTotal: String + let taxTotal: String +} diff --git a/WooCommerce/Classes/POS/Presentation/CartView.swift b/WooCommerce/Classes/POS/Presentation/CartView.swift index 88d5d0b3102..4f3db4ddd4f 100644 --- a/WooCommerce/Classes/POS/Presentation/CartView.swift +++ b/WooCommerce/Classes/POS/Presentation/CartView.swift @@ -195,9 +195,9 @@ private extension CartView { private extension CartView { var checkoutButton: some View { Button { - posModel.submitCart() - // Remove when totalsViewModel doesn't do the submission any more - cartViewModel.submitCart() + Task { @MainActor in + await posModel.submitCart() + } } label: { Text(Localization.checkoutButtonTitle) } @@ -250,18 +250,18 @@ import class WooFoundation.MockAnalyticsPreview import class WooFoundation.MockAnalyticsProviderPreview #Preview { + let posModel = PointOfSaleAggregateModel( + itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService(), + orderService: POSOrderPreviewService()) // TODO: // Simplify this by mocking `CartViewModel` - let totalsViewModel = TotalsViewModel(orderService: POSOrderPreviewService(), + let totalsViewModel = TotalsViewModel(posModel: posModel, cardPresentPaymentService: CardPresentPaymentPreviewService(), - currencyFormatter: .init(currencySettings: .init()), paymentState: .acceptingCard) - let cartViewModel = CartViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), - cardPresentPaymentService: CardPresentPaymentPreviewService())) - let itemsListViewModel = ItemListViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), - cardPresentPaymentService: CardPresentPaymentPreviewService())) - let dashboardViewModel = PointOfSaleDashboardViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), - cardPresentPaymentService: CardPresentPaymentPreviewService()), + let cartViewModel = CartViewModel(posModel: posModel) + let itemsListViewModel = ItemListViewModel(posModel: posModel) + let dashboardViewModel = PointOfSaleDashboardViewModel(posModel: posModel, totalsViewModel: totalsViewModel, cartViewModel: cartViewModel, itemListViewModel: itemsListViewModel, diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index 2e96da4ad48..8de84457429 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -248,7 +248,11 @@ private extension ItemListView { #if DEBUG #Preview { - ItemListView(viewModel: ItemListViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), - cardPresentPaymentService: CardPresentPaymentPreviewService()))) + ItemListView( + viewModel: ItemListViewModel( + posModel: PointOfSaleAggregateModel( + itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService(), + orderService: POSOrderPreviewService()))) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index 32f27433302..ff17787e9d0 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -207,16 +207,16 @@ import class WooFoundation.MockAnalyticsPreview import class WooFoundation.MockAnalyticsProviderPreview #Preview { - let totalsVM = TotalsViewModel(orderService: POSOrderPreviewService(), + let posModel = PointOfSaleAggregateModel( + itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService(), + orderService: POSOrderPreviewService()) + let totalsVM = TotalsViewModel(posModel: posModel, cardPresentPaymentService: CardPresentPaymentPreviewService(), - currencyFormatter: .init(currencySettings: .init()), paymentState: .acceptingCard) - let cartVM = CartViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), - cardPresentPaymentService: CardPresentPaymentPreviewService())) - let itemsListVM = ItemListViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), - cardPresentPaymentService: CardPresentPaymentPreviewService())) - let posVM = PointOfSaleDashboardViewModel(posModel: PointOfSaleAggregateModel(itemProvider: POSItemProviderPreview(), - cardPresentPaymentService: CardPresentPaymentPreviewService()), + let cartVM = CartViewModel(posModel: posModel) + let itemsListVM = ItemListViewModel(posModel: posModel) + let posVM = PointOfSaleDashboardViewModel(posModel: posModel, totalsViewModel: totalsVM, cartViewModel: cartVM, itemListViewModel: itemsListVM, diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index e4d87c2de4a..552b6a280ce 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -23,10 +23,10 @@ struct PointOfSaleEntryPointView: View { self.onPointOfSaleModeActiveStateChange = onPointOfSaleModeActiveStateChange let posModel = PointOfSaleAggregateModel(itemProvider: itemProvider, - cardPresentPaymentService: cardPresentPaymentService) - let totalsViewModel = TotalsViewModel(orderService: orderService, + cardPresentPaymentService: cardPresentPaymentService, + orderService: orderService) + let totalsViewModel = TotalsViewModel(posModel: posModel, cardPresentPaymentService: cardPresentPaymentService, - currencyFormatter: currencyFormatter, paymentState: .acceptingCard) let cartViewModel = CartViewModel(posModel: posModel) let shouldShowGhostableItemCard = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.displayInfiniteScrollingUIDetailsInPointOfSale) diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index 874a0a8d26b..1db488bdc19 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -1,6 +1,7 @@ import SwiftUI struct TotalsView: View { + @EnvironmentObject private var posModel: PointOfSaleAggregateModel @ObservedObject private var viewModel: TotalsViewModel /// Used together with .matchedGeometryEffect to synchronize the animations of shimmeringLineView and text fields. @@ -21,7 +22,7 @@ struct TotalsView: View { var body: some View { HStack { - switch viewModel.orderState { + switch posModel.orderState { case .idle, .syncing, .loaded: VStack(alignment: .center) { Spacer() @@ -65,7 +66,7 @@ struct TotalsView: View { } .background(backgroundColor) .animation(.default, value: viewModel.paymentState) - .animation(.default, value: viewModel.orderState.isError) + .animation(.default, value: posModel.orderState.isError) .onDisappear { viewModel.onTotalsViewDisappearance() } @@ -89,39 +90,47 @@ private extension TotalsView { var totalsFieldsView: some View { HStack(alignment: .center) { Spacer() - VStack() { - subtotalFieldView(title: Localization.subtotal, - formattedPrice: viewModel.formattedCartTotalPrice, - shimmeringActive: viewModel.isShimmering, - redacted: viewModel.isSubtotalFieldRedacted, - matchedGeometryId: Constants.matchedGeometrySubtotalId) - Spacer().frame(height: Constants.subtotalsVerticalSpacing) - subtotalFieldView(title: Localization.taxes, - formattedPrice: viewModel.formattedOrderTotalTaxPrice, - shimmeringActive: viewModel.isShimmering, - redacted: viewModel.isTaxFieldRedacted, - matchedGeometryId: Constants.matchedGeometryTaxId) - Spacer().frame(height: Constants.totalVerticalSpacing) - Divider() - .overlay(Constants.separatorColor) - Spacer().frame(height: Constants.totalVerticalSpacing) - totalFieldView(formattedPrice: viewModel.formattedOrderTotalPrice, - shimmeringActive: viewModel.isShimmering, - redacted: viewModel.isTotalPriceFieldRedacted, - matchedGeometryId: Constants.matchedGeometryTotalId) + switch posModel.orderState { + case .idle, + .syncing, + .error: + totalsFields(orderTotals: nil) + case .loaded(let orderTotals): + totalsFields(orderTotals: orderTotals) } - .padding(Constants.totalsLineViewPadding) - .frame(minWidth: Constants.pricesIdealWidth) - .fixedSize(horizontal: true, vertical: false) Spacer() } } + @ViewBuilder func totalsFields(orderTotals: PointOfSaleOrderTotals?) -> some View { + let totalsLoading = orderTotals == nil + VStack { + subtotalFieldView(title: Localization.subtotal, + formattedPrice: orderTotals?.cartTotal, + shimmeringActive: totalsLoading, + matchedGeometryId: Constants.matchedGeometrySubtotalId) + Spacer().frame(height: Constants.subtotalsVerticalSpacing) + subtotalFieldView(title: Localization.taxes, + formattedPrice: orderTotals?.taxTotal, + shimmeringActive: totalsLoading, + matchedGeometryId: Constants.matchedGeometryTaxId) + Spacer().frame(height: Constants.totalVerticalSpacing) + Divider() + .overlay(Constants.separatorColor) + Spacer().frame(height: Constants.totalVerticalSpacing) + totalFieldView(formattedPrice: orderTotals?.orderTotal, + shimmeringActive: totalsLoading, + matchedGeometryId: Constants.matchedGeometryTotalId) + } + .padding(Constants.totalsLineViewPadding) + .frame(minWidth: Constants.pricesIdealWidth) + .fixedSize(horizontal: true, vertical: false) + } + @ViewBuilder func subtotalFieldView(title: String, formattedPrice: String?, shimmeringActive: Bool, - redacted: Bool, matchedGeometryId: String) -> some View { if shimmeringActive { shimmeringLineView(width: Constants.shimmeringWidth, height: Constants.subtotalsShimmeringHeight) @@ -133,7 +142,7 @@ private extension TotalsView { Spacer() Text(formattedPrice ?? "") .font(Constants.subtotalAmountFont) - .redacted(reason: redacted ? [.placeholder] : []) + .redacted(reason: shimmeringActive ? [.placeholder] : []) } .accessibilityElement(children: .combine) .foregroundColor(Color.posPrimaryText) @@ -144,7 +153,6 @@ private extension TotalsView { @ViewBuilder func totalFieldView(formattedPrice: String?, shimmeringActive: Bool, - redacted: Bool, matchedGeometryId: String) -> some View { if shimmeringActive { shimmeringLineView(width: Constants.shimmeringWidth, height: Constants.totalShimmeringHeight) @@ -157,7 +165,7 @@ private extension TotalsView { Spacer(minLength: Constants.totalsHorizontalSpacing) Text(formattedPrice ?? "") .font(Constants.totalAmountFont) - .redacted(reason: redacted ? [.placeholder] : []) + .redacted(reason: shimmeringActive ? [.placeholder] : []) } .accessibilityElement(children: .combine) .accessibilityAddTraits(.isHeader) @@ -387,10 +395,14 @@ private extension View { #if DEBUG #Preview { - let totalsVM = TotalsViewModel(orderService: POSOrderPreviewService(), - cardPresentPaymentService: CardPresentPaymentPreviewService(), - currencyFormatter: .init(currencySettings: .init()), - paymentState: .acceptingCard) - return TotalsView(viewModel: totalsVM) + let posModel = PointOfSaleAggregateModel( + itemProvider: POSItemProviderPreview(), + cardPresentPaymentService: CardPresentPaymentPreviewService(), + orderService: POSOrderPreviewService()) + let totalsVM = TotalsViewModel( + posModel: posModel, + cardPresentPaymentService: CardPresentPaymentPreviewService(), + paymentState: .acceptingCard) + TotalsView(viewModel: totalsVM) } #endif diff --git a/WooCommerce/Classes/POS/ViewModels/CartViewModel.swift b/WooCommerce/Classes/POS/ViewModels/CartViewModel.swift index f146af348e7..45911317897 100644 --- a/WooCommerce/Classes/POS/ViewModels/CartViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/CartViewModel.swift @@ -3,16 +3,10 @@ import Combine import protocol Yosemite.POSItem final class CartViewModel: CartViewModelProtocol { - /// Emits cart items when the CTA is tapped to submit the cart. - let cartSubmissionPublisher: AnyPublisher<[CartItem], Never> - private let cartSubmissionSubject: PassthroughSubject<[CartItem], Never> = .init() - let posModel: PointOfSaleAggregateModel init(posModel: PointOfSaleAggregateModel) { self.posModel = posModel - - cartSubmissionPublisher = cartSubmissionSubject.eraseToAnyPublisher() } var itemsInCartLabel: String? { @@ -24,8 +18,4 @@ final class CartViewModel: CartViewModelProtocol { singular: "%1$d item", plural: "%1$d items") } - - func submitCart() { - cartSubmissionSubject.send(posModel.cart) - } } diff --git a/WooCommerce/Classes/POS/ViewModels/CartViewModelProtocol.swift b/WooCommerce/Classes/POS/ViewModels/CartViewModelProtocol.swift index f502c3653c3..ee5922690a1 100644 --- a/WooCommerce/Classes/POS/ViewModels/CartViewModelProtocol.swift +++ b/WooCommerce/Classes/POS/ViewModels/CartViewModelProtocol.swift @@ -3,9 +3,5 @@ import Combine import protocol Yosemite.POSItem protocol CartViewModelProtocol: ObservableObject { - var cartSubmissionPublisher: AnyPublisher<[CartItem], Never> { get } - var itemsInCartLabel: String? { get } - - func submitCart() } diff --git a/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift b/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift index f42e59f150e..e941c418e9e 100644 --- a/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift @@ -11,7 +11,7 @@ final class PointOfSaleDashboardViewModel: ObservableObject { let totalsViewModel: any TotalsViewModelProtocol let itemListViewModel: any ItemListViewModelProtocol - let posModel: PointOfSaleAggregateModelProtocol + @ObservedObject var posModel: PointOfSaleAggregateModel private let connectivityObserver: ConnectivityObserver @@ -26,7 +26,7 @@ final class PointOfSaleDashboardViewModel: ObservableObject { private var cancellables: Set = [] - init(posModel: PointOfSaleAggregateModelProtocol, + init(posModel: PointOfSaleAggregateModel, totalsViewModel: any TotalsViewModelProtocol, cartViewModel: any CartViewModelProtocol, itemListViewModel: any ItemListViewModelProtocol, @@ -38,7 +38,6 @@ final class PointOfSaleDashboardViewModel: ObservableObject { self.connectivityObserver = connectivityObserver observeSelectedItemToAddToCart() - observeCartSubmission() observePaymentStateForButtonDisabledProperties() observeTotalsOrderActions() observeConnectivity() @@ -47,10 +46,6 @@ final class PointOfSaleDashboardViewModel: ObservableObject { private func startNewOrder() { posModel.startNewCart() } - - private func cartSubmitted(cartItems: [CartItem]) { - totalsViewModel.checkOutTapped(with: cartItems, allItems: posModel.allItems) - } } private extension PointOfSaleDashboardViewModel { @@ -62,17 +57,8 @@ private extension PointOfSaleDashboardViewModel { .store(in: &cancellables) } - func observeCartSubmission() { - cartViewModel.cartSubmissionPublisher - .sink { [weak self] cartItems in - guard let self else { return } - self.cartSubmitted(cartItems: cartItems) - } - .store(in: &cancellables) - } - func observePaymentStateForButtonDisabledProperties() { - Publishers.CombineLatest(totalsViewModel.paymentStatePublisher, totalsViewModel.orderStatePublisher) + Publishers.CombineLatest(totalsViewModel.paymentStatePublisher, posModel.$orderState) .map { paymentState, orderState in switch paymentState { case .processingPayment, diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift index 97be27ac991..1f5aa9a0d0c 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift @@ -1,13 +1,7 @@ import SwiftUI import Combine import protocol WooFoundation.Analytics -import protocol Yosemite.POSOrderServiceProtocol import protocol Yosemite.POSItem -import struct Yosemite.Order -import struct Yosemite.OrderItem -import struct Yosemite.POSCartItem -import class WooFoundation.CurrencyFormatter -import class WooFoundation.CurrencySettings final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { enum PaymentState { @@ -28,17 +22,11 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { @Published private(set) var isShowingCardReaderStatus: Bool = false @Published private(set) var isShowingTotalsFields: Bool = false - @Published private(set) var order: Order? = nil - private var totalsCalculator: OrderTotalsCalculator? = nil - @Published private(set) var orderState: OrderState = .idle - @Published private(set) var paymentState: PaymentState @Published private(set) var connectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected - @Published private(set) var formattedCartTotalPrice: String? - @Published private(set) var formattedOrderTotalPrice: String? - @Published private(set) var formattedOrderTotalTaxPrice: String? + @ObservedObject var posModel: PointOfSaleAggregateModel private let startNewOrderActionSubject = PassthroughSubject() var startNewOrderActionPublisher: AnyPublisher { @@ -51,70 +39,29 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { } var isShimmering: Bool { - orderState.isSyncing - } - - var isSubtotalFieldRedacted: Bool { - formattedCartTotalPrice == nil || orderState.isSyncing + posModel.orderState.isSyncing } - var isTaxFieldRedacted: Bool { - formattedOrderTotalTaxPrice == nil || orderState.isSyncing - } - - var isTotalPriceFieldRedacted: Bool { - formattedOrderTotalPrice == nil || orderState.isSyncing - } - - private let orderService: POSOrderServiceProtocol private let cardPresentPaymentService: CardPresentPaymentFacade - private let currencyFormatter: CurrencyFormatter private let analytics: Analytics - init(orderService: POSOrderServiceProtocol, + init(posModel: PointOfSaleAggregateModel, cardPresentPaymentService: CardPresentPaymentFacade, - currencyFormatter: CurrencyFormatter, paymentState: PaymentState, analytics: Analytics = ServiceLocator.analytics) { - self.orderService = orderService + self.posModel = posModel self.cardPresentPaymentService = cardPresentPaymentService - self.currencyFormatter = currencyFormatter self.paymentState = paymentState self.analytics = analytics - self.formattedCartTotalPrice = nil - self.formattedOrderTotalPrice = nil - self.formattedOrderTotalTaxPrice = nil // Initialize all properties before calling methods self.observeConnectedReaderForStatus() self.observeCardPresentPaymentEvents() } - var orderStatePublisher: Published.Publisher { $orderState } var paymentStatePublisher: Published.Publisher { $paymentState } private var cardPresentPaymentAlertViewModelPublisher: Published.Publisher { $cardPresentPaymentAlertViewModel } private var connectionStatusPublisher: Published.Publisher { $connectionStatus } - private var formattedCartTotalPricePublisher: Published.Publisher { $formattedCartTotalPrice } - private var formattedOrderTotalPricePublisher: Published.Publisher { $formattedOrderTotalPrice } - private var formattedOrderTotalTaxPricePublisher: Published.Publisher { $formattedOrderTotalTaxPrice } - - private var startPaymentOnReaderConnection: AnyCancellable? - private var cardReaderDisconnection: AnyCancellable? - - func checkOutTapped(with cartItems: [CartItem], allItems: [POSItem]) { - Task { @MainActor in - await startSyncingOrder(with: cartItems, allItems: allItems) - } - } - - private func startSyncingOrder(with cartItems: [CartItem], allItems: [POSItem]) async { - guard CartItem.areOrderAndCartDifferent(order: order, cartItems: cartItems) else { - await startPaymentWhenReaderConnected() - return - } - // calculate totals and sync order if there was a change in the cart - await syncOrder(for: cartItems, allItems: allItems) - } func connectReaderTapped() { Task { @MainActor in @@ -128,7 +75,6 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { func startNewOrder() { paymentState = .acceptingCard - clearOrder() cardPresentPaymentInlineMessage = nil startNewOrderActionSubject.send(()) } @@ -162,127 +108,28 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { // This is a backup – it's not called until transitions are complete when using the back button. // The delay can lead to race conditions with tapping a card. // It's likely that the payment will already have been cancelled due to the change of orderStage. - cancelReaderPreparation() + posModel.cancelCardReaderPreparation() } func startShowingTotalsView() { - observeReaderReconnection() + posModel.observeReaderReconnection() } func stopShowingTotalsView() { - cancelReaderPreparation() - } - - private func cancelReaderPreparation() { - cardPresentPaymentService.cancelPayment() - startPaymentOnReaderConnection?.cancel() - cardReaderDisconnection?.cancel() - } -} - -// MARK: - Order syncing - -extension TotalsViewModel { - @MainActor - func syncOrder(for cartProducts: [CartItem], allItems: [POSItem]) async { - guard orderState.isSyncing == false else { - return - } - orderState = .syncing - let cart = cartProducts.map { - POSCartItem(itemID: nil, product: $0.item, quantity: Decimal($0.quantity)) - } - - do { - let syncedOrder = try await orderService.syncOrder(cart: cart, order: order, allProducts: allItems) - self.updateOrder(syncedOrder) - orderState = .loaded - await startPaymentWhenReaderConnected() - DDLogInfo("🟢 [POS] Synced order: \(syncedOrder)") - } catch { - DDLogError("🔴 [POS] Error syncing order: \(error)") - - // Consider removing error or handle specific errors with our own formatting and localization - orderState = .error(.init(message: error.localizedDescription, handler: { [weak self] in - Task { - await self?.syncOrder(for: cartProducts, allItems: allItems) - } - })) - } - } - - private func updateOrder(_ updatedOrder: Order) { - self.order = updatedOrder - totalsCalculator = OrderTotalsCalculator(for: updatedOrder, using: currencyFormatter) - updateFormattedPrices() - } -} - -// MARK: - Price formatters - -private extension TotalsViewModel { - func updateFormattedPrices() { - formattedCartTotalPrice = computedFormattedCartTotalPrice - formattedOrderTotalPrice = computedFormattedOrderTotalPrice - formattedOrderTotalTaxPrice = computedFormattedOrderTotalTaxPrice - } - - func formattedPrice(_ price: String?, currency: String?) -> String? { - guard let price, let currency else { - return nil - } - return currencyFormatter.formatAmount(price, with: currency) - } - - var computedFormattedCartTotalPrice: String? { - formattedPrice(totalsCalculator?.itemsTotal.stringValue, currency: order?.currency) - } - - var computedFormattedOrderTotalPrice: String? { - formattedPrice(order?.total, currency: order?.currency) - } - - var computedFormattedOrderTotalTaxPrice: String? { - formattedPrice(order?.totalTax, currency: order?.currency) - } -} - -extension TotalsViewModel { - func clearOrder() { - order = nil + posModel.cancelCardReaderPreparation() } } // MARK: - Payment collection -private extension TotalsViewModel { - @MainActor - func collectPayment() async { - guard let order else { - return - } - do { - try await collectPayment(for: order) - } catch { - DDLogError("Error taking payment: \(error)") - } - } - - @MainActor - func collectPayment(for order: Order) async throws { - _ = try await cardPresentPaymentService.collectPayment(for: order, using: .bluetooth) - } -} - private extension TotalsViewModel { func observeConnectedReaderForStatus() { cardPresentPaymentService.readerConnectionStatusPublisher .assign(to: &$connectionStatus) - Publishers.CombineLatest4($connectionStatus, $orderState, $cardPresentPaymentInlineMessage, $order) - .map { connectionStatus, orderState, message, order in - guard order != nil, - orderState.isLoaded + Publishers.CombineLatest3(posModel.$cardReaderConnectionStatus, posModel.$orderState, $cardPresentPaymentInlineMessage) + .map { connectionStatus, orderState, message in + guard orderState.isLoaded else { // When the order's being created or synced, we only show the shimmering totals. // Before the order exists, we don’t want to show the card payment status, as it will @@ -301,40 +148,6 @@ private extension TotalsViewModel { .assign(to: &$isShowingCardReaderStatus) } - func observeReaderReconnection() { - cardReaderDisconnection = $connectionStatus - .filter({ $0 == .disconnected }) - .sink { [weak self] _ in - Task { @MainActor [weak self] in - await self?.startPaymentWhenReaderConnected() - } - } - } - - /// Starts a payment immediately if a reader is connected. - /// Otherwise, schedules a payment to start the next time a reader connects. - /// Note that any schedlued payments are cancelled by `cancelReaderPreparation` when the TotalsView goes offscreen. - func startPaymentWhenReaderConnected() async { - guard case .connected = connectionStatus else { - return startPaymentOnReaderConnection = $connectionStatus - .filter { status in - switch status { - case .connected: - return true - case .disconnected, .disconnecting, .cancellingConnection: - return false - } - } - .removeDuplicates() - .sink { _ in - Task { @MainActor [weak self] in - await self?.collectPayment() - } - } - } - await collectPayment() - } - func observeCardPresentPaymentEvents() { cardPresentPaymentService.paymentEventPublisher .map { [weak self] event -> CardPresentPaymentsOnboardingViewModel? in @@ -412,32 +225,30 @@ private extension TotalsViewModel { var presentationStyleDeterminerDependencies: PointOfSaleCardPresentPaymentEventPresentationStyle.Dependencies { let cancelThenCollectPaymentWithWeakSelf: () -> Void = { [weak self] in - self?.cancelThenCollectPayment() + self?.posModel.cancelThenCollectPayment() + } + + var orderTotal: String? + if case .loaded(let totals) = posModel.orderState { + orderTotal = totals.orderTotal } return PointOfSaleCardPresentPaymentEventPresentationStyle.Dependencies( tryPaymentAgainBackToCheckoutAction: cancelThenCollectPaymentWithWeakSelf, nonRetryableErrorExitAction: cancelThenCollectPaymentWithWeakSelf, - formattedOrderTotalPrice: formattedOrderTotalPrice, + formattedOrderTotalPrice: orderTotal, paymentCaptureErrorTryAgainAction: cancelThenCollectPaymentWithWeakSelf, paymentCaptureErrorNewOrderAction: { [weak self] in - self?.startNewOrder() + self?.posModel.startNewCart() }, paymentIntentCreationErrorEditOrderAction: { [weak self] in - self?.editOrder() + self?.posModel.addMoreToCart() }, dismissReaderConnectionModal: { [weak self] in self?.cardPresentPaymentAlertViewModel = nil } ) } - - func cancelThenCollectPayment() { - cardPresentPaymentService.cancelPayment() - Task { [weak self] in - await self?.collectPayment() - } - } } private extension TotalsViewModel.PaymentState { @@ -474,53 +285,3 @@ private extension TotalsViewModel.PaymentState { } } } - -// MARK: - Order State - -extension TotalsViewModel { - enum OrderState: Equatable { - case idle - case syncing - case loaded - case error(PointOfSaleOrderSyncErrorMessageViewModel) - - static func == (lhs: OrderState, rhs: OrderState) -> Bool { - switch (lhs, rhs) { - case (.idle, .idle), - (.syncing, .syncing), - (.loaded, .loaded), - (.error, .error): - return true - default: - return false - } - } - - var isSyncing: Bool { - switch self { - case .syncing: - return true - default: - return false - } - } - - var isLoaded: Bool { - switch self { - case .loaded: - return true - default: - return false - } - } - - var isError: Bool { - switch self { - case .error: - return true - default: - return false - } - } - } -} diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift index 5d2aaeeec92..2ec5e77ec88 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift @@ -6,16 +6,13 @@ protocol TotalsViewModelProtocol { var paymentState: TotalsViewModel.PaymentState { get } var connectionStatus: CardPresentPaymentReaderConnectionStatus { get } - var orderStatePublisher: Published.Publisher { get } var paymentStatePublisher: Published.Publisher { get } var startNewOrderActionPublisher: AnyPublisher { get } var editOrderActionPublisher: AnyPublisher { get } var cardPresentPaymentInlineMessage: PointOfSaleCardPresentPaymentMessageType? { get } - var order: Order? { get } func startNewOrder() - func checkOutTapped(with cartItems: [CartItem], allItems: [POSItem]) func startShowingTotalsView() func stopShowingTotalsView() diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 81fac79b75e..166c90af612 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -776,6 +776,8 @@ 203163BD2C1C9602001C96DA /* PointOfSaleCardPresentPaymentAlertType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203163BC2C1C9602001C96DA /* PointOfSaleCardPresentPaymentAlertType.swift */; }; 203A5C312AC5ADD700BF29A1 /* WooPaymentsDepositsOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203A5C302AC5ADD700BF29A1 /* WooPaymentsDepositsOverviewView.swift */; }; 2044158D2CE4DB480070BF54 /* PointOfSaleOrderStage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2044158C2CE4DB480070BF54 /* PointOfSaleOrderStage.swift */; }; + 2044158F2CE6181E0070BF54 /* PointOfSaleOrderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2044158E2CE6181E0070BF54 /* PointOfSaleOrderState.swift */; }; + 204415912CE622BA0070BF54 /* PointOfSaleOrderTotals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204415902CE622BA0070BF54 /* PointOfSaleOrderTotals.swift */; }; 204C9C742B6BDFFB007A94E0 /* UIUserInterfaceSizeClass+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204C9C732B6BDFFB007A94E0 /* UIUserInterfaceSizeClass+Helpers.swift */; }; 204CB80E2C0F8A5E000C9773 /* MockViewControllerPresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204CB80D2C0F8A5E000C9773 /* MockViewControllerPresenting.swift */; }; 204CB8102C10BB88000C9773 /* CardPresentPaymentPreviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204CB80F2C10BB88000C9773 /* CardPresentPaymentPreviewService.swift */; }; @@ -3890,6 +3892,8 @@ 203163BC2C1C9602001C96DA /* PointOfSaleCardPresentPaymentAlertType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentAlertType.swift; sourceTree = ""; }; 203A5C302AC5ADD700BF29A1 /* WooPaymentsDepositsOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsOverviewView.swift; sourceTree = ""; }; 2044158C2CE4DB480070BF54 /* PointOfSaleOrderStage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderStage.swift; sourceTree = ""; }; + 2044158E2CE6181E0070BF54 /* PointOfSaleOrderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderState.swift; sourceTree = ""; }; + 204415902CE622BA0070BF54 /* PointOfSaleOrderTotals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderTotals.swift; sourceTree = ""; }; 204C9C732B6BDFFB007A94E0 /* UIUserInterfaceSizeClass+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIUserInterfaceSizeClass+Helpers.swift"; sourceTree = ""; }; 204CB80D2C0F8A5E000C9773 /* MockViewControllerPresenting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewControllerPresenting.swift; sourceTree = ""; }; 204CB80F2C10BB88000C9773 /* CardPresentPaymentPreviewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentPreviewService.swift; sourceTree = ""; }; @@ -9462,6 +9466,8 @@ DAD988C52C4A9CF9009DE9E3 /* CartItem+Order.swift */, 20FCBCDC2CE223340082DCA3 /* PointOfSaleAggregateModel.swift */, 2044158C2CE4DB480070BF54 /* PointOfSaleOrderStage.swift */, + 2044158E2CE6181E0070BF54 /* PointOfSaleOrderState.swift */, + 204415902CE622BA0070BF54 /* PointOfSaleOrderTotals.swift */, ); path = Models; sourceTree = ""; @@ -15459,6 +15465,7 @@ 7E7C5F792719A8F900315B61 /* EditProductCategoryListViewModel.swift in Sources */, DE69C55527C5E317000BB888 /* ShippingLabelSampleData.swift in Sources */, DE525499268C8B32007A5829 /* UIRefreshControl+Woo.swift in Sources */, + 204415912CE622BA0070BF54 /* PointOfSaleOrderTotals.swift in Sources */, EEC259442B43EF3B004D703C /* BlazeEditAdViewModel.swift in Sources */, B541B2172189EED4008FE7C1 /* NSMutableAttributedString+Helpers.swift in Sources */, 26F94E2E267A96A000DB6CCF /* ProductAddOnViewModel.swift in Sources */, @@ -16200,6 +16207,7 @@ 202C6C562C7F667700413107 /* POSTextButtonStyle.swift in Sources */, EE289AE92C9D7CEF004AB1A6 /* ImageTextScanner.swift in Sources */, 027EB56C29C05F4B003CE551 /* StoreOnboardingLaunchStoreView.swift in Sources */, + 2044158F2CE6181E0070BF54 /* PointOfSaleOrderState.swift in Sources */, DEC75CC62BC4ED2100763801 /* DashboardCard+UI.swift in Sources */, 09EA565527C8ACEE00407D40 /* BulkUpdateViewController.swift in Sources */, 02BBD6E929A3024400243BE2 /* StoreOnboardingTaskView.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift index fe3a0a4e3d2..6f6df842c24 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift @@ -19,9 +19,7 @@ final class TotalsViewModelTests: XCTestCase { super.setUp() cardPresentPaymentService = MockCardPresentPaymentService() orderService = MockPOSOrderService() - sut = TotalsViewModel(orderService: orderService, - cardPresentPaymentService: cardPresentPaymentService, - currencyFormatter: .init(currencySettings: .init()), + sut = TotalsViewModel(cardPresentPaymentService: cardPresentPaymentService, paymentState: .acceptingCard) cancellables = Set() } From dbaf01018bbcc5d9cb4ff246a06f7524d0d2e212 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 14 Nov 2024 15:34:54 +0000 Subject: [PATCH 25/49] 14417 use startNewCart on posModel directly --- .../ViewModels/PointOfSaleDashboardViewModel.swift | 11 ----------- .../Classes/POS/ViewModels/TotalsViewModel.swift | 7 +------ .../POS/ViewModels/TotalsViewModelProtocol.swift | 1 - 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift b/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift index e941c418e9e..0af99c85bd6 100644 --- a/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift @@ -42,10 +42,6 @@ final class PointOfSaleDashboardViewModel: ObservableObject { observeTotalsOrderActions() observeConnectivity() } - - private func startNewOrder() { - posModel.startNewCart() - } } private extension PointOfSaleDashboardViewModel { @@ -116,13 +112,6 @@ private extension PointOfSaleDashboardViewModel { } func observeTotalsOrderActions() { - totalsViewModel.startNewOrderActionPublisher - .sink { [weak self] in - guard let self else { return } - self.startNewOrder() - } - .store(in: &cancellables) - totalsViewModel.editOrderActionPublisher .sink { [weak self] in guard let self else { return } diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift index 1f5aa9a0d0c..f698c23dd97 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift @@ -28,11 +28,6 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { @ObservedObject var posModel: PointOfSaleAggregateModel - private let startNewOrderActionSubject = PassthroughSubject() - var startNewOrderActionPublisher: AnyPublisher { - startNewOrderActionSubject.eraseToAnyPublisher() - } - private let editOrderActionSubject = PassthroughSubject() var editOrderActionPublisher: AnyPublisher { editOrderActionSubject.eraseToAnyPublisher() @@ -76,7 +71,7 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { func startNewOrder() { paymentState = .acceptingCard cardPresentPaymentInlineMessage = nil - startNewOrderActionSubject.send(()) + posModel.startNewCart() } /// Called when the onboarding UI is dismissed. diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift index 2ec5e77ec88..040e50ce7f2 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift @@ -7,7 +7,6 @@ protocol TotalsViewModelProtocol { var connectionStatus: CardPresentPaymentReaderConnectionStatus { get } var paymentStatePublisher: Published.Publisher { get } - var startNewOrderActionPublisher: AnyPublisher { get } var editOrderActionPublisher: AnyPublisher { get } var cardPresentPaymentInlineMessage: PointOfSaleCardPresentPaymentMessageType? { get } From 542a5cb0e4c87edf916163df9814d9e1c6722bb1 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 14 Nov 2024 15:35:07 +0000 Subject: [PATCH 26/49] 14417 fix existing aggregate model tests --- .../PointOfSaleAggregateModelTests.swift | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index ab0d3c9eaa7..89bb561748b 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -10,7 +10,8 @@ struct PointOfSaleAggregateModelTests { init() { self.sut = PointOfSaleAggregateModel(itemProvider: MockPOSItemProvider(), - cardPresentPaymentService: MockCardPresentPaymentService()) + cardPresentPaymentService: MockCardPresentPaymentService(), + orderService: MockPOSOrderService()) } @Test func inits_with_building_order_stage() async throws { @@ -20,7 +21,7 @@ struct PointOfSaleAggregateModelTests { @Test func startNewCart_removes_all_items_from_cart_and_moves_back_to_building() async throws { // Given sut.addToCart(makeItem()) - sut.submitCart() + await sut.submitCart() try #require(sut.orderStage == .finalizing) try #require(sut.cart.isNotEmpty) @@ -37,7 +38,7 @@ struct PointOfSaleAggregateModelTests { sut.addToCart(makeItem()) // When - sut.submitCart() + await sut.submitCart() // Then #expect(sut.orderStage == .finalizing) @@ -46,7 +47,7 @@ struct PointOfSaleAggregateModelTests { @Test func addMoreToCart_moves_to_building_order_stage() async throws { // Given sut.addToCart(makeItem()) - sut.submitCart() + await sut.submitCart() try #require(sut.orderStage == .finalizing) // When @@ -59,13 +60,14 @@ struct PointOfSaleAggregateModelTests { } struct ItemListTests { - private var itemProvider: MockPOSItemProvider + private let itemProvider: MockPOSItemProvider private let sut: PointOfSaleAggregateModel init() { itemProvider = MockPOSItemProvider() sut = PointOfSaleAggregateModel(itemProvider: itemProvider, - cardPresentPaymentService: MockCardPresentPaymentService()) + cardPresentPaymentService: MockCardPresentPaymentService(), + orderService: MockPOSOrderService()) } @Test func loadInitialItems_requests_first_page() async throws { @@ -153,7 +155,8 @@ struct PointOfSaleAggregateModelTests { let itemProvider = MockPOSItemProvider() itemProvider.shouldReturnZeroItems = true let sut = PointOfSaleAggregateModel(itemProvider: itemProvider, - cardPresentPaymentService: MockCardPresentPaymentService()) + cardPresentPaymentService: MockCardPresentPaymentService(), + orderService: MockPOSOrderService()) try #require(sut.itemListState == .initialLoading) @@ -211,7 +214,8 @@ struct PointOfSaleAggregateModelTests { let itemProvider = MockPOSItemProvider() itemProvider.shouldReturnZeroItems = true let sut = PointOfSaleAggregateModel(itemProvider: itemProvider, - cardPresentPaymentService: MockCardPresentPaymentService()) + cardPresentPaymentService: MockCardPresentPaymentService(), + orderService: MockPOSOrderService()) try #require(sut.itemListState == .initialLoading) @@ -309,14 +313,15 @@ struct PointOfSaleAggregateModelTests { struct CartTests { let sut: PointOfSaleAggregateModel - private var analytics: WooAnalytics! - private var analyticsProvider: MockAnalyticsProvider! + private let analytics: WooAnalytics! + private let analyticsProvider: MockAnalyticsProvider! init() { analyticsProvider = MockAnalyticsProvider() analytics = WooAnalytics(analyticsProvider: analyticsProvider) sut = PointOfSaleAggregateModel(itemProvider: MockPOSItemProvider(), cardPresentPaymentService: MockCardPresentPaymentService(), + orderService: MockPOSOrderService(), analytics: analytics) } @@ -395,6 +400,34 @@ struct PointOfSaleAggregateModelTests { #expect(event == "pos_item_added_to_cart") } } + + struct OrderTests { + private let itemProvider: MockPOSItemProvider + private let orderService: MockPOSOrderService + private let sut: PointOfSaleAggregateModel + + init() { + itemProvider = MockPOSItemProvider() + orderService = MockPOSOrderService() + sut = PointOfSaleAggregateModel(itemProvider: itemProvider, + cardPresentPaymentService: MockCardPresentPaymentService(), + orderService: orderService) + } + + @Test func startNewCart_sets_orderState_to_idle() async throws { + // Given + try #require(sut.orderState == .loaded(.init( + cartTotal: "", + orderTotal: "", + taxTotal: ""))) + + // When + sut.startNewCart() + + // Then + #expect(sut.orderState == .idle) + } + } } private func makeItem(name: String = "") -> POSItem { From 96960850cf043ea730963075637a367a6e216c78 Mon Sep 17 00:00:00 2001 From: Hafiz Rahman Date: Fri, 15 Nov 2024 11:07:16 +0700 Subject: [PATCH 27/49] Exclude flaky unit tests related to dashboard cards's custom range date. See report on https://github.com/woocommerce/woocommerce-ios/pull/14287/files#r1843059760 --- WooCommerce/WooCommerceTests/UnitTests.xctestplan | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WooCommerce/WooCommerceTests/UnitTests.xctestplan b/WooCommerce/WooCommerceTests/UnitTests.xctestplan index 6478f0088d8..6ca137aa850 100644 --- a/WooCommerce/WooCommerceTests/UnitTests.xctestplan +++ b/WooCommerce/WooCommerceTests/UnitTests.xctestplan @@ -33,7 +33,9 @@ "CardReaderConnectionControllerTests", "InAppPurchaseStoreTests\/test_user_is_entitled_to_product_returns_false_when_not_entitled()", "InAppPurchaseStoreTests\/test_user_is_entitled_to_product_returns_true_when_entitled()", - "StripeCardReaderIntegrationTests" + "StorePerformanceViewModelTests\/test_dates_for_custom_range_are_correct_for_non_custom_time_range()", + "StripeCardReaderIntegrationTests", + "TopPerformersDashboardViewModelTests\/test_dates_for_custom_range_are_correct_for_non_custom_time_range()" ], "target" : { "containerPath" : "container:WooCommerce.xcodeproj", From 4d64d0225f074600cb2d0ec3171c8b544e5b6b3b Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Thu, 14 Nov 2024 22:45:55 -0800 Subject: [PATCH 28/49] =?UTF-8?q?Update=20app=20translations=20=E2=80=93?= =?UTF-8?q?=20`Localizable.strings`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WooCommerce/Resources/de.lproj/Localizable.strings | 14 +++++++++++++- WooCommerce/Resources/fr.lproj/Localizable.strings | 14 +++++++++++++- WooCommerce/Resources/he.lproj/Localizable.strings | 14 +++++++++++++- WooCommerce/Resources/ja.lproj/Localizable.strings | 14 +++++++++++++- .../Resources/zh-Hans.lproj/Localizable.strings | 14 +++++++++++++- .../Resources/zh-Hant.lproj/Localizable.strings | 14 +++++++++++++- 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Resources/de.lproj/Localizable.strings b/WooCommerce/Resources/de.lproj/Localizable.strings index f5d99964ffb..f58ad66bb5f 100644 --- a/WooCommerce/Resources/de.lproj/Localizable.strings +++ b/WooCommerce/Resources/de.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-11-06 09:54:03+0000 */ +/* Translation-Revision-Date: 2024-11-14 09:54:03+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/2.4.0-alpha */ /* Language: de */ @@ -3113,6 +3113,9 @@ which should be translated separately and considered part of this sentence. */ /* Hint regarding available/pending balances shown in the WooPayments Deposits View%1$d will be replaced by the number of days balances pend, and will be one of 2/4/5/7. */ "Funds become available after pending for %1$d days." = "Guthaben, das seit %1$d Tagen aussteht, wird zur Verfügung gestellt."; +/* Format of the Global Unique Identifier on the Inventory Settings row */ +"GTIN, UPC, EAN, ISBN: %@" = "GTIN, UPC, EAN oder ISBN: %@"; + /* Country option for a site address. */ "Gabon" = "Gabun"; @@ -3657,6 +3660,9 @@ which should be translated separately and considered part of this sentence. */ /* Error message for invalid format of ITN in Customs screen of Shipping Label flow */ "Invalid ITN format" = "Ungültiges ITN-Format"; +/* Error when an empty Identifier is returned from the barcode scanner */ +"Invalid Identifier" = "Ungültige ID"; + /* The title of the alert when there is an error with the package name */ "Invalid Package Name" = "Ungültiger Paketname"; @@ -5559,6 +5565,12 @@ which should be translated separately and considered part of this sentence. */ /* Title of the Product Type row on Product main screen */ "Product type" = "Produkttyp"; +/* Error message when the scanner found a product but isn't purchasable.%@ is the Identifier code. */ +"Product with Identifier \"%@\" is not purchasable." = "Das Produkt mit der ID „%@“ ist nicht käuflich."; + +/* Error message when the scanner cannot find a matching product.%@ is the Identifier barcode. */ +"Product with Identifier \"%@\" not found." = "Das Produkt mit der ID „%@“ wurde nicht gefunden."; + /* The instruction text below the scan area in the barcode scanner for product barcode. */ "ProductBarcodeInputScanner.instructionText" = "Produkt-Barcode oder QR-Code scannen"; diff --git a/WooCommerce/Resources/fr.lproj/Localizable.strings b/WooCommerce/Resources/fr.lproj/Localizable.strings index 3c4b4f1318a..1db7705da05 100644 --- a/WooCommerce/Resources/fr.lproj/Localizable.strings +++ b/WooCommerce/Resources/fr.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-11-05 15:54:04+0000 */ +/* Translation-Revision-Date: 2024-11-13 15:54:04+0000 */ /* Plural-Forms: nplurals=2; plural=n > 1; */ /* Generator: GlotPress/2.4.0-alpha */ /* Language: fr */ @@ -3113,6 +3113,9 @@ which should be translated separately and considered part of this sentence. */ /* Hint regarding available/pending balances shown in the WooPayments Deposits View%1$d will be replaced by the number of days balances pend, and will be one of 2/4/5/7. */ "Funds become available after pending for %1$d days." = "Les fonds sont disponibles après avoir passé %1$d jours en attente."; +/* Format of the Global Unique Identifier on the Inventory Settings row */ +"GTIN, UPC, EAN, ISBN: %@" = "GTIN, UPC, EAN, ISBN : %@"; + /* Country option for a site address. */ "Gabon" = "Gabon"; @@ -3657,6 +3660,9 @@ which should be translated separately and considered part of this sentence. */ /* Error message for invalid format of ITN in Customs screen of Shipping Label flow */ "Invalid ITN format" = "Format de NTI non valide"; +/* Error when an empty Identifier is returned from the barcode scanner */ +"Invalid Identifier" = "Identifiant non valide"; + /* The title of the alert when there is an error with the package name */ "Invalid Package Name" = "Nom de colis non valide"; @@ -5559,6 +5565,12 @@ which should be translated separately and considered part of this sentence. */ /* Title of the Product Type row on Product main screen */ "Product type" = "Type de produit"; +/* Error message when the scanner found a product but isn't purchasable.%@ is the Identifier code. */ +"Product with Identifier \"%@\" is not purchasable." = "Le produit avec l’identifiant « %@ » ne peut pas être acheté."; + +/* Error message when the scanner cannot find a matching product.%@ is the Identifier barcode. */ +"Product with Identifier \"%@\" not found." = "Le produit avec l’identifiant « %@ » n’a pas été trouvé."; + /* The instruction text below the scan area in the barcode scanner for product barcode. */ "ProductBarcodeInputScanner.instructionText" = "Scanner le code-barres ou le code QR du produit"; diff --git a/WooCommerce/Resources/he.lproj/Localizable.strings b/WooCommerce/Resources/he.lproj/Localizable.strings index 258593c6072..07e76ab3970 100644 --- a/WooCommerce/Resources/he.lproj/Localizable.strings +++ b/WooCommerce/Resources/he.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-11-06 15:54:04+0000 */ +/* Translation-Revision-Date: 2024-11-14 09:54:04+0000 */ /* Plural-Forms: nplurals=2; plural=n != 1; */ /* Generator: GlotPress/2.4.0-alpha */ /* Language: he_IL */ @@ -3113,6 +3113,9 @@ which should be translated separately and considered part of this sentence. */ /* Hint regarding available/pending balances shown in the WooPayments Deposits View%1$d will be replaced by the number of days balances pend, and will be one of 2/4/5/7. */ "Funds become available after pending for %1$d days." = "כספים נעשים זמינים לאחר תקופת המתנה בת ⁦%1$d⁩ ימים."; +/* Format of the Global Unique Identifier on the Inventory Settings row */ +"GTIN, UPC, EAN, ISBN: %@" = "GTIN, ‏UPC, ‏EAN, ‏ISBN‏: %@"; + /* Country option for a site address. */ "Gabon" = "גאבון"; @@ -3657,6 +3660,9 @@ which should be translated separately and considered part of this sentence. */ /* Error message for invalid format of ITN in Customs screen of Shipping Label flow */ "Invalid ITN format" = "פורמט לא חוקי ל-ITN"; +/* Error when an empty Identifier is returned from the barcode scanner */ +"Invalid Identifier" = "מזהה לא תקף"; + /* The title of the alert when there is an error with the package name */ "Invalid Package Name" = "שם חבילה לא תקף"; @@ -5559,6 +5565,12 @@ which should be translated separately and considered part of this sentence. */ /* Title of the Product Type row on Product main screen */ "Product type" = "סוג מוצר"; +/* Error message when the scanner found a product but isn't purchasable.%@ is the Identifier code. */ +"Product with Identifier \"%@\" is not purchasable." = "לא ניתן לרכוש מוצר עם המזהה \"%@\"."; + +/* Error message when the scanner cannot find a matching product.%@ is the Identifier barcode. */ +"Product with Identifier \"%@\" not found." = "לא ניתן למצוא מוצר עם המזהה \"%@\"."; + /* The instruction text below the scan area in the barcode scanner for product barcode. */ "ProductBarcodeInputScanner.instructionText" = "יש לסרוק את הברקוד או קוד ה-QR של המוצר"; diff --git a/WooCommerce/Resources/ja.lproj/Localizable.strings b/WooCommerce/Resources/ja.lproj/Localizable.strings index 8353d3f0aa4..b71c2de410c 100644 --- a/WooCommerce/Resources/ja.lproj/Localizable.strings +++ b/WooCommerce/Resources/ja.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-11-05 09:54:19+0000 */ +/* Translation-Revision-Date: 2024-11-12 09:54:04+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/2.4.0-alpha */ /* Language: ja_JP */ @@ -3113,6 +3113,9 @@ which should be translated separately and considered part of this sentence. */ /* Hint regarding available/pending balances shown in the WooPayments Deposits View%1$d will be replaced by the number of days balances pend, and will be one of 2/4/5/7. */ "Funds become available after pending for %1$d days." = "資金は%1$d日間保留された後に利用可能になります。"; +/* Format of the Global Unique Identifier on the Inventory Settings row */ +"GTIN, UPC, EAN, ISBN: %@" = "GTIN、UPC、EAN、ISBN: %@"; + /* Country option for a site address. */ "Gabon" = "ガボン"; @@ -3657,6 +3660,9 @@ which should be translated separately and considered part of this sentence. */ /* Error message for invalid format of ITN in Customs screen of Shipping Label flow */ "Invalid ITN format" = "無効な ITN フォーマットです"; +/* Error when an empty Identifier is returned from the barcode scanner */ +"Invalid Identifier" = "間違った ID"; + /* The title of the alert when there is an error with the package name */ "Invalid Package Name" = "無効な荷物名"; @@ -5559,6 +5565,12 @@ which should be translated separately and considered part of this sentence. */ /* Title of the Product Type row on Product main screen */ "Product type" = "商品タイプ"; +/* Error message when the scanner found a product but isn't purchasable.%@ is the Identifier code. */ +"Product with Identifier \"%@\" is not purchasable." = "識別子 \"%@\" の商品は購入できません。"; + +/* Error message when the scanner cannot find a matching product.%@ is the Identifier barcode. */ +"Product with Identifier \"%@\" not found." = "識別子 \"%@\" の商品が見つかりません。"; + /* The instruction text below the scan area in the barcode scanner for product barcode. */ "ProductBarcodeInputScanner.instructionText" = "商品のバーコードまたは QR コードをスキャン"; diff --git a/WooCommerce/Resources/zh-Hans.lproj/Localizable.strings b/WooCommerce/Resources/zh-Hans.lproj/Localizable.strings index e2edf018182..eaa08c2dffc 100644 --- a/WooCommerce/Resources/zh-Hans.lproj/Localizable.strings +++ b/WooCommerce/Resources/zh-Hans.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-11-05 09:54:19+0000 */ +/* Translation-Revision-Date: 2024-11-12 09:54:04+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/2.4.0-alpha */ /* Language: zh_CN */ @@ -3113,6 +3113,9 @@ which should be translated separately and considered part of this sentence. */ /* Hint regarding available/pending balances shown in the WooPayments Deposits View%1$d will be replaced by the number of days balances pend, and will be one of 2/4/5/7. */ "Funds become available after pending for %1$d days." = "资金将在等待 %1$d 天后到账。"; +/* Format of the Global Unique Identifier on the Inventory Settings row */ +"GTIN, UPC, EAN, ISBN: %@" = "GTIN、UPC、EAN、ISBN:%@"; + /* Country option for a site address. */ "Gabon" = "加蓬"; @@ -3657,6 +3660,9 @@ which should be translated separately and considered part of this sentence. */ /* Error message for invalid format of ITN in Customs screen of Shipping Label flow */ "Invalid ITN format" = "ITN 格式无效"; +/* Error when an empty Identifier is returned from the barcode scanner */ +"Invalid Identifier" = "无效标识符"; + /* The title of the alert when there is an error with the package name */ "Invalid Package Name" = "包裹名称无效"; @@ -5559,6 +5565,12 @@ which should be translated separately and considered part of this sentence. */ /* Title of the Product Type row on Product main screen */ "Product type" = "产品类型"; +/* Error message when the scanner found a product but isn't purchasable.%@ is the Identifier code. */ +"Product with Identifier \"%@\" is not purchasable." = "无法购买标识符为“%@”的产品。"; + +/* Error message when the scanner cannot find a matching product.%@ is the Identifier barcode. */ +"Product with Identifier \"%@\" not found." = "未找到标识符为“%@”的产品。"; + /* The instruction text below the scan area in the barcode scanner for product barcode. */ "ProductBarcodeInputScanner.instructionText" = "扫描产品条形码或二维码"; diff --git a/WooCommerce/Resources/zh-Hant.lproj/Localizable.strings b/WooCommerce/Resources/zh-Hant.lproj/Localizable.strings index 51ee0eff957..7fe31d2958d 100644 --- a/WooCommerce/Resources/zh-Hant.lproj/Localizable.strings +++ b/WooCommerce/Resources/zh-Hant.lproj/Localizable.strings @@ -1,4 +1,4 @@ -/* Translation-Revision-Date: 2024-11-06 11:54:04+0000 */ +/* Translation-Revision-Date: 2024-11-13 11:54:04+0000 */ /* Plural-Forms: nplurals=1; plural=0; */ /* Generator: GlotPress/2.4.0-alpha */ /* Language: zh_TW */ @@ -3113,6 +3113,9 @@ which should be translated separately and considered part of this sentence. */ /* Hint regarding available/pending balances shown in the WooPayments Deposits View%1$d will be replaced by the number of days balances pend, and will be one of 2/4/5/7. */ "Funds become available after pending for %1$d days." = "款項待確認 %1$d 天後,現在已可提供。"; +/* Format of the Global Unique Identifier on the Inventory Settings row */ +"GTIN, UPC, EAN, ISBN: %@" = "GTIN、UPC、EAN、ISBN:%@"; + /* Country option for a site address. */ "Gabon" = "加彭"; @@ -3657,6 +3660,9 @@ which should be translated separately and considered part of this sentence. */ /* Error message for invalid format of ITN in Customs screen of Shipping Label flow */ "Invalid ITN format" = "ITN 格式無效"; +/* Error when an empty Identifier is returned from the barcode scanner */ +"Invalid Identifier" = "識別碼無效"; + /* The title of the alert when there is an error with the package name */ "Invalid Package Name" = "無效的包裹名稱"; @@ -5559,6 +5565,12 @@ which should be translated separately and considered part of this sentence. */ /* Title of the Product Type row on Product main screen */ "Product type" = "產品類型"; +/* Error message when the scanner found a product but isn't purchasable.%@ is the Identifier code. */ +"Product with Identifier \"%@\" is not purchasable." = "無法購買識別碼為「%@」的商品。"; + +/* Error message when the scanner cannot find a matching product.%@ is the Identifier barcode. */ +"Product with Identifier \"%@\" not found." = "找不到識別碼為「%@」的商品。"; + /* The instruction text below the scan area in the barcode scanner for product barcode. */ "ProductBarcodeInputScanner.instructionText" = "掃描商品條碼或 QR 碼"; From a9eed2f8085d12da434e2d5720c9edc9ff5e2602 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Thu, 14 Nov 2024 22:45:57 -0800 Subject: [PATCH 29/49] Update metadata translations --- fastlane/metadata/de-DE/release_notes.txt | 1 + fastlane/metadata/es-ES/release_notes.txt | 1 + fastlane/metadata/fr-FR/release_notes.txt | 1 + fastlane/metadata/he/release_notes.txt | 1 + fastlane/metadata/id/release_notes.txt | 1 + fastlane/metadata/it/release_notes.txt | 1 + fastlane/metadata/ja/release_notes.txt | 1 + fastlane/metadata/ko/release_notes.txt | 1 + fastlane/metadata/nl-NL/release_notes.txt | 1 + fastlane/metadata/pt-BR/release_notes.txt | 2 +- fastlane/metadata/ru/release_notes.txt | 1 + fastlane/metadata/sv/release_notes.txt | 1 + fastlane/metadata/tr/release_notes.txt | 1 + fastlane/metadata/zh-Hans/release_notes.txt | 1 + fastlane/metadata/zh-Hant/release_notes.txt | 1 + 15 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/de-DE/release_notes.txt create mode 100644 fastlane/metadata/es-ES/release_notes.txt create mode 100644 fastlane/metadata/fr-FR/release_notes.txt create mode 100644 fastlane/metadata/he/release_notes.txt create mode 100644 fastlane/metadata/id/release_notes.txt create mode 100644 fastlane/metadata/it/release_notes.txt create mode 100644 fastlane/metadata/ja/release_notes.txt create mode 100644 fastlane/metadata/ko/release_notes.txt create mode 100644 fastlane/metadata/nl-NL/release_notes.txt create mode 100644 fastlane/metadata/ru/release_notes.txt create mode 100644 fastlane/metadata/sv/release_notes.txt create mode 100644 fastlane/metadata/tr/release_notes.txt create mode 100644 fastlane/metadata/zh-Hans/release_notes.txt create mode 100644 fastlane/metadata/zh-Hant/release_notes.txt diff --git a/fastlane/metadata/de-DE/release_notes.txt b/fastlane/metadata/de-DE/release_notes.txt new file mode 100644 index 00000000000..130bceaf5a1 --- /dev/null +++ b/fastlane/metadata/de-DE/release_notes.txt @@ -0,0 +1 @@ +Genieße ein reibungsloseres WooCommerce-Erlebnis! Wir haben die Watch-App optimiert, damit deine Bestelldetails einwandfrei angezeigt werden. Außerdem haben Fehlermeldungen bei Zahlungslesegeräten jetzt ein klareres Erscheinungsbild. Führe für ein besseres Erlebnis sowohl auf deiner Watch als auch auf deinem Smartphone ein Update durch. diff --git a/fastlane/metadata/es-ES/release_notes.txt b/fastlane/metadata/es-ES/release_notes.txt new file mode 100644 index 00000000000..ed88b766568 --- /dev/null +++ b/fastlane/metadata/es-ES/release_notes.txt @@ -0,0 +1 @@ +Sumérgete en una experiencia de WooCommerce más fluida. Hemos perfeccionado la aplicación Watch para que muestre los detalles de tu pedido a la perfección. Además, hemos añadido mensajes de error más claros para las conexiones de lectores de pagos. Actualízate para tener un pulso empresarial aún mejor en tu muñeca y en tu bolsillo. diff --git a/fastlane/metadata/fr-FR/release_notes.txt b/fastlane/metadata/fr-FR/release_notes.txt new file mode 100644 index 00000000000..704e6cdc44e --- /dev/null +++ b/fastlane/metadata/fr-FR/release_notes.txt @@ -0,0 +1 @@ +Plongez-vous dans une expérience WooCommerce plus fluide ! Nous avons peaufiné l’application Watch pour mettre parfaitement en vitrine les détails de vos commandes. Par ailleurs, nous avons ajouté des messages d’erreur plus clairs pour les connexions de lecteurs de paiement. Mettez à jour pour mieux prendre le pouls de votre activité à votre poignet et dans votre poche. diff --git a/fastlane/metadata/he/release_notes.txt b/fastlane/metadata/he/release_notes.txt new file mode 100644 index 00000000000..fd45aeb6edf --- /dev/null +++ b/fastlane/metadata/he/release_notes.txt @@ -0,0 +1 @@ +אפשר ליהנות מחוויית שימוש חלקה יותר עם WooCommerce כבר עכשיו! שיפרנו את אפליקציית Watch כדי להציג את פרטי ההזמנה בצורה מדויקת. בנוסף, הוספנו הודעות שגיאה ברורות יותר לחיבורים של קורא כרטיסים לתשלומים. יש לעדכן כדי ליהנות מאפשרויות עסקיות טובות יותר על צג השעון או על מסך הטלפון. diff --git a/fastlane/metadata/id/release_notes.txt b/fastlane/metadata/id/release_notes.txt new file mode 100644 index 00000000000..cba84705017 --- /dev/null +++ b/fastlane/metadata/id/release_notes.txt @@ -0,0 +1 @@ +Nikmati pengalaman menggunakan WooCommerce yang lebih baik! Kami sudah menyempurnakan aplikasi Watch agar menampilkan rincian pesanan dengan benar. Kami juga menambahkan pesan error untuk sambungan alat pembaca pembayaran. Perbarui untuk memantau perkembangan bisnis lebih saksama lewat smartwatch dan ponsel Anda. diff --git a/fastlane/metadata/it/release_notes.txt b/fastlane/metadata/it/release_notes.txt new file mode 100644 index 00000000000..f2cf686d536 --- /dev/null +++ b/fastlane/metadata/it/release_notes.txt @@ -0,0 +1 @@ +Goditi un'esperienza WooCommerce più fluida. Abbiamo perfezionato l'app Watch per mostrare i dettagli degli ordini in modo impeccabile. Inoltre, abbiamo aggiunto messaggi di errore più chiari per i collegamenti con il lettore dei pagamenti. Effettua l'aggiornamento per avere sempre al polso e in tasca la situazione aggiornata della tua attività. diff --git a/fastlane/metadata/ja/release_notes.txt b/fastlane/metadata/ja/release_notes.txt new file mode 100644 index 00000000000..41db60ae443 --- /dev/null +++ b/fastlane/metadata/ja/release_notes.txt @@ -0,0 +1 @@ +よりスムーズな WooCommerce エクスペリエンスをご確認ください ! Watch アプリに微調整を加え、注文詳細が問題なく表示されるようになりました。 さらに、支払いリーダーの接続に関するエラーメッセージをより明確にしました。 アップデートして、手首やポケットの中でビジネスにさらなる活力を加えましょう。 diff --git a/fastlane/metadata/ko/release_notes.txt b/fastlane/metadata/ko/release_notes.txt new file mode 100644 index 00000000000..41d8100172e --- /dev/null +++ b/fastlane/metadata/ko/release_notes.txt @@ -0,0 +1 @@ +더 원활한 우커머스 환경을 누리세요! 주문 상세 정보가 온전하게 보이도록 시계 앱이 수정되었습니다. 또한 결제 리더 연결 시 더 명확한 오류 메시지가 추가되었습니다. 업데이트하여 손목과 주머니에서도 자세한 비즈니스 정보를 확인해 보세요. diff --git a/fastlane/metadata/nl-NL/release_notes.txt b/fastlane/metadata/nl-NL/release_notes.txt new file mode 100644 index 00000000000..db16e1d83ed --- /dev/null +++ b/fastlane/metadata/nl-NL/release_notes.txt @@ -0,0 +1 @@ +Profiteer van een soepelere WooCommerce-ervaring! We hebben de Watch-app verbeterd zodat je bestelgegevens vlekkeloos worden weergeven. Bovendien hebben we duidelijkere foutmeldingen toegevoegd voor verbindingen met betalingslezers. Een nog beter inzicht in je bedrijf, direct om je pols en in je zak. diff --git a/fastlane/metadata/pt-BR/release_notes.txt b/fastlane/metadata/pt-BR/release_notes.txt index c893350e170..abaf84a9ace 100644 --- a/fastlane/metadata/pt-BR/release_notes.txt +++ b/fastlane/metadata/pt-BR/release_notes.txt @@ -1 +1 @@ -Vá mais a fundo em uma experiência eficiente no WooCommerce! Ajustamos o app para Apple Watch para mostrar os detalhes do pedido perfeitamente. E mais, adicionamos mensagens de erro mais claras para conexões do leitor de pagamento. Atualize para acompanhar os negócios diretamente do seu pulso e no seu bolso. +Tenha uma experiência mais fluida no WooCommerce! Ajustamos o app para Apple Watch para mostrar os detalhes do pedido perfeitamente. E mais, adicionamos mensagens de erro mais claras para conexões de leitores de pagamento. Atualize para acompanhar melhor os negócios diretamente do seu pulso e da palma da sua mão. diff --git a/fastlane/metadata/ru/release_notes.txt b/fastlane/metadata/ru/release_notes.txt new file mode 100644 index 00000000000..ddf438ce830 --- /dev/null +++ b/fastlane/metadata/ru/release_notes.txt @@ -0,0 +1 @@ +WooCommerce становится всё удобнее. Надеемся, вам понравится! Мы настроили приложение Watch так, чтобы все ваши данные отображались без помех. Кроме того, мы добавили более чёткие сообщения об ошибках при подключении платёжных терминалов. Установите обновления и носите удобный бизнес-центр на руке или в кармане. diff --git a/fastlane/metadata/sv/release_notes.txt b/fastlane/metadata/sv/release_notes.txt new file mode 100644 index 00000000000..592d8615eb3 --- /dev/null +++ b/fastlane/metadata/sv/release_notes.txt @@ -0,0 +1 @@ +Ta del av en smidigare WooCommerce-upplevelse. Vi har finjusterat Watch-appen för felfri visning av din beställningsinformation. Vi har dessutom lagt till tydligare felmeddelanden för betalningsläsaranslutningar. Uppdatera nu för att få bättre koll på din verksamhet, både i klocka och telefon. diff --git a/fastlane/metadata/tr/release_notes.txt b/fastlane/metadata/tr/release_notes.txt new file mode 100644 index 00000000000..f7f8d3fcf04 --- /dev/null +++ b/fastlane/metadata/tr/release_notes.txt @@ -0,0 +1 @@ +Daha kolay bir WooCommerce deneyimine dalın! Sipariş ayrıntılarınızı kusursuz bir şekilde görüntüleyebilmeniz için Watch uygulamasında ince ayar yaptık. Ayrıca ödeme okuyucu bağlantıları için daha net hata mesajları ekledik. İşlerin nabzınızı bileğinizde ve cebinizde daha iyi şekilde hissetmek için güncelleyin. diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt new file mode 100644 index 00000000000..8517c58879d --- /dev/null +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -0,0 +1 @@ +充分感受更顺畅的 WooCommerce 体验! 我们对 Watch 应用程序进行了微调,以便完美展示您的订单详情。 此外,我们还为付款读卡器连接添加了更清晰的错误消息。 敬请更新,以便时刻都能更深入地掌控您的业务。 diff --git a/fastlane/metadata/zh-Hant/release_notes.txt b/fastlane/metadata/zh-Hant/release_notes.txt new file mode 100644 index 00000000000..9dd8ed245b9 --- /dev/null +++ b/fastlane/metadata/zh-Hant/release_notes.txt @@ -0,0 +1 @@ +深入探索更順暢的 WooCommerce 體驗! 我們已微調 Watch 應用程式,讓你可以完美顯示訂單詳細資料。 此外,我們也為付款讀卡機連線新增了更清楚的錯誤訊息。 在智慧手錶或行動裝置更新,進一步掌握業務脈動。 From 90d1f3dd3961388f4fddfc874a0cd676f8dc5159 Mon Sep 17 00:00:00 2001 From: Automattic Release Bot Date: Thu, 14 Nov 2024 22:46:08 -0800 Subject: [PATCH 30/49] Bump version number --- config/Version.Public.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/Version.Public.xcconfig b/config/Version.Public.xcconfig index a78500ee7ff..76428bb7472 100644 --- a/config/Version.Public.xcconfig +++ b/config/Version.Public.xcconfig @@ -1,4 +1,4 @@ CURRENT_PROJECT_VERSION = $VERSION_LONG MARKETING_VERSION = $VERSION_SHORT -VERSION_LONG = 21.1.0.1 +VERSION_LONG = 21.1.0.2 VERSION_SHORT = 21.1 From 20271fedefa0660f075c426bd4add98c1088dbf0 Mon Sep 17 00:00:00 2001 From: Hafiz Rahman Date: Fri, 15 Nov 2024 14:44:18 +0700 Subject: [PATCH 31/49] Remove all icons and leave only a single 1024x1024 png. --- .../AppIcon.appiconset/Contents.json | 114 +----------------- .../AppIcon.appiconset/Icon-120.png | Bin 3977 -> 0 bytes .../AppIcon.appiconset/Icon-121.png | Bin 3977 -> 0 bytes .../AppIcon.appiconset/Icon-152.png | Bin 5086 -> 0 bytes .../AppIcon.appiconset/Icon-167.png | Bin 5454 -> 0 bytes .../AppIcon.appiconset/Icon-180.png | Bin 5941 -> 0 bytes .../AppIcon.appiconset/Icon-20.png | Bin 669 -> 0 bytes .../AppIcon.appiconset/Icon-29.png | Bin 965 -> 0 bytes .../AppIcon.appiconset/Icon-40.png | Bin 1263 -> 0 bytes .../AppIcon.appiconset/Icon-41.png | Bin 1263 -> 0 bytes .../AppIcon.appiconset/Icon-42.png | Bin 1263 -> 0 bytes .../AppIcon.appiconset/Icon-58.png | Bin 1841 -> 0 bytes .../AppIcon.appiconset/Icon-59.png | Bin 1841 -> 0 bytes .../AppIcon.appiconset/Icon-60.png | Bin 1859 -> 0 bytes .../AppIcon.appiconset/Icon-76.png | Bin 2528 -> 0 bytes .../AppIcon.appiconset/Icon-80.png | Bin 2651 -> 0 bytes .../AppIcon.appiconset/Icon-81.png | Bin 2651 -> 0 bytes .../AppIcon.appiconset/Icon-87.png | Bin 2896 -> 0 bytes 18 files changed, 6 insertions(+), 108 deletions(-) delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-120.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-121.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-152.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-167.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-180.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-41.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-42.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-58.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-59.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-80.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-81.png delete mode 100644 WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-87.png diff --git a/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index ffb73053fd0..82f5bf1024b 100644 --- a/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,116 +1,14 @@ { "images" : [ { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-40.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-60.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-58.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-87.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-80.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-120.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-121.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-180.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-42.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-59.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-41.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-81.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-152.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-167.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", "filename" : "Icon-1024.png", - "scale" : "1x" + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-120.png b/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-120.png deleted file mode 100644 index a12193c05ff976aa5b5ea160d81f40aea92aa853..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3977 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 diff --git a/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-121.png b/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-121.png deleted file mode 100644 index a12193c05ff976aa5b5ea160d81f40aea92aa853..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3977 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 diff --git a/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-152.png b/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-152.png deleted file mode 100644 index 5ee8cb43eb70c1f549186f7fd4aee90f1f132da9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5086 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 diff --git a/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-167.png b/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-167.png deleted file mode 100644 index 0c78494eb4452c1cf8462a859b729541d7baa974..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5454 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 s^j5q*k|I$R`Q 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 diff --git a/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png b/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png deleted file mode 100644 index e917c649dd977c01f9fe6d8bfd83d8ec38560d73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1263 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1cXUMK~z{r?Uwsf zlvNnV`zQLjnM%7jnU!58H80gvmVjc20m&kwqeF?7qB1};DJ;m zKoHPYByXTXVJ3-~2%!+m?tZ89Jnl=%zR~jR|LZ9TS*(|3Ajtx*TdYISd@Zu} zRHL@&66#CZP-ki#R}E#C(Nu9oc5FP=o7!+fteqWOgP;XzShq;$#6gk))Fa@{RFoZT z!G9H11?TbkTl;v|#_~7;u6-vBVV(xqtae<#dKZwiE<1cyE~h@9OO3I{I>1HvSs4 zptt893}V}2HXcKP?iBY)F+4Pa0{toM5o7eso$dkdx7+QA2si}Qe9a>)Bw4`lRSy8E zPCTBC#SfW3a+{*RcM$8}OGn1H$GKge_Y*#OItGPVW{g;D*e>RJ%~Zp8P7*xD-_#vt z{G4Dw!j@uq&Ui4k{P{TS^gA^A19tfnp!N|K(u9B=7rXK4O9{|MRB)RjZ(qac;uJLI zJu4p9F881?+l*z;#6YvF9PQ_CB3$gm>Q|G{)71}i&Cd)T6`0RsiCazFZWQLCm=fTh zcLxzH+(nZqdT$LNL_8DtwgwyC-;eI!`Vbv-gy;SK>s@@k@&Fe)WtpNt*94Clu`pM6 zK(n(PKC=^WrKO9GKn-XcwrhW!&V$jJ_+AflCbH+40c_S$QTuO z2+@3Vxh{ehi^--1h?6qW= zjLq2iK?c6^HuAYgS=_Qbhh3i%UW%R@{e1AMLkoExMU*O^e@MrLo19%Tk`(FRRB?@- z5nE3&X+X*tMP>du2Bfi7FDJ4YC@3QVj+-VsrkXPz`f(Nmx18*BWG{Xcn4Ie~I}yjf zzrY#w6VBi4VZhK-O(Fqyw%vkio`y3Zn1gnQcC~?9P${XSt8UX0v1W zhND>e)NVf0q#*FUVa3pi(vaY!pvWF|mR3E$O=$Tp!-M8QiDi2}Cu{=(IByuBtC`eA8&ajIgZH%kscgnHI z0+PsmGimo@WqqXzl2=NBCWG45V=Z zNrA!~HG}mPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1cXUMK~z{r?Uwsf zlvNnV`zQLjnM%7jnU!58H80gvmVjc20m&kwqeF?7qB1};DJ;m zKoHPYByXTXVJ3-~2%!+m?tZ89Jnl=%zR~jR|LZ9TS*(|3Ajtx*TdYISd@Zu} zRHL@&66#CZP-ki#R}E#C(Nu9oc5FP=o7!+fteqWOgP;XzShq;$#6gk))Fa@{RFoZT z!G9H11?TbkTl;v|#_~7;u6-vBVV(xqtae<#dKZwiE<1cyE~h@9OO3I{I>1HvSs4 zptt893}V}2HXcKP?iBY)F+4Pa0{toM5o7eso$dkdx7+QA2si}Qe9a>)Bw4`lRSy8E zPCTBC#SfW3a+{*RcM$8}OGn1H$GKge_Y*#OItGPVW{g;D*e>RJ%~Zp8P7*xD-_#vt z{G4Dw!j@uq&Ui4k{P{TS^gA^A19tfnp!N|K(u9B=7rXK4O9{|MRB)RjZ(qac;uJLI zJu4p9F881?+l*z;#6YvF9PQ_CB3$gm>Q|G{)71}i&Cd)T6`0RsiCazFZWQLCm=fTh zcLxzH+(nZqdT$LNL_8DtwgwyC-;eI!`Vbv-gy;SK>s@@k@&Fe)WtpNt*94Clu`pM6 zK(n(PKC=^WrKO9GKn-XcwrhW!&V$jJ_+AflCbH+40c_S$QTuO z2+@3Vxh{ehi^--1h?6qW= zjLq2iK?c6^HuAYgS=_Qbhh3i%UW%R@{e1AMLkoExMU*O^e@MrLo19%Tk`(FRRB?@- z5nE3&X+X*tMP>du2Bfi7FDJ4YC@3QVj+-VsrkXPz`f(Nmx18*BWG{Xcn4Ie~I}yjf zzrY#w6VBi4VZhK-O(Fqyw%vkio`y3Zn1gnQcC~?9P${XSt8UX0v1W zhND>e)NVf0q#*FUVa3pi(vaY!pvWF|mR3E$O=$Tp!-M8QiDi2}Cu{=(IByuBtC`eA8&ajIgZH%kscgnHI z0+PsmGimo@WqqXzl2=NBCWG45V=Z zNrA!~HG}mPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1cXUMK~z{r?Uwsf zlvNnV`zQLjnM%7jnU!58H80gvmVjc20m&kwqeF?7qB1};DJ;m zKoHPYByXTXVJ3-~2%!+m?tZ89Jnl=%zR~jR|LZ9TS*(|3Ajtx*TdYISd@Zu} zRHL@&66#CZP-ki#R}E#C(Nu9oc5FP=o7!+fteqWOgP;XzShq;$#6gk))Fa@{RFoZT z!G9H11?TbkTl;v|#_~7;u6-vBVV(xqtae<#dKZwiE<1cyE~h@9OO3I{I>1HvSs4 zptt893}V}2HXcKP?iBY)F+4Pa0{toM5o7eso$dkdx7+QA2si}Qe9a>)Bw4`lRSy8E zPCTBC#SfW3a+{*RcM$8}OGn1H$GKge_Y*#OItGPVW{g;D*e>RJ%~Zp8P7*xD-_#vt z{G4Dw!j@uq&Ui4k{P{TS^gA^A19tfnp!N|K(u9B=7rXK4O9{|MRB)RjZ(qac;uJLI zJu4p9F881?+l*z;#6YvF9PQ_CB3$gm>Q|G{)71}i&Cd)T6`0RsiCazFZWQLCm=fTh zcLxzH+(nZqdT$LNL_8DtwgwyC-;eI!`Vbv-gy;SK>s@@k@&Fe)WtpNt*94Clu`pM6 zK(n(PKC=^WrKO9GKn-XcwrhW!&V$jJ_+AflCbH+40c_S$QTuO z2+@3Vxh{ehi^--1h?6qW= zjLq2iK?c6^HuAYgS=_Qbhh3i%UW%R@{e1AMLkoExMU*O^e@MrLo19%Tk`(FRRB?@- z5nE3&X+X*tMP>du2Bfi7FDJ4YC@3QVj+-VsrkXPz`f(Nmx18*BWG{Xcn4Ie~I}yjf zzrY#w6VBi4VZhK-O(Fqyw%vkio`y3Zn1gnQcC~?9P${XSt8UX0v1W zhND>e)NVf0q#*FUVa3pi(vaY!pvWF|mR3E$O=$Tp!-M8QiDi2}Cu{=(IByuBtC`eA8&ajIgZH%kscgnHI z0+PsmGimo@WqqXzl2=NBCWG45V=Z zNrA!~HG}mPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2F6K5K~!i%?O69$ z6;}|>f0JKQjL{fljEOPHNi*@NC@7+ch#Gr2mJn;qu}~i!DS{}1(rgi|C;}ENU?Ud9 z0*E4rfRx8%zS&(_b$t)?y(8zj-#On|-rc#o^WB*{bC>s~ej82i0!^W=MpLM((G==x zG=;hvO`)zvQ>d%a6zXa;g&J|n*O4^&tB77fANIw_$5*jiLZ^HaN#py6kl%MPG;c&A z%^$H@u5W|dyQh60BR+}XVa9&zX;&y7 zk0gmo?Y#!|SNE|y`3(6_NTsq(4>t= znM6<9xWZWmHfR@<=t3xJ<=mj$n%XfXtXPI^9Ds|-Tg+I z@#7})_%wuq7VMTWJzAPtX^rm=sfZ7%9?KY_1qTt)KX~rrN6j)0@nd+wP&LOfadrDB zfVPL9koy|0l~$1-S5)^;f_RY?JIq@+I+^|H5}Ov`K%rKvmHzbHCS%Lk`3{br9ytl8 zX!M5xlh1*w@R8P+Vp>(aRO6*V% zAP9?uTCrec63rQ&Aalm_38c8S2gMF>s8Aqe@g9z7Firm;RJO$)E?`&?*dfKAJm!{i zyPjtL6eq%G{TxS)4J~5paG~juh2k^{(xub4)luS!7mA(* zXW)G9_H#1!fFp~2B#oORiZsMrI3Mo+uh{zyi;+~!=Ja;&t3;`^_?FBE+FCnkkz2An zcf*`)s^vYdy+3T}KGjM=R$Q^zVL`8)y(?o57kaC_R>a^%v8xZr81CaY1fmcj0;tZ1 zGD>OvtSs4|5CO!j&J$Z4-?V}SG}JXoN}|w3F5fTFhY;9ear8jD!#MnaKY+u9qIJw4 zxmlcFJ1v9mRn&>FiG#vu;ix2zQl*?bCk+ax<&(D4A}%HnjXefL{Iy>Z%4cT1(2`>} zp6$_He5|XudS9Y0LV5pcKuV(!!glWOiIOFN!-WC?^KwKeUi7!$P7wtMvs8!HOx+=S z7^&>bQHE0x2&4q(i}Mg)%+(iYZ||U$K4~%s$IXf>_hmaEV%Vd|3JrPb=jC~LM%0R2 zb<{~hGh&rJf>fQv1pyAt`yh%J(P?JQu`lyoUb5P*ALY=?G(1w+beIFfx)4iONC48TIb2EBS)=yBCbfm4Ly z6G>~_h>Y~JdZnLfKy`)~>|Yd^?D(#Ng3l|oA5>#R9ub~BJf6-MR;U(T*b!AIpNJl8 z2U{#k68rPwxz`~=wX?1sk-X<{6a!!N2^Y0ISF!06Bz;zoPImM{(JjhNJ5NP<<$M@; zexdZhSr^eon(HI32ZUn5j5KJpDHJJ+2L2-cyHF+jGeJE88R2P5o6JSpoj!719d!;Y z+-p&|j5KJpDHK%`siQS!J#ZlC4)krr8Mt|+M*a(;9aLULn?gZEZq_exQZexv<_w@e z_7s?d^qB4~hQ-q*@AI%7%wEjnZb2>PGXP zqxx58^yPkH1YSd0Z*tZk7vCFlmxoluSL8G--=L;=!@v5NZZS2?$9#+u*V`8 z*}f^?s!)wMzv4RQw*` diff --git a/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-59.png b/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-59.png deleted file mode 100644 index 6d150ab9d37d542f68e753a22b233a0c7cce968d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1841 zcmV-12hRA3P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2F6K5K~!i%?O69$ z6;}|>f0JKQjL{fljEOPHNi*@NC@7+ch#Gr2mJn;qu}~i!DS{}1(rgi|C;}ENU?Ud9 z0*E4rfRx8%zS&(_b$t)?y(8zj-#On|-rc#o^WB*{bC>s~ej82i0!^W=MpLM((G==x zG=;hvO`)zvQ>d%a6zXa;g&J|n*O4^&tB77fANIw_$5*jiLZ^HaN#py6kl%MPG;c&A z%^$H@u5W|dyQh60BR+}XVa9&zX;&y7 zk0gmo?Y#!|SNE|y`3(6_NTsq(4>t= znM6<9xWZWmHfR@<=t3xJ<=mj$n%XfXtXPI^9Ds|-Tg+I z@#7})_%wuq7VMTWJzAPtX^rm=sfZ7%9?KY_1qTt)KX~rrN6j)0@nd+wP&LOfadrDB zfVPL9koy|0l~$1-S5)^;f_RY?JIq@+I+^|H5}Ov`K%rKvmHzbHCS%Lk`3{br9ytl8 zX!M5xlh1*w@R8P+Vp>(aRO6*V% zAP9?uTCrec63rQ&Aalm_38c8S2gMF>s8Aqe@g9z7Firm;RJO$)E?`&?*dfKAJm!{i zyPjtL6eq%G{TxS)4J~5paG~juh2k^{(xub4)luS!7mA(* zXW)G9_H#1!fFp~2B#oORiZsMrI3Mo+uh{zyi;+~!=Ja;&t3;`^_?FBE+FCnkkz2An zcf*`)s^vYdy+3T}KGjM=R$Q^zVL`8)y(?o57kaC_R>a^%v8xZr81CaY1fmcj0;tZ1 zGD>OvtSs4|5CO!j&J$Z4-?V}SG}JXoN}|w3F5fTFhY;9ear8jD!#MnaKY+u9qIJw4 zxmlcFJ1v9mRn&>FiG#vu;ix2zQl*?bCk+ax<&(D4A}%HnjXefL{Iy>Z%4cT1(2`>} zp6$_He5|XudS9Y0LV5pcKuV(!!glWOiIOFN!-WC?^KwKeUi7!$P7wtMvs8!HOx+=S z7^&>bQHE0x2&4q(i}Mg)%+(iYZ||U$K4~%s$IXf>_hmaEV%Vd|3JrPb=jC~LM%0R2 zb<{~hGh&rJf>fQv1pyAt`yh%J(P?JQu`lyoUb5P*ALY=?G(1w+beIFfx)4iONC48TIb2EBS)=yBCbfm4Ly z6G>~_h>Y~JdZnLfKy`)~>|Yd^?D(#Ng3l|oA5>#R9ub~BJf6-MR;U(T*b!AIpNJl8 z2U{#k68rPwxz`~=wX?1sk-X<{6a!!N2^Y0ISF!06Bz;zoPImM{(JjhNJ5NP<<$M@; zexdZhSr^eon(HI32ZUn5j5KJpDHJJ+2L2-cyHF+jGeJE88R2P5o6JSpoj!719d!;Y z+-p&|j5KJpDHK%`siQS!J#ZlC4)krr8Mt|+M*a(;9aLULn?gZEZq_exQZexv<_w@e z_7s?d^qB4~hQ-q*@AI%7%wEjnZb2>PGXP zqxx58^yPkH1YSd0Z*tZk7vCFlmxoluSL8G--=L;=!@v5NZZS2?$9#+u*V`8 z*}f^?s!)wMzv4RQw*` diff --git a/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60.png b/WooCommerce/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60.png deleted file mode 100644 index a343197774a9124e901e9c8f6dc85dcc8ad6f923..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1859 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 t3OFpXg(g_j zYCAKYIUF4DY8K>}+0ehg%dQw#BFA$3pf1|WPjs|5?@QZQBJ|h>#wh?{=>s;XK<)99 zKMQa=UrQfS4E0v8t?XVU6Od`$hY-0M5N z_OE`!jq%RWuuJ48^?9v^a)UBkp{qE4J3QXb#QC3Qq6C`oSSs5pK0b5X`<2+9S{Td7 z1Hp{ys~B=Oy57s-&`Sb3xtQY{i>noNb1xWSQdgi8u`$uZU1_}CenF>rC8pY{PJf@ zdu4s9e*!a6HBGSR0ypCg?Rk>i*@O=#6v(7}6k0=Q*Hfddf1g+-s2bRSQ6o|A1E9R= zVk5Pm9whMjs{EW`+zhwetebj4WQSK?0)ma@F5~rbhh((gfwi!5{M3II8#Vm7t>O+y zBL&gW$Qxy<@NTSsdmF_;MCe2%WxV**@_@s{O5_?;f_~@DZ>njOta{~fY?$P0XoGjW zM)BB9?t^j5KfXF}CE0UpCvgH&-DvslK~7BA*I}h!OS!RdhR!F2fXe2U^TnOGipCSK z54?Dd#e{3l6NK}2fOL^|_1>oknT^4}gsO!F^!mU_x+EljJ?EN3jFwv|GHdoWxUwfE z$9twmgQ@9O06BN3#V@drKM1+(xRSH+{B_69ancr#e}-#_pc=?ky5rH%QkTo=_?j%p z!ybx$l^PT7^($ZHu1gaL5lAAI!c_Pc@A;XT`G>jD1 zA&Gqm`@H|)%mEu+)Z@(>1pbIC8zuYxL^0+(?jLW90=QVw8DHEk>0x63{yv6s@nuB; zR-2=T>4)w%5}r0*H+o}~`?o1~N#1^J#$L}R{$>%S3*|`vTpq33>mtP*pKBBsNW{>6 z)U!H%3$$sXyMXbAafNJh)+C;Bqrsr2DrpE1toFOu$DM|l?JFX@QC%5NANQrf9}9(nAf+l`1i$`!7~Q`@CTt`#0%ec z&q+1V_#YbSO>MG(*S=@u($^VEQlk}TS_VX7c&@}!|9 zYbtAk(xjz~ITu|;z_vhVU^AH@0YFl)Z*>PgA9VedBm!>nOB_2c1K(`^$Rd_1&tN^w z4d}=Kdu_)|HN0$b0kH-?hhd%xLp`!Ana*Vf6z;4>VqfVnqtv{vG8gZyg*N{MUQP4T zC#oYRx8=<*22PH~b zgg7Toi93)>p+?Z-j8hT?)zbK<7m$XH zB;G&ZkTuQ$Rq^ec?FP@tU0}fH2-TDP8^7{m1A=cEe#%`dTX5{Qwwb?`~2 z{jw5IB^2p?PmfrhH(hU(Q?HJ|CNY<^Q;d7td_@z3WjbUFh|%P^6Q)~M623}Lh<+MR zJ9-d(EU0+%Tl@X}(jEek) BL;-*4$?Y&5<)?H23w{WF%%XGLa)G^BEbr%@O9> z2h~au(KlD^dywqkaaNfy`LarHJ>B^_4|wVXo#`C!QxM zimK@opX;lWZO;jzcvt}FEDzZ>sf|0aRkQ`qOkYG&Jgi4|Mt4W?tF~Lkt4psqM%;a$ z^}>N$`jIjKoj}k>8q|?$ZesU;KrWH$VMF_&#lA9TQ>&C68}phKh t3OFpXg(g_j zYCAKYIUF4DY8K>}+0ehg%dQw#BFA$3pf1|WPjs|5?@QZQBJ|h>#wh?{=>s;XK<)99 zKMQa=UrQfS4E0v8t?XVU6Od`$hY-0M5N z_OE`!jq%RWuuJ48^?9v^a)UBkp{qE4J3QXb#QC3Qq6C`oSSs5pK0b5X`<2+9S{Td7 z1Hp{ys~B=Oy57s-&`Sb3xtQY{i>noNb1xWSQdgi8u`$uZU1_}CenF>rC8pY{PJf@ zdu4s9e*!a6HBGSR0ypCg?Rk>i*@O=#6v(7}6k0=Q*Hfddf1g+-s2bRSQ6o|A1E9R= zVk5Pm9whMjs{EW`+zhwetebj4WQSK?0)ma@F5~rbhh((gfwi!5{M3II8#Vm7t>O+y zBL&gW$Qxy<@NTSsdmF_;MCe2%WxV**@_@s{O5_?;f_~@DZ>njOta{~fY?$P0XoGjW zM)BB9?t^j5KfXF}CE0UpCvgH&-DvslK~7BA*I}h!OS!RdhR!F2fXe2U^TnOGipCSK z54?Dd#e{3l6NK}2fOL^|_1>oknT^4}gsO!F^!mU_x+EljJ?EN3jFwv|GHdoWxUwfE z$9twmgQ@9O06BN3#V@drKM1+(xRSH+{B_69ancr#e}-#_pc=?ky5rH%QkTo=_?j%p z!ybx$l^PT7^($ZHu1gaL5lAAI!c_Pc@A;XT`G>jD1 zA&Gqm`@H|)%mEu+)Z@(>1pbIC8zuYxL^0+(?jLW90=QVw8DHEk>0x63{yv6s@nuB; zR-2=T>4)w%5}r0*H+o}~`?o1~N#1^J#$L}R{$>%S3*|`vTpq33>mtP*pKBBsNW{>6 z)U!H%3$$sXyMXbAafNJh)+C;Bqrsr2DrpE1toFOu$DM|l?JFX@QC%5NANQrf9}9(nAf+l`1i$`!7~Q`@CTt`#0%ec z&q+1V_#YbSO>MG(*S=@u($^VEQlk}TS_VX7c&@}!|9 zYbtAk(xjz~ITu|;z_vhVU^AH@0YFl)Z*>PgA9VedBm!>nOB_2c1K(`^$Rd_1&tN^w z4d}=Kdu_)|HN0$b0kH-?hhd%xLp`!Ana*Vf6z;4>VqfVnqtv{vG8gZyg*N{MUQP4T zC#oYRx8=<*22PH~b zgg7Toi93)>p+?Z-j8hT?)zbK<7m$XH zB;G&ZkTuQ$Rq^ec?FP@tU0}fH2-TDP8^7{m1A=cEe#%`dTX5{Qwwb?`~2 z{jw5IB^2p?PmfrhH(hU(Q?HJ|CNY<^Q;d7td_@z3WjbUFh|%P^6Q)~M623}Lh<+MR zJ9-d(EU0+%Tl@X}(jEek) B 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 W>WuBTn2)C`m<4WPY20Awp8P+Qe}KsV From af7f6e5cca984a87be4d501ca3eb527b6562dbf7 Mon Sep 17 00:00:00 2001 From: Hafiz Rahman Date: Fri, 15 Nov 2024 15:06:11 +0700 Subject: [PATCH 32/49] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 021620ff9e0..90333c8c6d0 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -12,6 +12,7 @@ - [internal] Allow login for WPCOM suspended sites [https://github.com/woocommerce/woocommerce-ios/pull/14257] - [*] Menu tab > Payments: in the "deposits" summary view at the top, all mentions of "deposits/deposit/deposited" have been renamed to "payouts/payout/paid out" respectively to match the renaming in web. [https://github.com/woocommerce/woocommerce-ios/pull/14402] - [*] Receipts: Added message confirming receipt sent to customer after successful payment. [https://github.com/woocommerce/woocommerce-ios/pull/14390] +- [internal] Simplifies App Icon by only using a single image for all supported platforms. [https://github.com/woocommerce/woocommerce-ios/pull/14429] 21.1 ----- From 35b4a7efd100879c4afa9b90e6463215e4fe4364 Mon Sep 17 00:00:00 2001 From: Hafiz Rahman Date: Fri, 15 Nov 2024 15:00:12 +0700 Subject: [PATCH 33/49] Exclude flaky unit tests related to dashboard cards's custom range date. (#14426) --- WooCommerce/WooCommerceTests/UnitTests.xctestplan | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WooCommerce/WooCommerceTests/UnitTests.xctestplan b/WooCommerce/WooCommerceTests/UnitTests.xctestplan index 6478f0088d8..6ca137aa850 100644 --- a/WooCommerce/WooCommerceTests/UnitTests.xctestplan +++ b/WooCommerce/WooCommerceTests/UnitTests.xctestplan @@ -33,7 +33,9 @@ "CardReaderConnectionControllerTests", "InAppPurchaseStoreTests\/test_user_is_entitled_to_product_returns_false_when_not_entitled()", "InAppPurchaseStoreTests\/test_user_is_entitled_to_product_returns_true_when_entitled()", - "StripeCardReaderIntegrationTests" + "StorePerformanceViewModelTests\/test_dates_for_custom_range_are_correct_for_non_custom_time_range()", + "StripeCardReaderIntegrationTests", + "TopPerformersDashboardViewModelTests\/test_dates_for_custom_range_are_correct_for_non_custom_time_range()" ], "target" : { "containerPath" : "container:WooCommerce.xcodeproj", From 49cd185659b069cb77bb5db1268c1ec6abe98c36 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 15 Nov 2024 09:29:18 +0000 Subject: [PATCH 34/49] Test fixes WIP --- .../Models/PointOfSaleAggregateModel.swift | 1 + .../POS/ViewModels/TotalsViewModel.swift | 1 + .../Mocks/MockCardPresentPaymentService.swift | 2 + .../Mocks/MockPointOfSaleAggregateModel.swift | 8 +- .../POS/Mocks/MockTotalsViewModel.swift | 14 -- .../PointOfSaleAggregateModelTests.swift | 124 +++++++++++++- .../POS/ViewModels/CartViewModelTests.swift | 21 +-- .../PointOfSaleDashboardViewModelTests.swift | 39 ++--- .../POS/ViewModels/TotalsViewModelTests.swift | 158 +++--------------- 9 files changed, 159 insertions(+), 209 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 8c57e5ff421..5d4ee0ea971 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -295,6 +295,7 @@ extension PointOfSaleAggregateModel { private func clearOrder() { order = nil + orderState = .idle } } diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift index f698c23dd97..74d3d39f1cd 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift @@ -99,6 +99,7 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { editOrderActionSubject.send(()) } + // These three functions could potentially move to posModel and be based on orderStage. func onTotalsViewDisappearance() { // This is a backup – it's not called until transitions are complete when using the back button. // The delay can lead to race conditions with tapping a card. diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockCardPresentPaymentService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockCardPresentPaymentService.swift index 0ce95a6a79f..1bd8b7d54a3 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockCardPresentPaymentService.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockCardPresentPaymentService.swift @@ -36,7 +36,9 @@ final class MockCardPresentPaymentService: CardPresentPaymentFacade { } var onCollectPaymentCalled: (() -> Void)? + var collectPaymentWasCalled = false func collectPayment(for order: Yosemite.Order, using connectionMethod: CardReaderConnectionMethod) async throws -> CardPresentPaymentResult { + collectPaymentWasCalled = true onCollectPaymentCalled?() paymentEvent = .show(eventDetails: CardPresentPaymentEventDetails.paymentSuccess(done: {})) return .success(CardPresentPaymentTransaction(receiptURL: URL(string: "https://example.net/receipts/123")!)) diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift index 8c72c1fc6b9..918d4ef8bc3 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift @@ -11,6 +11,8 @@ final class MockPointOfSaleAggregateModel: PointOfSaleAggregateModelProtocol { var orderStage: PointOfSaleOrderStage + var orderState: WooCommerce.PointOfSaleOrderState + var allItems: [POSItem] { switch itemListState { case .empty, @@ -27,10 +29,12 @@ final class MockPointOfSaleAggregateModel: PointOfSaleAggregateModelProtocol { init(cardReaderConnectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected, itemListState: ItemListState = .initialLoading, - orderStage: PointOfSaleOrderStage = .building) { + orderStage: PointOfSaleOrderStage = .building, + orderState: PointOfSaleOrderState = .idle) { self.cardReaderConnectionStatus = cardReaderConnectionStatus self.itemListState = itemListState self.orderStage = orderStage + self.orderState = orderState } func loadInitialItems() async { } @@ -52,6 +56,8 @@ final class MockPointOfSaleAggregateModel: PointOfSaleAggregateModelProtocol { func submitCart() { } + func checkOut() async { } + func addMoreToCart() { } func startNewCart() { } diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockTotalsViewModel.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockTotalsViewModel.swift index 64892924670..3693b592700 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockTotalsViewModel.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockTotalsViewModel.swift @@ -6,25 +6,15 @@ import struct Yosemite.Order final class MockTotalsViewModel: TotalsViewModelProtocol { - var order: Yosemite.Order? - - @Published var orderState: TotalsViewModel.OrderState = .loaded @Published var paymentState: TotalsViewModel.PaymentState = .idle @Published var cardPresentPaymentEvent: CardPresentPaymentEvent = .idle @Published var connectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected - @Published var startNewOrderAction: Void = () @Published var editOrderAction: Void = () - var orderStatePublisher: Published.Publisher { $orderState } var paymentStatePublisher: Published.Publisher { $paymentState } - var startNewOrderActionPublisher: AnyPublisher { $startNewOrderAction.eraseToAnyPublisher() } var editOrderActionPublisher: AnyPublisher { $editOrderAction.eraseToAnyPublisher() } - var isSyncingOrder: Bool { - return orderState.isSyncing - } - var cardPresentPaymentInlineMessage: PointOfSaleCardPresentPaymentMessageType? { // Provide a mock implementation if needed nil @@ -34,10 +24,6 @@ final class MockTotalsViewModel: TotalsViewModelProtocol { paymentState = .acceptingCard } - func checkOutTapped(with cartItems: [CartItem], allItems: [POSItem]) { - orderState = .syncing - } - var spyStopShowingTotalsViewCalled = false func stopShowingTotalsView() { spyStopShowingTotalsViewCalled = true diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 89bb561748b..488ea69f942 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -3,6 +3,8 @@ import Foundation @testable import WooCommerce import protocol Yosemite.POSItem @testable import struct Yosemite.POSProduct +import struct Yosemite.Order +import Combine struct PointOfSaleAggregateModelTests { struct OrderStageTests { @@ -402,22 +404,27 @@ struct PointOfSaleAggregateModelTests { } struct OrderTests { - private let itemProvider: MockPOSItemProvider - private let orderService: MockPOSOrderService + private let cardPresentPaymentService = MockCardPresentPaymentService() + private let itemProvider = MockPOSItemProvider() + private let orderService = MockPOSOrderService() private let sut: PointOfSaleAggregateModel init() { - itemProvider = MockPOSItemProvider() - orderService = MockPOSOrderService() - sut = PointOfSaleAggregateModel(itemProvider: itemProvider, - cardPresentPaymentService: MockCardPresentPaymentService(), - orderService: orderService) + orderService.orderToReturn = Order.fake() + + sut = PointOfSaleAggregateModel( + itemProvider: itemProvider, + cardPresentPaymentService: cardPresentPaymentService, + orderService: orderService) + + sut.addToCart(makeItem()) } @Test func startNewCart_sets_orderState_to_idle() async throws { // Given + await sut.checkOut() try #require(sut.orderState == .loaded(.init( - cartTotal: "", + cartTotal: "$0.00", orderTotal: "", taxTotal: ""))) @@ -427,6 +434,107 @@ struct PointOfSaleAggregateModelTests { // Then #expect(sut.orderState == .idle) } + + @Test func checkOut_when_reader_connects_collectPayment_called() async throws { + // Given + cardPresentPaymentService.connectedReader = nil + await sut.checkOut() + + // Then + await confirmation() { confirmation in + cardPresentPaymentService.onCollectPaymentCalled = { + confirmation() + } + // When + cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) + } + } + + @Test func checkOut_when_reader_is_already_connected_collectPayment_called() async throws { + // Given + cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) + orderService.orderToReturn = Order.fake().copy(items: [.fake()]) + + // When + await sut.checkOut() + + // Then + #expect(cardPresentPaymentService.collectPaymentWasCalled) + } + + @Test func after_disconnection_when_reader_reconnects_collectPayment_called() async throws { + // Given + cardPresentPaymentService.connectedReader = CardPresentPaymentCardReader(name: "Test", batteryLevel: 0.5) + sut.observeReaderReconnection() + await cardPresentPaymentService.disconnectReader() + + // Then + await confirmation() { confirmation in + cardPresentPaymentService.onCollectPaymentCalled = { + confirmation() + } + // When + cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) + } + } + + @Test func checkOut_with_no_previous_order_sets_orderState_syncing_then_loaded() async throws { + // Given + var cancellables = Set() + var orderStates: [PointOfSaleOrderState] = [] + await confirmation() { confirmation in + // We can use `withObservationTracking` when we move to @Observable + sut.$orderState.collect(3) + .sink { orderState in + orderStates.append(contentsOf: orderState) + confirmation() + } + .store(in: &cancellables) + + // When + await sut.checkOut() + } + + // Then + #expect(orderStates == [.idle, .syncing, .loaded(.init(cartTotal: "$0.00", orderTotal: "", taxTotal: ""))]) + } + + @Test func checkOut_with_order_sync_failure_sets_orderState_syncing_then_error() async throws { + // Given + orderService.orderToReturn = nil + + var cancellables = Set() + var orderStates: [PointOfSaleOrderState] = [] + await confirmation() { confirmation in + // We can use `withObservationTracking` when we move to @Observable + sut.$orderState.collect(3) + .sink { orderState in + orderStates.append(contentsOf: orderState) + confirmation() + } + .store(in: &cancellables) + + // When + await sut.checkOut() + } + + // Then + #expect(orderStates == [.idle, .syncing, .error(.init(message: "", handler: {}))]) + } + } + + struct PaymentTests { + private let cardPresentPaymentService = MockCardPresentPaymentService() + private let sut: PointOfSaleAggregateModel + + init() { + sut = PointOfSaleAggregateModel( + itemProvider: MockPOSItemProvider(), + cardPresentPaymentService: cardPresentPaymentService, + orderService: MockPOSOrderService()) + } + + } } diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewModelTests.swift index 223e03dc6d5..48299649b1c 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewModelTests.swift @@ -13,7 +13,8 @@ final class CartViewModelTests: XCTestCase { override func setUp() { super.setUp() posModel = PointOfSaleAggregateModel(itemProvider: MockPOSItemProvider(), - cardPresentPaymentService: MockCardPresentPaymentService()) + cardPresentPaymentService: MockCardPresentPaymentService(), + orderService: MockPOSOrderService()) sut = CartViewModel(posModel: posModel) } @@ -22,24 +23,6 @@ final class CartViewModelTests: XCTestCase { super.tearDown() } - func test_cart_when_submitCart_is_invoked_then_cartSubmissionPublisher_emits_cart_items() { - // Given - var cancellables: Set = [] - let item = Self.makeItem() - let anotherItem = Self.makeItem() - - // When - posModel.addToCart(item) - posModel.addToCart(anotherItem) - sut.cartSubmissionPublisher.sink(receiveValue: { cartItems in - // Then - XCTAssertEqual(cartItems.count, 2) - }) - .store(in: &cancellables) - - sut.submitCart() - } - func test_removeItemFromCart() { /* TODO: https://github.com/woocommerce/woocommerce-ios/issues/13209 diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift index af8d2fbe563..c5b9a229b21 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift @@ -6,7 +6,7 @@ import Combine final class PointOfSaleDashboardViewModelTests: XCTestCase { private var sut: PointOfSaleDashboardViewModel! - private var mockPOSModel: MockPointOfSaleAggregateModel! + private var mockPOSModel: PointOfSaleAggregateModel! private var cardPresentPaymentService: MockCardPresentPaymentService! private var itemProvider: MockPOSItemProvider! private var mockCartViewModel: MockCartViewModel! @@ -18,13 +18,16 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { override func setUp() { super.setUp() - mockPOSModel = MockPointOfSaleAggregateModel() cardPresentPaymentService = MockCardPresentPaymentService() itemProvider = MockPOSItemProvider() mockCartViewModel = MockCartViewModel() mockTotalsViewModel = MockTotalsViewModel() mockItemListViewModel = MockItemListViewModel() mockConnectivityObserver = MockConnectivityObserver() + mockPOSModel = PointOfSaleAggregateModel( + itemProvider: itemProvider, + cardPresentPaymentService: cardPresentPaymentService, + orderService: MockPOSOrderService()) sut = PointOfSaleDashboardViewModel(posModel: mockPOSModel, totalsViewModel: mockTotalsViewModel, cartViewModel: mockCartViewModel, @@ -54,11 +57,11 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { XCTAssertEqual(sut.isExitPOSDisabled, expectedExitPOSButtonDisabledState) } - func test_isAddMoreDisabled_is_true_when_order_is_syncing_and_paymentState_is_idle() { + func test_isAddMoreDisabled_is_true_when_order_is_syncing_and_paymentState_is_idle() async { // Given let expectation = XCTestExpectation(description: "Expect isAddMoreDisabled to be true while syncing order and payment state is idle") - sut.$isAddMoreDisabled + await sut.$isAddMoreDisabled .dropFirst() .sink { value in XCTAssertTrue(value) @@ -67,10 +70,10 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { .store(in: &cancellables) // When - let customCartItems = [CartItem(id: UUID(), item: Self.makeItem(), quantity: 1)] - sut.totalsViewModel.checkOutTapped(with: customCartItems, allItems: []) + mockPOSModel.addToCart(Self.makeItem()) + await mockPOSModel.checkOut() - wait(for: [expectation], timeout: 1.0) + await fulfillment(of: [expectation], timeout: 1.0) } func test_isAddMoreDisabled_is_true_for_collectPayment_success() { @@ -179,28 +182,6 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } - func test_observeCartSubmission_starts_syncing_order() { - // Given - let expectation = XCTestExpectation(description: "Expect orderStage to be .finalizing and isSyncingOrder to be true") - let customCartItems = [CartItem(id: UUID(), item: Self.makeItem(), quantity: 1)] - var receivedIsSyncingOrder: Bool = false - - // Attach sink to observe changes to isSyncingOrder - mockTotalsViewModel.orderStatePublisher - .sink { orderState in - receivedIsSyncingOrder = orderState.isSyncing - expectation.fulfill() - } - .store(in: &cancellables) - - // When - mockCartViewModel.submitCart(with: customCartItems) - - // Then - wait(for: [expectation], timeout: 1.0) - XCTAssertTrue(receivedIsSyncingOrder) - } - func test_showsConnectivityError_when_nonReachable_then_shows_error() { // Given mockConnectivityObserver.setStatus(.notReachable) diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift index 6f6df842c24..33ed2e04024 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift @@ -13,25 +13,23 @@ final class TotalsViewModelTests: XCTestCase { private var sut: TotalsViewModel! private var cardPresentPaymentService: MockCardPresentPaymentService! private var orderService: MockPOSOrderService! + private var posModel: PointOfSaleAggregateModel! private var cancellables = Set() override func setUp() { super.setUp() cardPresentPaymentService = MockCardPresentPaymentService() orderService = MockPOSOrderService() - sut = TotalsViewModel(cardPresentPaymentService: cardPresentPaymentService, + posModel = PointOfSaleAggregateModel( + itemProvider: MockPOSItemProvider(), + cardPresentPaymentService: cardPresentPaymentService, + orderService: orderService) + sut = TotalsViewModel(posModel: posModel, + cardPresentPaymentService: cardPresentPaymentService, paymentState: .acceptingCard) cancellables = Set() } - func test_order_when_clearOrder_invoked_then_order_is_set_to_nil() { - // When - sut.clearOrder() - - // Then - XCTAssertNil(sut.order) - } - func test_startNewOrder_after_collecting_payment() async throws { // Given let paymentState: TotalsViewModel.PaymentState = .acceptingCard @@ -39,38 +37,23 @@ final class TotalsViewModelTests: XCTestCase { orderService.orderToReturn = Order.fake() - await sut.syncOrder(for: [CartItem(id: UUID(), item: item, quantity: 1)], allItems: [item]) - XCTAssertNotNil(sut.order) - - var startNewOrderEventWasPublished = false - sut.startNewOrderActionPublisher.sink { _ in - startNewOrderEventWasPublished = true - }.store(in: &cancellables) - XCTAssertFalse(startNewOrderEventWasPublished) - // When - guard let order = sut.order else { - return XCTFail("Expected order. Got nothing") - } - _ = try await cardPresentPaymentService.collectPayment(for: order, using: .bluetooth) - sut.startNewOrder() + // Lots deleted here temporarily; may need to be added back when we move paymentState and messages. + await sut.startNewOrder() // Then - XCTAssertTrue(startNewOrderEventWasPublished) XCTAssertEqual(sut.paymentState, paymentState) - XCTAssertNil(sut.order) XCTAssertNil(sut.cardPresentPaymentInlineMessage) } func test_isShowingCardReaderStatus_when_order_not_loaded_then_false() async { // Given - sut = TotalsViewModel(orderService: orderService, - cardPresentPaymentService: cardPresentPaymentService, - currencyFormatter: .init(currencySettings: .init()), - paymentState: .acceptingCard) - orderService.orderToReturn = nil +// orderService.orderToReturn = nil - await sut.syncOrder(for: [], allItems: []) + // When +// await sut.syncOrder(for: [], allItems: []) + // If this needs testing, it should rely on posModel.orderState now + // we can't mock posModel properties until paymentState is moved, because we currently need the published properties in TotalsViewModel // Then XCTAssertFalse(sut.isShowingCardReaderStatus) @@ -83,7 +66,7 @@ final class TotalsViewModelTests: XCTestCase { cardPresentPaymentService.paymentEvent = .show(eventDetails: .preparingForPayment(cancelPayment: {})) let item = Self.makeItem() - await sut.syncOrder(for: [CartItem(id: UUID(), item: item, quantity: 1)], allItems: [item]) +// await sut.syncOrder(for: [CartItem(id: UUID(), item: item, quantity: 1)], allItems: [item]) // Then XCTAssertTrue(sut.isShowingCardReaderStatus) @@ -116,46 +99,14 @@ final class TotalsViewModelTests: XCTestCase { XCTAssertTrue(sut.isShowingTotalsFields) } - func test_when_a_reader_connects_collectPayment_is_attempted() async { - // Given - orderService.orderToReturn = Order.fake().copy(items: [OrderItem.fake()]) - await sut.syncOrder(for: [], allItems: []) - - waitFor { promise in - self.cardPresentPaymentService.onCollectPaymentCalled = { - // Then - promise(()) - } - // When - self.cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) - } - } - - func test_if_a_reader_is_already_connected_collectPayment_is_attempted_immediately() async { - // Given - cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) - - orderService.orderToReturn = Order.fake().copy(items: [OrderItem.fake()]) - await sut.syncOrder(for: [], allItems: []) - - waitFor { promise in - self.cardPresentPaymentService.onCollectPaymentCalled = { - // Then - promise(()) - } - // When - self.sut.checkOutTapped(with: [], allItems: []) - } - } - func test_cardPresentPaymentInlineMessage_when_paymentSuccess_then_total_set() async { // Given orderService.orderToReturn = Order.fake().copy(currency: "$", total: "52.30") - await sut.syncOrder(for: [], allItems: []) +// await sut.syncOrder(for: [], allItems: []) // When cardPresentPaymentService.paymentEvent = .show(eventDetails: .paymentSuccess(done: { })) - let message = sut.cardPresentPaymentInlineMessage + let message = await sut.cardPresentPaymentInlineMessage // Then if case .paymentSuccess(let viewModel) = message { @@ -166,79 +117,12 @@ final class TotalsViewModelTests: XCTestCase { } } - - func test_orderState_when_syncOrder_succeeds_then_syncing_and_loaded() async { - // Given sync order succeeds - let expectation = XCTestExpectation(description: "OrderState should change 2 times when syncing order") - orderService.orderToReturn = Order.fake() - - // When we sync order - var orderStates: [TotalsViewModel.OrderState] = [] - sut.orderStatePublisher - .collect(3) - .sink { orderState in - orderStates.append(contentsOf: orderState) - expectation.fulfill() - } - .store(in: &cancellables) - await sut.syncOrder(for: [], allItems: []) - await fulfillment(of: [expectation], timeout: 1) - - // Then OrderState changes from idle to syncing to loaded - XCTAssertEqual(orderStates, [.idle, .syncing, .loaded]) - } - - func test_orderState_when_syncOrder_fails_then_syncing_and_error() async { - // Given sync order fails - let expectation = XCTestExpectation(description: "OrderState should change 2 times when syncing order") - orderService.orderToReturn = nil - - // When we sync order - var orderStates: [TotalsViewModel.OrderState] = [] - sut.orderStatePublisher - .collect(3) - .sink { orderState in - orderStates.append(contentsOf: orderState) - expectation.fulfill() - } - .store(in: &cancellables) - await sut.syncOrder(for: [], allItems: []) - await fulfillment(of: [expectation], timeout: 1) - - // Then OrderState changes from idle to syncing to error - XCTAssertEqual(orderStates, [.idle, .syncing, .error(.init(message: "", handler: {}))]) - } - - func test_when_reader_reconnects_on_TotalsView_reader_is_prepared_for_payment() async throws { - try XCTSkipIf(true, "This test is flaky in CI and should be improved. See #14005") - // Given a reader has been connected, with the order synced, on the TotalsView - sut.startShowingTotalsView() - cardPresentPaymentService.connectedReader = CardPresentPaymentCardReader(name: "Test", batteryLevel: 0.5) - - orderService.orderToReturn = Order.fake() - await sut.syncOrder(for: [], allItems: []) - // And that reader has subsequently disconnected - await cardPresentPaymentService.disconnectReader() - - let collectPaymentCalled = waitFor { promise in - // Then the reader is prepared for payment - self.cardPresentPaymentService.onCollectPaymentCalled = { - promise(true) - } - - // When a reader reconnects - self.cardPresentPaymentService.connectedReader = CardPresentPaymentCardReader(name: "Test", batteryLevel: 0.5) - } - - XCTAssertTrue(collectPaymentCalled) - } - func test_paymentIntentCreationErrorMessage_when_paymentIntentCreationError() async { // Given struct TestError: Error {} let item = Self.makeItem() orderService.orderToReturn = Order.fake() - await sut.syncOrder(for: [CartItem(id: UUID(), item: item, quantity: 1)], allItems: [item]) +// await sut.syncOrder(for: [CartItem(id: UUID(), item: item, quantity: 1)], allItems: [item]) var editOrderCalled = false sut.editOrderActionPublisher.sink { _ in @@ -299,9 +183,8 @@ final class TotalsViewModelTests: XCTestCase { // Given let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let sut = TotalsViewModel(orderService: orderService, + let sut = TotalsViewModel(posModel: posModel, cardPresentPaymentService: cardPresentPaymentService, - currencyFormatter: .init(currencySettings: .init()), paymentState: .acceptingCard, analytics: analytics) let onboardingViewModel = CardPresentPaymentsOnboardingViewModel(fixedState: .noConnectionError) @@ -320,9 +203,8 @@ final class TotalsViewModelTests: XCTestCase { // Given let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let sut = TotalsViewModel(orderService: orderService, + let sut = TotalsViewModel(posModel: posModel, cardPresentPaymentService: cardPresentPaymentService, - currencyFormatter: .init(currencySettings: .init()), paymentState: .acceptingCard, analytics: analytics) From 2113b1b9f4452042fc4942b0c8867faee5669b95 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 15 Nov 2024 10:00:44 +0000 Subject: [PATCH 35/49] Tidy up last page loaded logic --- .../Models/PointOfSaleAggregateModel.swift | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 893a2e9dbd4..2b9ac3cc714 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -30,7 +30,7 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt private let analytics: Analytics private var currentPage: Int = Constants.initialPage - private var pageIsOutOfRange: Bool = false + private var mightHaveMorePages: Bool = true init(itemProvider: POSItemProvider, analytics: Analytics = ServiceLocator.analytics) { @@ -43,7 +43,7 @@ class PointOfSaleAggregateModel: ObservableObject, PointOfSaleAggregateModelProt extension PointOfSaleAggregateModel { @MainActor func loadInitialItems() async { - pageIsOutOfRange = false + mightHaveMorePages = true itemListState = .initialLoading try? await load(pageNumber: Constants.initialPage) } @@ -51,22 +51,14 @@ extension PointOfSaleAggregateModel { @MainActor func loadNextItems() async { do { - guard !pageIsOutOfRange else { + guard mightHaveMorePages else { return } itemListState = .loading(allItems) let nextPage = currentPage + 1 try await load(pageNumber: nextPage) - pageIsOutOfRange = false currentPage = nextPage - } catch POSProductProviderError.pageOutOfRange { - if allItems.count == 0 { - itemListState = .empty - } else { - itemListState = .loaded(allItems) - } - pageIsOutOfRange = true } catch { // No need to do anything; this avoids us incorrectly incrementing currentPage. } @@ -76,7 +68,7 @@ extension PointOfSaleAggregateModel { func reload() async { allItems.removeAll() currentPage = Constants.initialPage - pageIsOutOfRange = false + mightHaveMorePages = true itemListState = .loading(allItems) try? await load(pageNumber: currentPage) } @@ -85,6 +77,13 @@ extension PointOfSaleAggregateModel { private func load(pageNumber: Int) async throws { do { try await fetchItems(pageNumber: pageNumber) + + mightHaveMorePages = true + updateItemListStateAfterLoadAttempt() + } catch POSProductProviderError.pageOutOfRange { + mightHaveMorePages = false + updateItemListStateAfterLoadAttempt() + throw POSProductProviderError.pageOutOfRange } catch { itemListState = .error(PointOfSaleErrorState.errorOnLoadingProducts()) throw error @@ -98,7 +97,9 @@ extension PointOfSaleAggregateModel { !allItems.contains(where: { $0.productID == newItem.productID }) } allItems.append(contentsOf: uniqueNewItems) + } + private func updateItemListStateAfterLoadAttempt() { if allItems.count == 0 { itemListState = .empty } else { From 07b649c6416cc89b1461263ccf2fd12695986123 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 15 Nov 2024 14:04:31 +0000 Subject: [PATCH 36/49] 14417 Move/update tests after moving order sync --- .../PointOfSaleAggregateModelTests.swift | 35 ++++++++++++------- .../PointOfSaleDashboardViewModelTests.swift | 4 ++- .../POS/ViewModels/TotalsViewModelTests.swift | 33 ++++++++++------- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 488ea69f942..c8b7c0adf9a 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -439,14 +439,19 @@ struct PointOfSaleAggregateModelTests { // Given cardPresentPaymentService.connectedReader = nil await sut.checkOut() + cardPresentPaymentService.collectPaymentWasCalled = false + + // When + // `await confirmation` callback only waits until this completes, not until some timeout. + // Since this is synchonous but triggers async combine behaviour, we can't use that approach. + cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) // Then - await confirmation() { confirmation in - cardPresentPaymentService.onCollectPaymentCalled = { - confirmation() - } - // When - cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) + let timeout = ContinuousClock.now + .seconds(1) + + while cardPresentPaymentService.collectPaymentWasCalled != true { + try! await Task.sleep(for: .milliseconds(1)) + try #require(.now < timeout) } } @@ -466,15 +471,21 @@ struct PointOfSaleAggregateModelTests { // Given cardPresentPaymentService.connectedReader = CardPresentPaymentCardReader(name: "Test", batteryLevel: 0.5) sut.observeReaderReconnection() + await sut.checkOut() await cardPresentPaymentService.disconnectReader() + cardPresentPaymentService.collectPaymentWasCalled = false + + // When + // `await confirmation` callback only waits until this completes, not until some timeout. + // Since this is synchonous but triggers async combine behaviour, we can't use that approach. + cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) // Then - await confirmation() { confirmation in - cardPresentPaymentService.onCollectPaymentCalled = { - confirmation() - } - // When - cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) + let timeout = ContinuousClock.now + .seconds(1) + + while cardPresentPaymentService.collectPaymentWasCalled != true { + try! await Task.sleep(for: .milliseconds(1)) + try #require(.now < timeout) } } diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift index c5b9a229b21..1ad6359841c 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift @@ -24,10 +24,12 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { mockTotalsViewModel = MockTotalsViewModel() mockItemListViewModel = MockItemListViewModel() mockConnectivityObserver = MockConnectivityObserver() + let mockOrderService = MockPOSOrderService() + mockOrderService.orderToReturn = Order.fake() mockPOSModel = PointOfSaleAggregateModel( itemProvider: itemProvider, cardPresentPaymentService: cardPresentPaymentService, - orderService: MockPOSOrderService()) + orderService: mockOrderService) sut = PointOfSaleDashboardViewModel(posModel: mockPOSModel, totalsViewModel: mockTotalsViewModel, cartViewModel: mockCartViewModel, diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift index 33ed2e04024..48155635188 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift @@ -66,7 +66,8 @@ final class TotalsViewModelTests: XCTestCase { cardPresentPaymentService.paymentEvent = .show(eventDetails: .preparingForPayment(cancelPayment: {})) let item = Self.makeItem() -// await sut.syncOrder(for: [CartItem(id: UUID(), item: item, quantity: 1)], allItems: [item]) + posModel.addToCart(item) + await posModel.checkOut() // Then XCTAssertTrue(sut.isShowingCardReaderStatus) @@ -102,7 +103,10 @@ final class TotalsViewModelTests: XCTestCase { func test_cardPresentPaymentInlineMessage_when_paymentSuccess_then_total_set() async { // Given orderService.orderToReturn = Order.fake().copy(currency: "$", total: "52.30") -// await sut.syncOrder(for: [], allItems: []) + posModel.addToCart(Self.makeItem()) + await posModel.checkOut() + // We can't actually mock this right now, but this would be the way: +// posModel.orderState = .loaded(.init(cartTotal: "", orderTotal: "$52.30", taxTotal: "")) // When cardPresentPaymentService.paymentEvent = .show(eventDetails: .paymentSuccess(done: { })) @@ -120,9 +124,9 @@ final class TotalsViewModelTests: XCTestCase { func test_paymentIntentCreationErrorMessage_when_paymentIntentCreationError() async { // Given struct TestError: Error {} - let item = Self.makeItem() orderService.orderToReturn = Order.fake() -// await sut.syncOrder(for: [CartItem(id: UUID(), item: item, quantity: 1)], allItems: [item]) + posModel.addToCart(Self.makeItem()) + await posModel.checkOut() var editOrderCalled = false sut.editOrderActionPublisher.sink { _ in @@ -147,19 +151,22 @@ final class TotalsViewModelTests: XCTestCase { } // Try again action emits payment cancelation and collection + let shouldCollectPayment = XCTestExpectation(description: "Collect payment should be called after retrying payment") + cardPresentPaymentService.onCollectPaymentCalled = { + shouldCollectPayment.fulfill() + } + XCTAssertFalse(cardPresentPaymentService.cancelPaymentCalled) tryAgainAction?() XCTAssertTrue(cardPresentPaymentService.cancelPaymentCalled) - let expectation = XCTestExpectation(description: "Collect payment should be called after retrying payment") - cardPresentPaymentService.onCollectPaymentCalled = { - expectation.fulfill() - } - await fulfillment(of: [expectation], timeout: 1) - // Edit order action emits edit order event - XCTAssertFalse(editOrderCalled) - editOrderAction?() - XCTAssertTrue(editOrderCalled) + await fulfillment(of: [shouldCollectPayment], timeout: 3) + + // Edit order action calls addMoreToCart + // we can't test this until we can properly mock posModel... but by then this behaviour may have moved. +// XCTAssertFalse(posModel.addMoreToCartWasCalled) +// editOrderAction?() +// XCTAssertTrue(posModel.addMoreToCartWasCalled) } // MARK: Onboarding From 58ad3e777ba8b448145537eea591226242a8dbde Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 15 Nov 2024 14:20:58 +0000 Subject: [PATCH 37/49] 14417 Update edit order action to use posModel --- .../POS/ViewModels/PointOfSaleDashboardViewModel.swift | 10 ---------- .../Classes/POS/ViewModels/TotalsViewModel.swift | 9 ++------- .../POS/ViewModels/TotalsViewModelProtocol.swift | 1 - 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift b/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift index 0af99c85bd6..bd9ce8b0ef8 100644 --- a/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift @@ -39,7 +39,6 @@ final class PointOfSaleDashboardViewModel: ObservableObject { observeSelectedItemToAddToCart() observePaymentStateForButtonDisabledProperties() - observeTotalsOrderActions() observeConnectivity() } } @@ -110,15 +109,6 @@ private extension PointOfSaleDashboardViewModel { .assign(to: &$isReaderDisconnectionDisabled) } - - func observeTotalsOrderActions() { - totalsViewModel.editOrderActionPublisher - .sink { [weak self] in - guard let self else { return } - posModel.addMoreToCart() - } - .store(in: &cancellables) - } } private extension PointOfSaleDashboardViewModel { diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift index 74d3d39f1cd..3d8844c455c 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift @@ -28,11 +28,6 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { @ObservedObject var posModel: PointOfSaleAggregateModel - private let editOrderActionSubject = PassthroughSubject() - var editOrderActionPublisher: AnyPublisher { - editOrderActionSubject.eraseToAnyPublisher() - } - var isShimmering: Bool { posModel.orderState.isSyncing } @@ -96,7 +91,7 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { private func editOrder() { paymentState = .idle cardPresentPaymentInlineMessage = nil - editOrderActionSubject.send(()) + posModel.addMoreToCart() } // These three functions could potentially move to posModel and be based on orderStage. @@ -238,7 +233,7 @@ private extension TotalsViewModel { self?.posModel.startNewCart() }, paymentIntentCreationErrorEditOrderAction: { [weak self] in - self?.posModel.addMoreToCart() + self?.editOrder() }, dismissReaderConnectionModal: { [weak self] in self?.cardPresentPaymentAlertViewModel = nil diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift index 040e50ce7f2..9b701a79a39 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift @@ -7,7 +7,6 @@ protocol TotalsViewModelProtocol { var connectionStatus: CardPresentPaymentReaderConnectionStatus { get } var paymentStatePublisher: Published.Publisher { get } - var editOrderActionPublisher: AnyPublisher { get } var cardPresentPaymentInlineMessage: PointOfSaleCardPresentPaymentMessageType? { get } From 928b41610305e09627a1350e77939bdaeeec7755 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 15 Nov 2024 14:51:12 +0000 Subject: [PATCH 38/49] 14417 fix tests --- .../POS/ViewModels/TotalsViewModelTests.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift index 48155635188..56f2d072b2f 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/TotalsViewModelTests.swift @@ -128,12 +128,6 @@ final class TotalsViewModelTests: XCTestCase { posModel.addToCart(Self.makeItem()) await posModel.checkOut() - var editOrderCalled = false - sut.editOrderActionPublisher.sink { _ in - editOrderCalled = true - } - .store(in: &cancellables) - // When paymentIntentCreationError event is received cardPresentPaymentService.paymentEvent = .show( eventDetails: .paymentIntentCreationError(error: TestError(), cancelPayment: {}) From 3f39424d4203e0904dfb8ab01eeb0a2bba880f70 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 15 Nov 2024 15:27:26 +0000 Subject: [PATCH 39/49] 14417 update tests to match intent --- .../PointOfSaleDashboardViewModelTests.swift | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift index 1ad6359841c..36b5adf28e0 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/PointOfSaleDashboardViewModelTests.swift @@ -61,18 +61,18 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { func test_isAddMoreDisabled_is_true_when_order_is_syncing_and_paymentState_is_idle() async { // Given + mockPOSModel.addToCart(Self.makeItem()) let expectation = XCTestExpectation(description: "Expect isAddMoreDisabled to be true while syncing order and payment state is idle") await sut.$isAddMoreDisabled - .dropFirst() - .sink { value in - XCTAssertTrue(value) - expectation.fulfill() + .sink { disabled in + if disabled { + expectation.fulfill() + } } .store(in: &cancellables) // When - mockPOSModel.addToCart(Self.makeItem()) await mockPOSModel.checkOut() await fulfillment(of: [expectation], timeout: 1.0) @@ -83,10 +83,10 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { let expectation = XCTestExpectation(description: "Expect isAddMoreDisabled to be true after successfully collecting payment") sut.$isAddMoreDisabled - .dropFirst() - .sink { value in - XCTAssertTrue(value) - expectation.fulfill() + .sink { disabled in + if disabled { + expectation.fulfill() + } } .store(in: &cancellables) @@ -101,10 +101,10 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { let expectation = XCTestExpectation(description: "Expect isAddMoreDisabled to be true when paymentState is processingPayment or cardPaymentSuccessful") sut.$isAddMoreDisabled - .dropFirst() - .sink { value in - XCTAssertTrue(value) - expectation.fulfill() + .sink { disabled in + if disabled { + expectation.fulfill() + } } .store(in: &cancellables) @@ -119,10 +119,10 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { let expectation = XCTestExpectation(description: "Expect isExitPOSDisabled to be true when paymentState is processingPayment") sut.$isExitPOSDisabled - .dropFirst() - .sink { value in - XCTAssertTrue(value) - expectation.fulfill() + .sink { disabled in + if disabled { + expectation.fulfill() + } } .store(in: &cancellables) @@ -137,9 +137,10 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { let expectation = XCTestExpectation(description: "Expect isExitPOSDisabled to be false when paymentState is idle") sut.$isExitPOSDisabled - .sink { value in - XCTAssertFalse(value) - expectation.fulfill() + .sink { disabled in + if !disabled { + expectation.fulfill() + } } .store(in: &cancellables) @@ -155,9 +156,10 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { sut.$isTotalsViewFullScreen .dropFirst() - .sink { value in - XCTAssertTrue(value) - expectation.fulfill() + .sink { fullscreen in + if fullscreen { + expectation.fulfill() + } } .store(in: &cancellables) @@ -172,9 +174,10 @@ final class PointOfSaleDashboardViewModelTests: XCTestCase { let expectation = XCTestExpectation(description: "Expect isTotalsViewFullScreen to be false when paymentState is idle") sut.$isTotalsViewFullScreen - .sink { value in - XCTAssertFalse(value) - expectation.fulfill() + .sink { fullscreen in + if !fullscreen { + expectation.fulfill() + } } .store(in: &cancellables) From 64cb7723efc35b5a6964a85ac998752121584986 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:05:21 +0200 Subject: [PATCH 40/49] Fix typo in tests --- .../ViewModels/Receipts/ReceiptEmailViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEmailViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEmailViewModelTests.swift index a81d890c5d9..04fa3277b46 100644 --- a/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEmailViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEmailViewModelTests.swift @@ -22,7 +22,7 @@ struct ReceiptEmailViewModelTests { } @Test func sendReceipt_when_action_succeeds() async { - // Given send receipt action suceeds + // Given send receipt action succeeds sut.email = "test@test.com" stores.whenReceivingAction(ofType: ReceiptAction.self) { action in switch action { From 7fb7a886ab7eda2d8592ec6327073f2f727aff71 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:49:23 +0200 Subject: [PATCH 41/49] Update naming and structure for receipt action enum cases --- ...entPaymentsTransactionAlertsProvider.swift | 6 +- ...toothCardReaderPaymentAlertsProvider.swift | 20 +++---- ...iltInCardReaderPaymentAlertsProvider.swift | 20 +++---- ...erTransactionAlertEmailReceiptAction.swift | 16 ------ ...rdReaderTransactionAlertReceiptState.swift | 28 ++++++++++ ...CardReaderTransactionAlertsProviding.swift | 4 +- .../CollectOrderPaymentUseCase.swift | 55 +++++++++++-------- .../WooCommerce.xcodeproj/project.pbxproj | 8 +-- 8 files changed, 84 insertions(+), 73 deletions(-) delete mode 100644 WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift create mode 100644 WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertReceiptState.swift diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsTransactionAlertsProvider.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsTransactionAlertsProvider.swift index 464991f278d..058cb9e5c46 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsTransactionAlertsProvider.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsTransactionAlertsProvider.swift @@ -28,10 +28,8 @@ struct CardPresentPaymentsTransactionAlertsProvider: CardReaderTransactionAlerts .processing } - func success(printReceipt: @escaping () -> Void, - emailReceipt: CardReaderTransactionAlertEmailReceiptAction, - noReceiptAction: @escaping () -> Void) -> CardPresentPaymentEventDetails { - .paymentSuccess(done: noReceiptAction) + func success(receiptState: CardReaderTransactionAlertReceiptState) -> CardPresentPaymentEventDetails { + .paymentSuccess(done: receiptState.noReceiptAction) } func error(error: any Error, diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift index e8a39b52e0f..80e1ea73c59 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift @@ -46,20 +46,18 @@ final class BluetoothCardReaderPaymentAlertsProvider: CardReaderTransactionAlert return CardPresentModalProcessing(name: name, amount: amount, transactionType: transactionType) } - func success(printReceipt: @escaping () -> Void, - emailReceipt: CardReaderTransactionAlertEmailReceiptAction, - noReceiptAction: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { - switch emailReceipt { - case let .emailSent(email): - return CardPresentModalSuccessEmailSent(printReceipt: printReceipt, + func success(receiptState: CardReaderTransactionAlertReceiptState) -> CardPresentPaymentsModalViewModel { + switch receiptState { + case let .paymentSuccessEmailSent(email, printReceiptAction, noReceiptAction): + return CardPresentModalSuccessEmailSent(printReceipt: printReceiptAction, noReceiptAction: noReceiptAction, email: email) - case let .sendEmail(emailReceipt): - return CardPresentModalSuccess(printReceipt: printReceipt, - emailReceipt: emailReceipt, + case let .promptToSendEmailReceipt(printReceiptAction, emailReceiptAction, noReceiptAction): + return CardPresentModalSuccess(printReceipt: printReceiptAction, + emailReceipt: emailReceiptAction, noReceiptAction: noReceiptAction) - case .noEmail: - return CardPresentModalSuccessWithoutEmail(printReceipt: printReceipt, noReceiptAction: noReceiptAction) + case let .emailSendingNotSupported(printReceiptAction, noReceiptAction): + return CardPresentModalSuccessWithoutEmail(printReceipt: printReceiptAction, noReceiptAction: noReceiptAction) } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift index 9e39473511e..7e11c937b3e 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift @@ -41,20 +41,18 @@ final class BuiltInCardReaderPaymentAlertsProvider: CardReaderTransactionAlertsP return CardPresentModalBuiltInReaderProcessing(name: name, amount: amount) } - func success(printReceipt: @escaping () -> Void, - emailReceipt: CardReaderTransactionAlertEmailReceiptAction, - noReceiptAction: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { - switch emailReceipt { - case let .emailSent(email): - return CardPresentModalSuccessEmailSent(printReceipt: printReceipt, + func success(receiptState: CardReaderTransactionAlertReceiptState) -> CardPresentPaymentsModalViewModel { + switch receiptState { + case let .paymentSuccessEmailSent(email, printReceiptAction, noReceiptAction): + return CardPresentModalSuccessEmailSent(printReceipt: printReceiptAction, noReceiptAction: noReceiptAction, email: email) - case let .sendEmail(emailReceipt): - return CardPresentModalSuccess(printReceipt: printReceipt, - emailReceipt: emailReceipt, + case let .promptToSendEmailReceipt(printReceiptAction, emailReceiptAction, noReceiptAction): + return CardPresentModalSuccess(printReceipt: printReceiptAction, + emailReceipt: emailReceiptAction, noReceiptAction: noReceiptAction) - case .noEmail: - return CardPresentModalSuccessWithoutEmail(printReceipt: printReceipt, noReceiptAction: noReceiptAction) + case let .emailSendingNotSupported(printReceiptAction, noReceiptAction): + return CardPresentModalSuccessWithoutEmail(printReceipt: printReceiptAction, noReceiptAction: noReceiptAction) } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift deleted file mode 100644 index 2a6532ab1d5..00000000000 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertEmailReceiptAction.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import MessageUI - -enum CardReaderTransactionAlertEmailReceiptAction { - case emailSent(String) - case sendEmail(() -> Void) - case noEmail - - init(callback: @escaping () -> Void) { - if MFMailComposeViewController.canSendMail() { - self = .sendEmail(callback) - } else { - self = .noEmail - } - } -} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertReceiptState.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertReceiptState.swift new file mode 100644 index 00000000000..d240b045018 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertReceiptState.swift @@ -0,0 +1,28 @@ +import Foundation +import MessageUI + +enum CardReaderTransactionAlertReceiptState { + case paymentSuccessEmailSent(email: String, printReceiptAction: () -> Void, noReceiptAction: () -> Void) + case promptToSendEmailReceipt(printReceiptAction: () -> Void, emailReceiptAction: () -> Void, noReceiptAction: () -> Void) + case emailSendingNotSupported(printReceiptAction: () -> Void, noReceiptAction: () -> Void) + + init(printReceipt: @escaping () -> Void, + emailReceipt: @escaping () -> Void, + noReceiptAction: @escaping () -> Void + ) { + if MFMailComposeViewController.canSendMail() { + self = .promptToSendEmailReceipt(printReceiptAction: printReceipt, emailReceiptAction: emailReceipt, noReceiptAction: noReceiptAction) + } else { + self = .emailSendingNotSupported(printReceiptAction: printReceipt, noReceiptAction: noReceiptAction) + } + } + + var noReceiptAction: () -> Void { + switch self { + case .paymentSuccessEmailSent(_, _, let noReceiptAction), + .promptToSendEmailReceipt(_, _, let noReceiptAction), + .emailSendingNotSupported(_, let noReceiptAction): + return noReceiptAction + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift index c66a2de680d..d9299c8b2c3 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift @@ -32,9 +32,7 @@ protocol CardReaderTransactionAlertsProviding { /// An alert to display successful transaction and provide options related to receipts /// - func success(printReceipt: @escaping () -> Void, - emailReceipt: CardReaderTransactionAlertEmailReceiptAction, - noReceiptAction: @escaping () -> Void) -> AlertDetails + func success(receiptState: CardReaderTransactionAlertReceiptState) -> AlertDetails /// An alert to display a retriable and cancellable error /// diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index 564b4393a3c..43f45c7f607 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -552,42 +552,49 @@ private extension CollectOrderPaymentUseCase { func presentBackendReceiptAlert( alertProvider paymentAlerts: any CardReaderTransactionAlertsProviding, onCompleted: @escaping () -> ()) { - // Handles receipt presentation for both print and email actions - let receiptPresentationCompletionAction: () -> Void = { [weak self] in - guard let self else { return } - self.paymentOrchestrator.presentBackendReceipt(for: self.order, onCompletion: { [weak self] result in + // Handles receipt presentation for both print and email actions + let receiptPresentationCompletionAction: () -> Void = { [weak self] in guard let self else { return } - switch result { - case let .success(receipt): - self.presentBackendReceiptModally(receipt: receipt, onCompleted: onCompleted) - case let .failure(error): - self.presentReceiptFailedNotice(with: error, onCompleted: onCompleted) - } - }) - } + self.paymentOrchestrator.presentBackendReceipt(for: self.order, onCompletion: { [weak self] result in + guard let self else { return } + switch result { + case let .success(receipt): + self.presentBackendReceiptModally(receipt: receipt, onCompleted: onCompleted) + case let .failure(error): + self.presentReceiptFailedNotice(with: error, onCompleted: onCompleted) + } + }) + } // Sends receipt via API let addCustomerEmailAndSendReceiptCompletionAction: () -> Void = { [weak self] in self?.presentSendReceiptAfterPayment(onCompleted: onCompleted) } + let noReceiptAction: () -> Void = { onCompleted() } + // Presents receipt alert receiptEligibilityUseCase.isEligibleSendingReceiptAfterPayment { isEligibleSendingReceiptAfterPayment in - let emailReceiptAction: CardReaderTransactionAlertEmailReceiptAction + let receiptState: CardReaderTransactionAlertReceiptState if let email = self.order.billingAddress?.email, email.isNotEmpty { - emailReceiptAction = .emailSent(email) + receiptState = .paymentSuccessEmailSent(email: email, + printReceiptAction: receiptPresentationCompletionAction, + noReceiptAction: noReceiptAction) } else if isEligibleSendingReceiptAfterPayment { - emailReceiptAction = .sendEmail(addCustomerEmailAndSendReceiptCompletionAction) + receiptState = .promptToSendEmailReceipt(printReceiptAction: receiptPresentationCompletionAction, + emailReceiptAction: addCustomerEmailAndSendReceiptCompletionAction, + noReceiptAction: noReceiptAction) } else if MFMailComposeViewController.canSendMail() { - emailReceiptAction = .sendEmail(receiptPresentationCompletionAction) + receiptState = .promptToSendEmailReceipt(printReceiptAction: receiptPresentationCompletionAction, + emailReceiptAction: receiptPresentationCompletionAction, + noReceiptAction: noReceiptAction) } else { - emailReceiptAction = .noEmail + receiptState = .emailSendingNotSupported(printReceiptAction: receiptPresentationCompletionAction, + noReceiptAction: noReceiptAction) } - self.alertsPresenter.present(viewModel: paymentAlerts.success(printReceipt: receiptPresentationCompletionAction, - emailReceipt: emailReceiptAction, - noReceiptAction: { onCompleted() })) + self.alertsPresenter.present(viewModel: paymentAlerts.success(receiptState: receiptState)) } - } + } /// Allow merchants to print or email locally-generated receipts. /// @@ -595,7 +602,7 @@ private extension CollectOrderPaymentUseCase { alertProvider paymentAlerts: any CardReaderTransactionAlertsProviding, onCompleted: @escaping () -> ()) { // Present receipt alert - alertsPresenter.present(viewModel: paymentAlerts.success(printReceipt: { [order, configuration, weak self] in + alertsPresenter.present(viewModel: paymentAlerts.success(receiptState: .init(printReceipt: { [order, configuration, weak self] in guard let self = self else { return } guard let receiptParameters else { @@ -615,7 +622,7 @@ private extension CollectOrderPaymentUseCase { // Inform about flow completion. onCompleted() } - }, emailReceipt: .init { [order, analyticsTracker, paymentOrchestrator, weak self] in + }, emailReceipt: { [order, analyticsTracker, paymentOrchestrator, weak self] in guard let self = self else { return } analyticsTracker.trackEmailTapped() @@ -633,7 +640,7 @@ private extension CollectOrderPaymentUseCase { }, noReceiptAction: { // Inform about flow completion. onCompleted() - })) + }))) } /// Presents the native email client with the provided content. diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 647143a4b43..8a691bfa891 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -35,7 +35,7 @@ 0174DDBB2CE5FD60005D20CA /* ReceiptEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */; }; 0174DDBF2CE600C5005D20CA /* ReceiptEmailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0174DDBE2CE600C0005D20CA /* ReceiptEmailViewModelTests.swift */; }; 0182C8BE2CE3B11300474355 /* MockReceiptEligibilityUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182C8BD2CE3B10E00474355 /* MockReceiptEligibilityUseCase.swift */; }; - 0182C8C02CE4DDC700474355 /* CardReaderTransactionAlertEmailReceiptAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertEmailReceiptAction.swift */; }; + 0182C8C02CE4DDC700474355 /* CardReaderTransactionAlertReceiptState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertReceiptState.swift */; }; 0182C8C22CE4F0DB00474355 /* ReceiptEmailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0182C8C12CE4F0DB00474355 /* ReceiptEmailView.swift */; }; 0188CA0F2C65622A0051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0188CA0E2C65622A0051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift */; }; 0188CA112C6565320051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0188CA102C6565320051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift */; }; @@ -3153,7 +3153,7 @@ 0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModel.swift; sourceTree = ""; }; 0174DDBE2CE600C0005D20CA /* ReceiptEmailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModelTests.swift; sourceTree = ""; }; 0182C8BD2CE3B10E00474355 /* MockReceiptEligibilityUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockReceiptEligibilityUseCase.swift; sourceTree = ""; }; - 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertEmailReceiptAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderTransactionAlertEmailReceiptAction.swift; sourceTree = ""; }; + 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertReceiptState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderTransactionAlertReceiptState.swift; sourceTree = ""; }; 0182C8C12CE4F0DB00474355 /* ReceiptEmailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailView.swift; sourceTree = ""; }; 0188CA0E2C65622A0051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentValidatingOrderErrorMessageViewModel.swift; sourceTree = ""; }; 0188CA102C6565320051BF1C /* PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentValidatingOrderErrorMessageView.swift; sourceTree = ""; }; @@ -8544,7 +8544,7 @@ 311D21EC264AF0E700102316 /* CardReaderSettingsAlerts.swift */, 03E471BF293A158C001A58AD /* CardReaderConnectionAlertsProviding.swift */, 03E471CF293FA62B001A58AD /* CardReaderTransactionAlertsProviding.swift */, - 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertEmailReceiptAction.swift */, + 0182C8BF2CE4DDC100474355 /* CardReaderTransactionAlertReceiptState.swift */, 03E471D1293FA8B2001A58AD /* BluetoothCardReaderPaymentAlertsProvider.swift */, 03E471C1293A1F6B001A58AD /* BluetoothReaderConnectionAlertsProvider.swift */, 03E471C3293A1F8D001A58AD /* BuiltInReaderConnectionAlertsProvider.swift */, @@ -16282,7 +16282,7 @@ DE19BB1A26C3B5DC00AB70D9 /* ShippingLabelCustomsFormItemDetailsViewModel.swift in Sources */, 2023E2AE2C21D8EA00FC365A /* PointOfSaleCardPresentPaymentInLineMessage.swift in Sources */, B6F3796C293794A000718561 /* AnalyticsHubYearToDateRangeData.swift in Sources */, - 0182C8C02CE4DDC700474355 /* CardReaderTransactionAlertEmailReceiptAction.swift in Sources */, + 0182C8C02CE4DDC700474355 /* CardReaderTransactionAlertReceiptState.swift in Sources */, 2667BFE52530DCF4008099D4 /* RefundItemsValuesCalculationUseCase.swift in Sources */, 203163BB2C1C5F72001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedUpdatePostalCodeView.swift in Sources */, CEC3CC6B2C92FDB700B93FBE /* WooShippingItemRowViewModel.swift in Sources */, From 333e8b1170ad36e67944a5a6f1cc41efaa939ca2 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 18 Nov 2024 10:38:58 +0000 Subject: [PATCH 42/49] Remove unnecessary forced `try!` in throwing tests Co-authored-by: Gabriel Maldonado --- .../POS/Models/PointOfSaleAggregateModelTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 58a2dc53044..435b34965d1 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -483,7 +483,7 @@ struct PointOfSaleAggregateModelTests { let timeout = ContinuousClock.now + .seconds(1) while cardPresentPaymentService.collectPaymentWasCalled != true { - try! await Task.sleep(for: .milliseconds(1)) + try await Task.sleep(for: .milliseconds(1)) try #require(.now < timeout) } } @@ -517,7 +517,7 @@ struct PointOfSaleAggregateModelTests { let timeout = ContinuousClock.now + .seconds(1) while cardPresentPaymentService.collectPaymentWasCalled != true { - try! await Task.sleep(for: .milliseconds(1)) + try await Task.sleep(for: .milliseconds(1)) try #require(.now < timeout) } } From 6b8e4e527d2271552e2b6c21154cff4caa1ed886 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:48:56 +0200 Subject: [PATCH 43/49] Add a success state to PrimaryLoadingButtonStyle --- .../SwiftUI Components/ButtonStyles.swift | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift index f304d9ac394..d8e4de5cde4 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift @@ -133,34 +133,57 @@ struct RoundedBorderedStyle: ButtonStyle { } } -/// Adds a primary button style while showing a progress view on top of the button when required. +/// Adds a primary button style while showing a progress view or checkmark on top of the button when required. /// struct PrimaryLoadingButtonStyle: PrimitiveButtonStyle { - - /// Set it to true to show a progress view within the button. + /// Set to show a progress view or checkmark within the button. /// - let isLoading: Bool + enum State { + case loading + case success + case idle + } + + var state: State = .idle + + init(isLoading: Bool) { + if isLoading { + state = .loading + } else { + state = .idle + } + } + + init(state: State) { + self.state = state + } /// Returns a `ProgressView` if the view is loading. Return nil otherwise /// private var progressViewOverlay: ProgressView? { - isLoading ? ProgressView() : nil + state == .loading ? ProgressView() : nil + } + + private var checkmark: some View { + state == .success ? Image(systemName: "checkmark.circle").font(.title2).foregroundStyle(Color(.primaryButtonBackground)) : nil } func makeBody(configuration: Configuration) -> some View { /// Only send trigger if the view is not loading. /// return Button(configuration) - .buttonStyle(PrimaryButtonStyle(hideContent: isLoading)) + .buttonStyle(PrimaryButtonStyle(hideContent: state != .idle)) .onTapGesture { dispatchTrigger(configuration) } - .disabled(isLoading) + .disabled(state != .idle) .overlay(progressViewOverlay) + .overlay(checkmark) + .animation(.default, value: state) } /// Only dispatch events while the view is not loading. /// private func dispatchTrigger(_ configuration: Configuration) { - guard !isLoading else { return } + guard state == .idle else { return } configuration.trigger() } } From 0ed160d25b5b20585aeb20582f4ca3626e0b214f Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:49:21 +0200 Subject: [PATCH 44/49] Show a success state on ReceiptEmailView before dismissing --- .../Receipts/ReceiptEmail/ReceiptEmailView.swift | 2 +- .../Receipts/ReceiptEmail/ReceiptEmailViewModel.swift | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailView.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailView.swift index d17765b750e..ee24cd82ea1 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailView.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailView.swift @@ -28,7 +28,7 @@ struct ReceiptEmailView: View { viewModel.sendReceipt() } .disabled(!viewModel.isEmailValid) - .buttonStyle(PrimaryLoadingButtonStyle(isLoading: viewModel.isLoading)) + .buttonStyle(PrimaryLoadingButtonStyle(state: viewModel.state)) .padding() } .padding(.top) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailViewModel.swift index e15b838ae86..6846dd47624 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Receipts/ReceiptEmail/ReceiptEmailViewModel.swift @@ -4,7 +4,7 @@ import Yosemite final class ReceiptEmailViewModel: ObservableObject { @Published var email: String = "" - @Published var isLoading: Bool = false + @Published var state: PrimaryLoadingButtonStyle.State = .idle private let order: Order private let stores: StoresManager @@ -31,10 +31,13 @@ final class ReceiptEmailViewModel: ObservableObject { let action = ReceiptAction.sendReceipt(order: order, email: email) { [weak self] result in DispatchQueue.main.async { guard let self else { return } - self.isLoading = false + self.state = .idle switch result { case .success: - self.onDismiss(true) + self.state = .success + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + self.onDismiss(true) + } case let .failure(error): DDLogError("Sending email receipt failed: \(error.localizedDescription)") self.noticePresenter.enqueue(notice: Notice(title: Localization.errorNotice, feedbackType: .error)) @@ -42,7 +45,7 @@ final class ReceiptEmailViewModel: ObservableObject { } } - self.isLoading = true + self.state = .loading stores.dispatch(action) } } From dd4f2f0ffa1695a99e9cc62359a90b7d2b7f31bc Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:56:22 +0200 Subject: [PATCH 45/49] Add a feature flag for sending receipts after payment --- Experiments/Experiments/DefaultFeatureFlagService.swift | 2 ++ Experiments/Experiments/FeatureFlag.swift | 3 +++ .../Order Details/Receipts/ReceiptEligibilityUseCase.swift | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index 7407ba81331..3a18b33a388 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -91,6 +91,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return buildConfig == .localDeveloper || buildConfig == .alpha case .paymentsOnboardingInPointOfSale: return buildConfig == .localDeveloper + case .sendReceiptAfterPayment: + return false default: return true } diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index 7d34cd02836..15e33a3ef26 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -196,4 +196,7 @@ public enum FeatureFlag: Int { /// Supports Woo Payments onboarding in POS so that merchants who have not completed onboarding can access POS. /// case paymentsOnboardingInPointOfSale + + /// Enables the new Blaze campaign creation flow + case sendReceiptAfterPayment } diff --git a/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift b/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift index 9462cc8b7d9..e6aa2d0c946 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift @@ -46,7 +46,7 @@ final class ReceiptEligibilityUseCase: ReceiptEligibilityUseCaseProtocol { func isEligibleSendingReceiptAfterPayment(onCompletion: @escaping (Bool) -> Void) { // TODO: WooCommerce 9.5.0 - onCompletion(false) + onCompletion(featureFlagService.isFeatureFlagEnabled(.sendReceiptAfterPayment)) } } From 58fff1ba2b2692feebfff514892e2a9885c7b37e Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:51:47 +0200 Subject: [PATCH 46/49] Update FeatureFlag.swift --- Experiments/Experiments/FeatureFlag.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index 15e33a3ef26..818baa8aba3 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -197,6 +197,6 @@ public enum FeatureFlag: Int { /// case paymentsOnboardingInPointOfSale - /// Enables the new Blaze campaign creation flow + /// Enables sending receipt after the payment via the API case sendReceiptAfterPayment } From 0322fe4f25521b18197752d299818a4b6bb7a5b5 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 18 Nov 2024 20:58:16 +0200 Subject: [PATCH 47/49] Update ButtonStyles.swift --- .../ReusableViews/SwiftUI Components/ButtonStyles.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift index d8e4de5cde4..bf8685e1b1a 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift @@ -177,7 +177,6 @@ struct PrimaryLoadingButtonStyle: PrimitiveButtonStyle { .disabled(state != .idle) .overlay(progressViewOverlay) .overlay(checkmark) - .animation(.default, value: state) } /// Only dispatch events while the view is not loading. From 37d46d7db3a31a92b30cc9e1098ca027ac07b77e Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:27:31 +0200 Subject: [PATCH 48/49] Implement eligibility checker for sending receipt after payment Sending receipt after payment depends on two plugins: - WooCommerce from 9.5 supports API calls to actions/send_order_details to trigger sending order details email - WooPayments from 8.6 supports sending email to attached customer when order payment fails --- .../Receipts/ReceiptEligibilityUseCase.swift | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift b/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift index e6aa2d0c946..b01744cf770 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/Receipts/ReceiptEligibilityUseCase.swift @@ -32,12 +32,12 @@ final class ReceiptEligibilityUseCase: ReceiptEligibilityUseCaseProtocol { return onCompletion(false) } // 2. If WooCommerce version is any of the specific API development branches, mark as eligible - if Constants.wcPluginDevVersion.contains(wcPlugin.version) { + if Constants.BackendReceipt.wcPluginDevVersion.contains(wcPlugin.version) { onCompletion(true) } else { // 3. Else, if WooCommerce version is higher than minimum required version, mark as eligible let isSupported = VersionHelpers.isVersionSupported(version: wcPlugin.version, - minimumRequired: Constants.wcPluginMinimumVersion) + minimumRequired: Constants.BackendReceipt.wcPluginMinimumVersion) onCompletion(isSupported) } } @@ -45,15 +45,63 @@ final class ReceiptEligibilityUseCase: ReceiptEligibilityUseCaseProtocol { } func isEligibleSendingReceiptAfterPayment(onCompletion: @escaping (Bool) -> Void) { - // TODO: WooCommerce 9.5.0 - onCompletion(featureFlagService.isFeatureFlagEnabled(.sendReceiptAfterPayment)) + guard featureFlagService.isFeatureFlagEnabled(.sendReceiptAfterPayment) else { + return onCompletion(false) + } + + Task { @MainActor in + async let isWooCommerceSupported = isPluginSupported(Constants.wcPluginName, + minimumVersion: Constants.ReceiptAfterPayment.wcPluginMinimumVersion) + async let isWooPaymentsSupported = isPluginSupported(Constants.wcPayPluginName, + minimumVersion: Constants.ReceiptAfterPayment.wcPayPluginMinimumVersion) + let wooCommerceResult = await isWooCommerceSupported + let wooPaymentsResult = await isWooPaymentsSupported + let isSupported = wooCommerceResult && wooPaymentsResult + + onCompletion(isSupported) + } + } +} + +private extension ReceiptEligibilityUseCase { + @MainActor + func isPluginSupported(_ pluginName: String, minimumVersion: String) async -> Bool { + await withCheckedContinuation { continuation in + let action = SystemStatusAction.fetchSystemPlugin(siteID: siteID, systemPluginName: pluginName) { plugin in + // Plugin must be installed and active + guard let plugin, plugin.active else { + return continuation.resume(returning: false) + } + + // Checking for concrete versions to cover dev and beta versions + if plugin.version.contains(minimumVersion) { + return continuation.resume(returning: true) + } + + // If plugin version is higher than minimum required version, mark as eligible + let isSupported = VersionHelpers.isVersionSupported(version: plugin.version, + minimumRequired: minimumVersion) + continuation.resume(returning: isSupported) + } + stores.dispatch(action) + } } } private extension ReceiptEligibilityUseCase { enum Constants { static let wcPluginName = "WooCommerce" - static let wcPluginMinimumVersion = "8.7.0" - static let wcPluginDevVersion: [String] = ["8.7.0-dev", "8.6.0-dev"] + static let wcPayPluginName = "WooPayments" + + enum BackendReceipt { + static let wcPluginMinimumVersion = "8.7.0" + static let wcPluginDevVersion: [String] = ["8.7.0-dev", "8.6.0-dev"] + } + + enum ReceiptAfterPayment { + static let wcPluginMinimumVersion = "9.5.0" + static let wcPayPluginMinimumVersion = "8.6.0" + } + } } From 01c94d9e1ce6d6f0e2db8dbf32df0566cdc2af9c Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:28:16 +0200 Subject: [PATCH 49/49] Added tests for sendingReceiptAfterPayment eligibility --- .../Mocks/MockFeatureFlagService.swift | 7 +- .../ReceiptEligibilityUseCaseTests.swift | 151 ++++++++++++++++++ 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift index eee5cfe2f90..86c0fe0e997 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift @@ -24,6 +24,7 @@ struct MockFeatureFlagService: FeatureFlagService { private let favoriteProducts: Bool private let paymentsOnboardingInPointOfSale: Bool private let isProductGlobalUniqueIdentifierSupported: Bool + private let isSendReceiptAfterPaymentEnabled: Bool init(isInboxOn: Bool = false, isShowInboxCTAEnabled: Bool = false, @@ -46,7 +47,8 @@ struct MockFeatureFlagService: FeatureFlagService { viewEditCustomFieldsInProductsAndOrders: Bool = false, favoriteProducts: Bool = false, paymentsOnboardingInPointOfSale: Bool = false, - isProductGlobalUniqueIdentifierSupported: Bool = false) { + isProductGlobalUniqueIdentifierSupported: Bool = false, + isSendReceiptAfterPaymentEnabled: Bool = false) { self.isInboxOn = isInboxOn self.isShowInboxCTAEnabled = isShowInboxCTAEnabled self.isUpdateOrderOptimisticallyOn = isUpdateOrderOptimisticallyOn @@ -69,6 +71,7 @@ struct MockFeatureFlagService: FeatureFlagService { self.favoriteProducts = favoriteProducts self.paymentsOnboardingInPointOfSale = paymentsOnboardingInPointOfSale self.isProductGlobalUniqueIdentifierSupported = isProductGlobalUniqueIdentifierSupported + self.isSendReceiptAfterPaymentEnabled = isSendReceiptAfterPaymentEnabled } func isFeatureFlagEnabled(_ featureFlag: FeatureFlag) -> Bool { @@ -117,6 +120,8 @@ struct MockFeatureFlagService: FeatureFlagService { return paymentsOnboardingInPointOfSale case .productGlobalUniqueIdentifierSupport: return isProductGlobalUniqueIdentifierSupported + case .sendReceiptAfterPayment: + return isSendReceiptAfterPaymentEnabled default: return false } diff --git a/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEligibilityUseCaseTests.swift b/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEligibilityUseCaseTests.swift index 9b3bb0fa299..7afe3922e25 100644 --- a/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEligibilityUseCaseTests.swift +++ b/WooCommerce/WooCommerceTests/ViewModels/Receipts/ReceiptEligibilityUseCaseTests.swift @@ -136,4 +136,155 @@ final class ReceiptEligibilityUseCaseTests: XCTestCase { // Then XCTAssertTrue(isEligible) } + + // MARK: - Send Receipt After Payment + + func test_isEligibleSendingReceiptAfterPayment_when_feature_flag_is_disabled_then_returns_false() { + // Given + let featureFlag = MockFeatureFlagService(isSendReceiptAfterPaymentEnabled: false) + let stores = MockStoresManager(sessionManager: .makeForTesting()) + let sut = ReceiptEligibilityUseCase(stores: stores, featureFlagService: featureFlag) + + // When + let isEligible: Bool = waitFor { promise in + sut.isEligibleSendingReceiptAfterPayment(onCompletion: { result in + promise(result) + }) + } + + // Then + XCTAssertFalse(isEligible) + } + + func test_isEligibleSendingReceiptAfterPayment_when_plugins_are_inactive_then_returns_false() { + // Given + let featureFlag = MockFeatureFlagService(isSendReceiptAfterPaymentEnabled: true) + let stores = MockStoresManager(sessionManager: .makeForTesting()) + let wooCommercePlugin = SystemPlugin.fake().copy(name: "WooCommerce", version: "9.6.0", active: false) + let wooPaymentsPlugin = SystemPlugin.fake().copy(name: "WooPayments", version: "8.9.0", active: false) + + stores.whenReceivingAction(ofType: SystemStatusAction.self) { action in + switch action { + case let .fetchSystemPlugin(_, systemPluginName, onCompletion): + if systemPluginName == "WooCommerce" { + onCompletion(wooCommercePlugin) + } else if systemPluginName == "WooPayments" { + onCompletion(wooPaymentsPlugin) + } + default: + XCTFail("Unexpected action") + } + } + + let sut = ReceiptEligibilityUseCase(stores: stores, featureFlagService: featureFlag) + + // When + let isEligible: Bool = waitFor { promise in + sut.isEligibleSendingReceiptAfterPayment(onCompletion: { result in + promise(result) + }) + } + + // Then + XCTAssertFalse(isEligible) + } + + func test_isEligibleSendingReceiptAfterPayment_when_plugins_are_supported_then_returns_true() { + // Given + let featureFlag = MockFeatureFlagService(isSendReceiptAfterPaymentEnabled: true) + let stores = MockStoresManager(sessionManager: .makeForTesting()) + let wooCommercePlugin = SystemPlugin.fake().copy(name: "WooCommerce", version: "9.5.0", active: true) + let wooPaymentsPlugin = SystemPlugin.fake().copy(name: "WooPayments", version: "8.6.0", active: true) + + stores.whenReceivingAction(ofType: SystemStatusAction.self) { action in + switch action { + case let .fetchSystemPlugin(_, systemPluginName, onCompletion): + if systemPluginName == "WooCommerce" { + onCompletion(wooCommercePlugin) + } else if systemPluginName == "WooPayments" { + onCompletion(wooPaymentsPlugin) + } + default: + XCTFail("Unexpected action") + } + } + + let sut = ReceiptEligibilityUseCase(stores: stores, featureFlagService: featureFlag) + + // When + let isEligible: Bool = waitFor { promise in + sut.isEligibleSendingReceiptAfterPayment(onCompletion: { result in + promise(result) + }) + } + + // Then + XCTAssertTrue(isEligible) + } + + func test_isEligibleSendingReceiptAfterPayment_when_plugins_are_supported_dev_then_returns_true() { + // Given + let featureFlag = MockFeatureFlagService(isSendReceiptAfterPaymentEnabled: true) + let stores = MockStoresManager(sessionManager: .makeForTesting()) + let wooCommercePlugin = SystemPlugin.fake().copy(name: "WooCommerce", version: "9.6.0-dev-1181231238", active: true) + let wooPaymentsPlugin = SystemPlugin.fake().copy(name: "WooPayments", version: "8.6.0-test-1", active: true) + + stores.whenReceivingAction(ofType: SystemStatusAction.self) { action in + switch action { + case let .fetchSystemPlugin(_, systemPluginName, onCompletion): + if systemPluginName == "WooCommerce" { + onCompletion(wooCommercePlugin) + } else if systemPluginName == "WooPayments" { + onCompletion(wooPaymentsPlugin) + } + default: + XCTFail("Unexpected action") + } + } + + let sut = ReceiptEligibilityUseCase(stores: stores, featureFlagService: featureFlag) + + // When + let isEligible: Bool = waitFor { promise in + sut.isEligibleSendingReceiptAfterPayment(onCompletion: { result in + promise(result) + }) + } + + // Then + XCTAssertTrue(isEligible) + } + + func test_isEligibleSendingReceiptAfterPayment_when_woopayments_version_is_incorrect_then_returns_false() { + // Given + let featureFlag = MockFeatureFlagService(isSendReceiptAfterPaymentEnabled: true) + let stores = MockStoresManager(sessionManager: .makeForTesting()) + let wooCommercePlugin = SystemPlugin.fake().copy(name: "WooCommerce", version: "9.5.0", active: true) + let wooPaymentsPlugin = SystemPlugin.fake().copy(name: "WooPayments", version: "5.0.0-dev", active: true) + + stores.whenReceivingAction(ofType: SystemStatusAction.self) { action in + switch action { + case let .fetchSystemPlugin(_, systemPluginName, onCompletion): + if systemPluginName == "WooCommerce" { + onCompletion(wooCommercePlugin) + } else if systemPluginName == "WooPayments" { + onCompletion(wooPaymentsPlugin) + } + default: + XCTFail("Unexpected action") + } + } + + let sut = ReceiptEligibilityUseCase(stores: stores, featureFlagService: featureFlag) + + // When + let isEligible: Bool = waitFor { promise in + sut.isEligibleSendingReceiptAfterPayment(onCompletion: { result in + promise(result) + }) + } + + // Then + XCTAssertFalse(isEligible) + } }