From 46c6e11016b7851350f3a64da41e1693ef6aee37 Mon Sep 17 00:00:00 2001 From: Ingenico ePayments Date: Thu, 15 Feb 2024 16:40:20 +0100 Subject: [PATCH] Release 5.13.0. --- .gitignore | 2 +- Cartfile | 4 +- Cartfile.resolved | 6 +- .../Migration Guide v5.12 to v5.13.md | 111 +++ IngenicoConnectKit.podspec | 2 +- IngenicoConnectKit.xcodeproj/project.pbxproj | 137 ++- IngenicoConnectKit/ApiErrorItem.swift | 19 + IngenicoConnectKit/ApiErrorResponse.swift | 14 + IngenicoConnectKit/AssetManager.swift | 11 + .../AuthenticationIndicator.swift | 2 +- IngenicoConnectKit/C2SCommunicator.swift | 602 ++++++++---- .../C2SCommunicatorConfiguration.swift | 18 + IngenicoConnectKit/ClientApi.swift | 340 +++++++ .../ClientApiCommunicator.swift | 747 +++++++++++++++ IngenicoConnectKit/ConnectSDK.swift | 163 ++++ .../ConnectSDKConfiguration.swift | 29 + IngenicoConnectKit/ConnectSDKError.swift | 32 + .../Constants/SDKConstants.swift | 4 +- IngenicoConnectKit/ConvertedAmount.swift | 13 + .../Cryptography/Encryptor.swift | 7 +- .../Cryptography/JOSEEncryptor.swift | 15 +- .../AccountOnFileAttributeStatus.swift | 8 +- .../Enumerations/CountryCode.swift | 1 + .../Enumerations/DisplayElementType.swift | 2 +- .../Enumerations/FieldType.swift | 14 +- .../Enumerations/FormElementType.swift | 12 +- .../Enumerations/IINStatus.swift | 14 +- .../Enumerations/PreferredInputType.swift | 14 +- IngenicoConnectKit/Enumerations/Region.swift | 1 + .../Enumerations/SessionError.swift | 9 + .../Extensions/String+Extensions.swift | 3 +- IngenicoConnectKit/FileManager.swift | 1 + .../Formatters/StringFormatter.swift | 6 +- .../DirectoryEntries/DirectoryEntries.swift | 23 +- .../DirectoryEntries/DirectoryEntry.swift | 24 +- .../PaymentAmountOfMoney.swift | 20 +- .../Models/IINDetails/IINDetail.swift | 23 +- .../IINDetails/IINDetailsResponse.swift | 52 +- .../PaymentProducts/AccountOnFile.swift | 42 +- .../AccountOnFileAttribute.swift | 4 +- .../AccountOnFileDisplayHints.swift | 26 +- .../PaymentProducts/BasicPaymentProduct.swift | 77 +- .../BasicPaymentProductGroup.swift | 41 +- .../BasicPaymentProductGroups.swift | 18 +- .../BasicPaymentProducts.swift | 19 +- .../PaymentProducts/CustomerDetails.swift | 24 - .../CustomerDetailsError.swift | 17 - .../PaymentProducts/DataRestrictions.swift | 86 +- .../PaymentProducts/DisplayElement.swift | 11 +- .../Models/PaymentProducts/FormElement.swift | 21 +- .../PaymentProducts/LabelTemplateItem.swift | 4 +- .../PaymentItemDisplayHints.swift | 20 +- .../Models/PaymentProducts/PaymentItems.swift | 28 +- .../PaymentProducts/PaymentProduct.swift | 25 + .../PaymentProduct302SpecificData.swift | 12 +- .../PaymentProduct320SpecificData.swift | 13 +- .../PaymentProduct863SpecificData.swift | 12 +- .../PaymentProducts/PaymentProductField.swift | 106 ++- .../PaymentProductFieldDisplayHints.swift | 39 +- .../PaymentProducts/PaymentProductGroup.swift | 74 +- .../PaymentProductNetworks.swift | 27 +- .../Models/PaymentProducts/ToolTip.swift | 18 +- .../Models/PaymentProducts/Validator.swift | 35 +- .../ValidatorBoletoBancarioRequiredness.swift | 60 +- .../ValidatorEmailAddress.swift | 46 +- .../ValidatorExpirationDate.swift | 84 +- .../PaymentProducts/ValidatorFixedList.swift | 58 +- .../PaymentProducts/ValidatorIBAN.swift | 85 +- .../PaymentProducts/ValidatorLength.swift | 61 +- .../PaymentProducts/ValidatorLuhn.swift | 57 +- .../PaymentProducts/ValidatorRange.swift | 60 +- .../ValidatorRegularExpression.swift | 63 +- .../ValidatorResidentIdNumber.swift | 54 +- .../ValidatorTermsAndConditions.swift | 41 +- .../Models/PaymentProducts/Validators.swift | 58 +- .../PaymentProducts/ValueMappingItem.swift | 26 +- .../PaymentRequest/PaymentRequest.swift | 72 +- .../PreparedPaymentRequest.swift | 2 +- .../Models/PublicKeys/PublicKeyResponse.swift | 19 +- .../ThirdPartyStatus/ThirdPartyStatus.swift | 5 +- .../ThirdPartyStatusResponse.swift | 3 +- .../ValidationErrors/ValidationErrors.swift | 18 +- IngenicoConnectKit/PaymentConfiguration.swift | 17 + IngenicoConnectKit/PaymentContext.swift | 34 +- .../Protocols/BasicPaymentItem.swift | 2 +- .../ResponseObjectSerializable.swift | 1 + IngenicoConnectKit/Session.swift | 235 +++-- IngenicoConnectKit/SessionConfiguration.swift | 21 + IngenicoConnectKit/Util.swift | 17 +- IngenicoConnectKit/ValidationRule.swift | 13 + IngenicoConnectKit/ValidationType.swift | 25 + .../Wrappers/AlamofireWrapper.swift | 94 +- .../AssetManagerTestCase.swift | 44 +- .../Base64/Base64TestCase.swift | 12 +- ...C2SCommunicatorConfigurationTestCase.swift | 20 +- .../C2SCommunicatorTestCase.swift | 198 ++-- .../ClientApiTestCase.swift | 899 ++++++++++++++++++ .../ConnectSDKTestCase.swift | 120 +++ .../Cryptography/EncryptorTestCase.swift | 10 +- .../Formatters/StringFormatterTestCase.swift | 10 - .../GlobalCollectSDKTests.swift | 36 - .../AccountOnFileAttributesTestCase.swift | 66 +- .../Models/AccountsOnFileTestCase.swift | 78 +- .../BasicPaymentProductGroupsTestCase.swift | 63 +- .../Models/BasicPaymentProductTestCase.swift | 103 +- .../Models/CustomerDetailsTestCase.swift | 44 + .../Models/IINDetailsResponseTestCase.swift | 37 +- .../Models/LabelTemplateTestCase.swift | 33 +- .../Models/PaymentAmountOfMoneyTestCase.swift | 10 - .../Models/PaymentItemsTestCase.swift | 88 +- .../Models/PaymentProductFieldTestCase.swift | 117 ++- .../Models/PaymentProductFieldsTestCase.swift | 87 +- .../Models/PaymentProductGroupTestCase.swift | 101 +- .../Models/PaymentProductTestCase.swift | 62 +- .../Models/PaymentProductsTestCase.swift | 68 +- .../Models/PaymentRequestTestCase.swift | 138 +-- .../PreparedPaymentRequestTestCase.swift | 10 - .../ValidatorEmailAddressTestCase.swift | 43 +- .../ValidatorExpirationDateTestCase.swift | 46 +- .../Models/ValidatorFixedListTestCase.swift | 46 +- .../Models/ValidatorLengthTestCase.swift | 43 +- .../Models/ValidatorLuhnTestCase.swift | 43 +- .../Models/ValidatorRangeTestCase.swift | 43 +- .../ValidatorRegularExpressionTestCase.swift | 43 +- .../ValidatorResidentIdNumberTestCase.swift | 44 +- .../Models/ValueMappingItemTestCase.swift | 23 +- IngenicoConnectKitTests/SessionTestCase.swift | 126 ++- .../Stubs/StubClientApi.swift | 31 + .../Stubs/StubSession.swift | 20 + IngenicoConnectKitTests/Stubs/StubUtil.swift | 9 - IngenicoConnectKitTests/UtilTestCase.swift | 13 +- .../Wrappers/AlamofireWrapperTestCase.swift | 293 +++--- README.md | 4 +- 133 files changed, 6506 insertions(+), 1453 deletions(-) create mode 100644 Documentation/Migration Guide v5.12 to v5.13.md create mode 100644 IngenicoConnectKit/ApiErrorItem.swift create mode 100644 IngenicoConnectKit/ApiErrorResponse.swift create mode 100644 IngenicoConnectKit/ClientApi.swift create mode 100644 IngenicoConnectKit/ClientApiCommunicator.swift create mode 100644 IngenicoConnectKit/ConnectSDK.swift create mode 100644 IngenicoConnectKit/ConnectSDKConfiguration.swift create mode 100644 IngenicoConnectKit/ConnectSDKError.swift create mode 100644 IngenicoConnectKit/ConvertedAmount.swift delete mode 100644 IngenicoConnectKit/Models/PaymentProducts/CustomerDetails.swift delete mode 100644 IngenicoConnectKit/Models/PaymentProducts/CustomerDetailsError.swift create mode 100644 IngenicoConnectKit/PaymentConfiguration.swift create mode 100644 IngenicoConnectKit/SessionConfiguration.swift create mode 100644 IngenicoConnectKit/ValidationRule.swift create mode 100644 IngenicoConnectKit/ValidationType.swift create mode 100644 IngenicoConnectKitTests/ClientApiTestCase.swift create mode 100644 IngenicoConnectKitTests/ConnectSDKTestCase.swift delete mode 100644 IngenicoConnectKitTests/GlobalCollectSDKTests.swift create mode 100644 IngenicoConnectKitTests/Models/CustomerDetailsTestCase.swift create mode 100644 IngenicoConnectKitTests/Stubs/StubClientApi.swift create mode 100644 IngenicoConnectKitTests/Stubs/StubSession.swift diff --git a/.gitignore b/.gitignore index 3e58d5e..c919910 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ timeline.xctimeline playground.xcworkspace # Carthage -Carthage/Build +Carthage/ # MacOS .DS_Store diff --git a/Cartfile b/Cartfile index e79f24b..a418b65 100644 --- a/Cartfile +++ b/Cartfile @@ -1,3 +1,3 @@ github "Alamofire/Alamofire" ~> 5.4 -github "krzyzanowskim/CryptoSwift" == 1.4.0 -github "AliSoftware/OHHTTPStubs" ~> 8.0.0 +github "krzyzanowskim/CryptoSwift" ~> 1.5.0 +github "AliSoftware/OHHTTPStubs" ~> 9.0.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index 056ca06..3c1ca92 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,3 +1,3 @@ -github "Alamofire/Alamofire" "5.4.3" -github "AliSoftware/OHHTTPStubs" "8.0.0" -github "krzyzanowskim/CryptoSwift" "1.4.0" +github "Alamofire/Alamofire" "5.8.1" +github "AliSoftware/OHHTTPStubs" "9.1.0" +github "krzyzanowskim/CryptoSwift" "1.8.0" diff --git a/Documentation/Migration Guide v5.12 to v5.13.md b/Documentation/Migration Guide v5.12 to v5.13.md new file mode 100644 index 0000000..e1b99fc --- /dev/null +++ b/Documentation/Migration Guide v5.12 to v5.13.md @@ -0,0 +1,111 @@ +# Migrating from version 5.12 to version 5.13 +This migration guide will help you migrate from version 5.12 to version 5.13. + +For version 5.13, the SDK was updated with a new architecture where initialization and error handling of the SDK have been improved. These changes are *not* mandatory to move to version 5.13 of the SDK. The old architecture has been deprecated, but is still available and will not be removed until a future major version update. + +## Migrating to the new architecture + +> *Note:* The changes described in this section are not breaking at this time. The old architecture of the SDK, using an instance of `Session`, has been deprecated, but will be available until the next major version update. + +Major improvements have been made to the SDKs error handling, and to the initialization of the SDK. This is an overview of all architecture changes: + +- The SDK has a new entry point: `ConnectSDK`. Its initialize method should be used to provide `ConnectSDKConfiguration`, containing `SessionConfiguration`, obtained through the Server to Server API, and `PaymentConfiguration`. +- Session has been replaced by `ClientApi`. Once the SDK has been initialized, an instance of `ClientApi` can be obtained by calling `ConnectSDK.clientApi`. +- Error handling has been improved when making requests through `ClientApi`. When making a call, an additional argument must be provided which will be called to handle an API error when one occurs. This means that every call will always require at least the following parameters: + - `success: (_ response: ExpectedResponseType) -> Void` - Called when the request was successful. The response parameter will contain the response object, according to the existing, unchanged domain model. + - `failure: (_ error: Error) -> Void` - Called when an exception occurred while executing the request. The error parameter will contain the `Error` that indicates what went wrong. + - `apiFailure: (_ errorResponse: ApiErrorResponse) -> Void` - Called when the Connect gateway returned an error response. The errorResponse parameter will contain further information on the error, such as the errorId and a list of `ApiErrorItem`s which contains all returned errors. + +### Using the new architecture + +The steps below describe how to migrate to the new SDK architecture in version 5.13 of the SDK. + +1. Upgrade to the latest Swift Connect SDK version in your `Podfile`: +``` +pod 'IngenicoConnectKit', '~> 5.13' +``` + +Afterwards, run the following command: + +``` +$ pod install +``` + +2. Replace `Session` initialization with `ConnectSDK` initialization: +```swift +let sessionConfiguration = SessionConfiguration( + clientSessionId: "e030f01dda4c4f94891c3cb23b3ccf61", + customerId: "9008-9a1e01fbbafd4889a77cbb11abb5e688", + clientApiUrl: "https://ams1.sandbox.api-ingenico.com/client", + assetUrl: "https://assets.pay1.sandbox.secured-by-ingenico.com/" +) + +let connectSDKConfiguration = ConnectSDKConfiguration( + sessionConfiguration: sessionConfiguration, + enableNetworkLogs: true, // should be set to false in production + applicationId: "my-application-id", // optional + ipAddress: nil, // optional + preLoadImages: true // true, by default if parameter is not explicitly set +) + +let paymentConfiguration = PaymentConfiguration( + paymentContext: paymentContext, + groupPaymentProducts: false +) + +ConnectSDK.initialize( + connectSDKConfiguration: connectSDKConfiguration, + paymentConfiguration: paymentConfiguration +) +``` + +`ConnectSDKConfiguration` contains properties that allow you to set additional SDK settings: + +- `enableNetworkLogs` will log all network requests and responses to the console. This setting can be used to investigate issues. Must be set to false in production targets. +- `applicationId` is the identifier or name that you choose for your app. +- `ipAddress` will be included in the client's meta info when encrypting a `PaymentRequest`. +- `preloadImages` determines whether image resources, initially returned by the API as their location, will be retrieved by the SDK, or whether you will retrieve them on the go when required. The SDK loads the images by default, to make sure behaviour is as it used to be. We have added the option to disable preloading to allow you to use frameworks for image loading on demand. + +`PaymentConfiguration` has a property used to determine whether or not to group payment items. + +3. Replace all API calls that were previously called via `Session` with `ConnectSDK.clientApi` calls. API calls now require the additional argument `apiFailure`: +```swift +ConnectSDK.clientApi.paymentItems( + success: { paymentItems in + // display the contents of paymentItems & accountsOnFile to your customer + }, + failure: { error in + // process failure + }, + apiFailure: { errorResponse in + // process api failure + } +) +``` + +4. Implement error handling for the `failure` and `apiFailure` callbacks. + +For more information about version 5.13 or the Swift SDK in general, also review the Swift SDK developer documentation and/or the Swift example apps. + +### Deprecations + +This is the full list of classes that have become deprecated related to the architecture change. Move away from using these classes, as they will be removed or made unavailable in a future major release. + +- `Session` +- `AlamofireWrapper` +- `C2SCommunicator` +- `C2SCommunicatorConfiguration` +- `AssetManager` +- `FileManager` +- `Util` +- `Encryptor` +- `JOSEEncryptor` +- `SessionError` +- `CustomerDetailsError` + +## Relevant links +- [Swift Connect SDK on GitHub](https://github.com/Ingenico-ePayments/connect-sdk-client-swift) +- [SwiftUI example app on GitHub](https://github.com/Ingenico-ePayments/connect-sdk-client-swift-example-swiftui) +- [UIKit example app on GitHub](https://github.com/Ingenico-ePayments/connect-sdk-client-swift-example) +- [Swift SDK documentation](https://docs.connect.worldline-solutions.com/documentation/sdk/mobile/swift/) +- [Client API Reference](https://apireference.connect.worldline-solutions.com/c2sapi/v1/en_US/index.html?paymentPlatform=ALL) diff --git a/IngenicoConnectKit.podspec b/IngenicoConnectKit.podspec index 591ead2..903d360 100644 --- a/IngenicoConnectKit.podspec +++ b/IngenicoConnectKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "IngenicoConnectKit" - s.version = "5.12.0" + s.version = "5.13.0" s.summary = "Ingenico Connect Swift SDK" s.description = <<-DESC This native iOS SDK facilitates handling payments in your apps diff --git a/IngenicoConnectKit.xcodeproj/project.pbxproj b/IngenicoConnectKit.xcodeproj/project.pbxproj index 6c12b0b..2854b0f 100644 --- a/IngenicoConnectKit.xcodeproj/project.pbxproj +++ b/IngenicoConnectKit.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -87,10 +87,8 @@ 73819ECF1DF98F1200B6FC0F /* EncryptorTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73819ECE1DF98F1200B6FC0F /* EncryptorTestCase.swift */; }; 73AABACC1F1F804200B76782 /* ThirdPartyStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73AABACA1F1F804200B76782 /* ThirdPartyStatus.swift */; }; 73AABACD1F1F804200B76782 /* ThirdPartyStatusResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73AABACB1F1F804200B76782 /* ThirdPartyStatusResponse.swift */; }; - 73AABAD01F1F806B00B76782 /* CustomerDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73AABACE1F1F806B00B76782 /* CustomerDetails.swift */; }; 73AABAD11F1F806B00B76782 /* DisplayElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73AABACF1F1F806B00B76782 /* DisplayElement.swift */; }; 73AABAD31F1F80A000B76782 /* DisplayElementType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73AABAD21F1F80A000B76782 /* DisplayElementType.swift */; }; - 73CBAE531F9E2CC500FAB37F /* CustomerDetailsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73CBAE521F9E2CC500FAB37F /* CustomerDetailsError.swift */; }; 7BC4941129657E5500F6138A /* ValidatorResidentIdNumberTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC4941029657E5500F6138A /* ValidatorResidentIdNumberTestCase.swift */; }; 7BC494192965AA3C00F6138A /* ValidatorResidentIdNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC494182965AA3C00F6138A /* ValidatorResidentIdNumber.swift */; }; 7BEEA28929BA2F160085406C /* AuthenticationIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BEEA28829BA2F160085406C /* AuthenticationIndicator.swift */; }; @@ -140,6 +138,23 @@ B4D349EF1EAA1FC000487866 /* CountryCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D349EE1EAA1FC000487866 /* CountryCode.swift */; }; B4D349F11EAA1FCE00487866 /* CurrencyCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D349F01EAA1FCE00487866 /* CurrencyCode.swift */; }; B4D77C6B1DE72EB900F7B1C1 /* IngenicoConnectKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4D77C611DE72EB900F7B1C1 /* IngenicoConnectKit.framework */; }; + BF3BEB522B16073200CF882A /* ConnectSDKTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEB512B16073200CF882A /* ConnectSDKTestCase.swift */; }; + BF3BEB542B16073800CF882A /* ClientApiTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEB532B16073800CF882A /* ClientApiTestCase.swift */; }; + BF3BEB562B16074F00CF882A /* StubClientApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEB552B16074F00CF882A /* StubClientApi.swift */; }; + BF3BEB582B16075800CF882A /* StubSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEB572B16075800CF882A /* StubSession.swift */; }; + BF8163BC2B303500007FB83E /* ValidationRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8163BB2B303500007FB83E /* ValidationRule.swift */; }; + BF8163BE2B30373F007FB83E /* ValidationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8163BD2B30373F007FB83E /* ValidationType.swift */; }; + BF9DD4972B0E02B200DFC9F2 /* ApiErrorResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9DD4962B0E02B200DFC9F2 /* ApiErrorResponse.swift */; }; + BF9DD4992B0E02C800DFC9F2 /* ApiErrorItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9DD4982B0E02C800DFC9F2 /* ApiErrorItem.swift */; }; + BF9FDEED2B10A9DD004BBA7F /* CustomerDetailsTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9FDEEC2B10A9DD004BBA7F /* CustomerDetailsTestCase.swift */; }; + BF9FDEEF2B10C9E3004BBA7F /* ConnectSDK.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9FDEEE2B10C9E3004BBA7F /* ConnectSDK.swift */; }; + BF9FDEF22B10CC0A004BBA7F /* ClientApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9FDEF12B10CC0A004BBA7F /* ClientApi.swift */; }; + BF9FDEF42B10CC17004BBA7F /* ConnectSDKConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9FDEF32B10CC17004BBA7F /* ConnectSDKConfiguration.swift */; }; + BF9FDEF62B10CC1E004BBA7F /* SessionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9FDEF52B10CC1E004BBA7F /* SessionConfiguration.swift */; }; + BF9FDEF82B10CC25004BBA7F /* PaymentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9FDEF72B10CC25004BBA7F /* PaymentConfiguration.swift */; }; + BF9FDEFB2B10D9C3004BBA7F /* ClientApiCommunicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9FDEFA2B10D9C3004BBA7F /* ClientApiCommunicator.swift */; }; + BFAD73602B14CB9F0011677F /* ConnectSDKError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAD735F2B14CB9F0011677F /* ConnectSDKError.swift */; }; + BFBF63EE2B0F82160094E205 /* ConvertedAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBF63ED2B0F82160094E205 /* ConvertedAmount.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -256,10 +271,8 @@ 73819ECE1DF98F1200B6FC0F /* EncryptorTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EncryptorTestCase.swift; path = IngenicoConnectKitTests/Cryptography/EncryptorTestCase.swift; sourceTree = ""; }; 73AABACA1F1F804200B76782 /* ThirdPartyStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ThirdPartyStatus.swift; path = Models/ThirdPartyStatus/ThirdPartyStatus.swift; sourceTree = ""; }; 73AABACB1F1F804200B76782 /* ThirdPartyStatusResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ThirdPartyStatusResponse.swift; path = Models/ThirdPartyStatus/ThirdPartyStatusResponse.swift; sourceTree = ""; }; - 73AABACE1F1F806B00B76782 /* CustomerDetails.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CustomerDetails.swift; path = Models/PaymentProducts/CustomerDetails.swift; sourceTree = ""; }; 73AABACF1F1F806B00B76782 /* DisplayElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DisplayElement.swift; path = Models/PaymentProducts/DisplayElement.swift; sourceTree = ""; }; 73AABAD21F1F80A000B76782 /* DisplayElementType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DisplayElementType.swift; path = Enumerations/DisplayElementType.swift; sourceTree = ""; }; - 73CBAE521F9E2CC500FAB37F /* CustomerDetailsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CustomerDetailsError.swift; path = Models/PaymentProducts/CustomerDetailsError.swift; sourceTree = ""; }; 7BC4941029657E5500F6138A /* ValidatorResidentIdNumberTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ValidatorResidentIdNumberTestCase.swift; path = IngenicoConnectKitTests/Models/ValidatorResidentIdNumberTestCase.swift; sourceTree = ""; }; 7BC494182965AA3C00F6138A /* ValidatorResidentIdNumber.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ValidatorResidentIdNumber.swift; path = Models/PaymentProducts/ValidatorResidentIdNumber.swift; sourceTree = ""; }; 7BEEA28829BA2F160085406C /* AuthenticationIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationIndicator.swift; sourceTree = ""; }; @@ -311,6 +324,23 @@ B4D77C611DE72EB900F7B1C1 /* IngenicoConnectKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IngenicoConnectKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B4D77C651DE72EB900F7B1C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B4D77C6A1DE72EB900F7B1C1 /* IngenicoConnectKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IngenicoConnectKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + BF3BEB512B16073200CF882A /* ConnectSDKTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ConnectSDKTestCase.swift; path = IngenicoConnectKitTests/ConnectSDKTestCase.swift; sourceTree = ""; }; + BF3BEB532B16073800CF882A /* ClientApiTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ClientApiTestCase.swift; path = IngenicoConnectKitTests/ClientApiTestCase.swift; sourceTree = ""; }; + BF3BEB552B16074F00CF882A /* StubClientApi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StubClientApi.swift; path = IngenicoConnectKitTests/Stubs/StubClientApi.swift; sourceTree = ""; }; + BF3BEB572B16075800CF882A /* StubSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StubSession.swift; path = IngenicoConnectKitTests/Stubs/StubSession.swift; sourceTree = ""; }; + BF8163BB2B303500007FB83E /* ValidationRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationRule.swift; sourceTree = ""; }; + BF8163BD2B30373F007FB83E /* ValidationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationType.swift; sourceTree = ""; }; + BF9DD4962B0E02B200DFC9F2 /* ApiErrorResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiErrorResponse.swift; sourceTree = ""; }; + BF9DD4982B0E02C800DFC9F2 /* ApiErrorItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiErrorItem.swift; sourceTree = ""; }; + BF9FDEEC2B10A9DD004BBA7F /* CustomerDetailsTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CustomerDetailsTestCase.swift; path = IngenicoConnectKitTests/Models/CustomerDetailsTestCase.swift; sourceTree = ""; }; + BF9FDEEE2B10C9E3004BBA7F /* ConnectSDK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectSDK.swift; sourceTree = ""; }; + BF9FDEF12B10CC0A004BBA7F /* ClientApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientApi.swift; sourceTree = ""; }; + BF9FDEF32B10CC17004BBA7F /* ConnectSDKConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectSDKConfiguration.swift; sourceTree = ""; }; + BF9FDEF52B10CC1E004BBA7F /* SessionConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionConfiguration.swift; sourceTree = ""; }; + BF9FDEF72B10CC25004BBA7F /* PaymentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentConfiguration.swift; sourceTree = ""; }; + BF9FDEFA2B10D9C3004BBA7F /* ClientApiCommunicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientApiCommunicator.swift; sourceTree = ""; }; + BFAD735F2B14CB9F0011677F /* ConnectSDKError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectSDKError.swift; sourceTree = ""; }; + BFBF63ED2B0F82160094E205 /* ConvertedAmount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertedAmount.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -347,6 +377,7 @@ 1C1E0BF01E93C27200F9DC5E /* Payment */ = { isa = PBXGroup; children = ( + BF9FDEEC2B10A9DD004BBA7F /* CustomerDetailsTestCase.swift */, 6D8BD90C1DFE9E470013B869 /* PaymentRequestTestCase.swift */, 6D8BD90A1DFE9CDE0013B869 /* PaymentProductFieldsTestCase.swift */, 6DA4A88B1DF9842C007492C2 /* BasicPaymentProductTestCase.swift */, @@ -402,17 +433,19 @@ children = ( 6D7822491DF0476400B5A9AB /* Validators.swift */, 6D7822471DF0470500B5A9AB /* Validator.swift */, + B431CA811E69B4CD0064AC16 /* ValidatorBoletoBancarioRequiredness.swift */, + 6D7822551DF0586900B5A9AB /* ValidatorEmailAddress.swift */, 6D78225F1DF0635900B5A9AB /* ValidatorExpirationDate.swift */, 6D78225D1DF0626500B5A9AB /* ValidatorFixedList.swift */, + 7307A966202C65E9002124B8 /* ValidatorIBAN.swift */, 6D7822591DF0616100B5A9AB /* ValidatorLength.swift */, 6D7822571DF05B5A00B5A9AB /* ValidatorLuhn.swift */, 6D78224B1DF0520600B5A9AB /* ValidatorRange.swift */, 6D78224D1DF053FE00B5A9AB /* ValidatorRegularExpression.swift */, - 6D7822551DF0586900B5A9AB /* ValidatorEmailAddress.swift */, - B431CA811E69B4CD0064AC16 /* ValidatorBoletoBancarioRequiredness.swift */, - 732F8664200522BA00A7DE68 /* ValidatorTermsAndConditions.swift */, - 7307A966202C65E9002124B8 /* ValidatorIBAN.swift */, 7BC494182965AA3C00F6138A /* ValidatorResidentIdNumber.swift */, + 732F8664200522BA00A7DE68 /* ValidatorTermsAndConditions.swift */, + BF8163BB2B303500007FB83E /* ValidationRule.swift */, + BF8163BD2B30373F007FB83E /* ValidationType.swift */, ); name = Validators; sourceTree = ""; @@ -426,6 +459,7 @@ B4A011DF1DF0573C00FCB738 /* Util.swift */, B4D77C651DE72EB900F7B1C1 /* Info.plist */, B40F4B3F1E02C99F002A3AC8 /* FileManager.swift */, + BF9FDEEE2B10C9E3004BBA7F /* ConnectSDK.swift */, ); name = Essentials; sourceTree = ""; @@ -481,6 +515,8 @@ B461901D1DF57AB400A55FB1 /* StubUtil.swift */, B40575E11E02BF7D0099DDBB /* StubBundle.swift */, B40F4B411E02CA80002A3AC8 /* StubFileManager.swift */, + BF3BEB552B16074F00CF882A /* StubClientApi.swift */, + BF3BEB572B16075800CF882A /* StubSession.swift */, ); name = Stubs; sourceTree = ""; @@ -559,6 +595,9 @@ B4D77C631DE72EB900F7B1C1 /* IngenicoConnectKit */ = { isa = PBXGroup; children = ( + BF9FDEF92B10D9B7004BBA7F /* API communication */, + BF9FDEF02B10CBC2004BBA7F /* SDK configuration */, + BF9DD4932B0E028500DFC9F2 /* API errors */, B4D77E401DE730EC00F7B1C1 /* Models */, B4D77DDD1DE72FF300F7B1C1 /* Protocols */, B4D77DDC1DE72FEE00F7B1C1 /* Constants */, @@ -590,6 +629,8 @@ 6D8BD9101DFEACBD0013B869 /* AssetManagerTestCase.swift */, 6DD70EA91E029F5400A585BC /* C2SCommunicatorTestCase.swift */, B473DF361E1A58EF009AC284 /* SessionTestCase.swift */, + BF3BEB532B16073800CF882A /* ClientApiTestCase.swift */, + BF3BEB512B16073200CF882A /* ConnectSDKTestCase.swift */, ); name = IngenicoConnectKitTests; sourceTree = ""; @@ -633,6 +674,7 @@ 1C0513331E799F2D00D11633 /* SessionError.swift */, B4D349EE1EAA1FC000487866 /* CountryCode.swift */, B4D349F01EAA1FCE00487866 /* CurrencyCode.swift */, + BFAD735F2B14CB9F0011677F /* ConnectSDKError.swift */, ); name = Enumerations; sourceTree = ""; @@ -658,6 +700,7 @@ B4D77E401DE730EC00F7B1C1 /* Models */ = { isa = PBXGroup; children = ( + BFBF63EC2B0F820B0094E205 /* ConvertedAmount */, 73AABAC91F1F801F00B76782 /* ThirdPartyStatus */, B4D77E411DE730F300F7B1C1 /* DirectoryEntries */, B4D77E421DE730FE00F7B1C1 /* C2SPaymentProductContext */, @@ -727,8 +770,6 @@ B4D77E481DE7317E00F7B1C1 /* PaymentProducts */ = { isa = PBXGroup; children = ( - 73AABACE1F1F806B00B76782 /* CustomerDetails.swift */, - 73CBAE521F9E2CC500FAB37F /* CustomerDetailsError.swift */, 7BEEA28829BA2F160085406C /* AuthenticationIndicator.swift */, 73AABACF1F1F806B00B76782 /* DisplayElement.swift */, 6D7822651DF066C700B5A9AB /* DataRestrictions.swift */, @@ -756,6 +797,42 @@ name = PaymentProducts; sourceTree = ""; }; + BF9DD4932B0E028500DFC9F2 /* API errors */ = { + isa = PBXGroup; + children = ( + BF9DD4962B0E02B200DFC9F2 /* ApiErrorResponse.swift */, + BF9DD4982B0E02C800DFC9F2 /* ApiErrorItem.swift */, + ); + name = "API errors"; + sourceTree = ""; + }; + BF9FDEF02B10CBC2004BBA7F /* SDK configuration */ = { + isa = PBXGroup; + children = ( + BF9FDEF12B10CC0A004BBA7F /* ClientApi.swift */, + BF9FDEF32B10CC17004BBA7F /* ConnectSDKConfiguration.swift */, + BF9FDEF52B10CC1E004BBA7F /* SessionConfiguration.swift */, + BF9FDEF72B10CC25004BBA7F /* PaymentConfiguration.swift */, + ); + name = "SDK configuration"; + sourceTree = ""; + }; + BF9FDEF92B10D9B7004BBA7F /* API communication */ = { + isa = PBXGroup; + children = ( + BF9FDEFA2B10D9C3004BBA7F /* ClientApiCommunicator.swift */, + ); + name = "API communication"; + sourceTree = ""; + }; + BFBF63EC2B0F820B0094E205 /* ConvertedAmount */ = { + isa = PBXGroup; + children = ( + BFBF63ED2B0F82160094E205 /* ConvertedAmount.swift */, + ); + name = ConvertedAmount; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -925,6 +1002,7 @@ 73AABAD31F1F80A000B76782 /* DisplayElementType.swift in Sources */, 6D78224A1DF0476400B5A9AB /* Validators.swift in Sources */, B43C1F521DEC334B00E210DF /* StringFormatter.swift in Sources */, + BF9DD4972B0E02B200DFC9F2 /* ApiErrorResponse.swift in Sources */, 6D7822541DF0580C00B5A9AB /* AccountOnFileDisplayHints.swift in Sources */, 1C0513341E799F2D00D11633 /* SessionError.swift in Sources */, 6D78225C1DF0620D00B5A9AB /* ValueMappingItem.swift in Sources */, @@ -934,12 +1012,17 @@ B40F4B401E02C99F002A3AC8 /* FileManager.swift in Sources */, 6D7822701DF0754600B5A9AB /* PaymentProduct.swift in Sources */, 6D78226E1DF0739D00B5A9AB /* PaymentProductFields.swift in Sources */, + BF8163BE2B30373F007FB83E /* ValidationType.swift in Sources */, B4679CDD1E0C1C4400E25DFD /* JOSEEncryptor.swift in Sources */, B43C1F591DEC407900E210DF /* Base64+Extensions.swift in Sources */, + BF9FDEEF2B10C9E3004BBA7F /* ConnectSDK.swift in Sources */, 7307A967202C65E9002124B8 /* ValidatorIBAN.swift in Sources */, 6D4E7AA91DF5669500BAE336 /* BasicPaymentProductGroups.swift in Sources */, + BF9FDEF22B10CC0A004BBA7F /* ClientApi.swift in Sources */, 6D4E7ABB1DF58F6700BAE336 /* PreparedPaymentRequest.swift in Sources */, 6D7822271DF039E100B5A9AB /* IINDetailsResponse.swift in Sources */, + BF8163BC2B303500007FB83E /* ValidationRule.swift in Sources */, + BF9FDEF62B10CC1E004BBA7F /* SessionConfiguration.swift in Sources */, 6D7822601DF0635900B5A9AB /* ValidatorExpirationDate.swift in Sources */, 73AABACD1F1F804200B76782 /* ThirdPartyStatusResponse.swift in Sources */, 7354044F2153C70500771041 /* PaymentProduct320SpecificData.swift in Sources */, @@ -964,18 +1047,20 @@ 6D78224E1DF053FE00B5A9AB /* ValidatorRegularExpression.swift in Sources */, 6D8BD9131DFEB3080013B869 /* C2SCommunicator.swift in Sources */, 6D7822621DF064E400B5A9AB /* BasicPaymentProducts.swift in Sources */, - 73CBAE531F9E2CC500FAB37F /* CustomerDetailsError.swift in Sources */, 6DED71E51DEC3DA00023EFA2 /* Macros.swift in Sources */, 73AABACC1F1F804200B76782 /* ThirdPartyStatus.swift in Sources */, B4D349EF1EAA1FC000487866 /* CountryCode.swift in Sources */, - 73AABAD01F1F806B00B76782 /* CustomerDetails.swift in Sources */, + BF9DD4992B0E02C800DFC9F2 /* ApiErrorItem.swift in Sources */, 6D4E7AAF1DF5729300BAE336 /* PaymentProductGroup.swift in Sources */, + BF9FDEFB2B10D9C3004BBA7F /* ClientApiCommunicator.swift in Sources */, 6D7822281DF039E100B5A9AB /* IINDetail.swift in Sources */, B431CA821E69B4CD0064AC16 /* ValidatorBoletoBancarioRequiredness.swift in Sources */, 6D7822681DF0673800B5A9AB /* PaymentProductField.swift in Sources */, 6D7822061DF022A100B5A9AB /* PaymentAmountOfMoney.swift in Sources */, + BFAD73602B14CB9F0011677F /* ConnectSDKError.swift in Sources */, B43C1F6C1DEC59E300E210DF /* IINStatus.swift in Sources */, 6D4E7AB91DF57E2900BAE336 /* PaymentRequest.swift in Sources */, + BF9FDEF82B10CC25004BBA7F /* PaymentConfiguration.swift in Sources */, 6D4443A41E30B1540007070E /* String+Extensions.swift in Sources */, 6D78224C1DF0520600B5A9AB /* ValidatorRange.swift in Sources */, 6D7822041DF0228F00B5A9AB /* DirectoryEntry.swift in Sources */, @@ -984,11 +1069,13 @@ B43C1F691DEC59B200E210DF /* Environment.swift in Sources */, 6D78226A1DF067A900B5A9AB /* ToolTip.swift in Sources */, B4D349F11EAA1FCE00487866 /* CurrencyCode.swift in Sources */, + BF9FDEF42B10CC17004BBA7F /* ConnectSDKConfiguration.swift in Sources */, 732F8665200522BA00A7DE68 /* ValidatorTermsAndConditions.swift in Sources */, 6D7822481DF0470500B5A9AB /* Validator.swift in Sources */, 6D4E7AA51DF5611700BAE336 /* BasicPaymentProduct.swift in Sources */, 6D7822721DF0781100B5A9AB /* AccountsOnFile.swift in Sources */, B46190271DF5989300A55FB1 /* AccountOnFileAttribute.swift in Sources */, + BFBF63EE2B0F82160094E205 /* ConvertedAmount.swift in Sources */, 6D7822441DF03C7600B5A9AB /* PaymentProductNetworks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -999,12 +1086,15 @@ files = ( 6D8BD9051DFE965A0013B869 /* ValidatorRangeTestCase.swift in Sources */, 6DA4A88E1DF98580007492C2 /* PaymentProductTestCase.swift in Sources */, + BF3BEB582B16075800CF882A /* StubSession.swift in Sources */, 6D8BD90D1DFE9E470013B869 /* PaymentRequestTestCase.swift in Sources */, 1C99FF521E9D142300461C06 /* PaymentAmountOfMoneyTestCase.swift in Sources */, 7BC4941129657E5500F6138A /* ValidatorResidentIdNumberTestCase.swift in Sources */, + BF3BEB562B16074F00CF882A /* StubClientApi.swift in Sources */, 6D8BD9091DFE9A020013B869 /* ValidatorEmailAddressTestCase.swift in Sources */, 6D2256381DF95DE9009DB416 /* AccountsOnFileTestCase.swift in Sources */, 6DA4A88C1DF9842C007492C2 /* BasicPaymentProductTestCase.swift in Sources */, + BF3BEB542B16073800CF882A /* ClientApiTestCase.swift in Sources */, B461901E1DF57AB400A55FB1 /* StubUtil.swift in Sources */, B473DF381E1A5913009AC284 /* SessionTestCase.swift in Sources */, 6DD70EAA1E029F5400A585BC /* C2SCommunicatorTestCase.swift in Sources */, @@ -1022,11 +1112,13 @@ 1CEBAC181E92725E0091A69C /* PaymentProductGroupTestCase.swift in Sources */, 1C1E0BF61E93E57000F9DC5E /* IINDetailsResponseTestCase.swift in Sources */, 1C4B7DBB1E9268DB00AB1162 /* BasicPaymentProductGroupsTestCase.swift in Sources */, + BF3BEB522B16073200CF882A /* ConnectSDKTestCase.swift in Sources */, 1CE7AB061E9641BE00A80648 /* PreparedPaymentRequestTestCase.swift in Sources */, B46190181DF56C8700A55FB1 /* UtilTestCase.swift in Sources */, 73819ECF1DF98F1200B6FC0F /* EncryptorTestCase.swift in Sources */, 6DA4A8981DF999AD007492C2 /* ValidatorFixedListTestCase.swift in Sources */, 6DA4A8961DF997BE007492C2 /* ValidatorExpirationDateTestCase.swift in Sources */, + BF9FDEED2B10A9DD004BBA7F /* CustomerDetailsTestCase.swift in Sources */, 6D8BD9031DFE95760013B869 /* ValidatorLuhnTestCase.swift in Sources */, 6D8BD90B1DFE9CDE0013B869 /* PaymentProductFieldsTestCase.swift in Sources */, 6DA4A8901DF98A6B007492C2 /* PaymentProductFieldTestCase.swift in Sources */, @@ -1261,10 +1353,12 @@ PRODUCT_BUNDLE_IDENTIFIER = com.ingenicoconnect.IngenicoConnectKit; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -1296,8 +1390,10 @@ PRODUCT_BUNDLE_IDENTIFIER = com.ingenicoconnect.IngenicoConnectKit; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -1319,9 +1415,11 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.ingenicoconnect.IngenicoConnectKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KeychainEntitlementTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/KeychainEntitlementTestApp"; }; name = Debug; @@ -1344,8 +1442,19 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.ingenicoconnect.IngenicoConnectKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = NO; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BF9DD4922B0DFF7800DFC9F2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + PRODUCT_NAME = KeychainEntitlementTestApp; }; name = Release; }; @@ -1356,8 +1465,10 @@ isa = XCConfigurationList; buildConfigurations = ( 7BFB26E5295456DC00A95841 /* Debug */, + BF9DD4922B0DFF7800DFC9F2 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; B4D77C5B1DE72EB900F7B1C1 /* Build configuration list for PBXProject "IngenicoConnectKit" */ = { isa = XCConfigurationList; diff --git a/IngenicoConnectKit/ApiErrorItem.swift b/IngenicoConnectKit/ApiErrorItem.swift new file mode 100644 index 0000000..9a27dc1 --- /dev/null +++ b/IngenicoConnectKit/ApiErrorItem.swift @@ -0,0 +1,19 @@ +// +// ApiErrorItem.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 22/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import Foundation + +public class ApiErrorItem: NSObject, Codable { + public let category: String? + public let code: String + public let httpStatusCode: Int + public let id: String? + public let message: String + public let propertyName: String? + public let requestId: String? +} diff --git a/IngenicoConnectKit/ApiErrorResponse.swift b/IngenicoConnectKit/ApiErrorResponse.swift new file mode 100644 index 0000000..e38c576 --- /dev/null +++ b/IngenicoConnectKit/ApiErrorResponse.swift @@ -0,0 +1,14 @@ +// +// ApiErrorResponse.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 22/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import Foundation + +public class ApiErrorResponse: NSObject, Codable { + public let errorId: String + public let errors: [ApiErrorItem] +} diff --git a/IngenicoConnectKit/AssetManager.swift b/IngenicoConnectKit/AssetManager.swift index 3766134..a1f44b1 100644 --- a/IngenicoConnectKit/AssetManager.swift +++ b/IngenicoConnectKit/AssetManager.swift @@ -21,6 +21,17 @@ extension UserDefaults { } } +@available( + *, + deprecated, + message: + """ + In a future release this class, its functions and its properties will be removed. + Instead please retrieve the logo / tooltip image directly from the PaymentItem. + If an image is not available, or you wish to retrieve it again, you can retrieve it + from the logo's / tooltip image's URL that is also stored in the PaymentItem. + """ +) public class AssetManager { public let logoFormat = "pp_logo_%@" public let tooltipFormat = "pp_%@_tooltip_%@" diff --git a/IngenicoConnectKit/AuthenticationIndicator.swift b/IngenicoConnectKit/AuthenticationIndicator.swift index 445c960..0a8660a 100644 --- a/IngenicoConnectKit/AuthenticationIndicator.swift +++ b/IngenicoConnectKit/AuthenticationIndicator.swift @@ -8,7 +8,7 @@ import Foundation -public class AuthenticationIndicator: ResponseObjectSerializable { +public class AuthenticationIndicator: ResponseObjectSerializable, Codable { public var name: String public var value: String diff --git a/IngenicoConnectKit/C2SCommunicator.swift b/IngenicoConnectKit/C2SCommunicator.swift index bf0facc..3a43e2e 100644 --- a/IngenicoConnectKit/C2SCommunicator.swift +++ b/IngenicoConnectKit/C2SCommunicator.swift @@ -10,6 +10,15 @@ import Foundation import Alamofire import PassKit +@available( + *, + deprecated, + message: + """ + In a future release, this class, its functions and its properties will become internal to the SDK. + Please use Session to interact with the API. + """ +) public class C2SCommunicator { public var configuration: C2SCommunicatorConfiguration public var networkingWrapper = AlamofireWrapper.shared @@ -34,6 +43,7 @@ public class C2SCommunicator { return configuration.loggingEnabled } + @available(*, deprecated, message: "In a future release, this property will be removed.") public var headers: NSDictionary { return [ "Authorization": "GCS v1Client:\(clientSessionId)", @@ -41,6 +51,13 @@ public class C2SCommunicator { ] } + private var httpHeaders: HTTPHeaders { + return [ + "Authorization": "GCS v1Client:\(clientSessionId)", + "X-GCS-ClientMetaInfo": base64EncodedClientMetaInfo + ] + } + @available(*, deprecated, message: "This function is dependant on Environment, and will therefore be removed.") public var isEnvironmentTypeProduction: Bool { return configuration.environment == .production ? true : false @@ -53,61 +70,36 @@ public class C2SCommunicator { public func thirdPartyStatus( forPayment paymentId: String, success: @escaping (_ thirdPartyStatusResponse: ThirdPartyStatusResponse) -> Void, - failure: @escaping (_ error: Error) -> Void + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil ) { let URL = "\(baseURL)/\(self.configuration.customerId)/payments/\(paymentId)/thirdpartystatus" - getResponse(forURL: URL, withParameters: [:], success: { (responseObject) in - guard let responseDic = responseObject as? [String: Any], - let thirdPartyStatusResponse = ThirdPartyStatusResponse(json: responseDic) else { - failure(SessionError.RuntimeError("Response was not a dictionary. Raw response: \(responseObject)")) - return - } - - success(thirdPartyStatusResponse) - }, failure: { error in - failure(error) - }) - } - - public func customerDetails( - forProductId productId: String, - withLookupValues lookupValues: [[String: String]], - countryCode: String, - success: @escaping (_ paymentProduct: CustomerDetails) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - let URL = "\(baseURL)/\(configuration.customerId)/products/\(productId)/customerDetails" - let params = ["values": lookupValues, "countryCode": countryCode] as [String: Any] - - postResponse( + getResponse( forURL: URL, - withParameters: params, - additionalAcceptableStatusCodes: IndexSet([404, 400]), - success: { (responseObject) in - guard let responseDic = responseObject as? [String: Any], - let customerDetails = CustomerDetails(json: responseDic), responseDic["errors"] == nil else { - let errors = (responseObject as? [String: Any])?["errors"] - if let errors = errors as? [[String: Any]] { - let customerDetailsError = CustomerDetailsError(responseValues: errors) - failure(customerDetailsError) - return - } - failure(SessionError.RuntimeError("Response was not a dictionary. Raw response: \(responseObject)")) + withParameters: [:], + success: { (responseObject: ThirdPartyStatusResponse?) in + guard let thirdPartyStatusResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) return } - success(customerDetails) + + success(thirdPartyStatusResponse) }, failure: { error in failure(error) - }) - + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) + } + ) } public func paymentProducts( forContext context: PaymentContext, success: @escaping (_ paymentProducts: BasicPaymentProducts) -> Void, - failure: @escaping (_ error: Error) -> Void + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil ) { let isRecurring = context.isRecurring ? "true" : "false" let URL = "\(baseURL)/\(configuration.customerId)/products" @@ -124,24 +116,46 @@ public class C2SCommunicator { params["locale"] = locale } - getResponse(forURL: URL, withParameters: params, success: { (responseObject) in - guard let responseDic = responseObject as? [String: Any] else { - failure(SessionError.RuntimeError("Response was not a dictionary. Raw response: \(responseObject)")) - return - } - var paymentProducts = BasicPaymentProducts(json: responseDic) - paymentProducts = self.filterAndroidPayFromProducts(paymentProducts: paymentProducts) + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: BasicPaymentProducts?) in + guard var paymentProductsResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } - paymentProducts = self.checkApplePayAvailability(with: paymentProducts, for: context, success: { - success(paymentProducts) - }, failure: { error in + paymentProductsResponse = self.checkApplePayAvailability( + with: paymentProductsResponse, + for: context, + success: { + success(paymentProductsResponse) + }, + failure: { error in + failure(error) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) + } + ) + }, + failure: { error in failure(error) - }) - }, failure: { error in - failure(error) - }) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) + } + ) } + @available( + *, + deprecated, + message: + """ + In a future release, this function will be removed since GooglePay can also be used on iOS. + """ + ) public func filterAndroidPayFromProducts(paymentProducts: BasicPaymentProducts) -> BasicPaymentProducts { if let androidPayPaymentProduct = paymentProducts.paymentProduct(withIdentifier: SDKConstants.kAndroidPayIdentifier), @@ -155,7 +169,9 @@ public class C2SCommunicator { public func checkApplePayAvailability(with paymentProducts: BasicPaymentProducts, for context: PaymentContext, success: @escaping () -> Void, - failure: @escaping (_ error: Error) -> Void) -> BasicPaymentProducts { + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil + ) -> BasicPaymentProducts { if let applePayPaymentProduct = paymentProducts.paymentProduct(withIdentifier: SDKConstants.kApplePayIdentifier) { if SDKConstants.SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v: "8.0") && @@ -163,7 +179,7 @@ public class C2SCommunicator { paymentProductNetworks( forProduct: SDKConstants.kApplePayIdentifier, context: context, - success: {(_ paymentProductNetworks: PaymentProductNetworks) -> Void in + success: { (_ paymentProductNetworks: PaymentProductNetworks) -> Void in if let product = paymentProducts.paymentProducts.firstIndex(of: applePayPaymentProduct), !PKPaymentAuthorizationViewController.canMakePayments( usingNetworks: paymentProductNetworks.paymentProductNetworks @@ -174,6 +190,9 @@ public class C2SCommunicator { }, failure: { error in failure(error) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) } ) } else { @@ -193,7 +212,9 @@ public class C2SCommunicator { public func paymentProductNetworks(forProduct paymentProductId: String, context: PaymentContext, success: @escaping (_ paymentProductNetworks: PaymentProductNetworks) -> Void, - failure: @escaping (_ error: Error) -> Void) { + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil + ) { let isRecurring = context.isRecurring ? "true" : "false" guard let locale = context.locale else { failure(SessionError.RuntimeError("Locale was nil.")) @@ -210,26 +231,31 @@ public class C2SCommunicator { "isRecurring": isRecurring ] - getResponse(forURL: URL, withParameters: params, success: { (responseObject) in - guard let response = responseObject as? [String: Any] else { - failure(SessionError.RuntimeError("Response was not a dictionary. Raw response: \(responseObject)")) - return - } - let rawProductNetworks = response["networks"] - let paymentProductNetworks = PaymentProductNetworks() - if let productNetworks = rawProductNetworks as? [PKPaymentNetwork] { - paymentProductNetworks.paymentProductNetworks.append(contentsOf: productNetworks) + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: PaymentProductNetworks?) in + guard let paymentProductNetworks = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(paymentProductNetworks) + }, + failure: { error in + failure(error) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) } - success(paymentProductNetworks) - }, failure: { error in - failure(error) - }) + ) } public func paymentProductGroups( forContext context: PaymentContext, success: @escaping (_ paymentProductGroups: BasicPaymentProductGroups) -> Void, - failure: @escaping (_ error: Error) -> Void + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil ) { let isRecurring = context.isRecurring ? "true" : "false" guard let locale = context.locale else { @@ -248,61 +274,85 @@ public class C2SCommunicator { "isRecurring": isRecurring ] - getResponse(forURL: URL, withParameters: params, success: { (responseObject) in - guard let responseDic = responseObject as? [String: Any] else { - failure(SessionError.RuntimeError("Response was not a dictionary. Raw response: \(responseObject)")) - return + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: BasicPaymentProductGroups?) in + guard let paymentProductGroups = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(paymentProductGroups) + }, + failure: { error in + failure(error) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) } - let paymentProductGroups = BasicPaymentProductGroups(json: responseDic) - success(paymentProductGroups) - }, failure: { error in - failure(error) - }) + ) } public func paymentProduct(withIdentifier paymentProductId: String, context: PaymentContext, success: @escaping (_ paymentProduct: PaymentProduct) -> Void, - failure: @escaping (_ error: Error) -> Void) { - - checkAvailability(forProduct: paymentProductId, context: context, success: {() -> Void in - let isRecurring = context.isRecurring ? "true" : "false" + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil + ) { + checkAvailability( + forProduct: paymentProductId, + context: context, + success: {() -> Void in + let isRecurring = context.isRecurring ? "true" : "false" + + let URL = "\(self.baseURL)/\(self.configuration.customerId)/products/\(paymentProductId)/" + var params: [String: Any] = + [ + "countryCode": context.countryCodeString, + "currencyCode": context.amountOfMoney.currencyCodeString, + "amount": context.amountOfMoney.totalAmount, + "isRecurring": isRecurring + ] + if let forceBasicFlow = context.forceBasicFlow { + params["forceBasicFlow"] = forceBasicFlow ? "true" : "false" + } + if let locale = context.locale { + params["locale"] = locale + } - let URL = "\(self.baseURL)/\(self.configuration.customerId)/products/\(paymentProductId)/" - var params: [String: Any] = - [ - "countryCode": context.countryCodeString, - "currencyCode": context.amountOfMoney.currencyCodeString, - "amount": context.amountOfMoney.totalAmount, - "isRecurring": isRecurring - ] - if let forceBasicFlow = context.forceBasicFlow { - params["forceBasicFlow"] = forceBasicFlow ? "true" : "false" - } - if let locale = context.locale { - params["locale"] = locale - } + self.getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: PaymentProduct?) in + guard let paymentProduct = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } - self.getResponse(forURL: URL, withParameters: params, success: { (responseObject) in - guard let responseDic = responseObject as? [String: Any], - let paymentProduct = PaymentProduct(json: responseDic) else { - failure(SessionError.RuntimeError("Response was not a dictionary. Raw response: \(responseObject)")) - return - } - success(paymentProduct) - }, failure: { error in + success(paymentProduct) + }, failure: { error in + failure(error) + }, apiFailure: {errorResponse in + apiFailure?(errorResponse) + } + ) + }, + failure: { error in failure(error) - }) - }, failure: { error in - failure(error) - }) + }, + apiFailure: {errorResponse in + apiFailure?(errorResponse) + } + ) } public func checkAvailability( forProduct paymentProductId: String, context: PaymentContext, success: @escaping () -> Void, - failure: @escaping (_ error: Error) -> Void + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil ) { if paymentProductId == SDKConstants.kApplePayIdentifier { if SDKConstants.SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v: "8.0") && @@ -310,7 +360,7 @@ public class C2SCommunicator { paymentProductNetworks( forProduct: SDKConstants.kApplePayIdentifier, context: context, - success: {(_ paymentProductNetworks: PaymentProductNetworks) -> Void in + success: { (_ paymentProductNetworks: PaymentProductNetworks) -> Void in if !PKPaymentAuthorizationViewController.canMakePayments( usingNetworks: paymentProductNetworks.paymentProductNetworks ) { @@ -321,7 +371,11 @@ public class C2SCommunicator { }, failure: { error in failure(error) - }) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) + } + ) } else { failure(badRequestError(forProduct: paymentProductId, context: context)) } @@ -365,7 +419,9 @@ public class C2SCommunicator { public func paymentProductGroup(withIdentifier paymentProductGroupId: String, context: PaymentContext, success: @escaping (_ paymentProductGroup: PaymentProductGroup) -> Void, - failure: @escaping (_ error: Error) -> Void) { + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil + ) { let isRecurring = context.isRecurring ? "true" : "false" guard let locale = context.locale else { @@ -383,41 +439,58 @@ public class C2SCommunicator { "isRecurring": isRecurring ] - self.getResponse(forURL: URL, withParameters: params, success: { (responseObject) in - guard let responseDic = responseObject as? [String: Any], - let paymentProductGroup = PaymentProductGroup(json: responseDic) else { - failure(SessionError.RuntimeError("Response was not a dictionary. Raw response: \(responseObject)")) - return + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: PaymentProductGroup?) in + guard let paymentProductGroup = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(paymentProductGroup) + }, + failure: { error in + failure(error) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) } - success(paymentProductGroup) - }, failure: { error in - failure(error) - }) + ) } public func publicKey( success: @escaping (_ publicKeyResponse: PublicKeyResponse) -> Void, - failure: @escaping (_ error: Error) -> Void + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil ) { let URL = "\(baseURL)/\(configuration.customerId)/crypto/publickey" - getResponse(forURL: URL, success: {(_ responseObject: Any) -> Void in - guard let rawPublicKeyResponse = responseObject as? [AnyHashable: Any], - let keyId = rawPublicKeyResponse["keyId"] as? String, - let encodedPublicKey = rawPublicKeyResponse["publicKey"] as? String else { - failure(SessionError.RuntimeError("Response was invalid. Raw response: \(responseObject)")) - return + + getResponse( + forURL: URL, + success: { (responseObject: PublicKeyResponse?) -> Void in + guard let publicKeyResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(publicKeyResponse) + }, + failure: { error in + failure(error) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) } - let response = PublicKeyResponse(keyId: keyId, encodedPublicKey: encodedPublicKey) - success(response) - }, failure: { error in - failure(error) - }) + ) } public func paymentProductId(byPartialCreditCardNumber partialCreditCardNumber: String, context: PaymentContext?, success: @escaping (_ iinDetailsResponse: IINDetailsResponse) -> Void, - failure: @escaping (_ error: Error) -> Void) { + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil + ) { let URL = "\(baseURL)/\(configuration.customerId)/services/getIINdetails" var parameters: [String: Any] = [:] @@ -441,16 +514,19 @@ public class C2SCommunicator { forURL: URL, withParameters: parameters, additionalAcceptableStatusCodes: additionalAcceptableStatusCodes, - success: {(responseObject) -> Void in - guard let json = responseObject as? [String: Any] else { - failure(SessionError.RuntimeError("Response was not a dictionary. Raw response: \(responseObject)")) + success: { (responseObject: IINDetailsResponse?) -> Void in + guard let iinDetailsResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) return } - let response = IINDetailsResponse(json: json) - success(response) + + success(iinDetailsResponse) }, failure: { error in failure(error) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) } ) } @@ -475,25 +551,63 @@ public class C2SCommunicator { source: String, target: String, success: @escaping (_ convertedAmountInCents: Int) -> Void, - failure: @escaping (_ error: Error?) -> Void + failure: @escaping (_ error: Error?) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil ) { let amount = "\(amountInCents)" let URL = "\(baseURL)/\(configuration.customerId)/services/convert/amount" let params: [String: Any] = ["source": source, "target": target, "amount": amount] - getResponse(forURL: URL, withParameters: params, success: { (responseObject) in - guard let json = responseObject as? [String: Any] else { - failure(SessionError.RuntimeError("Response was not a dictionary. Raw response: \(responseObject)")) - return + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: ConvertedAmountResponse?) in + guard let convertedAmountResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(convertedAmountResponse.convertedAmount) + }, + failure: { error in + failure(error) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) } - if let input = json["convertedAmount"] as? Int { - success(input) - } else { - failure(nil) + ) + } + + internal func convert( + amountInCents: Int, + source: String, + target: String, + success: @escaping (_ convertedAmountResponse: ConvertedAmountResponse) -> Void, + failure: @escaping (_ error: Error?) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil + ) { + let amount = "\(amountInCents)" + let URL = "\(baseURL)/\(configuration.customerId)/services/convert/amount" + let params: [String: Any] = ["source": source, "target": target, "amount": amount] + + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: ConvertedAmountResponse?) in + guard let convertedAmountResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(convertedAmountResponse) + }, + failure: { error in + failure(error) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) } - }, failure: { error in - failure(error) - }) + ) } public func directory( @@ -501,36 +615,39 @@ public class C2SCommunicator { countryCode: String, currencyCode: String, success: @escaping (_ directoryEntries: DirectoryEntries) -> Void, - failure: @escaping (_ error: Error) -> Void + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil ) { let URL = "\(baseURL)/\(self.configuration.customerId)/products/\(paymentProductId)/directory" let params: [String: Any] = ["countryCode": countryCode, "currencyCode": currencyCode] - getResponse(forURL: URL, withParameters: params, success: { (responseObject) in - guard let responseDic = responseObject as? [String: Any] else { - failure(SessionError.RuntimeError("Response was not a dictionary. Raw response: \(responseObject)")) - return + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: DirectoryEntries?) in + guard let directoryEntries = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(directoryEntries) + }, + failure: { error in + failure(error) + }, + apiFailure: { errorResponse in + apiFailure?(errorResponse) } - let directoryEntries = DirectoryEntries(json: responseDic) - success(directoryEntries) - }, failure: { error in - failure(error) - }) + ) } + @available(*, deprecated, message: "In a future release, this function will be removed.") public func getResponse( forURL URL: String, withParameters parameters: Parameters? = nil, success: @escaping (_ responseObject: Any) -> Void, failure: @escaping (_ error: Error) -> Void ) { - var httpHeaders: [HTTPHeader] = [] - headers.forEach { - if let key = $0.key as? String, let value = $0.value as? String { - httpHeaders.append(HTTPHeader(name: key, value: value)) - } - } - if loggingEnabled { logRequest(forURL: URL, requestMethod: .get) } @@ -538,7 +655,7 @@ public class C2SCommunicator { networkingWrapper.getResponse( forURL: URL, withParameters: parameters, - headers: HTTPHeaders(httpHeaders), + headers: httpHeaders, additionalAcceptableStatusCodes: nil, success: { response in if self.loggingEnabled { @@ -555,6 +672,46 @@ public class C2SCommunicator { ) } + private func getResponse( + forURL URL: String, + withParameters parameters: Parameters? = nil, + success: @escaping (_ responseObject: T?) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil + ) { + if loggingEnabled { + logRequest(forURL: URL, requestMethod: .get) + } + + let successHandler: (T?, Int?) -> Void = { (responseObject, statusCode) -> Void in + if self.loggingEnabled { + self.logSuccessResponse(forURL: URL, withResponseCode: statusCode, forResponse: responseObject) + } + success(responseObject) + } + + networkingWrapper.getResponse( + forURL: URL, + headers: httpHeaders, + withParameters: parameters, + additionalAcceptableStatusCodes: nil, + success: successHandler, + failure: { error in + if self.loggingEnabled { + self.logFailureResponse(forURL: URL, forError: error) + } + failure(error) + }, + apiFailure: { errorResponse in + if self.loggingEnabled { + self.logApiFailureResponse(forURL: URL, forApiError: errorResponse) + } + apiFailure?(errorResponse) + } + ) + } + + @available(*, deprecated, message: "In a future release, this function will be removed.") public func postResponse( forURL URL: String, withParameters parameters: [AnyHashable: Any], @@ -562,20 +719,13 @@ public class C2SCommunicator { success: @escaping (_ responseObject: Any) -> Void, failure: @escaping (_ error: Error) -> Void ) { - var httpHeaders: [HTTPHeader] = [] - headers.forEach { - if let key = $0.key as? String, let value = $0.value as? String { - httpHeaders.append(HTTPHeader(name: key, value: value)) - } - } - if loggingEnabled { logRequest(forURL: URL, requestMethod: .post, postBody: parameters as? Parameters) } networkingWrapper.postResponse( forURL: URL, - headers: HTTPHeaders(httpHeaders), + headers: httpHeaders, withParameters: parameters as? Parameters, additionalAcceptableStatusCodes: additionalAcceptableStatusCodes, success: { response in @@ -593,6 +743,46 @@ public class C2SCommunicator { ) } + private func postResponse( + forURL URL: String, + withParameters parameters: [AnyHashable: Any], + additionalAcceptableStatusCodes: IndexSet?, + success: @escaping (_ responseObject: T?) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: ((_ errorResponse: ApiErrorResponse) -> Void)? = nil + ) { + if loggingEnabled { + logRequest(forURL: URL, requestMethod: .post, postBody: parameters as? Parameters) + } + + let successHandler: (T?, Int?) -> Void = { (responseObject, statusCode) -> Void in + if self.loggingEnabled { + self.logSuccessResponse(forURL: URL, withResponseCode: statusCode, forResponse: responseObject) + } + success(responseObject) + } + + networkingWrapper.postResponse( + forURL: URL, + headers: httpHeaders, + withParameters: parameters as? Parameters, + additionalAcceptableStatusCodes: additionalAcceptableStatusCodes, + success: successHandler, + failure: { error in + if self.loggingEnabled { + self.logFailureResponse(forURL: URL, forError: error) + } + failure(error) + }, + apiFailure: { errorResponse in + if self.loggingEnabled { + self.logApiFailureResponse(forURL: URL, forApiError: errorResponse) + } + apiFailure?(errorResponse) + } + ) + } + private func responseWithoutStatusCode(response: [String: Any]?) -> [String: Any]? { var originalResponse = response originalResponse?.removeValue(forKey: "statusCode") @@ -600,6 +790,14 @@ public class C2SCommunicator { return originalResponse } + @available( + *, + deprecated, + message: + """ + This function can be removed once the deprecated code of non-codables is removed. + """ + ) private func logSuccessResponse(forURL URL: String, forResponse response: [String: Any]?) { let responseCode = response?["statusCode"] as? Int @@ -608,6 +806,20 @@ public class C2SCommunicator { self.logResponse(forURL: URL, responseCode: responseCode, responseBody: "\(originalResponse as AnyObject)") } + private func logSuccessResponse( + forURL URL: String, + withResponseCode responseCode: Int?, + forResponse response: T + ) { + guard let responseData = try? JSONEncoder().encode(response) else { + print("Success response received, but could not be encoded.") + return + } + + let responseString = String(decoding: responseData, as: UTF8.self) + self.logResponse(forURL: URL, responseCode: responseCode, responseBody: responseString) + } + private func logFailureResponse(forURL URL: String, forError error: Error) { self.logResponse( forURL: URL, @@ -617,6 +829,15 @@ public class C2SCommunicator { ) } + private func logApiFailureResponse(forURL URL: String, forApiError errorResponse: ApiErrorResponse) { + self.logResponse( + forURL: URL, + responseCode: nil, + responseBody: errorResponse.errors[0].message, + isApiError: true + ) + } + /** * Logs all request headers, url and body */ @@ -628,7 +849,7 @@ public class C2SCommunicator { Request Headers : \n """ - headers.forEach { header in + httpHeaders.forEach { header in requestLog += " \(header) \n" } @@ -642,7 +863,13 @@ public class C2SCommunicator { /** * Logs all response headers, status code and body */ - private func logResponse(forURL URL: String, responseCode: Int?, responseBody: String, isError: Bool = false) { + private func logResponse( + forURL URL: String, + responseCode: Int?, + responseBody: String, + isError: Bool = false, + isApiError: Bool = false + ) { var responseLog = """ Response URL : \(URL) @@ -657,11 +884,18 @@ public class C2SCommunicator { responseLog += "Response Headers : \n" - headers.forEach { header in + httpHeaders.forEach { header in responseLog += " \(header) \n" } - responseLog += isError ? "Response Error : " : "Response Body : " + if isApiError { + responseLog += "API Error : " + } else if isError { + responseLog += "Response Error : " + } else { + responseLog += "Response Body : " + } + responseLog += responseBody print(responseLog) diff --git a/IngenicoConnectKit/C2SCommunicatorConfiguration.swift b/IngenicoConnectKit/C2SCommunicatorConfiguration.swift index 8385187..67e729e 100644 --- a/IngenicoConnectKit/C2SCommunicatorConfiguration.swift +++ b/IngenicoConnectKit/C2SCommunicatorConfiguration.swift @@ -8,6 +8,14 @@ import Foundation +@available( + *, + deprecated, + message: + """ + In a future release, this class, its functions and its properties will become internal to the SDK. + """ +) public class C2SCommunicatorConfiguration { let clientSessionId: String let customerId: String @@ -150,7 +158,17 @@ public class C2SCommunicatorConfiguration { self.assetsBaseURL = assetBaseURL } + @available( + *, + deprecated, + message: + """ + In a future release, this property will become private to this class. Use baseURL instead. + """ + ) + // swiftlint:disable identifier_name public var _baseURL: String? + // swiftlint:enable identifier_name /// New base URL should be a valid URL public var baseURL: String { diff --git a/IngenicoConnectKit/ClientApi.swift b/IngenicoConnectKit/ClientApi.swift new file mode 100644 index 0000000..6f7b1ba --- /dev/null +++ b/IngenicoConnectKit/ClientApi.swift @@ -0,0 +1,340 @@ +// +// ClientApi.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 24/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import UIKit + +public class ClientApi { + private let clientApiCommunicator: ClientApiCommunicator + + internal var iinLookupPending = false + + private let preLoadImages: Bool + + private var groupPaymentProducts: Bool { + return clientApiCommunicator.groupPaymentProducts + } + + internal var base64EncodedClientMetaInfo: String { + return clientApiCommunicator.base64EncodedClientMetaInfo + } + + private var assetUrl: String { + return clientApiCommunicator.assetUrl + } + + public init(sdkConfiguration: ConnectSDKConfiguration, paymentConfiguration: PaymentConfiguration) { + self.preLoadImages = sdkConfiguration.preLoadImages + self.clientApiCommunicator = + ClientApiCommunicator(sdkConfiguration: sdkConfiguration, paymentConfiguration: paymentConfiguration) + } + + public func paymentProducts( + success: @escaping (_ paymentProducts: BasicPaymentProducts) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApiCommunicator.paymentProducts( + success: { paymentProducts in + if self.preLoadImages { + self.setLogoForPaymentItems(for: paymentProducts.paymentProducts) { + success(paymentProducts) + } + } else { + success(paymentProducts) + } + }, + failure: failure, + apiFailure: apiFailure + ) + } + + public func paymentProductNetworks( + forProduct paymentProductId: String, + success: @escaping (_ paymentProductNetworks: PaymentProductNetworks) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApiCommunicator.paymentProductNetworks( + forProduct: paymentProductId, + success: success, + failure: failure, + apiFailure: apiFailure + ) + } + + public func paymentProductGroups( + success: @escaping (_ paymentProductGroups: BasicPaymentProductGroups) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApiCommunicator.paymentProductGroups( + success: { paymentProductGroups in + if self.preLoadImages { + self.setLogoForPaymentProductGroups(for: paymentProductGroups.paymentProductGroups) { + success(paymentProductGroups) + } + } else { + success(paymentProductGroups) + } + }, + failure: failure, + apiFailure: apiFailure + ) + } + + public func paymentItems( + success: @escaping (_ paymentItems: PaymentItems) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApiCommunicator.paymentProducts( + success: { paymentProducts in + if self.preLoadImages { + self.setLogoForPaymentItems(for: paymentProducts.paymentProducts) { + if self.groupPaymentProducts { + self.clientApiCommunicator.paymentProductGroups( + success: { paymentProductGroups in + self.setLogoForPaymentProductGroups( + for: paymentProductGroups.paymentProductGroups + ) { + let items = + PaymentItems(products: paymentProducts, groups: paymentProductGroups) + success(items) + } + + }, + failure: failure, + apiFailure: apiFailure + ) + } else { + let items = PaymentItems(products: paymentProducts, groups: nil) + success(items) + } + + } + } else { + let items = PaymentItems(products: paymentProducts, groups: nil) + success(items) + } + }, + failure: failure, + apiFailure: apiFailure + ) + } + + public func paymentProduct( + withId paymentProductId: String, + success: @escaping (_ paymentProduct: PaymentProduct) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApiCommunicator.paymentProduct( + withId: paymentProductId, + success: { paymentProduct in + if self.preLoadImages { + self.setTooltipImages(for: paymentProduct) + self.setLogoForDisplayHints(for: paymentProduct.displayHints) { + success(paymentProduct) + } + } else { + success(paymentProduct) + } + }, + failure: failure, + apiFailure: apiFailure + ) + } + + public func paymentProductGroup( + withId paymentProductGroupId: String, + success: @escaping (_ paymentProductGroup: PaymentProductGroup) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApiCommunicator.paymentProductGroup( + withId: paymentProductGroupId, + success: { paymentProductGroup in + if self.preLoadImages { + self.setLogoForDisplayHints(for: paymentProductGroup.displayHints) { + success(paymentProductGroup) + } + } else { + success(paymentProductGroup) + } + }, + failure: failure, + apiFailure: apiFailure + ) + } + + public func iinDetails( + forPartialCreditCardNumber partialCreditCardNumber: String, + success: @escaping (_ iinDetailsResponse: IINDetailsResponse) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + if partialCreditCardNumber.count < 6 { + let response = IINDetailsResponse(status: .notEnoughDigits) + success(response) + } else if self.iinLookupPending == true { + let response = IINDetailsResponse(status: .pending) + success(response) + } else { + self.iinLookupPending = true + clientApiCommunicator.iinDetails( + forPartialCreditCardNumber: partialCreditCardNumber, + success: { response in + self.iinLookupPending = false + success(response) + }, + failure: { error in + self.iinLookupPending = false + failure(error) + }, + apiFailure: { errorResponse in + self.iinLookupPending = false + apiFailure(errorResponse) + } + ) + } + } + + // swiftlint:disable function_parameter_count + public func convert( + amountInCents: Int, + sourceCurrency: String, + targetCurrency: String, + success: @escaping (_ convertedAmountResponse: ConvertedAmountResponse) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApiCommunicator.convert( + amountInCents: amountInCents, + sourceCurrency: sourceCurrency, + targetCurrency: targetCurrency, + success: success, + failure: failure, + apiFailure: apiFailure + ) + } + // swiftlint:enable function_parameter_count + + public func directory( + forProduct paymentProductId: String, + success: @escaping (_ directory: DirectoryEntries) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApiCommunicator.directory( + forProduct: paymentProductId, + success: success, + failure: failure, + apiFailure: apiFailure + ) + } + + public func thirdPartyStatus( + forPayment paymentId: String, + success: @escaping (_ thirdPartyStatusResponse: ThirdPartyStatusResponse) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApiCommunicator.thirdPartyStatus( + forPayment: paymentId, + success: success, + failure: failure, + apiFailure: apiFailure + ) + } + + public func publicKey( + success: @escaping (_ publicKeyResponse: PublicKeyResponse) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApiCommunicator.publicKey( + success: success, + failure: failure, + apiFailure: apiFailure + ) + } + + private func setLogoForPaymentItems(for paymentItems: [BasicPaymentItem], completion: @escaping() -> Void) { + var counter = 0 + for paymentItem in paymentItems { + setLogoForDisplayHints(for: paymentItem.displayHints, completion: { + counter += 1 + if counter == paymentItems.count { + completion() + } + }) + } + } + + private func setLogoForPaymentProductGroups( + for paymentProductGroups: [BasicPaymentProductGroup], + completion: @escaping() -> Void + ) { + var counter = 0 + for paymentProductGroup in paymentProductGroups { + setLogoForDisplayHints(for: paymentProductGroup.displayHints, completion: { + counter += 1 + if counter == paymentProductGroups.count { + completion() + } + }) + } + } + + private func setLogoForDisplayHints(for displayHints: PaymentItemDisplayHints, completion: @escaping() -> Void) { + self.getLogoByStringURL(from: displayHints.logoPath) { data, _, error in + if let imageData = data, error == nil { + displayHints.logoImage = UIImage(data: imageData) + } + completion() + } + } + + private func setTooltipImages(for paymentItem: PaymentItem) { + for field in paymentItem.fields.paymentProductFields { + guard let tooltip = field.displayHints.tooltip, + let imagePath = tooltip.imagePath else { return } + + self.getLogoByStringURL(from: imagePath) { data, _, error in + if let imageData = data, error == nil { + tooltip.image = UIImage(data: imageData) + } + } + } + } + + internal func getLogoByStringURL( + from url: String, + completion: @escaping (Data?, URLResponse?, Error?) -> Void + ) { + let completeUrl = assetUrl + url + + guard let encodedUrlString = completeUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + Macros.DLog(message: "Unable to decode URL for url string: \(url)") + completion(nil, nil, nil) + return + } + + guard let encodedUrl = URL(string: encodedUrlString) else { + Macros.DLog(message: "Unable to create URL for url string: \(encodedUrlString)") + completion(nil, nil, nil) + return + } + + URLSession.shared.dataTask(with: encodedUrl, completionHandler: {data, response, error in + DispatchQueue.main.async { + completion(data, response, error) + } + }).resume() + } +} diff --git a/IngenicoConnectKit/ClientApiCommunicator.swift b/IngenicoConnectKit/ClientApiCommunicator.swift new file mode 100644 index 0000000..c9520d4 --- /dev/null +++ b/IngenicoConnectKit/ClientApiCommunicator.swift @@ -0,0 +1,747 @@ +// +// ClientApiCommunicator.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 24/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import Foundation +import Alamofire +import PassKit + +internal struct ClientApiCommunicator { + private var sdkConfiguration: ConnectSDKConfiguration + private let paymentConfiguration: PaymentConfiguration + private let networkingWrapper = AlamofireWrapper.shared + private let util = Util() + + private var baseURL: String + + private var clientSessionId: String { + return sdkConfiguration.sessionConfiguration.clientSessionId + } + + private var customerId: String { + return sdkConfiguration.sessionConfiguration.customerId + } + + internal var assetUrl: String { + return sdkConfiguration.sessionConfiguration.assetUrl + } + + internal var base64EncodedClientMetaInfo: String { + return + util.base64EncodedClientMetaInfo( + withAppIdentifier: sdkConfiguration.applicationId, + ipAddress: sdkConfiguration.ipAddress + ) ?? "" + } + + private var loggingEnabled: Bool { + return sdkConfiguration.enableNetworkLogs + } + + private var paymentContext: PaymentContext { + return paymentConfiguration.paymentContext + } + + internal var groupPaymentProducts: Bool { + return paymentConfiguration.groupPaymentProducts + } + + private var httpHeaders: HTTPHeaders { + return [ + "Authorization": "GCS v1Client:\(clientSessionId)", + "X-GCS-ClientMetaInfo": base64EncodedClientMetaInfo + ] + } + + init(sdkConfiguration: ConnectSDKConfiguration, paymentConfiguration: PaymentConfiguration) { + self.sdkConfiguration = sdkConfiguration + self.paymentConfiguration = paymentConfiguration + self.baseURL = Self.fixBaseURL(url: sdkConfiguration.sessionConfiguration.clientApiUrl) + } + + func thirdPartyStatus( + forPayment paymentId: String, + success: @escaping (_ thirdPartyStatusResponse: ThirdPartyStatusResponse) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let URL = "\(baseURL)/\(customerId)/payments/\(paymentId)/thirdpartystatus" + + getResponse( + forURL: URL, + withParameters: [:], + success: { (responseObject: ThirdPartyStatusResponse?) in + guard let thirdPartyStatusResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(thirdPartyStatusResponse) + }, + failure: failure, + apiFailure: apiFailure + ) + } + + func paymentProducts( + success: @escaping (_ paymentProducts: BasicPaymentProducts) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let isRecurring = paymentContext.isRecurring ? "true" : "false" + let URL = "\(baseURL)/\(customerId)/products" + var params: [String: Any] = + [ + "countryCode": paymentContext.countryCodeString, + "currencyCode": paymentContext.amountOfMoney.currencyCodeString, + "amount": paymentContext.amountOfMoney.totalAmount, + "hide": "fields", + "isRecurring": isRecurring + ] + + if let locale = paymentContext.locale { + params["locale"] = locale + } + + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: BasicPaymentProducts?) in + guard let paymentProductsResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + self.checkApplePayAvailability( + with: paymentProductsResponse, + success: { paymentProductsFilteredApplePay in + success(paymentProductsFilteredApplePay) + }, + failure: failure, + apiFailure: apiFailure + ) + }, + failure: failure, + apiFailure: apiFailure + ) + } + + func checkApplePayAvailability( + with paymentProducts: BasicPaymentProducts, + success: @escaping (_ paymentProducts: BasicPaymentProducts) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + if let applePayPaymentProduct = + paymentProducts.paymentProduct(withIdentifier: SDKConstants.kApplePayIdentifier) { + if SDKConstants.SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v: "8.0") && + PKPaymentAuthorizationViewController.canMakePayments() { + paymentProductNetworks( + forProduct: SDKConstants.kApplePayIdentifier, + success: { (_ paymentProductNetworks: PaymentProductNetworks) -> Void in + if let product = paymentProducts.paymentProducts.firstIndex(of: applePayPaymentProduct), + !PKPaymentAuthorizationViewController.canMakePayments( + usingNetworks: paymentProductNetworks.paymentProductNetworks + ) { + paymentProducts.paymentProducts.remove(at: product) + } + success(paymentProducts) + }, + failure: failure, + apiFailure: apiFailure + ) + } else { + if let product = paymentProducts.paymentProducts.firstIndex(of: applePayPaymentProduct) { + paymentProducts.paymentProducts.remove(at: product) + } + + success(paymentProducts) + } + } else { + success(paymentProducts) + } + } + + func paymentProductNetworks( + forProduct paymentProductId: String, + success: @escaping (_ paymentProductNetworks: PaymentProductNetworks) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let isRecurring = paymentContext.isRecurring ? "true" : "false" + guard let locale = paymentContext.locale else { + failure(SessionError.RuntimeError("Locale was nil.")) + return + } + let URL = "\(self.baseURL)/\(customerId)/products/\(paymentProductId)/networks" + let params: [String: Any] = + [ + "countryCode": paymentContext.countryCodeString, + "locale": locale, + "currencyCode": paymentContext.amountOfMoney.currencyCodeString, + "amount": paymentContext.amountOfMoney.totalAmount, + "hide": "fields", + "isRecurring": isRecurring + ] + + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: PaymentProductNetworks?) in + guard let paymentProductNetworks = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(paymentProductNetworks) + }, + failure: failure, + apiFailure: apiFailure + ) + } + + func paymentProductGroups( + success: @escaping (_ paymentProductGroups: BasicPaymentProductGroups) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let isRecurring = paymentContext.isRecurring ? "true" : "false" + guard let locale = paymentContext.locale else { + failure(SessionError.RuntimeError("Locale was nil.")) + return + } + + let URL = "\(baseURL)/\(customerId)/productgroups" + let params: [String: Any] = + [ + "countryCode": paymentContext.countryCodeString, + "locale": locale, + "currencyCode": paymentContext.amountOfMoney.currencyCodeString, + "amount": paymentContext.amountOfMoney.totalAmount, + "hide": "fields", + "isRecurring": isRecurring + ] + + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: BasicPaymentProductGroups?) in + guard let paymentProductGroups = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(paymentProductGroups) + }, + failure: failure, + apiFailure: apiFailure + ) + } + + func paymentProduct( + withId paymentProductId: String, + success: @escaping (_ paymentProduct: PaymentProduct) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + checkApplePayAvailability( + forProduct: paymentProductId, + success: {() -> Void in + let isRecurring = paymentContext.isRecurring ? "true" : "false" + + let URL = "\(self.baseURL)/\(customerId)/products/\(paymentProductId)/" + var params: [String: Any] = + [ + "countryCode": paymentContext.countryCodeString, + "currencyCode": paymentContext.amountOfMoney.currencyCodeString, + "amount": paymentContext.amountOfMoney.totalAmount, + "isRecurring": isRecurring + ] + if let forceBasicFlow = paymentContext.forceBasicFlow { + params["forceBasicFlow"] = forceBasicFlow ? "true" : "false" + } + if let locale = paymentContext.locale { + params["locale"] = locale + } + + self.getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: PaymentProduct?) in + guard let paymentProduct = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(paymentProduct) + }, + failure: failure, + apiFailure: apiFailure + ) + }, + failure: failure, + apiFailure: apiFailure + ) + } + + private func checkApplePayAvailability( + forProduct paymentProductId: String, + success: @escaping () -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + if paymentProductId == SDKConstants.kApplePayIdentifier { + if SDKConstants.SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v: "8.0") && + PKPaymentAuthorizationViewController.canMakePayments() { + paymentProductNetworks( + forProduct: SDKConstants.kApplePayIdentifier, + success: { (_ paymentProductNetworks: PaymentProductNetworks) -> Void in + if !PKPaymentAuthorizationViewController.canMakePayments( + usingNetworks: paymentProductNetworks.paymentProductNetworks + ) { + failure(self.badRequestError(forProduct: paymentProductId)) + } else { + success() + } + }, + failure: failure, + apiFailure: apiFailure + ) + } else { + failure(badRequestError(forProduct: paymentProductId)) + } + } else { + success() + } + } + + private func badRequestError(forProduct paymentProductId: String) -> Error { + let url = createBadRequestErrorURL(forProduct: paymentProductId) + let errorUserInfo = + [ + "com.alamofire.serialization.response.error.response": + HTTPURLResponse( + url: URL(string: url)!, + statusCode: 400, + httpVersion: nil, + headerFields: ["Connection": "close"] + )!, + "NSErrorFailingURLKey": url, + "com.alamofire.serialization.response.error.data": Data(), + "NSLocalizedDescription": "Request failed: bad request (400)" + ] as [String: Any] + let error = + NSError( + domain: "com.alamofire.serialization.response.error.response", + code: -1011, + userInfo: errorUserInfo + ) + return error + } + + private func createBadRequestErrorURL(forProduct paymentProductId: String) -> String { + let isRecurring = paymentContext.isRecurring ? "true" : "false" + // swiftlint:disable line_length + return + "\(baseURL)/\(customerId)/products/\(paymentProductId)/?countryCode=\(paymentContext.countryCodeString)&locale=\(paymentContext.locale!)¤cyCode=\(paymentContext.amountOfMoney.currencyCodeString)&amount=\(UInt(paymentContext.amountOfMoney.totalAmount))&isRecurring=\(isRecurring)" + // swiftlint:enable line_length + } + + func paymentProductGroup( + withId paymentProductGroupId: String, + success: @escaping (_ paymentProductGroup: PaymentProductGroup) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let isRecurring = paymentContext.isRecurring ? "true" : "false" + + guard let locale = paymentContext.locale else { + failure(SessionError.RuntimeError("Locale was nil.")) + return + } + + let URL = "\(baseURL)/\(customerId)/productgroups/\(paymentProductGroupId)/" + let params: [String: Any] = + [ + "countryCode": paymentContext.countryCodeString, + "locale": locale, + "currencyCode": paymentContext.amountOfMoney.currencyCodeString, + "amount": paymentContext.amountOfMoney.totalAmount, + "isRecurring": isRecurring + ] + + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: PaymentProductGroup?) in + guard let paymentProductGroup = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(paymentProductGroup) + }, + failure: failure, + apiFailure: apiFailure + ) + } + + func publicKey( + success: @escaping (_ publicKeyResponse: PublicKeyResponse) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let URL = "\(baseURL)/\(customerId)/crypto/publickey" + + getResponse( + forURL: URL, + success: { (responseObject: PublicKeyResponse?) -> Void in + guard let publicKeyResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(publicKeyResponse) + }, + failure: failure, + apiFailure: apiFailure + ) + } + + func iinDetails( + forPartialCreditCardNumber partialCreditCardNumber: String, + success: @escaping (_ iinDetailsResponse: IINDetailsResponse) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let URL = "\(baseURL)/\(customerId)/services/getIINdetails" + + var parameters: [String: Any] = [:] + parameters["bin"] = getIINDigitsFrom(partialCreditCardNumber: partialCreditCardNumber) + + var paymentContextParameters: [String: Any] = [:] + paymentContextParameters["isRecurring"] = paymentContext.isRecurring ? "true" : "false" + paymentContextParameters["countryCode"] = paymentContext.countryCodeString + + var amountOfMoney: [String: Any] = [:] + amountOfMoney["amount"] = String(paymentContext.amountOfMoney.totalAmount) + amountOfMoney["currencyCode"] = paymentContext.amountOfMoney.currencyCodeString + paymentContextParameters["amountOfMoney"] = amountOfMoney + + parameters["paymentContext"] = paymentContextParameters + + let additionalAcceptableStatusCodes = IndexSet(integer: 404) + postResponse( + forURL: URL, + withParameters: parameters, + additionalAcceptableStatusCodes: additionalAcceptableStatusCodes, + success: { (responseObject: IINDetailsResponse?) -> Void in + guard let iinDetailsResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(iinDetailsResponse) + }, + failure: failure, + apiFailure: apiFailure + ) + } + + private func getIINDigitsFrom(partialCreditCardNumber: String) -> String { + let max: Int + if partialCreditCardNumber.count >= 8 { + max = 8 + } else { + max = min(partialCreditCardNumber.count, 6) + } + return + String( + partialCreditCardNumber[ + .. Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let amount = "\(amountInCents)" + let URL = "\(baseURL)/\(customerId)/services/convert/amount" + let params: [String: Any] = ["source": sourceCurrency, "target": targetCurrency, "amount": amount] + + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: ConvertedAmountResponse?) in + guard let convertedAmountResponse = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(convertedAmountResponse) + }, + failure: failure, + apiFailure: apiFailure + ) + } + // swiftlint:enable function_parameter_count + + func directory( + forProduct paymentProductId: String, + success: @escaping (_ directoryEntries: DirectoryEntries) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let URL = "\(baseURL)/\(customerId)/products/\(paymentProductId)/directory" + let params: [String: Any] = + [ + "countryCode": paymentContext.countryCodeString, + "currencyCode": paymentContext.amountOfMoney.currencyCodeString + ] + + getResponse( + forURL: URL, + withParameters: params, + success: { (responseObject: DirectoryEntries?) in + guard let directoryEntries = responseObject else { + failure(SessionError.RuntimeError("Response was empty.")) + return + } + + success(directoryEntries) + }, + failure: failure, + apiFailure: apiFailure + ) + } + + private func getResponse( + forURL URL: String, + withParameters parameters: Parameters? = nil, + success: @escaping (_ responseObject: T?) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + if loggingEnabled { + logRequest(forURL: URL, requestMethod: .get) + } + + let successHandler: (T?, Int?) -> Void = { (responseObject, statusCode) -> Void in + if self.loggingEnabled { + self.logSuccessResponse(forURL: URL, withResponseCode: statusCode, forResponse: responseObject) + } + success(responseObject) + } + + networkingWrapper.getResponse( + forURL: URL, + headers: httpHeaders, + withParameters: parameters, + additionalAcceptableStatusCodes: nil, + success: successHandler, + failure: { error in + if self.loggingEnabled { + self.logFailureResponse(forURL: URL, forError: error) + } + failure(error) + }, + apiFailure: { errorResponse in + if self.loggingEnabled { + self.logApiFailureResponse(forURL: URL, forApiError: errorResponse) + } + apiFailure(errorResponse) + } + ) + } + + // swiftlint:disable function_parameter_count + private func postResponse( + forURL URL: String, + withParameters parameters: [AnyHashable: Any], + additionalAcceptableStatusCodes: IndexSet?, + success: @escaping (_ responseObject: T?) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + if loggingEnabled { + logRequest(forURL: URL, requestMethod: .post, postBody: parameters as? Parameters) + } + + let successHandler: (T?, Int?) -> Void = { (responseObject, statusCode) -> Void in + if self.loggingEnabled { + self.logSuccessResponse(forURL: URL, withResponseCode: statusCode, forResponse: responseObject) + } + success(responseObject) + } + + networkingWrapper.postResponse( + forURL: URL, + headers: httpHeaders, + withParameters: parameters as? Parameters, + additionalAcceptableStatusCodes: additionalAcceptableStatusCodes, + success: successHandler, + failure: { error in + if self.loggingEnabled { + self.logFailureResponse(forURL: URL, forError: error) + } + failure(error) + }, + apiFailure: { errorResponse in + if self.loggingEnabled { + self.logApiFailureResponse(forURL: URL, forApiError: errorResponse) + } + apiFailure(errorResponse) + } + ) + } + // swiftlint:enable function_parameter_count + + private func logSuccessResponse( + forURL URL: String, + withResponseCode responseCode: Int?, + forResponse response: T + ) { + guard let responseData = try? JSONEncoder().encode(response) else { + print("Success response received, but could not be encoded.") + return + } + + let responseString = String(decoding: responseData, as: UTF8.self) + self.logResponse(forURL: URL, responseCode: responseCode, responseBody: responseString) + } + + private func logFailureResponse(forURL URL: String, forError error: Error) { + self.logResponse( + forURL: URL, + responseCode: error.asAFError?.responseCode, + responseBody: "\(error.localizedDescription)", + isError: true + ) + } + + private func logApiFailureResponse(forURL URL: String, forApiError errorResponse: ApiErrorResponse) { + self.logResponse( + forURL: URL, + responseCode: nil, + responseBody: errorResponse.errors[0].message, + isApiError: true + ) + } + + /** + * Logs all request headers, url and body + */ + private func logRequest(forURL URL: String, requestMethod: HTTPMethod, postBody: Parameters? = nil) { + var requestLog = + """ + Request URL : \(URL) + Request Method : \(requestMethod.rawValue) + Request Headers : \n + """ + + httpHeaders.forEach { header in + requestLog += " \(header) \n" + } + + if requestMethod == .post { + requestLog += "Body: \(postBody?.description ?? "")" + } + + print(requestLog) + } + + /** + * Logs all response headers, status code and body + */ + private func logResponse( + forURL URL: String, + responseCode: Int?, + responseBody: String, + isError: Bool = false, + isApiError: Bool = false + ) { + var responseLog = + """ + Response URL : \(URL) + Response Code : + """ + + if let responseCode { + responseLog += " \(responseCode) \n" + } else { + responseLog += " Nil \n" + } + + responseLog += "Response Headers : \n" + + httpHeaders.forEach { header in + responseLog += " \(header) \n" + } + + if isApiError { + responseLog += "API Error : " + } else if isError { + responseLog += "Response Error : " + } else { + responseLog += "Response Body : " + } + + responseLog += responseBody + + print(responseLog) + } + + private static func fixBaseURL(url: String) -> String { + guard var finalComponents = URLComponents(string: url) else { + fatalError("The provided url: \(url) is malformed.") + } + + var components = finalComponents.path.split(separator: "/").map { String($0)} + let versionComponents = (SDKConstants.kApiVersion as NSString).pathComponents + let error = { + fatalError( + """ + This version of the connectSDK is only compatible with \(versionComponents.joined(separator: "/")), + you supplied: '\(components.joined(separator: "/"))' + """ + ) + } + + switch components.count { + case 0: + components = versionComponents + case 1: + if components[0] != versionComponents[0] { + error() + } + components[0] = components[0] + components.append(versionComponents[1]) + case 2: + if components[0] != versionComponents[0] { + error() + } + if components[1] != versionComponents[1] { + error() + } + default: + error() + } + finalComponents.path = "/" + components.joined(separator: "/") + guard let finalComponentsUrl = finalComponents.url else { + fatalError("Could not return the url of \(finalComponents).") + } + + return finalComponentsUrl.absoluteString + } +} diff --git a/IngenicoConnectKit/ConnectSDK.swift b/IngenicoConnectKit/ConnectSDK.swift new file mode 100644 index 0000000..9cfce0f --- /dev/null +++ b/IngenicoConnectKit/ConnectSDK.swift @@ -0,0 +1,163 @@ +// +// ConnectSDK.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 24/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import Foundation + +public class ConnectSDK { + public static var clientApi: ClientApi { + guard let api = _clientApi else { + fatalError(ConnectSDKError.connectSDKNotInitialized.localizedDescription) + } + + return api + } + private static var _clientApi: ClientApi? + + public static var connectSDKConfiguration: ConnectSDKConfiguration { + guard let sdkConfig = _connectSDKConfiguration else { + fatalError(ConnectSDKError.connectSDKNotInitialized.localizedDescription) + } + + return sdkConfig + } + private static var _connectSDKConfiguration: ConnectSDKConfiguration? + + public static var paymentConfiguration: PaymentConfiguration { + guard let paymentConfig = _paymentConfiguration else { + fatalError(ConnectSDKError.connectSDKNotInitialized.localizedDescription) + } + + return paymentConfig + } + private static var _paymentConfiguration: PaymentConfiguration? + + private static let encryptor = Encryptor() + private static let joseEncryptor = JOSEEncryptor(encryptor: encryptor) + + public static func initialize( + connectSDKConfiguration: ConnectSDKConfiguration, + paymentConfiguration: PaymentConfiguration + ) { + self._clientApi = ClientApi( + sdkConfiguration: connectSDKConfiguration, + paymentConfiguration: paymentConfiguration + ) + self._connectSDKConfiguration = connectSDKConfiguration + self._paymentConfiguration = paymentConfiguration + } + + public static func close() { + self._clientApi = nil + self._connectSDKConfiguration = nil + self._paymentConfiguration = nil + } + + public static func encryptPaymentRequest( + _ paymentRequest: PaymentRequest, + success: @escaping (_ preparedPaymentRequest: PreparedPaymentRequest) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + clientApi.publicKey( + success: { publicKeyResponse in + let publicKeyAsData = publicKeyResponse.encodedPublicKey.decode() + guard let strippedPublicKeyAsData = self.encryptor.stripPublicKey(data: publicKeyAsData) else { + failure(ConnectSDKError.publicKeyDecodeError) + return + } + let tag = "globalcollect-sdk-public-key-swift" + + self.encryptor.deleteRSAKey(withTag: tag) + self.encryptor.storePublicKey(publicKey: strippedPublicKeyAsData, tag: tag) + + guard let publicKey = self.encryptor.RSAKey(withTag: tag) else { + failure(ConnectSDKError.rsaKeyNotFound) + return + } + + let paymentRequestJSON = + self.preparePaymentRequestJSON( + forClientSessionId: connectSDKConfiguration.sessionConfiguration.clientSessionId, + paymentRequest: paymentRequest + ) + let encryptedFields = + self.joseEncryptor.encryptToCompactSerialization( + JSON: paymentRequestJSON, + withPublicKey: publicKey, + keyId: publicKeyResponse.keyId + ) + let encodedClientMetaInfo = clientApi.base64EncodedClientMetaInfo + let preparedRequest = + PreparedPaymentRequest( + encryptedFields: encryptedFields, + encodedClientMetaInfo: encodedClientMetaInfo + ) + success(preparedRequest) + }, + failure: failure, + apiFailure: apiFailure + ) + } + + private static func preparePaymentRequestJSON( + forClientSessionId clientSessionId: String, + paymentRequest: PaymentRequest + ) -> String { + var paymentRequestJSON = String() + + guard let paymentProduct = paymentRequest.paymentProduct else { + NSException( + name: NSExceptionName(rawValue: "Invalid payment product"), + reason: "Payment product is invalid" + ).raise() + // Return is mandatory but will never be reached because of the exception above. + return "Invalid payment product" + } + + let clientSessionId = "{\"clientSessionId\": \"\(clientSessionId)\", " + paymentRequestJSON += clientSessionId + let nonce = "\"nonce\": \"\(self.encryptor.generateUUID())\", " + paymentRequestJSON += nonce + let paymentProductJSON = "\"paymentProductId\": \(paymentProduct.identifier), " + paymentRequestJSON += paymentProductJSON + + if let accountOnFile = paymentRequest.accountOnFile { + paymentRequestJSON += "\"accountOnFileId\": \(accountOnFile.identifier), " + } + if paymentRequest.tokenize { + let tokenize = "\"tokenize\": true, " + paymentRequestJSON += tokenize + } + if let fieldVals = paymentRequest.unmaskedFieldValues, + let values = self.keyValueJSONFromDictionary(dictionary: fieldVals) { + let paymentValues = "\"paymentValues\": \(values)}" + paymentRequestJSON += paymentValues + } + + return paymentRequestJSON + } + + private static func keyValueJSONFromDictionary(dictionary: [String: String]) -> String? { + let keyValuePairs = self.keyValuePairs(from: dictionary) + guard let JSONAsData = try? JSONSerialization.data(withJSONObject: keyValuePairs) else { + Macros.DLog(message: "Unable to create JSON data from dictionary") + return nil + } + + return String(bytes: JSONAsData, encoding: String.Encoding.utf8) + } + + private static func keyValuePairs(from dictionary: [String: String]) -> [[String: String]] { + var keyValuePairs = [[String: String]]() + for (key, value) in dictionary { + let pair = ["key": key, "value": value] + keyValuePairs.append(pair) + } + return keyValuePairs + } +} diff --git a/IngenicoConnectKit/ConnectSDKConfiguration.swift b/IngenicoConnectKit/ConnectSDKConfiguration.swift new file mode 100644 index 0000000..f087ba1 --- /dev/null +++ b/IngenicoConnectKit/ConnectSDKConfiguration.swift @@ -0,0 +1,29 @@ +// +// ConnectSDKConfiguration.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 24/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +public class ConnectSDKConfiguration: Decodable { + public let sessionConfiguration: SessionConfiguration + public let enableNetworkLogs: Bool + public let applicationId: String? + public let ipAddress: String? + public let preLoadImages: Bool + + public init( + sessionConfiguration: SessionConfiguration, + enableNetworkLogs: Bool = false, + applicationId: String? = nil, + ipAddress: String? = nil, + preLoadImages: Bool = true + ) { + self.sessionConfiguration = sessionConfiguration + self.enableNetworkLogs = enableNetworkLogs + self.applicationId = applicationId + self.ipAddress = ipAddress + self.preLoadImages = preLoadImages + } +} diff --git a/IngenicoConnectKit/ConnectSDKError.swift b/IngenicoConnectKit/ConnectSDKError.swift new file mode 100644 index 0000000..9d28ba0 --- /dev/null +++ b/IngenicoConnectKit/ConnectSDKError.swift @@ -0,0 +1,32 @@ +// +// ConnectSDKError.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 27/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import Foundation + +public enum ConnectSDKError: Int, Error { + case connectSDKNotInitialized + case publicKeyDecodeError + case rsaKeyNotFound +} + +extension ConnectSDKError: LocalizedError { + public var errorDescription: String { + switch self { + case .connectSDKNotInitialized: + return + """ + ConnectSDK must be initialized before you can perform this operation. + Initialize it by calling ConnectSDK.initialize() + """ + case .publicKeyDecodeError: + return "Failed to decode Public key." + case .rsaKeyNotFound: + return "Failed to find RSA key." + } + } +} diff --git a/IngenicoConnectKit/Constants/SDKConstants.swift b/IngenicoConnectKit/Constants/SDKConstants.swift index 11d3183..30e8819 100644 --- a/IngenicoConnectKit/Constants/SDKConstants.swift +++ b/IngenicoConnectKit/Constants/SDKConstants.swift @@ -5,7 +5,6 @@ // Created for Ingenico ePayments on 15/12/2016. // Copyright © 2016 Global Collect Services. All rights reserved. // -// swiftlint:disable identifier_name import Foundation import UIKit @@ -25,8 +24,9 @@ public class SDKConstants { public static var kSDKBundlePath = Bundle(identifier: SDKConstants.kSDKBundleIdentifier)?.path(forResource: "IngenicoConnectKit", ofType: "bundle") + // swiftlint:disable identifier_name public static func SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v: String) -> Bool { return UIDevice.current.systemVersion.compare(v, options: String.CompareOptions.numeric) != .orderedAscending } - + // swiftlint:enable identifier_name } diff --git a/IngenicoConnectKit/ConvertedAmount.swift b/IngenicoConnectKit/ConvertedAmount.swift new file mode 100644 index 0000000..1f3a282 --- /dev/null +++ b/IngenicoConnectKit/ConvertedAmount.swift @@ -0,0 +1,13 @@ +// +// ConvertedAmountResponse.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on on 23/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import Foundation + +public class ConvertedAmountResponse: Codable { + public let convertedAmount: Int +} diff --git a/IngenicoConnectKit/Cryptography/Encryptor.swift b/IngenicoConnectKit/Cryptography/Encryptor.swift index 5e45c81..69fe05f 100644 --- a/IngenicoConnectKit/Cryptography/Encryptor.swift +++ b/IngenicoConnectKit/Cryptography/Encryptor.swift @@ -5,12 +5,12 @@ // Created for Ingenico ePayments on 15/12/2016. // Copyright © 2016 Global Collect Services. All rights reserved. // -// swiftlint:disable identifier_name import Foundation import CryptoSwift import Security +@available(*, deprecated, message: "In a future release, this class and its functions will become internal to the SDK.") public class Encryptor { public func generateRSAKeyPair(withPublicTag publicTag: String, privateTag: String) { @@ -230,6 +230,7 @@ public class Encryptor { return Array(publicKey[prefixLength.. (Data?) { let plaintext = convertDataToByteArray(data: data) @@ -276,7 +277,8 @@ public class Encryptor { public func decryptAES(ciphertext: [UInt8], key: String, IV: String) -> ([UInt8]?) { return self.decryptAES(ciphertext: ciphertext, key: key.bytes, IV: IV.bytes) } - func decryptAES(ciphertext: [UInt8], key: [UInt8], IV: [UInt8]) -> ([UInt8]?) { + + public func decryptAES(ciphertext: [UInt8], key: [UInt8], IV: [UInt8]) -> ([UInt8]?) { guard let aes = try? AES(key: key, blockMode: CBC(iv: IV), padding: .pkcs7), let plaintext = try? aes.decrypt(ciphertext) else { return nil @@ -284,6 +286,7 @@ public class Encryptor { return plaintext } + // swiftlint:enable identifier_name public func generateHMAC(data: Data, key: Data) -> (Data?) { let input = convertDataToByteArray(data: data) diff --git a/IngenicoConnectKit/Cryptography/JOSEEncryptor.swift b/IngenicoConnectKit/Cryptography/JOSEEncryptor.swift index b151dc7..f68036a 100644 --- a/IngenicoConnectKit/Cryptography/JOSEEncryptor.swift +++ b/IngenicoConnectKit/Cryptography/JOSEEncryptor.swift @@ -5,10 +5,17 @@ // Created for Ingenico ePayments on 15/12/2016. // Copyright © 2016 Global Collect Services. All rights reserved. // -// swiftlint:disable identifier_name import Foundation +@available( + *, + deprecated, + message: + """ + In a future release, this class, its functions and its properties will become internal to the SDK. + """ +) public class JOSEEncryptor { public var encryptor = Encryptor() @@ -45,7 +52,9 @@ public class JOSEEncryptor { guard let additionalAuthenticatedData = encodedProtectedHeader.data(using: String.Encoding.ascii) else { return "" } + // swiftlint:disable identifier_name let AL = computeAL(forData: additionalAuthenticatedData) + // swiftlint:enable identifier_name guard let ciphertext = encryptor.encryptAES( @@ -94,7 +103,9 @@ public class JOSEEncryptor { guard let additionalAuthenticatedData = components[0].data(using: String.Encoding.ascii) else { return "" } + // swiftlint:disable identifier_name let AL = computeAL(forData: additionalAuthenticatedData) + // swiftlint:enable identifier_name var authenticationData = additionalAuthenticatedData authenticationData.append(initializationVector) @@ -119,8 +130,10 @@ public class JOSEEncryptor { public func computeAL(forData data: Data) -> Data { var lengthInBits = data.count * 8 + // swiftlint:disable identifier_name var AL = Data(bytes: &lengthInBits, count: MemoryLayout.size) AL.reverse() return AL + // swiftlint:enable identifier_name } } diff --git a/IngenicoConnectKit/Enumerations/AccountOnFileAttributeStatus.swift b/IngenicoConnectKit/Enumerations/AccountOnFileAttributeStatus.swift index d2746aa..ed58238 100644 --- a/IngenicoConnectKit/Enumerations/AccountOnFileAttributeStatus.swift +++ b/IngenicoConnectKit/Enumerations/AccountOnFileAttributeStatus.swift @@ -6,8 +6,8 @@ // Copyright © 2016 Global Collect Services. All rights reserved. // -public enum AccountOnFileAttributeStatus { - case readOnly - case canWrite - case mustWrite +public enum AccountOnFileAttributeStatus: String, Codable { + case readOnly = "READ_ONLY" + case canWrite = "CAN_WRITE" + case mustWrite = "MUST_WRITE" } diff --git a/IngenicoConnectKit/Enumerations/CountryCode.swift b/IngenicoConnectKit/Enumerations/CountryCode.swift index 377a180..c58ae13 100644 --- a/IngenicoConnectKit/Enumerations/CountryCode.swift +++ b/IngenicoConnectKit/Enumerations/CountryCode.swift @@ -281,3 +281,4 @@ public enum CountryCode: String { TV, UG, UA, AE, US, UM, UY, UZ, VU, VE, GB, VN, VG, VI, WF, EH, YE, ZM, ZW ] } +// swiftlint:enable identifier_name diff --git a/IngenicoConnectKit/Enumerations/DisplayElementType.swift b/IngenicoConnectKit/Enumerations/DisplayElementType.swift index 7edd0e9..9a53fee 100644 --- a/IngenicoConnectKit/Enumerations/DisplayElementType.swift +++ b/IngenicoConnectKit/Enumerations/DisplayElementType.swift @@ -7,7 +7,7 @@ // import Foundation -public enum DisplayElementType: String { +public enum DisplayElementType: String, Codable { case string = "STRING" case integer = "INTEGER" case currency = "CURRENCY" diff --git a/IngenicoConnectKit/Enumerations/FieldType.swift b/IngenicoConnectKit/Enumerations/FieldType.swift index 847b49d..b648dcc 100644 --- a/IngenicoConnectKit/Enumerations/FieldType.swift +++ b/IngenicoConnectKit/Enumerations/FieldType.swift @@ -6,11 +6,11 @@ // Copyright © 2016 Global Collect Services. All rights reserved. // -public enum FieldType { - case string - case integer - case expirationDate - case numericString - case boolString - case dateString +public enum FieldType: String, Codable { + case string = "string" + case integer = "integer" + case expirationDate = "expirydate" + case numericString = "numericstring" + case boolString = "boolean" + case dateString = "date" } diff --git a/IngenicoConnectKit/Enumerations/FormElementType.swift b/IngenicoConnectKit/Enumerations/FormElementType.swift index 7979e55..9f548f5 100644 --- a/IngenicoConnectKit/Enumerations/FormElementType.swift +++ b/IngenicoConnectKit/Enumerations/FormElementType.swift @@ -6,10 +6,10 @@ // Copyright © 2016 Global Collect Services. All rights reserved. // -public enum FormElementType { - case textType - case listType - case currencyType - case boolType - case dateType +public enum FormElementType: String, Codable { + case textType = "text" + case listType = "list" + case currencyType = "currency" + case boolType = "boolean" + case dateType = "date" } diff --git a/IngenicoConnectKit/Enumerations/IINStatus.swift b/IngenicoConnectKit/Enumerations/IINStatus.swift index f2cbec3..68f4fcd 100644 --- a/IngenicoConnectKit/Enumerations/IINStatus.swift +++ b/IngenicoConnectKit/Enumerations/IINStatus.swift @@ -6,11 +6,11 @@ // Copyright © 2016 Global Collect Services. All rights reserved. // -public enum IINStatus { - case supported - case unsupported - case unknown - case notEnoughDigits - case pending - case existingButNotAllowed +public enum IINStatus: String, Codable { + case supported = "SUPPORTED" + case unsupported = "UNSUPPORTED" + case unknown = "UNKNOWN" + case notEnoughDigits = "NOT_ENOUGH_DIGITS" + case pending = "PENDING" + case existingButNotAllowed = "EXISTING_BUT_NOT_ALLOWED" } diff --git a/IngenicoConnectKit/Enumerations/PreferredInputType.swift b/IngenicoConnectKit/Enumerations/PreferredInputType.swift index 2f5eee5..a2004ec 100644 --- a/IngenicoConnectKit/Enumerations/PreferredInputType.swift +++ b/IngenicoConnectKit/Enumerations/PreferredInputType.swift @@ -6,11 +6,11 @@ // Copyright © 2016 Global Collect Services. All rights reserved. // -public enum PreferredInputType { - case stringKeyboard - case integerKeyboard - case emailAddressKeyboard - case phoneNumberKeyboard - case dateKeyboard - case noKeyboard +public enum PreferredInputType: String, Codable { + case stringKeyboard = "StringKeyboard" + case integerKeyboard = "IntegerKeyboard" + case emailAddressKeyboard = "EmailAddressKeyboard" + case phoneNumberKeyboard = "PhoneNumberKeyboard" + case dateKeyboard = "DateKeyboard" + case noKeyboard = "NoKeyboard" } diff --git a/IngenicoConnectKit/Enumerations/Region.swift b/IngenicoConnectKit/Enumerations/Region.swift index 36a2ee8..64d991a 100644 --- a/IngenicoConnectKit/Enumerations/Region.swift +++ b/IngenicoConnectKit/Enumerations/Region.swift @@ -21,3 +21,4 @@ public enum Region { case AMS case PAR } +// swiftlint:enable identifier_name diff --git a/IngenicoConnectKit/Enumerations/SessionError.swift b/IngenicoConnectKit/Enumerations/SessionError.swift index fc15541..b88577b 100644 --- a/IngenicoConnectKit/Enumerations/SessionError.swift +++ b/IngenicoConnectKit/Enumerations/SessionError.swift @@ -7,6 +7,15 @@ // // swiftlint:disable identifier_name +@available( + *, + deprecated, + message: + """ + In a future release, this enum will be removed. The SDK will throw a ConnectSDKError instead. + """ +) public enum SessionError: Error { case RuntimeError(String) } +// swiftlint:enable identifier_name diff --git a/IngenicoConnectKit/Extensions/String+Extensions.swift b/IngenicoConnectKit/Extensions/String+Extensions.swift index e2c81be..c545098 100644 --- a/IngenicoConnectKit/Extensions/String+Extensions.swift +++ b/IngenicoConnectKit/Extensions/String+Extensions.swift @@ -5,12 +5,12 @@ // Created for Ingenico ePayments on 15/12/2016. // Copyright © 2016 Global Collect Services. All rights reserved. // -// swiftlint:disable identifier_name import Foundation extension String { + // swiftlint:disable identifier_name subscript (i: Int) -> String { return self[i ..< i + 1] } @@ -30,6 +30,7 @@ extension String { let end = index(start, offsetBy: range.upperBound - range.lowerBound) return String(self[start ..< end]) } + // swiftlint:enable identifier_name public func base64URLDecode() -> Data { let underscoreReplaced = self.replacingOccurrences(of: "-", with: "+") diff --git a/IngenicoConnectKit/FileManager.swift b/IngenicoConnectKit/FileManager.swift index f0ebf72..51efa11 100644 --- a/IngenicoConnectKit/FileManager.swift +++ b/IngenicoConnectKit/FileManager.swift @@ -8,6 +8,7 @@ import UIKit +@available(*, deprecated, message: "In a future release, this class and its functions will be removed.") public class FileManager { public func dict(atPath path: String) -> NSDictionary? { return NSDictionary(contentsOfFile: path) diff --git a/IngenicoConnectKit/Formatters/StringFormatter.swift b/IngenicoConnectKit/Formatters/StringFormatter.swift index 5b8cec7..59caee1 100644 --- a/IngenicoConnectKit/Formatters/StringFormatter.swift +++ b/IngenicoConnectKit/Formatters/StringFormatter.swift @@ -5,8 +5,6 @@ // Created for Ingenico ePayments on 15/12/2016. // Copyright © 2016 Global Collect Services. All rights reserved. // -// swiftlint:disable function_parameter_count -// swiftlint:disable cyclomatic_complexity import Foundation @@ -82,6 +80,8 @@ public class StringFormatter { return result } + // swiftlint:disable function_parameter_count + // swiftlint:disable cyclomatic_complexity public func processMatch( match: String, string: String, @@ -154,6 +154,8 @@ public class StringFormatter { return result } + // swiftlint:enable function_parameter_count + // swiftlint:enable cyclomatic_complexity func parts(ofMask mask: String) -> [String] { guard let regex = try? NSRegularExpression(pattern: "\\{\\{|\\}\\}|([^\\{\\}]|\\{(?!\\{)|\\}(?!\\}))*") else { diff --git a/IngenicoConnectKit/Models/DirectoryEntries/DirectoryEntries.swift b/IngenicoConnectKit/Models/DirectoryEntries/DirectoryEntries.swift index c504d50..5920310 100644 --- a/IngenicoConnectKit/Models/DirectoryEntries/DirectoryEntries.swift +++ b/IngenicoConnectKit/Models/DirectoryEntries/DirectoryEntries.swift @@ -8,13 +8,13 @@ import Foundation -public class DirectoryEntries: ResponseObjectSerializable { +public class DirectoryEntries: ResponseObjectSerializable, Codable { public var directoryEntries: [DirectoryEntry] = [] - public init() { - - } + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") + public init() {} + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init(json: [String: Any]) { if let entries = json["entries"] as? [[String: Any]] { for inputEntry in entries { @@ -24,4 +24,19 @@ public class DirectoryEntries: ResponseObjectSerializable { } } } + + private enum CodingKeys: String, CodingKey { + case entries + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.directoryEntries = (try? container.decodeIfPresent([DirectoryEntry].self, forKey: .entries)) ?? [] + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(directoryEntries, forKey: .entries) + } } diff --git a/IngenicoConnectKit/Models/DirectoryEntries/DirectoryEntry.swift b/IngenicoConnectKit/Models/DirectoryEntries/DirectoryEntry.swift index aea68c9..8cc8d18 100644 --- a/IngenicoConnectKit/Models/DirectoryEntries/DirectoryEntry.swift +++ b/IngenicoConnectKit/Models/DirectoryEntries/DirectoryEntry.swift @@ -8,12 +8,13 @@ import Foundation -public class DirectoryEntry: ResponseObjectSerializable { +public class DirectoryEntry: ResponseObjectSerializable, Codable { public var issuerIdentifier: String public var issuerList: String public var issuerName: String public var countryNames: [String] = [] + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { if let input = json["issuerId"] as? String { issuerIdentifier = input @@ -37,4 +38,25 @@ public class DirectoryEntry: ResponseObjectSerializable { } } } + + private enum CodingKeys: String, CodingKey { + case issuerId, issuerList, issuerName, countryNames + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.issuerIdentifier = try container.decode(String.self, forKey: .issuerId) + self.issuerList = try container.decode(String.self, forKey: .issuerList) + self.issuerName = try container.decode(String.self, forKey: .issuerName) + self.countryNames = (try? container.decodeIfPresent([String].self, forKey: .countryNames)) ?? [] + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(issuerIdentifier, forKey: .issuerId) + try? container.encode(issuerList, forKey: .issuerList) + try? container.encode(issuerName, forKey: .issuerName) + try? container.encode(countryNames, forKey: .countryNames) + } } diff --git a/IngenicoConnectKit/Models/G2SPaymentProductContext/PaymentAmountOfMoney.swift b/IngenicoConnectKit/Models/G2SPaymentProductContext/PaymentAmountOfMoney.swift index bd1ceec..3e6d0e8 100644 --- a/IngenicoConnectKit/Models/G2SPaymentProductContext/PaymentAmountOfMoney.swift +++ b/IngenicoConnectKit/Models/G2SPaymentProductContext/PaymentAmountOfMoney.swift @@ -8,7 +8,7 @@ import Foundation -public class PaymentAmountOfMoney { +public class PaymentAmountOfMoney: Decodable { public var totalAmount = 0 @available(*, deprecated, message: "In the next major release, the type of currencyCode will change to String.") public var currencyCode: CurrencyCode @@ -25,6 +25,24 @@ public class PaymentAmountOfMoney { self.currencyCodeString = currencyCode } + enum CodingKeys: CodingKey { + case amount, currencyCode + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.totalAmount = try container.decode(Int.self, forKey: .amount) + + if let currencyCodeString = try? container.decodeIfPresent(String.self, forKey: .currencyCode) { + self.currencyCodeString = currencyCodeString + self.currencyCode = CurrencyCode.init(rawValue: currencyCodeString) ?? .USD + } else { + self.currencyCodeString = "USD" + self.currencyCode = .USD + } + } + public var description: String { return "\(totalAmount)-\(currencyCodeString)" } diff --git a/IngenicoConnectKit/Models/IINDetails/IINDetail.swift b/IngenicoConnectKit/Models/IINDetails/IINDetail.swift index 61e29e5..79d54ff 100644 --- a/IngenicoConnectKit/Models/IINDetails/IINDetail.swift +++ b/IngenicoConnectKit/Models/IINDetails/IINDetail.swift @@ -8,10 +8,11 @@ import Foundation -public class IINDetail: ResponseObjectSerializable { +public class IINDetail: ResponseObjectSerializable, Codable { public var paymentProductId: String public var allowedInContext: Bool = false + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init?(json: [String: Any]) { if let input = json["paymentProductId"] as? Int { paymentProductId = "\(input)" @@ -23,8 +24,28 @@ public class IINDetail: ResponseObjectSerializable { } } + @available(*, deprecated, message: "In a future release, this intializer will become internal to the SDK.") public init(paymentProductId: String, allowedInContext: Bool) { self.paymentProductId = paymentProductId self.allowedInContext = allowedInContext } + + private enum CodingKeys: String, CodingKey { + case paymentProductId, isAllowedInContext + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let paymentProductIdInt = try container.decode(Int.self, forKey: .paymentProductId) + self.paymentProductId = "\(paymentProductIdInt)" + if let allowedInContext = try? container.decodeIfPresent(Bool.self, forKey: .isAllowedInContext) { + self.allowedInContext = allowedInContext + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(paymentProductId, forKey: .paymentProductId) + try? container.encode(allowedInContext, forKey: .isAllowedInContext) + } } diff --git a/IngenicoConnectKit/Models/IINDetails/IINDetailsResponse.swift b/IngenicoConnectKit/Models/IINDetails/IINDetailsResponse.swift index bef1ee7..bef454d 100644 --- a/IngenicoConnectKit/Models/IINDetails/IINDetailsResponse.swift +++ b/IngenicoConnectKit/Models/IINDetails/IINDetailsResponse.swift @@ -8,7 +8,7 @@ import Foundation -public class IINDetailsResponse: ResponseObjectSerializable { +public class IINDetailsResponse: ResponseObjectSerializable, Codable { public var paymentProductId: String? public var status: IINStatus = .supported @@ -21,6 +21,7 @@ public class IINDetailsResponse: ResponseObjectSerializable { private init() { } + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init(json: [String: Any]) { if let input = json["isAllowedInContext"] as? Bool { allowedInContext = input @@ -28,11 +29,13 @@ public class IINDetailsResponse: ResponseObjectSerializable { if let input = json["paymentProductId"] as? Int { paymentProductId = "\(input)" - } else if !allowedInContext { - status = .existingButNotAllowed + if !allowedInContext { + status = .existingButNotAllowed + } } else { status = .unknown } + if let input = json["countryCode"] as? String { countryCode = CountryCode(rawValue: input) countryCodeString = input @@ -48,12 +51,52 @@ public class IINDetailsResponse: ResponseObjectSerializable { } } + private enum CodingKeys: String, CodingKey { + case paymentProductId, coBrands, countryCode, isAllowedInContext, status + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let allowedInContext = try? container.decodeIfPresent(Bool.self, forKey: .isAllowedInContext) { + self.allowedInContext = allowedInContext + } + + if let paymentProductId = try? container.decodeIfPresent(Int.self, forKey: .paymentProductId) { + self.paymentProductId = "\(paymentProductId)" + if !allowedInContext { + status = .existingButNotAllowed + } + } else { + status = .unknown + } + + if let countryCodeString = try? container.decodeIfPresent(String.self, forKey: .countryCode) { + self.countryCodeString = countryCodeString + self.countryCode = CountryCode(rawValue: countryCodeString) + } + + if let coBrands = try? container.decodeIfPresent([IINDetail].self, forKey: .coBrands) { + self.coBrands = coBrands + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encodeIfPresent(paymentProductId, forKey: .paymentProductId) + try? container.encode(status, forKey: .status) + try? container.encode(coBrands, forKey: .coBrands) + try? container.encodeIfPresent(countryCodeString, forKey: .countryCode) + try? container.encode(allowedInContext, forKey: .isAllowedInContext) + } + + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") convenience public init(status: IINStatus) { self.init() self.status = status } - @available(*, deprecated, message: "Use init(String, IINStatus, [IINDEtail], String) instead") + @available(*, deprecated, message: "Use init(String, IINStatus, [IINDetail], String) instead") convenience public init( paymentProductId: String, status: IINStatus, @@ -70,6 +113,7 @@ public class IINDetailsResponse: ResponseObjectSerializable { ) } + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") convenience public init( paymentProductId: String, status: IINStatus, diff --git a/IngenicoConnectKit/Models/PaymentProducts/AccountOnFile.swift b/IngenicoConnectKit/Models/PaymentProducts/AccountOnFile.swift index 0010f39..621adec 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/AccountOnFile.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/AccountOnFile.swift @@ -8,7 +8,7 @@ import Foundation -public class AccountOnFile: ResponseObjectSerializable { +public class AccountOnFile: ResponseObjectSerializable, Codable { public var identifier: String public var paymentProductIdentifier: String @@ -16,6 +16,7 @@ public class AccountOnFile: ResponseObjectSerializable { public var attributes = AccountOnFileAttributes() public var stringFormatter = StringFormatter() + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { guard let identifier = json["id"] as? Int, @@ -43,6 +44,45 @@ public class AccountOnFile: ResponseObjectSerializable { } } + private enum CodingKeys: String, CodingKey { + case id, paymentProductId, displayHints, attributes + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let idInt = try? container.decode(Int.self, forKey: .id) { + self.identifier = "\(idInt)" + } else { + self.identifier = try container.decode(String.self, forKey: .id) + } + + if let paymentProductIdInt = try? container.decode(Int.self, forKey: .paymentProductId) { + self.paymentProductIdentifier = "\(paymentProductIdInt)" + } else { + self.paymentProductIdentifier = try container.decode(String.self, forKey: .paymentProductId) + } + + if let displayHints = try? container.decodeIfPresent(AccountOnFileDisplayHints.self, forKey: .displayHints) { + self.displayHints = displayHints + } + + if let accountOnFileAttributes = + try? container.decodeIfPresent([AccountOnFileAttribute].self, forKey: .attributes) { + for accountOnFileAttribute in accountOnFileAttributes { + self.attributes.attributes.append(accountOnFileAttribute) + } + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(identifier, forKey: .id) + try? container.encode(paymentProductIdentifier, forKey: .paymentProductId) + try? container.encode(displayHints, forKey: .displayHints) + try? container.encode(attributes.attributes, forKey: .attributes) + } + public func maskedValue(forField paymentProductFieldId: String) -> String { let mask = displayHints.labelTemplate.mask(forAttributeKey: paymentProductFieldId) return maskedValue(forField: paymentProductFieldId, mask: mask) diff --git a/IngenicoConnectKit/Models/PaymentProducts/AccountOnFileAttribute.swift b/IngenicoConnectKit/Models/PaymentProducts/AccountOnFileAttribute.swift index 73a52ee..d02c55e 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/AccountOnFileAttribute.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/AccountOnFileAttribute.swift @@ -8,13 +8,14 @@ import Foundation -public class AccountOnFileAttribute { +public class AccountOnFileAttribute: Codable { public var key: String public var value: String? public var status: AccountOnFileAttributeStatus public var mustWriteReason: String? + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init?(json: [String: Any]) { guard let key = json["key"] as? String else { return nil @@ -35,5 +36,4 @@ public class AccountOnFileAttribute { return nil } } - } diff --git a/IngenicoConnectKit/Models/PaymentProducts/AccountOnFileDisplayHints.swift b/IngenicoConnectKit/Models/PaymentProducts/AccountOnFileDisplayHints.swift index d008616..bf59385 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/AccountOnFileDisplayHints.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/AccountOnFileDisplayHints.swift @@ -8,9 +8,33 @@ import Foundation -public class AccountOnFileDisplayHints { +public class AccountOnFileDisplayHints: Codable { public var labelTemplate: LabelTemplate = LabelTemplate() public var logo: String? + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") + public init() {} + + private enum CodingKeys: String, CodingKey { + case labelTemplate, logo + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let labelTemplates = try? container.decodeIfPresent([LabelTemplateItem].self, forKey: .labelTemplate) { + for labelTemplate in labelTemplates { + self.labelTemplate.labelTemplateItems.append(labelTemplate) + } + } + + self.logo = try? container.decodeIfPresent(String.self, forKey: .logo) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(labelTemplate.labelTemplateItems, forKey: .labelTemplate) + try? container.encode(logo, forKey: .logo) + } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProduct.swift b/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProduct.swift index d94a730..78e2176 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProduct.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProduct.swift @@ -8,7 +8,7 @@ import Foundation -public class BasicPaymentProduct: Equatable, BasicPaymentItem, ResponseObjectSerializable { +public class BasicPaymentProduct: Equatable, BasicPaymentItem, ResponseObjectSerializable, Codable { public var identifier: String public var displayHints: PaymentItemDisplayHints @@ -48,6 +48,7 @@ public class BasicPaymentProduct: Equatable, BasicPaymentItem, ResponseObjectSer } } + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { guard let identifier = json["id"] as? Int, let paymentMethod = json["paymentMethod"] as? String, @@ -100,7 +101,80 @@ public class BasicPaymentProduct: Equatable, BasicPaymentItem, ResponseObjectSer } } } + } + private enum CodingKeys: String, CodingKey { + case id, displayHints, accountsOnFile, acquirerCountry, allowsTokenization, allowsRecurring, autoTokenized, + allowsInstallments, authenticationIndicator, deviceFingerprintEnabled, minAmount, maxAmount, paymentMethod, + mobileIntegrationLevel, usesRedirectionTo3rdParty, paymentProductGroup, supportsMandates, + paymentProduct302SpecificData, paymentProduct320SpecificData, paymentProduct863SpecificData + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let idInt = try? container.decode(Int.self, forKey: .id) { + self.identifier = "\(idInt)" + } else { + self.identifier = try container.decode(String.self, forKey: .id) + } + self.paymentMethod = try container.decode(String.self, forKey: .paymentMethod) + self.displayHints = try container.decode(PaymentItemDisplayHints.self, forKey: .displayHints) + + self.paymentProduct302SpecificData = + try? container.decodeIfPresent(PaymentProduct302SpecificData.self, forKey: .paymentProduct302SpecificData) + self.paymentProduct320SpecificData = + try? container.decodeIfPresent(PaymentProduct320SpecificData.self, forKey: .paymentProduct320SpecificData) + self.paymentProduct863SpecificData = + try? container.decodeIfPresent(PaymentProduct863SpecificData.self, forKey: .paymentProduct863SpecificData) + + self.acquirerCountry = try? container.decodeIfPresent(String.self, forKey: .acquirerCountry) + self.allowsTokenization = (try? container.decodeIfPresent(Bool.self, forKey: .allowsTokenization)) ?? false + self.allowsRecurring = (try? container.decodeIfPresent(Bool.self, forKey: .allowsRecurring)) ?? false + self.autoTokenized = (try? container.decodeIfPresent(Bool.self, forKey: .autoTokenized)) ?? false + self.allowsInstallments = (try? container.decodeIfPresent(Bool.self, forKey: .allowsInstallments)) ?? false + self.authenticationIndicator = + try? container.decodeIfPresent(AuthenticationIndicator.self, forKey: .authenticationIndicator) + self.deviceFingerprintEnabled = + (try? container.decodeIfPresent(Bool.self, forKey: .deviceFingerprintEnabled)) ?? false + + self.minAmount = try? container.decodeIfPresent(Int.self, forKey: .minAmount) + self.maxAmount = try? container.decodeIfPresent(Int.self, forKey: .maxAmount) + + self.mobileIntegrationLevel = try? container.decodeIfPresent(String.self, forKey: .mobileIntegrationLevel) + self.usesRedirectionTo3rdParty = + (try? container.decodeIfPresent(Bool.self, forKey: .usesRedirectionTo3rdParty)) ?? false + self.paymentProductGroup = try? container.decodeIfPresent(String.self, forKey: .paymentProductGroup) + self.supportsMandates = (try? container.decodeIfPresent(Bool.self, forKey: .supportsMandates)) ?? false + + if let accountsOnFile = try? container.decodeIfPresent([AccountOnFile].self, forKey: .accountsOnFile) { + for accountOnFile in accountsOnFile { + self.accountsOnFile.accountsOnFile.append(accountOnFile) + } + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(identifier, forKey: .id) + try? container.encode(paymentMethod, forKey: .paymentMethod) + try? container.encode(displayHints, forKey: .displayHints) + try? container.encodeIfPresent(paymentProduct302SpecificData, forKey: .paymentProduct302SpecificData) + try? container.encodeIfPresent(paymentProduct320SpecificData, forKey: .paymentProduct320SpecificData) + try? container.encodeIfPresent(paymentProduct863SpecificData, forKey: .paymentProduct863SpecificData) + try? container.encodeIfPresent(acquirerCountry, forKey: .acquirerCountry) + try? container.encode(allowsTokenization, forKey: .allowsTokenization) + try? container.encode(allowsRecurring, forKey: .allowsRecurring) + try? container.encode(autoTokenized, forKey: .autoTokenized) + try? container.encode(allowsInstallments, forKey: .allowsInstallments) + try? container.encodeIfPresent(authenticationIndicator, forKey: .authenticationIndicator) + try? container.encode(deviceFingerprintEnabled, forKey: .deviceFingerprintEnabled) + try? container.encodeIfPresent(minAmount, forKey: .minAmount) + try? container.encodeIfPresent(maxAmount, forKey: .maxAmount) + try? container.encodeIfPresent(mobileIntegrationLevel, forKey: .mobileIntegrationLevel) + try? container.encode(usesRedirectionTo3rdParty, forKey: .usesRedirectionTo3rdParty) + try? container.encodeIfPresent(paymentProductGroup, forKey: .paymentProductGroup) + try? container.encode(supportsMandates, forKey: .supportsMandates) + try? container.encode(accountsOnFile.accountsOnFile, forKey: .accountsOnFile) } public func accountOnFile(withIdentifier identifier: String) -> AccountOnFile? { @@ -110,5 +184,4 @@ public class BasicPaymentProduct: Equatable, BasicPaymentItem, ResponseObjectSer public static func == (lhs: BasicPaymentProduct, rhs: BasicPaymentProduct) -> Bool { return lhs.identifier == rhs.identifier } - } diff --git a/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProductGroup.swift b/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProductGroup.swift index a46cbe3..9f80d25 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProductGroup.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProductGroup.swift @@ -8,12 +8,19 @@ import Foundation -public class BasicPaymentProductGroup: ResponseObjectSerializable, BasicPaymentItem { +public class BasicPaymentProductGroup: ResponseObjectSerializable, BasicPaymentItem, Codable { public var identifier: String public var displayHints: PaymentItemDisplayHints public var accountsOnFile = AccountsOnFile() + @available( + *, + deprecated, + message: "In a future release, this property will be removed since it is not returned from the API." + ) public var acquirerCountry: String? + public var deviceFingerprintEnabled = false + public var allowsInstallments = false public var stringFormatter: StringFormatter? { get { return accountsOnFile.accountsOnFile.first?.stringFormatter } @@ -26,6 +33,7 @@ public class BasicPaymentProductGroup: ResponseObjectSerializable, BasicPaymentI } } + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { guard let identifier = json["id"] as? String, let hints = json["displayHints"] as? [String: Any], @@ -34,7 +42,8 @@ public class BasicPaymentProductGroup: ResponseObjectSerializable, BasicPaymentI } self.identifier = identifier self.displayHints = displayHints - self.acquirerCountry = json["acquirerCountry"] as? String ?? "" + self.deviceFingerprintEnabled = json["deviceFingerprintEnabled"] as? Bool ?? false + self.allowsInstallments = json["allowsInstallments"] as? Bool ?? false if let input = json["accountsOnFile"] as? [[String: Any]] { for accountInput in input { @@ -45,6 +54,34 @@ public class BasicPaymentProductGroup: ResponseObjectSerializable, BasicPaymentI } } + private enum CodingKeys: String, CodingKey { + case id, displayHints, deviceFingerprintEnabled, allowsInstallments, accountsOnFile + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.identifier = try container.decode(String.self, forKey: .id) + self.displayHints = try container.decode(PaymentItemDisplayHints.self, forKey: .displayHints) + self.deviceFingerprintEnabled = + (try container.decodeIfPresent(Bool.self, forKey: .deviceFingerprintEnabled)) ?? false + self.allowsInstallments = (try container.decodeIfPresent(Bool.self, forKey: .allowsInstallments)) ?? false + + if let accountsOnFile = try? container.decodeIfPresent([AccountOnFile].self, forKey: .accountsOnFile) { + for accountOnFile in accountsOnFile { + self.accountsOnFile.accountsOnFile.append(accountOnFile) + } + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(identifier, forKey: .id) + try? container.encode(displayHints, forKey: .displayHints) + try? container.encode(deviceFingerprintEnabled, forKey: .deviceFingerprintEnabled) + try? container.encode(allowsInstallments, forKey: .allowsInstallments) + try? container.encode(accountsOnFile.accountsOnFile, forKey: .accountsOnFile) + } + public func accountOnFile(withIdentifier identifier: String) -> AccountOnFile? { return accountsOnFile.accountOnFile(withIdentifier: identifier) } diff --git a/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProductGroups.swift b/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProductGroups.swift index 9facbaf..eea79d2 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProductGroups.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProductGroups.swift @@ -8,7 +8,7 @@ import Foundation -public class BasicPaymentProductGroups: ResponseObjectSerializable { +public class BasicPaymentProductGroups: ResponseObjectSerializable, Codable { public var paymentProductGroups = [BasicPaymentProductGroup]() @@ -41,9 +41,10 @@ public class BasicPaymentProductGroups: ResponseObjectSerializable { } } - public init() { - } + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") + public init() {} + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init(json: [String: Any]) { if let input = json["paymentProductGroups"] as? [[String: Any]] { for groupInput in input { @@ -56,6 +57,17 @@ public class BasicPaymentProductGroups: ResponseObjectSerializable { } } + enum CodingKeys: CodingKey { + case paymentProductGroups + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.paymentProductGroups = ( + try? container.decodeIfPresent([BasicPaymentProductGroup].self, forKey: .paymentProductGroups)) ?? + [BasicPaymentProductGroup]() + } + public func logoPath(forProductGroup identifier: String) -> String? { let productGroup = paymentProductGroup(withIdentifier: identifier) return productGroup?.displayHints.logoPath diff --git a/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProducts.swift b/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProducts.swift index 4cde963..833dafe 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProducts.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/BasicPaymentProducts.swift @@ -8,7 +8,7 @@ import Foundation -public class BasicPaymentProducts: Equatable, ResponseObjectSerializable { +public class BasicPaymentProducts: Equatable, ResponseObjectSerializable, Codable { public var paymentProducts = [BasicPaymentProduct]() public var stringFormatter: StringFormatter? { get { return paymentProducts.first?.stringFormatter } @@ -20,6 +20,7 @@ public class BasicPaymentProducts: Equatable, ResponseObjectSerializable { } } } + public var hasAccountsOnFile: Bool { for product in paymentProducts where product.accountsOnFile.accountsOnFile.count > 0 { @@ -39,9 +40,10 @@ public class BasicPaymentProducts: Equatable, ResponseObjectSerializable { return accountsOnFile } - public init() { - } + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") + public init() {} + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init(json: [String: Any]) { guard let paymentProductsInput = json["paymentProducts"] as? [[String: Any]] else { return @@ -54,6 +56,17 @@ public class BasicPaymentProducts: Equatable, ResponseObjectSerializable { } } + private enum CodingKeys: String, CodingKey { + case paymentProducts + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.paymentProducts = + (try? container.decodeIfPresent([BasicPaymentProduct].self, forKey: .paymentProducts)) ?? + [BasicPaymentProduct]() + } + public func logoPath(forPaymentProduct identifier: String) -> String? { let product = paymentProduct(withIdentifier: identifier) return product?.displayHints.logoPath diff --git a/IngenicoConnectKit/Models/PaymentProducts/CustomerDetails.swift b/IngenicoConnectKit/Models/PaymentProducts/CustomerDetails.swift deleted file mode 100644 index 8396d63..0000000 --- a/IngenicoConnectKit/Models/PaymentProducts/CustomerDetails.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CustomerDetails.swift -// Pods -// -// Created for Ingenico ePayments on 12/07/2017. -// -// - -import Foundation -public class CustomerDetails: ResponseObjectSerializable { - public func value(key: String) -> String { - return self.dict[key]! - } - public var values: [String: String] { - return self.dict - } - private var dict: [String: String] = [:] - public required init?(json: [String: Any]) { - if let dict = json as? [String: String] { - self.dict = dict - } - } - -} diff --git a/IngenicoConnectKit/Models/PaymentProducts/CustomerDetailsError.swift b/IngenicoConnectKit/Models/PaymentProducts/CustomerDetailsError.swift deleted file mode 100644 index 24b67ae..0000000 --- a/IngenicoConnectKit/Models/PaymentProducts/CustomerDetailsError.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CustomerDetailsError.swift -// Pods -// -// Created for Ingenico ePayments on 15/12/2016. -// Copyright © 2016 Global Collect Services. All rights reserved. -// -// - -import Foundation - -public class CustomerDetailsError: Error { - public let responseValues: [[String: Any]] - init(responseValues: [[String: Any]]) { - self.responseValues = responseValues - } -} diff --git a/IngenicoConnectKit/Models/PaymentProducts/DataRestrictions.swift b/IngenicoConnectKit/Models/PaymentProducts/DataRestrictions.swift index 92086df..9d9dc37 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/DataRestrictions.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/DataRestrictions.swift @@ -8,14 +8,15 @@ import Foundation -public class DataRestrictions: ResponseObjectSerializable { +public class DataRestrictions: ResponseObjectSerializable, Codable { public var isRequired = false public var validators = Validators() - public init() { - } + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") + public init() {} + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init(json: [String: Any]) { if let input = json["isRequired"] as? Bool { isRequired = input @@ -25,6 +26,34 @@ public class DataRestrictions: ResponseObjectSerializable { } } + private enum CodingKeys: String, CodingKey { + case isRequired, validators, validationRules + } + + private enum ValidationTypeKey: CodingKey { + case validationType + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let isRequired = try? container.decodeIfPresent(Bool.self, forKey: .isRequired) { + self.isRequired = isRequired + } + if let validators = try? container.decodeIfPresent(Validators.self, forKey: .validators) { + self.validators = validators + } else if var validatorsContainer = try? + container.nestedUnkeyedContainer(forKey: .validationRules) { + setValidators(validatorsContainer: &validatorsContainer) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(isRequired, forKey: .isRequired) + try? container.encode(validators.validators, forKey: .validationRules) + } + + // swiftlint:disable cyclomatic_complexity private func setValidators(input: [String: Any]) { if input.index(forKey: "luhn") != nil { let validator = ValidatorLuhn() @@ -72,4 +101,55 @@ public class DataRestrictions: ResponseObjectSerializable { validators.validators.append(validator) } } + // swiftlint:enable cyclomatic_complexity + + private func setValidators(validatorsContainer: inout UnkeyedDecodingContainer) { + var validatorsArray = validatorsContainer + while !validatorsContainer.isAtEnd { + guard let validationRule = try? validatorsContainer.nestedContainer(keyedBy: ValidationTypeKey.self), + let type = try? validationRule.decodeIfPresent(ValidationType.self, forKey: .validationType) else { + return + } + let validatorType = getValidatorType(type: type) + addValidator(validatorType: validatorType, validatorsArray: &validatorsArray) + } + } + + private func addValidator(validatorType: T.Type, validatorsArray: inout UnkeyedDecodingContainer) { + guard let validator = try? validatorsArray.decode(validatorType.self) else { + return + } + self.validators.validators.append(validator) + } + + // swiftlint:disable cyclomatic_complexity + private func getValidatorType(type: ValidationType) -> Validator.Type { + switch type { + case .expirationDate: + return ValidatorExpirationDate.self + case .emailAddress: + return ValidatorEmailAddress.self + case .fixedList: + return ValidatorFixedList.self + case .iban: + return ValidatorIBAN.self + case .length: + return ValidatorLength.self + case .luhn: + return ValidatorLuhn.self + case .range: + return ValidatorRange.self + case .regularExpression: + return ValidatorRegularExpression.self + case .required, .type: + return Validator.self + case .boletoBancarioRequiredness: + return ValidatorBoletoBancarioRequiredness.self + case .termsAndConditions: + return ValidatorTermsAndConditions.self + case .residentIdNumber: + return ValidatorResidentIdNumber.self + } + } + // swiftlint:enable cyclomatic_complexity } diff --git a/IngenicoConnectKit/Models/PaymentProducts/DisplayElement.swift b/IngenicoConnectKit/Models/PaymentProducts/DisplayElement.swift index aa98e43..dbdbb75 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/DisplayElement.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/DisplayElement.swift @@ -8,7 +8,12 @@ import Foundation -public class DisplayElement: ResponseObjectSerializable { +public class DisplayElement: ResponseObjectSerializable, Codable { + public var id: String + public var type: DisplayElementType + public var value: String + + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { guard let id = json["id"] as? String else { return nil @@ -24,9 +29,7 @@ public class DisplayElement: ResponseObjectSerializable { self.type = type } - public var id: String - public var type: DisplayElementType - public var value: String + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") init(id: String, type: DisplayElementType, value: String) { self.id = id self.type = type diff --git a/IngenicoConnectKit/Models/PaymentProducts/FormElement.swift b/IngenicoConnectKit/Models/PaymentProducts/FormElement.swift index bd02bb9..7d24db1 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/FormElement.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/FormElement.swift @@ -8,10 +8,11 @@ import Foundation -public class FormElement: ResponseObjectSerializable { +public class FormElement: ResponseObjectSerializable, Codable { public var type: FormElementType public var valueMapping = [ValueMappingItem]() + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init?(json: [String: Any]) { switch json["type"] as? String { case "text"?: @@ -36,4 +37,22 @@ public class FormElement: ResponseObjectSerializable { } } } + + private enum CodingKeys: String, CodingKey { + case type, valueMapping + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.type = try container.decode(FormElementType.self, forKey: .type) + + self.valueMapping = + (try? container.decodeIfPresent([ValueMappingItem].self, forKey: .valueMapping)) ?? [ValueMappingItem]() + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(type.rawValue, forKey: .type) + try? container.encode(valueMapping, forKey: .valueMapping) + } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/LabelTemplateItem.swift b/IngenicoConnectKit/Models/PaymentProducts/LabelTemplateItem.swift index 252ab60..0023bdc 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/LabelTemplateItem.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/LabelTemplateItem.swift @@ -8,11 +8,12 @@ import Foundation -public class LabelTemplateItem: ResponseObjectSerializable { +public class LabelTemplateItem: ResponseObjectSerializable, Codable { public var attributeKey: String public var mask: String? + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init?(json: [String: Any]) { guard let attributeKey = json["attributeKey"] as? String else { return nil @@ -21,5 +22,4 @@ public class LabelTemplateItem: ResponseObjectSerializable { mask = json["mask"] as? String } - } diff --git a/IngenicoConnectKit/Models/PaymentProducts/PaymentItemDisplayHints.swift b/IngenicoConnectKit/Models/PaymentProducts/PaymentItemDisplayHints.swift index 89555ff..339f0f6 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/PaymentItemDisplayHints.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/PaymentItemDisplayHints.swift @@ -9,13 +9,14 @@ import Foundation import UIKit -public class PaymentItemDisplayHints { +public class PaymentItemDisplayHints: Codable { public var displayOrder: Int? public var label: String? public var logoPath: String public var logoImage: UIImage? + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init?(json: [String: Any]) { if let input = json["label"] as? String { label = input @@ -28,4 +29,21 @@ public class PaymentItemDisplayHints { displayOrder = json["displayOrder"] as? Int } + private enum CodingKeys: String, CodingKey { + case displayOrder, label, logo + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.label = try? container.decodeIfPresent(String.self, forKey: .label) + self.logoPath = try container.decode(String.self, forKey: .logo) + self.displayOrder = try? container.decodeIfPresent(Int.self, forKey: .displayOrder) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encodeIfPresent(label, forKey: .label) + try? container.encode(logoPath, forKey: .logo) + try? container.encodeIfPresent(displayOrder, forKey: .displayOrder) + } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/PaymentItems.swift b/IngenicoConnectKit/Models/PaymentProducts/PaymentItems.swift index d08015c..8901747 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/PaymentItems.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/PaymentItems.swift @@ -8,7 +8,7 @@ import Foundation -public class PaymentItems { +public class PaymentItems: Encodable { public var paymentItems = [BasicPaymentItem]() public var stringFormatter: StringFormatter? @@ -31,6 +31,7 @@ public class PaymentItems { return accountsOnFile } + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") public init(products: BasicPaymentProducts, groups: BasicPaymentProductGroups?) { paymentItems = createPaymentItemsFromProducts(products: products, groups: groups) @@ -42,6 +43,18 @@ public class PaymentItems { } } + private enum CodingKeys: String, CodingKey { + case basicPaymentItems, accountsOnFile + } + + public func encode(to encoder: Encoder) throws { + let wrappedBasicPaymentItems = paymentItems.map { BasicPaymentItemWrapper(basicPaymentItem: $0)} + + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(wrappedBasicPaymentItems, forKey: .basicPaymentItems) + try? container.encode(accountsOnFile, forKey: .accountsOnFile) + } + public func createPaymentItemsFromProducts( products: BasicPaymentProducts, groups: BasicPaymentProductGroups? @@ -98,3 +111,16 @@ public class PaymentItems { } } } + +private struct BasicPaymentItemWrapper: Encodable { + let basicPaymentItem: BasicPaymentItem + + private enum CodingKeys: String, CodingKey { + case basicPaymentItem + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(basicPaymentItem, forKey: .basicPaymentItem) + } +} diff --git a/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct.swift b/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct.swift index 243bc0e..4fc1dea 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct.swift @@ -13,6 +13,7 @@ public class PaymentProduct: BasicPaymentProduct, PaymentItem { public var fields: PaymentProductFields = PaymentProductFields() public var fieldsWarning: String? + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { super.init(json: json) @@ -29,6 +30,30 @@ public class PaymentProduct: BasicPaymentProduct, PaymentItem { fieldsWarning = json["fieldsWarning"] as? String } + private enum CodingKeys: String, CodingKey { + case fields, fieldsWarning + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let fieldsInput = try? container.decodeIfPresent([PaymentProductField].self, forKey: .fields) { + for field in fieldsInput { + self.fields.paymentProductFields.append(field) + } + } + self.fieldsWarning = try? container.decodeIfPresent(String.self, forKey: .fieldsWarning) + + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(fields.paymentProductFields, forKey: .fields) + try? container.encodeIfPresent(fieldsWarning, forKey: .fieldsWarning) + + try super.encode(to: encoder) + } + public func paymentProductField(withId: String) -> PaymentProductField? { for field in fields.paymentProductFields where field.identifier.isEqual(withId) { return field diff --git a/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct302SpecificData.swift b/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct302SpecificData.swift index 95ac6da..3855171 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct302SpecificData.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct302SpecificData.swift @@ -8,12 +8,22 @@ import Foundation -public class PaymentProduct302SpecificData { +public class PaymentProduct302SpecificData: Codable { public var networks: [String] = [] + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { if let networks = json["networks"] as? [String] { self.networks = networks } } + + private enum CodingKeys: String, CodingKey { + case networks + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.networks = (try? container.decode([String].self, forKey: .networks)) ?? [] + } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct320SpecificData.swift b/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct320SpecificData.swift index 063eccb..a5072a9 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct320SpecificData.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct320SpecificData.swift @@ -8,10 +8,11 @@ import Foundation -public class PaymentProduct320SpecificData { +public class PaymentProduct320SpecificData: Codable { public var gateway: String = "" public var networks: [String] = [] + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { if let gateway = json["gateway"] as? String { self.gateway = gateway @@ -20,4 +21,14 @@ public class PaymentProduct320SpecificData { self.networks = networks } } + + private enum CodingKeys: String, CodingKey { + case gateway, networks + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.gateway = (try? container.decode(String.self, forKey: .gateway)) ?? "" + self.networks = (try? container.decode([String].self, forKey: .networks)) ?? [] + } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct863SpecificData.swift b/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct863SpecificData.swift index f4e7ad6..c7d4cf6 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct863SpecificData.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/PaymentProduct863SpecificData.swift @@ -8,12 +8,22 @@ import Foundation -public class PaymentProduct863SpecificData { +public class PaymentProduct863SpecificData: Codable { public var integrationTypes: [String] = [] + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { if let integrationTypes = json["integrationTypes"] as? [String] { self.integrationTypes = integrationTypes } } + + private enum CodingKeys: String, CodingKey { + case integrationTypes + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.integrationTypes = (try? container.decode([String].self, forKey: .integrationTypes)) ?? [] + } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/PaymentProductField.swift b/IngenicoConnectKit/Models/PaymentProducts/PaymentProductField.swift index e8b550f..2bb19fd 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/PaymentProductField.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/PaymentProductField.swift @@ -8,7 +8,7 @@ import Foundation -public class PaymentProductField: ResponseObjectSerializable { +public class PaymentProductField: ResponseObjectSerializable, Codable { public var identifier: String public var usedForLookup: Bool = false @@ -16,11 +16,21 @@ public class PaymentProductField: ResponseObjectSerializable { public var displayHints: PaymentProductFieldDisplayHints public var type: FieldType + @available(*, deprecated, message: "In a future release, this property will become private to this class.") public var numberFormatter = NumberFormatter() + @available(*, deprecated, message: "In a future release, this property will become private to this class.") public var numericStringCheck: NSRegularExpression + private let stringFormatter = StringFormatter() + public var errorMessageIds: [ValidationError] = [] + @available( + *, + deprecated, + message: "In a future release, this property will be removed. Use errorMessageIds instead." + ) public var errors: [ValidationError] = [] + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { guard let identifier = json["id"] as? String, let hints = json["displayHints"] as? [String: Any], @@ -62,32 +72,96 @@ public class PaymentProductField: ResponseObjectSerializable { } } - public func validateValue(value: String, for request: PaymentRequest) { + private enum CodingKeys: String, CodingKey { + case id, displayHints, dataRestrictions, usedForLookup, type + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.identifier = try container.decode(String.self, forKey: .id) + self.displayHints = try container.decode(PaymentProductFieldDisplayHints.self, forKey: .displayHints) + self.dataRestrictions = + (try? container.decodeIfPresent(DataRestrictions.self, forKey: .dataRestrictions)) ?? DataRestrictions() + self.usedForLookup = (try? container.decodeIfPresent(Bool.self, forKey: .usedForLookup)) ?? false + self.type = (try? container.decodeIfPresent(FieldType.self, forKey: .type)) ?? .string + + self.numberFormatter.numberStyle = .decimal + guard let numericStringCheck = try? NSRegularExpression(pattern: "^\\d+$") else { + throw SessionError.RuntimeError("Could not create regular expression") + } + self.numericStringCheck = numericStringCheck + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(identifier, forKey: .id) + try? container.encode(displayHints, forKey: .displayHints) + try? container.encode(dataRestrictions, forKey: .dataRestrictions) + try? container.encode(usedForLookup, forKey: .usedForLookup) + try? container.encode(type, forKey: .type) + } + + // periphery:ignore + @available( + *, + deprecated, + message: + """ + In a future release, this function will be removed. + Please use validateValue(value:) or validateValue(for:) instead. + """ + ) + public func validateValue(value: String, for request: PaymentRequest) -> [ValidationError] { + return validateValue(value: value) + } + + public func validateValue(for request: PaymentRequest) -> [ValidationError] { + guard let value = request.getValue(forField: identifier) else { + return validateValue(value: "") + } + + return validateValue(value: value) + } + + public func validateValue(value: String) -> [ValidationError] { errors.removeAll() + errorMessageIds.removeAll() if dataRestrictions.isRequired && value.isEqual("") { - let error = ValidationErrorIsRequired() + let error = + ValidationErrorIsRequired( + errorMessage: "required", + paymentProductFieldId: identifier, + rule: nil + ) errors.append(error) + errorMessageIds.append(error) } else if dataRestrictions.isRequired || !value.isEqual("") || dataRestrictions.validators.variableRequiredness { for rule in dataRestrictions.validators.validators { - rule.validate(value: value, for: request) + _ = rule.validate(value: value, for: identifier) errors.append(contentsOf: rule.errors) + errorMessageIds.append(contentsOf: rule.errors) } + } - switch type { - case .integer where numberFormatter.number(from: value) != nil: - let error = ValidationErrorInteger() - errors.append(error) - - case .numericString where - numericStringCheck.numberOfMatches(in: value, range: NSRange(location: 0, length: value.count)) != 1: - let error = ValidationErrorNumericString() - errors.append(error) - default: - break - } + return errorMessageIds + } + + public func applyMask(value: String) -> String { + if let mask = displayHints.mask { + return stringFormatter.formatString(string: value, mask: mask) + } + + return value + } + + public func removeMask(value: String) -> String { + if let mask = displayHints.mask { + return stringFormatter.unformatString(string: value, mask: mask) } + + return value } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/PaymentProductFieldDisplayHints.swift b/IngenicoConnectKit/Models/PaymentProducts/PaymentProductFieldDisplayHints.swift index 6f2586c..b98bd18 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/PaymentProductFieldDisplayHints.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/PaymentProductFieldDisplayHints.swift @@ -8,7 +8,7 @@ import Foundation -public class PaymentProductFieldDisplayHints: ResponseObjectSerializable { +public class PaymentProductFieldDisplayHints: ResponseObjectSerializable, Codable { public var alwaysShow = false public var displayOrder: Int? @@ -21,6 +21,7 @@ public class PaymentProductFieldDisplayHints: ResponseObjectSerializable { public var link: URL? public var preferredInputType: PreferredInputType = .noKeyboard + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init?(json: [String: Any]) { guard let input = json["formElement"] as? [String: Any], let formElement = FormElement(json: input) else { @@ -65,6 +66,42 @@ public class PaymentProductFieldDisplayHints: ResponseObjectSerializable { } } + private enum CodingKeys: String, CodingKey { + case alwaysShow, displayOrder, formElement, mask, obfuscate, placeholderLabel, tooltip, label, + link, preferredInputType + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.formElement = try container.decode(FormElement.self, forKey: .formElement) + self.alwaysShow = (try? container.decodeIfPresent(Bool.self, forKey: .alwaysShow)) ?? false + self.displayOrder = try? container.decodeIfPresent(Int.self, forKey: .displayOrder) + self.mask = try? container.decodeIfPresent(String.self, forKey: .mask) + self.obfuscate = (try? container.decodeIfPresent(Bool.self, forKey: .obfuscate)) ?? false + self.placeholderLabel = try? container.decodeIfPresent(String.self, forKey: .placeholderLabel) + self.label = try? container.decodeIfPresent(String.self, forKey: .label) + if let linkString = try? container.decodeIfPresent(String.self, forKey: .link) { + self.link = URL(string: linkString) + } + self.preferredInputType = + (try? container.decodeIfPresent(PreferredInputType.self, forKey: .preferredInputType)) ?? .noKeyboard + self.tooltip = try? container.decodeIfPresent(ToolTip.self, forKey: .tooltip) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(formElement, forKey: .formElement) + try? container.encode(alwaysShow, forKey: .alwaysShow) + try? container.encodeIfPresent(displayOrder, forKey: .displayOrder) + try? container.encodeIfPresent(mask, forKey: .mask) + try? container.encode(obfuscate, forKey: .obfuscate) + try? container.encodeIfPresent(placeholderLabel, forKey: .placeholderLabel) + try? container.encodeIfPresent(label, forKey: .label) + try? container.encodeIfPresent(link?.absoluteString, forKey: .link) + try? container.encode(preferredInputType.rawValue, forKey: .preferredInputType) + try? container.encodeIfPresent(tooltip, forKey: .tooltip) + } + private func getPreferredInputType(preferredInputType: String) -> PreferredInputType { switch preferredInputType { case "StringKeyboard": diff --git a/IngenicoConnectKit/Models/PaymentProducts/PaymentProductGroup.swift b/IngenicoConnectKit/Models/PaymentProducts/PaymentProductGroup.swift index bc5d425..74942f9 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/PaymentProductGroup.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/PaymentProductGroup.swift @@ -8,47 +8,35 @@ import Foundation -public class PaymentProductGroup: PaymentItem, ResponseObjectSerializable { +public class PaymentProductGroup: BasicPaymentProductGroup, PaymentItem { - public var identifier: String - public var displayHints: PaymentItemDisplayHints - public var accountsOnFile = AccountsOnFile() - public var acquirerCountry: String? + @available( + *, + deprecated, + message: "In a future release, this property will be removed since it is not returned from the API." + ) public var allowsTokenization = false + @available( + *, + deprecated, + message: "In a future release, this property will be removed since it is not returned from the API." + ) public var allowsRecurring = false + @available( + *, + deprecated, + message: "In a future release, this property will be removed since it is not returned from the API." + ) public var autoTokenized = false public var fields = PaymentProductFields() - public var stringFormatter: StringFormatter? { - get { return accountsOnFile.accountsOnFile.first?.stringFormatter } - set { - if let stringFormatter = newValue { - for accountOnFile in accountsOnFile.accountsOnFile { - accountOnFile.stringFormatter = stringFormatter - } - } - } - } - + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { - guard let identifier = json["id"] as? String, - let hints = json["displayHints"] as? [String: Any], - let displayHints = PaymentItemDisplayHints(json: hints), - let fields = json["fields"] as? [[String: Any]] else { - return nil - } - self.identifier = identifier - self.displayHints = displayHints - - self.acquirerCountry = json["acquirerCountry"] as? String ?? "" + super.init(json: json) - if let input = json["accountsOnFile"] as? [[String: Any]] { - for accountInput in input { - if let accountFile = AccountOnFile(json: accountInput) { - accountsOnFile.accountsOnFile.append(accountFile) - } - } + guard let fields = json["fields"] as? [[String: Any]] else { + return nil } for field in fields { @@ -58,8 +46,26 @@ public class PaymentProductGroup: PaymentItem, ResponseObjectSerializable { } } - public func accountOnFile(withIdentifier identifier: String) -> AccountOnFile? { - return accountsOnFile.accountOnFile(withIdentifier: identifier) + private enum CodingKeys: String, CodingKey { + case fields + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let fieldsInput = try? container.decodeIfPresent([PaymentProductField].self, forKey: .fields) { + for field in fieldsInput { + self.fields.paymentProductFields.append(field) + } + } + + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(fields.paymentProductFields, forKey: .fields) + + try super.encode(to: encoder) } public func paymentProductField(withId paymentProductFieldId: String) -> PaymentProductField? { diff --git a/IngenicoConnectKit/Models/PaymentProducts/PaymentProductNetworks.swift b/IngenicoConnectKit/Models/PaymentProducts/PaymentProductNetworks.swift index 60c73da..8e644af 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/PaymentProductNetworks.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/PaymentProductNetworks.swift @@ -9,8 +9,33 @@ import Foundation import PassKit -public class PaymentProductNetworks { +public class PaymentProductNetworks: Codable { public var paymentProductNetworks = [PKPaymentNetwork]() + private enum CodingKeys: String, CodingKey { + case networks + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let networks = try? container.decode([String].self, forKey: .networks) { + for network in networks { + let paymentNetwork = PKPaymentNetwork(rawValue: network) + self.paymentProductNetworks.append(paymentNetwork) + } + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + var networks = [String]() + + for network in paymentProductNetworks { + networks.append(network.rawValue) + } + + try? container.encode(networks, forKey: .networks) + } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ToolTip.swift b/IngenicoConnectKit/Models/PaymentProducts/ToolTip.swift index abf8bfb..f5556e4 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ToolTip.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ToolTip.swift @@ -9,7 +9,7 @@ import Foundation import UIKit -public class ToolTip: ResponseObjectSerializable { +public class ToolTip: ResponseObjectSerializable, Codable { public var label: String? public var imagePath: String? @@ -21,4 +21,20 @@ public class ToolTip: ResponseObjectSerializable { label = input } } + + private enum CodingKeys: String, CodingKey { + case image, label + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.imagePath = try? container.decodeIfPresent(String.self, forKey: .image) + self.label = try? container.decodeIfPresent(String.self, forKey: .label) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encodeIfPresent(imagePath, forKey: .image) + try? container.encodeIfPresent(label, forKey: .label) + } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/Validator.swift b/IngenicoConnectKit/Models/PaymentProducts/Validator.swift index 6913d9d..d0dbb6b 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/Validator.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/Validator.swift @@ -8,10 +8,43 @@ import Foundation -public class Validator { +public class Validator: Codable { public var errors: [ValidationError] = [] + public var messageId: String = "" + public var validationType: ValidationType = .type + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") + public init() {} + + internal init(messageId: String, validationType: ValidationType) { + self.messageId = messageId + self.validationType = validationType + } + + private enum CodingKeys: String, CodingKey { + case messageId, validationType + } + + public required init(from decoder: Decoder) throws {} + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(messageId, forKey: .messageId) + try container.encodeIfPresent(validationType, forKey: .validationType) + } + + @available(*, deprecated, message: "In a future release, this function will be removed.") public func validate(value: String, for: PaymentRequest) { + clearErrors() + } + + internal func validate(value: String, for fieldId: String?) -> Bool { + clearErrors() + + return true + } + + internal func clearErrors() { errors.removeAll() } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorBoletoBancarioRequiredness.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorBoletoBancarioRequiredness.swift index 9a6a04f..fdc2dbb 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorBoletoBancarioRequiredness.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorBoletoBancarioRequiredness.swift @@ -6,23 +6,73 @@ // Copyright © 2016 Global Collect Services. All rights reserved. // -public class ValidatorBoletoBancarioRequiredness: Validator { +public class ValidatorBoletoBancarioRequiredness: Validator, ValidationRule { public var fiscalNumberLength: Int + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init?(json: [String: Any]) { guard let input = json["fiscalNumberLength"] as? Int else { return nil } fiscalNumberLength = input + + super.init(messageId: "fiscalNumberBoletoBancario", validationType: .boletoBancarioRequiredness) + } + + private enum CodingKeys: String, CodingKey { + case fiscalNumberLength, fiscalNumberLengthToValidate + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let fiscalNumberLengthFromApi = try? container.decode(Int.self, forKey: .fiscalNumberLength) { + self.fiscalNumberLength = fiscalNumberLengthFromApi + } else { + self.fiscalNumberLength = try container.decode(Int.self, forKey: .fiscalNumberLengthToValidate) + } + + super.init(messageId: "fiscalNumberBoletoBancario", validationType: .boletoBancarioRequiredness) + } + + public override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(fiscalNumberLength, forKey: .fiscalNumberLengthToValidate) + + try? super.encode(to: encoder) } + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) override public func validate(value: String, for request: PaymentRequest) { - super.validate(value: value, for: request) + _ = validate(value: value, for: nil, in: request) + } - if let fiscalNumber = request.unmaskedValue(forField: "fiscalNumber"), - fiscalNumber.count == fiscalNumberLength && value.isEmpty { - let error = ValidationErrorIsRequired() + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + let fieldValue = request.getValue(forField: fieldId) ?? "" + + return validate(value: fieldValue, for: fieldId, in: request) + } + + private func validate(value: String, for fieldId: String?, in request: PaymentRequest) -> Bool { + self.clearErrors() + + let fiscalNumber = request.unmaskedValue(forField: "fiscalNumber") + + if fiscalNumber?.count == fiscalNumberLength && value.isEmpty { + let error = + ValidationErrorIsRequired( + errorMessage: "required", + paymentProductFieldId: fieldId, + rule: nil + ) errors.append(error) + return false } + + return true } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorEmailAddress.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorEmailAddress.swift index 84f572a..29ca202 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorEmailAddress.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorEmailAddress.swift @@ -8,25 +8,61 @@ import Foundation -public class ValidatorEmailAddress: Validator { +public class ValidatorEmailAddress: Validator, ValidationRule { + private let regexString = "^[^@\\.]+(\\.[^@\\.]+)*@([^@\\.]+\\.)*[^@\\.]+\\.[^@\\.][^@\\.]+$" public var expression: NSRegularExpression + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") public override init() { - let regex = "^[^@\\.]+(\\.[^@\\.]+)*@([^@\\.]+\\.)*[^@\\.]+\\.[^@\\.][^@\\.]+$" + guard let regex = try? NSRegularExpression(pattern: regexString) else { + fatalError("Could not create Regular Expression") + } + expression = regex - guard let regex = try? NSRegularExpression(pattern: regex) else { + super.init(messageId: "emailAddress", validationType: .emailAddress) + } + + // periphery:ignore:parameters decoder + public required init(from decoder: Decoder) throws { + guard let regex = try? NSRegularExpression(pattern: regexString) else { fatalError("Could not create Regular Expression") } expression = regex + + super.init(messageId: "emailAddress", validationType: .emailAddress) } + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) public override func validate(value: String, for request: PaymentRequest) { - super.validate(value: value, for: request) + _ = validate(value: value, for: nil) + } + + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + guard let fieldValue = request.getValue(forField: fieldId) else { + return false + } + + return validate(value: fieldValue, for: fieldId) + } + + internal override func validate(value: String, for fieldId: String?) -> Bool { + self.clearErrors() let numberOfMatches = expression.numberOfMatches(in: value, range: NSRange(location: 0, length: value.count)) if numberOfMatches != 1 { - let error = ValidationErrorEmailAddress() + let error = + ValidationErrorEmailAddress( + errorMessage: self.messageId, + paymentProductFieldId: fieldId, + rule: self + ) errors.append(error) + return false } + return true } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorExpirationDate.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorExpirationDate.swift index 021c8a1..2ea588e 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorExpirationDate.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorExpirationDate.swift @@ -8,60 +8,100 @@ import Foundation -public class ValidatorExpirationDate: Validator { +public class ValidatorExpirationDate: Validator, ValidationRule { public var dateFormatter = DateFormatter() private var fullYearDateFormatter = DateFormatter() private var monthAndFullYearDateFormatter = DateFormatter() + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") public override init() { dateFormatter.dateFormat = "MMyy" fullYearDateFormatter.dateFormat = "yyyy" monthAndFullYearDateFormatter.dateFormat = "MMyyyy" + + super.init(messageId: "expirationDate", validationType: .expirationDate) + } + + // periphery:ignore:parameters decoder + public required init(from decoder: Decoder) throws { + dateFormatter.dateFormat = "MMyy" + fullYearDateFormatter.dateFormat = "yyyy" + monthAndFullYearDateFormatter.dateFormat = "MMyyyy" + + super.init(messageId: "expirationDate", validationType: .expirationDate) } + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) public override func validate(value: String, for request: PaymentRequest) { - super.validate(value: value, for: request) + _ = validate(value: value, for: nil) + } - // Test whether the date can be parsed normally - guard dateFormatter.date(from: value) != nil else { - let error = ValidationErrorExpirationDate() - errors.append(error) - return + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + guard let fieldValue = request.getValue(forField: fieldId) else { + return false } - let gregorianCalendar = Calendar(identifier: .gregorian) + return validate(value: fieldValue, for: fieldId) + } + + internal override func validate(value: String, for fieldId: String?) -> Bool { + self.clearErrors() - guard let enteredDate = obtainEnteredDateFromValue(value: value) else { - let error = ValidationErrorExpirationDate() - errors.append(error) - return + // Test whether the date can be parsed normally + guard dateFormatter.date(from: value) != nil else { + addExpirationDateError(fieldId: fieldId) + return false } - var componentsForFutureDate = DateComponents() - componentsForFutureDate.year = gregorianCalendar.component(.year, from: Date()) + 25 + let enteredDate = obtainEnteredDateFromValue(value: value, fieldId: fieldId) - guard let futureDate = gregorianCalendar.date(from: componentsForFutureDate) else { - let error = ValidationErrorExpirationDate() - errors.append(error) - return + guard let futureDate = obtainFutureDate() else { + addExpirationDateError(fieldId: fieldId) + return false } if !validateDateIsBetween(now: Date(), futureDate: futureDate, dateToValidate: enteredDate) { - let error = ValidationErrorExpirationDate() - errors.append(error) + addExpirationDateError(fieldId: fieldId) + return false } + + return true + } + + private func addExpirationDateError(fieldId: String?) { + let error = + ValidationErrorExpirationDate( + errorMessage: self.messageId, + paymentProductFieldId: fieldId, + rule: self + ) + errors.append(error) } - internal func obtainEnteredDateFromValue(value: String) -> Date? { + internal func obtainEnteredDateFromValue(value: String, fieldId: String?) -> Date { let year = fullYearDateFormatter.string(from: Date()) let valueWithCentury = value.substring(to: 2) + year.substring(to: 2) + value.substring(from: 2) guard let dateMonthAndFullYear = monthAndFullYearDateFormatter.date(from: valueWithCentury) else { - return nil + addExpirationDateError(fieldId: fieldId) + return Date() } return dateMonthAndFullYear } + private func obtainFutureDate() -> Date? { + let gregorianCalendar = Calendar(identifier: .gregorian) + + var componentsForFutureDate = DateComponents() + componentsForFutureDate.year = gregorianCalendar.component(.year, from: Date()) + 25 + + return gregorianCalendar.date(from: componentsForFutureDate) + } + internal func validateDateIsBetween(now: Date, futureDate: Date, dateToValidate: Date) -> Bool { let gregorianCalendar = Calendar(identifier: .gregorian) diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorFixedList.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorFixedList.swift index 6a36224..99b4a06 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorFixedList.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorFixedList.swift @@ -8,29 +8,79 @@ import Foundation -public class ValidatorFixedList: Validator, ResponseObjectSerializable { +public class ValidatorFixedList: Validator, ValidationRule, ResponseObjectSerializable { public var allowedValues: [String] = [] + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") public init(allowedValues: [String]) { self.allowedValues = allowedValues + + super.init(messageId: "fixedList", validationType: .fixedList) } + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init(json: [String: Any]) { if let input = json["allowedValues"] as? [String] { for inputString in input { allowedValues.append(inputString) } } + + super.init(messageId: "fixedList", validationType: .fixedList) + } + + private enum CodingKeys: String, CodingKey { + case allowedValues + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let allowedValues = try? container.decodeIfPresent([String].self, forKey: .allowedValues) { + self.allowedValues = allowedValues + } + + super.init(messageId: "fixedList", validationType: .fixedList) + } + + public override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(allowedValues, forKey: .allowedValues) + + try? super.encode(to: encoder) } + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) public override func validate(value: String, for request: PaymentRequest) { - super.validate(value: value, for: request) + _ = validate(value: value, for: nil) + } + + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + guard let fieldValue = request.getValue(forField: fieldId) else { + return false + } + + return validate(value: fieldValue, for: fieldId) + } + + internal override func validate(value: String, for fieldId: String?) -> Bool { + self.clearErrors() for allowedValue in allowedValues where allowedValue.isEqual(value) { - return + return true } - let error = ValidationErrorFixedList() + let error = + ValidationErrorFixedList( + errorMessage: self.messageId, + paymentProductFieldId: fieldId, + rule: self + ) errors.append(error) + + return false } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorIBAN.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorIBAN.swift index 1e3a24e..912f7f3 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorIBAN.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorIBAN.swift @@ -8,7 +8,17 @@ import Foundation -public class ValidatorIBAN: Validator { +public class ValidatorIBAN: Validator, ValidationRule { + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") + public override init() { + super.init(messageId: "iban", validationType: .iban) + } + + // periphery:ignore:parameters decoder + public required init(from decoder: Decoder) throws { + super.init(messageId: "iban", validationType: .iban) + } + private func charToIndex(mychar: Character) -> Int? { let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if let index = alphabet.firstIndex(of: mychar) { @@ -20,6 +30,7 @@ public class ValidatorIBAN: Validator { } return nil } + private func modulo(numericString: String, modulo: Int) -> Int { var remainder = numericString repeat { @@ -32,31 +43,57 @@ public class ValidatorIBAN: Validator { } while remainder.count > 2 return (Int(remainder)!) % modulo } + + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) public override func validate(value: String, for request: PaymentRequest) { - super.validate(value: value, for: request) + _ = validate(value: value, for: nil) + } + + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + guard let fieldValue = request.getValue(forField: fieldId) else { + return false + } + + return validate(value: fieldValue, for: fieldId) + } + + internal override func validate(value: String, for fieldId: String?) -> Bool { + self.clearErrors() + let strippedText = value.components(separatedBy: .whitespacesAndNewlines).joined().uppercased() - do { - let formatRegex = try NSRegularExpression(pattern: "^[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}$") - let numberOfMatches = - formatRegex.numberOfMatches(in: strippedText, range: NSRange(location: 0, length: strippedText.count)) - if numberOfMatches == 1 { - let endIndex = - strippedText.index( - strippedText.startIndex, - offsetBy: min(4, strippedText.count), - limitedBy: strippedText.endIndex - )! - let prefix = strippedText[strippedText.startIndex ..< endIndex] - let numericString = (strippedText.dropFirst(4) + prefix).map { (character: Character) in - return String(charToIndex(mychar: character)!) - }.joined() - if modulo(numericString: numericString, modulo: 97) == 1 { - // Success - return - } - } - } catch { + + guard let formatRegex = + try? NSRegularExpression(pattern: "^[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}$") else { + return false + } + + if numberOfMatches(regex: formatRegex, text: strippedText) == 1 && + modulo(numericString: numericString(of: strippedText), modulo: 97) == 1 { + // Success + return true } - errors.append(ValidationErrorIBAN()) + + let error = ValidationErrorIBAN(errorMessage: self.messageId, paymentProductFieldId: fieldId, rule: self) + errors.append(error) + + return false + } + + private func numberOfMatches(regex: NSRegularExpression, text: String) -> Int { + return regex.numberOfMatches(in: text, range: NSRange(location: 0, length: text.count)) + } + + private func numericString(of text: String) -> String { + let endIndex = text.index(text.startIndex, offsetBy: min(4, text.count), limitedBy: text.endIndex)! + let prefix = text[text.startIndex ..< endIndex] + let numericString = (text.dropFirst(4) + prefix).map { (character: Character) in + return String(charToIndex(mychar: character)!) + }.joined() + + return numericString } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorLength.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorLength.swift index ea0fb02..564e734 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorLength.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorLength.swift @@ -8,15 +8,19 @@ import Foundation -public class ValidatorLength: Validator, ResponseObjectSerializable { +public class ValidatorLength: Validator, ValidationRule, ResponseObjectSerializable { public var minLength = 0 public var maxLength = 0 + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") public init(minLength: Int?, maxLength: Int?) { self.minLength = minLength ?? 0 self.maxLength = maxLength ?? 0 + + super.init(messageId: "length", validationType: .length) } + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init(json: [String: Any]) { if let input = json["maxLength"] as? Int { maxLength = input @@ -24,17 +28,64 @@ public class ValidatorLength: Validator, ResponseObjectSerializable { if let input = json["minLength"] as? Int { minLength = input } + + super.init(messageId: "length", validationType: .length) } + private enum CodingKeys: String, CodingKey { + case minLength, maxLength + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.minLength = (try? container.decodeIfPresent(Int.self, forKey: .minLength)) ?? 0 + self.maxLength = (try? container.decodeIfPresent(Int.self, forKey: .maxLength)) ?? 0 + + super.init(messageId: "length", validationType: .length) + } + + public override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(minLength, forKey: .minLength) + try? container.encode(maxLength, forKey: .maxLength) + + try? super.encode(to: encoder) + } + + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) public override func validate(value: String, for request: PaymentRequest) { - super.validate(value: value, for: request) + _ = validate(value: value, for: nil) + } + + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + guard let fieldValue = request.getValue(forField: fieldId) else { + return false + } - let error = ValidationErrorLength() - error.minLength = minLength - error.maxLength = maxLength + return validate(value: fieldValue, for: fieldId) + } + + internal override func validate(value: String, for fieldId: String?) -> Bool { + self.clearErrors() if value.count < minLength || value.count > maxLength { + let error = + ValidationErrorLength( + errorMessage: self.messageId, + paymentProductFieldId: fieldId, + rule: self + ) + error.minLength = minLength + error.maxLength = maxLength errors.append(error) + + return false } + + return true } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorLuhn.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorLuhn.swift index d372c59..c3d350b 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorLuhn.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorLuhn.swift @@ -8,18 +8,61 @@ import Foundation -public class ValidatorLuhn: Validator { +public class ValidatorLuhn: Validator, ValidationRule { + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") + public override init() { + super.init(messageId: "luhn", validationType: .luhn) + } + + // periphery:ignore:parameters decoder + public required init(from decoder: Decoder) throws { + super.init(messageId: "luhn", validationType: .luhn) + } + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) public override func validate (value: String, for request: PaymentRequest) { - super.validate(value: value, for: request) + _ = validate(value: value, for: nil) + } + + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + guard let fieldValue = request.getValue(forField: fieldId) else { + return false + } + + return validate(value: fieldValue, for: fieldId) + } + internal override func validate(value: String, for fieldId: String?) -> Bool { + self.clearErrors() + + if modulo(of: value, modulo: 10) != 0 { + let error = + ValidationErrorLuhn( + errorMessage: self.messageId, + paymentProductFieldId: fieldId, + rule: self + ) + errors.append(error) + + return false + } + + return true + } + + private func modulo(of value: String, modulo: Int) -> Int { var evenSum = 0 var oddSum = 0 - var digit = 0 for index in 1 ... value.count { let reversedIndex = value.count - index - digit = Int(value[reversedIndex])! + guard var digit = Int(value[reversedIndex]) else { + return 1 + } if index % 2 == 1 { evenSum += digit @@ -31,10 +74,6 @@ public class ValidatorLuhn: Validator { } let total = evenSum + oddSum - if total % 10 != 0 { - let error = ValidationErrorLuhn() - errors.append(error) - } + return total % modulo } - } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorRange.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorRange.swift index 208b448..66fcc82 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorRange.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorRange.swift @@ -8,16 +8,20 @@ import Foundation -public class ValidatorRange: Validator, ResponseObjectSerializable { +public class ValidatorRange: Validator, ValidationRule, ResponseObjectSerializable { public var minValue = 0 public var maxValue = 0 public var formatter = NumberFormatter() + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") public init(minValue: Int?, maxValue: Int?) { self.minValue = minValue ?? 0 self.maxValue = maxValue ?? 0 + + super.init(messageId: "range", validationType: .range) } + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init(json: [String: Any]) { if let input = json["maxValue"] as? Int { maxValue = input @@ -25,24 +29,66 @@ public class ValidatorRange: Validator, ResponseObjectSerializable { if let input = json["minValue"] as? Int { minValue = input } + + super.init(messageId: "range", validationType: .range) + } + + private enum CodingKeys: String, CodingKey { + case minValue, maxValue + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.minValue = (try? container.decodeIfPresent(Int.self, forKey: .minValue)) ?? 0 + self.maxValue = (try? container.decodeIfPresent(Int.self, forKey: .maxValue)) ?? 0 + + super.init(messageId: "range", validationType: .range) + } + + public override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(minValue, forKey: .minValue) + try? container.encode(maxValue, forKey: .maxValue) + + try? super.encode(to: encoder) } + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) public override func validate(value: String, for request: PaymentRequest) { - super.validate(value: value, for: request) + _ = validate(value: value, for: nil) + } + + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + guard let fieldValue = request.getValue(forField: fieldId) else { + return false + } + + return validate(value: fieldValue, for: fieldId) + } - let error = ValidationErrorRange() + internal override func validate(value: String, for fieldId: String?) -> Bool { + self.clearErrors() + + let error = ValidationErrorRange(errorMessage: self.messageId, paymentProductFieldId: fieldId, rule: self) error.minValue = minValue error.maxValue = maxValue guard let number = formatter.number(from: value) else { errors.append(error) - return + + return false } - if Int(truncating: number) < minValue { - errors.append(error) - } else if Int(truncating: number) > maxValue { + if Int(truncating: number) < minValue || Int(truncating: number) > maxValue { errors.append(error) + + return false } + + return true } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorRegularExpression.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorRegularExpression.swift index 3742e69..5fe88ab 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorRegularExpression.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorRegularExpression.swift @@ -8,14 +8,18 @@ import Foundation -public class ValidatorRegularExpression: Validator, ResponseObjectSerializable { +public class ValidatorRegularExpression: Validator, ValidationRule, ResponseObjectSerializable { public var regularExpression: NSRegularExpression + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") public init(regularExpression: NSRegularExpression) { self.regularExpression = regularExpression + + super.init(messageId: "regularExpression", validationType: .regularExpression) } + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { guard let input = json["regularExpression"] as? String, let regularExpression = try? NSRegularExpression(pattern: input) else { @@ -24,16 +28,69 @@ public class ValidatorRegularExpression: Validator, ResponseObjectSerializable { } self.regularExpression = regularExpression + + super.init(messageId: "regularExpression", validationType: .regularExpression) + } + + private enum CodingKeys: String, CodingKey { + case regularExpression, regex } + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + guard let regularExpressionInput = try? + container.decodeIfPresent(String.self, forKey: .regularExpression) ?? + container.decodeIfPresent(String.self, forKey: .regex), + let regularExpression = try? NSRegularExpression(pattern: regularExpressionInput) else { + throw SessionError.RuntimeError("Regular expression is invalid") + } + self.regularExpression = regularExpression + + super.init(messageId: "regularExpression", validationType: .regularExpression) + } + + public override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(regularExpression.pattern, forKey: .regex) + + try? super.encode(to: encoder) + } + + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) public override func validate(value: String, for request: PaymentRequest) { - super.validate(value: value, for: request) + _ = validate(value: value, for: nil) + } + + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + guard let fieldValue = request.getValue(forField: fieldId) else { + return false + } + + return validate(value: fieldValue, for: fieldId) + } + + internal override func validate(value: String, for fieldId: String?) -> Bool { + self.clearErrors() let numberOfMatches = regularExpression.numberOfMatches(in: value, range: NSRange(location: 0, length: value.count)) if numberOfMatches != 1 { - let error = ValidationErrorRegularExpression() + let error = + ValidationErrorRegularExpression( + errorMessage: self.messageId, + paymentProductFieldId: fieldId, + rule: self + ) errors.append(error) + + return false } + + return true } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorResidentIdNumber.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorResidentIdNumber.swift index e4bdaf1..2b04663 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorResidentIdNumber.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorResidentIdNumber.swift @@ -8,8 +8,23 @@ import Foundation -public class ValidatorResidentIdNumber: Validator { +public class ValidatorResidentIdNumber: Validator, ValidationRule { + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") + public override init() { + super.init(messageId: "residentIdNumber", validationType: .residentIdNumber) + } + + // periphery:ignore:parameters decoder + public required init(from decoder: Decoder) throws { + super.init(messageId: "residentIdNumber", validationType: .residentIdNumber) + } + + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) /** Validates a Chinese Resident ID Number. - Parameters: @@ -18,7 +33,28 @@ public class ValidatorResidentIdNumber: Validator { - Important: The return value can be obtained by reading the errors array of this class */ public override func validate(value: String, for: PaymentRequest) { - errors.removeAll() + _ = validate(value: value, for: nil) + } + + /** + Validates a Chinese Resident ID Number. + - Parameters: + - Field: The field which contains the ID to be verified, 15 to 18 characters long + - PaymentRequest: The Payment request that the id is a part of + - Important: Any possible errors can be obtained by reading the errors array of this class + */ + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + guard let fieldValue = request.getValue(forField: fieldId) else { + return false + } + + return validate(value: fieldValue, for: fieldId) + } + + internal override func validate(value: String, for fieldId: String?) -> Bool { + self.clearErrors() + + let error = ValidationErrorResidentId(errorMessage: self.messageId, paymentProductFieldId: fieldId, rule: self) if value.count == 15 { // We perform no checksum validation for IDs with a length of 15 @@ -26,18 +62,20 @@ public class ValidatorResidentIdNumber: Validator { // We only check if the id is a valid Integer if Int(value) == nil { - errors.append(ValidationErrorResidentId()) - return + errors.append(error) + return false } } else if value.count == 18 { if !checkSumIsValid(for: value) { - errors.append(ValidationErrorResidentId()) - return + errors.append(error) + return false } } else { - errors.append(ValidationErrorResidentId()) - return + errors.append(error) + return false } + + return true } /** diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValidatorTermsAndConditions.swift b/IngenicoConnectKit/Models/PaymentProducts/ValidatorTermsAndConditions.swift index 0c8e5ed..cb894e3 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValidatorTermsAndConditions.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValidatorTermsAndConditions.swift @@ -8,16 +8,49 @@ import Foundation -public class ValidatorTermsAndConditions: Validator { +public class ValidatorTermsAndConditions: Validator, ValidationRule { + @available(*, deprecated, message: "In a future release, this initializer will become internal to the SDK.") public override init() { - super.init() + super.init(messageId: "termsAndConditions", validationType: .termsAndConditions) } + // periphery:ignore:parameters decoder + public required init(from decoder: Decoder) throws { + super.init(messageId: "termsAndConditions", validationType: .termsAndConditions) + } + + @available( + *, + deprecated, + message: "In a future release, this function will be removed. Please use validate(field:in:) instead." + ) public override func validate(value: String, for request: PaymentRequest) { - super.validate(value: value, for: request) + _ = validate(value: value, for: nil) + } + + public func validate(field fieldId: String, in request: PaymentRequest) -> Bool { + guard let fieldValue = request.getValue(forField: fieldId) else { + return false + } + + return validate(value: fieldValue, for: fieldId) + } + + internal override func validate(value: String, for fieldId: String?) -> Bool { + self.clearErrors() + if !(Bool(value) ?? false) { - let error = ValidationErrorTermsAndConditions() + let error = + ValidationErrorTermsAndConditions( + errorMessage: self.messageId, + paymentProductFieldId: fieldId, + rule: self + ) errors.append(error) + + return false } + + return true } } diff --git a/IngenicoConnectKit/Models/PaymentProducts/Validators.swift b/IngenicoConnectKit/Models/PaymentProducts/Validators.swift index 873e265..32d1873 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/Validators.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/Validators.swift @@ -8,8 +8,64 @@ import Foundation -public class Validators { +public class Validators: Decodable { var variableRequiredness = false public var validators = [Validator]() + + internal init() {} + + private enum CodingKeys: String, CodingKey { + case luhn, expirationDate, range, length, fixedList, emailAddress, residentIdNumber, regularExpression, + termsAndConditions, iban, boletoBancarioRequiredness + } + + // swiftlint:disable cyclomatic_complexity + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let validatorLuhn = try? container.decodeIfPresent(ValidatorLuhn.self, forKey: .luhn) { + self.validators.append(validatorLuhn) + } + if let validatorExpirationDate = + try? container.decodeIfPresent(ValidatorExpirationDate.self, forKey: .expirationDate) { + self.validators.append(validatorExpirationDate) + } + if let validatorRange = try? container.decodeIfPresent(ValidatorRange.self, forKey: .range) { + self.validators.append(validatorRange) + } + if let validatorLength = try? container.decodeIfPresent(ValidatorLength.self, forKey: .length) { + self.validators.append(validatorLength) + } + if let validatorFixedList = try? container.decodeIfPresent(ValidatorFixedList.self, forKey: .fixedList) { + self.validators.append(validatorFixedList) + } + if let validatorEmailAddress = + try? container.decodeIfPresent(ValidatorEmailAddress.self, forKey: .emailAddress) { + self.validators.append(validatorEmailAddress) + } + if let validatorResidentIdNumber = + try? container.decodeIfPresent(ValidatorResidentIdNumber.self, forKey: .residentIdNumber) { + self.validators.append(validatorResidentIdNumber) + } + if let validatorRegularExpression = + try? container.decodeIfPresent(ValidatorRegularExpression.self, forKey: .regularExpression) { + self.validators.append(validatorRegularExpression) + } + if let validatorTermsAndConditions = + try? container.decodeIfPresent(ValidatorTermsAndConditions.self, forKey: .termsAndConditions) { + self.validators.append(validatorTermsAndConditions) + } + if let validatorIBAN = try? container.decodeIfPresent(ValidatorIBAN.self, forKey: .iban) { + self.validators.append(validatorIBAN) + } + if let validatorBoletoBancarioRequiredness = + try? container.decodeIfPresent( + ValidatorBoletoBancarioRequiredness.self, + forKey: .boletoBancarioRequiredness + ) { + self.variableRequiredness = true + self.validators.append(validatorBoletoBancarioRequiredness) + } + } + // swiftlint:enable cyclomatic_complexity } diff --git a/IngenicoConnectKit/Models/PaymentProducts/ValueMappingItem.swift b/IngenicoConnectKit/Models/PaymentProducts/ValueMappingItem.swift index e3b879a..c9f180d 100644 --- a/IngenicoConnectKit/Models/PaymentProducts/ValueMappingItem.swift +++ b/IngenicoConnectKit/Models/PaymentProducts/ValueMappingItem.swift @@ -8,18 +8,18 @@ import Foundation -public class ValueMappingItem: ResponseObjectSerializable { +public class ValueMappingItem: ResponseObjectSerializable, Codable { public var displayName: String? - public var displayElements: [DisplayElement] + public var displayElements: [DisplayElement] = [] public var value: String + @available(*, deprecated, message: "In a future release, this initializer will be removed.") required public init?(json: [String: Any]) { guard let value = json["value"] as? String else { return nil } self.value = value - self.displayElements = [] if let displayElements = json["displayElements"] as? [[String: Any]] { for element in displayElements { if let displayElement = DisplayElement(json: element) { @@ -40,4 +40,24 @@ public class ValueMappingItem: ResponseObjectSerializable { } } } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.value = try container.decode(String.self, forKey: .value) + + self.displayElements = (try? container.decodeIfPresent([DisplayElement].self, forKey: .displayElements)) ?? [] + + if let displayName = try? container.decodeIfPresent(String.self, forKey: .displayName) { + self.displayName = displayName + if self.displayElements.filter({ $0.id == "displayName" }).count == 0 && displayName != "" { + let newElement = DisplayElement(id: "displayName", type: .string, value: displayName) + self.displayElements.append(newElement) + } + } else { + let displayNames = self.displayElements.filter { $0.id == "displayName" } + if displayNames.count > 0 { + self.displayName = displayNames.first?.value + } + } + } } diff --git a/IngenicoConnectKit/Models/PaymentRequest/PaymentRequest.swift b/IngenicoConnectKit/Models/PaymentRequest/PaymentRequest.swift index c0da981..cc4af7c 100644 --- a/IngenicoConnectKit/Models/PaymentRequest/PaymentRequest.swift +++ b/IngenicoConnectKit/Models/PaymentRequest/PaymentRequest.swift @@ -8,9 +8,15 @@ import Foundation -public class PaymentRequest { +public class PaymentRequest: Decodable { public var paymentProduct: PaymentProduct? + public var errorMessageIds: [ValidationError] = [] + @available( + *, + deprecated, + message: "In a future release, this property will be removed. Use errorMessageIds instead." + ) public var errors: [ValidationError] = [] public var tokenize = false @@ -25,6 +31,22 @@ public class PaymentRequest { self.tokenize = tokenize ?? false } + private enum CodingKeys: String, CodingKey { + case paymentProduct, errorMessageIds, tokenize, fieldValues, accountOnFile + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.paymentProduct = try container.decodeIfPresent(PaymentProduct.self, forKey: .paymentProduct) + self.errorMessageIds = try container.decodeIfPresent([ValidationError].self, forKey: .errorMessageIds) ?? [] + self.errors = try container.decodeIfPresent([ValidationError].self, forKey: .errorMessageIds) ?? [] + self.tokenize = try container.decode(Bool.self, forKey: .tokenize) + self.fieldValues = + try container.decodeIfPresent([String: String].self, forKey: .fieldValues) ?? [String: String]() + self.accountOnFile = try? container.decodeIfPresent(AccountOnFile.self, forKey: .accountOnFile) + } + public func setValue(forField paymentProductFieldId: String, value: String) { fieldValues[paymentProductFieldId] = value } @@ -99,24 +121,54 @@ public class PaymentRequest { return mask } - public func validate() { + public func validate() -> [ValidationError] { + errors.removeAll() + errorMessageIds.removeAll() + + guard let paymentProduct = paymentProduct else { + errors.append(ValidationErrorInvalidPaymentProduct()) + errorMessageIds.append(ValidationErrorInvalidPaymentProduct()) + return errorMessageIds + } + + for field in paymentProduct.fields.paymentProductFields { + if let fieldValue = unmaskedValue(forField: field.identifier) { + if !isPartOfAccountOnFile(field: field.identifier) { + let fieldErrors = field.validateValue(value: fieldValue, for: self) + errors.append(contentsOf: fieldErrors) + errorMessageIds.append(contentsOf: fieldErrors) + } + } else { + let error = + ValidationErrorIsRequired( + errorMessage: "required", + paymentProductFieldId: field.identifier, + rule: nil + ) + errors.append(error) + errorMessageIds.append(error) + } + } + return errorMessageIds + } + + public var maskedFieldValues: [String: String]? { guard let paymentProduct = paymentProduct else { NSException( name: NSExceptionName(rawValue: "Invalid payment product"), reason: "Payment product is invalid" ).raise() - return + return nil } - errors.removeAll() + var maskedFieldValues = [String: String]() for field in paymentProduct.fields.paymentProductFields { - if let fieldValue = unmaskedValue(forField: field.identifier), - !isPartOfAccountOnFile(field: field.identifier) { - field.validateValue(value: fieldValue, for: self) - errors.append(contentsOf: field.errors) - } + let masked = maskedValue(forField: field.identifier) + maskedFieldValues[field.identifier] = masked } + + return maskedFieldValues } public var unmaskedFieldValues: [String: String]? { @@ -130,7 +182,7 @@ public class PaymentRequest { var unmaskedFieldValues = [String: String]() - for field in paymentProduct.fields.paymentProductFields where !isReadOnly(field: field.identifier) { + for field in paymentProduct.fields.paymentProductFields { let unmasked = unmaskedValue(forField: field.identifier) unmaskedFieldValues[field.identifier] = unmasked } diff --git a/IngenicoConnectKit/Models/PaymentRequest/PreparedPaymentRequest.swift b/IngenicoConnectKit/Models/PaymentRequest/PreparedPaymentRequest.swift index aa45043..4b87a49 100644 --- a/IngenicoConnectKit/Models/PaymentRequest/PreparedPaymentRequest.swift +++ b/IngenicoConnectKit/Models/PaymentRequest/PreparedPaymentRequest.swift @@ -8,7 +8,7 @@ import Foundation -public class PreparedPaymentRequest { +public class PreparedPaymentRequest: Codable { public var encryptedFields: String public var encodedClientMetaInfo: String diff --git a/IngenicoConnectKit/Models/PublicKeys/PublicKeyResponse.swift b/IngenicoConnectKit/Models/PublicKeys/PublicKeyResponse.swift index 40b0e09..91cac5e 100644 --- a/IngenicoConnectKit/Models/PublicKeys/PublicKeyResponse.swift +++ b/IngenicoConnectKit/Models/PublicKeys/PublicKeyResponse.swift @@ -8,12 +8,29 @@ import Foundation -public class PublicKeyResponse { +public class PublicKeyResponse: Codable { public var keyId: String public var encodedPublicKey: String + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public init(keyId: String, encodedPublicKey: String) { self.keyId = keyId self.encodedPublicKey = encodedPublicKey } + + private enum CodingKeys: String, CodingKey { + case keyId, publicKey + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.keyId = try container.decode(String.self, forKey: .keyId) + self.encodedPublicKey = try container.decode(String.self, forKey: .publicKey) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try? container.encode(keyId, forKey: .keyId) + try? container.encode(encodedPublicKey, forKey: .publicKey) + } } diff --git a/IngenicoConnectKit/Models/ThirdPartyStatus/ThirdPartyStatus.swift b/IngenicoConnectKit/Models/ThirdPartyStatus/ThirdPartyStatus.swift index a4bba5c..e471b9a 100644 --- a/IngenicoConnectKit/Models/ThirdPartyStatus/ThirdPartyStatus.swift +++ b/IngenicoConnectKit/Models/ThirdPartyStatus/ThirdPartyStatus.swift @@ -4,13 +4,14 @@ // // Created for Ingenico ePayments on 15/06/2017. // -// swiftlint:disable identifier_name import UIKit -public enum ThirdPartyStatus: String { +public enum ThirdPartyStatus: String, Codable { + // swiftlint:disable identifier_name case Waiting = "WAITING" case Initialized = "INITIALIZED" case Authorized = "AUTHORIZED" case Completed = "COMPLETED" + // swiftlint:enable identifier_name } diff --git a/IngenicoConnectKit/Models/ThirdPartyStatus/ThirdPartyStatusResponse.swift b/IngenicoConnectKit/Models/ThirdPartyStatus/ThirdPartyStatusResponse.swift index ae0a0ad..ced3678 100644 --- a/IngenicoConnectKit/Models/ThirdPartyStatus/ThirdPartyStatusResponse.swift +++ b/IngenicoConnectKit/Models/ThirdPartyStatus/ThirdPartyStatusResponse.swift @@ -8,9 +8,10 @@ import UIKit -public class ThirdPartyStatusResponse: ResponseObjectSerializable { +public class ThirdPartyStatusResponse: ResponseObjectSerializable, Codable { public var thirdPartyStatus: ThirdPartyStatus + @available(*, deprecated, message: "In a future release, this initializer will be removed.") public required init?(json: [String: Any]) { if let urlStr = json["thirdPartyStatus"] as? String { if let input = ThirdPartyStatus(rawValue: urlStr) { diff --git a/IngenicoConnectKit/Models/ValidationErrors/ValidationErrors.swift b/IngenicoConnectKit/Models/ValidationErrors/ValidationErrors.swift index 06ba511..9057899 100644 --- a/IngenicoConnectKit/Models/ValidationErrors/ValidationErrors.swift +++ b/IngenicoConnectKit/Models/ValidationErrors/ValidationErrors.swift @@ -8,15 +8,29 @@ import Foundation -public class ValidationError { public init() {} } +public class ValidationError: Codable { + public var errorMessage: String = "" + public var paymentProductFieldId: String? + public var rule: Validator? + + public init() {} + + public init(errorMessage: String, paymentProductFieldId: String?, rule: Validator?) { + self.errorMessage = errorMessage + self.paymentProductFieldId = paymentProductFieldId + self.rule = rule + } +} public class ValidationErrorAllowed: ValidationError {} public class ValidationErrorEmailAddress: ValidationError {} public class ValidationErrorExpirationDate: ValidationError {} public class ValidationErrorFixedList: ValidationError {} +@available(*, deprecated, message: "In a future release, this class will be removed.") public class ValidationErrorInteger: ValidationError {} public class ValidationErrorIsRequired: ValidationError {} public class ValidationErrorLuhn: ValidationError {} +@available(*, deprecated, message: "In a future release, this class will be removed.") public class ValidationErrorNumericString: ValidationError {} public class ValidationErrorRegularExpression: ValidationError {} public class ValidationErrorTermsAndConditions: ValidationError {} @@ -34,3 +48,5 @@ public class ValidationErrorRange: ValidationError { public var minValue = 0 public var maxValue = 0 } + +public class ValidationErrorInvalidPaymentProduct: ValidationError {} diff --git a/IngenicoConnectKit/PaymentConfiguration.swift b/IngenicoConnectKit/PaymentConfiguration.swift new file mode 100644 index 0000000..8e53c58 --- /dev/null +++ b/IngenicoConnectKit/PaymentConfiguration.swift @@ -0,0 +1,17 @@ +// +// PaymentConfiguration.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 24/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +public class PaymentConfiguration: Decodable { + public let paymentContext: PaymentContext + public let groupPaymentProducts: Bool + + public init(paymentContext: PaymentContext, groupPaymentProducts: Bool = false) { + self.paymentContext = paymentContext + self.groupPaymentProducts = groupPaymentProducts + } +} diff --git a/IngenicoConnectKit/PaymentContext.swift b/IngenicoConnectKit/PaymentContext.swift index 72cbe01..b830a3a 100644 --- a/IngenicoConnectKit/PaymentContext.swift +++ b/IngenicoConnectKit/PaymentContext.swift @@ -8,7 +8,7 @@ import Foundation -public class PaymentContext { +public class PaymentContext: Decodable { @available(*, deprecated, message: "In the next major release, the type of countryCode will change to String.") public var countryCode: CountryCode public var countryCodeString: String @@ -36,6 +36,38 @@ public class PaymentContext { } } + enum CodingKeys: CodingKey { + case countryCode, forceBasicFlow, amountOfMoney, isRecurring + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let countryCodeString = try? container.decodeIfPresent(String.self, forKey: .countryCode) { + self.countryCodeString = countryCodeString + self.countryCode = CountryCode.init(rawValue: countryCodeString) ?? .US + } else { + self.countryCodeString = "US" + self.countryCode = .US + } + + if let forceBasicFlow = try? container.decodeIfPresent(Bool.self, forKey: .forceBasicFlow) { + self.forceBasicFlow = forceBasicFlow + } + + self.amountOfMoney = try container.decode(PaymentAmountOfMoney.self, forKey: .amountOfMoney) + + self.isRecurring = try container.decodeIfPresent(Bool.self, forKey: .isRecurring) ?? false + + if let languageCode = Locale.current.languageCode { + self.locale = languageCode.appending("_") + } + if let regionCode = Locale.current.regionCode, + let locale = self.locale { + self.locale = locale.appending(regionCode) + } + } + public var description: String { return "\(amountOfMoney.description)-\(countryCodeString)-\(isRecurring ? "YES" : "NO")" } diff --git a/IngenicoConnectKit/Protocols/BasicPaymentItem.swift b/IngenicoConnectKit/Protocols/BasicPaymentItem.swift index 06757e0..3b8ab15 100644 --- a/IngenicoConnectKit/Protocols/BasicPaymentItem.swift +++ b/IngenicoConnectKit/Protocols/BasicPaymentItem.swift @@ -8,7 +8,7 @@ import Foundation -public protocol BasicPaymentItem { +public protocol BasicPaymentItem: Encodable { var identifier: String { get set } var displayHints: PaymentItemDisplayHints { get set } var accountsOnFile: AccountsOnFile { get set } diff --git a/IngenicoConnectKit/Protocols/ResponseObjectSerializable.swift b/IngenicoConnectKit/Protocols/ResponseObjectSerializable.swift index 5809e6b..3e1674a 100644 --- a/IngenicoConnectKit/Protocols/ResponseObjectSerializable.swift +++ b/IngenicoConnectKit/Protocols/ResponseObjectSerializable.swift @@ -8,6 +8,7 @@ import Foundation +@available(*, deprecated, message: "In a future release, this protocol will be removed.") public protocol ResponseObjectSerializable { init?(json: [String: Any]) } diff --git a/IngenicoConnectKit/Session.swift b/IngenicoConnectKit/Session.swift index 3658f81..864ad8c 100644 --- a/IngenicoConnectKit/Session.swift +++ b/IngenicoConnectKit/Session.swift @@ -8,6 +8,16 @@ import PassKit +@available( + *, + deprecated, + message: + """ + In a future release, this class, its functions and its properties will be removed. + Session has been replaced by ClientApi. + Obtain an instance by initializing ConnectSDK and access the ClientApi by calling ConnectSDK.clientApi. + """ +) public class Session { public var communicator: C2SCommunicator public var assetManager: AssetManager @@ -55,6 +65,15 @@ public class Session { } } + public var clientSessionId: String { + return communicator.clientSessionId + } + + @available(*, deprecated, message: "This function is dependant on Environment, and will therefore be removed.") + public var isEnvironmentTypeProduction: Bool { + return communicator.isEnvironmentTypeProduction + } + public init( communicator: C2SCommunicator, assetManager: AssetManager, @@ -97,7 +116,14 @@ public class Session { } - @available(*, deprecated, message: "Use init(clientSessionId:customerId:baseURL:assetBaseURL:appIdentifier:loggingEnabled:) instead") + @available( + *, + deprecated, + message: + """ + Use init(clientSessionId:customerId:baseURL:assetBaseURL:appIdentifier:loggingEnabled:) instead + """ + ) public init( clientSessionId: String, customerId: String, @@ -133,14 +159,9 @@ public class Session { communicator.paymentProducts(forContext: context, success: { paymentProducts in self.paymentProducts = paymentProducts self.paymentProducts.stringFormatter = self.stringFormatter - self.assetManager.initializeImages(for: paymentProducts.paymentProducts) - self.assetManager.updateImagesAsync( - for: paymentProducts.paymentProducts, - baseURL: self.communicator.assetsBaseURL - ) { + self.setLogoForPaymentItems(for: paymentProducts.paymentProducts) { success(paymentProducts) } - }, failure: { error in failure(error) }) @@ -164,39 +185,6 @@ public class Session { ) } - @available(*, deprecated, message: "Use customerDetails(String:[String,String]:String:) instead") - public func customerDetails( - forProductId productId: String, - withLookupValues lookupValues: [[String: String]], - countryCode: CountryCode, - success: @escaping (_ paymentProduct: CustomerDetails) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - communicator.customerDetails( - forProductId: productId, - withLookupValues: lookupValues, - countryCode: countryCode.rawValue, - success: success, - failure: failure - ) - } - - public func customerDetails( - forProductId productId: String, - withLookupValues lookupValues: [[String: String]], - countryCode: String, - success: @escaping (_ paymentProduct: CustomerDetails) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - communicator.customerDetails( - forProductId: productId, - withLookupValues: lookupValues, - countryCode: countryCode, - success: success, - failure: failure - ) - } - public func paymentProductGroups( for context: PaymentContext, success: @escaping (_ paymentProductGroups: BasicPaymentProductGroups) -> Void, @@ -205,12 +193,9 @@ public class Session { communicator.paymentProductGroups(forContext: context, success: { paymentProductGroups in self.paymentProductGroups = paymentProductGroups self.paymentProductGroups.stringFormatter = self.stringFormatter - self.assetManager.initializeImages(for: paymentProductGroups.paymentProductGroups) - self.assetManager.updateImagesAsync( - for: paymentProductGroups.paymentProductGroups, - baseURL: self.communicator.assetsBaseURL - ) - success(paymentProductGroups) + self.setLogoForPaymentProductGroups(for: paymentProductGroups.paymentProductGroups) { + success(paymentProductGroups) + } }, failure: { error in failure(error) }) @@ -225,22 +210,12 @@ public class Session { communicator.paymentProducts(forContext: context, success: { paymentProducts in self.paymentProducts = paymentProducts self.paymentProducts.stringFormatter = self.stringFormatter - // self.assetManager.initializeImages(for: paymentProducts.paymentProducts) - self.assetManager.updateImagesAsync( - for: paymentProducts.paymentProducts, - baseURL: self.communicator.assetsBaseURL - ) { - self.assetManager.initializeImages(for: paymentProducts.paymentProducts) + self.setLogoForPaymentItems(for: paymentProducts.paymentProducts) { if groupPaymentProducts { self.communicator.paymentProductGroups(forContext: context, success: { paymentProductGroups in self.paymentProductGroups = paymentProductGroups self.paymentProductGroups.stringFormatter = self.stringFormatter - // self.assetManager.initializeImages(for: paymentProductGroups.paymentProductGroups) - self.assetManager.updateImagesAsync( - for: paymentProductGroups.paymentProductGroups, - baseURL: self.communicator.assetsBaseURL - ) { - self.assetManager.initializeImages(for: paymentProductGroups.paymentProductGroups) + self.setLogoForPaymentProductGroups(for: paymentProductGroups.paymentProductGroups) { let items = PaymentItems(products: paymentProducts, groups: paymentProductGroups) success(items) } @@ -269,10 +244,10 @@ public class Session { } else { communicator.paymentProduct(withIdentifier: paymentProductId, context: context, success: { paymentProduct in self.paymentProductMapping[key] = paymentProduct - self.assetManager.initializeImages(for: paymentProduct) - self.assetManager.updateImagesAsync(for: paymentProduct, baseURL: self.communicator.assetsBaseURL) - - success(paymentProduct) + self.setTooltipImages(for: paymentProduct) + self.setLogoForDisplayHints(for: paymentProduct.displayHints) { + success(paymentProduct) + } }, failure: { error in failure(error) }) @@ -293,12 +268,9 @@ public class Session { context: context, success: { paymentProductGroup in self.paymentProductGroupMapping[key] = paymentProductGroup - self.assetManager.initializeImages(for: paymentProductGroup) - self.assetManager.updateImagesAsync( - for: paymentProductGroup, - baseURL: self.communicator.assetsBaseURL - ) - success(paymentProductGroup) + self.setLogoForDisplayHints(for: paymentProductGroup.displayHints) { + success(paymentProductGroup) + } }, failure: { error in failure(error) @@ -334,7 +306,14 @@ public class Session { } } - @available(*, deprecated, message: "Use convert(Int:String:Sring:) instead") + @available( + *, + deprecated, + message: + """ + Use convert(Int, String, String, (ConvertedAmountResponse) -> Void, (Error) -> Void) instead + """ + ) public func convert( amountInCents: Int, source: CurrencyCode, @@ -345,6 +324,14 @@ public class Session { self.convert(amountInCents: amountInCents, source: source, target: target, success: success, failure: failure) } + @available( + *, + deprecated, + message: + """ + Use convert(Int, String, String, (ConvertedAmountResponse) -> Void, (Error) -> Void) instead + """ + ) public func convert( amountInCents: Int, source: String, @@ -367,6 +354,28 @@ public class Session { ) } + public func convert( + amountInCents: Int, + source: String, + target: String, + success: @escaping (_ convertedAmountResponse: ConvertedAmountResponse) -> Void, + failure: @escaping (_ error: Error) -> Void + ) { + communicator.convert( + amountInCents: amountInCents, + source: source, + target: target, + success: { convertedAmountResponse in + success(convertedAmountResponse) + }, + failure: { error in + if let error = error { + failure(error) + } + } + ) + } + @available(*, deprecated, message: "Use directory(String:String:Sring:) instead") public func directory( forProductId paymentProductId: String, @@ -522,14 +531,6 @@ public class Session { return paymentRequestJSON } - public var clientSessionId: String { - return communicator.clientSessionId - } - - @available(*, deprecated, message: "This function is dependant on Environment, and will therefore be removed.") - public var isEnvironmentTypeProduction: Bool { - return communicator.isEnvironmentTypeProduction - } public func keyValuePairs(from dictionary: [String: String]) -> [[String: String]] { var keyValuePairs = [[String: String]]() for (key, value) in dictionary { @@ -548,4 +549,84 @@ public class Session { return String(bytes: JSONAsData, encoding: String.Encoding.utf8) } + + private func setLogoForPaymentItems(for paymentItems: [BasicPaymentItem], completion: @escaping() -> Void) { + var counter = 0 + for paymentItem in paymentItems { + setLogoForDisplayHints(for: paymentItem.displayHints, completion: { + counter += 1 + if counter == paymentItems.count { + completion() + } + }) + } + } + + private func setLogoForPaymentProductGroups( + for paymentProductGroups: [BasicPaymentProductGroup], + completion: @escaping() -> Void + ) { + var counter = 0 + for paymentProductGroup in paymentProductGroups { + setLogoForDisplayHints(for: paymentProductGroup.displayHints, completion: { + counter += 1 + if counter == paymentProductGroups.count { + completion() + } + }) + } + } + + private func setLogoForDisplayHints(for displayHints: PaymentItemDisplayHints, completion: @escaping() -> Void) { + self.getLogoByStringURL(from: displayHints.logoPath) { data, _, error in + if let imageData = data, error == nil { + displayHints.logoImage = UIImage(data: imageData) + } + completion() + } + } + + private func setTooltipImages(for paymentItem: PaymentItem) { + for field in paymentItem.fields.paymentProductFields { + guard let tooltip = field.displayHints.tooltip, + let imagePath = tooltip.imagePath else { return } + + self.getLogoByStringURL(from: imagePath) { data, _, error in + if let imageData = data, error == nil { + tooltip.image = UIImage(data: imageData) + } + } + } + } + + internal func getLogoByStringURL( + from url: String, + completion: @escaping (Data?, URLResponse?, Error?) -> Void + ) { + guard let assetsBaseURL else { + Macros.DLog(message: "assetsBaseURL is nil") + completion(nil, nil, nil) + return + } + + let completeUrl = assetsBaseURL + url + + guard let encodedUrlString = completeUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + Macros.DLog(message: "Unable to decode URL for url string: \(url)") + completion(nil, nil, nil) + return + } + + guard let encodedUrl = URL(string: encodedUrlString) else { + Macros.DLog(message: "Unable to create URL for url string: \(encodedUrlString)") + completion(nil, nil, nil) + return + } + + URLSession.shared.dataTask(with: encodedUrl, completionHandler: {data, response, error in + DispatchQueue.main.async { + completion(data, response, error) + } + }).resume() + } } diff --git a/IngenicoConnectKit/SessionConfiguration.swift b/IngenicoConnectKit/SessionConfiguration.swift new file mode 100644 index 0000000..8f891c8 --- /dev/null +++ b/IngenicoConnectKit/SessionConfiguration.swift @@ -0,0 +1,21 @@ +// +// SessionConfiguration.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 24/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +public class SessionConfiguration: Decodable { + public let clientSessionId: String + public let customerId: String + public let clientApiUrl: String + public let assetUrl: String + + public init(clientSessionId: String, customerId: String, clientApiUrl: String, assetUrl: String) { + self.clientSessionId = clientSessionId + self.customerId = customerId + self.clientApiUrl = clientApiUrl + self.assetUrl = assetUrl + } +} diff --git a/IngenicoConnectKit/Util.swift b/IngenicoConnectKit/Util.swift index 4b708e9..e2a4959 100644 --- a/IngenicoConnectKit/Util.swift +++ b/IngenicoConnectKit/Util.swift @@ -5,10 +5,17 @@ // Created for Ingenico ePayments on 15/12/2016. // Copyright © 2016 Global Collect Services. All rights reserved. // -// swiftlint:disable identifier_name import UIKit +@available( + *, + deprecated, + message: + """ + In a future release, this class, its functions and its properties will become internal to the SDK. + """ +) public class Util { static let shared = Util() public var metaInfo: [String: String]? @@ -43,7 +50,7 @@ public class Util { public init() { metaInfo = [ "platformIdentifier": platformIdentifier, - "sdkIdentifier": "SwiftClientSDK/v5.12.0", + "sdkIdentifier": "SwiftClientSDK/v5.13.0", "sdkCreator": "Ingenico", "screenSize": screenSize, "deviceBrand": "Apple", @@ -91,6 +98,8 @@ public class Util { return base64EncodedString(fromDictionary: metaInfo!) } + // swiftlint:disable identifier_name + // swiftlint:disable cyclomatic_complexity @available( *, deprecated, @@ -141,8 +150,8 @@ public class Util { return "https://par.sandbox.api-ingenico.com/client/v1" } } - } + // swiftlint:enable identifier_name @available( *, @@ -194,8 +203,8 @@ public class Util { return "https://assets.pay4.sandbox.secured-by-ingenico.com" } } - } + // swiftlint:enable cyclomatic_complexity public func base64EncodedString(fromDictionary dictionary: [AnyHashable: Any]) -> String? { guard let json = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { diff --git a/IngenicoConnectKit/ValidationRule.swift b/IngenicoConnectKit/ValidationRule.swift new file mode 100644 index 0000000..41148ad --- /dev/null +++ b/IngenicoConnectKit/ValidationRule.swift @@ -0,0 +1,13 @@ +// +// ValidationRule.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 18/12/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import Foundation + +public protocol ValidationRule { + func validate(field fieldId: String, in request: PaymentRequest) -> Bool +} diff --git a/IngenicoConnectKit/ValidationType.swift b/IngenicoConnectKit/ValidationType.swift new file mode 100644 index 0000000..8384f72 --- /dev/null +++ b/IngenicoConnectKit/ValidationType.swift @@ -0,0 +1,25 @@ +// +// ValidationType.swift +// IngenicoConnectKit +// +// Created for Ingenico ePayments on 18/12/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import Foundation + +public enum ValidationType: String, Codable { + case expirationDate = "EXPIRATIONDATE" + case emailAddress = "EMAILADDRESS" + case fixedList = "FIXEDLIST" + case iban = "IBAN" + case length = "LENGTH" + case luhn = "LUHN" + case range = "RANGE" + case regularExpression = "REGULAREXPRESSION" + case required = "REQUIRED" + case type = "TYPE" + case boletoBancarioRequiredness = "BOLETOBANCARIOREQUIREDNESS" + case termsAndConditions = "TERMSANDCONDITIONS" + case residentIdNumber = "RESIDENTIDNUMBER" +} diff --git a/IngenicoConnectKit/Wrappers/AlamofireWrapper.swift b/IngenicoConnectKit/Wrappers/AlamofireWrapper.swift index 5f685eb..b09d50a 100644 --- a/IngenicoConnectKit/Wrappers/AlamofireWrapper.swift +++ b/IngenicoConnectKit/Wrappers/AlamofireWrapper.swift @@ -5,11 +5,19 @@ // Created for Ingenico ePayments on 15/12/2016. // Copyright © 2016 Global Collect Services. All rights reserved. // -// swiftlint:disable function_parameter_count import Foundation import Alamofire +@available( + *, + deprecated, + message: + """ + In a future release, this class, its functions and its properties will become internal to the SDK. + Please use Session to interact with the API. + """ +) public class AlamofireWrapper { static let shared = AlamofireWrapper() @@ -30,6 +38,8 @@ public class AlamofireWrapper { } } + // swiftlint:disable function_parameter_count + @available(*, deprecated, message: "In a future release, this function will be removed.") public func getResponse(forURL URL: String, withParameters parameters: Parameters? = nil, headers: HTTPHeaders?, @@ -56,10 +66,51 @@ public class AlamofireWrapper { message: "Error while retrieving response for URL \(URL): \(error.localizedDescription)" ) failure(error) + } } + } + + internal func getResponse(forURL URL: String, + headers: HTTPHeaders?, + withParameters parameters: Parameters? = nil, + additionalAcceptableStatusCodes: IndexSet?, + success: @escaping ((responseObject: T?, statusCode: Int?)) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let acceptableStatusCodes = NSMutableIndexSet(indexesIn: NSRange(location: 200, length: 100)) + if let additionalAcceptableStatusCodes = additionalAcceptableStatusCodes { + acceptableStatusCodes.add(additionalAcceptableStatusCodes) } + + AF.request(URL, method: .get, parameters: parameters, headers: headers) + .validate(statusCode: acceptableStatusCodes) + .responseDecodable(of: T.self) { response in + if let error = response.error { + if error.responseCode != nil { + // Error related to unacceptable status code + // If decoding fails, return a failure instead of api failure + guard let apiError = + try? JSONDecoder().decode(ApiErrorResponse.self, from: response.data ?? Data()) else { + failure(error) + return + } + + apiFailure(apiError) + } else { + // Error unrelated to status codes + Macros.DLog( + message: "Error while retrieving response for URL \(URL): \(error.localizedDescription)" + ) + failure(error) + } + } else { + success((response.value, response.response?.statusCode)) + } + } } + @available(*, deprecated, message: "In a future release, this function will be removed.") public func postResponse(forURL URL: String, headers: HTTPHeaders?, withParameters parameters: Parameters?, @@ -86,7 +137,48 @@ public class AlamofireWrapper { message: "Error while retrieving response for URL \(URL): \(error.localizedDescription)" ) failure(error) + } } + } + + internal func postResponse(forURL URL: String, + headers: HTTPHeaders?, + withParameters parameters: Parameters?, + additionalAcceptableStatusCodes: IndexSet?, + success: @escaping ((responseObject: T?, statusCode: Int?)) -> Void, + failure: @escaping (_ error: Error) -> Void, + apiFailure: @escaping (_ errorResponse: ApiErrorResponse) -> Void + ) { + let acceptableStatusCodes = NSMutableIndexSet(indexesIn: NSRange(location: 200, length: 100)) + if let additionalAcceptableStatusCodes = additionalAcceptableStatusCodes { + acceptableStatusCodes.add(additionalAcceptableStatusCodes) } + + AF.request(URL, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers) + .validate(statusCode: acceptableStatusCodes) + .responseDecodable(of: T.self) { response in + if let error = response.error { + if error.responseCode != nil { + // Error related to unacceptable status code + // If decoding fails, return a failure instead of api failure + guard let apiError = + try? JSONDecoder().decode(ApiErrorResponse.self, from: response.data ?? Data()) else { + failure(error) + return + } + + apiFailure(apiError) + } else { + // Error unrelated to status codes + Macros.DLog( + message: "Error while retrieving response for URL \(URL): \(error.localizedDescription)" + ) + failure(error) + } + } else { + success((response.value, response.response?.statusCode)) + } + } } + // swiftlint:enable function_parameter_count } diff --git a/IngenicoConnectKitTests/AssetManagerTestCase.swift b/IngenicoConnectKitTests/AssetManagerTestCase.swift index f7d04b6..8a29b9b 100644 --- a/IngenicoConnectKitTests/AssetManagerTestCase.swift +++ b/IngenicoConnectKitTests/AssetManagerTestCase.swift @@ -20,40 +20,46 @@ class AssetManagerTestCase: XCTestCase { assetManager.fileManager = StubFileManager() assetManager.sdkBundle = StubBundle() - paymentItem = PaymentProduct(json: [ - "fields": [[:]], + let paymentItemJSON = Data(""" + { + "fields": [], "id": 1, "paymentMethod": "card", - "displayHints": [ + "displayHints": { "displayOrder": 20, "label": "Visa", "logo": "/this/is_a_test.png" - ] - ])! + }, + "usesRedirectionTo3rdParty": false + } + """.utf8) + paymentItem = try? JSONDecoder().decode(PaymentProduct.self, from: paymentItemJSON) - paymentItem.fields = PaymentProductFields() for index in 0..<5 { - let field = PaymentProductField(json: [ - "displayHints": [ - "formElement": [ + let fieldJSON = Data(""" + { + "displayHints": { + "displayOrder": 0, + "formElement": { "type": "text" - ], - "tooltip": [ + }, + "tooltip": { "image": "/tooltips/are_here.png" - ] - ], + } + }, "id": "field\(index)", "type": "numericstring" - ])! + } + """.utf8) + guard let field = try? JSONDecoder().decode(PaymentProductField.self, from: fieldJSON) else { + XCTFail("Not a valid PaymentProductField") + return + } - paymentItem.fields.paymentProductFields.append(field) + paymentItem.fields.paymentProductFields.append(field) } } - override func tearDown() { - super.tearDown() - } - func testLogoIdentifier() { var logoIdentifier = assetManager.logoIdentifier(with: paymentItem) XCTAssertEqual(logoIdentifier, "is_a", "Did not properly identify logo identifier with multiple underscores") diff --git a/IngenicoConnectKitTests/Base64/Base64TestCase.swift b/IngenicoConnectKitTests/Base64/Base64TestCase.swift index 8c128f1..b50361d 100644 --- a/IngenicoConnectKitTests/Base64/Base64TestCase.swift +++ b/IngenicoConnectKitTests/Base64/Base64TestCase.swift @@ -11,23 +11,15 @@ import XCTest class Base64TestCase: XCTestCase { - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - func testEncodeRevertable() { - let input = Data(bytes: [0, 255, 43, 1]) + let input = Data([0, 255, 43, 1]) let string = input.encode() let output = string.decode() XCTAssertEqual(output, input, "encoded and decoded data differs from the untransformed data") } func testURLEncodeRevertable() { - let input = Data(bytes: [0, 255, 43, 1]) + let input = Data([0, 255, 43, 1]) let string = input.base64URLEncode() let output = string.base64URLDecode() XCTAssertEqual(output, input, "URL encoded and URL decoded data differs from the untransformed data") diff --git a/IngenicoConnectKitTests/C2SCommunicatorConfigurationTestCase.swift b/IngenicoConnectKitTests/C2SCommunicatorConfigurationTestCase.swift index f9dcf3f..61d1e40 100644 --- a/IngenicoConnectKitTests/C2SCommunicatorConfigurationTestCase.swift +++ b/IngenicoConnectKitTests/C2SCommunicatorConfigurationTestCase.swift @@ -20,27 +20,27 @@ class C2SCommunicatorConfigurationTestCase: XCTestCase { C2SCommunicatorConfiguration( clientSessionId: "", customerId: "", - region: .EU, - environment: .sandbox, + baseURL: "https://ams1.sandbox.api-ingenico.com/client/v1", + assetBaseURL: "https://ams1.sandbox.api-ingenico.com/client/v1/assets", appIdentifier: "", - util: util + util: util, + loggingEnabled: false ) } - override func tearDown() { - super.tearDown() - } - func testBaseURL() { - XCTAssertEqual(configuration.baseURL, "c2sbaseurlbyregion", "Unexpected base URL") + XCTAssertEqual(configuration.baseURL, "https://ams1.sandbox.api-ingenico.com/client/v1", "Unexpected base URL") } func testAssetsBaseURL() { - XCTAssertEqual(configuration.assetsBaseURL, "assetsbaseurlbyregion", "Unexpected assets base URL") + XCTAssertEqual( + configuration.assetsBaseURL, + "https://ams1.sandbox.api-ingenico.com/client/v1/assets", + "Unexpected assets base URL" + ) } func testBase64EncodedClientMetaInfo() { - print(configuration.base64EncodedClientMetaInfo ?? "leeg") XCTAssertEqual( configuration.base64EncodedClientMetaInfo, "base64encodedclientmetainfo", diff --git a/IngenicoConnectKitTests/C2SCommunicatorTestCase.swift b/IngenicoConnectKitTests/C2SCommunicatorTestCase.swift index 9f4035a..c20eac3 100644 --- a/IngenicoConnectKitTests/C2SCommunicatorTestCase.swift +++ b/IngenicoConnectKitTests/C2SCommunicatorTestCase.swift @@ -17,93 +17,77 @@ class C2SCommunicatorTestCase: XCTestCase { var configuration: C2SCommunicatorConfiguration! let context = PaymentContext( - amountOfMoney: PaymentAmountOfMoney(totalAmount: 3, currencyCode: .EUR), + amountOfMoney: PaymentAmountOfMoney(totalAmount: 3, currencyCode: "EUR"), isRecurring: true, - countryCode: .NL + countryCode: "NL" ) var applePaymentProduct: BasicPaymentProduct! - var androidPaymentProduct: BasicPaymentProduct! override func setUp() { super.setUp() - applePaymentProduct = BasicPaymentProduct(json: [ - "fields": [[:]], - "id": Int(SDKConstants.kApplePayIdentifier)!, + let applePaymentProductJSON = Data(""" + { + "fields": [], + "id": \(Int(SDKConstants.kApplePayIdentifier)!), "paymentMethod": "card", - "displayHints": [ + "displayHints": { "displayOrder": 20, "label": "Visa", "logo": "/templates/master/global/css/img/ppimages/pp_logo_1_v1.png" - ] - ])! + }, + "usesRedirectionTo3rdParty": false + } + """.utf8) - androidPaymentProduct = BasicPaymentProduct(json: [ - "fields": [[:]], - "id": Int(SDKConstants.kAndroidPayIdentifier)!, + applePaymentProduct = try? JSONDecoder().decode(BasicPaymentProduct.self, from: applePaymentProductJSON) + + let androidPaymentProductJSON = Data(""" + { + "fields": [], + "id": \(Int(SDKConstants.kAndroidPayIdentifier)!), "paymentMethod": "card", - "displayHints": [ + "displayHints": { "displayOrder": 20, "label": "Visa", "logo": "/templates/master/global/css/img/ppimages/pp_logo_1_v1.png" - ] - ])! + }, + "usesRedirectionTo3rdParty": false + } + """.utf8) + + _ = try? JSONDecoder().decode(BasicPaymentProduct.self, from: androidPaymentProductJSON) configuration = C2SCommunicatorConfiguration( clientSessionId: "1", customerId: "1", - region: Region.EU, - environment: Environment.sandbox, + baseURL: "https://ams1.sandbox.api-ingenico.com/client/v1", + assetBaseURL: "https://ams1.sandbox.api-ingenico.com/client/v1", appIdentifier: "", ipAddress: "" ) communicator = C2SCommunicator(configuration: configuration) } - override func tearDown() { - super.tearDown() - } - - func testFilterAndroidPayFromProducts() { - var paymentProducts = BasicPaymentProducts() - paymentProducts.paymentProducts = [applePaymentProduct, androidPaymentProduct] - paymentProducts = communicator.filterAndroidPayFromProducts(paymentProducts: paymentProducts) - - var correct = false - if paymentProducts.paymentProducts.count == 1 { - if let product = paymentProducts.paymentProducts.first { - if product === applePaymentProduct { - correct = true - } - } - } - - XCTAssert(correct, "filterAndroidPayFromProduct did not filter out Android properly") - } - func testApplePayAvailabilityWithoutApplePay() { let paymentProducts = BasicPaymentProducts() - let androidProduct = PaymentProduct(json: [ - "fields": [[:]], - "id": Int(SDKConstants.kAndroidPayIdentifier)!, - "paymentMethod": "card", - "displayHints": [ - "displayOrder": 20, - "label": "Visa", - "logo": "/templates/master/global/css/img/ppimages/pp_logo_1_v1.png" - ] - ])! - paymentProducts.paymentProducts.append(androidProduct) - let expectation = self.expectation(description: "Response provided") _ = communicator.checkApplePayAvailability(with: paymentProducts, for: context, success: { expectation.fulfill() }, failure: { (error) in - XCTFail("Unexpected failure while testing checkApplePayAvailability: \(error.localizedDescription)") + XCTFail( + "Unexpected failure while testing applePayAvailabilityWithoutApplePay: \(error.localizedDescription)" + ) + }, apiFailure: { errorResponse in + XCTFail( + """ + Unexpected failure while testing applePayAvailabilityWithoutApplePay: \(errorResponse.errors[0].message) + """ + ) }) waitForExpectations(timeout: 3) { error in @@ -119,7 +103,7 @@ class C2SCommunicatorTestCase: XCTestCase { "networks": [ "amex", "discover", "masterCard", "visa" ] ] return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"] @@ -136,6 +120,8 @@ class C2SCommunicatorTestCase: XCTestCase { }, failure: { (error) in XCTFail("Unexpected failure while testing checkApplePayAvailability: \(error.localizedDescription)") + }, apiFailure: { errorResponse in + XCTFail("Unexpected failure while testing checkApplePayAvailability: \(errorResponse.errors[0].message)") }) waitForExpectations(timeout: 3) { error in @@ -194,7 +180,7 @@ class C2SCommunicatorTestCase: XCTestCase { ] ] return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"] @@ -213,6 +199,8 @@ class C2SCommunicatorTestCase: XCTestCase { expectation.fulfill() }, failure: { error in XCTFail("Unexpected failure while testing paymentProductForContext: \(error.localizedDescription)") + }, apiFailure: { errorResponse in + XCTFail("Unexpected failure while testing paymentProductForContext: \(errorResponse.errors[0].message)") }) waitForExpectations(timeout: 3) { error in @@ -235,7 +223,7 @@ class C2SCommunicatorTestCase: XCTestCase { ] // swiftlint:enable line_length return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"] @@ -263,6 +251,8 @@ class C2SCommunicatorTestCase: XCTestCase { // swiftlint:enable line_length }, failure: { (error) in XCTFail("Unexpected failure while testing publicKey: \(error.localizedDescription)") + }, apiFailure: { errorResponse in + XCTFail("Unexpected failure while testing publicKey: \(errorResponse.errors[0].message)") }) waitForExpectations(timeout: 3) { error in @@ -282,12 +272,14 @@ class C2SCommunicatorTestCase: XCTestCase { "label": "Cards", "logo": "/templates/master/global/css/img/ppimages/group-card.png" ], - "id": "cards" + "id": "cards", + "deviceFingerprintEnabled": true, + "allowsInstallments": false ] ] ] return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"] @@ -313,6 +305,12 @@ class C2SCommunicatorTestCase: XCTestCase { }, failure: { (error) in XCTFail("Unexpected failure while testing paymentProductGroupsForContext: \(error.localizedDescription)") + }, apiFailure: { errorResponse in + XCTFail( + """ + Unexpected failure while testing paymentProductGroupsForContext: \(errorResponse.errors[0].message) + """ + ) }) waitForExpectations(timeout: 3) { error in @@ -393,7 +391,7 @@ class C2SCommunicatorTestCase: XCTestCase { "paymentProductGroup": "cards" ] as [String: Any] return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"] @@ -471,6 +469,8 @@ class C2SCommunicatorTestCase: XCTestCase { }, failure: { (error) in XCTFail("Unexpected failure while testing paymentProductWithId: \(error.localizedDescription)") + }, apiFailure: { errorResponse in + XCTFail("Unexpected failure while testing paymentProductWithId: \(errorResponse.errors[0].message)") }) waitForExpectations(timeout: 3) { error in @@ -574,10 +574,12 @@ class C2SCommunicatorTestCase: XCTestCase { "id": "cvv", "type": "numericstring" ]], - "id": "cards" + "id": "cards", + "deviceFingerprintEnabled": true, + "allowsInstallments": false ] as [String: Any] return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"] @@ -592,6 +594,13 @@ class C2SCommunicatorTestCase: XCTestCase { }, failure: { (_) in XCTFail("Product group with id failed.") expectation.fulfill() + }, apiFailure: { errorResponse in + XCTFail( + """ + Unexpected failure while testing testPaymentProductNetworksForProductId: + \(errorResponse.errors[0].message) + """ + ) }) waitForExpectations(timeout: 3) { error in if let error = error { @@ -618,7 +627,7 @@ class C2SCommunicatorTestCase: XCTestCase { "networks": ["Visa", "MasterCard"] ] as [String: Any] return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"] @@ -635,7 +644,15 @@ class C2SCommunicatorTestCase: XCTestCase { "Unexpected failure while testing testPaymentProductNetworksForProductId: \(error.localizedDescription)" ) expectation.fulfill() + }, apiFailure: { errorResponse in + XCTFail( + """ + Unexpected failure while testing testPaymentProductNetworksForProductId: + \(errorResponse.errors[0].message) + """ + ) }) + waitForExpectations(timeout: 3) { error in if let error = error { print("Timeout error: \(error.localizedDescription)") @@ -656,7 +673,7 @@ class C2SCommunicatorTestCase: XCTestCase { "paymentProductId": 3 ] as [String: Any] return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"] @@ -671,7 +688,7 @@ class C2SCommunicatorTestCase: XCTestCase { success: { (gciinDetailsResponse) in expectation.fulfill() - XCTAssertEqual(gciinDetailsResponse.countryCode, .RU, "Received countrycode not as expected") + XCTAssertEqual(gciinDetailsResponse.countryCodeString, "RU", "Received countrycode not as expected") XCTAssertEqual(gciinDetailsResponse.paymentProductId, "3", "Received paymentProductId not as expected") }, failure: { (error) in @@ -681,6 +698,14 @@ class C2SCommunicatorTestCase: XCTestCase { \(error.localizedDescription) """ ) + }, + apiFailure: { errorResponse in + XCTFail( + """ + Unexpected failure while testing paymentProductWithIdPartialCreditCard: + \(errorResponse.errors[0].message) + """ + ) } ) @@ -697,7 +722,7 @@ class C2SCommunicatorTestCase: XCTestCase { "convertedAmount": 138 ] return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"] @@ -706,15 +731,24 @@ class C2SCommunicatorTestCase: XCTestCase { let expectation = self.expectation(description: "Response provided") - communicator.convert(amountInCents: 3, source: "EUR", target: "USD", success: { (amount) in - expectation.fulfill() + communicator.convert( + amountInCents: 3, + source: "EUR", + target: "USD", + success: { (convertedAmountResponse: ConvertedAmountResponse) in + expectation.fulfill() - XCTAssertEqual(amount, 138, "Received convertedAmount not as expected") - }, failure: { (error) in - XCTFail( - "Unexpected failure while testing convertAmount: \(String(describing: error?.localizedDescription))" - ) - }) + XCTAssertEqual(convertedAmountResponse.convertedAmount, 138, "Received convertedAmount not as expected") + }, + failure: { (error) in + XCTFail( + "Unexpected failure while testing convertAmount: \(String(describing: error?.localizedDescription))" + ) + }, + apiFailure: { errorResponse in + XCTFail("Unexpected failure while testing convertAmount: \(errorResponse.errors[0].message)") + } + ) waitForExpectations(timeout: 3) { error in if let error = error { @@ -725,16 +759,17 @@ class C2SCommunicatorTestCase: XCTestCase { func testConvertAmountNotWorking() { stub(condition: isHost("ams1.sandbox.api-ingenico.com")) { _ in - return OHHTTPStubsResponse(jsonObject: [], statusCode: 200, headers: ["Content-Type": "application/json"]) + return HTTPStubsResponse(jsonObject: [], statusCode: 200, headers: ["Content-Type": "application/json"]) } let expectation = self.expectation(description: "Response provided") - communicator.convert(amountInCents: 3, source: "EUR", target: "USD", success: { (_) in + communicator.convert(amountInCents: 3, source: "EUR", target: "USD", success: { (_: ConvertedAmountResponse) in expectation.fulfill() - XCTFail("Unexpected success") - }, failure: { (_) in + }, failure: { _ in + expectation.fulfill() + }, apiFailure: { _ in expectation.fulfill() }) @@ -761,7 +796,7 @@ class C2SCommunicatorTestCase: XCTestCase { ] ] ] return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"] @@ -794,6 +829,8 @@ class C2SCommunicatorTestCase: XCTestCase { expectation.fulfill() }, failure: { (error) in XCTFail("Unexpected failure while testing directoryForPaymentProductId: \(error.localizedDescription)") + }, apiFailure: { errorResponse in + XCTFail("Unexpected failure while testing directoryForPaymentProductId: \(errorResponse.errors[0].message)") }) waitForExpectations(timeout: 3) { error in @@ -820,7 +857,7 @@ class C2SCommunicatorTestCase: XCTestCase { ] ] ] return - OHHTTPStubsResponse( + HTTPStubsResponse( jsonObject: response, statusCode: 403, headers: ["Content-Type": "application/json"] @@ -839,6 +876,13 @@ class C2SCommunicatorTestCase: XCTestCase { "Response validation failed expected." ) expectation.fulfill() + }, apiFailure: { errorResponse in + XCTAssertEqual( + errorResponse.errors[0].message, + "Response status code was unacceptable: 403.", + "Response validation failed expected." + ) + expectation.fulfill() }) waitForExpectations(timeout: 3) { error in diff --git a/IngenicoConnectKitTests/ClientApiTestCase.swift b/IngenicoConnectKitTests/ClientApiTestCase.swift new file mode 100644 index 0000000..7b84728 --- /dev/null +++ b/IngenicoConnectKitTests/ClientApiTestCase.swift @@ -0,0 +1,899 @@ +// +// ClientApiTestCase.swift +// IngenicoConnectKitTests +// +// Created for Ingenico ePayments on 28/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import XCTest +import OHHTTPStubs + +@testable import IngenicoConnectKit + +class ClientApiTestCase: XCTestCase { + let host = "ams1.sandbox.api-ingenico.com" + + let sdkConfiguration = ConnectSDKConfiguration( + sessionConfiguration: SessionConfiguration( + clientSessionId: "client-session-id", + customerId: "customer-id", + clientApiUrl: "https://ams1.sandbox.api-ingenico.com/client/v1", + assetUrl: "https://ams1.sandbox.api-ingenico.com/client/v1/assets" + ), + enableNetworkLogs: false, + applicationId: "application-id", + ipAddress: "ip-address", + preLoadImages: true + ) + + let paymentConfiguration = PaymentConfiguration( + paymentContext: PaymentContext( + amountOfMoney: PaymentAmountOfMoney(totalAmount: 3, currencyCode: "EUR"), + isRecurring: true, + countryCode: "NL" + ), + groupPaymentProducts: true + ) + + var stubClientApi: StubClientApi! + + override func setUp() { + super.setUp() + + stubClientApi = StubClientApi(sdkConfiguration: sdkConfiguration, paymentConfiguration: paymentConfiguration) + } + + func testPaymentProducts() { + stub(condition: isHost(host) && isPath("/client/v1/customer-id/products") && isMethodGET()) { _ in + let response = [ + "paymentProducts": [ + [ + "allowsRecurring": true, + "allowsTokenization": true, + "displayHints": [ + "displayOrder": 20, + "label": "Visa", + "logo": "https://example.com/templates/master/global/css/img/ppimages/pp_logo_1_v1.png" + ], + "id": 1, + "maxAmount": 1000000, + "mobileIntegrationLevel": "OPTIMISED_SUPPORT", + "paymentMethod": "card", + "paymentProductGroup": "cards" + ], + [ + "allowsRecurring": true, + "allowsTokenization": true, + "displayHints": [ + "displayOrder": 19, + "label": "American Express", + "logo": "https://example.com/templates/master/global/css/img/ppimages/pp_logo_2_v1.png" + ], + "id": 2, + "maxAmount": 1000000, + "mobileIntegrationLevel": "OPTIMISED_SUPPORT", + "paymentMethod": "card", + "paymentProductGroup": "cards" + ], + [ + "allowsRecurring": true, + "allowsTokenization": true, + "displayHints": [ + "displayOrder": 18, + "label": "MasterCard", + "logo": "https://example.com/templates/master/global/css/img/ppimages/pp_logo_3_v1.png" + ], + "id": 3, + "maxAmount": 1000000, + "mobileIntegrationLevel": "OPTIMISED_SUPPORT", + "paymentMethod": "card", + "paymentProductGroup": "cards" + ] + ] + ] + return + HTTPStubsResponse( + jsonObject: response, + statusCode: 200, + headers: ["Content-Type": "application/json"] + ) + } + + let expectation = self.expectation(description: "Response provided") + stubClientApi.paymentProducts( + success: { _ in + expectation.fulfill() + }, + failure: { error in + XCTFail("Unexpected failure during testPaymentProducts: \(error.localizedDescription)") + expectation.fulfill() + }, + apiFailure: { errorResponse in + XCTFail("Unexpected api failure during testPaymentProducts: \(errorResponse.errors[0].message)") + expectation.fulfill() + } + ) + + waitForExpectations(timeout: 3) { error in + if let error = error { + print("Timeout error: \(error.localizedDescription)") + } + } + } + + func testPaymentProductNetworks() { + stub(condition: isHost(host)) { _ in + let response = [ + "networks": ["Visa", "MasterCard"] + ] as [String: Any] + return + HTTPStubsResponse( + jsonObject: response, + statusCode: 200, + headers: ["Content-Type": "application/json"] + ) + } + + let expectation = self.expectation(description: "Response provided") + stubClientApi.paymentProductNetworks( + forProduct: "1", + success: { paymentProductNetworks in + self.check(paymentProductNetworks: paymentProductNetworks) + + expectation.fulfill() + }, + failure: { error in + XCTFail( + "Unexpected failure during testPaymentProductNetworks: \(error.localizedDescription)" + ) + expectation.fulfill() + }, + apiFailure: { errorResponse in + XCTFail( + "Unexpected api failure during testPaymentProductNetworks: \(errorResponse.errors[0].message)" + ) + expectation.fulfill() + } + ) + + waitForExpectations(timeout: 3) { error in + if let error = error { + print("Timeout error: \(error.localizedDescription)") + } + } + } + + private func check(paymentProductNetworks: PaymentProductNetworks) { + XCTAssertEqual(paymentProductNetworks.paymentProductNetworks.count, 2) + XCTAssertEqual(paymentProductNetworks.paymentProductNetworks[0].rawValue, "Visa") + XCTAssertEqual(paymentProductNetworks.paymentProductNetworks[1].rawValue, "MasterCard") + } + + func testPaymentProductWithId() { + stub(condition: isHost(host)) { _ in + let response = [ + "paymentProductGroups": [ + [ + "displayHints": [ + "displayOrder": 20, + "label": "Cards", + "logo": "https://example.com/templates/master/global/css/img/ppimages/group-card.png" + ], + "id": "cards" + ] + ], + "allowsRecurring": true, + "allowsTokenization": true, + "displayHints": [ + "displayOrder": 20, + "label": "Visa", + "logo": "https://example.com/templates/master/global/css/img/ppimages/pp_logo_1_v1.png" + ], + "fields": [ + [ + "dataRestrictions": [ + "isRequired": true, + "validators": [ + "length": [ + "maxLength": 19, + "minLength": 12 + ], + "luhn": [ + + ], "expirationDate": [ + + ], + "regularExpression": [ + "regularExpression": "(?:0[1-9]|1[0-2])[0-9]{2}" + ] + ] + ], + "displayHints": [ + "displayOrder": 10, + "formElement": [ + "type": "currency" + ], + "label": "Card number:", + "mask": "{{9999}} {{9999}} {{9999}} {{9999}} {{999}}", + "obfuscate": false, + "placeholderLabel": "**** **** **** ****", + "preferredInputType": "IntegerKeyboard" + ], + "id": "cardNumber", + "type": "numericstring" + ] + ], + "id": 1, + "maxAmount": 1000000, + "mobileIntegrationLevel": "OPTIMISED_SUPPORT", + "paymentMethod": "card", + "paymentProductGroup": "cards" + ] as [String: Any] + return + HTTPStubsResponse( + jsonObject: response, + statusCode: 200, + headers: ["Content-Type": "application/json"] + ) + } + + let expectation = self.expectation(description: "Response provided") + + stubClientApi.paymentProduct( + withId: "1", + success: { product in + self.check(paymentProduct: product) + + // Check initializeImages + for index in 0.. Void) { + // Create an image existing of only a colour + // This is done just to have an image available + let image = UIColor.blue.image() + + completion(image.pngData(), nil, nil) + } +} + +extension UIColor { + func image() -> UIImage { + let size = CGSize(width: 1, height: 1) + + return UIGraphicsImageRenderer(size: size).image { rendererContext in + self.setFill() + rendererContext.fill(CGRect(origin: .zero, size: size)) + } + } +} diff --git a/IngenicoConnectKitTests/Stubs/StubSession.swift b/IngenicoConnectKitTests/Stubs/StubSession.swift new file mode 100644 index 0000000..02ad03f --- /dev/null +++ b/IngenicoConnectKitTests/Stubs/StubSession.swift @@ -0,0 +1,20 @@ +// +// StubSession.swift +// IngenicoConnectKitTests +// +// Created for Ingenico ePayments on 28/11/2023. +// Copyright © 2023 Global Collect Services. All rights reserved. +// + +import UIKit +@testable import IngenicoConnectKit + +class StubSession: IngenicoConnectKit.Session { + override func getLogoByStringURL(from url: String, completion: @escaping (Data?, URLResponse?, Error?) -> Void) { + // Create an image existing of only a colour + // This is done just to have an image available + let image = UIColor.blue.image() + + completion(image.pngData(), nil, nil) + } +} diff --git a/IngenicoConnectKitTests/Stubs/StubUtil.swift b/IngenicoConnectKitTests/Stubs/StubUtil.swift index bfc3da4..a015cbb 100644 --- a/IngenicoConnectKitTests/Stubs/StubUtil.swift +++ b/IngenicoConnectKitTests/Stubs/StubUtil.swift @@ -9,15 +9,6 @@ @testable import IngenicoConnectKit class StubUtil: Util { - - override func C2SBaseURL(by region: Region, environment: Environment) -> String { - return "c2sbaseurlbyregion" - } - - override func assetsBaseURL(by region: Region, environment: Environment) -> String { - return "assetsbaseurlbyregion" - } - override func base64EncodedClientMetaInfo(withAppIdentifier appIdentifier: String?, ipAddress: String?) -> String? { return "base64encodedclientmetainfo" } diff --git a/IngenicoConnectKitTests/UtilTestCase.swift b/IngenicoConnectKitTests/UtilTestCase.swift index 881d636..0a1f01a 100644 --- a/IngenicoConnectKitTests/UtilTestCase.swift +++ b/IngenicoConnectKitTests/UtilTestCase.swift @@ -12,14 +12,6 @@ import XCTest class UtilTestCase: XCTestCase { let util = Util.shared - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - func testBase64EncodedClientMetaInfo() { if let info = util.base64EncodedClientMetaInfo { let decodedInfo = info.decode() @@ -30,7 +22,10 @@ class UtilTestCase: XCTestCase { } XCTAssertEqual(JSON["deviceBrand"], "Apple", "Incorrect device brand in meta info") - XCTAssertEqual(JSON["deviceType"], "x86_64", "Incorrect device type in meta info") + XCTAssert( + JSON["deviceType"] == "arm64" || JSON["deviceType"] == "x86_64", + "Incorrect device type in meta info" + ) } } diff --git a/IngenicoConnectKitTests/Wrappers/AlamofireWrapperTestCase.swift b/IngenicoConnectKitTests/Wrappers/AlamofireWrapperTestCase.swift index f7bcf8c..0ee8537 100644 --- a/IngenicoConnectKitTests/Wrappers/AlamofireWrapperTestCase.swift +++ b/IngenicoConnectKitTests/Wrappers/AlamofireWrapperTestCase.swift @@ -13,151 +13,184 @@ import OHHTTPStubs @testable import IngenicoConnectKit class AlamofireWrapperTestCase: XCTestCase { - let region = Region.EU - let environment = Environment.sandbox - var baseURL: String? - - let host = "ams1.sandbox.api-ingenico.com" - let merchantId = 1234 - - override func setUp() { - super.setUp() - - baseURL = Util.shared.C2SBaseURL(by: region, environment: environment) - - // Stub GET request - stub(condition: isHost("\(host)") && isPath("/client/v1/\(merchantId)/crypto/publickey") && isMethodGET()) { _ in - let response = [ - "errors": [[ - "code": 9002, - "message": "MISSING_OR_INVALID_AUTHORIZATION" - ]] - ] - return OHHTTPStubsResponse(jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"]) - } + let baseURL = "https://ams1.sandbox.api-ingenico.com/client/v1" + + let host = "ams1.sandbox.api-ingenico.com" + let merchantId = 1234 + + override func setUp() { + super.setUp() + + // Stub GET request + stub( + condition: isHost("\(host)") && + isPath("/client/v1/\(merchantId)/crypto/publickey") && + isMethodGET() + ) { _ in + let response = [ + "errorId": "id", + "errors": [[ + "category": "Test failure", + "code": "9002", + "httpStatusCode": 200, + "id": "1", + "message": "MISSING_OR_INVALID_AUTHORIZATION" + ]] + ] + return + HTTPStubsResponse(jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"]) + } - // Stub POST request - stub(condition: isHost("\(host)") && isPath("/client/v1/\(merchantId)/sessions") && isMethodPOST()) { _ in - let response = [ - "errors": [[ - "code": 9002, - "message": "MISSING_OR_INVALID_AUTHORIZATION" - ]] - ] - return OHHTTPStubsResponse(jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"]) - } + // Stub POST request + stub(condition: isHost("\(host)") && isPath("/client/v1/\(merchantId)/sessions") && isMethodPOST()) { _ in + let response = [ + "errorId": "id", + "errors": [[ + "category": "Test failure", + "code": "9002", + "httpStatusCode": 200, + "id": "1", + "message": "MISSING_OR_INVALID_AUTHORIZATION" + ]] + ] + return + HTTPStubsResponse(jsonObject: response, statusCode: 200, headers: ["Content-Type": "application/json"]) + } - stub(condition: isHost("\(host)") && isPath("/client/v1/noerror") && isMethodGET()) { _ in - return OHHTTPStubsResponse(jsonObject: [], statusCode: 401, headers: ["Content-Type": "application/json"]) - } + stub(condition: isHost("\(host)") && isPath("/client/v1/noerror") && isMethodGET()) { _ in + let response = [ + "convertedAmount": 123 + ] + return + HTTPStubsResponse(jsonObject: response, statusCode: 401, headers: ["Content-Type": "application/json"]) + } - stub(condition: isHost("\(host)") && isPath("/client/v1/error") && isMethodGET()) { _ in - return OHHTTPStubsResponse(jsonObject: [], statusCode: 500, headers: ["Content-Type": "application/json"]) + stub(condition: isHost("\(host)") && isPath("/client/v1/error") && isMethodGET()) { _ in + return HTTPStubsResponse(jsonObject: [], statusCode: 500, headers: ["Content-Type": "application/json"]) + } } - } - - func testPost() { - let sessionsURL = "\(baseURL!)/\(merchantId)/sessions" - let expectation = self.expectation(description: "Response provided") - - AlamofireWrapper.shared.postResponse( - forURL: sessionsURL, - headers: nil, - withParameters: nil, - additionalAcceptableStatusCodes: nil, - success: { responseObject in - self.assertErrorResponse(responseObject, expectation: expectation) - }, - failure: { error in - XCTFail("Unexpected failure while testing POST request: \(error.localizedDescription)") + + func testPost() { + let sessionsURL = "\(baseURL)/\(merchantId)/sessions" + let expectation = self.expectation(description: "Response provided") + + let successHandler: (ApiErrorResponse?, Int?) -> Void = { (responseObject, _) -> Void in + self.assertApiError(responseObject, expectation: expectation) } - ) - waitForExpectations(timeout: 3) { error in - if let error = error { - print("Timeout error: \(error.localizedDescription)") - } + AlamofireWrapper.shared.postResponse( + forURL: sessionsURL, + headers: nil, + withParameters: nil, + additionalAcceptableStatusCodes: nil, + success: successHandler, + failure: { error in + XCTFail("Unexpected failure while testing POST request: \(error.localizedDescription)") + }, + apiFailure: { errorResponse in + XCTFail("Unexpected failure while testing POST request: \(errorResponse.errors[0].message)") + } + ) + + waitForExpectations(timeout: 3) { error in + if let error = error { + print("Timeout error: \(error.localizedDescription)") + } + } } - } - - func testGet() { - let publicKeyURL = "\(baseURL!)/\(merchantId)/crypto/publickey" - let expectation = self.expectation(description: "Response provided") - - AlamofireWrapper.shared.getResponse( - forURL: publicKeyURL, - headers: nil, - additionalAcceptableStatusCodes: nil, - success: { responseObject in - self.assertErrorResponse(responseObject, expectation: expectation) - }, - failure: { error in - XCTFail("Unexpected failure while testing GET request: \(error.localizedDescription)") + + func testGet() { + let publicKeyURL = "\(baseURL)/\(merchantId)/crypto/publickey" + let expectation = self.expectation(description: "Response provided") + + let successHandler: (ApiErrorResponse?, Int?) -> Void = { (responseObject, _) -> Void in + self.assertApiError(responseObject, expectation: expectation) } - ) - waitForExpectations(timeout: 3) { error in - if let error = error { - print("Timeout error: \(error.localizedDescription)") - } + AlamofireWrapper.shared.getResponse( + forURL: publicKeyURL, + headers: nil, + additionalAcceptableStatusCodes: nil, + success: successHandler, + failure: { error in + XCTFail("Unexpected failure while testing GET request: \(error.localizedDescription)") + }, + apiFailure: { errorResponse in + XCTFail("Unexpected failure while testing POST request: \(errorResponse.errors[0].message)") + } + ) + + waitForExpectations(timeout: 3) { error in + if let error = error { + print("Timeout error: \(error.localizedDescription)") + } + } } - } - - func testAdditionalStatusCodeAcceptance() { - let publicKeyURL = "\(baseURL!)/noerror" - let expectation = self.expectation(description: "Response provided") - let additionalAcceptableStatusCodes: IndexSet = [401] - - AlamofireWrapper.shared.getResponse( - forURL: publicKeyURL, - headers: nil, - additionalAcceptableStatusCodes: additionalAcceptableStatusCodes, - success: { _ in - expectation.fulfill() - }, - failure: { error in - XCTFail("Additional status code did not accept: \(error.localizedDescription)") + + func testAdditionalStatusCodeAcceptance() { + let publicKeyURL = "\(baseURL)/noerror" + let expectation = self.expectation(description: "Response provided") + let additionalAcceptableStatusCodes: IndexSet = [401] + + let successHandler: (ConvertedAmountResponse?, Int?) -> Void = { (_, _) -> Void in + expectation.fulfill() } - ) - waitForExpectations(timeout: 3) { error in - if let error = error { - print("Timeout error: \(error.localizedDescription)") - } + AlamofireWrapper.shared.getResponse( + forURL: publicKeyURL, + headers: nil, + additionalAcceptableStatusCodes: additionalAcceptableStatusCodes, + success: successHandler, + failure: { error in + XCTFail("Additional status code did not accept: \(error.localizedDescription)") + }, + apiFailure: { errorResponse in + XCTFail("Additional status code did not accept: \(errorResponse.errors[0].message)") + } + ) + + waitForExpectations(timeout: 3) { error in + if let error = error { + print("Timeout error: \(error.localizedDescription)") + } + } } - } - - func testRequestFailure() { - let customerId = "1234" - let publicKeyURL = "\(baseURL!)/\(customerId)/error" - let expectation = self.expectation(description: "Response provided") - - AlamofireWrapper.shared.getResponse( - forURL: publicKeyURL, - headers: nil, - additionalAcceptableStatusCodes: nil, - success: { _ in - XCTFail("Failure should have been called") - }, - failure: { _ in - expectation.fulfill() + + func testRequestFailure() { + let customerId = "1234" + let publicKeyURL = "\(baseURL)/\(customerId)/error" + let expectation = self.expectation(description: "Response provided") + + let successHandler: (ApiErrorResponse?, Int?) -> Void = { (_, _) -> Void in + XCTFail("Failure should have been called") } - ) - waitForExpectations(timeout: 3) { error in - if let error = error { - print("Timeout error: \(error.localizedDescription)") - } + AlamofireWrapper.shared.getResponse( + forURL: publicKeyURL, + headers: nil, + additionalAcceptableStatusCodes: nil, + success: successHandler, + failure: { _ in + expectation.fulfill() + }, + apiFailure: { _ in + expectation.fulfill() + } + ) + + waitForExpectations(timeout: 3) { error in + if let error = error { + print("Timeout error: \(error.localizedDescription)") + } + } } - } - - fileprivate func assertErrorResponse(_ errorResponse: [String: Any]?, expectation: XCTestExpectation) { - if let errorResponse = errorResponse, - let errors = errorResponse["errors"] as? [[String: Any]], - let firstError = errors.first { - XCTAssertEqual(firstError["code"] as? Int, 9002) - XCTAssertEqual(firstError["message"] as? String, "MISSING_OR_INVALID_AUTHORIZATION") - expectation.fulfill() + + fileprivate func assertApiError(_ apiError: ApiErrorResponse?, expectation: XCTestExpectation) { + if let apiError { + let apiErrorItem = apiError.errors[0] + XCTAssertEqual(apiErrorItem.code, "9002") + XCTAssertEqual(apiErrorItem.message, "MISSING_OR_INVALID_AUTHORIZATION") + expectation.fulfill() + } } - } } diff --git a/README.md b/README.md index 3129e97..76c9fee 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,11 @@ $ github "Ingenico-ePayments/connect-sdk-client-swift" Afterwards, run the following command: ``` -$ carthage update +$ carthage update --platform ios --use-xcframeworks ``` +Navigate to the `Carthage/Build` directory, which was created in the same directory as where the `.xcodeproj` or `.xcworkspace` is. Inside this directory the `.xcframework` bundle is stored. Drag the `.xcframework` into the "Framework, Libraries and Embedded Content" section of the desired target. Make sure that it is set to "Embed & Sign". + Run the SDK locally ------------