Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@

# PayPal iOS SDK Release Notes

## unreleased
* PaymentButtons
* Add `CardButton` to be used for PayPal guest checkout using card funding option
* PaymentButtons
* Add `card` option in `PayPalWebChecoutFundingSource` for web card field option

## 2.0.0 (2025-03-18)
* Breaking Changes
Expand Down
13 changes: 12 additions & 1 deletion Demo/Demo/Extensions/PaymentButtonEnums+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ extension PayPalCreditButton.Color {
}
}

extension CardButton.Color {

public static var allCases: [CardButton.Color] {
[.black, .white]
}

static func allCasesAsString() -> [String] {
Self.allCases.map { $0.rawValue }
}
}

extension PaymentButtonEdges {

public static var allCases: [PaymentButtonEdges] {
Expand All @@ -58,7 +69,7 @@ extension PaymentButtonSize {
extension PaymentButtonFundingSource {

public static var allCases: [PaymentButtonFundingSource] {
[.payPal, .payLater, .credit]
[.payPal, .payLater, .credit, .card]
}

static func allCasesAsString() -> [String] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct PayPalWebButtonsView: View {
Text("PayPal").tag(PayPalWebCheckoutFundingSource.paypal)
Text("PayPal Credit").tag(PayPalWebCheckoutFundingSource.paypalCredit)
Text("Pay Later").tag(PayPalWebCheckoutFundingSource.paylater)
Text("Card").tag(PayPalWebCheckoutFundingSource.card)
}
.pickerStyle(SegmentedPickerStyle())
ZStack {
Expand All @@ -38,6 +39,10 @@ struct PayPalWebButtonsView: View {
PayPalButton.Representable(color: .blue, size: .full) {
payPalWebViewModel.paymentButtonTapped(funding: .paypal)
}
case .card:
CardButton.Representable(color: .black, size: .full) {
payPalWebViewModel.paymentButtonTapped(funding: .card)
}
}
if payPalWebViewModel.state.approveResultResponse == .loading &&
payPalWebViewModel.checkoutResult == nil &&
Expand Down
11 changes: 11 additions & 0 deletions Demo/Demo/SwiftUIComponents/SwiftUIPaymentButtonDemo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ struct SwiftUIPaymentButtonDemo: View {
size: selectedSize
)
.id(buttonID)

case .card:
CardButton.Representable(
color: CardButton.Color.allCases[colorsIndex],
edges: selectedEdge,
size: selectedSize
)
.id(buttonID)
}
}
.padding()
Expand All @@ -136,6 +144,9 @@ struct SwiftUIPaymentButtonDemo: View {

case .credit:
return PayPalCreditButton.Color.allCasesAsString()

case .card:
return CardButton.Color.allCasesAsString()
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions PayPal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
3B783DC32B7A69C2004623DB /* FakeUpdateSetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B783DC22B7A69C2004623DB /* FakeUpdateSetupTokenResponse.swift */; };
3B79E4F72A8503CA00C01D06 /* UpdateVaultVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */; };
3B80D50C2A27979000D2EAC4 /* FailingJSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */; };
3B8F84842D92F95500A151AC /* CardButton_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8F84832D92F94600A151AC /* CardButton_Tests.swift */; };
3B8F848C2D93380B00A151AC /* TestShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80E743F8270E40CE00BACECA /* TestShared.framework */; };
3BD014C82D8B4D60005565FE /* CardButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD014C72D8B4D60005565FE /* CardButton.swift */; };
3BD82DBB2A835AF900CBE764 /* UpdateSetupTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */; };
3BDB34942A80CE6E008100D7 /* CardVaultRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDB34932A80CE6E008100D7 /* CardVaultRequest.swift */; };
3BE738682B9A66D800598F05 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3BE738622B9A482800598F05 /* PrivacyInfo.xcprivacy */; };
Expand Down Expand Up @@ -199,8 +201,10 @@
3B783DC22B7A69C2004623DB /* FakeUpdateSetupTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeUpdateSetupTokenResponse.swift; sourceTree = "<group>"; };
3B79E4F62A8503C900C01D06 /* UpdateVaultVariables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateVaultVariables.swift; sourceTree = "<group>"; };
3B80D50B2A27979000D2EAC4 /* FailingJSONEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailingJSONEncoder.swift; sourceTree = "<group>"; };
3B8F84832D92F94600A151AC /* CardButton_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardButton_Tests.swift; sourceTree = "<group>"; };
3B8F84852D93318D00A151AC /* UpdateClientConfigAP_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateClientConfigAP_Tests.swift; sourceTree = "<group>"; };
3B8F84912D933FAE00A151AC /* MockUpdateClientConfigAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUpdateClientConfigAPI.swift; sourceTree = "<group>"; };
3BD014C72D8B4D60005565FE /* CardButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardButton.swift; sourceTree = "<group>"; };
3BD82DBA2A835AF900CBE764 /* UpdateSetupTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSetupTokenResponse.swift; sourceTree = "<group>"; };
3BDB34932A80CE6E008100D7 /* CardVaultRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVaultRequest.swift; sourceTree = "<group>"; };
3BE738622B9A482800598F05 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
Expand Down Expand Up @@ -626,6 +630,7 @@
isa = PBXGroup;
children = (
BE00B79D2742F9F000758C63 /* Assets.xcassets */,
3BD014C72D8B4D60005565FE /* CardButton.swift */,
BEDB7FE32788AB8E00CEA554 /* Coordinator.swift */,
BE00B7AA2743FD9F00758C63 /* PaymentButton.swift */,
BE9F36DE274859A000AFC7DA /* PaymentButtonColor.swift */,
Expand All @@ -647,6 +652,7 @@
BCFAC72227ED05D900C3AF00 /* PaymentButtonsTests */ = {
isa = PBXGroup;
children = (
3B8F84832D92F94600A151AC /* CardButton_Tests.swift */,
BEF3FF1627AC5DF3006B4B69 /* Coordinator_Tests.swift */,
BE00B7AC27444FE900758C63 /* PayPalButton_Tests.swift */,
BE9F36E3275520E700AFC7DA /* PayPalCreditButton_Tests.swift */,
Expand Down Expand Up @@ -1327,6 +1333,7 @@
BCFAC71F27ED04C800C3AF00 /* Coordinator.swift in Sources */,
BCFAC71C27ED04C000C3AF00 /* PaymentButton.swift in Sources */,
CB1A47F22820AFED00BD8184 /* PayPalPayLaterButton.swift in Sources */,
3BD014C82D8B4D60005565FE /* CardButton.swift in Sources */,
CB1A47F42820BA5D00BD8184 /* PaymentButtonEdges.swift in Sources */,
BCFAC71A27ED04AC00C3AF00 /* PaymentButtonColor.swift in Sources */,
);
Expand All @@ -1337,6 +1344,7 @@
buildActionMask = 0;
files = (
BCFAC70E27ED043100C3AF00 /* PayPalButton_Tests.swift in Sources */,
3B8F84842D92F95500A151AC /* CardButton_Tests.swift in Sources */,
BCFAC70F27ED043100C3AF00 /* Coordinator_Tests.swift in Sources */,
CB22C018291049500097E592 /* PayPalPayLaterButton_Tests.swift in Sources */,
BCFAC71327ED043100C3AF00 /* PayPalCreditButton_Tests.swift in Sources */,
Expand Down
10 changes: 5 additions & 5 deletions Sources/PayPalWebPayments/PayPalWebCheckoutClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public class PayPalWebCheckoutClient: NSObject {
public func vault(_ vaultRequest: PayPalVaultRequest, completion: @escaping (Result<PayPalVaultResult, CoreSDKError>) -> Void) {
analyticsService = AnalyticsService(coreConfig: config, setupToken: vaultRequest.setupTokenID)
analyticsService?.sendEvent("paypal-web-payments:vault-wo-purchase:started")

Task {
do {
_ = try await clientConfigAPI.updateClientConfig(
Expand All @@ -169,12 +169,12 @@ public class PayPalWebCheckoutClient: NSObject {
var vaultURLComponents = URLComponents(url: config.environment.paypalVaultCheckoutURL, resolvingAgainstBaseURL: false)
let queryItems = [URLQueryItem(name: "approval_session_id", value: vaultRequest.setupTokenID)]
vaultURLComponents?.queryItems = queryItems

guard let vaultCheckoutURL = vaultURLComponents?.url else {
notifyVaultFailure(with: PayPalError.payPalURLError, completion: completion)
return
}

webAuthenticationSession.start(
url: vaultCheckoutURL,
context: self,
Expand All @@ -199,7 +199,7 @@ public class PayPalWebCheckoutClient: NSObject {
return
}
}

if let url = url {
guard let tokenID = self.getQueryStringParameter(url: url.absoluteString, param: "approval_token_id"),
let approvalSessionID = self.getQueryStringParameter(url: url.absoluteString, param: "approval_session_id"),
Expand All @@ -208,7 +208,7 @@ public class PayPalWebCheckoutClient: NSObject {
self.notifyVaultFailure(with: PayPalError.payPalVaultResponseError, completion: completion)
return
}

let paypalVaultResult = PayPalVaultResult(tokenID: tokenID, approvalSessionID: approvalSessionID)
self.notifyVaultSuccess(for: paypalVaultResult, completion: completion)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ public enum PayPalWebCheckoutFundingSource: String {

/// PayPal will launch the web checkout for a one-time PayPal Checkout flow
case paypal = "paypal"

/// PayPal will launch the web guest checkout for card funding source
case card = "card"
}
122 changes: 122 additions & 0 deletions Sources/PaymentButtons/CardButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import UIKit
import SwiftUI

/// Configuration for Card button
public final class CardButton: PaymentButton {

/// Available colors for CardButton.
public enum Color: String {
case white
case black

var color: PaymentButtonColor {
PaymentButtonColor(rawValue: rawValue) ?? .black
}
}

/// Available labels for CardButton.
public enum Label: String {
/// Display "Debit or Credit Card" on the left side of the button's logo
case card = "Debit or Credit Card"

var label: PaymentButtonLabel? {
PaymentButtonLabel(rawValue: rawValue)
}
}

/// Initialize a PayPalButton
/// - Parameters:
/// - insets: Edge insets of the button, defining the spacing of the button's edges relative to its content.
/// - color: Color of the button. Default to black if not provided.
/// - edges: Edges of the button. Default to softEdges if not provided.
/// - size: Size of the button. Default to collapsed if not provided.
/// - label: Label displayed next to the button's logo. Default to no label.
public convenience init(
insets: NSDirectionalEdgeInsets? = nil,
color: Color = .black,
edges: PaymentButtonEdges = .softEdges,
size: PaymentButtonSize = .collapsed,
label: Label = .card,
cardImage: UIImage? = nil
) {
self.init(
fundingSource: .card,
color: color.color,
edges: edges,
size: size,
insets: insets,
label: label.label
)
}

deinit {}
}

public extension CardButton {

/// CardlButton for SwiftUI
struct Representable: UIViewRepresentable {

private var action: () -> Void = { }

private let button: CardButton

/// Initialize a CardButton
/// - Parameters:
/// - insets: Edge insets of the button, defining the spacing of the button's edges relative to its content.
/// - color: Color of the button. Default to gold if not provided.
/// - edges: Edges of the button. Default to softEdges if not provided.
/// - size: Size of the button. Default to expanded if not provided.
/// - label: Label displayed next to the button's logo. Default to no label.
public init(
insets: NSDirectionalEdgeInsets? = nil,
color: CardButton.Color = .black,
edges: PaymentButtonEdges = .softEdges,
size: PaymentButtonSize = .expanded,
label: CardButton.Label = .card,
_ action: @escaping () -> Void = { }
) {
button = CardButton(
fundingSource: .card,
color: color.color,
edges: edges,
size: size,
insets: insets,
label: label.label
)
self.action = action
}

// MARK: - UIViewRepresentable methods
// TODO: Make unit test for UIVRepresentable methods: https://engineering.paypalcorp.com/jira/browse/DTNOR-623

public func makeCoordinator() -> Coordinator {
Coordinator(action: action)
}

public func makeUIView(context: Context) -> PaymentButton {
button.addTarget(context.coordinator, action: #selector(Coordinator.onAction(_:)), for: .touchUpInside)
return button
}

public func updateUIView(_ uiView: PaymentButton, context: Context) {
context.coordinator.action = action
}
}
}

// MARK: PayPalButton Preview

struct CardButtonView: View {

var body: some View {
CardButton.Representable()
}
}

struct CardButtonView_Preview: PreviewProvider {

static var previews: some View {
CardButtonView()
}
}
6 changes: 6 additions & 0 deletions Sources/PaymentButtons/PaymentButton+ImageAsset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ extension PaymentButton {

var imageAccessibilityLabel: String {
// NEXT_MAJOR_VERSION: - To be replaced with translation strings.
if fundingSource == .card {
return "Credit Card"
}

return fileName.starts(with: "credit") ? "PayPal Credit" : "PayPal"
}
Expand All @@ -32,6 +35,9 @@ extension PaymentButton {

case .credit:
imageAssetString += "credit_"

case .card:
imageAssetString += "card_"
}

switch color {
Expand Down
1 change: 1 addition & 0 deletions Sources/PaymentButtons/PaymentButtonFundingSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public enum PaymentButtonFundingSource: String {
case payPal = "PayPal"
case payLater = "Pay Later"
case credit = "Credit"
case card = "Card"
}
5 changes: 4 additions & 1 deletion Sources/PaymentButtons/PaymentButtonLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ public enum PaymentButtonLabel: String {
/// Add "Pay later" to the right of button's logo, only used for PayPalPayLaterButton
case payLater = "Pay Later"

/// Add "Debit or Credit Card" to the right of button's logo, only used for CardButton
case card = "Debit or Credit Card"

enum Position {
case prefix
case suffix
}

var position: Position {
switch self {
case .checkout, .buyNow, .payLater:
case .card, .checkout, .buyNow, .payLater:
return .suffix

case .payWith:
Expand Down
38 changes: 38 additions & 0 deletions UnitTests/PaymentButtonsTests/CardButton_Tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import XCTest
@testable import PaymentButtons

class CardButton_Tests: XCTestCase {

// MARK: - CardButton for UIKit

func testInit_whenCardButtonCreated_hasUIImageFromAssets() {
let sut = CardButton()
XCTAssertEqual(sut.imageView?.image, UIImage(named: "card-black"))
}

func testInit_whenPayPalButtonCreated_hasDefaultUIValuess() {
let sut = CardButton()
XCTAssertEqual(sut.edges, PaymentButtonEdges.softEdges)
XCTAssertEqual(sut.size, PaymentButtonSize.collapsed)
XCTAssertEqual(sut.color, PaymentButtonColor.black)
XCTAssertEqual(sut.label, PaymentButtonLabel.card)
XCTAssertNil(sut.insets)
}

// MARK: - PayPalButton.Representable for SwiftUI

func testMakeCoordinator_whenOnActionIsCalled_executesActionPassedInInitializer() {
let expectation = expectation(description: "Action is called")
let sut = CardButton.Representable {
expectation.fulfill()
}
let coordinator = sut.makeCoordinator()

coordinator.onAction(self)
waitForExpectations(timeout: 1) { error in
if error != nil {
XCTFail("Action passed in CardButton.Representable was never called.")
}
}
}
}