diff --git a/CHANGELOG.md b/CHANGELOG.md index ac409241a..0cbab35b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # PayPal iOS SDK Release Notes +## unreleased +* PaymentButtons + * Add `CardButton` to be used for PayPal guest checkout using card funding option +* PaymentButtons + * Add `card` option in `PayPalWebChecoutFundingSource` for web card field option + ## 2.0.0 (2025-03-18) * Breaking Changes * PayPalNativePayments diff --git a/Demo/Demo/Extensions/PaymentButtonEnums+Extension.swift b/Demo/Demo/Extensions/PaymentButtonEnums+Extension.swift index 8c1e67fb2..19545d70b 100644 --- a/Demo/Demo/Extensions/PaymentButtonEnums+Extension.swift +++ b/Demo/Demo/Extensions/PaymentButtonEnums+Extension.swift @@ -33,6 +33,17 @@ extension PayPalCreditButton.Color { } } +extension CardButton.Color { + + public static var allCases: [CardButton.Color] { + [.black, .white] + } + + static func allCasesAsString() -> [String] { + Self.allCases.map { $0.rawValue } + } +} + extension PaymentButtonEdges { public static var allCases: [PaymentButtonEdges] { @@ -58,7 +69,7 @@ extension PaymentButtonSize { extension PaymentButtonFundingSource { public static var allCases: [PaymentButtonFundingSource] { - [.payPal, .payLater, .credit] + [.payPal, .payLater, .credit, .card] } static func allCasesAsString() -> [String] { diff --git a/Demo/Demo/PayPalWebPayments/PayPalWebPaymentsView/PayPalWebButtonsView.swift b/Demo/Demo/PayPalWebPayments/PayPalWebPaymentsView/PayPalWebButtonsView.swift index bac812076..8d09a5f6b 100644 --- a/Demo/Demo/PayPalWebPayments/PayPalWebPaymentsView/PayPalWebButtonsView.swift +++ b/Demo/Demo/PayPalWebPayments/PayPalWebPaymentsView/PayPalWebButtonsView.swift @@ -22,6 +22,7 @@ struct PayPalWebButtonsView: View { Text("PayPal").tag(PayPalWebCheckoutFundingSource.paypal) Text("PayPal Credit").tag(PayPalWebCheckoutFundingSource.paypalCredit) Text("Pay Later").tag(PayPalWebCheckoutFundingSource.paylater) + Text("Card").tag(PayPalWebCheckoutFundingSource.card) } .pickerStyle(SegmentedPickerStyle()) ZStack { @@ -38,6 +39,10 @@ struct PayPalWebButtonsView: View { PayPalButton.Representable(color: .blue, size: .full) { payPalWebViewModel.paymentButtonTapped(funding: .paypal) } + case .card: + CardButton.Representable(color: .black, size: .full) { + payPalWebViewModel.paymentButtonTapped(funding: .card) + } } if payPalWebViewModel.state.approveResultResponse == .loading && payPalWebViewModel.checkoutResult == nil && diff --git a/Demo/Demo/SwiftUIComponents/SwiftUIPaymentButtonDemo.swift b/Demo/Demo/SwiftUIComponents/SwiftUIPaymentButtonDemo.swift index 145da5d35..04be2e744 100644 --- a/Demo/Demo/SwiftUIComponents/SwiftUIPaymentButtonDemo.swift +++ b/Demo/Demo/SwiftUIComponents/SwiftUIPaymentButtonDemo.swift @@ -118,6 +118,14 @@ struct SwiftUIPaymentButtonDemo: View { size: selectedSize ) .id(buttonID) + + case .card: + CardButton.Representable( + color: CardButton.Color.allCases[colorsIndex], + edges: selectedEdge, + size: selectedSize + ) + .id(buttonID) } } .padding() @@ -136,6 +144,9 @@ struct SwiftUIPaymentButtonDemo: View { case .credit: return PayPalCreditButton.Color.allCasesAsString() + + case .card: + return CardButton.Color.allCasesAsString() } } } diff --git a/PayPal.podspec b/PayPal.podspec index 019a1bedd..c47ac589c 100644 --- a/PayPal.podspec +++ b/PayPal.podspec @@ -27,7 +27,7 @@ Pod::Spec.new do |s| end s.subspec "PayPalWebPayments" do |s| - s.source_files = "Sources/PayPalWebPayments/*.swift" + s.source_files = "Sources/PayPalWebPayments/**/*.swift" s.dependency "PayPal/CorePayments" s.resource_bundle = { "PayPalWebPayments_PrivacyInfo" => "Sources/PayPalWebPayments/PrivacyInfo.xcprivacy" } end diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index 2a7bb6dc9..0eee8badd 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -16,9 +16,17 @@ 3B22E8B82A841AEA00962E34 /* CardVaultResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */; }; 3B29C3972B9148F70077741D /* PayPalVaultRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B29C3962B9148F70077741D /* PayPalVaultRequest.swift */; }; 3B3C511E2B2395B5009125FE /* PayPalVaultResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3C511D2B2395B5009125FE /* PayPalVaultResult.swift */; }; + 3B7387212D919B5A007354C2 /* ClientConfigResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7387202D919B5A007354C2 /* ClientConfigResponse.swift */; }; 3B783DC32B7A69C2004623DB /* FakeUpdateSetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B783DC22B7A69C2004623DB /* FakeUpdateSetupTokenResponse.swift */; }; 3B79E4F72A8503CA00C01D06 /* UpdateVaultVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */; }; 3B80D50C2A27979000D2EAC4 /* FailingJSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */; }; + 3B8F84842D92F95500A151AC /* CardButton_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8F84832D92F94600A151AC /* CardButton_Tests.swift */; }; + 3B8F84862D93319800A151AC /* UpdateClientConfigAP_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8F84852D93318D00A151AC /* UpdateClientConfigAP_Tests.swift */; }; + 3B8F848C2D93380B00A151AC /* TestShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80E743F8270E40CE00BACECA /* TestShared.framework */; }; + 3B8F84922D933FBA00A151AC /* MockUpdateClientConfigAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8F84912D933FAE00A151AC /* MockUpdateClientConfigAPI.swift */; }; + 3BD014C82D8B4D60005565FE /* CardButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD014C72D8B4D60005565FE /* CardButton.swift */; }; + 3BD014CB2D8DE711005565FE /* UpdateClientConfigAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD014CA2D8DE701005565FE /* UpdateClientConfigAPI.swift */; }; + 3BD014CD2D8DF2B8005565FE /* UpdateClientConfigVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD014CC2D8DF2AF005565FE /* UpdateClientConfigVariables.swift */; }; 3BD82DBB2A835AF900CBE764 /* UpdateSetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */; }; 3BDB34942A80CE6E008100D7 /* CardVaultRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDB34932A80CE6E008100D7 /* CardVaultRequest.swift */; }; 3BE738682B9A66D800598F05 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3BE738622B9A482800598F05 /* PrivacyInfo.xcprivacy */; }; @@ -133,6 +141,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 3B8F84892D9337FF00A151AC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = OBJ_1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = BCD5C46727E9200800B074D5; + remoteInfo = PayPalWebPayments; + }; + 3B8F848E2D93380B00A151AC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = OBJ_1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 80E743F7270E40CE00BACECA; + remoteInfo = TestShared; + }; 8034A9E826B875C90055AF13 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = OBJ_1 /* Project object */; @@ -154,13 +176,6 @@ remoteGlobalIDString = BCF735C227D1583200A52E03; remoteInfo = PayPalDataCollector; }; - BCD5C49627E9208000B074D5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = OBJ_1 /* Project object */; - proxyType = 1; - remoteGlobalIDString = BCD5C46727E9200800B074D5; - remoteInfo = PayPalWebCheckout; - }; BCFAC72027ED059C00C3AF00 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = OBJ_1 /* Project object */; @@ -183,9 +198,16 @@ 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultResult.swift; sourceTree = ""; }; 3B29C3962B9148F70077741D /* PayPalVaultRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalVaultRequest.swift; sourceTree = ""; }; 3B3C511D2B2395B5009125FE /* PayPalVaultResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalVaultResult.swift; sourceTree = ""; }; + 3B7387202D919B5A007354C2 /* ClientConfigResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConfigResponse.swift; sourceTree = ""; }; 3B783DC22B7A69C2004623DB /* FakeUpdateSetupTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeUpdateSetupTokenResponse.swift; sourceTree = ""; }; 3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateVaultVariables.swift; sourceTree = ""; }; 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailingJSONEncoder.swift; sourceTree = ""; }; + 3B8F84832D92F94600A151AC /* CardButton_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardButton_Tests.swift; sourceTree = ""; }; + 3B8F84852D93318D00A151AC /* UpdateClientConfigAP_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateClientConfigAP_Tests.swift; sourceTree = ""; }; + 3B8F84912D933FAE00A151AC /* MockUpdateClientConfigAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUpdateClientConfigAPI.swift; sourceTree = ""; }; + 3BD014C72D8B4D60005565FE /* CardButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardButton.swift; sourceTree = ""; }; + 3BD014CA2D8DE701005565FE /* UpdateClientConfigAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateClientConfigAPI.swift; sourceTree = ""; }; + 3BD014CC2D8DF2AF005565FE /* UpdateClientConfigVariables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateClientConfigVariables.swift; sourceTree = ""; }; 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSetupTokenResponse.swift; sourceTree = ""; }; 3BDB34932A80CE6E008100D7 /* CardVaultRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultRequest.swift; sourceTree = ""; }; 3BE738622B9A482800598F05 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -353,6 +375,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 0; files = ( + 3B8F848C2D93380B00A151AC /* TestShared.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -444,6 +467,24 @@ path = Models; sourceTree = ""; }; + 3B8F84902D933FA700A151AC /* Mocks */ = { + isa = PBXGroup; + children = ( + 3B8F84912D933FAE00A151AC /* MockUpdateClientConfigAPI.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + 3BD014C92D8DE6EF005565FE /* APIRequests */ = { + isa = PBXGroup; + children = ( + 3B7387202D919B5A007354C2 /* ClientConfigResponse.swift */, + 3BD014CC2D8DF2AF005565FE /* UpdateClientConfigVariables.swift */, + 3BD014CA2D8DE701005565FE /* UpdateClientConfigAPI.swift */, + ); + path = APIRequests; + sourceTree = ""; + }; 8036C1DE270F9BCF00C0F091 /* PaymentsCoreTests */ = { isa = PBXGroup; children = ( @@ -559,20 +600,14 @@ BCE8A7F027EA52BF00AC301B /* PayPalWebPaymentsTests */ = { isa = PBXGroup; children = ( - BCE8A7F127EA52EB00AC301B /* Mocks */, + 3B8F84902D933FA700A151AC /* Mocks */, + 3B8F84852D93318D00A151AC /* UpdateClientConfigAP_Tests.swift */, BCE8A7FA27EA5B1D00AC301B /* Environment+PayPalWebCheckout_Tests.swift */, BCE8A7F827EA555800AC301B /* PayPalWebCheckoutClient_Tests.swift */, ); path = PayPalWebPaymentsTests; sourceTree = ""; }; - BCE8A7F127EA52EB00AC301B /* Mocks */ = { - isa = PBXGroup; - children = ( - ); - path = Mocks; - sourceTree = ""; - }; BCF735C127D157CD00A52E03 /* FraudProtection */ = { isa = PBXGroup; children = ( @@ -605,6 +640,7 @@ isa = PBXGroup; children = ( BE00B79D2742F9F000758C63 /* Assets.xcassets */, + 3BD014C72D8B4D60005565FE /* CardButton.swift */, BEDB7FE32788AB8E00CEA554 /* Coordinator.swift */, BE00B7AA2743FD9F00758C63 /* PaymentButton.swift */, BE9F36DE274859A000AFC7DA /* PaymentButtonColor.swift */, @@ -626,6 +662,7 @@ BCFAC72227ED05D900C3AF00 /* PaymentButtonsTests */ = { isa = PBXGroup; children = ( + 3B8F84832D92F94600A151AC /* CardButton_Tests.swift */, BEF3FF1627AC5DF3006B4B69 /* Coordinator_Tests.swift */, BE00B7AC27444FE900758C63 /* PayPalButton_Tests.swift */, BE9F36E3275520E700AFC7DA /* PayPalCreditButton_Tests.swift */, @@ -637,6 +674,7 @@ BE4F784227EB627100FF4C0E /* PayPalWebPayments */ = { isa = PBXGroup; children = ( + 3BD014C92D8DE6EF005565FE /* APIRequests */, BE4F784327EB629100FF4C0E /* Environment+PayPalWebCheckout.swift */, BE4F784627EB629100FF4C0E /* PayPalWebCheckoutClient.swift */, BE4F784427EB629100FF4C0E /* PayPalError.swift */, @@ -883,7 +921,8 @@ buildRules = ( ); dependencies = ( - BCD5C49727E9208000B074D5 /* PBXTargetDependency */, + 3B8F848A2D9337FF00A151AC /* PBXTargetDependency */, + 3B8F848F2D93380B00A151AC /* PBXTargetDependency */, ); name = PayPalWebPaymentsTests; productName = PayPalTests; @@ -1242,8 +1281,11 @@ BE4F785327EB656400FF4C0E /* Environment+PayPalWebCheckout.swift in Sources */, BE4F785427EB656400FF4C0E /* PayPalWebCheckoutClient.swift in Sources */, BE4F785527EB656400FF4C0E /* PayPalError.swift in Sources */, + 3B7387212D919B5A007354C2 /* ClientConfigResponse.swift in Sources */, + 3BD014CD2D8DF2B8005565FE /* UpdateClientConfigVariables.swift in Sources */, BE4F785727EB656400FF4C0E /* PayPalWebCheckoutRequest.swift in Sources */, BE4F785827EB656400FF4C0E /* PayPalWebCheckoutResult.swift in Sources */, + 3BD014CB2D8DE711005565FE /* UpdateClientConfigAPI.swift in Sources */, CB51FF7227FCB947001A97F5 /* PayPalWebCheckoutFundingSource.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1253,6 +1295,8 @@ buildActionMask = 0; files = ( BCE8A7FB27EA5B1D00AC301B /* Environment+PayPalWebCheckout_Tests.swift in Sources */, + 3B8F84862D93319800A151AC /* UpdateClientConfigAP_Tests.swift in Sources */, + 3B8F84922D933FBA00A151AC /* MockUpdateClientConfigAPI.swift in Sources */, BCE8A7F927EA555800AC301B /* PayPalWebCheckoutClient_Tests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1297,6 +1341,7 @@ BCFAC71F27ED04C800C3AF00 /* Coordinator.swift in Sources */, BCFAC71C27ED04C000C3AF00 /* PaymentButton.swift in Sources */, CB1A47F22820AFED00BD8184 /* PayPalPayLaterButton.swift in Sources */, + 3BD014C82D8B4D60005565FE /* CardButton.swift in Sources */, CB1A47F42820BA5D00BD8184 /* PaymentButtonEdges.swift in Sources */, BCFAC71A27ED04AC00C3AF00 /* PaymentButtonColor.swift in Sources */, ); @@ -1307,6 +1352,7 @@ buildActionMask = 0; files = ( BCFAC70E27ED043100C3AF00 /* PayPalButton_Tests.swift in Sources */, + 3B8F84842D92F95500A151AC /* CardButton_Tests.swift in Sources */, BCFAC70F27ED043100C3AF00 /* Coordinator_Tests.swift in Sources */, CB22C018291049500097E592 /* PayPalPayLaterButton_Tests.swift in Sources */, BCFAC71327ED043100C3AF00 /* PayPalCreditButton_Tests.swift in Sources */, @@ -1316,6 +1362,24 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 3B8F848A2D9337FF00A151AC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilters = ( + driverkit, + ios, + macos, + tvos, + watchos, + xros, + ); + target = BCD5C46727E9200800B074D5 /* PayPalWebPayments */; + targetProxy = 3B8F84892D9337FF00A151AC /* PBXContainerItemProxy */; + }; + 3B8F848F2D93380B00A151AC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 80E743F7270E40CE00BACECA /* TestShared */; + targetProxy = 3B8F848E2D93380B00A151AC /* PBXContainerItemProxy */; + }; 8034A9E926B875C90055AF13 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 80B9F85026B8750000D67843 /* CorePayments */; @@ -1331,11 +1395,6 @@ target = BCF735C227D1583200A52E03 /* FraudProtection */; targetProxy = BC9C18DF27D2B1850019B541 /* PBXContainerItemProxy */; }; - BCD5C49727E9208000B074D5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = BCD5C46727E9200800B074D5 /* PayPalWebPayments */; - targetProxy = BCD5C49627E9208000B074D5 /* PBXContainerItemProxy */; - }; BCFAC72127ED059C00C3AF00 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = BCFAC6EB27ED042500C3AF00 /* PaymentButtons */; diff --git a/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift b/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift index da0dd7e0f..90256e3de 100644 --- a/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift +++ b/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift @@ -6,7 +6,7 @@ import CorePayments /// This class coordinates networking logic for communicating with the /graphql?UpdateVaultSetupToken API. class VaultPaymentTokensAPI { - // MARK: - Private Propertires + // MARK: - Private Properties private let coreConfig: CoreConfig private let networkingClient: NetworkingClient diff --git a/Sources/CorePayments/Networking/Enums/HTTPHeader.swift b/Sources/CorePayments/Networking/Enums/HTTPHeader.swift index 637bfabef..523544dce 100644 --- a/Sources/CorePayments/Networking/Enums/HTTPHeader.swift +++ b/Sources/CorePayments/Networking/Enums/HTTPHeader.swift @@ -7,4 +7,5 @@ public enum HTTPHeader: String { case authorization = "Authorization" case contentType = "Content-Type" case origin = "Origin" + case payPalClientContext = "paypal-client-context" } diff --git a/Sources/CorePayments/Networking/HTTPResponseParser.swift b/Sources/CorePayments/Networking/HTTPResponseParser.swift index af06319e4..72f98a475 100644 --- a/Sources/CorePayments/Networking/HTTPResponseParser.swift +++ b/Sources/CorePayments/Networking/HTTPResponseParser.swift @@ -69,3 +69,25 @@ public class HTTPResponseParser { } } } + +extension HTTPResponseParser { + + // to extract correlationId from GraphQL response + public func parseGraphQLDictionary(_ httpResponse: HTTPResponse) throws -> [String: Any] { + + guard httpResponse.status == 200 else { + throw NetworkingError.urlSessionError + } + + guard let body = httpResponse.body else { + throw NetworkingError.noResponseDataError + } + + let jsonObject = try JSONSerialization.jsonObject(with: body, options: []) + guard let topLevel = jsonObject as? [String: Any] else { + throw NetworkingError.invalidURLResponseError + } + + return topLevel + } +} diff --git a/Sources/CorePayments/Networking/NetworkingClient.swift b/Sources/CorePayments/Networking/NetworkingClient.swift index bd54bda6d..e69c8d2e5 100644 --- a/Sources/CorePayments/Networking/NetworkingClient.swift +++ b/Sources/CorePayments/Networking/NetworkingClient.swift @@ -57,22 +57,31 @@ public class NetworkingClient { /// This function executes a network request from a GraphQLRequest and returns HTTPResponse /// which contains status (Int type) and body (optional Data type) + /// client context is header included for UpdateClientConfig call for BCDC flow @_documentation(visibility: private) - public func fetch(request: GraphQLRequest) async throws -> HTTPResponse { + public func fetch( + request: GraphQLRequest, + clientContext: String? = nil + ) async throws -> HTTPResponse { let url = try constructGraphQLURL(queryName: request.queryNameForURL) // TODO: - Move JSON encoding into custom class let postBody = GraphQLHTTPPostBody(query: request.query, variables: request.variables) // TODO: - encoding `Data` results in mumbo jumbo string. Why let postData = try JSONEncoder().encode(postBody) - + + var headers: [HTTPHeader: String] = [ + .contentType: "application/json", + .accept: "application/json", + .appName: "ppcpmobilesdk", + .origin: coreConfig.environment.graphQLURL.absoluteString + ] + + if let clientContext { + headers[.payPalClientContext] = clientContext + } let httpRequest = HTTPRequest( - headers: [ - .contentType: "application/json", - .accept: "application/json", - .appName: "ppcpmobilesdk", - .origin: coreConfig.environment.graphQLURL.absoluteString - ], + headers: headers, method: .post, url: url, body: postData diff --git a/Sources/PayPalWebPayments/APIRequests/ClientConfigResponse.swift b/Sources/PayPalWebPayments/APIRequests/ClientConfigResponse.swift new file mode 100644 index 000000000..45c98477f --- /dev/null +++ b/Sources/PayPalWebPayments/APIRequests/ClientConfigResponse.swift @@ -0,0 +1,6 @@ +import Foundation + +struct ClientConfigResponse: Decodable { + + let updateClientConfig: String? +} diff --git a/Sources/PayPalWebPayments/APIRequests/UpdateClientConfigAPI.swift b/Sources/PayPalWebPayments/APIRequests/UpdateClientConfigAPI.swift new file mode 100644 index 000000000..e9b3e5230 --- /dev/null +++ b/Sources/PayPalWebPayments/APIRequests/UpdateClientConfigAPI.swift @@ -0,0 +1,70 @@ +import Foundation +#if canImport(CorePayments) +import CorePayments +#endif + +/// This class coordinates networking logic for communicating with the /graphql?UpdateClientConfig API. +class UpdateClientConfigAPI { + + // MARK: - Private Properties + + private let coreConfig: CoreConfig + private let networkingClient: NetworkingClient + + // MARK: - Initializer + + init(coreConfig: CoreConfig) { + self.coreConfig = coreConfig + self.networkingClient = NetworkingClient(coreConfig: coreConfig) + } + + /// Exposed for injecting MockNetworkingClient in tests + init(coreConfig: CoreConfig, networkingClient: NetworkingClient) { + self.coreConfig = coreConfig + self.networkingClient = networkingClient + } + + // MARK: - Internal Methods + + func updateClientConfig(request: PayPalWebCheckoutRequest) async throws -> ClientConfigResponse { + + let queryString = """ + mutation UpdateClientConfig( + $orderID: String!, + $fundingSource: ButtonFundingSourceType!, + $integrationArtifact: IntegrationArtifactType!, + $userExperienceFlow: UserExperienceFlowType!, + $productFlow: ProductFlowType!, + $buttonSessionID: String + ) { + updateClientConfig( + token: $orderID + fundingSource: $fundingSource + integrationArtifact: $integrationArtifact, + userExperienceFlow: $userExperienceFlow, + productFlow: $productFlow, + buttonSessionID: $buttonSessionID + ) + } + """ + + let variables = UpdateClientConfigVariables( + orderID: request.orderID, + fundingSource: request.fundingSource.rawValue, + integrationArtifact: "PAYPAL_JS_SDK", // PAYPAL_JS_SDK or NATIVE_SDK + userExperienceFlow: "INCONTEXT", // INCONTEXT or INLINE + productFlow: "SMART_PAYMENT_BUTTONS", // NATIVE or SMART_PAYMENT_BUTTONS + buttonSessionID: nil + ) + + let graphQLRequest = GraphQLRequest( + query: queryString, + variables: variables, + queryNameForURL: "UpdateClientConfig" + ) + + let httpResponse = try await networkingClient.fetch(request: graphQLRequest, clientContext: request.orderID) + + return try HTTPResponseParser().parseGraphQL(httpResponse, as: ClientConfigResponse.self) + } +} diff --git a/Sources/PayPalWebPayments/APIRequests/UpdateClientConfigVariables.swift b/Sources/PayPalWebPayments/APIRequests/UpdateClientConfigVariables.swift new file mode 100644 index 000000000..ff220a255 --- /dev/null +++ b/Sources/PayPalWebPayments/APIRequests/UpdateClientConfigVariables.swift @@ -0,0 +1,11 @@ +import Foundation + +struct UpdateClientConfigVariables: Encodable { + + let orderID: String + let fundingSource: String + let integrationArtifact: String + let userExperienceFlow: String + let productFlow: String + let buttonSessionID: String? +} diff --git a/Sources/PayPalWebPayments/PayPalWebCheckoutClient.swift b/Sources/PayPalWebPayments/PayPalWebCheckoutClient.swift index e7b9d126b..a38e0e368 100644 --- a/Sources/PayPalWebPayments/PayPalWebCheckoutClient.swift +++ b/Sources/PayPalWebPayments/PayPalWebCheckoutClient.swift @@ -7,6 +7,8 @@ import CorePayments public class PayPalWebCheckoutClient: NSObject { let config: CoreConfig + + private let clientConfigAPI: UpdateClientConfigAPI private let webAuthenticationSession: WebAuthenticationSession private let networkingClient: NetworkingClient private var analyticsService: AnalyticsService? @@ -18,13 +20,20 @@ public class PayPalWebCheckoutClient: NSObject { self.config = config self.webAuthenticationSession = WebAuthenticationSession() self.networkingClient = NetworkingClient(coreConfig: config) + self.clientConfigAPI = UpdateClientConfigAPI(coreConfig: config) } /// For internal use for testing/mocking purpose - init(config: CoreConfig, networkingClient: NetworkingClient, webAuthenticationSession: WebAuthenticationSession) { + init( + config: CoreConfig, + networkingClient: NetworkingClient, + clientConfigAPI: UpdateClientConfigAPI, + webAuthenticationSession: WebAuthenticationSession + ) { self.config = config self.webAuthenticationSession = webAuthenticationSession self.networkingClient = networkingClient + self.clientConfigAPI = clientConfigAPI } /// Launch the PayPal web flow @@ -40,55 +49,66 @@ public class PayPalWebCheckoutClient: NSObject { analyticsService = AnalyticsService(coreConfig: config, orderID: request.orderID) analyticsService?.sendEvent("paypal-web-payments:checkout:started") - let baseURLString = config.environment.payPalBaseURL.absoluteString - let payPalCheckoutURLString = - "\(baseURLString)/checkoutnow?token=\(request.orderID)" + - "&fundingSource=\(request.fundingSource.rawValue)" - - guard let payPalCheckoutURL = URL(string: payPalCheckoutURLString), - let payPalCheckoutURLComponents = payPalCheckoutReturnURL(payPalCheckoutURL: payPalCheckoutURL) - else { - self.notifyCheckoutFailure(with: PayPalError.payPalURLError, completion: completion) - return - } - - webAuthenticationSession.start( - url: payPalCheckoutURLComponents, - context: self, - sessionDidDisplay: { [weak self] didDisplay in - if didDisplay { - self?.analyticsService?.sendEvent("paypal-web-payments:checkout:auth-challenge-presentation:succeeded") - } else { - self?.analyticsService?.sendEvent("paypal-web-payments:checkout:auth-challenge-presentation:failed") - } - }, - sessionDidComplete: { url, error in - if let error = error { - switch error { - case ASWebAuthenticationSessionError.canceledLogin: - self.notifyCheckoutCancelWithError( - with: PayPalError.checkoutCanceledError, - completion: completion - ) - return - default: - self.notifyCheckoutFailure(with: PayPalError.webSessionError(error), completion: completion) - return - } + Task { + if request.fundingSource == .card { + do { + let configResult = try await clientConfigAPI.updateClientConfig(request: request) + print("configResult: \(configResult)") + } catch { + print("error in calling graphQL: \(error.localizedDescription)") + self.notifyCheckoutFailure(with: PayPalError.payPalURLError, completion: completion) } + } + let baseURLString = config.environment.payPalBaseURL.absoluteString + let payPalCheckoutURLString = + "\(baseURLString)/checkoutnow?token=\(request.orderID)" + + "&fundingSource=\(request.fundingSource.rawValue)" - if let url = url { - guard let orderID = self.getQueryStringParameter(url: url.absoluteString, param: "token"), - let payerID = self.getQueryStringParameter(url: url.absoluteString, param: "PayerID") else { - self.notifyCheckoutFailure(with: PayPalError.malformedResultError, completion: completion) - return + guard let payPalCheckoutURL = URL(string: payPalCheckoutURLString), + let payPalCheckoutURLComponents = payPalCheckoutReturnURL(payPalCheckoutURL: payPalCheckoutURL) + else { + self.notifyCheckoutFailure(with: PayPalError.payPalURLError, completion: completion) + return + } + + webAuthenticationSession.start( + url: payPalCheckoutURLComponents, + context: self, + sessionDidDisplay: { [weak self] didDisplay in + if didDisplay { + self?.analyticsService?.sendEvent("paypal-web-payments:checkout:auth-challenge-presentation:succeeded") + } else { + self?.analyticsService?.sendEvent("paypal-web-payments:checkout:auth-challenge-presentation:failed") } + }, + sessionDidComplete: { url, error in + if let error = error { + switch error { + case ASWebAuthenticationSessionError.canceledLogin: + self.notifyCheckoutCancelWithError( + with: PayPalError.checkoutCanceledError, + completion: completion + ) + return + default: + self.notifyCheckoutFailure(with: PayPalError.webSessionError(error), completion: completion) + return + } + } + + if let url = url { + guard let orderID = self.getQueryStringParameter(url: url.absoluteString, param: "token"), + let payerID = self.getQueryStringParameter(url: url.absoluteString, param: "PayerID") else { + self.notifyCheckoutFailure(with: PayPalError.malformedResultError, completion: completion) + return + } - let result = PayPalWebCheckoutResult(orderID: orderID, payerID: payerID) - self.notifyCheckoutSuccess(for: result, completion: completion) + let result = PayPalWebCheckoutResult(orderID: orderID, payerID: payerID) + self.notifyCheckoutSuccess(for: result, completion: completion) + } } - } - ) + ) + } } /// Launch the PayPal web flow diff --git a/Sources/PayPalWebPayments/PayPalWebCheckoutFundingSource.swift b/Sources/PayPalWebPayments/PayPalWebCheckoutFundingSource.swift index b64f53e28..1a100b263 100644 --- a/Sources/PayPalWebPayments/PayPalWebCheckoutFundingSource.swift +++ b/Sources/PayPalWebPayments/PayPalWebCheckoutFundingSource.swift @@ -13,4 +13,7 @@ public enum PayPalWebCheckoutFundingSource: String { /// PayPal will launch the web checkout for a one-time PayPal Checkout flow case paypal = "paypal" + + /// PayPal will launch the web guest checkout for card funding source + case card = "card" } diff --git a/Sources/PaymentButtons/Assets.xcassets/Logos/card_color.imageset/Contents.json b/Sources/PaymentButtons/Assets.xcassets/Logos/card_color.imageset/Contents.json new file mode 100644 index 000000000..ca18414ca --- /dev/null +++ b/Sources/PaymentButtons/Assets.xcassets/Logos/card_color.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "card-black.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Sources/PaymentButtons/Assets.xcassets/Logos/card_color.imageset/card-black.svg b/Sources/PaymentButtons/Assets.xcassets/Logos/card_color.imageset/card-black.svg new file mode 100644 index 000000000..f5be081fc --- /dev/null +++ b/Sources/PaymentButtons/Assets.xcassets/Logos/card_color.imageset/card-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Sources/PaymentButtons/Assets.xcassets/Logos/card_monochrome.imageset/Contents.json b/Sources/PaymentButtons/Assets.xcassets/Logos/card_monochrome.imageset/Contents.json new file mode 100644 index 000000000..902a79c28 --- /dev/null +++ b/Sources/PaymentButtons/Assets.xcassets/Logos/card_monochrome.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "card-white.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/PaymentButtons/Assets.xcassets/Logos/card_monochrome.imageset/card-white.svg b/Sources/PaymentButtons/Assets.xcassets/Logos/card_monochrome.imageset/card-white.svg new file mode 100644 index 000000000..8078297f7 --- /dev/null +++ b/Sources/PaymentButtons/Assets.xcassets/Logos/card_monochrome.imageset/card-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Sources/PaymentButtons/CardButton.swift b/Sources/PaymentButtons/CardButton.swift new file mode 100644 index 000000000..57a374cc4 --- /dev/null +++ b/Sources/PaymentButtons/CardButton.swift @@ -0,0 +1,122 @@ +import UIKit +import SwiftUI + +/// Configuration for Card button +public final class CardButton: PaymentButton { + + /// Available colors for CardButton. + public enum Color: String { + case white + case black + + var color: PaymentButtonColor { + PaymentButtonColor(rawValue: rawValue) ?? .black + } + } + + /// Available labels for CardButton. + public enum Label: String { + /// Display "Debit or Credit Card" on the left side of the button's logo + case card = "Debit or Credit Card" + + var label: PaymentButtonLabel? { + PaymentButtonLabel(rawValue: rawValue) + } + } + + /// Initialize a PayPalButton + /// - Parameters: + /// - insets: Edge insets of the button, defining the spacing of the button's edges relative to its content. + /// - color: Color of the button. Default to black if not provided. + /// - edges: Edges of the button. Default to softEdges if not provided. + /// - size: Size of the button. Default to collapsed if not provided. + /// - label: Label displayed next to the button's logo. Default to no label. + public convenience init( + insets: NSDirectionalEdgeInsets? = nil, + color: Color = .black, + edges: PaymentButtonEdges = .softEdges, + size: PaymentButtonSize = .collapsed, + label: Label = .card, + cardImage: UIImage? = nil + ) { + self.init( + fundingSource: .card, + color: color.color, + edges: edges, + size: size, + insets: insets, + label: label.label + ) + } + + deinit {} +} + +public extension CardButton { + + /// CardlButton for SwiftUI + struct Representable: UIViewRepresentable { + + private var action: () -> Void = { } + + private let button: CardButton + + /// Initialize a CardButton + /// - Parameters: + /// - insets: Edge insets of the button, defining the spacing of the button's edges relative to its content. + /// - color: Color of the button. Default to gold if not provided. + /// - edges: Edges of the button. Default to softEdges if not provided. + /// - size: Size of the button. Default to expanded if not provided. + /// - label: Label displayed next to the button's logo. Default to no label. + public init( + insets: NSDirectionalEdgeInsets? = nil, + color: CardButton.Color = .black, + edges: PaymentButtonEdges = .softEdges, + size: PaymentButtonSize = .expanded, + label: CardButton.Label = .card, + _ action: @escaping () -> Void = { } + ) { + button = CardButton( + fundingSource: .card, + color: color.color, + edges: edges, + size: size, + insets: insets, + label: label.label + ) + self.action = action + } + + // MARK: - UIViewRepresentable methods + // TODO: Make unit test for UIVRepresentable methods: https://engineering.paypalcorp.com/jira/browse/DTNOR-623 + + public func makeCoordinator() -> Coordinator { + Coordinator(action: action) + } + + public func makeUIView(context: Context) -> PaymentButton { + button.addTarget(context.coordinator, action: #selector(Coordinator.onAction(_:)), for: .touchUpInside) + return button + } + + public func updateUIView(_ uiView: PaymentButton, context: Context) { + context.coordinator.action = action + } + } +} + +// MARK: PayPalButton Preview + +struct CardButtonView: View { + + var body: some View { + CardButton.Representable() + } +} + +struct CardButtonView_Preview: PreviewProvider { + + static var previews: some View { + CardButtonView() + } +} diff --git a/Sources/PaymentButtons/PaymentButton+ImageAsset.swift b/Sources/PaymentButtons/PaymentButton+ImageAsset.swift index 32844ff21..74a491beb 100644 --- a/Sources/PaymentButtons/PaymentButton+ImageAsset.swift +++ b/Sources/PaymentButtons/PaymentButton+ImageAsset.swift @@ -10,7 +10,11 @@ extension PaymentButton { var imageAccessibilityLabel: String { // NEXT_MAJOR_VERSION: - To be replaced with translation strings. - fileName.starts(with: "credit") ? "PayPal Credit" : "PayPal" + if fundingSource == .card { + return "Credit Card" + } + + return fileName.starts(with: "credit") ? "PayPal Credit" : "PayPal" } // MARK: - Private Properties @@ -31,6 +35,9 @@ extension PaymentButton { case .credit: imageAssetString += "credit_" + + case .card: + imageAssetString += "card_" } switch color { diff --git a/Sources/PaymentButtons/PaymentButtonFundingSource.swift b/Sources/PaymentButtons/PaymentButtonFundingSource.swift index b6f60b57a..e79a164bc 100644 --- a/Sources/PaymentButtons/PaymentButtonFundingSource.swift +++ b/Sources/PaymentButtons/PaymentButtonFundingSource.swift @@ -5,4 +5,5 @@ public enum PaymentButtonFundingSource: String { case payPal = "PayPal" case payLater = "Pay Later" case credit = "Credit" + case card = "Card" } diff --git a/Sources/PaymentButtons/PaymentButtonLabel.swift b/Sources/PaymentButtons/PaymentButtonLabel.swift index 4be605489..d91ac1113 100644 --- a/Sources/PaymentButtons/PaymentButtonLabel.swift +++ b/Sources/PaymentButtons/PaymentButtonLabel.swift @@ -15,6 +15,9 @@ public enum PaymentButtonLabel: String { /// Add "Pay later" to the right of button's logo, only used for PayPalPayLaterButton case payLater = "Pay Later" + /// Add "Debit or Credit Card" to the right of button's logo, only used for CardButton + case card = "Debit or Credit Card" + enum Position { case prefix case suffix @@ -22,7 +25,7 @@ public enum PaymentButtonLabel: String { var position: Position { switch self { - case .checkout, .buyNow, .payLater: + case .card, .checkout, .buyNow, .payLater: return .suffix case .payWith: diff --git a/UnitTests/PayPalWebPaymentsTests/Mocks/MockUpdateClientConfigAPI.swift b/UnitTests/PayPalWebPaymentsTests/Mocks/MockUpdateClientConfigAPI.swift new file mode 100644 index 000000000..368feddb5 --- /dev/null +++ b/UnitTests/PayPalWebPaymentsTests/Mocks/MockUpdateClientConfigAPI.swift @@ -0,0 +1,24 @@ +import Foundation +@testable import PayPalWebPayments +@testable import CorePayments + +class MockClientConfigAPI: UpdateClientConfigAPI { + + var stubSetupTokenResponse: ClientConfigResponse? + var stubError: Error? + + var paypalWebRequest: PayPalWebCheckoutRequest? + + override func updateClientConfig(request: PayPalWebCheckoutRequest) async throws -> ClientConfigResponse { + + if let stubError { + throw stubError + } + + if let stubSetupTokenResponse { + return stubSetupTokenResponse + } + + throw CoreSDKError(code: 0, domain: "", errorDescription: "Stubbed responses not implemented for this mock.") + } +} diff --git a/UnitTests/PayPalWebPaymentsTests/PayPalWebCheckoutClient_Tests.swift b/UnitTests/PayPalWebPaymentsTests/PayPalWebCheckoutClient_Tests.swift index 497c46445..fbf666c63 100644 --- a/UnitTests/PayPalWebPaymentsTests/PayPalWebCheckoutClient_Tests.swift +++ b/UnitTests/PayPalWebPaymentsTests/PayPalWebCheckoutClient_Tests.swift @@ -10,16 +10,21 @@ class PayPalClient_Tests: XCTestCase { var mockWebAuthenticationSession: MockWebAuthenticationSession! var payPalClient: PayPalWebCheckoutClient! var mockNetworkingClient: MockNetworkingClient! + var mockClientConfigAPI: UpdateClientConfigAPI! + override func setUp() { super.setUp() config = CoreConfig(clientID: "testClientID", environment: .sandbox) mockWebAuthenticationSession = MockWebAuthenticationSession() mockNetworkingClient = MockNetworkingClient(http: MockHTTP(coreConfig: config)) - + mockClientConfigAPI = UpdateClientConfigAPI(coreConfig: config, networkingClient: mockNetworkingClient) + + payPalClient = PayPalWebCheckoutClient( config: config, networkingClient: mockNetworkingClient, + clientConfigAPI: mockClientConfigAPI, webAuthenticationSession: mockWebAuthenticationSession ) } @@ -36,6 +41,7 @@ class PayPalClient_Tests: XCTestCase { let payPalClient = PayPalWebCheckoutClient( config: config, networkingClient: mockNetworkingClient, + clientConfigAPI: mockClientConfigAPI, webAuthenticationSession: mockWebAuthenticationSession ) diff --git a/UnitTests/PayPalWebPaymentsTests/UpdateClientConfigAP_Tests.swift b/UnitTests/PayPalWebPaymentsTests/UpdateClientConfigAP_Tests.swift new file mode 100644 index 000000000..6c564419f --- /dev/null +++ b/UnitTests/PayPalWebPaymentsTests/UpdateClientConfigAP_Tests.swift @@ -0,0 +1,93 @@ +import Foundation +import XCTest +@testable import PayPalWebPayments +@testable import CorePayments +@testable import TestShared + +class UpdateClientConfigAPI_Tests: XCTestCase { + + // MARK: - Helper Properties + + var sut: UpdateClientConfigAPI! + var mockNetworkingClient: MockNetworkingClient! + let coreConfig = CoreConfig(clientID: "fake-client-id", environment: .sandbox) + let request = PayPalWebCheckoutRequest( + orderID: "testID", + fundingSource: .card + ) + + // MARK: - Test Lifecycle + + override func setUp() { + super.setUp() + + let mockHTTP = MockHTTP(coreConfig: coreConfig) + mockNetworkingClient = MockNetworkingClient(http: mockHTTP) + sut = UpdateClientConfigAPI(coreConfig: coreConfig, networkingClient: mockNetworkingClient) + } + + // MARK: - updateSetupToken() + + func testUpdateClientConfig_constructsGraphQLRequest() async { + let expectedQueryString = """ + mutation UpdateClientConfig( + $orderID: String!, + $fundingSource: ButtonFundingSourceType!, + $integrationArtifact: IntegrationArtifactType!, + $userExperienceFlow: UserExperienceFlowType!, + $productFlow: ProductFlowType!, + $buttonSessionID: String + ) { + updateClientConfig( + token: $orderID + fundingSource: $fundingSource + integrationArtifact: $integrationArtifact, + userExperienceFlow: $userExperienceFlow, + productFlow: $productFlow, + buttonSessionID: $buttonSessionID + ) + } + """ + + _ = try? await sut.updateClientConfig(request: request) + XCTAssertEqual(mockNetworkingClient.capturedGraphQLRequest?.query, expectedQueryString) + XCTAssertEqual(mockNetworkingClient.capturedGraphQLRequest?.queryNameForURL, "UpdateClientConfig") + + let variables = mockNetworkingClient.capturedGraphQLRequest?.variables as! UpdateClientConfigVariables + XCTAssertEqual(variables.orderID, "testID") + XCTAssertEqual(variables.integrationArtifact, "PAYPAL_JS_SDK") + XCTAssertEqual(variables.userExperienceFlow, "INCONTEXT") + XCTAssertEqual(variables.productFlow, "SMART_PAYMENT_BUTTONS") + } + + func testUpdateClientConfig_whenNetworkingClientError_bubblesError() async { + mockNetworkingClient.stubHTTPError = CoreSDKError(code: 123, domain: "api-client-error", errorDescription: "error-desc") + + do { + _ = try await sut.updateClientConfig(request: request) + XCTFail("Expected error throw.") + } catch { + let error = error as! CoreSDKError + XCTAssertEqual(error.domain, "api-client-error") + XCTAssertEqual(error.code, 123) + XCTAssertEqual(error.localizedDescription, "error-desc") + } + } + + func testUpdateSetupToken_whenSuccess_returnsParsedUpdateSetupTokenResponse() async throws { + let successsResponseJSON = """ + { + "data": { + "updateClientConfig": "some value" + } + } + """ + + let data = successsResponseJSON.data(using: .utf8) + let stubbedHTTPResponse = HTTPResponse(status: 200, body: data) + mockNetworkingClient.stubHTTPResponse = stubbedHTTPResponse + + let response = try await sut.updateClientConfig(request: request) + XCTAssertNotNil(response.updateClientConfig) + } +} diff --git a/UnitTests/PaymentButtonsTests/CardButton_Tests.swift b/UnitTests/PaymentButtonsTests/CardButton_Tests.swift new file mode 100644 index 000000000..4e6f99947 --- /dev/null +++ b/UnitTests/PaymentButtonsTests/CardButton_Tests.swift @@ -0,0 +1,38 @@ +import XCTest +@testable import PaymentButtons + +class CardButton_Tests: XCTestCase { + + // MARK: - CardButton for UIKit + + func testInit_whenCardButtonCreated_hasUIImageFromAssets() { + let sut = CardButton() + XCTAssertEqual(sut.imageView?.image, UIImage(named: "card-black")) + } + + func testInit_whenPayPalButtonCreated_hasDefaultUIValuess() { + let sut = CardButton() + XCTAssertEqual(sut.edges, PaymentButtonEdges.softEdges) + XCTAssertEqual(sut.size, PaymentButtonSize.collapsed) + XCTAssertEqual(sut.color, PaymentButtonColor.black) + XCTAssertEqual(sut.label, PaymentButtonLabel.card) + XCTAssertNil(sut.insets) + } + + // MARK: - PayPalButton.Representable for SwiftUI + + func testMakeCoordinator_whenOnActionIsCalled_executesActionPassedInInitializer() { + let expectation = expectation(description: "Action is called") + let sut = CardButton.Representable { + expectation.fulfill() + } + let coordinator = sut.makeCoordinator() + + coordinator.onAction(self) + waitForExpectations(timeout: 1) { error in + if error != nil { + XCTFail("Action passed in CardButton.Representable was never called.") + } + } + } +} diff --git a/UnitTests/TestShared/MockNetworkingClient.swift b/UnitTests/TestShared/MockNetworkingClient.swift index 34d55af3a..becae1bec 100644 --- a/UnitTests/TestShared/MockNetworkingClient.swift +++ b/UnitTests/TestShared/MockNetworkingClient.swift @@ -23,7 +23,7 @@ class MockNetworkingClient: NetworkingClient { throw CoreSDKError(code: 0, domain: "", errorDescription: "Stubbed responses not implemented for this mock.") } - override func fetch(request: GraphQLRequest) async throws -> HTTPResponse { + override func fetch(request: GraphQLRequest, clientContext: String? = nil) async throws -> HTTPResponse { capturedGraphQLRequest = request if let stubHTTPError {