Skip to content

Commit

Permalink
Prepare Release 1.1.0 (#14)
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)

* chore: Add Unreleased Section

* Chore: Add Podspec (#9)

* refactor: Simply Generic Method

* chore: Add public podspec

Add public podspec. This implies also updating the readme.md file to something more descriptive and user-friendly.

* feat: Add Stripe as First Payment Gateway (#11)

Add all necessary logic and changes to make payment processing using Stripe including Protocols, Factories and Models to deal with the StripePayments pod. Unit tests are included.

* Feat: Add Gateway property to OSPMTDetailsModel (#12)

Add `gateway` property to `OSPMTDetailsModel` struct. When authorising payment, confirm that this property corresponds to a configured gateway (on plist).

* feat: Add AccessToken to Full Payment Process (#13)

Add the optional parameter `accessToken` to the `OSPMTActionDelegate`'s `set` method and the `OSPMTStripeWrapper`'s `processPaymentWithDetails` method. Apply consequent changes to it.
In case of a full payment is triggered but no access token is passed, a `tokenIssue` is returned.

* chore: Set Release Tag

Set release tag on the repo's CHANGELOG file.
  • Loading branch information
OS-ricardomoreirasilva committed Jan 10, 2023
1 parent e1d22e9 commit ce50ffe
Show file tree
Hide file tree
Showing 607 changed files with 51,762 additions and 507 deletions.
64 changes: 60 additions & 4 deletions OSPaymentsLib.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions OSPaymentsLib/Error/OSPMTError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ 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? {
switch self {
Expand All @@ -30,6 +36,17 @@ 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: 11 additions & 6 deletions OSPaymentsLib/Extensions/PKPayment+Adapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ 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() -> OSPMTScopeModel? {
var result: [String: Any] = [OSPMTScopeModel.CodingKeys.paymentData.rawValue: self.createTokenDataData()]
func createScopeModel(for paymentGatewayModel: OSPMTServiceProviderInfoModel? = nil) -> OSPMTScopeModel? {
var result: [String: Any] = [OSPMTScopeModel.CodingKeys.paymentData.rawValue: self.createTokenDataData(for: paymentGatewayModel)]
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() -> [String: Any] {
private func createTokenDataData(for paymentGatewayModel: OSPMTServiceProviderInfoModel?) -> [String: Any] {
var result: [String: Any] = [
OSPMTDataModel.CodingKeys.tokenData.rawValue: self.createTokenData(for: self.token.paymentData)
]
Expand All @@ -32,6 +33,11 @@ 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 @@ -40,7 +46,6 @@ 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
15 changes: 15 additions & 0 deletions OSPaymentsLib/Gateways/OSPMTGateway.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// Payment Service Provider enum object
enum OSPMTGateway: String {
case stripe = "Stripe"
}

extension OSPMTGateway {

/// Converts a string into a `OSPMTGateway` object.
/// - Parameter text: Text to convert.
/// - Returns: A `OSPMTGateway` enum object if successful. `nil` is returned in case of error.
static func convert(from text: String?) -> OSPMTGateway? {
guard let text = text, text.lowercased() == "stripe" else { return nil }
return .stripe
}
}
18 changes: 18 additions & 0 deletions OSPaymentsLib/Gateways/OSPMTGatewayFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation

/// Structure responsible for creating a Wrapper for the configured Gateway.
struct OSPMTGatewayFactory {
/// Creates the correct wrapper for the gateway the user has configured.
/// - Parameter configuration: Model with the gateway configuration information.
/// - Returns: The wrapper object is everything is correctly configured. `nil` is returned otherwise.
static func createWrapper(for configuration: OSPMTGatewayModel) -> OSPMTGatewayDelegate? {
guard let gateway = configuration.gatewayEnum, let url = URL(string: configuration.requestURL) else { return nil }
let urlRequest = URLRequest(url: url)

switch gateway {
case .stripe:
guard let publishableKey = configuration.publishableKey else { return nil }
return OSPMTStripeWrapper(urlRequest: urlRequest, publishableKey: publishableKey)
}
}
}
37 changes: 37 additions & 0 deletions OSPaymentsLib/Gateways/Stripe/OSPMTStripeAPIDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import PassKit
import StripePayments

/// Delegate class containing the required calls for Stripe's SDK to process.
protocol OSPMTStripeAPIDelegate: AnyObject {
/// Sets the required publishable key, required to trigger payments through Stripe
/// - Parameter publishableKey: Key obtained via Stripe's Dashboard.
func set(_ publishableKey: String)

/// Retrieves Stripe's Payment Method's Identifier in exchange for Apple Pay's payment request result.
/// - Parameters:
/// - payment: Apple Pay's payment request result.
/// - completion: The exchange operation result. In case of success, it returns the Payment Method Id or an error otherwise.
func getPaymentMethodId(from payment: PKPayment, _ completion: @escaping (Result<String, OSPMTError>) -> Void)
}

extension STPAPIClient: OSPMTStripeAPIDelegate {
/// Sets the required publishable key, required to trigger payments through Stripe
/// - Parameter publishableKey: Key obtained via Stripe's Dashboard.
func set(_ publishableKey: String) {
self.publishableKey = publishableKey
}

/// Retrieves Stripe's Payment Method's Identifier in exchange for Apple Pay's payment request result.
/// - Parameters:
/// - payment: Apple Pay's payment request result.
/// - completion: The exchange operation result. In case of success, it returns the Payment Method Id or an error otherwise.
func getPaymentMethodId(from payment: PKPayment, _ completion: @escaping (Result<String, OSPMTError>) -> Void) {
self.createPaymentMethod(with: payment) { paymentMethod, _ in
if let paymentMethod = paymentMethod {
completion(.success(paymentMethod.stripeId))
} else {
completion(.failure(.stripePaymentMethodCreation))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/// Model to manage Stripe's payment process request parameters. This is based on `OSPMTRequestParametersModel`.
final class OSPMTStripeRequestParametersModel: OSPMTRequestParametersModel {
let paymentMethodId: String
let confirm: Bool

/// Keys used to encode and decode the model.
enum CodingKeys: String, CodingKey {
case paymentMethodId = "payment_method"
case confirm
}

/// Constructor method.
/// - Parameters:
/// - amount: Amount to charge.
/// - currency: Currency to charge.
/// - paymentMethodId: Stripe object that represents the customer's payment instruments.
/// - confirm: Automatically confirm the triggered payment process.
init(amount: Int, currency: String, paymentMethodId: String, confirm: Bool = true) {
self.paymentMethodId = paymentMethodId
self.confirm = confirm
super.init(amount: amount, currency: currency)
}

/// 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.
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(paymentMethodId, forKey: .paymentMethodId)
try container.encode(confirm, forKey: .confirm)
try super.encode(to: encoder)
}
}
45 changes: 45 additions & 0 deletions OSPaymentsLib/Gateways/Stripe/OSPMTStripeWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import PassKit
import StripeCore

/// Object responsible for making a Stripe payment process. The Wrapper deals with all calls that are required to Stripe's SDK.
final class OSPMTStripeWrapper: OSPMTGatewayDelegate {
var urlRequest: URLRequest
var urlSession: URLSession
let apiDelegate: OSPMTStripeAPIDelegate

/// Constructor method.
/// - Parameters:
/// - urlRequest: URL load request object.
/// - urlSession: Coordinator object for network data transfer tasks.
/// - publishableKey: Key required for Stripe's API to trigger calls.
/// - apiDelegate: Object responsible for Stripe's API calls.
init(urlRequest: URLRequest, urlSession: URLSession = .shared, publishableKey: String, apiDelegate: OSPMTStripeAPIDelegate = STPAPIClient.shared) {
self.urlRequest = urlRequest
self.urlSession = urlSession

apiDelegate.set(publishableKey)
self.apiDelegate = apiDelegate
}
}

extension OSPMTStripeWrapper {
/// Triggers the process through the configured gateway.
/// - Parameters:
/// - payment: Apple Pay's payment request result.
/// - details: Payment details to trigger processing.
/// - accessToken: Authorisation token related with a full payment type.
/// - completion: Payment process result. If returns the process result in case of success or an error otherwise.
func process(_ payment: PKPayment, with details: OSPMTDetailsModel, and accessToken: String, _ completion: @escaping (Result<OSPMTServiceProviderInfoModel, OSPMTError>) -> Void) {
self.apiDelegate.getPaymentMethodId(from: payment) { result in
switch result {
case .success(let paymentMethodId):
let requestParametersModel = OSPMTStripeRequestParametersModel(
amount: details.paymentAmount.multiplying(by: 100).intValue, currency: details.currency, paymentMethodId: paymentMethodId
)
self.processURLRequest(requestParametersModel, and: accessToken, completion)
case .failure(let error):
completion(.failure(error))
}
}
}
}
77 changes: 74 additions & 3 deletions OSPaymentsLib/Models/OSPMTConfigurationModel.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,43 @@
import PassKit

/// Protocol that contains all properties needed to configure a payment service.
/// 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.
class OSPMTConfigurationModel: Encodable {
// MARK: Merchant Information
var merchantID: String?
Expand All @@ -18,6 +55,9 @@ 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 @@ -28,6 +68,7 @@ class OSPMTConfigurationModel: Encodable {
case paymentSupportedCardCountries
case shippingSupportedContacts
case billingSupportedContacts
case gatewayModel = "tokenization"
}

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

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

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

Expand All @@ -97,6 +153,11 @@ 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.
Expand All @@ -120,6 +181,15 @@ class OSPMTApplePayConfiguration: OSPMTConfigurationModel {
// 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)
}

self.init(
merchantID: merchantID,
merchantName: merchantName,
Expand All @@ -128,7 +198,8 @@ class OSPMTApplePayConfiguration: OSPMTConfigurationModel {
paymentSupportedCapabilities: paymentSupportedCapabilities,
paymentSupportedCardCountries: paymentSupportedCardCountries,
shippingSupportedContacts: shippingSupportedContacts,
billingSupportedContacts: billingSupportedContacts
billingSupportedContacts: billingSupportedContacts,
gatewayModel: gatewayModel
)
}
}
Expand Down
Loading

0 comments on commit ce50ffe

Please sign in to comment.