Skip to content

Commit

Permalink
[GLA Analytics] Add view model for Google Campaigns analytics card (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
rachelmcr authored Jun 28, 2024
2 parents 70cd9f3 + 7a0c0b3 commit 1b1a5b5
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ struct AnalyticsTopPerformersCard: View {
///
let statsErrorMessage: String

/// Title for top performers list.
///
let topPerformersTitle: String

/// Top performers data to render.
///
let topPerformersData: [TopPerformersRow.Data]
Expand Down Expand Up @@ -90,7 +94,7 @@ struct AnalyticsTopPerformersCard: View {
.padding(.top, Layout.columnSpacing)
}

TopPerformersView(itemTitle: title.localizedCapitalized,
TopPerformersView(itemTitle: topPerformersTitle.localizedCapitalized,
valueTitle: statTitle,
rows: topPerformersData,
isRedacted: isTopPerformersRedacted)
Expand Down Expand Up @@ -134,7 +138,8 @@ struct AnalyticsItemsSoldCardPreviews: PreviewProvider {
deltaTextColor: .textInverted,
isStatsRedacted: false,
showStatsError: false,
statsErrorMessage: "Unable to load product analytics",
statsErrorMessage: "Unable to load product analytics",
topPerformersTitle: "Products",
topPerformersData: [
.init(imageURL: imageURL, name: "Tabletop Photos", details: "Net Sales: $1,232", value: "32"),
.init(imageURL: imageURL, name: "Kentya Palm", details: "Net Sales: $800", value: "10"),
Expand All @@ -160,6 +165,7 @@ struct AnalyticsItemsSoldCardPreviews: PreviewProvider {
isStatsRedacted: false,
showStatsError: true,
statsErrorMessage: "Unable to load product analytics",
topPerformersTitle: "Products",
topPerformersData: [],
isTopPerformersRedacted: false,
showTopPerformersError: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ extension AnalyticsTopPerformersCard {
self.reportViewModel = bundlesViewModel.reportViewModel

// Top performers list
self.topPerformersTitle = bundlesViewModel.title
self.topPerformersData = bundlesViewModel.bundlesSoldData
self.isTopPerformersRedacted = bundlesViewModel.isRedacted
self.showTopPerformersError = bundlesViewModel.showBundlesSoldError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct AnalyticsWebReport {
case products
case bundles
case giftCards
case googlePrograms
}

/// Provides the URL for a web analytics report
Expand Down Expand Up @@ -62,6 +63,8 @@ struct AnalyticsWebReport {
return "/analytics/bundles"
case .giftCards:
return "/analytics/gift-cards"
case .googlePrograms:
return "/google/reports&reportKey=programs"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import Foundation
import Yosemite

/// Analytics Hub Google Ads Campaign Card ViewModel.
/// Used to transmit Google Ads campaigns analytics data.
///
final class GoogleAdsCampaignReportCardViewModel {
/// Campaign stats for the current period
///
private var currentPeriodStats: GoogleAdsCampaignStats?

/// Campaign stats for the previous period
///
private var previousPeriodStats: GoogleAdsCampaignStats?

/// Selected time range
///
private var timeRange: AnalyticsHubTimeRangeSelection.SelectionType

/// Analytics Usage Tracks Event Emitter
///
private let usageTracksEventEmitter: StoreStatsUsageTracksEventEmitter

/// Store admin URL
///
private let storeAdminURL: String?

/// Indicates if the values should be hidden (for loading state)
///
var isRedacted: Bool

init(currentPeriodStats: GoogleAdsCampaignStats?,
previousPeriodStats: GoogleAdsCampaignStats?,
timeRange: AnalyticsHubTimeRangeSelection.SelectionType,
isRedacted: Bool = false,
usageTracksEventEmitter: StoreStatsUsageTracksEventEmitter,
storeAdminURL: String? = ServiceLocator.stores.sessionManager.defaultSite?.adminURL) {
self.currentPeriodStats = currentPeriodStats
self.previousPeriodStats = previousPeriodStats
self.timeRange = timeRange
self.isRedacted = isRedacted
self.usageTracksEventEmitter = usageTracksEventEmitter
self.storeAdminURL = storeAdminURL
}
}

extension GoogleAdsCampaignReportCardViewModel {

/// Card Title
///
var title: String {
Localization.title
}

// MARK: Total Sales

/// Total Sales title
///
var totalSalesTitle: String {
return Localization.totalSales
}

/// Total Sales value
///
var totalSales: String {
guard !isRedacted else {
return "1000"
}
return StatsDataTextFormatter.formatAmount(currentPeriodStats?.totals.sales)
}

/// Total Sales delta percentage
///
var delta: DeltaPercentage {
isRedacted ? DeltaPercentage(string: "0%", direction: .zero)
: StatsDataTextFormatter.createDeltaPercentage(from: previousPeriodStats?.totals.sales, to: currentPeriodStats?.totals.sales)
}

// MARK: Campaigns report

/// Title for campaigns list.
///
var campaignsTitle: String {
Localization.campaignsTitle
}

/// Campaigns data to render.
///
var campaignsData: [TopPerformersRow.Data] {
isRedacted ? [.init(showImage: false, name: "Campaign", details: "Spend: $100", value: "$500")] : campaignRows(from: currentPeriodStats)
}

/// Indicates if there was an error loading campaigns part of the card.
///
var showCampaignsError: Bool {
isRedacted ? false : currentPeriodStats == nil
}

/// Error message if there was an error loading campaigns part of the card.
///
var campaignsErrorMessage: String {
Localization.noCampaignStats
}

/// View model for the web analytics report link
///
var reportViewModel: AnalyticsReportLinkViewModel? {
guard let url = AnalyticsWebReport.getUrl(for: .googlePrograms, timeRange: timeRange, storeAdminURL: storeAdminURL) else {
return nil
}
return AnalyticsReportLinkViewModel(reportType: .googlePrograms,
period: timeRange,
webViewTitle: Localization.reportTitle,
reportURL: url,
usageTracksEventEmitter: usageTracksEventEmitter)
}

/// Helper functions to create `TopPerformersRow.Data` items from the provided `GoogleAdsCampaignStats`.
///
private func campaignRows(from stats: GoogleAdsCampaignStats?) -> [TopPerformersRow.Data] {
// Sort campaigns by their total sales.
guard let sortedCampaigns = stats?.campaigns.sorted(by: { $0.subtotals.sales ?? 0 > $1.subtotals.sales ?? 0 }) else {
return []
}

// Extract top five campaigns for display.
let topCampaigns = Array(sortedCampaigns.prefix(5))

return topCampaigns.map { campaign in
return TopPerformersRow.Data(showImage: false,
name: campaign.campaignName ?? "",
details: Localization.spend(value: StatsDataTextFormatter.formatAmount(campaign.subtotals.spend)),
value: StatsDataTextFormatter.formatAmount(campaign.subtotals.sales))
}
}
}

/// Convenience extension to create an `AnalyticsItemsSoldCard` from a view model.
///
extension AnalyticsTopPerformersCard {
init(campaignsViewModel: GoogleAdsCampaignReportCardViewModel) {
// Header with selected metric stats
self.title = campaignsViewModel.title
self.statTitle = campaignsViewModel.totalSalesTitle
self.statValue = campaignsViewModel.totalSales
self.delta = campaignsViewModel.delta.string
self.deltaBackgroundColor = campaignsViewModel.delta.direction.deltaBackgroundColor
self.deltaTextColor = campaignsViewModel.delta.direction.deltaTextColor
self.isStatsRedacted = campaignsViewModel.isRedacted
// This card gets its metrics and campaigns list from the same source.
// If there is a problem loading stats data, the error message only appears once at the bottom of the card.
self.showStatsError = false
self.statsErrorMessage = ""
self.reportViewModel = campaignsViewModel.reportViewModel

// Top performers (campaigns) list
self.topPerformersTitle = campaignsViewModel.campaignsTitle
self.topPerformersData = campaignsViewModel.campaignsData
self.isTopPerformersRedacted = campaignsViewModel.isRedacted
self.showTopPerformersError = campaignsViewModel.showCampaignsError
self.topPerformersErrorMessage = campaignsViewModel.campaignsErrorMessage
}
}

// MARK: Constants
private extension GoogleAdsCampaignReportCardViewModel {
enum Localization {
static let reportTitle = NSLocalizedString("analyticsHub.googleCampaigns.reportTitle",
value: "Programs Report",
comment: "Title for the Google Programs report linked in the Analytics Hub")
static let title = NSLocalizedString("analyticsHub.googleCampaigns.title",
value: "Google Campaigns",
comment: "Title for the Google campaigns card on the analytics hub screen.").localizedUppercase
static let campaignsTitle = NSLocalizedString("analyticsHub.googleCampaigns.campaignsList.title",
value: "Campaigns",
comment: "Title for the list of campaigns on the Google campaigns card on the analytics hub screen.")
static let totalSales = NSLocalizedString("analyticsHub.googleCampaigns.totalSalesTitle",
value: "Total Sales",
comment: "Title for the Total Sales column on the Google Ads campaigns card on the analytics hub screen.")
static let noCampaignStats = NSLocalizedString("analyticsHub.googleCampaigns.noCampaignStats",
value: "Unable to load Google campaigns analytics",
comment: "Text displayed when there is an error loading Google Ads campaigns stats data.")
static func spend(value: String) -> String {
String.localizedStringWithFormat(NSLocalizedString("analyticsHub.googleCampaigns.spendSubtitle",
value: "Spend: %@",
comment: "Label for the total spend amount on a Google Ads campaign in the Analytics Hub."
+ "The placeholder is a formatted monetary amount, e.g. Spend: $123."),
value)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ extension AnalyticsTopPerformersCard {
self.reportViewModel = statsViewModel.reportViewModel

// Top performers list
self.topPerformersTitle = statsViewModel.title
self.topPerformersData = itemsViewModel.itemsSoldData
self.isTopPerformersRedacted = itemsViewModel.isRedacted
self.showTopPerformersError = itemsViewModel.showItemsSoldError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ struct TopPerformersRow: View {
///
let imageURL: URL?

/// Whether to show the item image
///
let showImage: Bool

/// Image placeholder.
///
let placeHolder = UIImage.productPlaceholderImage
Expand All @@ -90,8 +94,9 @@ struct TopPerformersRow: View {
/// Handles the tap action if the row is tappable. If the row is not tappable, `nil` is set.
let tapHandler: (() -> Void)?

init(imageURL: URL? = nil, name: String, details: String, value: String, tapHandler: (() -> Void)? = nil) {
init(imageURL: URL? = nil, showImage: Bool = true, name: String, details: String, value: String, tapHandler: (() -> Void)? = nil) {
self.imageURL = imageURL
self.showImage = showImage
self.name = name
self.details = details
self.value = value
Expand All @@ -115,6 +120,7 @@ struct TopPerformersRow: View {
.resizable()
.frame(width: imageWidth, height: imageWidth)
.cornerRadius(Layout.imageCornerRadius)
.renderedIf(data.showImage)

// Text Labels + Value + Divider
VStack {
Expand Down
Loading

0 comments on commit 1b1a5b5

Please sign in to comment.