Skip to content

Commit

Permalink
Merge branch 'main' into button-typeface-update
Browse files Browse the repository at this point in the history
  • Loading branch information
stechiu authored Jan 17, 2024
2 parents 5dc8aed + 157712a commit 50a268d
Show file tree
Hide file tree
Showing 14 changed files with 194 additions and 726 deletions.
17 changes: 11 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@

## unreleased
* PaymentButtons
* Deprecate `PaymentButtonColor` `.black`, `.silver`, `.blue`, and `.darkBlue`
* Add `PaymentButtonColor.gold` for `PayPalCreditButton`
* Add `custom` case for `PaymentButtonEdges`
* Support VoiceOver by adding button accessibility labels

* Deprecate `PaymentButtonColor` `.black`, `.silver`, `.blue`, and `.darkBlue`
* Add `PaymentButtonColor.gold` for `PayPalCreditButton`
* Add `custom` case for `PaymentButtonEdges`
* Support VoiceOver by adding button accessibility labels
* Add `custom` case for `PaymentButtonEdges`
* Support VoiceOver by adding button accessibility labels
* CardPayments
* Add `liabilityShift` property to `CardResult`
* Add `CardClientError.threeDSVerificationError` for invalid verification
* Add `CardClientError.missingDeeplinkURLError` for missing deeplink URL
* Add `CardClientError.malformedDeeplinkURLError` for malformed or invalid deeplink
* PayPalWebPayments
* Add `vault(url:)` method to `PayPalWebCheckoutClient`
* Add `PayPalVaultResult` type to return vault result
* Add `PayPalVaultDelegate` to handle results from vault flow
* Add `PayPalWebCheckoutClientError.paypalVaultResponseError` for missing or invalid response from vaulting

* Breaking Changes
* PaymentButtons
* Font typeface changed to "PayPalOpen" to meet brand guidelines
Expand Down
23 changes: 23 additions & 0 deletions Demo/Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
CBC16DD929ED90B600307117 /* UpdateOrderParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBC16DD829ED90B600307117 /* UpdateOrderParams.swift */; };
CBDEEA222989990200A460A6 /* CorePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CBDEEA212989990200A460A6 /* CorePayments.framework */; };
CBDEEA232989990200A460A6 /* CorePayments.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CBDEEA212989990200A460A6 /* CorePayments.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
EE58CBCF2B5080C1003F2666 /* PayPalCheckout in Frameworks */ = {isa = PBXBuildFile; productRef = EE58CBCE2B5080C1003F2666 /* PayPalCheckout */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -227,6 +228,7 @@
CB1AC3B82982AAD70081AED6 /* CardPayments.framework in Frameworks */,
CB1AC3C02982C4030081AED6 /* PayPalWebPayments.framework in Frameworks */,
CB1AC3BB2982BB130081AED6 /* PaymentButtons.framework in Frameworks */,
EE58CBCF2B5080C1003F2666 /* PayPalCheckout in Frameworks */,
CBDEEA222989990200A460A6 /* CorePayments.framework in Frameworks */,
8052E2A529B684BA00B33FBC /* PPRiskMagnes.xcframework in Frameworks */,
CB1AC3C72982E32D0081AED6 /* FraudProtection.framework in Frameworks */,
Expand Down Expand Up @@ -525,6 +527,7 @@
);
name = Demo;
packageProductDependencies = (
EE58CBCE2B5080C1003F2666 /* PayPalCheckout */,
);
productName = Demo;
productReference = 806F1E3526B85367007A60E6 /* Demo.app */;
Expand Down Expand Up @@ -577,6 +580,7 @@
);
mainGroup = 806F1E2C26B85367007A60E6;
packageReferences = (
EE58CBCD2B5080C1003F2666 /* XCRemoteSwiftPackageReference "paypalcheckout-ios" */,
);
productRefGroup = 806F1E3626B85367007A60E6 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -946,6 +950,25 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
EE58CBCD2B5080C1003F2666 /* XCRemoteSwiftPackageReference "paypalcheckout-ios" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/paypal/paypalcheckout-ios";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.0;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
EE58CBCE2B5080C1003F2666 /* PayPalCheckout */ = {
isa = XCSwiftPackageProductDependency;
package = EE58CBCD2B5080C1003F2666 /* XCRemoteSwiftPackageReference "paypalcheckout-ios" */;
productName = PayPalCheckout;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 806F1E2D26B85367007A60E6 /* Project object */;
}
11 changes: 2 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,9 @@ This SDK supports:

