Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Networking Refactor - PR Series #195

Merged
merged 7 commits into from
Sep 6, 2023
Merged
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