Skip to content

Commit d37e1f7

Browse files
authored
Merge branch 'trunk' into woomob-1332-add-support-for-network-switching-in-pos
2 parents bc6201c + 710519d commit d37e1f7

34 files changed

+1322
-153
lines changed

Modules/Sources/Networking/Remote/POSCatalogSyncRemote.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ public protocol POSCatalogSyncRemoteProtocol {
3939
/// - pageNumber: Page number for pagination.
4040
/// - Returns: Paginated list of POS product variations.
4141
func loadProductVariations(siteID: Int64, pageNumber: Int) async throws -> PagedItems<POSProductVariation>
42+
43+
/// Gets the total count of products for the specified site.
44+
///
45+
/// - Parameter siteID: Site ID to get product count for.
46+
/// - Returns: Total number of products.
47+
func getProductCount(siteID: Int64) async throws -> Int
48+
49+
/// Gets the total count of product variations for the specified site.
50+
///
51+
/// - Parameter siteID: Site ID to get variation count for.
52+
/// - Returns: Total number of variations.
53+
func getProductVariationCount(siteID: Int64) async throws -> Int
4254
}
4355

4456
/// POS Catalog Sync: Remote Endpoints
@@ -174,6 +186,58 @@ public class POSCatalogSyncRemote: Remote, POSCatalogSyncRemoteProtocol {
174186

175187
return createPagedItems(items: variations, responseHeaders: responseHeaders, currentPageNumber: pageNumber)
176188
}
189+
190+
// MARK: - Count Endpoints
191+
192+
/// Gets the total count of products for the specified site.
193+
///
194+
/// - Parameter siteID: Site ID to get product count for.
195+
/// - Returns: Total number of products.
196+
public func getProductCount(siteID: Int64) async throws -> Int {
197+
let path = Path.products
198+
let parameters = [
199+
ParameterKey.page: String(1),
200+
ParameterKey.perPage: String(1),
201+
ParameterKey.fields: POSProductVariation.requestFields.first ?? ""
202+
]
203+
204+
let request = JetpackRequest(
205+
wooApiVersion: .mark3,
206+
method: .get,
207+
siteID: siteID,
208+
path: path,
209+
parameters: parameters,
210+
availableAsRESTRequest: true
211+
)
212+
let responseHeaders = try await enqueueWithResponseHeaders(request)
213+
214+
return totalItemsCount(from: responseHeaders) ?? 0
215+
}
216+
217+
/// Gets the total count of product variations for the specified site.
218+
///
219+
/// - Parameter siteID: Site ID to get variation count for.
220+
/// - Returns: Total number of variations.
221+
public func getProductVariationCount(siteID: Int64) async throws -> Int {
222+
let path = Path.variations
223+
let parameters = [
224+
ParameterKey.page: String(1),
225+
ParameterKey.perPage: String(1),
226+
ParameterKey.fields: POSProductVariation.requestFields.first ?? ""
227+
]
228+
229+
let request = JetpackRequest(
230+
wooApiVersion: .wcAnalytics,
231+
method: .get,
232+
siteID: siteID,
233+
path: path,
234+
parameters: parameters,
235+
availableAsRESTRequest: true
236+
)
237+
let responseHeaders = try await enqueueWithResponseHeaders(request)
238+
239+
return totalItemsCount(from: responseHeaders) ?? 0
240+
}
177241
}
178242

179243
// MARK: - Constants

Modules/Sources/NetworkingCore/ApplicationPassword/RequestProcessor.swift

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Alamofire
22
import Foundation
33

44
protocol RequestProcessorDelegate: AnyObject {
5-
func didFailToAuthenticateRequestWithAppPassword(siteID: Int64)
5+
func didFailToAuthenticateRequestWithAppPassword(siteID: Int64, error: Error)
66
}
77

