From 9c297ce5364513ca56fd1707077d062a672c7a7e Mon Sep 17 00:00:00 2001 From: Luca Allievi <101414321+luca-gr4vy@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:39:22 +0200 Subject: [PATCH] task: add support for guest checkout buyer (#14) * Pass buyer option * Add missing properties and address / taxId structs * Add buyer tests * Bump pod version * Separate billing and shipping details structs * Add test for shippingDetails * Update README --- README.md | 1 + gr4vy-iOS.xcodeproj/project.pbxproj | 8 +- gr4vy-iOS/Gr4vy.swift | 4 +- gr4vy-iOS/Models/Gr4vyBuyer.swift | 119 ++++++++++++++++++++++++++++ gr4vy-iOS/Models/Gr4vySetup.swift | 5 +- gr4vy-iOSTests/gr4vy_iOSTests.swift | 48 +++++++++++ gr4vy-ios.podspec | 2 +- 7 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 gr4vy-iOS/Models/Gr4vyBuyer.swift diff --git a/README.md b/README.md index ea0e783..dbfe82d 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ These are the parameteres available on the `launch` method: | `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) | | `connectionOptionsString`| `Optional` | A JSON String of connectionOptions | +| `buyer`| `Optional` | An optional buyer object to allow guest checkout (see https://docs.gr4vy.com/reference/transactions/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 11e8085..c3e5921 100644 --- a/gr4vy-iOS.xcodeproj/project.pbxproj +++ b/gr4vy-iOS.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 59FE69722722C131006C1C08 /* gr4vy_ios.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5978B285271C7F1300F5CC00 /* gr4vy_ios.framework */; }; 59FE69732722C131006C1C08 /* gr4vy_ios.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5978B285271C7F1300F5CC00 /* gr4vy_ios.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 59FE6978272C1188006C1C08 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FE6977272C1188006C1C08 /* SettingsViewController.swift */; }; + 81125F232C6C9C4500C26DDA /* Gr4vyBuyer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81125F222C6C9C4500C26DDA /* Gr4vyBuyer.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -137,6 +138,7 @@ 59FE69692722BBD7006C1C08 /* Gr4vyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gr4vyViewController.swift; sourceTree = ""; }; 59FE696B2722BC38006C1C08 /* CheckoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutViewController.swift; sourceTree = ""; }; 59FE6977272C1188006C1C08 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; + 81125F222C6C9C4500C26DDA /* Gr4vyBuyer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gr4vyBuyer.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -254,6 +256,7 @@ 59C5D45F29E34D56008FFEBC /* Gr4vySetup.swift */, 59C5D46129F162C0008FFEBC /* Gr4vyStore.swift */, 59160F792AF6EDDD00143DA2 /* Gr4vyConnectionOptionsValue.swift */, + 81125F222C6C9C4500C26DDA /* Gr4vyBuyer.swift */, ); path = Models; sourceTree = ""; @@ -404,7 +407,7 @@ }; }; }; - buildConfigurationList = 5978B27F271C7F1300F5CC00 /* Build configuration list for PBXProject "gr4vy-ios" */; + buildConfigurationList = 5978B27F271C7F1300F5CC00 /* Build configuration list for PBXProject "gr4vy-iOS" */; compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; @@ -493,6 +496,7 @@ 59C5D45529E34C51008FFEBC /* Gr4vyTheme.swift in Sources */, 598D6A4A272F3AEF00EE5777 /* Gr4vyMessage.swift in Sources */, 59C5D46229F162C0008FFEBC /* Gr4vyStore.swift in Sources */, + 81125F232C6C9C4500C26DDA /* Gr4vyBuyer.swift in Sources */, 59FE696A2722BBD7006C1C08 /* Gr4vyViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -916,7 +920,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 5978B27F271C7F1300F5CC00 /* Build configuration list for PBXProject "gr4vy-ios" */ = { + 5978B27F271C7F1300F5CC00 /* Build configuration list for PBXProject "gr4vy-iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 5978B297271C7F1400F5CC00 /* Debug */, diff --git a/gr4vy-iOS/Gr4vy.swift b/gr4vy-iOS/Gr4vy.swift index 95f595f..3002d97 100644 --- a/gr4vy-iOS/Gr4vy.swift +++ b/gr4vy-iOS/Gr4vy.swift @@ -59,6 +59,7 @@ public class Gr4vy { merchantAccountId: String? = nil, connectionOptions: [String: [String: Gr4vyConnectionOptionsValue]]? = nil, connectionOptionsString: String? = nil, + buyer: Gr4vyBuyer? = nil, debugMode: Bool = false, onEvent: Gr4vyCompletionHandler? = nil) { @@ -85,7 +86,8 @@ public class Gr4vy { requireSecurityCode: requireSecurityCode, shippingDetailsId: shippingDetailsId, merchantAccountId: merchantAccountId, - connectionOptions: Gr4vyUtility.getConnectionOptions(from: connectionOptions, connectionOptionsString: connectionOptionsString)) + connectionOptions: Gr4vyUtility.getConnectionOptions(from: connectionOptions, connectionOptionsString: connectionOptionsString), + buyer: buyer) self.debugMode = debugMode self.onEvent = onEvent diff --git a/gr4vy-iOS/Models/Gr4vyBuyer.swift b/gr4vy-iOS/Models/Gr4vyBuyer.swift new file mode 100644 index 0000000..98a695c --- /dev/null +++ b/gr4vy-iOS/Models/Gr4vyBuyer.swift @@ -0,0 +1,119 @@ +// +// Gr4vyBuyer.swift +// gr4vy-ios +// + +import Foundation + +public struct Gr4vyBuyer: Codable { + let displayName: String? + let externalIdentifier: String? + let billingDetails: Gr4vyBillingDetails? + let shippingDetails: Gr4vyShippingDetails? + + public init( + displayName: String? = nil, + externalIdentifier: String? = nil, + billingDetails: Gr4vyBillingDetails? = nil, + shippingDetails: Gr4vyShippingDetails? = nil + ) { + self.displayName = displayName + self.externalIdentifier = externalIdentifier + self.billingDetails = billingDetails + self.shippingDetails = shippingDetails + } +} + +public struct Gr4vyBillingDetails: Codable { + let firstName: String? + let lastName: String? + let emailAddress: String? + let phoneNumber: String? + let address: Gr4vyAddress? + let taxId: Gr4vyTaxId? + + public init( + firstName: String? = nil, + lastName: String? = nil, + emailAddress: String? = nil, + phoneNumber: String? = nil, + address: Gr4vyAddress? = nil, + taxId: Gr4vyTaxId? = nil + ) { + self.firstName = firstName + self.lastName = lastName + self.emailAddress = emailAddress + self.phoneNumber = phoneNumber + self.address = address + self.taxId = taxId + } +} + +public struct Gr4vyShippingDetails: Codable { + let firstName: String? + let lastName: String? + let emailAddress: String? + let phoneNumber: String? + let address: Gr4vyAddress? + + public init( + firstName: String? = nil, + lastName: String? = nil, + emailAddress: String? = nil, + phoneNumber: String? = nil, + address: Gr4vyAddress? = nil + ) { + self.firstName = firstName + self.lastName = lastName + self.emailAddress = emailAddress + self.phoneNumber = phoneNumber + self.address = address + } +} + +public struct Gr4vyAddress: Codable { + let houseNumberOrName: String? + let line1: String? + let line2: String? + let organization: String? + let city: String? + let postalCode: String? + let country: String? + let state: String? + let stateCode: String? + + public init( + houseNumberOrName: String? = nil, + line1: String? = nil, + line2: String? = nil, + organization: String? = nil, + city: String? = nil, + postalCode: String? = nil, + country: String? = nil, + state: String? = nil, + stateCode: String? = nil + ) { + self.houseNumberOrName = houseNumberOrName + self.line1 = line1 + self.line2 = line2 + self.organization = organization + self.city = city + self.postalCode = postalCode + self.country = country + self.state = state + self.stateCode = stateCode + } +} + +public struct Gr4vyTaxId: Codable { + let value: String? + let kind: String? + + public init( + value: String? = nil, + kind: String? = nil + ) { + self.value = value + self.kind = kind + } +} diff --git a/gr4vy-iOS/Models/Gr4vySetup.swift b/gr4vy-iOS/Models/Gr4vySetup.swift index 4bc26fd..8d72c5a 100644 --- a/gr4vy-iOS/Models/Gr4vySetup.swift +++ b/gr4vy-iOS/Models/Gr4vySetup.swift @@ -30,6 +30,7 @@ struct Gr4vySetup: Encodable { var shippingDetailsId: String? var merchantAccountId: String? var connectionOptions: [String: [String: Gr4vyConnectionOptionsValue]]? + var buyer: Gr4vyBuyer? var apiHost: String? var apiUrl: String? var supportedApplePayVersion: Int = 0 @@ -58,12 +59,13 @@ struct Gr4vySetup: Encodable { case shippingDetailsId case merchantAccountId case connectionOptions + case buyer case apiHost case apiUrl case supportedApplePayVersion } - 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, applePayMerchantName: String? = nil, theme: Gr4vyTheme? = nil, buyerExternalIdentifier: String? = nil, locale: String? = nil, statementDescriptor: Gr4vyStatementDescriptor? = nil, requireSecurityCode: Bool? = nil, shippingDetailsId: String? = nil, merchantAccountId: String? = nil, connectionOptions: [String: [String: Gr4vyConnectionOptionsValue]]? = 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, applePayMerchantName: String? = nil, theme: Gr4vyTheme? = nil, buyerExternalIdentifier: String? = nil, locale: String? = nil, statementDescriptor: Gr4vyStatementDescriptor? = nil, requireSecurityCode: Bool? = nil, shippingDetailsId: String? = nil, merchantAccountId: String? = nil, connectionOptions: [String: [String: Gr4vyConnectionOptionsValue]]? = nil, buyer: Gr4vyBuyer? = nil) { self.gr4vyId = gr4vyId self.token = token self.amount = amount @@ -88,5 +90,6 @@ struct Gr4vySetup: Encodable { self.shippingDetailsId = shippingDetailsId self.merchantAccountId = merchantAccountId self.connectionOptions = connectionOptions + self.buyer = buyer } } diff --git a/gr4vy-iOSTests/gr4vy_iOSTests.swift b/gr4vy-iOSTests/gr4vy_iOSTests.swift index 905cc03..bc4d0ad 100644 --- a/gr4vy-iOSTests/gr4vy_iOSTests.swift +++ b/gr4vy-iOSTests/gr4vy_iOSTests.swift @@ -1128,6 +1128,54 @@ class gr4vy_iOSTests: XCTestCase { XCTAssertEqual(Gr4vyUtility.getConnectionOptions(from: permutation, connectionOptionsString: "{\"keyA\":{\"subKeyA\":\"valueA\"}}"), permutation) } } + + func testGenerateUpdateOptionsSucceedsWithBuyer() { + setup.buyerId = nil + setup.buyer = Gr4vyBuyer() + + var sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ \"channel\": 123, \"type\": \"updateOptions\", \"data\": {\"amount\":100,\"apiHost\":\"api.ID123.gr4vy.app\",\"apiUrl\":\"https:\\/\\/api.ID123.gr4vy.app\",\"buyer\":{},\"country\":\"GB\",\"currency\":\"GBP\",\"supportedApplePayVersion\":0,\"token\":\"TOKEN123\"}})", sut) + + setup.buyer = Gr4vyBuyer(displayName: "displayName", externalIdentifier: "externalIdentifier") + + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ \"channel\": 123, \"type\": \"updateOptions\", \"data\": {\"amount\":100,\"apiHost\":\"api.ID123.gr4vy.app\",\"apiUrl\":\"https:\\/\\/api.ID123.gr4vy.app\",\"buyer\":{\"displayName\":\"displayName\",\"externalIdentifier\":\"externalIdentifier\"},\"country\":\"GB\",\"currency\":\"GBP\",\"supportedApplePayVersion\":0,\"token\":\"TOKEN123\"}})", sut) + + setup.buyer = Gr4vyBuyer(billingDetails: Gr4vyBillingDetails(firstName: "firstName")) + + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ \"channel\": 123, \"type\": \"updateOptions\", \"data\": {\"amount\":100,\"apiHost\":\"api.ID123.gr4vy.app\",\"apiUrl\":\"https:\\/\\/api.ID123.gr4vy.app\",\"buyer\":{\"billingDetails\":{\"firstName\":\"firstName\"}},\"country\":\"GB\",\"currency\":\"GBP\",\"supportedApplePayVersion\":0,\"token\":\"TOKEN123\"}})", sut) + + setup.buyer = Gr4vyBuyer(billingDetails: Gr4vyBillingDetails(lastName: "lastName")) + + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ \"channel\": 123, \"type\": \"updateOptions\", \"data\": {\"amount\":100,\"apiHost\":\"api.ID123.gr4vy.app\",\"apiUrl\":\"https:\\/\\/api.ID123.gr4vy.app\",\"buyer\":{\"billingDetails\":{\"lastName\":\"lastName\"}},\"country\":\"GB\",\"currency\":\"GBP\",\"supportedApplePayVersion\":0,\"token\":\"TOKEN123\"}})", sut) + + setup.buyer = Gr4vyBuyer(billingDetails: Gr4vyBillingDetails(emailAddress: "emailAddress")) + + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ \"channel\": 123, \"type\": \"updateOptions\", \"data\": {\"amount\":100,\"apiHost\":\"api.ID123.gr4vy.app\",\"apiUrl\":\"https:\\/\\/api.ID123.gr4vy.app\",\"buyer\":{\"billingDetails\":{\"emailAddress\":\"emailAddress\"}},\"country\":\"GB\",\"currency\":\"GBP\",\"supportedApplePayVersion\":0,\"token\":\"TOKEN123\"}})", sut) + + setup.buyer = Gr4vyBuyer(billingDetails: Gr4vyBillingDetails(phoneNumber: "phoneNumber")) + + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ \"channel\": 123, \"type\": \"updateOptions\", \"data\": {\"amount\":100,\"apiHost\":\"api.ID123.gr4vy.app\",\"apiUrl\":\"https:\\/\\/api.ID123.gr4vy.app\",\"buyer\":{\"billingDetails\":{\"phoneNumber\":\"phoneNumber\"}},\"country\":\"GB\",\"currency\":\"GBP\",\"supportedApplePayVersion\":0,\"token\":\"TOKEN123\"}})", sut) + + setup.buyer = Gr4vyBuyer(billingDetails: Gr4vyBillingDetails(address: Gr4vyAddress(houseNumberOrName: "houseNumberOrName", line1: "line1", line2: "line2", organization: "organization", city: "city", postalCode: "postalCode", country: "country", state: "state", stateCode: "stateCode"))) + + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ \"channel\": 123, \"type\": \"updateOptions\", \"data\": {\"amount\":100,\"apiHost\":\"api.ID123.gr4vy.app\",\"apiUrl\":\"https:\\/\\/api.ID123.gr4vy.app\",\"buyer\":{\"billingDetails\":{\"address\":{\"city\":\"city\",\"country\":\"country\",\"houseNumberOrName\":\"houseNumberOrName\",\"line1\":\"line1\",\"line2\":\"line2\",\"organization\":\"organization\",\"postalCode\":\"postalCode\",\"state\":\"state\",\"stateCode\":\"stateCode\"}}},\"country\":\"GB\",\"currency\":\"GBP\",\"supportedApplePayVersion\":0,\"token\":\"TOKEN123\"}})", sut) + + setup.buyer = Gr4vyBuyer(billingDetails: Gr4vyBillingDetails(taxId: Gr4vyTaxId(value: "value", kind: "kind"))) + + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ \"channel\": 123, \"type\": \"updateOptions\", \"data\": {\"amount\":100,\"apiHost\":\"api.ID123.gr4vy.app\",\"apiUrl\":\"https:\\/\\/api.ID123.gr4vy.app\",\"buyer\":{\"billingDetails\":{\"taxId\":{\"kind\":\"kind\",\"value\":\"value\"}}},\"country\":\"GB\",\"currency\":\"GBP\",\"supportedApplePayVersion\":0,\"token\":\"TOKEN123\"}})", sut) + + setup.buyer = Gr4vyBuyer(shippingDetails: Gr4vyShippingDetails(firstName: "firstName", lastName: "lastName", address: Gr4vyAddress(houseNumberOrName: "houseNumberOrName", line1: "line1", line2: "line2", organization: "organization", city: "city", postalCode: "postalCode", country: "country", state: "state", stateCode: "stateCode"))) + + sut = Gr4vyUtility.generateUpdateOptions(from: setup) + XCTAssertEqual("window.postMessage({ \"channel\": 123, \"type\": \"updateOptions\", \"data\": {\"amount\":100,\"apiHost\":\"api.ID123.gr4vy.app\",\"apiUrl\":\"https:\\/\\/api.ID123.gr4vy.app\",\"buyer\":{\"shippingDetails\":{\"address\":{\"city\":\"city\",\"country\":\"country\",\"houseNumberOrName\":\"houseNumberOrName\",\"line1\":\"line1\",\"line2\":\"line2\",\"organization\":\"organization\",\"postalCode\":\"postalCode\",\"state\":\"state\",\"stateCode\":\"stateCode\"},\"firstName\":\"firstName\",\"lastName\":\"lastName\"}},\"country\":\"GB\",\"currency\":\"GBP\",\"supportedApplePayVersion\":0,\"token\":\"TOKEN123\"}})", sut) + } } extension gr4vy_iOSTests { diff --git a/gr4vy-ios.podspec b/gr4vy-ios.podspec index a61c2ac..cf94aa3 100644 --- a/gr4vy-ios.podspec +++ b/gr4vy-ios.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'gr4vy-ios' - s.version = '2.3.0' + s.version = '2.4.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'