From aa8edab6cfd60c4108560da3c44624cff24c06f7 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 6 Jan 2023 15:22:28 +0000 Subject: [PATCH 1/4] 8314 Expose Stripe checkSupport func from Hardware We will need the checkSupport function Stripe provide to check whether a given device supports the built-in card reader. The simpler Apple version is more permissive, e.g. it says support is available from iOS 15.5, however via Stripe, 16.0 is the minimum. --- .../CardReader/CardReaderService.swift | 6 ++++ .../CardReaderType+Stripe.swift | 15 ++++++++++ .../NoOpCardReaderService.swift | 8 +++++- .../StripeCardReaderService.swift | 28 +++++++++++++++++-- .../MockCardReaderService.swift | 17 +++++++++++ 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/Hardware/Hardware/CardReader/CardReaderService.swift b/Hardware/Hardware/CardReader/CardReaderService.swift index 107dce7b011..fbc8fbfbd5e 100644 --- a/Hardware/Hardware/CardReader/CardReaderService.swift +++ b/Hardware/Hardware/CardReader/CardReaderService.swift @@ -18,6 +18,12 @@ public protocol CardReaderService { // MARK: - Commands + /// Checks for support of a given reader type and discovery method combination. Does not start discovery. + /// + func checkSupport(for cardReaderType: CardReaderType, + configProvider: CardReaderConfigProvider, + discoveryMethod: CardReaderDiscoveryMethod) -> Bool + /// Starts the service. /// That could imply, for example, that the reader discovery process starts func start(_ configProvider: CardReaderConfigProvider, discoveryMethod: CardReaderDiscoveryMethod) throws diff --git a/Hardware/Hardware/CardReader/StripeCardReader/CardReaderType+Stripe.swift b/Hardware/Hardware/CardReader/StripeCardReader/CardReaderType+Stripe.swift index f6c9391dc1a..dc264833a64 100644 --- a/Hardware/Hardware/CardReader/StripeCardReader/CardReaderType+Stripe.swift +++ b/Hardware/Hardware/CardReader/StripeCardReader/CardReaderType+Stripe.swift @@ -19,5 +19,20 @@ extension CardReaderType { return .other } } + + func toStripe() -> DeviceType? { + switch self { + case .chipper: + return .chipper2X + case .stripeM2: + return .stripeM2 + case .wisepad3: + return .wisePad3 + case .appleBuiltIn: + return .appleBuiltIn + case .other: + return nil + } + } } #endif diff --git a/Hardware/Hardware/CardReader/StripeCardReader/NoOpCardReaderService.swift b/Hardware/Hardware/CardReader/StripeCardReader/NoOpCardReaderService.swift index 1986fe6fa9e..3517bda6da3 100644 --- a/Hardware/Hardware/CardReader/StripeCardReader/NoOpCardReaderService.swift +++ b/Hardware/Hardware/CardReader/StripeCardReader/NoOpCardReaderService.swift @@ -1,5 +1,5 @@ import Combine -/// The adapter wrapping the Stripe Terminal SDK +/// A no-op replacement for the adapter wrapping the Stripe Terminal SDK public struct NoOpCardReaderService: CardReaderService { // MARK: - Queries /// The publisher that emits the list of discovered readers whenever the service discovers a new reader. @@ -18,6 +18,12 @@ public struct NoOpCardReaderService: CardReaderService { public init() {} // MARK: - Commands + public func checkSupport(for cardReaderType: CardReaderType, + configProvider: CardReaderConfigProvider, + discoveryMethod: CardReaderDiscoveryMethod) -> Bool { + return false + } + /// Starts the service. /// That could imply, for example, that the reader discovery process starts public func start(_ configProvider: CardReaderConfigProvider, discoveryMethod: CardReaderDiscoveryMethod) throws { diff --git a/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift b/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift index 7cb5c1ca31c..9d9d5d5e15b 100644 --- a/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift +++ b/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift @@ -62,8 +62,27 @@ extension StripeCardReaderService: CardReaderService { // MARK: - CardReaderService conformance. Commands - public func start(_ configProvider: CardReaderConfigProvider, - discoveryMethod: CardReaderDiscoveryMethod) throws { + public func checkSupport(for cardReaderType: CardReaderType, + configProvider: CardReaderConfigProvider, + discoveryMethod: CardReaderDiscoveryMethod) -> Bool { + guard let deviceType = cardReaderType.toStripe() else { + return false + } + + prepare(using: configProvider) + + let result = Terminal.shared.supportsReaders(of: deviceType, + discoveryMethod: discoveryMethod.toStripe(), + simulated: shouldUseSimulatedCardReader) + switch result { + case .success: + return true + case .failure: + return false + } + } + + func prepare(using configProvider: CardReaderConfigProvider) { setConfigProvider(configProvider) Terminal.setLogListener { message in @@ -75,6 +94,11 @@ extension StripeCardReaderService: CardReaderService { DDLogDebug("💳 [StripeTerminal] \(message)") } Terminal.shared.logLevel = terminalLogLevel + } + + public func start(_ configProvider: CardReaderConfigProvider, + discoveryMethod: CardReaderDiscoveryMethod) throws { + prepare(using: configProvider) if shouldUseSimulatedCardReader { // You can test with different reader software update scenarios. diff --git a/Yosemite/YosemiteTests/Mocks/CardPresentPayments/MockCardReaderService.swift b/Yosemite/YosemiteTests/Mocks/CardPresentPayments/MockCardReaderService.swift index 5d8830eb041..82af7234f38 100644 --- a/Yosemite/YosemiteTests/Mocks/CardPresentPayments/MockCardReaderService.swift +++ b/Yosemite/YosemiteTests/Mocks/CardPresentPayments/MockCardReaderService.swift @@ -50,6 +50,12 @@ final class MockCardReaderService: CardReaderService { /// The publisher to return in `capturePayment`. private var capturePaymentPublisher: AnyPublisher? + + var didCheckSupport = false + var spyCheckSupportCardReaderType: CardReaderType? = nil + var spyCheckSupportConfigProvider: CardReaderConfigProvider? = nil + var spyCheckSupportDiscoveryMethod: CardReaderDiscoveryMethod? = nil + /// The future to return in `waitForInsertedCardToBeRemoved`. private var waitForInsertedCardToBeRemovedFuture: Future? @@ -61,6 +67,17 @@ final class MockCardReaderService: CardReaderService { } + func checkSupport(for cardReaderType: Hardware.CardReaderType, + configProvider: Hardware.CardReaderConfigProvider, + discoveryMethod: Hardware.CardReaderDiscoveryMethod) -> Bool { + didCheckSupport = true + spyCheckSupportCardReaderType = cardReaderType + spyCheckSupportConfigProvider = configProvider + spyCheckSupportDiscoveryMethod = discoveryMethod + + return true + } + func start(_ configProvider: Hardware.CardReaderConfigProvider, discoveryMethod: Hardware.CardReaderDiscoveryMethod) throws { didHitStart = true didReceiveAConfigurationProvider = true From 469f8a67bf6fc3cc6f8d80c3caf567c6fb28fcb9 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 9 Jan 2023 13:39:31 +0000 Subject: [PATCH 2/4] 8314 Add checkDeviceSupport to Card Present Store --- .../Actions/CardPresentPaymentAction.swift | 5 +++ .../Stores/CardPresentPaymentStore.swift | 40 +++++++++++++------ .../Stores/CardPresentPaymentStoreTests.swift | 33 +++++++++++++++ 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/Yosemite/Yosemite/Actions/CardPresentPaymentAction.swift b/Yosemite/Yosemite/Actions/CardPresentPaymentAction.swift index 25cb31572a5..1e56df145e6 100644 --- a/Yosemite/Yosemite/Actions/CardPresentPaymentAction.swift +++ b/Yosemite/Yosemite/Actions/CardPresentPaymentAction.swift @@ -23,6 +23,11 @@ public enum CardPresentPaymentAction: Action { /// case loadAccounts(siteID: Int64, onCompletion: (Result) -> Void) + case checkDeviceSupport(siteID: Int64, + cardReaderType: CardReaderType, + discoveryMethod: CardReaderDiscoveryMethod, + onCompletion: (Bool) -> Void) + /// Start the Card Reader discovery process. /// case startCardReaderDiscovery(siteID: Int64, diff --git a/Yosemite/Yosemite/Stores/CardPresentPaymentStore.swift b/Yosemite/Yosemite/Stores/CardPresentPaymentStore.swift index 57465e819ed..2fbda6f8f31 100644 --- a/Yosemite/Yosemite/Stores/CardPresentPaymentStore.swift +++ b/Yosemite/Yosemite/Stores/CardPresentPaymentStore.swift @@ -84,6 +84,11 @@ public final class CardPresentPaymentStore: Store { case .loadAccounts(let siteID, let onCompletion): loadAccounts(siteID: siteID, onCompletion: onCompletion) + case .checkDeviceSupport(let siteID, let cardReaderType, let discoveryMethod, let completion): + checkDeviceSupport(siteID: siteID, + cardReaderType: cardReaderType, + discoveryMethod: discoveryMethod, + onCompletion: completion) case .startCardReaderDiscovery(let siteID, let discoveryMethod, let onReaderDiscovered, let onError): startCardReaderDiscovery(siteID: siteID, discoveryMethod: discoveryMethod, @@ -128,19 +133,30 @@ public final class CardPresentPaymentStore: Store { // MARK: - Services // private extension CardPresentPaymentStore { - func startCardReaderDiscovery(siteID: Int64, - discoveryMethod: CardReaderDiscoveryMethod, - onReaderDiscovered: @escaping (_ readers: [CardReader]) -> Void, - onError: @escaping (Error) -> Void) { + func checkDeviceSupport(siteID: Int64, + cardReaderType: CardReaderType, + discoveryMethod: CardReaderDiscoveryMethod, + onCompletion: (Bool) -> Void) { + prepareConfigProvider(siteID: siteID) + onCompletion(cardReaderService.checkSupport(for: cardReaderType, configProvider: commonReaderConfigProvider, discoveryMethod: discoveryMethod)) + } + + func prepareConfigProvider(siteID: Int64) { + switch usingBackend { + case .wcpay: + commonReaderConfigProvider.setContext(siteID: siteID, remote: self.remote) + case .stripe: + commonReaderConfigProvider.setContext(siteID: siteID, remote: self.stripeRemote) + } + } + + func startCardReaderDiscovery(siteID: Int64, + discoveryMethod: CardReaderDiscoveryMethod, + onReaderDiscovered: @escaping (_ readers: [CardReader]) -> Void, + onError: @escaping (Error) -> Void) { + prepareConfigProvider(siteID: siteID) do { - switch usingBackend { - case .wcpay: - commonReaderConfigProvider.setContext(siteID: siteID, remote: self.remote) - try cardReaderService.start(commonReaderConfigProvider, discoveryMethod: discoveryMethod) - case .stripe: - commonReaderConfigProvider.setContext(siteID: siteID, remote: self.stripeRemote) - try cardReaderService.start(commonReaderConfigProvider, discoveryMethod: discoveryMethod) - } + try cardReaderService.start(commonReaderConfigProvider, discoveryMethod: discoveryMethod) } catch { return onError(error) } diff --git a/Yosemite/YosemiteTests/Stores/CardPresentPaymentStoreTests.swift b/Yosemite/YosemiteTests/Stores/CardPresentPaymentStoreTests.swift index 64013abb265..98b566caf3e 100644 --- a/Yosemite/YosemiteTests/Stores/CardPresentPaymentStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/CardPresentPaymentStoreTests.swift @@ -775,4 +775,37 @@ final class CardPresentPaymentStoreTests: XCTestCase { // Then XCTAssertEqual(result, account) } + + func test_checkDeviceSupport_action_passes_configuration_provider_to_service() { + let cardPresentStore = CardPresentPaymentStore(dispatcher: dispatcher, + storageManager: storageManager, + network: network, + cardReaderService: mockCardReaderService) + + let action = CardPresentPaymentAction.checkDeviceSupport(siteID: sampleSiteID, + cardReaderType: .appleBuiltIn, + discoveryMethod: .localMobile, + onCompletion: { _ in }) + + cardPresentStore.onAction(action) + + XCTAssertNotNil(mockCardReaderService.spyCheckSupportConfigProvider) + } + + func test_checkDeviceSupport_action_passes_reader_type_and_discovery_method_to_service() { + let cardPresentStore = CardPresentPaymentStore(dispatcher: dispatcher, + storageManager: storageManager, + network: network, + cardReaderService: mockCardReaderService) + + let action = CardPresentPaymentAction.checkDeviceSupport(siteID: sampleSiteID, + cardReaderType: .chipper, + discoveryMethod: .bluetoothScan, + onCompletion: { _ in }) + + cardPresentStore.onAction(action) + + assertEqual(.bluetoothScan, mockCardReaderService.spyCheckSupportDiscoveryMethod) + assertEqual(.chipper, mockCardReaderService.spyCheckSupportCardReaderType) + } } From 78267b25bceadb2ab58e0b578d7c44d12568aaf5 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 9 Jan 2023 13:42:18 +0000 Subject: [PATCH 3/4] 8314 Use checkDeviceSupport to show reader choice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the logic for when we show the `Tap to Pay on iPhone` or `Bluetooth reader` screen was in the preflight controller, using Apple’s API’s. Stripe’s support is more restricted than Apple’s, and has been exposed via the CardPresentPaymentsStore. This commit updates the PreflightController to use the exposed Stripe check. --- ...ardPresentPaymentPreflightController.swift | 23 ++++++++++--------- .../CollectOrderPaymentUseCase.swift | 4 +++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift b/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift index 9fbbaa88d98..0f0a4194d82 100644 --- a/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift +++ b/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift @@ -82,7 +82,8 @@ final class CardPresentPaymentPreflightController { analyticsTracker: analyticsTracker) } - func start() { + @MainActor + func start() async { configureBackend() observeConnectedReaders() // If we're already connected to a reader, return it @@ -94,7 +95,7 @@ final class CardPresentPaymentPreflightController { // TODO: Run onboarding if needed // Ask for a Reader type if supported by device/in country - guard localMobileReaderSupported(), + guard await localMobileReaderSupported(), configuration.supportedReaders.contains(.appleBuiltIn) else { // Attempt to find a bluetooth reader and connect @@ -120,16 +121,16 @@ final class CardPresentPaymentPreflightController { })) } - private func localMobileReaderSupported() -> Bool { - #if targetEnvironment(simulator) - return true - #else - if #available(iOS 15.4, *) { - return PaymentCardReader.isSupported - } else { - return false + @MainActor + private func localMobileReaderSupported() async -> Bool { + await withCheckedContinuation { continuation in + let action = CardPresentPaymentAction.checkDeviceSupport(siteID: siteID, + cardReaderType: .appleBuiltIn, + discoveryMethod: .localMobile) { result in + continuation.resume(returning: result) + } + stores.dispatch(action) } - #endif } private func handleConnectionResult(_ result: Result) { diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index a790437fba9..1ce9cb1cbdd 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -178,7 +178,9 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol { } .store(in: &cancellables) - preflightController?.start() + Task { + await preflightController?.start() + } } } From b2a9b8107b42e127e7cf9d20a76cbfbcd68b1f5b Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 9 Jan 2023 15:49:36 +0000 Subject: [PATCH 4/4] 8314 Update Stripe TTP requirements description --- WooCommerce/Classes/Model/BetaFeature.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/Model/BetaFeature.swift b/WooCommerce/Classes/Model/BetaFeature.swift index 71072c876a8..9994a6ab3ba 100644 --- a/WooCommerce/Classes/Model/BetaFeature.swift +++ b/WooCommerce/Classes/Model/BetaFeature.swift @@ -155,7 +155,7 @@ private extension BetaFeature { "phone's built in reader") static let tapToPayOnIPhoneDescription = NSLocalizedString( "Test out In-Person Payments using your phone's built-in card reader, as we get ready to launch. " + - "Supported on iPhone XS and newer phones, running iOS 15.5 or above.", + "Supported on iPhone XS and newer phones, running iOS 16 or above, for US-based stores.", comment: "Cell description on beta features screen to enable in-app purchases") } }