Skip to content

Commit 2172859

Browse files
Copilotstaskus
andcommitted
Add currency mismatch warning banner to dashboard
- Created CurrencyMismatchBanner SwiftUI view component - Added currency mismatch detection in DashboardViewModel - Display banner when site and payment gateway currencies differ - Banner is dismissible and tracks dismissal in UserDefaults - Added analytics event for banner dismissal Co-authored-by: staskus <[email protected]>
1 parent 834f46c commit 2172859

File tree

6 files changed

+169
-0
lines changed

6 files changed

+169
-0
lines changed

Modules/Sources/WooFoundationCore/Analytics/WooAnalyticsStat.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public enum WooAnalyticsStat: String {
138138
case dashboardMainStatsWaitingTimeLoaded = "dashboard_main_stats_waiting_time_loaded"
139139
case dashboardTopPerformersWaitingTimeLoaded = "dashboard_top_performers_waiting_time_loaded"
140140
case dashboardStoreTimezoneDifferFromDevice = "dashboard_store_timezone_differ_from_device"
141+
case dashboardCurrencyMismatchBannerDismissed = "dashboard_currency_mismatch_banner_dismissed"
141142

142143
// MARK: Dashboard stats custom range
143144
case dashboardStatsCustomRangeAddButtonTapped = "dashboard_stats_custom_range_add_button_tapped"

WooCommerce/Classes/Analytics/WooAnalyticsEvent+Dashboard.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ extension WooAnalyticsEvent {
4444
properties: [Keys.storeTimezone: storeTimezoneText,
4545
Keys.localTimezone: localTimezoneText])
4646
}
47+
48+
/// Tracked when the currency mismatch banner is dismissed.
49+
static let currencyMismatchBannerDismissed = WooAnalyticsEvent(statName: .dashboardCurrencyMismatchBannerDismissed, properties: [:])
4750
}
4851
}
4952

WooCommerce/Classes/Extensions/UserDefaults+Woo.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ extension UserDefaults {
7676

7777
// CIAB Bookings tab availability
7878
case ciabBookingsTabAvailable
79+
80+
// Currency mismatch banner dismissal
81+
case currencyMismatchBannerDismissed
7982
}
8083
}
8184

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import SwiftUI
2+
3+
/// A banner warning about currency mismatch between site and user account
4+
struct CurrencyMismatchBanner: View {
5+
let siteCurrency: String
6+
let accountCurrency: String
7+
8+
/// Closure invoked when the dismiss button is tapped
9+
var dismissAction: () -> Void = {}
10+
11+
// Tracks the scale of the view due to accessibility changes
12+
@ScaledMetric private var scale: CGFloat = 1.0
13+
14+
var body: some View {
15+
Group {
16+
HStack(alignment: .top, spacing: Layout.horizontalSpacing) {
17+
Image(systemName: "exclamationmark.triangle.fill")
18+
.resizable()
19+
.frame(width: Layout.iconDimension * scale, height: Layout.iconDimension * scale)
20+
.foregroundColor(Color(.wooOrange))
21+
VStack(alignment: .leading, spacing: Layout.verticalTextSpacing) {
22+
Text(Localization.title)
23+
.fontWeight(.semibold)
24+
.bodyStyle()
25+
Text(String(format: Localization.message, siteCurrency, accountCurrency))
26+
.bodyStyle()
27+
.foregroundColor(Color(.text.secondary))
28+
}
29+
Spacer()
30+
Button(action: dismissAction) {
31+
Image(systemName: "xmark")
32+
.foregroundColor(Color(.text.secondary))
33+
}
34+
}
35+
.padding(insets: Layout.padding)
36+
}
37+
.background(Color(.bannerBackground))
38+
.fixedSize(horizontal: false, vertical: true)
39+
.contentShape(Rectangle())
40+
}
41+
}
42+
43+
private extension CurrencyMismatchBanner {
44+
enum Localization {
45+
static let title = NSLocalizedString(
46+
"currencyMismatchBanner.title",
47+
value: "Currency mismatch detected",
48+
comment: "Title of the currency mismatch warning banner on the dashboard."
49+
)
50+
static let message = NSLocalizedString(
51+
"currencyMismatchBanner.message",
52+
value: "Your site uses %1$@ but your payment account uses %2$@. This may cause issues with payments. Please update your account currency to match your site's currency in your payment gateway settings.",
53+
comment: "Message of the currency mismatch warning banner. %1$@ is the site currency code (e.g. USD), %2$@ is the account currency code (e.g. GBP)."
54+
)
55+
}
56+
57+
enum Layout {
58+
static let padding = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
59+
static let iconDimension = CGFloat(20)
60+
static let horizontalSpacing = CGFloat(12)
61+
static let verticalTextSpacing = CGFloat(8)
62+
}
63+
}
64+
65+
struct CurrencyMismatchBanner_Previews: PreviewProvider {
66+
static var previews: some View {
67+
CurrencyMismatchBanner(siteCurrency: "USD", accountCurrency: "GBP")
68+
.preferredColorScheme(.light)
69+
.previewLayout(.sizeThatFits)
70+
CurrencyMismatchBanner(siteCurrency: "USD", accountCurrency: "GBP")
71+
.preferredColorScheme(.dark)
72+
.previewLayout(.sizeThatFits)
73+
CurrencyMismatchBanner(siteCurrency: "EUR", accountCurrency: "CAD")
74+
.preferredColorScheme(.light)
75+
.environment(\.sizeCategory, .extraExtraExtraLarge)
76+
.previewLayout(.sizeThatFits)
77+
}
78+
}

