Skip to content

Commit

Permalink
feat: Set Details and Trigger Payment (#3)
Browse files Browse the repository at this point in the history
Configure the missing payment details and, by mixing it with the configuration info, trigger the payment request.
  • Loading branch information
OS-ricardomoreirasilva committed Apr 11, 2024
1 parent fb74bb8 commit 4f58bb5
Show file tree
Hide file tree
Showing 32 changed files with 1,723 additions and 66 deletions.
98 changes: 87 additions & 11 deletions OSPaymentsLib.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions OSPaymentsLib/Error/OSPMTError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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."
}
}
}
18 changes: 18 additions & 0 deletions OSPaymentsLib/Extensions/PKContactField+Adapter.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import PassKit

protocol OSPMTWalletAvailabilityDelegate: AnyObject {
static func isWalletAvailable() -> Bool
}

extension PKPassLibrary: OSPMTWalletAvailabilityDelegate {
static func isWalletAvailable() -> Bool {
Self.isPassLibraryAvailable()
Expand Down
79 changes: 79 additions & 0 deletions OSPaymentsLib/Extensions/PKPayment+Adapter.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<OSPMTApplePayRequestTriggerDelegate, OSPMTError> {
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)
}
}

This file was deleted.

49 changes: 49 additions & 0 deletions OSPaymentsLib/Models/OSPMTAddressModel.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
40 changes: 40 additions & 0 deletions OSPaymentsLib/Models/OSPMTContactInfoModel.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
43 changes: 43 additions & 0 deletions OSPaymentsLib/Models/OSPMTDataModel.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
53 changes: 53 additions & 0 deletions OSPaymentsLib/Models/OSPMTDetailsModel.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 4f58bb5

Please sign in to comment.