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 authored Aug 26, 2022
1 parent 80a1af4 commit 85bc27b
Show file tree
Hide file tree
Showing 189 changed files with 18,741 additions and 40 deletions.
33 changes: 33 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
disabled_rules:
- trailing_whitespace
opt_in_rules:
- empty_count
- empty_string
excluded:
- Carthage
- Pods
- vendor
- SwiftLint/Common/3rdPartyLib
line_length:
warning: 150
error: 200
ignores_function_declarations: true
ignores_comments: true
ignores_urls: true
function_body_length:
warning: 300
error: 500
function_parameter_count:
warning: 6
error: 8
type_body_length:
warning: 300
error: 500
file_length:
warning: 1000
error: 1500
ignore_comment_only_lines: true
cyclomatic_complexity:
warning: 15
error: 25
reporter: "xcode"
301 changes: 295 additions & 6 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 = "1330"
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
10 changes: 10 additions & 0 deletions OSPaymentsLib.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
33 changes: 33 additions & 0 deletions OSPaymentsLib/Error/OSPMTError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/// All plugin errors that can be thrown
enum OSPMTError: Int, CustomNSError, LocalizedError {
case invalidConfiguration = 1
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 "Couldn't obtain the payment's informations from the configurations file."
case .walletNotAvailable:
return "The Apple Pay is not available in the device."
case .paymentNotAvailable:
return "There is no payment method configured."
case .setupPaymentNotAvailable:
return "There are no valid payment cards for the supported networks and/or capabilities."
case .invalidDecodeDetails:
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 .paymentCancelled:
return "Payment was cancelled by the user."
}
}
}
21 changes: 21 additions & 0 deletions OSPaymentsLib/Extensions/PKContactField+Adapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import PassKit

extension PKContactField {
/// Allows the conversion of the contact text associated to Shipping and Billing Information into an object of `PKContactField` type.
/// - Parameter text: Contact field text to convert.
/// - Returns: The equivalent `PKContactField` object.
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
}
}
}
21 changes: 21 additions & 0 deletions OSPaymentsLib/Extensions/PKMerchantCapability+Adapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import PassKit

extension PKMerchantCapability {
/// Allows the conversion of the text associated to a Merchant Capability into an object of `PKMerchantCapability` type.
/// - Parameter text: Merchant capability text to convert.
/// - Returns: The equivalent `PKMerchantCapability` object.
static func convert(from text: String) -> PKMerchantCapability? {
switch text.lowercased() {
case "debit":
return .capabilityDebit
case "credit":
return .capabilityCredit
case "3ds":
return .capability3DS
case "emv":
return .capabilityEMV
default:
return nil
}
}
}
9 changes: 9 additions & 0 deletions OSPaymentsLib/Extensions/PKPassLibrary+Adapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import PassKit

extension PKPassLibrary: OSPMTWalletAvailabilityDelegate {
/// Verifies if the wallet is available for usage.
/// - Returns: A boolean indicating if the wallet is available.
static func isWalletAvailable() -> Bool {
Self.isPassLibraryAvailable()
}
}
93 changes: 93 additions & 0 deletions OSPaymentsLib/Extensions/PKPayment+Adapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import PassKit

extension PKPayment {
/// Converts a `PKPayment` object into a `OSPMTScopeModel` one. Returns `nil` if it can't.
/// - 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()]
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
}

/// Converts a `PKPayment` object into a dictionary that relates to an `OSPMTDataModel` object.
/// - Returns: The corresponding `OSPMTDataModel` dictionary object.
private 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
}

/// 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.
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) {
result[OSPMTTokenInfoModel.CodingKeys.token.rawValue] = token
}

return result
}

/// 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.
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)
}
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, !givenName.isEmpty, !familyName.isEmpty {
result[OSPMTContactInfoModel.CodingKeys.name.rawValue] = "\(givenName) \(familyName)"
}
if let email = contact.emailAddress {
result[OSPMTContactInfoModel.CodingKeys.email.rawValue] = email
}

return !result.isEmpty ? result : nil
}

/// 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.
private 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,66 @@
import PassKit

extension PKPaymentAuthorizationController: OSPMTApplePaySetupAvailabilityDelegate {
/// Verifies if a payment is available for usage.
/// - Returns: A boolean indicating if a payment is available.
static func isPaymentAvailable() -> Bool {
Self.canMakePayments()
}

/// Verifies if a payment is available for usage, given the passed payment networks and marchant capabilities
/// - Parameters:
/// - networks: Array of payment networks available by a merchant
/// - merchantCapabilities: Bit set containing the payment capabilities available by a merchant.
/// - Returns: A boolean indicating if the payment is available.
static func isPaymentAvailable(using networks: [PKPaymentNetwork]?, and merchantCapabilities: PKMerchantCapability?) -> Bool {
guard let networks = networks, let merchantCapabilities = merchantCapabilities else { return false }
return Self.canMakePayments(usingNetworks: networks, capabilities: merchantCapabilities)
}
}

extension PKPaymentAuthorizationController: OSPMTApplePayRequestTriggerDelegate {
/// Triggers a payment request. The result is processed asyncrhonously and returned by the `completion` parameters.
/// - Parameter completion: Block that returns the success of the payment request operation.
func triggerPayment(_ completion: @escaping OSPMTRequestTriggerCompletion) {
self.present(completion: completion)
}

/// Creates an object responsible for dealing with the payment request process, delegating the details to the passed parameter.
/// - Parameters:
/// - detailsModel: Payment details.
/// - delegate: The object responsible for the request process' response.
/// - Returns: An instance of the object or an error, it the instatiation fails.
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 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: detailsModel.billingContact.isCustom
? detailsModel.billingContact.contactArray
: delegate.configuration.billingSupportedContacts
)
paymentRequest.requiredShippingContactFields = delegate.getContactFields(
for: detailsModel.shippingContact.isCustom
? detailsModel.shippingContact.contactArray
: delegate.configuration.shippingSupportedContacts
)
paymentRequest.supportedCountries = delegate.configuration.supportedCountries
paymentRequest.supportedNetworks = supportedNetworks

let paymentAuthorizationController = PKPaymentAuthorizationController(paymentRequest: paymentRequest)
paymentAuthorizationController.delegate = delegate
return .success(paymentAuthorizationController)
}
}
21 changes: 21 additions & 0 deletions OSPaymentsLib/Extensions/PKPaymentNetwork+Adapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import PassKit

extension PKPaymentNetwork {
/// Allows the conversion of the text associated to a Payment Network into an object of `PKPaymentNetwork` type.
/// - Parameter text: Payment network text to convert.
/// - Returns: The equivalent `PKPaymentNetwork` object.
static func convert(from text: String) -> PKPaymentNetwork? {
switch text.lowercased() {
case "amex":
return .amex
case "discover":
return .discover
case "visa":
return .visa
case "mastercard":
return .masterCard
default:
return nil
}
}
}
Loading

0 comments on commit 85bc27b

Please sign in to comment.