Skip to content

Commit 9040c91

Browse files
authored
Merge release/23.0 into trunk (#16015)
2 parents 047ccd8 + 0fbae53 commit 9040c91

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1909
-202
lines changed

Modules/Sources/NetworkingCore/ApplicationPassword/ApplicationPasswordUseCase.swift

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import Foundation
22
import enum Alamofire.AFError
33
import KeychainAccess
44

5+
#if canImport(UIKit)
6+
import UIKit
7+
#endif
8+
59
public enum ApplicationPasswordUseCaseError: Error {
610
case duplicateName
711
case applicationPasswordsDisabled
@@ -29,25 +33,6 @@ public protocol ApplicationPasswordUseCase {
2933
func deletePassword() async throws
3034
}
3135

32-
/// A wrapper for the `UIDevice` `model` and `identifierForVendor` properties.
33-
///
34-
/// This is necessary because `UIDevice` is part of UIKit which we cannot use when targeting watchOS.
35-
/// So, to keep this package compatible with watchOS, we need to abstract UIKit away and delegate it to the consumers to provide us
36-
/// with the device information.
37-
///
38-
/// This approach is feasible because only the `applicationPasswordName` method in
39-
/// `DefaultApplicationPasswordUseCase` needs access to the information and watchOS does not need to create application
40-
/// passwords. We can therefore pass a `nil` value to it to satisfy the compilation without issues for the user experience.
41-
public struct DeviceModelIdentifierInfo {
42-
let model: String
43-
let identifierForVendor: String
44-
45-
public init(model: String, identifierForVendor: String) {
46-
self.model = model
47-
self.identifierForVendor = identifierForVendor
48-
}
49-
}
50-
5136
final public class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase {
5237
/// Site Address
5338
///
@@ -65,31 +50,27 @@ final public class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase
6550
///
6651
private let storage: ApplicationPasswordStorage
6752

68-
private let deviceModelIdentifierInfo: DeviceModelIdentifierInfo?
69-
7053
/// Used to name the password in wpadmin.
7154
///
7255
private var applicationPasswordName: String {
73-
get {
74-
guard let deviceModelIdentifierInfo else {
75-
return "" // This is not needed on watchOS as the watch does not create application passwords.
76-
}
77-
78-
let bundleIdentifier = Bundle.main.bundleIdentifier ?? "Unknown"
79-
return "\(bundleIdentifier).ios-app-client.\(deviceModelIdentifierInfo.model).\(deviceModelIdentifierInfo.identifierForVendor)"
80-
}
56+
#if !os(watchOS)
57+
let bundleIdentifier = Bundle.main.bundleIdentifier ?? "Unknown"
58+
let model = UIDevice.current.model
59+
let identifierForVendor = UIDevice.current.identifierForVendor?.uuidString ?? ""
60+
return "\(bundleIdentifier).ios-app-client.\(model).\(identifierForVendor)"
61+
#else
62+
fatalError("Unexpected error: Application password should not be generated through watch app")
63+
#endif
8164
}
8265

8366
public init(username: String,
8467
password: String,
8568
siteAddress: String,
86-
deviceModelIdentifierInfo: DeviceModelIdentifierInfo? = nil,
8769
network: Network? = nil,
8870
keychain: Keychain = Keychain(service: WooConstants.keychainServiceName)) throws {
8971
self.siteAddress = siteAddress
9072
self.username = username
9173
self.storage = ApplicationPasswordStorage(keychain: keychain)
92-
self.deviceModelIdentifierInfo = deviceModelIdentifierInfo
9374

9475
if let network {
9576
self.network = network
@@ -154,7 +135,7 @@ final public class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase
154135
if let uuidFromLocalPassword {
155136
return uuidFromLocalPassword
156137
} else {
157-
return try await self.fetchUUIDForApplicationPassword(await applicationPasswordName)
138+
return try await self.fetchUUIDForApplicationPassword(applicationPasswordName)
158139
}
159140
}()
160141
try await deleteApplicationPassword(uuidToBeDeleted)
@@ -167,7 +148,7 @@ private extension DefaultApplicationPasswordUseCase {
167148
/// - Returns: Generated `ApplicationPassword`
168149
///
169150
func createApplicationPassword() async throws -> ApplicationPassword {
170-
let passwordName = await applicationPasswordName
151+
let passwordName = applicationPasswordName
171152

172153
let parameters = [ParameterKey.name: passwordName]
173154
let request = RESTRequest(siteURL: siteAddress, method: .post, path: Path.applicationPasswords, parameters: parameters)

WooCommerce/Classes/Analytics/WooAnalyticsEvent+BackgroudUpdates.swift

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,51 @@
11
import Foundation
2+
import WooFoundation
23

34
extension WooAnalyticsEvent {
45
enum BackgroundUpdates {
56

67
private enum Keys {
78
static let timeTaken = "time_taken"
9+
static let backgroundTimeGranted = "background_time_granted"
10+
static let networkType = "network_type"
11+
static let isExpensiveConnection = "is_expensive_connection"
12+
static let isLowDataMode = "is_low_data_mode"
13+
static let isPowered = "is_powered"
14+
static let batteryLevel = "battery_level"
15+
static let isLowPowerMode = "is_low_power_mode"
16+
static let timeSinceLastRun = "time_since_last_run"
817
}
918

10-
static func dataSynced(timeTaken: TimeInterval) -> WooAnalyticsEvent {
11-
WooAnalyticsEvent(statName: .backgroundDataSynced, properties: [Keys.timeTaken: timeTaken])
19+
static func dataSynced(
20+
timeTaken: TimeInterval,
21+
backgroundTimeGranted: TimeInterval?,
22+
networkType: String,
23+
isExpensiveConnection: Bool,
24+
isLowDataMode: Bool,
25+
isPowered: Bool,
26+
batteryLevel: Float,
27+
isLowPowerMode: Bool,
28+
timeSinceLastRun: TimeInterval?
29+
) -> WooAnalyticsEvent {
30+
var properties: [String: WooAnalyticsEventPropertyType] = [
31+
Keys.timeTaken: Int64(timeTaken),
32+
Keys.networkType: networkType,
33+
Keys.isExpensiveConnection: isExpensiveConnection,
34+
Keys.isLowDataMode: isLowDataMode,
35+
Keys.isPowered: isPowered,
36+
Keys.batteryLevel: Float64(batteryLevel),
37+
Keys.isLowPowerMode: isLowPowerMode
38+
]
39+
40+
if let backgroundTimeGranted = backgroundTimeGranted {
41+
properties[Keys.backgroundTimeGranted] = Int64(backgroundTimeGranted)
42+
}
43+
44+
if let timeSinceLastRun = timeSinceLastRun {
45+
properties[Keys.timeSinceLastRun] = Int64(timeSinceLastRun)
46+
}
47+
48+
return WooAnalyticsEvent(statName: .backgroundDataSynced, properties: properties)
1249
}
1350

1451
static func dataSyncError(_ error: Error) -> WooAnalyticsEvent {

WooCommerce/Classes/Authentication/AuthenticationManager.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -752,8 +752,7 @@ private extension AuthenticationManager {
752752
guard let useCase = try? DefaultApplicationPasswordUseCase(
753753
username: siteCredentials.username,
754754
password: siteCredentials.password,
755-
siteAddress: siteCredentials.siteURL,
756-
deviceModelIdentifierInfo: UIDevice.current.deviceModelIdentifierInfo
755+
siteAddress: siteCredentials.siteURL
757756
) else {
758757
return assertionFailure("⛔️ Error creating application password use case")
759758
}

WooCommerce/Classes/Extensions/UserDefaults+Woo.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ extension UserDefaults {
5050

5151
// Background Task Refresh
5252
case latestBackgroundOrderSyncDate
53+
case lastBackgroundRefreshCompletionTime
5354

5455
// Blaze Local notification
5556
case blazeNoCampaignReminderOpened

WooCommerce/Classes/System/SessionManager.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ final class SessionManager: SessionManagerProtocol {
234234
return try? DefaultApplicationPasswordUseCase(username: username,
235235
password: password,
236236
siteAddress: siteAddress,
237-
deviceModelIdentifierInfo: UIDevice.current.deviceModelIdentifierInfo,
238237
keychain: keychain)
239238
case let .applicationPassword(_, _, siteAddress):
240239
return OneTimeApplicationPasswordUseCase(siteAddress: siteAddress, keychain: keychain)

WooCommerce/Classes/System/UIDevice+DeviceModelIdentifierInfo.swift

Lines changed: 0 additions & 12 deletions
This file was deleted.

WooCommerce/Classes/Tools/BackgroundTasks/BackgroundTaskRefreshDispatcher.swift

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import UIKit
22
import Foundation
33
import BackgroundTasks
4+
import Network
45

56
final class BackgroundTaskRefreshDispatcher {
67

@@ -60,6 +61,7 @@ final class BackgroundTaskRefreshDispatcher {
6061
// Launch all refresh tasks in parallel.
6162
let refreshTasks = Task {
6263
do {
64+
async let systemInfo = BackgroundTaskSystemInfo()
6365

6466
let startTime = Date.now
6567

@@ -78,7 +80,27 @@ final class BackgroundTaskRefreshDispatcher {
7880
}
7981

8082
let timeTaken = round(Date.now.timeIntervalSince(startTime))
81-
ServiceLocator.analytics.track(event: .BackgroundUpdates.dataSynced(timeTaken: timeTaken))
83+
84+
var timeSinceLastRun: TimeInterval? = nil
85+
if let lastRunTime = UserDefaults.standard[.lastBackgroundRefreshCompletionTime] as? Date {
86+
timeSinceLastRun = round(lastRunTime.timeIntervalSinceNow.magnitude)
87+
}
88+
89+
await ServiceLocator.analytics.track(event: .BackgroundUpdates.dataSynced(
90+
timeTaken: timeTaken,
91+
backgroundTimeGranted: systemInfo.backgroundTimeGranted,
92+
networkType: systemInfo.networkType,
93+
isExpensiveConnection: systemInfo.isExpensiveConnection,
94+
isLowDataMode: systemInfo.isLowDataMode,
95+
isPowered: systemInfo.isPowered,
96+
batteryLevel: systemInfo.batteryLevel,
97+
isLowPowerMode: systemInfo.isLowPowerMode,
98+
timeSinceLastRun: timeSinceLastRun
99+
))
100+
101+
// Save date, for use in analytics next time we refresh
102+
UserDefaults.standard[.lastBackgroundRefreshCompletionTime] = Date.now
103+
82104
backgroundTask.setTaskCompleted(success: true)
83105

84106
} catch {
@@ -93,7 +115,7 @@ final class BackgroundTaskRefreshDispatcher {
93115
ServiceLocator.analytics.track(event: .BackgroundUpdates.dataSyncError(BackgroundError.expired))
94116
refreshTasks.cancel()
95117
}
96-
}
118+
}
97119
}
98120

99121
private extension BackgroundTaskRefreshDispatcher {
@@ -109,3 +131,87 @@ extension BackgroundTaskRefreshDispatcher {
109131
case expired
110132
}
111133
}
134+
135+
// MARK: - System Information Helper
136+
137+
private struct NetworkInfo {
138+
let type: String
139+
let isExpensive: Bool
140+
let isLowDataMode: Bool
141+
}
142+
143+
private struct BackgroundTaskSystemInfo {
144+
let backgroundTimeGranted: TimeInterval?
145+
private let networkInfo: NetworkInfo
146+
let isPowered: Bool
147+
let batteryLevel: Float
148+
let isLowPowerMode: Bool
149+
150+
// Computed properties for clean external access
151+
var networkType: String { networkInfo.type }
152+
var isExpensiveConnection: Bool { networkInfo.isExpensive }
153+
var isLowDataMode: Bool { networkInfo.isLowDataMode }
154+
155+
@MainActor
156+
init() async {
157+
// Background time granted (nil if foreground/unlimited)
158+
let backgroundTime = UIApplication.shared.backgroundTimeRemaining
159+
self.backgroundTimeGranted = backgroundTime < Double.greatestFiniteMagnitude ? backgroundTime : nil
160+
161+
// Network info
162+
self.networkInfo = await Self.getNetworkInfo()
163+
164+
// Power and battery info
165+
let device = UIDevice.current
166+
device.isBatteryMonitoringEnabled = true
167+
168+
self.isPowered = device.batteryState == .charging || device.batteryState == .full
169+
self.batteryLevel = device.batteryLevel
170+
self.isLowPowerMode = ProcessInfo.processInfo.isLowPowerModeEnabled
171+
172+
device.isBatteryMonitoringEnabled = false
173+
}
174+
175+
private static func getNetworkInfo() async -> NetworkInfo {
176+
return await withCheckedContinuation { continuation in
177+
let monitor = NWPathMonitor()
178+
179+
monitor.pathUpdateHandler = { path in
180+
continuation.resume(returning: NetworkInfo(path: path))
181+
monitor.cancel()
182+
}
183+
184+
let queue = DispatchQueue(label: "network.monitor.queue")
185+
monitor.start(queue: queue)
186+
}
187+
}
188+
}
189+
190+
private extension NetworkInfo {
191+
init(path: NWPath) {
192+
guard path.status == .satisfied else {
193+
self.type = "no_connection"
194+
self.isExpensive = false
195+
self.isLowDataMode = false
196+
return
197+
}
198+
199+
self.type = Self.networkType(from: path)
200+
self.isExpensive = path.isExpensive
201+
self.isLowDataMode = path.isConstrained
202+
}
203+
204+
private static func networkType(from path: NWPath) -> String {
205+
if path.usesInterfaceType(.wifi) {
206+
return "wifi"
207+
} else if path.usesInterfaceType(.cellular) {
208+
return "cellular"
209+
} else if path.usesInterfaceType(.wiredEthernet) {
210+
return "ethernet"
211+
} else if path.usesInterfaceType(.loopback) {
212+
return "loopback"
213+
} else {
214+
return "other"
215+
}
216+
}
217+
}

0 commit comments

Comments
 (0)