Skip to content

Commit

Permalink
Merge pull request #195 from paypal/networking-refactor
Browse files Browse the repository at this point in the history
Networking Refactor - PR Series
  • Loading branch information
scannillo authored Sep 6, 2023
2 parents c55371b + 9c2fc60 commit bb72d32
Show file tree
Hide file tree
Showing 50 changed files with 1,618 additions and 964 deletions.
154 changes: 96 additions & 58 deletions PayPal.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions Sources/CardPayments/APIRequests/CheckoutOrdersAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Foundation
#if canImport(CorePayments)
import CorePayments
#endif

/// This class coordinates networking logic for communicating with the v2/checkout/orders API.
///
/// Details on this PayPal API can be found in PPaaS under Merchant > Checkout > Orders > v2.
class CheckoutOrdersAPI {

// MARK: - Private Propertires

private let coreConfig: CoreConfig
private let apiClient: APIClient

// MARK: - Initializer

init(coreConfig: CoreConfig) {
self.coreConfig = coreConfig
self.apiClient = APIClient(coreConfig: coreConfig)
}

/// Exposed for injecting MockAPIClient in tests
init(coreConfig: CoreConfig, apiClient: APIClient) {
self.coreConfig = coreConfig
self.apiClient = apiClient
}

// MARK: - Internal Methods

func confirmPaymentSource(cardRequest: CardRequest) async throws -> ConfirmPaymentSourceResponse {
let confirmData = ConfirmPaymentSourceRequest(cardRequest: cardRequest)

let restRequest = RESTRequest(
path: "/v2/checkout/orders/\(cardRequest.orderID)/confirm-payment-source",
method: .post,
queryParameters: nil,
postParameters: confirmData
)

let httpResponse = try await apiClient.fetch(request: restRequest)
return try HTTPResponseParser().parseREST(httpResponse, as: ConfirmPaymentSourceResponse.self)
}
}
166 changes: 62 additions & 104 deletions Sources/CardPayments/APIRequests/ConfirmPaymentSourceRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,132 +4,90 @@ import CorePayments
#endif