88
/// Authenticates and retries requests
@@ -85,7 +85,7 @@ private extension RequestProcessor {
8585
generateApplicationPassword()
8686
} else {
8787
isAuthenticating = false
88-
completeRequests(false)
88+
completeRequests(false, error: error)
8989
if let currentSiteID {
9090
notifyFailureIfNeeded(error, for: currentSiteID)
9191
}
@@ -131,7 +131,7 @@ private extension RequestProcessor {
131131
}
132132
}()
133133
if appPasswordNotSupported {
134-
delegate?.didFailToAuthenticateRequestWithAppPassword(siteID: siteID)
134+
delegate?.didFailToAuthenticateRequestWithAppPassword(siteID: siteID, error: error)
135135
}
136136
}
137137

@@ -146,8 +146,16 @@ private extension RequestProcessor {
146146
}
147147
}
148148

149-
func completeRequests(_ shouldRetry: Bool) {
150-
let result: RetryResult = shouldRetry ? .retryWithDelay(0) : .doNotRetry
149+
func completeRequests(_ shouldRetry: Bool, error: Error? = nil) {
150+
let result: RetryResult = {
151+
if shouldRetry {
152+
.retryWithDelay(0)
153+
} else if let error {
154+
.doNotRetryWithError(error)
155+
} else {
156+
.doNotRetry
157+
}
158+
}()
151159
requestsToRetry.forEach { (completion) in
152160
completion(result)
153161
}
@@ -165,4 +173,12 @@ public extension NSNotification.Name {
165173
/// Posted when generating an application password fails
166174
///
167175
static let ApplicationPasswordsGenerationFailed = NSNotification.Name(rawValue: "ApplicationPasswordsGenerationFailed")
176+
177+
/// Posted when site is flagged as unsupported for app password
178+
///
179+
static let JetpackSiteFlaggedUnsupportedForApplicationPassword = NSNotification.Name(rawValue: "JetpackSiteFlaggedUnsupportedForApplicationPassword")
180+
181+
/// Posted when site is detected as eligible for app password authentication
182+
///
183+
static let JetpackSiteEligibleForAppPasswordSupport = NSNotification.Name(rawValue: "JetpackSiteEligibleForAppPasswordSupport")
168184
}

Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ private extension AlamofireNetwork {
311311
network: self
312312
))
313313
requestAuthenticator.delegate = self
314-
errorHandler.resetFailureCount(for: site.siteID) // reset failure count
314+
errorHandler.prepareAppPasswordSupport(for: site.siteID) // reset failure count
315315
updateAuthenticationMode(.appPasswordsWithJetpack)
316316
}
317317
}
@@ -338,8 +338,13 @@ private extension AlamofireNetwork {
338338
// MARK: `RequestProcessorDelegate` conformance
339339
//
340340
extension AlamofireNetwork: RequestProcessorDelegate {
341-
func didFailToAuthenticateRequestWithAppPassword(siteID: Int64) {
342-
errorHandler.flagSiteAsUnsupported(for: siteID)
341+
func didFailToAuthenticateRequestWithAppPassword(siteID: Int64, error: Error) {
342+
errorHandler.flagSiteAsUnsupported(
343+
for: siteID,
344+
flow: .appPasswordGeneration,
345+
cause: .majorError,
346+
error: error
347+
)
343348
}
344349
}
345350

@@ -379,8 +384,8 @@ extension Alamofire.DataResponse {
379384
return error
380385
}
381386

382-
if case .some(AFError.requestAdaptationFailed) = error?.asAFError {
383-
return error
387+
if case .some(AFError.requestRetryFailed) = error?.asAFError {
388+
return error?.asAFError
384389
}
385390

386391
return response.flatMap { response in

Modules/Sources/NetworkingCore/Network/AlamofireNetworkErrorHandler.swift

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ final class AlamofireNetworkErrorHandler {
66
private let queue = DispatchQueue(label: "com.networkingcore.errorhandler", attributes: .concurrent)
77
private let userDefaults: UserDefaults
88
private let credentials: Credentials?
9+
private let notificationCenter: NotificationCenter
910

1011
private var _appPasswordFailures: [Int64: Int] = [:]
1112
private var _retriedJetpackRequests: [RetriedJetpackRequest] = []
1213

13-
init(credentials: Credentials?, userDefaults: UserDefaults = .standard) {
14+
init(credentials: Credentials?,
15+
userDefaults: UserDefaults = .standard,
16+
notificationCenter: NotificationCenter = .default) {
1417
self.credentials = credentials
1518
self.userDefaults = userDefaults
19+
self.notificationCenter = notificationCenter
1620
}
1721

1822
// MARK: - Thread-safe property access
@@ -41,8 +45,9 @@ final class AlamofireNetworkErrorHandler {
4145

4246
// MARK: - Public interface
4347

44-
func resetFailureCount(for siteID: Int64) {
48+
func prepareAppPasswordSupport(for siteID: Int64) {
4549
appPasswordFailures.removeValue(forKey: siteID)
50+
notificationCenter.post(name: .JetpackSiteEligibleForAppPasswordSupport, object: siteID)
4651
}
4752

4853
func shouldRetryJetpackRequest(originalRequest: URLRequestConvertible,
@@ -51,13 +56,14 @@ final class AlamofireNetworkErrorHandler {
5156
guard let error = failure,
5257
let request = originalRequest as? JetpackRequest,
5358
convertedRequest is RESTRequest,
59+
let convertedURLRequest = try? convertedRequest.asURLRequest(),
5460
case .some(.wpcom) = self.credentials else {
5561
return false
5662
}
5763

5864
let isExpectedError: Bool = {
5965
switch error {
60-
case AFError.requestAdaptationFailed:
66+
case AFError.requestRetryFailed:
6167
return true
6268
case _ as NetworkError:
6369
return true
@@ -69,6 +75,7 @@ final class AlamofireNetworkErrorHandler {
6975
if isExpectedError {
7076
let retriedRequest = RetriedJetpackRequest(request: request, error: error)
7177
retriedJetpackRequests.append(retriedRequest)
78+
logRequestFailure(request: convertedURLRequest, error: error)
7279
return true
7380
}
7481
return false
@@ -86,7 +93,7 @@ final class AlamofireNetworkErrorHandler {
8693

8794
guard let index = retriedRequestIndex else { return }
8895

89-
let retriedRequest = retriedJetpackRequests[index]
96+
let retriedRequest = retriedJetpackRequests.remove(at: index)
9097

9198
if failure == nil {
9299
let siteID = retriedRequest.request.siteID
@@ -95,19 +102,27 @@ final class AlamofireNetworkErrorHandler {
95102
case NetworkError.unacceptableStatusCode(statusCode: 401, _),
96103
NetworkError.unacceptableStatusCode(statusCode: 403, _),
97104
NetworkError.unacceptableStatusCode(statusCode: 429, _):
98-
flagSiteAsUnsupported(for: siteID)
105+
flagSiteAsUnsupported(
106+
for: siteID,
107+
flow: .apiRequest,
108+
cause: .majorError,
109+
error: originalFailure
110+
)
99111
default:
100112
if let networkError = originalFailure as? NetworkError,
101113
let code = networkError.errorCode,
102114
AppPasswordConstants.disabledCodes.contains(code) {
103-
flagSiteAsUnsupported(for: siteID)
115+
flagSiteAsUnsupported(
116+
for: siteID,
117+
flow: .apiRequest,
118+
cause: .majorError,
119+
error: originalFailure
120+
)
104121
} else {
105-
incrementFailureCount(for: siteID)
122+
incrementFailureCount(for: siteID, originalFailure: originalFailure)
106123
}
107124
}
108125
}
109-
110-
retriedJetpackRequests.remove(at: index)
111126
}
112127

113128
func handleFailureForDirectRequestIfNeeded(originalRequest: URLRequestConvertible,
@@ -133,12 +148,24 @@ final class AlamofireNetworkErrorHandler {
133148
}
134149
}
135150

136-
func flagSiteAsUnsupported(for siteID: Int64) {
151+
func flagSiteAsUnsupported(for siteID: Int64, flow: RequestFlow, cause: AppPasswordFlagCause, error: Error) {
137152
queue.sync(flags: .barrier) {
138153
var currentList = userDefaults.applicationPasswordUnsupportedList
139154
currentList[String(siteID)] = Date()
140155
userDefaults.applicationPasswordUnsupportedList = currentList
141156
}
157+
158+
/// Tracks error
159+
let apiErrorCode = (error as? NetworkError)?.errorCode ?? error.localizedDescription
160+
let httpStatusCode = (error as? NetworkError)?.responseCode ?? (error as NSError).code
161+
162+
let tracksProperties: [String: Any] = [
163+
TracksProperty.flow.rawValue: flow.rawValue,
164+
TracksProperty.cause.rawValue: cause.rawValue,
165+
TracksProperty.apiErrorCode.rawValue: apiErrorCode,
166+
TracksProperty.httpStatusCode.rawValue: httpStatusCode
167+
]
168+
notificationCenter.post(name: .JetpackSiteFlaggedUnsupportedForApplicationPassword, object: tracksProperties)
142169
}
143170

144171
func siteFlaggedAsUnsupported(siteID: Int64, unsupportedList: [String: Date]) -> Bool {
@@ -156,13 +183,38 @@ final class AlamofireNetworkErrorHandler {
156183
}
157184
}
158185

186+
enum RequestFlow: String {
187+
case appPasswordGeneration = "app_password_generation"
188+
case apiRequest = "api_request"
189+
}
190+
191+
enum AppPasswordFlagCause: String {
192+
case majorError = "major_error"
193+
case generalFailuresThresholdReached = "general_failures_threshold_reached"
194+
}
195+
159196
// MARK: Private helpers
160197
private extension AlamofireNetworkErrorHandler {
161-
func incrementFailureCount(for siteID: Int64) {
198+
func incrementFailureCount(for siteID: Int64, originalFailure: Error) {
162199
let currentFailureCount = appPasswordFailures[siteID] ?? 0
163200
let updatedCount = currentFailureCount + 1
164201
if updatedCount == AppPasswordConstants.requestFailureThreshold {
165-
flagSiteAsUnsupported(for: siteID)
202+
let flow: RequestFlow
203+
let failure: Error
204+
switch originalFailure {
205+
case AFError.requestRetryFailed(let error, _):
206+
flow = .appPasswordGeneration
207+
failure = error
208+
default:
209+
flow = .apiRequest
210+
failure = originalFailure
211+
}
212+
flagSiteAsUnsupported(
213+
for: siteID,
214+
flow: flow,
215+
cause: .generalFailuresThresholdReached,
216+
error: failure
217+
)
166218
}
167219
appPasswordFailures[siteID] = updatedCount
168220
}
@@ -176,9 +228,46 @@ private extension AlamofireNetworkErrorHandler {
176228
}
177229
}
178230

231+
func logRequestFailure(request: URLRequest, error: Error) {
232+
let networkError: NetworkError? = {
233+
switch error {
234+
case AFError.requestRetryFailed(let retryError, _):
235+
return (retryError as? NetworkError)
236+
case let networkError as NetworkError:
237+
return networkError
238+
default:
239+
return nil
240+
}
241+
}()
242+
243+
let siteURL = request.url?.host() ?? ""
244+
let path = request.url?.path(percentEncoded: false) ?? ""
245+
let method = request.httpMethod ?? ""
246+
let apiErrorCode = networkError?.errorCode ?? error.localizedDescription
247+
let httpCode = networkError?.responseCode ?? (error as NSError).code
248+
249+
DDLogError(
250+
"""
251+
⛔️ Request failed using Application Passwords for Jetpack Site:
252+
- Site URL: \(siteURL)
253+
- Path: \(path)
254+
- Method: \(method)
255+
- Error: HTTP status code \(httpCode)
256+
- Error message: \(apiErrorCode)
257+
"""
258+
)
259+
}
260+
179261
enum Constants {
180262
static let flagRefreshDuration: Double = 60 * 60 * 24 * 14 // flag can be reset after 14 days.
181263
}
264+
265+
enum TracksProperty: String {
266+
case flow
267+
case cause
268+
case apiErrorCode = "api_error_code"
269+
case httpStatusCode = "http_status_code"
270+
}
182271
}
183272
/// Helper type to keep track of retried requests with accompanied error
184273
struct RetriedJetpackRequest {

0 commit comments

Comments
 (0)