Skip to content

Commit 5bb1535

Browse files
committed
Merge branch 'trunk' into feat/8453-check-role-eligibility
2 parents 62b7043 + 8bb8110 commit 5bb1535

File tree

128 files changed

+2214
-788
lines changed

Some content is hidden

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

128 files changed

+2214
-788
lines changed

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
5858
case .applicationPasswordAuthenticationForSiteCredentialLogin:
5959
// Enable this to test application password authentication (WIP)
6060
return false
61+
case .generateAllVariations:
62+
return buildConfig == .localDeveloper || buildConfig == .alpha
6163
default:
6264
return true
6365
}

Experiments/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,8 @@ public enum FeatureFlag: Int {
137137
/// Whether application password authentication should be used when a user logs in with site credentials.
138138
///
139139
case applicationPasswordAuthenticationForSiteCredentialLogin
140+
141+
/// Allows merchants to create all variations from a single button
142+
///
143+
case generateAllVariations
140144
}

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 85 additions & 17 deletions
Large diffs are not rendered by default.

Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import enum Alamofire.AFError
66
public enum ApplicationPasswordUseCaseError: Error {
77
case duplicateName
88
case applicationPasswordsDisabled
9-
case invalidSiteAddress
9+
case failedToConstructLoginOrAdminURLUsingSiteAddress
1010
}
1111

