Skip to content

[Watch] Share Currency Settings #12778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 20, 2024
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
18 changes: 13 additions & 5 deletions WooCommerce/Classes/System/SessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,13 @@ final class SessionManager: SessionManagerProtocol {
removeCredentials()

guard let credentials = newValue else {
return watchDependenciesSynchronizer.update(storeID: nil, storeName: nil, credentials: nil)
return watchDependenciesSynchronizer.update(storeID: nil, storeName: nil, currencySettings: nil, credentials: nil)
}

saveCredentials(credentials)
watchDependenciesSynchronizer.update(storeID: defaultStoreID, storeName: defaultSite?.name, credentials: credentials)
watchDependenciesSynchronizer.update(storeID: defaultStoreID,
storeName: defaultSite?.name,
currencySettings: ServiceLocator.currencySettings,
credentials: credentials)
}
}

Expand Down Expand Up @@ -103,7 +105,10 @@ final class SessionManager: SessionManagerProtocol {
defaults[.defaultStoreID] = newValue
defaultStoreIDSubject.send(newValue)

watchDependenciesSynchronizer.update(storeID: defaultStoreID, storeName: defaultSite?.name, credentials: defaultCredentials)
watchDependenciesSynchronizer.update(storeID: defaultStoreID,
storeName: defaultSite?.name,
currencySettings: ServiceLocator.currencySettings,
credentials: defaultCredentials)
}
}

