Skip to content

Commit bcbd53c

Browse files
authored
Merge pull request #6584 from woocommerce/feat/5984-add-card_reader_model-prop
IPP Analytics: add `card_reader_model` property to card reader software update events
2 parents 6242f86 + 7a56439 commit bcbd53c

File tree

12 files changed

+371
-146
lines changed

12 files changed

+371
-146
lines changed

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 35 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,15 @@ 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?,
748+
updateType: SoftwareUpdateTypeProperty,
749+
countryCode: String,
750+
cardReaderModel: String) -> WooAnalyticsEvent {
732751
WooAnalyticsEvent(statName: .cardReaderSoftwareUpdateSuccess,
733752
properties: [
753+
Keys.cardReaderModel: cardReaderModel,
734754
Keys.countryCode: countryCode,
735755
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID),
736756
Keys.softwareUpdateType: updateType.rawValue
@@ -744,12 +764,15 @@ extension WooAnalyticsEvent {
744764
/// - forGatewayID: the plugin (e.g. "woocommerce-payments" or "woocommerce-gateway-stripe") to be included in the event properties in Tracks.
745765
/// - updateType: `.required` or `.optional`.
746766
/// - countryCode: the country code of the store.
767+
/// - cardReaderModel: the model type of the card reader.
747768
///
748769
static func cardReaderSoftwareUpdateCancelTapped(forGatewayID: String?,
749770
updateType: SoftwareUpdateTypeProperty,
750-
countryCode: String) -> WooAnalyticsEvent {
771+
countryCode: String,
772+
cardReaderModel: String) -> WooAnalyticsEvent {
751773
WooAnalyticsEvent(statName: .cardReaderSoftwareUpdateCancelTapped,
752774
properties: [
775+
Keys.cardReaderModel: cardReaderModel,
753776
Keys.countryCode: countryCode,
754777
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID),
755778
Keys.softwareUpdateType: updateType.rawValue
@@ -763,10 +786,15 @@ extension WooAnalyticsEvent {
763786
/// - forGatewayID: the plugin (e.g. "woocommerce-payments" or "woocommerce-gateway-stripe") to be included in the event properties in Tracks.
764787
/// - updateType: `.required` or `.optional`.
765788
/// - countryCode: the country code of the store.
789+
/// - cardReaderModel: the model type of the card reader.
766790
///
767-
static func cardReaderSoftwareUpdateCanceled(forGatewayID: String?, updateType: SoftwareUpdateTypeProperty, countryCode: String) -> WooAnalyticsEvent {
791+
static func cardReaderSoftwareUpdateCanceled(forGatewayID: String?,
792+
updateType: SoftwareUpdateTypeProperty,
793+
countryCode: String,
794+
cardReaderModel: String) -> WooAnalyticsEvent {
768795
WooAnalyticsEvent(statName: .cardReaderSoftwareUpdateCanceled,
769796
properties: [
797+
Keys.cardReaderModel: cardReaderModel,
770798
Keys.countryCode: countryCode,
771799
Keys.gatewayID: gatewayID(forGatewayID: forGatewayID),
772800
Keys.softwareUpdateType: updateType.rawValue
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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 var 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 var countryCode: String {
22+
configuration.countryCode
23+
}
24+
25+
private(set) var optionalReaderUpdateAvailable: Bool = false
26+
27+
/// Gateway ID to include in tracks events, which could be set in initialization and/or externally.
28+
private var gatewayID: String?
29+
30+
private var softwareUpdateCancelable: AnyCancellable?
31+
32+
private let configuration: CardPresentPaymentsConfiguration
33+
private let stores: StoresManager
34+
private let analytics: Analytics
35+
36+
init(configuration: CardPresentPaymentsConfiguration,
37+
stores: StoresManager = ServiceLocator.stores,
38+
analytics: Analytics = ServiceLocator.analytics) {
39+
self.configuration = configuration
40+
self.stores = stores
41+
self.analytics = analytics
42+
43+
observeConnectedReader()
44+
}
45+
46+
/// Since gateway ID could be fetched asynchronously, this can also be set externally in addition to the initializer.
47+
func setGatewayID(gatewayID: String?) {
48+
self.gatewayID = gatewayID
49+
}
50+
51+
func setCandidateReader(_ reader: CardReader?) {
52+
candidateReader = reader
53+
if reader != nil {
54+
observeSoftwareUpdateState()
55+
} else {
56+
softwareUpdateCancelable?.cancel()
57+
}
58+
}
59+
60+
/// Called when the user taps to update card reader software when it is available.
61+
func cardReaderSoftwareUpdateTapped() {
62+
analytics.track(event: WooAnalyticsEvent.InPersonPayments
63+
.cardReaderSoftwareUpdateTapped(forGatewayID: gatewayID,
64+
updateType: updateType,
65+
countryCode: configuration.countryCode,
66+
cardReaderModel: cardReaderModel))
67+
}
68+
69+
/// Called when the user taps to cancel card reader software update.
70+
func cardReaderSoftwareUpdateCancelTapped() {
71+
analytics.track(event: WooAnalyticsEvent.InPersonPayments
72+
.cardReaderSoftwareUpdateCancelTapped(forGatewayID: gatewayID,
73+
updateType: .required,
74+
countryCode: countryCode,
75+
cardReaderModel: cardReaderModel))
76+
}
77+
78+
/// Called after the card reader software update is canceled.
79+
func cardReaderSoftwareUpdateCanceled() {
80+
softwareUpdateCancelable?.cancel()
81+
analytics.track(event: WooAnalyticsEvent.InPersonPayments
82+
.cardReaderSoftwareUpdateCanceled(forGatewayID: gatewayID,
83+
updateType: .required,
84+
countryCode: countryCode,
85+
cardReaderModel: cardReaderModel))
86+
completeCardReaderUpdate(success: false)
87+
}
88+
89+
/// Called when the user taps to disconnect card reader.
90+
func cardReaderDisconnectTapped() {
91+
analytics.track(event: WooAnalyticsEvent.InPersonPayments
92+
.cardReaderDisconnectTapped(forGatewayID: gatewayID,
93+
countryCode: configuration.countryCode,
94+
cardReaderModel: cardReaderModel))
95+
}
96+
}
97+
98+
private extension CardReaderConnectionAnalyticsTracker {
99+
func observeConnectedReader() {
100+
let action = CardPresentPaymentAction.observeConnectedReaders() { [weak self] readers in
101+
self?.connectedReader = readers.first
102+
}
103+
stores.dispatch(action)
104+
}
105+
106+
func observeSoftwareUpdateState() {
107+
let softwareUpdateAction = CardPresentPaymentAction.observeCardReaderUpdateState { [weak self] softwareUpdateEvents in
108+
guard let self = self else { return }
109+
self.softwareUpdateCancelable = softwareUpdateEvents
110+
.sink { [weak self] state in
111+
guard let self = self else { return }
112+
switch state {
113+
case .started:
114+
self.analytics.track(
115+
event: WooAnalyticsEvent.InPersonPayments
116+
.cardReaderSoftwareUpdateStarted(forGatewayID: self.gatewayID,
117+
updateType: self.updateType,
118+
countryCode: self.countryCode,
119+
cardReaderModel: self.cardReaderModel)
120+
)
121+
case .failed(error: let error):
122+
if case CardReaderServiceError.softwareUpdate(underlyingError: let underlyingError, batteryLevel: _) = error,
123+
underlyingError == .readerSoftwareUpdateFailedInterrupted {
124+
// Update was cancelled, don't treat this as an error
125+
break
126+
}
127+
self.analytics.track(event: WooAnalyticsEvent.InPersonPayments
128+
.cardReaderSoftwareUpdateFailed(forGatewayID: self.gatewayID,
129+
updateType: self.updateType,
130+
error: error,
131+
countryCode: self.countryCode,
132+
cardReaderModel: self.cardReaderModel))
133+
self.completeCardReaderUpdate(success: false)
134+
case .completed:
135+
self.softwareUpdateCancelable?.cancel()
136+
self.analytics.track(event: WooAnalyticsEvent.InPersonPayments
137+
.cardReaderSoftwareUpdateSuccess(forGatewayID: self.gatewayID,
138+
updateType: self.updateType,
139+
countryCode: self.countryCode,
140+
cardReaderModel: self.cardReaderModel))
141+
self.completeCardReaderUpdate(success: true)
142+
case .available:
143+
self.optionalReaderUpdateAvailable = true
144+
case .none:
145+
self.optionalReaderUpdateAvailable = false
146+
default:
147+
break
148+
}
149+
}
150+
}
151+
stores.dispatch(softwareUpdateAction)
152+
}
153+
154+
func completeCardReaderUpdate(success: Bool) {
155+
// Avoids a failed mandatory reader update being shown as optional
156+
optionalReaderUpdateAvailable = optionalReaderUpdateAvailable && !success
157+
}
158+
}

WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift

Lines changed: 20 additions & 19 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 let 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

@@ -129,7 +134,8 @@ final class CardReaderConnectionController {
129134
stores: StoresManager = ServiceLocator.stores,
130135
knownReaderProvider: CardReaderSettingsKnownReaderProvider,
131136
alertsProvider: CardReaderSettingsAlertsProvider,
132-
configuration: CardPresentPaymentsConfiguration
137+
configuration: CardPresentPaymentsConfiguration,
138+
analyticsTracker: CardReaderConnectionAnalyticsTracker
133139
) {
134140
siteID = forSiteID
135141
self.storageManager = storageManager
@@ -141,6 +147,7 @@ final class CardReaderConnectionController {
141147
knownReaderID = nil
142148
skippedReaderIDs = []
143149
self.configuration = configuration
150+
self.analyticsTracker = analyticsTracker
144151

145152
configureResultsControllers()
146153
loadPaymentGatewayAccounts()
@@ -484,24 +491,12 @@ private extension CardReaderConnectionController {
484491
return { [weak self] in
485492
guard let self = self else { return }
486493
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
494+
self.analyticsTracker.cardReaderSoftwareUpdateCancelTapped()
495+
cancelable.cancel { [weak self] result in
495496
if case .failure(let error) = result {
496497
DDLogError("💳 Error: canceling software update \(error)")
497498
} else {
498-
ServiceLocator.analytics.track(
499-
event: WooAnalyticsEvent.InPersonPayments.cardReaderSoftwareUpdateCanceled(
500-
forGatewayID: self.gatewayID,
501-
updateType: .required,
502-
countryCode: self.configuration.countryCode
503-
)
504-
)
499+
self?.analyticsTracker.cardReaderSoftwareUpdateCanceled()
505500
}
506501
}
507502
}
@@ -544,10 +539,14 @@ private extension CardReaderConnectionController {
544539
return
545540
}
546541

542+
analyticsTracker.setCandidateReader(candidateReader)
543+
547544
let softwareUpdateAction = CardPresentPaymentAction.observeCardReaderUpdateState { [weak self] softwareUpdateEvents in
548545
guard let self = self else { return }
549546

550-
softwareUpdateEvents.sink { event in
547+
softwareUpdateEvents.sink { [weak self] event in
548+
guard let self = self else { return }
549+
551550
switch event {
552551
case .started(cancelable: let cancelable):
553552
self.softwareUpdateCancelable = cancelable
@@ -569,9 +568,12 @@ private extension CardReaderConnectionController {
569568
stores.dispatch(softwareUpdateAction)
570569

571570
let action = CardPresentPaymentAction.connect(reader: candidateReader) { [weak self] result in
571+
guard let self = self else { return }
572+
573+
self.analyticsTracker.setCandidateReader(nil)
574+
572575
switch result {
573576
case .success(let reader):
574-
guard let self = self else { return }
575577
self.knownCardReaderProvider.rememberCardReader(cardReaderID: reader.id)
576578
ServiceLocator.analytics.track(
577579
event: WooAnalyticsEvent.InPersonPayments
@@ -590,7 +592,6 @@ private extension CardReaderConnectionController {
590592
self.returnSuccess(connected: true)
591593
}
592594
case .failure(let error):
593-
guard let self = self else { return }
594595
ServiceLocator.analytics.track(
595596
event: WooAnalyticsEvent.InPersonPayments.cardReaderConnectionFailed(forGatewayID: self.gatewayID,
596597
error: error,

0 commit comments

Comments
 (0)