From bc6201c4099a2332737584b3739c3ffcf0c9b8e5 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 12 Sep 2025 15:52:07 +0700 Subject: [PATCH 1/4] Make selectedSite param required for AlamofireNetwork init --- .../Network/AlamofireNetwork.swift | 4 ++- ...ointOfSaleCouponFetchStrategyFactory.swift | 5 ++- .../Coupons/PointOfSaleCouponService.swift | 5 ++- .../Eligibility/POSSystemStatusService.swift | 7 ++-- .../Items/PointOfSaleBarcodeScanService.swift | 5 ++- .../PointOfSaleItemFetchStrategyFactory.swift | 7 ++-- ...tOfSaleOrderListFetchStrategyFactory.swift | 5 ++- .../Tools/POS/POSCatalogFullSyncService.swift | 11 ++++-- .../POSCatalogIncrementalSyncService.swift | 11 ++++-- .../Yosemite/Tools/POS/POSOrderService.swift | 6 ++-- .../Tools/POS/POSReceiptService.swift | 5 +-- .../Tools/POS/POSSiteSettingService.swift | 5 +-- .../POS/PointOfSaleSettingsService.swift | 5 ++- .../Payments/WooPaymentsPayoutService.swift | 6 ++-- .../Network/AlamofireNetworkTests.swift | 34 +++++++++---------- .../POS/POSCatalogFullSyncServiceTests.swift | 13 +++++-- .../JetpackSetupViewModel.swift | 2 +- .../POS/TabBar/POSTabCoordinator.swift | 21 ++++++++++-- .../Classes/System/SessionManager.swift | 2 +- .../ConnectivityToolViewModel.swift | 2 +- .../Payments Menu/InPersonPaymentsMenu.swift | 10 +++++- .../POS/POSTabEligibilityChecker.swift | 19 ++++++++--- .../Hub Menu/HubMenuViewModel.swift | 11 ++++-- .../JetpackSetupCoordinator.swift | 2 +- .../Classes/Yosemite/AuthenticatedState.swift | 8 +++-- .../Yosemite/DeauthenticatedState.swift | 2 +- .../OrderNotificationDataService.swift | 2 +- .../StoreWidgets/StoreInfoDataService.swift | 2 +- .../Orders/OrdersDataService.swift | 2 +- .../PointOfSaleCouponsControllerTests.swift | 13 ++++--- 30 files changed, 165 insertions(+), 67 deletions(-) diff --git a/Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift b/Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift index 84ae16d39ec..455e832bb47 100644 --- a/Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift +++ b/Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift @@ -67,11 +67,12 @@ public class AlamofireNetwork: Network { /// - Parameters: /// - credentials: Authentication credentials for requests. /// - selectedSite: Publisher for site selection changes. + /// This is necessary if you wish to enable network switching to direct requests while authenticated with WPCOM for better performance. /// - sessionManager: Optional pre-configured session manager. /// - ensuresSessionManagerIsInitialized: If true, the session is always set during initialization immediately to avoid lazy initialization race conditions. /// Defaults to false for backward compatibility. Set to true when making concurrent requests immediately after initialization. public required init(credentials: Credentials?, - selectedSite: AnyPublisher? = nil, + selectedSite: AnyPublisher?, userDefaults: UserDefaults = .standard, sessionManager: Alamofire.Session? = nil, ensuresSessionManagerIsInitialized: Bool = false) { @@ -121,6 +122,7 @@ public class AlamofireNetwork: Network { requestAuthenticator.updateAuthenticator(DefaultRequestAuthenticator(credentials: credentials)) requestAuthenticator.delegate = nil updateAuthenticationMode(.jetpackTunnel) + subscription = nil } } diff --git a/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponFetchStrategyFactory.swift index dabb35cecaa..04cf34826a5 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponFetchStrategyFactory.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponFetchStrategyFactory.swift @@ -3,6 +3,8 @@ import class WooFoundation.CurrencySettings import protocol Storage.StorageManagerType import class Networking.CouponsRemote import class Networking.AlamofireNetwork +import struct Combine.AnyPublisher +import struct NetworkingCore.JetpackSite public struct PointOfSaleCouponFetchStrategyFactory { private let siteID: Int64 @@ -13,8 +15,9 @@ public struct PointOfSaleCouponFetchStrategyFactory { public init(siteID: Int64, currencySettings: CurrencySettings, credentials: Credentials?, + selectedSite: AnyPublisher, storage: StorageManagerType) { - let network = AlamofireNetwork(credentials: credentials) + let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) let remote = CouponsRemote(network: network) self.siteID = siteID self.currencySettings = currencySettings diff --git a/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponService.swift b/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponService.swift index 20edcd69553..452d25750e9 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponService.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponService.swift @@ -5,6 +5,8 @@ import class WooFoundation.CurrencyFormatter import class WooFoundation.CurrencySettings import Storage import enum Alamofire.AFError +import struct Combine.AnyPublisher +import struct NetworkingCore.JetpackSite public protocol PointOfSaleCouponServiceProtocol { func provideLocalPointOfSaleCoupons(fetchStrategy: PointOfSaleCouponFetchStrategy) async throws -> [POSItem] @@ -30,8 +32,9 @@ public final class PointOfSaleCouponService: PointOfSaleCouponServiceProtocol { public convenience init(siteID: Int64, currencySettings: CurrencySettings, credentials: Credentials?, + selectedSite: AnyPublisher, storage: StorageManagerType) { - let network = AlamofireNetwork(credentials: credentials) + let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) self.init(siteID: siteID, currencySettings: currencySettings, settingStoreMethods: SettingStoreMethods(storageManager: storage, network: network), diff --git a/Modules/Sources/Yosemite/PointOfSale/Eligibility/POSSystemStatusService.swift b/Modules/Sources/Yosemite/PointOfSale/Eligibility/POSSystemStatusService.swift index e94bb60cbcd..2b72ecc23c7 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Eligibility/POSSystemStatusService.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Eligibility/POSSystemStatusService.swift @@ -1,6 +1,7 @@ import Foundation import Networking import Storage +import struct Combine.AnyPublisher public protocol POSSystemStatusServiceProtocol { /// Loads WooCommerce plugin and POS feature switch value remotely for eligibility checks. @@ -27,8 +28,10 @@ public final class POSSystemStatusService: POSSystemStatusServiceProtocol { private let storageManager: StorageManagerType private let pluginsService: PluginsServiceProtocol - public init(credentials: Credentials?, storageManager: StorageManagerType) { - let network = AlamofireNetwork(credentials: credentials) + public init(credentials: Credentials?, + selectedSite: AnyPublisher, + storageManager: StorageManagerType) { + let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) self.remote = SystemStatusRemote(network: network) self.storageManager = storageManager self.pluginsService = PluginsService(storageManager: storageManager) diff --git a/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleBarcodeScanService.swift b/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleBarcodeScanService.swift index a21813d090a..2c2bee3c43d 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleBarcodeScanService.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleBarcodeScanService.swift @@ -4,6 +4,8 @@ import class Networking.ProductsRemote import class WooFoundation.CurrencySettings import class Networking.AlamofireNetwork import enum Networking.NetworkError +import struct Combine.AnyPublisher +import struct NetworkingCore.JetpackSite public protocol PointOfSaleBarcodeScanServiceProtocol { func getItem(barcode: String) async throws(PointOfSaleBarcodeScanError) -> POSItem @@ -40,8 +42,9 @@ public final class PointOfSaleBarcodeScanService: PointOfSaleBarcodeScanServiceP public convenience init(siteID: Int64, credentials: Credentials?, + selectedSite: AnyPublisher, currencySettings: CurrencySettings) { - let network = AlamofireNetwork(credentials: credentials) + let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) self.init(siteID: siteID, productsRemote: ProductsRemote(network: network), currencySettings: currencySettings) diff --git a/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleItemFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleItemFetchStrategyFactory.swift index 95d36bbad7a..31e500758c6 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleItemFetchStrategyFactory.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleItemFetchStrategyFactory.swift @@ -2,6 +2,8 @@ import Foundation import class Networking.ProductsRemote import class Networking.ProductVariationsRemote import class Networking.AlamofireNetwork +import struct Combine.AnyPublisher +import struct NetworkingCore.JetpackSite public protocol PointOfSaleItemFetchStrategyFactoryProtocol { func defaultStrategy(analytics: POSItemFetchAnalyticsTracking) -> PointOfSalePurchasableItemFetchStrategy @@ -18,9 +20,10 @@ public final class PointOfSaleItemFetchStrategyFactory: PointOfSaleItemFetchStra private let variationsRemote: ProductVariationsRemote public init(siteID: Int64, - credentials: Credentials?) { + credentials: Credentials?, + selectedSite: AnyPublisher? = nil) { self.siteID = siteID - let network = AlamofireNetwork(credentials: credentials) + let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) self.productsRemote = ProductsRemote(network: network) self.variationsRemote = ProductVariationsRemote(network: network) } diff --git a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift index eebaf996b6b..f541aaa7c8b 100644 --- a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift +++ b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift @@ -2,6 +2,8 @@ import Foundation import class Networking.AlamofireNetwork import class Networking.OrdersRemote import class WooFoundationCore.CurrencyFormatter +import struct Combine.AnyPublisher +import struct NetworkingCore.JetpackSite public protocol PointOfSaleOrderListFetchStrategyFactoryProtocol { func defaultStrategy() -> PointOfSaleOrderListFetchStrategy @@ -15,9 +17,10 @@ public final class PointOfSaleOrderListFetchStrategyFactory: PointOfSaleOrderLis public init(siteID: Int64, credentials: Credentials?, + selectedSite: AnyPublisher, currencyFormatter: CurrencyFormatter) { self.siteID = siteID - let network = AlamofireNetwork(credentials: credentials) + let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) self.ordersRemote = OrdersRemote(network: network) self.currencyFormatter = currencyFormatter } diff --git a/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift b/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift index 462523bc997..9e368c19b19 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift @@ -3,6 +3,8 @@ import protocol Networking.POSCatalogSyncRemoteProtocol import class Networking.AlamofireNetwork import class Networking.POSCatalogSyncRemote import Storage +import struct Combine.AnyPublisher +import struct NetworkingCore.JetpackSite // TODO - remove the periphery ignore comment when the catalog is integrated with POS. // periphery:ignore @@ -30,12 +32,17 @@ public final class POSCatalogFullSyncService: POSCatalogFullSyncServiceProtocol private let persistenceService: POSCatalogPersistenceServiceProtocol private let batchedLoader: BatchedRequestLoader - public convenience init?(credentials: Credentials?, batchSize: Int = 2, grdbManager: GRDBManagerProtocol) { + public convenience init?(credentials: Credentials?, + selectedSite: AnyPublisher, + batchSize: Int = 2, + grdbManager: GRDBManagerProtocol) { guard let credentials else { DDLogError("⛔️ Could not create POSCatalogFullSyncService due missing credentials") return nil } - let network = AlamofireNetwork(credentials: credentials, ensuresSessionManagerIsInitialized: true) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + ensuresSessionManagerIsInitialized: true) let syncRemote = POSCatalogSyncRemote(network: network) let persistenceService = POSCatalogPersistenceService(grdbManager: grdbManager) self.init(syncRemote: syncRemote, batchSize: batchSize, persistenceService: persistenceService) diff --git a/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift b/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift index e9d8573fd47..d1b56d96b51 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift @@ -3,6 +3,8 @@ import protocol Networking.POSCatalogSyncRemoteProtocol import class Networking.AlamofireNetwork import class Networking.POSCatalogSyncRemote import protocol Storage.GRDBManagerProtocol +import struct NetworkingCore.JetpackSite +import struct Combine.AnyPublisher // TODO - remove the periphery ignore comment when the service is integrated with POS. // periphery:ignore @@ -22,12 +24,17 @@ public final class POSCatalogIncrementalSyncService: POSCatalogIncrementalSyncSe private let persistenceService: POSCatalogPersistenceServiceProtocol private let batchedLoader: BatchedRequestLoader - public convenience init?(credentials: Credentials?, batchSize: Int = 1, grdbManager: GRDBManagerProtocol) { + public convenience init?(credentials: Credentials?, + selectedSite: AnyPublisher, + batchSize: Int = 1, + grdbManager: GRDBManagerProtocol) { guard let credentials else { DDLogError("⛔️ Could not create POSCatalogIncrementalSyncService due missing credentials") return nil } - let network = AlamofireNetwork(credentials: credentials, ensuresSessionManagerIsInitialized: true) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + ensuresSessionManagerIsInitialized: true) let syncRemote = POSCatalogSyncRemote(network: network) let persistenceService = POSCatalogPersistenceService(grdbManager: grdbManager) self.init(syncRemote: syncRemote, batchSize: batchSize, persistenceService: persistenceService) diff --git a/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift b/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift index cddca87befa..76ae4087a45 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift @@ -2,6 +2,8 @@ import Foundation import Networking import class WooFoundation.CurrencyFormatter import enum WooFoundation.CurrencyCode +import struct Combine.AnyPublisher +import struct NetworkingCore.JetpackSite public protocol POSOrderServiceProtocol { /// Syncs order based on the cart. @@ -17,12 +19,12 @@ public final class POSOrderService: POSOrderServiceProtocol { private let siteID: Int64 private let ordersRemote: POSOrdersRemoteProtocol - public convenience init?(siteID: Int64, credentials: Credentials?) { + public convenience init?(siteID: Int64, credentials: Credentials?, selectedSite: AnyPublisher) { guard let credentials else { DDLogError("⛔️ Could not create POSOrderService due to not finding credentials") return nil } - let network = AlamofireNetwork(credentials: credentials) + let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) self.init(siteID: siteID, ordersRemote: OrdersRemote(network: network)) } diff --git a/Modules/Sources/Yosemite/Tools/POS/POSReceiptService.swift b/Modules/Sources/Yosemite/Tools/POS/POSReceiptService.swift index 3a9eaa0f3e4..11ae84b64ac 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSReceiptService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSReceiptService.swift @@ -1,5 +1,6 @@ import SwiftUI import Networking +import struct Combine.AnyPublisher public protocol POSReceiptServiceProtocol { func sendReceipt(orderID: Int64, recipientEmail: String, isEligibleForPOSReceipt: Bool) async throws @@ -9,12 +10,12 @@ public final class POSReceiptService: POSReceiptServiceProtocol { private let siteID: Int64 private let receiptsRemote: POSReceiptsRemoteProtocol - public convenience init?(siteID: Int64, credentials: Credentials?) { + public convenience init?(siteID: Int64, credentials: Credentials?, selectedSite: AnyPublisher) { guard let credentials else { DDLogError("⛔️ Could not create POSReceiptService due to not finding credentials") return nil } - let network = AlamofireNetwork(credentials: credentials) + let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) self.init(siteID: siteID, receiptsRemote: ReceiptRemote(network: network)) } diff --git a/Modules/Sources/Yosemite/Tools/POS/POSSiteSettingService.swift b/Modules/Sources/Yosemite/Tools/POS/POSSiteSettingService.swift index 71f95cec48b..0112cbd96bb 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSSiteSettingService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSSiteSettingService.swift @@ -1,5 +1,6 @@ import Foundation import Networking +import struct Combine.AnyPublisher /// Protocol for POS site setting service that manages WooCommerce feature flags. public protocol POSSiteSettingServiceProtocol { @@ -17,8 +18,8 @@ public protocol POSSiteSettingServiceProtocol { public final class POSSiteSettingService: POSSiteSettingServiceProtocol { private let remote: SiteSettingsRemoteProtocol - public init(credentials: Credentials?) { - let network = AlamofireNetwork(credentials: credentials) + public init(credentials: Credentials?, selectedSite: AnyPublisher) { + let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) self.remote = SiteSettingsRemote(network: network) } diff --git a/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift b/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift index f706639be27..c1276d1c1c7 100644 --- a/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift @@ -1,6 +1,8 @@ import Foundation import Networking import Storage +import struct Combine.AnyPublisher +import struct NetworkingCore.JetpackSite public protocol PointOfSaleSettingsServiceProtocol { func retrievePointOfSaleSettings() async throws -> POSReceiptInformation @@ -18,8 +20,9 @@ public final class PointOfSaleSettingsService: PointOfSaleSettingsServiceProtoco public convenience init(siteID: Int64, credentials: Credentials?, + selectedSite: AnyPublisher, storage: StorageManagerType) { - let network = AlamofireNetwork(credentials: credentials) + let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) self.init(siteID: siteID, settingStoreMethods: SettingStoreMethods(storageManager: storage, network: network)) } diff --git a/Modules/Sources/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift b/Modules/Sources/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift index 518c6045a19..cbb9ff1193c 100644 --- a/Modules/Sources/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift +++ b/Modules/Sources/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift @@ -1,6 +1,8 @@ import Foundation import Networking import WooFoundation +import struct Combine.AnyPublisher +import struct NetworkingCore.JetpackSite public protocol WooPaymentsPayoutServiceProtocol { func fetchPayoutsOverview() async throws -> [WooPaymentsPayoutsOverviewByCurrency] @@ -14,12 +16,12 @@ public final class WooPaymentsPayoutService: WooPaymentsPayoutServiceProtocol { // MARK: - Initialization - public convenience init?(siteID: Int64, credentials: Credentials?) { + public convenience init?(siteID: Int64, credentials: Credentials?, selectedSite: AnyPublisher) { 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)) + self.init(siteID: siteID, network: AlamofireNetwork(credentials: credentials, selectedSite: selectedSite)) } public init(siteID: Int64, network: Network) { diff --git a/Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift b/Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift index 2467b326b8f..50a2526f16c 100644 --- a/Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift +++ b/Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift @@ -28,7 +28,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 401, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) let error = waitFor { promise in network.responseData(for: request) { data, error in promise(error) @@ -50,7 +50,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "not_found"], statusCode: 404, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) let error = waitFor { promise in network.responseData(for: request) { data, error in promise(error) @@ -72,7 +72,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 200, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) let error = waitFor { promise in network.responseData(for: request) { data, error in promise(error) @@ -95,7 +95,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 500, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) let result = waitFor { promise in network.responseData(for: request) { result in promise(result) @@ -117,7 +117,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 200, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) let result = waitFor { promise in network.responseData(for: request) { result in promise(result) @@ -140,7 +140,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 500, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) let result = waitFor { promise in self.responseDataSubscription = network.responseDataPublisher(for: request) .sink { result in @@ -163,7 +163,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 200, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) let result = waitFor { promise in self.responseDataSubscription = network.responseDataPublisher(for: request) .sink { result in @@ -180,7 +180,7 @@ final class AlamofireNetworkTests: XCTestCase { func test_didFailToAuthenticateRequestWithAppPassword_adds_siteID_to_unsupported_list() { // Given let siteID: Int64 = 123 - let network = AlamofireNetwork(credentials: nil, userDefaults: userDefaults) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, userDefaults: userDefaults) XCTAssertTrue(userDefaults.applicationPasswordUnsupportedList.isEmpty) // When @@ -195,7 +195,7 @@ final class AlamofireNetworkTests: XCTestCase { let existingSiteID: Int64 = 456 let newSiteID: Int64 = 123 userDefaults.applicationPasswordUnsupportedList = [String(existingSiteID): Date()] - let network = AlamofireNetwork(credentials: nil, userDefaults: userDefaults) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, userDefaults: userDefaults) // When network.didFailToAuthenticateRequestWithAppPassword(siteID: newSiteID) @@ -210,7 +210,7 @@ final class AlamofireNetworkTests: XCTestCase { func test_concurrent_requests_do_not_fail_with_sessionDeinitialized_error_when_ensuresSessionManagerIsInitialized_is_true() async throws { // Given let request = JetpackRequest(wooApiVersion: .mark1, method: .get, siteID: -1, path: "test") - let network = AlamofireNetwork(credentials: nil, ensuresSessionManagerIsInitialized: true) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, ensuresSessionManagerIsInitialized: true) // When async let request1 = network.responseDataAndHeaders(for: request) @@ -231,7 +231,7 @@ final class AlamofireNetworkTests: XCTestCase { func test_concurrent_requests_fail_with_sessionDeinitialized_error_when_ensuresSessionManagerIsInitialized_is_false() async throws { // Given let request = JetpackRequest(wooApiVersion: .mark1, method: .get, siteID: 1, path: "test") - let network = AlamofireNetwork(credentials: nil, ensuresSessionManagerIsInitialized: false) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, ensuresSessionManagerIsInitialized: false) // When async let request1 = network.responseDataAndHeaders(for: request) @@ -650,7 +650,7 @@ final class AlamofireNetworkTests: XCTestCase { let siteID: Int64 = 333 let jetpackRequest = createJetpackRequest(siteID: siteID, path: "test") let wpcomCredentials = createWPComCredentials() - let network = AlamofireNetwork(credentials: wpcomCredentials, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: wpcomCredentials, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) // Mock Jetpack request to fail let jetpackUrlRequest = try XCTUnwrap(try? jetpackRequest.asURLRequest()) @@ -676,7 +676,7 @@ final class AlamofireNetworkTests: XCTestCase { let wporgCredentials = Credentials.wporg(username: "user", password: "pass", siteAddress: "https://example.com") // When - let network = AlamofireNetwork(credentials: wporgCredentials, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: wporgCredentials, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) // Then let expectation = XCTestExpectation(description: "Authentication mode should be set") @@ -692,7 +692,7 @@ final class AlamofireNetworkTests: XCTestCase { let appPasswordCredentials = Credentials.applicationPassword(username: "user", password: "pass", siteAddress: "https://example.com") // When - let network = AlamofireNetwork(credentials: appPasswordCredentials, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: appPasswordCredentials, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) // Then let expectation = XCTestExpectation(description: "Authentication mode should be set") @@ -708,7 +708,7 @@ final class AlamofireNetworkTests: XCTestCase { let wpcomCredentials = createWPComCredentials() // When - let network = AlamofireNetwork(credentials: wpcomCredentials, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: wpcomCredentials, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) // Then let expectation = XCTestExpectation(description: "Authentication mode should be set") @@ -721,7 +721,7 @@ final class AlamofireNetworkTests: XCTestCase { func test_authenticationMode_is_nil_for_no_credentials() { // When - let network = AlamofireNetwork(credentials: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) // Then let expectation = XCTestExpectation(description: "Authentication mode should be set") @@ -791,7 +791,7 @@ final class AlamofireNetworkTests: XCTestCase { func test_authenticationMode_does_not_change_for_non_wpcom_credentials() { // Given let wporgCredentials = Credentials.wporg(username: "user", password: "pass", siteAddress: "https://example.com") - let network = AlamofireNetwork(credentials: wporgCredentials, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: wporgCredentials, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) // When - Try to enable app password switching (should have no effect) network.updateAppPasswordSwitching(enabled: true) diff --git a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift index 1c57b8b43aa..17fbef2cff0 100644 --- a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift +++ b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift @@ -1,5 +1,6 @@ import Foundation import Testing +import Combine @testable import Networking @testable import Yosemite @testable import Storage @@ -137,7 +138,11 @@ struct POSCatalogFullSyncServiceTests { let grdbManager = try GRDBManager() // When - let service = POSCatalogFullSyncService(credentials: credentials, grdbManager: grdbManager) + let service = POSCatalogFullSyncService( + credentials: credentials, + selectedSite: Just(Site.fake()).map { $0.toJetpackSite() }.eraseToAnyPublisher(), + grdbManager: grdbManager + ) // Then #expect(service != nil) @@ -148,7 +153,11 @@ struct POSCatalogFullSyncServiceTests { let grdbManager = try GRDBManager() // When - let service = POSCatalogFullSyncService(credentials: nil, grdbManager: grdbManager) + let service = POSCatalogFullSyncService( + credentials: nil, + selectedSite: Just(Site.fake()).map { $0.toJetpackSite() }.eraseToAnyPublisher(), + grdbManager: grdbManager + ) // Then #expect(service == nil) diff --git a/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift b/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift index 4d291dedd06..6f65deb970a 100644 --- a/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift +++ b/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift @@ -431,7 +431,7 @@ private extension JetpackSetupViewModel { } func finalizeSiteConnection(blogID: Int64, provisionResponse: JetpackConnectionProvisionResponse) { - let network = AlamofireNetwork(credentials: wpcomCredentials) + let network = AlamofireNetwork(credentials: wpcomCredentials, selectedSite: nil) stores.dispatch(JetpackConnectionAction.finalizeConnection( siteID: blogID, siteURL: siteURL, diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index f22a57e4016..ab356da501e 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -5,6 +5,8 @@ import Yosemite import class WooFoundation.CurrencySettings import protocol Storage.StorageManagerType import class WooFoundationCore.CurrencyFormatter +import struct NetworkingCore.JetpackSite +import struct Combine.AnyPublisher /// View controller that provides the tab bar item for the Point of Sale tab. /// It is never visible on the screen, only used to provide the tab bar item as all POS UI is full-screen. @@ -32,7 +34,7 @@ final class POSTabCoordinator { private let eligibilityChecker: POSEntryPointEligibilityCheckerProtocol private lazy var posItemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory = { - PointOfSaleItemFetchStrategyFactory(siteID: siteID, credentials: credentials) + PointOfSaleItemFetchStrategyFactory(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher) }() private lazy var posPopularItemFetchStrategyFactory: PointOfSaleFixedItemFetchStrategyFactory = { @@ -43,6 +45,7 @@ final class POSTabCoordinator { PointOfSaleCouponFetchStrategyFactory(siteID: siteID, currencySettings: currencySettings, credentials: credentials, + selectedSite: defaultSitePublisher, storage: storageManager) }() @@ -50,15 +53,20 @@ final class POSTabCoordinator { return PointOfSaleCouponService(siteID: siteID, currencySettings: currencySettings, credentials: credentials, + selectedSite: defaultSitePublisher, storage: storageManager) }() private lazy var barcodeScanService: PointOfSaleBarcodeScanService = { PointOfSaleBarcodeScanService(siteID: siteID, credentials: credentials, + selectedSite: defaultSitePublisher, currencySettings: currencySettings) }() + /// Publisher to send to `AlamofireNetwork` for request authentication mode switching. + private let defaultSitePublisher: AnyPublisher + init(siteID: Int64, tabContainerController: TabContainerController, viewControllerToPresent: UIViewController, @@ -69,6 +77,9 @@ final class POSTabCoordinator { eligibilityChecker: POSEntryPointEligibilityCheckerProtocol) { self.siteID = siteID self.storesManager = storesManager + self.defaultSitePublisher = storesManager.sessionManager.defaultSitePublisher + .map { $0?.toJetpackSite() } + .eraseToAnyPublisher() self.tabContainerController = tabContainerController self.viewControllerToPresent = viewControllerToPresent self.credentials = storesManager.sessionManager.defaultCredentials @@ -95,14 +106,17 @@ private extension POSTabCoordinator { collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker) let settingsService = PointOfSaleSettingsService(siteID: siteID, credentials: credentials, + selectedSite: defaultSitePublisher, storage: storageManager) let pluginsService = PluginsService(storageManager: storageManager) let siteTimezone = storesManager.sessionManager.defaultSite?.siteTimezone ?? .current if let receiptService = POSReceiptService(siteID: siteID, - credentials: credentials), + credentials: credentials, + selectedSite: defaultSitePublisher), let orderService = POSOrderService(siteID: siteID, - credentials: credentials), + credentials: credentials, + selectedSite: defaultSitePublisher), #available(iOS 17.0, *) { let receiptSender = POSReceiptSender(siteID: siteID, orderService: orderService, @@ -126,6 +140,7 @@ private extension POSTabCoordinator { orderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactory( siteID: siteID, credentials: credentials, + selectedSite: defaultSitePublisher, currencyFormatter: CurrencyFormatter(currencySettings: currencySettings) ) ), diff --git a/WooCommerce/Classes/System/SessionManager.swift b/WooCommerce/Classes/System/SessionManager.swift index 51bd8dec01c..5abf310a5ce 100644 --- a/WooCommerce/Classes/System/SessionManager.swift +++ b/WooCommerce/Classes/System/SessionManager.swift @@ -247,7 +247,7 @@ final class SessionManager: SessionManagerProtocol { guard let siteID = defaultStoreID else { return nil } - let network = AlamofireNetwork(credentials: credentials) + let network = AlamofireNetwork(credentials: credentials, selectedSite: nil) return DefaultApplicationPasswordUseCase(type: .wpcom(siteID: siteID), network: network) case .none: return nil diff --git a/WooCommerce/Classes/ViewRelated/Connectivity Tool/ConnectivityToolViewModel.swift b/WooCommerce/Classes/ViewRelated/Connectivity Tool/ConnectivityToolViewModel.swift index b89b7a8a790..55d5fac14cc 100644 --- a/WooCommerce/Classes/ViewRelated/Connectivity Tool/ConnectivityToolViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Connectivity Tool/ConnectivityToolViewModel.swift @@ -28,7 +28,7 @@ final class ConnectivityToolViewModel { init(session: SessionManagerProtocol = ServiceLocator.stores.sessionManager) { - let network = AlamofireNetwork(credentials: session.defaultCredentials) + let network = AlamofireNetwork(credentials: session.defaultCredentials, selectedSite: nil) self.announcementsRemote = AnnouncementsRemote(network: network) self.systemStatusRemote = SystemStatusRemote(network: network) self.orderRemote = OrdersRemote(network: network) 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 9f6c9cbb825..e8b89c10ec7 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 @@ -334,7 +334,15 @@ struct InPersonPaymentsMenu_Previews: PreviewProvider { cardPresentPaymentsConfiguration: .init(country: .US), onboardingUseCase: CardPresentPaymentsOnboardingUseCase(), cardReaderSupportDeterminer: CardReaderSupportDeterminer(siteID: 0), - wooPaymentsPayoutService: WooPaymentsPayoutService(siteID: 0, credentials: .init(authToken: "")))) + wooPaymentsPayoutService: WooPaymentsPayoutService( + siteID: 0, + credentials: .init(authToken: ""), + selectedSite: ServiceLocator.stores.sessionManager.defaultSitePublisher + .map { $0?.toJetpackSite() } + .eraseToAnyPublisher() + ) + ) + ) static var previews: some View { NavigationStack { InPersonPaymentsMenu(viewModel: viewModel) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index 04ba3334b3f..684760235f1 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -61,17 +61,26 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { eligibilityService: POSEligibilityServiceProtocol = POSEligibilityService(), stores: StoresManager = ServiceLocator.stores, featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, - systemStatusService: POSSystemStatusServiceProtocol = POSSystemStatusService(credentials: ServiceLocator.stores.sessionManager.defaultCredentials, - storageManager: ServiceLocator.storageManager), - siteSettingService: POSSiteSettingServiceProtocol = POSSiteSettingService(credentials: ServiceLocator.stores.sessionManager.defaultCredentials)) { + systemStatusService: POSSystemStatusServiceProtocol? = nil, + siteSettingService: POSSiteSettingServiceProtocol? = nil) { self.siteID = siteID self.userInterfaceIdiom = userInterfaceIdiom self.siteSettings = siteSettings self.eligibilityService = eligibilityService self.stores = stores self.featureFlagService = featureFlagService - self.systemStatusService = systemStatusService - self.siteSettingService = siteSettingService + + let credentials = stores.sessionManager.defaultCredentials + let selectedSite = stores.sessionManager.defaultSitePublisher.map { $0?.toJetpackSite() }.eraseToAnyPublisher() + self.systemStatusService = systemStatusService ?? POSSystemStatusService( + credentials: credentials, + selectedSite: selectedSite, + storageManager: ServiceLocator.storageManager + ) + self.siteSettingService = siteSettingService ?? POSSiteSettingService( + credentials: credentials, + selectedSite: selectedSite + ) } /// Checks the initial visibility of the POS tab without dependance on network requests. diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index c45795497ac..28fdb8edcda 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -111,8 +111,15 @@ final class HubMenuViewModel: ObservableObject { cardPresentPaymentsConfiguration: CardPresentConfigurationLoader().configuration, onboardingUseCase: CardPresentPaymentsOnboardingUseCase(), cardReaderSupportDeterminer: CardReaderSupportDeterminer(siteID: siteID), - wooPaymentsPayoutService: WooPaymentsPayoutService(siteID: siteID, - credentials: credentials))) + wooPaymentsPayoutService: WooPaymentsPayoutService( + siteID: siteID, + credentials: credentials, + selectedSite: stores.sessionManager.defaultSitePublisher + .map { $0?.toJetpackSite() } + .eraseToAnyPublisher() + ) + ) + ) }() private(set) var cardPresentPaymentService: CardPresentPaymentFacade? diff --git a/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift b/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift index ae0efd0b632..fe482d726eb 100644 --- a/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift @@ -310,7 +310,7 @@ private extension JetpackSetupCoordinator { @MainActor func loadWPComAccountUsername(authToken: String) async -> String? { await withCheckedContinuation { continuation in - let network = AlamofireNetwork(credentials: Credentials(authToken: authToken)) + let network = AlamofireNetwork(credentials: Credentials(authToken: authToken), selectedSite: nil) let accountAction = JetpackConnectionAction.loadWPComAccount(network: network) { account in continuation.resume(returning: account?.username) } diff --git a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift index 1f251d3ed8b..27da95228d4 100644 --- a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift +++ b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift @@ -29,7 +29,7 @@ class AuthenticatedState: StoresManagerState { /// private let trackEventRequestNotificationHandler: TrackEventRequestNotificationHandler - private let network: AlamofireNetwork + let network: AlamofireNetwork private var cancellables: Set = [] @@ -147,8 +147,10 @@ class AuthenticatedState: StoresManagerState { // Initialize POS catalog sync coordinator if feature flag is enabled if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1), - let fullSyncService = POSCatalogFullSyncService(credentials: credentials, grdbManager: ServiceLocator.grdbManager), - let incrementalSyncService = POSCatalogIncrementalSyncService(credentials: credentials, grdbManager: ServiceLocator.grdbManager) { + let fullSyncService = POSCatalogFullSyncService(credentials: credentials, + selectedSite: site, + grdbManager: ServiceLocator.grdbManager), + let incrementalSyncService = POSCatalogIncrementalSyncService(credentials: credentials, selectedSite: site, grdbManager: ServiceLocator.grdbManager) { posCatalogSyncCoordinator = POSCatalogSyncCoordinator( fullSyncService: fullSyncService, incrementalSyncService: incrementalSyncService, diff --git a/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift b/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift index d78bbccf276..113ebaf6193 100644 --- a/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift +++ b/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift @@ -15,7 +15,7 @@ class DeauthenticatedState: StoresManagerState { init() { // Used for logged-out state without a WPCOM auth token. - let network = AlamofireNetwork(credentials: nil) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil) services = [ JetpackConnectionStore(dispatcher: dispatcher), AccountCreationStore(dotcomClientID: ApiCredentials.dotcomAppId, diff --git a/WooCommerce/NotificationExtension/OrderNotificationDataService.swift b/WooCommerce/NotificationExtension/OrderNotificationDataService.swift index 7fe95472c55..c30b173aba9 100644 --- a/WooCommerce/NotificationExtension/OrderNotificationDataService.swift +++ b/WooCommerce/NotificationExtension/OrderNotificationDataService.swift @@ -26,7 +26,7 @@ final class OrderNotificationDataService { private let network: AlamofireNetwork init(credentials: Credentials) { - network = AlamofireNetwork(credentials: credentials) + network = AlamofireNetwork(credentials: credentials, selectedSite: nil) // opt out from network switching ordersRemote = OrdersRemote(network: network) notesRemote = NotificationsRemote(network: network) } diff --git a/WooCommerce/StoreWidgets/StoreInfoDataService.swift b/WooCommerce/StoreWidgets/StoreInfoDataService.swift index b8d6729cf73..4c2e9290822 100644 --- a/WooCommerce/StoreWidgets/StoreInfoDataService.swift +++ b/WooCommerce/StoreWidgets/StoreInfoDataService.swift @@ -38,7 +38,7 @@ final class StoreInfoDataService { private let isAuthenticatedWithoutWPCom: Bool init(credentials: Credentials) { - network = AlamofireNetwork(credentials: credentials) + network = AlamofireNetwork(credentials: credentials, selectedSite: nil) // opt out from network switching orderStatsRemoteV4 = OrderStatsRemoteV4(network: network) siteStatsRemote = SiteStatsRemote(network: network) if case .wpcom = credentials { diff --git a/WooCommerce/Woo Watch App/Orders/OrdersDataService.swift b/WooCommerce/Woo Watch App/Orders/OrdersDataService.swift index f4fbb07a33a..166b3e77785 100644 --- a/WooCommerce/Woo Watch App/Orders/OrdersDataService.swift +++ b/WooCommerce/Woo Watch App/Orders/OrdersDataService.swift @@ -13,7 +13,7 @@ final class OrdersDataService { private let network: AlamofireNetwork init(credentials: Credentials) { - network = AlamofireNetwork(credentials: credentials) + network = AlamofireNetwork(credentials: credentials, selectedSite: nil) // opt out from network switching ordersRemote = OrdersRemote(network: network) } diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleCouponsControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleCouponsControllerTests.swift index 19a3162b953..64fef0831ac 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleCouponsControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleCouponsControllerTests.swift @@ -3,15 +3,20 @@ import Testing import Foundation import struct Yosemite.PointOfSaleCouponFetchStrategyFactory import class WooFoundation.CurrencySettings +import struct Yosemite.Site +import Combine struct PointOfSaleCouponsControllerTests { private let fetchStrategyFactory: PointOfSaleCouponFetchStrategyFactory init() { - fetchStrategyFactory = PointOfSaleCouponFetchStrategyFactory(siteID: 123, - currencySettings: CurrencySettings(), - credentials: nil, - storage: MockStorageManager()) + fetchStrategyFactory = PointOfSaleCouponFetchStrategyFactory( + siteID: 123, + currencySettings: CurrencySettings(), + credentials: nil, + selectedSite: Just(Site.fake()).map { $0.toJetpackSite() }.eraseToAnyPublisher(), + storage: MockStorageManager() + ) } @Test func loadItems_when_empty_coupons_then_results_in_empty_state() async throws { From 38f00eaeb483a71ad216b134f98036f1f81cf56a Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 16 Sep 2025 18:31:31 +0700 Subject: [PATCH 2/4] Send support availability to AlamofireNetwork --- .../Network/AlamofireNetwork.swift | 43 ++++++--- ...ointOfSaleCouponFetchStrategyFactory.swift | 5 +- .../Coupons/PointOfSaleCouponService.swift | 5 +- .../Eligibility/POSSystemStatusService.swift | 5 +- .../Items/PointOfSaleBarcodeScanService.swift | 5 +- .../PointOfSaleItemFetchStrategyFactory.swift | 7 +- ...tOfSaleOrderListFetchStrategyFactory.swift | 5 +- .../Tools/POS/POSCatalogFullSyncService.swift | 2 + .../POSCatalogIncrementalSyncService.swift | 2 + .../Yosemite/Tools/POS/POSOrderService.swift | 9 +- .../Tools/POS/POSReceiptService.swift | 9 +- .../Tools/POS/POSSiteSettingService.swift | 8 +- .../POS/PointOfSaleSettingsService.swift | 5 +- .../Payments/WooPaymentsPayoutService.swift | 9 +- .../Network/AlamofireNetworkTests.swift | 93 +++++++++++++------ .../POS/POSCatalogFullSyncServiceTests.swift | 2 + .../JetpackSetupViewModel.swift | 2 +- .../POS/TabBar/POSTabCoordinator.swift | 22 ++++- .../Classes/System/SessionManager.swift | 2 +- .../ConnectivityToolViewModel.swift | 2 +- .../ApplicationPasswordsExperimentState.swift | 17 ++-- .../Payments Menu/InPersonPaymentsMenu.swift | 3 + .../POS/POSTabEligibilityChecker.swift | 5 +- .../Hub Menu/HubMenuViewModel.swift | 3 + .../JetpackSetupCoordinator.swift | 2 +- .../Classes/Yosemite/AuthenticatedState.swift | 60 ++++++------ .../Yosemite/DeauthenticatedState.swift | 2 +- .../OrderNotificationDataService.swift | 2 +- .../StoreWidgets/StoreInfoDataService.swift | 2 +- .../Orders/OrdersDataService.swift | 2 +- .../PointOfSaleCouponsControllerTests.swift | 1 + ...icationPasswordsExperimentStateTests.swift | 51 +++++++--- 32 files changed, 271 insertions(+), 121 deletions(-) diff --git a/Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift b/Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift index 36d1e529fa2..a60e01b3658 100644 --- a/Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift +++ b/Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift @@ -57,11 +57,13 @@ public class AlamofireNetwork: Network { public var session: URLSession { Session.default.session } - private var subscription: AnyCancellable? + private var siteSubscription: AnyCancellable? /// Thread-safe error handler for failure tracking and retry logic private let errorHandler: AlamofireNetworkErrorHandler + private var appPasswordSupportSubscription: AnyCancellable? + /// Public Initializer /// /// - Parameters: @@ -73,6 +75,7 @@ public class AlamofireNetwork: Network { /// Defaults to false for backward compatibility. Set to true when making concurrent requests immediately after initialization. public required init(credentials: Credentials?, selectedSite: AnyPublisher?, + appPasswordSupportState: AnyPublisher?, userDefaults: UserDefaults = .standard, sessionManager: Alamofire.Session? = nil, ensuresSessionManagerIsInitialized: Bool = false) { @@ -111,18 +114,8 @@ public class AlamofireNetwork: Network { } }() updateAuthenticationMode(authenticationMode) - } - - public func updateAppPasswordSwitching(enabled: Bool) { - guard let credentials, case .wpcom = credentials else { return } - if enabled, let selectedSite { - observeSelectedSite(selectedSite) - } else { - requestConverter = RequestConverter(siteAddress: nil) - requestAuthenticator.updateAuthenticator(DefaultRequestAuthenticator(credentials: credentials)) - requestAuthenticator.delegate = nil - updateAuthenticationMode(.jetpackTunnel) - subscription = nil + if let appPasswordSupportState { + observeAppPasswordSupportState(appPasswordSupportState) } } @@ -279,6 +272,28 @@ public class AlamofireNetwork: Network { } private extension AlamofireNetwork { + + func observeAppPasswordSupportState(_ appPasswordSupportState: AnyPublisher) { + appPasswordSupportSubscription = appPasswordSupportState + .removeDuplicates() + .sink { [weak self] enabled in + self?.updateAppPasswordSwitching(enabled: enabled) + } + } + + func updateAppPasswordSwitching(enabled: Bool) { + guard let credentials, case .wpcom = credentials else { return } + if enabled, let selectedSite { + observeSelectedSite(selectedSite) + } else { + requestConverter = RequestConverter(siteAddress: nil) + requestAuthenticator.updateAuthenticator(DefaultRequestAuthenticator(credentials: credentials)) + requestAuthenticator.delegate = nil + updateAuthenticationMode(.jetpackTunnel) + siteSubscription = nil + } + } + /// Creates a session manager with request retrier and adapter /// func makeSession(configuration sessionConfiguration: URLSessionConfiguration) -> Alamofire.Session { @@ -288,7 +303,7 @@ private extension AlamofireNetwork { /// Updates `requestConverter` and `requestAuthenticator` when selected site changes /// func observeSelectedSite(_ selectedSite: AnyPublisher) { - subscription = selectedSite + siteSubscription = selectedSite .removeDuplicates() .combineLatest(userDefaults.publisher(for: \.applicationPasswordUnsupportedList)) .sink { [weak self] site, unsupportedList in diff --git a/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponFetchStrategyFactory.swift index 04cf34826a5..4f656844be4 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponFetchStrategyFactory.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponFetchStrategyFactory.swift @@ -16,8 +16,11 @@ public struct PointOfSaleCouponFetchStrategyFactory { currencySettings: CurrencySettings, credentials: Credentials?, selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher, storage: StorageManagerType) { - let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) let remote = CouponsRemote(network: network) self.siteID = siteID self.currencySettings = currencySettings diff --git a/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponService.swift b/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponService.swift index 452d25750e9..36463c7b9cb 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponService.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Coupons/PointOfSaleCouponService.swift @@ -33,8 +33,11 @@ public final class PointOfSaleCouponService: PointOfSaleCouponServiceProtocol { currencySettings: CurrencySettings, credentials: Credentials?, selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher, storage: StorageManagerType) { - let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) self.init(siteID: siteID, currencySettings: currencySettings, settingStoreMethods: SettingStoreMethods(storageManager: storage, network: network), diff --git a/Modules/Sources/Yosemite/PointOfSale/Eligibility/POSSystemStatusService.swift b/Modules/Sources/Yosemite/PointOfSale/Eligibility/POSSystemStatusService.swift index 2b72ecc23c7..f7c6f65399b 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Eligibility/POSSystemStatusService.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Eligibility/POSSystemStatusService.swift @@ -30,8 +30,11 @@ public final class POSSystemStatusService: POSSystemStatusServiceProtocol { public init(credentials: Credentials?, selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher, storageManager: StorageManagerType) { - let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) self.remote = SystemStatusRemote(network: network) self.storageManager = storageManager self.pluginsService = PluginsService(storageManager: storageManager) diff --git a/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleBarcodeScanService.swift b/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleBarcodeScanService.swift index 2c2bee3c43d..ff59863e051 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleBarcodeScanService.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleBarcodeScanService.swift @@ -43,8 +43,11 @@ public final class PointOfSaleBarcodeScanService: PointOfSaleBarcodeScanServiceP public convenience init(siteID: Int64, credentials: Credentials?, selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher, currencySettings: CurrencySettings) { - let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) self.init(siteID: siteID, productsRemote: ProductsRemote(network: network), currencySettings: currencySettings) diff --git a/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleItemFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleItemFetchStrategyFactory.swift index 31e500758c6..b047d47207d 100644 --- a/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleItemFetchStrategyFactory.swift +++ b/Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleItemFetchStrategyFactory.swift @@ -21,9 +21,12 @@ public final class PointOfSaleItemFetchStrategyFactory: PointOfSaleItemFetchStra public init(siteID: Int64, credentials: Credentials?, - selectedSite: AnyPublisher? = nil) { + selectedSite: AnyPublisher? = nil, + appPasswordSupportState: AnyPublisher? = nil) { self.siteID = siteID - let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) self.productsRemote = ProductsRemote(network: network) self.variationsRemote = ProductVariationsRemote(network: network) } diff --git a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift index f541aaa7c8b..3755453a3b4 100644 --- a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift +++ b/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift @@ -18,9 +18,12 @@ public final class PointOfSaleOrderListFetchStrategyFactory: PointOfSaleOrderLis public init(siteID: Int64, credentials: Credentials?, selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher, currencyFormatter: CurrencyFormatter) { self.siteID = siteID - let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) self.ordersRemote = OrdersRemote(network: network) self.currencyFormatter = currencyFormatter } diff --git a/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift b/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift index 9e368c19b19..288eeeadac5 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift @@ -34,6 +34,7 @@ public final class POSCatalogFullSyncService: POSCatalogFullSyncServiceProtocol public convenience init?(credentials: Credentials?, selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher, batchSize: Int = 2, grdbManager: GRDBManagerProtocol) { guard let credentials else { @@ -42,6 +43,7 @@ public final class POSCatalogFullSyncService: POSCatalogFullSyncServiceProtocol } let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState, ensuresSessionManagerIsInitialized: true) let syncRemote = POSCatalogSyncRemote(network: network) let persistenceService = POSCatalogPersistenceService(grdbManager: grdbManager) diff --git a/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift b/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift index d1b56d96b51..55c2f8bc578 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift @@ -26,6 +26,7 @@ public final class POSCatalogIncrementalSyncService: POSCatalogIncrementalSyncSe public convenience init?(credentials: Credentials?, selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher, batchSize: Int = 1, grdbManager: GRDBManagerProtocol) { guard let credentials else { @@ -34,6 +35,7 @@ public final class POSCatalogIncrementalSyncService: POSCatalogIncrementalSyncSe } let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState, ensuresSessionManagerIsInitialized: true) let syncRemote = POSCatalogSyncRemote(network: network) let persistenceService = POSCatalogPersistenceService(grdbManager: grdbManager) diff --git a/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift b/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift index 76ae4087a45..270e2436dbb 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSOrderService.swift @@ -19,12 +19,17 @@ public final class POSOrderService: POSOrderServiceProtocol { private let siteID: Int64 private let ordersRemote: POSOrdersRemoteProtocol - public convenience init?(siteID: Int64, credentials: Credentials?, selectedSite: AnyPublisher) { + public convenience init?(siteID: Int64, + credentials: Credentials?, + selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher) { guard let credentials else { DDLogError("⛔️ Could not create POSOrderService due to not finding credentials") return nil } - let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) self.init(siteID: siteID, ordersRemote: OrdersRemote(network: network)) } diff --git a/Modules/Sources/Yosemite/Tools/POS/POSReceiptService.swift b/Modules/Sources/Yosemite/Tools/POS/POSReceiptService.swift index 11ae84b64ac..1a020d25afe 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSReceiptService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSReceiptService.swift @@ -10,12 +10,17 @@ public final class POSReceiptService: POSReceiptServiceProtocol { private let siteID: Int64 private let receiptsRemote: POSReceiptsRemoteProtocol - public convenience init?(siteID: Int64, credentials: Credentials?, selectedSite: AnyPublisher) { + public convenience init?(siteID: Int64, + credentials: Credentials?, + selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher) { guard let credentials else { DDLogError("⛔️ Could not create POSReceiptService due to not finding credentials") return nil } - let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) self.init(siteID: siteID, receiptsRemote: ReceiptRemote(network: network)) } diff --git a/Modules/Sources/Yosemite/Tools/POS/POSSiteSettingService.swift b/Modules/Sources/Yosemite/Tools/POS/POSSiteSettingService.swift index 0112cbd96bb..d5a817fcffd 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSSiteSettingService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSSiteSettingService.swift @@ -18,8 +18,12 @@ public protocol POSSiteSettingServiceProtocol { public final class POSSiteSettingService: POSSiteSettingServiceProtocol { private let remote: SiteSettingsRemoteProtocol - public init(credentials: Credentials?, selectedSite: AnyPublisher) { - let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) + public init(credentials: Credentials?, + selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher) { + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) self.remote = SiteSettingsRemote(network: network) } diff --git a/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift b/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift index c1276d1c1c7..8f18fb67540 100644 --- a/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/PointOfSaleSettingsService.swift @@ -21,8 +21,11 @@ public final class PointOfSaleSettingsService: PointOfSaleSettingsServiceProtoco public convenience init(siteID: Int64, credentials: Credentials?, selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher, storage: StorageManagerType) { - let network = AlamofireNetwork(credentials: credentials, selectedSite: selectedSite) + let network = AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState) self.init(siteID: siteID, settingStoreMethods: SettingStoreMethods(storageManager: storage, network: network)) } diff --git a/Modules/Sources/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift b/Modules/Sources/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift index cbb9ff1193c..951394a7562 100644 --- a/Modules/Sources/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift +++ b/Modules/Sources/Yosemite/Tools/Payments/WooPaymentsPayoutService.swift @@ -16,12 +16,17 @@ public final class WooPaymentsPayoutService: WooPaymentsPayoutServiceProtocol { // MARK: - Initialization - public convenience init?(siteID: Int64, credentials: Credentials?, selectedSite: AnyPublisher) { + public convenience init?(siteID: Int64, + credentials: Credentials?, + selectedSite: AnyPublisher, + appPasswordSupportState: AnyPublisher) { 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, selectedSite: selectedSite)) + self.init(siteID: siteID, network: AlamofireNetwork(credentials: credentials, + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState)) } public init(siteID: Int64, network: Network) { diff --git a/Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift b/Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift index fa26b06e61c..0e8e349b704 100644 --- a/Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift +++ b/Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift @@ -28,7 +28,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 401, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, sessionManager: createSessionWithMockURLProtocol()) let error = waitFor { promise in network.responseData(for: request) { data, error in promise(error) @@ -50,7 +50,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "not_found"], statusCode: 404, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, sessionManager: createSessionWithMockURLProtocol()) let error = waitFor { promise in network.responseData(for: request) { data, error in promise(error) @@ -72,7 +72,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 200, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, sessionManager: createSessionWithMockURLProtocol()) let error = waitFor { promise in network.responseData(for: request) { data, error in promise(error) @@ -95,7 +95,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 500, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, sessionManager: createSessionWithMockURLProtocol()) let result = waitFor { promise in network.responseData(for: request) { result in promise(result) @@ -117,7 +117,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 200, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, sessionManager: createSessionWithMockURLProtocol()) let result = waitFor { promise in network.responseData(for: request) { result in promise(result) @@ -140,7 +140,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 500, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, sessionManager: createSessionWithMockURLProtocol()) let result = waitFor { promise in self.responseDataSubscription = network.responseDataPublisher(for: request) .sink { result in @@ -163,7 +163,7 @@ final class AlamofireNetworkTests: XCTestCase { MockURLProtocol.Mocks.mockResponse(["error": "http_request_failed"], statusCode: 200, for: urlRequest) // When - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, sessionManager: createSessionWithMockURLProtocol()) let result = waitFor { promise in self.responseDataSubscription = network.responseDataPublisher(for: request) .sink { result in @@ -180,7 +180,7 @@ final class AlamofireNetworkTests: XCTestCase { func test_didFailToAuthenticateRequestWithAppPassword_adds_siteID_to_unsupported_list() { // Given let siteID: Int64 = 123 - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, userDefaults: userDefaults) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, userDefaults: userDefaults) XCTAssertTrue(userDefaults.applicationPasswordUnsupportedList.isEmpty) // When @@ -195,7 +195,7 @@ final class AlamofireNetworkTests: XCTestCase { let existingSiteID: Int64 = 456 let newSiteID: Int64 = 123 userDefaults.applicationPasswordUnsupportedList = [String(existingSiteID): Date()] - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, userDefaults: userDefaults) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, userDefaults: userDefaults) // When network.didFailToAuthenticateRequestWithAppPassword(siteID: newSiteID, error: NetworkError.notFound(response: nil)) @@ -210,7 +210,7 @@ final class AlamofireNetworkTests: XCTestCase { func test_concurrent_requests_do_not_fail_with_sessionDeinitialized_error_when_ensuresSessionManagerIsInitialized_is_true() async throws { // Given let request = JetpackRequest(wooApiVersion: .mark1, method: .get, siteID: -1, path: "test") - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, ensuresSessionManagerIsInitialized: true) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, ensuresSessionManagerIsInitialized: true) // When async let request1 = network.responseDataAndHeaders(for: request) @@ -231,7 +231,7 @@ final class AlamofireNetworkTests: XCTestCase { func test_concurrent_requests_fail_with_sessionDeinitialized_error_when_ensuresSessionManagerIsInitialized_is_false() async throws { // Given let request = JetpackRequest(wooApiVersion: .mark1, method: .get, siteID: 1, path: "test") - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, ensuresSessionManagerIsInitialized: false) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, ensuresSessionManagerIsInitialized: false) // When async let request1 = network.responseDataAndHeaders(for: request) @@ -650,7 +650,10 @@ final class AlamofireNetworkTests: XCTestCase { let siteID: Int64 = 333 let jetpackRequest = createJetpackRequest(siteID: siteID, path: "test") let wpcomCredentials = createWPComCredentials() - let network = AlamofireNetwork(credentials: wpcomCredentials, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: wpcomCredentials, + selectedSite: nil, + appPasswordSupportState: nil, + sessionManager: createSessionWithMockURLProtocol()) // Mock Jetpack request to fail let jetpackUrlRequest = try XCTUnwrap(try? jetpackRequest.asURLRequest()) @@ -676,7 +679,10 @@ final class AlamofireNetworkTests: XCTestCase { let wporgCredentials = Credentials.wporg(username: "user", password: "pass", siteAddress: "https://example.com") // When - let network = AlamofireNetwork(credentials: wporgCredentials, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: wporgCredentials, + selectedSite: nil, + appPasswordSupportState: nil, + sessionManager: createSessionWithMockURLProtocol()) // Then let expectation = XCTestExpectation(description: "Authentication mode should be set") @@ -692,7 +698,10 @@ final class AlamofireNetworkTests: XCTestCase { let appPasswordCredentials = Credentials.applicationPassword(username: "user", password: "pass", siteAddress: "https://example.com") // When - let network = AlamofireNetwork(credentials: appPasswordCredentials, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: appPasswordCredentials, + selectedSite: nil, + appPasswordSupportState: nil, + sessionManager: createSessionWithMockURLProtocol()) // Then let expectation = XCTestExpectation(description: "Authentication mode should be set") @@ -708,7 +717,10 @@ final class AlamofireNetworkTests: XCTestCase { let wpcomCredentials = createWPComCredentials() // When - let network = AlamofireNetwork(credentials: wpcomCredentials, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: wpcomCredentials, + selectedSite: nil, + appPasswordSupportState: nil, + sessionManager: createSessionWithMockURLProtocol()) // Then let expectation = XCTestExpectation(description: "Authentication mode should be set") @@ -721,7 +733,7 @@ final class AlamofireNetworkTests: XCTestCase { func test_authenticationMode_is_nil_for_no_credentials() { // When - let network = AlamofireNetwork(credentials: nil, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil, sessionManager: createSessionWithMockURLProtocol()) // Then let expectation = XCTestExpectation(description: "Authentication mode should be set") @@ -736,10 +748,16 @@ final class AlamofireNetworkTests: XCTestCase { // Given let siteID: Int64 = 123 let wpcomCredentials = createWPComCredentials() - let network = createNetworkWithSelectedSite(siteID: siteID, credentials: wpcomCredentials, userDefaults: userDefaults) + let appPasswordSupportStream = CurrentValueSubject(false) + let network = createNetworkWithSelectedSite( + siteID: siteID, + credentials: wpcomCredentials, + userDefaults: userDefaults, + appPasswordSupport: appPasswordSupportStream.eraseToAnyPublisher() + ) // When - Enable app password switching - network.updateAppPasswordSwitching(enabled: true) + appPasswordSupportStream.send(true) // Then let expectation = XCTestExpectation(description: "Authentication mode should change") @@ -754,11 +772,17 @@ final class AlamofireNetworkTests: XCTestCase { // Given let siteID: Int64 = 456 let wpcomCredentials = createWPComCredentials() - let network = createNetworkWithSelectedSite(siteID: siteID, credentials: wpcomCredentials, userDefaults: userDefaults) + let appPasswordSupportStream = CurrentValueSubject(false) + let network = createNetworkWithSelectedSite( + siteID: siteID, + credentials: wpcomCredentials, + userDefaults: userDefaults, + appPasswordSupport: appPasswordSupportStream.eraseToAnyPublisher() + ) // When - Enable then disable app password switching - network.updateAppPasswordSwitching(enabled: true) - network.updateAppPasswordSwitching(enabled: false) + appPasswordSupportStream.send(true) + appPasswordSupportStream.send(false) // Then let expectation = XCTestExpectation(description: "Authentication mode should revert") @@ -773,11 +797,17 @@ final class AlamofireNetworkTests: XCTestCase { // Given let siteID: Int64 = 789 let wpcomCredentials = createWPComCredentials() + let appPasswordSupportStream = CurrentValueSubject(false) userDefaults.applicationPasswordUnsupportedList = [String(siteID): Date()] - let network = createNetworkWithSelectedSite(siteID: siteID, credentials: wpcomCredentials, userDefaults: userDefaults) + let network = createNetworkWithSelectedSite( + siteID: siteID, + credentials: wpcomCredentials, + userDefaults: userDefaults, + appPasswordSupport: appPasswordSupportStream.eraseToAnyPublisher() + ) // When - Enable app password switching for an unsupported site - network.updateAppPasswordSwitching(enabled: true) + appPasswordSupportStream.send(true) // Then let expectation = XCTestExpectation(description: "Authentication mode should remain jetpackTunnel") @@ -791,10 +821,10 @@ final class AlamofireNetworkTests: XCTestCase { func test_authenticationMode_does_not_change_for_non_wpcom_credentials() { // Given let wporgCredentials = Credentials.wporg(username: "user", password: "pass", siteAddress: "https://example.com") - let network = AlamofireNetwork(credentials: wporgCredentials, selectedSite: nil, sessionManager: createSessionWithMockURLProtocol()) - - // When - Try to enable app password switching (should have no effect) - network.updateAppPasswordSwitching(enabled: true) + let network = AlamofireNetwork(credentials: wporgCredentials, + selectedSite: nil, + appPasswordSupportState: nil, + sessionManager: createSessionWithMockURLProtocol()) // Then let expectation = XCTestExpectation(description: "Authentication mode should remain unchanged") @@ -839,16 +869,21 @@ private extension AlamofireNetworkTests { return Just(site).eraseToAnyPublisher() } - func createNetworkWithSelectedSite(siteID: Int64, credentials: Credentials? = nil, userDefaults: UserDefaults? = nil) -> AlamofireNetwork { + func createNetworkWithSelectedSite( + siteID: Int64, + credentials: Credentials? = nil, + userDefaults: UserDefaults? = nil, + appPasswordSupport: AnyPublisher = Just(true).eraseToAnyPublisher() + ) -> AlamofireNetwork { let networkCredentials = credentials ?? createWPComCredentials() let selectedSite = createSelectedSitePublisher(siteID: siteID) let network = AlamofireNetwork( credentials: networkCredentials, selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupport, userDefaults: userDefaults ?? .standard, sessionManager: createSessionWithMockURLProtocol() ) - network.updateAppPasswordSwitching(enabled: true) return network } diff --git a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift index 17fbef2cff0..8b271c3eb81 100644 --- a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift +++ b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift @@ -141,6 +141,7 @@ struct POSCatalogFullSyncServiceTests { let service = POSCatalogFullSyncService( credentials: credentials, selectedSite: Just(Site.fake()).map { $0.toJetpackSite() }.eraseToAnyPublisher(), + appPasswordSupportState: Just(false).eraseToAnyPublisher(), grdbManager: grdbManager ) @@ -156,6 +157,7 @@ struct POSCatalogFullSyncServiceTests { let service = POSCatalogFullSyncService( credentials: nil, selectedSite: Just(Site.fake()).map { $0.toJetpackSite() }.eraseToAnyPublisher(), + appPasswordSupportState: Just(false).eraseToAnyPublisher(), grdbManager: grdbManager ) diff --git a/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift b/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift index 6f65deb970a..d38ea3fdcac 100644 --- a/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift +++ b/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift @@ -431,7 +431,7 @@ private extension JetpackSetupViewModel { } func finalizeSiteConnection(blogID: Int64, provisionResponse: JetpackConnectionProvisionResponse) { - let network = AlamofireNetwork(credentials: wpcomCredentials, selectedSite: nil) + let network = AlamofireNetwork(credentials: wpcomCredentials, selectedSite: nil, appPasswordSupportState: nil) stores.dispatch(JetpackConnectionAction.finalizeConnection( siteID: blogID, siteURL: siteURL, diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index ab356da501e..c4a6056b3df 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -34,7 +34,10 @@ final class POSTabCoordinator { private let eligibilityChecker: POSEntryPointEligibilityCheckerProtocol private lazy var posItemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory = { - PointOfSaleItemFetchStrategyFactory(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher) + PointOfSaleItemFetchStrategyFactory(siteID: siteID, + credentials: credentials, + selectedSite: defaultSitePublisher, + appPasswordSupportState: appPasswordSupportState) }() private lazy var posPopularItemFetchStrategyFactory: PointOfSaleFixedItemFetchStrategyFactory = { @@ -46,6 +49,7 @@ final class POSTabCoordinator { currencySettings: currencySettings, credentials: credentials, selectedSite: defaultSitePublisher, + appPasswordSupportState: appPasswordSupportState, storage: storageManager) }() @@ -54,6 +58,7 @@ final class POSTabCoordinator { currencySettings: currencySettings, credentials: credentials, selectedSite: defaultSitePublisher, + appPasswordSupportState: appPasswordSupportState, storage: storageManager) }() @@ -61,12 +66,16 @@ final class POSTabCoordinator { PointOfSaleBarcodeScanService(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, + appPasswordSupportState: appPasswordSupportState, currencySettings: currencySettings) }() /// Publisher to send to `AlamofireNetwork` for request authentication mode switching. private let defaultSitePublisher: AnyPublisher + /// Publisher to send to `AlamofireNetwork` the state of app password support for JP sites + private let appPasswordSupportState: AnyPublisher + init(siteID: Int64, tabContainerController: TabContainerController, viewControllerToPresent: UIViewController, @@ -80,6 +89,9 @@ final class POSTabCoordinator { self.defaultSitePublisher = storesManager.sessionManager.defaultSitePublisher .map { $0?.toJetpackSite() } .eraseToAnyPublisher() + self.appPasswordSupportState = ApplicationPasswordsExperimentState() + .$isAvailableAndEnabled + .eraseToAnyPublisher() self.tabContainerController = tabContainerController self.viewControllerToPresent = viewControllerToPresent self.credentials = storesManager.sessionManager.defaultCredentials @@ -107,16 +119,19 @@ private extension POSTabCoordinator { let settingsService = PointOfSaleSettingsService(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, + appPasswordSupportState: appPasswordSupportState, storage: storageManager) let pluginsService = PluginsService(storageManager: storageManager) let siteTimezone = storesManager.sessionManager.defaultSite?.siteTimezone ?? .current if let receiptService = POSReceiptService(siteID: siteID, credentials: credentials, - selectedSite: defaultSitePublisher), + selectedSite: defaultSitePublisher, + appPasswordSupportState: appPasswordSupportState), let orderService = POSOrderService(siteID: siteID, credentials: credentials, - selectedSite: defaultSitePublisher), + selectedSite: defaultSitePublisher, + appPasswordSupportState: appPasswordSupportState), #available(iOS 17.0, *) { let receiptSender = POSReceiptSender(siteID: siteID, orderService: orderService, @@ -141,6 +156,7 @@ private extension POSTabCoordinator { siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, + appPasswordSupportState: appPasswordSupportState, currencyFormatter: CurrencyFormatter(currencySettings: currencySettings) ) ), diff --git a/WooCommerce/Classes/System/SessionManager.swift b/WooCommerce/Classes/System/SessionManager.swift index 5abf310a5ce..0053ef59a8c 100644 --- a/WooCommerce/Classes/System/SessionManager.swift +++ b/WooCommerce/Classes/System/SessionManager.swift @@ -247,7 +247,7 @@ final class SessionManager: SessionManagerProtocol { guard let siteID = defaultStoreID else { return nil } - let network = AlamofireNetwork(credentials: credentials, selectedSite: nil) + let network = AlamofireNetwork(credentials: credentials, selectedSite: nil, appPasswordSupportState: nil) return DefaultApplicationPasswordUseCase(type: .wpcom(siteID: siteID), network: network) case .none: return nil diff --git a/WooCommerce/Classes/ViewRelated/Connectivity Tool/ConnectivityToolViewModel.swift b/WooCommerce/Classes/ViewRelated/Connectivity Tool/ConnectivityToolViewModel.swift index 55d5fac14cc..0daee2c642e 100644 --- a/WooCommerce/Classes/ViewRelated/Connectivity Tool/ConnectivityToolViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Connectivity Tool/ConnectivityToolViewModel.swift @@ -28,7 +28,7 @@ final class ConnectivityToolViewModel { init(session: SessionManagerProtocol = ServiceLocator.stores.sessionManager) { - let network = AlamofireNetwork(credentials: session.defaultCredentials, selectedSite: nil) + let network = AlamofireNetwork(credentials: session.defaultCredentials, selectedSite: nil, appPasswordSupportState: nil) self.announcementsRemote = AnnouncementsRemote(network: network) self.systemStatusRemote = SystemStatusRemote(network: network) self.orderRemote = OrdersRemote(network: network) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/ApplicationPasswordsExperimentState.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/ApplicationPasswordsExperimentState.swift index 03b178fd721..658f41d9b6a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/ApplicationPasswordsExperimentState.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/ApplicationPasswordsExperimentState.swift @@ -11,15 +11,10 @@ final class ApplicationPasswordsExperimentState { ) { self.stores = stores self.availabilityChecker = availabilityChecker + updateAvailability() } - var isAvailableAndEnabled: Bool { - get async { - let isAvailable = await availabilityChecker.fetchAvailability() - let isEnabled = await isEnabled - return isAvailable && isEnabled - } - } + @Published private(set) var isAvailableAndEnabled: Bool = true @MainActor private var isEnabled: Bool { @@ -33,6 +28,14 @@ final class ApplicationPasswordsExperimentState { } } } + + private func updateAvailability() { + Task { @MainActor in + let isAvailable = await availabilityChecker.fetchAvailability() + let isEnabled = await isEnabled + isAvailableAndEnabled = isAvailable && isEnabled + } + } } protocol ApplicationPasswordsExperimentAvailabilityCheckerProtocol { 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 e8b89c10ec7..7e4f6a9a623 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 @@ -339,6 +339,9 @@ struct InPersonPaymentsMenu_Previews: PreviewProvider { credentials: .init(authToken: ""), selectedSite: ServiceLocator.stores.sessionManager.defaultSitePublisher .map { $0?.toJetpackSite() } + .eraseToAnyPublisher(), + appPasswordSupportState: ApplicationPasswordsExperimentState() + .$isAvailableAndEnabled .eraseToAnyPublisher() ) ) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index 684760235f1..6de6516aaba 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -72,14 +72,17 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { let credentials = stores.sessionManager.defaultCredentials let selectedSite = stores.sessionManager.defaultSitePublisher.map { $0?.toJetpackSite() }.eraseToAnyPublisher() + let appPasswordSupportState = ApplicationPasswordsExperimentState().$isAvailableAndEnabled.eraseToAnyPublisher() self.systemStatusService = systemStatusService ?? POSSystemStatusService( credentials: credentials, selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState, storageManager: ServiceLocator.storageManager ) self.siteSettingService = siteSettingService ?? POSSiteSettingService( credentials: credentials, - selectedSite: selectedSite + selectedSite: selectedSite, + appPasswordSupportState: appPasswordSupportState ) } diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index 28fdb8edcda..c86ad62c196 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -116,6 +116,9 @@ final class HubMenuViewModel: ObservableObject { credentials: credentials, selectedSite: stores.sessionManager.defaultSitePublisher .map { $0?.toJetpackSite() } + .eraseToAnyPublisher(), + appPasswordSupportState: ApplicationPasswordsExperimentState() + .$isAvailableAndEnabled .eraseToAnyPublisher() ) ) diff --git a/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift b/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift index fe482d726eb..29ea47fc75b 100644 --- a/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift @@ -310,7 +310,7 @@ private extension JetpackSetupCoordinator { @MainActor func loadWPComAccountUsername(authToken: String) async -> String? { await withCheckedContinuation { continuation in - let network = AlamofireNetwork(credentials: Credentials(authToken: authToken), selectedSite: nil) + let network = AlamofireNetwork(credentials: Credentials(authToken: authToken), selectedSite: nil, appPasswordSupportState: nil) let accountAction = JetpackConnectionAction.loadWPComAccount(network: network) { account in continuation.resume(returning: account?.username) } diff --git a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift index 3848ea6ca84..830a47696fc 100644 --- a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift +++ b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift @@ -29,7 +29,7 @@ class AuthenticatedState: StoresManagerState { /// private let trackEventRequestNotificationHandler: TrackEventRequestNotificationHandler - let network: AlamofireNetwork + private let network: AlamofireNetwork private var cancellables: Set = [] @@ -37,6 +37,8 @@ class AuthenticatedState: StoresManagerState { /// private(set) var posCatalogSyncCoordinator: POSCatalogSyncCoordinator? + private var appPasswordSupportState: PassthroughSubject + /// Designated Initializer /// init(credentials: Credentials, sessionManager: SessionManagerProtocol) { @@ -46,7 +48,12 @@ class AuthenticatedState: StoresManagerState { .map { $0?.toJetpackSite() } .eraseToAnyPublisher() - self.network = AlamofireNetwork(credentials: credentials, selectedSite: site) + self.appPasswordSupportState = .init() + self.network = AlamofireNetwork( + credentials: credentials, + selectedSite: site, + appPasswordSupportState: appPasswordSupportState.eraseToAnyPublisher() + ) var services: [ActionsProcessor] = [ AppSettingsStore(dispatcher: dispatcher, @@ -149,8 +156,14 @@ class AuthenticatedState: StoresManagerState { if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1), let fullSyncService = POSCatalogFullSyncService(credentials: credentials, selectedSite: site, + appPasswordSupportState: appPasswordSupportState.eraseToAnyPublisher(), grdbManager: ServiceLocator.grdbManager), - let incrementalSyncService = POSCatalogIncrementalSyncService(credentials: credentials, selectedSite: site, grdbManager: ServiceLocator.grdbManager) { + let incrementalSyncService = POSCatalogIncrementalSyncService( + credentials: credentials, + selectedSite: site, + appPasswordSupportState: appPasswordSupportState.eraseToAnyPublisher(), + grdbManager: ServiceLocator.grdbManager + ) { let syncRemote = POSCatalogSyncRemote(network: network) let catalogSizeChecker = POSCatalogSizeChecker(syncRemote: syncRemote) posCatalogSyncCoordinator = POSCatalogSyncCoordinator( @@ -166,11 +179,7 @@ class AuthenticatedState: StoresManagerState { trackEventRequestNotificationHandler = TrackEventRequestNotificationHandler() startListeningToNotifications() - observeExperimentFeatureSettings() - - DispatchQueue.main.async { - self.checkApplicationPasswordExperimentFeatureState() - } + observeAppPasswordSupportState() } /// Convenience Initializer @@ -224,16 +233,6 @@ private extension AuthenticatedState { func tunnelTimeoutWasReceived(note: Notification) { ServiceLocator.analytics.track(.jetpackTunnelTimeout) } - - func checkApplicationPasswordExperimentFeatureState() { - Task { - let isAvailableAndEnabled = await ApplicationPasswordsExperimentState().isAvailableAndEnabled - - await MainActor.run { - network.updateAppPasswordSwitching(enabled: isAvailableAndEnabled) - } - } - } } @@ -250,19 +249,18 @@ private extension AuthenticatedState { } } -/// Observe beta experiment settings private extension AuthenticatedState { - func observeExperimentFeatureSettings() { - ServiceLocator - .generalAppSettings - .betaFeatureEnabledPublisher( - .applicationPasswords - ) - .dropFirst() - .removeDuplicates() - .sink { [weak self] _ in - self?.checkApplicationPasswordExperimentFeatureState() - } - .store(in: &cancellables) + func observeAppPasswordSupportState() { + DispatchQueue.main.async { [self] in + /// The state needs to be created on the main thread to avoid creating a new ServiceLocator.stores in a different thread. + /// Without this, race condition can happen. + ApplicationPasswordsExperimentState() + .$isAvailableAndEnabled + .receive(on: DispatchQueue.main) + .sink { [weak self] enabled in + self?.appPasswordSupportState.send(enabled) + } + .store(in: &cancellables) + } } } diff --git a/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift b/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift index 113ebaf6193..90aaadebd17 100644 --- a/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift +++ b/WooCommerce/Classes/Yosemite/DeauthenticatedState.swift @@ -15,7 +15,7 @@ class DeauthenticatedState: StoresManagerState { init() { // Used for logged-out state without a WPCOM auth token. - let network = AlamofireNetwork(credentials: nil, selectedSite: nil) + let network = AlamofireNetwork(credentials: nil, selectedSite: nil, appPasswordSupportState: nil) services = [ JetpackConnectionStore(dispatcher: dispatcher), AccountCreationStore(dotcomClientID: ApiCredentials.dotcomAppId, diff --git a/WooCommerce/NotificationExtension/OrderNotificationDataService.swift b/WooCommerce/NotificationExtension/OrderNotificationDataService.swift index c30b173aba9..a0d7ab1887d 100644 --- a/WooCommerce/NotificationExtension/OrderNotificationDataService.swift +++ b/WooCommerce/NotificationExtension/OrderNotificationDataService.swift @@ -26,7 +26,7 @@ final class OrderNotificationDataService { private let network: AlamofireNetwork init(credentials: Credentials) { - network = AlamofireNetwork(credentials: credentials, selectedSite: nil) // opt out from network switching + network = AlamofireNetwork(credentials: credentials, selectedSite: nil, appPasswordSupportState: nil) // opt out from network switching ordersRemote = OrdersRemote(network: network) notesRemote = NotificationsRemote(network: network) } diff --git a/WooCommerce/StoreWidgets/StoreInfoDataService.swift b/WooCommerce/StoreWidgets/StoreInfoDataService.swift index 4c2e9290822..1855ebf1e2e 100644 --- a/WooCommerce/StoreWidgets/StoreInfoDataService.swift +++ b/WooCommerce/StoreWidgets/StoreInfoDataService.swift @@ -38,7 +38,7 @@ final class StoreInfoDataService { private let isAuthenticatedWithoutWPCom: Bool init(credentials: Credentials) { - network = AlamofireNetwork(credentials: credentials, selectedSite: nil) // opt out from network switching + network = AlamofireNetwork(credentials: credentials, selectedSite: nil, appPasswordSupportState: nil) // opt out from network switching orderStatsRemoteV4 = OrderStatsRemoteV4(network: network) siteStatsRemote = SiteStatsRemote(network: network) if case .wpcom = credentials { diff --git a/WooCommerce/Woo Watch App/Orders/OrdersDataService.swift b/WooCommerce/Woo Watch App/Orders/OrdersDataService.swift index 166b3e77785..d9a29be8420 100644 --- a/WooCommerce/Woo Watch App/Orders/OrdersDataService.swift +++ b/WooCommerce/Woo Watch App/Orders/OrdersDataService.swift @@ -13,7 +13,7 @@ final class OrdersDataService { private let network: AlamofireNetwork init(credentials: Credentials) { - network = AlamofireNetwork(credentials: credentials, selectedSite: nil) // opt out from network switching + network = AlamofireNetwork(credentials: credentials, selectedSite: nil, appPasswordSupportState: nil) // opt out from network switching ordersRemote = OrdersRemote(network: network) } diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleCouponsControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleCouponsControllerTests.swift index 64fef0831ac..bfae866df05 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleCouponsControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleCouponsControllerTests.swift @@ -15,6 +15,7 @@ struct PointOfSaleCouponsControllerTests { currencySettings: CurrencySettings(), credentials: nil, selectedSite: Just(Site.fake()).map { $0.toJetpackSite() }.eraseToAnyPublisher(), + appPasswordSupportState: Just(false).eraseToAnyPublisher(), storage: MockStorageManager() ) } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Settings/ApplicationPasswordsExperimentStateTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Settings/ApplicationPasswordsExperimentStateTests.swift index 79202d23308..5ac3b8978d1 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Settings/ApplicationPasswordsExperimentStateTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Settings/ApplicationPasswordsExperimentStateTests.swift @@ -1,11 +1,13 @@ import XCTest import Yosemite +import Combine @testable import WooCommerce final class ApplicationPasswordsExperimentStateTests: XCTestCase { private var sut: ApplicationPasswordsExperimentState! private var availabilityChecker: ApplicationPasswordsExperimentAvailabilityCheckerMock! private var stores: MockStoresManager! + private var cancellables: AnyCancellable? override func setUp() { super.setUp() @@ -18,10 +20,11 @@ final class ApplicationPasswordsExperimentStateTests: XCTestCase { sut = nil availabilityChecker = nil stores = nil + cancellables = nil super.tearDown() } - func test_when_available_and_enabled_then_isAvailableAndEnabled_returns_true() async { + func test_when_available_and_enabled_then_isAvailableAndEnabled_stream_returns_true() { // Given availabilityChecker.mockedAvailability = true stores.whenReceivingAction(ofType: AppSettingsAction.self) { action in @@ -31,13 +34,19 @@ final class ApplicationPasswordsExperimentStateTests: XCTestCase { } // When - let result = await sut.isAvailableAndEnabled + var values: [Bool] = [] + cancellables = sut.$isAvailableAndEnabled + .sink { result in + values.append(result) + } // Then - XCTAssertTrue(result) + waitUntil { + values == [true, true] + } } - func test_when_available_and_disabled_then_isAvailableAndEnabled_returns_false() async { + func test_when_available_and_disabled_then_isAvailableAndEnabled_stream_returns_false() { // Given availabilityChecker.mockedAvailability = true stores.whenReceivingAction(ofType: AppSettingsAction.self) { action in @@ -47,13 +56,19 @@ final class ApplicationPasswordsExperimentStateTests: XCTestCase { } // When - let result = await sut.isAvailableAndEnabled + var values: [Bool] = [] + cancellables = sut.$isAvailableAndEnabled + .sink { result in + values.append(result) + } // Then - XCTAssertFalse(result) + waitUntil { + values == [true, false] + } } - func test_when_unavailable_and_enabled_then_isAvailableAndEnabled_returns_false() async { + func test_when_unavailable_and_enabled_then_isAvailableAndEnabled_stream_returns_false() { // Given availabilityChecker.mockedAvailability = false stores.whenReceivingAction(ofType: AppSettingsAction.self) { action in @@ -63,13 +78,19 @@ final class ApplicationPasswordsExperimentStateTests: XCTestCase { } // When - let result = await sut.isAvailableAndEnabled + var values: [Bool] = [] + cancellables = sut.$isAvailableAndEnabled + .sink { result in + values.append(result) + } // Then - XCTAssertFalse(result) + waitUntil { + values == [true, false] + } } - func test_when_unavailable_and_disabled_then_isAvailableAndEnabled_returns_false() async { + func test_when_unavailable_and_disabled_then_isAvailableAndEnabled_stream_returns_false() { // Given availabilityChecker.mockedAvailability = false stores.whenReceivingAction(ofType: AppSettingsAction.self) { action in @@ -79,10 +100,16 @@ final class ApplicationPasswordsExperimentStateTests: XCTestCase { } // When - let result = await sut.isAvailableAndEnabled + var values: [Bool] = [] + cancellables = sut.$isAvailableAndEnabled + .sink { result in + values.append(result) + } // Then - XCTAssertFalse(result) + waitUntil { + values == [true, false] + } } } From 43e4592269d15f5a4f43881be1604bd195a007f1 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 17 Sep 2025 19:02:11 +0700 Subject: [PATCH 3/4] Hold strong ref of ApplicationPasswordsExperimentState to keep streams alive --- .../POS/TabBar/POSTabCoordinator.swift | 21 ++++++++++-------- .../ApplicationPasswordsExperimentState.swift | 22 +++++++++++++++++-- .../POS/POSTabEligibilityChecker.swift | 4 +++- .../Hub Menu/HubMenuViewModel.swift | 3 ++- .../Classes/Yosemite/AuthenticatedState.swift | 5 ++++- 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index 0aee0e7418b..a08d1b6348f 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -37,7 +37,7 @@ final class POSTabCoordinator { PointOfSaleItemFetchStrategyFactory(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, - appPasswordSupportState: appPasswordSupportState) + appPasswordSupportState: isAppPasswordSupported) }() private lazy var posPopularItemFetchStrategyFactory: PointOfSaleFixedItemFetchStrategyFactory = { @@ -49,7 +49,7 @@ final class POSTabCoordinator { currencySettings: currencySettings, credentials: credentials, selectedSite: defaultSitePublisher, - appPasswordSupportState: appPasswordSupportState, + appPasswordSupportState: isAppPasswordSupported, storage: storageManager) }() @@ -58,7 +58,7 @@ final class POSTabCoordinator { currencySettings: currencySettings, credentials: credentials, selectedSite: defaultSitePublisher, - appPasswordSupportState: appPasswordSupportState, + appPasswordSupportState: isAppPasswordSupported, storage: storageManager) }() @@ -66,15 +66,17 @@ final class POSTabCoordinator { PointOfSaleBarcodeScanService(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, - appPasswordSupportState: appPasswordSupportState, + appPasswordSupportState: isAppPasswordSupported, currencySettings: currencySettings) }() /// Publisher to send to `AlamofireNetwork` for request authentication mode switching. private let defaultSitePublisher: AnyPublisher + private let appPasswordSupportState: ApplicationPasswordsExperimentState + /// Publisher to send to `AlamofireNetwork` the state of app password support for JP sites - private let appPasswordSupportState: AnyPublisher + private let isAppPasswordSupported: AnyPublisher init(siteID: Int64, tabContainerController: TabContainerController, @@ -90,6 +92,7 @@ final class POSTabCoordinator { .map { $0?.toJetpackSite() } .eraseToAnyPublisher() self.appPasswordSupportState = ApplicationPasswordsExperimentState() + self.isAppPasswordSupported = appPasswordSupportState .$isAvailableAndEnabled .eraseToAnyPublisher() self.tabContainerController = tabContainerController @@ -119,7 +122,7 @@ private extension POSTabCoordinator { let settingsService = PointOfSaleSettingsService(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, - appPasswordSupportState: appPasswordSupportState, + appPasswordSupportState: isAppPasswordSupported, storage: storageManager) let pluginsService = PluginsService(storageManager: storageManager) let siteTimezone = storesManager.sessionManager.defaultSite?.siteTimezone ?? .current @@ -127,11 +130,11 @@ private extension POSTabCoordinator { if let receiptService = POSReceiptService(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, - appPasswordSupportState: appPasswordSupportState), + appPasswordSupportState: isAppPasswordSupported), let orderService = POSOrderService(siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, - appPasswordSupportState: appPasswordSupportState), + appPasswordSupportState: isAppPasswordSupported), #available(iOS 17.0, *) { let receiptSender = POSReceiptSender(siteID: siteID, orderService: orderService, @@ -156,7 +159,7 @@ private extension POSTabCoordinator { siteID: siteID, credentials: credentials, selectedSite: defaultSitePublisher, - appPasswordSupportState: appPasswordSupportState, + appPasswordSupportState: isAppPasswordSupported, currencyFormatter: CurrencyFormatter(currencySettings: currencySettings) ) ), diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/ApplicationPasswordsExperimentState.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/ApplicationPasswordsExperimentState.swift index 658f41d9b6a..bff04ed9f0e 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/ApplicationPasswordsExperimentState.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Beta features/ApplicationPasswordsExperimentState.swift @@ -1,17 +1,24 @@ +import Combine import Foundation import Yosemite +import struct Storage.GeneralAppSettingsStorage final class ApplicationPasswordsExperimentState { private let stores: StoresManager private let availabilityChecker: ApplicationPasswordsExperimentAvailabilityCheckerProtocol + private let generalAppSettings: GeneralAppSettingsStorage + + private var experimentalFlagSubscription: AnyCancellable? init( stores: StoresManager = ServiceLocator.stores, - availabilityChecker: ApplicationPasswordsExperimentAvailabilityCheckerProtocol = ApplicationPasswordsExperimentAvailabilityChecker() + availabilityChecker: ApplicationPasswordsExperimentAvailabilityCheckerProtocol = ApplicationPasswordsExperimentAvailabilityChecker(), + generalAppSettings: GeneralAppSettingsStorage = ServiceLocator.generalAppSettings ) { self.stores = stores self.availabilityChecker = availabilityChecker - updateAvailability() + self.generalAppSettings = generalAppSettings + observeExperimentalFlag() } @Published private(set) var isAvailableAndEnabled: Bool = true @@ -36,6 +43,17 @@ final class ApplicationPasswordsExperimentState { isAvailableAndEnabled = isAvailable && isEnabled } } + + private func observeExperimentalFlag() { + experimentalFlagSubscription = generalAppSettings + .betaFeatureEnabledPublisher( + .applicationPasswords + ) + .removeDuplicates() + .sink { [weak self] _ in + self?.updateAvailability() + } + } } protocol ApplicationPasswordsExperimentAvailabilityCheckerProtocol { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index 6de6516aaba..a377934b66a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -54,6 +54,7 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { private let featureFlagService: FeatureFlagService private let systemStatusService: POSSystemStatusServiceProtocol private let siteSettingService: POSSiteSettingServiceProtocol + private let appPasswordSupportState: ApplicationPasswordsExperimentState init(siteID: Int64, userInterfaceIdiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom, @@ -69,10 +70,11 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { self.eligibilityService = eligibilityService self.stores = stores self.featureFlagService = featureFlagService + self.appPasswordSupportState = ApplicationPasswordsExperimentState() let credentials = stores.sessionManager.defaultCredentials let selectedSite = stores.sessionManager.defaultSitePublisher.map { $0?.toJetpackSite() }.eraseToAnyPublisher() - let appPasswordSupportState = ApplicationPasswordsExperimentState().$isAvailableAndEnabled.eraseToAnyPublisher() + let appPasswordSupportState = appPasswordSupportState.$isAvailableAndEnabled.eraseToAnyPublisher() self.systemStatusService = systemStatusService ?? POSSystemStatusService( credentials: credentials, selectedSite: selectedSite, diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index c86ad62c196..d638ea8fa46 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -86,6 +86,7 @@ final class HubMenuViewModel: ObservableObject { private let blazeEligibilityChecker: BlazeEligibilityCheckerProtocol private let googleAdsEligibilityChecker: GoogleAdsEligibilityChecker private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol + private let appPasswordSupportState = ApplicationPasswordsExperimentState() private(set) lazy var inboxViewModel = InboxViewModel(siteID: siteID) @@ -117,7 +118,7 @@ final class HubMenuViewModel: ObservableObject { selectedSite: stores.sessionManager.defaultSitePublisher .map { $0?.toJetpackSite() } .eraseToAnyPublisher(), - appPasswordSupportState: ApplicationPasswordsExperimentState() + appPasswordSupportState: appPasswordSupportState .$isAvailableAndEnabled .eraseToAnyPublisher() ) diff --git a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift index 830a47696fc..3dd608c144d 100644 --- a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift +++ b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift @@ -37,6 +37,7 @@ class AuthenticatedState: StoresManagerState { /// private(set) var posCatalogSyncCoordinator: POSCatalogSyncCoordinator? + private var appPasswordSupportStateHandler: ApplicationPasswordsExperimentState? private var appPasswordSupportState: PassthroughSubject /// Designated Initializer @@ -254,7 +255,9 @@ private extension AuthenticatedState { DispatchQueue.main.async { [self] in /// The state needs to be created on the main thread to avoid creating a new ServiceLocator.stores in a different thread. /// Without this, race condition can happen. - ApplicationPasswordsExperimentState() + let appPasswordSupportStateHandler = ApplicationPasswordsExperimentState() + self.appPasswordSupportStateHandler = appPasswordSupportStateHandler // strong ref to keep the stream alive + appPasswordSupportStateHandler .$isAvailableAndEnabled .receive(on: DispatchQueue.main) .sink { [weak self] enabled in From 946237335dd81bf44c168c63c477c1160e8ff072 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 17 Sep 2025 22:08:00 +0700 Subject: [PATCH 4/4] Ignore periphery warning --- WooCommerce/Classes/Yosemite/AuthenticatedState.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift index 3dd608c144d..5177f8695a6 100644 --- a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift +++ b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift @@ -37,6 +37,7 @@ class AuthenticatedState: StoresManagerState { /// private(set) var posCatalogSyncCoordinator: POSCatalogSyncCoordinator? + // periphery:ignore - keep strong reference to keep the state publisher alive private var appPasswordSupportStateHandler: ApplicationPasswordsExperimentState? private var appPasswordSupportState: PassthroughSubject