Skip to content

Commit 03c3c20

Browse files
authored
Merge pull request #7991 from woocommerce/issue/7855-utm-params-and-analytics-on-JITM-cta
[Just In Time Messages] Add UTM params and analytics to JITM Call to Action
2 parents d7d05c1 + 4abfd84 commit 03c3c20

File tree

12 files changed

+407
-87
lines changed

12 files changed

+407
-87
lines changed

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,29 @@ extension WooAnalyticsEvent {
619619
}
620620
}
621621

622+
// MARK: - Just In Time Messages
623+
//
624+
extension WooAnalyticsEvent {
625+
enum JustInTimeMessage {
626+
private enum Keys {
627+
static let source = "source"
628+
static let justInTimeMessageID = "jitm_id"
629+
static let justInTimeMessageGroup = "jitm_group"
630+
}
631+
632+
static func callToActionTapped(source: String,
633+
messageID: String,
634+
featureClass: String) -> WooAnalyticsEvent {
635+
WooAnalyticsEvent(statName: .justInTimeMessageCallToActionTapped,
636+
properties: [
637+
Keys.source: source,
638+
Keys.justInTimeMessageID: messageID,
639+
Keys.justInTimeMessageGroup: featureClass
640+
])
641+
}
642+
}
643+
}
644+
622645
// MARK: - Simple Payments
623646
//
624647
extension WooAnalyticsEvent {

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,9 @@ public enum WooAnalyticsStat: String {
627627
case featureCardDismissed = "feature_card_dismissed"
628628
case featureCardCtaTapped = "feature_card_cta_tapped"
629629

630+
// MARK: Just In Time Messages events
631+
case justInTimeMessageCallToActionTapped = "jitm_cta_tapped"
632+
630633
// MARK: Simple Payments events
631634
//
632635
case simplePaymentsFlowStarted = "simple_payments_flow_started"
Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,94 @@
11
import Foundation
2+
import WooFoundation
23
import UIKit
4+
import Yosemite
5+
import Combine
36

4-
struct JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewModelProtocol {
5-
var showDividers: Bool = false
7+
final class JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewModelProtocol {
8+
private let siteID: Int64
69

7-
var badgeType: BadgeView.BadgeType = .tip
10+
private let analytics: Analytics
811

9-
var title: String
12+
// MARK: - Message properties
13+
let title: String
1014

11-
var message: String
15+
let message: String
1216

13-
var buttonTitle: String?
17+
let buttonTitle: String?
1418

15-
var image: UIImage = .paymentsFeatureBannerImage
19+
private let url: URL?
1620

17-
func onAppear() {
18-
// No-op
19-
}
21+
private let messageID: String
2022

21-
let onCTATapped: (() -> Void)?
23+
private let featureClass: String
2224

23-
func ctaTapped() {
24-
onCTATapped?()
25+
private let screenName: String
26+
27+
init(justInTimeMessage: YosemiteJustInTimeMessage,
28+
screenName: String,
29+
siteID: Int64,
30+
analytics: Analytics = ServiceLocator.analytics) {
31+
self.siteID = siteID
32+
self.analytics = analytics
33+
let utmProvider = WooCommerceComUTMProvider(
34+
campaign: "jitm_group_\(justInTimeMessage.featureClass)",
35+
source: screenName,
36+
content: "jitm_\(justInTimeMessage.messageID)",
37+
siteID: siteID)
38+
self.url = utmProvider.urlWithUtmParams(string: justInTimeMessage.url)
39+
self.messageID = justInTimeMessage.messageID
40+
self.featureClass = justInTimeMessage.featureClass
41+
self.screenName = screenName
42+
self.title = justInTimeMessage.title
43+
self.message = justInTimeMessage.detail
44+
self.buttonTitle = justInTimeMessage.buttonTitle
2545
}
2646

47+
// MARK: - output streams
48+
@Published private(set) var showWebViewSheet: WebViewSheetViewModel?
49+
50+
// MARK: - default AnnouncementCardViewModelProtocol conformance
51+
let showDividers: Bool = false
52+
53+
let badgeType: BadgeView.BadgeType = .tip
54+
55+
let image: UIImage = .paymentsFeatureBannerImage
56+
2757
var showDismissButton: Bool = true
2858

29-
var showDismissConfirmation: Bool = false
59+
let showDismissConfirmation: Bool = false
60+
61+
let dismissAlertTitle: String = ""
3062

31-
var dismissAlertTitle: String = ""
63+
let dismissAlertMessage: String = ""
3264

33-
var dismissAlertMessage: String = ""
65+
// MARK: - AnnouncementCardViewModelProtocol methods
66+
func onAppear() {
67+
// No-op
68+
}
69+
70+
func ctaTapped() {
71+
analytics.track(event: WooAnalyticsEvent.JustInTimeMessage.callToActionTapped(
72+
source: screenName,
73+
messageID: messageID,
74+
featureClass: featureClass))
75+
76+
guard let url = url else {
77+
return
78+
}
79+
let webViewModel = WebViewSheetViewModel(
80+
url: url,
81+
navigationTitle: title,
82+
wpComAuthenticated: needsAuthenticatedWebView(url: url))
83+
showWebViewSheet = webViewModel
84+
}
85+
86+
private func needsAuthenticatedWebView(url: URL) -> Bool {
87+
guard let host = url.host else {
88+
return false
89+
}
90+
return Constants.trustedDomains.contains(host)
91+
}
3492

3593
func dontShowAgainTapped() {
3694
// No-op
@@ -39,5 +97,10 @@ struct JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewModelProt
3997
func remindLaterTapped() {
4098
// No-op
4199
}
100+
}
42101

102+
extension JustInTimeMessageAnnouncementCardViewModel {
103+
enum Constants {
104+
static let trustedDomains = ["woocommerce.com", "wordpress.com"]
105+
}
43106
}

WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -156,33 +156,17 @@ final class DashboardViewModel {
156156
switch result {
157157
case let .success(.some(message)):
158158
let viewModel = JustInTimeMessageAnnouncementCardViewModel(
159-
title: message.title,
160-
message: message.detail,
161-
buttonTitle: message.buttonTitle,
162-
onCTATapped: { [weak self] in
163-
guard let self = self,
164-
let url = URL(string: message.url)
165-
else { return }
166-
let webViewModel = WebViewSheetViewModel(
167-
url: url,
168-
navigationTitle: message.title,
169-
wpComAuthenticated: self.needsAuthenticatedWebView(url: url))
170-
self.showWebViewSheet = webViewModel
171-
})
159+
justInTimeMessage: message,
160+
screenName: Constants.dashboardScreenName,
161+
siteID: siteID)
172162
self.announcementViewModel = viewModel
163+
viewModel.$showWebViewSheet.assign(to: &self.$showWebViewSheet)
173164
default:
174165
break
175166
}
176167
}
177168
stores.dispatch(action)
178169
}
179-
180-
private func needsAuthenticatedWebView(url: URL) -> Bool {
181-
guard let host = url.host else {
182-
return false
183-
}
184-
return Constants.trustedDomains.contains(host)
185-
}
186170
}
187171

188172
// MARK: - Constants
@@ -191,6 +175,5 @@ private extension DashboardViewModel {
191175
enum Constants {
192176
static let topEarnerStatsLimit: Int = 5
193177
static let dashboardScreenName = "my_store"
194-
static let trustedDomains = ["woocommerce.com", "wordpress.com"]
195178
}
196179
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@
447447
03191AE628E1DF0600670723 /* WooCommercePluginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03191AE528E1DF0600670723 /* WooCommercePluginViewModel.swift */; };
448448
03191AE928E20C9200670723 /* PluginDetailsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03191AE828E20C9200670723 /* PluginDetailsRowView.swift */; };
449449
031B10E3274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031B10E2274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift */; };
450+
035BA3A8291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */; };
450451
035C6DEB273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035C6DEA273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift */; };
451452
035F2308275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035F2307275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift */; };
452453
0366EAE12909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0366EAE02909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift */; };
@@ -2394,6 +2395,7 @@
23942395
03191AE528E1DF0600670723 /* WooCommercePluginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooCommercePluginViewModel.swift; sourceTree = "<group>"; };
23952396
03191AE828E20C9200670723 /* PluginDetailsRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginDetailsRowView.swift; sourceTree = "<group>"; };
23962397
031B10E2274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalConnectionFailedUpdateAddress.swift; sourceTree = "<group>"; };
2398+
035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageAnnouncementCardViewModelTests.swift; sourceTree = "<group>"; };
23972399
035C6DEA273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdateTypeProperty.swift; sourceTree = "<group>"; };
23982400
035F2307275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalConnectingFailedUpdatePostalCode.swift; sourceTree = "<group>"; };
23992401
0366EAE02909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageAnnouncementCardViewModel.swift; sourceTree = "<group>"; };
@@ -4934,6 +4936,7 @@
49344936
isa = PBXGroup;
49354937
children = (
49364938
0371C3692876DBCA00277E2C /* FeatureAnnouncementCardViewModelTests.swift */,
4939+
035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */,
49374940
);
49384941
path = "Feature Announcement Cards";
49394942
sourceTree = "<group>";
@@ -10659,6 +10662,7 @@
1065910662
B958A7D328B52A2300823EEF /* MockRoute.swift in Sources */,
1066010663
02153211242376B5003F2BBD /* ProductPriceSettingsViewModelTests.swift in Sources */,
1066110664
45C8B25D231529410002FA77 /* CustomerInfoTableViewCellTests.swift in Sources */,
10665+
035BA3A8291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift in Sources */,
1066210666
023EC2E624DAB1270021DA91 /* EditableProductVariationModelTests.swift in Sources */,
1066310667
09BE3A9127C921A70070B69D /* BulkUpdatePriceSettingsViewModelTests.swift in Sources */,
1066410668
095A077E27CF486C007A61D2 /* ValueOneTableViewCellTests.swift in Sources */,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import XCTest
2+
import TestKit
3+
import Fakes
4+
import Yosemite
5+
import Combine
6+
7+
@testable import WooCommerce
8+
9+
final class JustInTimeMessageAnnouncementCardViewModelTests: XCTestCase {
10+
private var subscriptions = Set<AnyCancellable>()
11+
private var webviewPublishes: [WebViewSheetViewModel]!
12+
private var analyticsProvider: MockAnalyticsProvider!
13+
private var analytics: Analytics!
14+
private var sut: JustInTimeMessageAnnouncementCardViewModel!
15+
16+
override func setUp() {
17+
subscriptions = Set<AnyCancellable>()
18+
webviewPublishes = [WebViewSheetViewModel]()
19+
analyticsProvider = MockAnalyticsProvider()
20+
analytics = WooAnalytics(analyticsProvider: analyticsProvider)
21+
}
22+
23+
func setUp(with message: YosemiteJustInTimeMessage) {
24+
sut = JustInTimeMessageAnnouncementCardViewModel(justInTimeMessage: message,
25+
screenName: "my_store",
26+
siteID: 1234,
27+
analytics: analytics)
28+
29+
sut.$showWebViewSheet
30+
.sink { [weak self] webViewSheetViewModel in
31+
if let webViewSheetViewModel = webViewSheetViewModel {
32+
self?.webviewPublishes.append(webViewSheetViewModel)
33+
}
34+
}
35+
.store(in: &self.subscriptions)
36+
}
37+
38+
func test_ctaTapped_presents_a_webview_with_the_url_adding_correct_utm_parameters() throws {
39+
// Given
40+
setUp(with: YosemiteJustInTimeMessage.fake().copy(messageID: "message_id",
41+
featureClass: "feature_class",
42+
url: "https://woocommerce.com/take-action"))
43+
44+
// When
45+
sut.ctaTapped()
46+
47+
// Then
48+
let actualUrl = try XCTUnwrap(webviewPublishes.last?.url)
49+
let query = try XCTUnwrap(URLComponents(url: actualUrl, resolvingAgainstBaseURL: false)?.query)
50+
assertThat(query, contains: "utm_source=my_store")
51+
assertThat(query, contains: "utm_campaign=jitm_group_feature_class")
52+
assertThat(query, contains: "utm_content=jitm_message_id")
53+
assertThat(query, contains: "utm_term=1234")
54+
}
55+
56+
func test_ctaTapped_presents_an_authenticated_webview_for_woocommerce() throws {
57+
// Given
58+
setUp(with: YosemiteJustInTimeMessage.fake().copy(url: "https://woocommerce.com/take-action"))
59+
60+
// When
61+
sut.ctaTapped()
62+
63+
// Then
64+
let webViewViewModel = try XCTUnwrap(webviewPublishes.last)
65+
XCTAssertTrue(webViewViewModel.wpComAuthenticated)
66+
}
67+
68+
func test_ctaTapped_presents_an_authenticated_webview_for_wordpress() throws {
69+
// Given
70+
setUp(with: YosemiteJustInTimeMessage.fake().copy(url: "https://wordpress.com/take-action"))
71+
72+
// When
73+
sut.ctaTapped()
74+
75+
// Then
76+
let webViewViewModel = try XCTUnwrap(webviewPublishes.last)
77+
XCTAssertTrue(webViewViewModel.wpComAuthenticated)
78+
}
79+
80+
func test_ctaTapped_presents_an_unauthenticated_webview_for_other_url() throws {
81+
// Given
82+
setUp(with: YosemiteJustInTimeMessage.fake().copy(url: "https://example.com/take-action"))
83+
84+
// When
85+
sut.ctaTapped()
86+
87+
// Then
88+
let webViewViewModel = try XCTUnwrap(webviewPublishes.last)
89+
XCTAssertFalse(webViewViewModel.wpComAuthenticated)
90+
}
91+
92+
func test_ctaTapped_tracks_jitm_cta_tapped_event() {
93+
// Given
94+
setUp(with: YosemiteJustInTimeMessage.fake().copy(messageID: "test-message-id", featureClass: "test-feature-class"))
95+
96+
// When
97+
sut.ctaTapped()
98+
99+
// Then
100+
guard let eventIndex = analyticsProvider.receivedEvents.firstIndex(of: "jitm_cta_tapped")
101+
else {
102+
return XCTFail("Analytics not logged")
103+
}
104+
let properties = analyticsProvider.receivedProperties[eventIndex] as? [String: String]
105+
let expectedProperties = ["jitm_id": "test-message-id",
106+
"jitm_group": "test-feature-class",
107+
"source": "my_store"]
108+
assertEqual(expectedProperties, properties)
109+
}
110+
}