The PayPal SDK uses a client ID for authentication. This can be found in your [PayPal Developer Dashboard](https://developer.paypal.com/api/rest/#link-getstarted).

## Modules
## Documentation

Each feature module has its own onboarding guide:

- [CardPayments](docs/CardPayments)
- [PaymentButtons](docs/PaymentButtons)
- [PayPal Native Payments](docs/PayPalNativePayments)
- [PayPal Web Payments](docs/PayPalWebPayments)

To accept a certain payment method in your app, you only need to include that payment-specific submodule.
Documentation for the PayPal iOS SDK can be found [here](https://developer.paypal.com/docs/checkout/advanced/ios/).

## Demo

Expand Down
45 changes: 33 additions & 12 deletions Sources/CardPayments/CardClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class CardClient: NSObject {
let result = try await vaultAPI.updateSetupToken(cardVaultRequest: vaultRequest).updateVaultSetupToken

// TODO: handle 3DS contingency with helios link & add unit tests
if let link = result.links.first(where: { $0.rel == "approve" && $0.href.contains("helios") }) {
if let link = result.links.first(where: { $0.rel == "approve" && $0.href.contains("helios") }) {
let url = link.href
print("3DS url \(url)")
} else {
Expand Down Expand Up @@ -74,13 +74,21 @@ public class CardClient: NSObject {
do {
let result = try await checkoutOrdersAPI.confirmPaymentSource(cardRequest: request)

if let url: String = result.links?.first(where: { $0.rel == "payer-action" })?.href {
if result.status == "PAYER_ACTION_REQUIRED",
let url = result.links?.first(where: { $0.rel == "payer-action" })?.href {
guard getQueryStringParameter(url: url, param: "flow") == "3ds",
url.contains("helios"),
let url = URL(string: url) else {
self.notifyFailure(with: CardClientError.threeDSecureURLError)
return
}

analyticsService?.sendEvent("card-payments:3ds:confirm-payment-source:challenge-required")
startThreeDSecureChallenge(url: url, orderId: result.id)
} else {
analyticsService?.sendEvent("card-payments:3ds:confirm-payment-source:succeeded")

let cardResult = CardResult(orderID: result.id, deepLinkURL: nil)
let cardResult = CardResult(orderID: result.id, deepLinkURL: nil, liabilityShift: nil)
notifySuccess(for: cardResult)
}
} catch let error as CoreSDKError {
Expand All @@ -94,18 +102,13 @@ public class CardClient: NSObject {
}

private func startThreeDSecureChallenge(
url: String,
url: URL,
orderId: String
) {
guard let threeDSURL = URL(string: url) else {
self.notifyFailure(with: CardClientError.threeDSecureURLError)
return
}

delegate?.cardThreeDSecureWillLaunch(self)

webAuthenticationSession.start(
url: threeDSURL,
url: url,
context: self,
sessionDidDisplay: { [weak self] didDisplay in
if didDisplay {
Expand All @@ -127,11 +130,29 @@ public class CardClient: NSObject {
}
}

let cardResult = CardResult(orderID: orderId, deepLinkURL: url)
self.notifySuccess(for: cardResult)
guard let url else {
self.notifyFailure(with: CardClientError.missingDeeplinkURLError)
return
}

if self.getQueryStringParameter(url: url.absoluteString, param: "state") != nil
&& self.getQueryStringParameter(url: url.absoluteString, param: "code") != nil {
let liabilityShift = self.getQueryStringParameter(url: url.absoluteString, param: "liability_shift")
let cardResult = CardResult(orderID: orderId, deepLinkURL: url, liabilityShift: liabilityShift)
self.notifySuccess(for: cardResult)
} else if self.getQueryStringParameter(url: url.absoluteString, param: "error") != nil {
self.notifyFailure(with: CardClientError.threeDSVerificationError)
} else {
self.notifyFailure(with: CardClientError.malformedDeeplinkURLError)
}
}
)
}

private func getQueryStringParameter(url: String, param: String) -> String? {
guard let url = URLComponents(string: url) else { return nil }
return url.queryItems?.first { $0.name == param }?.value
}

private func notifySuccess(for result: CardResult) {
analyticsService?.sendEvent("card-payments:3ds:succeeded")
Expand Down
27 changes: 27 additions & 0 deletions Sources/CardPayments/CardClientError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ enum CardClientError {

/// 6. GraphQLClient is unexpectedly nil
case nilGraphQLClientError

/// 7. An error from 3DS verification
case threeDSVerificationError

/// 8. Missing Deeplink URL from 3DS
case missingDeeplinkURLError

/// 9. Malformed Deeplink URL from 3DS
case malformedDeeplinkURLError
}

static let unknownError = CoreSDKError(
Expand Down Expand Up @@ -73,4 +82,22 @@ enum CardClientError {
domain: domain,
errorDescription: "GraphQLClient is unexpectedly nil."
)

static let threeDSVerificationError = CoreSDKError(
code: Code.threeDSVerificationError.rawValue,
domain: domain,
errorDescription: "3DS Verification is returning an error."
)

static let missingDeeplinkURLError = CoreSDKError(
code: Code.missingDeeplinkURLError.rawValue,
domain: domain,
errorDescription: "Missing deeplink URL from 3DS."
)

static let malformedDeeplinkURLError = CoreSDKError(
code: Code.malformedDeeplinkURLError.rawValue,
domain: domain,
errorDescription: "Malformed deeplink URL."
)
}
4 changes: 3 additions & 1 deletion Sources/CardPayments/Models/CardResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ public struct CardResult {
/// The order ID associated with the transaction
public let orderID: String

// TODO: parse contents of this URL once we are clear on values returned from 3ds
/// This is the deep link url returned from 3DS authentication
@_documentation(visibility: private)
public let deepLinkURL: URL?

/// Liability shift value returned from 3DS verification
public let liabilityShift: String?
}
91 changes: 91 additions & 0 deletions UnitTests/CardPaymentsTests/CardClient_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AuthenticationServices
@testable import CardPayments
@testable import TestShared

// swiftlint:disable type_body_length
class CardClient_Tests: XCTestCase {

// MARK: - Helper Properties
Expand Down Expand Up @@ -206,11 +207,14 @@ class CardClient_Tests: XCTestCase {
func testApproveOrder_withThreeDSecure_browserSwitchLaunches_getOrderReturnsSuccess() {
mockCheckoutOrdersAPI.stubConfirmResponse = FakeConfirmPaymentResponse.withValid3DSURL

mockWebAuthSession.cannedResponseURL = .init(string: "sdk.ios.paypal://card/success?state=undefined&code=undefined&liability_shift=POSSIBLE")

let expectation = expectation(description: "approveOrder() completed")

let mockCardDelegate = MockCardDelegate(
success: {_, result in
XCTAssertEqual(result.orderID, "testOrderId")
XCTAssertEqual(result.liabilityShift, "POSSIBLE")
expectation.fulfill()
},
error: { _, error in
Expand Down Expand Up @@ -293,4 +297,91 @@ class CardClient_Tests: XCTestCase {

waitForExpectations(timeout: 10)
}

func testApproveOrder_withThreeDSecure_returns3DSVerificationError() {
mockCheckoutOrdersAPI.stubConfirmResponse = FakeConfirmPaymentResponse.withValid3DSURL

mockWebAuthSession.cannedResponseURL = .init(string: "sdk.ios.paypal://card/success?error=error&error_description=error&liability_shift=NO")

let expectation = expectation(description: "approveOrder() completed")

let mockCardDelegate = MockCardDelegate(
success: {_, _ in
XCTFail("Invoked success() callback. Should invoke error().")
expectation.fulfill()
},
error: { _, error in
XCTAssertEqual(error.domain, CardClientError.domain)
XCTAssertEqual(error.code, CardClientError.Code.threeDSVerificationError.rawValue)
XCTAssertEqual(error.errorDescription, "3DS Verification is returning an error.")
expectation.fulfill()
},
cancel: { _ in
XCTFail("Invoked cancel() callback. Should invoke error().")
expectation.fulfill()
},
threeDSWillLaunch: { _ in XCTAssert(true) },
threeDSLaunched: { _ in XCTAssert(true) })

sut.delegate = mockCardDelegate
sut.approveOrder(request: cardRequest)

waitForExpectations(timeout: 10)
}

func testApproveOrder_withThreeDSecure_returnsMissingDeeplinkError() {
mockCheckoutOrdersAPI.stubConfirmResponse = FakeConfirmPaymentResponse.withValid3DSURL

mockWebAuthSession.cannedResponseURL = nil

let expectation = expectation(description: "approveOrder() completed")

let mockCardDelegate = MockCardDelegate(
success: {_, _ in
XCTFail("Invoked success() callback. Should invoke error().")
expectation.fulfill()
},
error: { _, error in
XCTAssertEqual(error.domain, CardClientError.domain)
XCTAssertEqual(error.code, CardClientError.Code.missingDeeplinkURLError.rawValue)
XCTAssertEqual(error.errorDescription, "Missing deeplink URL from 3DS.")
expectation.fulfill()
},
cancel: { _ in XCTFail("Invoked cancel() callback. Should invoke success().") },
threeDSWillLaunch: { _ -> Void in XCTAssert(true) },
threeDSLaunched: { _ -> Void in XCTAssert(true) })

sut.delegate = mockCardDelegate
sut.approveOrder(request: cardRequest)

waitForExpectations(timeout: 10)
}

func testApproveOrder_withThreeDSecure_returnsMalformedDeeplinkError() {
mockCheckoutOrdersAPI.stubConfirmResponse = FakeConfirmPaymentResponse.withValid3DSURL

mockWebAuthSession.cannedResponseURL = .init(string: "https://fakeURL")

let expectation = expectation(description: "approveOrder() completed")

let mockCardDelegate = MockCardDelegate(
success: {_, _ in
XCTFail("Invoked success() callback. Should invoke error().")
expectation.fulfill()
},
error: { _, error in
XCTAssertEqual(error.domain, CardClientError.domain)
XCTAssertEqual(error.code, CardClientError.Code.malformedDeeplinkURLError.rawValue)
XCTAssertEqual(error.errorDescription, "Malformed deeplink URL.")
expectation.fulfill()
},
cancel: { _ in XCTFail("Invoked cancel() callback. Should invoke success().") },
threeDSWillLaunch: { _ -> Void in XCTAssert(true) },
threeDSLaunched: { _ -> Void in XCTAssert(true) })

sut.delegate = mockCardDelegate
sut.approveOrder(request: cardRequest)

waitForExpectations(timeout: 10)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ enum FakeConfirmPaymentResponse {

static let withValid3DSURL = ConfirmPaymentSourceResponse(
id: "testOrderId",
status: "APPROVED",
status: "PAYER_ACTION_REQUIRED",
paymentSource: PaymentSource(
card: PaymentSource.Card(
lastFourDigits: "7321",
Expand All @@ -15,7 +15,7 @@ enum FakeConfirmPaymentResponse {
),
links: [
Link(
href: "https://fakeURL?PayerID=98765",
href: "https://fakeURL/helios?flow=3ds",
rel: "payer-action",
method: nil
)
Expand All @@ -24,7 +24,7 @@ enum FakeConfirmPaymentResponse {

static let withInvalid3DSURL = ConfirmPaymentSourceResponse(
id: "testOrderId",
status: "APPROVED",
status: "PAYER_ACTION_REQUIRED",
paymentSource: PaymentSource(
card: PaymentSource.Card(
lastFourDigits: "7321",
Expand All @@ -35,7 +35,7 @@ enum FakeConfirmPaymentResponse {
),
links: [
Link(
href: "",
href: "https://sandbox.paypal.com",
rel: "payer-action",
method: nil
)
Expand Down
Loading

0 comments on commit 50a268d

Please sign in to comment.