Skip to content

Commit ad2305a

Browse files
authored
Merge pull request #8650 from woocommerce/issue/8290-ttpoi-setup-cancellation
[Mobile Payments] Fix connection loop when Tap To Pay terms acceptance is cancelled
2 parents 5d5677d + ed2bda1 commit ad2305a

File tree

7 files changed

+78
-49
lines changed

7 files changed

+78
-49
lines changed

Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,10 @@ extension StripeCardReaderService: CardReaderService {
470470
.softwareUpdate(underlyingError: underlyingError, batteryLevel: nil) :
471471
.connection(underlyingError: underlyingError)
472472
promise(.failure(serviceError))
473+
474+
if case .appleBuiltInReaderTOSAcceptanceCanceled = underlyingError {
475+
self.discoveryCancellable?.cancel({ _ in })
476+
}
473477
}
474478

475479
if let reader = reader {

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,7 @@ extension WooAnalyticsEvent {
896896
static let softwareUpdateType = "software_update_type"
897897
static let source = "source"
898898
static let enabled = "enabled"
899+
static let cancellationSource = "cancellation_source"
899900
}
900901

901902
static let unknownGatewayID = "unknown"
@@ -1171,16 +1172,34 @@ extension WooAnalyticsEvent {
11711172
/// - countryCode: the country code of the store.
11721173
/// - cardReaderModel: the model type of the card reader.
11731174
///
1174-
static func collectPaymentCanceled(forGatewayID: String?, countryCode: String, cardReaderModel: String) -> WooAnalyticsEvent {
1175+
static func collectPaymentCanceled(forGatewayID: String?,
1176+
countryCode: String,
1177+
cardReaderModel: String,
1178+
cancellationSource: CancellationSource) -> WooAnalyticsEvent {
11751179
WooAnalyticsEvent(statName: .collectPaymentCanceled,
11761180
properties: [
11771181
Keys.cardReaderModel: cardReaderModel,
11781182
Keys.countryCode: countryCode,
1179-
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID)
1183+
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID),
1184+
Keys.cancellationSource: cancellationSource.rawValue
11801185
]
11811186
)
11821187
}
11831188

1189+
enum CancellationSource: String {
1190+
case appleTOSAcceptance = "apple_tap_to_pay_terms_acceptance"
1191+
case reader = "card_reader"
1192+
case selectReaderType = "preflight_select_reader_type"
1193+
case searchingForReader = "searching_for_reader"
1194+
case foundReader = "found_reader"
1195+
case foundSeveralReaders = "found_several_readers"
1196+
case paymentPreparingReader = "payment_preparing_reader"
1197+
case paymentWaitingForInput = "payment_waiting_for_input"
1198+
case connectionError = "connection_error"
1199+
case readerSoftwareUpdate = "reader_software_update"
1200+
case other = "unknown"
1201+
}
1202+
11841203
/// Tracked when payment collection succeeds
11851204
///
11861205
/// - Parameters:

WooCommerce/Classes/ViewRelated/CardPresentPayments/BuiltInCardReaderConnectionController.swift

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ final class BuiltInCardReaderConnectionController {
5252
/// will be called with a `success` `Bool` `False` result. The view controller passed to `searchAndConnect` will be
5353
/// dereferenced and the state set to `idle`
5454
///
55-
case cancel
55+
case cancel(WooAnalyticsEvent.InPersonPayments.CancellationSource)
5656

5757
/// A failure occurred. The completion passed to `searchAndConnect`
5858
/// will be called with a `failure` result. The view controller passed to `searchAndConnect` will be
@@ -163,8 +163,8 @@ private extension BuiltInCardReaderConnectionController {
163163
onSearching()
164164
case .retry:
165165
onRetry()
166-
case .cancel:
167-
onCancel()
166+
case .cancel(let cancellationSource):
167+
onCancel(from: cancellationSource)
168168
case .connectToReader:
169169
onConnectToReader()
170170
case .connectingFailed(let error):
@@ -269,7 +269,7 @@ private extension BuiltInCardReaderConnectionController {
269269
/// stay in this state
270270
///
271271
alertsPresenter.present(viewModel: alertsProvider.scanningForReader(cancel: {
272-
self.state = .cancel
272+
self.state = .cancel(.searchingForReader)
273273
}))
274274
}
275275

@@ -279,7 +279,7 @@ private extension BuiltInCardReaderConnectionController {
279279
let cancel = softwareUpdateCancelable.map { cancelable in
280280
return { [weak self] in
281281
guard let self = self else { return }
282-
self.state = .cancel
282+
self.state = .cancel(.searchingForReader)
283283
self.analyticsTracker.cardReaderSoftwareUpdateCancelTapped()
284284
cancelable.cancel { [weak self] result in
285285
if case .failure(let error) = result {
@@ -309,9 +309,9 @@ private extension BuiltInCardReaderConnectionController {
309309

310310
/// End the search for a card reader
311311
///
312-
func onCancel() {
312+
func onCancel(from cancellationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource) {
313313
let action = CardPresentPaymentAction.cancelCardReaderDiscovery() { [weak self] _ in
314-
self?.returnSuccess(result: .canceled)
314+
self?.returnSuccess(result: .canceled(cancellationSource))
315315
}
316316
stores.dispatch(action)
317317
}
@@ -375,13 +375,19 @@ private extension BuiltInCardReaderConnectionController {
375375
self.returnSuccess(result: .connected(reader))
376376
}
377377
case .failure(let error):
378-
ServiceLocator.analytics.track(
379-
event: WooAnalyticsEvent.InPersonPayments.cardReaderConnectionFailed(forGatewayID: self.gatewayID,
380-
error: error,
381-
countryCode: self.configuration.countryCode,
382-
cardReaderModel: candidateReader.readerType.model)
383-
)
384-
self.state = .connectingFailed(error)
378+
// The TOS acceptance flow happens during connection, not discovery, and cancelations from Apple's
379+
// screen are returned as failures here.
380+
if case .connection(.appleBuiltInReaderTOSAcceptanceCanceled) = error as? CardReaderServiceError {
381+
return self.state = .cancel(.appleTOSAcceptance)
382+
} else {
383+
ServiceLocator.analytics.track(
384+
event: WooAnalyticsEvent.InPersonPayments.cardReaderConnectionFailed(forGatewayID: self.gatewayID,
385+
error: error,
386+
countryCode: self.configuration.countryCode,
387+
cardReaderModel: candidateReader.readerType.model)
388+
)
389+
self.state = .connectingFailed(error)
390+
}
385391
}
386392
}
387393
stores.dispatch(action)
@@ -441,7 +447,7 @@ private extension BuiltInCardReaderConnectionController {
441447
}
442448

443449
let cancelSearch = {
444-
self.state = .cancel
450+
self.state = .cancel(.connectionError)
445451
}
446452

447453
guard case CardReaderServiceError.connection(let underlyingError) = error else {

WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import ProximityReader
77

88
enum CardReaderConnectionResult {
99
case connected(CardReader)
10-
case canceled
10+
case canceled(WooAnalyticsEvent.InPersonPayments.CancellationSource)
1111
}
1212

1313
final class CardPresentPaymentPreflightController {
@@ -117,7 +117,7 @@ final class CardPresentPaymentPreflightController {
117117
cancelAction: { [weak self] in
118118
guard let self = self else { return }
119119
self.alertsPresenter.dismiss()
120-
self.handleConnectionResult(.success(.canceled))
120+
self.handleConnectionResult(.success(.canceled(.selectReaderType)))
121121
}))
122122
}
123123

@@ -135,13 +135,10 @@ final class CardPresentPaymentPreflightController {
135135

136136
private func handleConnectionResult(_ result: Result<CardReaderConnectionResult, Error>) {
137137
let connectionResult = result.map { connection in
138-
switch connection {
139-
case .connected(let reader):
138+
if case .connected(let reader) = connection {
140139
self.connectedReader = reader
141-
return CardReaderConnectionResult.connected(reader)
142-
case .canceled:
143-
return CardReaderConnectionResult.canceled
144140
}
141+
return connection
145142
}
146143

147144
switch connectionResult {

WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ final class CardReaderConnectionController {
6060
/// will be called with a `success` `Bool` `False` result. The view controller passed to `searchAndConnect` will be
6161
/// dereferenced and the state set to `idle`
6262
///
63-
case cancel
63+
case cancel(WooAnalyticsEvent.InPersonPayments.CancellationSource)
6464

6565
/// A failure occurred. The completion passed to `searchAndConnect`
6666
/// will be called with a `failure` result. The view controller passed to `searchAndConnect` will be
@@ -200,8 +200,8 @@ private extension CardReaderConnectionController {
200200
onFoundSeveralReaders()
201201
case .retry:
202202
onRetry()
203-
case .cancel:
204-
onCancel()
203+
case .cancel(let cancellationSource):
204+
onCancel(from: cancellationSource)
205205
case .connectToReader:
206206
onConnectToReader()
207207
case .connectingFailed(let error):
@@ -424,7 +424,7 @@ private extension CardReaderConnectionController {
424424
/// stay in this state
425425
///
426426
alertsPresenter.present(viewModel: alertsProvider.scanningForReader(cancel: {
427-
self.state = .cancel
427+
self.state = .cancel(.searchingForReader)
428428
}))
429429
}
430430

@@ -449,7 +449,7 @@ private extension CardReaderConnectionController {
449449
self.state = .searching
450450
},
451451
cancelSearch: { [weak self] in
452-
self?.state = .cancel
452+
self?.state = .cancel(.foundReader)
453453
}))
454454
}
455455

@@ -467,7 +467,7 @@ private extension CardReaderConnectionController {
467467
self.state = .connectToReader
468468
},
469469
cancelSearch: { [weak self] in
470-
self?.state = .cancel
470+
self?.state = .cancel(.foundSeveralReaders)
471471
}
472472
)
473473
}
@@ -478,7 +478,7 @@ private extension CardReaderConnectionController {
478478
let cancel = softwareUpdateCancelable.map { cancelable in
479479
return { [weak self] in
480480
guard let self = self else { return }
481-
self.state = .cancel
481+
self.state = .cancel(.readerSoftwareUpdate)
482482
self.analyticsTracker.cardReaderSoftwareUpdateCancelTapped()
483483
cancelable.cancel { [weak self] result in
484484
if case .failure(let error) = result {
@@ -508,9 +508,9 @@ private extension CardReaderConnectionController {
508508

509509
/// End the search for a card reader
510510
///
511-
func onCancel() {
511+
func onCancel(from cancellationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource) {
512512
let action = CardPresentPaymentAction.cancelCardReaderDiscovery() { [weak self] _ in
513-
self?.returnSuccess(result: .canceled)
513+
self?.returnSuccess(result: .canceled(cancellationSource))
514514
}
515515
stores.dispatch(action)
516516
}
@@ -639,7 +639,7 @@ private extension CardReaderConnectionController {
639639
}
640640

641641
let cancelSearch = {
642-
self.state = .cancel
642+
self.state = .cancel(.connectionError)
643643
}
644644

645645
guard case CardReaderServiceError.connection(let underlyingError) = error else {

WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
167167
onCompleted: onCompleted)
168168
}
169169
})
170-
case .canceled:
171-
self.handlePaymentCancellation()
170+
case .canceled(let cancellationSource):
171+
self.handlePaymentCancellation(from: cancellationSource)
172172
onCancel()
173173
case .none:
174174
break
@@ -247,9 +247,9 @@ private extension CollectOrderPaymentUseCase {
247247
stripeSmallestCurrencyUnitMultiplier: configuration.stripeSmallestCurrencyUnitMultiplier,
248248
onPreparingReader: { [weak self] in
249249
self?.alertsPresenter.present(viewModel: paymentAlerts.preparingReader(onCancel: {
250-
self?.cancelPayment(onCompleted: {
250+
self?.cancelPayment(from: .paymentPreparingReader) {
251251
onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser))
252-
})
252+
}
253253
}))
254254
},
255255
onWaitingForInput: { [weak self] inputMethods in
@@ -260,7 +260,7 @@ private extension CollectOrderPaymentUseCase {
260260
amount: self.formattedAmount,
261261
inputMethods: inputMethods,
262262
onCancel: { [weak self] in
263-
self?.cancelPayment {
263+
self?.cancelPayment(from: .paymentWaitingForInput) {
264264
onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser))
265265
}
266266
})
@@ -285,7 +285,7 @@ private extension CollectOrderPaymentUseCase {
285285
case .reader:
286286
self?.handlePaymentCancellationFromReader(alertProvider: paymentAlerts)
287287
default:
288-
self?.handlePaymentCancellation()
288+
self?.handlePaymentCancellation(from: .other)
289289
}
290290
case .failure(let error):
291291
self?.handlePaymentFailureAndRetryPayment(error, alertProvider: paymentAlerts, onCompletion: onCompletion)
@@ -309,13 +309,13 @@ private extension CollectOrderPaymentUseCase {
309309
onCompletion(.success(capturedPaymentData))
310310
}
311311

312-
func handlePaymentCancellation() {
313-
trackPaymentCancelation()
312+
func handlePaymentCancellation(from cancellationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource) {
313+
trackPaymentCancelation(cancelationSource: cancellationSource)
314314
alertsPresenter.dismiss()
315315
}
316316

317317
func handlePaymentCancellationFromReader(alertProvider paymentAlerts: CardReaderTransactionAlertsProviding) {
318-
trackPaymentCancelation()
318+
trackPaymentCancelation(cancelationSource: .reader)
319319
guard let dismissedOnReaderAlert = paymentAlerts.cancelledOnReader() else {
320320
return alertsPresenter.dismiss()
321321
}
@@ -383,7 +383,7 @@ private extension CollectOrderPaymentUseCase {
383383
tryAgain: { [weak self] in
384384

385385
// Cancel current payment
386-
self?.paymentOrchestrator.cancelPayment { [weak self] result in
386+
self?.paymentOrchestrator.cancelPayment() { [weak self] result in
387387
guard let self = self else { return }
388388

389389
switch result {
@@ -418,17 +418,19 @@ private extension CollectOrderPaymentUseCase {
418418

419419
/// Cancels payment and record analytics.
420420
///
421-
func cancelPayment(onCompleted: @escaping () -> ()) {
421+
func cancelPayment(from cancelationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource,
422+
onCompleted: @escaping () -> ()) {
422423
paymentOrchestrator.cancelPayment { [weak self] _ in
423-
self?.trackPaymentCancelation()
424+
self?.trackPaymentCancelation(cancelationSource: cancelationSource)
424425
onCompleted()
425426
}
426427
}
427428

428-
func trackPaymentCancelation() {
429+
func trackPaymentCancelation(cancelationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource) {
429430
analytics.track(event: WooAnalyticsEvent.InPersonPayments.collectPaymentCanceled(forGatewayID: paymentGatewayAccount.gatewayID,
430431
countryCode: configuration.countryCode,
431-
cardReaderModel: connectedReader?.readerType.model ?? ""))
432+
cardReaderModel: connectedReader?.readerType.model ?? "",
433+
cancellationSource: cancelationSource))
432434
}
433435

434436
/// Allow merchants to print or email the payment receipt.

WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,8 @@ private extension LegacyCollectOrderPaymentUseCase {
389389
func trackPaymentCancelation() {
390390
analytics.track(event: WooAnalyticsEvent.InPersonPayments.collectPaymentCanceled(forGatewayID: paymentGatewayAccount.gatewayID,
391391
countryCode: configuration.countryCode,
392-
cardReaderModel: connectedReader?.readerType.model ?? ""))
392+
cardReaderModel: connectedReader?.readerType.model ?? "",
393+
cancellationSource: .other))
393394
}
394395

395396
/// Allow merchants to print or email the payment receipt.

0 commit comments

Comments
 (0)