Skip to content

Commit 5233d70

Browse files
committed
8172 Use localMobile discovery under feature flag
1 parent ad05060 commit 5233d70

File tree

12 files changed

+208
-24
lines changed

12 files changed

+208
-24
lines changed

Hardware/Hardware.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
02B5147A28254ED300750B71 /* Codegen in Frameworks */ = {isa = PBXBuildFile; productRef = 02B5147928254ED300750B71 /* Codegen */; };
1212
030338102705F7D400764131 /* ReceiptTotalLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0303380F2705F7D400764131 /* ReceiptTotalLine.swift */; };
1313
035DBA3929251ED6003E5125 /* CardReaderInputOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DBA3829251ED6003E5125 /* CardReaderInputOptions.swift */; };
14+
035DBA41292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035DBA40292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift */; };
1415
039D948B2760C0660044EF38 /* NoOpCardReaderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039D948A2760C0660044EF38 /* NoOpCardReaderService.swift */; };
1516
03B440AA2754DFC400759429 /* UnderlyingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B440A92754DFC400759429 /* UnderlyingError.swift */; };
1617
03CF78D327C6710C00523706 /* interac.svg in Resources */ = {isa = PBXBuildFile; fileRef = 03CF78D227C6710B00523706 /* interac.svg */; };
@@ -155,6 +156,7 @@
155156
028C39DF28255CFE0007BA25 /* Models+Copiable.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Models+Copiable.generated.swift"; sourceTree = "<group>"; };
156157
0303380F2705F7D400764131 /* ReceiptTotalLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptTotalLine.swift; sourceTree = "<group>"; };
157158
035DBA3829251ED6003E5125 /* CardReaderInputOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderInputOptions.swift; sourceTree = "<group>"; };
159+
035DBA40292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderDiscoveryMethod.swift; sourceTree = "<group>"; };
158160
039D948A2760C0660044EF38 /* NoOpCardReaderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoOpCardReaderService.swift; sourceTree = "<group>"; };
159161
03B440A92754DFC400759429 /* UnderlyingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnderlyingError.swift; sourceTree = "<group>"; };
160162
03CF78D227C6710B00523706 /* interac.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = interac.svg; sourceTree = "<group>"; };
@@ -478,6 +480,7 @@
478480
D845BDCB262D9B7700A3E40F /* CardBrand+Stripe.swift */,
479481
D845BDC5262D9A4200A3E40F /* CardPresentDetails+Stripe.swift */,
480482
D8DF5F4925DD9F7A008AFE25 /* CardReader+Stripe.swift */,
483+
035DBA40292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift */,
481484
D865C61D261CE001006717B8 /* CardReaderEvent+Stripe.swift */,
482485
D8DF5F4D25DD9F91008AFE25 /* CardReaderType+Stripe.swift */,
483486
D89B8F1125DDCBCD0001C726 /* Charge+Stripe.swift */,
@@ -831,6 +834,7 @@
831834
D89B8F0C25DDC9D30001C726 /* ChargeStatus.swift in Sources */,
832835
035DBA3929251ED6003E5125 /* CardReaderInputOptions.swift in Sources */,
833836
E140F61C2668CDC900FDB5FF /* Logging.swift in Sources */,
837+
035DBA41292BBEB2003E5125 /* CardReaderDiscoveryMethod.swift in Sources */,
834838
03CF78D727DF9BE600523706 /* RefundParameters.swift in Sources */,
835839
03B440AA2754DFC400759429 /* UnderlyingError.swift in Sources */,
836840
E1E125AC26EB582B0068A9B0 /* CardReaderSoftwareUpdateState.swift in Sources */,

