Skip to content

Commit

Permalink
[Mobile Payments] Show IPP address errors when using site credentials (
Browse files Browse the repository at this point in the history
  • Loading branch information
joshheald authored Nov 7, 2024
2 parents 8eb48c1 + ef4970f commit b90f934
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 14 deletions.
17 changes: 17 additions & 0 deletions Fakes/Fakes/Networking.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1747,6 +1747,23 @@ extension Networking.Refund {
)
}
}
extension Networking.RemoteReaderLocation {
/// Returns a "ready to use" type filled with fake values.
///
public static func fake() -> Networking.RemoteReaderLocation {
.init(
locationID: .fake(),
city: .fake(),
country: .fake(),
addressLine1: .fake(),
addressLine2: .fake(),
postalCode: .fake(),
stateProvinceRegion: .fake(),
displayName: .fake(),
liveMode: .fake()
)
}
}
extension Networking.ShipmentTracking {
/// Returns a "ready to use" type filled with fake values.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public protocol CardReaderConfigProvider: ReaderLocationProvider, ReaderTokenPro
/// May include URL for wp-admin page to update address.
/// - invalidPostalCode: The location could not be created because the Store postal code configured for the site failed validation.
///
public enum CardReaderConfigError: Error, LocalizedError {
public enum CardReaderConfigError: Error, LocalizedError, Equatable {
case incompleteStoreAddress(adminUrl: URL?)
case invalidPostalCode
}
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ extension StripeCardReaderService: CardReaderService {
return promise(.failure(CardReaderServiceError.connection(underlyingError: underlyingError)))
}
case .failure(let error):
let underlyingError = UnderlyingError(with: error)
let underlyingError = Self.logAndDecodeError(error)
return promise(.failure(CardReaderServiceError.connection(underlyingError: underlyingError)))
}
}
Expand Down Expand Up @@ -509,7 +509,7 @@ extension StripeCardReaderService: CardReaderService {
return promise(.failure(CardReaderServiceError.connection(underlyingError: underlyingError)))
}
case .failure(let error):
let underlyingError = UnderlyingError(with: error)
let underlyingError = Self.logAndDecodeError(error)
return promise(.failure(CardReaderServiceError.connection(underlyingError: underlyingError)))
}
}
Expand Down Expand Up @@ -778,7 +778,7 @@ extension StripeCardReaderService {
self.refundCancellable = nil
if let error = processError {
promise(.failure(CardReaderServiceError.refundPayment(
underlyingError: UnderlyingError(with: error),
underlyingError: Self.logAndDecodeError(error),
shouldRetry: self.shouldRetryRefund(after: error)
)))
} else if let refund = processedRefund {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,7 @@ extension Networking.GoogleAdsCampaign {
rawStatus: CopiableProp<String> = .copy,
rawType: CopiableProp<String> = .copy,
amount: CopiableProp<Double> = .copy,
country: CopiableProp<String> = .copy,
country: NullableCopiableProp<String> = .copy,
targetedLocations: CopiableProp<[String]> = .copy
) -> Networking.GoogleAdsCampaign {
let id = id ?? self.id
Expand Down Expand Up @@ -2853,6 +2853,42 @@ extension Networking.Refund {
}
}

extension Networking.RemoteReaderLocation {
public func copy(
locationID: CopiableProp<String> = .copy,
city: NullableCopiableProp<String> = .copy,
country: CopiableProp<String> = .copy,
addressLine1: CopiableProp<String> = .copy,
addressLine2: NullableCopiableProp<String> = .copy,
postalCode: NullableCopiableProp<String> = .copy,
stateProvinceRegion: NullableCopiableProp<String> = .copy,
displayName: CopiableProp<String> = .copy,
liveMode: CopiableProp<Bool> = .copy
) -> Networking.RemoteReaderLocation {
let locationID = locationID ?? self.locationID
let city = city ?? self.city
let country = country ?? self.country
let addressLine1 = addressLine1 ?? self.addressLine1
let addressLine2 = addressLine2 ?? self.addressLine2
let postalCode = postalCode ?? self.postalCode
let stateProvinceRegion = stateProvinceRegion ?? self.stateProvinceRegion
let displayName = displayName ?? self.displayName
let liveMode = liveMode ?? self.liveMode

return Networking.RemoteReaderLocation(
locationID: locationID,
city: city,
country: country,
addressLine1: addressLine1,
addressLine2: addressLine2,
postalCode: postalCode,
stateProvinceRegion: stateProvinceRegion,
displayName: displayName,
liveMode: liveMode
)
}
}

extension Networking.ShipmentTracking {
public func copy(
siteID: CopiableProp<Int64> = .copy,
Expand Down
4 changes: 3 additions & 1 deletion Networking/Networking/Model/RemoteReaderLocation.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/// Represent a Remote Reader Location Entity.
///
public struct RemoteReaderLocation: Decodable {

import Codegen
public struct RemoteReaderLocation: Decodable, GeneratedFakeable, GeneratedCopiable {
public let locationID: String
public let city: String?
public let country: String
Expand Down
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- [internal] Updated Xcode version to 16.1 [https://github.com/woocommerce/woocommerce-ios/pull/14225]
- [**] Fixed: properly open and show order details in Watch app [https://github.com/woocommerce/woocommerce-ios/pull/14306]
- [*] Payments: Improved error messages for reader connections when using site credentials for login [https://github.com/woocommerce/woocommerce-ios/pull/14336]

21.0
-----
Expand Down
8 changes: 8 additions & 0 deletions Yosemite/Yosemite.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@
209AD3CE2AC1A9C200825D76 /* WooPaymentsDepositsOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209AD3CD2AC1A9C200825D76 /* WooPaymentsDepositsOverview.swift */; };
20BCF6F22B0E554500954840 /* SystemStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6F12B0E554500954840 /* SystemStatusService.swift */; };
20BCF6F52B0E57AB00954840 /* MockSystemStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6F42B0E57AB00954840 /* MockSystemStatusService.swift */; };
20D035002CDBBD6400C0F901 /* CommonReaderConfigProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D034FF2CDBBD6400C0F901 /* CommonReaderConfigProviderTests.swift */; };
20D035022CDBBE2A00C0F901 /* MockCardReaderCapableRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D035012CDBBE2A00C0F901 /* MockCardReaderCapableRemote.swift */; };
20D210C12B177EEF0099E517 /* WooPaymentsDepositServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D210C02B177EEF0099E517 /* WooPaymentsDepositServiceTests.swift */; };
24163B9E257F41A600F94EC3 /* StoresManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24163B9D257F41A600F94EC3 /* StoresManager.swift */; };
24163BA8257F41C500F94EC3 /* SessionManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24163BA7257F41C500F94EC3 /* SessionManagerProtocol.swift */; };
Expand Down Expand Up @@ -655,6 +657,8 @@
209AD3CD2AC1A9C200825D76 /* WooPaymentsDepositsOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsOverview.swift; sourceTree = "<group>"; };
20BCF6F12B0E554500954840 /* SystemStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemStatusService.swift; sourceTree = "<group>"; };
20BCF6F42B0E57AB00954840 /* MockSystemStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MockSystemStatusService.swift; path = ../../../WooCommerce/WooCommerceTests/Mocks/MockSystemStatusService.swift; sourceTree = "<group>"; };
20D034FF2CDBBD6400C0F901 /* CommonReaderConfigProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonReaderConfigProviderTests.swift; sourceTree = "<group>"; };
20D035012CDBBE2A00C0F901 /* MockCardReaderCapableRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardReaderCapableRemote.swift; sourceTree = "<group>"; };
20D210C02B177EEF0099E517 /* WooPaymentsDepositServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositServiceTests.swift; sourceTree = "<group>"; };
24163B9D257F41A600F94EC3 /* StoresManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoresManager.swift; sourceTree = "<group>"; };
24163BA7257F41C500F94EC3 /* SessionManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManagerProtocol.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1889,6 +1893,7 @@
022F9318257F24730011CD94 /* MockShippingLabel.swift */,
022F931C257F27B40011CD94 /* MockShippingLabelAddress.swift */,
20BCF6F42B0E57AB00954840 /* MockSystemStatusService.swift */,
20D035012CDBBE2A00C0F901 /* MockCardReaderCapableRemote.swift */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -1966,6 +1971,7 @@
B54EAF2021188C470029C35E /* EntityListenerTests.swift */,
B5F2AE9420EBAD6000FEDC59 /* ResultsControllerTests.swift */,
031FD89F26FC970300B315C7 /* RosettaTestingHelper.swift */,
20D034FF2CDBBD6400C0F901 /* CommonReaderConfigProviderTests.swift */,
);
path = Tools;
sourceTree = "<group>";
Expand Down Expand Up @@ -2557,6 +2563,7 @@
buildActionMask = 2147483647;
files = (
4552073D25811B4E001CF873 /* ProductAttributeStoreTests.swift in Sources */,
20D035022CDBBE2A00C0F901 /* MockCardReaderCapableRemote.swift in Sources */,
458C6DE825ACC554009B300D /* AppSettingsStoreTests+ProductsSettings.swift in Sources */,
D88303F025E45E6F00C877F9 /* MockCardReaderService.swift in Sources */,
022F00C524728B0C008CD97F /* SiteNotificationCountFileContentsTests.swift in Sources */,
Expand Down Expand Up @@ -2603,6 +2610,7 @@
45ED4F16239E939A004F1BE3 /* TaxStoreTests.swift in Sources */,
57264572250BE2E7005BBD7C /* OrdersUpsertUseCaseTests.swift in Sources */,
0286A1BE2A0CC4810099EF94 /* MockFeatureFlagRemote.swift in Sources */,
20D035002CDBBD6400C0F901 /* CommonReaderConfigProviderTests.swift in Sources */,
0225512522FC312400D98613 /* OrderStatsV4Interval+DateTests.swift in Sources */,
D8652E4826307A5000350F37 /* MockReceiptPrinterService.swift in Sources */,
020B2F9623BDE4DD00BD79AD /* ProductStoreTests+Validation.swift in Sources */,
Expand Down
37 changes: 29 additions & 8 deletions Yosemite/Yosemite/Tools/CommonReaderConfigProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,39 @@ final public class CommonReaderConfigProvider: CommonReaderConfigProviding {

private extension CardReaderConfigError {
init?(error: Error) {
guard let dotcomError = error as? DotcomError else {
return nil
}
switch dotcomError {
case .unknown("store_address_is_incomplete", let message):
switch error {
case DotcomError.unknown(code: "store_address_is_incomplete", let message):
self = .incompleteStoreAddress(adminUrl: URL(string: message ?? ""))
return
case .unknown("postal_code_invalid", _):
case DotcomError.unknown(code: "postal_code_invalid", _):
self = .invalidPostalCode
return
case NetworkError.unacceptableStatusCode(_, let responseData):
guard let responseData,
let details = try? JSONDecoder().decode(CardReaderConfigNetworkErrorDetails.self, from: responseData) else {
return nil
}
switch details.code {
case .storeAddressIncomplete:
self = .incompleteStoreAddress(adminUrl: URL(string: details.message ?? ""))
case .postalCodeInvalid:
self = .invalidPostalCode
}
default:
return nil
}
}
}

private struct CardReaderConfigNetworkErrorDetails: Decodable {
let code: ErrorCode
let message: String?

enum CodingKeys: CodingKey {
case code
case message
}

enum ErrorCode: String, Decodable {
case storeAddressIncomplete = "store_address_is_incomplete"
case postalCodeInvalid = "postal_code_invalid"
}
}
16 changes: 16 additions & 0 deletions Yosemite/YosemiteTests/Mocks/MockCardReaderCapableRemote.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation
import Yosemite
import Networking
import Fakes

struct MockCardReaderCapableRemote: CardReaderCapableRemote {
func loadConnectionToken(for siteID: Int64, completion: @escaping (Result<Networking.ReaderConnectionToken, any Error>) -> Void) {
// no-op
}

var resultForDefaultReaderLocation: (Result<RemoteReaderLocation, any Error>) = .success(RemoteReaderLocation.fake())
func loadDefaultReaderLocation(for siteID: Int64, onCompletion: @escaping (Result<RemoteReaderLocation, any Error>) -> Void) {
onCompletion(resultForDefaultReaderLocation)
}

}
132 changes: 132 additions & 0 deletions Yosemite/YosemiteTests/Tools/CommonReaderConfigProviderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import Testing

@testable import Yosemite
import Networking

struct CommonReaderConfigProviderTests {

@Test func fetchDefaultLocationID_returns_incompleteStoreAddress_error_when_jetpack_site_has_incomplete_store_address() async throws {
let adminURL = "https://example.com/wp-admin/set-address"
let mockRemote = MockCardReaderCapableRemote(
resultForDefaultReaderLocation: .failure(
DotcomError.unknown(code: "store_address_is_incomplete",
message: adminURL)))

let sut = CommonReaderConfigProvider(siteID: 123,
readerConfigRemote: mockRemote)

let expectedError = CardReaderConfigError.incompleteStoreAddress(adminUrl: URL(string: adminURL)!)
await #expect(throws: expectedError) {
try await withCheckedThrowingContinuation { continuation in
sut.fetchDefaultLocationID { result in
continuation.resume(with: result)
}
}
}
}

@Test func fetchDefaultLocationID_returns_invalidPostalCode_error_when_jetpack_site_has_no_postcode() async throws {
let mockRemote = MockCardReaderCapableRemote(
resultForDefaultReaderLocation: .failure(
DotcomError.unknown(code: "postal_code_invalid",
message: "")))

let sut = CommonReaderConfigProvider(siteID: 123,
readerConfigRemote: mockRemote)

await #expect(throws: CardReaderConfigError.invalidPostalCode) {
try await withCheckedThrowingContinuation { continuation in
sut.fetchDefaultLocationID { result in
continuation.resume(with: result)
}
}
}
}

@Test(
.bug("https://github.com/woocommerce/woocommerce-ios/issues/14333", id: "14333")
)
func fetchDefaultLocationID_returns_incompleteStoreAddress_error_when_site_has_incomplete_store_address_and_siteCredentials_used() async throws {
let errorResponseJSON = """
{"code":"store_address_is_incomplete","message":"https://example.com/wp-admin/admin.php?page=wc-settings&tab=general","data":null}
"""
let mockRemote = MockCardReaderCapableRemote(
resultForDefaultReaderLocation: .failure(
NetworkError.unacceptableStatusCode(
statusCode: 500,
response: errorResponseJSON.data(using: .utf8)
)
)
)

let sut = CommonReaderConfigProvider(siteID: 123,
readerConfigRemote: mockRemote)

let expectedError = CardReaderConfigError.incompleteStoreAddress(
adminUrl: URL(string: "https://example.com/wp-admin/admin.php?page=wc-settings&tab=general")!)
await #expect(throws: expectedError) {
try await withCheckedThrowingContinuation { continuation in
sut.fetchDefaultLocationID { result in
continuation.resume(with: result)
}
}
}
}

@Test
func fetchDefaultLocationID_returns_incompleteStoreAddress_error_when_site_has_incomplete_store_address_without_url_and_siteCredentials_used() async throws {
let errorResponseJSON = """
{"code":"store_address_is_incomplete","message":null,"data":null}
"""
let mockRemote = MockCardReaderCapableRemote(
resultForDefaultReaderLocation: .failure(
NetworkError.unacceptableStatusCode(
statusCode: 500,
response: errorResponseJSON.data(using: .utf8)
)
)
)

let sut = CommonReaderConfigProvider(siteID: 123,
readerConfigRemote: mockRemote)

let expectedError = CardReaderConfigError.incompleteStoreAddress(adminUrl: nil)
await #expect(throws: expectedError) {
try await withCheckedThrowingContinuation { continuation in
sut.fetchDefaultLocationID { result in
continuation.resume(with: result)
}
}
}
}

@Test(
.bug("https://github.com/woocommerce/woocommerce-ios/issues/14333", id: "14333")
)
func fetchDefaultLocationID_returns_invalidPostalCode_error_when_site_has_no_postcode_and_siteCredentials_used() async throws {
let errorResponseJSON = """
{"code":"postal_code_invalid"}
"""
let mockRemote = MockCardReaderCapableRemote(
resultForDefaultReaderLocation: .failure(
NetworkError.unacceptableStatusCode(
statusCode: 500,
response: errorResponseJSON.data(using: .utf8)
)
)
)

let sut = CommonReaderConfigProvider(siteID: 123,
readerConfigRemote: mockRemote)

let expectedError = CardReaderConfigError.invalidPostalCode
await #expect(throws: expectedError) {
try await withCheckedThrowingContinuation { continuation in
sut.fetchDefaultLocationID { result in
continuation.resume(with: result)
}
}
}
}

}

0 comments on commit b90f934

Please sign in to comment.