/// Describes request to confirm a payment source (approve an order)
struct ConfirmPaymentSourceRequest: APIRequest {
struct ConfirmPaymentSourceRequest: Encodable {

private let orderID: String
private let pathFormat: String = "/v2/checkout/orders/%@/confirm-payment-source"
private let base64EncodedCredentials: String
var jsonEncoder: JSONEncoder
// MARK: - Coding Keys

/// Creates a request to attach a payment source to a specific order.
/// In order to use this initializer, the `paymentSource` parameter has to
/// contain the entire dictionary as it exists underneath the `payment_source` key.
init(
clientID: String,
cardRequest: CardRequest,
encoder: JSONEncoder = JSONEncoder() // exposed for test injection
) throws {
self.jsonEncoder = encoder
var confirmPaymentSource = ConfirmPaymentSource()

confirmPaymentSource.applicationContext = ApplicationContext(
returnUrl: PayPalCoreConstants.callbackURLScheme + "://card/success",
cancelUrl: PayPalCoreConstants.callbackURLScheme + "://card/cancel"
)

confirmPaymentSource.paymentSource = PaymentSource(
card: cardRequest.card,
scaType: cardRequest.sca
)

self.orderID = cardRequest.orderID
self.base64EncodedCredentials = Data(clientID.appending(":").utf8).base64EncodedString()
path = String(format: pathFormat, orderID)

jsonEncoder.keyEncodingStrategy = .convertToSnakeCase
do {
body = try jsonEncoder.encode(confirmPaymentSource)
} catch {
throw CardClientError.encodingError
}

// TODO - The complexity in this `init` signals to reconsider our use/design of the `APIRequest` protocol.
// Existing pattern doesn't provide clear, testable interface for encoding JSON POST bodies.
private enum TopLevelKeys: String, CodingKey {
case paymentSource = "payment_source"
case applicationContext = "application_context"
}

// MARK: - APIRequest

typealias ResponseType = ConfirmPaymentSourceResponse

var path: String
var method: HTTPMethod = .post
var body: Data?

var headers: [HTTPHeader: String] {
[
.contentType: "application/json", .acceptLanguage: "en_US",
.authorization: "Basic \(base64EncodedCredentials)"
]
private enum ApplicationContextKeys: String, CodingKey {
case returnURL = "return_url"
case cancelURL = "cancel_url"
}

private struct ConfirmPaymentSource: Encodable {

var paymentSource: PaymentSource?
var applicationContext: ApplicationContext?
private enum PaymentSourceKeys: String, CodingKey {
case card
}

private struct ApplicationContext: Encodable {

let returnUrl: String
let cancelUrl: String
}

enum CodingKeys: String, CodingKey {
private enum CardKeys: String, CodingKey {
case number
case expiry
case securityCode
case name
case billingAddress
case name
case attributes
}

private struct EncodedCard: Encodable {

let number: String
let securityCode: String
let billingAddress: Address?
let name: String?
let expiry: String
let attributes: Attributes?

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(number, forKey: .number)
try container.encode(expiry, forKey: .expiry)
try container.encode(securityCode, forKey: .securityCode)
try container.encode(name, forKey: .name)
try container.encode(billingAddress, forKey: .billingAddress)
try container.encode(attributes, forKey: .attributes)
}
private enum BillingAddressKeys: String, CodingKey {
case addressLine1 = "address_line_1"
case addressLine2 = "address_line_2"
case adminArea1 = "admin_area_1"
case adminArea2 = "admin_area_2"
case postalCode
case countryCode
}

private struct Verification: Encodable {

let method: String
private enum AttributesKeys: String, CodingKey {
case vault
case verification
}

private struct Attributes: Encodable {

let verification: Verification

init(verificationMethod: String) {
self.verification = Verification(method: verificationMethod)
}
private enum VerificationKeys: String, CodingKey {
case method
}

// MARK: - Internal Properties
// Exposed for testing

let returnURL = PayPalCoreConstants.callbackURLScheme + "://card/success"
let cancelURL = PayPalCoreConstants.callbackURLScheme + "://card/cancel"
let cardRequest: CardRequest

// MARK: - Initializer

init(cardRequest: CardRequest) {
self.cardRequest = cardRequest
}

private struct PaymentSource: Encodable {
// MARK: - Custom Encoder

func encode(to encoder: Encoder) throws {
var topLevel = encoder.container(keyedBy: TopLevelKeys.self)

let card: EncodedCard
var applicationContext = topLevel.nestedContainer(keyedBy: ApplicationContextKeys.self, forKey: .applicationContext)
try applicationContext.encode(returnURL, forKey: .returnURL)
try applicationContext.encode(cancelURL, forKey: .cancelURL)

init(card: Card, scaType: SCA) {
self.card = EncodedCard(
number: card.number,
securityCode: card.securityCode,
billingAddress: card.billingAddress,
name: card.cardholderName,
expiry: "\(card.expirationYear)-\(card.expirationMonth)",
attributes: Attributes(verificationMethod: scaType.rawValue)
)
var paymentSource = topLevel.nestedContainer(keyedBy: PaymentSourceKeys.self, forKey: .paymentSource)
var card = paymentSource.nestedContainer(keyedBy: CardKeys.self, forKey: .card)
try card.encode(cardRequest.card.number, forKey: .number)
try card.encode("\(cardRequest.card.expirationYear)-\(cardRequest.card.expirationMonth)", forKey: .expiry)
try card.encodeIfPresent(cardRequest.card.cardholderName, forKey: .name)

if let cardBillingInfo = cardRequest.card.billingAddress {
var billingAddress = card.nestedContainer(keyedBy: BillingAddressKeys.self, forKey: .billingAddress)
try billingAddress.encodeIfPresent(cardBillingInfo.addressLine1, forKey: .addressLine1)
try billingAddress.encodeIfPresent(cardBillingInfo.addressLine2, forKey: .addressLine2)
try billingAddress.encodeIfPresent(cardBillingInfo.postalCode, forKey: .postalCode)
try billingAddress.encodeIfPresent(cardBillingInfo.region, forKey: .adminArea1)
try billingAddress.encodeIfPresent(cardBillingInfo.locality, forKey: .adminArea2)
try billingAddress.encodeIfPresent(cardBillingInfo.countryCode, forKey: .countryCode)
}

var attributes = card.nestedContainer(keyedBy: AttributesKeys.self, forKey: .attributes)
var verification = attributes.nestedContainer(keyedBy: VerificationKeys.self, forKey: .verification)
try verification.encode(cardRequest.sca.rawValue, forKey: .method)
}
}
83 changes: 0 additions & 83 deletions Sources/CardPayments/APIRequests/UpdateSetupTokenQuery.swift

This file was deleted.

52 changes: 52 additions & 0 deletions Sources/CardPayments/APIRequests/UpdateVaultVariables.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation

struct UpdateVaultVariables: Encodable {

// MARK: - Coding Keys

private enum TopLevelKeys: String, CodingKey {
case clientID
case vaultSetupToken
case paymentSource
}

private enum PaymentSourceKeys: String, CodingKey {
case card
}

private enum CardKeys: String, CodingKey {
case number
case securityCode
case expiry
case name
}

// MARK: - Internal Properties
// Exposed for testing

let vaultRequest: CardVaultRequest
let clientID: String

// MARK: - Initializer

init(cardVaultRequest: CardVaultRequest, clientID: String) {
self.vaultRequest = cardVaultRequest
self.clientID = clientID
}

// MARK: - Custom Encoder

func encode(to encoder: Encoder) throws {
var topLevel = encoder.container(keyedBy: TopLevelKeys.self)
try topLevel.encode(clientID, forKey: .clientID)
try topLevel.encode(vaultRequest.setupTokenID, forKey: .vaultSetupToken)

var paymentSource = topLevel.nestedContainer(keyedBy: PaymentSourceKeys.self, forKey: .paymentSource)

var card = paymentSource.nestedContainer(keyedBy: CardKeys.self, forKey: .card)
try card.encode(vaultRequest.card.number, forKey: .number)
try card.encode(vaultRequest.card.securityCode, forKey: .securityCode)
try card.encode(vaultRequest.card.expiry, forKey: .expiry)
try card.encodeIfPresent(vaultRequest.card.cardholderName, forKey: .name)
}
}
Loading

0 comments on commit bb72d32

Please sign in to comment.