Skip to content

Commit

Permalink
Merge pull request #271 from afterpay/markmroz/button
Browse files Browse the repository at this point in the history
Button with Cash App Pay
  • Loading branch information
mmroz authored Jun 6, 2024
2 parents 0a504ab + 75cae89 commit 211f56a
Show file tree
Hide file tree
Showing 19 changed files with 879 additions and 24 deletions.
24 changes: 24 additions & 0 deletions Afterpay.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
557511BF2644CAA50040CC51 /* WKWebView+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557511BE2644CAA50040CC51 /* WKWebView+Cache.swift */; };
557511C12644D0890040CC51 /* WKWebViewConfiguration+UserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557511C02644D0890040CC51 /* WKWebViewConfiguration+UserAgent.swift */; };
55A2D307261BB36C00D8E23A /* Money.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55A2D306261BB36C00D8E23A /* Money.swift */; };
5FB958DA2C0A526800137468 /* CheckoutV3CashAppPayResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB958D92C0A526800137468 /* CheckoutV3CashAppPayResult.swift */; };
5FB958DC2C0A528300137468 /* ConfirmationV3+CashAppPay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB958DB2C0A528300137468 /* ConfirmationV3+CashAppPay.swift */; };
5FB958DE2C0E00DC00137468 /* AfterpayV3Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB958DD2C0E00DC00137468 /* AfterpayV3Tests.swift */; };
5FB958E32C0E126200137468 /* URLSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB958E22C0E126200137468 /* URLSessionMock.swift */; };
6602EF0F25358A8000A0468C /* ColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6602EF0E25358A8000A0468C /* ColorScheme.swift */; };
6605666324E5199500DA588E /* Locales.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6605666224E5199500DA588E /* Locales.swift */; };
66169312257A06B200DF6CF4 /* CheckoutV2Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66169311257A06B200DF6CF4 /* CheckoutV2Message.swift */; };
Expand Down Expand Up @@ -140,6 +144,10 @@
557511BE2644CAA50040CC51 /* WKWebView+Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKWebView+Cache.swift"; sourceTree = "<group>"; };
557511C02644D0890040CC51 /* WKWebViewConfiguration+UserAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKWebViewConfiguration+UserAgent.swift"; sourceTree = "<group>"; };
55A2D306261BB36C00D8E23A /* Money.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Money.swift; sourceTree = "<group>"; };
5FB958D92C0A526800137468 /* CheckoutV3CashAppPayResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutV3CashAppPayResult.swift; sourceTree = "<group>"; };
5FB958DB2C0A528300137468 /* ConfirmationV3+CashAppPay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfirmationV3+CashAppPay.swift"; sourceTree = "<group>"; };
5FB958DD2C0E00DC00137468 /* AfterpayV3Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AfterpayV3Tests.swift; sourceTree = "<group>"; };
5FB958E22C0E126200137468 /* URLSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionMock.swift; sourceTree = "<group>"; };
6602EF0E25358A8000A0468C /* ColorScheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorScheme.swift; sourceTree = "<group>"; };
6605666224E5199500DA588E /* Locales.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locales.swift; sourceTree = "<group>"; };
66169311257A06B200DF6CF4 /* CheckoutV2Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutV2Message.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -233,6 +241,8 @@
4509D358294017C500952DAD /* CashAppSigningResponse.swift */,
4509D356293FFB5200952DAD /* CashAppPayCheckout.swift */,
458E7D36296E1F9D001B696F /* CashAppSigningResult.swift */,
5FB958D92C0A526800137468 /* CheckoutV3CashAppPayResult.swift */,
5FB958DB2C0A528300137468 /* ConfirmationV3+CashAppPay.swift */,
);
path = CashApp;
sourceTree = "<group>";
Expand All @@ -246,6 +256,14 @@
path = Widget;
sourceTree = "<group>";
};
5FB958E12C0E124E00137468 /* Mocks */ = {
isa = PBXGroup;
children = (
5FB958E22C0E126200137468 /* URLSessionMock.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
661B233024DA87EA0010EBCD /* Views */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -293,6 +311,7 @@
665FC5782488766C00A5A93E /* AfterpayTests */ = {
isa = PBXGroup;
children = (
5FB958E12C0E124E00137468 /* Mocks */,
556DEBF92653531000BFC277 /* mock-widget-bootstrap.js */,
6635B95E24CAA9F000EBB3A6 /* ConfigurationTests.swift */,
66C3F7FA25397A810086DD0A /* CurrencyFormatterTests.swift */,
Expand All @@ -303,6 +322,7 @@
550D48142625539900C0B0C6 /* WidgetStatusTests.swift */,
557511BA264259C30040CC51 /* CombineWrapperTests.swift */,
4573E4B32A4D4DCB00F5CEAA /* LocaleTests.swift */,
5FB958DD2C0E00DC00137468 /* AfterpayV3Tests.swift */,
);
path = AfterpayTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -610,8 +630,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5FB958E32C0E126200137468 /* URLSessionMock.swift in Sources */,
551BEDF125F98FA800FDF9EE /* FeaturesTests.swift in Sources */,
6635B95F24CAA9F000EBB3A6 /* ConfigurationTests.swift in Sources */,
5FB958DE2C0E00DC00137468 /* AfterpayV3Tests.swift in Sources */,
66DAAC8D24E109D200127460 /* PriceBreakdownTests.swift in Sources */,
550D481B26255D8600C0B0C6 /* WidgetEventTests.swift in Sources */,
550D48152625539900C0B0C6 /* WidgetStatusTests.swift in Sources */,
Expand All @@ -633,8 +655,10 @@
6602EF0F25358A8000A0468C /* ColorScheme.swift in Sources */,
42087EE727A746F700BE5442 /* MoreInfoOptions.swift in Sources */,
66B5458C256B65B7002B3DD5 /* CheckoutHost.swift in Sources */,
5FB958DA2C0A526800137468 /* CheckoutV3CashAppPayResult.swift in Sources */,
66169312257A06B200DF6CF4 /* CheckoutV2Message.swift in Sources */,
45144E7427FD11470061EBE8 /* AfterpayBundleFinder.swift in Sources */,
5FB958DC2C0A528300137468 /* ConfirmationV3+CashAppPay.swift in Sources */,
666818202591CB9800A2003E /* Alerts.swift in Sources */,
45144E7027FCEFA30061EBE8 /* LockupView.swift in Sources */,
6689536C24C96CB5005090B4 /* Configuration.swift in Sources */,
Expand Down
180 changes: 180 additions & 0 deletions AfterpayTests/AfterpayV3Tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//
// AfterpayV3Tests.swift
// AfterpayTests
//
// Created by Mark Mroz on 2024-06-03.
// Copyright © 2024 Afterpay. All rights reserved.
//

import XCTest
@testable import Afterpay

final class AfterpayV3Tests: XCTestCase {

private var v3Configuration: CheckoutV3Configuration!

override func setUpWithError() throws {
try super.setUpWithError()
v3Configuration = CheckoutV3Configuration(
shopDirectoryMerchantId: "merchant_id",
region: .US,
environment: .production
)

let v2Configuration = try Configuration(
minimumAmount: nil,
maximumAmount: "150",
currencyCode: "USD",
locale: Locale(identifier: "en_US"),
environment: .production
)
Afterpay.setConfiguration(v2Configuration)
}

override func tearDown() {
v3Configuration = nil
Afterpay.setConfiguration(nil)
super.tearDown()
}

func testCheckoutV3WithCashAppPay() throws {
let checkoutV3Expectation = self.expectation(description: "Checkout V3")
let checkoutHandler: URLRequestHandler = { (request, response) in
URLSessionMock(dataTaskHandler: { url in
XCTAssertEqual(url.absoluteString, "https://api-plus.us.afterpay.com/v3/button")
return (Fixtures.checkoutV3WithCashAppPayResponse, nil, nil)
}).dataTask(with: request.url!) { data, urlResponse, error in
checkoutV3Expectation.fulfill()
response(data, urlResponse, error)
}
}

let signTokenExpectation = self.expectation(description: "Sign Token")
let signHandler = URLSessionMock(requestDataTaskHandler: { request in
XCTAssertEqual(request.url?.absoluteString, "https://api-plus.us.afterpay.com/v2/payments/sign-payment")
signTokenExpectation.fulfill()
return (Fixtures.signPaymentResponse, HTTPURLResponse(), nil)
})

let checkoutExpectation = self.expectation(description: "Completed Checkout")
Afterpay.checkoutV3WithCashAppPay(
consumer: Consumer(email: "[email protected]"),
orderTotal: OrderTotal(total: 10, shipping: 0, tax: 0),
items: [Product(name: "Coffee", quantity: 1, price: 10)],
configuration: v3Configuration,
urlSession: signHandler,
requestHandler: checkoutHandler
) { result in
switch result {
case .success(let data):
XCTAssertEqual(data.singleUseCardToken, "AQI")
XCTAssertEqual(data.token, "002.x")
XCTAssertEqual(data.cashAppSigningData.amount, 5000)
XCTAssertEqual(data.cashAppSigningData.brandId, "BRAND_ID")
XCTAssertEqual(data.cashAppSigningData.merchantId, "MMI_6nvgu9voweagwt5dn0kdteaio")
XCTAssertEqual(
data.cashAppSigningData.redirectUri.absoluteString,
"https://static-us.afterpay.com/javascript/button/index.html"
)
case .cancelled, .failure:
XCTFail("Expected success")
}
checkoutExpectation.fulfill()
}
waitForExpectations(timeout: 0.5)
}

func testCheckoutV3ConfirmForCashAppPay() {

let confirmRequestExpectation = self.expectation(description: "Confirmed")
let checkoutHandler: URLRequestHandler = { (request, response) in
URLSessionMock(dataTaskHandler: { url in
XCTAssertEqual(url.absoluteString, "https://api-plus.us.afterpay.com/v3/button/confirm")
return (Fixtures.confirmResponse, nil, nil)
}).dataTask(with: request.url!) { data, urlResponse, error in
confirmRequestExpectation.fulfill()
response(data, urlResponse, error)
}
}

let confirmExpectation = self.expectation(description: "Confirmed")
Afterpay.checkoutV3ConfirmForCashAppPay(
token: "002.x",
singleUseCardToken: "AQI",
cashAppPayCustomerID: "CUST_ID",
cashAppPayGrantID: "GRR_ID",
jwt: "JWT",
configuration: v3Configuration,
requestHandler: checkoutHandler
) { result in
switch result {
case .success(let data):
XCTAssertEqual(data.paymentDetails.virtualCard?.cardType, "VISA")
XCTAssertEqual(data.paymentDetails.virtualCard?.cardNumber, "4111111111111111")
XCTAssertEqual(data.paymentDetails.virtualCard?.cvc, "737")
XCTAssertEqual(data.paymentDetails.virtualCard?.expiryMonth, 3)
XCTAssertEqual(data.paymentDetails.virtualCard?.expiryYear, 30)
XCTAssertNotNil(data.cardValidUntil)
case .failure:
XCTFail("Expected success")
}

confirmExpectation.fulfill()
}

waitForExpectations(timeout: 0.5)
}
}

// MARK: - Private

private extension AfterpayV3Tests {
struct Product: CheckoutV3Item {
let name: String
let quantity: UInt
let price: Decimal
let sku: String? = nil
let pageUrl: URL? = nil
let imageUrl: URL? = nil
let categories: [[String]]? = nil
let estimatedShipmentDate: String? = nil
}
}

// MARK: - Fixtures

extension AfterpayV3Tests {
// swiftlint:disable line_length indentation_width
enum Fixtures {
static let checkoutV3WithCashAppPayResponse = """
{
"token": "002.x",
"confirmMustBeCalledBefore": "2024-06-04T02:29:53.803Z",
"redirectCheckoutUrl": "https://portal.sandbox.afterpay.com/us/checkout/?token=002.x",
"singleUseCardToken": "AQI"
}
""".data(using: .utf8)

static let signPaymentResponse = """
{
"jwtToken": "eyJraWQiOiJrZXkxIiwiYWxnIjoiRVMyNTYiLCJ0dGwiOiIxNzE3NDM3Nzc1In0.eyJleHRlcm5hbE1lcmNoYW50SWQiOiJNTUlfNm52Z3U5dm93ZWFnd3Q1ZG4wa2R0ZWFpbyIsInRva2VuIjoiMDAyLmlscXFqZjJ1ZG11MnJwM3hjdTdjYjZtenVwNnRlZndiM2Q1eWs2Y3pyZnZqd3Nmb2NuIiwiYW1vdW50Ijp7ImFtb3VudCI6IjUwLjAwIiwiY3VycmVuY3kiOiJVU0QiLCJzeW1ib2wiOiIkIn0sInJlZGlyZWN0VXJsIjoiaHR0cHM6Ly9zdGF0aWMtdXMuYWZ0ZXJwYXkuY29tL2phdmFzY3JpcHQvYnV0dG9uL2luZGV4Lmh0bWwifQ.KRVxIHwrH_QPDTX2WF3Ei5wI7InE_v7xvPDDXFF2YBka2hUROkSX6ubdrFufIkE6yaFHyrlAGoQiS17VB80IDA",
"redirectUrl": "https://static-us.afterpay.com",
"externalBrandId": "BRAND_ID"
}
""".data(using: .utf8)

static let confirmResponse = """
{
"paymentDetails": {
"virtualCard": {
"cardType": "VISA",
"cardNumber": "4111111111111111",
"cvc": "737",
"expiry": "30-03"
}
},
"cardValidUntil": "2024-06-03T18:31:32.096071575Z"
}
""".data(using: .utf8)
}
}
62 changes: 62 additions & 0 deletions AfterpayTests/Mocks/URLSessionMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// URLSessionMock.swift
// AfterpayTests
//
// Created by Mark Mroz on 2024-06-03.
// Copyright © 2024 Afterpay. All rights reserved.
//

import Foundation

// MARK: - URLSessionDataTaskMock

final class URLSessionDataTaskMock: URLSessionDataTask {
private let resumeHandler: () -> Void

init(resumeHandler: @escaping () -> Void) {
self.resumeHandler = resumeHandler
}

override func resume() {
resumeHandler()
}
}

// MARK: - URLSessionMock

final class URLSessionMock: URLSession {
typealias RequestDataTaskHandler = (URLRequest) -> (Data?, URLResponse?, Error?)
typealias URLDataTaskHandler = (URL) -> (Data?, URLResponse?, Error?)
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void

private let dataTaskHandler: URLDataTaskHandler
private let requestDataTaskHandler: RequestDataTaskHandler

init(
dataTaskHandler: @escaping URLDataTaskHandler = { _ in (nil, nil, nil) },
requestDataTaskHandler: @escaping RequestDataTaskHandler = { _ in (nil, nil, nil) }
) {
self.dataTaskHandler = dataTaskHandler
self.requestDataTaskHandler = requestDataTaskHandler
}

override func dataTask(
with url: URL,
completionHandler: @escaping CompletionHandler
) -> URLSessionDataTask {
let (data, response, error) = dataTaskHandler(url)
return URLSessionDataTaskMock {
completionHandler(data, response, error)
}
}

override func dataTask(
with request: URLRequest,
completionHandler: @escaping CompletionHandler
) -> URLSessionDataTask {
let (data, response, error) = requestDataTaskHandler(request)
return URLSessionDataTaskMock {
completionHandler(data, response, error)
}
}
}
4 changes: 4 additions & 0 deletions Example/Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
55432838263B7EC2005512E4 /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55432837263B7EC2005512E4 /* ExampleUITests.swift */; };
55719BA926363ED800634B27 /* SwiftUIWidgetExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55719BA826363ED800634B27 /* SwiftUIWidgetExample.swift */; };
55FA7270260025DC0006EFCB /* WidgetHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FA726F260025DC0006EFCB /* WidgetHandler.swift */; };
5FB958E62C0E3D7B00137468 /* CheckoutPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FB958E52C0E3D7B00137468 /* CheckoutPickerView.swift */; };
660072B724A1B55E00E9A2BC /* TextSettingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660072B624A1B55E00E9A2BC /* TextSettingCell.swift */; };
6620B5D224934FB3004162BC /* AppFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6620B5D124934FB3004162BC /* AppFlowController.swift */; };
663A228E249B030D0027C296 /* WidgetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663A228D249B030D0027C296 /* WidgetViewController.swift */; };
Expand Down Expand Up @@ -100,6 +101,7 @@
55432839263B7EC2005512E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
55719BA826363ED800634B27 /* SwiftUIWidgetExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIWidgetExample.swift; sourceTree = "<group>"; };
55FA726F260025DC0006EFCB /* WidgetHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHandler.swift; sourceTree = "<group>"; };
5FB958E52C0E3D7B00137468 /* CheckoutPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutPickerView.swift; sourceTree = "<group>"; };
660072B624A1B55E00E9A2BC /* TextSettingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextSettingCell.swift; sourceTree = "<group>"; };
6620B5D124934FB3004162BC /* AppFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlowController.swift; sourceTree = "<group>"; };
663A228D249B030D0027C296 /* WidgetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -223,6 +225,7 @@
83F7DEE2269D2BAA00F9FB75 /* SingleUseCardResultViewController.swift */,
66BD11B324ADB7EB00039DA6 /* TitleSubtitleCell.swift */,
55FA726F260025DC0006EFCB /* WidgetHandler.swift */,
5FB958E52C0E3D7B00137468 /* CheckoutPickerView.swift */,
);
path = Purchase;
sourceTree = "<group>";
Expand Down Expand Up @@ -503,6 +506,7 @@
files = (
1535ACB725DBA8AD00727818 /* CheckoutHandler.swift in Sources */,
152224B124AAF7AE006E7D78 /* Objc.m in Sources */,
5FB958E62C0E3D7B00137468 /* CheckoutPickerView.swift in Sources */,
6650F41C24BC03DC00B16A57 /* Colors.swift in Sources */,
6620B5D224934FB3004162BC /* AppFlowController.swift in Sources */,
664722A724A5D9B00079B1FB /* AlertFactory.swift in Sources */,
Expand Down
Loading

0 comments on commit 211f56a

Please sign in to comment.