WooFoundation/WooFoundation.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
03597A9928F8799C005E4A98 /* MockUTMParameterProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03597A9728F87948005E4A98 /* MockUTMParameterProvider.swift */; };
1212
03597A9B28F87BFC005E4A98 /* WooCommerceComUTMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03597A9A28F87BFC005E4A98 /* WooCommerceComUTMProvider.swift */; };
1313
03597A9D28F93409005E4A98 /* WooCommerceComUTMProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03597A9C28F93409005E4A98 /* WooCommerceComUTMProviderTests.swift */; };
14+
035BA3A6290FF98D0056F0AD /* UTMProviderProtocolExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BA3A5290FF98D0056F0AD /* UTMProviderProtocolExtensionTests.swift */; };
1415
036563D728F93F8D00D84BFD /* TestKit in Frameworks */ = {isa = PBXBuildFile; productRef = 036563D628F93F8D00D84BFD /* TestKit */; };
1516
265C99D828B93F04005E6117 /* ColorPalette.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 265C99D728B93F04005E6117 /* ColorPalette.xcassets */; };
1617
265C99DD28B941D5005E6117 /* UIColor+Muriel-Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265C99DC28B941D5005E6117 /* UIColor+Muriel-Tests.swift */; };
@@ -54,6 +55,7 @@
5455
03597A9728F87948005E4A98 /* MockUTMParameterProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUTMParameterProvider.swift; sourceTree = "<group>"; };
5556
03597A9A28F87BFC005E4A98 /* WooCommerceComUTMProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooCommerceComUTMProvider.swift; sourceTree = "<group>"; };
5657
03597A9C28F93409005E4A98 /* WooCommerceComUTMProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooCommerceComUTMProviderTests.swift; sourceTree = "<group>"; };
58+
035BA3A5290FF98D0056F0AD /* UTMProviderProtocolExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMProviderProtocolExtensionTests.swift; sourceTree = "<group>"; };
5759
265C99D728B93F04005E6117 /* ColorPalette.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = ColorPalette.xcassets; sourceTree = "<group>"; };
5860
265C99DC28B941D5005E6117 /* UIColor+Muriel-Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Muriel-Tests.swift"; sourceTree = "<group>"; };
5961
265C99DE28B94271005E6117 /* MurielColorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MurielColorTests.swift; sourceTree = "<group>"; };
@@ -160,6 +162,7 @@
160162
265C99DB28B941D5005E6117 /* Colors */,
161163
686BE914288EE2CA00967C86 /* TypedPredicateTests.swift */,
162164
03597A9C28F93409005E4A98 /* WooCommerceComUTMProviderTests.swift */,
165+
035BA3A5290FF98D0056F0AD /* UTMProviderProtocolExtensionTests.swift */,
163166
);
164167
path = Utilities;
165168
sourceTree = "<group>";
@@ -500,6 +503,7 @@
500503
265C99DF28B94271005E6117 /* MurielColorTests.swift in Sources */,
501504
AE948D0D28CF6D50009F3246 /* DateStartAndEndTests.swift in Sources */,
502505
265C99DD28B941D5005E6117 /* UIColor+Muriel-Tests.swift in Sources */,
506+
035BA3A6290FF98D0056F0AD /* UTMProviderProtocolExtensionTests.swift in Sources */,
503507
);
504508
runOnlyForDeploymentPostprocessing = 0;
505509
};

WooFoundation/WooFoundation/Mocks/MockUTMParameterProvider.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import Foundation
22

33
public struct MockUTMParameterProvider: UTMParametersProviding {
4+
public var limitToHosts: [String]?
5+
46
public var parameters: [UTMParameterKey: String?]
57

68
public init(parameters: [UTMParameterKey: String?] = [.medium: "woo_ios"]) {

0 commit comments

Comments
 (0)