Skip to content

Commit

Permalink
fix: Errors and Contact Management (#6)
Browse files Browse the repository at this point in the history
Clean errors and its codes and messages accordingly.
New OSPMTContact struct that allows the management of the correct shipping and billing information to use on a payment request. This is required due to a limitation on OutSystems related with nullable lists.
Change the OSPMTConfigurationDelegate to OSPMTConfigurationModel, in order to comply with the new OutSystems structure.
Clean code (privatise local methods and make OSPMTPayment's delegate property weak, in order to avoid possible retain cycles).
  • Loading branch information
OS-ricardomoreirasilva committed Aug 25, 2022
1 parent 0232cc1 commit 78504d7
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 439 deletions.
8 changes: 4 additions & 4 deletions OSPaymentsLib.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
75EC4D1A2894156E00CF50E2 /* OSPMTError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC4D192894156E00CF50E2 /* OSPMTError.swift */; };
75EC4D1C2894160400CF50E2 /* OSPMTActionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC4D1B2894160400CF50E2 /* OSPMTActionDelegate.swift */; };
75EC4D1E289416B600CF50E2 /* OSPMTApplePayHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC4D1D289416B600CF50E2 /* OSPMTApplePayHandler.swift */; };
75EC4D2228942C3800CF50E2 /* OSPMTConfigurationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC4D2128942C3800CF50E2 /* OSPMTConfigurationDelegate.swift */; };
75EC4D2228942C3800CF50E2 /* OSPMTConfigurationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC4D2128942C3800CF50E2 /* OSPMTConfigurationModel.swift */; };
75EC4D242894449700CF50E2 /* OSPMTApplePayHandlerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC4D232894449700CF50E2 /* OSPMTApplePayHandlerSpec.swift */; };
75EC4D262894478200CF50E2 /* OSPMTTestConfigurations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75EC4D252894478200CF50E2 /* OSPMTTestConfigurations.swift */; };
7E9C30CBBF9DFE5A747787FF /* Pods_OSPaymentsLib_OSPaymentsLibTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6BD4033DE465D38CA50CA9E /* Pods_OSPaymentsLib_OSPaymentsLibTests.framework */; };
Expand Down Expand Up @@ -91,7 +91,7 @@
75EC4D192894156E00CF50E2 /* OSPMTError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTError.swift; sourceTree = "<group>"; };
75EC4D1B2894160400CF50E2 /* OSPMTActionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTActionDelegate.swift; sourceTree = "<group>"; };
75EC4D1D289416B600CF50E2 /* OSPMTApplePayHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTApplePayHandler.swift; sourceTree = "<group>"; };
75EC4D2128942C3800CF50E2 /* OSPMTConfigurationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTConfigurationDelegate.swift; sourceTree = "<group>"; };
75EC4D2128942C3800CF50E2 /* OSPMTConfigurationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTConfigurationModel.swift; sourceTree = "<group>"; };
75EC4D232894449700CF50E2 /* OSPMTApplePayHandlerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTApplePayHandlerSpec.swift; sourceTree = "<group>"; };
75EC4D252894478200CF50E2 /* OSPMTTestConfigurations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPMTTestConfigurations.swift; sourceTree = "<group>"; };
8CDB829B99BC8A28609750EF /* Pods-OSPaymentsLib-OSPaymentsLibTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OSPaymentsLib-OSPaymentsLibTests.debug.xcconfig"; path = "Target Support Files/Pods-OSPaymentsLib-OSPaymentsLibTests/Pods-OSPaymentsLib-OSPaymentsLibTests.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -196,6 +196,7 @@
isa = PBXGroup;
children = (
755A8061289BD56300426EAA /* OSPMTAddressModel.swift */,
75EC4D2128942C3800CF50E2 /* OSPMTConfigurationModel.swift */,
755A8063289BD58900426EAA /* OSPMTContactInfoModel.swift */,
755A805F289BD54E00426EAA /* OSPMTDataModel.swift */,
755A8053289BC8EE00426EAA /* OSPMTDetailsModel.swift */,
Expand Down Expand Up @@ -223,7 +224,6 @@
75EC4D1B2894160400CF50E2 /* OSPMTActionDelegate.swift */,
7509DC7C28995A07005BA0D4 /* OSPMTAvailabilityDelegate.swift */,
75EC4D142894143A00CF50E2 /* OSPMTCallbackDelegate.swift */,
75EC4D2128942C3800CF50E2 /* OSPMTConfigurationDelegate.swift */,
75EC4D162894150F00CF50E2 /* OSPMTHandlerDelegate.swift */,
755A8057289BC90A00426EAA /* OSPMTRequestDelegate.swift */,
);
Expand Down Expand Up @@ -449,7 +449,7 @@
755A8055289BC8EE00426EAA /* OSPMTDetailsModel.swift in Sources */,
755A804B289BB96900426EAA /* PKPassLibrary+Adapter.swift in Sources */,
75760F83289C1F99001BDCEC /* PKPayment+Adapter.swift in Sources */,
75EC4D2228942C3800CF50E2 /* OSPMTConfigurationDelegate.swift in Sources */,
75EC4D2228942C3800CF50E2 /* OSPMTConfigurationModel.swift in Sources */,
75EC4D172894150F00CF50E2 /* OSPMTHandlerDelegate.swift in Sources */,
755A8058289BC90A00426EAA /* OSPMTRequestDelegate.swift in Sources */,
755A8062289BD56300426EAA /* OSPMTAddressModel.swift in Sources */,
Expand Down
30 changes: 15 additions & 15 deletions OSPaymentsLib/Error/OSPMTError.swift
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
/// All plugin errors that can be thrown
enum OSPMTError: Int, CustomNSError, LocalizedError {
case invalidConfiguration = 1
case walletNotAvailable = 2
case paymentNotAvailable = 3
case setupPaymentNotAvailable = 4
case invalidDecodeDetails = 6
case paymentTriggerPresentationFailed = 7
case paymentTriggerNotCompleted = 8
case walletNotAvailable = 3
case paymentNotAvailable = 5
case setupPaymentNotAvailable = 6
case invalidDecodeDetails = 8
case invalidEncodeScope = 9
case paymentTriggerPresentationFailed = 10
case paymentCancelled = 11

/// Textual description
var errorDescription: String? {
switch self {
case .invalidConfiguration:
return "An invalid configuration was provided to the plugin."
return "Couldn't obtain the payment's informations from the configurations file."
case .walletNotAvailable:
return "Wallet is not available on this device."
return "The Apple Pay is not available in the device."
case .paymentNotAvailable:
return "Payment is not available on this device."
return "There is no payment method configured."
case .setupPaymentNotAvailable:
return "Payment through the configured networks and capabilities is not available on this device."
return "There are no valid payment cards for the supported networks and/or capabilities."
case .invalidDecodeDetails:
return "Couldn't decode payment details."
return "Couldn't decode the payment details."
case .invalidEncodeScope:
return "Couldn't encode the payment scope."
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."
case .paymentCancelled:
return "Payment was cancelled by the user."
}
}
}
8 changes: 4 additions & 4 deletions OSPaymentsLib/Extensions/PKPayment+Adapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension PKPayment {

/// Converts a `PKPayment` object into a dictionary that relates to an `OSPMTDataModel` object.
/// - Returns: The corresponding `OSPMTDataModel` dictionary object.
func createTokenDataData() -> [String: Any] {
private func createTokenDataData() -> [String: Any] {
var result: [String: Any] = [
OSPMTDataModel.CodingKeys.tokenData.rawValue: self.createTokenData(for: self.token.paymentData)
]
Expand All @@ -39,7 +39,7 @@ extension PKPayment {
/// Takes the passed payment data object and converts it into a dictionary that relates to an `OSPMTTokenInfoModel` object.
/// - Parameter paymentData: `Data` type object that contains information related to a payment token.
/// - Returns: The corresponding `OSPMTTokenInfoModel` dictionary object.
func createTokenData(for paymentData: Data) -> [String: String] {
private func createTokenData(for paymentData: Data) -> [String: String] {
// TODO: The type passed here will probably be changed into the Payment Service Provider's name when this is implemented.
var result = [OSPMTTokenInfoModel.CodingKeys.type.rawValue: "Apple Pay"]

Expand All @@ -53,7 +53,7 @@ extension PKPayment {
/// Takes the passed contact object and converts it into a dictionary that relates to an `OSPMTContactInfoModel` object.
/// - Parameter contact: `PKContact` type object that contains information related to the filled Billing or Shipping Information
/// - Returns: The corresponding `OSPMTContactInfoModel` dictionary object.
func createContactInfoData(for contact: PKContact) -> [String: Any]? {
private 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)
Expand All @@ -74,7 +74,7 @@ extension PKPayment {
/// Takes the passed address object and converts it into a dictionary that relates to an `OSPMTAddressModel` object.
/// - Parameter postalAddress: `CNPostalAddress` type object that contains an address related to the filled Billing or Shipping Information.
/// - Returns: The corresponding `OSPMTAddressModel` dictionary object.
func createAddressData(for postalAddress: CNPostalAddress) -> [String: String] {
private func createAddressData(for postalAddress: CNPostalAddress) -> [String: String] {
var result = [
OSPMTAddressModel.CodingKeys.postalCode.rawValue: postalAddress.postalCode,
OSPMTAddressModel.CodingKeys.fullAddress.rawValue: postalAddress.street,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ extension PKPaymentAuthorizationController: OSPMTApplePayRequestTriggerDelegate
paymentRequest.merchantCapabilities = merchantCapabilities
paymentRequest.paymentSummaryItems = paymentSummaryItems
paymentRequest.requiredBillingContactFields = delegate.getContactFields(
for: detailsModel.billingContactArray ?? delegate.configuration.billingSupportedContacts
for: detailsModel.billingContact.isCustom
? detailsModel.billingContact.contactArray
: delegate.configuration.billingSupportedContacts
)
paymentRequest.requiredShippingContactFields = delegate.getContactFields(
for: detailsModel.shippingContactArray ?? delegate.configuration.shippingSupportedContacts
for: detailsModel.shippingContact.isCustom
? detailsModel.shippingContact.contactArray
: delegate.configuration.shippingSupportedContacts
)
paymentRequest.supportedCountries = delegate.configuration.supportedCountries
paymentRequest.supportedNetworks = supportedNetworks
Expand Down
198 changes: 198 additions & 0 deletions OSPaymentsLib/Models/OSPMTConfigurationModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import PassKit

/// Protocol that contains all properties needed to configure a payment service.
class OSPMTConfigurationModel: Encodable {
// MARK: Merchant Information
var merchantID: String?
var merchantName: String?
var merchantCountryCode: String?

// MARK: Payment Information
var paymentAllowedNetworks: [String]?
var paymentSupportedCapabilities: [String]?
var paymentSupportedCardCountries: [String]?

// MARK: Shipping Information
var shippingSupportedContacts: [String]?

// MARK: Billing Information
var billingSupportedContacts: [String]?

/// Keys used to encode and decode the model.
enum CodingKeys: String, CodingKey {
case merchantID
case merchantName
case merchantCountryCode
case paymentAllowedNetworks
case paymentSupportedCapabilities
case paymentSupportedCardCountries
case shippingSupportedContacts
case billingSupportedContacts
}

/// Constructor method.
/// - Parameters:
/// - merchantID: Merchant ID configured
/// - merchantName: Merchant Name configured
/// - merchantCountryCode: Merchant Country Code configured
/// - paymentAllowedNetworks: Payment Allowed Networks configured
/// - paymentSupportedCapabilities: Payment Supported Capabilities configured
/// - paymentSupportedCardCountries: Payment Support Card Countries configured
/// - shippingSupportedContacts: Shipping Supported Contacts configured
/// - billingSupportedContacts: Billing Supported Contacts configured
init(merchantID: String?, merchantName: String?, merchantCountryCode: String?, paymentAllowedNetworks: [String]?, paymentSupportedCapabilities: [String]?, paymentSupportedCardCountries: [String]?, shippingSupportedContacts: [String]?, billingSupportedContacts: [String]?) {
self.merchantID = merchantID
self.merchantName = merchantName
self.merchantCountryCode = merchantCountryCode
self.paymentAllowedNetworks = paymentAllowedNetworks
self.paymentSupportedCapabilities = paymentSupportedCapabilities
self.paymentSupportedCardCountries = paymentSupportedCardCountries
self.shippingSupportedContacts = shippingSupportedContacts
self.billingSupportedContacts = billingSupportedContacts
}

/// Encodes this value into the given encoder.
///
/// If the value fails to encode anything, `encoder` will encode an empty
/// keyed container in its place.
///
/// This function throws an error if any values are invalid for the given
/// encoder's format.
///
/// - Parameter encoder: The encoder to write data to.
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

// MARK: Merchant Information
try container.encodeIfPresent(merchantID, forKey: .merchantID)
try container.encodeIfPresent(merchantName, forKey: .merchantName)
try container.encodeIfPresent(merchantCountryCode, forKey: .merchantCountryCode)

// MARK: Payment Information
try container.encodeIfPresent(paymentAllowedNetworks, forKey: .paymentAllowedNetworks)
try container.encodeIfPresent(paymentSupportedCapabilities, forKey: .paymentSupportedCapabilities)
try container.encodeIfPresent(paymentSupportedCardCountries, forKey: .paymentSupportedCardCountries)

// MARK: Shipping Information
try container.encodeIfPresent(shippingSupportedContacts, forKey: .shippingSupportedContacts)

// MARK: Billing Information
try container.encodeIfPresent(billingSupportedContacts, forKey: .billingSupportedContacts)
}
}

typealias OSPMTConfiguration = [String: Any]

/// Manages all configuration properties required to enable Apple Pay in the plugin.
class OSPMTApplePayConfiguration: OSPMTConfigurationModel {
struct ConfigurationKeys {
static let merchantID = "ApplePayMerchantID"
static let merchantName = "ApplePayMerchantName"
static let merchantCountryCode = "ApplePayMerchantCountryCode"

static let paymentAllowedNetworks = "ApplePayPaymentAllowedNetworks"
static let paymentSupportedCapabilities = "ApplePayPaymentSupportedCapabilities"
static let paymentSupportedCardCountries = "ApplePayPaymentSupportedCardCountries"

static let shippingSupportedContacts = "ApplePayShippingSupportedContacts"

static let billingSupportedContacts = "ApplePayBillingSupportedContacts"
}

/// Constructor method.
/// - Parameter source: Source class contaning the configuration.
convenience init(source: OSPMTConfiguration) {
// MARK: Merchant Information
let merchantID = Self.getRequiredProperty(
ofType: String.self, forSource: source, andKey: ConfigurationKeys.merchantID
)
let merchantName = Self.getRequiredProperty(
ofType: String.self, forSource: source, andKey: ConfigurationKeys.merchantName
)
let merchantCountryCode = Self.getRequiredProperty(
ofType: String.self, forSource: source, andKey: ConfigurationKeys.merchantCountryCode
)

// MARK: Payment Information
let paymentAllowedNetworks = Self.getRequiredProperty(
ofType: [String].self, forSource: source, andKey: ConfigurationKeys.paymentAllowedNetworks
)
let paymentSupportedCapabilities = Self.getRequiredProperty(
ofType: [String].self, forSource: source, andKey: ConfigurationKeys.paymentSupportedCapabilities
)
let paymentSupportedCardCountries = Self.getProperty(
ofType: [String].self, forSource: source, andKey: ConfigurationKeys.paymentSupportedCardCountries
)

// MARK: Shipping Information
let shippingSupportedContacts = Self.getProperty(
ofType: [String].self, forSource: source, andKey: ConfigurationKeys.shippingSupportedContacts
)

// MARK: Billing Information
let billingSupportedContacts = Self.getProperty(
ofType: [String].self, forSource: source, andKey: ConfigurationKeys.billingSupportedContacts
)

self.init(
merchantID: merchantID,
merchantName: merchantName,
merchantCountryCode: merchantCountryCode,
paymentAllowedNetworks: paymentAllowedNetworks,
paymentSupportedCapabilities: paymentSupportedCapabilities,
paymentSupportedCardCountries: paymentSupportedCardCountries,
shippingSupportedContacts: shippingSupportedContacts,
billingSupportedContacts: billingSupportedContacts
)
}
}

private extension OSPMTApplePayConfiguration {
/// Fetches the parameter property, if it exists.
/// - Parameters:
/// - type: Type of variable to return.
/// - key: Property key to search from.
/// - isRequired: Indicates if the property is mandatory or not.
/// - Returns: The configuration property, if it exists.
static func getProperty<T: Collection>(ofType type: T.Type, forSource source: OSPMTConfiguration, andKey key: String, isRequired: Bool = false) -> T? {
let result = source[key] as? T
return !isRequired || result?.isEmpty == false ? result : nil
}

/// An acelerator for `getProperty(ofType:forKey:isRequired:)`, that should be used for mandatory properties.
/// - Parameters:
/// - type: Type of variable to return.
/// - key: Property key to search from.
/// - Returns: The configuration property, if it exists.
static func getRequiredProperty<T: Collection>(ofType type: T.Type, forSource source: OSPMTConfiguration, andKey key: String) -> T? {
self.getProperty(ofType: type, forSource: source, andKey: key, isRequired: true)
}
}

extension OSPMTApplePayConfiguration {
var supportedNetworks: [PKPaymentNetwork]? {
guard let paymentAllowedNetworks = self.paymentAllowedNetworks else { return nil }
let result = paymentAllowedNetworks.compactMap(PKPaymentNetwork.convert(from:))

return !result.isEmpty ? result : nil
}

var merchantCapabilities: PKMerchantCapability? {
guard let paymentSupportedCapabilities = self.paymentSupportedCapabilities else { return nil }

var result: PKMerchantCapability = []
result = paymentSupportedCapabilities.reduce(into: result) { partialResult, capability in
let merchantCapability = PKMerchantCapability.convert(from: capability)
if let merchantCapability = merchantCapability {
partialResult.insert(merchantCapability)
}
}

return !result.isEmpty ? result : nil
}

var supportedCountries: Set<String>? {
guard let paymentSupportedCardCountries = self.paymentSupportedCardCountries, !paymentSupportedCardCountries.isEmpty else { return nil }
return Set(paymentSupportedCardCountries)
}
}
Loading

0 comments on commit 78504d7

Please sign in to comment.