Skip to content

Commit 705c0c7

Browse files
authored
Merge branch 'trunk' into woomob-73-login-with-captcha
2 parents 99e7a4d + dd2f59c commit 705c0c7

File tree

78 files changed

+1646
-190
lines changed

Some content is hidden

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

78 files changed

+1646
-190
lines changed

Modules/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
reference = "container:Tests/NetworkingTests/NetworkingTests.xctestplan"
3434
default = "YES">
3535
</TestPlanReference>
36+
<TestPlanReference
37+
reference = "container:Tests/NetworkingTests/RequestAuthenticatorPressureTests.xctestplan">
38+
</TestPlanReference>
3639
</TestPlans>
3740
</TestAction>
3841
<LaunchAction

Modules/Sources/NetworkingCore/ApplicationPassword/RequestProcessor.swift

Lines changed: 110 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,29 @@ protocol RequestProcessorDelegate: AnyObject {
88
/// Authenticates and retries requests
99
///
1010
final class RequestProcessor: RequestInterceptor {
11-
private var requestsToRetry = [(RetryResult) -> Void]()
12-
13-
private var isAuthenticating = false
14-
15-
private var requestAuthenticator: RequestAuthenticator
16-
1711
private let notificationCenter: NotificationCenter
1812

19-
private var currentSiteID: Int64?
13+
private let state: RequestProcessorState
2014

2115
weak var delegate: RequestProcessorDelegate?
2216

2317
init(requestAuthenticator: RequestAuthenticator,
2418
notificationCenter: NotificationCenter = .default) {
25-
self.requestAuthenticator = requestAuthenticator
2619
self.notificationCenter = notificationCenter
20+
self.state = RequestProcessorState(requestAuthenticator: requestAuthenticator)
2721
}
2822

2923
func updateAuthenticator(_ authenticator: RequestAuthenticator) {
30-
requestAuthenticator = authenticator
31-
currentSiteID = authenticator.jetpackSiteID
24+
state.updateAuthenticator(authenticator)
3225
}
3326
}
3427

3528
// MARK: Request Authentication
3629
//
3730
extension RequestProcessor: RequestAdapter {
3831
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
39-
let result = Result { try requestAuthenticator.authenticate(urlRequest) }
32+
let authenticator = state.authenticator
33+
let result = Result { try authenticator.authenticate(urlRequest) }
4034
completion(result)
4135
}
4236
}
@@ -45,18 +39,27 @@ extension RequestProcessor: RequestAdapter {
4539
//
4640
extension RequestProcessor: RequestRetrier {
4741
func retry(_ request: Alamofire.Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
48-
guard
49-
request.retryCount == 0, // Only retry once
50-
let urlRequest = request.request,
51-
requestAuthenticator.shouldRetry(urlRequest), // Retry only REST API requests that use application password
52-
shouldRetry(error) // Retry only specific errors
53-
else {
54-
return completion(.doNotRetry)
55-
}
56-
57-
requestsToRetry.append(completion)
58-
if !isAuthenticating {
59-
isAuthenticating = true
42+
guard request.retryCount == 0, // Only retry once
43+
let urlRequest = request.request else {
44+
completion(.doNotRetry)
45+
return
46+
}
47+
48+
guard shouldRetry(error) else {
49+
completion(.doNotRetry)
50+
return
51+
}
52+
53+
let shouldRetryRequest = state.shouldRetry(urlRequest)
54+
55+
guard shouldRetryRequest else {
56+
completion(.doNotRetry)
57+
return
58+
}
59+
60+
let shouldStartAuthentication = state.enqueueRetry(completion)
61+
62+
if shouldStartAuthentication {
6063
generateApplicationPassword()
6164
}
6265
}
@@ -66,28 +69,30 @@ extension RequestProcessor: RequestRetrier {
6669
//
6770
private extension RequestProcessor {
6871
func generateApplicationPassword() {
69-
Task(priority: .medium) { @MainActor in
72+
Task(priority: .medium) { @MainActor [weak self] in
73+
guard let self else { return }
74+
let authenticator = self.state.authenticator
7075
do {
71-
let _ = try await requestAuthenticator.generateApplicationPassword()
72-
isAuthenticating = false
76+
let _ = try await authenticator.generateApplicationPassword()
77+
self.state.setAuthenticating(false)
7378

7479
// Post a notification for tracking
75-
notificationCenter.post(name: .ApplicationPasswordsNewPasswordCreated, object: nil, userInfo: nil)
80+
self.notificationCenter.post(name: .ApplicationPasswordsNewPasswordCreated, object: nil, userInfo: nil)
7681

77-
completeRequests(true)
82+
self.completeRequests(true)
7883
} catch {
7984

8085
// Post a notification for tracking
81-
notificationCenter.post(name: .ApplicationPasswordsGenerationFailed, object: error, userInfo: nil)
86+
self.notificationCenter.post(name: .ApplicationPasswordsGenerationFailed, object: error, userInfo: nil)
8287

83-
let shouldRetry = await checkIfRetryingGenerationIsNeeded(for: error)
88+
let shouldRetry = await self.checkIfRetryingGenerationIsNeeded(for: error)
8489
if shouldRetry {
85-
generateApplicationPassword()
90+
self.generateApplicationPassword()
8691
} else {
87-
isAuthenticating = false
88-
completeRequests(false, error: error)
89-
if let currentSiteID {
90-
notifyFailureIfNeeded(error, for: currentSiteID)
92+
self.state.setAuthenticating(false)
93+
self.completeRequests(false, error: error)
94+
if let siteID = self.state.currentSiteID {
95+
self.notifyFailureIfNeeded(error, for: siteID)
9196
}
9297
}
9398
}
@@ -98,14 +103,15 @@ private extension RequestProcessor {
98103
/// Returns whether retry is needed.
99104
@MainActor
100105
func checkIfRetryingGenerationIsNeeded(for error: Error) async -> Bool {
101-
guard currentSiteID != nil else {
106+
guard state.currentSiteID != nil else {
102107
return false
103108
}
104109
switch error {
105110
case NetworkError.unacceptableStatusCode(let statusCode, _) where statusCode == 409:
106111
/// Password with the same name already exists. Request deletion remotely and retry.
107112
do {
108-
try await requestAuthenticator.deleteApplicationPassword()
113+
let authenticator = state.authenticator
114+
try await authenticator.deleteApplicationPassword()
109115
return true
110116
} catch {
111117
return false
@@ -156,10 +162,10 @@ private extension RequestProcessor {
156162
.doNotRetry
157163
}
158164
}()
159-
requestsToRetry.forEach { (completion) in
165+
let pendingCompletions = state.drainPendingRetries()
166+
pendingCompletions.forEach { completion in
160167
completion(result)
161168
}
162-
requestsToRetry.removeAll()
163169
}
164170
}
165171

@@ -182,3 +188,68 @@ public extension NSNotification.Name {
182188
///
183189
static let JetpackSiteEligibleForAppPasswordSupport = NSNotification.Name(rawValue: "JetpackSiteEligibleForAppPasswordSupport")
184190
}
191+
192+
private extension RequestProcessor {
193+
final class RequestProcessorState {
194+
private var requestsToRetry: [(RetryResult) -> Void]
195+
private var isAuthenticating: Bool
196+
private var requestAuthenticator: RequestAuthenticator
197+
private var siteID: Int64?
198+
199+
private let queue = DispatchQueue(
200+
label: "com.woocommerce.networking.request-processor.state-queue",
201+
qos: .userInitiated
202+
)
203+
204+
init(requestAuthenticator: RequestAuthenticator) {
205+
self.requestsToRetry = []
206+
self.isAuthenticating = false
207+
self.requestAuthenticator = requestAuthenticator
208+
self.siteID = requestAuthenticator.jetpackSiteID
209+
}
210+
211+
var authenticator: RequestAuthenticator {
212+
queue.sync { requestAuthenticator }
213+
}
214+
215+
var currentSiteID: Int64? {
216+
queue.sync { siteID }
217+
}
218+
219+
func shouldRetry(_ request: URLRequest) -> Bool {
220+
queue.sync { requestAuthenticator.shouldRetry(request) }
221+
}
222+
223+
func enqueueRetry(_ completion: @escaping (RetryResult) -> Void) -> Bool {
224+
queue.sync {
225+
requestsToRetry.append(completion)
226+
if isAuthenticating {
227+
return false
228+
}
229+
isAuthenticating = true
230+
return true
231+
}
232+
}
233+
234+
func setAuthenticating(_ value: Bool) {
235+
queue.sync {
236+
isAuthenticating = value
237+
}
238+
}
239+
240+
func drainPendingRetries() -> [(RetryResult) -> Void] {
241+
queue.sync {
242+
let completions = requestsToRetry
243+
requestsToRetry.removeAll()
244+
return completions
245+
}
246+
}
247+
248+
func updateAuthenticator(_ authenticator: RequestAuthenticator) {
249+
queue.sync {
250+
requestAuthenticator = authenticator
251+
siteID = authenticator.jetpackSiteID
252+
}
253+
}
254+
}
255+
}

Modules/Sources/PointOfSale/Controllers/PointOfSaleItemsController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22
import Observation
33
import enum Yosemite.POSItem
4+
import struct Yosemite.POSSimpleProduct
45
import class Yosemite.PointOfSaleItemService
56
import protocol Yosemite.PointOfSaleItemServiceProtocol
67
import protocol Yosemite.PointOfSaleItemFetchStrategyFactoryProtocol

Modules/Sources/PointOfSale/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ struct CardReaderConnectionStatusView: View {
3434
.padding(.horizontal, Constants.horizontalPadding)
3535
.frame(maxHeight: .infinity)
3636
}
37+
.accessibilityIdentifier("pos-reader-connected")
3738
case .disconnecting:
3839
progressIndicatingCardReaderStatus(title: Localization.readerDisconnecting)
3940
case .cancellingConnection:
@@ -55,6 +56,7 @@ struct CardReaderConnectionStatusView: View {
5556
)
5657
.padding(Constants.disconnectedBorderInset)
5758
}
59+
.accessibilityIdentifier("pos-connect-reader-button")
5860
}
5961
}
6062
.font(Constants.font)

Modules/Sources/PointOfSale/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ struct PointOfSaleCardPresentPaymentTapSwipeInsertCardMessageView: View {
2424
}
2525
}
2626
.multilineTextAlignment(.center)
27+
.accessibilityIdentifier("pos-card-payment-message")
2728
}
2829
}
2930

Modules/Sources/PointOfSale/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSalePaymentSuccessView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct PointOfSalePaymentSuccessView: View {
3131
}
3232
}
3333
}
34+
.accessibilityIdentifier("pos-payment-success-view")
3435
.onAppear {
3536
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
3637
isViewLoaded = true

Modules/Sources/PointOfSale/Presentation/CartView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ struct CartView: View {
8080
})
8181
.background(backgroundColor.ignoresSafeArea(.all))
8282
.accessibilityElement(children: .contain)
83+
.accessibilityIdentifier("pos-cart-view")
8384
}
8485
}
8586
}
@@ -175,6 +176,7 @@ private extension CartView {
175176
}
176177
.buttonStyle(POSFilledButtonStyle(size: .normal))
177178
.disabled(CartViewHelper().hasUnresolvedItems(cart: posModel.cart))
179+
.accessibilityIdentifier("pos-checkout-button")
178180
}
179181