Expand Down Expand Up @@ -156,7 +161,10 @@ final class SessionManager: SessionManagerProtocol {
///
@Published var defaultSite: Site? {
didSet {
watchDependenciesSynchronizer.update(storeID: defaultStoreID, storeName: defaultSite?.name, credentials: loadCredentials())
watchDependenciesSynchronizer.update(storeID: defaultStoreID,
storeName: defaultSite?.name,
currencySettings: ServiceLocator.currencySettings,
credentials: loadCredentials())
}
}

Expand Down
36 changes: 30 additions & 6 deletions WooCommerce/Classes/System/WatchDependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import Foundation

#if canImport(Networking)
import enum Networking.Credentials
#elseif canImport(NetworkingWatchOS)
import enum NetworkingWatchOS.Credentials
#endif

#if canImport(NetworkingWatchOS)
import enum NetworkingWatchOS.Credentials
#if canImport(WooFoundation)
import class WooFoundation.CurrencySettings
#elseif canImport(WooFoundationWatchOS)
import class WooFoundationWatchOS.CurrencySettings
#endif



/// WatchOS session dependencies.
///
public struct WatchDependencies {
Expand All @@ -22,15 +28,18 @@ public struct WatchDependencies {
static let address = "address"
static let id = "id"
static let name = "name"
static let currencySettings = "currency-settings"
}

let storeID: Int64
let storeName: String
let currencySettings: CurrencySettings
let credentials: Credentials

public init(storeID: Int64, storeName: String, credentials: Credentials) {
public init(storeID: Int64, storeName: String, currencySettings: CurrencySettings, credentials: Credentials) {
self.storeID = storeID
self.storeName = storeName
self.currencySettings = currencySettings
self.credentials = credentials
}

Expand All @@ -44,6 +53,18 @@ public struct WatchDependencies {
let storeID = storeDic[Keys.id] as? Int64
let storeName = storeDic[Keys.name] as? String

// Read currency settings as a base64-string
let currencySettings: CurrencySettings = {
// If we could not find any setting, use a default one.
guard let base64Settings = storeDic[Keys.currencySettings] as? String,
let currencyData = Data(base64Encoded: base64Settings),
let settings = try? JSONDecoder().decode(CurrencySettings.self, from: currencyData) else {
return CurrencySettings()
}
return settings
}()


let credentials: Credentials? = {
guard let credentialsDic = dictionary[Keys.credentials] as? [String: String],
let type = credentialsDic[Keys.type],
Expand All @@ -69,13 +90,15 @@ public struct WatchDependencies {
return nil
}

self.init(storeID: storeID, storeName: storeName, credentials: credentials)
self.init(storeID: storeID, storeName: storeName, currencySettings: currencySettings, credentials: credentials)
}

/// Dictionary to be transferred between sessions.
///
public func toDictionary() -> [String: Any] {
[
// Send currency settings as a base64-string because a Data type can't be transferred to the watch.
let currencySettingJsonAsBase64 = (try? JSONEncoder().encode(currencySettings))?.base64EncodedString() ?? ""
return [
Keys.credentials: [
Keys.type: credentials.rawType,
Keys.username: credentials.username,
Expand All @@ -84,7 +107,8 @@ public struct WatchDependencies {
],
Keys.store: [
Keys.id: storeID,
Keys.name: storeName
Keys.name: storeName,
Keys.currencySettings: currencySettingJsonAsBase64
]
]
}
Expand Down
12 changes: 8 additions & 4 deletions WooCommerce/Classes/System/WatchDependenciesSynchronizer.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import WatchConnectivity
import enum Networking.Credentials
import class WooFoundation.CurrencySettings

/// Type that syncs the necessary dependencies to the watch session.
/// Dependencies:
Expand Down Expand Up @@ -32,13 +33,13 @@ final class WatchDependenciesSynchronizer: NSObject, WCSessionDelegate {

/// Syncs credentials to the watch session.
///
func update(storeID: Int64?, storeName: String?, credentials: Credentials?) {
func update(storeID: Int64?, storeName: String?, currencySettings: CurrencySettings?, credentials: Credentials?) {

let dependencies: WatchDependencies? = {
guard let storeID, let storeName, let credentials else {
guard let storeID, let storeName, let credentials, let currencySettings else {
return nil
}
return .init(storeID: storeID, storeName: storeName, credentials: credentials)
return .init(storeID: storeID, storeName: storeName, currencySettings: currencySettings, credentials: credentials)
}()

// Enqueue dependencies if the session is not yet activated.
Expand All @@ -59,7 +60,10 @@ final class WatchDependenciesSynchronizer: NSObject, WCSessionDelegate {
DDLogInfo("🔵 WatchSession activated \(activationState)")

if case .queued(let watchDependencies) = queuedDependencies {
update(storeID: watchDependencies?.storeID, storeName: watchDependencies?.storeName, credentials: watchDependencies?.credentials)
update(storeID: watchDependencies?.storeID,
storeName: watchDependencies?.storeName,
currencySettings: watchDependencies?.currencySettings,
credentials: watchDependencies?.credentials)
self.queuedDependencies = .notQueued
}
}
Expand Down
51 changes: 51 additions & 0 deletions WooCommerce/StoreWidgets/StoreInfoFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation

#if canImport(WooFoundation)
import WooFoundation
#elseif canImport(WooFoundationWatchOS)
import WooFoundationWatchOS
#endif

/// Type to help formatting values for presentation.
///
struct StoreInfoFormatter {
/// Formats values using the given currency setting.
///
static func formattedAmountString(for amountValue: Decimal, with currencySettings: CurrencySettings?) -> String {
let currencyFormatter = CurrencyFormatter(currencySettings: currencySettings ?? CurrencySettings())
return currencyFormatter.formatAmount(amountValue) ?? Constants.valuePlaceholderText
}

/// Formats values with a compact format using the given currency setting.
///
static func formattedAmountCompactString(for amountValue: Decimal, with currencySettings: CurrencySettings?) -> String {
let currencyFormatter = CurrencyFormatter(currencySettings: currencySettings ?? CurrencySettings())
return currencyFormatter.formatHumanReadableAmount(amountValue) ?? Constants.valuePlaceholderText
}

/// Formats the conversion as a percentage.
///
static func formattedConversionString(for conversionRate: Double) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .percent
numberFormatter.minimumFractionDigits = 1

// do not add 0 fraction digit if the percentage is round
let minimumFractionDigits = floor(conversionRate * 100.0) == conversionRate * 100.0 ? 0 : 1
numberFormatter.minimumFractionDigits = minimumFractionDigits
return numberFormatter.string(from: conversionRate as NSNumber) ?? Constants.valuePlaceholderText
}

/// Returns the current time formatted as `10:24 PM` or `22:24` depending on the phone settings.
///
static func currentFormattedTime() -> String {
let timeFormatter = DateFormatter()
timeFormatter.timeStyle = .short
timeFormatter.dateStyle = .none
return timeFormatter.string(from: Date())
}

enum Constants {
static let valuePlaceholderText = "-"
}
}
55 changes: 11 additions & 44 deletions WooCommerce/StoreWidgets/StoreInfoProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,12 @@ private extension StoreInfoProvider {
static func placeholderEntry(for dependencies: Dependencies?) -> StoreInfoEntry {
StoreInfoEntry.data(.init(range: Localization.today,
name: dependencies?.storeName ?? Localization.myShop,
revenue: Self.formattedAmountString(for: 132.234, with: dependencies?.storeCurrencySettings),
revenueCompact: Self.formattedAmountCompactString(for: 132.234, with: dependencies?.storeCurrencySettings),
revenue: StoreInfoFormatter.formattedAmountString(for: 132.234, with: dependencies?.storeCurrencySettings),
revenueCompact: StoreInfoFormatter.formattedAmountCompactString(for: 132.234, with: dependencies?.storeCurrencySettings),
visitors: "67",
orders: "23",
conversion: Self.formattedConversionString(for: 23/67),
updatedTime: Self.currentFormattedTime()))
conversion: StoreInfoFormatter.formattedConversionString(for: 23/67),
updatedTime: StoreInfoFormatter.currentFormattedTime()))
}

/// Real data entry.
Expand All @@ -179,56 +179,23 @@ private extension StoreInfoProvider {
if let visitors = todayStats.totalVisitors {
return "\(visitors)"
}
return Constants.valuePlaceholderText
return StoreInfoFormatter.Constants.valuePlaceholderText
}()
let conversion: String = {
if let conversion = todayStats.conversion {
return Self.formattedConversionString(for: conversion)
return StoreInfoFormatter.formattedConversionString(for: conversion)
}
return Constants.valuePlaceholderText
return StoreInfoFormatter.Constants.valuePlaceholderText
}()
return StoreInfoEntry.data(.init(range: Localization.today,
name: dependencies.storeName,
revenue: Self.formattedAmountString(for: todayStats.revenue, with: dependencies.storeCurrencySettings),
revenueCompact: Self.formattedAmountCompactString(for: todayStats.revenue, with: dependencies.storeCurrencySettings),
revenue: StoreInfoFormatter.formattedAmountString(for: todayStats.revenue, with: dependencies.storeCurrencySettings),
revenueCompact: StoreInfoFormatter.formattedAmountCompactString(for: todayStats.revenue,
with: dependencies.storeCurrencySettings),
visitors: visitors,
orders: "\(todayStats.totalOrders)",
conversion: conversion,
updatedTime: Self.currentFormattedTime()))
}

static func formattedAmountString(for amountValue: Decimal, with currencySettings: CurrencySettings?) -> String {
let currencyFormatter = CurrencyFormatter(currencySettings: currencySettings ?? CurrencySettings())
return currencyFormatter.formatAmount(amountValue) ?? Constants.valuePlaceholderText
}

static func formattedAmountCompactString(for amountValue: Decimal, with currencySettings: CurrencySettings?) -> String {
let currencyFormatter = CurrencyFormatter(currencySettings: currencySettings ?? CurrencySettings())
return currencyFormatter.formatHumanReadableAmount(amountValue) ?? Constants.valuePlaceholderText
}

static func formattedConversionString(for conversionRate: Double) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .percent
numberFormatter.minimumFractionDigits = 1

// do not add 0 fraction digit if the percentage is round
let minimumFractionDigits = floor(conversionRate * 100.0) == conversionRate * 100.0 ? 0 : 1
numberFormatter.minimumFractionDigits = minimumFractionDigits
return numberFormatter.string(from: conversionRate as NSNumber) ?? Constants.valuePlaceholderText
}

/// Returns the current time formatted as `10:24 PM` or `22:24` depending on the phone settings.
///
static func currentFormattedTime() -> String {
let timeFormatter = DateFormatter()
timeFormatter.timeStyle = .short
timeFormatter.dateStyle = .none
return timeFormatter.string(from: Date())
}

enum Constants {
static let valuePlaceholderText = "-"
updatedTime: StoreInfoFormatter.currentFormattedTime()))
}

enum Localization {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftUI
import class WooFoundationWatchOS.CurrencySettings

/// Environment dependencies setup
///
Expand All @@ -21,6 +22,6 @@ extension WatchDependencies {
/// Fake object, useful as a default value and for previews.
///
static func fake() -> Self {
.init(storeID: .zero, storeName: "", credentials: .init(authToken: ""))
.init(storeID: .zero, storeName: "", currencySettings: CurrencySettings(), credentials: .init(authToken: ""))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation
import WatchConnectivity
import KeychainAccess
import NetworkingWatchOS
import WooFoundationWatchOS

/// Type that receives and stores the necessary dependencies from the phone session.
///
Expand Down Expand Up @@ -66,7 +67,15 @@ final class PhoneDependenciesSynchronizer: NSObject, ObservableObject, WCSession
return nil
}

return WatchDependencies(storeID: storeID, storeName: storeName, credentials: credentials)
let currencySettings: CurrencySettings = {
guard let currencySettingsData = userDefaults[.defaultStoreCurrencySettings] as? Data,
let currencySettings = try? JSONDecoder().decode(CurrencySettings.self, from: currencySettingsData) else {
return CurrencySettings()
}
return currencySettings
}()

return WatchDependencies(storeID: storeID, storeName: storeName, currencySettings: currencySettings, credentials: credentials)
}

/// Store dependencies from the app context
Expand All @@ -77,6 +86,7 @@ final class PhoneDependenciesSynchronizer: NSObject, ObservableObject, WCSession
userDefaults[.defaultStoreID] = dependencies?.storeID
userDefaults[.defaultStoreName] = dependencies?.storeName
userDefaults[.defaultUsername] = dependencies?.credentials.username
userDefaults[.defaultStoreCurrencySettings] = try? JSONEncoder().encode(dependencies?.currencySettings)
userDefaults[.defaultCredentialsType] = dependencies?.credentials.rawType
userDefaults[.defaultSiteAddress] = dependencies?.credentials.siteAddress
keychain[WooConstants.authToken] = dependencies?.credentials.secret
Expand Down
Loading