Hardware/Hardware/CardReader/CardReaderService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public protocol CardReaderService {
2020

2121
/// Starts the service.
2222
/// That could imply, for example, that the reader discovery process starts
23-
func start(_ configProvider: CardReaderConfigProvider) throws
23+
func start(_ configProvider: CardReaderConfigProvider, discoveryMethod: CardReaderDiscoveryMethod) throws
2424

2525
/// Cancels the discovery process.
2626
func cancelDiscovery() -> Future<Void, Error>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#if !targetEnvironment(macCatalyst)
2+
import Foundation
3+
import StripeTerminal
4+
5+
public enum CardReaderDiscoveryMethod {
6+
case localMobile
7+
case bluetoothProximity
8+
9+
func toStripe() -> DiscoveryMethod {
10+
switch self {
11+
case .localMobile:
12+
return .localMobile
13+
case .bluetoothProximity:
14+
return .bluetoothProximity
15+
}
16+
}
17+
}
18+
#endif

Hardware/Hardware/CardReader/StripeCardReader/NoOpCardReaderService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public struct NoOpCardReaderService: CardReaderService {
2020

2121
/// Starts the service.
2222
/// That could imply, for example, that the reader discovery process starts
23-
public func start(_ configProvider: CardReaderConfigProvider) throws {
23+
public func start(_ configProvider: CardReaderConfigProvider, discoveryMethod: CardReaderDiscoveryMethod) throws {
2424
// no-op
2525
}
2626

Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift

Lines changed: 123 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ extension StripeCardReaderService: CardReaderService {
5858

5959
// MARK: - CardReaderService conformance. Commands
6060

61-
public func start(_ configProvider: CardReaderConfigProvider) throws {
61+
public func start(_ configProvider: CardReaderConfigProvider,
62+
discoveryMethod: CardReaderDiscoveryMethod) throws {
6263
setConfigProvider(configProvider)
6364

6465
Terminal.setLogListener { message in
@@ -85,13 +86,12 @@ extension StripeCardReaderService: CardReaderService {
8586
}
8687

8788
let config = DiscoveryConfiguration(
88-
discoveryMethod: .bluetoothScan,
89+
discoveryMethod: discoveryMethod.toStripe(),
8990
simulated: shouldUseSimulatedCardReader
9091
)
9192

92-
// If we're using the simulated reader, we don't want to check for Bluetooth permissions
93-
// as the simulator won't have Bluetooth available.
94-
guard shouldUseSimulatedCardReader || CBCentralManager.authorization != .denied else {
93+
guard shouldSkipBluetoothCheck(discoveryConfiguration: config) ||
94+
CBCentralManager.authorization != .denied else {
9595
throw CardReaderServiceError.bluetoothDenied
9696
}
9797

@@ -123,6 +123,14 @@ extension StripeCardReaderService: CardReaderService {
123123
})
124124
}
125125

126+
127+
// If we're using the simulated reader, we don't want to check for Bluetooth permissions
128+
// as the simulator won't have Bluetooth available.
129+
// If we're using the built-in reader, bluetooth is not required.
130+
private func shouldSkipBluetoothCheck(discoveryConfiguration: DiscoveryConfiguration) -> Bool {
131+
shouldUseSimulatedCardReader || discoveryConfiguration.discoveryMethod == .localMobile
132+
}
133+
126134
public func cancelDiscovery() -> Future <Void, Error> {
127135
Future { [weak self] promise in
128136
/**
@@ -311,9 +319,20 @@ extension StripeCardReaderService: CardReaderService {
311319
}.eraseToAnyPublisher()
312320
}
313321

314-
return getBluetoothConfiguration(stripeReader).flatMap { configuration in
315-
self.connect(stripeReader, configuration: configuration)
316-
}.eraseToAnyPublisher()
322+
switch stripeReader.deviceType {
323+
case .appleBuiltIn:
324+
return getLocalMobileConfiguration(stripeReader).flatMap { configuration in
325+
self.connect(stripeReader, configuration: configuration)
326+
}
327+
.share()
328+
.eraseToAnyPublisher()
329+
default:
330+
return getBluetoothConfiguration(stripeReader).flatMap { configuration in
331+
self.connect(stripeReader, configuration: configuration)
332+
}
333+
.share()
334+
.eraseToAnyPublisher()
335+
}
317336
}
318337

319338
private func getBluetoothConfiguration(_ reader: StripeTerminal.Reader) -> Future<BluetoothConnectionConfiguration, Error> {
@@ -337,6 +356,27 @@ extension StripeCardReaderService: CardReaderService {
337356
}
338357
}
339358

359+
private func getLocalMobileConfiguration(_ reader: StripeTerminal.Reader) -> Future<LocalMobileConnectionConfiguration, Error> {
360+
return Future() { [weak self] promise in
361+
guard let self = self else {
362+
promise(.failure(CardReaderServiceError.connection()))
363+
return
364+
}
365+
366+
// TODO - If we've recently connected to this reader, use the cached locationId from the
367+
// Terminal SDK instead of making this fetch. See #5116 and #5087
368+
self.readerLocationProvider?.fetchDefaultLocationID { result in
369+
switch result {
370+
case .success(let locationId):
371+
return promise(.success(LocalMobileConnectionConfiguration(locationId: locationId)))
372+
case .failure(let error):
373+
let underlyingError = UnderlyingError(with: error)
374+
return promise(.failure(CardReaderServiceError.connection(underlyingError: underlyingError)))
375+
}
376+
}
377+
}
378+
}
379+
340380
public func connect(_ reader: StripeTerminal.Reader, configuration: BluetoothConnectionConfiguration) -> Future <CardReader, Error> {
341381
// Keep a copy of the battery level in case the connection fails due to low battery
342382
// If that happens, the reader object won't be accessible anymore, and we want to show
@@ -376,6 +416,40 @@ extension StripeCardReaderService: CardReaderService {
376416
}
377417
}
378418

419+
public func connect(_ reader: StripeTerminal.Reader, configuration: LocalMobileConnectionConfiguration) -> Future <CardReader, Error> {
420+
return Future { [weak self] promise in
421+
guard let self = self else {
422+
promise(.failure(CardReaderServiceError.connection()))
423+
return
424+
}
425+
426+
Terminal.shared.connectLocalMobileReader(reader, delegate: self, connectionConfig: configuration) { [weak self] (reader, error) in
427+
guard let self = self else {
428+
promise(.failure(CardReaderServiceError.connection()))
429+
return
430+
}
431+
// Clear cached readers, as per Stripe's documentation.
432+
self.discoveredStripeReadersCache.clear()
433+
434+
if let error = error {
435+
let underlyingError = UnderlyingError(with: error)
436+
// Starting with StripeTerminal 2.0, required software updates happen transparently on connection
437+
// Any error related to that will be reported here, but we don't want to treat it as a connection error
438+
let serviceError: CardReaderServiceError = underlyingError.isSoftwareUpdateError ?
439+
.softwareUpdate(underlyingError: underlyingError, batteryLevel: nil) :
440+
.connection(underlyingError: underlyingError)
441+
promise(.failure(serviceError))
442+
}
443+
444+
if let reader = reader {
445+
self.connectedReadersSubject.send([CardReader(reader: reader)])
446+
self.switchStatusToIdle()
447+
promise(.success(CardReader(reader: reader)))
448+
}
449+
}
450+
}
451+
}
452+
379453
public func installUpdate() -> Void {
380454
Terminal.shared.installAvailableUpdate()
381455
}
@@ -443,6 +517,7 @@ private extension StripeCardReaderService {
443517

444518
if underlyingError == .commandCancelled {
445519
DDLogWarn("💳 Warning: collect payment error cancelled. We actively ignore this error \(error)")
520+
promise(.failure(CardReaderServiceError.paymentCancellation(underlyingError: underlyingError)))
446521
}
447522

448523
}
@@ -696,6 +771,46 @@ extension StripeCardReaderService: BluetoothReaderDelegate {
696771
}
697772
}
698773

774+
extension StripeCardReaderService: LocalMobileReaderDelegate {
775+
public func localMobileReader(_ reader: Reader, didRequestReaderInput inputOptions: ReaderInputOptions = []) {
776+
sendReaderEvent(CardReaderEvent.make(stripeReaderInputOptions: inputOptions))
777+
}
778+
779+
public func localMobileReader(_ reader: Reader, didRequestReaderDisplayMessage displayMessage: ReaderDisplayMessage) {
780+
sendReaderEvent(CardReaderEvent.make(displayMessage: displayMessage))
781+
}
782+
783+
784+
// TODO: use a specific `deviceSetup` in these three functions instead of reusing the softwareUpdateSubject
785+
// https://github.com/woocommerce/woocommerce-ios/issues/8088
786+
public func localMobileReader(_ reader: Reader, didStartInstallingUpdate update: ReaderSoftwareUpdate, cancelable: Cancelable?) {
787+
softwareUpdateSubject.send(.started(cancelable: cancelable.map(StripeCancelable.init(cancelable:))))
788+
}
789+
790+
public func localMobileReader(_ reader: Reader, didReportReaderSoftwareUpdateProgress progress: Float) {
791+
softwareUpdateSubject.send(.installing(progress: progress))
792+
}
793+
794+
public func localMobileReader(_ reader: Reader, didFinishInstallingUpdate update: ReaderSoftwareUpdate?, error: Error?) {
795+
if let error = error {
796+
softwareUpdateSubject.send(.failed(
797+
error: CardReaderServiceError.softwareUpdate(underlyingError: UnderlyingError(with: error),
798+
batteryLevel: reader.batteryLevel?.doubleValue))
799+
)
800+
if let requiredDate = update?.requiredAt,
801+
requiredDate > Date() {
802+
softwareUpdateSubject.send(.available)
803+
} else {
804+
softwareUpdateSubject.send(.none)
805+
}
806+
} else {
807+
softwareUpdateSubject.send(.completed)
808+
connectedReadersSubject.send([CardReader(reader: reader)])
809+
softwareUpdateSubject.send(.none)
810+
}
811+
}
812+
}
813+
699814
// MARK: - Terminal delegate
700815
extension StripeCardReaderService: TerminalDelegate {
701816
public func terminal(_ terminal: Terminal, didReportUnexpectedReaderDisconnect reader: Reader) {

WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,13 @@ private extension CardReaderConnectionController {
321321
self.state = .searching
322322
var didAutoAdvance = false
323323

324+
// TODO: make this a choice for the user, when the switch is enabled
325+
let tapOnIphoneEnabled = ServiceLocator.generalAppSettings.settings.isTapToPayOnIPhoneSwitchEnabled
326+
let discoveryMethod: CardReaderDiscoveryMethod = tapOnIphoneEnabled ? .localMobile : .bluetoothProximity
327+
324328
let action = CardPresentPaymentAction.startCardReaderDiscovery(
325329
siteID: siteID,
330+
discoveryMethod: discoveryMethod,
326331
onReaderDiscovered: { [weak self] cardReaders in
327332
guard let self = self else {
328333
return

WooCommerce/WooCommerceTests/Stripe Integration Tests/StripeCardReaderIntegrationTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ final class StripeCardReaderIntegrationTests: XCTestCase {
3232
}
3333
}.store(in: &cancellables)
3434

35-
try! readerService.start(MockTokenProvider())
35+
try! readerService.start(MockTokenProvider(), discoveryMethod: .bluetoothProximity)
3636
wait(for: [receivedReaders], timeout: Constants.expectationTimeout)
3737
}
3838

@@ -68,7 +68,7 @@ final class StripeCardReaderIntegrationTests: XCTestCase {
6868
.fulfillOnCompletion(expectation: discoveredReaders)
6969
}.store(in: &cancellables)
7070

71-
try! readerService.start(MockTokenProvider())
71+
try! readerService.start(MockTokenProvider(), discoveryMethod: .bluetoothProximity)
7272
wait(for: [discoveredReaders], timeout: Constants.expectationTimeout)
7373
}
7474

@@ -105,7 +105,7 @@ final class StripeCardReaderIntegrationTests: XCTestCase {
105105
}
106106
}.store(in: &self.cancellables)
107107

108-
try! readerService.start(MockTokenProvider())
108+
try! readerService.start(MockTokenProvider(), discoveryMethod: .bluetoothProximity)
109109
wait(for: [discoveredReaders, connectedToReader, connectedreaderIsPublished], timeout: Constants.expectationTimeout)
110110
}
111111
}

Yosemite/Yosemite/Actions/CardPresentPaymentAction.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ public enum CardPresentPaymentAction: Action {
2525

2626
/// Start the Card Reader discovery process.
2727
///
28-
case startCardReaderDiscovery(siteID: Int64, onReaderDiscovered: ([CardReader]) -> Void, onError: (Error) -> Void)
28+
case startCardReaderDiscovery(siteID: Int64,
29+
discoveryMethod: CardReaderDiscoveryMethod,
30+
onReaderDiscovered: ([CardReader]) -> Void,
31+
onError: (Error) -> Void)
2932

3033
/// Cancels the Card Reader discovery process.
3134
///

Yosemite/Yosemite/Model/Model.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ public typealias User = Networking.User
137137
public typealias WooAPIVersion = Networking.WooAPIVersion
138138
public typealias StoredProductSettings = Networking.StoredProductSettings
139139
public typealias CardReader = Hardware.CardReader
140+
public typealias CardReaderDiscoveryMethod = Hardware.CardReaderDiscoveryMethod
140141
public typealias CardReaderEvent = Hardware.CardReaderEvent
141142
public typealias CardReaderInput = Hardware.CardReaderInput
142143
public typealias CardReaderSoftwareUpdateState = Hardware.CardReaderSoftwareUpdateState

Yosemite/Yosemite/Stores/CardPresentPaymentStore.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,11 @@ public final class CardPresentPaymentStore: Store {
8484
case .loadAccounts(let siteID, let onCompletion):
8585
loadAccounts(siteID: siteID,
8686
onCompletion: onCompletion)
87-
case .startCardReaderDiscovery(let siteID, let onReaderDiscovered, let onError):
88-
startCardReaderDiscovery(siteID: siteID, onReaderDiscovered: onReaderDiscovered, onError: onError)
87+
case .startCardReaderDiscovery(let siteID, let discoveryMethod, let onReaderDiscovered, let onError):
88+
startCardReaderDiscovery(siteID: siteID,
89+
discoveryMethod: discoveryMethod,
90+
onReaderDiscovered: onReaderDiscovered,
91+
onError: onError)
8992
case .cancelCardReaderDiscovery(let completion):
9093
cancelCardReaderDiscovery(completion: completion)
9194
case .connect(let reader, let completion):
@@ -125,15 +128,18 @@ public final class CardPresentPaymentStore: Store {
125128
// MARK: - Services
126129
//
127130
private extension CardPresentPaymentStore {
128-
func startCardReaderDiscovery(siteID: Int64, onReaderDiscovered: @escaping (_ readers: [CardReader]) -> Void, onError: @escaping (Error) -> Void) {
131+
func startCardReaderDiscovery(siteID: Int64,
132+
discoveryMethod: CardReaderDiscoveryMethod,
133+
onReaderDiscovered: @escaping (_ readers: [CardReader]) -> Void,
134+
onError: @escaping (Error) -> Void) {
129135
do {
130136
switch usingBackend {
131137
case .wcpay:
132138
commonReaderConfigProvider.setContext(siteID: siteID, remote: self.remote)
133-
try cardReaderService.start(commonReaderConfigProvider)
139+
try cardReaderService.start(commonReaderConfigProvider, discoveryMethod: discoveryMethod)
134140
case .stripe:
135141
commonReaderConfigProvider.setContext(siteID: siteID, remote: self.stripeRemote)
136-
try cardReaderService.start(commonReaderConfigProvider)
142+
try cardReaderService.start(commonReaderConfigProvider, discoveryMethod: discoveryMethod)
137143
}
138144
} catch {
139145
return onError(error)

0 commit comments

Comments
 (0)