1212
public struct ApplicationPassword {
@@ -77,7 +77,7 @@ final public class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase
7777
guard let loginURL = URL(string: siteAddress + Constants.loginPath),
7878
let adminURL = URL(string: siteAddress + Constants.adminPath) else {
7979
DDLogWarn("⚠️ Cannot construct login URL and admin URL for site \(siteAddress)")
80-
throw ApplicationPasswordUseCaseError.invalidSiteAddress
80+
throw ApplicationPasswordUseCaseError.failedToConstructLoginOrAdminURLUsingSiteAddress
8181
}
8282
// Prepares the authenticator with username and password
8383
let authenticator = CookieNonceAuthenticator(username: username,
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
enum RequestAuthenticatorError: Error {
2+
case applicationPasswordUseCaseNotAvailable
3+
case applicationPasswordNotAvailable
4+
}
5+
6+
/// Authenticates request
7+
///
8+
public struct RequestAuthenticator {
9+
/// Credentials.
10+
///
11+
let credentials: Credentials?
12+
13+
/// The use case to handle authentication with application passwords.
14+
///
15+
private let applicationPasswordUseCase: ApplicationPasswordUseCase?
16+
17+
/// Sets up the authenticator with optional credentials and application password use case.
18+
/// `applicationPasswordUseCase` can be injected for unit tests.
19+
///
20+
init(credentials: Credentials?, applicationPasswordUseCase: ApplicationPasswordUseCase? = nil) {
21+
self.credentials = credentials
22+
let useCase: ApplicationPasswordUseCase? = {
23+
if let applicationPasswordUseCase {
24+
return applicationPasswordUseCase
25+
} else if case let .wporg(username, password, siteAddress) = credentials {
26+
return try? DefaultApplicationPasswordUseCase(username: username,
27+
password: password,
28+
siteAddress: siteAddress)
29+
} else {
30+
return nil
31+
}
32+
}()
33+
self.applicationPasswordUseCase = useCase
34+
}
35+
36+
func authenticate(_ urlRequest: URLRequest) throws -> URLRequest {
37+
if isRestAPIRequest(urlRequest) {
38+
return try authenticateUsingApplicationPasswordIfPossible(urlRequest)
39+
} else {
40+
return try authenticateUsingWPCOMTokenIfPossible(urlRequest)
41+
}
42+
}
43+
44+
func generateApplicationPassword() async throws {
45+
guard let applicationPasswordUseCase else {
46+
throw RequestAuthenticatorError.applicationPasswordUseCaseNotAvailable
47+
}
48+
let _ = try await applicationPasswordUseCase.generateNewPassword()
49+
return
50+
}
51+
52+
/// Checks whether the given URLRequest is eligible for retyring
53+
///
54+
func shouldRetry(_ urlRequest: URLRequest) -> Bool {
55+
isRestAPIRequest(urlRequest)
56+
}
57+
}
58+
59+
private extension RequestAuthenticator {
60+
/// To check whether the given URLRequest is a REST API request
61+
///
62+
func isRestAPIRequest(_ urlRequest: URLRequest) -> Bool {
63+
guard case let .wporg(_, _, siteAddress) = credentials,
64+
let url = urlRequest.url,
65+
url.absoluteString.hasPrefix(siteAddress.trimSlashes() + "/" + RESTRequest.Settings.basePath) else {
66+
return false
67+
}
68+
return true
69+
}
70+
71+
/// Attempts creating a request with WPCOM token if possible.
72+
///
73+
func authenticateUsingWPCOMTokenIfPossible(_ urlRequest: URLRequest) throws -> URLRequest {
74+
guard case let .wpcom(_, authToken, _) = credentials else {
75+
return UnauthenticatedRequest(request: urlRequest).asURLRequest()
76+
}
77+
78+
return AuthenticatedDotcomRequest(authToken: authToken, request: urlRequest).asURLRequest()
79+
}
80+
81+
/// Attempts creating a request with application password if possible.
82+
///
83+
func authenticateUsingApplicationPasswordIfPossible(_ urlRequest: URLRequest) throws -> URLRequest {
84+
guard let applicationPassword = applicationPasswordUseCase?.applicationPassword else {
85+
throw RequestAuthenticatorError.applicationPasswordNotAvailable
86+
}
87+
88+
return AuthenticatedRESTRequest(applicationPassword: applicationPassword, request: urlRequest).asURLRequest()
89+
}
90+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Alamofire
2+
3+
/// Converter to convert Jetpack tunnel requests into REST API requests if needed
4+
///
5+
struct RequestConverter {
6+
let credentials: Credentials?
7+
8+
func convert(_ request: URLRequestConvertible) -> URLRequestConvertible {
9+
guard let jetpackRequest = request as? JetpackRequest,
10+
case let .wporg(_, _, siteAddress) = credentials,
11+
let restRequest = jetpackRequest.asRESTRequest(with: siteAddress) else {
12+
return request
13+
}
14+
15+
return restRequest
16+
}
17+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import Alamofire
2+
import Foundation
3+
4+
/// Authenticates and retries requests
5+
///
6+
final class RequestProcessor {
7+
private var requestsToRetry = [RequestRetryCompletion]()
8+
9+
private var isAuthenticating = false
10+
11+
private let requestAuthenticator: RequestAuthenticator
12+
13+
init(credentials: Credentials?) {
14+
requestAuthenticator = RequestAuthenticator(credentials: credentials)
15+
}
16+
}
17+
18+
// MARK: Request Authentication
19+
//
20+
extension RequestProcessor: RequestAdapter {
21+
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
22+
return try requestAuthenticator.authenticate(urlRequest)
23+
}
24+
}
25+
26+
// MARK: Retrying Request
27+
//
28+
extension RequestProcessor: RequestRetrier {
29+
func should(_ manager: Alamofire.SessionManager,
30+
retry request: Alamofire.Request,
31+
with error: Error,
32+
completion: @escaping Alamofire.RequestRetryCompletion) {
33+
guard
34+
request.retryCount == 0, // Only retry once
35+
let urlRequest = request.request,
36+
requestAuthenticator.shouldRetry(urlRequest), // Retry only REST API requests that use application password
37+
shouldRetry(error) // Retry only specific errors
38+
else {
39+
return completion(false, 0.0)
40+
}
41+
42+
requestsToRetry.append(completion)
43+
if !isAuthenticating {
44+
generateApplicationPassword()
45+
}
46+
}
47+
}
48+
49+
// MARK: Helpers
50+
//
51+
private extension RequestProcessor {
52+
func generateApplicationPassword() {
53+
Task(priority: .medium) {
54+
isAuthenticating = true
55+
56+
do {
57+
let _ = try await requestAuthenticator.generateApplicationPassword()
58+
isAuthenticating = false
59+
completeRequests(true)
60+
} catch {
61+
isAuthenticating = false
62+
completeRequests(false)
63+
}
64+
}
65+
}
66+
67+
func shouldRetry(_ error: Error) -> Bool {
68+
// Need to generate application password
69+
if .applicationPasswordNotAvailable == error as? RequestAuthenticatorError {
70+
return true
71+
}
72+
73+
// Failed authorization
74+
if case .responseValidationFailed(reason: .unacceptableStatusCode(code: 401)) = error as? AFError {
75+
return true
76+
}
77+
78+
return false
79+
}
80+
81+
func completeRequests(_ shouldRetry: Bool) {
82+
requestsToRetry.forEach { (completion) in
83+
completion(shouldRetry, 0.0)
84+
}
85+
requestsToRetry.removeAll()
86+
}
87+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
extension String {
4+
/// Trims forward slash
5+
///
6+
/// - Returns: String after removing prefix and suffix "/"
7+
///
8+
func trimSlashes() -> String {
9+
removingPrefix("/").removingSuffix("/")
10+
}
11+
}

Networking/Networking/Mapper/CouponReportListMapper.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ struct CouponReportListMapper: Mapper {
88
///
99
func map(response: Data) throws -> [CouponReport] {
1010
let decoder = JSONDecoder()
11-
let reports = try decoder.decode(CouponReportsEnvelope.self, from: response).reports
12-
return reports
11+
do {
12+
return try decoder.decode(CouponReportsEnvelope.self, from: response).reports
13+
} catch {
14+
return try decoder.decode([CouponReport].self, from: response)
15+
}
1316
}
1417
}
1518

0 commit comments

Comments
 (0)