Skip to content

Commit cfa9f5c

Browse files
committed
Extract card connection event tracking to CardReaderConnectionAnalyticsTracker.
1 parent 6675046 commit cfa9f5c

File tree

5 files changed

+179
-55
lines changed

5 files changed

+179
-55
lines changed

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -672,10 +672,15 @@ extension WooAnalyticsEvent {
672672
/// - forGatewayID: the plugin (e.g. "woocommerce-payments" or "woocommerce-gateway-stripe") to be included in the event properties in Tracks.
673673
/// - updateType: `.required` or `.optional`.
674674
/// - countryCode: the country code of the store.
675+
/// - cardReaderModel: the model type of the card reader.
675676
///
676-
static func cardReaderSoftwareUpdateTapped(forGatewayID: String?, updateType: SoftwareUpdateTypeProperty, countryCode: String) -> WooAnalyticsEvent {
677+
static func cardReaderSoftwareUpdateTapped(forGatewayID: String?,
678+
updateType: SoftwareUpdateTypeProperty,
679+
countryCode: String,
680+
cardReaderModel: String) -> WooAnalyticsEvent {
677681
WooAnalyticsEvent(statName: .cardReaderSoftwareUpdateTapped,
678682
properties: [
683+
Keys.cardReaderModel: cardReaderModel,
679684
Keys.countryCode: countryCode,
680685
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID),
681686
Keys.softwareUpdateType: updateType.rawValue
@@ -689,10 +694,15 @@ extension WooAnalyticsEvent {
689694
/// - forGatewayID: the plugin (e.g. "woocommerce-payments" or "woocommerce-gateway-stripe") to be included in the event properties in Tracks.
690695
/// - updateType: `.required` or `.optional`.
691696
/// - countryCode: the country code of the store.
697+
/// - cardReaderModel: the model type of the card reader.
692698
///
693-
static func cardReaderSoftwareUpdateStarted(forGatewayID: String?, updateType: SoftwareUpdateTypeProperty, countryCode: String) -> WooAnalyticsEvent {
699+
static func cardReaderSoftwareUpdateStarted(forGatewayID: String?,
700+
updateType: SoftwareUpdateTypeProperty,
701+
countryCode: String,
702+
cardReaderModel: String) -> WooAnalyticsEvent {
694703
WooAnalyticsEvent(statName: .cardReaderSoftwareUpdateStarted,
695704
properties: [
705+
Keys.cardReaderModel: cardReaderModel,
696706
Keys.countryCode: countryCode,
697707
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID),
698708
Keys.softwareUpdateType: updateType.rawValue
@@ -707,12 +717,17 @@ extension WooAnalyticsEvent {
707717
/// - updateType: `.required` or `.optional`.
708718
/// - error: the error to be included in the event properties.
709719
/// - countryCode: the country code of the store.
720+
/// - cardReaderModel: the model type of the card reader.
710721
///
711-
static func cardReaderSoftwareUpdateFailed(
712-
forGatewayID: String?, updateType: SoftwareUpdateTypeProperty, error: Error, countryCode: String
722+
static func cardReaderSoftwareUpdateFailed(forGatewayID: String?,
723+
updateType: SoftwareUpdateTypeProperty,
724+
error: Error,
725+
countryCode: String,
726+
cardReaderModel: String
713727
) -> WooAnalyticsEvent {
714728
WooAnalyticsEvent(statName: .cardReaderSoftwareUpdateFailed,
715729
properties: [
730+
Keys.cardReaderModel: cardReaderModel,
716731
Keys.countryCode: countryCode,
717732
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID),
718733
Keys.softwareUpdateType: updateType.rawValue,
@@ -727,10 +742,12 @@ extension WooAnalyticsEvent {
727742
/// - forGatewayID: the plugin (e.g. "woocommerce-payments" or "woocommerce-gateway-stripe") to be included in the event properties in Tracks.
728743
/// - updateType: `.required` or `.optional`.
729744
/// - countryCode: the country code of the store.
745+
/// - cardReaderModel: the model type of the card reader.
730746
///
731-
static func cardReaderSoftwareUpdateSuccess(forGatewayID: String?, updateType: SoftwareUpdateTypeProperty, countryCode: String) -> WooAnalyticsEvent {
747+
static func cardReaderSoftwareUpdateSuccess(forGatewayID: String?, updateType: SoftwareUpdateTypeProperty, countryCode: String, cardReaderModel: String) -> WooAnalyticsEvent {
732748
WooAnalyticsEvent(statName: .cardReaderSoftwareUpdateSuccess,
733749
properties: [
750+
Keys.cardReaderModel: cardReaderModel,
734751
Keys.countryCode: countryCode,
735752
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID),
736753
Keys.softwareUpdateType: updateType.rawValue
@@ -744,12 +761,15 @@ extension WooAnalyticsEvent {
744761
/// - forGatewayID: the plugin (e.g. "woocommerce-payments" or "woocommerce-gateway-stripe") to be included in the event properties in Tracks.
745762
/// - updateType: `.required` or `.optional`.
746763
/// - countryCode: the country code of the store.
764+
/// - cardReaderModel: the model type of the card reader.
747765
///
748766
static func cardReaderSoftwareUpdateCancelTapped(forGatewayID: String?,
749767
updateType: SoftwareUpdateTypeProperty,
750-
countryCode: String) -> WooAnalyticsEvent {
768+
countryCode: String,
769+
cardReaderModel: String) -> WooAnalyticsEvent {
751770
WooAnalyticsEvent(statName: .cardReaderSoftwareUpdateCancelTapped,
752771
properties: [
772+
Keys.cardReaderModel: cardReaderModel,
753773
Keys.countryCode: countryCode,
754774
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID),
755775
Keys.softwareUpdateType: updateType.rawValue
@@ -763,10 +783,12 @@ extension WooAnalyticsEvent {
763783
/// - forGatewayID: the plugin (e.g. "woocommerce-payments" or "woocommerce-gateway-stripe") to be included in the event properties in Tracks.
764784
/// - updateType: `.required` or `.optional`.
765785
/// - countryCode: the country code of the store.
786+
/// - cardReaderModel: the model type of the card reader.
766787
///
767-
static func cardReaderSoftwareUpdateCanceled(forGatewayID: String?, updateType: SoftwareUpdateTypeProperty, countryCode: String) -> WooAnalyticsEvent {
788+
static func cardReaderSoftwareUpdateCanceled(forGatewayID: String?, updateType: SoftwareUpdateTypeProperty, countryCode: String, cardReaderModel: String) -> WooAnalyticsEvent {
768789
WooAnalyticsEvent(statName: .cardReaderSoftwareUpdateCanceled,
769790
properties: [
791+
Keys.cardReaderModel: cardReaderModel,
770792
Keys.countryCode: countryCode,
771793
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID),
772794
Keys.softwareUpdateType: updateType.rawValue
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import Combine
2+
import Foundation
3+
import Yosemite
4+
5+
/// Tracks events during card reader connection flow, including reader connection and optional/required software update events.
6+
final class CardReaderConnectionAnalyticsTracker {
7+
/// The reader the user has connected.
8+
private var connectedReader: CardReader?
9+
10+
/// The reader the user is trying to connect.
11+
private let candidateReader: CardReader
12+
13+
private var updateType: SoftwareUpdateTypeProperty {
14+
optionalReaderUpdateAvailable ? .optional : .required
15+
}
16+
17+
private var cardReaderModel: String {
18+
(connectedReader ?? candidateReader).readerType.model
19+
}
20+
21+
private(set) var optionalReaderUpdateAvailable: Bool = false
22+
23+
/// Gateway ID to include in tracks events, which could be set in initialization and/or externally.
24+
private var gatewayID: String?
25+
26+
private var softwareUpdateCancelable: FallibleCancelable? = nil
27+
private var subscriptions = Set<AnyCancellable>()
28+
29+
private let configuration: CardPresentPaymentsConfiguration
30+
private let stores: StoresManager
31+
private let analytics: Analytics
32+
33+
init(candidateReader: CardReader,
34+
configuration: CardPresentPaymentsConfiguration,
35+
gatewayID: String?,
36+
stores: StoresManager = ServiceLocator.stores,
37+
analytics: Analytics = ServiceLocator.analytics) {
38+
self.candidateReader = candidateReader
39+
self.configuration = configuration
40+
self.gatewayID = gatewayID
41+
self.stores = stores
42+
self.analytics = analytics
43+
44+
observeConnectedReaderAndSoftwareUpdate()
45+
}
46+
47+
/// Since gateway ID could be fetched asynchronously, this can also be set externally in addition to the initializer.
48+
func setGatewayID(gatewayID: String?) {
49+
self.gatewayID = gatewayID
50+
}
51+
52+
/// Called when the user taps to cancel card reader software update.
53+
func softwareUpdateCancelTapped() {
54+
analytics.track(event: WooAnalyticsEvent.InPersonPayments
55+
.cardReaderSoftwareUpdateCancelTapped(forGatewayID: gatewayID,
56+
updateType: .required,
57+
countryCode: configuration.countryCode,
58+
cardReaderModel: cardReaderModel))
59+
}
60+
61+
/// Called after the card reader software update is canceled.
62+
func softwareUpdateCanceled() {
63+
analytics.track(event: WooAnalyticsEvent.InPersonPayments
64+
.cardReaderSoftwareUpdateCanceled(forGatewayID: gatewayID,
65+
updateType: .required,
66+
countryCode: configuration.countryCode,
67+
cardReaderModel: cardReaderModel))
68+
}
69+
}
70+
71+
private extension CardReaderConnectionAnalyticsTracker {
72+
/// Dispatches actions to the CardPresentPaymentStore so that we can monitor changes to the list of
73+
/// connected readers and software update states.
74+
func observeConnectedReaderAndSoftwareUpdate() {
75+
let action = CardPresentPaymentAction.observeConnectedReaders() { [weak self] readers in
76+
self?.connectedReader = readers.first
77+
}
78+
stores.dispatch(action)
79+
80+
let softwareUpdateAction = CardPresentPaymentAction.observeCardReaderUpdateState { softwareUpdateEvents in
81+
softwareUpdateEvents
82+
.sink { [weak self] state in
83+
guard let self = self else { return }
84+
85+
switch state {
86+
case .started(cancelable: let cancelable):
87+
self.softwareUpdateCancelable = cancelable
88+
self.analytics.track(
89+
event: WooAnalyticsEvent.InPersonPayments
90+
.cardReaderSoftwareUpdateStarted(forGatewayID: self.gatewayID,
91+
updateType: self.updateType,
92+
countryCode: self.configuration.countryCode,
93+
cardReaderModel: self.cardReaderModel)
94+
)
95+
case .failed(error: let error):
96+
if case CardReaderServiceError.softwareUpdate(underlyingError: let underlyingError, batteryLevel: _) = error,
97+
underlyingError == .readerSoftwareUpdateFailedInterrupted {
98+
// Update was cancelled, don't treat this as an error
99+
break
100+
}
101+
self.analytics.track(event: WooAnalyticsEvent.InPersonPayments
102+
.cardReaderSoftwareUpdateFailed(forGatewayID: self.gatewayID,
103+
updateType: self.updateType,
104+
error: error,
105+
countryCode: self.configuration.countryCode,
106+
cardReaderModel: self.cardReaderModel))
107+
case .completed:
108+
self.softwareUpdateCancelable = nil
109+
self.analytics.track(event: WooAnalyticsEvent.InPersonPayments
110+
.cardReaderSoftwareUpdateSuccess(forGatewayID: self.gatewayID,
111+
updateType: self.updateType,
112+
countryCode: self.configuration.countryCode,
113+
cardReaderModel: self.cardReaderModel))
114+
case .available:
115+
self.optionalReaderUpdateAvailable = true
116+
case .none:
117+
self.optionalReaderUpdateAvailable = false
118+
default:
119+
break
120+
}
121+
}
122+
.store(in: &self.subscriptions)
123+
}
124+
stores.dispatch(softwareUpdateAction)
125+
}
126+
}

WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ final class CardReaderConnectionController {
9999
///
100100
private var candidateReader: CardReader?
101101

102+
/// Tracks analytics for card reader connection events
103+
///
104+
private var analyticsTracker: CardReaderConnectionAnalyticsTracker?
105+
102106
/// Since the number of readers can go greater than 1 and then back to 1, and we don't
103107
/// want to keep changing the UI from the several-readers-found list to a single prompt
104108
/// and back (as this would be visually quite annoying), this flag will tell us that we've
@@ -120,6 +124,7 @@ final class CardReaderConnectionController {
120124
private var gatewayID: String? {
121125
didSet {
122126
didSetGatewayID()
127+
analyticsTracker?.setGatewayID(gatewayID: gatewayID)
123128
}
124129
}
125130

@@ -484,24 +489,12 @@ private extension CardReaderConnectionController {
484489
return { [weak self] in
485490
guard let self = self else { return }
486491
self.state = .cancel
487-
ServiceLocator.analytics.track(
488-
event: WooAnalyticsEvent.InPersonPayments.cardReaderSoftwareUpdateCancelTapped(
489-
forGatewayID: self.gatewayID,
490-
updateType: .required,
491-
countryCode: self.configuration.countryCode
492-
)
493-
)
494-
cancelable.cancel { result in
492+
self.analyticsTracker?.softwareUpdateCancelTapped()
493+
cancelable.cancel { [weak self] result in
495494
if case .failure(let error) = result {
496495
DDLogError("💳 Error: canceling software update \(error)")
497496
} else {
498-
ServiceLocator.analytics.track(
499-
event: WooAnalyticsEvent.InPersonPayments.cardReaderSoftwareUpdateCanceled(
500-
forGatewayID: self.gatewayID,
501-
updateType: .required,
502-
countryCode: self.configuration.countryCode
503-
)
504-
)
497+
self?.analyticsTracker?.softwareUpdateCanceled()
505498
}
506499
}
507500
}
@@ -544,6 +537,12 @@ private extension CardReaderConnectionController {
544537
return
545538
}
546539

540+
let analyticsTracker = CardReaderConnectionAnalyticsTracker(candidateReader: candidateReader,
541+
configuration: configuration,
542+
gatewayID: gatewayID,
543+
stores: stores)
544+
self.analyticsTracker = analyticsTracker
545+
547546
let softwareUpdateAction = CardPresentPaymentAction.observeCardReaderUpdateState { [weak self] softwareUpdateEvents in
548547
guard let self = self else { return }
549548

0 commit comments

Comments
 (0)