Skip to content

Commit

Permalink
feat: Add Stripe as First Payment Gateway (#11)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
OS-ricardomoreirasilva committed Apr 11, 2024
1 parent 199b357 commit 8de483c
Show file tree
Hide file tree
Showing 600 changed files with 51,677 additions and 491 deletions.
64 changes: 60 additions & 4 deletions OSPaymentsLib.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions OSPaymentsLib/Error/OSPMTError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public enum OSPMTError: Int, CustomNSError, LocalizedError {
case paymentTriggerPresentationFailed = 10
case paymentCancelled = 11

case gatewaySetFailed = 12
case stripePaymentMethodCreation = 13
case paymentIssue = 14

/// Textual description
public var errorDescription: String? {
switch self {
Expand All @@ -30,6 +34,13 @@ 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."
}
}
}
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
14 changes: 14 additions & 0 deletions OSPaymentsLib/Gateways/OSPMTGateway.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// 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? {
return text.lowercased() == "stripe" ? .stripe : nil
}
}
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)
}
}
44 changes: 44 additions & 0 deletions OSPaymentsLib/Gateways/Stripe/OSPMTStripeWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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.
/// - completion: Payment process result. If returns the process result in case of success or an error otherwise.
func process(_ payment: PKPayment, with details: OSPMTDetailsModel, _ 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, 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 8de483c

Please sign in to comment.