From 00107331565548097c64294c6e8809536f0d6fab Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Thu, 10 Aug 2023 09:57:15 -0500 Subject: [PATCH 01/19] Remove APIRequest conformance from ClientIDRequest in Demo (#178) --- Demo/Demo/Models/ClientIDRequest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Demo/Demo/Models/ClientIDRequest.swift b/Demo/Demo/Models/ClientIDRequest.swift index 265ea80f4..08a726a84 100644 --- a/Demo/Demo/Models/ClientIDRequest.swift +++ b/Demo/Demo/Models/ClientIDRequest.swift @@ -1,7 +1,7 @@ import Foundation import CorePayments -struct ClientIDRequest: APIRequest { +struct ClientIDRequest { typealias ResponseType = ClientIDResponse From 4a43375b8aca06d35ac40efda2af993e95fc8f6f Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Thu, 10 Aug 2023 12:28:44 -0700 Subject: [PATCH 02/19] Remove Eligibility feature (#180) * Remove Eligibility feature * remove two types for Eligibility feature --- PayPal.xcodeproj/project.pbxproj | 36 ----- Sources/CorePayments/Eligibility.swift | 11 -- Sources/CorePayments/EligibilityAPI.swift | 52 ------- .../GraphQL/FundingEligibilityIntent.swift | 8 -- .../GraphQL/FundingEligibilityQuery.swift | 83 ----------- .../GraphQL/FundingEligibilityResponse.swift | 19 --- .../SupportedCountryCurrencyType.swift | 3 - .../GraphQL/SupportedPaymentMethodsType.swift | 5 - .../EligibilityAPI_Tests.swift | 135 ------------------ .../EligibilityParsing_Tests.swift | 87 ----------- .../GraphQLClient_Tests.swift | 24 ---- 11 files changed, 463 deletions(-) delete mode 100644 Sources/CorePayments/Eligibility.swift delete mode 100644 Sources/CorePayments/EligibilityAPI.swift delete mode 100644 Sources/CorePayments/Networking/GraphQL/FundingEligibilityIntent.swift delete mode 100644 Sources/CorePayments/Networking/GraphQL/FundingEligibilityQuery.swift delete mode 100644 Sources/CorePayments/Networking/GraphQL/FundingEligibilityResponse.swift delete mode 100644 Sources/CorePayments/Networking/GraphQL/SupportedCountryCurrencyType.swift delete mode 100644 Sources/CorePayments/Networking/GraphQL/SupportedPaymentMethodsType.swift delete mode 100644 UnitTests/PaymentsCoreTests/EligibilityAPI_Tests.swift delete mode 100644 UnitTests/PaymentsCoreTests/EligibilityParsing_Tests.swift diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index f21e004e8..accd623d5 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -17,7 +17,6 @@ 3B80D50C2A27979000D2EAC4 /* FailingJSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */; }; 3D1763A22720722A00652E1C /* CardResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1763A12720722A00652E1C /* CardResult.swift */; }; 3DC42BA927187E8300B71645 /* ErrorResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC42BA827187E8300B71645 /* ErrorResponse.swift */; }; - 537804FD28760BE2006442BD /* EligibilityAPI_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537804FC28760BE2006442BD /* EligibilityAPI_Tests.swift */; }; 53A2A4E228A182AC0093441C /* NativeCheckoutProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A2A4E128A182AC0093441C /* NativeCheckoutProvider.swift */; }; 80132D7229008C000088D30D /* TestShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80E743F8270E40CE00BACECA /* TestShared.framework */; }; 8021B69029144E6D000FBC54 /* PayPalCoreConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8021B68F29144E6D000FBC54 /* PayPalCoreConstants.swift */; }; @@ -132,18 +131,10 @@ CBC16DE029EE2F8200307117 /* PayPalNativePaysheetAction_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC16DDF29EE2F8200307117 /* PayPalNativePaysheetAction_Tests.swift */; }; CBC16DF629EECCB900307117 /* NativeCheckoutProvider_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC16DF529EECCB900307117 /* NativeCheckoutProvider_Tests.swift */; }; CBD6004728D0C24A00C3EFF6 /* MockPayPalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD6004628D0C24900C3EFF6 /* MockPayPalDelegate.swift */; }; - E6022E772857C6BE008B0E27 /* Eligibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5324C2D0283EB45A00DBC34F /* Eligibility.swift */; }; - E6022E782857C6BE008B0E27 /* EligibilityAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5324C2CB283EB40F00DBC34F /* EligibilityAPI.swift */; }; - E6022E792857C6BE008B0E27 /* FundingEligibilityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64623292836A90E008AC8E1 /* FundingEligibilityIntent.swift */; }; - E6022E7A2857C6BE008B0E27 /* SupportedCountryCurrencyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E646232B2836A9A9008AC8E1 /* SupportedCountryCurrencyType.swift */; }; - E6022E7B2857C6BE008B0E27 /* SupportedPaymentMethodsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E646232D2836A9CF008AC8E1 /* SupportedPaymentMethodsType.swift */; }; - E6022E7C2857C6BE008B0E27 /* FundingEligibilityResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E646232F2836AA71008AC8E1 /* FundingEligibilityResponse.swift */; }; - E6022E7F2857C6BE008B0E27 /* FundingEligibilityQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E646231E2836A645008AC8E1 /* FundingEligibilityQuery.swift */; }; E6022E802857C6BE008B0E27 /* GraphQLQueryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64623222836A69E008AC8E1 /* GraphQLQueryResponse.swift */; }; E6022E822857C6BE008B0E27 /* GraphQLError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64623262836A81E008AC8E1 /* GraphQLError.swift */; }; E6022E832857C6BE008B0E27 /* GraphQLQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E646231B28369B9B008AC8E1 /* GraphQLQuery.swift */; }; E6022E842857C6BE008B0E27 /* GraphQLClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64623372836AFC1008AC8E1 /* GraphQLClient.swift */; }; - E6022E872857C81E008B0E27 /* EligibilityParsing_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6022E862857C81E008B0E27 /* EligibilityParsing_Tests.swift */; }; E64763712899B60C00074113 /* MockAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64763702899B60C00074113 /* MockAPIClient.swift */; }; E699EC16285A388E0044A753 /* GraphQLClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E699EC15285A388E0044A753 /* GraphQLClient_Tests.swift */; }; /* End PBXBuildFile section */ @@ -209,9 +200,6 @@ 3D25238127344F330099E4EB /* NativeCheckoutStartable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NativeCheckoutStartable.swift; path = Sources/PayPalNativePayments/NativeCheckoutStartable.swift; sourceTree = SOURCE_ROOT; }; 3D25238B273979170099E4EB /* MockNativeCheckoutProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNativeCheckoutProvider.swift; sourceTree = ""; }; 3DC42BA827187E8300B71645 /* ErrorResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorResponse.swift; sourceTree = ""; }; - 5324C2CB283EB40F00DBC34F /* EligibilityAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EligibilityAPI.swift; sourceTree = ""; }; - 5324C2D0283EB45A00DBC34F /* Eligibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Eligibility.swift; sourceTree = ""; }; - 537804FC28760BE2006442BD /* EligibilityAPI_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EligibilityAPI_Tests.swift; sourceTree = ""; }; 53A2A4E128A182AC0093441C /* NativeCheckoutProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeCheckoutProvider.swift; sourceTree = ""; }; 8021B68F29144E6D000FBC54 /* PayPalCoreConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalCoreConstants.swift; sourceTree = ""; }; 802C4A732945670400896A5D /* MockHTTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHTTP.swift; sourceTree = ""; }; @@ -320,15 +308,9 @@ CBC16DDF29EE2F8200307117 /* PayPalNativePaysheetAction_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalNativePaysheetAction_Tests.swift; sourceTree = ""; }; CBC16DF529EECCB900307117 /* NativeCheckoutProvider_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeCheckoutProvider_Tests.swift; sourceTree = ""; }; CBD6004628D0C24900C3EFF6 /* MockPayPalDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPayPalDelegate.swift; sourceTree = ""; }; - E6022E862857C81E008B0E27 /* EligibilityParsing_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EligibilityParsing_Tests.swift; sourceTree = ""; }; E646231B28369B9B008AC8E1 /* GraphQLQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLQuery.swift; sourceTree = ""; }; - E646231E2836A645008AC8E1 /* FundingEligibilityQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FundingEligibilityQuery.swift; sourceTree = ""; }; E64623222836A69E008AC8E1 /* GraphQLQueryResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLQueryResponse.swift; sourceTree = ""; }; E64623262836A81E008AC8E1 /* GraphQLError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLError.swift; sourceTree = ""; }; - E64623292836A90E008AC8E1 /* FundingEligibilityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FundingEligibilityIntent.swift; sourceTree = ""; }; - E646232B2836A9A9008AC8E1 /* SupportedCountryCurrencyType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedCountryCurrencyType.swift; sourceTree = ""; }; - E646232D2836A9CF008AC8E1 /* SupportedPaymentMethodsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedPaymentMethodsType.swift; sourceTree = ""; }; - E646232F2836AA71008AC8E1 /* FundingEligibilityResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FundingEligibilityResponse.swift; sourceTree = ""; }; E64623372836AFC1008AC8E1 /* GraphQLClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLClient.swift; sourceTree = ""; }; E64763702899B60C00074113 /* MockAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAPIClient.swift; sourceTree = ""; }; E699EC15285A388E0044A753 /* GraphQLClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLClient_Tests.swift; sourceTree = ""; }; @@ -485,11 +467,9 @@ 8036C1DE270F9BCF00C0F091 /* PaymentsCoreTests */ = { isa = PBXGroup; children = ( - 537804FC28760BE2006442BD /* EligibilityAPI_Tests.swift */, 8036C1E0270F9BE700C0F091 /* APIClient_Tests.swift */, 802C4A752945676E00896A5D /* AnalyticsService_Tests.swift */, 8036C1E1270F9BE700C0F091 /* Environment_Tests.swift */, - E6022E862857C81E008B0E27 /* EligibilityParsing_Tests.swift */, E699EC15285A388E0044A753 /* GraphQLClient_Tests.swift */, 808EEA80291321FE001B6765 /* AnalyticsEventRequest_Tests.swift */, 80FC261C29847AC7008EC841 /* HTTP_Tests.swift */, @@ -511,8 +491,6 @@ isa = PBXGroup; children = ( BE4F784827EB629100FF4C0E /* WebAuthenticationSession.swift */, - 5324C2D0283EB45A00DBC34F /* Eligibility.swift */, - 5324C2CB283EB40F00DBC34F /* EligibilityAPI.swift */, 06CE009826F3D1660000CC46 /* CoreConfig.swift */, 065A4DBB26FCD8080007014A /* CoreSDKError.swift */, BEA100E526EF9EDA0036A6A5 /* Networking */, @@ -725,15 +703,10 @@ E646231928369B71008AC8E1 /* GraphQL */ = { isa = PBXGroup; children = ( - E64623292836A90E008AC8E1 /* FundingEligibilityIntent.swift */, - E646231E2836A645008AC8E1 /* FundingEligibilityQuery.swift */, - E646232F2836AA71008AC8E1 /* FundingEligibilityResponse.swift */, E64623372836AFC1008AC8E1 /* GraphQLClient.swift */, E64623262836A81E008AC8E1 /* GraphQLError.swift */, E646231B28369B9B008AC8E1 /* GraphQLQuery.swift */, E64623222836A69E008AC8E1 /* GraphQLQueryResponse.swift */, - E646232B2836A9A9008AC8E1 /* SupportedCountryCurrencyType.swift */, - E646232D2836A9CF008AC8E1 /* SupportedPaymentMethodsType.swift */, ); path = GraphQL; sourceTree = ""; @@ -1255,13 +1228,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 537804FD28760BE2006442BD /* EligibilityAPI_Tests.swift in Sources */, E699EC16285A388E0044A753 /* GraphQLClient_Tests.swift in Sources */, 808EEA81291321FE001B6765 /* AnalyticsEventRequest_Tests.swift in Sources */, 8036C1E5270F9BE700C0F091 /* Environment_Tests.swift in Sources */, 80FC261D29847AC7008EC841 /* HTTP_Tests.swift in Sources */, 802C4A762945676E00896A5D /* AnalyticsService_Tests.swift in Sources */, - E6022E872857C81E008B0E27 /* EligibilityParsing_Tests.swift in Sources */, 807BF5912A2A5D48002F32B3 /* HTTPResponseParser_Tests.swift in Sources */, 8036C1E4270F9BE700C0F091 /* APIClient_Tests.swift in Sources */, ); @@ -1271,14 +1242,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E6022E772857C6BE008B0E27 /* Eligibility.swift in Sources */, - E6022E782857C6BE008B0E27 /* EligibilityAPI.swift in Sources */, - E6022E792857C6BE008B0E27 /* FundingEligibilityIntent.swift in Sources */, - E6022E7A2857C6BE008B0E27 /* SupportedCountryCurrencyType.swift in Sources */, - E6022E7B2857C6BE008B0E27 /* SupportedPaymentMethodsType.swift in Sources */, - E6022E7C2857C6BE008B0E27 /* FundingEligibilityResponse.swift in Sources */, 807C5E67291027D400ECECD8 /* AnalyticsEventRequest.swift in Sources */, - E6022E7F2857C6BE008B0E27 /* FundingEligibilityQuery.swift in Sources */, E6022E802857C6BE008B0E27 /* GraphQLQueryResponse.swift in Sources */, 80E643832A1EBBD2008FD705 /* HTTPResponse.swift in Sources */, 807C5E6929102D9800ECECD8 /* AnalyticsEventData.swift in Sources */, diff --git a/Sources/CorePayments/Eligibility.swift b/Sources/CorePayments/Eligibility.swift deleted file mode 100644 index faf74a0fa..000000000 --- a/Sources/CorePayments/Eligibility.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -/// Class that represents merchant's eligibility for a set of payment methods -struct Eligibility { - - var isVenmoEligible: Bool - var isPaypalEligible: Bool - var isPaypalCreditEligible: Bool - var isPayLaterEligible: Bool - var isCreditCardEligible: Bool -} diff --git a/Sources/CorePayments/EligibilityAPI.swift b/Sources/CorePayments/EligibilityAPI.swift deleted file mode 100644 index e26411b4a..000000000 --- a/Sources/CorePayments/EligibilityAPI.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation - -/// API that return merchant's eligibility for different payment methods: Venmo, PayPal, PayPal Credit, Pay Later & credit card -class EligibilityAPI { - - private let graphQLClient: GraphQLClient - private let apiClient: APIClient - private let coreConfig: CoreConfig - - /// Initialize the eligibility API to check for payment methods eligibility - /// - Parameter coreConfig: configuration object - convenience init(coreConfig: CoreConfig) { - self.init( - coreConfig: coreConfig, - apiClient: APIClient(coreConfig: coreConfig), - graphQLClient: GraphQLClient(environment: coreConfig.environment) - ) - } - - init(coreConfig: CoreConfig, apiClient: APIClient, graphQLClient: GraphQLClient) { - self.coreConfig = coreConfig - self.apiClient = apiClient - self.graphQLClient = graphQLClient - } - - /// Checks merchants eligibility for different payment methods. - /// - Returns: a result object with either eligibility or an error - func checkEligibility() async throws -> Result { - let clientID = coreConfig.clientID - let fundingEligibilityQuery = FundingEligibilityQuery( - clientID: clientID, - fundingEligibilityIntent: FundingEligibilityIntent.CAPTURE, - currencyCode: SupportedCountryCurrencyType.USD, - enableFunding: [SupportedPaymentMethodsType.VENMO] - ) - let response: GraphQLQueryResponse - = try await graphQLClient.executeQuery(query: fundingEligibilityQuery) - if response.data == nil { - return Result.failure(GraphQLError(message: "error fetching eligibility", extensions: nil)) - } else { - let eligibility = response.data?.fundingEligibility - return Result.success( - Eligibility( - isVenmoEligible: eligibility?.venmo.eligible ?? false, - isPaypalEligible: eligibility?.paypal.eligible ?? false, - isPaypalCreditEligible: eligibility?.credit.eligible ?? false, - isPayLaterEligible: eligibility?.paylater.eligible ?? false, - isCreditCardEligible: eligibility?.card.eligible ?? false) - ) - } - } -} diff --git a/Sources/CorePayments/Networking/GraphQL/FundingEligibilityIntent.swift b/Sources/CorePayments/Networking/GraphQL/FundingEligibilityIntent.swift deleted file mode 100644 index cf39d30b3..000000000 --- a/Sources/CorePayments/Networking/GraphQL/FundingEligibilityIntent.swift +++ /dev/null @@ -1,8 +0,0 @@ -enum FundingEligibilityIntent: String { - case SALE - case CAPTURE - case AUTHORIZE - case ORDER - case TOKENIZE - case SUBSCRIPTION -} diff --git a/Sources/CorePayments/Networking/GraphQL/FundingEligibilityQuery.swift b/Sources/CorePayments/Networking/GraphQL/FundingEligibilityQuery.swift deleted file mode 100644 index 47d512cf1..000000000 --- a/Sources/CorePayments/Networking/GraphQL/FundingEligibilityQuery.swift +++ /dev/null @@ -1,83 +0,0 @@ -class FundingEligibilityQuery: GraphQLQuery { - - var query: String { - return rawQuery - } - - var variables: [String: Any] { - return [ - paramClientId: clientID, - paramIntent: fundingEligibilityIntent.rawValue, - paramCurrency: currencyCode.rawValue, - paramEnableFunding: enableFunding.toStringArray() - ] - } - - let clientID: String - let fundingEligibilityIntent: FundingEligibilityIntent - let currencyCode: SupportedCountryCurrencyType - var enableFunding: [SupportedPaymentMethodsType] - - init( - clientID: String, - fundingEligibilityIntent: FundingEligibilityIntent, - currencyCode: SupportedCountryCurrencyType, - enableFunding: [SupportedPaymentMethodsType] - ) { - self.clientID = clientID - self.fundingEligibilityIntent = fundingEligibilityIntent - self.currencyCode = currencyCode - self.enableFunding = enableFunding - } - - let paramClientId = "clientId" - let paramIntent = "intent" - let paramCurrency = "currency" - let paramEnableFunding = "enableFunding" - - private let rawQuery = """ - query getEligibility( - $clientId: String!, - $intent: FundingEligibilityIntent!, - $currency: SupportedCountryCurrencies!, - $enableFunding: [SupportedPaymentMethodsType] - ){ - fundingEligibility( - clientId: $clientId, - intent: $intent - currency: $currency, - enableFunding: $enableFunding){ - venmo{ - eligible - reasons - } - card{ - eligible - } - paypal{ - eligible - reasons - } - paylater{ - eligible - reasons - } - credit{ - eligible - reasons - } - } - } - """ -} - -extension Array -where Element == SupportedPaymentMethodsType { - func toStringArray() -> [String] { - var stringArray: [String] = [] - for element in self { - stringArray += [element.rawValue] - } - return stringArray - } -} diff --git a/Sources/CorePayments/Networking/GraphQL/FundingEligibilityResponse.swift b/Sources/CorePayments/Networking/GraphQL/FundingEligibilityResponse.swift deleted file mode 100644 index 642fb88a9..000000000 --- a/Sources/CorePayments/Networking/GraphQL/FundingEligibilityResponse.swift +++ /dev/null @@ -1,19 +0,0 @@ -struct FundingEligibilityResponse: Codable { - - let fundingEligibility: FundingEligibility -} - -struct FundingEligibility: Codable { - - let venmo: SupportedPaymentMethodsTypeEligibility - let card: SupportedPaymentMethodsTypeEligibility - let paypal: SupportedPaymentMethodsTypeEligibility - let paylater: SupportedPaymentMethodsTypeEligibility - let credit: SupportedPaymentMethodsTypeEligibility -} - -struct SupportedPaymentMethodsTypeEligibility: Codable { - - let eligible: Bool - let reasons: [String]? -} diff --git a/Sources/CorePayments/Networking/GraphQL/SupportedCountryCurrencyType.swift b/Sources/CorePayments/Networking/GraphQL/SupportedCountryCurrencyType.swift deleted file mode 100644 index 71a31c60d..000000000 --- a/Sources/CorePayments/Networking/GraphQL/SupportedCountryCurrencyType.swift +++ /dev/null @@ -1,3 +0,0 @@ -enum SupportedCountryCurrencyType: String { - case USD -} diff --git a/Sources/CorePayments/Networking/GraphQL/SupportedPaymentMethodsType.swift b/Sources/CorePayments/Networking/GraphQL/SupportedPaymentMethodsType.swift deleted file mode 100644 index a1a2358e2..000000000 --- a/Sources/CorePayments/Networking/GraphQL/SupportedPaymentMethodsType.swift +++ /dev/null @@ -1,5 +0,0 @@ -enum SupportedPaymentMethodsType: String { - case VENMO - case CREDIT - case PAYLATER -} diff --git a/UnitTests/PaymentsCoreTests/EligibilityAPI_Tests.swift b/UnitTests/PaymentsCoreTests/EligibilityAPI_Tests.swift deleted file mode 100644 index 70e96b0a0..000000000 --- a/UnitTests/PaymentsCoreTests/EligibilityAPI_Tests.swift +++ /dev/null @@ -1,135 +0,0 @@ -import XCTest -@testable import CorePayments -@testable import TestShared - -class EligibilityAPI_Tests: XCTestCase { - - let mockClientID = "mockClientId" - let mockURLSession = MockURLSession() - var coreConfig: CoreConfig! - var graphQLClient: GraphQLClient! - var eligibilityAPI: EligibilityAPI! - var apiClient: MockAPIClient! - - override func setUp() { - super.setUp() - coreConfig = CoreConfig(clientID: mockClientID, environment: .sandbox) - apiClient = MockAPIClient(coreConfig: coreConfig) - graphQLClient = GraphQLClient(environment: .sandbox, urlSession: mockURLSession) - } - - func testCheckEligibilityWithSuccessResponse() async throws { - mockURLSession.cannedError = nil - mockURLSession.cannedURLResponse = HTTPURLResponse( - // swiftlint:disable:next force_unwrapping - url: URL(string: "www.fake.com")!, - statusCode: 200, - httpVersion: "1", - headerFields: ["Paypal-Debug-Id": "454532"] - ) - mockURLSession.cannedJSONData = validFundingEligibilityResponse - eligibilityAPI = EligibilityAPI(coreConfig: coreConfig, apiClient: apiClient, graphQLClient: graphQLClient) - let result = try await eligibilityAPI.checkEligibility() - switch result { - case .success(let eligibility): - XCTAssertTrue(eligibility.isVenmoEligible) - XCTAssertTrue(eligibility.isPaypalEligible) - XCTAssertFalse(eligibility.isCreditCardEligible) - case .failure(let error): - XCTAssertNil(error) - } - } - func testCheckEligibilityErrorResponse() async throws { - mockURLSession.cannedURLResponse = HTTPURLResponse( - url: URL(string: "www.fake.com")!, - statusCode: 200, - httpVersion: "1", - headerFields: ["Paypal-Debug-Id": "454532"] - ) - mockURLSession.cannedJSONData = notValidFundingEligibilityResponse - eligibilityAPI = EligibilityAPI(coreConfig: coreConfig, apiClient: apiClient, graphQLClient: graphQLClient) - let result = try await eligibilityAPI.checkEligibility() - switch result { - case .success(let eligibility): - XCTAssertNil(eligibility) - case .failure(let failure): - XCTAssertNotNil(failure) - } - } - let notValidFundingEligibilityResponse = """ - { - - } - """ - let validFundingEligibilityResponse = """ - { - "data": { - "fundingEligibility": { - "venmo": { - "eligible": true, - "reasons": [ - "isPaymentMethodEnabled", - "isMSPEligible", - "isUnilateralPaymentSupported", - "isEnvEligible", - "isMerchantCountryEligible", - "isBuyerCountryEligible", - "isIntentEligible", - "isCommitEligible", - "isVaultEligible", - "isCurrencyEligible", - "isPaymentMethodDisabled", - "isDeviceEligible", - "VENMO OPT-IN WITH ENABLE_FUNDING" - ] - }, - "card": { - "eligible": false - }, - "paypal": { - "eligible": true, - "reasons": [ - "isPaymentMethodEnabled", - "isMSPEligible", - "isUnilateralPaymentSupported", - "isEnvEligible", - "isMerchantCountryEligible", - "isBuyerCountryEligible", - "isIntentEligible", - "isCommitEligible", - "isVaultEligible", - "isCurrencyEligible", - "isPaymentMethodDisabled", - "isDeviceEligible" - ] - }, - "paylater": { - "eligible": true, - "reasons": [ - "isPaymentMethodEnabled", - "isMSPEligible", - "isUnilateralPaymentSupported", - "isEnvEligible", - "isMerchantCountryEligible", - "isBuyerCountryEligible", - "isIntentEligible", - "isCommitEligible", - "isVaultEligible", - "isCurrencyEligible", - "isPaymentMethodDisabled", - "isDeviceEligible", - "CRC OFFERS SERV CALLED: Trmt_xo_xobuyernodeserv_call_crcoffersserv", - "CRC OFFERS SERV ELIGIBLE" - ] - }, - "credit": { - "eligible": false, - "reasons": [ - "INELIGIBLE DUE TO PAYLATER ELIGIBLE" - ] - } - } - } - } - """ -} diff --git a/UnitTests/PaymentsCoreTests/EligibilityParsing_Tests.swift b/UnitTests/PaymentsCoreTests/EligibilityParsing_Tests.swift deleted file mode 100644 index a84ef8375..000000000 --- a/UnitTests/PaymentsCoreTests/EligibilityParsing_Tests.swift +++ /dev/null @@ -1,87 +0,0 @@ -import XCTest -@testable import CorePayments - -class EligibilityParsing_Tests: XCTestCase { - - func testFundingEligibility_matchesResponse() throws { - if let jsonData = validFundingEligibilityResponse.data(using: .utf8) { - let fundingEligibility = try JSONDecoder().decode(FundingEligibilityResponse.self, from: jsonData).fundingEligibility - XCTAssertEqual(fundingEligibility.venmo.eligible, true) - XCTAssertEqual(fundingEligibility.card.eligible, false) - XCTAssertEqual(fundingEligibility.paypal.eligible, true) - XCTAssertEqual(fundingEligibility.paylater.eligible, true) - } else { - XCTFail() - } - } - - let validFundingEligibilityResponse = """ - { - "fundingEligibility": { - "venmo": { - "eligible": true, - "reasons": [ - "isPaymentMethodEnabled", - "isMSPEligible", - "isUnilateralPaymentSupported", - "isEnvEligible", - "isMerchantCountryEligible", - "isBuyerCountryEligible", - "isIntentEligible", - "isCommitEligible", - "isVaultEligible", - "isCurrencyEligible", - "isPaymentMethodDisabled", - "isDeviceEligible", - "VENMO OPT-IN WITH ENABLE_FUNDING" - ] - }, - "card": { - "eligible": false - }, - "paypal": { - "eligible": true, - "reasons": [ - "isPaymentMethodEnabled", - "isMSPEligible", - "isUnilateralPaymentSupported", - "isEnvEligible", - "isMerchantCountryEligible", - "isBuyerCountryEligible", - "isIntentEligible", - "isCommitEligible", - "isVaultEligible", - "isCurrencyEligible", - "isPaymentMethodDisabled", - "isDeviceEligible" - ] - }, - "paylater": { - "eligible": true, - "reasons": [ - "isPaymentMethodEnabled", - "isMSPEligible", - "isUnilateralPaymentSupported", - "isEnvEligible", - "isMerchantCountryEligible", - "isBuyerCountryEligible", - "isIntentEligible", - "isCommitEligible", - "isVaultEligible", - "isCurrencyEligible", - "isPaymentMethodDisabled", - "isDeviceEligible", - "CRC OFFERS SERV CALLED: Trmt_xo_xobuyernodeserv_call_crcoffersserv", - "CRC OFFERS SERV ELIGIBLE" - ] - }, - "credit": { - "eligible": false, - "reasons": [ - "INELIGIBLE DUE TO PAYLATER ELIGIBLE" - ] - } - } - } - """ -} diff --git a/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift b/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift index f1d65f778..426ffc8e5 100644 --- a/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift +++ b/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift @@ -26,15 +26,6 @@ class GraphQLClient_Tests: XCTestCase { mockURLSession.cannedJSONData = nil graphQLClient = GraphQLClient(environment: .sandbox, urlSession: mockURLSession) - - let fundingEligibilityQuery = FundingEligibilityQuery( - clientID: mockClientID, - fundingEligibilityIntent: FundingEligibilityIntent.CAPTURE, - currencyCode: SupportedCountryCurrencyType.USD, - enableFunding: [SupportedPaymentMethodsType.VENMO] - ) - - graphQLQuery = fundingEligibilityQuery } // MARK: - fetch() tests @@ -49,14 +40,6 @@ class GraphQLClient_Tests: XCTestCase { httpVersion: "1", headerFields: ["Paypal-Debug-Id": "454532"] ) - - do { - let response: GraphQLQueryResponse = try await graphQLClient.executeQuery(query: graphQLQuery) - XCTAssertTrue(response.data == nil) - } catch { - print(error.localizedDescription) - XCTFail("Expected success response") - } } func testGraphQLClient_verifyNonEmptyResponse() async throws { @@ -70,13 +53,6 @@ class GraphQLClient_Tests: XCTestCase { httpVersion: "1", headerFields: ["Paypal-Debug-Id": "454532"] ) - - do { - let response: GraphQLQueryResponse = try await graphQLClient.executeQuery(query: graphQLQuery) - XCTAssertTrue(response.data != nil) - } catch { - XCTAssertTrue(!error.localizedDescription.isEmpty) - } } let graphQLQueryResponseWithData = """ From 55b6c1b7ff3f02d60c2615ed0c24b86574a0bcbe Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:28:04 -0500 Subject: [PATCH 03/19] Remove `prefersEphemeralBrowser` setting from ASWebSession (#181) --- CHANGELOG.md | 2 ++ Sources/CorePayments/WebAuthenticationSession.swift | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d4e4369..57a393360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## unreleased * PayPalNativePayments * Bump `PayPalCheckout` to `1.0.0` +* PaymentsCore + * Allow `ASWebAuthenticationSession` used for PayPal Web & 3DS flows to share cookies with Safari (fixes #179) ## 0.0.9 (2023-06-23) * Breaking Changes diff --git a/Sources/CorePayments/WebAuthenticationSession.swift b/Sources/CorePayments/WebAuthenticationSession.swift index 6e27120a0..c57145a6d 100644 --- a/Sources/CorePayments/WebAuthenticationSession.swift +++ b/Sources/CorePayments/WebAuthenticationSession.swift @@ -15,7 +15,6 @@ public class WebAuthenticationSession: NSObject { completionHandler: sessionDidComplete ) - authenticationSession.prefersEphemeralWebBrowserSession = true authenticationSession.presentationContextProvider = context DispatchQueue.main.async { From 6e6ee65fdade779cd345bb10a6387c618172a2ae Mon Sep 17 00:00:00 2001 From: paypalsdks Date: Mon, 14 Aug 2023 16:32:10 +0000 Subject: [PATCH 04/19] Bump version to 0.0.10 --- CHANGELOG.md | 2 +- Demo/Demo/Info.plist | 2 +- PayPal.podspec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57a393360..62b43e7f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # PayPal iOS SDK Release Notes -## unreleased +## 0.0.10 (2023-08-14) * PayPalNativePayments * Bump `PayPalCheckout` to `1.0.0` * PaymentsCore diff --git a/Demo/Demo/Info.plist b/Demo/Demo/Info.plist index 6fd113979..5928ebc05 100644 --- a/Demo/Demo/Info.plist +++ b/Demo/Demo/Info.plist @@ -3,7 +3,7 @@ CFBundleShortVersionString - 0.0.9 + 0.0.10 UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/PayPal.podspec b/PayPal.podspec index f89ccbe2c..0ac342e0e 100644 --- a/PayPal.podspec +++ b/PayPal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "PayPal" - s.version = "0.0.9" + s.version = "0.0.10" s.summary = "The PayPal iOS SDK: Helps you accept card, PayPal, and alternative payment methods in your iOS app." s.homepage = "https://developer.paypal.com/home" s.license = "MIT" From a305fa50348f23ef214e5344fc49821decd5e454 Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Tue, 15 Aug 2023 08:17:45 -0700 Subject: [PATCH 05/19] UI for Vault with Purchase (#175) --- Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift b/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift index 36c16df7d..44a9716a2 100644 --- a/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift +++ b/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift @@ -35,12 +35,12 @@ struct SwiftUICardDemo: View { cvvText = cardFormatter.formatFieldWith(newValue, field: .cvv) } HStack { - Toggle("Should Vault", isOn: $shouldVaultSelected) + Toggle("Should Vault with Purchase", isOn: $shouldVaultSelected) + // TODO: turn on if vault with purchase on sample server is implemented + .disabled(true) Spacer() } - if shouldVaultSelected { - FloatingLabelTextField(placeholder: "Vault Customer ID (Optional)", text: $vaultCustomerID) - } + FloatingLabelTextField(placeholder: "Vault Customer ID (Optional)", text: $vaultCustomerID) Button("\(DemoSettings.intent.rawValue.capitalized) Order") { guard let card = baseViewModel.createCard( cardNumber: cardNumberText, From 7660350c4d41ed91bf61ab9f468f7cc488d2fe1f Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Wed, 16 Aug 2023 18:18:30 -0700 Subject: [PATCH 06/19] Vault without Purchase (#172) * vault without purchase * clean up cardClient initializer * Use associatedType for variables for GraphQLQuery protocol * moving setuptoken call to demo app * use APIRequest for merchantServer setupToken request * use enum for PaymentSourceInput for different payment options * get request for setup token details * move graphQL request, responses from Core to Card module * remove APIRequest use in demo app * Add VaultCardDelegate for VaultCardResult * Add payment token creation in demo app * remove duplicate UpdateSetupTokenQuery.swift * rename UpdateSetupTokenRequest to UpdateSetupTokenQuery * added newly named file reference to project * add mocks for unit tests, pr feedback * disable swiftlint for multiline query, target for card test * correct target for MockGraphQLClient * CardClient vault error tests * docStrings and CHANGELOG * PR feedback * PR feedback * PR feedback nodoc String for GraphQLQuery requestBody() * spacing in paymentTokenRequest * PR feedback * PR feedback from Jax 8/16/23 * PR feedback from Jax cleanup --- CHANGELOG.md | 7 ++ Demo/Demo.xcodeproj/project.pbxproj | 16 ++++ Demo/Demo/Models/PaymentTokenRequest.swift | 31 +++++++ Demo/Demo/Models/PaymentTokenResponse.swift | 38 +++++++++ Demo/Demo/Models/SetupTokenRequest.swift | 35 ++++++++ Demo/Demo/Models/SetupTokenResponse.swift | 6 ++ Demo/Demo/Networking/DemoMerchantAPI.swift | 83 ++++++++++++++++++- .../SwiftUIComponents/SwiftUICardDemo.swift | 17 ++++ Demo/Demo/ViewModels/BaseViewModel.swift | 32 ++++++- PayPal.xcodeproj/project.pbxproj | 28 +++++++ .../APIRequests/UpdateSetupTokenQuery.swift | 83 +++++++++++++++++++ .../UpdateSetupTokenResponse.swift | 17 ++++ Sources/CardPayments/CardClient.swift | 61 +++++++++++++- Sources/CardPayments/CardClientError.swift | 27 ++++++ Sources/CardPayments/CardVaultDelegate.swift | 20 +++++ Sources/CardPayments/Models/Card.swift | 7 +- .../Models/CardVaultRequest.swift | 20 +++++ .../CardPayments/Models/CardVaultResult.swift | 14 ++++ .../Networking/GraphQL/GraphQLClient.swift | 43 ++++++---- .../Networking/GraphQL/GraphQLError.swift | 5 ++ .../Networking/GraphQL/GraphQLQuery.swift | 6 +- .../GraphQL/GraphQLQueryResponse.swift | 5 +- .../Networking/URLSessionProtocol.swift | 3 +- .../CardPaymentsTests/CardClient_Tests.swift | 76 ++++++++++++++++- .../MockCardVaultDelegate.swift | 24 ++++++ .../CardPaymentsTests/MockGraphQLClient.swift | 20 +++++ .../GraphQLClient_Tests.swift | 3 +- 27 files changed, 694 insertions(+), 33 deletions(-) create mode 100644 Demo/Demo/Models/PaymentTokenRequest.swift create mode 100644 Demo/Demo/Models/PaymentTokenResponse.swift create mode 100644 Demo/Demo/Models/SetupTokenRequest.swift create mode 100644 Demo/Demo/Models/SetupTokenResponse.swift create mode 100644 Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift create mode 100644 Sources/CardPayments/APIRequests/UpdateSetupTokenResponse.swift create mode 100644 Sources/CardPayments/CardVaultDelegate.swift create mode 100644 Sources/CardPayments/Models/CardVaultRequest.swift create mode 100644 Sources/CardPayments/Models/CardVaultResult.swift create mode 100644 UnitTests/CardPaymentsTests/MockCardVaultDelegate.swift create mode 100644 UnitTests/CardPaymentsTests/MockGraphQLClient.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b43e7f4..20af0e3b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # PayPal iOS SDK Release Notes +## unreleased +* CardPayments + * Add `vault` method + * Add `CardVaultRequest` and `CardVaultResult` types for interacting with `vault` method + * Add `CardVaultDelegate` protocol to receive success and failure results + * Add `CardVaultDelegate` property to `CardClient` + ## 0.0.10 (2023-08-14) * PayPalNativePayments * Bump `PayPalCheckout` to `1.0.0` diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 50fe859dc..cb56d6749 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -7,9 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 3B22E8BA2A842D8900962E34 /* PaymentTokenRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B92A842D8900962E34 /* PaymentTokenRequest.swift */; }; + 3B22E8BC2A84397600962E34 /* PaymentTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8BB2A84397600962E34 /* PaymentTokenResponse.swift */; }; 3B80D50E2A291C0800D2EAC4 /* ClientIDRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50D2A291C0800D2EAC4 /* ClientIDRequest.swift */; }; 3B80D5102A291CB100D2EAC4 /* ClientIDResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50F2A291CB100D2EAC4 /* ClientIDResponse.swift */; }; 3BB7A9772A5CA6FD00C05140 /* MerchantIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB7A9762A5CA6FD00C05140 /* MerchantIntegration.swift */; }; + 3BDB348E2A7CB02C008100D7 /* SetupTokenRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDB348D2A7CB02C008100D7 /* SetupTokenRequest.swift */; }; + 3BDB34922A7CB5DE008100D7 /* SetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDB34912A7CB5DE008100D7 /* SetupTokenResponse.swift */; }; 5301468C28918B4D00184F22 /* ApprovalResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5301468B28918B4D00184F22 /* ApprovalResult.swift */; }; 536A5CA82898AA2A005C053D /* SwiftUINativeCheckoutDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A5CA72898AA2A005C053D /* SwiftUINativeCheckoutDemo.swift */; }; 53B9E8EA28C93B4400719239 /* OrderRequestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B9E8E928C93B4400719239 /* OrderRequestHelpers.swift */; }; @@ -97,9 +101,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3B22E8B92A842D8900962E34 /* PaymentTokenRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentTokenRequest.swift; sourceTree = ""; }; + 3B22E8BB2A84397600962E34 /* PaymentTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentTokenResponse.swift; sourceTree = ""; }; 3B80D50D2A291C0800D2EAC4 /* ClientIDRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientIDRequest.swift; sourceTree = ""; }; 3B80D50F2A291CB100D2EAC4 /* ClientIDResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientIDResponse.swift; sourceTree = ""; }; 3BB7A9762A5CA6FD00C05140 /* MerchantIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MerchantIntegration.swift; sourceTree = ""; }; + 3BDB348D2A7CB02C008100D7 /* SetupTokenRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupTokenRequest.swift; sourceTree = ""; }; + 3BDB34912A7CB5DE008100D7 /* SetupTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupTokenResponse.swift; sourceTree = ""; }; 5301468B28918B4D00184F22 /* ApprovalResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApprovalResult.swift; sourceTree = ""; }; 536A5CA22898A48C005C053D /* PayPalNativeCheckout.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PayPalNativeCheckout.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 536A5CA72898AA2A005C053D /* SwiftUINativeCheckoutDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUINativeCheckoutDemo.swift; sourceTree = ""; }; @@ -271,6 +279,10 @@ 80F33CEC26F8E7A9006811B1 /* Order.swift */, 80F33CF026F8E7D9006811B1 /* ProcessOrderParams.swift */, CBC16DD829ED90B600307117 /* UpdateOrderParams.swift */, + 3BDB348D2A7CB02C008100D7 /* SetupTokenRequest.swift */, + 3BDB34912A7CB5DE008100D7 /* SetupTokenResponse.swift */, + 3B22E8B92A842D8900962E34 /* PaymentTokenRequest.swift */, + 3B22E8BB2A84397600962E34 /* PaymentTokenResponse.swift */, ); path = Models; sourceTree = ""; @@ -481,6 +493,7 @@ CB34B32328BE3A9A001325B9 /* PayPalViewModel.swift in Sources */, BED04233271084DF00C80954 /* CardFormatter.swift in Sources */, CB9ED44E28411B120081F4DE /* SwiftUIPaymentButtonDemo.swift in Sources */, + 3B22E8BC2A84397600962E34 /* PaymentTokenResponse.swift in Sources */, 80561B3E26FB72D80023138C /* FeatureBaseViewController.swift in Sources */, BE9F36DC274578D100AFC7DA /* FeatureBaseViewControllerRepresentable.swift in Sources */, BECD84A027036DC2007CCAE4 /* Environment.swift in Sources */, @@ -490,6 +503,7 @@ CB9ED44C283FDA900081F4DE /* PaymentButtonEnums+Extension.swift in Sources */, BED041B1270CB33900C80954 /* CustomTextField.swift in Sources */, 3B80D50E2A291C0800D2EAC4 /* ClientIDRequest.swift in Sources */, + 3BDB348E2A7CB02C008100D7 /* SetupTokenRequest.swift in Sources */, 80F33CF326F8EA50006811B1 /* DemoSettings.swift in Sources */, BEDE304A275EA33500D275FD /* UIViewController+Extension.swift in Sources */, 80F33CE826F8DE29006811B1 /* DemoMerchantAPI.swift in Sources */, @@ -498,10 +512,12 @@ BED041AF270CA0FB00C80954 /* CustomButton.swift in Sources */, 3BB7A9772A5CA6FD00C05140 /* MerchantIntegration.swift in Sources */, BE1766B326F911A2007EF438 /* URLResponseError.swift in Sources */, + 3BDB34922A7CB5DE008100D7 /* SetupTokenResponse.swift in Sources */, CBC16DD929ED90B600307117 /* UpdateOrderParams.swift in Sources */, BE9F36D82745490400AFC7DA /* FloatingLabelTextField.swift in Sources */, BE9F36E7275548A600AFC7DA /* BaseViewModel.swift in Sources */, 806F1E3926B85367007A60E6 /* AppDelegate.swift in Sources */, + 3B22E8BA2A842D8900962E34 /* PaymentTokenRequest.swift in Sources */, 536A5CA82898AA2A005C053D /* SwiftUINativeCheckoutDemo.swift in Sources */, 806F1E3B26B85367007A60E6 /* SceneDelegate.swift in Sources */, BC6460CD2A12A2A0002B974B /* EmptyBodyParams.swift in Sources */, diff --git a/Demo/Demo/Models/PaymentTokenRequest.swift b/Demo/Demo/Models/PaymentTokenRequest.swift new file mode 100644 index 000000000..d75778b4f --- /dev/null +++ b/Demo/Demo/Models/PaymentTokenRequest.swift @@ -0,0 +1,31 @@ +import Foundation + +struct PaymentTokenRequest { + + let setupToken: String + + var path: String { + "/payment_tokens/" + } + + var method: String { + "POST" + } + + var headers: [String: String] { + ["Content-Type": "application/json"] + } + + var body: Data? { + let requestBody: [String: Any] = [ + "payment_source": [ + "token": [ + "id": setupToken, + "type": "SETUP_TOKEN" + ] + ] + ] + + return try? JSONSerialization.data(withJSONObject: requestBody) + } +} diff --git a/Demo/Demo/Models/PaymentTokenResponse.swift b/Demo/Demo/Models/PaymentTokenResponse.swift new file mode 100644 index 000000000..8799e48ca --- /dev/null +++ b/Demo/Demo/Models/PaymentTokenResponse.swift @@ -0,0 +1,38 @@ +import Foundation + +struct PaymentTokenResponse: Decodable { + + let id: String + let customer: Customer + let paymentSource: PaymentSource + + + enum CodingKeys: String, CodingKey { + case id + case customer + case paymentSource = "payment_source" + } +} + +struct Customer: Decodable { + + let id: String +} + +struct PaymentSource: Decodable { + + let card: BasicCard +} + +struct BasicCard: Decodable { + + let brand: String? + let lastDigits: String + let expiry: String + + enum CodingKeys: String, CodingKey { + case brand + case lastDigits = "last_digits" + case expiry + } +} diff --git a/Demo/Demo/Models/SetupTokenRequest.swift b/Demo/Demo/Models/SetupTokenRequest.swift new file mode 100644 index 000000000..9c3b34c8f --- /dev/null +++ b/Demo/Demo/Models/SetupTokenRequest.swift @@ -0,0 +1,35 @@ +import Foundation + +struct SetUpTokenRequest { + + let customerID: String? + + var path: String { + "/setup_tokens/" + } + + var method: String { + "POST" + } + + var headers: [String: String] { + ["Content-Type": "application/json"] + } + + var body: Data? { + let requestBody: [String: Any] = [ + "customer": [ + "id": customerID + ], + "payment_source": [ + "card": [:], + "experience_context": [ + "returnUrl": "https://example.com/returnUrl", + "cancelUrl": "https://example.com/returnUrl" + ] + ] + ] + + return try? JSONSerialization.data(withJSONObject: requestBody) + } +} diff --git a/Demo/Demo/Models/SetupTokenResponse.swift b/Demo/Demo/Models/SetupTokenResponse.swift new file mode 100644 index 000000000..9ebd82308 --- /dev/null +++ b/Demo/Demo/Models/SetupTokenResponse.swift @@ -0,0 +1,6 @@ +import Foundation + +struct SetUpTokenResponse: Decodable { + + let id, status: String +} diff --git a/Demo/Demo/Networking/DemoMerchantAPI.swift b/Demo/Demo/Networking/DemoMerchantAPI.swift index 1efdedc18..da82c3321 100644 --- a/Demo/Demo/Networking/DemoMerchantAPI.swift +++ b/Demo/Demo/Networking/DemoMerchantAPI.swift @@ -19,6 +19,39 @@ final class DemoMerchantAPI { // MARK: Public Methods + func getSetupToken(customerID: String? = nil, selectedMerchantIntegration: MerchantIntegration) async throws -> SetUpTokenResponse { + do { + let request = SetUpTokenRequest(customerID: customerID) + let urlRequest = try createSetupTokenUrlRequest( + setupTokenRequest: request, + environment: DemoSettings.environment, + selectedMerchantIntegration: selectedMerchantIntegration + ) + + let data = try await data(for: urlRequest) + return try parse(from: data) + } catch { + print("error with the create setup token request: \(error.localizedDescription)") + throw error + } + } + + func getPaymentToken(setupToken: String, selectedMerchantIntegration: MerchantIntegration) async throws -> PaymentTokenResponse { + do { + let request = PaymentTokenRequest(setupToken: setupToken) + let urlRequest = try createPaymentTokenUrlRequest( + paymentTokenRequest: request, + environment: DemoSettings.environment, + selectedMerchantIntegration: selectedMerchantIntegration + ) + let data = try await data(for: urlRequest) + return try parse(from: data) + } catch { + print("error with the create payment token request: \(error.localizedDescription)") + throw error + } + } + func captureOrder(orderID: String, selectedMerchantIntegration: MerchantIntegration) async throws -> Order { guard let url = buildBaseURL(with: "/orders/\(orderID)/capture", selectedMerchantIntegration: selectedMerchantIntegration) else { throw URLResponseError.invalidURL @@ -168,7 +201,9 @@ final class DemoMerchantAPI { } private func createUrlRequest( - clientIDRequest: ClientIDRequest, environment: Demo.Environment, selectedMerchantIntegration: MerchantIntegration + clientIDRequest: ClientIDRequest, + environment: Demo.Environment, + selectedMerchantIntegration: MerchantIntegration ) throws -> URLRequest { var completeUrl = environment.baseURL @@ -185,4 +220,50 @@ final class DemoMerchantAPI { } return request } + + private func createSetupTokenUrlRequest( + setupTokenRequest: SetUpTokenRequest, + environment: Demo.Environment, + selectedMerchantIntegration: MerchantIntegration + ) throws -> URLRequest { + var completeUrl = environment.baseURL + completeUrl += selectedMerchantIntegration.path + completeUrl.append(contentsOf: setupTokenRequest.path) + + guard let url = URL(string: completeUrl) else { + throw URLResponseError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = setupTokenRequest.method + request.httpBody = setupTokenRequest.body + setupTokenRequest.headers.forEach { key, value in + request.addValue(value, forHTTPHeaderField: key) + } + + return request + } + + private func createPaymentTokenUrlRequest( + paymentTokenRequest: PaymentTokenRequest, + environment: Demo.Environment, + selectedMerchantIntegration: MerchantIntegration + ) throws -> URLRequest { + var completeUrl = environment.baseURL + completeUrl += selectedMerchantIntegration.path + completeUrl.append(contentsOf: paymentTokenRequest.path) + + guard let url = URL(string: completeUrl) else { + throw URLResponseError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = paymentTokenRequest.method + request.httpBody = paymentTokenRequest.body + paymentTokenRequest.headers.forEach { key, value in + request.addValue(value, forHTTPHeaderField: key) + } + + return request + } } diff --git a/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift b/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift index 44a9716a2..d308d26ae 100644 --- a/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift +++ b/Demo/Demo/SwiftUIComponents/SwiftUICardDemo.swift @@ -69,6 +69,23 @@ struct SwiftUICardDemo: View { ) .cornerRadius(10) .disabled(!baseViewModel.isCardFormValid(cardNumber: cardNumberText, expirationDate: expirationDateText, cvv: cvvText)) + Button("Vault Card without Purchase") { + guard let card = baseViewModel.createCard( + cardNumber: cardNumberText, + expirationDate: expirationDateText, + cvv: cvvText + ) else { + return + } + Task { + await baseViewModel.vaultCard(card: card, customerID: vaultCustomerID.isEmpty ? nil : vaultCustomerID) + } + } + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(.blue) + .cornerRadius(10) } .padding(.horizontal, 16) } diff --git a/Demo/Demo/ViewModels/BaseViewModel.swift b/Demo/Demo/ViewModels/BaseViewModel.swift index 8edf6e812..b1f0c21a7 100644 --- a/Demo/Demo/ViewModels/BaseViewModel.swift +++ b/Demo/Demo/ViewModels/BaseViewModel.swift @@ -8,7 +8,7 @@ import PayPalCheckout /// This class is used to share the orderID across shared views, update the text of `bottomStatusLabel` in our `FeatureBaseViewController` /// as well as share the logic of `processOrder` across our duplicate (SwiftUI and UIKit) card views. -class BaseViewModel: ObservableObject, PayPalWebCheckoutDelegate, CardDelegate { +class BaseViewModel: ObservableObject, PayPalWebCheckoutDelegate, CardDelegate, CardVaultDelegate { /// Weak reference to associated view weak var view: FeatureBaseViewController? @@ -93,6 +93,22 @@ class BaseViewModel: ObservableObject, PayPalWebCheckoutDelegate, CardDelegate { let cardRequest = CardRequest(orderID: orderID, card: card, sca: .scaAlways) cardClient.approveOrder(request: cardRequest) } + + func vaultCard(card: Card, customerID: String? = nil) async { + do { + guard let config = await getCoreConfig() else { return } + let cardClient = CardClient(config: config) + cardClient.vaultDelegate = self + let tokenResponse = try await DemoMerchantAPI.sharedService.getSetupToken( + customerID: customerID, selectedMerchantIntegration: selectedMerchantIntegration + ) + + let cardVaultRequest = CardVaultRequest(card: card, setupTokenID: tokenResponse.id) + cardClient.vault(cardVaultRequest) + } catch { + print("Error in getSetupToken: \(error.localizedDescription)") + } + } func isCardFormValid(cardNumber: String, expirationDate: String, cvv: String) -> Bool { guard orderID != nil else { @@ -216,6 +232,20 @@ class BaseViewModel: ObservableObject, PayPalWebCheckoutDelegate, CardDelegate { updateTitle("3DS challenge has finished") print("3DS challenge has finished") } + + // MARK: - CardVault Delegate + + func card(_ cardClient: CardClient, didFinishWithVaultResult vaultResult: CardVaultResult) { + updateTitle( + "Vault without Purchase has finished. \n SetupTokenID: \(vaultResult.setupTokenID) \nVault Status: \(vaultResult.status)" + ) + print("Vault without purchase has finished: \(vaultResult)") + } + + func card(_ cardClient: CardClient, didFinishWithVaultError vaultError: CoreSDKError) { + updateTitle("Vault without purchase has failed: \(vaultError.localizedDescription)") + print("❌ There was an error: \(vaultError)") + } func getClientID() async -> String? { await DemoMerchantAPI.sharedService.getClientID( diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index accd623d5..f6704bcfb 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -14,7 +14,14 @@ 067C92E527270FDE009E3054 /* PayPalEnvironment_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067C92E427270FDE009E3054 /* PayPalEnvironment_Tests.swift */; }; 06CE009926F3D1660000CC46 /* CoreConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CE009826F3D1660000CC46 /* CoreConfig.swift */; }; 06CE009B26F3D5A40000CC46 /* CardClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CE009A26F3D5A40000CC46 /* CardClient.swift */; }; + 3B109B3C2A85CC6200D8135F /* MockCardVaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B109B3A2A85B54800D8135F /* MockCardVaultDelegate.swift */; }; + 3B109B3D2A85D1CA00D8135F /* MockGraphQLClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B109B382A85A9DB00D8135F /* MockGraphQLClient.swift */; }; + 3B22E8B62A840ECF00962E34 /* CardVaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B52A840ECF00962E34 /* CardVaultDelegate.swift */; }; + 3B22E8B82A841AEA00962E34 /* CardVaultResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */; }; + 3B79E4F72A8503CA00C01D06 /* UpdateSetupTokenQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B79E4F62A8503C900C01D06 /* UpdateSetupTokenQuery.swift */; }; 3B80D50C2A27979000D2EAC4 /* FailingJSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */; }; + 3BD82DBB2A835AF900CBE764 /* UpdateSetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */; }; + 3BDB34942A80CE6E008100D7 /* CardVaultRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDB34932A80CE6E008100D7 /* CardVaultRequest.swift */; }; 3D1763A22720722A00652E1C /* CardResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1763A12720722A00652E1C /* CardResult.swift */; }; 3DC42BA927187E8300B71645 /* ErrorResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC42BA827187E8300B71645 /* ErrorResponse.swift */; }; 53A2A4E228A182AC0093441C /* NativeCheckoutProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A2A4E128A182AC0093441C /* NativeCheckoutProvider.swift */; }; @@ -195,7 +202,14 @@ 06CE009A26F3D5A40000CC46 /* CardClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardClient.swift; sourceTree = ""; }; 06CE009F26F3DF100000CC46 /* ConfirmPaymentSourceRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmPaymentSourceRequest.swift; sourceTree = ""; }; 06CE00A226F3E32A0000CC46 /* ConfirmPaymentSourceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmPaymentSourceResponse.swift; sourceTree = ""; }; + 3B109B382A85A9DB00D8135F /* MockGraphQLClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGraphQLClient.swift; sourceTree = ""; }; + 3B109B3A2A85B54800D8135F /* MockCardVaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardVaultDelegate.swift; sourceTree = ""; }; + 3B22E8B52A840ECF00962E34 /* CardVaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultDelegate.swift; sourceTree = ""; }; + 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultResult.swift; sourceTree = ""; }; + 3B79E4F62A8503C900C01D06 /* UpdateSetupTokenQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateSetupTokenQuery.swift; sourceTree = ""; }; 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailingJSONEncoder.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 = ""; }; 3D1763A12720722A00652E1C /* CardResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardResult.swift; sourceTree = ""; }; 3D25238127344F330099E4EB /* NativeCheckoutStartable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NativeCheckoutStartable.swift; path = Sources/PayPalNativePayments/NativeCheckoutStartable.swift; sourceTree = SOURCE_ROOT; }; 3D25238B273979170099E4EB /* MockNativeCheckoutProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNativeCheckoutProvider.swift; sourceTree = ""; }; @@ -452,6 +466,8 @@ 3D1763A12720722A00652E1C /* CardResult.swift */, CB4BE285284802C200EA2DD1 /* PaymentSource.swift */, CB4BE27C2847AF6F00EA2DD1 /* SCA.swift */, + 3BDB34932A80CE6E008100D7 /* CardVaultRequest.swift */, + 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */, ); path = Models; sourceTree = ""; @@ -525,8 +541,10 @@ 80DBC9D829C336D500462539 /* APIRequests */ = { isa = PBXGroup; children = ( + 3B79E4F62A8503C900C01D06 /* UpdateSetupTokenQuery.swift */, 06CE009F26F3DF100000CC46 /* ConfirmPaymentSourceRequest.swift */, 06CE00A226F3E32A0000CC46 /* ConfirmPaymentSourceResponse.swift */, + 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */, ); path = APIRequests; sourceTree = ""; @@ -552,6 +570,7 @@ 06CE009A26F3D5A40000CC46 /* CardClient.swift */, 80DCC59D2719DB6F00EC7C5A /* CardClientError.swift */, CB4BE27F2847F01000EA2DD1 /* CardDelegate.swift */, + 3B22E8B52A840ECF00962E34 /* CardVaultDelegate.swift */, 80DBC9D829C336D500462539 /* APIRequests */, 065A4DBD26FCDA270007014A /* Models */, ); @@ -566,6 +585,8 @@ 065A4DC226FCE1D20007014A /* ConfirmPaymentSourceRequest_Tests.swift */, CB16E6D7285B7A2B00FD6F52 /* CardResponses.swift */, CB16E6D9285B7B7300FD6F52 /* MockCardDelegate.swift */, + 3B109B382A85A9DB00D8135F /* MockGraphQLClient.swift */, + 3B109B3A2A85B54800D8135F /* MockCardVaultDelegate.swift */, ); path = CardPaymentsTests; sourceTree = ""; @@ -1307,13 +1328,19 @@ buildActionMask = 2147483647; files = ( 06CE009B26F3D5A40000CC46 /* CardClient.swift in Sources */, + 3B79E4F72A8503CA00C01D06 /* UpdateSetupTokenQuery.swift in Sources */, 8048D28C270B9DE00072214A /* ConfirmPaymentSourceResponse.swift in Sources */, + 3B109B3D2A85D1CA00D8135F /* MockGraphQLClient.swift in Sources */, 3D1763A22720722A00652E1C /* CardResult.swift in Sources */, BC0A82A5270B9533006E9A21 /* ConfirmPaymentSourceRequest.swift in Sources */, CB4BE2802847F01000EA2DD1 /* CardDelegate.swift in Sources */, + 3BD82DBB2A835AF900CBE764 /* UpdateSetupTokenResponse.swift in Sources */, 80E8DAE126B8784600FAFC3F /* Card.swift in Sources */, + 3B22E8B82A841AEA00962E34 /* CardVaultResult.swift in Sources */, CBC16DD529E99B4600307117 /* PaymentSource.swift in Sources */, 80DCC59E2719DB6F00EC7C5A /* CardClientError.swift in Sources */, + 3B22E8B62A840ECF00962E34 /* CardVaultDelegate.swift in Sources */, + 3BDB34942A80CE6E008100D7 /* CardVaultRequest.swift in Sources */, CB4BE27D2847AF6F00EA2DD1 /* SCA.swift in Sources */, 0647E70E2714962800F8E517 /* Address.swift in Sources */, BC7F8123275FC1350011EDC8 /* CardRequest.swift in Sources */, @@ -1325,6 +1352,7 @@ buildActionMask = 2147483647; files = ( 065A4DBF26FCDA5B0007014A /* CardClient_Tests.swift in Sources */, + 3B109B3C2A85CC6200D8135F /* MockCardVaultDelegate.swift in Sources */, CB16E6D8285B7A2B00FD6F52 /* CardResponses.swift in Sources */, CB16E6DA285B7B7300FD6F52 /* MockCardDelegate.swift in Sources */, BC0A82A6270B9954006E9A21 /* ConfirmPaymentSourceRequest_Tests.swift in Sources */, diff --git a/Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift b/Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift new file mode 100644 index 000000000..41d76e448 --- /dev/null +++ b/Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift @@ -0,0 +1,83 @@ +import Foundation +#if canImport(CorePayments) +import CorePayments +#endif + +class UpdateSetupTokenQuery: Codable, GraphQLQuery { + + struct VaultCard: Codable { + + public let number: String + public let expiry: String + public let securityCode: String + public let name: String? + public let billingAddress: Address? + + init(number: String, expiry: String, securityCode: String, name: String? = nil, billingAddress: Address? = nil) { + self.number = number + self.expiry = expiry + self.securityCode = securityCode + self.name = name + self.billingAddress = billingAddress + } + } + + struct PaymentSource: Codable { + + let card: VaultCard + } + + struct Variables: Codable { + + let clientID: String + let vaultSetupToken: String + let paymentSource: PaymentSource + } + + var query: String + var variables: Variables? + + init( + clientID: String, + vaultSetupToken: String, + card: Card + ) { + let vaultCard = VaultCard( + number: card.number, + expiry: card.expiry, + securityCode: card.securityCode, + name: card.cardholderName, + billingAddress: card.billingAddress + ) + + let paymentSource = PaymentSource(card: vaultCard) + + self.variables = Variables( + clientID: clientID, + vaultSetupToken: vaultSetupToken, + paymentSource: paymentSource + ) + // swiftlint:disable indentation_width + let queryString = """ + mutation UpdateVaultSetupToken( + $clientID: String!, + $vaultSetupToken: String!, + $paymentSource: PaymentSource + ) { + updateVaultSetupToken( + clientId: $clientID + vaultSetupToken: $vaultSetupToken + paymentSource: $paymentSource + ) { + id, + status, + links { + rel, href + } + } + } + """ + self.query = queryString + } + // swiftlint:enable indentation_width +} diff --git a/Sources/CardPayments/APIRequests/UpdateSetupTokenResponse.swift b/Sources/CardPayments/APIRequests/UpdateSetupTokenResponse.swift new file mode 100644 index 000000000..05623f911 --- /dev/null +++ b/Sources/CardPayments/APIRequests/UpdateSetupTokenResponse.swift @@ -0,0 +1,17 @@ +struct UpdateSetupTokenResponse: Codable { + + let updateVaultSetupToken: TokenDetails +} + +struct TokenDetails: Codable { + + struct Link: Codable { + + let rel: String + let href: String + } + + let id: String + let status: String + let links: [Link] +} diff --git a/Sources/CardPayments/CardClient.swift b/Sources/CardPayments/CardClient.swift index 093860be7..0cc5ebf6e 100644 --- a/Sources/CardPayments/CardClient.swift +++ b/Sources/CardPayments/CardClient.swift @@ -7,11 +7,13 @@ import CorePayments public class CardClient: NSObject { public weak var delegate: CardDelegate? - + public weak var vaultDelegate: CardVaultDelegate? + private let apiClient: APIClient private let config: CoreConfig private let webAuthenticationSession: WebAuthenticationSession private var analyticsService: AnalyticsService? + private var graphQLClient: GraphQLClient? /// Initialize a CardClient to process card payment /// - Parameter config: The CoreConfig object @@ -19,15 +21,61 @@ public class CardClient: NSObject { self.config = config self.apiClient = APIClient(coreConfig: config) self.webAuthenticationSession = WebAuthenticationSession() + self.graphQLClient = GraphQLClient(environment: config.environment) } /// For internal use for testing/mocking purpose - init(config: CoreConfig, apiClient: APIClient, webAuthenticationSession: WebAuthenticationSession) { + init( + config: CoreConfig, + apiClient: APIClient, + webAuthenticationSession: WebAuthenticationSession, + graphQLClient: GraphQLClient? = nil + ) { self.config = config self.apiClient = apiClient self.webAuthenticationSession = webAuthenticationSession + self.graphQLClient = graphQLClient + } + + public func vault(_ vaultRequest: CardVaultRequest) { + Task { + do { + let card = vaultRequest.card + let setupTokenID = vaultRequest.setupTokenID + let result = try await updateSetupToken(vaultSetupTokenID: setupTokenID, card: card) + // TODO: handle 3DS contingency with helios link + if let link = result.links.first(where: { $0.rel == "approve" && $0.href.contains("helios") }) { + let url = link.href + print("3DS url \(url)") + } else { + let vaultResult = CardVaultResult(setupTokenID: result.id, status: result.status) + notifyVaultSuccess(for: vaultResult) + } + } catch let error as CoreSDKError { + notifyVaultFailure(with: error) + } catch { + notifyVaultFailure(with: CardClientError.vaultTokenError) + } + } } + + func updateSetupToken(vaultSetupTokenID: String, card: Card) async throws -> TokenDetails { + guard let graphQLClient else { + throw CardClientError.nilGraphQLClientError + } + let clientID = config.clientID + let query = UpdateSetupTokenQuery(clientID: clientID, vaultSetupToken: vaultSetupTokenID, card: card) + let response: GraphQLQueryResponse = try await graphQLClient.callGraphQL( + name: "UpdateVaultSetupToken", query: query + ) + guard let data = response.data else { + throw CardClientError.noVaultTokenDataError + } + + return data.updateVaultSetupToken + } + /// Approve an order with a card, which validates buyer's card, and if valid, attaches the card as the payment source to the order. /// After the order has been successfully approved, you will need to handle capturing/authorizing the order in your server. /// - Parameters: @@ -45,7 +93,6 @@ public class CardClient: NSObject { if let url: String = result.links?.first(where: { $0.rel == "payer-action" })?.href { analyticsService?.sendEvent("card-payments:3ds:confirm-payment-source:challenge-required") - startThreeDSecureChallenge(url: url, orderId: result.id) } else { analyticsService?.sendEvent("card-payments:3ds:confirm-payment-source:succeeded") @@ -112,6 +159,14 @@ public class CardClient: NSObject { analyticsService?.sendEvent("card-payments:3ds:failed") delegate?.card(self, didFinishWithError: error) } + + private func notifyVaultSuccess(for vaultResult: CardVaultResult) { + vaultDelegate?.card(self, didFinishWithVaultResult: vaultResult) + } + + private func notifyVaultFailure(with vaultError: CoreSDKError) { + vaultDelegate?.card(self, didFinishWithVaultError: vaultError) + } private func notifyCancellation() { analyticsService?.sendEvent("card-payments:3ds:challenge:user-canceled") diff --git a/Sources/CardPayments/CardClientError.swift b/Sources/CardPayments/CardClientError.swift index 062d7dafd..9974858a5 100644 --- a/Sources/CardPayments/CardClientError.swift +++ b/Sources/CardPayments/CardClientError.swift @@ -19,6 +19,15 @@ enum CardClientError { /// 3 . An invalid 3DS challenge URL was returned by `/confirm-payment-source` case threeDSecureURLError + + /// 4. No data was returned from updating setup token + case noVaultTokenDataError + + /// 5. An error occurred during updating setup token + case vaultTokenError + + /// 6. GraphQLClient is unexpectedly nil + case nilGraphQLClientError } static let unknownError = CoreSDKError( @@ -46,4 +55,22 @@ enum CardClientError { domain: domain, errorDescription: "An invalid 3DS URL was returned. Contact developer.paypal.com/support." ) + + static let noVaultTokenDataError = CoreSDKError( + code: Code.noVaultTokenDataError.rawValue, + domain: domain, + errorDescription: "No data was returned from update setup token service." + ) + + static let vaultTokenError = CoreSDKError( + code: Code.vaultTokenError.rawValue, + domain: domain, + errorDescription: "An error occurred while vaulting a card." + ) + + static let nilGraphQLClientError = CoreSDKError( + code: Code.nilGraphQLClientError.rawValue, + domain: domain, + errorDescription: "GraphQLClient is unexpectedly nil." + ) } diff --git a/Sources/CardPayments/CardVaultDelegate.swift b/Sources/CardPayments/CardVaultDelegate.swift new file mode 100644 index 000000000..5fa1eef12 --- /dev/null +++ b/Sources/CardPayments/CardVaultDelegate.swift @@ -0,0 +1,20 @@ +import Foundation +#if canImport(CorePayments) +import CorePayments +#endif + +/// CardVault delegate to handle events from CardClient +public protocol CardVaultDelegate: AnyObject { + + /// Notify that the Card vault flow finished with a successful result + /// - Parameters: + /// - client: the CardClient associated with delegate + /// - didFinishWithResult: the successful result from the flow + func card(_ cardClient: CardClient, didFinishWithVaultResult vaultResult: CardVaultResult) + + /// Notify that an error occurred in the Card vault flow + /// - Parameters: + /// - client: the CardClient associated with delegate + /// - didFinishWithError: the error returned by the Card vault flow + func card(_ cardClient: CardClient, didFinishWithVaultError vaultError: CoreSDKError) +} diff --git a/Sources/CardPayments/Models/Card.swift b/Sources/CardPayments/Models/Card.swift index d21f849c1..b295307d8 100644 --- a/Sources/CardPayments/Models/Card.swift +++ b/Sources/CardPayments/Models/Card.swift @@ -1,7 +1,7 @@ import Foundation /// Represents raw credit or debit card data provided by the customer. -public struct Card { +public struct Card: Encodable { /// The primary account number (PAN) for the payment card. public var number: String @@ -20,9 +20,10 @@ public struct Card { /// Optional. The billing address public var billingAddress: Address? - + + //// :nodoc: This is exposed for convenience in copying card contents to UpdateSetupTokenQuery /// The expiration year and month, in ISO-8601 `YYYY-MM` date format. - private var expiry: String { + public var expiry: String { "\(expirationYear)-\(expirationMonth)" } diff --git a/Sources/CardPayments/Models/CardVaultRequest.swift b/Sources/CardPayments/Models/CardVaultRequest.swift new file mode 100644 index 000000000..0a19a8541 --- /dev/null +++ b/Sources/CardPayments/Models/CardVaultRequest.swift @@ -0,0 +1,20 @@ +import Foundation + +/// A vault request to attach payment method to setup token +public struct CardVaultRequest { + + /// The card for payment source to attach to the setup token + public let card: Card + + /// ID for the setup token to update + public let setupTokenID: String + + /// Creates an instance of a card vault request + /// - Parameters: + /// - card: The card for payment source to attach to the setup token + /// - setupTokenID: An ID for the setup token to update + public init(card: Card, setupTokenID: String) { + self.card = card + self.setupTokenID = setupTokenID + } +} diff --git a/Sources/CardPayments/Models/CardVaultResult.swift b/Sources/CardPayments/Models/CardVaultResult.swift new file mode 100644 index 000000000..fd9ca7f32 --- /dev/null +++ b/Sources/CardPayments/Models/CardVaultResult.swift @@ -0,0 +1,14 @@ +import Foundation +#if canImport(CorePayments) +import CorePayments +#endif + +/// The result of a vault without purchase flow. +public struct CardVaultResult { + + /// setupTokenID of token that was updated + public let setupTokenID: String + + /// status of the updated setup token + public let status: String +} diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLClient.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLClient.swift index af51429c0..889762494 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLClient.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLClient.swift @@ -1,18 +1,20 @@ import Foundation -class GraphQLClient { +//// :nodoc: This class handles urlRequests for GraphQL endpoints +public class GraphQLClient { - private let environment: Environment - private let urlSession: URLSessionProtocol - private let jsonDecoder = JSONDecoder() + public let environment: Environment + public let urlSession: URLSessionProtocol + public let jsonDecoder = JSONDecoder() public init(environment: Environment, urlSession: URLSessionProtocol = URLSession.shared) { self.environment = environment self.urlSession = urlSession } - func executeQuery(query: GraphQLQuery) async throws -> GraphQLQueryResponse { - var request = try createURLRequest(requestBody: query.requestBody()) + public func callGraphQL(name: String, query: Q) async throws -> GraphQLQueryResponse { + + var request = try createURLRequest(name: name, requestBody: query.requestBody()) headers().forEach { key, value in request.addValue(value, forHTTPHeaderField: key) } @@ -23,13 +25,23 @@ class GraphQLClient { let decoded: GraphQLQueryResponse = try parse(data: data) return decoded } - + func parse(data: Data) throws -> T { return try jsonDecoder.decode(T.self, from: data) } - func createURLRequest(requestBody: Data) throws -> URLRequest { - var urlRequest = URLRequest(url: environment.graphQLURL) + func createURLRequest(name: String? = nil, requestBody: Data) throws -> URLRequest { + var urlString = environment.graphQLURL.absoluteString + + if let name { + urlString.append("?\(name)") + } + + guard let url = URL(string: urlString) else { + throw GraphQLError(message: "error fetching url") + } + + var urlRequest = URLRequest(url: url) urlRequest.httpMethod = HTTPMethod.post.rawValue urlRequest.httpBody = requestBody return urlRequest @@ -45,14 +57,11 @@ class GraphQLClient { } } -extension GraphQLQuery { +extension GraphQLQuery where Self: Codable { - func requestBody() throws -> Data { - let body: [String: Any] = [ - "query": query, - "variables": variables - ] - let data = try JSONSerialization.data(withJSONObject: body, options: []) - return data + /// :nodoc: Converts GraphQLQuery into Data + public func requestBody() throws -> Data { + let encoder = JSONEncoder() + return try encoder.encode(self) } } diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLError.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLError.swift index 8bc0dcb5f..554a0ad41 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLError.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLError.swift @@ -2,6 +2,11 @@ public struct GraphQLError: Codable, Error { let message: String let extensions: [Extension]? + + init(message: String, extensions: [Extension]? = nil) { + self.message = message + self.extensions = extensions + } } struct Extension: Codable { diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLQuery.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLQuery.swift index ea1c50f2a..c029e7ee9 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLQuery.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLQuery.swift @@ -1,7 +1,9 @@ import Foundation -protocol GraphQLQuery { +//// :nodoc: Protocol for requests made to GraphQL endpoints +public protocol GraphQLQuery { + associatedtype VariablesType: Codable var query: String { get } - var variables: [String: Any] { get } + var variables: VariablesType? { get } func requestBody() throws -> Data } diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLQueryResponse.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLQueryResponse.swift index 6f29f25ff..85776f349 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLQueryResponse.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLQueryResponse.swift @@ -1,4 +1,5 @@ -struct GraphQLQueryResponse: Codable { +//// :nodoc: Struct to handle nested response from GraphQL response +public struct GraphQLQueryResponse: Codable { - let data: T? + public let data: T? } diff --git a/Sources/CorePayments/Networking/URLSessionProtocol.swift b/Sources/CorePayments/Networking/URLSessionProtocol.swift index 62e23c8dc..c65c00170 100644 --- a/Sources/CorePayments/Networking/URLSessionProtocol.swift +++ b/Sources/CorePayments/Networking/URLSessionProtocol.swift @@ -1,5 +1,6 @@ import Foundation -protocol URLSessionProtocol { +/// :nodoc: This is a protocol for function performing urlRequest in HTTP class and in GraphQLClient +public protocol URLSessionProtocol { func performRequest(with urlRequest: URLRequest) async throws -> (Data, URLResponse) } diff --git a/UnitTests/CardPaymentsTests/CardClient_Tests.swift b/UnitTests/CardPaymentsTests/CardClient_Tests.swift index 8ca00fd9c..f0d483657 100644 --- a/UnitTests/CardPaymentsTests/CardClient_Tests.swift +++ b/UnitTests/CardPaymentsTests/CardClient_Tests.swift @@ -25,6 +25,8 @@ class CardClient_Tests: XCTestCase { var mockAPIClient: MockAPIClient! var cardClient: CardClient! var cardRequest: CardRequest! + var mockGraphQLClient: MockGraphQLClient! + var mockCardVaultDelegate: MockCardVaultDelegate! // swiftlint:enable implicitly_unwrapped_optional // MARK: - Test lifecycle @@ -34,12 +36,82 @@ class CardClient_Tests: XCTestCase { config = CoreConfig(clientID: mockClientID, environment: .sandbox) mockAPIClient = MockAPIClient(coreConfig: config) cardRequest = CardRequest(orderID: "testOrderId", card: card) - + mockGraphQLClient = MockGraphQLClient(environment: .sandbox) + cardClient = CardClient( config: config, apiClient: mockAPIClient, - webAuthenticationSession: mockWebAuthSession + webAuthenticationSession: mockWebAuthSession, + graphQLClient: mockGraphQLClient + ) + } + + // MARK: - vault() tests + + func testVault_withValidResponse_returnsSuccess() { + let setupTokenID = "testToken1" + let vaultStatus = "APPROVED" + let vaultRequest = CardVaultRequest(card: card, setupTokenID: setupTokenID) + let updateSetupTokenResponse = UpdateSetupTokenResponse( + updateVaultSetupToken: TokenDetails(id: setupTokenID, status: vaultStatus, links: [TokenDetails.Link(rel: "df", href: "h")]) + ) + mockGraphQLClient.mockSuccessResponse = GraphQLQueryResponse(data: updateSetupTokenResponse) + + let expectation = expectation(description: "vault completed") + let cardVaultDelegate = MockCardVaultDelegate(success: {_, result in + XCTAssertEqual(result.setupTokenID, setupTokenID) + XCTAssertEqual(result.status, vaultStatus) + expectation.fulfill() + }, error: {_, _ in + XCTFail("Invoked error() callback. Should invoke success().") + }) + cardClient.vaultDelegate = cardVaultDelegate + cardClient.vault(vaultRequest) + + waitForExpectations(timeout: 10) + } + + func testVault_withNoData_ReturnsError() { + let setupTokenID = "testToken1" + let vaultRequest = CardVaultRequest(card: card, setupTokenID: setupTokenID) + + mockGraphQLClient.mockSuccessResponse = GraphQLQueryResponse(data: nil) + + let expectation = expectation(description: "vault completed") + let cardVaultDelegate = MockCardVaultDelegate(success: {_, _ in + XCTFail("Invoked success() callback. Should invoke error().") + }, error: {_, error in + XCTAssertEqual(error.domain, CardClientError.domain) + XCTAssertEqual(error.code, CardClientError.Code.noVaultTokenDataError.rawValue) + XCTAssertEqual(error.localizedDescription, "No data was returned from update setup token service.") + expectation.fulfill() + }) + cardClient.vaultDelegate = cardVaultDelegate + cardClient.vault(vaultRequest) + + waitForExpectations(timeout: 10) + } + + func testVault_whenGraphQLCallFails_returnsError() { + let setupTokenID = "testToken1" + let vaultRequest = CardVaultRequest(card: card, setupTokenID: setupTokenID) + + mockGraphQLClient.mockErrorResponse = GraphQLError( + message: "thee was an error fetching data from GraphQL endpoint", extensions: nil ) + let expectation = expectation(description: "vault completed") + let cardVaultDelegate = MockCardVaultDelegate(success: {_, _ in + XCTFail("Invoked success() callback. Should invoke error().") + }, error: {_, error in + XCTAssertEqual(error.domain, CardClientError.domain) + XCTAssertEqual(error.code, CardClientError.Code.vaultTokenError.rawValue) + XCTAssertEqual(error.localizedDescription, "An error occurred while vaulting a card.") + expectation.fulfill() + }) + cardClient.vaultDelegate = cardVaultDelegate + cardClient.vault(vaultRequest) + + waitForExpectations(timeout: 10) } // MARK: - approveOrder() tests diff --git a/UnitTests/CardPaymentsTests/MockCardVaultDelegate.swift b/UnitTests/CardPaymentsTests/MockCardVaultDelegate.swift new file mode 100644 index 000000000..73e229fa1 --- /dev/null +++ b/UnitTests/CardPaymentsTests/MockCardVaultDelegate.swift @@ -0,0 +1,24 @@ +@testable import CorePayments +@testable import CardPayments + +class MockCardVaultDelegate: CardVaultDelegate { + + private var success: ((CardClient, CardVaultResult) -> Void)? + private var failure: ((CardClient, CoreSDKError) -> Void)? + + required init( + success: ((CardClient, CardVaultResult) -> Void)? = nil, + error: ((CardClient, CoreSDKError) -> Void)? = nil + ) { + self.success = success + self.failure = error + } + + func card(_ cardClient: CardClient, didFinishWithVaultResult vaultResult: CardPayments.CardVaultResult) { + success?(cardClient, vaultResult) + } + + func card(_ cardClient: CardClient, didFinishWithVaultError vaultError: CorePayments.CoreSDKError) { + failure?(cardClient, vaultError) + } +} diff --git a/UnitTests/CardPaymentsTests/MockGraphQLClient.swift b/UnitTests/CardPaymentsTests/MockGraphQLClient.swift new file mode 100644 index 000000000..cbf96af2f --- /dev/null +++ b/UnitTests/CardPaymentsTests/MockGraphQLClient.swift @@ -0,0 +1,20 @@ +@testable import CorePayments + +class MockGraphQLClient: GraphQLClient { + + var mockSuccessResponse: GraphQLQueryResponse? + var mockErrorResponse: Error? + + override func callGraphQL( + name: String, + query: Q + ) async throws -> GraphQLQueryResponse where T: Decodable, T: Encodable, Q: GraphQLQuery { + if let response = mockSuccessResponse as? GraphQLQueryResponse { + return response + } else if let error = mockErrorResponse { + throw error + } else { + fatalError("MockGraphQLClient - either mockSuccessResponse or mockErrorResponse must be set") + } + } +} diff --git a/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift b/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift index 426ffc8e5..6be0fa6a8 100644 --- a/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift +++ b/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift @@ -13,7 +13,8 @@ class GraphQLClient_Tests: XCTestCase { var config: CoreConfig! var mockURLSession: MockURLSession! var graphQLClient: GraphQLClient! - var graphQLQuery: GraphQLQuery! + var graphQLQuery: (any GraphQLQuery)? + // TODO: add unit tests in appropriate class after networking & graphQL layer refactor is done // MARK: - Test lifecycle From d7209b9ca383a0e584e5904ded482b04d3413279 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Thu, 17 Aug 2023 15:55:43 -0500 Subject: [PATCH 07/19] WIP - Add VaultPPasSAPI.swift --- PayPal.xcodeproj/project.pbxproj | 4 ++ .../APIRequests/CheckoutOrdersAPI.swift | 2 +- .../APIRequests/UpdateSetupTokenQuery.swift | 51 ++++++++++++++ .../CardPayments/APIRequests/VaultAPI.swift | 70 +++++++++++++++++++ Sources/CardPayments/CardClient.swift | 48 +++++++------ .../GraphQL/GraphQLHTTPResponse.swift | 6 +- .../Networking/GraphQL/GraphQLRequest.swift | 6 ++ 7 files changed, 162 insertions(+), 25 deletions(-) create mode 100644 Sources/CardPayments/APIRequests/VaultAPI.swift diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index 199263775..ec8a7f0ca 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ 807D56AE2A869064009E591D /* GraphQLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807D56AD2A869064009E591D /* GraphQLRequest.swift */; }; 807D56B02A869F97009E591D /* GraphQLErrorResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807D56AF2A869F97009E591D /* GraphQLErrorResponse.swift */; }; 808EEA81291321FE001B6765 /* AnalyticsEventRequest_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 808EEA80291321FE001B6765 /* AnalyticsEventRequest_Tests.swift */; }; + 80B8B2FC2A8EBBFD00AB60CD /* VaultAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B8B2FB2A8EBBFD00AB60CD /* VaultAPI.swift */; }; 80D0C1382731CC9B00548A3D /* PayPalNativeCheckoutClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE7116152723227200165069 /* PayPalNativeCheckoutClient.swift */; }; 80DB2F762980795D00CFB86A /* CorePaymentsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DB2F752980795D00CFB86A /* CorePaymentsError.swift */; }; 80DB6F1726B89DC700277E54 /* CorePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80B9F85126B8750000D67843 /* CorePayments.framework */; platformFilter = ios; }; @@ -237,6 +238,7 @@ 807D56AD2A869064009E591D /* GraphQLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLRequest.swift; sourceTree = ""; }; 807D56AF2A869F97009E591D /* GraphQLErrorResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLErrorResponse.swift; sourceTree = ""; }; 808EEA80291321FE001B6765 /* AnalyticsEventRequest_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsEventRequest_Tests.swift; sourceTree = ""; }; + 80B8B2FB2A8EBBFD00AB60CD /* VaultAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultAPI.swift; sourceTree = ""; }; 80B9F85126B8750000D67843 /* CorePayments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CorePayments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 80DB2F752980795D00CFB86A /* CorePaymentsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePaymentsError.swift; sourceTree = ""; }; 80DB6F0E26B89D9600277E54 /* PayPalNativePayments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PayPalNativePayments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -551,6 +553,7 @@ 06CE00A226F3E32A0000CC46 /* ConfirmPaymentSourceResponse.swift */, 80E2FDBD2A83528B0045593D /* CheckoutOrdersAPI.swift */, 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */, + 80B8B2FB2A8EBBFD00AB60CD /* VaultAPI.swift */, ); path = APIRequests; sourceTree = ""; @@ -1353,6 +1356,7 @@ 80DCC59E2719DB6F00EC7C5A /* CardClientError.swift in Sources */, 3B22E8B62A840ECF00962E34 /* CardVaultDelegate.swift in Sources */, 3BDB34942A80CE6E008100D7 /* CardVaultRequest.swift in Sources */, + 80B8B2FC2A8EBBFD00AB60CD /* VaultAPI.swift in Sources */, CB4BE27D2847AF6F00EA2DD1 /* SCA.swift in Sources */, 0647E70E2714962800F8E517 /* Address.swift in Sources */, BC7F8123275FC1350011EDC8 /* CardRequest.swift in Sources */, diff --git a/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift b/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift index 38f76c741..859a10335 100644 --- a/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift +++ b/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift @@ -20,7 +20,7 @@ class CheckoutOrdersAPI { // MARK: - Internal Methods - func confirmPaymentSource(clientID: String, cardRequest: CardRequest) async throws -> ConfirmPaymentSourceResponse { + func confirmPaymentSource(cardRequest: CardRequest) async throws -> ConfirmPaymentSourceResponse { let apiClient = APIClient(coreConfig: coreConfig) let confirmData = ConfirmPaymentSourceRequest(cardRequest: cardRequest) diff --git a/Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift b/Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift index 04338ca5a..5baf699ee 100644 --- a/Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift +++ b/Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift @@ -81,3 +81,54 @@ class UpdateSetupTokenQuery: Codable { } // swiftlint:enable indentation_width } + +struct VaultDataEncodableVariables: Encodable { + + // MARK: - Coding KEys + + private enum TopLevelKeys: String, CodingKey { + case clientID + case vaultSetupToken + case paymentSource + } + + private enum PaymentSourceKeys: String, CodingKey { + case card + } + + private enum CardKeys: String, CodingKey { + case number + case securityCode + case expiry + case name + } + + // MARK: - Private Properties + + private let vaultRequest: CardVaultRequest + private let clientID: String + + // MARK: - Initializer + init(cardVaultRequest: CardVaultRequest, clientID: String) { + self.vaultRequest = cardVaultRequest + self.clientID = clientID + } + + // TODO: - Migrate details from VaultRequest introduced in Victoria's PR + + // MARK: - Custom Encoder + + func encode(to encoder: Encoder) throws { + var topLevel = encoder.container(keyedBy: TopLevelKeys.self) + try topLevel.encode(clientID, forKey: .clientID) + try topLevel.encode(vaultRequest.setupTokenID, forKey: .vaultSetupToken) + + var paymentSource = topLevel.nestedContainer(keyedBy: PaymentSourceKeys.self, forKey: .paymentSource) + + var card = paymentSource.nestedContainer(keyedBy: CardKeys.self, forKey: .card) + try card.encode(vaultRequest.card.number, forKey: .number) + try card.encode(vaultRequest.card.securityCode, forKey: .securityCode) + try card.encode(vaultRequest.card.expiry, forKey: .expiry) + try card.encodeIfPresent(vaultRequest.card.cardholderName, forKey: .name) + } +} diff --git a/Sources/CardPayments/APIRequests/VaultAPI.swift b/Sources/CardPayments/APIRequests/VaultAPI.swift new file mode 100644 index 000000000..b2ace5c4e --- /dev/null +++ b/Sources/CardPayments/APIRequests/VaultAPI.swift @@ -0,0 +1,70 @@ +import Foundation +#if canImport(CorePayments) +import CorePayments +#endif + +/// This class coordinates networking logic for communicating with the v2/checkout/orders API. +/// +/// Details on this PayPal API can be found in PPaaS under Merchant > Checkout > Orders > v2. +class VaultAPI { + + // MARK: - Private Propertires + + private let coreConfig: CoreConfig + + // MARK: - Initializer + + init(coreConfig: CoreConfig) { + self.coreConfig = coreConfig + } + + // MARK: - Internal Methods + + func vaultWithoutPurchase(cardVaultRequest: CardVaultRequest) async throws -> UpdateSetupTokenResponse { + let apiClient = APIClient(coreConfig: coreConfig) + + // TODO: - Move JSON encoding into custom class, similar to HTTPResponseParser + let encoder = JSONEncoder() + let variables = try encoder.encode(VaultDataEncodableVariables(cardVaultRequest: cardVaultRequest, clientID: coreConfig.clientID)) + + let queryString = """ + mutation UpdateVaultSetupToken( + $clientID: String!, + $vaultSetupToken: String!, + $paymentSource: PaymentSource + ) { + updateVaultSetupToken( + clientId: $clientID + vaultSetupToken: $vaultSetupToken + paymentSource: $paymentSource + ) { + id, + status, + links { + rel, href + } + } + } + """ + + let graphQLRequest = GraphQLRequest( + query: queryString, + variables: variables, + queryNameForURL: "UpdateVaultSetupToken" + ) + + let httpResponse = try await apiClient.fetch(request: graphQLRequest) + + do { + // TODO: - Move graphQL specific parsing logic into HTTPResponseParser + let parsedResponse = try HTTPResponseParser().parse(httpResponse, as: GraphQLHTTPResponse.self) + if let graphQLResponseData = parsedResponse.data { + return graphQLResponseData + } else { + throw CardClientError.encodingError // TODO + } + } catch { + throw error // TODO + } + } +} diff --git a/Sources/CardPayments/CardClient.swift b/Sources/CardPayments/CardClient.swift index 234b79b21..4bc6c4f02 100644 --- a/Sources/CardPayments/CardClient.swift +++ b/Sources/CardPayments/CardClient.swift @@ -10,6 +10,7 @@ public class CardClient: NSObject { public weak var vaultDelegate: CardVaultDelegate? private let checkoutOrdersAPI: CheckoutOrdersAPI + private let vaultAPI: VaultAPI private let config: CoreConfig private let webAuthenticationSession: WebAuthenticationSession @@ -20,36 +21,41 @@ public class CardClient: NSObject { public init(config: CoreConfig) { self.config = config self.checkoutOrdersAPI = CheckoutOrdersAPI(coreConfig: config) + self.vaultAPI = VaultAPI(coreConfig: config) self.webAuthenticationSession = WebAuthenticationSession() } /// For internal use for testing/mocking purpose - init(config: CoreConfig, checkoutOrdersAPI: CheckoutOrdersAPI, webAuthenticationSession: WebAuthenticationSession) { + init(config: CoreConfig, + checkoutOrdersAPI: CheckoutOrdersAPI, + vaultAPI: VaultAPI, + webAuthenticationSession: WebAuthenticationSession + ) { self.config = config self.checkoutOrdersAPI = checkoutOrdersAPI + self.vaultAPI = vaultAPI self.webAuthenticationSession = webAuthenticationSession } public func vault(_ vaultRequest: CardVaultRequest) { -// Task { -// do { -// let card = vaultRequest.card -// let setupTokenID = vaultRequest.setupTokenID -// let result = try await updateSetupToken(vaultSetupTokenID: setupTokenID, card: card) -// // TODO: handle 3DS contingency with helios link -// if let link = result.links.first(where: { $0.rel == "approve" && $0.href.contains("helios") }) { -// let url = link.href -// print("3DS url \(url)") -// } else { -// let vaultResult = CardVaultResult(setupTokenID: result.id, status: result.status) -// notifyVaultSuccess(for: vaultResult) -// } -// } catch let error as CoreSDKError { -// notifyVaultFailure(with: error) -// } catch { -// notifyVaultFailure(with: CardClientError.vaultTokenError) -// } -// } + Task { + do { + let result = try await vaultAPI.vaultWithoutPurchase(cardVaultRequest: vaultRequest).updateVaultSetupToken + + // TODO: handle 3DS contingency with helios link + if let link = result.links.first(where: { $0.rel == "approve" && $0.href.contains("helios") }) { + let url = link.href + print("3DS url \(url)") + } else { + let vaultResult = CardVaultResult(setupTokenID: result.id, status: result.status) + notifyVaultSuccess(for: vaultResult) + } + } catch let error as CoreSDKError { + notifyVaultFailure(with: error) + } catch { + notifyVaultFailure(with: CardClientError.vaultTokenError) + } + } } // func updateSetupToken(vaultSetupTokenID: String, card: Card) async throws -> TokenDetails { @@ -81,7 +87,7 @@ public class CardClient: NSObject { analyticsService?.sendEvent("card-payments:3ds:started") Task { do { - let result = try await checkoutOrdersAPI.confirmPaymentSource(clientID: config.clientID, cardRequest: request) + let result = try await checkoutOrdersAPI.confirmPaymentSource(cardRequest: request) if let url: String = result.links?.first(where: { $0.rel == "payer-action" })?.href { analyticsService?.sendEvent("card-payments:3ds:confirm-payment-source:challenge-required") diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPResponse.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPResponse.swift index 6497122e6..122762f67 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPResponse.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPResponse.swift @@ -1,5 +1,5 @@ -/// Used to decode the HTTP reponse body of GraphQL requests -struct GraphQLHTTPResponse: Codable { +/// :nodoc: Used to decode the HTTP reponse body of GraphQL requests +public struct GraphQLHTTPResponse: Codable { - let data: T? + public let data: T? } diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift index 6731cfd64..124a27763 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift @@ -9,4 +9,10 @@ public struct GraphQLRequest { /// This is non-standard in the GraphQL language, but sometimes required by PayPal's GraphQL API. /// Some requests are sent to `https://www.api.paypal.com/graphql?` let queryNameForURL: String? + + public init(query: String, variables: Data, queryNameForURL: String?) { + self.query = query + self.variables = variables + self.queryNameForURL = queryNameForURL + } } From f91dd0a315b3ab09c003b5940e67503e087c6888 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Thu, 17 Aug 2023 17:22:33 -0500 Subject: [PATCH 08/19] Leave TODOs to fix variable encoding bug --- Sources/CorePayments/Networking/APIClient.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/CorePayments/Networking/APIClient.swift b/Sources/CorePayments/Networking/APIClient.swift index 444dcadaf..b7a1539d3 100644 --- a/Sources/CorePayments/Networking/APIClient.swift +++ b/Sources/CorePayments/Networking/APIClient.swift @@ -49,10 +49,19 @@ public class APIClient { public func fetch(request: GraphQLRequest) async throws -> HTTPResponse { let url = try constructGraphQLURL(queryName: request.queryNameForURL) + // TODO: - Let's have all encoding in 1 place (variables as dict not data) + let postBody = GraphQLHTTPPostBody(query: request.query, variables: request.variables) + // TODO: - encoding `Data` results in mumbo jumbo string. Why let postData = try JSONEncoder().encode(postBody) - - let httpRequest = HTTPRequest(headers: [.contentType: "application/json"], method: .post, url: url, body: postData) + + let httpRequest = HTTPRequest( + headers: [.contentType: "application/json", + .accept: "application/json"], + method: .post, + url: url, + body: postData + ) return try await http.performRequest(httpRequest) } From bbc3f2046570eb206151f097977f31f756040e2f Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 23 Aug 2023 06:37:34 -0500 Subject: [PATCH 09/19] Add x-app-name & origin headers (fixing 401 code); use dictionary over data in GraphQL request types to prevent double encoding --- Sources/CardPayments/APIRequests/VaultAPI.swift | 16 ++++++++++++++-- Sources/CorePayments/Networking/APIClient.swift | 14 +++++++------- .../Networking/Enums/HTTPHeader.swift | 4 +++- .../Networking/GraphQL/GraphQLHTTPPostBody.swift | 13 ++++++++++--- .../Networking/GraphQL/GraphQLRequest.swift | 4 ++-- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Sources/CardPayments/APIRequests/VaultAPI.swift b/Sources/CardPayments/APIRequests/VaultAPI.swift index b2ace5c4e..c9f5ab92c 100644 --- a/Sources/CardPayments/APIRequests/VaultAPI.swift +++ b/Sources/CardPayments/APIRequests/VaultAPI.swift @@ -24,8 +24,20 @@ class VaultAPI { let apiClient = APIClient(coreConfig: coreConfig) // TODO: - Move JSON encoding into custom class, similar to HTTPResponseParser - let encoder = JSONEncoder() - let variables = try encoder.encode(VaultDataEncodableVariables(cardVaultRequest: cardVaultRequest, clientID: coreConfig.clientID)) +// let encoder = JSONEncoder() +// let variables = try encoder.encode(VaultDataEncodableVariables(cardVaultRequest: cardVaultRequest, clientID: coreConfig.clientID)) + let variables: [String: Any] = [ + "clientID": coreConfig.clientID, + "vaultSetupToken": cardVaultRequest.setupTokenID, + "paymentSource": [ + "card": [ + "number": cardVaultRequest.card.number, + "securityCode": cardVaultRequest.card.securityCode, + "expiry": cardVaultRequest.card.expiry, + "name": cardVaultRequest.card.cardholderName + ] + ] + ] let queryString = """ mutation UpdateVaultSetupToken( diff --git a/Sources/CorePayments/Networking/APIClient.swift b/Sources/CorePayments/Networking/APIClient.swift index b7a1539d3..37587c74d 100644 --- a/Sources/CorePayments/Networking/APIClient.swift +++ b/Sources/CorePayments/Networking/APIClient.swift @@ -48,16 +48,16 @@ public class APIClient { /// :nodoc: public func fetch(request: GraphQLRequest) async throws -> HTTPResponse { let url = try constructGraphQLURL(queryName: request.queryNameForURL) - - // TODO: - Let's have all encoding in 1 place (variables as dict not data) - let postBody = GraphQLHTTPPostBody(query: request.query, variables: request.variables) - // TODO: - encoding `Data` results in mumbo jumbo string. Why - let postData = try JSONEncoder().encode(postBody) + let postData = try GraphQLHTTPPostBody(query: request.query, variables: request.variables).data let httpRequest = HTTPRequest( - headers: [.contentType: "application/json", - .accept: "application/json"], + headers: [ + .contentType: "application/json", + .accept: "application/json", + .appName: "ppcpmobilesdk", + .origin: coreConfig.environment.graphQLURL.absoluteString + ], method: .post, url: url, body: postData diff --git a/Sources/CorePayments/Networking/Enums/HTTPHeader.swift b/Sources/CorePayments/Networking/Enums/HTTPHeader.swift index 585344432..637bfabef 100644 --- a/Sources/CorePayments/Networking/Enums/HTTPHeader.swift +++ b/Sources/CorePayments/Networking/Enums/HTTPHeader.swift @@ -3,6 +3,8 @@ import Foundation public enum HTTPHeader: String { case accept = "Accept" case acceptLanguage = "Accept-Language" - case contentType = "Content-Type" + case appName = "x-app-name" case authorization = "Authorization" + case contentType = "Content-Type" + case origin = "Origin" } diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPPostBody.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPPostBody.swift index c01f15f91..997be3cbb 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPPostBody.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPPostBody.swift @@ -1,8 +1,15 @@ import Foundation /// The GraphQL query and variable details encoded to be sent in the POST body of a HTTP request -struct GraphQLHTTPPostBody: Encodable { +struct GraphQLHTTPPostBody { - let query: String - let variables: Data + let data: Data + + init(query: String, variables: [String: Any]) throws { + let body: [String: Any] = [ + "query": query, + "variables": variables + ] + self.data = try JSONSerialization.data(withJSONObject: body, options: []) + } } diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift index 124a27763..e935bed27 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift @@ -4,13 +4,13 @@ import Foundation public struct GraphQLRequest { let query: String - let variables: Data + let variables: [String: Any] /// This is non-standard in the GraphQL language, but sometimes required by PayPal's GraphQL API. /// Some requests are sent to `https://www.api.paypal.com/graphql?` let queryNameForURL: String? - public init(query: String, variables: Data, queryNameForURL: String?) { + public init(query: String, variables: [String: Any], queryNameForURL: String?) { self.query = query self.variables = variables self.queryNameForURL = queryNameForURL From be7fac68fd65f866cc818791515bdb2ccf8cdfd1 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 23 Aug 2023 06:56:35 -0500 Subject: [PATCH 10/19] Replace [String: Any] dict with Encodable for GraphQLQuery variables type --- .../CardPayments/APIRequests/VaultAPI.swift | 27 +++++++++---------- .../CorePayments/Networking/APIClient.swift | 5 ++-- .../GraphQL/GraphQLHTTPPostBody.swift | 25 +++++++++++------ .../Networking/GraphQL/GraphQLRequest.swift | 4 +-- 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/Sources/CardPayments/APIRequests/VaultAPI.swift b/Sources/CardPayments/APIRequests/VaultAPI.swift index c9f5ab92c..7ec002e72 100644 --- a/Sources/CardPayments/APIRequests/VaultAPI.swift +++ b/Sources/CardPayments/APIRequests/VaultAPI.swift @@ -24,20 +24,19 @@ class VaultAPI { let apiClient = APIClient(coreConfig: coreConfig) // TODO: - Move JSON encoding into custom class, similar to HTTPResponseParser -// let encoder = JSONEncoder() -// let variables = try encoder.encode(VaultDataEncodableVariables(cardVaultRequest: cardVaultRequest, clientID: coreConfig.clientID)) - let variables: [String: Any] = [ - "clientID": coreConfig.clientID, - "vaultSetupToken": cardVaultRequest.setupTokenID, - "paymentSource": [ - "card": [ - "number": cardVaultRequest.card.number, - "securityCode": cardVaultRequest.card.securityCode, - "expiry": cardVaultRequest.card.expiry, - "name": cardVaultRequest.card.cardholderName - ] - ] - ] + let variables = VaultDataEncodableVariables(cardVaultRequest: cardVaultRequest, clientID: coreConfig.clientID) +// let variables: [String: Any] = [ +// "clientID": coreConfig.clientID, +// "vaultSetupToken": cardVaultRequest.setupTokenID, +// "paymentSource": [ +// "card": [ +// "number": cardVaultRequest.card.number, +// "securityCode": cardVaultRequest.card.securityCode, +// "expiry": cardVaultRequest.card.expiry, +// "name": cardVaultRequest.card.cardholderName +// ] +// ] +// ] let queryString = """ mutation UpdateVaultSetupToken( diff --git a/Sources/CorePayments/Networking/APIClient.swift b/Sources/CorePayments/Networking/APIClient.swift index 37587c74d..5a2765ac4 100644 --- a/Sources/CorePayments/Networking/APIClient.swift +++ b/Sources/CorePayments/Networking/APIClient.swift @@ -48,8 +48,9 @@ public class APIClient { /// :nodoc: public func fetch(request: GraphQLRequest) async throws -> HTTPResponse { let url = try constructGraphQLURL(queryName: request.queryNameForURL) - - let postData = try GraphQLHTTPPostBody(query: request.query, variables: request.variables).data + + let postBody = GraphQLHTTPPostBody(query: request.query, variables: request.variables) + let postData = try JSONEncoder().encode(postBody) let httpRequest = HTTPRequest( headers: [ diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPPostBody.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPPostBody.swift index 997be3cbb..cbf1fbd2b 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPPostBody.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPPostBody.swift @@ -1,15 +1,24 @@ import Foundation /// The GraphQL query and variable details encoded to be sent in the POST body of a HTTP request -struct GraphQLHTTPPostBody { +struct GraphQLHTTPPostBody: Encodable { - let data: Data + private let query: String + private let variables: Encodable - init(query: String, variables: [String: Any]) throws { - let body: [String: Any] = [ - "query": query, - "variables": variables - ] - self.data = try JSONSerialization.data(withJSONObject: body, options: []) + enum CodingKeys: CodingKey { + case query + case variables + } + + init(query: String, variables: Encodable) { + self.query = query + self.variables = variables + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.query, forKey: .query) + try container.encode(self.variables, forKey: .variables) } } diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift index e935bed27..7f317c7e4 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLRequest.swift @@ -4,13 +4,13 @@ import Foundation public struct GraphQLRequest { let query: String - let variables: [String: Any] + let variables: Encodable /// This is non-standard in the GraphQL language, but sometimes required by PayPal's GraphQL API. /// Some requests are sent to `https://www.api.paypal.com/graphql?` let queryNameForURL: String? - public init(query: String, variables: [String: Any], queryNameForURL: String?) { + public init(query: String, variables: Encodable, queryNameForURL: String?) { self.query = query self.variables = variables self.queryNameForURL = queryNameForURL From 17731ac06c2064caf5ba62c488cbe77d6a29259a Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 23 Aug 2023 07:07:42 -0500 Subject: [PATCH 11/19] Move REST request encoding into APIClient & use Encodable over Data type at PPaaSAPI levels --- .../CardPayments/APIRequests/CheckoutOrdersAPI.swift | 7 +------ Sources/CorePayments/Networking/APIClient.swift | 11 ++++++++++- Sources/CorePayments/Networking/RESTRequest.swift | 6 +++--- .../CorePayments/Networking/TrackingEventsAPI.swift | 7 +------ 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift b/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift index 859a10335..49553f115 100644 --- a/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift +++ b/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift @@ -25,16 +25,11 @@ class CheckoutOrdersAPI { let confirmData = ConfirmPaymentSourceRequest(cardRequest: cardRequest) - // TODO: - Move JSON encoding into custom class, similar to HTTPResponseParser - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - let body = try encoder.encode(confirmData) - let restRequest = RESTRequest( path: "/v2/checkout/orders/\(cardRequest.orderID)/confirm-payment-source", method: .post, queryParameters: nil, - body: body + postParameters: confirmData ) let httpResponse = try await apiClient.fetch(request: restRequest) diff --git a/Sources/CorePayments/Networking/APIClient.swift b/Sources/CorePayments/Networking/APIClient.swift index 5a2765ac4..e7932e94a 100644 --- a/Sources/CorePayments/Networking/APIClient.swift +++ b/Sources/CorePayments/Networking/APIClient.swift @@ -40,7 +40,15 @@ public class APIClient { headers[.contentType] = "application/json" } - let httpRequest = HTTPRequest(headers: headers, method: request.method, url: url, body: request.body) + // TODO: - Move JSON encoding into custom class, similar to HTTPResponseParser + var data: Data? + if let postBody = request.postParameters { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + data = try encoder.encode(postBody) + } + + let httpRequest = HTTPRequest(headers: headers, method: request.method, url: url, body: data) return try await http.performRequest(httpRequest) } @@ -49,6 +57,7 @@ public class APIClient { public func fetch(request: GraphQLRequest) 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) let postData = try JSONEncoder().encode(postBody) diff --git a/Sources/CorePayments/Networking/RESTRequest.swift b/Sources/CorePayments/Networking/RESTRequest.swift index 8456e42c3..e7d2350e4 100644 --- a/Sources/CorePayments/Networking/RESTRequest.swift +++ b/Sources/CorePayments/Networking/RESTRequest.swift @@ -6,17 +6,17 @@ public struct RESTRequest { var path: String var method: HTTPMethod var queryParameters: [String: String]? - var body: Data? + var postParameters: Encodable? public init( path: String, method: HTTPMethod, queryParameters: [String: String]? = nil, - body: Data? = nil + postParameters: Encodable? = nil ) { self.path = path self.method = method self.queryParameters = queryParameters - self.body = body + self.postParameters = postParameters } } diff --git a/Sources/CorePayments/Networking/TrackingEventsAPI.swift b/Sources/CorePayments/Networking/TrackingEventsAPI.swift index 907ddff30..550553a87 100644 --- a/Sources/CorePayments/Networking/TrackingEventsAPI.swift +++ b/Sources/CorePayments/Networking/TrackingEventsAPI.swift @@ -11,16 +11,11 @@ class TrackingEventsAPI { // api.sandbox.paypal.com does not currently send FPTI events to BigQuery/Looker let apiClient = APIClient(coreConfig: CoreConfig(clientID: analyticsEventData.clientID, environment: .live)) - // TODO: - Move JSON encoding into custom class, similar to HTTPResponseParser - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - let body = try encoder.encode(analyticsEventData) - let restRequest = RESTRequest( path: "v1/tracking/events", method: .post, queryParameters: nil, - body: body + postParameters: analyticsEventData ) return try await apiClient.fetch(request: restRequest) From 221c62737fb6aaaeee7873f3b5de0409000a17b7 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 23 Aug 2023 07:38:07 -0500 Subject: [PATCH 12/19] Cleanup CardClient.swift diff updating from main --- Sources/CardPayments/CardClient.swift | 31 +++++++-------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/Sources/CardPayments/CardClient.swift b/Sources/CardPayments/CardClient.swift index 4bc6c4f02..5a004284e 100644 --- a/Sources/CardPayments/CardClient.swift +++ b/Sources/CardPayments/CardClient.swift @@ -8,7 +8,7 @@ public class CardClient: NSObject { public weak var delegate: CardDelegate? public weak var vaultDelegate: CardVaultDelegate? - + private let checkoutOrdersAPI: CheckoutOrdersAPI private let vaultAPI: VaultAPI @@ -26,10 +26,11 @@ public class CardClient: NSObject { } /// For internal use for testing/mocking purpose - init(config: CoreConfig, - checkoutOrdersAPI: CheckoutOrdersAPI, - vaultAPI: VaultAPI, - webAuthenticationSession: WebAuthenticationSession + init( + config: CoreConfig, + checkoutOrdersAPI: CheckoutOrdersAPI, + vaultAPI: VaultAPI, + webAuthenticationSession: WebAuthenticationSession ) { self.config = config self.checkoutOrdersAPI = checkoutOrdersAPI @@ -57,23 +58,6 @@ public class CardClient: NSObject { } } } - -// func updateSetupToken(vaultSetupTokenID: String, card: Card) async throws -> TokenDetails { -// guard let graphQLClient else { -// throw CardClientError.nilGraphQLClientError -// } -// -// let clientID = config.clientID -// let query = UpdateSetupTokenQuery(clientID: clientID, vaultSetupToken: vaultSetupTokenID, card: card) -// let response: GraphQLQueryResponse = try await graphQLClient.callGraphQL( -// name: "UpdateVaultSetupToken", query: query -// ) -// guard let data = response.data else { -// throw CardClientError.noVaultTokenDataError -// } -// -// return data.updateVaultSetupToken -// } /// Approve an order with a card, which validates buyer's card, and if valid, attaches the card as the payment source to the order. /// After the order has been successfully approved, you will need to handle capturing/authorizing the order in your server. @@ -87,7 +71,8 @@ public class CardClient: NSObject { analyticsService?.sendEvent("card-payments:3ds:started") Task { do { - let result = try await checkoutOrdersAPI.confirmPaymentSource(cardRequest: request) + let confirmPaymentRequest = try ConfirmPaymentSourceRequest(clientID: config.clientID, cardRequest: request) + let (result) = try await apiClient.fetch(request: confirmPaymentRequest) if let url: String = result.links?.first(where: { $0.rel == "payer-action" })?.href { analyticsService?.sendEvent("card-payments:3ds:confirm-payment-source:challenge-required") From a87f8f2a3d2337c1f6ad38a0df22bd7a4ee8fc01 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 23 Aug 2023 08:01:03 -0500 Subject: [PATCH 13/19] PR cleanup --- PayPal.xcodeproj/project.pbxproj | 16 +-- .../APIRequests/UpdateSetupTokenQuery.swift | 134 ------------------ .../APIRequests/UpdateVaultVariables.swift | 51 +++++++ ...tAPI.swift => VaultPaymentTokensAPI.swift} | 25 +--- Sources/CardPayments/CardClient.swift | 11 +- 5 files changed, 70 insertions(+), 167 deletions(-) delete mode 100644 Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift create mode 100644 Sources/CardPayments/APIRequests/UpdateVaultVariables.swift rename Sources/CardPayments/APIRequests/{VaultAPI.swift => VaultPaymentTokensAPI.swift} (67%) diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index ec8a7f0ca..68b9c9c8d 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -18,7 +18,7 @@ 3B109B3D2A85D1CA00D8135F /* MockGraphQLClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B109B382A85A9DB00D8135F /* MockGraphQLClient.swift */; }; 3B22E8B62A840ECF00962E34 /* CardVaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B52A840ECF00962E34 /* CardVaultDelegate.swift */; }; 3B22E8B82A841AEA00962E34 /* CardVaultResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */; }; - 3B79E4F72A8503CA00C01D06 /* UpdateSetupTokenQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B79E4F62A8503C900C01D06 /* UpdateSetupTokenQuery.swift */; }; + 3B79E4F72A8503CA00C01D06 /* UpdateVaultVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */; }; 3B80D50C2A27979000D2EAC4 /* FailingJSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */; }; 3BD82DBB2A835AF900CBE764 /* UpdateSetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */; }; 3BDB34942A80CE6E008100D7 /* CardVaultRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDB34932A80CE6E008100D7 /* CardVaultRequest.swift */; }; @@ -49,7 +49,7 @@ 807D56AE2A869064009E591D /* GraphQLRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807D56AD2A869064009E591D /* GraphQLRequest.swift */; }; 807D56B02A869F97009E591D /* GraphQLErrorResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807D56AF2A869F97009E591D /* GraphQLErrorResponse.swift */; }; 808EEA81291321FE001B6765 /* AnalyticsEventRequest_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 808EEA80291321FE001B6765 /* AnalyticsEventRequest_Tests.swift */; }; - 80B8B2FC2A8EBBFD00AB60CD /* VaultAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B8B2FB2A8EBBFD00AB60CD /* VaultAPI.swift */; }; + 80B8B2FC2A8EBBFD00AB60CD /* VaultPaymentTokensAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B8B2FB2A8EBBFD00AB60CD /* VaultPaymentTokensAPI.swift */; }; 80D0C1382731CC9B00548A3D /* PayPalNativeCheckoutClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE7116152723227200165069 /* PayPalNativeCheckoutClient.swift */; }; 80DB2F762980795D00CFB86A /* CorePaymentsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DB2F752980795D00CFB86A /* CorePaymentsError.swift */; }; 80DB6F1726B89DC700277E54 /* CorePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80B9F85126B8750000D67843 /* CorePayments.framework */; platformFilter = ios; }; @@ -210,7 +210,7 @@ 3B109B3A2A85B54800D8135F /* MockCardVaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardVaultDelegate.swift; sourceTree = ""; }; 3B22E8B52A840ECF00962E34 /* CardVaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultDelegate.swift; sourceTree = ""; }; 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultResult.swift; sourceTree = ""; }; - 3B79E4F62A8503C900C01D06 /* UpdateSetupTokenQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateSetupTokenQuery.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 = ""; }; 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSetupTokenResponse.swift; sourceTree = ""; }; 3BDB34932A80CE6E008100D7 /* CardVaultRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultRequest.swift; sourceTree = ""; }; @@ -238,7 +238,7 @@ 807D56AD2A869064009E591D /* GraphQLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLRequest.swift; sourceTree = ""; }; 807D56AF2A869F97009E591D /* GraphQLErrorResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLErrorResponse.swift; sourceTree = ""; }; 808EEA80291321FE001B6765 /* AnalyticsEventRequest_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsEventRequest_Tests.swift; sourceTree = ""; }; - 80B8B2FB2A8EBBFD00AB60CD /* VaultAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultAPI.swift; sourceTree = ""; }; + 80B8B2FB2A8EBBFD00AB60CD /* VaultPaymentTokensAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultPaymentTokensAPI.swift; sourceTree = ""; }; 80B9F85126B8750000D67843 /* CorePayments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CorePayments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 80DB2F752980795D00CFB86A /* CorePaymentsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePaymentsError.swift; sourceTree = ""; }; 80DB6F0E26B89D9600277E54 /* PayPalNativePayments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PayPalNativePayments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -548,12 +548,12 @@ 80DBC9D829C336D500462539 /* APIRequests */ = { isa = PBXGroup; children = ( - 3B79E4F62A8503C900C01D06 /* UpdateSetupTokenQuery.swift */, + 3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */, 06CE009F26F3DF100000CC46 /* ConfirmPaymentSourceRequest.swift */, 06CE00A226F3E32A0000CC46 /* ConfirmPaymentSourceResponse.swift */, 80E2FDBD2A83528B0045593D /* CheckoutOrdersAPI.swift */, 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */, - 80B8B2FB2A8EBBFD00AB60CD /* VaultAPI.swift */, + 80B8B2FB2A8EBBFD00AB60CD /* VaultPaymentTokensAPI.swift */, ); path = APIRequests; sourceTree = ""; @@ -1343,7 +1343,7 @@ files = ( 06CE009B26F3D5A40000CC46 /* CardClient.swift in Sources */, 80E2FDBE2A83528B0045593D /* CheckoutOrdersAPI.swift in Sources */, - 3B79E4F72A8503CA00C01D06 /* UpdateSetupTokenQuery.swift in Sources */, + 3B79E4F72A8503CA00C01D06 /* UpdateVaultVariables.swift in Sources */, 8048D28C270B9DE00072214A /* ConfirmPaymentSourceResponse.swift in Sources */, 3B109B3D2A85D1CA00D8135F /* MockGraphQLClient.swift in Sources */, 3D1763A22720722A00652E1C /* CardResult.swift in Sources */, @@ -1356,7 +1356,7 @@ 80DCC59E2719DB6F00EC7C5A /* CardClientError.swift in Sources */, 3B22E8B62A840ECF00962E34 /* CardVaultDelegate.swift in Sources */, 3BDB34942A80CE6E008100D7 /* CardVaultRequest.swift in Sources */, - 80B8B2FC2A8EBBFD00AB60CD /* VaultAPI.swift in Sources */, + 80B8B2FC2A8EBBFD00AB60CD /* VaultPaymentTokensAPI.swift in Sources */, CB4BE27D2847AF6F00EA2DD1 /* SCA.swift in Sources */, 0647E70E2714962800F8E517 /* Address.swift in Sources */, BC7F8123275FC1350011EDC8 /* CardRequest.swift in Sources */, diff --git a/Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift b/Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift deleted file mode 100644 index 5baf699ee..000000000 --- a/Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift +++ /dev/null @@ -1,134 +0,0 @@ -import Foundation -#if canImport(CorePayments) -import CorePayments -#endif - -class UpdateSetupTokenQuery: Codable { - - struct VaultCard: Codable { - - public let number: String - public let expiry: String - public let securityCode: String - public let name: String? - public let billingAddress: Address? - - init(number: String, expiry: String, securityCode: String, name: String? = nil, billingAddress: Address? = nil) { - self.number = number - self.expiry = expiry - self.securityCode = securityCode - self.name = name - self.billingAddress = billingAddress - } - } - - struct PaymentSource: Codable { - - let card: VaultCard - } - - struct Variables: Codable { - - let clientID: String - let vaultSetupToken: String - let paymentSource: PaymentSource - } - - var query: String - var variables: Variables? - - init( - clientID: String, - vaultSetupToken: String, - card: Card - ) { - let vaultCard = VaultCard( - number: card.number, - expiry: card.expiry, - securityCode: card.securityCode, - name: card.cardholderName, - billingAddress: card.billingAddress - ) - - let paymentSource = PaymentSource(card: vaultCard) - - self.variables = Variables( - clientID: clientID, - vaultSetupToken: vaultSetupToken, - paymentSource: paymentSource - ) - // swiftlint:disable indentation_width - let queryString = """ - mutation UpdateVaultSetupToken( - $clientID: String!, - $vaultSetupToken: String!, - $paymentSource: PaymentSource - ) { - updateVaultSetupToken( - clientId: $clientID - vaultSetupToken: $vaultSetupToken - paymentSource: $paymentSource - ) { - id, - status, - links { - rel, href - } - } - } - """ - self.query = queryString - } - // swiftlint:enable indentation_width -} - -struct VaultDataEncodableVariables: Encodable { - - // MARK: - Coding KEys - - private enum TopLevelKeys: String, CodingKey { - case clientID - case vaultSetupToken - case paymentSource - } - - private enum PaymentSourceKeys: String, CodingKey { - case card - } - - private enum CardKeys: String, CodingKey { - case number - case securityCode - case expiry - case name - } - - // MARK: - Private Properties - - private let vaultRequest: CardVaultRequest - private let clientID: String - - // MARK: - Initializer - init(cardVaultRequest: CardVaultRequest, clientID: String) { - self.vaultRequest = cardVaultRequest - self.clientID = clientID - } - - // TODO: - Migrate details from VaultRequest introduced in Victoria's PR - - // MARK: - Custom Encoder - - func encode(to encoder: Encoder) throws { - var topLevel = encoder.container(keyedBy: TopLevelKeys.self) - try topLevel.encode(clientID, forKey: .clientID) - try topLevel.encode(vaultRequest.setupTokenID, forKey: .vaultSetupToken) - - var paymentSource = topLevel.nestedContainer(keyedBy: PaymentSourceKeys.self, forKey: .paymentSource) - - var card = paymentSource.nestedContainer(keyedBy: CardKeys.self, forKey: .card) - try card.encode(vaultRequest.card.number, forKey: .number) - try card.encode(vaultRequest.card.securityCode, forKey: .securityCode) - try card.encode(vaultRequest.card.expiry, forKey: .expiry) - try card.encodeIfPresent(vaultRequest.card.cardholderName, forKey: .name) - } -} diff --git a/Sources/CardPayments/APIRequests/UpdateVaultVariables.swift b/Sources/CardPayments/APIRequests/UpdateVaultVariables.swift new file mode 100644 index 000000000..a5ecf27ae --- /dev/null +++ b/Sources/CardPayments/APIRequests/UpdateVaultVariables.swift @@ -0,0 +1,51 @@ +import Foundation + +struct UpdateVaultVariables: Encodable { + + // MARK: - Coding Keys + + private enum TopLevelKeys: String, CodingKey { + case clientID + case vaultSetupToken + case paymentSource + } + + private enum PaymentSourceKeys: String, CodingKey { + case card + } + + private enum CardKeys: String, CodingKey { + case number + case securityCode + case expiry + case name + } + + // MARK: - Private Properties + + private let vaultRequest: CardVaultRequest + private let clientID: String + + // MARK: - Initializer + + init(cardVaultRequest: CardVaultRequest, clientID: String) { + self.vaultRequest = cardVaultRequest + self.clientID = clientID + } + + // MARK: - Custom Encoder + + func encode(to encoder: Encoder) throws { + var topLevel = encoder.container(keyedBy: TopLevelKeys.self) + try topLevel.encode(clientID, forKey: .clientID) + try topLevel.encode(vaultRequest.setupTokenID, forKey: .vaultSetupToken) + + var paymentSource = topLevel.nestedContainer(keyedBy: PaymentSourceKeys.self, forKey: .paymentSource) + + var card = paymentSource.nestedContainer(keyedBy: CardKeys.self, forKey: .card) + try card.encode(vaultRequest.card.number, forKey: .number) + try card.encode(vaultRequest.card.securityCode, forKey: .securityCode) + try card.encode(vaultRequest.card.expiry, forKey: .expiry) + try card.encodeIfPresent(vaultRequest.card.cardholderName, forKey: .name) + } +} diff --git a/Sources/CardPayments/APIRequests/VaultAPI.swift b/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift similarity index 67% rename from Sources/CardPayments/APIRequests/VaultAPI.swift rename to Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift index 7ec002e72..7f765bc0e 100644 --- a/Sources/CardPayments/APIRequests/VaultAPI.swift +++ b/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift @@ -3,10 +3,10 @@ import Foundation import CorePayments #endif -/// This class coordinates networking logic for communicating with the v2/checkout/orders API. +/// This class coordinates networking logic for communicating with the /graphql?UpdateVaultSetupToken API. /// -/// Details on this PayPal API can be found in PPaaS under Merchant > Checkout > Orders > v2. -class VaultAPI { +/// Details on this PayPal API can be found in PPaaS under Merchant > Data Vault > Payment Method Tokens > v3. +class VaultPaymentTokensAPI { // MARK: - Private Propertires @@ -20,23 +20,8 @@ class VaultAPI { // MARK: - Internal Methods - func vaultWithoutPurchase(cardVaultRequest: CardVaultRequest) async throws -> UpdateSetupTokenResponse { + func updateSetupToken(cardVaultRequest: CardVaultRequest) async throws -> UpdateSetupTokenResponse { let apiClient = APIClient(coreConfig: coreConfig) - - // TODO: - Move JSON encoding into custom class, similar to HTTPResponseParser - let variables = VaultDataEncodableVariables(cardVaultRequest: cardVaultRequest, clientID: coreConfig.clientID) -// let variables: [String: Any] = [ -// "clientID": coreConfig.clientID, -// "vaultSetupToken": cardVaultRequest.setupTokenID, -// "paymentSource": [ -// "card": [ -// "number": cardVaultRequest.card.number, -// "securityCode": cardVaultRequest.card.securityCode, -// "expiry": cardVaultRequest.card.expiry, -// "name": cardVaultRequest.card.cardholderName -// ] -// ] -// ] let queryString = """ mutation UpdateVaultSetupToken( @@ -57,6 +42,8 @@ class VaultAPI { } } """ + + let variables = UpdateVaultVariables(cardVaultRequest: cardVaultRequest, clientID: coreConfig.clientID) let graphQLRequest = GraphQLRequest( query: queryString, diff --git a/Sources/CardPayments/CardClient.swift b/Sources/CardPayments/CardClient.swift index 5a004284e..22c2b3f8c 100644 --- a/Sources/CardPayments/CardClient.swift +++ b/Sources/CardPayments/CardClient.swift @@ -10,7 +10,7 @@ public class CardClient: NSObject { public weak var vaultDelegate: CardVaultDelegate? private let checkoutOrdersAPI: CheckoutOrdersAPI - private let vaultAPI: VaultAPI + private let vaultAPI: VaultPaymentTokensAPI private let config: CoreConfig private let webAuthenticationSession: WebAuthenticationSession @@ -21,7 +21,7 @@ public class CardClient: NSObject { public init(config: CoreConfig) { self.config = config self.checkoutOrdersAPI = CheckoutOrdersAPI(coreConfig: config) - self.vaultAPI = VaultAPI(coreConfig: config) + self.vaultAPI = VaultPaymentTokensAPI(coreConfig: config) self.webAuthenticationSession = WebAuthenticationSession() } @@ -29,7 +29,7 @@ public class CardClient: NSObject { init( config: CoreConfig, checkoutOrdersAPI: CheckoutOrdersAPI, - vaultAPI: VaultAPI, + vaultAPI: VaultPaymentTokensAPI, webAuthenticationSession: WebAuthenticationSession ) { self.config = config @@ -41,7 +41,7 @@ public class CardClient: NSObject { public func vault(_ vaultRequest: CardVaultRequest) { Task { do { - let result = try await vaultAPI.vaultWithoutPurchase(cardVaultRequest: vaultRequest).updateVaultSetupToken + let result = try await vaultAPI.updateSetupToken(cardVaultRequest: vaultRequest).updateVaultSetupToken // TODO: handle 3DS contingency with helios link if let link = result.links.first(where: { $0.rel == "approve" && $0.href.contains("helios") }) { @@ -71,8 +71,7 @@ public class CardClient: NSObject { analyticsService?.sendEvent("card-payments:3ds:started") Task { do { - let confirmPaymentRequest = try ConfirmPaymentSourceRequest(clientID: config.clientID, cardRequest: request) - let (result) = try await apiClient.fetch(request: confirmPaymentRequest) + let (result) = try await checkoutOrdersAPI.confirmPaymentSource(cardRequest: request) if let url: String = result.links?.first(where: { $0.rel == "payer-action" })?.href { analyticsService?.sendEvent("card-payments:3ds:confirm-payment-source:challenge-required") From 185a8de1f395a9d49f947c5b2e938bab706bb5f0 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 23 Aug 2023 08:42:00 -0500 Subject: [PATCH 14/19] Add GraphQL parsing capability to HTTPResponseParser --- .../APIRequests/CheckoutOrdersAPI.swift | 2 +- .../APIRequests/VaultPaymentTokensAPI.swift | 12 +--- Sources/CardPayments/CardClient.swift | 2 +- .../Networking/APIClientError.swift | 9 +++ .../GraphQL/GraphQLHTTPResponse.swift | 2 +- .../Networking/HTTPResponseParser.swift | 58 +++++++++++++++---- 6 files changed, 60 insertions(+), 25 deletions(-) diff --git a/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift b/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift index 49553f115..7a6c99656 100644 --- a/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift +++ b/Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift @@ -33,6 +33,6 @@ class CheckoutOrdersAPI { ) let httpResponse = try await apiClient.fetch(request: restRequest) - return try HTTPResponseParser().parse(httpResponse, as: ConfirmPaymentSourceResponse.self) + return try HTTPResponseParser().parseREST(httpResponse, as: ConfirmPaymentSourceResponse.self) } } diff --git a/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift b/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift index 7f765bc0e..894269b74 100644 --- a/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift +++ b/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift @@ -53,16 +53,6 @@ class VaultPaymentTokensAPI { let httpResponse = try await apiClient.fetch(request: graphQLRequest) - do { - // TODO: - Move graphQL specific parsing logic into HTTPResponseParser - let parsedResponse = try HTTPResponseParser().parse(httpResponse, as: GraphQLHTTPResponse.self) - if let graphQLResponseData = parsedResponse.data { - return graphQLResponseData - } else { - throw CardClientError.encodingError // TODO - } - } catch { - throw error // TODO - } + return try HTTPResponseParser().parseGraphQL(httpResponse, as: UpdateSetupTokenResponse.self) } } diff --git a/Sources/CardPayments/CardClient.swift b/Sources/CardPayments/CardClient.swift index 22c2b3f8c..9e5f36e81 100644 --- a/Sources/CardPayments/CardClient.swift +++ b/Sources/CardPayments/CardClient.swift @@ -71,7 +71,7 @@ public class CardClient: NSObject { analyticsService?.sendEvent("card-payments:3ds:started") Task { do { - let (result) = try await checkoutOrdersAPI.confirmPaymentSource(cardRequest: request) + let result = try await checkoutOrdersAPI.confirmPaymentSource(cardRequest: request) if let url: String = result.links?.first(where: { $0.rel == "payer-action" })?.href { analyticsService?.sendEvent("card-payments:3ds:confirm-payment-source:challenge-required") diff --git a/Sources/CorePayments/Networking/APIClientError.swift b/Sources/CorePayments/Networking/APIClientError.swift index f459b9f0a..94fd4bef4 100644 --- a/Sources/CorePayments/Networking/APIClientError.swift +++ b/Sources/CorePayments/Networking/APIClientError.swift @@ -25,6 +25,9 @@ enum APIClientError { /// 6. The server's response body returned an error message. case serverResponseError + + /// 7. Missing expected GraphQL response data key. + case noGraphQLDataKey } static let unknownError = CoreSDKError( @@ -74,4 +77,10 @@ enum APIClientError { errorDescription: description ) } + + static let noGraphQLDataKey = CoreSDKError( + code: Code.noResponseData.rawValue, + domain: domain, + errorDescription: "An error occured due to missing `data` key in GraphQL query response. Contact developer.paypal.com/support." + ) } diff --git a/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPResponse.swift b/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPResponse.swift index 122762f67..131e00eba 100644 --- a/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPResponse.swift +++ b/Sources/CorePayments/Networking/GraphQL/GraphQLHTTPResponse.swift @@ -1,5 +1,5 @@ /// :nodoc: Used to decode the HTTP reponse body of GraphQL requests -public struct GraphQLHTTPResponse: Codable { +public struct GraphQLHTTPResponse: Decodable { public let data: T? } diff --git a/Sources/CorePayments/Networking/HTTPResponseParser.swift b/Sources/CorePayments/Networking/HTTPResponseParser.swift index 058c71bae..060ae85f8 100644 --- a/Sources/CorePayments/Networking/HTTPResponseParser.swift +++ b/Sources/CorePayments/Networking/HTTPResponseParser.swift @@ -4,32 +4,68 @@ import Foundation public class HTTPResponseParser { private let decoder: JSONDecoder + + // MARK: - Initializer public init(decoder: JSONDecoder = JSONDecoder()) { // exposed for test injection self.decoder = decoder decoder.keyDecodingStrategy = .convertFromSnakeCase } - // TODO: - Update this func (or file) to handle both GraphQL and REST error parsing - public func parse(_ httpResponse: HTTPResponse, as type: T.Type) throws -> T { + // MARK: - Public Methods + + public func parseREST(_ httpResponse: HTTPResponse, as type: T.Type) throws -> T { guard let data = httpResponse.body else { throw APIClientError.noResponseDataError } if httpResponse.isSuccessful { - do { - let decodedData = try decoder.decode(T.self, from: data) - return (decodedData) - } catch { - throw APIClientError.jsonDecodingError(error.localizedDescription) - } + return try parseSuccessResult(data, as: T.self) + } else { + return try parseErrorResult(data, as: T.self) + } + } + + public func parseGraphQL(_ httpResponse: HTTPResponse, as type: T.Type) throws -> T { + guard let data = httpResponse.body else { + throw APIClientError.noResponseDataError + } + + if httpResponse.isSuccessful { + return try parseSuccessResult(data, as: T.self, isGraphQL: true) } else { - do { + return try parseErrorResult(data, as: T.self, isGraphQL: true) + } + } + + // MARK: - Private Methods + + private func parseSuccessResult(_ data: Data, as type: T.Type, isGraphQL: Bool = false) throws -> T { + do { + if isGraphQL { + guard let data = try decoder.decode(GraphQLHTTPResponse.self, from: data).data else { + throw APIClientError.noGraphQLDataKey + } + return data + } else { + return try decoder.decode(T.self, from: data) + } + } catch { + throw APIClientError.jsonDecodingError(error.localizedDescription) + } + } + + private func parseErrorResult(_ data: Data, as type: T.Type, isGraphQL: Bool = false) throws -> T { + do { + if isGraphQL { + let errorData = try decoder.decode(GraphQLErrorResponse.self, from: data) + throw APIClientError.serverResponseError(errorData.error) + } else { let errorData = try decoder.decode(ErrorResponse.self, from: data) throw APIClientError.serverResponseError(errorData.readableDescription) - } catch { - throw APIClientError.jsonDecodingError(error.localizedDescription) } + } catch { + throw APIClientError.jsonDecodingError(error.localizedDescription) } } } From f8300bd7a3caa342157ebb7caaa9878ecf65a104 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 23 Aug 2023 09:12:04 -0500 Subject: [PATCH 15/19] Update HTTPResponseParser unit tests --- .../HTTPResponseParser_Tests.swift | 100 +++++++++++++++--- 1 file changed, 86 insertions(+), 14 deletions(-) diff --git a/UnitTests/PaymentsCoreTests/HTTPResponseParser_Tests.swift b/UnitTests/PaymentsCoreTests/HTTPResponseParser_Tests.swift index d836ec664..0887726f6 100644 --- a/UnitTests/PaymentsCoreTests/HTTPResponseParser_Tests.swift +++ b/UnitTests/PaymentsCoreTests/HTTPResponseParser_Tests.swift @@ -8,49 +8,48 @@ class HTTPResponseParser_Tests: XCTestCase { let sut = HTTPResponseParser() - // MARK: - parse() + // MARK: - parseREST() - func testParse_whenNoBody_returnsError() { + func testParseREST_whenNoBody_returnsError() { let mockHTTPResponse = HTTPResponse(status: 200, body: nil) do { - _ = try sut.parse(mockHTTPResponse, as: FakeResponse.self) + _ = try sut.parseREST(mockHTTPResponse, as: FakeResponse.self) XCTFail("Expected parse() to throw") - } catch let error as CoreSDKError { + } catch { + let error = error as! CoreSDKError XCTAssertEqual(error.localizedDescription, "An error occured due to missing HTTP response data. Contact developer.paypal.com/support.") XCTAssertEqual(error.domain, APIClientError.domain) - } catch { - XCTFail("Unexpected error type") } } - func testParse_whenSuccessStatusCodeWithValidBody_returnsDecodable() { + func testParseREST_whenSuccessStatusCodeWithValidBody_returnsDecodable() { let jsonResponse = #"{ "fake_param": "fake-response" }"# let mockHTTPResponse = HTTPResponse(status: 203, body: jsonResponse.data(using: .utf8)!) do { - let response = try sut.parse(mockHTTPResponse, as: FakeResponse.self) + let response = try sut.parseREST(mockHTTPResponse, as: FakeResponse.self) XCTAssertEqual(response.fakeParam, "fake-response") } catch { XCTFail("Expected parse() to succeed") } } - func testParse_whenSuccessStatusCodeWithDecodingError_bubblesDecodingErrorMessage() { + func testParseREST_whenSuccessStatusCodeWithDecodingError_bubblesDecodingErrorMessage() { let jsonResponse = #"{ "incorrect_body": "borked-response" }"# let mockHTTPResponse = HTTPResponse(status: 203, body: jsonResponse.data(using: .utf8)!) let sut = HTTPResponseParser(decoder: FailingJSONDecoder()) do { - _ = try sut.parse(mockHTTPResponse, as: FakeResponse.self) + _ = try sut.parseREST(mockHTTPResponse, as: FakeResponse.self) XCTFail("Expected parse() to throw") } catch { XCTAssertEqual(error.localizedDescription, "Stub message from JSONDecoder.") } } - func testParse_whenBadStatusCodeWithErrorBody_returnsReadableError() { + func testParseREST_whenBadStatusCodeWithErrorBody_returnsReadableError() { let jsonResponse = """ { "name": "fake-error-name", @@ -60,13 +59,86 @@ class HTTPResponseParser_Tests: XCTestCase { let mockHTTPResponse = HTTPResponse(status: 500, body: jsonResponse.data(using: .utf8)!) do { - _ = try sut.parse(mockHTTPResponse, as: FakeResponse.self) + _ = try sut.parseREST(mockHTTPResponse, as: FakeResponse.self) XCTFail("Expected parse() to throw") - } catch let error as CoreSDKError { + } catch { + let error = error as! CoreSDKError XCTAssertEqual(error.localizedDescription, "fake-error-name: fake-message") XCTAssertEqual(error.domain, APIClientError.domain) + } + } + + // MARK: - parseGraphQL() + + func testParseGraphQL_whenNoBody_returnsError() { + let mockHTTPResponse = HTTPResponse(status: 200, body: nil) + + do { + _ = try sut.parseGraphQL(mockHTTPResponse, as: FakeResponse.self) + XCTFail("Expected parse() to throw") } catch { - XCTFail("Unexpected error type") + let error = error as! CoreSDKError + XCTAssertEqual(error.localizedDescription, "An error occured due to missing HTTP response data. Contact developer.paypal.com/support.") + XCTAssertEqual(error.domain, APIClientError.domain) + } + } + + func testParseGraphQL_whenSuccessStatusCodeWithValidBody_returnsDecodable() { + let jsonResponse = #"{ "data": { "fake_param": "fake-response" } }"# + let mockHTTPResponse = HTTPResponse(status: 203, body: jsonResponse.data(using: .utf8)!) + + do { + let response = try sut.parseGraphQL(mockHTTPResponse, as: FakeResponse.self) + XCTAssertEqual(response.fakeParam, "fake-response") + } catch { + XCTFail("Expected parse() to succeed") + } + } + + func testParseGraphQL_whenSuccessStatusCodeWithMissingDataField_returnsError() { + let jsonResponse = #"{ "fake_param": "fake-response" }"# + let mockHTTPResponse = HTTPResponse(status: 203, body: jsonResponse.data(using: .utf8)!) + + do { + _ = try sut.parseGraphQL(mockHTTPResponse, as: FakeResponse.self) + XCTFail("Expected parse() to throw") + } catch { + let error = error as! CoreSDKError + XCTAssertEqual(error.localizedDescription, "An error occured due to missing `data` key in GraphQL query response. Contact developer.paypal.com/support.") + XCTAssertEqual(error.domain, APIClientError.domain) + } + } + + func testParseGraphQL_whenSuccessStatusCodeWithDecodingError_bubblesDecodingErrorMessage() { + let jsonResponse = #"{ "incorrect_body": "borked-response" }"# + let mockHTTPResponse = HTTPResponse(status: 203, body: jsonResponse.data(using: .utf8)!) + + let sut = HTTPResponseParser(decoder: FailingJSONDecoder()) + + do { + _ = try sut.parseGraphQL(mockHTTPResponse, as: FakeResponse.self) + XCTFail("Expected parse() to throw") + } catch { + XCTAssertEqual(error.localizedDescription, "Stub message from JSONDecoder.") + } + } + + func testParseGraphQL_whenBadStatusCodeWithErrorBody_returnsReadableError() { + let jsonResponse = """ + { + "error": "fake-error-description", + "correlation_id": "fake-correlation-id" + } + """ + let mockHTTPResponse = HTTPResponse(status: 500, body: jsonResponse.data(using: .utf8)!) + + do { + _ = try sut.parseGraphQL(mockHTTPResponse, as: FakeResponse.self) + XCTFail("Expected parse() to throw") + } catch { + let error = error as! CoreSDKError + XCTAssertEqual(error.localizedDescription, "fake-error-description") + XCTAssertEqual(error.domain, APIClientError.domain) } } } From 85aea4330de2f2921099f4354a823d7ec35f1c1f Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 23 Aug 2023 09:19:46 -0500 Subject: [PATCH 16/19] REmove GraphQLClient unit test file --- PayPal.xcodeproj/project.pbxproj | 4 -- .../GraphQLClient_Tests.swift | 70 ------------------- 2 files changed, 74 deletions(-) delete mode 100644 UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index 68b9c9c8d..a761e336f 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -147,7 +147,6 @@ CBD6004728D0C24A00C3EFF6 /* MockPayPalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD6004628D0C24900C3EFF6 /* MockPayPalDelegate.swift */; }; E6022E802857C6BE008B0E27 /* GraphQLHTTPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64623222836A69E008AC8E1 /* GraphQLHTTPResponse.swift */; }; E64763712899B60C00074113 /* MockAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64763702899B60C00074113 /* MockAPIClient.swift */; }; - E699EC16285A388E0044A753 /* GraphQLClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E699EC15285A388E0044A753 /* GraphQLClient_Tests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -335,7 +334,6 @@ CBD6004628D0C24900C3EFF6 /* MockPayPalDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPayPalDelegate.swift; sourceTree = ""; }; E64623222836A69E008AC8E1 /* GraphQLHTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLHTTPResponse.swift; sourceTree = ""; }; E64763702899B60C00074113 /* MockAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAPIClient.swift; sourceTree = ""; }; - E699EC15285A388E0044A753 /* GraphQLClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLClient_Tests.swift; sourceTree = ""; }; OBJ_16 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; "PayPal::PayPalTests::Product" /* PayPalNativeCheckoutTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = PayPalNativeCheckoutTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -494,7 +492,6 @@ 8036C1E0270F9BE700C0F091 /* APIClient_Tests.swift */, 802C4A752945676E00896A5D /* AnalyticsService_Tests.swift */, 8036C1E1270F9BE700C0F091 /* Environment_Tests.swift */, - E699EC15285A388E0044A753 /* GraphQLClient_Tests.swift */, 808EEA80291321FE001B6765 /* AnalyticsEventRequest_Tests.swift */, 80FC261C29847AC7008EC841 /* HTTP_Tests.swift */, 807BF5902A2A5D48002F32B3 /* HTTPResponseParser_Tests.swift */, @@ -1261,7 +1258,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E699EC16285A388E0044A753 /* GraphQLClient_Tests.swift in Sources */, 808EEA81291321FE001B6765 /* AnalyticsEventRequest_Tests.swift in Sources */, 8036C1E5270F9BE700C0F091 /* Environment_Tests.swift in Sources */, 80FC261D29847AC7008EC841 /* HTTP_Tests.swift in Sources */, diff --git a/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift b/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift deleted file mode 100644 index 6be0fa6a8..000000000 --- a/UnitTests/PaymentsCoreTests/GraphQLClient_Tests.swift +++ /dev/null @@ -1,70 +0,0 @@ -import XCTest -@testable import CorePayments -@testable import TestShared - -class GraphQLClient_Tests: XCTestCase { - - let mockClientID = "mockClientId" - - // MARK: - Helper Properties - - let successURLResponse = HTTPURLResponse(url: URL(string: "www.test.com")!, statusCode: 200, httpVersion: "https", headerFields: [:]) - let fakeRequest = FakeRequest() - var config: CoreConfig! - var mockURLSession: MockURLSession! - var graphQLClient: GraphQLClient! - var graphQLQuery: (any GraphQLQuery)? - // TODO: add unit tests in appropriate class after networking & graphQL layer refactor is done - - // MARK: - Test lifecycle - - override func setUp() { - super.setUp() - config = CoreConfig(clientID: mockClientID, environment: .sandbox) - mockURLSession = MockURLSession() - mockURLSession.cannedError = nil - mockURLSession.cannedURLResponse = nil - mockURLSession.cannedJSONData = nil - - graphQLClient = GraphQLClient(environment: .sandbox, urlSession: mockURLSession) - } - - // MARK: - fetch() tests - func testGraphQLClient_verifyEmptyResponse() async throws { - mockURLSession.cannedURLResponse = successURLResponse - mockURLSession.cannedJSONData = graphQLQueryResponseWithoutData - - - mockURLSession.cannedURLResponse = HTTPURLResponse( - url: URL(string: "www.fake.com")!, - statusCode: 200, - httpVersion: "1", - headerFields: ["Paypal-Debug-Id": "454532"] - ) - } - - func testGraphQLClient_verifyNonEmptyResponse() async throws { - mockURLSession.cannedURLResponse = successURLResponse - mockURLSession.cannedJSONData = graphQLQueryResponseWithData - - - mockURLSession.cannedURLResponse = HTTPURLResponse( - url: URL(string: "www.fake.com")!, - statusCode: 200, - httpVersion: "1", - headerFields: ["Paypal-Debug-Id": "454532"] - ) - } - - let graphQLQueryResponseWithData = """ - { - "data": {} - } - """ - - let graphQLQueryResponseWithoutData = """ - { - - } - """ -} From 6ed48aa08b5bbe69203baee68952bd5f350a7a87 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 23 Aug 2023 09:36:05 -0500 Subject: [PATCH 17/19] Remove MockGraphQLClient --- PayPal.xcodeproj/project.pbxproj | 4 ---- .../CardPaymentsTests/MockGraphQLClient.swift | 20 ------------------- 2 files changed, 24 deletions(-) delete mode 100644 UnitTests/CardPaymentsTests/MockGraphQLClient.swift diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index a761e336f..a22f8b4f7 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ 06CE009926F3D1660000CC46 /* CoreConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CE009826F3D1660000CC46 /* CoreConfig.swift */; }; 06CE009B26F3D5A40000CC46 /* CardClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CE009A26F3D5A40000CC46 /* CardClient.swift */; }; 3B109B3C2A85CC6200D8135F /* MockCardVaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B109B3A2A85B54800D8135F /* MockCardVaultDelegate.swift */; }; - 3B109B3D2A85D1CA00D8135F /* MockGraphQLClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B109B382A85A9DB00D8135F /* MockGraphQLClient.swift */; }; 3B22E8B62A840ECF00962E34 /* CardVaultDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B52A840ECF00962E34 /* CardVaultDelegate.swift */; }; 3B22E8B82A841AEA00962E34 /* CardVaultResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */; }; 3B79E4F72A8503CA00C01D06 /* UpdateVaultVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */; }; @@ -205,7 +204,6 @@ 06CE009A26F3D5A40000CC46 /* CardClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardClient.swift; sourceTree = ""; }; 06CE009F26F3DF100000CC46 /* ConfirmPaymentSourceRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmPaymentSourceRequest.swift; sourceTree = ""; }; 06CE00A226F3E32A0000CC46 /* ConfirmPaymentSourceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmPaymentSourceResponse.swift; sourceTree = ""; }; - 3B109B382A85A9DB00D8135F /* MockGraphQLClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGraphQLClient.swift; sourceTree = ""; }; 3B109B3A2A85B54800D8135F /* MockCardVaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardVaultDelegate.swift; sourceTree = ""; }; 3B22E8B52A840ECF00962E34 /* CardVaultDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultDelegate.swift; sourceTree = ""; }; 3B22E8B72A841AEA00962E34 /* CardVaultResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultResult.swift; sourceTree = ""; }; @@ -591,7 +589,6 @@ 065A4DC226FCE1D20007014A /* ConfirmPaymentSourceRequest_Tests.swift */, CB16E6D7285B7A2B00FD6F52 /* CardResponses.swift */, CB16E6D9285B7B7300FD6F52 /* MockCardDelegate.swift */, - 3B109B382A85A9DB00D8135F /* MockGraphQLClient.swift */, 3B109B3A2A85B54800D8135F /* MockCardVaultDelegate.swift */, ); path = CardPaymentsTests; @@ -1341,7 +1338,6 @@ 80E2FDBE2A83528B0045593D /* CheckoutOrdersAPI.swift in Sources */, 3B79E4F72A8503CA00C01D06 /* UpdateVaultVariables.swift in Sources */, 8048D28C270B9DE00072214A /* ConfirmPaymentSourceResponse.swift in Sources */, - 3B109B3D2A85D1CA00D8135F /* MockGraphQLClient.swift in Sources */, 3D1763A22720722A00652E1C /* CardResult.swift in Sources */, BC0A82A5270B9533006E9A21 /* ConfirmPaymentSourceRequest.swift in Sources */, CB4BE2802847F01000EA2DD1 /* CardDelegate.swift in Sources */, diff --git a/UnitTests/CardPaymentsTests/MockGraphQLClient.swift b/UnitTests/CardPaymentsTests/MockGraphQLClient.swift deleted file mode 100644 index 3a34e331c..000000000 --- a/UnitTests/CardPaymentsTests/MockGraphQLClient.swift +++ /dev/null @@ -1,20 +0,0 @@ -@testable import CorePayments - -//class MockGraphQLClient: GraphQLClient { -// -// var mockSuccessResponse: GraphQLQueryResponse? -// var mockErrorResponse: Error? -// -// override func callGraphQL( -// name: String, -// query: Q -// ) async throws -> GraphQLQueryResponse where T: Decodable, T: Encodable, Q: GraphQLQuery { -// if let response = mockSuccessResponse as? GraphQLQueryResponse { -// return response -// } else if let error = mockErrorResponse { -// throw error -// } else { -// fatalError("MockGraphQLClient - either mockSuccessResponse or mockErrorResponse must be set") -// } -// } -//} From c2d71e773a23fd98cefc25576813bb3cfce1ce3c Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 23 Aug 2023 10:55:59 -0500 Subject: [PATCH 18/19] PR Feedback - newline each comman in queryString --- Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift b/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift index 894269b74..3a1c2fc0b 100644 --- a/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift +++ b/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift @@ -37,7 +37,8 @@ class VaultPaymentTokensAPI { id, status, links { - rel, href + rel, + href } } } From af560e2acd8a55ca671c9b4a817bad589acf7961 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Thu, 24 Aug 2023 17:05:16 -0500 Subject: [PATCH 19/19] Remove PPaaS navigation instruction from GraphQL call; unable to find docs for this :( --- Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift b/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift index 3a1c2fc0b..2a3fe2b1c 100644 --- a/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift +++ b/Sources/CardPayments/APIRequests/VaultPaymentTokensAPI.swift @@ -4,8 +4,6 @@ import CorePayments #endif /// This class coordinates networking logic for communicating with the /graphql?UpdateVaultSetupToken API. -/// -/// Details on this PayPal API can be found in PPaaS under Merchant > Data Vault > Payment Method Tokens > v3. class VaultPaymentTokensAPI { // MARK: - Private Propertires