Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,29 @@ extension WooAnalyticsEvent {
}
}

// MARK: - Just In Time Messages
//
extension WooAnalyticsEvent {
enum JustInTimeMessage {
private enum Keys {
static let source = "source"
static let justInTimeMessageID = "jitm_id"
static let justInTimeMessageGroup = "jitm_group"
}

static func callToActionTapped(source: String,
messageID: String,
featureClass: String) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .justInTimeMessageCallToActionTapped,
properties: [
Keys.source: source,
Keys.justInTimeMessageID: messageID,
Keys.justInTimeMessageGroup: featureClass
])
}
}
}

// MARK: - Simple Payments
//
extension WooAnalyticsEvent {
Expand Down
3 changes: 3 additions & 0 deletions WooCommerce/Classes/Analytics/WooAnalyticsStat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,9 @@ public enum WooAnalyticsStat: String {
case featureCardDismissed = "feature_card_dismissed"
case featureCardCtaTapped = "feature_card_cta_tapped"

// MARK: Just In Time Messages events
case justInTimeMessageCallToActionTapped = "jitm_cta_tapped"

// MARK: Simple Payments events
//
case simplePaymentsFlowStarted = "simple_payments_flow_started"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,94 @@
import Foundation
import WooFoundation
import UIKit
import Yosemite
import Combine

struct JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewModelProtocol {
var showDividers: Bool = false
final class JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewModelProtocol {
private let siteID: Int64

var badgeType: BadgeView.BadgeType = .tip
private let analytics: Analytics

var title: String
// MARK: - Message properties
let title: String

var message: String
let message: String

var buttonTitle: String?
let buttonTitle: String?

var image: UIImage = .paymentsFeatureBannerImage
private let url: URL?

func onAppear() {
// No-op
}
private let messageID: String

let onCTATapped: (() -> Void)?
private let featureClass: String

func ctaTapped() {
onCTATapped?()
private let screenName: String

init(justInTimeMessage: YosemiteJustInTimeMessage,
screenName: String,
siteID: Int64,
analytics: Analytics = ServiceLocator.analytics) {
self.siteID = siteID
self.analytics = analytics
let utmProvider = WooCommerceComUTMProvider(
campaign: "jitm_group_\(justInTimeMessage.featureClass)",
source: screenName,
content: "jitm_\(justInTimeMessage.messageID)",
siteID: siteID)
self.url = utmProvider.urlWithUtmParams(string: justInTimeMessage.url)
self.messageID = justInTimeMessage.messageID
self.featureClass = justInTimeMessage.featureClass
self.screenName = screenName
self.title = justInTimeMessage.title
self.message = justInTimeMessage.detail
self.buttonTitle = justInTimeMessage.buttonTitle
}

// MARK: - output streams
@Published private(set) var showWebViewSheet: WebViewSheetViewModel?

// MARK: - default AnnouncementCardViewModelProtocol conformance
let showDividers: Bool = false

let badgeType: BadgeView.BadgeType = .tip

let image: UIImage = .paymentsFeatureBannerImage

var showDismissButton: Bool = true

var showDismissConfirmation: Bool = false
let showDismissConfirmation: Bool = false

let dismissAlertTitle: String = ""

var dismissAlertTitle: String = ""
let dismissAlertMessage: String = ""

var dismissAlertMessage: String = ""
// MARK: - AnnouncementCardViewModelProtocol methods
func onAppear() {
// No-op
}

func ctaTapped() {
analytics.track(event: WooAnalyticsEvent.JustInTimeMessage.callToActionTapped(
source: screenName,
messageID: messageID,
featureClass: featureClass))

guard let url = url else {
return
}
let webViewModel = WebViewSheetViewModel(
url: url,
navigationTitle: title,
wpComAuthenticated: needsAuthenticatedWebView(url: url))
showWebViewSheet = webViewModel
}

private func needsAuthenticatedWebView(url: URL) -> Bool {
guard let host = url.host else {
return false
}
return Constants.trustedDomains.contains(host)
}

func dontShowAgainTapped() {
// No-op
Expand All @@ -39,5 +97,10 @@ struct JustInTimeMessageAnnouncementCardViewModel: AnnouncementCardViewModelProt
func remindLaterTapped() {
// No-op
}
}

extension JustInTimeMessageAnnouncementCardViewModel {
enum Constants {
static let trustedDomains = ["woocommerce.com", "wordpress.com"]
}
}
25 changes: 4 additions & 21 deletions WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,33 +160,17 @@ final class DashboardViewModel {
switch result {
case let .success(.some(message)):
let viewModel = JustInTimeMessageAnnouncementCardViewModel(
title: message.title,
message: message.detail,
buttonTitle: message.buttonTitle,
onCTATapped: { [weak self] in
guard let self = self,
let url = URL(string: message.url)
else { return }
let webViewModel = WebViewSheetViewModel(
url: url,
navigationTitle: message.title,
wpComAuthenticated: self.needsAuthenticatedWebView(url: url))
self.showWebViewSheet = webViewModel
})
justInTimeMessage: message,
screenName: Constants.dashboardScreenName,
siteID: siteID)
self.announcementViewModel = viewModel
viewModel.$showWebViewSheet.assign(to: &self.$showWebViewSheet)
default:
break
}
}
stores.dispatch(action)
}

private func needsAuthenticatedWebView(url: URL) -> Bool {
guard let host = url.host else {
return false
}
return Constants.trustedDomains.contains(host)
}
}

// MARK: - Constants
Expand All @@ -195,6 +179,5 @@ private extension DashboardViewModel {
enum Constants {
static let topEarnerStatsLimit: Int = 5
static let dashboardScreenName = "my_store"
static let trustedDomains = ["woocommerce.com", "wordpress.com"]
}
}
4 changes: 4 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@
03191AE628E1DF0600670723 /* WooCommercePluginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03191AE528E1DF0600670723 /* WooCommercePluginViewModel.swift */; };
03191AE928E20C9200670723 /* PluginDetailsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03191AE828E20C9200670723 /* PluginDetailsRowView.swift */; };
031B10E3274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031B10E2274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift */; };
035BA3A8291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */; };
035C6DEB273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035C6DEA273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift */; };
035F2308275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035F2307275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift */; };
0366EAE12909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0366EAE02909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift */; };
Expand Down Expand Up @@ -2392,6 +2393,7 @@
03191AE528E1DF0600670723 /* WooCommercePluginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooCommercePluginViewModel.swift; sourceTree = "<group>"; };
03191AE828E20C9200670723 /* PluginDetailsRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginDetailsRowView.swift; sourceTree = "<group>"; };
031B10E2274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalConnectionFailedUpdateAddress.swift; sourceTree = "<group>"; };
035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageAnnouncementCardViewModelTests.swift; sourceTree = "<group>"; };
035C6DEA273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdateTypeProperty.swift; sourceTree = "<group>"; };
035F2307275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalConnectingFailedUpdatePostalCode.swift; sourceTree = "<group>"; };
0366EAE02909A37800B51755 /* JustInTimeMessageAnnouncementCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustInTimeMessageAnnouncementCardViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4932,6 +4934,7 @@
isa = PBXGroup;
children = (
0371C3692876DBCA00277E2C /* FeatureAnnouncementCardViewModelTests.swift */,
035BA3A7291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift */,
);
path = "Feature Announcement Cards";
sourceTree = "<group>";
Expand Down Expand Up @@ -10655,6 +10658,7 @@
B958A7D328B52A2300823EEF /* MockRoute.swift in Sources */,
02153211242376B5003F2BBD /* ProductPriceSettingsViewModelTests.swift in Sources */,
45C8B25D231529410002FA77 /* CustomerInfoTableViewCellTests.swift in Sources */,
035BA3A8291000E90056F0AD /* JustInTimeMessageAnnouncementCardViewModelTests.swift in Sources */,
023EC2E624DAB1270021DA91 /* EditableProductVariationModelTests.swift in Sources */,
09BE3A9127C921A70070B69D /* BulkUpdatePriceSettingsViewModelTests.swift in Sources */,
095A077E27CF486C007A61D2 /* ValueOneTableViewCellTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import XCTest
import TestKit
import Fakes
import Yosemite
import Combine

@testable import WooCommerce

final class JustInTimeMessageAnnouncementCardViewModelTests: XCTestCase {
private var subscriptions = Set<AnyCancellable>()
private var webviewPublishes: [WebViewSheetViewModel]!
private var analyticsProvider: MockAnalyticsProvider!
private var analytics: Analytics!
private var sut: JustInTimeMessageAnnouncementCardViewModel!

override func setUp() {
subscriptions = Set<AnyCancellable>()
webviewPublishes = [WebViewSheetViewModel]()
analyticsProvider = MockAnalyticsProvider()
analytics = WooAnalytics(analyticsProvider: analyticsProvider)
}

func setUp(with message: YosemiteJustInTimeMessage) {
sut = JustInTimeMessageAnnouncementCardViewModel(justInTimeMessage: message,
screenName: "my_store",
siteID: 1234,
analytics: analytics)

sut.$showWebViewSheet
.sink { [weak self] webViewSheetViewModel in
if let webViewSheetViewModel = webViewSheetViewModel {
self?.webviewPublishes.append(webViewSheetViewModel)
}
}
.store(in: &self.subscriptions)
}

func test_ctaTapped_presents_a_webview_with_the_url_adding_correct_utm_parameters() throws {
// Given
setUp(with: YosemiteJustInTimeMessage.fake().copy(messageID: "message_id",
featureClass: "feature_class",
url: "https://woocommerce.com/take-action"))

// When
sut.ctaTapped()

// Then
let actualUrl = try XCTUnwrap(webviewPublishes.last?.url)
let query = try XCTUnwrap(URLComponents(url: actualUrl, resolvingAgainstBaseURL: false)?.query)
assertThat(query, contains: "utm_source=my_store")
assertThat(query, contains: "utm_campaign=jitm_group_feature_class")
assertThat(query, contains: "utm_content=jitm_message_id")
assertThat(query, contains: "utm_term=1234")
}

func test_ctaTapped_presents_an_authenticated_webview_for_woocommerce() throws {
// Given
setUp(with: YosemiteJustInTimeMessage.fake().copy(url: "https://woocommerce.com/take-action"))

// When
sut.ctaTapped()

// Then
let webViewViewModel = try XCTUnwrap(webviewPublishes.last)
XCTAssertTrue(webViewViewModel.wpComAuthenticated)
}

func test_ctaTapped_presents_an_authenticated_webview_for_wordpress() throws {
// Given
setUp(with: YosemiteJustInTimeMessage.fake().copy(url: "https://wordpress.com/take-action"))

// When
sut.ctaTapped()

// Then
let webViewViewModel = try XCTUnwrap(webviewPublishes.last)
XCTAssertTrue(webViewViewModel.wpComAuthenticated)
}

func test_ctaTapped_presents_an_unauthenticated_webview_for_other_url() throws {
// Given
setUp(with: YosemiteJustInTimeMessage.fake().copy(url: "https://example.com/take-action"))

// When
sut.ctaTapped()

// Then
let webViewViewModel = try XCTUnwrap(webviewPublishes.last)
XCTAssertFalse(webViewViewModel.wpComAuthenticated)
}

func test_ctaTapped_tracks_jitm_cta_tapped_event() {
// Given
setUp(with: YosemiteJustInTimeMessage.fake().copy(messageID: "test-message-id", featureClass: "test-feature-class"))

// When
sut.ctaTapped()

// Then
guard let eventIndex = analyticsProvider.receivedEvents.firstIndex(of: "jitm_cta_tapped")
else {
return XCTFail("Analytics not logged")
}
let properties = analyticsProvider.receivedProperties[eventIndex] as? [String: String]
let expectedProperties = ["jitm_id": "test-message-id",
"jitm_group": "test-feature-class",
"source": "my_store"]
assertEqual(expectedProperties, properties)
}
}
4 changes: 4 additions & 0 deletions WooFoundation/WooFoundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
03597A9928F8799C005E4A98 /* MockUTMParameterProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03597A9728F87948005E4A98 /* MockUTMParameterProvider.swift */; };
03597A9B28F87BFC005E4A98 /* WooCommerceComUTMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03597A9A28F87BFC005E4A98 /* WooCommerceComUTMProvider.swift */; };
03597A9D28F93409005E4A98 /* WooCommerceComUTMProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03597A9C28F93409005E4A98 /* WooCommerceComUTMProviderTests.swift */; };
035BA3A6290FF98D0056F0AD /* UTMProviderProtocolExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BA3A5290FF98D0056F0AD /* UTMProviderProtocolExtensionTests.swift */; };
036563D728F93F8D00D84BFD /* TestKit in Frameworks */ = {isa = PBXBuildFile; productRef = 036563D628F93F8D00D84BFD /* TestKit */; };
265C99D828B93F04005E6117 /* ColorPalette.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 265C99D728B93F04005E6117 /* ColorPalette.xcassets */; };
265C99DD28B941D5005E6117 /* UIColor+Muriel-Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265C99DC28B941D5005E6117 /* UIColor+Muriel-Tests.swift */; };
Expand Down Expand Up @@ -54,6 +55,7 @@
03597A9728F87948005E4A98 /* MockUTMParameterProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUTMParameterProvider.swift; sourceTree = "<group>"; };
03597A9A28F87BFC005E4A98 /* WooCommerceComUTMProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooCommerceComUTMProvider.swift; sourceTree = "<group>"; };
03597A9C28F93409005E4A98 /* WooCommerceComUTMProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooCommerceComUTMProviderTests.swift; sourceTree = "<group>"; };
035BA3A5290FF98D0056F0AD /* UTMProviderProtocolExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMProviderProtocolExtensionTests.swift; sourceTree = "<group>"; };
265C99D728B93F04005E6117 /* ColorPalette.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = ColorPalette.xcassets; sourceTree = "<group>"; };
265C99DC28B941D5005E6117 /* UIColor+Muriel-Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Muriel-Tests.swift"; sourceTree = "<group>"; };
265C99DE28B94271005E6117 /* MurielColorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MurielColorTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -160,6 +162,7 @@
265C99DB28B941D5005E6117 /* Colors */,
686BE914288EE2CA00967C86 /* TypedPredicateTests.swift */,
03597A9C28F93409005E4A98 /* WooCommerceComUTMProviderTests.swift */,
035BA3A5290FF98D0056F0AD /* UTMProviderProtocolExtensionTests.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -500,6 +503,7 @@
265C99DF28B94271005E6117 /* MurielColorTests.swift in Sources */,
AE948D0D28CF6D50009F3246 /* DateStartAndEndTests.swift in Sources */,
265C99DD28B941D5005E6117 /* UIColor+Muriel-Tests.swift in Sources */,
035BA3A6290FF98D0056F0AD /* UTMProviderProtocolExtensionTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Foundation

public struct MockUTMParameterProvider: UTMParametersProviding {
public var limitToHosts: [String]?

public var parameters: [UTMParameterKey: String?]

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