From d3bb66699961c18b6b4d34edcf02a86589b25bdf Mon Sep 17 00:00:00 2001 From: Adam Gask Date: Tue, 7 Nov 2023 09:01:17 +0000 Subject: [PATCH] Added connectionOptions --- .../CheckoutViewController.swift | 5 +- README.md | 1 + gr4vy-iOS.xcodeproj/project.pbxproj | 4 + gr4vy-iOS/Gr4vy.swift | 4 +- gr4vy-iOS/Gr4vyUtility.swift | 4 + gr4vy-iOS/Models/Gr4vyConnectionOptions.swift | 35 ++++++ gr4vy-iOS/Models/Gr4vySetup.swift | 4 +- gr4vy-iOSTests/gr4vy_iOSTests.swift | 106 ++++++++++++++++++ gr4vy-ios.podspec | 2 +- 9 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 gr4vy-iOS/Models/Gr4vyConnectionOptions.swift diff --git a/Gr4vy UIKit Sample App/CheckoutViewController.swift b/Gr4vy UIKit Sample App/CheckoutViewController.swift index 12b3333..8d07017 100644 --- a/Gr4vy UIKit Sample App/CheckoutViewController.swift +++ b/Gr4vy UIKit Sample App/CheckoutViewController.swift @@ -62,7 +62,10 @@ class CheckoutViewController: UIViewController { borderWidths: Gr4vyBorderWidths(container: "thin", input: "thin"), radii: Gr4vyRadii(container: "subtle", input: "subtle"), shadows: Gr4vyShadows(focusRing: "0 0 0 2px #ffffff, 0 0 0 4px #4844ff")), - debugMode: true + connectionOptions: Gr4vyConnectionOptions(data: [ + "forter-anti-fraud": [ + "is_guest_buyer": false]]), + debugMode: true ) else { print("Unable to load Gr4vy") return diff --git a/README.md b/README.md index 5edf859..5f70ebe 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ These are the parameteres available on the `launch` method: | `requireSecurityCode`| `Optional` | An optional boolean which forces security code to be prompted for stored card payments. | | `shippingDetailsId`| `Optional` | An optional unique identifier of a set of shipping details stored for the buyer. | | `merchantAccountId`| `Optional` | An optional merchant account ID. | +| `connectionOptions`| `Optional` | An optional set of options passed to a connection when processing a transaction (see https://docs.gr4vy.com/reference#operation/authorize-new-transaction) | | `debugMode`| `Optional` | `true`, `false`. Defaults to `false`, this prints to the console. | | `onEvent` | `Optional` | **Please see below for more details.** | diff --git a/gr4vy-iOS.xcodeproj/project.pbxproj b/gr4vy-iOS.xcodeproj/project.pbxproj index 0df5e30..81413bb 100644 --- a/gr4vy-iOS.xcodeproj/project.pbxproj +++ b/gr4vy-iOS.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 59160F7A2AF6EDDD00143DA2 /* Gr4vyConnectionOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59160F792AF6EDDD00143DA2 /* Gr4vyConnectionOptions.swift */; }; 592C4BBC28BF9AEE00063AC7 /* Gr4vy_SwiftUI_Sample_App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 592C4BBB28BF9AEE00063AC7 /* Gr4vy_SwiftUI_Sample_App.swift */; }; 592C4BC028BF9AEF00063AC7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 592C4BBF28BF9AEF00063AC7 /* Assets.xcassets */; }; 592C4BC328BF9AEF00063AC7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 592C4BC228BF9AEF00063AC7 /* Preview Assets.xcassets */; }; @@ -96,6 +97,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 59160F792AF6EDDD00143DA2 /* Gr4vyConnectionOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gr4vyConnectionOptions.swift; sourceTree = ""; }; 592C4BB928BF9AEE00063AC7 /* Gr4vy SwiftUI Sample App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Gr4vy SwiftUI Sample App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 592C4BBB28BF9AEE00063AC7 /* Gr4vy_SwiftUI_Sample_App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gr4vy_SwiftUI_Sample_App.swift; sourceTree = ""; }; 592C4BBF28BF9AEF00063AC7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -251,6 +253,7 @@ 59C5D45D29E34D32008FFEBC /* Gr4vyStatementDescriptor.swift */, 59C5D45F29E34D56008FFEBC /* Gr4vySetup.swift */, 59C5D46129F162C0008FFEBC /* Gr4vyStore.swift */, + 59160F792AF6EDDD00143DA2 /* Gr4vyConnectionOptions.swift */, ); path = Models; sourceTree = ""; @@ -478,6 +481,7 @@ files = ( 59C5D45C29E34D19008FFEBC /* Gr4vyPaymentSource.swift in Sources */, 59C5D45A29E34D04008FFEBC /* Gr4vyCartItem.swift in Sources */, + 59160F7A2AF6EDDD00143DA2 /* Gr4vyConnectionOptions.swift in Sources */, 59FE69502722BB7A006C1C08 /* Gr4vy.swift in Sources */, 59F72C012A17BE2B00DD2833 /* UIImage+Extensions.swift in Sources */, 5978B28A271C7F1300F5CC00 /* gr4vy_iOS.docc in Sources */, diff --git a/gr4vy-iOS/Gr4vy.swift b/gr4vy-iOS/Gr4vy.swift index bcd7aee..a86fb7f 100644 --- a/gr4vy-iOS/Gr4vy.swift +++ b/gr4vy-iOS/Gr4vy.swift @@ -56,6 +56,7 @@ public class Gr4vy { requireSecurityCode: Bool? = nil, shippingDetailsId: String? = nil, merchantAccountId: String? = nil, + connectionOptions: Gr4vyConnectionOptions? = nil, debugMode: Bool = false, onEvent: Gr4vyCompletionHandler? = nil) { @@ -80,7 +81,8 @@ public class Gr4vy { statementDescriptor: statementDescriptor, requireSecurityCode: requireSecurityCode, shippingDetailsId: shippingDetailsId, - merchantAccountId: merchantAccountId) + merchantAccountId: merchantAccountId, + connectionOptions: connectionOptions) self.debugMode = debugMode self.onEvent = onEvent diff --git a/gr4vy-iOS/Gr4vyUtility.swift b/gr4vy-iOS/Gr4vyUtility.swift index 2311a58..e351169 100644 --- a/gr4vy-iOS/Gr4vyUtility.swift +++ b/gr4vy-iOS/Gr4vyUtility.swift @@ -132,6 +132,10 @@ struct Gr4vyUtility { optionalData = optionalData + ", merchantAccountId: '\(merchantAccountId)'" } + if let connectionOptions = setup.connectionOptions, let connectionOptionsString = connectionOptions.convertedString { + optionalData = optionalData + ", connectionOptions: \(connectionOptionsString)" + } + let content = "window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.\(setup.instance).gr4vy.app', apiUrl: 'https://api.\(setup.instance).gr4vy.app', token: '\(setup.token)', amount: \(setup.amount), country: '\(setup.country)', currency: '\(setup.currency)'" + diff --git a/gr4vy-iOS/Models/Gr4vyConnectionOptions.swift b/gr4vy-iOS/Models/Gr4vyConnectionOptions.swift new file mode 100644 index 0000000..24764ec --- /dev/null +++ b/gr4vy-iOS/Models/Gr4vyConnectionOptions.swift @@ -0,0 +1,35 @@ +// +// Gr4vyConnectionOptions.swift +// gr4vy-ios +// +// + +import Foundation + +public struct Gr4vyConnectionOptions { + var data: [String: [String: Any]] + + public init(data: [String : [String : Any]]) { + self.data = data + } + + var convertedString: String? { + + guard !data.isEmpty else { + return nil + } + let jsonData: Data + do { + jsonData = try JSONSerialization.data(withJSONObject: data) + } catch { + return nil + } + + + if !jsonData.isEmpty, let jsonString = String(data: jsonData, encoding: .utf8), !jsonString.isEmpty { + return jsonString.replacingOccurrences(of: "\"", with: "'") + } else { + return nil + } + } +} diff --git a/gr4vy-iOS/Models/Gr4vySetup.swift b/gr4vy-iOS/Models/Gr4vySetup.swift index b962fe3..bd0a103 100644 --- a/gr4vy-iOS/Models/Gr4vySetup.swift +++ b/gr4vy-iOS/Models/Gr4vySetup.swift @@ -28,11 +28,12 @@ struct Gr4vySetup { var requireSecurityCode: Bool? var shippingDetailsId: String? var merchantAccountId: String? + var connectionOptions: Gr4vyConnectionOptions? var instance: String { return environment == .production ? gr4vyId : "sandbox.\(gr4vyId)" } - public init(gr4vyId: String, token: String, amount: Int, currency: String, country: String, buyerId: String? = nil, environment: Gr4vyEnvironment, externalIdentifier: String? = nil, store: Gr4vyStore? = nil, display: String? = nil, intent: String? = nil, metadata: [String : String]? = nil, paymentSource: Gr4vyPaymentSource? = nil, cartItems: [Gr4vyCartItem]? = nil, applePayMerchantId: String? = nil, theme: Gr4vyTheme? = nil, buyerExternalIdentifier: String? = nil, locale: String? = nil, statementDescriptor: Gr4vyStatementDescriptor? = nil, requireSecurityCode: Bool? = nil, shippingDetailsId: String? = nil, merchantAccountId: String? = nil) { + public init(gr4vyId: String, token: String, amount: Int, currency: String, country: String, buyerId: String? = nil, environment: Gr4vyEnvironment, externalIdentifier: String? = nil, store: Gr4vyStore? = nil, display: String? = nil, intent: String? = nil, metadata: [String : String]? = nil, paymentSource: Gr4vyPaymentSource? = nil, cartItems: [Gr4vyCartItem]? = nil, applePayMerchantId: String? = nil, theme: Gr4vyTheme? = nil, buyerExternalIdentifier: String? = nil, locale: String? = nil, statementDescriptor: Gr4vyStatementDescriptor? = nil, requireSecurityCode: Bool? = nil, shippingDetailsId: String? = nil, merchantAccountId: String? = nil, connectionOptions: Gr4vyConnectionOptions? = nil) { self.gr4vyId = gr4vyId self.token = token self.amount = amount @@ -55,5 +56,6 @@ struct Gr4vySetup { self.requireSecurityCode = requireSecurityCode self.shippingDetailsId = shippingDetailsId self.merchantAccountId = merchantAccountId + self.connectionOptions = connectionOptions } } diff --git a/gr4vy-iOSTests/gr4vy_iOSTests.swift b/gr4vy-iOSTests/gr4vy_iOSTests.swift index 82baa21..8e8a0bb 100644 --- a/gr4vy-iOSTests/gr4vy_iOSTests.swift +++ b/gr4vy-iOSTests/gr4vy_iOSTests.swift @@ -834,6 +834,112 @@ class gr4vy_iOSTests: XCTestCase { sut = Gr4vyUtility.handleOpenLink(from: payload) XCTAssertNotNil(sut) } + + func testConnectionOptionsEncoding() { + + let permutations = [ + Gr4vyConnectionOptions(data: ["key1": ["subKey1": "value1"]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": 1]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": true]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": "value1", "subKey2": 1]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": "value1", "subKey2": true]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": 1, "subKey2": true]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": "value1", "subKey2": 1, "subKey3": true]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": "value1"], "key2": ["subKey1": 1]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": "value1"], "key2": ["subKey1": true]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": 1], "key2": ["subKey1": true]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": "value1", "subKey2": 1], "key2": ["subKey1": 1, "subKey2": true]]), + Gr4vyConnectionOptions(data: ["key1": ["subKey1": "value1", "subKey2": true], "key2": ["subKey1": 1, "subKey2": "value2"]]) + ] + + for obj in permutations { + XCTAssertNotNil(obj.convertedString) + } + } + + func testGenerateUpdateOptionsSucceedsWithConnectionOptionsPermutations() { + + struct ConnectionOptionsTestWrapper { + var input: Gr4vyConnectionOptions + var expectedOutput: String + } + + let permutations: [ConnectionOptionsTestWrapper] = [ + ConnectionOptionsTestWrapper(input: Gr4vyConnectionOptions(data: ["key1": ["subKey1": "value1"]]), expectedOutput: "connectionOptions: {'key1':{'subKey1':'value1'}"), + ConnectionOptionsTestWrapper(input: Gr4vyConnectionOptions(data: ["key1": ["subKey1": 1]]), expectedOutput: "connectionOptions: {'key1':{'subKey1':1}"), + ConnectionOptionsTestWrapper(input: Gr4vyConnectionOptions(data: ["key1": ["subKey1": true]]), expectedOutput: "connectionOptions: {'key1':{'subKey1':true}"), + ] + + var sut = Gr4vyUtility.generateUpdateOptions(from: setup) + + for permutation in permutations { + setup.connectionOptions = permutation.input + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertTrue(sut.contains(permutation.expectedOutput)) + } + } + + func testA() { + setup.connectionOptions = Gr4vyConnectionOptions(data: [ + "adyen-card": ["additionalData": "value"] + ]) + var sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.ID123.gr4vy.app', apiUrl: 'https://api.ID123.gr4vy.app', token: 'TOKEN123', amount: 100, country: 'GB', currency: 'GBP', buyerId: 'BUYER123', supportedApplePayVersion: 0, connectionOptions: {'adyen-card':{'additionalData':'value'}}},})", sut) + + setup.connectionOptions = Gr4vyConnectionOptions(data: [ + "cybersource-anti-fraud": ["merchant_defined_data": "value"] + ]) + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.ID123.gr4vy.app', apiUrl: 'https://api.ID123.gr4vy.app', token: 'TOKEN123', amount: 100, country: 'GB', currency: 'GBP', buyerId: 'BUYER123', supportedApplePayVersion: 0, connectionOptions: {'cybersource-anti-fraud':{'merchant_defined_data':'value'}}},})", sut) + + setup.connectionOptions = Gr4vyConnectionOptions(data: [ + "forter-anti-fraud": ["delivery_method": "email"] + ]) + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.ID123.gr4vy.app', apiUrl: 'https://api.ID123.gr4vy.app', token: 'TOKEN123', amount: 100, country: 'GB', currency: 'GBP', buyerId: 'BUYER123', supportedApplePayVersion: 0, connectionOptions: {'forter-anti-fraud':{'delivery_method':'email'}}},})", sut) + + setup.connectionOptions = Gr4vyConnectionOptions(data: [ + "forter-anti-fraud": ["delivery_type": "PHYSICAL"] + ]) + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.ID123.gr4vy.app', apiUrl: 'https://api.ID123.gr4vy.app', token: 'TOKEN123', amount: 100, country: 'GB', currency: 'GBP', buyerId: 'BUYER123', supportedApplePayVersion: 0, connectionOptions: {'forter-anti-fraud':{'delivery_type':'PHYSICAL'}}},})", sut) + + setup.connectionOptions = Gr4vyConnectionOptions(data: [ + "paypal-paypal": ["additional_data": "value"], + ]) + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.ID123.gr4vy.app', apiUrl: 'https://api.ID123.gr4vy.app', token: 'TOKEN123', amount: 100, country: 'GB', currency: 'GBP', buyerId: 'BUYER123', supportedApplePayVersion: 0, connectionOptions: {'paypal-paypal':{'additional_data':'value'}}},})", sut) + + setup.connectionOptions = Gr4vyConnectionOptions(data: [ + "paypal-paypalpaylater": ["additional_data": "value"] + ]) + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.ID123.gr4vy.app', apiUrl: 'https://api.ID123.gr4vy.app', token: 'TOKEN123', amount: 100, country: 'GB', currency: 'GBP', buyerId: 'BUYER123', supportedApplePayVersion: 0, connectionOptions: {'paypal-paypalpaylater':{'additional_data':'value'}}},})", sut) + + setup.connectionOptions = Gr4vyConnectionOptions(data: [ + "int": ["additional_data": 1] + ]) + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.ID123.gr4vy.app', apiUrl: 'https://api.ID123.gr4vy.app', token: 'TOKEN123', amount: 100, country: 'GB', currency: 'GBP', buyerId: 'BUYER123', supportedApplePayVersion: 0, connectionOptions: {'int':{'additional_data':1}}},})", sut) + + setup.connectionOptions = Gr4vyConnectionOptions(data: [ + "bool": ["additional_data": true] + ]) + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.ID123.gr4vy.app', apiUrl: 'https://api.ID123.gr4vy.app', token: 'TOKEN123', amount: 100, country: 'GB', currency: 'GBP', buyerId: 'BUYER123', supportedApplePayVersion: 0, connectionOptions: {'bool':{'additional_data':true}}},})", sut) + + setup.connectionOptions = Gr4vyConnectionOptions(data: [ + "string": ["additional_data": "gr4vy"] + ]) + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.ID123.gr4vy.app', apiUrl: 'https://api.ID123.gr4vy.app', token: 'TOKEN123', amount: 100, country: 'GB', currency: 'GBP', buyerId: 'BUYER123', supportedApplePayVersion: 0, connectionOptions: {'string':{'additional_data':'gr4vy'}}},})", sut) + + setup.connectionOptions = Gr4vyConnectionOptions(data: [:]) + + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ channel: 123, type: 'updateOptions', data: { apiHost: 'api.ID123.gr4vy.app', apiUrl: 'https://api.ID123.gr4vy.app', token: 'TOKEN123', amount: 100, country: 'GB', currency: 'GBP', buyerId: 'BUYER123', supportedApplePayVersion: 0},})", sut) + + } } extension gr4vy_iOSTests { diff --git a/gr4vy-ios.podspec b/gr4vy-ios.podspec index 54f3c60..6aefb47 100644 --- a/gr4vy-ios.podspec +++ b/gr4vy-ios.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'gr4vy-ios' - s.version = '1.6.3' + s.version = '1.7.0' s.license = 'MIT' s.summary = 'Quickly embed Gr4vy in your iOS app to store card details, authorize payments, and capture a transaction.' s.homepage = 'https://github.com/gr4vy/gr4vy-ios'