diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 78951639a3..073fea228a 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -13291,7 +13291,7 @@ repositoryURL = "https://github.com/zcash/ZcashLightClientKit"; requirement = { kind = exactVersion; - version = 2.1.5; + version = 2.1.8; }; }; D3993DAA28F42549008720FB /* XCRemoteSwiftPackageReference "WalletConnectSwiftV2" */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/StatManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/StatManager.swift index 712d827ffa..31a812e118 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/StatManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/StatManager.swift @@ -1,4 +1,5 @@ import Alamofire +import Combine import Foundation import HsToolKit import MarketKit @@ -10,6 +11,8 @@ func stat(page: StatPage, section: StatSection? = nil, event: StatEvent) { class StatManager { private static let keyLastSent = "stat_last_sent" + private static let keySendingAllowed = "sending_allowed" + private static let sendThreshold: TimeInterval = 1 * 60 * 60 // 1 hour private let marketKit: MarketKit.Kit @@ -18,6 +21,18 @@ class StatManager { private let appVersion: String private let appId: String? + var allowed: Bool { + didSet { + userDefaultsStorage.set(value: allowed, for: Self.keySendingAllowed) + allowedSubject.send(allowed) + } + } + + private let allowedSubject = PassthroughSubject() + var allowedPublisher: AnyPublisher { + allowedSubject.eraseToAnyPublisher() + } + init(marketKit: MarketKit.Kit, storage: StatStorage, userDefaultsStorage: UserDefaultsStorage) { self.marketKit = marketKit self.storage = storage @@ -25,76 +40,79 @@ class StatManager { appVersion = AppConfig.appVersion appId = AppConfig.appId + allowed = userDefaultsStorage.value(for: StatManager.keySendingAllowed) ?? true } - func logStat(eventPage _: StatPage, eventSection _: StatSection? = nil, event _: StatEvent) { -// var parameters: [String: Any]? -// -// if let params = event.params { -// parameters = [String: Any]() -// -// for (key, value) in params { -// parameters?[key.rawValue] = value -// } -// } -// -// let record = StatRecord( -// timestamp: Int(Date().timeIntervalSince1970), -// eventPage: eventPage.rawValue, -// eventSection: eventSection?.rawValue, -// event: event.name, -// params: parameters -// ) -// -// do { -// try storage.save(record: record) -// } catch { -// print("Cannot save StatRecord: \(error)") -// } + func logStat(eventPage: StatPage, eventSection: StatSection? = nil, event: StatEvent) { + guard allowed else { return } + var parameters: [String: Any]? + + if let params = event.params { + parameters = [String: Any]() + + for (key, value) in params { + parameters?[key.rawValue] = value + } + } + + let record = StatRecord( + timestamp: Int(Date().timeIntervalSince1970), + eventPage: eventPage.rawValue, + eventSection: eventSection?.rawValue, + event: event.name, + params: parameters + ) + + do { + try storage.save(record: record) + } catch { + print("Cannot save StatRecord: \(error)") + } } func sendStats() { -// let lastSent: Double? = userDefaultsStorage.value(for: Self.keyLastSent) -// -// if let lastSent, Date().timeIntervalSince1970 - lastSent < Self.sendThreshold { -// return -// } -// -// Task { [storage] in -// let records = try storage.all() -// -// guard !records.isEmpty else { -// return -// } -// -// let stats = records.map { record in -// var object: [String: Any] = [ -// "time": record.timestamp, -// "event_page": record.eventPage, -// "event": record.event, -// ] -// -// if let eventSection = record.eventSection { -// object["event_section"] = eventSection -// } -// -// if let params = record.params { -// for (key, value) in params { -// object[key] = value -// } -// } -// -// return object -// } -// - //// let data = try JSONSerialization.data(withJSONObject: stats) - //// let string = String(data: data, encoding: .utf8) - //// print(string ?? "N/A") -// -// try await marketKit.send(stats: stats, appVersion: appVersion, appId: appId) -// -// userDefaultsStorage.set(value: Date().timeIntervalSince1970, for: Self.keyLastSent) -// try storage.clear() -// } + guard allowed else { return } + let lastSent: Double? = userDefaultsStorage.value(for: Self.keyLastSent) + + if let lastSent, Date().timeIntervalSince1970 - lastSent < Self.sendThreshold { + return + } + + Task { [storage] in + let records = try storage.all() + + guard !records.isEmpty else { + return + } + + let stats = records.map { record in + var object: [String: Any] = [ + "time": record.timestamp, + "event_page": record.eventPage, + "event": record.event, + ] + + if let eventSection = record.eventSection { + object["event_section"] = eventSection + } + + if let params = record.params { + for (key, value) in params { + object[key] = value + } + } + + return object + } + +// let data = try JSONSerialization.data(withJSONObject: stats) +// let string = String(data: data, encoding: .utf8) +// print(string ?? "N/A") + + try await marketKit.send(stats: stats, appVersion: appVersion, appId: appId) + + userDefaultsStorage.set(value: Date().timeIntervalSince1970, for: Self.keyLastSent) + try storage.clear() + } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift index af7b57ee0b..8b707f0488 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Privacy/PrivacyPolicyViewController.swift @@ -55,16 +55,40 @@ class PrivacyPolicyViewController: ThemeViewController { return [ Section( id: "privacy-section", - footerState: .margin(height: .margin32), + footerState: .margin(height: .margin16), rows: infoRows ), ] } + + private var statAllowedSections: [SectionProtocol] { + [ + Section( + id: "stat-allowed-section", + footerState: .margin(height: .margin32), + rows: [ + tableView.universalRow48( + id: "stat-allowed-cell", + image: .local(UIImage(named: "share_1_24")), + title: .body("settings.privacy.allow".localized), + accessoryType: .switch( + isOn: App.shared.statManager.allowed, + onSwitch: { enabled in + App.shared.statManager.allowed = enabled + } + ), + isFirst: true, + isLast: true + ), + ] + ), + ] + } } extension PrivacyPolicyViewController: SectionsDataSource { func buildSections() -> [SectionProtocol] { - privacySections + privacySections + statAllowedSections } } @@ -79,9 +103,8 @@ extension PrivacyPolicyViewController { title: "settings.privacy".localized, description: "settings.privacy.description".localized(AppConfig.appName), viewItems: [ - "settings.privacy.statement.user_data_storage".localized, "settings.privacy.statement.data_usage".localized, - "settings.privacy.statement.data_privacy".localized, + "settings.privacy.statement.data_storage".localized, "settings.privacy.statement.user_account".localized, ] ) diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 0c2ccbccdb..b662df8c1b 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -1532,11 +1532,11 @@ // Settings -> Privacy "settings.privacy" = "Privacy"; -"settings.privacy.description" = "%@ doesn't collect any data or use analytics tools that may expose any data about its users. The wallet is designed to ensure a high level of privacy for its users."; -"settings.privacy.statement.user_data_storage" = "User data always remains on the user's device."; +"settings.privacy.description" = "%@ doesn't collect personal data that expose your private information i.e. coin balances or addresses. While we gather some UI usage statistics, it's solely for understanding our user base and app usage trends. That can be disabled if you wish."; "settings.privacy.statement.data_usage" = "The wallet doesn't collect any data about users."; -"settings.privacy.statement.data_privacy" = "The wallet doesn't share any data about users."; -"settings.privacy.statement.user_account" = "There are no user accounts or databases keeping user data elsewhere."; +"settings.privacy.statement.data_storage" = "There are no user accounts or databases storing user data."; +"settings.privacy.statement.user_account" = "If allowed the wallet will share app usage habits with Unstoppable team. This is to understand which features are being used (or not) by our users. Being a privacy focused app we need some way to evaluate our efforts and without this we have no idea whether the features we built are being used or not."; +"settings.privacy.allow" = "Share UI Data"; // Settings -> Appearance