diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 9e53a88650..4b69393ae3 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -90,6 +90,7 @@ 6298A1992B91010600E46EDF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 6298A1982B91010600E46EDF /* PrivacyInfo.xcprivacy */; }; 62A659A42B98CB23008DFD67 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62A659A32B98CB23008DFD67 /* PrivacyInfo.xcprivacy */; }; 62A746412B9255AC003D32FF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62A746402B9255AC003D32FF /* PrivacyInfo.xcprivacy */; }; + 62B811882CC002470024A688 /* BTPayPalPhoneNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B811872CC002470024A688 /* BTPayPalPhoneNumber.swift */; }; 62D5EC502B9F6E9D00D09C5D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62D5EC4F2B9F6E9D00D09C5D /* PrivacyInfo.xcprivacy */; }; 62DE8FBF2B9656BF00F08F53 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62DE8FBE2B9656BF00F08F53 /* PrivacyInfo.xcprivacy */; }; 62EA90492B63071800DD79BC /* BTEligiblePaymentMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */; }; @@ -829,6 +830,7 @@ 6298A1982B91010600E46EDF /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 62A659A32B98CB23008DFD67 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 62A746402B9255AC003D32FF /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 62B811872CC002470024A688 /* BTPayPalPhoneNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalPhoneNumber.swift; sourceTree = ""; }; 62D5EC4F2B9F6E9D00D09C5D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 62DE8FBE2B9656BF00F08F53 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentMethods.swift; sourceTree = ""; }; @@ -1401,6 +1403,7 @@ BE349110294B77E100D2CF68 /* BTPayPalVaultRequest.swift */, 62A659A32B98CB23008DFD67 /* PrivacyInfo.xcprivacy */, 807D22F32C29ADA8009FFEA4 /* RecurringBillingMetadata */, + 62B811872CC002470024A688 /* BTPayPalPhoneNumber.swift */, ); path = BraintreePayPal; sourceTree = ""; @@ -3285,6 +3288,7 @@ 57544F582952298900DEB7B0 /* BTPayPalAccountNonce.swift in Sources */, 8014221C2BAE935B009F9999 /* BTPayPalApprovalURLParser.swift in Sources */, BE349111294B77E100D2CF68 /* BTPayPalVaultRequest.swift in Sources */, + 62B811882CC002470024A688 /* BTPayPalPhoneNumber.swift in Sources */, 807D22F52C29ADE2009FFEA4 /* BTPayPalRecurringBillingPlanType.swift in Sources */, 57544820294A2EBE00DEB7B0 /* BTPayPalCreditFinancing.swift in Sources */, 807D22EE2C29A918009FFEA4 /* BTPayPalRecurringBillingDetails.swift in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c095de4b..4d21b71a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Braintree iOS SDK Release Notes +## unreleased +* BraintreePayPal + * Add `BTPayPalRequest.userPhoneNumber` optional property + ## 6.24.0 (2024-10-15) * BraintreePayPal * Add `BTPayPalRecurringBillingDetails` and `BTPayPalRecurringBillingPlanType` opt-in request objects. Including these details will provide transparency to users on their billing schedule, dates, and amounts, as well as launch a modernized checkout UI. diff --git a/Demo/Application/Features/PayPalWebCheckoutViewController.swift b/Demo/Application/Features/PayPalWebCheckoutViewController.swift index dd38980ada..453241f5b0 100644 --- a/Demo/Application/Features/PayPalWebCheckoutViewController.swift +++ b/Demo/Application/Features/PayPalWebCheckoutViewController.swift @@ -24,6 +24,34 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { return textField }() + lazy var countryCodeLabel: UILabel = { + let label = UILabel() + label.text = "Country Code:" + return label + }() + + lazy var countryCodeTextField: UITextField = { + let textField = UITextField() + textField.placeholder = "1" + textField.backgroundColor = .systemBackground + textField.keyboardType = .phonePad + return textField + }() + + lazy var nationalNumberLabel: UILabel = { + let label = UILabel() + label.text = "National Number:" + return label + }() + + lazy var nationalNumberTextField: UITextField = { + let textField = UITextField() + textField.placeholder = "000-000-000" + textField.backgroundColor = .systemBackground + textField.keyboardType = .phonePad + return textField + }() + lazy var payLaterToggleLabel: UILabel = { let label = UILabel() label.text = "Offer Pay Later" @@ -74,6 +102,8 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { let stackView = UIStackView(arrangedSubviews: [ UIStackView(arrangedSubviews: [emailLabel, emailTextField]), + UIStackView(arrangedSubviews: [countryCodeLabel, countryCodeTextField]), + UIStackView(arrangedSubviews: [nationalNumberLabel, nationalNumberTextField]), oneTimeCheckoutStackView, vaultStackView ]) @@ -104,6 +134,10 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { let request = BTPayPalCheckoutRequest(amount: "5.00") request.userAuthenticationEmail = emailTextField.text + request.userPhoneNumber = BTPayPalPhoneNumber( + countryCode: countryCodeTextField.text ?? "", + nationalNumber: nationalNumberTextField.text ?? "" + ) let lineItem = BTPayPalLineItem(quantity: "1", unitAmount: "5.00", name: "item one 1234567", kind: .debit) lineItem.upcCode = "123456789" @@ -135,6 +169,10 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { var request = BTPayPalVaultRequest() request.userAuthenticationEmail = emailTextField.text + request.userPhoneNumber = BTPayPalPhoneNumber( + countryCode: countryCodeTextField.text ?? "", + nationalNumber: nationalNumberTextField.text ?? "" + ) if rbaDataToggle.isOn { let billingPricing = BTPayPalBillingPricing( diff --git a/Sources/BraintreeCore/Encodable+Dictionary.swift b/Sources/BraintreeCore/Encodable+Dictionary.swift index e3a969d2b3..3543cfed01 100644 --- a/Sources/BraintreeCore/Encodable+Dictionary.swift +++ b/Sources/BraintreeCore/Encodable+Dictionary.swift @@ -1,7 +1,7 @@ import Foundation // TODO: - To be removed once entire SDK is formatting POST bodies using Encodable -extension Encodable { +public extension Encodable { /// Converts to dictionary `[String: Any]` type. /// diff --git a/Sources/BraintreePayPal/BTPayPalPhoneNumber.swift b/Sources/BraintreePayPal/BTPayPalPhoneNumber.swift new file mode 100644 index 0000000000..95a555e437 --- /dev/null +++ b/Sources/BraintreePayPal/BTPayPalPhoneNumber.swift @@ -0,0 +1,21 @@ +import Foundation + +public struct BTPayPalPhoneNumber: Encodable { + + private let countryCode: String + private let nationalNumber: String + + private enum CodingKeys: String, CodingKey { + case countryCode = "country_code" + case nationalNumber = "national_number" + } + + /// Intialize a `BTPayPalPhoneNumber` + /// - Parameters: + /// - countryCode: The international country code for the shopper's phone number i.e. "1" for US + /// - nationalNumber: The national segment of the shopper's phone number + public init(countryCode: String, nationalNumber: String) { + self.countryCode = countryCode + self.nationalNumber = nationalNumber + } +} diff --git a/Sources/BraintreePayPal/BTPayPalRequest.swift b/Sources/BraintreePayPal/BTPayPalRequest.swift index 9027c09abf..c7d733d977 100644 --- a/Sources/BraintreePayPal/BTPayPalRequest.swift +++ b/Sources/BraintreePayPal/BTPayPalRequest.swift @@ -96,6 +96,10 @@ import BraintreeCore /// :nodoc: Exposed publicly for use by PayPal Native Checkout module. This property is not covered by semantic versioning. @_documentation(visibility: private) public var paymentType: BTPayPalPaymentType + + /// Optional: A user's phone number to initiate a quicker authentication flow in the scenario where the user has a PayPal account + /// identified with the same phone number. + public var userPhoneNumber: BTPayPalPhoneNumber? // MARK: - Static Properties @@ -115,7 +119,8 @@ import BraintreeCore merchantAccountID: String? = nil, lineItems: [BTPayPalLineItem]? = nil, billingAgreementDescription: String? = nil, - riskCorrelationId: String? = nil + riskCorrelationId: String? = nil, + userPhoneNumber: BTPayPalPhoneNumber? = nil ) { self.hermesPath = hermesPath self.paymentType = paymentType @@ -129,6 +134,7 @@ import BraintreeCore self.lineItems = lineItems self.billingAgreementDescription = billingAgreementDescription self.riskCorrelationID = riskCorrelationId + self.userPhoneNumber = userPhoneNumber } // MARK: Public Methods @@ -169,6 +175,10 @@ import BraintreeCore let lineItemsArray = lineItems.compactMap { $0.requestParameters() } parameters["line_items"] = lineItemsArray } + + if let userPhoneNumberDict = try? userPhoneNumber?.toDictionary() { + parameters["phone_number"] = userPhoneNumberDict + } parameters["return_url"] = BTCoreConstants.callbackURLScheme + "://\(BTPayPalRequest.callbackURLHostAndPath)success" parameters["cancel_url"] = BTCoreConstants.callbackURLScheme + "://\(BTPayPalRequest.callbackURLHostAndPath)cancel" diff --git a/Sources/BraintreePayPal/BTPayPalVaultRequest.swift b/Sources/BraintreePayPal/BTPayPalVaultRequest.swift index 96c7eb569a..a39cee1cd4 100644 --- a/Sources/BraintreePayPal/BTPayPalVaultRequest.swift +++ b/Sources/BraintreePayPal/BTPayPalVaultRequest.swift @@ -71,7 +71,7 @@ import BraintreeCore if let userAuthenticationEmail { baseParameters["payer_email"] = userAuthenticationEmail } - + if let universalLink, enablePayPalAppSwitch, isPayPalAppInstalled { let appSwitchParameters: [String: Any] = [ "launch_paypal_app": enablePayPalAppSwitch, diff --git a/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift index eb59465928..e082e120f7 100644 --- a/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift +++ b/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift @@ -87,6 +87,7 @@ class BTPayPalCheckoutRequest_Tests: XCTestCase { request.billingAgreementDescription = "description" request.userAction = .payNow request.userAuthenticationEmail = "fake@email.com" + request.userPhoneNumber = BTPayPalPhoneNumber(countryCode: "1", nationalNumber: "4087463271") let shippingAddress = BTPostalAddress() shippingAddress.streetAddress = "123 Main" @@ -114,6 +115,13 @@ class BTPayPalCheckoutRequest_Tests: XCTestCase { XCTAssertEqual(parameters["recipient_name"] as? String, "Recipient") XCTAssertEqual(parameters["payer_email"] as? String, "fake@email.com") XCTAssertEqual(parameters["request_billing_agreement"] as? Bool, true) + + guard let userPhoneNumberDetails = parameters["phone_number"] as? [String: String] else { + XCTFail() + return + } + XCTAssertEqual(userPhoneNumberDetails["country_code"], "1") + XCTAssertEqual(userPhoneNumberDetails["national_number"], "4087463271") guard let billingAgreementDetails = parameters["billing_agreement_details"] as? [String : String] else { XCTFail() diff --git a/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift index a7959bd4dd..645bbbd4db 100644 --- a/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift +++ b/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift @@ -49,6 +49,7 @@ class BTPayPalVaultRequest_Tests: XCTestCase { request.isShippingAddressEditable = true request.offerCredit = true request.userAuthenticationEmail = "fake@email.com" + request.userPhoneNumber = BTPayPalPhoneNumber(countryCode: "1", nationalNumber: "4087463271") let parameters = request.parameters(with: configuration) @@ -60,6 +61,13 @@ class BTPayPalVaultRequest_Tests: XCTestCase { XCTAssertNil(parameters["os_type"]) XCTAssertNil(parameters["merchant_app_return_url"]) + guard let userPhoneNumberDetails = parameters["phone_number"] as? [String: String] else { + XCTFail() + return + } + XCTAssertEqual(userPhoneNumberDetails["country_code"], "1") + XCTAssertEqual(userPhoneNumberDetails["national_number"], "4087463271") + guard let shippingParams = parameters["shipping_address"] as? [String:String] else { XCTFail(); return } XCTAssertEqual(shippingParams["line1"], "123 Main")