diff --git a/OSPaymentsLib.xcodeproj/project.pbxproj b/OSPaymentsLib.xcodeproj/project.pbxproj index 1b06ec7..323ffa2 100644 --- a/OSPaymentsLib.xcodeproj/project.pbxproj +++ b/OSPaymentsLib.xcodeproj/project.pbxproj @@ -13,9 +13,24 @@ 7509DC7D28995A07005BA0D4 /* OSPMTAvailabilityDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7509DC7C28995A07005BA0D4 /* OSPMTAvailabilityDelegate.swift */; }; 7509DC7F28996482005BA0D4 /* OSPMTApplePayConfigurationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7509DC7E28996482005BA0D4 /* OSPMTApplePayConfigurationSpec.swift */; }; 7509DC81289967A6005BA0D4 /* PKPaymentNetwork+Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7509DC80289967A6005BA0D4 /* PKPaymentNetwork+Adapter.swift */; }; - 755A804B289BB96900426EAA /* PKPassLibrary+OSPMTWalletAvailabilityDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A804A289BB96900426EAA /* PKPassLibrary+OSPMTWalletAvailabilityDelegate.swift */; }; - 755A804D289BBAB300426EAA /* PKPaymentAuthorizationController+OSPMTSetupAvailabilityDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A804C289BBAB300426EAA /* PKPaymentAuthorizationController+OSPMTSetupAvailabilityDelegate.swift */; }; + 755A804B289BB96900426EAA /* PKPassLibrary+Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A804A289BB96900426EAA /* PKPassLibrary+Adapter.swift */; }; + 755A804D289BBAB300426EAA /* PKPaymentAuthorizationController+Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A804C289BBAB300426EAA /* PKPaymentAuthorizationController+Adapter.swift */; }; 755A8050289BBDF000426EAA /* OSPMTApplePayAvailabilityBehaviourSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A804F289BBDF000426EAA /* OSPMTApplePayAvailabilityBehaviourSpec.swift */; }; + 755A8055289BC8EE00426EAA /* OSPMTDetailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A8053289BC8EE00426EAA /* OSPMTDetailsModel.swift */; }; + 755A8056289BC8EE00426EAA /* OSPMTScopeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A8054289BC8EE00426EAA /* OSPMTScopeModel.swift */; }; + 755A8058289BC90A00426EAA /* OSPMTRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A8057289BC90A00426EAA /* OSPMTRequestDelegate.swift */; }; + 755A805A289BC92A00426EAA /* PKContactField+Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A8059289BC92A00426EAA /* PKContactField+Adapter.swift */; }; + 755A805C289BD4C600426EAA /* OSPMTApplePayRequestBehaviourSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A805B289BD4C600426EAA /* OSPMTApplePayRequestBehaviourSpec.swift */; }; + 755A805E289BD53000426EAA /* OSPMTTokenInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A805D289BD53000426EAA /* OSPMTTokenInfoModel.swift */; }; + 755A8060289BD54E00426EAA /* OSPMTDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A805F289BD54E00426EAA /* OSPMTDataModel.swift */; }; + 755A8062289BD56300426EAA /* OSPMTAddressModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A8061289BD56300426EAA /* OSPMTAddressModel.swift */; }; + 755A8064289BD58900426EAA /* OSPMTContactInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755A8063289BD58900426EAA /* OSPMTContactInfoModel.swift */; }; + 75760F78289C102D001BDCEC /* OSPMTAddressModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75760F77289C102D001BDCEC /* OSPMTAddressModelSpec.swift */; }; + 75760F7A289C17AB001BDCEC /* OSPMTContactInfoModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75760F79289C17AB001BDCEC /* OSPMTContactInfoModelSpec.swift */; }; + 75760F7D289C1AED001BDCEC /* OSPMTTokenInfoModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75760F7C289C1AED001BDCEC /* OSPMTTokenInfoModelSpec.swift */; }; + 75760F7F289C1BF9001BDCEC /* OSPMTDataModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75760F7E289C1BF9001BDCEC /* OSPMTDataModelSpec.swift */; }; + 75760F81289C1DF3001BDCEC /* OSPMTScopeModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75760F80289C1DF3001BDCEC /* OSPMTScopeModelSpec.swift */; }; + 75760F83289C1F99001BDCEC /* PKPayment+Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75760F82289C1F99001BDCEC /* PKPayment+Adapter.swift */; }; 75E4DCB12897C8AD002277FD /* OSPMTPaymentsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75E4DCB02897C8AD002277FD /* OSPMTPaymentsSpec.swift */; }; 75EC4D122893FC3C00CF50E2 /* OSPMTPayments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC4D112893FC3C00CF50E2 /* OSPMTPayments.swift */; }; 75EC4D152894143A00CF50E2 /* OSPMTCallbackDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC4D142894143A00CF50E2 /* OSPMTCallbackDelegate.swift */; }; @@ -51,9 +66,24 @@ 7509DC7C28995A07005BA0D4 /* OSPMTAvailabilityDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTAvailabilityDelegate.swift; sourceTree = ""; }; 7509DC7E28996482005BA0D4 /* OSPMTApplePayConfigurationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTApplePayConfigurationSpec.swift; sourceTree = ""; }; 7509DC80289967A6005BA0D4 /* PKPaymentNetwork+Adapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKPaymentNetwork+Adapter.swift"; sourceTree = ""; }; - 755A804A289BB96900426EAA /* PKPassLibrary+OSPMTWalletAvailabilityDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKPassLibrary+OSPMTWalletAvailabilityDelegate.swift"; sourceTree = ""; }; - 755A804C289BBAB300426EAA /* PKPaymentAuthorizationController+OSPMTSetupAvailabilityDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKPaymentAuthorizationController+OSPMTSetupAvailabilityDelegate.swift"; sourceTree = ""; }; + 755A804A289BB96900426EAA /* PKPassLibrary+Adapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKPassLibrary+Adapter.swift"; sourceTree = ""; }; + 755A804C289BBAB300426EAA /* PKPaymentAuthorizationController+Adapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKPaymentAuthorizationController+Adapter.swift"; sourceTree = ""; }; 755A804F289BBDF000426EAA /* OSPMTApplePayAvailabilityBehaviourSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTApplePayAvailabilityBehaviourSpec.swift; sourceTree = ""; }; + 755A8053289BC8EE00426EAA /* OSPMTDetailsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSPMTDetailsModel.swift; sourceTree = ""; }; + 755A8054289BC8EE00426EAA /* OSPMTScopeModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSPMTScopeModel.swift; sourceTree = ""; }; + 755A8057289BC90A00426EAA /* OSPMTRequestDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSPMTRequestDelegate.swift; sourceTree = ""; }; + 755A8059289BC92A00426EAA /* PKContactField+Adapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PKContactField+Adapter.swift"; sourceTree = ""; }; + 755A805B289BD4C600426EAA /* OSPMTApplePayRequestBehaviourSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSPMTApplePayRequestBehaviourSpec.swift; sourceTree = ""; }; + 755A805D289BD53000426EAA /* OSPMTTokenInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTTokenInfoModel.swift; sourceTree = ""; }; + 755A805F289BD54E00426EAA /* OSPMTDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTDataModel.swift; sourceTree = ""; }; + 755A8061289BD56300426EAA /* OSPMTAddressModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTAddressModel.swift; sourceTree = ""; }; + 755A8063289BD58900426EAA /* OSPMTContactInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTContactInfoModel.swift; sourceTree = ""; }; + 75760F77289C102D001BDCEC /* OSPMTAddressModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTAddressModelSpec.swift; sourceTree = ""; }; + 75760F79289C17AB001BDCEC /* OSPMTContactInfoModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTContactInfoModelSpec.swift; sourceTree = ""; }; + 75760F7C289C1AED001BDCEC /* OSPMTTokenInfoModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTTokenInfoModelSpec.swift; sourceTree = ""; }; + 75760F7E289C1BF9001BDCEC /* OSPMTDataModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTDataModelSpec.swift; sourceTree = ""; }; + 75760F80289C1DF3001BDCEC /* OSPMTScopeModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTScopeModelSpec.swift; sourceTree = ""; }; + 75760F82289C1F99001BDCEC /* PKPayment+Adapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKPayment+Adapter.swift"; sourceTree = ""; }; 75E4DCB02897C8AD002277FD /* OSPMTPaymentsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTPaymentsSpec.swift; sourceTree = ""; }; 75EC4D112893FC3C00CF50E2 /* OSPMTPayments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTPayments.swift; sourceTree = ""; }; 75EC4D142894143A00CF50E2 /* OSPMTCallbackDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTCallbackDelegate.swift; sourceTree = ""; }; @@ -124,8 +154,9 @@ 7507FC1427FC2AAE003809F6 /* OSPaymentsLib */ = { isa = PBXGroup; children = ( - 7509DC7528993C26005BA0D4 /* Extensions */, 75EC4D182894155D00CF50E2 /* Error */, + 7509DC7528993C26005BA0D4 /* Extensions */, + 755A8052289BC8EE00426EAA /* Models */, 75EC4D132894142000CF50E2 /* Protocols */, 7507FC1527FC2AAE003809F6 /* OSPaymentsLib.h */, 75EC4D1D289416B600CF50E2 /* OSPMTApplePayHandler.swift */, @@ -137,9 +168,11 @@ 7507FC1E27FC2AAE003809F6 /* OSPaymentsLibTests */ = { isa = PBXGroup; children = ( + 75760F7B289C17B1001BDCEC /* ModelSpecs */, 755A804F289BBDF000426EAA /* OSPMTApplePayAvailabilityBehaviourSpec.swift */, 7509DC7E28996482005BA0D4 /* OSPMTApplePayConfigurationSpec.swift */, 75EC4D232894449700CF50E2 /* OSPMTApplePayHandlerSpec.swift */, + 755A805B289BD4C600426EAA /* OSPMTApplePayRequestBehaviourSpec.swift */, 75E4DCB02897C8AD002277FD /* OSPMTPaymentsSpec.swift */, 75EC4D252894478200CF50E2 /* OSPMTTestConfigurations.swift */, ); @@ -149,22 +182,50 @@ 7509DC7528993C26005BA0D4 /* Extensions */ = { isa = PBXGroup; children = ( + 755A8059289BC92A00426EAA /* PKContactField+Adapter.swift */, 7509DC7628993C4C005BA0D4 /* PKMerchantCapability+Adapter.swift */, - 755A804A289BB96900426EAA /* PKPassLibrary+OSPMTWalletAvailabilityDelegate.swift */, - 755A804C289BBAB300426EAA /* PKPaymentAuthorizationController+OSPMTSetupAvailabilityDelegate.swift */, + 755A804A289BB96900426EAA /* PKPassLibrary+Adapter.swift */, + 75760F82289C1F99001BDCEC /* PKPayment+Adapter.swift */, + 755A804C289BBAB300426EAA /* PKPaymentAuthorizationController+Adapter.swift */, 7509DC80289967A6005BA0D4 /* PKPaymentNetwork+Adapter.swift */, ); path = Extensions; sourceTree = ""; }; + 755A8052289BC8EE00426EAA /* Models */ = { + isa = PBXGroup; + children = ( + 755A8061289BD56300426EAA /* OSPMTAddressModel.swift */, + 755A8063289BD58900426EAA /* OSPMTContactInfoModel.swift */, + 755A805F289BD54E00426EAA /* OSPMTDataModel.swift */, + 755A8053289BC8EE00426EAA /* OSPMTDetailsModel.swift */, + 755A8054289BC8EE00426EAA /* OSPMTScopeModel.swift */, + 755A805D289BD53000426EAA /* OSPMTTokenInfoModel.swift */, + ); + path = Models; + sourceTree = ""; + }; + 75760F7B289C17B1001BDCEC /* ModelSpecs */ = { + isa = PBXGroup; + children = ( + 75760F77289C102D001BDCEC /* OSPMTAddressModelSpec.swift */, + 75760F79289C17AB001BDCEC /* OSPMTContactInfoModelSpec.swift */, + 75760F7E289C1BF9001BDCEC /* OSPMTDataModelSpec.swift */, + 75760F80289C1DF3001BDCEC /* OSPMTScopeModelSpec.swift */, + 75760F7C289C1AED001BDCEC /* OSPMTTokenInfoModelSpec.swift */, + ); + path = ModelSpecs; + sourceTree = ""; + }; 75EC4D132894142000CF50E2 /* Protocols */ = { isa = PBXGroup; children = ( 75EC4D1B2894160400CF50E2 /* OSPMTActionDelegate.swift */, + 7509DC7C28995A07005BA0D4 /* OSPMTAvailabilityDelegate.swift */, 75EC4D142894143A00CF50E2 /* OSPMTCallbackDelegate.swift */, - 75EC4D162894150F00CF50E2 /* OSPMTHandlerDelegate.swift */, 75EC4D2128942C3800CF50E2 /* OSPMTConfigurationDelegate.swift */, - 7509DC7C28995A07005BA0D4 /* OSPMTAvailabilityDelegate.swift */, + 75EC4D162894150F00CF50E2 /* OSPMTHandlerDelegate.swift */, + 755A8057289BC90A00426EAA /* OSPMTRequestDelegate.swift */, ); path = Protocols; sourceTree = ""; @@ -383,15 +444,24 @@ files = ( 7509DC7D28995A07005BA0D4 /* OSPMTAvailabilityDelegate.swift in Sources */, 75EC4D1A2894156E00CF50E2 /* OSPMTError.swift in Sources */, + 755A8056289BC8EE00426EAA /* OSPMTScopeModel.swift in Sources */, 7509DC7728993C4C005BA0D4 /* PKMerchantCapability+Adapter.swift in Sources */, - 755A804B289BB96900426EAA /* PKPassLibrary+OSPMTWalletAvailabilityDelegate.swift in Sources */, + 755A8055289BC8EE00426EAA /* OSPMTDetailsModel.swift in Sources */, + 755A804B289BB96900426EAA /* PKPassLibrary+Adapter.swift in Sources */, + 75760F83289C1F99001BDCEC /* PKPayment+Adapter.swift in Sources */, 75EC4D2228942C3800CF50E2 /* OSPMTConfigurationDelegate.swift in Sources */, 75EC4D172894150F00CF50E2 /* OSPMTHandlerDelegate.swift in Sources */, - 755A804D289BBAB300426EAA /* PKPaymentAuthorizationController+OSPMTSetupAvailabilityDelegate.swift in Sources */, + 755A8058289BC90A00426EAA /* OSPMTRequestDelegate.swift in Sources */, + 755A8062289BD56300426EAA /* OSPMTAddressModel.swift in Sources */, + 755A8060289BD54E00426EAA /* OSPMTDataModel.swift in Sources */, + 755A804D289BBAB300426EAA /* PKPaymentAuthorizationController+Adapter.swift in Sources */, 75EC4D1E289416B600CF50E2 /* OSPMTApplePayHandler.swift in Sources */, 7509DC81289967A6005BA0D4 /* PKPaymentNetwork+Adapter.swift in Sources */, + 755A8064289BD58900426EAA /* OSPMTContactInfoModel.swift in Sources */, 75EC4D152894143A00CF50E2 /* OSPMTCallbackDelegate.swift in Sources */, + 755A805E289BD53000426EAA /* OSPMTTokenInfoModel.swift in Sources */, 75EC4D1C2894160400CF50E2 /* OSPMTActionDelegate.swift in Sources */, + 755A805A289BC92A00426EAA /* PKContactField+Adapter.swift in Sources */, 75EC4D122893FC3C00CF50E2 /* OSPMTPayments.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -400,11 +470,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 755A805C289BD4C600426EAA /* OSPMTApplePayRequestBehaviourSpec.swift in Sources */, + 75760F7A289C17AB001BDCEC /* OSPMTContactInfoModelSpec.swift in Sources */, 75EC4D262894478200CF50E2 /* OSPMTTestConfigurations.swift in Sources */, + 75760F81289C1DF3001BDCEC /* OSPMTScopeModelSpec.swift in Sources */, 755A8050289BBDF000426EAA /* OSPMTApplePayAvailabilityBehaviourSpec.swift in Sources */, + 75760F7D289C1AED001BDCEC /* OSPMTTokenInfoModelSpec.swift in Sources */, 7509DC7F28996482005BA0D4 /* OSPMTApplePayConfigurationSpec.swift in Sources */, + 75760F7F289C1BF9001BDCEC /* OSPMTDataModelSpec.swift in Sources */, 75EC4D242894449700CF50E2 /* OSPMTApplePayHandlerSpec.swift in Sources */, 75E4DCB12897C8AD002277FD /* OSPMTPaymentsSpec.swift in Sources */, + 75760F78289C102D001BDCEC /* OSPMTAddressModelSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/OSPaymentsLib/Error/OSPMTError.swift b/OSPaymentsLib/Error/OSPMTError.swift index 097cdc9..cfe478f 100644 --- a/OSPaymentsLib/Error/OSPMTError.swift +++ b/OSPaymentsLib/Error/OSPMTError.swift @@ -3,6 +3,10 @@ enum OSPMTError: Int, CustomNSError, LocalizedError { case walletNotAvailable = 2 case paymentNotAvailable = 3 case setupPaymentNotAvailable = 4 + case invalidDecodeDetails = 6 + case paymentTriggerPresentationFailed = 7 + case paymentTriggerNotCompleted = 8 + case invalidEncodeScope = 9 var errorDescription: String? { switch self { @@ -14,6 +18,14 @@ enum OSPMTError: Int, CustomNSError, LocalizedError { return "Payment is not available on this device." case .setupPaymentNotAvailable: return "Payment through the configured networks and capabilities is not available on this device." + case .invalidDecodeDetails: + return "Couldn't decode payment details." + case .paymentTriggerPresentationFailed: + return "Couldn't present the Apple Pay screen." + case .paymentTriggerNotCompleted: + return "Couldn't complete the payment trigger process." + case .invalidEncodeScope: + return "Couldn't encode payment scope." } } } diff --git a/OSPaymentsLib/Extensions/PKContactField+Adapter.swift b/OSPaymentsLib/Extensions/PKContactField+Adapter.swift new file mode 100644 index 0000000..e112995 --- /dev/null +++ b/OSPaymentsLib/Extensions/PKContactField+Adapter.swift @@ -0,0 +1,18 @@ +import PassKit + +extension PKContactField { + static func convert(from text: String) -> PKContactField? { + switch text.lowercased() { + case "email": + return .emailAddress + case "name": + return .name + case "phone": + return .phoneNumber + case "postal_address": + return .postalAddress + default: + return nil + } + } +} diff --git a/OSPaymentsLib/Extensions/PKPassLibrary+OSPMTWalletAvailabilityDelegate.swift b/OSPaymentsLib/Extensions/PKPassLibrary+Adapter.swift similarity index 62% rename from OSPaymentsLib/Extensions/PKPassLibrary+OSPMTWalletAvailabilityDelegate.swift rename to OSPaymentsLib/Extensions/PKPassLibrary+Adapter.swift index 646fcc8..6561361 100644 --- a/OSPaymentsLib/Extensions/PKPassLibrary+OSPMTWalletAvailabilityDelegate.swift +++ b/OSPaymentsLib/Extensions/PKPassLibrary+Adapter.swift @@ -1,9 +1,5 @@ import PassKit -protocol OSPMTWalletAvailabilityDelegate: AnyObject { - static func isWalletAvailable() -> Bool -} - extension PKPassLibrary: OSPMTWalletAvailabilityDelegate { static func isWalletAvailable() -> Bool { Self.isPassLibraryAvailable() diff --git a/OSPaymentsLib/Extensions/PKPayment+Adapter.swift b/OSPaymentsLib/Extensions/PKPayment+Adapter.swift new file mode 100644 index 0000000..22bf2b8 --- /dev/null +++ b/OSPaymentsLib/Extensions/PKPayment+Adapter.swift @@ -0,0 +1,79 @@ +import PassKit + +extension PKPayment { + func createScopeModel() -> OSPMTScopeModel? { + var result: [String: Any] = [OSPMTScopeModel.CodingKeys.paymentData.rawValue: self.createTokenDataData()] + if let shippingContact = self.shippingContact { + result[OSPMTScopeModel.CodingKeys.shippingInfo.rawValue] = self.createContactInfoData(for: shippingContact) + } + + guard + let scopeData = try? JSONSerialization.data(withJSONObject: result), + let scopeModel = try? JSONDecoder().decode(OSPMTScopeModel.self, from: scopeData) + else { return nil } + return scopeModel + } + + func createTokenDataData() -> [String: Any] { + var result: [String: Any] = [ + OSPMTDataModel.CodingKeys.tokenData.rawValue: self.createTokenData(for: self.token.paymentData) + ] + if let billingContact = self.billingContact { + result[OSPMTDataModel.CodingKeys.billingInfo.rawValue] = self.createContactInfoData(for: billingContact) + } + if let paymentMethodName = self.token.paymentMethod.displayName { + let cardInfo = paymentMethodName.components(separatedBy: " ") + if let cardNetwork = cardInfo.first, let cardDetails = cardInfo.last { + result[OSPMTDataModel.CodingKeys.cardDetails.rawValue] = cardDetails + result[OSPMTDataModel.CodingKeys.cardNetwork.rawValue] = cardNetwork + } + } + + return result + } + + func createTokenData(for paymentData: Data) -> [String: String] { + var result = [OSPMTTokenInfoModel.CodingKeys.type.rawValue: "Payment Service Provider Name"] + + if let token = String(data: paymentData, encoding: .utf8) { + result[OSPMTTokenInfoModel.CodingKeys.token.rawValue] = token + } + + return result + } + + func createContactInfoData(for contact: PKContact) -> [String: Any] { + var result = [String: Any]() + if let address = contact.postalAddress { + result[OSPMTContactInfoModel.CodingKeys.address.rawValue] = self.createAddressData(for: address) + } + if let phoneNumber = contact.phoneNumber { + result[OSPMTContactInfoModel.CodingKeys.phoneNumber.rawValue] = phoneNumber.stringValue + } + if let name = contact.name, let givenName = name.givenName, let familyName = name.familyName { + result[OSPMTContactInfoModel.CodingKeys.name.rawValue] = "\(givenName) \(familyName)" + } + if let email = contact.emailAddress { + result[OSPMTContactInfoModel.CodingKeys.email.rawValue] = email + } + + return result + } + + func createAddressData(for postalAddress: CNPostalAddress) -> [String: String] { + var result = [ + OSPMTAddressModel.CodingKeys.postalCode.rawValue: postalAddress.postalCode, + OSPMTAddressModel.CodingKeys.fullAddress.rawValue: postalAddress.street, + OSPMTAddressModel.CodingKeys.countryCode.rawValue: postalAddress.isoCountryCode, + OSPMTAddressModel.CodingKeys.city.rawValue: postalAddress.city + ] + if !postalAddress.subAdministrativeArea.isEmpty { + result[OSPMTAddressModel.CodingKeys.administrativeArea.rawValue] = postalAddress.subAdministrativeArea + } + if !postalAddress.state.isEmpty { + result[OSPMTAddressModel.CodingKeys.state.rawValue] = postalAddress.state + } + + return result + } +} diff --git a/OSPaymentsLib/Extensions/PKPaymentAuthorizationController+Adapter.swift b/OSPaymentsLib/Extensions/PKPaymentAuthorizationController+Adapter.swift new file mode 100644 index 0000000..83f47d3 --- /dev/null +++ b/OSPaymentsLib/Extensions/PKPaymentAuthorizationController+Adapter.swift @@ -0,0 +1,45 @@ +import PassKit + +extension PKPaymentAuthorizationController: OSPMTApplePaySetupAvailabilityDelegate { + static func isPaymentAvailable() -> Bool { + Self.canMakePayments() + } + + static func isPaymentAvailable(using networks: [PKPaymentNetwork], and merchantCapabilities: PKMerchantCapability) -> Bool { + Self.canMakePayments(usingNetworks: networks, capabilities: merchantCapabilities) + } +} + +extension PKPaymentAuthorizationController: OSPMTApplePayRequestTriggerDelegate { + static func createRequestTriggerBehaviour(for detailsModel: OSPMTDetailsModel, andDelegate delegate: OSPMTApplePayRequestBehaviour?) -> Result { + guard + let delegate = delegate, + let merchantIdentifier = delegate.configuration.merchantID, + let countryCode = delegate.configuration.merchantCountryCode, + let merchantCapabilities = delegate.configuration.merchantCapabilities, + let paymentSummaryItems = delegate.getPaymentSummaryItems(for: detailsModel), + let requiredBillingContactFields = detailsModel.billingContactArray ?? delegate.configuration.billingSupportedContacts, + let requiredShippingContactFields = detailsModel.shippingContactArray ?? delegate.configuration.shippingSupportedContacts, + let supportedNetworks = delegate.configuration.supportedNetworks + else { return .failure(.invalidConfiguration) } + + let paymentRequest = PKPaymentRequest() + paymentRequest.merchantIdentifier = merchantIdentifier + paymentRequest.countryCode = countryCode + paymentRequest.currencyCode = detailsModel.currency + paymentRequest.merchantCapabilities = merchantCapabilities + paymentRequest.paymentSummaryItems = paymentSummaryItems + paymentRequest.requiredBillingContactFields = delegate.getContactFields(for: requiredBillingContactFields) + paymentRequest.requiredShippingContactFields = delegate.getContactFields(for: requiredShippingContactFields) + paymentRequest.supportedCountries = delegate.configuration.supportedCountries + paymentRequest.supportedNetworks = supportedNetworks + + let paymentAuthorizationController = PKPaymentAuthorizationController(paymentRequest: paymentRequest) + paymentAuthorizationController.delegate = delegate + return .success(paymentAuthorizationController) + } + + func triggerPayment(_ completion: @escaping OSPMTRequestTriggerCompletion) { + self.present(completion: completion) + } +} diff --git a/OSPaymentsLib/Extensions/PKPaymentAuthorizationController+OSPMTSetupAvailabilityDelegate.swift b/OSPaymentsLib/Extensions/PKPaymentAuthorizationController+OSPMTSetupAvailabilityDelegate.swift deleted file mode 100644 index 5bbd727..0000000 --- a/OSPaymentsLib/Extensions/PKPaymentAuthorizationController+OSPMTSetupAvailabilityDelegate.swift +++ /dev/null @@ -1,19 +0,0 @@ -import PassKit - -protocol OSPMTSetupAvailabilityDelegate: AnyObject { - static func isPaymentAvailable() -> Bool -} - -protocol OSPMTApplePaySetupAvailabilityDelegate: OSPMTSetupAvailabilityDelegate { - static func isPaymentAvailable(using networks: [PKPaymentNetwork], and merchantCapabilities: PKMerchantCapability) -> Bool -} - -extension PKPaymentAuthorizationController: OSPMTApplePaySetupAvailabilityDelegate { - static func isPaymentAvailable() -> Bool { - Self.canMakePayments() - } - - static func isPaymentAvailable(using networks: [PKPaymentNetwork], and merchantCapabilities: PKMerchantCapability) -> Bool { - Self.canMakePayments(usingNetworks: networks, capabilities: merchantCapabilities) - } -} diff --git a/OSPaymentsLib/Models/OSPMTAddressModel.swift b/OSPaymentsLib/Models/OSPMTAddressModel.swift new file mode 100644 index 0000000..0db2cb0 --- /dev/null +++ b/OSPaymentsLib/Models/OSPMTAddressModel.swift @@ -0,0 +1,49 @@ +struct OSPMTAddressModel: Codable, Equatable { + let postalCode: String + let fullAddress: String + let countryCode: String + let city: String + let administrativeArea: String? + let state: String? + + enum CodingKeys: String, CodingKey { + case postalCode, fullAddress, countryCode, city, administrativeArea, state + } + + init(postalCode: String, fullAddress: String, countryCode: String, city: String, administrativeArea: String? = nil, state: String? = nil) { + self.postalCode = postalCode + self.fullAddress = fullAddress + self.countryCode = countryCode + self.city = city + self.administrativeArea = administrativeArea + self.state = state + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let postalCode = try container.decode(String.self, forKey: .postalCode) + let fullAddress = try container.decode(String.self, forKey: .fullAddress) + let countryCode = try container.decode(String.self, forKey: .countryCode) + let city = try container.decode(String.self, forKey: .city) + let administrativeArea = try container.decodeIfPresent(String.self, forKey: .administrativeArea) + let state = try container.decodeIfPresent(String.self, forKey: .state) + self.init( + postalCode: postalCode, + fullAddress: fullAddress, + countryCode: countryCode, + city: city, + administrativeArea: administrativeArea, + state: state + ) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(postalCode, forKey: .postalCode) + try container.encode(fullAddress, forKey: .fullAddress) + try container.encode(countryCode, forKey: .countryCode) + try container.encode(city, forKey: .city) + try container.encodeIfPresent(administrativeArea, forKey: .administrativeArea) + try container.encodeIfPresent(state, forKey: .state) + } +} diff --git a/OSPaymentsLib/Models/OSPMTContactInfoModel.swift b/OSPaymentsLib/Models/OSPMTContactInfoModel.swift new file mode 100644 index 0000000..5867e96 --- /dev/null +++ b/OSPaymentsLib/Models/OSPMTContactInfoModel.swift @@ -0,0 +1,40 @@ +struct OSPMTContactInfoModel: Codable { + let address: OSPMTAddressModel? + let phoneNumber: String? + let name: String? + let email: String? + + enum CodingKeys: String, CodingKey { + case address, phoneNumber, name, email + } + + init(address: OSPMTAddressModel? = nil, phoneNumber: String? = nil, name: String? = nil, email: String? = nil) { + self.address = address + self.phoneNumber = phoneNumber + self.name = name + self.email = email + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let address = try container.decodeIfPresent(OSPMTAddressModel.self, forKey: .address) + let phoneNumber = try container.decodeIfPresent(String.self, forKey: .phoneNumber) + let name = try container.decodeIfPresent(String.self, forKey: .name) + let email = try container.decodeIfPresent(String.self, forKey: .email) + self.init(address: address, phoneNumber: phoneNumber, name: name, email: email) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(address, forKey: .address) + try container.encodeIfPresent(phoneNumber, forKey: .phoneNumber) + try container.encodeIfPresent(name, forKey: .name) + try container.encodeIfPresent(email, forKey: .email) + } +} + +extension OSPMTContactInfoModel: Equatable { + static func == (lhs: OSPMTContactInfoModel, rhs: OSPMTContactInfoModel) -> Bool { + lhs.address == rhs.address && lhs.phoneNumber == rhs.phoneNumber && lhs.name == rhs.name && lhs.email == rhs.email + } +} diff --git a/OSPaymentsLib/Models/OSPMTDataModel.swift b/OSPaymentsLib/Models/OSPMTDataModel.swift new file mode 100644 index 0000000..b5edd50 --- /dev/null +++ b/OSPaymentsLib/Models/OSPMTDataModel.swift @@ -0,0 +1,43 @@ +struct OSPMTDataModel: Codable { + let billingInfo: OSPMTContactInfoModel + let cardDetails: String + let cardNetwork: String + let tokenData: OSPMTTokenInfoModel + + enum CodingKeys: String, CodingKey { + case billingInfo, cardDetails, cardNetwork, tokenData + } + + init(billingInfo: OSPMTContactInfoModel, cardDetails: String, cardNetwork: String, tokenData: OSPMTTokenInfoModel) { + self.billingInfo = billingInfo + self.cardDetails = cardDetails + self.cardNetwork = cardNetwork + self.tokenData = tokenData + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let billingInfo = try container.decode(OSPMTContactInfoModel.self, forKey: .billingInfo) + let cardDetails = try container.decode(String.self, forKey: .cardDetails) + let cardNetwork = try container.decode(String.self, forKey: .cardNetwork) + let tokenData = try container.decode(OSPMTTokenInfoModel.self, forKey: .tokenData) + self.init(billingInfo: billingInfo, cardDetails: cardDetails, cardNetwork: cardNetwork, tokenData: tokenData) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(billingInfo, forKey: .billingInfo) + try container.encode(cardDetails, forKey: .cardDetails) + try container.encode(cardNetwork, forKey: .cardNetwork) + try container.encode(tokenData, forKey: .tokenData) + } +} + +extension OSPMTDataModel: Equatable { + static func == (lhs: OSPMTDataModel, rhs: OSPMTDataModel) -> Bool { + lhs.billingInfo == rhs.billingInfo + && lhs.cardDetails == rhs.cardDetails + && lhs.cardNetwork == rhs.cardNetwork + && lhs.tokenData == rhs.tokenData + } +} diff --git a/OSPaymentsLib/Models/OSPMTDetailsModel.swift b/OSPaymentsLib/Models/OSPMTDetailsModel.swift new file mode 100644 index 0000000..5b514f9 --- /dev/null +++ b/OSPaymentsLib/Models/OSPMTDetailsModel.swift @@ -0,0 +1,53 @@ +import PassKit + +enum OSPMTStatus: String, Codable { + case final + case pending +} + +struct OSPMTDetailsModel: Codable { + let amount: Decimal + let currency: String + let status: OSPMTStatus + let shippingContactArray: [String]? + let billingContactArray: [String]? + + enum CodingKeys: String, CodingKey { + case amount + case currency + case status + case shippingContactArray = "shippingContacts" + case billingContactArray = "billingContacts" + } + + init(amount: Decimal, currency: String, status: OSPMTStatus, shippingContactArray: [String]? = nil, billingContactArray: [String]? = nil) { + self.amount = amount + self.currency = currency + self.status = status + self.shippingContactArray = shippingContactArray + self.billingContactArray = billingContactArray + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let amount = try container.decode(Decimal.self, forKey: .amount) + let currency = try container.decode(String.self, forKey: .currency) + let status = try container.decode(OSPMTStatus.self, forKey: .status) + let shippingContactArray = try container.decodeIfPresent([String].self, forKey: .shippingContactArray) + let billingContactArray = try container.decodeIfPresent([String].self, forKey: .billingContactArray) + self.init( + amount: amount, currency: currency, status: status, shippingContactArray: shippingContactArray, billingContactArray: billingContactArray + ) + } +} + +// MARK: Apple Pay extension +extension OSPMTDetailsModel { + var paymentAmount: NSDecimalNumber { + NSDecimalNumber(decimal: self.amount) + } + + var paymentSummaryItemType: PKPaymentSummaryItemType { + self.status == .pending ? .pending : .final + } +} diff --git a/OSPaymentsLib/Models/OSPMTScopeModel.swift b/OSPaymentsLib/Models/OSPMTScopeModel.swift new file mode 100644 index 0000000..b6cca51 --- /dev/null +++ b/OSPaymentsLib/Models/OSPMTScopeModel.swift @@ -0,0 +1,34 @@ +import PassKit + +struct OSPMTScopeModel: Codable { + let paymentData: OSPMTDataModel + let shippingInfo: OSPMTContactInfoModel + + enum CodingKeys: String, CodingKey { + case paymentData, shippingInfo + } + + init(paymentData: OSPMTDataModel, shippingInfo: OSPMTContactInfoModel) { + self.paymentData = paymentData + self.shippingInfo = shippingInfo + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let paymentData = try container.decode(OSPMTDataModel.self, forKey: .paymentData) + let shippingInfo = try container.decode(OSPMTContactInfoModel.self, forKey: .shippingInfo) + self.init(paymentData: paymentData, shippingInfo: shippingInfo) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(paymentData, forKey: .paymentData) + try container.encode(shippingInfo, forKey: .shippingInfo) + } +} + +extension OSPMTScopeModel: Equatable { + static func == (lhs: OSPMTScopeModel, rhs: OSPMTScopeModel) -> Bool { + lhs.paymentData == rhs.paymentData && lhs.shippingInfo == rhs.shippingInfo + } +} diff --git a/OSPaymentsLib/Models/OSPMTTokenInfoModel.swift b/OSPaymentsLib/Models/OSPMTTokenInfoModel.swift new file mode 100644 index 0000000..f5d4828 --- /dev/null +++ b/OSPaymentsLib/Models/OSPMTTokenInfoModel.swift @@ -0,0 +1,26 @@ +struct OSPMTTokenInfoModel: Codable, Equatable { + let token: String + let type: String + + enum CodingKeys: String, CodingKey { + case token, type + } + + init(token: String, type: String) { + self.token = token + self.type = type + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let token = try container.decode(String.self, forKey: .token) + let type = try container.decode(String.self, forKey: .type) + self.init(token: token, type: type) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(token, forKey: .token) + try container.encode(type, forKey: .type) + } +} diff --git a/OSPaymentsLib/OSPMTApplePayHandler.swift b/OSPaymentsLib/OSPMTApplePayHandler.swift index 99194d2..cd59059 100644 --- a/OSPaymentsLib/OSPMTApplePayHandler.swift +++ b/OSPaymentsLib/OSPMTApplePayHandler.swift @@ -1,17 +1,22 @@ class OSPMTApplePayHandler: NSObject { let configuration: OSPMTConfigurationDelegate let availabilityBehaviour: OSPMTAvailabilityDelegate + let requestBehaviour: OSPMTRequestDelegate - init(configuration: OSPMTConfigurationDelegate, availabilityBehaviour: OSPMTAvailabilityDelegate) { + init(configuration: OSPMTConfigurationDelegate, availabilityBehaviour: OSPMTAvailabilityDelegate, requestBehaviour: OSPMTRequestDelegate) { self.configuration = configuration self.availabilityBehaviour = availabilityBehaviour + self.requestBehaviour = requestBehaviour super.init() } convenience init(configurationSource: OSPMTConfiguration = Bundle.main.infoDictionary!) { let applePayConfiguration = OSPMTApplePayConfiguration(source: configurationSource) let applePayAvailabilityBehaviour = OSPMTApplePayAvailabilityBehaviour(configuration: applePayConfiguration) - self.init(configuration: applePayConfiguration, availabilityBehaviour: applePayAvailabilityBehaviour) + let applePayRequestBehaviour = OSPMTApplePayRequestBehaviour(configuration: applePayConfiguration) + self.init( + configuration: applePayConfiguration, availabilityBehaviour: applePayAvailabilityBehaviour, requestBehaviour: applePayRequestBehaviour + ) } } @@ -24,4 +29,8 @@ extension OSPMTApplePayHandler: OSPMTHandlerDelegate { func checkWalletAvailability() -> OSPMTError? { self.availabilityBehaviour.checkWallet() ?? self.availabilityBehaviour.checkPayment() ?? self.availabilityBehaviour.checkPaymentSetup() } + + func set(_ detailsModel: OSPMTDetailsModel, completion: @escaping OSPMTCompletionHandler) { + self.requestBehaviour.trigger(with: detailsModel, completion) + } } diff --git a/OSPaymentsLib/OSPMTPayments.swift b/OSPaymentsLib/OSPMTPayments.swift index f768c7f..1bd1755 100644 --- a/OSPaymentsLib/OSPMTPayments.swift +++ b/OSPaymentsLib/OSPMTPayments.swift @@ -33,4 +33,44 @@ extension OSPMTPayments: OSPMTActionDelegate { self.delegate.callbackSuccess() } } + + func set(_ details: String) { + let detailsResult = self.decode(details) + switch detailsResult { + case .success(let detailsModel): + self.handler.set(detailsModel) { [weak self] result in + guard let self = self else { return } + switch result { + case .success(let scopeModel): + let scopeResult = self.encode(scopeModel) + switch scopeResult { + case .success(let scopeText): + self.delegate.callback(result: scopeText) + case .failure(let error): + self.delegate.callback(error: error) + } + case .failure(let error): + self.delegate.callback(error: error) + } + } + case .failure(let error): + self.delegate.callback(error: error) + } + } +} + +extension OSPMTPayments { + func decode(_ detailsText: String) -> Result { + guard + let detailsData = detailsText.data(using: .utf8), + let detailsModel = try? JSONDecoder().decode(OSPMTDetailsModel.self, from: detailsData) + else { return .failure(.invalidDecodeDetails) } + return .success(detailsModel) + } + + func encode(_ scopeModel: OSPMTScopeModel) -> Result { + guard let scopeData = try? JSONEncoder().encode(scopeModel), let scopeText = String(data: scopeData, encoding: .utf8) + else { return .failure(.invalidEncodeScope) } + return .success(scopeText) + } } diff --git a/OSPaymentsLib/Protocols/OSPMTActionDelegate.swift b/OSPaymentsLib/Protocols/OSPMTActionDelegate.swift index 2807a84..9cbb349 100644 --- a/OSPaymentsLib/Protocols/OSPMTActionDelegate.swift +++ b/OSPaymentsLib/Protocols/OSPMTActionDelegate.swift @@ -1,4 +1,5 @@ protocol OSPMTActionDelegate: AnyObject { func setupConfiguration() func checkWalletSetup() + func set(_ details: String) } diff --git a/OSPaymentsLib/Protocols/OSPMTAvailabilityDelegate.swift b/OSPaymentsLib/Protocols/OSPMTAvailabilityDelegate.swift index 4f91048..f89d9cc 100644 --- a/OSPaymentsLib/Protocols/OSPMTAvailabilityDelegate.swift +++ b/OSPaymentsLib/Protocols/OSPMTAvailabilityDelegate.swift @@ -1,11 +1,23 @@ import PassKit +protocol OSPMTWalletAvailabilityDelegate: AnyObject { + static func isWalletAvailable() -> Bool +} + +protocol OSPMTSetupAvailabilityDelegate: AnyObject { + static func isPaymentAvailable() -> Bool +} + protocol OSPMTAvailabilityDelegate: AnyObject { func checkWallet() -> OSPMTError? func checkPayment() -> OSPMTError? func checkPaymentSetup() -> OSPMTError? } +protocol OSPMTApplePaySetupAvailabilityDelegate: OSPMTSetupAvailabilityDelegate { + static func isPaymentAvailable(using networks: [PKPaymentNetwork], and merchantCapabilities: PKMerchantCapability) -> Bool +} + class OSPMTApplePayAvailabilityBehaviour: OSPMTAvailabilityDelegate { let configuration: OSPMTApplePayConfiguration let walletAvailableBehaviour: OSPMTWalletAvailabilityDelegate.Type diff --git a/OSPaymentsLib/Protocols/OSPMTConfigurationDelegate.swift b/OSPaymentsLib/Protocols/OSPMTConfigurationDelegate.swift index 203fcbb..f9139a2 100644 --- a/OSPaymentsLib/Protocols/OSPMTConfigurationDelegate.swift +++ b/OSPaymentsLib/Protocols/OSPMTConfigurationDelegate.swift @@ -97,7 +97,8 @@ class OSPMTApplePayConfiguration: OSPMTConfigurationDelegate { let merchantCountryCode = self.merchantCountryCode, let paymentAllowedNetworks = self.paymentAllowedNetworks, let paymentSupportedCapabilities = self.paymentSupportedCapabilities, - let paymentSupportedCardCountries = self.paymentSupportedCardCountries + let shippingSupportedContacts = self.shippingSupportedContacts, + let billingSupportedContacts = self.billingSupportedContacts else { return "" } var configurationDict: [String: Any] = [ @@ -107,15 +108,14 @@ class OSPMTApplePayConfiguration: OSPMTConfigurationDelegate { ConfigurationKeys.paymentAllowedNetworks: paymentAllowedNetworks, ConfigurationKeys.paymentSupportedCapabilities: paymentSupportedCapabilities, - ConfigurationKeys.paymentSupportedCardCountries: paymentSupportedCardCountries + + ConfigurationKeys.shippingSupportedContacts: shippingSupportedContacts, + + ConfigurationKeys.billingSupportedContacts: billingSupportedContacts ] - if let shippingSupportedContacts = shippingSupportedContacts { - configurationDict[ConfigurationKeys.shippingSupportedContacts] = shippingSupportedContacts - } - - if let billingSupportedContacts = self.billingSupportedContacts { - configurationDict[ConfigurationKeys.billingSupportedContacts] = billingSupportedContacts + if let paymentSupportedCardCountries = self.paymentSupportedCardCountries { + configurationDict[ConfigurationKeys.paymentSupportedCardCountries] = paymentSupportedCardCountries } guard let data = try? JSONSerialization.data(withJSONObject: configurationDict), let result = String(data: data, encoding: .utf8) @@ -145,4 +145,9 @@ extension OSPMTApplePayConfiguration { return !result.isEmpty ? result : nil } + + var supportedCountries: Set? { + guard let paymentSupportedCardCountries = self.paymentSupportedCardCountries else { return nil } + return !paymentSupportedCardCountries.isEmpty ? Set(paymentSupportedCardCountries) : nil + } } diff --git a/OSPaymentsLib/Protocols/OSPMTHandlerDelegate.swift b/OSPaymentsLib/Protocols/OSPMTHandlerDelegate.swift index ace3401..166c078 100644 --- a/OSPaymentsLib/Protocols/OSPMTHandlerDelegate.swift +++ b/OSPaymentsLib/Protocols/OSPMTHandlerDelegate.swift @@ -1,6 +1,7 @@ -typealias OSPMTCompletionHandler = (Result) -> Void +typealias OSPMTCompletionHandler = (Result) -> Void protocol OSPMTHandlerDelegate: AnyObject { func setupConfiguration() -> Result func checkWalletAvailability() -> OSPMTError? + func set(_ detailsModel: OSPMTDetailsModel, completion: @escaping OSPMTCompletionHandler) } diff --git a/OSPaymentsLib/Protocols/OSPMTRequestDelegate.swift b/OSPaymentsLib/Protocols/OSPMTRequestDelegate.swift new file mode 100644 index 0000000..d011296 --- /dev/null +++ b/OSPaymentsLib/Protocols/OSPMTRequestDelegate.swift @@ -0,0 +1,85 @@ +import PassKit + +typealias OSPMTRequestTriggerCompletion = ((Bool) -> Void) + +protocol OSPMTRequestTriggerDelegate: AnyObject { + func triggerPayment(_ completion: @escaping OSPMTRequestTriggerCompletion) +} + +protocol OSPMTRequestDelegate: AnyObject { + func trigger(with detailsModel: OSPMTDetailsModel, _ completion: @escaping OSPMTCompletionHandler) +} + +protocol OSPMTApplePayRequestTriggerDelegate: OSPMTRequestTriggerDelegate { + static func createRequestTriggerBehaviour(for detailsModel: OSPMTDetailsModel, andDelegate delegate: OSPMTApplePayRequestBehaviour?) -> Result +} + +class OSPMTApplePayRequestBehaviour: NSObject, OSPMTRequestDelegate { + let configuration: OSPMTApplePayConfiguration + var requestTriggerBehaviour: OSPMTApplePayRequestTriggerDelegate.Type + + var paymentStatus: PKPaymentAuthorizationStatus = .failure + var paymentScope: OSPMTScopeModel? + var completionHandler: OSPMTCompletionHandler! + + init(configuration: OSPMTApplePayConfiguration, requestTriggerBehaviour: OSPMTApplePayRequestTriggerDelegate.Type = PKPaymentAuthorizationController.self) { + self.configuration = configuration + self.requestTriggerBehaviour = requestTriggerBehaviour + } + + func trigger(with detailsModel: OSPMTDetailsModel, _ completion: @escaping OSPMTCompletionHandler) { + self.completionHandler = completion + + let result = self.requestTriggerBehaviour.createRequestTriggerBehaviour(for: detailsModel, andDelegate: self) + switch result { + case .success(let paymentController): + paymentController.triggerPayment { [weak self] presented in + guard let self = self else { return } + + self.paymentStatus = .failure + if !presented { + completion(.failure(.paymentTriggerPresentationFailed)) + } + } + case .failure(let error): + completion(.failure(error)) + } + } +} + +extension OSPMTApplePayRequestBehaviour { + func getPaymentSummaryItems(for detailsModel: OSPMTDetailsModel) -> [PKPaymentSummaryItem]? { + guard let label = self.configuration.merchantName else { return nil } + return [PKPaymentSummaryItem(label: label, amount: detailsModel.paymentAmount, type: detailsModel.paymentSummaryItemType)] + } + + func getContactFields(for text: [String]) -> Set { + return Set(text.compactMap(PKContactField.convert(from:))) + } +} + +// MARK: - Set up PKPaymentAuthorizationControllerDelegate conformance +extension OSPMTApplePayRequestBehaviour: PKPaymentAuthorizationControllerDelegate { + func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) { + controller.dismiss() + // The payment sheet doesn't automatically dismiss once it has finished. Dismiss the payment sheet. + DispatchQueue.main.async { + if self.paymentStatus == .success, let paymentScope = self.paymentScope { + self.completionHandler(.success(paymentScope)) + } else { + self.completionHandler(.failure(.paymentTriggerNotCompleted)) + } + } + } + + func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) { + if let scopeModel = payment.createScopeModel() { + self.paymentScope = scopeModel + self.paymentStatus = .success + } else { + self.paymentStatus = .failure + } + + completion(PKPaymentAuthorizationResult(status: self.paymentStatus, errors: [])) + } +} diff --git a/OSPaymentsLibTests/ModelSpecs/OSPMTAddressModelSpec.swift b/OSPaymentsLibTests/ModelSpecs/OSPMTAddressModelSpec.swift new file mode 100644 index 0000000..5048f1c --- /dev/null +++ b/OSPaymentsLibTests/ModelSpecs/OSPMTAddressModelSpec.swift @@ -0,0 +1,221 @@ +import Nimble +import Quick + +@testable import OSPaymentsLib + +class OSPMTAddressModelSpec: QuickSpec { + struct TestConfiguration { + static let fullConfig = [ + OSPMTAddressModel.CodingKeys.postalCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.fullAddress.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.countryCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.city.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.administrativeArea.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.state.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noPostalCodeConfig = [ + OSPMTAddressModel.CodingKeys.fullAddress.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.countryCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.city.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.administrativeArea.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.state.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noFullAddressConfig = [ + OSPMTAddressModel.CodingKeys.postalCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.countryCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.city.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.administrativeArea.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.state.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noCountryCodeConfig = [ + OSPMTAddressModel.CodingKeys.postalCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.fullAddress.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.city.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.administrativeArea.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.state.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noCityConfig = [ + OSPMTAddressModel.CodingKeys.postalCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.fullAddress.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.countryCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.administrativeArea.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.state.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noAdministrativeAreaConfig = [ + OSPMTAddressModel.CodingKeys.postalCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.fullAddress.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.countryCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.city.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.state.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noStateConfig = [ + OSPMTAddressModel.CodingKeys.postalCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.fullAddress.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.countryCode.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.city.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTAddressModel.CodingKeys.administrativeArea.rawValue: OSPMTTestConfigurations.dummyString + ] + + static let fullModel = OSPMTAddressModel( + postalCode: OSPMTTestConfigurations.dummyString, + fullAddress: OSPMTTestConfigurations.dummyString, + countryCode: OSPMTTestConfigurations.dummyString, + city: OSPMTTestConfigurations.dummyString, + administrativeArea: OSPMTTestConfigurations.dummyString, + state: OSPMTTestConfigurations.dummyString + ) + static let noAdministrativeAreaModel = OSPMTAddressModel( + postalCode: OSPMTTestConfigurations.dummyString, + fullAddress: OSPMTTestConfigurations.dummyString, + countryCode: OSPMTTestConfigurations.dummyString, + city: OSPMTTestConfigurations.dummyString, + state: OSPMTTestConfigurations.dummyString + ) + static let noStateModel = OSPMTAddressModel( + postalCode: OSPMTTestConfigurations.dummyString, + fullAddress: OSPMTTestConfigurations.dummyString, + countryCode: OSPMTTestConfigurations.dummyString, + city: OSPMTTestConfigurations.dummyString, + administrativeArea: OSPMTTestConfigurations.dummyString + ) + } + + override func spec() { + describe("Given a full configuration") { + context("When decoding the Address Model") { + it("Should return a filled object") { + if let addressModel = OSPMTTestConfigurations.decode(for: OSPMTAddressModel.self, TestConfiguration.fullConfig) { + expect(addressModel).toNot(beNil()) + expect(addressModel.postalCode).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.fullAddress).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.countryCode).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.city).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.administrativeArea).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.state).to(equal(OSPMTTestConfigurations.dummyString)) + } else { + fail() + } + } + } + } + + describe("Given a full configuration without Postal Code") { + context("When decoding the Address Model") { + it("Should return a nil value") { + if OSPMTTestConfigurations.decode(for: OSPMTAddressModel.self, TestConfiguration.noPostalCodeConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full configuration without Full Address") { + context("When decoding the Address Model") { + it("Should return a nil value") { + if OSPMTTestConfigurations.decode(for: OSPMTAddressModel.self, TestConfiguration.noFullAddressConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full configuration without Country Code") { + context("When decoding the Address Model") { + it("Should return a nil value") { + if OSPMTTestConfigurations.decode(for: OSPMTAddressModel.self, TestConfiguration.noCountryCodeConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full configuration without City") { + context("When decoding the Address Model") { + it("Should return a nil value") { + if OSPMTTestConfigurations.decode(for: OSPMTAddressModel.self, TestConfiguration.noCityConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full configuration without Administrative Area") { + context("When decoding the Address Model") { + it("Should return a filled object") { + if let addressModel = OSPMTTestConfigurations.decode(for: OSPMTAddressModel.self, TestConfiguration.noAdministrativeAreaConfig) { + expect(addressModel).toNot(beNil()) + expect(addressModel.postalCode).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.fullAddress).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.countryCode).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.city).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.state).to(equal(OSPMTTestConfigurations.dummyString)) + } else { + fail() + } + } + } + } + + describe("Given a full configuration without State") { + context("When decoding the Address Model") { + it("Should return a filled object") { + if let addressModel = OSPMTTestConfigurations.decode(for: OSPMTAddressModel.self, TestConfiguration.fullConfig) { + expect(addressModel).toNot(beNil()) + expect(addressModel.postalCode).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.fullAddress).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.countryCode).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.city).to(equal(OSPMTTestConfigurations.dummyString)) + expect(addressModel.administrativeArea).to(equal(OSPMTTestConfigurations.dummyString)) + } else { + fail() + } + } + } + } + + describe("Given a full model") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let addressText = OSPMTTestConfigurations.encode(TestConfiguration.fullModel) { + expect(addressText).toNot(beEmpty()) + } else { + fail() + } + } + } + } + + describe("Given a full model without Adminsitrative Area") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let addressText = OSPMTTestConfigurations.encode(TestConfiguration.noAdministrativeAreaModel) { + expect(addressText).toNot(beEmpty()) + } else { + fail() + } + } + } + } + + describe("Given a full model without State") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let addressText = OSPMTTestConfigurations.encode(TestConfiguration.noStateModel) { + expect(addressText).toNot(beEmpty()) + } else { + fail() + } + } + } + } + } + +} diff --git a/OSPaymentsLibTests/ModelSpecs/OSPMTContactInfoModelSpec.swift b/OSPaymentsLibTests/ModelSpecs/OSPMTContactInfoModelSpec.swift new file mode 100644 index 0000000..fc4c0c5 --- /dev/null +++ b/OSPaymentsLibTests/ModelSpecs/OSPMTContactInfoModelSpec.swift @@ -0,0 +1,200 @@ +import Nimble +import Quick + +@testable import OSPaymentsLib + +class OSPMTContactInfoModelSpec: QuickSpec { + struct TestConfiguration { + static let fullConfig: [String: Any] = [ + OSPMTContactInfoModel.CodingKeys.address.rawValue: OSPMTAddressModelSpec.TestConfiguration.fullConfig, + OSPMTContactInfoModel.CodingKeys.phoneNumber.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTContactInfoModel.CodingKeys.name.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTContactInfoModel.CodingKeys.email.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noAddressConfig: [String: Any] = [ + OSPMTContactInfoModel.CodingKeys.phoneNumber.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTContactInfoModel.CodingKeys.name.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTContactInfoModel.CodingKeys.email.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noPhoneNumberConfig: [String: Any] = [ + OSPMTContactInfoModel.CodingKeys.address.rawValue: OSPMTTestConfigurations.dummyContactInfoModel, + OSPMTContactInfoModel.CodingKeys.name.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTContactInfoModel.CodingKeys.email.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noNameConfig: [String: Any] = [ + OSPMTContactInfoModel.CodingKeys.address.rawValue: OSPMTTestConfigurations.dummyContactInfoModel, + OSPMTContactInfoModel.CodingKeys.phoneNumber.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTContactInfoModel.CodingKeys.email.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noEmailConfig: [String: Any] = [ + OSPMTContactInfoModel.CodingKeys.address.rawValue: OSPMTTestConfigurations.dummyContactInfoModel, + OSPMTContactInfoModel.CodingKeys.phoneNumber.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTContactInfoModel.CodingKeys.name.rawValue: OSPMTTestConfigurations.dummyString + ] + + static let fullModel = OSPMTContactInfoModel( + address: OSPMTTestConfigurations.dummyAddressModel, + phoneNumber: OSPMTTestConfigurations.dummyString, + name: OSPMTTestConfigurations.dummyString, + email: OSPMTTestConfigurations.dummyString + ) + static let noAddressModel = OSPMTContactInfoModel( + phoneNumber: OSPMTTestConfigurations.dummyString, + name: OSPMTTestConfigurations.dummyString, + email: OSPMTTestConfigurations.dummyString + ) + static let noPhoneNumberModel = OSPMTContactInfoModel( + address: OSPMTTestConfigurations.dummyAddressModel, + name: OSPMTTestConfigurations.dummyString, + email: OSPMTTestConfigurations.dummyString + ) + static let noNameModel = OSPMTContactInfoModel( + address: OSPMTTestConfigurations.dummyAddressModel, + phoneNumber: OSPMTTestConfigurations.dummyString, + email: OSPMTTestConfigurations.dummyString + ) + static let noEmailModel = OSPMTContactInfoModel( + address: OSPMTTestConfigurations.dummyAddressModel, + phoneNumber: OSPMTTestConfigurations.dummyString, + name: OSPMTTestConfigurations.dummyString + ) + } + + override func spec() { + describe("Given a full configuration") { + context("When decoding the Contact Info Model") { + it("Should return a filled object") { + if let model = OSPMTTestConfigurations.decode(for: OSPMTContactInfoModel.self, TestConfiguration.fullConfig) { + expect(model).toNot(beNil()) + expect(model.address).to(equal(OSPMTAddressModelSpec.TestConfiguration.fullModel)) + expect(model.phoneNumber).to(equal(OSPMTTestConfigurations.dummyString)) + expect(model.name).to(equal(OSPMTTestConfigurations.dummyString)) + expect(model.email).to(equal(OSPMTTestConfigurations.dummyString)) + } else { + fail() + } + } + } + } + + describe("Given a full configuration without address") { + context("When decoding the Contact Info Model") { + it("Should return a filled object") { + if let model = OSPMTTestConfigurations.decode(for: OSPMTContactInfoModel.self, TestConfiguration.fullConfig) { + expect(model).toNot(beNil()) + expect(model.phoneNumber).to(equal(OSPMTTestConfigurations.dummyString)) + expect(model.name).to(equal(OSPMTTestConfigurations.dummyString)) + expect(model.email).to(equal(OSPMTTestConfigurations.dummyString)) + } else { + fail() + } + } + } + } + + describe("Given a full configuration without phone number") { + context("When decoding the Contact Info Model") { + it("Should return a filled object") { + if let model = OSPMTTestConfigurations.decode(for: OSPMTContactInfoModel.self, TestConfiguration.fullConfig) { + expect(model).toNot(beNil()) + expect(model.address).to(equal(OSPMTAddressModelSpec.TestConfiguration.fullModel)) + expect(model.name).to(equal(OSPMTTestConfigurations.dummyString)) + expect(model.email).to(equal(OSPMTTestConfigurations.dummyString)) + } else { + fail() + } + } + } + } + + describe("Given a full configuration without name") { + context("When decoding the Contact Info Model") { + it("Should return a filled object") { + if let model = OSPMTTestConfigurations.decode(for: OSPMTContactInfoModel.self, TestConfiguration.fullConfig) { + expect(model).toNot(beNil()) + expect(model.address).to(equal(OSPMTAddressModelSpec.TestConfiguration.fullModel)) + expect(model.phoneNumber).to(equal(OSPMTTestConfigurations.dummyString)) + expect(model.email).to(equal(OSPMTTestConfigurations.dummyString)) + } else { + fail() + } + } + } + } + + describe("Given a full configuration without email") { + context("When decoding the Contact Info Model") { + it("Should return a filled object") { + if let model = OSPMTTestConfigurations.decode(for: OSPMTContactInfoModel.self, TestConfiguration.fullConfig) { + expect(model).toNot(beNil()) + expect(model.address).to(equal(OSPMTAddressModelSpec.TestConfiguration.fullModel)) + expect(model.phoneNumber).to(equal(OSPMTTestConfigurations.dummyString)) + expect(model.name).to(equal(OSPMTTestConfigurations.dummyString)) + } else { + fail() + } + } + } + } + + describe("Given a full model") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let text = OSPMTTestConfigurations.encode(TestConfiguration.fullModel) { + expect(text).toNot(beEmpty()) + } else { + fail() + } + } + } + } + + describe("Given a full model without address") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let text = OSPMTTestConfigurations.encode(TestConfiguration.noAddressModel) { + expect(text).toNot(beEmpty()) + } else { + fail() + } + } + } + } + + describe("Given a full model without phone number") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let text = OSPMTTestConfigurations.encode(TestConfiguration.noPhoneNumberModel) { + expect(text).toNot(beEmpty()) + } else { + fail() + } + } + } + } + + describe("Given a full model without name") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let text = OSPMTTestConfigurations.encode(TestConfiguration.noNameModel) { + expect(text).toNot(beEmpty()) + } else { + fail() + } + } + } + } + + describe("Given a full model without email") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let text = OSPMTTestConfigurations.encode(TestConfiguration.noEmailModel) { + expect(text).toNot(beEmpty()) + } else { + fail() + } + } + } + } + } +} diff --git a/OSPaymentsLibTests/ModelSpecs/OSPMTDataModelSpec.swift b/OSPaymentsLibTests/ModelSpecs/OSPMTDataModelSpec.swift new file mode 100644 index 0000000..b71e035 --- /dev/null +++ b/OSPaymentsLibTests/ModelSpecs/OSPMTDataModelSpec.swift @@ -0,0 +1,120 @@ +import Nimble +import Quick + +@testable import OSPaymentsLib + +class OSPMTDataModelSpec: QuickSpec { + struct TestConfiguration { + static let fullConfig: [String: Any] = [ + OSPMTDataModel.CodingKeys.billingInfo.rawValue: OSPMTContactInfoModelSpec.TestConfiguration.fullConfig, + OSPMTDataModel.CodingKeys.cardDetails.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTDataModel.CodingKeys.cardNetwork.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTDataModel.CodingKeys.tokenData.rawValue: OSPMTTokenInfoModelSpec.TestConfiguration.fullConfig + ] + static let noBillingInfoConfig: [String: Any] = [ + OSPMTDataModel.CodingKeys.cardDetails.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTDataModel.CodingKeys.cardNetwork.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTDataModel.CodingKeys.tokenData.rawValue: OSPMTTokenInfoModelSpec.TestConfiguration.fullConfig + ] + static let noCardDetailsConfig: [String: Any] = [ + OSPMTDataModel.CodingKeys.billingInfo.rawValue: OSPMTContactInfoModelSpec.TestConfiguration.fullConfig, + OSPMTDataModel.CodingKeys.cardNetwork.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTDataModel.CodingKeys.tokenData.rawValue: OSPMTTokenInfoModelSpec.TestConfiguration.fullConfig + ] + static let noCardNetworkConfig: [String: Any] = [ + OSPMTDataModel.CodingKeys.billingInfo.rawValue: OSPMTContactInfoModelSpec.TestConfiguration.fullConfig, + OSPMTDataModel.CodingKeys.cardDetails.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTDataModel.CodingKeys.tokenData.rawValue: OSPMTTokenInfoModelSpec.TestConfiguration.fullConfig + ] + static let noTokenDataConfig: [String: Any] = [ + OSPMTDataModel.CodingKeys.billingInfo.rawValue: OSPMTContactInfoModelSpec.TestConfiguration.fullConfig, + OSPMTDataModel.CodingKeys.cardDetails.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTDataModel.CodingKeys.cardNetwork.rawValue: OSPMTTestConfigurations.dummyString + ] + + static let fullModel = OSPMTDataModel( + billingInfo: OSPMTContactInfoModelSpec.TestConfiguration.fullModel, + cardDetails: OSPMTTestConfigurations.dummyString, + cardNetwork: OSPMTTestConfigurations.dummyString, + tokenData: OSPMTTokenInfoModelSpec.TestConfiguration.fullModel + ) + } + + override func spec() { + describe("Given a full configuration") { + context("When decoding the Data Model") { + it("Should return a filled object") { + if let model = OSPMTTestConfigurations.decode(for: OSPMTDataModel.self, TestConfiguration.fullConfig) { + expect(model).toNot(beNil()) + expect(model.billingInfo).toNot(beNil()) + expect(model.cardDetails).to(equal(OSPMTTestConfigurations.dummyString)) + expect(model.cardNetwork).to(equal(OSPMTTestConfigurations.dummyString)) + expect(model.tokenData).to(equal(OSPMTTokenInfoModelSpec.TestConfiguration.fullModel)) + } else { + fail() + } + } + } + } + + describe("Given a full configuration without Billing Info") { + context("When decoding the Data Model") { + it("Should return a nil object") { + if OSPMTTestConfigurations.decode(for: OSPMTDataModel.self, TestConfiguration.noBillingInfoConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full configuration without Card Details") { + context("When decoding the Data Model") { + it("Should return a nil object") { + if OSPMTTestConfigurations.decode(for: OSPMTDataModel.self, TestConfiguration.noCardDetailsConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full configuration without Card Network") { + context("When decoding the Data Model") { + it("Should return a nil object") { + if OSPMTTestConfigurations.decode(for: OSPMTDataModel.self, TestConfiguration.noCardNetworkConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full configuration without Token Data") { + context("When decoding the Data Model") { + it("Should return a nil object") { + if OSPMTTestConfigurations.decode(for: OSPMTDataModel.self, TestConfiguration.noTokenDataConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full model") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let text = OSPMTTestConfigurations.encode(TestConfiguration.fullModel) { + expect(text).toNot(beEmpty()) + } else { + fail() + } + } + } + } + } +} diff --git a/OSPaymentsLibTests/ModelSpecs/OSPMTScopeModelSpec.swift b/OSPaymentsLibTests/ModelSpecs/OSPMTScopeModelSpec.swift new file mode 100644 index 0000000..05a89c9 --- /dev/null +++ b/OSPaymentsLibTests/ModelSpecs/OSPMTScopeModelSpec.swift @@ -0,0 +1,75 @@ +import Nimble +import Quick + +@testable import OSPaymentsLib +import PassKit + +class OSPMTScopeModelSpec: QuickSpec { + struct TestConfiguration { + static let fullConfig = [ + OSPMTScopeModel.CodingKeys.paymentData.rawValue: OSPMTDataModelSpec.TestConfiguration.fullConfig, + OSPMTScopeModel.CodingKeys.shippingInfo.rawValue: OSPMTContactInfoModelSpec.TestConfiguration.fullConfig + ] + static let noPaymentDataConfig = [OSPMTScopeModel.CodingKeys.shippingInfo.rawValue: OSPMTContactInfoModelSpec.TestConfiguration.fullConfig] + static let noShippingInfoConfig = [OSPMTScopeModel.CodingKeys.paymentData.rawValue: OSPMTDataModelSpec.TestConfiguration.fullConfig] + + static let fullModel = OSPMTScopeModel( + paymentData: OSPMTDataModelSpec.TestConfiguration.fullModel, + shippingInfo: OSPMTContactInfoModelSpec.TestConfiguration.fullModel + ) + + static let paymentRequest = PKPaymentRequest() + } + + override func spec() { + describe("Given a full configuration") { + context("When decoding the Data Model") { + it("Should return a filled object") { + if let model = OSPMTTestConfigurations.decode(for: OSPMTScopeModel.self, TestConfiguration.fullConfig) { + expect(model).toNot(beNil()) + expect(model.paymentData).toNot(beNil()) + expect(model.shippingInfo).toNot(beNil()) + } else { + fail() + } + } + } + } + + describe("Given a full configuration without Payment Data") { + context("When decoding the Data Model") { + it("Should return a nil object") { + if OSPMTTestConfigurations.decode(for: OSPMTScopeModel.self, TestConfiguration.noPaymentDataConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full configuration without Billing Info") { + context("When decoding the Data Model") { + it("Should return a nil object") { + if OSPMTTestConfigurations.decode(for: OSPMTScopeModel.self, TestConfiguration.noShippingInfoConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full model") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let text = OSPMTTestConfigurations.encode(TestConfiguration.fullModel) { + expect(text).toNot(beEmpty()) + } else { + fail() + } + } + } + } + } +} diff --git a/OSPaymentsLibTests/ModelSpecs/OSPMTTokenInfoModelSpec.swift b/OSPaymentsLibTests/ModelSpecs/OSPMTTokenInfoModelSpec.swift new file mode 100644 index 0000000..6bce82b --- /dev/null +++ b/OSPaymentsLibTests/ModelSpecs/OSPMTTokenInfoModelSpec.swift @@ -0,0 +1,69 @@ +import Nimble +import Quick + +@testable import OSPaymentsLib + +class OSPMTTokenInfoModelSpec: QuickSpec { + struct TestConfiguration { + static let fullConfig = [ + OSPMTTokenInfoModel.CodingKeys.token.rawValue: OSPMTTestConfigurations.dummyString, + OSPMTTokenInfoModel.CodingKeys.type.rawValue: OSPMTTestConfigurations.dummyString + ] + static let noTokenConfig = [OSPMTTokenInfoModel.CodingKeys.type.rawValue: OSPMTTestConfigurations.dummyString] + static let noTypeConfig = [OSPMTTokenInfoModel.CodingKeys.token.rawValue: OSPMTTestConfigurations.dummyString] + + static let fullModel = OSPMTTokenInfoModel(token: OSPMTTestConfigurations.dummyString, type: OSPMTTestConfigurations.dummyString) + } + + override func spec() { + describe("Given a full configuration") { + context("When decoding the Token Info Model") { + it("Should return a filled object") { + if let model = OSPMTTestConfigurations.decode(for: OSPMTTokenInfoModel.self, TestConfiguration.fullConfig) { + expect(model).toNot(beNil()) + expect(model.token).to(equal(OSPMTTestConfigurations.dummyString)) + expect(model.type).to(equal(OSPMTTestConfigurations.dummyString)) + } else { + fail() + } + } + } + } + + describe("Given a full configuration without token") { + context("When decoding the Token Info Model") { + it("Should return a nil object") { + if OSPMTTestConfigurations.decode(for: OSPMTTokenInfoModel.self, TestConfiguration.noTokenConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full configuration without type") { + context("When decoding the Token Info Model") { + it("Should return a nil object") { + if OSPMTTestConfigurations.decode(for: OSPMTTokenInfoModel.self, TestConfiguration.noTypeConfig) != nil { + fail() + } else { + expect(true).to(beTruthy()) + } + } + } + } + + describe("Given a full model") { + context("When encoding it into JSON") { + it("Should return a filled object") { + if let text = OSPMTTestConfigurations.encode(TestConfiguration.fullModel) { + expect(text).toNot(beEmpty()) + } else { + fail() + } + } + } + } + } +} diff --git a/OSPaymentsLibTests/OSPMTApplePayConfigurationSpec.swift b/OSPaymentsLibTests/OSPMTApplePayConfigurationSpec.swift index 0b2f8ef..29258cb 100644 --- a/OSPaymentsLibTests/OSPMTApplePayConfigurationSpec.swift +++ b/OSPaymentsLibTests/OSPMTApplePayConfigurationSpec.swift @@ -23,6 +23,12 @@ class OSPMTApplePayConfigurationSpec: QuickSpec { expect(applePayConfiguration.merchantCapabilities).to(equal([PKMerchantCapability.capability3DS, .capabilityEMV])) } } + + context("When checking the Supported Countries property") { + it("should return a valid value") { + expect(applePayConfiguration.supportedCountries).to(equal(Set([OSPMTTestConfigurations.dummyString]))) + } + } } describe("Given a configuration with both valid an invalid values") { @@ -59,6 +65,12 @@ class OSPMTApplePayConfigurationSpec: QuickSpec { expect(applePayConfiguration.merchantCapabilities).to(beNil()) } } + + context("When checking the Supported Countries property") { + it("should return a nil value") { + expect(applePayConfiguration.supportedCountries).to(beNil()) + } + } } describe("Given a nil configuration") { diff --git a/OSPaymentsLibTests/OSPMTApplePayHandlerSpec.swift b/OSPaymentsLibTests/OSPMTApplePayHandlerSpec.swift index b940491..c51ce63 100644 --- a/OSPaymentsLibTests/OSPMTApplePayHandlerSpec.swift +++ b/OSPaymentsLibTests/OSPMTApplePayHandlerSpec.swift @@ -26,10 +26,29 @@ class OSPMTMockAvailabilityBehaviour: OSPMTAvailabilityDelegate { } } +class OSPMTMockRequestBehaviour: OSPMTRequestDelegate { + var error: OSPMTError? + var scopeModel: OSPMTScopeModel? + + init(error: OSPMTError? = nil, scopeModel: OSPMTScopeModel? = nil) { + self.error = error + self.scopeModel = scopeModel + } + + func trigger(with detailsModel: OSPMTDetailsModel, _ completion: @escaping OSPMTCompletionHandler) { + if let error = self.error { + completion(.failure(error)) + } else { + completion(.success(self.scopeModel ?? OSPMTTestConfigurations.dummyScopeModel)) + } + } +} + class OSPMTApplePayHandlerSpec: QuickSpec { override func spec() { var applePayHandler: OSPMTApplePayHandler! var mockAvailabilityBehaviour: OSPMTMockAvailabilityBehaviour! + var mockRequestBehaviour: OSPMTMockRequestBehaviour! describe("Given a configuration") { beforeEach { @@ -160,11 +179,11 @@ class OSPMTApplePayHandlerSpec: QuickSpec { } context("When Apple Pay Handler is initialized") { - it("should return a InvalidConfiguration error") { + it("should return a successful configuration string") { let result = applePayHandler.setupConfiguration() - if case let .failure(error) = result { - expect(error).to(equal(.invalidConfiguration)) + if case let .success(text) = result { + expect(text).toNot(beEmpty()) } else { fail() } @@ -178,11 +197,11 @@ class OSPMTApplePayHandlerSpec: QuickSpec { } context("When Apple Pay Handler is initialized") { - it("should return a successful configuration string") { + it("should return a InvalidConfiguration error") { let result = applePayHandler.setupConfiguration() - if case let .success(text) = result { - expect(text).toNot(beEmpty()) + if case let .failure(error) = result { + expect(error).to(equal(.invalidConfiguration)) } else { fail() } @@ -196,11 +215,11 @@ class OSPMTApplePayHandlerSpec: QuickSpec { } context("When Apple Pay Handler is initialized") { - it("should return a successful configuration string") { + it("should return a InvalidConfiguration error") { let result = applePayHandler.setupConfiguration() - if case let .success(text) = result { - expect(text).toNot(beEmpty()) + if case let .failure(error) = result { + expect(error).to(equal(.invalidConfiguration)) } else { fail() } @@ -214,11 +233,14 @@ class OSPMTApplePayHandlerSpec: QuickSpec { beforeEach { applePayConfiguration = OSPMTApplePayConfiguration(source: OSPMTTestConfigurations.fullConfig) + mockRequestBehaviour = OSPMTMockRequestBehaviour() } context("When Wallet is not available for usage") { beforeEach { mockAvailabilityBehaviour = OSPMTMockAvailabilityBehaviour(error: .walletNotAvailable) - applePayHandler = OSPMTApplePayHandler(configuration: applePayConfiguration, availabilityBehaviour: mockAvailabilityBehaviour) + applePayHandler = OSPMTApplePayHandler( + configuration: applePayConfiguration, availabilityBehaviour: mockAvailabilityBehaviour, requestBehaviour: mockRequestBehaviour + ) } it("should return the walletNotAvailable error") { @@ -231,7 +253,9 @@ class OSPMTApplePayHandlerSpec: QuickSpec { context("When Payment is not available for usage") { beforeEach { mockAvailabilityBehaviour = OSPMTMockAvailabilityBehaviour(error: .paymentNotAvailable) - applePayHandler = OSPMTApplePayHandler(configuration: applePayConfiguration, availabilityBehaviour: mockAvailabilityBehaviour) + applePayHandler = OSPMTApplePayHandler( + configuration: applePayConfiguration, availabilityBehaviour: mockAvailabilityBehaviour, requestBehaviour: mockRequestBehaviour + ) } it("should return the paymentNotAvailable error") { @@ -244,7 +268,9 @@ class OSPMTApplePayHandlerSpec: QuickSpec { context("When Payment is not available for usage") { beforeEach { mockAvailabilityBehaviour = OSPMTMockAvailabilityBehaviour(error: .setupPaymentNotAvailable) - applePayHandler = OSPMTApplePayHandler(configuration: applePayConfiguration, availabilityBehaviour: mockAvailabilityBehaviour) + applePayHandler = OSPMTApplePayHandler( + configuration: applePayConfiguration, availabilityBehaviour: mockAvailabilityBehaviour, requestBehaviour: mockRequestBehaviour + ) } it("should return the setupPaymentNotAvailable error") { @@ -257,7 +283,9 @@ class OSPMTApplePayHandlerSpec: QuickSpec { context("When everything is available for usage") { beforeEach { mockAvailabilityBehaviour = OSPMTMockAvailabilityBehaviour() - applePayHandler = OSPMTApplePayHandler(configuration: applePayConfiguration, availabilityBehaviour: mockAvailabilityBehaviour) + applePayHandler = OSPMTApplePayHandler( + configuration: applePayConfiguration, availabilityBehaviour: mockAvailabilityBehaviour, requestBehaviour: mockRequestBehaviour + ) } it("should return a nil error") { @@ -266,6 +294,52 @@ class OSPMTApplePayHandlerSpec: QuickSpec { expect(error).to(beNil()) } } - } + } + + describe("Given an RequestBehaviour") { + var applePayConfiguration: OSPMTApplePayConfiguration! + + beforeEach { + applePayConfiguration = OSPMTApplePayConfiguration(source: OSPMTTestConfigurations.fullConfig) + mockAvailabilityBehaviour = OSPMTMockAvailabilityBehaviour() + } + context("When an error occurs while triggering") { + beforeEach { + mockRequestBehaviour = OSPMTMockRequestBehaviour(error: .invalidConfiguration) + applePayHandler = OSPMTApplePayHandler( + configuration: applePayConfiguration, availabilityBehaviour: mockAvailabilityBehaviour, requestBehaviour: mockRequestBehaviour + ) + } + + it("should return that same error") { + applePayHandler.set(OSPMTTestConfigurations.dummyDetailsModel) { result in + if case let .failure(error) = result { + expect(error).to(equal(mockRequestBehaviour.error)) + } else { + fail() + } + } + } + } + + context("When the trigger is successful") { + beforeEach { + mockRequestBehaviour = OSPMTMockRequestBehaviour(scopeModel: OSPMTTestConfigurations.dummyScopeModel) + applePayHandler = OSPMTApplePayHandler( + configuration: applePayConfiguration, availabilityBehaviour: mockAvailabilityBehaviour, requestBehaviour: mockRequestBehaviour + ) + } + + it("should return the resulting Payment Scope value") { + applePayHandler.set(OSPMTTestConfigurations.dummyDetailsModel) { result in + if case let .success(scopeModel) = result { + expect(scopeModel).to(equal(mockRequestBehaviour.scopeModel)) + } else { + fail() + } + } + } + } + } } } diff --git a/OSPaymentsLibTests/OSPMTApplePayRequestBehaviourSpec.swift b/OSPaymentsLibTests/OSPMTApplePayRequestBehaviourSpec.swift new file mode 100644 index 0000000..3015499 --- /dev/null +++ b/OSPaymentsLibTests/OSPMTApplePayRequestBehaviourSpec.swift @@ -0,0 +1,164 @@ +import Nimble +import PassKit +import Quick +@testable import OSPaymentsLib + +class MockRequestTriggerBehaviour: OSPMTApplePayRequestTriggerDelegate { + static var error: OSPMTError? + static var isCompleted: Bool = false + static var paymentScope: OSPMTScopeModel? + + required init() {} + + static func createRequestTriggerBehaviour(for detailsModel: OSPMTDetailsModel, andDelegate delegate: OSPMTApplePayRequestBehaviour?) -> Result { + if let error = error { + return .failure(error) + } else { + delegate?.paymentScope = Self.paymentScope + return .success(Self.init()) + } + } + + func triggerPayment(_ completion: @escaping OSPMTRequestTriggerCompletion) { + completion(Self.isCompleted) + } +} + +class OSPMTApplePayRequestBehaviourSpec: QuickSpec { + override func spec() { + var applePayRequestBehaviour: OSPMTApplePayRequestBehaviour! + + var mockConfiguration: OSPMTApplePayConfiguration! + + describe("Given a Request Behaviour") { + context("When it's incorrectly configured") { + beforeEach { + mockConfiguration = OSPMTApplePayConfiguration(source: OSPMTTestConfigurations.validNetworkCapabilityConfig) + applePayRequestBehaviour = OSPMTApplePayRequestBehaviour( + configuration: mockConfiguration, requestTriggerBehaviour: MockRequestTriggerBehaviour.self + ) + MockRequestTriggerBehaviour.error = .invalidConfiguration + } + + it("should return an InvalidConfiguration error") { + applePayRequestBehaviour.trigger(with: OSPMTTestConfigurations.dummyDetailsModel) { result in + if case let .failure(error) = result { + expect(error).to(equal(.invalidConfiguration)) + } else { + fail() + } + } + } + } + + context("When it's not possible to trigger Apple Pay") { + beforeEach { + mockConfiguration = OSPMTApplePayConfiguration(source: OSPMTTestConfigurations.validNetworkCapabilityConfig) + applePayRequestBehaviour = OSPMTApplePayRequestBehaviour( + configuration: mockConfiguration, requestTriggerBehaviour: MockRequestTriggerBehaviour.self + ) + MockRequestTriggerBehaviour.error = nil + MockRequestTriggerBehaviour.isCompleted = false + } + + it("should return a PaymentTriggerPresentationFailed error") { + applePayRequestBehaviour.trigger(with: OSPMTTestConfigurations.dummyDetailsModel) { result in + if case let .failure(error) = result { + expect(error).to(equal(.paymentTriggerPresentationFailed)) + } else { + fail() + } + } + } + } + + context("When it's possible to trigger Apple Pay") { + beforeEach { + mockConfiguration = OSPMTApplePayConfiguration(source: OSPMTTestConfigurations.validNetworkCapabilityConfig) + applePayRequestBehaviour = OSPMTApplePayRequestBehaviour( + configuration: mockConfiguration, requestTriggerBehaviour: MockRequestTriggerBehaviour.self + ) + MockRequestTriggerBehaviour.error = nil + MockRequestTriggerBehaviour.isCompleted = true + MockRequestTriggerBehaviour.paymentScope = OSPMTTestConfigurations.dummyScopeModel + } + + it("should return a valid Scope Model") { + applePayRequestBehaviour.trigger(with: OSPMTTestConfigurations.dummyDetailsModel) { result in + if case let .success(scopeModel) = result { + expect(scopeModel).to(equal(OSPMTTestConfigurations.dummyScopeModel)) + } else { + fail() + } + } + } + } + + context("When checking the payment networks") { + context("and the configuration lacks the merchant's name") { + beforeEach { + mockConfiguration = OSPMTApplePayConfiguration(source: OSPMTTestConfigurations.noMerchantNameConfig) + applePayRequestBehaviour = OSPMTApplePayRequestBehaviour( + configuration: mockConfiguration, requestTriggerBehaviour: MockRequestTriggerBehaviour.self + ) + } + it("should return a nil value") { + let result = applePayRequestBehaviour.getPaymentSummaryItems(for: OSPMTTestConfigurations.dummyDetailsModel) + + expect(result).to(beNil()) + } + } + + context("and the configuration has the merchant's name") { + beforeEach { + mockConfiguration = OSPMTApplePayConfiguration(source: OSPMTTestConfigurations.fullConfig) + applePayRequestBehaviour = OSPMTApplePayRequestBehaviour( + configuration: mockConfiguration, requestTriggerBehaviour: MockRequestTriggerBehaviour.self + ) + } + it("should return a non empty value") { + let result = applePayRequestBehaviour.getPaymentSummaryItems(for: OSPMTTestConfigurations.dummyDetailsModel) + + expect(result).toNot(beEmpty()) + } + } + } + + context("When checking the contact fields") { + beforeEach { + mockConfiguration = OSPMTApplePayConfiguration(source: OSPMTTestConfigurations.fullConfig) + applePayRequestBehaviour = OSPMTApplePayRequestBehaviour( + configuration: mockConfiguration, requestTriggerBehaviour: MockRequestTriggerBehaviour.self + ) + } + context("and no contact is provided") { + it("should return a nil value") { + let result = applePayRequestBehaviour.getContactFields(for: OSPMTTestConfigurations.emptyContactFieldArray) + expect(result).to(beEmpty()) + } + } + + context("and a invalid contact is provided") { + it("should return a nil value") { + let result = applePayRequestBehaviour.getContactFields(for: OSPMTTestConfigurations.invalidContactFieldArray) + expect(result).to(beEmpty()) + } + } + + context("and a invalid contact and a valid is provided") { + it("should return the a array with the valid value") { + let result = applePayRequestBehaviour.getContactFields(for: OSPMTTestConfigurations.withInvalidContactFieldArray) + expect(result.count).to(equal(1)) + } + } + + context("and a valid contacts is provided") { + it("should return the a array with the valid value") { + let result = applePayRequestBehaviour.getContactFields(for: OSPMTTestConfigurations.validContactFieldArray) + expect(result.count).to(equal(2)) + } + } + } + } + } +} diff --git a/OSPaymentsLibTests/OSPMTPaymentsSpec.swift b/OSPaymentsLibTests/OSPMTPaymentsSpec.swift index bda672b..7f1818c 100644 --- a/OSPaymentsLibTests/OSPMTPaymentsSpec.swift +++ b/OSPaymentsLibTests/OSPMTPaymentsSpec.swift @@ -15,10 +15,12 @@ class OSPMTMockCallback: OSPMTCallbackDelegate { class OSPMTMockHandler: OSPMTHandlerDelegate { var successText: String? var error: OSPMTError? + var scopeModel: OSPMTScopeModel? - init(successText: String? = nil, error: OSPMTError? = nil) { + init(successText: String? = nil, error: OSPMTError? = nil, scopeModel: OSPMTScopeModel? = nil) { self.successText = successText self.error = error + self.scopeModel = scopeModel } func setupConfiguration() -> Result { @@ -32,6 +34,14 @@ class OSPMTMockHandler: OSPMTHandlerDelegate { func checkWalletAvailability() -> OSPMTError? { return self.error } + + func set(_ detailsModel: OSPMTDetailsModel, completion: @escaping OSPMTCompletionHandler) { + if let error = self.error { + completion(.failure(error)) + } else if let scopeModel = self.scopeModel { + completion(.success(scopeModel)) + } + } } class OSPMTPaymentsSpec: QuickSpec { @@ -48,7 +58,9 @@ class OSPMTPaymentsSpec: QuickSpec { describe("and a correctly configured handler") { context("When the OSPMTPayments object is initialized") { beforeEach { - mockHandler = OSPMTMockHandler(successText: OSPMTTestConfigurations.dummyString) + mockHandler = OSPMTMockHandler( + successText: OSPMTTestConfigurations.dummyString, scopeModel: OSPMTTestConfigurations.dummyScopeModel + ) payments = OSPMTPayments(delegate: mockDelegate, handler: mockHandler) } @@ -65,6 +77,24 @@ class OSPMTPaymentsSpec: QuickSpec { expect(mockDelegate.successText).to(beEmpty()) expect(mockDelegate.error).to(beNil()) } + + it("Set payment details should return a successful scope model") { + if let detailsData = try? JSONSerialization.data(withJSONObject: OSPMTTestConfigurations.validDetailModel), + let detailsString = String(data: detailsData, encoding: .utf8) { + payments.set(detailsString) + + expect(mockDelegate.error).to(beNil()) + + let result = payments.encode(OSPMTTestConfigurations.dummyScopeModel) + if case let .success(scopeText) = result { + expect(mockDelegate.successText).to(equal(scopeText)) + } else { + fail() + } + } else { + fail() + } + } } } @@ -75,19 +105,51 @@ class OSPMTPaymentsSpec: QuickSpec { payments = OSPMTPayments(delegate: mockDelegate, handler: mockHandler) } - it("Setup configuration should return the error") { + it("Setup configuration should return an error") { payments.setupConfiguration() expect(mockDelegate.error).to(equal(.invalidConfiguration)) expect(mockDelegate.successText).to(beNil()) } - it("Check wallet setup should return the error") { + it("Check wallet setup should return an error") { payments.checkWalletSetup() expect(mockDelegate.error).to(equal(.invalidConfiguration)) expect(mockDelegate.successText).to(beNil()) } + + it("Set payment details should return an error") { + if let detailsData = try? JSONSerialization.data(withJSONObject: OSPMTTestConfigurations.validDetailModel), + let detailsString = String(data: detailsData, encoding: .utf8) { + payments.set(detailsString) + + expect(mockDelegate.error).to(equal(.invalidConfiguration)) + expect(mockDelegate.successText).to(beNil()) + } else { + fail() + } + } + } + } + + describe("and set payment details is called") { + beforeEach { + mockHandler = OSPMTMockHandler(scopeModel: OSPMTTestConfigurations.dummyScopeModel) + payments = OSPMTPayments(delegate: mockDelegate, handler: mockHandler) + } + context("When an invalid detail model is passed as a parameter") { + it("should return a error") { + if let detailsData = try? JSONSerialization.data(withJSONObject: OSPMTTestConfigurations.invalidStatusDetailModel), + let detailsString = String(data: detailsData, encoding: .utf8) { + payments.set(detailsString) + + expect(mockDelegate.error).to(equal(.invalidDecodeDetails)) + expect(mockDelegate.successText).to(beNil()) + } else { + fail() + } + } } } diff --git a/OSPaymentsLibTests/OSPMTTestConfigurations.swift b/OSPaymentsLibTests/OSPMTTestConfigurations.swift index 7285d9e..2076a6c 100644 --- a/OSPaymentsLibTests/OSPMTTestConfigurations.swift +++ b/OSPaymentsLibTests/OSPMTTestConfigurations.swift @@ -1,4 +1,5 @@ @testable import OSPaymentsLib +import PassKit struct OSPMTTestConfigurations { // MARK: - General Configurations @@ -104,12 +105,14 @@ struct OSPMTTestConfigurations { static let validNetworkCapabilityConfig: OSPMTConfiguration = [ OSPMTApplePayConfiguration.ConfigurationKeys.paymentAllowedNetworks: [Self.networkVisa, Self.networkMasterCard], - OSPMTApplePayConfiguration.ConfigurationKeys.paymentSupportedCapabilities: [Self.capability3DS, Self.capabilityEMV] + OSPMTApplePayConfiguration.ConfigurationKeys.paymentSupportedCapabilities: [Self.capability3DS, Self.capabilityEMV], + OSPMTApplePayConfiguration.ConfigurationKeys.paymentSupportedCardCountries: [Self.dummyString] ] static let validNetworkCapabilityWithErrorConfig: OSPMTConfiguration = [ OSPMTApplePayConfiguration.ConfigurationKeys.paymentAllowedNetworks: [Self.networkVisa, Self.dummyString], - OSPMTApplePayConfiguration.ConfigurationKeys.paymentSupportedCapabilities: [Self.capability3DS, Self.dummyString] + OSPMTApplePayConfiguration.ConfigurationKeys.paymentSupportedCapabilities: [Self.capability3DS, Self.dummyString], + OSPMTApplePayConfiguration.ConfigurationKeys.paymentSupportedCardCountries: [Self.dummyString] ] static let invalidNetworkCapabilityConfig: OSPMTConfiguration = [ @@ -118,4 +121,52 @@ struct OSPMTTestConfigurations { ] static let emptyNetworkCapabilityConfig: OSPMTConfiguration = [:] + + // MARK: - OSPMTApplePayRequestBehaviourSpec Configurations + static let dummyDetailsModel = OSPMTDetailsModel(amount: 1, currency: Self.dummyString, status: .final) + static let dummyAddressModel = OSPMTAddressModel( + postalCode: Self.dummyString, fullAddress: Self.dummyString, countryCode: Self.dummyString, city: Self.dummyString + ) + static let dummyContactInfoModel = OSPMTContactInfoModel() + static let dummyTokenInfoModel = OSPMTTokenInfoModel(token: Self.dummyString, type: Self.dummyString) + static let dummyDataModel = OSPMTDataModel( + billingInfo: Self.dummyContactInfoModel, cardDetails: Self.dummyString, cardNetwork: Self.dummyString, tokenData: Self.dummyTokenInfoModel + ) + static let dummyScopeModel = OSPMTScopeModel(paymentData: Self.dummyDataModel, shippingInfo: Self.dummyContactInfoModel) + + static let emptyContactFieldArray = [String]() + static let invalidContactFieldArray = [Self.dummyString] + static let withInvalidContactFieldArray = [Self.dummyString, PKContactField.name.rawValue.lowercased()] + static let validContactFieldArray = [PKContactField.phoneNumber.rawValue.lowercased(), PKContactField.name.rawValue.lowercased()] + + // MARK: - OSPMTPaymentSpec Configurations + static let validDetailModel: [String: Any] = [ + OSPMTDetailsModel.CodingKeys.amount.rawValue: 1, + OSPMTDetailsModel.CodingKeys.currency.rawValue: Self.dummyString, + OSPMTDetailsModel.CodingKeys.status.rawValue: OSPMTStatus.final.rawValue, + OSPMTDetailsModel.CodingKeys.billingContactArray.rawValue: [Self.dummyString], + OSPMTDetailsModel.CodingKeys.shippingContactArray.rawValue: [Self.dummyString] + ] + + static let invalidStatusDetailModel: [String: Any] = [ + OSPMTDetailsModel.CodingKeys.amount.rawValue: 1, + OSPMTDetailsModel.CodingKeys.currency.rawValue: Self.dummyString, + OSPMTDetailsModel.CodingKeys.status.rawValue: Self.dummyString, + OSPMTDetailsModel.CodingKeys.billingContactArray.rawValue: [Self.dummyString], + OSPMTDetailsModel.CodingKeys.shippingContactArray.rawValue: [Self.dummyString] + ] + + // MARK: - ModelSpec Methods + static func decode(for type: T.Type, _ dictionary: [String: Any]) -> T? { + guard + let data = try? JSONSerialization.data(withJSONObject: dictionary), + let model = try? JSONDecoder().decode(type, from: data) + else { return nil } + return model + } + + static func encode(_ model: T) -> String? { + guard let data = try? JSONEncoder().encode(model), let result = String(data: data, encoding: .utf8) else { return nil } + return result + } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 49138d5..873a933 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +### 2022-08-03 + +- Feat: Set Payment Details (https://outsystemsrd.atlassian.net/browse/RMET-1723). + ### 2022-08-02 - Feat: Check Wallet Availability for Payment (https://outsystemsrd.atlassian.net/browse/RMET-1695). diff --git a/sonar-project.properties b/sonar-project.properties index 06f3f79..79ab51d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -25,7 +25,7 @@ sonar.test.inclusions=**/*Test*/** sonar.test.inclusions=*.swift sonar.exclusions=**/*.xml,Pods/**/*,Reports/**/*,**/*Test*/** -sonar.coverage.exclusions=**/*Test*/**,**/*Error.swift +sonar.coverage.exclusions=**/*Test*/**,**/*Error.swift,**/*Extensions*/** sonar.tests=OSPaymentsLibTests