Skip to content

Commit

Permalink
Merge Everything into Main (#8)
Browse files Browse the repository at this point in the history
* chore: Include swiftlint

* feat: Plugin init + Setup Configurations (#1)

Initialise plugin with required structure.
Create structure to deal with Apple Pay. Despite being ready to deal with any kind of dictionary, provide an accelerator to read the configuration from the main bundle.
Add Nimble and Quick through Cocoapods to use BDD for unit testing.

* feat: Check Wallet and Payment Availability (#2)

Add verification for wallet and payment availability. Payment verification is enhanced by also checking it against the configured payment networks and supported capabilities.

* feat: Set Details and Trigger Payment (#3)

Configure the missing payment details and, by mixing it with the configuration info, trigger the payment request.

* refactor: Add DocC documentation and minor fixes. (#4)

Add DocC documentation.
Add empty value check and mandatory fields when fetching configuration properties.

* fix: Payment Setup Verification Failed on Invalid Configuration (#5)

Fix error when verifying payment setup on ReadyToPay method. If some payment network or merchant capabilities are missing, return the associated error.

* fix: Errors and Contact Management (#6)

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).

* fix: Check if GivenName and FamilyName are empty (#7)
  • Loading branch information
OS-ricardomoreirasilva committed Apr 11, 2024
1 parent 72d27df commit b928ec7
Show file tree
Hide file tree
Showing 122 changed files with 4,634 additions and 3,488 deletions.
96 changes: 8 additions & 88 deletions OSPaymentsLib.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down Expand Up @@ -45,7 +45,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "1"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
Expand Down
23 changes: 2 additions & 21 deletions OSPaymentsLib/Error/OSPMTError.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Foundation

/// All plugin errors that can be thrown
public enum OSPMTError: Int, CustomNSError, LocalizedError {
enum OSPMTError: Int, CustomNSError, LocalizedError {
case invalidConfiguration = 1
case walletNotAvailable = 3
case paymentNotAvailable = 5
Expand All @@ -11,14 +9,8 @@ public enum OSPMTError: Int, CustomNSError, LocalizedError {
case paymentTriggerPresentationFailed = 10
case paymentCancelled = 11

case gatewaySetFailed = 12
case stripePaymentMethodCreation = 13
case paymentIssue = 14
case gatewayNotConfigured = 15
case tokenIssue = 19

/// Textual description
public var errorDescription: String? {
var errorDescription: String? {
switch self {
case .invalidConfiguration:
return "Couldn't obtain the payment's informations from the configurations file."
Expand All @@ -36,17 +28,6 @@ public enum OSPMTError: Int, CustomNSError, LocalizedError {
return "Couldn't present the Apple Pay screen."
case .paymentCancelled:
return "Payment was cancelled by the user."

case .gatewaySetFailed:
return "Couldn't set payment service provider."
case .stripePaymentMethodCreation:
return "Couldn't obtain the PaymentMethod from Stripe."
case .paymentIssue:
return "Couldn't process payment."
case .gatewayNotConfigured:
return "Couldn't trigger the payment. The requested payment service provider is not configured yet."
case .tokenIssue:
return "Couldn’t trigger the payment. The access token is not defined."
}
}
}
17 changes: 6 additions & 11 deletions OSPaymentsLib/Extensions/PKPayment+Adapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@ import PassKit

extension PKPayment {
/// Converts a `PKPayment` object into a `OSPMTScopeModel` one. Returns `nil` if it can't.
/// - Parameter paymentGatewayModel: model that contains the payment gateway information resulting from completing a payment process.
/// - Returns: The corresponding `OSPMTScopeModel` object. Can also return `nil` if the conversion fails.
func createScopeModel(for paymentGatewayModel: OSPMTServiceProviderInfoModel? = nil) -> OSPMTScopeModel? {
var result: [String: Any] = [OSPMTScopeModel.CodingKeys.paymentData.rawValue: self.createTokenDataData(for: paymentGatewayModel)]
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),
guard
let scopeData = try? JSONSerialization.data(withJSONObject: result),
let scopeModel = try? JSONDecoder().decode(OSPMTScopeModel.self, from: scopeData)
else { return nil }
return scopeModel
}

/// Converts a `PKPayment` object into a dictionary that relates to an `OSPMTDataModel` object.
/// - Parameter paymentGatewayModel: model that contains the payment gateway information resulting from completing a payment process.
/// - Returns: The corresponding `OSPMTDataModel` dictionary object.
private func createTokenDataData(for paymentGatewayModel: OSPMTServiceProviderInfoModel?) -> [String: Any] {
private func createTokenDataData() -> [String: Any] {
var result: [String: Any] = [
OSPMTDataModel.CodingKeys.tokenData.rawValue: self.createTokenData(for: self.token.paymentData)
]
Expand All @@ -33,11 +32,6 @@ extension PKPayment {
result[OSPMTDataModel.CodingKeys.cardNetwork.rawValue] = cardNetwork
}
}
if let paymentGatewayModel = paymentGatewayModel,
let paymentGatewayData = try? JSONEncoder().encode(paymentGatewayModel),
let paymentGatewayDict = try? JSONSerialization.jsonObject(with: paymentGatewayData) as? [String: String] {
result[OSPMTDataModel.CodingKeys.paymentServiceProviderData.rawValue] = paymentGatewayDict
}

return result
}
Expand All @@ -46,6 +40,7 @@ extension PKPayment {
/// - Parameter paymentData: `Data` type object that contains information related to a payment token.
/// - Returns: The corresponding `OSPMTTokenInfoModel` dictionary object.
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"]

if let token = String(data: paymentData, encoding: .utf8) {
Expand Down
117 changes: 30 additions & 87 deletions OSPaymentsLib/Models/OSPMTConfigurationModel.swift
Original file line number Diff line number Diff line change
@@ -1,43 +1,6 @@
import PassKit

/// Model that manages the Payment Service Provider's configuration.
struct OSPMTGatewayModel: Encodable {
let gateway: String
let publishableKey: String?
let requestURL: String

/// Keys used to encode and decode the model.
enum CodingKeys: String, CodingKey {
case gateway
case publishableKey
case requestURL
}

/// 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)

try container.encode(gateway, forKey: .gateway)
try container.encodeIfPresent(publishableKey, forKey: .publishableKey)
try container.encode(requestURL, forKey: .requestURL)
}
}

extension OSPMTGatewayModel {
var gatewayEnum: OSPMTGateway? {
return OSPMTGateway.convert(from: self.gateway)
}
}

/// Model that contains all properties needed to configure a payment service.
/// Protocol that contains all properties needed to configure a payment service.
class OSPMTConfigurationModel: Encodable {
// MARK: Merchant Information
var merchantID: String?
Expand All @@ -55,9 +18,6 @@ class OSPMTConfigurationModel: Encodable {
// MARK: Billing Information
var billingSupportedContacts: [String]?

// MARK: Payment Service Provider Information
var gatewayModel: OSPMTGatewayModel?

/// Keys used to encode and decode the model.
enum CodingKeys: String, CodingKey {
case merchantID
Expand All @@ -68,7 +28,6 @@ class OSPMTConfigurationModel: Encodable {
case paymentSupportedCardCountries
case shippingSupportedContacts
case billingSupportedContacts
case gatewayModel = "tokenization"
}

/// Constructor method.
Expand All @@ -81,18 +40,7 @@ class OSPMTConfigurationModel: Encodable {
/// - paymentSupportedCardCountries: Payment Support Card Countries configured
/// - shippingSupportedContacts: Shipping Supported Contacts configured
/// - billingSupportedContacts: Billing Supported Contacts configured
/// - tokenization: Payment Service Gateway configured
init(
merchantID: String?,
merchantName: String?,
merchantCountryCode: String?,
paymentAllowedNetworks: [String]?,
paymentSupportedCapabilities: [String]?,
paymentSupportedCardCountries: [String]?,
shippingSupportedContacts: [String]?,
billingSupportedContacts: [String]?,
gatewayModel: OSPMTGatewayModel?
) {
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
Expand All @@ -101,7 +49,6 @@ class OSPMTConfigurationModel: Encodable {
self.paymentSupportedCardCountries = paymentSupportedCardCountries
self.shippingSupportedContacts = shippingSupportedContacts
self.billingSupportedContacts = billingSupportedContacts
self.gatewayModel = gatewayModel
}

/// Encodes this value into the given encoder.
Expand Down Expand Up @@ -131,13 +78,10 @@ class OSPMTConfigurationModel: Encodable {

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

// MARK: Payment Service Provider Information
try container.encodeIfPresent(gatewayModel, forKey: .gatewayModel)
}
}

public typealias OSPMTConfiguration = [String: Any]
typealias OSPMTConfiguration = [String: Any]

/// Manages all configuration properties required to enable Apple Pay in the plugin.
class OSPMTApplePayConfiguration: OSPMTConfigurationModel {
Expand All @@ -153,42 +97,42 @@ class OSPMTApplePayConfiguration: OSPMTConfigurationModel {
static let shippingSupportedContacts = "ApplePayShippingSupportedContacts"

static let billingSupportedContacts = "ApplePayBillingSupportedContacts"

static let paymentGateway = "ApplePayPaymentGateway"
static let paymentGatewayName = "ApplePayPaymentGatewayName"
static let paymentRequestURL = "ApplePayRequestURL"
static let stripePublishableKey = "ApplePayStripePublishableKey"
}

/// Constructor method.
/// - Parameter source: Source class contaning the configuration.
convenience init(source: OSPMTConfiguration) {
// MARK: Merchant Information
let merchantID: String? = Self.getRequiredProperty(forSource: source, andKey: ConfigurationKeys.merchantID)
let merchantName: String? = Self.getRequiredProperty(forSource: source, andKey: ConfigurationKeys.merchantName)
let merchantCountryCode: String? = Self.getRequiredProperty(forSource: source, andKey: ConfigurationKeys.merchantCountryCode)
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: [String]? = Self.getRequiredProperty(forSource: source, andKey: ConfigurationKeys.paymentAllowedNetworks)
let paymentSupportedCapabilities: [String]? = Self.getRequiredProperty(
forSource: source, andKey: ConfigurationKeys.paymentSupportedCapabilities
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
)
let paymentSupportedCardCountries: [String]? = Self.getProperty(forSource: source, andKey: ConfigurationKeys.paymentSupportedCardCountries)

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

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

// MARK: Payment Service Provider Information
var gatewayModel: OSPMTGatewayModel?
if let providerGateway: [String: Any] = Self.getProperty(forSource: source, andKey: ConfigurationKeys.paymentGateway),
let providerGatewayName = providerGateway[ConfigurationKeys.paymentGatewayName] as? String,
let requestURL = providerGateway[ConfigurationKeys.paymentRequestURL] as? String {
let publishableKey = providerGateway[ConfigurationKeys.stripePublishableKey] as? String
gatewayModel = OSPMTGatewayModel(gateway: providerGatewayName, publishableKey: publishableKey, requestURL: requestURL)
}
let billingSupportedContacts = Self.getProperty(
ofType: [String].self, forSource: source, andKey: ConfigurationKeys.billingSupportedContacts
)

self.init(
merchantID: merchantID,
Expand All @@ -198,8 +142,7 @@ class OSPMTApplePayConfiguration: OSPMTConfigurationModel {
paymentSupportedCapabilities: paymentSupportedCapabilities,
paymentSupportedCardCountries: paymentSupportedCardCountries,
shippingSupportedContacts: shippingSupportedContacts,
billingSupportedContacts: billingSupportedContacts,
gatewayModel: gatewayModel
billingSupportedContacts: billingSupportedContacts
)
}
}
Expand All @@ -211,7 +154,7 @@ private extension OSPMTApplePayConfiguration {
/// - 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>(forSource source: OSPMTConfiguration, andKey key: String, isRequired: Bool = false) -> T? {
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
}
Expand All @@ -221,8 +164,8 @@ private extension OSPMTApplePayConfiguration {
/// - type: Type of variable to return.
/// - key: Property key to search from.
/// - Returns: The configuration property, if it exists.
static func getRequiredProperty<T: Collection>(forSource source: OSPMTConfiguration, andKey key: String) -> T? {
self.getProperty(forSource: source, andKey: key, isRequired: true)
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)
}
}

Expand Down
18 changes: 3 additions & 15 deletions OSPaymentsLib/Models/OSPMTDataModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ struct OSPMTDataModel: Codable {
let cardDetails: String
let cardNetwork: String
let tokenData: OSPMTTokenInfoModel
let paymentServiceProviderData: OSPMTServiceProviderInfoModel?

/// Keys used to encode and decode the model.
enum CodingKeys: String, CodingKey {
case billingInfo, cardDetails, cardNetwork, tokenData, paymentServiceProviderData
case billingInfo, cardDetails, cardNetwork, tokenData
}

/// Constructor method.
Expand All @@ -17,13 +16,11 @@ struct OSPMTDataModel: Codable {
/// - cardDetails: The last four digits of the card used for payment.
/// - cardNetwork: The network of the card used for payment.
/// - tokenData: The data of the token used for payment.
/// - paymentServiceProviderData: Information related with the payment gateway process result. If can be `nil` if no Gateway was configured
init(billingInfo: OSPMTContactInfoModel? = nil, cardDetails: String, cardNetwork: String, tokenData: OSPMTTokenInfoModel, paymentServiceProviderData: OSPMTServiceProviderInfoModel? = nil) {
init(billingInfo: OSPMTContactInfoModel? = nil, cardDetails: String, cardNetwork: String, tokenData: OSPMTTokenInfoModel) {
self.billingInfo = billingInfo
self.cardDetails = cardDetails
self.cardNetwork = cardNetwork
self.tokenData = tokenData
self.paymentServiceProviderData = paymentServiceProviderData
}

/// Creates a new instance by decoding from the given decoder.
Expand All @@ -38,14 +35,7 @@ struct OSPMTDataModel: Codable {
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)
let paymentServiceProviderData = try container.decodeIfPresent(OSPMTServiceProviderInfoModel.self, forKey: .paymentServiceProviderData)
self.init(
billingInfo: billingInfo,
cardDetails: cardDetails,
cardNetwork: cardNetwork,
tokenData: tokenData,
paymentServiceProviderData: paymentServiceProviderData
)
self.init(billingInfo: billingInfo, cardDetails: cardDetails, cardNetwork: cardNetwork, tokenData: tokenData)
}

/// Encodes this value into the given encoder.
Expand All @@ -63,7 +53,6 @@ struct OSPMTDataModel: Codable {
try container.encode(cardDetails, forKey: .cardDetails)
try container.encode(cardNetwork, forKey: .cardNetwork)
try container.encode(tokenData, forKey: .tokenData)
try container.encodeIfPresent(paymentServiceProviderData, forKey: .paymentServiceProviderData)
}
}

Expand All @@ -82,6 +71,5 @@ extension OSPMTDataModel: Equatable {
&& lhs.cardDetails == rhs.cardDetails
&& lhs.cardNetwork == rhs.cardNetwork
&& lhs.tokenData == rhs.tokenData
&& lhs.paymentServiceProviderData == rhs.paymentServiceProviderData
}
}
Loading

0 comments on commit b928ec7

Please sign in to comment.