Skip to content

Commit 3e929e5

Browse files
authored
Merge branch 'trunk' into woomob-980-mark-manually-saved-addresses-as-unverified
2 parents 260b35d + 97dd8eb commit 3e929e5

File tree

65 files changed

+2213
-274
lines changed

Some content is hidden

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

65 files changed

+2213
-274
lines changed

Modules/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ let package = Package(
66
name: "Modules",
77
platforms: [
88
// Keep in sync with Common.xcconfig
9-
.iOS(.v16),
9+
.iOS(.v17),
1010
.macOS(.v10_14),
1111
.watchOS(.v9),
1212
],

Modules/Sources/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
103103
return true
104104
case .pointOfSaleBarcodeScanningi2:
105105
return true
106+
case .pointOfSaleSettingsi1:
107+
return false
106108
case .orderAddressMapSearch:
107109
return true
108110
default:

Modules/Sources/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ public enum FeatureFlag: Int {
211211
///
212212
case pointOfSaleBarcodeScanningi2
213213

214+
/// Enables the entry point for Point of Sale Settings
215+
///
216+
case pointOfSaleSettingsi1
217+
214218
/// Enables the CTA to search for an address in the map in order details > shipping address.
215219
///
216220
case orderAddressMapSearch

Modules/Sources/Networking/Remote/JetpackConnectionRemote.swift

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import Foundation
44
///
55
public final class JetpackConnectionRemote: Remote {
66
private let siteURL: String
7-
7+
private let network: Network
88
private var accountConnectionURL: URL?
99

1010
public init(siteURL: String, network: Network) {
1111
self.siteURL = siteURL
12+
self.network = network
1213
super.init(network: network)
1314
}
1415

@@ -49,6 +50,41 @@ public final class JetpackConnectionRemote: Remote {
4950
enqueue(request, mapper: mapper, completion: completion)
5051
}
5152

53+
/// Registers Jetpack site connection by requesting the input URL while disabling automatic redirection,
54+
/// and returns the URL in the requested redirection.
55+
/// To simplify redirection manipulation, we'll use a `URLSession` here instead of `Network`.
56+
///
57+
public func registerJetpackSiteConnection(with url: URL, completion: @escaping (Result<URL, Error>) -> Void) {
58+
59+
let configuration = URLSessionConfiguration.default
60+
for cookie in network.session.configuration.httpCookieStorage?.cookies ?? [] {
61+
configuration.httpCookieStorage?.setCookie(cookie)
62+
}
63+
64+
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
65+
do {
66+
let request = try URLRequest(url: url, method: .get)
67+
let task = session.dataTask(with: request) { [weak self] data, response, error in
68+
if let result = self?.accountConnectionURL {
69+
DispatchQueue.main.async {
70+
completion(.success(result))
71+
}
72+
return
73+
}
74+
// We don't expect any response here since we'll cancel the task as soon as a redirect request is received.
75+
// So always complete with a failure here.
76+
let returnedError = error ?? JetpackConnectionError.accountConnectionURLNotFound
77+
DispatchQueue.main.async {
78+
completion(.failure(returnedError))
79+
}
80+
return
81+
}
82+
task.resume()
83+
} catch {
84+
completion(.failure(error))
85+
}
86+
}
87+
5288
/// Fetches the connection state with the site's Jetpack for the authenticated user.
5389
///
5490
public func fetchJetpackConnectionData(completion: @escaping (Result<JetpackConnectionData, Error>) -> Void) {
@@ -84,6 +120,24 @@ public final class JetpackConnectionRemote: Remote {
84120
}
85121
}
86122

123+
// MARK: - URLSessionDataDelegate conformance
124+
//
125+
extension JetpackConnectionRemote: URLSessionDataDelegate {
126+
public func urlSession(_ session: URLSession,
127+
task: URLSessionTask,
128+
willPerformHTTPRedirection response: HTTPURLResponse,
129+
newRequest request: URLRequest) async -> URLRequest? {
130+
// Disables redirection if the request is to load the Jetpack account connection URL
131+
if let url = request.url,
132+
url.absoluteString.hasPrefix(Constants.jetpackAccountConnectionURL) {
133+
accountConnectionURL = url
134+
task.cancel()
135+
return nil
136+
}
137+
return request
138+
}
139+
}
140+
87141
/// periphery: ignore - used in test module and on the UI layer
88142
public enum JetpackConnectionError: Error, Equatable {
89143
case malformedURL

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)

Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ public enum JetpackConnectionAction: Action {
1313
/// Updates Jetpack the plugin for the current site.
1414
case activateJetpackPlugin(completion: (Result<Void, Error>) -> Void)
1515
/// Fetches the URL used for setting up Jetpack connection.
16-
case fetchJetpackConnectionURL(completion: (Result<URL, Error>) -> Void)
16+
case fetchJetpackConnectionURL(authenticatedWithWPCom: Bool,
17+
completion: (Result<URL, Error>) -> Void)
1718
/// Fetches connection state with the given site's Jetpack.
1819
case fetchJetpackConnectionData(completion: (Result<JetpackConnectionData, Error>) -> Void)
1920
/// Establishes site-level connection and returns WordPress.com blog ID.

Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ public final class JetpackConnectionStore: DeauthenticatedStore {
4141
installJetpackPlugin(completion: completion)
4242
case .activateJetpackPlugin(let completion):
4343
activateJetpackPlugin(completion: completion)
44-
case .fetchJetpackConnectionURL(let completion):
45-
fetchJetpackConnectionURL(completion: completion)
44+
case let .fetchJetpackConnectionURL(authenticatedWithWPCom, completion):
45+
fetchJetpackConnectionURL(authenticatedWithWPCom: authenticatedWithWPCom,
46+
completion: completion)
4647
case .fetchJetpackConnectionData(let completion):
4748
fetchJetpackConnectionData(completion: completion)
4849
case .registerSite(let completion):
@@ -88,8 +89,26 @@ private extension JetpackConnectionStore {
8889
})
8990
}
9091

91-
func fetchJetpackConnectionURL(completion: @escaping (Result<URL, Error>) -> Void) {
92-
jetpackConnectionRemote?.fetchJetpackConnectionURL(completion: completion)
92+
func fetchJetpackConnectionURL(authenticatedWithWPCom: Bool,
93+
completion: @escaping (Result<URL, Error>) -> Void) {
94+
guard authenticatedWithWPCom else {
95+
jetpackConnectionRemote?.fetchJetpackConnectionURL(completion: completion)
96+
return
97+
}
98+
jetpackConnectionRemote?.fetchJetpackConnectionURL { [weak self] result in
99+
guard let self else { return }
100+
switch result {
101+
case .success(let url):
102+
// If we get the account connection URL, return it immediately.
103+
if url.absoluteString.hasPrefix(Constants.jetpackAccountConnectionURL) {
104+
return completion(.success(url))
105+
}
106+
// Otherwise, request the url with redirection disabled and retrieve the URL in LOCATION header
107+
self.jetpackConnectionRemote?.registerJetpackSiteConnection(with: url, completion: completion)
108+
case .failure(let error):
109+
completion(.failure(error))
110+
}
111+
}
93112
}
94113

95114
func fetchJetpackConnectionData(completion: @escaping (Result<JetpackConnectionData, Error>) -> Void) {
@@ -152,3 +171,9 @@ private extension JetpackConnectionStore {
152171
self.accountRemote = remote
153172
}
154173
}
174+
175+
private extension JetpackConnectionStore {
176+
enum Constants {
177+
static let jetpackAccountConnectionURL = "https://jetpack.wordpress.com/jetpack.authorize"
178+
}
179+
}

Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ final class JetpackConnectionStoreTests: XCTestCase {
167167

168168
// When
169169
let result: Result<URL, Error> = waitFor { promise in
170-
let action = JetpackConnectionAction.fetchJetpackConnectionURL { result in
170+
let action = JetpackConnectionAction.fetchJetpackConnectionURL(authenticatedWithWPCom: true) { result in
171171
promise(result)
172172
}
173173
store.onAction(action)
@@ -192,7 +192,7 @@ final class JetpackConnectionStoreTests: XCTestCase {
192192

193193
// When
194194
let result: Result<URL, Error> = waitFor { promise in
195-
let action = JetpackConnectionAction.fetchJetpackConnectionURL { result in
195+
let action = JetpackConnectionAction.fetchJetpackConnectionURL(authenticatedWithWPCom: false) { result in
196196
promise(result)
197197
}
198198
store.onAction(action)

RELEASE-NOTES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
23.1
55
-----
66
- [internal] Remove oldest (<30) core data models [https://github.com/woocommerce/woocommerce-ios/pull/15987]
7+
- [***] Increased app's minimum deployment target to iOS17 [https://github.com/woocommerce/woocommerce-ios/pull/16003]
78
- [*] Jetpack setup: Native experience for the connection step [https://github.com/woocommerce/woocommerce-ios/pull/15983]
89
- [*] Payments: Updated the In-Person Payments `Learn More` redirection to display the correct page based on the selected payment provider [https://github.com/woocommerce/woocommerce-ios/pull/15998]
10+
- [*] Add "POS" label in Most recent orders in My store dashboard [https://github.com/woocommerce/woocommerce-ios/pull/16006]
911
- [*] Order details: Always show shipping labels section if order is eligible for label creation. [https://github.com/woocommerce/woocommerce-ios/pull/16010]
1012
- [*] Order details: Remove print buttons from shipment details [https://github.com/woocommerce/woocommerce-ios/pull/16012]
1113
- [*] Shipping labels: Mark manually saved addresses as unverified [https://github.com/woocommerce/woocommerce-ios/pull/16013]
14+
- [*] Shipping Labels: Updated incorrect HS Tariff info page. [https://github.com/woocommerce/woocommerce-ios/pull/16016]
1215

1316
23.0
1417
-----

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 {

0 commit comments

Comments
 (0)