From 39c14a059b0f6b8bba6dfedd67d36b594def9b47 Mon Sep 17 00:00:00 2001 From: Mark Mroz Date: Mon, 15 Apr 2024 17:08:16 -0400 Subject: [PATCH] Release 0.6.0 --- CashAppPayKit.podspec | 2 +- CashAppPayKitUI.podspec | 2 +- RELEASE-NOTES.md | 10 + Sources/PayKit/CashAppPay.swift | 4 +- .../ObjcWrapper/CashAppPayState+ObjC.swift | 373 +++++++++++ .../ObjcWrapper/CustomerRequest+ObjC.swift | 623 ++++++++++++++++++ Sources/PayKit/ObjcWrapper/Errors+ObjC.swift | 300 +++++++++ Sources/PayKit/ObjcWrapper/ObjCWrapper.swift | 179 +++++ .../Analytics/JSONEncoder+Analytics.swift | 1 + Tests/PayKitTests/AnalyticsEventTests.swift | 18 +- .../CashAppPayState+ObjCTests.swift | 316 +++++++++ .../CreateCustomerRequestParamsTests.swift | 4 +- .../CustomerRequest+ObjCTests.swift | 343 ++++++++++ Tests/PayKitTests/Errors+ObjCTests.swift | 107 +++ Tests/PayKitTests/LoggableTests.swift | 6 +- Tests/PayKitTests/ObjcWrapperTests.swift | 175 +++++ .../ResilientRestServiceTests.swift | 2 +- .../Resources/XCTestCase+Fixtures.swift | 26 +- 18 files changed, 2468 insertions(+), 23 deletions(-) create mode 100644 Sources/PayKit/ObjcWrapper/CashAppPayState+ObjC.swift create mode 100644 Sources/PayKit/ObjcWrapper/CustomerRequest+ObjC.swift create mode 100644 Sources/PayKit/ObjcWrapper/Errors+ObjC.swift create mode 100644 Sources/PayKit/ObjcWrapper/ObjCWrapper.swift create mode 100644 Tests/PayKitTests/CashAppPayState+ObjCTests.swift create mode 100644 Tests/PayKitTests/CustomerRequest+ObjCTests.swift create mode 100644 Tests/PayKitTests/Errors+ObjCTests.swift create mode 100644 Tests/PayKitTests/ObjcWrapperTests.swift diff --git a/CashAppPayKit.podspec b/CashAppPayKit.podspec index f577c39..0affa5c 100644 --- a/CashAppPayKit.podspec +++ b/CashAppPayKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'CashAppPayKit' - s.version = '0.5.1' + s.version = '0.6.0' s.summary = 'PayKit iOS SDK' s.homepage = 'https://github.com/cashapp/cash-app-pay-ios-sdk' s.license = 'Apache License, Version 2.0' diff --git a/CashAppPayKitUI.podspec b/CashAppPayKitUI.podspec index 40f8599..377ce11 100644 --- a/CashAppPayKitUI.podspec +++ b/CashAppPayKitUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'CashAppPayKitUI' - s.version = "0.5.1" + s.version = "0.6.0" s.summary = 'UI components for the PayKit iOS SDK' s.homepage = 'https://github.com/cashapp/cash-app-pay-ios-sdk' s.license = 'Apache License, Version 2.0' diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 7b43cab..587f661 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,13 @@ +## PayKit 0.6.0 Release Notes + +Pay Kit 0.6.0 supports iOS and requires Xcode 11 or later. The minimum supported Base SDK is 12.0. + +Pay Kit 0.6.0 includes the following new features and enhancements. + +- **PayKit Supports Objective-C** + + PayKit now provides Objective-C bindings. + ## PayKit 0.5.1 Release Notes Pay Kit 0.5.1 supports iOS and requires Xcode 11 or later. The minimum supported Base SDK is 12.0. diff --git a/Sources/PayKit/CashAppPay.swift b/Sources/PayKit/CashAppPay.swift index a9f7638..9a986a2 100644 --- a/Sources/PayKit/CashAppPay.swift +++ b/Sources/PayKit/CashAppPay.swift @@ -19,7 +19,7 @@ import UIKit public class CashAppPay { - public static let version = "0.5.1" + public static let version = "0.6.0" public static let RedirectNotification: Notification.Name = Notification.Name("CashAppPayRedirect") @@ -122,7 +122,7 @@ public enum CashAppPayState: Equatable { case creatingCustomerRequest(CreateCustomerRequestParams) /// CustomerRequest is being updated. For information only. case updatingCustomerRequest(request: CustomerRequest, params: UpdateCustomerRequestParams) - /// CustomerRequest has been created, waiting for customer to press "Pay with Cash App Pay" button0. + /// CustomerRequest has been created, waiting for customer to press "Pay with Cash App Pay" button. case readyToAuthorize(CustomerRequest) /// SDK is redirecting to Cash App for authorization. Show loading indicator if desired. case redirecting(CustomerRequest) diff --git a/Sources/PayKit/ObjcWrapper/CashAppPayState+ObjC.swift b/Sources/PayKit/ObjcWrapper/CashAppPayState+ObjC.swift new file mode 100644 index 0000000..f41639c --- /dev/null +++ b/Sources/PayKit/ObjcWrapper/CashAppPayState+ObjC.swift @@ -0,0 +1,373 @@ +// +// CashAppPayState+ObjC.swift +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc open class CAPCashAppPayState: NSObject { + override init() { + super.init() + } +} + +/// Ready for a Create Customer Request to be initiated. +@objc public final class CAPCashAppPayStateNotStarted: CAPCashAppPayState { + + override init() { + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + return object is CAPCashAppPayStateNotStarted + } +} + +/// CustomerRequest is being created. For information only. +@objcMembers public final class CAPCashAppPayStateCreatingCustomerRequest: CAPCashAppPayState { + public let params: CAPCreateCustomerRequestParams + + // MARK: - Init + + init(params: CAPCreateCustomerRequestParams) { + self.params = params + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateCreatingCustomerRequest else { + return false + } + return params == otherObject.params + } +} + +/// CustomerRequest is being updated. For information only. +@objcMembers public final class CAPCashAppPayStateUpdatingCustomerRequest: CAPCashAppPayState { + public let request: CAPCustomerRequest + public let params: CAPUpdateCustomerRequestParams + + init(request: CAPCustomerRequest, params: CAPUpdateCustomerRequestParams) { + self.request = request + self.params = params + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateUpdatingCustomerRequest else { + return false + } + return request == otherObject.request && params == otherObject.params + } +} + +/// CustomerRequest has been created, waiting for customer to press "Pay with Cash App Pay" button. +@objcMembers public final class CAPCashAppPayStateReadyToAuthorize: CAPCashAppPayState { + public let request: CAPCustomerRequest + + init(request: CAPCustomerRequest) { + self.request = request + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateReadyToAuthorize else { + return false + } + return request == otherObject.request + } +} + +/// SDK is redirecting to Cash App for authorization. Show loading indicator if desired. +@objcMembers public final class CAPCashAppPayStateRedirecting: CAPCashAppPayState { + public let request: CAPCustomerRequest + + init(request: CAPCustomerRequest) { + self.request = request + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateRedirecting else { + return false + } + return request == otherObject.request + } +} + +/// SDK is redirecting to Cash App for authorization. Show loading indicator if desired. +@objcMembers public final class CAPCashAppPayStatePolling: CAPCashAppPayState { + public let request: CAPCustomerRequest + + init(request: CAPCustomerRequest) { + self.request = request + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStatePolling else { + return false + } + return request == otherObject.request + } +} + +/// SDK is redirecting to Cash App for authorization. Show loading indicator if desired. +@objcMembers public final class CAPCashAppPayStateDeclined: CAPCashAppPayState { + public let request: CAPCustomerRequest + + init(request: CAPCustomerRequest) { + self.request = request + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateDeclined else { + return false + } + return request == otherObject.request + } +} + +/// CustomerRequest was approved. Update UI to show payment info or $cashtag. +@objcMembers public final class CAPCashAppPayStateApproved: CAPCashAppPayState { + public let request: CAPCustomerRequest + public let grants: [CAPCustomerRequestGrant] + + init(request: CAPCustomerRequest, grants: [CAPCustomerRequestGrant]) { + self.request = request + self.grants = grants + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateApproved else { + return false + } + return request == otherObject.request && grants == otherObject.grants + } +} + +/// CustomerRequest is being refreshed as a result of the AuthFlowTriggers expiring. +/// Show loading indicator if desired. +@objcMembers public final class CAPCashAppPayStateRefreshing: CAPCashAppPayState { + public let request: CAPCustomerRequest + + init(request: CAPCustomerRequest) { + self.request = request + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateRefreshing else { + return false + } + return request == otherObject.request + } +} + +/// An error with the Cash App Pay API that can manifest at runtime. +/// If an `APIError` is received, the integration is degraded and Cash App Pay functionality +/// should be temporarily removed from the app's UI. +@objcMembers public final class CAPCashAppPayStateAPIError: CAPCashAppPayState { + public let apiError: APIError + + init(apiError: APIError) { + self.apiError = apiError + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateAPIError else { + return false + } + return apiError == otherObject.apiError + } +} + +/// An error in the integration that should be resolved before shipping to production. +/// Examples include authorization issues, incorrect brand IDs, validation errors, etc. +@objcMembers public final class CAPCashAppPayStateIntegrationError: CAPCashAppPayState { + public let integrationError: IntegrationError + + init(integrationError: IntegrationError) { + self.integrationError = integrationError + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateIntegrationError else { + return false + } + return integrationError == otherObject.integrationError + } +} + +/// A networking error, likely due to poor internet connectivity. +@objcMembers public final class CAPCashAppPayStateNetworkError: CAPCashAppPayState { + public let networkError: NSError + + init(networkError: NSError) { + self.networkError = networkError + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateNetworkError else { + return false + } + return networkError == otherObject.networkError + } +} + +/// An unexpected error. Please report any errors of this kind (and what caused them) to Cash App Developer Support. +@objcMembers public final class CAPCashAppPayStateUnexpectedError: CAPCashAppPayState { + public let unexpectedError: UnexpectedError + + init(unexpectedError: UnexpectedError) { + self.unexpectedError = unexpectedError + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherObject = object as? CAPCashAppPayStateUnexpectedError else { + return false + } + return unexpectedError == otherObject.unexpectedError + } +} + +extension CashAppPayState { + var asCAPCashAppPayState: CAPCashAppPayState { + switch self { + case .notStarted: + return CAPCashAppPayStateNotStarted() + case .creatingCustomerRequest(let createCustomerRequestParams): + return CAPCashAppPayStateCreatingCustomerRequest( + params: .init(createCustomerRequestParams: createCustomerRequestParams) + ) + case .updatingCustomerRequest(let request, let params): + return CAPCashAppPayStateUpdatingCustomerRequest( + request: .init(customerRequest: request), + params: .init(updateCustomerRequestParams: params) + ) + case .readyToAuthorize(let customerRequest): + return CAPCashAppPayStateReadyToAuthorize( + request: .init(customerRequest: customerRequest) + ) + case .redirecting(let customerRequest): + return CAPCashAppPayStateRedirecting( + request: .init(customerRequest: customerRequest) + ) + case .polling(let customerRequest): + return CAPCashAppPayStatePolling( + request: .init(customerRequest: customerRequest) + ) + case .declined(let customerRequest): + return CAPCashAppPayStateDeclined( + request: .init(customerRequest: customerRequest) + ) + case .approved(let request, let grants): + return CAPCashAppPayStateApproved( + request: .init(customerRequest: request), + grants: grants.map(CAPCustomerRequestGrant.init(grant:)) + ) + case .refreshing(let customerRequest): + return CAPCashAppPayStateRefreshing( + request: .init(customerRequest: customerRequest) + ) + case .apiError(let apiError): + return CAPCashAppPayStateAPIError( + apiError: apiError + ) + case .integrationError(let integrationError): + return CAPCashAppPayStateIntegrationError( + integrationError: integrationError + ) + case .networkError(.noResponse): + return CAPCashAppPayStateNetworkError( + networkError: CAPNetworkErrorNoResponse() + ) + case .networkError(.nilData(let response)): + return CAPCashAppPayStateNetworkError( + networkError: CAPNetworkErrorNilData(response: response) + ) + case .networkError(.invalidJSON(let data)): + return CAPCashAppPayStateNetworkError( + networkError: CAPNetworkErrorInvalidJSON(data: data) + ) + case .networkError(.systemError(let error)): + return CAPCashAppPayStateNetworkError( + networkError: error + ) + case .unexpectedError(let unexpectedError): + return CAPCashAppPayStateUnexpectedError( + unexpectedError: unexpectedError + ) + } + } +} diff --git a/Sources/PayKit/ObjcWrapper/CustomerRequest+ObjC.swift b/Sources/PayKit/ObjcWrapper/CustomerRequest+ObjC.swift new file mode 100644 index 0000000..1a8b0b5 --- /dev/null +++ b/Sources/PayKit/ObjcWrapper/CustomerRequest+ObjC.swift @@ -0,0 +1,623 @@ +// +// CustomerRequest+ObjC.swift +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objcMembers public final class CAPCreateCustomerRequestParams: NSObject { + + // MARK: - Properties + + let createCustomerRequestParams: CreateCustomerRequestParams + + // MARK: - Public Properties + + public var actions: [CAPPaymentAction] { + createCustomerRequestParams + .actions + .map(CAPPaymentAction.init(paymentAction:)) + } + + public var channel: String { + createCustomerRequestParams.channel.rawValue + } + + public var redirectURL: URL { + createCustomerRequestParams.redirectURL + } + + public var referenceID: String? { + createCustomerRequestParams.referenceID + } + + public var metadata: [String: String]? { + createCustomerRequestParams.metadata + } + + // MARK: - Init + + init(createCustomerRequestParams: CreateCustomerRequestParams) { + self.createCustomerRequestParams = createCustomerRequestParams + } + + // MARK: - Public Init + + public init( + actions: [CAPPaymentAction], + channel: CAPChannel, + redirectURL: URL, + referenceID: String?, + metadata: [String: String]? + ) { + createCustomerRequestParams = CreateCustomerRequestParams( + actions: actions.map(\.paymentAction), + channel: channel.channel, + redirectURL: redirectURL, + referenceID: referenceID, + metadata: metadata + ) + } + + public init( + actions: [CAPPaymentAction], + redirectURL: URL, + referenceID: String?, + metadata: [String: String]? + ) { + createCustomerRequestParams = CreateCustomerRequestParams( + actions: actions.map(\.paymentAction), + channel: .IN_APP, + redirectURL: redirectURL, + referenceID: referenceID, + metadata: metadata + ) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Equatable + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherCAPCreateCustomerRequestParams = object as? CAPCreateCustomerRequestParams else { + return false + } + return createCustomerRequestParams == otherCAPCreateCustomerRequestParams.createCustomerRequestParams + } +} + +@objcMembers public final class CAPUpdateCustomerRequestParams: NSObject { + + // MARK: - Properties + + let updateCustomerRequestParams: UpdateCustomerRequestParams + + // MARK: - Public Properties + + public var actions: [CAPPaymentAction] { + updateCustomerRequestParams + .actions + .map(CAPPaymentAction.init(paymentAction:)) + } + + public var referenceID: String? { + updateCustomerRequestParams.referenceID + } + + public var metadata: [String: String]? { + updateCustomerRequestParams.metadata + } + + // MARK: - Init + + init(updateCustomerRequestParams: UpdateCustomerRequestParams) { + self.updateCustomerRequestParams = updateCustomerRequestParams + } + + // MARK: - Public Init + + public init( + actions: [CAPPaymentAction], + referenceID: String?, + metadata: [String: String]? + ) { + self.updateCustomerRequestParams = UpdateCustomerRequestParams( + actions: actions.map(\.paymentAction), + referenceID: referenceID, + metadata: metadata + ) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Equatable + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherCAPUpdateCustomerRequestParams = object as? CAPUpdateCustomerRequestParams else { + return false + } + return updateCustomerRequestParams == otherCAPUpdateCustomerRequestParams.updateCustomerRequestParams + } +} + +@objcMembers public final class CAPCustomerRequest: NSObject { + + // MARK: - Properties + + let customerRequest: CustomerRequest + + // MARK: - Public Properties + + public var id: String { + customerRequest.id + } + + public var status: String { + customerRequest.status.rawValue + } + + public var actions: [CAPPaymentAction] { + customerRequest + .actions + .map(CAPPaymentAction.init(paymentAction:)) + } + + public var authFlowTriggers: CAPCustomerRequestAuthFlowTriggers? { + customerRequest + .authFlowTriggers + .map(CAPCustomerRequestAuthFlowTriggers.init(authFlowTriggers:)) + } + + public var redirectURL: URL? { + customerRequest.redirectURL + } + + public var createdAt: Date { + customerRequest.createdAt + } + + public var updatedAt: Date { + customerRequest.updatedAt + } + + public var expiresAt: Date { + customerRequest.expiresAt + } + + public var origin: CAPCustomerRequestOrigin? { + customerRequest + .origin + .map(CAPCustomerRequestOrigin.init(origin:)) + } + + public var channel: String { + customerRequest.channel.rawValue + } + + public var grants: [CAPCustomerRequestGrant]? { + customerRequest.grants?.map(CAPCustomerRequestGrant.init(grant:)) + } + + public var referenceID: String? { + customerRequest.referenceID + } + + public var requesterProfile: CAPCustomerRequestRequesterProfile? { + customerRequest + .requesterProfile + .map(CAPCustomerRequestRequesterProfile.init(requesterProfile:)) + } + + public var customerProfile: CAPCustomerRequestCustomerProfile? { + customerRequest + .customerProfile + .map(CAPCustomerRequestCustomerProfile.init(customerProfile:)) + } + + public var metadata: [String: String]? { + customerRequest.metadata + } + + // MARK: - Init + + init(customerRequest: CustomerRequest) { + self.customerRequest = customerRequest + } + + // MARK: - Equatable + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherCAPCustomerRequest = object as? CAPCustomerRequest else { + return false + } + return customerRequest == otherCAPCustomerRequest.customerRequest + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +@objcMembers public final class CAPCustomerRequestGrant: NSObject { + + // MARK: - Properties + + let grant: CustomerRequest.Grant + + // MARK: - Public Properties + + public var id: String { + grant.id + } + + public var customerID: String { + grant.customerID + } + + public var action: CAPPaymentAction { + CAPPaymentAction(paymentAction: grant.action) + } + + public var status: String { + grant.status.rawValue + } + + public var channel: String { + grant.channel.rawValue + } + + public var createdAt: Date { + grant.createdAt + } + + public var updatedAt: Date { + grant.updatedAt + } + + public var expiresAt: Date? { + grant.expiresAt + } + + // MARK: - Init + + init(grant: CustomerRequest.Grant) { + self.grant = grant + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Equatable + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherGrant = object as? CAPCustomerRequestGrant else { + return false + } + return grant == otherGrant.grant + } +} + +@objcMembers public final class CAPCustomerRequestCustomerProfile: NSObject { + + // MARK: - Properties + + let customerProfile: CustomerRequest.CustomerProfile + + // MARK: - Public Properties + + public var id: String { + customerProfile.id + } + + public var cashtag: String { + customerProfile.cashtag + } + + // MARK: - Init + + init(customerProfile: CustomerRequest.CustomerProfile) { + self.customerProfile = customerProfile + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Equatable + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherCustomerProfile = object as? CAPCustomerRequestCustomerProfile else { + return false + } + return customerProfile == otherCustomerProfile.customerProfile + } +} + +@objcMembers public final class CAPCustomerRequestRequesterProfile: NSObject { + + // MARK: - Properties + + let requesterProfile: CustomerRequest.RequesterProfile + + // MARK: - Public Properties + + public var name: String { + requesterProfile.name + } + + public var logoURL: URL { + requesterProfile.logoURL + } + + // MARK: - init + + init(requesterProfile: CustomerRequest.RequesterProfile) { + self.requesterProfile = requesterProfile + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Equatable + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherRequestRequesterProfile = object as? CAPCustomerRequestRequesterProfile else { + return false + } + return requesterProfile == otherRequestRequesterProfile.requesterProfile + } +} + +@objcMembers public final class CAPCustomerRequestOrigin: NSObject { + + // MARK: - Properties + + let origin: CustomerRequest.Origin + + // MARK: - Public Properties + + public var type: String { + origin.type.rawValue + } + + public var id: String? { + origin.id + } + + // MARK: - Init + + init(origin: CustomerRequest.Origin) { + self.origin = origin + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Equatable + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherOrigin = object as? CAPCustomerRequestOrigin else { + return false + } + return origin == otherOrigin.origin + } +} + +@objcMembers public final class CAPCustomerRequestAuthFlowTriggers: NSObject { + + // MARK: - Properties + + let authFlowTriggers: CustomerRequest.AuthFlowTriggers + + // MARK: - Public Properties + + public var qrCodeImageURL: URL { + authFlowTriggers.qrCodeImageURL + } + + public var qrCodeSVGURL: URL { + authFlowTriggers.qrCodeSVGURL + } + + public var mobileURL: URL { + authFlowTriggers.mobileURL + } + + public var refreshesAt: Date { + authFlowTriggers.refreshesAt + } + + // MARK: - Init + + init(authFlowTriggers: CustomerRequest.AuthFlowTriggers) { + self.authFlowTriggers = authFlowTriggers + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Equatable + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherAuthFlowTriggers = object as? CAPCustomerRequestAuthFlowTriggers else { + return false + } + return authFlowTriggers == otherAuthFlowTriggers.authFlowTriggers + } +} + +// MARK: - PaymentAction + +@objcMembers public final class CAPPaymentAction: NSObject { + + // MARK: - Private Properties + + let paymentAction: PaymentAction + + // MARK: - Public Properties + + public var type: String { + paymentAction.type.rawValue + } + + public var scopeID: String { + paymentAction.scopeID + } + + public var money: CAPMoney? { + paymentAction.money?.capMoney + } + + public var accountReferenceID: String? { + paymentAction.accountReferenceID + } + + // MARK: - Init + + init(paymentAction: PaymentAction) { + self.paymentAction = paymentAction + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public Static Methods + + public static func oneTimePayment(scopeID: String, money: CAPMoney?) -> CAPPaymentAction { + let paymentAction = PaymentAction.oneTimePayment(scopeID: scopeID, money: money?.money) + return CAPPaymentAction(paymentAction: paymentAction) + } + + public static func onFilePayment(scopeID: String, accountReferenceID: String?) -> CAPPaymentAction { + let paymentAction = PaymentAction.onFilePayment(scopeID: scopeID, accountReferenceID: accountReferenceID) + return CAPPaymentAction(paymentAction: paymentAction) + } + + // MARK: - Equatable + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherPaymentAction = object as? CAPPaymentAction else { + return false + } + return paymentAction == otherPaymentAction.paymentAction + } +} + +// MARK: - Money + +@objcMembers public final class CAPMoney: NSObject { + + // MARK: - Properties + + let money: Money + + // MARK: - Public Properties + + public var amount: UInt { + money.amount + } + + public var currency: CAPCurrency { + money.currency.capCurrency + } + + // MARK: - Init + + init(money: Money) { + self.money = money + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public init(amount: UInt, currency: CAPCurrency) { + self.money = Money(amount: amount, currency: currency.currency) + } + + // MARK: - Equatable + + public override func isEqual(_ object: Any?) -> Bool { + guard let otherMoney = object as? CAPMoney else { + return false + } + return money == otherMoney.money + } +} + +private extension Money { + var capMoney: CAPMoney { + CAPMoney(amount: amount, currency: currency.capCurrency) + } +} + +// MARK: - Currency + +@objc public enum CAPCurrency: Int { + case USD + + var currency: Currency { + switch self { + case .USD: return .USD + } + } +} + +extension Currency { + var capCurrency: CAPCurrency { + switch self { + case .USD: return .USD + } + } +} + +// MARK: - Channel + +@objc public enum CAPChannel: Int { + /// The customer is redirected to Cash App by a mobile application. + /// Use this channel for native apps on a customer's device. + case IN_APP + /// The customer presents or scans a QR code at a physical location to approve the request. + case IN_PERSON + /// The customer scans a QR code or is redirected to Cash App by a website. + /// Not recommended for mobile applications. + case ONLINE + + var channel: Channel { + switch self { + case .IN_APP: return .IN_APP + case .IN_PERSON: return .IN_PERSON + case .ONLINE: return .ONLINE + } + } +} diff --git a/Sources/PayKit/ObjcWrapper/Errors+ObjC.swift b/Sources/PayKit/ObjcWrapper/Errors+ObjC.swift new file mode 100644 index 0000000..50f9a4b --- /dev/null +++ b/Sources/PayKit/ObjcWrapper/Errors+ObjC.swift @@ -0,0 +1,300 @@ +// +// Errors+ObjCTests.swift +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: - APIError + +@objcMembers public final class CAPApiError: NSError { + + // MARK: - Properties + + let apiError: APIError + + // MARK: - Public Properties + + public var category: CAPApiErrorCategory { + apiError.category.capApiErrorCategory + } + + public override var code: Int { + apiError.code.capApiErrorCode.rawValue + } + + public var detail: String? { + apiError.detail + } + + public var field: String? { + apiError.field + } + + // MARK: - Init + + init(apiError: APIError) { + self.apiError = apiError + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension APIError.Category { + var capApiErrorCategory: CAPApiErrorCategory { + switch self { + case .API_ERROR: return .API_ERROR + } + } +} + +@objc public enum CAPApiErrorCategory: Int { + case API_ERROR +} + +extension APIError.ErrorCode { + var capApiErrorCode: CAPApiErrorCode { + switch self { + case .INTERNAL_SERVER_ERROR: + return .INTERNAL_SERVER_ERROR + case .SERVICE_UNAVAILABLE: + return .SERVICE_UNAVAILABLE + case .GATEWAY_TIMEOUT: + return .GATEWAY_TIMEOUT + } + } +} + +@objc public enum CAPApiErrorCode: Int { + case INTERNAL_SERVER_ERROR + case SERVICE_UNAVAILABLE + case GATEWAY_TIMEOUT +} + +// MARK: - IntegrationError + +@objcMembers public final class CAPIntegrationError: NSError { + + // MARK: - Properties + + let integrationError: IntegrationError + + // MARK: - Public Properties + + public var category: CAPIntegrationErrorCategory { + integrationError.category.capIntegrationErrorCategory + } + + public override var code: Int { + integrationError.code.capIntegrationErrorCode.rawValue + } + + public var detail: String? { + integrationError.detail + } + public var field: String? { + integrationError.field + } + + // MARK: - Init + + init(integrationError: IntegrationError) { + self.integrationError = integrationError + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension IntegrationError.Category { + var capIntegrationErrorCategory: CAPIntegrationErrorCategory { + switch self { + case .AUTHENTICATION_ERROR: + return .AUTHENTICATION_ERROR + case .BRAND_ERROR: + return .BRAND_ERROR + case .MERCHANT_ERROR: + return .MERCHANT_ERROR + case .INVALID_REQUEST_ERROR: + return .INVALID_REQUEST_ERROR + case .RATE_LIMIT_ERROR: + return .RATE_LIMIT_ERROR + } + } +} + +@objc public enum CAPIntegrationErrorCategory: Int { + case AUTHENTICATION_ERROR + case BRAND_ERROR + case MERCHANT_ERROR + case INVALID_REQUEST_ERROR + case RATE_LIMIT_ERROR +} + +extension IntegrationError.ErrorCode { + var capIntegrationErrorCode: CAPIntegrationErrorCode { + switch self { + case .UNAUTHORIZED: + return .UNAUTHORIZED + case .CLIENT_DISABLED: + return .CLIENT_DISABLED + case .FORBIDDEN: + return .FORBIDDEN + case .VALUE_TOO_LONG: + return .VALUE_TOO_LONG + case .VALUE_TOO_SHORT: + return .VALUE_TOO_SHORT + case .VALUE_EMPTY: + return .VALUE_EMPTY + case .VALUE_REGEX_MISMATCH: + return .VALUE_REGEX_MISMATCH + case .INVALID_URL: + return .INVALID_URL + case .VALUE_TOO_HIGH: + return .VALUE_TOO_HIGH + case .VALUE_TOO_LOW: + return .VALUE_TOO_LOW + case .ARRAY_LENGTH_TOO_LONG: + return .ARRAY_LENGTH_TOO_LONG + case .ARRAY_LENGTH_TOO_SHORT: + return .ARRAY_LENGTH_TOO_SHORT + case .INVALID_ARRAY_TYPE: + return .INVALID_ARRAY_TYPE + case .NOT_FOUND: + return .NOT_FOUND + case .CONFLICT: + return .CONFLICT + case .INVALID_STATE_TRANSITION: + return .INVALID_STATE_TRANSITION + case .CLIENT_NOT_FOUND: + return .CLIENT_NOT_FOUND + case .RATE_LIMITED: + return .RATE_LIMITED + case .BRAND_NOT_FOUND: + return .BRAND_NOT_FOUND + case .MERCHANT_MISSING_ADDRESS_OR_SITE: + return .MERCHANT_MISSING_ADDRESS_OR_SITE + } + } +} + +@objc public enum CAPIntegrationErrorCode: Int { + // Authentication Errors + case UNAUTHORIZED + case CLIENT_DISABLED + case FORBIDDEN + + // Invalid Request Errors + case VALUE_TOO_LONG + case VALUE_TOO_SHORT + case VALUE_EMPTY + case VALUE_REGEX_MISMATCH + case INVALID_URL + case VALUE_TOO_HIGH + case VALUE_TOO_LOW + case ARRAY_LENGTH_TOO_LONG + case ARRAY_LENGTH_TOO_SHORT + case INVALID_ARRAY_TYPE + case NOT_FOUND + case CONFLICT + case INVALID_STATE_TRANSITION + case CLIENT_NOT_FOUND + + // Rate Limit Errors + case RATE_LIMITED + + // Brand Errors + case BRAND_NOT_FOUND + + // Merchant Errors + case MERCHANT_MISSING_ADDRESS_OR_SITE +} + +// MARK: - UnexpectedError + +@objcMembers public final class CAPUnexpectedError: NSError { + + // MARK: - Properties + + let unexpectedError: UnexpectedError + + // MARK: - Public Properties + + public var category: String { + unexpectedError.category + } + + public var codeMessage: String { + unexpectedError.code + } + + public var detail: String? { + unexpectedError.detail + } + + public var field: String? { + unexpectedError.field + } + + // MARK: - Init + + init(unexpectedError: UnexpectedError) { + self.unexpectedError = unexpectedError + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Network Error + +@objc public final class CAPNetworkErrorNoResponse: NSError {} + +@objc public final class CAPNetworkErrorNilData: NSError { + @objc public var response: HTTPURLResponse + + init(response: HTTPURLResponse) { + self.response = response + super.init(domain: "Missing response body", code: NSURLErrorCannotParseResponse) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +@objc public final class CAPNetworkErrorInvalidJSON: NSError { + @objc public var data: Data + + init(data: Data) { + self.data = data + super.init(domain: "Invalid JSON", code: -1) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Sources/PayKit/ObjcWrapper/ObjCWrapper.swift b/Sources/PayKit/ObjcWrapper/ObjCWrapper.swift new file mode 100644 index 0000000..7e3a929 --- /dev/null +++ b/Sources/PayKit/ObjcWrapper/ObjCWrapper.swift @@ -0,0 +1,179 @@ +// +// ObjCWrapper.swift +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc public protocol CAPCashAppPayObserver: NSObjectProtocol { + func stateDidChange(to state: CAPCashAppPayState) +} + +@objc(CAPCashAppPay) +public final class ObjCWrapper: NSObject { + @objc public static var sdkVersion: String { + CashAppPay.version + } + + @objc public static var RedirectNotification: Notification.Name { + CashAppPay.RedirectNotification + } + + private let cashAppPay: CashAppPay + private var observations = [ObjectIdentifier: Observation]() + + @objc public var endpoint: CAPEndpoint { + cashAppPay.endpoint.capEndpoint + } + + @objc public convenience init(clientID: String, endpoint: CAPEndpoint = .production) { + let cashAppPay = CashAppPay( + clientID: clientID, + endpoint: endpoint.endpoint + ) + self.init(cashAppPay: cashAppPay) + } + + init(cashAppPay: CashAppPay) { + self.cashAppPay = cashAppPay + super.init() + cashAppPay.addObserver(self) + } + + @objc public func retrieveCustomerRequest( + id: String, + completion: @escaping (CAPCustomerRequest?, Error?) -> Void + ) { + cashAppPay.retrieveCustomerRequest(id: id) { [weak self] result in + guard let self else { return } + switch result { + case .success(let customerRequest): + let capCustomerRequest = CAPCustomerRequest(customerRequest: customerRequest) + completion(capCustomerRequest, nil) + case .failure(let error): + let objcError = self.mapErrorToObjC(error) + completion(nil, objcError) + } + } + } + + @objc public func createCustomerRequest( + params: CAPCreateCustomerRequestParams + ) { + cashAppPay.createCustomerRequest(params: params.createCustomerRequestParams) + } + + @objc public func updateCustomerRequest( + _ request: CAPCustomerRequest, + with params: CAPUpdateCustomerRequestParams + ) { + cashAppPay.updateCustomerRequest( + request.customerRequest, + with: params.updateCustomerRequestParams + ) + } + + @objc public func authorizeCustomerRequest( + _ request: CAPCustomerRequest + ) { + cashAppPay.authorizeCustomerRequest(request.customerRequest) + } +} + +// MARK: - Errors + +extension ObjCWrapper { + func mapErrorToObjC(_ error: Error) -> Error { + switch error { + case let apiError as APIError: + return CAPApiError(apiError: apiError) + case let integrationError as IntegrationError: + return CAPIntegrationError(integrationError: integrationError) + case let unexpectedError as UnexpectedError: + return CAPUnexpectedError(unexpectedError: unexpectedError) + case let networkError as NetworkError: + switch networkError { + case .noResponse: + return CAPNetworkErrorNoResponse() + case .nilData(let response): + return CAPNetworkErrorNilData(response: response) + case .invalidJSON(let json): + return CAPNetworkErrorInvalidJSON(data: json) + case .systemError(let error): + return error + } + default: + return error + } + } +} + +// MARK: - Observations + +extension ObjCWrapper { + private struct Observation { + weak var observer: CAPCashAppPayObserver? + } + + @objc public func addObserver(_ observer: CAPCashAppPayObserver) { + let id = ObjectIdentifier(observer) + observations[id] = Observation(observer: observer) + } + + @objc func removeObserver(_ observer: CAPCashAppPayObserver) { + let id = ObjectIdentifier(observer) + observations.removeValue(forKey: id) + } +} + +// MARK: - CashAppPayObserver + +extension ObjCWrapper: CashAppPayObserver { + public func stateDidChange(to state: CashAppPayState) { + for (id, observation) in observations { + // Clean up any observer that is no longer in memory + guard let observer = observation.observer else { + observations.removeValue(forKey: id) + continue + } + observer.stateDidChange(to: state.asCAPCashAppPayState) + } + } +} + +// MARK: - CAPEndpoint + +@objc public enum CAPEndpoint: Int { + case production + case sandbox + case staging + + var endpoint: CashAppPay.Endpoint { + switch self { + case .production: return .production + case .sandbox: return .sandbox + case .staging: return .staging + } + } +} + +private extension CashAppPay.Endpoint { + var capEndpoint: CAPEndpoint { + switch self { + case .production: return .production + case .sandbox: return .sandbox + case .staging: return .staging + } + } +} diff --git a/Sources/PayKit/Services/Analytics/JSONEncoder+Analytics.swift b/Sources/PayKit/Services/Analytics/JSONEncoder+Analytics.swift index e59d909..d3244ec 100644 --- a/Sources/PayKit/Services/Analytics/JSONEncoder+Analytics.swift +++ b/Sources/PayKit/Services/Analytics/JSONEncoder+Analytics.swift @@ -23,6 +23,7 @@ extension JSONEncoder { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase encoder.dateEncodingStrategy = .microsecondsSince1970 + encoder.outputFormatting = .sortedKeys return encoder } } diff --git a/Tests/PayKitTests/AnalyticsEventTests.swift b/Tests/PayKitTests/AnalyticsEventTests.swift index bcf81c5..8e69778 100644 --- a/Tests/PayKitTests/AnalyticsEventTests.swift +++ b/Tests/PayKitTests/AnalyticsEventTests.swift @@ -76,7 +76,7 @@ class AnalyticsEventTests: XCTestCase { ]) XCTAssertEqual(sortedFieldValues(event.fields), [ "create", - "[\"{\"type\":\"ON_FILE_PAYMENT\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"account_reference_id\":\"FILTERED\"}\"]", + "[\"{\"account_reference_id\":\"FILTERED\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"}\"]", "IN_APP", "{\"key1\":\"Valuation\",\"key2\":\"ValuWorld\",\"key3\":\"Valuminous\"}", "FILTERED", @@ -95,7 +95,7 @@ class AnalyticsEventTests: XCTestCase { ]) XCTAssertEqual(sortedFieldValues(event.fields), [ "update", - "[\"{\"type\":\"ON_FILE_PAYMENT\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"account_reference_id\":\"FILTERED\"}\"]", + "[\"{\"account_reference_id\":\"FILTERED\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"}\"]", "https://sandbox.api.cash.app/customer-request/v1/requests/GRR_mg3saamyqdm29jj9pqjqkedm/interstitial", "IN_APP", "1666296978051000", @@ -106,7 +106,7 @@ class AnalyticsEventTests: XCTestCase { "FILTERED", "SDK Hacking: The Brand", "PENDING", - "[\"{\"type\":\"ON_FILE_PAYMENT\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"account_reference_id\":null}\"]", + "[\"{\"account_reference_id\":null,\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"}\"]", "1666296978051000", ]) @@ -117,7 +117,7 @@ class AnalyticsEventTests: XCTestCase { XCTAssertEqual(event.fields.keys.sorted(), expectedKeys) XCTAssertEqual(sortedFieldValues(event.fields), [ "ready_to_authorize", - "[\"{\"type\":\"ON_FILE_PAYMENT\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"account_reference_id\":\"FILTERED\"}\"]", + "[\"{\"account_reference_id\":\"FILTERED\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"}\"]", "https://sandbox.api.cash.app/customer-request/v1/requests/GRR_mg3saamyqdm29jj9pqjqkedm/interstitial", "IN_APP", "1666296978051000", @@ -137,7 +137,7 @@ class AnalyticsEventTests: XCTestCase { XCTAssertEqual(event.fields.keys.sorted(), expectedKeys) XCTAssertEqual(sortedFieldValues(event.fields), [ "redirect", - "[\"{\"type\":\"ON_FILE_PAYMENT\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"account_reference_id\":\"FILTERED\"}\"]", + "[\"{\"account_reference_id\":\"FILTERED\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"}\"]", "https://sandbox.api.cash.app/customer-request/v1/requests/GRR_mg3saamyqdm29jj9pqjqkedm/interstitial", "IN_APP", "1666296978051000", @@ -157,7 +157,7 @@ class AnalyticsEventTests: XCTestCase { XCTAssertEqual(event.fields.keys.sorted(), expectedKeys) XCTAssertEqual(sortedFieldValues(event.fields), [ "polling", - "[\"{\"type\":\"ON_FILE_PAYMENT\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"account_reference_id\":\"FILTERED\"}\"]", + "[\"{\"account_reference_id\":\"FILTERED\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"}\"]", "https://sandbox.api.cash.app/customer-request/v1/requests/GRR_mg3saamyqdm29jj9pqjqkedm/interstitial", "IN_APP", "1666296978051000", @@ -177,7 +177,7 @@ class AnalyticsEventTests: XCTestCase { XCTAssertEqual(event.fields.keys.sorted(), expectedKeys) XCTAssertEqual(sortedFieldValues(event.fields), [ "declined", - "[\"{\"type\":\"ON_FILE_PAYMENT\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"account_reference_id\":\"FILTERED\"}\"]", + "[\"{\"account_reference_id\":\"FILTERED\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"}\"]", "https://sandbox.api.cash.app/customer-request/v1/requests/GRR_mg3saamyqdm29jj9pqjqkedm/interstitial", "IN_APP", "1666296978051000", @@ -212,8 +212,8 @@ class AnalyticsEventTests: XCTestCase { ]) XCTAssertEqual(sortedFieldValues(event.fields), [ "approved", - "[\"{\"type\":\"ON_FILE_PAYMENT\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"account_reference_id\":\"FILTERED\"}\"]", - "[\"{\"status\":\"ACTIVE\",\"channel\":\"IN_APP\",\"action\":{\"type\":\"ON_FILE_PAYMENT\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"account_reference_id\":\"FILTERED\"},\"id\":\"GRG_AZYyHv2DwQltw0SiCLTaRb73y40XFe2dWM690WDF9Btqn-uTCYAUROa4ciwCdDnZcG4PuY1m_i3gwHODiO8DSf9zdMmRl1T0SM267vzuldnBs246-duHZhcehhXtmhfU8g\",\"created_at\":1666299823249000,\"expires_at\":1823979823159000,\"type\":\"EXTENDED\",\"customer_id\":\"CST_AYVkuLw-sT3OKZ7a_nhNTC_L2ekahLgGrS-EM_QhW4OTrGMbi59X1eCclH0cjaxoLObc\",\"updated_at\":1666299823249000}\"]", + "[\"{\"account_reference_id\":\"FILTERED\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"}\"]", + "[\"{\"action\":{\"account_reference_id\":\"FILTERED\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"},\"channel\":\"IN_APP\",\"created_at\":1666299823249000,\"customer_id\":\"CST_AYVkuLw-sT3OKZ7a_nhNTC_L2ekahLgGrS-EM_QhW4OTrGMbi59X1eCclH0cjaxoLObc\",\"expires_at\":1823979823159000,\"id\":\"GRG_AZYyHv2DwQltw0SiCLTaRb73y40XFe2dWM690WDF9Btqn-uTCYAUROa4ciwCdDnZcG4PuY1m_i3gwHODiO8DSf9zdMmRl1T0SM267vzuldnBs246-duHZhcehhXtmhfU8g\",\"status\":\"ACTIVE\",\"type\":\"EXTENDED\",\"updated_at\":1666299823249000}\"]", "https://sandbox.api.cash.app/customer-request/v1/requests/GRR_mg3saamyqdm29jj9pqjqkedm/interstitial", "IN_APP", "1666296978051000", diff --git a/Tests/PayKitTests/CashAppPayState+ObjCTests.swift b/Tests/PayKitTests/CashAppPayState+ObjCTests.swift new file mode 100644 index 0000000..ceea8b5 --- /dev/null +++ b/Tests/PayKitTests/CashAppPayState+ObjCTests.swift @@ -0,0 +1,316 @@ +// +// CashAppPayState+ObjCTests.swift +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@testable import PayKit +import XCTest + +final class CashAppPayState_ObjCTests: XCTestCase { + func test_notStarted() { + let state = CashAppPayState.notStarted.asCAPCashAppPayState + XCTAssert(state is CAPCashAppPayStateNotStarted) + } + + func test_notStarted_isEqual() { + let state = CAPCashAppPayStateNotStarted() + XCTAssert(state.isEqual(CAPCashAppPayStateNotStarted())) + } + + func test_creatingCustomerRequest() throws { + let state = CashAppPayState + .creatingCustomerRequest(TestValues.createCustomerRequestParams) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateCreatingCustomerRequest) + XCTAssertEqual( + unwrapped.params.createCustomerRequestParams, + TestValues.createCustomerRequestParams + ) + } + + func test_creatingCustomerRequest_isEqual() { + let state = CAPCashAppPayStateCreatingCustomerRequest( + params: .init(createCustomerRequestParams: TestValues.createCustomerRequestParams) + ) + let otherState = CAPCashAppPayStateCreatingCustomerRequest( + params: .init(createCustomerRequestParams: TestValues.createCustomerRequestParams) + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_updatingCustomerRequest() throws { + let state = CashAppPayState + .updatingCustomerRequest( + request: TestValues.customerRequest, + params: TestValues.updateCustomerRequestParams + ) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateUpdatingCustomerRequest) + XCTAssertEqual( + unwrapped.request.customerRequest, + TestValues.customerRequest + ) + XCTAssertEqual( + unwrapped.params.updateCustomerRequestParams, + TestValues.updateCustomerRequestParams + ) + } + + func test_updatingCustomerRequest_isEqual() { + let state = CAPCashAppPayStateUpdatingCustomerRequest( + request: .init(customerRequest: TestValues.customerRequest), + params: .init(updateCustomerRequestParams: TestValues.updateCustomerRequestParams) + ) + let otherState = CAPCashAppPayStateUpdatingCustomerRequest( + request: .init(customerRequest: TestValues.customerRequest), + params: .init(updateCustomerRequestParams: TestValues.updateCustomerRequestParams) + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_readyToAuthorize() throws { + let state = CashAppPayState + .readyToAuthorize(TestValues.customerRequest) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateReadyToAuthorize) + XCTAssertEqual( + unwrapped.request.customerRequest, + TestValues.customerRequest + ) + } + + func test_readyToAuthorize_isEqual() { + let state = CAPCashAppPayStateReadyToAuthorize( + request: .init(customerRequest: TestValues.customerRequest) + ) + let otherState = CAPCashAppPayStateReadyToAuthorize( + request: .init(customerRequest: TestValues.customerRequest) + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_redirecting() throws { + let state = CashAppPayState + .redirecting(TestValues.customerRequest) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateRedirecting) + XCTAssertEqual( + unwrapped.request.customerRequest, + TestValues.customerRequest + ) + } + + func test_redirecting_isEqual() throws { + let state = CAPCashAppPayStateRedirecting( + request: .init(customerRequest: TestValues.customerRequest) + ) + let otherState = CAPCashAppPayStateRedirecting( + request: .init(customerRequest: TestValues.customerRequest) + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_polling() throws { + let state = CashAppPayState + .polling(TestValues.customerRequest) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStatePolling) + XCTAssertEqual( + unwrapped.request.customerRequest, + TestValues.customerRequest + ) + } + + func test_polling_isEqual() throws { + let state = CAPCashAppPayStatePolling( + request: .init(customerRequest: TestValues.customerRequest) + ) + let otherState = CAPCashAppPayStatePolling( + request: .init(customerRequest: TestValues.customerRequest) + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_declined() throws { + let state = CashAppPayState + .declined(TestValues.customerRequest) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateDeclined) + XCTAssertEqual( + unwrapped.request.customerRequest, + TestValues.customerRequest + ) + } + + func test_declined_isEqual() throws { + let state = CAPCashAppPayStateDeclined( + request: .init(customerRequest: TestValues.customerRequest) + ) + let otherState = CAPCashAppPayStateDeclined( + request: .init(customerRequest: TestValues.customerRequest) + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_approved() throws { + let state = CashAppPayState + .approved(request: TestValues.customerRequest, grants: TestValues.approvedRequestGrants) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateApproved) + XCTAssertEqual( + unwrapped.request.customerRequest, + TestValues.customerRequest + ) + XCTAssertEqual( + unwrapped.grants.map(\.grant), + TestValues.approvedRequestGrants + ) + } + + func test_approved_isEqual() throws { + let state = CAPCashAppPayStateApproved( + request: .init(customerRequest: TestValues.customerRequest), + grants: TestValues.approvedRequestGrants.map(CAPCustomerRequestGrant.init(grant:)) + ) + let otherState = CAPCashAppPayStateApproved( + request: .init(customerRequest: TestValues.customerRequest), + grants: TestValues.approvedRequestGrants.map(CAPCustomerRequestGrant.init(grant:)) + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_refreshing() throws { + let state = CashAppPayState + .refreshing(TestValues.customerRequest) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateRefreshing) + XCTAssertEqual( + unwrapped.request.customerRequest, + TestValues.customerRequest + ) + } + + func test_refreshing_isEqual() throws { + let state = CAPCashAppPayStateRefreshing( + request: .init(customerRequest: TestValues.customerRequest) + ) + let otherState = CAPCashAppPayStateRefreshing( + request: .init(customerRequest: TestValues.customerRequest) + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_apiError() throws { + let state = CashAppPayState + .apiError(TestValues.internalServerError) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateAPIError) + XCTAssertEqual( + unwrapped.apiError, + TestValues.internalServerError + ) + } + + func test_apiError_isEqual() throws { + let state = CAPCashAppPayStateAPIError( + apiError: TestValues.internalServerError + ) + let otherState = CAPCashAppPayStateAPIError( + apiError: TestValues.internalServerError + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_integrationError() throws { + let state = CashAppPayState + .integrationError(TestValues.brandNotFoundError) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateIntegrationError) + XCTAssertEqual( + unwrapped.integrationError, + TestValues.brandNotFoundError + ) + } + + func test_integrationError_isEqual() throws { + let state = CAPCashAppPayStateIntegrationError( + integrationError: TestValues.unauthorizedError + ) + let otherState = CAPCashAppPayStateIntegrationError( + integrationError: TestValues.unauthorizedError + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_networkError_noResponse() throws { + let state = CashAppPayState + .networkError(TestValues.noResponseError) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateNetworkError) + XCTAssert(unwrapped.networkError is CAPNetworkErrorNoResponse) + } + + func test_networkError_nilData() throws { + let state = CashAppPayState + .networkError(TestValues.nilDataError) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateNetworkError) + XCTAssert(unwrapped.networkError is CAPNetworkErrorNilData) + } + + func test_networkError_invalidJSON() throws { + let state = CashAppPayState + .networkError(TestValues.invalidJSONError) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateNetworkError) + XCTAssert(unwrapped.networkError is CAPNetworkErrorInvalidJSON) + } + + func test_networkError_systemError() throws { + let error = NSError() + let state = CashAppPayState + .networkError(TestValues.systemError(underlyingError: error)) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateNetworkError) + XCTAssert(unwrapped.networkError === error) + } + + func test_networkError_isEqual() throws { + let state = CAPCashAppPayStateNetworkError( + networkError: NSError(domain: "domain", code: 1) + ) + let otherState = CAPCashAppPayStateNetworkError( + networkError: NSError(domain: "domain", code: 1) + ) + XCTAssert(state.isEqual(otherState)) + } + + func test_unexpectedError() throws { + let state = CashAppPayState + .unexpectedError(.emptyErrorArray) + .asCAPCashAppPayState + let unwrapped = try XCTUnwrap(state as? CAPCashAppPayStateUnexpectedError) + XCTAssertEqual(unwrapped.unexpectedError, .emptyErrorArray) + } + + func test_unexpectedError_isEqual() throws { + let state = CAPCashAppPayStateUnexpectedError( + unexpectedError: .emptyErrorArray + ) + let otherState = CAPCashAppPayStateUnexpectedError( + unexpectedError: .emptyErrorArray + ) + XCTAssert(state.isEqual(otherState)) + } +} diff --git a/Tests/PayKitTests/CreateCustomerRequestParamsTests.swift b/Tests/PayKitTests/CreateCustomerRequestParamsTests.swift index e8232fc..3b35d87 100644 --- a/Tests/PayKitTests/CreateCustomerRequestParamsTests.swift +++ b/Tests/PayKitTests/CreateCustomerRequestParamsTests.swift @@ -44,8 +44,8 @@ class CreateCustomerRequestParamsTests: XCTestCase { XCTAssertNotNil(serializedDict["request"]) XCTAssertNotNil(fixtureDict["request"]) XCTAssertEqual( - try XCTUnwrap(JSONSerialization.data(withJSONObject: serializedDict["request"]!)), - try XCTUnwrap(JSONSerialization.data(withJSONObject: fixtureDict["request"]!)) + try XCTUnwrap(JSONSerialization.data(withJSONObject: serializedDict["request"]!, options: .sortedKeys)), + try XCTUnwrap(JSONSerialization.data(withJSONObject: fixtureDict["request"]!, options: .sortedKeys)) ) } diff --git a/Tests/PayKitTests/CustomerRequest+ObjCTests.swift b/Tests/PayKitTests/CustomerRequest+ObjCTests.swift new file mode 100644 index 0000000..3f0544b --- /dev/null +++ b/Tests/PayKitTests/CustomerRequest+ObjCTests.swift @@ -0,0 +1,343 @@ +// +// CustomerRequest+ObjCTests.swift +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@testable import PayKit +import XCTest + +final class CustomerRequest_ObjCTests: XCTestCase { + + // MARK: - CAPCreateCustomerRequestParams + + func test_CAPCreateCustomerRequestParams_init_withCreateCustomerRequestParams() { + let params = CreateCustomerRequestParams( + actions: [oneTimePayment], + redirectURL: redirectURL, + referenceID: refrenceID, + metadata: metadata + ) + + let capParams = CAPCreateCustomerRequestParams(createCustomerRequestParams: params) + XCTAssertEqual(capParams.actions.map(\.paymentAction), [oneTimePayment]) + XCTAssertEqual(capParams.channel, "IN_APP") + XCTAssertEqual(capParams.redirectURL, redirectURL) + XCTAssertEqual(capParams.referenceID, refrenceID) + XCTAssertEqual(capParams.metadata, metadata) + } + + func test_CAPCreateCustomerRequestParams_initWithChannel() { + let params = CAPCreateCustomerRequestParams( + actions: [CAPPaymentAction(paymentAction: oneTimePayment)], + channel: .IN_PERSON, + redirectURL: redirectURL, + referenceID: refrenceID, + metadata: metadata + ) + + XCTAssertEqual(params.actions.map(\.paymentAction), [oneTimePayment]) + XCTAssertEqual(params.channel, "IN_PERSON") + XCTAssertEqual(params.redirectURL, redirectURL) + XCTAssertEqual(params.referenceID, refrenceID) + XCTAssertEqual(params.metadata, metadata) + } + + func test_CAPCreateCustomerRequestParams_init() { + let capParams = CAPCreateCustomerRequestParams( + actions: [CAPPaymentAction(paymentAction: oneTimePayment)], + redirectURL: redirectURL, + referenceID: refrenceID, + metadata: metadata + ) + + XCTAssertEqual(capParams.actions.map(\.paymentAction), [oneTimePayment]) + XCTAssertEqual(capParams.channel, "IN_APP") + XCTAssertEqual(capParams.redirectURL, redirectURL) + XCTAssertEqual(capParams.referenceID, refrenceID) + XCTAssertEqual(capParams.metadata, metadata) + } + + func test_CAPCreateCustomerRequestParams_isEqual() { + let capParams = CAPCreateCustomerRequestParams( + actions: [CAPPaymentAction(paymentAction: oneTimePayment)], + redirectURL: redirectURL, + referenceID: refrenceID, + metadata: metadata + ) + + let otherParams = CAPCreateCustomerRequestParams( + actions: [CAPPaymentAction(paymentAction: oneTimePayment)], + redirectURL: redirectURL, + referenceID: refrenceID, + metadata: metadata + ) + + XCTAssert(capParams.isEqual(otherParams)) + + let nonEqual = CAPCreateCustomerRequestParams( + actions: [], + redirectURL: redirectURL, + referenceID: nil, + metadata: nil + ) + XCTAssertFalse(capParams.isEqual(nonEqual)) + } + + // MARK: - CAPUpdateCustomerRequestParams + + func test_CAPUpdateCustomerRequestParams_init_withUpdateCustomerRequestParams() { + let params = UpdateCustomerRequestParams( + actions: [oneTimePayment], + referenceID: refrenceID, + metadata: metadata + ) + + let capParams = CAPUpdateCustomerRequestParams(updateCustomerRequestParams: params) + XCTAssertEqual(capParams.actions.map(\.paymentAction), [clearingOneTimePayment]) + XCTAssertEqual(capParams.referenceID, refrenceID) + XCTAssertEqual(capParams.metadata, metadata) + } + + func test_CAPUpdateCustomerRequestParams_init() { + let params = CAPUpdateCustomerRequestParams( + actions: [CAPPaymentAction(paymentAction: oneTimePayment)], + referenceID: refrenceID, + metadata: metadata + ) + + XCTAssertEqual(params.actions.map(\.paymentAction), [clearingOneTimePayment]) + XCTAssertEqual(params.referenceID, refrenceID) + XCTAssertEqual(params.metadata, metadata) + } + + func test_CAPUpdateCustomerRequestParams_isEqual() { + let capParams = CAPUpdateCustomerRequestParams( + actions: [CAPPaymentAction(paymentAction: oneTimePayment)], + referenceID: refrenceID, + metadata: metadata + ) + + let otherParams = CAPUpdateCustomerRequestParams( + actions: [CAPPaymentAction(paymentAction: oneTimePayment)], + referenceID: refrenceID, + metadata: metadata + ) + + XCTAssert(capParams.isEqual(otherParams)) + XCTAssertFalse(capParams.isEqual(CAPUpdateCustomerRequestParams(actions: [], referenceID: nil, metadata: nil))) + } + + // MARK: - CAPCustomerRequest + + func test_CAPCustomerRequest_init_withCustomerRequest() { + let capCustomerRequest = CAPCustomerRequest(customerRequest: customerRequest) + XCTAssertEqual(customerRequest.id, capCustomerRequest.id) + XCTAssertEqual(customerRequest.status.rawValue, capCustomerRequest.status) + XCTAssertEqual(customerRequest.actions, capCustomerRequest.actions.map(\.paymentAction)) + XCTAssertEqual(customerRequest.authFlowTriggers, capCustomerRequest.authFlowTriggers?.authFlowTriggers) + XCTAssertEqual(customerRequest.redirectURL, capCustomerRequest.redirectURL) + XCTAssertEqual(customerRequest.channel.rawValue, capCustomerRequest.channel) + XCTAssertEqual(customerRequest.origin, capCustomerRequest.origin?.origin) + XCTAssertEqual(customerRequest.referenceID, capCustomerRequest.referenceID) + XCTAssertEqual(customerRequest.requesterProfile, capCustomerRequest.requesterProfile?.requesterProfile) + XCTAssertEqual(customerRequest.customerProfile, capCustomerRequest.customerProfile?.customerProfile) + XCTAssertEqual(customerRequest.metadata, capCustomerRequest.metadata) + } + + func test_CAPCustomerRequest_isEqual() { + let customerRequest = CAPCustomerRequest(customerRequest: customerRequest) + let otherCustomerRequest = CAPCustomerRequest( + customerRequest: CustomerRequest( + id: "GRR_123", + status: .APPROVED, + actions: [oneTimePayment], + authFlowTriggers: authFlowTriggers, + redirectURL: redirectURL, + createdAt: fakeDate, + updatedAt: fakeDate, + expiresAt: fakeDate, + origin: origin, + channel: .IN_APP, + grants: [grant], + referenceID: refrenceID, + requesterProfile: requesterProfile, + customerProfile: customerProfile, + metadata: metadata + ) + ) + + XCTAssert(customerRequest.isEqual(otherCustomerRequest)) + } + + // MARK: - PaymentAction + + func test_CAPPaymentAction_init_withPaymentAction() { + let paymentAction = CAPPaymentAction(paymentAction: oneTimePayment) + XCTAssertEqual(oneTimePayment.type.rawValue, paymentAction.type) + XCTAssertEqual(oneTimePayment.scopeID, paymentAction.scopeID) + XCTAssertEqual(oneTimePayment.money, paymentAction.money?.money) + XCTAssertEqual(oneTimePayment.accountReferenceID, paymentAction.accountReferenceID) + } + + func test_CAPPaymentAction_oneTimePayment() { + let paymentAction = CAPPaymentAction.oneTimePayment( + scopeID: scopeID, + money: CAPMoney(amount: 100, currency: .USD) + ) + XCTAssertEqual(paymentAction.type, "ONE_TIME_PAYMENT") + XCTAssertEqual(paymentAction.scopeID, scopeID) + XCTAssertEqual(paymentAction.money?.money, Money(amount: 100, currency: .USD)) + XCTAssertNil(paymentAction.accountReferenceID) + } + + func test_CAPPaymentAction_onFilePayment() { + let paymentAction = CAPPaymentAction.onFilePayment(scopeID: scopeID, accountReferenceID: "accountReferenceID") + XCTAssertEqual(paymentAction.type, "ON_FILE_PAYMENT") + XCTAssertEqual(paymentAction.scopeID, scopeID) + XCTAssertEqual(paymentAction.accountReferenceID, "accountReferenceID") + XCTAssertNil(paymentAction.money) + } + + func test_CAPPaymentAction_isEqual() { + let oneTimePayment = CAPPaymentAction.oneTimePayment( + scopeID: scopeID, + money: CAPMoney(amount: 100, currency: .USD) + ) + let otherOneTimePayment = CAPPaymentAction.oneTimePayment( + scopeID: scopeID, + money: CAPMoney(amount: 100, currency: .USD) + ) + let onFilePayment = CAPPaymentAction.onFilePayment(scopeID: scopeID, accountReferenceID: nil) + XCTAssertFalse(oneTimePayment.isEqual(onFilePayment)) + XCTAssert(oneTimePayment.isEqual(otherOneTimePayment)) + } + + // MARK: - Money + + func test_CAPMoney_init_withMoney() { + let oneDollar = Money(amount: 100, currency: .USD) + let capMoney = CAPMoney(money: oneDollar) + XCTAssertEqual(oneDollar.amount, capMoney.amount) + XCTAssertEqual(oneDollar.currency, capMoney.currency.currency) + } + + func test_CAPMoney_init() { + let capMoney = CAPMoney(amount: 100, currency: .USD) + XCTAssertEqual(capMoney.amount, 100) + XCTAssertEqual(capMoney.currency, .USD) + } + + func test_CAPMoney_isEqual() { + let capMoney = CAPMoney(amount: 100, currency: .USD) + let otherCapMoney = CAPMoney(amount: 100, currency: .USD) + XCTAssert(capMoney.isEqual(otherCapMoney)) + XCTAssertFalse(capMoney.isEqual(CAPMoney(amount: 200, currency: .USD))) + } + + // MARK: - Currency + + func test_currency() { + XCTAssertEqual(CAPCurrency.USD.currency, Currency.USD) + XCTAssertEqual(Currency.USD.capCurrency, .USD) + } + + // MARK: - Channel + + func test_channel() { + XCTAssertEqual(CAPChannel.IN_APP.channel, .IN_APP) + XCTAssertEqual(CAPChannel.IN_PERSON.channel, .IN_PERSON) + XCTAssertEqual(CAPChannel.ONLINE.channel, .ONLINE) + } + + // MARK: - Private + + private let refrenceID = "refrenceID" + private let metadata = ["Meta": "Data"] + private var scopeID = "SCOPE_ID" + private var redirectURL = URL(string: "paykitdemo://callback")! + private let fakeDate = Date(timeIntervalSince1970: 1712793605) + + private var oneTimePayment: PaymentAction { + .oneTimePayment(scopeID: scopeID, money: Money(amount: 100, currency: .USD)) + } + + private var clearingOneTimePayment: PaymentAction { + var action = PaymentAction.oneTimePayment(scopeID: scopeID, money: Money(amount: 100, currency: .USD)) + action.clearing = true + return action + } + + private var requesterProfile: CustomerRequest.RequesterProfile { + CustomerRequest.RequesterProfile( + name: "Requester", + logoURL: URL(string: "https://requester.com/logo")! + ) + } + + private var customerProfile: CustomerRequest.CustomerProfile { + CustomerRequest.CustomerProfile( + id: "CUSTOMER_ID", + cashtag: "$CASH_TAG" + ) + } + + private var authFlowTriggers: CustomerRequest.AuthFlowTriggers { + let url = URL(string: "https://qrcode.com/image")! + + return CustomerRequest.AuthFlowTriggers( + qrCodeImageURL: url, + qrCodeSVGURL: url, + mobileURL: url, + refreshesAt: fakeDate + ) + } + + private var origin: CustomerRequest.Origin { + CustomerRequest.Origin(type: .DIRECT, id: "origin_id") + } + + private var grant: CustomerRequest.Grant { + CustomerRequest.Grant( + id: "GRANT_123", + customerID: "customer_ID", + action: oneTimePayment, + status: .ACTIVE, + type: .ONE_TIME, + channel: .IN_APP, + createdAt: fakeDate, + updatedAt: fakeDate, + expiresAt: fakeDate + ) + } + + private var customerRequest: CustomerRequest { + CustomerRequest( + id: "GRR_123", + status: .APPROVED, + actions: [oneTimePayment], + authFlowTriggers: authFlowTriggers, + redirectURL: redirectURL, + createdAt: fakeDate, + updatedAt: fakeDate, + expiresAt: fakeDate, + origin: origin, + channel: .IN_APP, + grants: [grant], + referenceID: refrenceID, + requesterProfile: requesterProfile, + customerProfile: customerProfile, + metadata: metadata + ) + } +} diff --git a/Tests/PayKitTests/Errors+ObjCTests.swift b/Tests/PayKitTests/Errors+ObjCTests.swift new file mode 100644 index 0000000..bb858b1 --- /dev/null +++ b/Tests/PayKitTests/Errors+ObjCTests.swift @@ -0,0 +1,107 @@ +// +// Errors+ObjCTests.swift +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@testable import PayKit +import XCTest + +final class Errors_ObjCTests: XCTestCase { + + // MARK: - APIError + + func test_apiError() { + let objcError = CAPApiError(apiError: TestValues.internalServerError) + XCTAssertEqual(objcError.category, TestValues.internalServerError.category.capApiErrorCategory) + XCTAssertEqual(objcError.code, TestValues.internalServerError.code.capApiErrorCode.rawValue) + XCTAssertEqual(objcError.detail, TestValues.internalServerError.detail) + XCTAssertEqual(objcError.field, TestValues.internalServerError.field) + } + + func test_apiError_category() { + XCTAssertEqual(APIError.Category.API_ERROR.capApiErrorCategory, .API_ERROR) + } + + func test_apiError_code() { + XCTAssertEqual(APIError.ErrorCode.INTERNAL_SERVER_ERROR.capApiErrorCode, .INTERNAL_SERVER_ERROR) + XCTAssertEqual(APIError.ErrorCode.SERVICE_UNAVAILABLE.capApiErrorCode, .SERVICE_UNAVAILABLE) + XCTAssertEqual(APIError.ErrorCode.GATEWAY_TIMEOUT.capApiErrorCode, .GATEWAY_TIMEOUT) + } + + // MARK: - IntegrationError + + func test_integrationError() { + let objcError = CAPIntegrationError(integrationError: TestValues.brandNotFoundError) + XCTAssertEqual(objcError.category, TestValues.brandNotFoundError.category.capIntegrationErrorCategory) + XCTAssertEqual(objcError.code, TestValues.brandNotFoundError.code.capIntegrationErrorCode.rawValue) + XCTAssertEqual(objcError.detail, TestValues.brandNotFoundError.detail) + XCTAssertEqual(objcError.field, TestValues.brandNotFoundError.field) + } + + func test_integrationError_category() { + XCTAssertEqual( + IntegrationError.Category.AUTHENTICATION_ERROR.capIntegrationErrorCategory, + .AUTHENTICATION_ERROR + ) + XCTAssertEqual(IntegrationError.Category.BRAND_ERROR.capIntegrationErrorCategory, .BRAND_ERROR) + XCTAssertEqual(IntegrationError.Category.MERCHANT_ERROR.capIntegrationErrorCategory, .MERCHANT_ERROR) + XCTAssertEqual( + IntegrationError.Category.INVALID_REQUEST_ERROR.capIntegrationErrorCategory, + .INVALID_REQUEST_ERROR + ) + XCTAssertEqual(IntegrationError.Category.RATE_LIMIT_ERROR.capIntegrationErrorCategory, .RATE_LIMIT_ERROR) + } + + func test_integrationError_code() { + XCTAssertEqual(IntegrationError.ErrorCode.UNAUTHORIZED.capIntegrationErrorCode, .UNAUTHORIZED) + XCTAssertEqual(IntegrationError.ErrorCode.CLIENT_DISABLED.capIntegrationErrorCode, .CLIENT_DISABLED) + XCTAssertEqual(IntegrationError.ErrorCode.FORBIDDEN.capIntegrationErrorCode, .FORBIDDEN) + XCTAssertEqual(IntegrationError.ErrorCode.VALUE_TOO_LONG.capIntegrationErrorCode, .VALUE_TOO_LONG) + XCTAssertEqual(IntegrationError.ErrorCode.VALUE_TOO_SHORT.capIntegrationErrorCode, .VALUE_TOO_SHORT) + XCTAssertEqual(IntegrationError.ErrorCode.VALUE_EMPTY.capIntegrationErrorCode, .VALUE_EMPTY) + XCTAssertEqual(IntegrationError.ErrorCode.VALUE_REGEX_MISMATCH.capIntegrationErrorCode, .VALUE_REGEX_MISMATCH) + XCTAssertEqual(IntegrationError.ErrorCode.INVALID_URL.capIntegrationErrorCode, .INVALID_URL) + XCTAssertEqual(IntegrationError.ErrorCode.VALUE_TOO_HIGH.capIntegrationErrorCode, .VALUE_TOO_HIGH) + XCTAssertEqual(IntegrationError.ErrorCode.VALUE_TOO_LOW.capIntegrationErrorCode, .VALUE_TOO_LOW) + XCTAssertEqual(IntegrationError.ErrorCode.ARRAY_LENGTH_TOO_LONG.capIntegrationErrorCode, .ARRAY_LENGTH_TOO_LONG) + XCTAssertEqual( + IntegrationError.ErrorCode.ARRAY_LENGTH_TOO_SHORT.capIntegrationErrorCode, + .ARRAY_LENGTH_TOO_SHORT + ) + XCTAssertEqual(IntegrationError.ErrorCode.INVALID_ARRAY_TYPE.capIntegrationErrorCode, .INVALID_ARRAY_TYPE) + XCTAssertEqual(IntegrationError.ErrorCode.NOT_FOUND.capIntegrationErrorCode, .NOT_FOUND) + XCTAssertEqual(IntegrationError.ErrorCode.CONFLICT.capIntegrationErrorCode, .CONFLICT) + XCTAssertEqual( + IntegrationError.ErrorCode.INVALID_STATE_TRANSITION.capIntegrationErrorCode, + .INVALID_STATE_TRANSITION + ) + XCTAssertEqual(IntegrationError.ErrorCode.CLIENT_NOT_FOUND.capIntegrationErrorCode, .CLIENT_NOT_FOUND) + XCTAssertEqual(IntegrationError.ErrorCode.RATE_LIMITED.capIntegrationErrorCode, .RATE_LIMITED) + XCTAssertEqual(IntegrationError.ErrorCode.BRAND_NOT_FOUND.capIntegrationErrorCode, .BRAND_NOT_FOUND) + XCTAssertEqual( + IntegrationError.ErrorCode.MERCHANT_MISSING_ADDRESS_OR_SITE.capIntegrationErrorCode, + .MERCHANT_MISSING_ADDRESS_OR_SITE + ) + } + + // MARK: - UnexpectedError + + func test_unexpectedError() { + let objcError = CAPUnexpectedError(unexpectedError: TestValues.idempotencyKeyReusedError) + XCTAssertEqual(objcError.category, TestValues.idempotencyKeyReusedError.category) + XCTAssertEqual(objcError.codeMessage, TestValues.idempotencyKeyReusedError.code) + XCTAssertEqual(objcError.detail, TestValues.idempotencyKeyReusedError.detail) + XCTAssertEqual(objcError.field, TestValues.idempotencyKeyReusedError.field) + } +} diff --git a/Tests/PayKitTests/LoggableTests.swift b/Tests/PayKitTests/LoggableTests.swift index 8a23a31..0aa836d 100644 --- a/Tests/PayKitTests/LoggableTests.swift +++ b/Tests/PayKitTests/LoggableTests.swift @@ -87,7 +87,7 @@ class LoggableTests: XCTestCase { ) let loggable = LoggablePaymentAction(paymentAction: paymentAction).loggableDescription // swiftlint:disable:next line_length - XCTAssertEqual(loggable, .string("{\"amount\":100,\"currency\":\"USD\",\"type\":\"ONE_TIME_PAYMENT\",\"scope_id\":\"test\"}")) + XCTAssertEqual(loggable, .string("{\"amount\":100,\"currency\":\"USD\",\"scope_id\":\"test\",\"type\":\"ONE_TIME_PAYMENT\"}")) } func test_loggable_payment_action_on_file_payment() { @@ -97,14 +97,14 @@ class LoggableTests: XCTestCase { ) let loggable = LoggablePaymentAction(paymentAction: paymentAction).loggableDescription // swiftlint:disable:next line_length - XCTAssertEqual(loggable, .string(#"{"type":"ON_FILE_PAYMENT","scope_id":"test","account_reference_id":"FILTERED"}"#)) + XCTAssertEqual(loggable, .string(#"{"account_reference_id":"FILTERED","scope_id":"test","type":"ON_FILE_PAYMENT"}"#)) } func test_loggable_grant() throws { let grant = try XCTUnwrap(TestValues.approvedRequestGrants.first) let loggable = LoggableGrant(grant: grant).loggableDescription // swiftlint:disable:next line_length - XCTAssertEqual(loggable, .string("{\"status\":\"ACTIVE\",\"channel\":\"IN_APP\",\"action\":{\"type\":\"ON_FILE_PAYMENT\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"account_reference_id\":\"FILTERED\"},\"id\":\"GRG_AZYyHv2DwQltw0SiCLTaRb73y40XFe2dWM690WDF9Btqn-uTCYAUROa4ciwCdDnZcG4PuY1m_i3gwHODiO8DSf9zdMmRl1T0SM267vzuldnBs246-duHZhcehhXtmhfU8g\",\"created_at\":1666299823249000,\"expires_at\":1823979823159000,\"type\":\"EXTENDED\",\"customer_id\":\"CST_AYVkuLw-sT3OKZ7a_nhNTC_L2ekahLgGrS-EM_QhW4OTrGMbi59X1eCclH0cjaxoLObc\",\"updated_at\":1666299823249000}")) + XCTAssertEqual(loggable, .string("{\"action\":{\"account_reference_id\":\"FILTERED\",\"scope_id\":\"BRAND_9kx6p0mkuo97jnl025q9ni94t\",\"type\":\"ON_FILE_PAYMENT\"},\"channel\":\"IN_APP\",\"created_at\":1666299823249000,\"customer_id\":\"CST_AYVkuLw-sT3OKZ7a_nhNTC_L2ekahLgGrS-EM_QhW4OTrGMbi59X1eCclH0cjaxoLObc\",\"expires_at\":1823979823159000,\"id\":\"GRG_AZYyHv2DwQltw0SiCLTaRb73y40XFe2dWM690WDF9Btqn-uTCYAUROa4ciwCdDnZcG4PuY1m_i3gwHODiO8DSf9zdMmRl1T0SM267vzuldnBs246-duHZhcehhXtmhfU8g\",\"status\":\"ACTIVE\",\"type\":\"EXTENDED\",\"updated_at\":1666299823249000}")) } func test_loggable_grant_init() throws { diff --git a/Tests/PayKitTests/ObjcWrapperTests.swift b/Tests/PayKitTests/ObjcWrapperTests.swift new file mode 100644 index 0000000..2336f5b --- /dev/null +++ b/Tests/PayKitTests/ObjcWrapperTests.swift @@ -0,0 +1,175 @@ +// +// ObjCWrapperTests.swift +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@testable import PayKit +import XCTest + +final class ObjCWrapperTests: XCTestCase { + + private var testClientID: String! + + override func setUp() { + super.setUp() + testClientID = "client_id" + } + + override func tearDown() { + testClientID = nil + super.tearDown() + } + + func test_redirectNotification() { + XCTAssertEqual(ObjCWrapper.RedirectNotification, CashAppPay.RedirectNotification) + } + + func testEndpoint_defaultsToProduction() { + let cap = ObjCWrapper(clientID: testClientID) + XCTAssertEqual(cap.endpoint, .production) + } + + func testEndpoint() { + let cap = ObjCWrapper(clientID: testClientID, endpoint: .sandbox) + XCTAssertEqual(cap.endpoint, .sandbox) + } + + func test_retrieveCustomerRequest() { + let expectation = self.expectation(description: "retrieve customer request") + let mockCashAppPay = MockCashAppPay(retrieveCustomerRequestHandler: { [weak self] id, _ in + self?.XCTAssertEqual(id, "GRR_id") + expectation.fulfill() + }) + let objcWrapper = ObjCWrapper(cashAppPay: mockCashAppPay) + objcWrapper.retrieveCustomerRequest(id: "GRR_id") { _, _ in } + waitForExpectations(timeout: 0.2) + } + + func test_createCustomerRequest() { + let expectation = self.expectation(description: "create customer request") + let mockCashAppPay = MockCashAppPay(createCustomerRequestHandler: { _ in + expectation.fulfill() + }) + let objcWrapper = ObjCWrapper(cashAppPay: mockCashAppPay) + let objcParams = CAPCreateCustomerRequestParams( + createCustomerRequestParams: TestValues.createCustomerRequestParams + ) + objcWrapper.createCustomerRequest(params: objcParams) + waitForExpectations(timeout: 0.2) + } + + func test_updateCustomerRequest() { + let expectation = self.expectation(description: "update customer request") + let mockCashAppPay = MockCashAppPay(updateCustomerRequestHandler: { _, _ in + expectation.fulfill() + }) + let objcWrapper = ObjCWrapper(cashAppPay: mockCashAppPay) + let objcCustomerRequest = CAPCustomerRequest(customerRequest: TestValues.customerRequest) + let objcParams = CAPUpdateCustomerRequestParams( + updateCustomerRequestParams: TestValues.updateCustomerRequestParams + ) + objcWrapper.updateCustomerRequest(objcCustomerRequest, with: objcParams) + waitForExpectations(timeout: 0.2) + } + + func test_authorizeCustomerRequest() { + let expectation = self.expectation(description: "authorize customer request") + let mockCashAppPay = MockCashAppPay(authorizeCustomerRequestHandler: { _, _ in + expectation.fulfill() + }) + let objcWrapper = ObjCWrapper(cashAppPay: mockCashAppPay) + let objcCustomerRequest = CAPCustomerRequest(customerRequest: TestValues.customerRequest) + objcWrapper.authorizeCustomerRequest(objcCustomerRequest) + waitForExpectations(timeout: 0.2) + } + + func test_notifyObserversOnStateChange() { + let expectation = self.expectation(description: "State did change observed") + expectation.expectedFulfillmentCount = 2 + + let firstObserver = MockObserver { state in + XCTAssert(state is CAPCashAppPayStateApproved) + expectation.fulfill() + } + + let secondObserver = MockObserver { state in + XCTAssert(state is CAPCashAppPayStateApproved) + expectation.fulfill() + } + + let objcWrapper = ObjCWrapper(cashAppPay: MockCashAppPay()) + objcWrapper.addObserver(firstObserver) + objcWrapper.addObserver(secondObserver) + objcWrapper.stateDidChange(to: .approved(request: TestValues.customerRequest, grants: [])) + + waitForExpectations(timeout: 0.3) + } + + // MARK: - Private + + private final class MockObserver: NSObject, CAPCashAppPayObserver { + + private let stateDidChangeHandler: (CAPCashAppPayState) -> Void + + init(stateDidChangeHandler: @escaping (CAPCashAppPayState) -> Void) { + self.stateDidChangeHandler = stateDidChangeHandler + } + + func stateDidChange(to state: CAPCashAppPayState) { + stateDidChangeHandler(state) + } + } + + private final class MockCashAppPay: CashAppPay { + + let retrieveCustomerRequestHandler: ((String, (Result) -> Void) -> Void)? + let createCustomerRequestHandler: ((CreateCustomerRequestParams) -> Void)? + let updateCustomerRequestHandler: ((CustomerRequest, UpdateCustomerRequestParams) -> Void)? + let authorizeCustomerRequestHandler: ((CustomerRequest, AuthorizationMethod) -> Void)? + + init( + retrieveCustomerRequestHandler: ((String, (Result) -> Void) -> Void)? = nil, + createCustomerRequestHandler: ((CreateCustomerRequestParams) -> Void)? = nil, + updateCustomerRequestHandler: ((CustomerRequest, UpdateCustomerRequestParams) -> Void)? = nil, + authorizeCustomerRequestHandler: ((CustomerRequest, AuthorizationMethod) -> Void)? = nil + ) { + self.retrieveCustomerRequestHandler = retrieveCustomerRequestHandler + self.createCustomerRequestHandler = createCustomerRequestHandler + self.updateCustomerRequestHandler = updateCustomerRequestHandler + self.authorizeCustomerRequestHandler = authorizeCustomerRequestHandler + let networkManager = MockNetworkManager() + let stateMachine = StateMachine(networkManager: networkManager, analyticsService: MockAnalytics()) + super.init(stateMachine: stateMachine, networkManager: networkManager, endpoint: .sandbox) + } + + override func retrieveCustomerRequest( + id: String, + completion: @escaping (Result) -> Void + ) { + retrieveCustomerRequestHandler?(id, completion) + } + + override func createCustomerRequest(params: CreateCustomerRequestParams) { + createCustomerRequestHandler?(params) + } + + override func updateCustomerRequest(_ request: CustomerRequest, with params: UpdateCustomerRequestParams) { + updateCustomerRequestHandler?(request, params) + } + + override func authorizeCustomerRequest(_ request: CustomerRequest, method: AuthorizationMethod = .DEEPLINK) { + authorizeCustomerRequestHandler?(request, method) + } + } +} diff --git a/Tests/PayKitTests/ResilientRestServiceTests.swift b/Tests/PayKitTests/ResilientRestServiceTests.swift index 8a025f4..96178ee 100644 --- a/Tests/PayKitTests/ResilientRestServiceTests.swift +++ b/Tests/PayKitTests/ResilientRestServiceTests.swift @@ -62,7 +62,7 @@ final class ResilientRESTServiceTests: XCTestCase { self.XCTAssertEqual((error as? NSError)?.code, 5) handlerExpectation.fulfill() } - waitForExpectations(timeout: 0.5) + waitForExpectations(timeout: 2) } func test_execute_with_retry_eventually_fails_and_calls_handler() { diff --git a/Tests/PayKitTests/Resources/XCTestCase+Fixtures.swift b/Tests/PayKitTests/Resources/XCTestCase+Fixtures.swift index 07f4364..eb0d093 100644 --- a/Tests/PayKitTests/Resources/XCTestCase+Fixtures.swift +++ b/Tests/PayKitTests/Resources/XCTestCase+Fixtures.swift @@ -288,7 +288,7 @@ enum TestValues { ) } - // Grants + // MARK: - Grants static var approvedRequestGrants: [CustomerRequest.Grant] { return [CustomerRequest.Grant( @@ -308,7 +308,7 @@ enum TestValues { ] } - // API Error + // MARK: - API Error static var internalServerError: APIError { APIError( @@ -319,7 +319,7 @@ enum TestValues { ) } - // Integration Error + // MARK: - Integration Error static var unauthorizedError: IntegrationError { IntegrationError( @@ -339,7 +339,7 @@ enum TestValues { ) } - // Unexpected Error + // MARK: - Unexpected Error static var idempotencyKeyReusedError: UnexpectedError { UnexpectedError( @@ -349,4 +349,22 @@ enum TestValues { field: nil ) } + + // MARK: - Network Error + + static var invalidJSONError: NetworkError { + NetworkError.invalidJSON(Data()) + } + + static var nilDataError: NetworkError { + NetworkError.nilData(HTTPURLResponse()) + } + + static var noResponseError: NetworkError { + NetworkError.noResponse + } + + static func systemError(underlyingError: NSError) -> NetworkError { + NetworkError.systemError(underlyingError) + } }