180182
var backButtonConfiguration: POSPageHeaderBackButtonConfiguration? {

Modules/Sources/PointOfSale/Presentation/Item Selector/ItemList.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ struct ItemListRow: View {
156156
}, label: {
157157
SimpleProductCardView(product: product)
158158
})
159+
.accessibilityIdentifier("pos-product-card-\(product.productID)")
159160
case let .variableParentProduct(parentProduct):
160161
if #available(iOS 18.0, *) {
161162
NavigationLink(value: item) {

Modules/Sources/PointOfSale/Presentation/POSFloatingControlView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ struct POSFloatingControlView: View {
4040
}
4141
.frame(width: Constants.size)
4242
}
43+
.accessibilityIdentifier("pos-menu-button")
4344
.background(backgroundColor)
4445
.cornerRadius(Constants.cornerRadius)
4546
.disabled(posModel.paymentState.card == .processingPayment)
@@ -78,6 +79,7 @@ private extension POSFloatingControlView {
7879
icon: { Image(systemName: "rectangle.portrait.and.arrow.forward") }
7980
)
8081
}
82+
.accessibilityIdentifier("pos-exit-menu-item")
8183
Button {
8284
analytics.track(.pointOfSaleSettingsMenuItemTapped)
8385
showSettings = true

Modules/Sources/PointOfSale/Presentation/PointOfSaleCollectCashView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ struct PointOfSaleCollectCashView: View {
9797
buttonFrame = $0
9898
}
9999
.buttonStyle(POSFilledButtonStyle(size: .normal, isLoading: isLoading))
100+
.accessibilityIdentifier("pos-mark-payment-complete-button")
100101
.frame(maxWidth: .infinity)
101102
.dynamicTypeSize(...DynamicTypeSize.accessibility1)
102103
.disabled(!isButtonEnabled)

0 commit comments

Comments
 (0)