WooCommerce/Classes/ViewRelated/Dashboard/DashboardView.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ struct DashboardView: View {
157157
viewModel.onPullToRefresh()
158158
}
159159
.safeAreaInset(edge: .bottom) {
160+
currencyMismatchBanner
161+
.renderedIf(viewModel.showCurrencyMismatchBanner)
162+
160163
jetpackBenefitBanner
161164
.renderedIf(shouldShowJetpackBenefitsBanner)
162165

@@ -340,6 +343,16 @@ private extension DashboardView {
340343
.padding(.horizontal, Layout.padding)
341344
}
342345

346+
var currencyMismatchBanner: some View {
347+
CurrencyMismatchBanner(
348+
siteCurrency: viewModel.siteCurrency,
349+
accountCurrency: viewModel.accountCurrency,
350+
dismissAction: {
351+
viewModel.dismissCurrencyMismatchBanner()
352+
}
353+
)
354+
}
355+
343356
var jetpackBenefitBanner: some View {
344357
JetpackBenefitsBanner(tapAction: {
345358
ServiceLocator.analytics.track(event: .jetpackBenefitsBanner(action: .tapped))

WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22
import Yosemite
33
import Combine
4+
import CocoaLumberjack
45
import enum Networking.DotcomError
56
import enum Storage.StatsVersion
67
import protocol Storage.StorageManagerType
@@ -65,6 +66,12 @@ final class DashboardViewModel: ObservableObject {
6566

6667
@Published private(set) var isSiteEligibleToInstallJetpack = true
6768

69+
@Published private(set) var showCurrencyMismatchBanner = false
70+
71+
@Published private(set) var siteCurrency: String = ""
72+
73+
@Published private(set) var accountCurrency: String = ""
74+
6875
@Published private var hasOrders = false
6976

7077
@Published private(set) var isEligibleForInbox = false
@@ -199,6 +206,8 @@ final class DashboardViewModel: ObservableObject {
199206
await reloadCardsWithBackgroundUpdateSupportIfNeeded()
200207

201208
await blazeLocalNotificationScheduler.scheduleNoCampaignReminder()
209+
210+
await checkCurrencyMismatch()
202211
}
203212

204213
func handleCustomizationDismissal() {
@@ -778,6 +787,68 @@ private extension DashboardViewModel {
778787
func updateJetpackBannerVisibilityFromAppSettings() async {
779788
jetpackBannerVisibleFromAppSettings = await loadJetpackBannerVisibilityFromAppSettings()
780789
}
790+
791+
@MainActor
792+
func checkCurrencyMismatch() async {
793+
// Check if banner was previously dismissed
794+
let isDismissed = userDefaults.bool(forKey: UserDefaults.Key.currencyMismatchBannerDismissed.rawValue)
795+
guard !isDismissed else {
796+
showCurrencyMismatchBanner = false
797+
return
798+
}
799+
800+
let siteCurrencyCode = ServiceLocator.currencySettings.currencyCode.rawValue
801+
802+
// Try to get payment gateway account currency
803+
guard let accountCurrencyCode = await loadPaymentGatewayAccountCurrency() else {
804+
showCurrencyMismatchBanner = false
805+
return
806+
}
807+
808+
// Check if currencies mismatch
809+
if siteCurrencyCode != accountCurrencyCode {
810+
siteCurrency = siteCurrencyCode
811+
accountCurrency = accountCurrencyCode
812+
showCurrencyMismatchBanner = true
813+
814+
// Log the mismatch for analytics/debugging
815+
DDLogWarn("⚠️ Currency mismatch detected: Site currency \(siteCurrencyCode) differs from payment account currency \(accountCurrencyCode)")
816+
} else {
817+
showCurrencyMismatchBanner = false
818+
}
819+
}
820+
821+
func dismissCurrencyMismatchBanner() {
822+
showCurrencyMismatchBanner = false
823+
userDefaults.set(true, forKey: UserDefaults.Key.currencyMismatchBannerDismissed.rawValue)
824+
ServiceLocator.analytics.track(event: .Dashboard.currencyMismatchBannerDismissed)
825+
}
826+
827+
@MainActor
828+
private func loadPaymentGatewayAccountCurrency() async -> String? {
829+
await withCheckedContinuation { continuation in
830+
// Load payment gateway accounts
831+
let action = CardPresentPaymentAction.loadAccounts(siteID: siteID) { [weak self] result in
832+
guard let self = self else {
833+
continuation.resume(returning: nil)
834+
return
835+
}
836+
837+
// Try to get the selected payment gateway account
838+
let selectedAccountAction = CardPresentPaymentAction.selectedPaymentGatewayAccount { account in
839+
if let account = account {
840+
continuation.resume(returning: account.defaultCurrency)
841+
} else {
842+
// If no account is selected, try to load from storage
843+
let accounts = self.storageManager.viewStorage.loadPaymentGatewayAccounts(siteID: self.siteID)
844+
continuation.resume(returning: accounts.first?.defaultCurrency)
845+
}
846+
}
847+
self.stores.dispatch(selectedAccountAction)
848+
}
849+
stores.dispatch(action)
850+
}
851+
}
781852
}
782853

783854
// MARK: InAppFeedback card

0 commit comments

Comments
 (0)