From 805fad88dc81dd9ee594625476bc684f88fad4a0 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:47:04 -0600 Subject: [PATCH 01/57] Add `BTShopperInsights` public API shell (#1142) - Add public API shell for the Payment Insights feature - `BTShopperInsightsClient` - `BTShopperInsightsResult` - `BTShopperInsightsRequest` - Add template unit test file for ` BTShopperInsightsClient_Tests` --- Braintree.xcodeproj/project.pbxproj | 16 +++++++++++ .../BTShopperInsightsClient.swift | 27 ++++++++++++++++++ .../BTShopperInsightsRequest.swift | 28 +++++++++++++++++++ .../BTShopperInsightsResult.swift | 12 ++++++++ .../BTShopperInsightsClient_Tests.swift | 27 ++++++++++++++++++ 5 files changed, 110 insertions(+) create mode 100644 Sources/BraintreeCore/BTShopperInsightsClient.swift create mode 100644 Sources/BraintreeCore/BTShopperInsightsRequest.swift create mode 100644 Sources/BraintreeCore/BTShopperInsightsResult.swift create mode 100644 UnitTests/BraintreeCoreTests/BTShopperInsightsClient_Tests.swift diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index b48912fe5b..05189c9654 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -86,6 +86,10 @@ 8053F05929FB2F700076F988 /* URL+IsPayPal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8053F05829FB2F700076F988 /* URL+IsPayPal.swift */; }; 80581A8C25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581A8B25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift */; }; 80581B1D2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */; }; + 8064F38F2B1E492F0059C4CB /* BTShopperInsightsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */; }; + 8064F3912B1E49E10059C4CB /* BTShopperInsightsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */; }; + 8064F3952B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */; }; + 8064F3972B1E63800059C4CB /* BTShopperInsightsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */; }; 80BA64AC29D788E000E15264 /* BTLocalPaymentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64AB29D788E000E15264 /* BTLocalPaymentResult.swift */; }; 80BA64B229D7937E00E15264 /* BTLocalPaymentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64B129D7937E00E15264 /* BTLocalPaymentRequest.swift */; }; 80BA64B429D795D000E15264 /* BTLocalPaymentRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64B329D795D000E15264 /* BTLocalPaymentRequestDelegate.swift */; }; @@ -699,6 +703,10 @@ 80581A8B25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BTConfiguration+ThreeDSecure_Tests.swift"; sourceTree = ""; }; 80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTGraphQLHTTP_SSLPinning_IntegrationTests.swift; sourceTree = ""; }; 805FD35B2331780F0000B514 /* BTPostalAddress_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPostalAddress_Tests.swift; sourceTree = ""; }; + 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsClient.swift; sourceTree = ""; }; + 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsResult.swift; sourceTree = ""; }; + 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsClient_Tests.swift; sourceTree = ""; }; + 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsRequest.swift; sourceTree = ""; }; 80A1EE3D2236AAC600F6218B /* BTThreeDSecureAdditionalInformation_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAdditionalInformation_Tests.swift; sourceTree = ""; }; 80B6190C28C9535F00FB5022 /* PayPalCheckout.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = PayPalCheckout.xcframework; path = Frameworks/XCFrameworks/PayPalCheckout.xcframework; sourceTree = ""; }; 80BA64AB29D788E000E15264 /* BTLocalPaymentResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTLocalPaymentResult.swift; sourceTree = ""; }; @@ -1184,6 +1192,9 @@ 5708E0A728809BC6007946B9 /* BTJSONError.swift */, 570B93D82853C6180041BAFE /* BTLogLevel.swift */, 570B93D62853C29A0041BAFE /* BTLogLevelDescription.swift */, + 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */, + 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */, + 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */, BE9FB82A2898324C00D6FE2F /* BTPaymentMethodNonce.swift */, BE9FB82C28984ADE00D6FE2F /* BTPaymentMethodNonceParser.swift */, BE63A3A6288F3026001936DA /* BTPostalAddress.swift */, @@ -1657,6 +1668,7 @@ BEBC6F272937BD1F004E25A0 /* BTGraphQLHTTP_Tests.swift */, BE54C0322912B68E009C6CEE /* BTHTTP_Tests.swift */, 16CD2E9E1B4077FC00E68495 /* BTJSON_Tests.swift */, + 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */, 4149C91C1BA218830090665E /* BTPaymentMethodNonceParser_Tests.swift */, 805FD35B2331780F0000B514 /* BTPostalAddress_Tests.swift */, A7E93E571B601EE900957223 /* BTURLUtils_Tests.swift */, @@ -2735,12 +2747,14 @@ BEC3F11328A4401E0074DF0F /* BTHTTPError.swift in Sources */, 574891EB286F7E4F0020DA36 /* BTClientMetadataIntegration.swift in Sources */, BE698EA428AD2C10001D9B10 /* BTCoreConstants.swift in Sources */, + 8064F3912B1E49E10059C4CB /* BTShopperInsightsResult.swift in Sources */, BE24C67528E7491E0067B11A /* BTAPIClientAuthorization.swift in Sources */, BE32ACBC2907744400A61FED /* BTCardNetwork.swift in Sources */, 570B93D92853C6180041BAFE /* BTLogLevel.swift in Sources */, 579DAEC5286E04A700FCE87F /* BTAppContextSwitcher.swift in Sources */, 570B93D72853C29A0041BAFE /* BTLogLevelDescription.swift in Sources */, 57CBBCE828B033760037F4EE /* BTGraphQLHTTP.swift in Sources */, + 8064F38F2B1E492F0059C4CB /* BTShopperInsightsClient.swift in Sources */, BE63A3A7288F3026001936DA /* BTPostalAddress.swift in Sources */, BE2F98D028A2BCCD008EF189 /* BTConfiguration.swift in Sources */, BED00CB228A57AD400D74AEC /* BTClientTokenError.swift in Sources */, @@ -2753,6 +2767,7 @@ BE698EA028AA8DCB001D9B10 /* BTHTTP.swift in Sources */, BC17F9B428D23C5C004B18CC /* BTGraphQLMultiErrorNode.swift in Sources */, 5708E0A828809BC6007946B9 /* BTJSONError.swift in Sources */, + 8064F3972B1E63800059C4CB /* BTShopperInsightsRequest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3035,6 +3050,7 @@ A908436324FD82C0004134CA /* BTBinData_Tests.swift in Sources */, A908436224FD82A9004134CA /* BTAppContextSwitcher_Tests.swift in Sources */, BEBC6F282937BD1F004E25A0 /* BTGraphQLHTTP_Tests.swift in Sources */, + 8064F3952B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift in Sources */, BE54C0352912B6BC009C6CEE /* BTHTTPTestProtocol.swift in Sources */, BEDA91A028EDDE64007441D9 /* FakeAnalyticsService.swift in Sources */, ); diff --git a/Sources/BraintreeCore/BTShopperInsightsClient.swift b/Sources/BraintreeCore/BTShopperInsightsClient.swift new file mode 100644 index 0000000000..0926b8a7ee --- /dev/null +++ b/Sources/BraintreeCore/BTShopperInsightsClient.swift @@ -0,0 +1,27 @@ +import Foundation + +/// Use `BTShopperInsightsClient` to optimize your checkout experience by prioritizing the customer’s preferred payment methods in your UI. +/// By customizing each customer’s checkout experience, you can improve conversion, increase sales/repeat buys and boost user retention/loyalty. +/// - Note: This feature is in beta. It's public API may change or be removed in future releases. +public class BTShopperInsightsClient { + + private let apiClient: BTAPIClient + + /// Creates a `BTShopperInsightsClient` + /// - Parameter apiClient: A `BTAPIClient` instance. + /// - Note: This features only works with a client token. + public init(apiClient: BTAPIClient) { + self.apiClient = apiClient + } + + /// This method confirms if the customer is a user of PayPal services using their email and phone number. + /// - Parameters: + /// - request: A `BTShopperInsightsRequest` containing the buyer's user information + /// - Returns: A `BTShopperInsightsResult` instance + /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult { + // TODO: - Add isAppInstalled checks for PP & Venmo. DTBTSDK-3176 + // TODO: - Make API call to PaymentReadyAPI. DTBTSDK-3176 + return BTShopperInsightsResult() + } +} diff --git a/Sources/BraintreeCore/BTShopperInsightsRequest.swift b/Sources/BraintreeCore/BTShopperInsightsRequest.swift new file mode 100644 index 0000000000..702e7e75ce --- /dev/null +++ b/Sources/BraintreeCore/BTShopperInsightsRequest.swift @@ -0,0 +1,28 @@ +import Foundation + +/// Buyer data required to use the Shopper Insights feature. +/// - Note: This feature is in beta. It's public API may change or be removed in future releases. +public struct BTShopperInsightsRequest { + + /// The buyer's email address. + private let email: String + + /// The buyer's country code prefix to the national telephone number. An identifier for a specific country. + /// Must not contain special characters. + private let phoneCountryCode: String + + /// The buyer's national phone number. Must not contain special characters. + private let phoneNationalNumber: String + + /// Initialize a `BTShopperInsightsRequest` + /// - Parameters: + /// - email: The buyer's email address. + /// - phoneCountryCode: The buyer's country code prefix to the national telephone number. An identifier for a specific country. Must not contain special characters. + /// - phoneNationalNumber: The buyer's national phone number. Must not contain special characters. + /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + public init(email: String, phoneCountryCode: String, phoneNationalNumber: String) { + self.email = email + self.phoneCountryCode = phoneCountryCode + self.phoneNationalNumber = phoneNationalNumber + } +} diff --git a/Sources/BraintreeCore/BTShopperInsightsResult.swift b/Sources/BraintreeCore/BTShopperInsightsResult.swift new file mode 100644 index 0000000000..7700177b54 --- /dev/null +++ b/Sources/BraintreeCore/BTShopperInsightsResult.swift @@ -0,0 +1,12 @@ +import Foundation + +/// A summary of the buyer's recommended payment methods. +/// - Note: This feature is in beta. It's public API may change or be removed in future releases. +public struct BTShopperInsightsResult { + + /// If true, display the PayPal button with high priority. + public var isPayPalRecommended = false + + /// If true, dislpay the Venmo button with high priority. + public var isVenmoRecommended = false +} diff --git a/UnitTests/BraintreeCoreTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeCoreTests/BTShopperInsightsClient_Tests.swift new file mode 100644 index 0000000000..d1d48a08ed --- /dev/null +++ b/UnitTests/BraintreeCoreTests/BTShopperInsightsClient_Tests.swift @@ -0,0 +1,27 @@ +import Foundation +import XCTest +@testable import BraintreeCore +@testable import BraintreeTestShared + +class BTShopperInsightsClient_Tests: XCTestCase { + + var mockAPIClient = MockAPIClient(authorization: "development_client_key")! + var sut: BTShopperInsightsClient! + + override func setUp() { + super.setUp() + sut = BTShopperInsightsClient(apiClient: mockAPIClient) + } + + func testGetRecommendedPaymentMethods_returnsDefaultRecommendations() async { + let request = BTShopperInsightsRequest( + email: "fake-email", + phoneCountryCode: "fake-country-code", + phoneNationalNumber: "fake-national-phone" + ) + let result = try? await sut.getRecommendedPaymentMethods(request: request) + + XCTAssertNotNil(result!.isPayPalRecommended) + XCTAssertNotNil(result!.isVenmoRecommended) + } +} From ef6421e456e27d570c60587b793149a8039e9f8f Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:37:49 -0600 Subject: [PATCH 02/57] Allow email OR phone to init `BTShopperInsightsRequest` (#1149) --- .../BTShopperInsightsRequest.swift | 53 ++++++++++++++----- .../BTShopperInsightsClient_Tests.swift | 8 +-- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/Sources/BraintreeCore/BTShopperInsightsRequest.swift b/Sources/BraintreeCore/BTShopperInsightsRequest.swift index 702e7e75ce..32ec9a95c5 100644 --- a/Sources/BraintreeCore/BTShopperInsightsRequest.swift +++ b/Sources/BraintreeCore/BTShopperInsightsRequest.swift @@ -4,25 +4,54 @@ import Foundation /// - Note: This feature is in beta. It's public API may change or be removed in future releases. public struct BTShopperInsightsRequest { - /// The buyer's email address. - private let email: String + // MARK: - Private Properties - /// The buyer's country code prefix to the national telephone number. An identifier for a specific country. - /// Must not contain special characters. - private let phoneCountryCode: String + private var email: String? + private var phone: Phone? - /// The buyer's national phone number. Must not contain special characters. - private let phoneNationalNumber: String + // MARK: - Initializers + + /// Initialize a `BTShopperInsightsRequest` + /// - Parameters: + /// - email: The buyer's email address. + /// - phone: The buyer's phone number details. + /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + public init(email: String, phone: Phone) { + self.email = email + self.phone = phone + } /// Initialize a `BTShopperInsightsRequest` /// - Parameters: /// - email: The buyer's email address. - /// - phoneCountryCode: The buyer's country code prefix to the national telephone number. An identifier for a specific country. Must not contain special characters. - /// - phoneNationalNumber: The buyer's national phone number. Must not contain special characters. /// - Note: This feature is in beta. It's public API may change or be removed in future releases. - public init(email: String, phoneCountryCode: String, phoneNationalNumber: String) { + public init(email: String) { self.email = email - self.phoneCountryCode = phoneCountryCode - self.phoneNationalNumber = phoneNationalNumber + } + + /// Initialize a `BTShopperInsightsRequest` + /// - Parameters: + /// - phone: The buyer's phone number details. + /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + public init(phone: Phone) { + self.phone = phone + } +} + +/// Buyer's phone number details for use with the Shopper Insights feature. +/// - Note: This feature is in beta. It's public API may change or be removed in future releases. +public struct Phone { + + private let countryCode: String + private let nationalNumber: String + + /// Initialize a `BTShopperInsightsRequest.Phone`. + /// - Parameters: + /// - countryCode: The buyer's country code prefix to the national telephone number. An identifier for a specific country. Must not contain special characters. + /// - nationalNumber: The buyer's national phone number. Must not contain special characters. Must not contain special characters. + /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + init(countryCode: String, nationalNumber: String) { + self.countryCode = countryCode + self.nationalNumber = nationalNumber } } diff --git a/UnitTests/BraintreeCoreTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeCoreTests/BTShopperInsightsClient_Tests.swift index d1d48a08ed..ea387db106 100644 --- a/UnitTests/BraintreeCoreTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeCoreTests/BTShopperInsightsClient_Tests.swift @@ -15,9 +15,11 @@ class BTShopperInsightsClient_Tests: XCTestCase { func testGetRecommendedPaymentMethods_returnsDefaultRecommendations() async { let request = BTShopperInsightsRequest( - email: "fake-email", - phoneCountryCode: "fake-country-code", - phoneNationalNumber: "fake-national-phone" + email: "my-email", + phone: Phone( + countryCode: "1", + nationalNumber: "1234567" + ) ) let result = try? await sut.getRecommendedPaymentMethods(request: request) From 5e36d0289471d90a05b021369498c72c38e8e6a4 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:33:54 -0600 Subject: [PATCH 03/57] Add ShopperInsights module (#1156) --- .github/workflows/release.yml | 3 +- Braintree.podspec | 5 + Braintree.xcodeproj/project.pbxproj | 434 +++++++++++++++++- .../BraintreeShopperInsights.xcscheme | 66 +++ .../xcshareddata/xcschemes/UnitTests.xcscheme | 16 + CHANGELOG.md | 3 + ...referredPaymentMethodsViewController.swift | 132 ++++++ Demo/Demo.xcodeproj/project.pbxproj | 6 + Package.swift | 8 + .../BTShopperInsightsClient.swift | 4 + .../BTShopperInsightsRequest.swift | 2 +- .../BTShopperInsightsResult.swift | 0 .../BTShopperInsightsClient_Tests.swift | 2 +- .../BraintreeShopperInsightsTests/Info.plist | 22 + 14 files changed, 686 insertions(+), 17 deletions(-) create mode 100644 Braintree.xcodeproj/xcshareddata/xcschemes/BraintreeShopperInsights.xcscheme create mode 100644 Demo/Application/Features/Preferred Payment Methods/BraintreeDemoPreferredPaymentMethodsViewController.swift rename Sources/{BraintreeCore => BraintreeShopperInsights}/BTShopperInsightsClient.swift (96%) rename Sources/{BraintreeCore => BraintreeShopperInsights}/BTShopperInsightsRequest.swift (97%) rename Sources/{BraintreeCore => BraintreeShopperInsights}/BTShopperInsightsResult.swift (100%) rename UnitTests/{BraintreeCoreTests => BraintreeShopperInsightsTests}/BTShopperInsightsClient_Tests.swift (95%) create mode 100644 UnitTests/BraintreeShopperInsightsTests/Info.plist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f65d3ee207..e46dc239a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,9 +110,10 @@ jobs: sourcekitten doc -- -workspace Braintree.xcworkspace -scheme BraintreeCard -destination 'name=iPhone 14,platform=iOS Simulator' > braintree-card.json sourcekitten doc -- -workspace Braintree.xcworkspace -scheme BraintreePayPal -destination 'name=iPhone 14,platform=iOS Simulator' > braintree-paypal.json sourcekitten doc -- -workspace Braintree.xcworkspace -scheme BraintreeVenmo -destination 'name=iPhone 14,platform=iOS Simulator' > braintree-venmo.json + sourcekitten doc -- -workspace Braintree.xcworkspace -scheme BraintreeShopperInsights -destination 'name=iPhone 14,platform=iOS Simulator' > braintree-shopper-insights.json # merge sourcekitten output - jq -s '.[0] + .[1] + .[2] + .[3] + .[4] + .[5] + .[6] + .[7] + .[8] + .[9] + .[10]' braintree-core.json braintree-pay-pal-native-checkout.json braintree-sepa-direct-debit.json braintree-american-express.json braintree-data-collector.json braintree-apple-pay.json braintree-local-payment.json braintree-three-d-secure.json braintree-card.json braintree-paypal.json braintree-venmo.json > swiftDoc.json + jq -s '.[0] + .[1] + .[2] + .[3] + .[4] + .[5] + .[6] + .[7] + .[8] + .[9] + .[10] + .[11]' braintree-core.json braintree-pay-pal-native-checkout.json braintree-sepa-direct-debit.json braintree-american-express.json braintree-data-collector.json braintree-apple-pay.json braintree-local-payment.json braintree-three-d-secure.json braintree-card.json braintree-paypal.json braintree-venmo.json braintree-shopper-insights.json> swiftDoc.json jazzy \ --sourcekitten-sourcefile swiftDoc.json \ diff --git a/Braintree.podspec b/Braintree.podspec index 8cd791dec3..c6996e0a05 100644 --- a/Braintree.podspec +++ b/Braintree.podspec @@ -65,6 +65,11 @@ Pod::Spec.new do |s| s.dependency "Braintree/Core" end + s.subspec "ShopperInsights" do |s| + s.source_files = "Sources/BraintreeShopperInsights/*.swift" + s.dependency "Braintree/Core" + end + s.subspec "PayPalNativeCheckout" do |s| s.source_files = "Sources/BraintreePayPalNativeCheckout/*.swift" s.dependency "Braintree/Core" diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 5c75a2c43b..a44894f4b1 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -74,6 +74,10 @@ 57D94372296CCA2F0079EAB1 /* String+NonceValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */; }; 800E78C429E0DD5300D1B0FC /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */; }; 800FC544257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */; }; + 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */; }; + 804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */; }; + 804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */; }; + 804698422B27C5530090878E /* BraintreeShopperInsights.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 804698302B27C5340090878E /* BraintreeShopperInsights.framework */; platformFilter = ios; }; 80482F8029D39A1D007E5F50 /* BTThreeDSecureRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F7F29D39A1D007E5F50 /* BTThreeDSecureRequest.swift */; }; 80482F8229D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F8129D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift */; }; 80482F8429D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F8329D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift */; }; @@ -85,10 +89,10 @@ 8053F05929FB2F700076F988 /* URL+IsPayPal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8053F05829FB2F700076F988 /* URL+IsPayPal.swift */; }; 80581A8C25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581A8B25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift */; }; 80581B1D2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */; }; - 8064F38F2B1E492F0059C4CB /* BTShopperInsightsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */; }; - 8064F3912B1E49E10059C4CB /* BTShopperInsightsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */; }; - 8064F3952B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */; }; - 8064F3972B1E63800059C4CB /* BTShopperInsightsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */; }; + 80AB424F2B27CAC200249218 /* BraintreeTestShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A903E1A624F9D34000C314E1 /* BraintreeTestShared.framework */; platformFilter = ios; }; + 80AB42542B27DF5B00249218 /* BraintreeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; platformFilter = ios; }; + 80AB42552B27DF5B00249218 /* BraintreeCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 80B59A132B27C7FC004C2FA3 /* BTShopperInsightsClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */; }; 80BA64AC29D788E000E15264 /* BTLocalPaymentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64AB29D788E000E15264 /* BTLocalPaymentResult.swift */; }; 80BA64B229D7937E00E15264 /* BTLocalPaymentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64B129D7937E00E15264 /* BTLocalPaymentRequest.swift */; }; 80BA64B429D795D000E15264 /* BTLocalPaymentRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64B329D795D000E15264 /* BTLocalPaymentRequestDelegate.swift */; }; @@ -305,6 +309,27 @@ remoteGlobalIDString = 2D941D371B59C76A0016EFB4; remoteInfo = BraintreePayPal; }; + 804698432B27C5530090878E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A75DA344192138F000D997A2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8046982F2B27C5340090878E; + remoteInfo = BraintreeShopperInsights; + }; + 80AB42512B27CAC200249218 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A75DA344192138F000D997A2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A903E1A524F9D34000C314E1; + remoteInfo = BraintreeTestShared; + }; + 80AB42562B27DF5B00249218 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A75DA344192138F000D997A2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 570B936A285397520041BAFE; + remoteInfo = BraintreeCore; + }; 9CE517A0282D54030013C740 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = A75DA344192138F000D997A2 /* Project object */; @@ -588,6 +613,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 80AB42582B27DF5B00249218 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 80AB42552B27DF5B00249218 /* BraintreeCore.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; A9E80AA024FEF45400196BD3 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -694,6 +730,9 @@ 800E23DC22206A8300C5D22E /* BTThreeDSecureAuthenticateJWT_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAuthenticateJWT_Tests.swift; sourceTree = ""; }; 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePayCardNonce_Tests.swift; sourceTree = ""; }; + 804698302B27C5340090878E /* BraintreeShopperInsights.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BraintreeShopperInsights.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8046983E2B27C5530090878E /* BraintreeShopperInsightsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BraintreeShopperInsightsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 804698482B27C71C0090878E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 80482F7F29D39A1D007E5F50 /* BTThreeDSecureRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureRequest.swift; sourceTree = ""; }; 80482F8129D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureRequestDelegate.swift; sourceTree = ""; }; 80482F8329D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAccountType.swift; sourceTree = ""; }; @@ -950,6 +989,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8046982D2B27C5340090878E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 80AB42542B27DF5B00249218 /* BraintreeCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8046983B2B27C5530090878E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 80AB424F2B27CAC200249218 /* BraintreeTestShared.framework in Frameworks */, + 804698422B27C5530090878E /* BraintreeShopperInsights.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9CE51771282D51DD0013C740 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1199,9 +1255,6 @@ 5708E0A728809BC6007946B9 /* BTJSONError.swift */, 570B93D82853C6180041BAFE /* BTLogLevel.swift */, 570B93D62853C29A0041BAFE /* BTLogLevelDescription.swift */, - 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */, - 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */, - 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */, BE9FB82A2898324C00D6FE2F /* BTPaymentMethodNonce.swift */, BE9FB82C28984ADE00D6FE2F /* BTPaymentMethodNonceParser.swift */, BE63A3A6288F3026001936DA /* BTPostalAddress.swift */, @@ -1240,6 +1293,25 @@ path = V2UICustomization; sourceTree = ""; }; + 804698292B27C4D70090878E /* BraintreeShopperInsights */ = { + isa = PBXGroup; + children = ( + 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */, + 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */, + 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */, + ); + path = BraintreeShopperInsights; + sourceTree = ""; + }; + 8046982A2B27C4E80090878E /* BraintreeShopperInsightsTests */ = { + isa = PBXGroup; + children = ( + 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */, + 804698482B27C71C0090878E /* Info.plist */, + ); + path = BraintreeShopperInsightsTests; + sourceTree = ""; + }; 8053F05529FAB6FF0076F988 /* Analytics */ = { isa = PBXGroup; children = ( @@ -1262,6 +1334,7 @@ 2D941D391B59C76A0016EFB4 /* BraintreePayPal */, 9CE51786282D527E0013C740 /* BraintreePayPalNativeCheckout */, BEF3F17027CE9E2600072467 /* BraintreeSEPADirectDebit */, + 804698292B27C4D70090878E /* BraintreeShopperInsights */, A91E1EAC24FEEF21007540E5 /* BraintreeThreeDSecure */, A7C889F01B5F0B30007A0E9C /* BraintreeVenmo */, ); @@ -1280,6 +1353,7 @@ 9CE5179C282D54030013C740 /* BraintreePayPalNativeCheckoutTests */, A9E5C1E124FD665D00EE691F /* BraintreePayPalTests */, BE642D8B27D0129D00694A5B /* BraintreeSEPADirectDebitTests */, + 8046982A2B27C4E80090878E /* BraintreeShopperInsightsTests */, A903E1A724F9D34000C314E1 /* BraintreeTestShared */, A91E1EF324FEF2E4007540E5 /* BraintreeThreeDSecureTests */, A948D6F124FAC8F900F4F178 /* BraintreeVenmoTests */, @@ -1399,6 +1473,8 @@ 9CE5179B282D54030013C740 /* BraintreePayPalNativeCheckoutTests.xctest */, BE642DA027D0132A00694A5B /* BraintreeSEPADirectDebitTests.xctest */, BEF3F17C27CE9EC600072467 /* BraintreeSEPADirectDebit.framework */, + 804698302B27C5340090878E /* BraintreeShopperInsights.framework */, + 8046983E2B27C5530090878E /* BraintreeShopperInsightsTests.xctest */, ); name = Products; sourceTree = ""; @@ -1680,7 +1756,6 @@ BEBC6F272937BD1F004E25A0 /* BTGraphQLHTTP_Tests.swift */, BE54C0322912B68E009C6CEE /* BTHTTP_Tests.swift */, 16CD2E9E1B4077FC00E68495 /* BTJSON_Tests.swift */, - 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */, 4149C91C1BA218830090665E /* BTPaymentMethodNonceParser_Tests.swift */, 805FD35B2331780F0000B514 /* BTPostalAddress_Tests.swift */, A7E93E571B601EE900957223 /* BTURLUtils_Tests.swift */, @@ -1760,6 +1835,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8046982B2B27C5340090878E /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9CE51773282D51DD0013C740 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -1896,6 +1978,45 @@ productReference = 570B93AC285397520041BAFE /* BraintreeCore.framework */; productType = "com.apple.product-type.framework"; }; + 8046982F2B27C5340090878E /* BraintreeShopperInsights */ = { + isa = PBXNativeTarget; + buildConfigurationList = 804698342B27C5340090878E /* Build configuration list for PBXNativeTarget "BraintreeShopperInsights" */; + buildPhases = ( + 8046982B2B27C5340090878E /* Headers */, + 8046982C2B27C5340090878E /* Sources */, + 8046982D2B27C5340090878E /* Frameworks */, + 8046982E2B27C5340090878E /* Resources */, + 80AB42582B27DF5B00249218 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 80AB42572B27DF5B00249218 /* PBXTargetDependency */, + ); + name = BraintreeShopperInsights; + productName = BraintreeShopperInsights; + productReference = 804698302B27C5340090878E /* BraintreeShopperInsights.framework */; + productType = "com.apple.product-type.framework"; + }; + 8046983D2B27C5530090878E /* BraintreeShopperInsightsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 804698452B27C5530090878E /* Build configuration list for PBXNativeTarget "BraintreeShopperInsightsTests" */; + buildPhases = ( + 8046983A2B27C5530090878E /* Sources */, + 8046983B2B27C5530090878E /* Frameworks */, + 8046983C2B27C5530090878E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 804698442B27C5530090878E /* PBXTargetDependency */, + 80AB42522B27CAC200249218 /* PBXTargetDependency */, + ); + name = BraintreeShopperInsightsTests; + productName = BraintreeShopperInsightsTests; + productReference = 8046983E2B27C5530090878E /* BraintreeShopperInsightsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 9CE51766282D51DD0013C740 /* BraintreePayPalNativeCheckout */ = { isa = PBXNativeTarget; buildConfigurationList = 9CE51781282D51DD0013C740 /* Build configuration list for PBXNativeTarget "BraintreePayPalNativeCheckout" */; @@ -2299,7 +2420,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; CLASSPREFIX = BT; - LastSwiftUpdateCheck = 1320; + LastSwiftUpdateCheck = 1500; LastUpgradeCheck = 1420; TargetAttributes = { 03502C831E9C0A1700F15EE6 = { @@ -2319,6 +2440,12 @@ 570B936A285397520041BAFE = { LastSwiftMigration = 1340; }; + 8046982F2B27C5340090878E = { + CreatedOnToolsVersion = 15.0.1; + }; + 8046983D2B27C5530090878E = { + CreatedOnToolsVersion = 15.0.1; + }; 9CE51766282D51DD0013C740 = { LastSwiftMigration = 1320; }; @@ -2416,8 +2543,10 @@ A9E5C1DF24FD665D00EE691F /* BraintreePayPalTests */, 9CE51766282D51DD0013C740 /* BraintreePayPalNativeCheckout */, 9CE5179A282D54030013C740 /* BraintreePayPalNativeCheckoutTests */, - BE642D8E27D0132A00694A5B /* BraintreeSEPADirectDebitTests */, BEF3F17227CE9EC600072467 /* BraintreeSEPADirectDebit */, + BE642D8E27D0132A00694A5B /* BraintreeSEPADirectDebitTests */, + 8046982F2B27C5340090878E /* BraintreeShopperInsights */, + 8046983D2B27C5530090878E /* BraintreeShopperInsightsTests */, A903E1A524F9D34000C314E1 /* BraintreeTestShared */, A91E1EAA24FEEF21007540E5 /* BraintreeThreeDSecure */, A91E1EF124FEF2E3007540E5 /* BraintreeThreeDSecureTests */, @@ -2457,6 +2586,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8046982E2B27C5340090878E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8046983C2B27C5530090878E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9CE51780282D51DD0013C740 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2758,14 +2901,12 @@ BEC3F11328A4401E0074DF0F /* BTHTTPError.swift in Sources */, 574891EB286F7E4F0020DA36 /* BTClientMetadataIntegration.swift in Sources */, BE698EA428AD2C10001D9B10 /* BTCoreConstants.swift in Sources */, - 8064F3912B1E49E10059C4CB /* BTShopperInsightsResult.swift in Sources */, BE24C67528E7491E0067B11A /* BTAPIClientAuthorization.swift in Sources */, BE32ACBC2907744400A61FED /* BTCardNetwork.swift in Sources */, 570B93D92853C6180041BAFE /* BTLogLevel.swift in Sources */, 579DAEC5286E04A700FCE87F /* BTAppContextSwitcher.swift in Sources */, 570B93D72853C29A0041BAFE /* BTLogLevelDescription.swift in Sources */, 57CBBCE828B033760037F4EE /* BTGraphQLHTTP.swift in Sources */, - 8064F38F2B1E492F0059C4CB /* BTShopperInsightsClient.swift in Sources */, BE63A3A7288F3026001936DA /* BTPostalAddress.swift in Sources */, BE2F98D028A2BCCD008EF189 /* BTConfiguration.swift in Sources */, BED00CB228A57AD400D74AEC /* BTClientTokenError.swift in Sources */, @@ -2778,7 +2919,24 @@ BE698EA028AA8DCB001D9B10 /* BTHTTP.swift in Sources */, BC17F9B428D23C5C004B18CC /* BTGraphQLMultiErrorNode.swift in Sources */, 5708E0A828809BC6007946B9 /* BTJSONError.swift in Sources */, - 8064F3972B1E63800059C4CB /* BTShopperInsightsRequest.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8046982C2B27C5340090878E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */, + 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */, + 804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8046983A2B27C5530090878E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 80B59A132B27C7FC004C2FA3 /* BTShopperInsightsClient_Tests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3066,7 +3224,6 @@ A908436324FD82C0004134CA /* BTBinData_Tests.swift in Sources */, A908436224FD82A9004134CA /* BTAppContextSwitcher_Tests.swift in Sources */, BEBC6F282937BD1F004E25A0 /* BTGraphQLHTTP_Tests.swift in Sources */, - 8064F3952B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift in Sources */, BE54C0352912B6BC009C6CEE /* BTHTTPTestProtocol.swift in Sources */, BEDA91A028EDDE64007441D9 /* FakeAnalyticsService.swift in Sources */, ); @@ -3108,6 +3265,24 @@ target = 2D941D371B59C76A0016EFB4 /* BraintreePayPal */; targetProxy = 3DF4B4CB285B77960072DC60 /* PBXContainerItemProxy */; }; + 804698442B27C5530090878E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = 8046982F2B27C5340090878E /* BraintreeShopperInsights */; + targetProxy = 804698432B27C5530090878E /* PBXContainerItemProxy */; + }; + 80AB42522B27CAC200249218 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = A903E1A524F9D34000C314E1 /* BraintreeTestShared */; + targetProxy = 80AB42512B27CAC200249218 /* PBXContainerItemProxy */; + }; + 80AB42572B27DF5B00249218 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = 570B936A285397520041BAFE /* BraintreeCore */; + targetProxy = 80AB42562B27DF5B00249218 /* PBXContainerItemProxy */; + }; 9CE517A1282D54030013C740 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; @@ -3808,6 +3983,219 @@ }; name = Release; }; + 804698352B27C5340090878E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 43253H4X22; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/BraintreeCore/Info.plist"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braintreepayments.BraintreeShopperInsights; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 804698362B27C5340090878E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 43253H4X22; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "$(SRCROOT)/Sources/BraintreeCore/Info.plist"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braintreepayments.BraintreeShopperInsights; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 804698462B27C5530090878E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 43253H4X22; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = UnitTests/BraintreeShopperInsightsTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braintreepayments.BraintreeShopperInsightsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 804698472B27C5530090878E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 43253H4X22; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = UnitTests/BraintreeShopperInsightsTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braintreepayments.BraintreeShopperInsightsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 9CE51782282D51DD0013C740 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5977,6 +6365,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 804698342B27C5340090878E /* Build configuration list for PBXNativeTarget "BraintreeShopperInsights" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 804698352B27C5340090878E /* Debug */, + 804698362B27C5340090878E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 804698452B27C5530090878E /* Build configuration list for PBXNativeTarget "BraintreeShopperInsightsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 804698462B27C5530090878E /* Debug */, + 804698472B27C5530090878E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 9CE51781282D51DD0013C740 /* Build configuration list for PBXNativeTarget "BraintreePayPalNativeCheckout" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Braintree.xcodeproj/xcshareddata/xcschemes/BraintreeShopperInsights.xcscheme b/Braintree.xcodeproj/xcshareddata/xcschemes/BraintreeShopperInsights.xcscheme new file mode 100644 index 0000000000..e8021ed093 --- /dev/null +++ b/Braintree.xcodeproj/xcshareddata/xcschemes/BraintreeShopperInsights.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme index bfb2e65b41..0e89c617be 100644 --- a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme +++ b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme @@ -5,6 +5,22 @@ + + + + + + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + From 1eec8804d9efbf1b4d387e9b78c3094e679c6d53 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Fri, 15 Dec 2023 10:28:22 -0600 Subject: [PATCH 04/57] Add Shopper Insights Demo feature (#1158) --- .../Base/ContainmentViewController.swift | 2 + .../Base/Settings/Settings.bundle/Root.plist | 2 + .../ShopperInsightsViewController.swift | 102 ++++++++++++++++++ Demo/Demo.xcodeproj/project.pbxproj | 4 + 4 files changed, 110 insertions(+) create mode 100644 Demo/Application/Features/ShopperInsightsViewController.swift diff --git a/Demo/Application/Base/ContainmentViewController.swift b/Demo/Application/Base/ContainmentViewController.swift index 72b9957989..5c0aa39576 100644 --- a/Demo/Application/Base/ContainmentViewController.swift +++ b/Demo/Application/Base/ContainmentViewController.swift @@ -208,6 +208,8 @@ class ContainmentViewController: UIViewController { return PayPalWebCheckoutViewController(authorization: authorization) case "SEPADirectDebitViewController": return SEPADirectDebitViewController(authorization: authorization) + case "ShopperInsightsViewController": + return ShopperInsightsViewController(authorization: authorization) case "ThreeDSecureViewController": return ThreeDSecureViewController(authorization: authorization) case "VenmoViewController": diff --git a/Demo/Application/Base/Settings/Settings.bundle/Root.plist b/Demo/Application/Base/Settings/Settings.bundle/Root.plist index 40e45bb96a..6e88cab14c 100644 --- a/Demo/Application/Base/Settings/Settings.bundle/Root.plist +++ b/Demo/Application/Base/Settings/Settings.bundle/Root.plist @@ -27,6 +27,7 @@ iDEAL Amex SEPA Direct Debit + Shopper Insights Values @@ -40,6 +41,7 @@ IdealViewController AmexViewController SEPADirectDebitViewController + ShopperInsightsViewController diff --git a/Demo/Application/Features/ShopperInsightsViewController.swift b/Demo/Application/Features/ShopperInsightsViewController.swift new file mode 100644 index 0000000000..f62332e2e7 --- /dev/null +++ b/Demo/Application/Features/ShopperInsightsViewController.swift @@ -0,0 +1,102 @@ +import UIKit +import BraintreeCore +import BraintreePayPal +import BraintreeVenmo +import BraintreeShopperInsights + +class ShopperInsightsViewController: PaymentButtonBaseViewController { + + lazy var shopperInsightsClient = BTShopperInsightsClient(apiClient: apiClient) + lazy var paypalClient = BTPayPalClient(apiClient: apiClient) + lazy var venmoClient = BTVenmoClient(apiClient: apiClient) + + lazy var shopperInsightsButton = createButton(title: "Fetch shopper insights", action: #selector(shopperInsightsButtonTapped)) + lazy var payPalCheckoutButton = createButton(title: "PayPal Checkout", action: #selector(payPalCheckoutButtonTapped)) + lazy var payPalVaultButton = createButton(title: "PayPal Vault", action: #selector(payPalVaultButtonTapped)) + lazy var venmoButton = createButton(title: "Venmo", action: #selector(venmoButtonTapped)) + + override func createPaymentButton() -> UIView { + let buttons = [shopperInsightsButton, payPalCheckoutButton, payPalVaultButton, venmoButton] + buttons[1...3].forEach { $0.isEnabled = false } + let stackView = UIStackView(arrangedSubviews: buttons) + stackView.axis = .vertical + stackView.alignment = .center + stackView.distribution = .fillEqually + stackView.translatesAutoresizingMaskIntoConstraints = false + + return stackView + } + + @objc func shopperInsightsButtonTapped(_ button: UIButton) { + self.progressBlock("Fetching shopper insights...") + + let request = BTShopperInsightsRequest( + email: "my-email@gmail.com", + phone: Phone( + countryCode: "1", + nationalNumber: "1234567" + ) + ) + Task { + do { + let result = try await shopperInsightsClient.getRecommendedPaymentMethods(request: request) + self.progressBlock("PayPal Recommended: \(result.isPayPalRecommended)\nVenmo Recommended: \(result.isVenmoRecommended)") + self.payPalCheckoutButton.isEnabled = result.isPayPalRecommended + self.payPalVaultButton.isEnabled = result.isPayPalRecommended + self.venmoButton.isEnabled = result.isVenmoRecommended + } catch { + self.progressBlock("Error: \(error.localizedDescription)") + } + } + } + + @objc func payPalCheckoutButtonTapped(_ button: UIButton) { + self.progressBlock("Tapped PayPal Checkout") + + button.setTitle("Processing...", for: .disabled) + button.isEnabled = false + + let paypalRequest = BTPayPalCheckoutRequest(amount: "4.30") + paypalClient.tokenize(paypalRequest) { (nonce, error) in + button.isEnabled = true + self.displayResultDetails(nonce: nonce, error: error) + } + } + + @objc func payPalVaultButtonTapped(_ button: UIButton) { + self.progressBlock("Tapped PayPal Vault") + + button.setTitle("Processing...", for: .disabled) + button.isEnabled = false + + let paypalRequest = BTPayPalVaultRequest() + paypalClient.tokenize(paypalRequest) { (nonce, error) in + button.isEnabled = true + self.displayResultDetails(nonce: nonce, error: error) + } + } + + @objc func venmoButtonTapped(_ button: UIButton) { + self.progressBlock("Tapped Venmo") + + button.setTitle("Processing...", for: .disabled) + button.isEnabled = false + + let venmoRequest = BTVenmoRequest(paymentMethodUsage: .multiUse) + venmoClient.tokenize(venmoRequest) { (nonce, error) in + button.isEnabled = true + self.displayResultDetails(nonce: nonce, error: error) + } + } + + private func displayResultDetails(nonce: BTPaymentMethodNonce?, error: Error?) { + if let error { + self.progressBlock(error.localizedDescription) + } else if let nonce { + self.completionBlock(nonce) + } else { + self.progressBlock("Canceled") + } + } +} + diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 5d274419ac..c81251b3d8 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 57108A152832E789004EB870 /* PayPalNativeCheckoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57108A142832E789004EB870 /* PayPalNativeCheckoutViewController.swift */; }; 57108A172832EA04004EB870 /* BraintreePayPalNativeCheckout.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57108A162832EA04004EB870 /* BraintreePayPalNativeCheckout.framework */; }; 57108A182832EA04004EB870 /* BraintreePayPalNativeCheckout.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 57108A162832EA04004EB870 /* BraintreePayPalNativeCheckout.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 8028B9762B28C9E100C88CE8 /* ShopperInsightsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8028B9752B28C9E100C88CE8 /* ShopperInsightsViewController.swift */; }; 8028B9782B28D42400C88CE8 /* BraintreeShopperInsights.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8028B9772B28D42400C88CE8 /* BraintreeShopperInsights.framework */; }; 8028B9792B28D42400C88CE8 /* BraintreeShopperInsights.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8028B9772B28D42400C88CE8 /* BraintreeShopperInsights.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 803D64F5256DAF9A00ACE692 /* BraintreeAmericanExpress.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 803D64EA256DAF9A00ACE692 /* BraintreeAmericanExpress.framework */; }; @@ -138,6 +139,7 @@ 57108A162832EA04004EB870 /* BraintreePayPalNativeCheckout.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BraintreePayPalNativeCheckout.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6D23244B5E9EE59BAB3F3003 /* Pods_Demo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Demo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 73498B4265CA7D315E2FBF38 /* Pods-Demo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.debug.xcconfig"; path = "Target Support Files/Pods-Demo/Pods-Demo.debug.xcconfig"; sourceTree = ""; }; + 8028B9752B28C9E100C88CE8 /* ShopperInsightsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShopperInsightsViewController.swift; sourceTree = ""; }; 8028B9772B28D42400C88CE8 /* BraintreeShopperInsights.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BraintreeShopperInsights.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 803D64EA256DAF9A00ACE692 /* BraintreeAmericanExpress.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BraintreeAmericanExpress.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 803D64EB256DAF9A00ACE692 /* BraintreeApplePay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BraintreeApplePay.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -346,6 +348,7 @@ 57108A142832E789004EB870 /* PayPalNativeCheckoutViewController.swift */, BED7C4C92ABDD35700EF8550 /* PayPalWebCheckoutViewController.swift */, BE777AC327D9370400487D23 /* SEPADirectDebitViewController.swift */, + 8028B9752B28C9E100C88CE8 /* ShopperInsightsViewController.swift */, BEBD52862AABA513005D6687 /* ThreeDSecureViewController.swift */, 80D36DED2A7967F20035380E /* VenmoViewController.swift */, 809E86A42ACF6607004998B0 /* Helpers */, @@ -653,6 +656,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8028B9762B28C9E100C88CE8 /* ShopperInsightsViewController.swift in Sources */, BEAAAA342A98E5E6001ECA63 /* IdealViewController.swift in Sources */, BE994B0D2AD838A500470773 /* PaymentButtonBaseViewController.swift in Sources */, BED7C4CA2ABDD35700EF8550 /* PayPalWebCheckoutViewController.swift in Sources */, From e0379d353044fe7d651d12be69f38a6335b380cf Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Tue, 19 Dec 2023 09:49:29 -0600 Subject: [PATCH 05/57] ShopperInsights - Merchant Analytics (#1162) --- Braintree.xcodeproj/project.pbxproj | 4 ++++ .../xcshareddata/xcschemes/UnitTests.xcscheme | 10 ++++++++ .../BTShopperInsightsAnalytics.swift | 11 +++++++++ .../BTShopperInsightsClient.swift | 24 +++++++++++++++++++ .../BTShopperInsightsClient_Tests.swift | 22 +++++++++++++++++ 5 files changed, 71 insertions(+) create mode 100644 Sources/BraintreeShopperInsights/BTShopperInsightsAnalytics.swift diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index a44894f4b1..a9c12e8893 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -74,6 +74,7 @@ 57D94372296CCA2F0079EAB1 /* String+NonceValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */; }; 800E78C429E0DD5300D1B0FC /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */; }; 800FC544257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */; }; + 8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */; }; 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */; }; 804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */; }; 804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */; }; @@ -730,6 +731,7 @@ 800E23DC22206A8300C5D22E /* BTThreeDSecureAuthenticateJWT_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAuthenticateJWT_Tests.swift; sourceTree = ""; }; 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePayCardNonce_Tests.swift; sourceTree = ""; }; + 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsAnalytics.swift; sourceTree = ""; }; 804698302B27C5340090878E /* BraintreeShopperInsights.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BraintreeShopperInsights.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8046983E2B27C5530090878E /* BraintreeShopperInsightsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BraintreeShopperInsightsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 804698482B27C71C0090878E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1296,6 +1298,7 @@ 804698292B27C4D70090878E /* BraintreeShopperInsights */ = { isa = PBXGroup; children = ( + 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */, 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */, 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */, 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */, @@ -2928,6 +2931,7 @@ files = ( 804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */, 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */, + 8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */, 804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme index 0e89c617be..c4e361af83 100644 --- a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme +++ b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme @@ -149,6 +149,16 @@ ReferencedContainer = "container:Braintree.xcodeproj"> + + + + Date: Wed, 3 Jan 2024 13:49:15 -0600 Subject: [PATCH 06/57] Add app installed checks to ShopperInsightsClient (#1167) --- .../BTShopperInsightsClient.swift | 30 ++++++++++++++++--- .../BTShopperInsightsClient_Tests.swift | 28 ++++++++++++----- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 20486d395d..332e0510be 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -1,4 +1,4 @@ -import Foundation +import UIKit #if canImport(BraintreeCore) import BraintreeCore @@ -9,6 +9,13 @@ import BraintreeCore /// - Note: This feature is in beta. It's public API may change or be removed in future releases. public class BTShopperInsightsClient { + // MARK: - Internal Properties + + /// Defaults to `UIApplication.shared`, but exposed for unit tests to mock calls to `canOpenURL`. + var application: URLOpener = UIApplication.shared + + // MARK: - Private Properties + private let apiClient: BTAPIClient /// Creates a `BTShopperInsightsClient` @@ -24,9 +31,12 @@ public class BTShopperInsightsClient { /// - Returns: A `BTShopperInsightsResult` instance /// - Note: This feature is in beta. It's public API may change or be removed in future releases. public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult { - // TODO: - Add isAppInstalled checks for PP & Venmo. DTBTSDK-3176 - // TODO: - Make API call to PaymentReadyAPI. DTBTSDK-3176 - return BTShopperInsightsResult() + if isVenmoAppInstalled() && isPayPalAppInstalled() { + return BTShopperInsightsResult(isPayPalRecommended: true, isVenmoRecommended: true) + } else { + // TODO: - Make API call to PaymentReadyAPI. DTBTSDK-3176 + return BTShopperInsightsResult() + } } /// Call this method when the PayPal button has been successfully displayed to the buyer. @@ -52,4 +62,16 @@ public class BTShopperInsightsClient { public func sendVenmoSelectedEvent() { apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.venmoSelected) } + + // MARK: - Private Methods + + private func isVenmoAppInstalled() -> Bool { + let venmoURL = URL(string: "com.venmo.touch.v2://")! + return application.canOpenURL(venmoURL) + } + + private func isPayPalAppInstalled() -> Bool { + let paypalURL = URL(string: "paypal://")! + return application.canOpenURL(paypalURL) + } } diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index c5e3bd3b88..3282d47a1f 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -8,25 +8,39 @@ class BTShopperInsightsClient_Tests: XCTestCase { var mockAPIClient = MockAPIClient(authorization: "development_client_key")! var sut: BTShopperInsightsClient! + let request = BTShopperInsightsRequest( + email: "my-email", + phone: Phone( + countryCode: "1", + nationalNumber: "1234567" + ) + ) + override func setUp() { super.setUp() sut = BTShopperInsightsClient(apiClient: mockAPIClient) } func testGetRecommendedPaymentMethods_returnsDefaultRecommendations() async { - let request = BTShopperInsightsRequest( - email: "my-email", - phone: Phone( - countryCode: "1", - nationalNumber: "1234567" - ) - ) let result = try? await sut.getRecommendedPaymentMethods(request: request) XCTAssertNotNil(result!.isPayPalRecommended) XCTAssertNotNil(result!.isVenmoRecommended) } + func testGetRecommendedPaymentMethods_whenBothAppsInstalled_returnsTrue() async { + let fakeApplication = FakeApplication() + fakeApplication.cannedCanOpenURL = false + fakeApplication.canOpenURLWhitelist.append(URL(string: "com.venmo.touch.v2://")!) + fakeApplication.canOpenURLWhitelist.append(URL(string: "paypal://")!) + sut.application = fakeApplication + + let result = try? await sut.getRecommendedPaymentMethods(request: request) + + XCTAssertTrue(result!.isPayPalRecommended) + XCTAssertTrue(result!.isVenmoRecommended) + } + // MARK: - Analytics func testSendPayPalPresentedEvent_sendsAnalytic() { From 0168cd1c79adfca5849ca46cd61618e023f1fc08 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 10 Jan 2024 13:04:42 -0600 Subject: [PATCH 07/57] WIP - add capability to BTApiClient & BTHTTP to network with PP underlying API --- Braintree.xcodeproj/project.pbxproj | 12 ++++++---- Sources/BraintreeCore/BTAPIClient.swift | 24 ++++++++++++++++++- .../BraintreeCore/BTAPIClientHTTPType.swift | 3 +++ Sources/BraintreeCore/BTAPIRequest.swift | 2 ++ Sources/BraintreeCore/BTHTTP.swift | 18 ++++++++++---- Sources/BraintreeCore/URL+IsPayPal.swift | 2 +- .../BTEligiblePaymentsRequest.swift | 11 +++++++++ .../BTShopperInsightsClient.swift | 4 ++++ 8 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 845ed845f1..1e97dcba12 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -73,13 +73,14 @@ 57D94370296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */; }; 57D94372296CCA2F0079EAB1 /* String+NonceValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */; }; 800E78C429E0DD5300D1B0FC /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */; }; + 800ED77F2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800ED77E2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift */; }; 800FC544257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */; }; 8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */; }; + 804326BF2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */; }; 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */; }; 804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */; }; 804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */; }; 804698422B27C5530090878E /* BraintreeShopperInsights.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 804698302B27C5340090878E /* BraintreeShopperInsights.framework */; platformFilter = ios; }; - 804326BF2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */; }; 80482F8029D39A1D007E5F50 /* BTThreeDSecureRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F7F29D39A1D007E5F50 /* BTThreeDSecureRequest.swift */; }; 80482F8229D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F8129D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift */; }; 80482F8429D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F8329D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift */; }; @@ -93,12 +94,12 @@ 8053F05929FB2F700076F988 /* URL+IsPayPal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8053F05829FB2F700076F988 /* URL+IsPayPal.swift */; }; 80581A8C25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581A8B25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift */; }; 80581B1D2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */; }; + 8075CBEE2B1B735200CA6265 /* BTAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */; }; + 80A6C6192B21205900416D50 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */; }; 80AB424F2B27CAC200249218 /* BraintreeTestShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A903E1A624F9D34000C314E1 /* BraintreeTestShared.framework */; platformFilter = ios; }; 80AB42542B27DF5B00249218 /* BraintreeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; platformFilter = ios; }; 80AB42552B27DF5B00249218 /* BraintreeCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 80B59A132B27C7FC004C2FA3 /* BTShopperInsightsClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */; }; - 8075CBEE2B1B735200CA6265 /* BTAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */; }; - 80A6C6192B21205900416D50 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */; }; 80BA3C292B23892700900BBB /* FakeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA3C282B23892700900BBB /* FakeRequest.swift */; }; 80BA64AC29D788E000E15264 /* BTLocalPaymentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64AB29D788E000E15264 /* BTLocalPaymentResult.swift */; }; 80BA64B229D7937E00E15264 /* BTLocalPaymentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64B129D7937E00E15264 /* BTLocalPaymentRequest.swift */; }; @@ -736,12 +737,13 @@ 59A4135427B90CD7AE94D5CC /* Pods-Tests-IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests-IntegrationTests.debug.xcconfig"; path = "Target Support Files/Pods-Tests-IntegrationTests/Pods-Tests-IntegrationTests.debug.xcconfig"; sourceTree = ""; }; 800E23DC22206A8300C5D22E /* BTThreeDSecureAuthenticateJWT_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAuthenticateJWT_Tests.swift; sourceTree = ""; }; 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; + 800ED77E2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentsRequest.swift; sourceTree = ""; }; 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePayCardNonce_Tests.swift; sourceTree = ""; }; 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsAnalytics.swift; sourceTree = ""; }; + 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePaymentTokensRequest.swift; sourceTree = ""; }; 804698302B27C5340090878E /* BraintreeShopperInsights.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BraintreeShopperInsights.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8046983E2B27C5530090878E /* BraintreeShopperInsightsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BraintreeShopperInsightsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 804698482B27C71C0090878E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePaymentTokensRequest.swift; sourceTree = ""; }; 80482F7F29D39A1D007E5F50 /* BTThreeDSecureRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureRequest.swift; sourceTree = ""; }; 80482F8129D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureRequestDelegate.swift; sourceTree = ""; }; 80482F8329D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAccountType.swift; sourceTree = ""; }; @@ -1318,6 +1320,7 @@ 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */, 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */, 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */, + 800ED77E2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift */, ); path = BraintreeShopperInsights; sourceTree = ""; @@ -2953,6 +2956,7 @@ files = ( 804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */, 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */, + 800ED77F2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */, 8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */, 804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */, ); diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 9264307d38..093293e5b8 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -26,7 +26,8 @@ import Foundation var http: BTHTTP? var graphQLHTTP: BTGraphQLHTTP? - + var paypalHTTP: BTHTTP? + var session: URLSession { let configurationQueue: OperationQueue = OperationQueue() configurationQueue.name = "com.braintreepayments.BTAPIClient" @@ -178,6 +179,14 @@ import Foundation graphQLHTTP = BTGraphQLHTTP(url: graphQLBaseURL, tokenizationKey: tokenizationKey) } } + + if paypalHTTP == nil { + let paypalBaseURL: URL? = paypalAPIURL(forEnvironment: configuration?.environment ?? "") + + if let clientToken, let paypalBaseURL { + paypalHTTP = BTHTTP(url: paypalBaseURL, authorizationFingerprint: clientToken.authorizationFingerprint) + } + } } completion(configuration, nil) @@ -357,6 +366,8 @@ import Foundation return parameters?.merging(["_meta": metadata.parameters]) { $1 } case .graphQLAPI: return parameters?.merging(["clientSdkMetadata": metadata.parameters]) { $1 } + case .paypalAPI: + return parameters } } @@ -469,6 +480,15 @@ import Foundation components.path = "/graphql" return components.url } + + func paypalAPIURL(forEnvironment environment: String) -> URL? { + if environment.caseInsensitiveCompare("sandbox") == .orderedSame || + environment.caseInsensitiveCompare("development") == .orderedSame { + return URL(string: "https://api-m.sandbox.paypal.com") + } else { + return URL(string: "https://api-m.paypal.com") + } + } func http(for httpType: BTAPIClientHTTPService) -> BTHTTP? { switch httpType { @@ -476,6 +496,8 @@ import Foundation return http case .graphQLAPI: return graphQLHTTP + case .paypalAPI: + return paypalHTTP } } } diff --git a/Sources/BraintreeCore/BTAPIClientHTTPType.swift b/Sources/BraintreeCore/BTAPIClientHTTPType.swift index ff35ddd8a8..c14c0001e7 100644 --- a/Sources/BraintreeCore/BTAPIClientHTTPType.swift +++ b/Sources/BraintreeCore/BTAPIClientHTTPType.swift @@ -8,4 +8,7 @@ import Foundation /// Use the GraphQL API case graphQLAPI + + /// Use the PayPal API + case paypalAPI } diff --git a/Sources/BraintreeCore/BTAPIRequest.swift b/Sources/BraintreeCore/BTAPIRequest.swift index 0d76e1bb9a..bdef2bf041 100644 --- a/Sources/BraintreeCore/BTAPIRequest.swift +++ b/Sources/BraintreeCore/BTAPIRequest.swift @@ -34,6 +34,8 @@ struct BTAPIRequest: Encodable { case .graphQLAPI: let metadataEncoder = metadataContainer.superEncoder(forKey: .graphQLMetadataKey) try self.metadata.encode(to: metadataEncoder) + case .paypalAPI: + break } } } diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index 96fc46cafc..0251623d7a 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -269,8 +269,20 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate { return } - var headers: [String: String] = defaultHeaders + var headers: [String: String] var request: URLRequest + + if url.isPayPalURL { + headers = [:] + if case .authorizationFingerprint(let key) = clientAuthorization { + headers["Authorization"] = "Bearer \(key)" + } + } else { + headers = defaultHeaders + if case .tokenizationKey(let key) = clientAuthorization { + headers["Client-Key"] = key + } + } if method == "GET" || method == "DELETE" { if !isDataURL { @@ -302,10 +314,6 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate { request.httpBody = bodyData headers["Content-Type"] = "application/json; charset=utf-8" } - - if case .tokenizationKey(let key) = clientAuthorization { - headers["Client-Key"] = key - } request.allHTTPHeaderFields = headers request.httpMethod = method diff --git a/Sources/BraintreeCore/URL+IsPayPal.swift b/Sources/BraintreeCore/URL+IsPayPal.swift index 50399b3e0a..dd7bf14ae0 100644 --- a/Sources/BraintreeCore/URL+IsPayPal.swift +++ b/Sources/BraintreeCore/URL+IsPayPal.swift @@ -4,6 +4,6 @@ extension URL { /// Used to determine if a the URL is a paypal.com domain in order to format API requests accordingly var isPayPalURL: Bool { - absoluteString.contains("api-m.paypal.com") + absoluteString.contains("api-m.paypal.com") || absoluteString.contains("api-m.sandbox.paypal.com") } } diff --git a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift new file mode 100644 index 0000000000..2e4d02e492 --- /dev/null +++ b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift @@ -0,0 +1,11 @@ +import Foundation +import PassKit + +/// The POST body for `v2/payments/find-eligible-methods` +struct BTEligiblePaymentsRequest: Encodable { + + init() { + + } + +} diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 332e0510be..570b26f068 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -35,6 +35,10 @@ public class BTShopperInsightsClient { return BTShopperInsightsResult(isPayPalRecommended: true, isVenmoRecommended: true) } else { // TODO: - Make API call to PaymentReadyAPI. DTBTSDK-3176 + apiClient.post("/v2/payments/find-eligible-methods", parameters: BTEligiblePaymentsRequest(), httpType: .paypalAPI) { json, response, error in + print("HELLO") + } + return BTShopperInsightsResult() } } From 0e3f6fb4ce06cefdf15dd3f4adfb8433c95a74aa Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 10 Jan 2024 14:57:05 -0600 Subject: [PATCH 08/57] Add formatted POST body for EligiblePayments API call --- Braintree.xcodeproj/project.pbxproj | 2 +- .../BTEligiblePaymentsRequest.swift | 38 ++++++++++++++++++- .../BTShopperInsightsClient.swift | 16 ++++++-- .../BTShopperInsightsRequest.swift | 12 +++--- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 1e97dcba12..59c5cd8969 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -1316,11 +1316,11 @@ 804698292B27C4D70090878E /* BraintreeShopperInsights */ = { isa = PBXGroup; children = ( + 800ED77E2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift */, 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */, 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */, 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */, 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */, - 800ED77E2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift */, ); path = BraintreeShopperInsights; sourceTree = ""; diff --git a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift index 2e4d02e492..9028a69672 100644 --- a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift +++ b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift @@ -4,8 +4,42 @@ import PassKit /// The POST body for `v2/payments/find-eligible-methods` struct BTEligiblePaymentsRequest: Encodable { - init() { - + private let customer: Customer + private let purchaseUnits: [PurchaseUnit] + private let preferences = Preferences() + + struct Customer: Encodable { + let countryCode: String = "US" + let email: String? + let phone: Phone? } + struct PurchaseUnit: Encodable { + let payee: Payee + let amount = Amount() + + struct Amount: Encodable { + let currencyCode = "USD" + } + + struct Payee: Encodable { + let merchantID: String + } + } + + struct Preferences: Encodable { + let includeAccountDetails = true + let includeVaultTokens = true + let paymentSourceConstraint = PaymentSourceConstraint() + + struct PaymentSourceConstraint: Encodable { + let constraintType = "INCLUDE" + let paymentSources = ["PAYPAL", "VENMO"] + } + } + + init(email: String?, phone: Phone?, merchantID: String) { + self.customer = Customer(email: email, phone: phone) + self.purchaseUnits = [PurchaseUnit(payee: PurchaseUnit.Payee(merchantID: merchantID))] + } } diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 570b26f068..c9d2276bcd 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -34,9 +34,19 @@ public class BTShopperInsightsClient { if isVenmoAppInstalled() && isPayPalAppInstalled() { return BTShopperInsightsResult(isPayPalRecommended: true, isVenmoRecommended: true) } else { - // TODO: - Make API call to PaymentReadyAPI. DTBTSDK-3176 - apiClient.post("/v2/payments/find-eligible-methods", parameters: BTEligiblePaymentsRequest(), httpType: .paypalAPI) { json, response, error in - print("HELLO") + // TODO: - Fill in appropriate merchantID (or ppClientID) from config once API team decides what we need to send + let postParameters = BTEligiblePaymentsRequest( + email: request.email, + phone: request.phone, + merchantID: "TODO-merchant-id-type" + ) + + apiClient.post( + "/v2/payments/find-eligible-methods", + parameters: postParameters, + httpType: .paypalAPI + ) { json, response, error in + // TODO: - Handle API Response. DTBTSDK-3176 } return BTShopperInsightsResult() diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift index 4bda29d311..4d014d4d5e 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift @@ -4,10 +4,10 @@ import Foundation /// - Note: This feature is in beta. It's public API may change or be removed in future releases. public struct BTShopperInsightsRequest { - // MARK: - Private Properties + // MARK: - Internal Properties - private var email: String? - private var phone: Phone? + var email: String? + var phone: Phone? // MARK: - Initializers @@ -40,10 +40,10 @@ public struct BTShopperInsightsRequest { /// Buyer's phone number details for use with the Shopper Insights feature. /// - Note: This feature is in beta. It's public API may change or be removed in future releases. -public struct Phone { +public struct Phone: Encodable { - private let countryCode: String - private let nationalNumber: String + let countryCode: String + let nationalNumber: String /// Initialize a `BTShopperInsightsRequest.Phone`. /// - Parameters: From 037f5c25a1eac1354844ba4dc16fe25e3983f3e1 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 10 Jan 2024 16:44:39 -0600 Subject: [PATCH 09/57] Add unit test for eligible payments API call formatting --- .../BTEligiblePaymentsRequest.swift | 25 +++++++++++++++++ .../BTShopperInsightsRequest.swift | 9 +++++-- .../BTShopperInsightsClient_Tests.swift | 27 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift index 9028a69672..9637873c2c 100644 --- a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift +++ b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift @@ -12,6 +12,12 @@ struct BTEligiblePaymentsRequest: Encodable { let countryCode: String = "US" let email: String? let phone: Phone? + + enum CodingKeys: String, CodingKey { + case countryCode = "country_code" + case email = "email" + case phone = "phone" + } } struct PurchaseUnit: Encodable { @@ -20,10 +26,18 @@ struct BTEligiblePaymentsRequest: Encodable { struct Amount: Encodable { let currencyCode = "USD" + + enum CodingKeys: String, CodingKey { + case currencyCode = "currency_code" + } } struct Payee: Encodable { let merchantID: String + + enum CodingKeys: String, CodingKey { + case merchantID = "merchant_id" + } } } @@ -32,9 +46,20 @@ struct BTEligiblePaymentsRequest: Encodable { let includeVaultTokens = true let paymentSourceConstraint = PaymentSourceConstraint() + enum CodingKeys: String, CodingKey { + case includeAccountDetails = "include_account_details" + case includeVaultTokens = "include_vault_tokens" + case paymentSourceConstraint = "payment_source_constraint" + } + struct PaymentSourceConstraint: Encodable { let constraintType = "INCLUDE" let paymentSources = ["PAYPAL", "VENMO"] + + enum CodingKeys: String, CodingKey { + case constraintType = "constraint_type" + case paymentSources = "payment_sources" + } } } diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift index 4d014d4d5e..5d7f872a2a 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift @@ -42,8 +42,13 @@ public struct BTShopperInsightsRequest { /// - Note: This feature is in beta. It's public API may change or be removed in future releases. public struct Phone: Encodable { - let countryCode: String - let nationalNumber: String + private let countryCode: String + private let nationalNumber: String + + private enum CodingKeys: String, CodingKey { + case countryCode = "country_code" + case nationalNumber = "national_number" + } /// Initialize a `BTShopperInsightsRequest.Phone`. /// - Parameters: diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 3282d47a1f..6465e1b0ec 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -21,6 +21,8 @@ class BTShopperInsightsClient_Tests: XCTestCase { sut = BTShopperInsightsClient(apiClient: mockAPIClient) } + // MARK: - getRecommendedPaymentMethods() + func testGetRecommendedPaymentMethods_returnsDefaultRecommendations() async { let result = try? await sut.getRecommendedPaymentMethods(request: request) @@ -41,6 +43,31 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertTrue(result!.isVenmoRecommended) } + func testGetRecommendedPaymentMethods_whenAppsNotInstalled_callsEligiblePaymentsAPI() async { + _ = try? await sut.getRecommendedPaymentMethods(request: request) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v2/payments/find-eligible-methods") + XCTAssertEqual(mockAPIClient.lastPOSTAPIClientHTTPType, .paypalAPI) + + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + + let customer = lastPostParameters["customer"] as! [String: Any] + XCTAssertEqual((customer["country_code"] as! String), "US") + XCTAssertEqual((customer["email"] as! String), "my-email") + XCTAssertEqual((customer["phone"] as! [String: String])["country_code"], "1") + XCTAssertEqual((customer["phone"] as! [String: String])["national_number"], "1234567") + + let preferences = lastPostParameters["preferences"] as! [String: Any] + XCTAssertTrue(preferences["include_account_details"] as! Bool) + XCTAssertTrue(preferences["include_vault_tokens"] as! Bool) + let paymentSourceConstraint = preferences["payment_source_constraint"] as! [String: Any] + XCTAssertEqual(paymentSourceConstraint["constraint_type"] as! String, "INCLUDE") + XCTAssertEqual(paymentSourceConstraint["payment_sources"] as! [String], ["PAYPAL", "VENMO"]) + } + // MARK: - Analytics func testSendPayPalPresentedEvent_sendsAnalytic() { From fda9fe81a7500c4c691a3f451ca1307b820b03fc Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 10 Jan 2024 17:08:17 -0600 Subject: [PATCH 10/57] Update tests for BTAPIClient & BTHTTP changes --- .../xcshareddata/xcschemes/UnitTests.xcscheme | 26 ------------------- .../BTAPIClient_Tests.swift | 16 ++++++++++++ .../BraintreeCoreTests/BTHTTP_Tests.swift | 19 ++++++++++++++ 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme index c4e361af83..bfb2e65b41 100644 --- a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme +++ b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme @@ -5,22 +5,6 @@ - - - - - - - - - - Date: Wed, 10 Jan 2024 17:10:53 -0600 Subject: [PATCH 11/57] Cleanup xcodeproj --- Braintree.xcodeproj/project.pbxproj | 8 +++--- .../xcshareddata/xcschemes/UnitTests.xcscheme | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 59c5cd8969..920973f534 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -73,7 +73,7 @@ 57D94370296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */; }; 57D94372296CCA2F0079EAB1 /* String+NonceValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */; }; 800E78C429E0DD5300D1B0FC /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */; }; - 800ED77F2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800ED77E2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift */; }; + 800ED7832B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */; }; 800FC544257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */; }; 8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */; }; 804326BF2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */; }; @@ -737,7 +737,7 @@ 59A4135427B90CD7AE94D5CC /* Pods-Tests-IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests-IntegrationTests.debug.xcconfig"; path = "Target Support Files/Pods-Tests-IntegrationTests/Pods-Tests-IntegrationTests.debug.xcconfig"; sourceTree = ""; }; 800E23DC22206A8300C5D22E /* BTThreeDSecureAuthenticateJWT_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAuthenticateJWT_Tests.swift; sourceTree = ""; }; 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; - 800ED77E2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentsRequest.swift; sourceTree = ""; }; + 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentsRequest.swift; sourceTree = ""; }; 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePayCardNonce_Tests.swift; sourceTree = ""; }; 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsAnalytics.swift; sourceTree = ""; }; 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePaymentTokensRequest.swift; sourceTree = ""; }; @@ -1316,11 +1316,11 @@ 804698292B27C4D70090878E /* BraintreeShopperInsights */ = { isa = PBXGroup; children = ( - 800ED77E2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift */, 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */, 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */, 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */, 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */, + 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */, ); path = BraintreeShopperInsights; sourceTree = ""; @@ -2956,7 +2956,7 @@ files = ( 804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */, 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */, - 800ED77F2B4DFE9E007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */, + 800ED7832B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */, 8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */, 804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */, ); diff --git a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme index bfb2e65b41..c4e361af83 100644 --- a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme +++ b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme @@ -5,6 +5,22 @@ + + + + + + + + + + Date: Wed, 10 Jan 2024 17:28:52 -0600 Subject: [PATCH 12/57] Add purchase_unit POST body tests --- .../BTShopperInsightsClient_Tests.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 6465e1b0ec..78935e5980 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -66,6 +66,12 @@ class BTShopperInsightsClient_Tests: XCTestCase { let paymentSourceConstraint = preferences["payment_source_constraint"] as! [String: Any] XCTAssertEqual(paymentSourceConstraint["constraint_type"] as! String, "INCLUDE") XCTAssertEqual(paymentSourceConstraint["payment_sources"] as! [String], ["PAYPAL", "VENMO"]) + + let purchaseUnits = lastPostParameters["purchase_units"] as! [String: Any] + let payee = purchaseUnits["payee"] as! [String: String] + XCTAssertEqual(payee["merchant_id"], "TODO-merchant-id-type") + let amount = purchaseUnits["payee"] as! [String: String] + XCTAssertEqual(amount["currency_code"], "USD") } // MARK: - Analytics From 44a62a52c269e12b68f792349ac018619d294008 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 10 Jan 2024 17:33:10 -0600 Subject: [PATCH 13/57] Fixup - improper snake casing in POST body --- .../BTEligiblePaymentsRequest.swift | 6 ++++++ .../BTShopperInsightsClient_Tests.swift | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift index 9637873c2c..227d836a4f 100644 --- a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift +++ b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift @@ -8,6 +8,12 @@ struct BTEligiblePaymentsRequest: Encodable { private let purchaseUnits: [PurchaseUnit] private let preferences = Preferences() + enum CodingKeys: String, CodingKey { + case customer = "customer" + case purchaseUnits = "purchase_units" + case preferences = "preferences" + } + struct Customer: Encodable { let countryCode: String = "US" let email: String? diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 78935e5980..06b2bf37a5 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -67,10 +67,10 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertEqual(paymentSourceConstraint["constraint_type"] as! String, "INCLUDE") XCTAssertEqual(paymentSourceConstraint["payment_sources"] as! [String], ["PAYPAL", "VENMO"]) - let purchaseUnits = lastPostParameters["purchase_units"] as! [String: Any] - let payee = purchaseUnits["payee"] as! [String: String] + let purchaseUnits = lastPostParameters["purchase_units"] as! [[String: Any]] + let payee = purchaseUnits.first?["payee"] as! [String: String] XCTAssertEqual(payee["merchant_id"], "TODO-merchant-id-type") - let amount = purchaseUnits["payee"] as! [String: String] + let amount = purchaseUnits.first?["amount"] as! [String: String] XCTAssertEqual(amount["currency_code"], "USD") } From 35cf4f54323c87ea86ba9cab1f0ea5b58cc1bbe5 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Thu, 11 Jan 2024 16:59:48 -0600 Subject: [PATCH 14/57] PR Feedback - remove includeVaultTokens bool propery on API request --- .../BraintreeShopperInsights/BTEligiblePaymentsRequest.swift | 2 -- .../BTShopperInsightsClient_Tests.swift | 1 - 2 files changed, 3 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift index 227d836a4f..7ea1159571 100644 --- a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift +++ b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift @@ -49,12 +49,10 @@ struct BTEligiblePaymentsRequest: Encodable { struct Preferences: Encodable { let includeAccountDetails = true - let includeVaultTokens = true let paymentSourceConstraint = PaymentSourceConstraint() enum CodingKeys: String, CodingKey { case includeAccountDetails = "include_account_details" - case includeVaultTokens = "include_vault_tokens" case paymentSourceConstraint = "payment_source_constraint" } diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 06b2bf37a5..4925c44d7f 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -62,7 +62,6 @@ class BTShopperInsightsClient_Tests: XCTestCase { let preferences = lastPostParameters["preferences"] as! [String: Any] XCTAssertTrue(preferences["include_account_details"] as! Bool) - XCTAssertTrue(preferences["include_vault_tokens"] as! Bool) let paymentSourceConstraint = preferences["payment_source_constraint"] as! [String: Any] XCTAssertEqual(paymentSourceConstraint["constraint_type"] as! String, "INCLUDE") XCTAssertEqual(paymentSourceConstraint["payment_sources"] as! [String], ["PAYPAL", "VENMO"]) From fcb09d4ec2dc385e3850b0bf95498617673a4f89 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Tue, 16 Jan 2024 10:15:40 -0600 Subject: [PATCH 15/57] Update Sources/BraintreeCore/URL+IsPayPal.swift Co-authored-by: agedd <105314544+agedd@users.noreply.github.com> --- Sources/BraintreeCore/URL+IsPayPal.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraintreeCore/URL+IsPayPal.swift b/Sources/BraintreeCore/URL+IsPayPal.swift index dd7bf14ae0..2470b54745 100644 --- a/Sources/BraintreeCore/URL+IsPayPal.swift +++ b/Sources/BraintreeCore/URL+IsPayPal.swift @@ -2,7 +2,7 @@ import Foundation extension URL { - /// Used to determine if a the URL is a paypal.com domain in order to format API requests accordingly + /// Used to determine if the URL is a paypal.com domain in order to format API requests accordingly var isPayPalURL: Bool { absoluteString.contains("api-m.paypal.com") || absoluteString.contains("api-m.sandbox.paypal.com") } From d944ed6fb987650ac4984f24d0dcfd9ccb6a56a2 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Tue, 16 Jan 2024 11:35:10 -0600 Subject: [PATCH 16/57] PR Feedback - use payPal variable name instead of paypal --- Sources/BraintreeCore/BTAPIClient.swift | 12 ++++++------ Sources/BraintreeCore/BTAPIClientHTTPType.swift | 2 +- Sources/BraintreeCore/BTAPIRequest.swift | 2 +- .../BTShopperInsightsClient.swift | 2 +- .../BTShopperInsightsClient_Tests.swift | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 093293e5b8..bdf955aa39 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -26,7 +26,7 @@ import Foundation var http: BTHTTP? var graphQLHTTP: BTGraphQLHTTP? - var paypalHTTP: BTHTTP? + var payPalHTTP: BTHTTP? var session: URLSession { let configurationQueue: OperationQueue = OperationQueue() @@ -180,11 +180,11 @@ import Foundation } } - if paypalHTTP == nil { + if payPalHTTP == nil { let paypalBaseURL: URL? = paypalAPIURL(forEnvironment: configuration?.environment ?? "") if let clientToken, let paypalBaseURL { - paypalHTTP = BTHTTP(url: paypalBaseURL, authorizationFingerprint: clientToken.authorizationFingerprint) + payPalHTTP = BTHTTP(url: paypalBaseURL, authorizationFingerprint: clientToken.authorizationFingerprint) } } } @@ -366,7 +366,7 @@ import Foundation return parameters?.merging(["_meta": metadata.parameters]) { $1 } case .graphQLAPI: return parameters?.merging(["clientSdkMetadata": metadata.parameters]) { $1 } - case .paypalAPI: + case .payPalAPI: return parameters } } @@ -496,8 +496,8 @@ import Foundation return http case .graphQLAPI: return graphQLHTTP - case .paypalAPI: - return paypalHTTP + case .payPalAPI: + return payPalHTTP } } } diff --git a/Sources/BraintreeCore/BTAPIClientHTTPType.swift b/Sources/BraintreeCore/BTAPIClientHTTPType.swift index c14c0001e7..5af2d054ce 100644 --- a/Sources/BraintreeCore/BTAPIClientHTTPType.swift +++ b/Sources/BraintreeCore/BTAPIClientHTTPType.swift @@ -10,5 +10,5 @@ import Foundation case graphQLAPI /// Use the PayPal API - case paypalAPI + case payPalAPI } diff --git a/Sources/BraintreeCore/BTAPIRequest.swift b/Sources/BraintreeCore/BTAPIRequest.swift index bdef2bf041..dab3958834 100644 --- a/Sources/BraintreeCore/BTAPIRequest.swift +++ b/Sources/BraintreeCore/BTAPIRequest.swift @@ -34,7 +34,7 @@ struct BTAPIRequest: Encodable { case .graphQLAPI: let metadataEncoder = metadataContainer.superEncoder(forKey: .graphQLMetadataKey) try self.metadata.encode(to: metadataEncoder) - case .paypalAPI: + case .payPalAPI: break } } diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index c9d2276bcd..9aae114658 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -44,7 +44,7 @@ public class BTShopperInsightsClient { apiClient.post( "/v2/payments/find-eligible-methods", parameters: postParameters, - httpType: .paypalAPI + httpType: .payPalAPI ) { json, response, error in // TODO: - Handle API Response. DTBTSDK-3176 } diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 4925c44d7f..bf4bf7238c 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -47,7 +47,7 @@ class BTShopperInsightsClient_Tests: XCTestCase { _ = try? await sut.getRecommendedPaymentMethods(request: request) XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v2/payments/find-eligible-methods") - XCTAssertEqual(mockAPIClient.lastPOSTAPIClientHTTPType, .paypalAPI) + XCTAssertEqual(mockAPIClient.lastPOSTAPIClientHTTPType, .payPalAPI) guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { XCTFail() From c446f79eeb9b580908b7d460d136a8aa8a156800 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 17 Jan 2024 15:14:13 -0600 Subject: [PATCH 17/57] Fixup - update JIRA ticket # for parsing result --- Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 9aae114658..2b33d07fd8 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -46,7 +46,7 @@ public class BTShopperInsightsClient { parameters: postParameters, httpType: .payPalAPI ) { json, response, error in - // TODO: - Handle API Response. DTBTSDK-3176 + // TODO: - Handle API Response. DTBTSDK-3388 } return BTShopperInsightsResult() From 335b594f5f61bfec610f8897c3fa96aa708c92f1 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Thu, 18 Jan 2024 09:42:40 -0600 Subject: [PATCH 18/57] PR Feedback - prefer payPal camelcase syntax & add unit test for PayPal API prod env setting --- Sources/BraintreeCore/BTAPIClient.swift | 4 ++-- .../BraintreeCoreTests/BTAPIClient_Tests.swift | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index bdf955aa39..6b36cf4d83 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -181,7 +181,7 @@ import Foundation } if payPalHTTP == nil { - let paypalBaseURL: URL? = paypalAPIURL(forEnvironment: configuration?.environment ?? "") + let paypalBaseURL: URL? = payPalAPIURL(forEnvironment: configuration?.environment ?? "") if let clientToken, let paypalBaseURL { payPalHTTP = BTHTTP(url: paypalBaseURL, authorizationFingerprint: clientToken.authorizationFingerprint) @@ -481,7 +481,7 @@ import Foundation return components.url } - func paypalAPIURL(forEnvironment environment: String) -> URL? { + func payPalAPIURL(forEnvironment environment: String) -> URL? { if environment.caseInsensitiveCompare("sandbox") == .orderedSame || environment.caseInsensitiveCompare("development") == .orderedSame { return URL(string: "https://api-m.sandbox.paypal.com") diff --git a/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift b/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift index 6249bf6115..70ef8d2a44 100644 --- a/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift +++ b/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift @@ -586,19 +586,25 @@ class BTAPIClient_Tests: XCTestCase { XCTAssertEqual(sandboxURL?.absoluteString, "https://payments.braintree-api.com/graphql") } - func testPayPalBaseURLForEnvironment_returnsSandbox() { + func testPayPalBaseURLForEnvironment_returnsSandboxURL() { let apiClientSand = BTAPIClient(authorization: "development_tokenization_key") - let baseURLSand: URL? = apiClientSand?.paypalAPIURL(forEnvironment: "sandbox") + let baseURLSand: URL? = apiClientSand?.payPalAPIURL(forEnvironment: "sandbox") XCTAssertEqual(baseURLSand?.absoluteString, "https://api-m.sandbox.paypal.com") let apiClientDev = BTAPIClient(authorization: "development_tokenization_key") - let baseURLDev: URL? = apiClientDev?.paypalAPIURL(forEnvironment: "development") + let baseURLDev: URL? = apiClientDev?.payPalAPIURL(forEnvironment: "development") XCTAssertEqual(baseURLDev?.absoluteString, "https://api-m.sandbox.paypal.com") } + func testPayPalBaseURLForEnvironment_returnsProductionURL() { + let apiClientSand = BTAPIClient(authorization: "development_tokenization_key") + let baseURLSand: URL? = apiClientSand?.payPalAPIURL(forEnvironment: "production") + XCTAssertEqual(baseURLSand?.absoluteString, "https://api-m.paypal.com") + } + func testPayPalBaseURLForEnvironment_returnsProductionURL_asDefault() { let apiClient = BTAPIClient(authorization: "development_tokenization_key") - let baseURL: URL? = apiClient?.paypalAPIURL(forEnvironment: "unknown") + let baseURL: URL? = apiClient?.payPalAPIURL(forEnvironment: "unknown") XCTAssertEqual(baseURL?.absoluteString, "https://api-m.paypal.com") } } From 36e8b36f1a150f0266d10b64530a7cf9bcef5fd4 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Thu, 18 Jan 2024 15:45:29 -0600 Subject: [PATCH 19/57] Add SDK triggered analytic strings & send from BTShopperInsightsClient.getRecommendedPaymentMethods() Signed-off-by: Alekhya Geddam --- .../BTShopperInsightsAnalytics.swift | 6 +++++ .../BTShopperInsightsClient.swift | 24 +++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsAnalytics.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsAnalytics.swift index 1c7305c3a0..0f8d385405 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsAnalytics.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsAnalytics.swift @@ -8,4 +8,10 @@ enum BTShopperInsightsAnalytics { static let paypalSelected = "shopper-insights:paypal-selected" static let venmoPresented = "shopper-insights:venmo-presented" static let venmoSelected = "shopper-insights:venmo-selected" + + // MARK: - SDK Triggered Events + + static let recommendedPaymentsStarted = "shopper-insights:get-recommended-payments:started" + static let recommendedPaymentsSucceeded = "shopper-insights:get-recommended-payments:succeeded" + static let recommendedPaymentsFailed = "shopper-insights:get-recommended-payments:failed" } diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 2b33d07fd8..a9917745c6 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -31,8 +31,12 @@ public class BTShopperInsightsClient { /// - Returns: A `BTShopperInsightsResult` instance /// - Note: This feature is in beta. It's public API may change or be removed in future releases. public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult { + apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsStarted) + if isVenmoAppInstalled() && isPayPalAppInstalled() { - return BTShopperInsightsResult(isPayPalRecommended: true, isVenmoRecommended: true) + let result = BTShopperInsightsResult(isPayPalRecommended: true, isVenmoRecommended: true) + notifySuccess(with: result) + return result } else { // TODO: - Fill in appropriate merchantID (or ppClientID) from config once API team decides what we need to send let postParameters = BTEligiblePaymentsRequest( @@ -45,7 +49,13 @@ public class BTShopperInsightsClient { "/v2/payments/find-eligible-methods", parameters: postParameters, httpType: .payPalAPI - ) { json, response, error in + ) { json, _, error in + Task { + if let error { + self.notifyFailure(with: error) + throw error + } + } // TODO: - Handle API Response. DTBTSDK-3388 } @@ -88,4 +98,14 @@ public class BTShopperInsightsClient { let paypalURL = URL(string: "paypal://")! return application.canOpenURL(paypalURL) } + + // MARK: - Analytics Helper Methods + + private func notifySuccess(with result: BTShopperInsightsResult) { + apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsSucceeded) + } + + private func notifyFailure(with error: Error) { + apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsFailed, errorDescription: error.localizedDescription) + } } From 296691aa606aa4cf5b802ba5f292a99159b4abfe Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:54:28 -0600 Subject: [PATCH 20/57] [ShopperInsights] Add EligiblePayments API Call (#1171) - Update `BTAPIClient` & `BTHTTP` to support API calls to paypal.com APIs - Add `BTEligiblePaymentsRequest` struct to house POST body for `v2/payments/find-eligible-methods` API call - Add relevant unit tests - Leave TODO for parsing result of API call --- Braintree.xcodeproj/project.pbxproj | 12 ++- Sources/BraintreeCore/BTAPIClient.swift | 24 +++++- .../BraintreeCore/BTAPIClientHTTPType.swift | 3 + Sources/BraintreeCore/BTAPIRequest.swift | 2 + Sources/BraintreeCore/BTHTTP.swift | 18 +++-- Sources/BraintreeCore/URL+IsPayPal.swift | 4 +- .../BTEligiblePaymentsRequest.swift | 74 +++++++++++++++++++ .../BTShopperInsightsClient.swift | 16 +++- .../BTShopperInsightsRequest.swift | 13 +++- .../BTAPIClient_Tests.swift | 22 ++++++ .../BraintreeCoreTests/BTHTTP_Tests.swift | 19 +++++ .../BTShopperInsightsClient_Tests.swift | 32 ++++++++ 12 files changed, 222 insertions(+), 17 deletions(-) create mode 100644 Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 845ed845f1..920973f534 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -73,13 +73,14 @@ 57D94370296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */; }; 57D94372296CCA2F0079EAB1 /* String+NonceValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */; }; 800E78C429E0DD5300D1B0FC /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */; }; + 800ED7832B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */; }; 800FC544257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */; }; 8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */; }; + 804326BF2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */; }; 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */; }; 804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */; }; 804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */; }; 804698422B27C5530090878E /* BraintreeShopperInsights.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 804698302B27C5340090878E /* BraintreeShopperInsights.framework */; platformFilter = ios; }; - 804326BF2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */; }; 80482F8029D39A1D007E5F50 /* BTThreeDSecureRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F7F29D39A1D007E5F50 /* BTThreeDSecureRequest.swift */; }; 80482F8229D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F8129D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift */; }; 80482F8429D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F8329D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift */; }; @@ -93,12 +94,12 @@ 8053F05929FB2F700076F988 /* URL+IsPayPal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8053F05829FB2F700076F988 /* URL+IsPayPal.swift */; }; 80581A8C25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581A8B25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift */; }; 80581B1D2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */; }; + 8075CBEE2B1B735200CA6265 /* BTAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */; }; + 80A6C6192B21205900416D50 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */; }; 80AB424F2B27CAC200249218 /* BraintreeTestShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A903E1A624F9D34000C314E1 /* BraintreeTestShared.framework */; platformFilter = ios; }; 80AB42542B27DF5B00249218 /* BraintreeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; platformFilter = ios; }; 80AB42552B27DF5B00249218 /* BraintreeCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 80B59A132B27C7FC004C2FA3 /* BTShopperInsightsClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */; }; - 8075CBEE2B1B735200CA6265 /* BTAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */; }; - 80A6C6192B21205900416D50 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */; }; 80BA3C292B23892700900BBB /* FakeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA3C282B23892700900BBB /* FakeRequest.swift */; }; 80BA64AC29D788E000E15264 /* BTLocalPaymentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64AB29D788E000E15264 /* BTLocalPaymentResult.swift */; }; 80BA64B229D7937E00E15264 /* BTLocalPaymentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64B129D7937E00E15264 /* BTLocalPaymentRequest.swift */; }; @@ -736,12 +737,13 @@ 59A4135427B90CD7AE94D5CC /* Pods-Tests-IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests-IntegrationTests.debug.xcconfig"; path = "Target Support Files/Pods-Tests-IntegrationTests/Pods-Tests-IntegrationTests.debug.xcconfig"; sourceTree = ""; }; 800E23DC22206A8300C5D22E /* BTThreeDSecureAuthenticateJWT_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAuthenticateJWT_Tests.swift; sourceTree = ""; }; 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; + 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentsRequest.swift; sourceTree = ""; }; 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePayCardNonce_Tests.swift; sourceTree = ""; }; 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsAnalytics.swift; sourceTree = ""; }; + 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePaymentTokensRequest.swift; sourceTree = ""; }; 804698302B27C5340090878E /* BraintreeShopperInsights.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BraintreeShopperInsights.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8046983E2B27C5530090878E /* BraintreeShopperInsightsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BraintreeShopperInsightsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 804698482B27C71C0090878E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePaymentTokensRequest.swift; sourceTree = ""; }; 80482F7F29D39A1D007E5F50 /* BTThreeDSecureRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureRequest.swift; sourceTree = ""; }; 80482F8129D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureRequestDelegate.swift; sourceTree = ""; }; 80482F8329D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAccountType.swift; sourceTree = ""; }; @@ -1318,6 +1320,7 @@ 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */, 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */, 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */, + 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */, ); path = BraintreeShopperInsights; sourceTree = ""; @@ -2953,6 +2956,7 @@ files = ( 804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */, 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */, + 800ED7832B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */, 8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */, 804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */, ); diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 9264307d38..6b36cf4d83 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -26,7 +26,8 @@ import Foundation var http: BTHTTP? var graphQLHTTP: BTGraphQLHTTP? - + var payPalHTTP: BTHTTP? + var session: URLSession { let configurationQueue: OperationQueue = OperationQueue() configurationQueue.name = "com.braintreepayments.BTAPIClient" @@ -178,6 +179,14 @@ import Foundation graphQLHTTP = BTGraphQLHTTP(url: graphQLBaseURL, tokenizationKey: tokenizationKey) } } + + if payPalHTTP == nil { + let paypalBaseURL: URL? = payPalAPIURL(forEnvironment: configuration?.environment ?? "") + + if let clientToken, let paypalBaseURL { + payPalHTTP = BTHTTP(url: paypalBaseURL, authorizationFingerprint: clientToken.authorizationFingerprint) + } + } } completion(configuration, nil) @@ -357,6 +366,8 @@ import Foundation return parameters?.merging(["_meta": metadata.parameters]) { $1 } case .graphQLAPI: return parameters?.merging(["clientSdkMetadata": metadata.parameters]) { $1 } + case .payPalAPI: + return parameters } } @@ -469,6 +480,15 @@ import Foundation components.path = "/graphql" return components.url } + + func payPalAPIURL(forEnvironment environment: String) -> URL? { + if environment.caseInsensitiveCompare("sandbox") == .orderedSame || + environment.caseInsensitiveCompare("development") == .orderedSame { + return URL(string: "https://api-m.sandbox.paypal.com") + } else { + return URL(string: "https://api-m.paypal.com") + } + } func http(for httpType: BTAPIClientHTTPService) -> BTHTTP? { switch httpType { @@ -476,6 +496,8 @@ import Foundation return http case .graphQLAPI: return graphQLHTTP + case .payPalAPI: + return payPalHTTP } } } diff --git a/Sources/BraintreeCore/BTAPIClientHTTPType.swift b/Sources/BraintreeCore/BTAPIClientHTTPType.swift index ff35ddd8a8..5af2d054ce 100644 --- a/Sources/BraintreeCore/BTAPIClientHTTPType.swift +++ b/Sources/BraintreeCore/BTAPIClientHTTPType.swift @@ -8,4 +8,7 @@ import Foundation /// Use the GraphQL API case graphQLAPI + + /// Use the PayPal API + case payPalAPI } diff --git a/Sources/BraintreeCore/BTAPIRequest.swift b/Sources/BraintreeCore/BTAPIRequest.swift index 0d76e1bb9a..dab3958834 100644 --- a/Sources/BraintreeCore/BTAPIRequest.swift +++ b/Sources/BraintreeCore/BTAPIRequest.swift @@ -34,6 +34,8 @@ struct BTAPIRequest: Encodable { case .graphQLAPI: let metadataEncoder = metadataContainer.superEncoder(forKey: .graphQLMetadataKey) try self.metadata.encode(to: metadataEncoder) + case .payPalAPI: + break } } } diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index 96fc46cafc..0251623d7a 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -269,8 +269,20 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate { return } - var headers: [String: String] = defaultHeaders + var headers: [String: String] var request: URLRequest + + if url.isPayPalURL { + headers = [:] + if case .authorizationFingerprint(let key) = clientAuthorization { + headers["Authorization"] = "Bearer \(key)" + } + } else { + headers = defaultHeaders + if case .tokenizationKey(let key) = clientAuthorization { + headers["Client-Key"] = key + } + } if method == "GET" || method == "DELETE" { if !isDataURL { @@ -302,10 +314,6 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate { request.httpBody = bodyData headers["Content-Type"] = "application/json; charset=utf-8" } - - if case .tokenizationKey(let key) = clientAuthorization { - headers["Client-Key"] = key - } request.allHTTPHeaderFields = headers request.httpMethod = method diff --git a/Sources/BraintreeCore/URL+IsPayPal.swift b/Sources/BraintreeCore/URL+IsPayPal.swift index 50399b3e0a..2470b54745 100644 --- a/Sources/BraintreeCore/URL+IsPayPal.swift +++ b/Sources/BraintreeCore/URL+IsPayPal.swift @@ -2,8 +2,8 @@ import Foundation extension URL { - /// Used to determine if a the URL is a paypal.com domain in order to format API requests accordingly + /// Used to determine if the URL is a paypal.com domain in order to format API requests accordingly var isPayPalURL: Bool { - absoluteString.contains("api-m.paypal.com") + absoluteString.contains("api-m.paypal.com") || absoluteString.contains("api-m.sandbox.paypal.com") } } diff --git a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift new file mode 100644 index 0000000000..7ea1159571 --- /dev/null +++ b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift @@ -0,0 +1,74 @@ +import Foundation +import PassKit + +/// The POST body for `v2/payments/find-eligible-methods` +struct BTEligiblePaymentsRequest: Encodable { + + private let customer: Customer + private let purchaseUnits: [PurchaseUnit] + private let preferences = Preferences() + + enum CodingKeys: String, CodingKey { + case customer = "customer" + case purchaseUnits = "purchase_units" + case preferences = "preferences" + } + + struct Customer: Encodable { + let countryCode: String = "US" + let email: String? + let phone: Phone? + + enum CodingKeys: String, CodingKey { + case countryCode = "country_code" + case email = "email" + case phone = "phone" + } + } + + struct PurchaseUnit: Encodable { + let payee: Payee + let amount = Amount() + + struct Amount: Encodable { + let currencyCode = "USD" + + enum CodingKeys: String, CodingKey { + case currencyCode = "currency_code" + } + } + + struct Payee: Encodable { + let merchantID: String + + enum CodingKeys: String, CodingKey { + case merchantID = "merchant_id" + } + } + } + + struct Preferences: Encodable { + let includeAccountDetails = true + let paymentSourceConstraint = PaymentSourceConstraint() + + enum CodingKeys: String, CodingKey { + case includeAccountDetails = "include_account_details" + case paymentSourceConstraint = "payment_source_constraint" + } + + struct PaymentSourceConstraint: Encodable { + let constraintType = "INCLUDE" + let paymentSources = ["PAYPAL", "VENMO"] + + enum CodingKeys: String, CodingKey { + case constraintType = "constraint_type" + case paymentSources = "payment_sources" + } + } + } + + init(email: String?, phone: Phone?, merchantID: String) { + self.customer = Customer(email: email, phone: phone) + self.purchaseUnits = [PurchaseUnit(payee: PurchaseUnit.Payee(merchantID: merchantID))] + } +} diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 332e0510be..2b33d07fd8 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -34,7 +34,21 @@ public class BTShopperInsightsClient { if isVenmoAppInstalled() && isPayPalAppInstalled() { return BTShopperInsightsResult(isPayPalRecommended: true, isVenmoRecommended: true) } else { - // TODO: - Make API call to PaymentReadyAPI. DTBTSDK-3176 + // TODO: - Fill in appropriate merchantID (or ppClientID) from config once API team decides what we need to send + let postParameters = BTEligiblePaymentsRequest( + email: request.email, + phone: request.phone, + merchantID: "TODO-merchant-id-type" + ) + + apiClient.post( + "/v2/payments/find-eligible-methods", + parameters: postParameters, + httpType: .payPalAPI + ) { json, response, error in + // TODO: - Handle API Response. DTBTSDK-3388 + } + return BTShopperInsightsResult() } } diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift index 4bda29d311..5d7f872a2a 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift @@ -4,10 +4,10 @@ import Foundation /// - Note: This feature is in beta. It's public API may change or be removed in future releases. public struct BTShopperInsightsRequest { - // MARK: - Private Properties + // MARK: - Internal Properties - private var email: String? - private var phone: Phone? + var email: String? + var phone: Phone? // MARK: - Initializers @@ -40,11 +40,16 @@ public struct BTShopperInsightsRequest { /// Buyer's phone number details for use with the Shopper Insights feature. /// - Note: This feature is in beta. It's public API may change or be removed in future releases. -public struct Phone { +public struct Phone: Encodable { private let countryCode: String private let nationalNumber: String + private enum CodingKeys: String, CodingKey { + case countryCode = "country_code" + case nationalNumber = "national_number" + } + /// Initialize a `BTShopperInsightsRequest.Phone`. /// - Parameters: /// - countryCode: The buyer's country code prefix to the national telephone number. An identifier for a specific country. Must not contain special characters. diff --git a/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift b/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift index 3d2c5bfa46..70ef8d2a44 100644 --- a/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift +++ b/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift @@ -585,4 +585,26 @@ class BTAPIClient_Tests: XCTestCase { let sandboxURL: URL? = apiClient?.graphQLURL(forEnvironment: "unknown") XCTAssertEqual(sandboxURL?.absoluteString, "https://payments.braintree-api.com/graphql") } + + func testPayPalBaseURLForEnvironment_returnsSandboxURL() { + let apiClientSand = BTAPIClient(authorization: "development_tokenization_key") + let baseURLSand: URL? = apiClientSand?.payPalAPIURL(forEnvironment: "sandbox") + XCTAssertEqual(baseURLSand?.absoluteString, "https://api-m.sandbox.paypal.com") + + let apiClientDev = BTAPIClient(authorization: "development_tokenization_key") + let baseURLDev: URL? = apiClientDev?.payPalAPIURL(forEnvironment: "development") + XCTAssertEqual(baseURLDev?.absoluteString, "https://api-m.sandbox.paypal.com") + } + + func testPayPalBaseURLForEnvironment_returnsProductionURL() { + let apiClientSand = BTAPIClient(authorization: "development_tokenization_key") + let baseURLSand: URL? = apiClientSand?.payPalAPIURL(forEnvironment: "production") + XCTAssertEqual(baseURLSand?.absoluteString, "https://api-m.paypal.com") + } + + func testPayPalBaseURLForEnvironment_returnsProductionURL_asDefault() { + let apiClient = BTAPIClient(authorization: "development_tokenization_key") + let baseURL: URL? = apiClient?.payPalAPIURL(forEnvironment: "unknown") + XCTAssertEqual(baseURL?.absoluteString, "https://api-m.paypal.com") + } } diff --git a/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift b/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift index 0b2d728eb0..da65fb6f72 100644 --- a/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift +++ b/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift @@ -503,6 +503,25 @@ final class BTHTTP_Tests: XCTestCase { waitForExpectations(timeout: 2) } + + func testPOSTRequests_whenBTHTTPInitializedWithPayPalAPIURL_sendsAuthorizationInHeader() { + let expectation = expectation(description: "POST callback") + + let http = BTHTTP(url: URL(string: "https://api-m.paypal.com")!, authorizationFingerprint: "some-fingerprint") + http.session = testURLSession + + http.post("200.json") { body, response, error in + XCTAssertNotNil(body) + XCTAssertNotNil(response) + XCTAssertNil(error) + + let httpRequest = BTHTTPTestProtocol.parseRequestFromTestResponseBody(body!) + XCTAssertEqual(httpRequest.allHTTPHeaderFields?["Authorization"], "Bearer some-fingerprint") + expectation.fulfill() + } + + waitForExpectations(timeout: 2) + } func testPOSTRequests_whenBTHTTPInitializedWithTokenizationKey_sendAuthorization() { http = BTHTTP(url: BTHTTPTestProtocol.testBaseURL(), tokenizationKey: "development_tokenization_key") diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 3282d47a1f..bf4bf7238c 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -21,6 +21,8 @@ class BTShopperInsightsClient_Tests: XCTestCase { sut = BTShopperInsightsClient(apiClient: mockAPIClient) } + // MARK: - getRecommendedPaymentMethods() + func testGetRecommendedPaymentMethods_returnsDefaultRecommendations() async { let result = try? await sut.getRecommendedPaymentMethods(request: request) @@ -41,6 +43,36 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertTrue(result!.isVenmoRecommended) } + func testGetRecommendedPaymentMethods_whenAppsNotInstalled_callsEligiblePaymentsAPI() async { + _ = try? await sut.getRecommendedPaymentMethods(request: request) + + XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v2/payments/find-eligible-methods") + XCTAssertEqual(mockAPIClient.lastPOSTAPIClientHTTPType, .payPalAPI) + + guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { + XCTFail() + return + } + + let customer = lastPostParameters["customer"] as! [String: Any] + XCTAssertEqual((customer["country_code"] as! String), "US") + XCTAssertEqual((customer["email"] as! String), "my-email") + XCTAssertEqual((customer["phone"] as! [String: String])["country_code"], "1") + XCTAssertEqual((customer["phone"] as! [String: String])["national_number"], "1234567") + + let preferences = lastPostParameters["preferences"] as! [String: Any] + XCTAssertTrue(preferences["include_account_details"] as! Bool) + let paymentSourceConstraint = preferences["payment_source_constraint"] as! [String: Any] + XCTAssertEqual(paymentSourceConstraint["constraint_type"] as! String, "INCLUDE") + XCTAssertEqual(paymentSourceConstraint["payment_sources"] as! [String], ["PAYPAL", "VENMO"]) + + let purchaseUnits = lastPostParameters["purchase_units"] as! [[String: Any]] + let payee = purchaseUnits.first?["payee"] as! [String: String] + XCTAssertEqual(payee["merchant_id"], "TODO-merchant-id-type") + let amount = purchaseUnits.first?["amount"] as! [String: String] + XCTAssertEqual(amount["currency_code"], "USD") + } + // MARK: - Analytics func testSendPayPalPresentedEvent_sendsAnalytic() { From 21385ba34ae7e990b31cc697d629dbf348af302c Mon Sep 17 00:00:00 2001 From: ageddam Date: Fri, 19 Jan 2024 12:17:33 -0600 Subject: [PATCH 21/57] added basic analytics tests Co-authored-by: Sammy Cannillo --- Braintree.xcodeproj/project.pbxproj | 4 ++++ Podfile.lock | 2 +- .../BTShopperInsightsAnalytics_Tests.swift | 15 +++++++++++++++ .../BTShopperInsightsClient_Tests.swift | 2 ++ 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsAnalytics_Tests.swift diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 920973f534..73f5607aeb 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ 57D9436E2968A8080079EAB1 /* BTPayPalLocaleCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9436D2968A8080079EAB1 /* BTPayPalLocaleCode.swift */; }; 57D94370296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */; }; 57D94372296CCA2F0079EAB1 /* String+NonceValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */; }; + 62970D102B5AF2C000BAB584 /* BTShopperInsightsAnalytics_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */; }; 800E78C429E0DD5300D1B0FC /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */; }; 800ED7832B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */; }; 800FC544257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */; }; @@ -735,6 +736,7 @@ 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BraintreePayPal_IntegrationTests.swift; sourceTree = ""; }; 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+NonceValidation.swift"; sourceTree = ""; }; 59A4135427B90CD7AE94D5CC /* Pods-Tests-IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests-IntegrationTests.debug.xcconfig"; path = "Target Support Files/Pods-Tests-IntegrationTests/Pods-Tests-IntegrationTests.debug.xcconfig"; sourceTree = ""; }; + 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsAnalytics_Tests.swift; sourceTree = ""; }; 800E23DC22206A8300C5D22E /* BTThreeDSecureAuthenticateJWT_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAuthenticateJWT_Tests.swift; sourceTree = ""; }; 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentsRequest.swift; sourceTree = ""; }; @@ -1329,6 +1331,7 @@ isa = PBXGroup; children = ( 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */, + 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */, 804698482B27C71C0090878E /* Info.plist */, ); path = BraintreeShopperInsightsTests; @@ -2967,6 +2970,7 @@ buildActionMask = 2147483647; files = ( 80B59A132B27C7FC004C2FA3 /* BTShopperInsightsClient_Tests.swift in Sources */, + 62970D102B5AF2C000BAB584 /* BTShopperInsightsAnalytics_Tests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Podfile.lock b/Podfile.lock index e185e7e5d0..fcfb8736fa 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -37,4 +37,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 224cc2d6666b79d3d6adb45cccd6dfbc6fe74d18 -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.2 diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsAnalytics_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsAnalytics_Tests.swift new file mode 100644 index 0000000000..40599bf5a5 --- /dev/null +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsAnalytics_Tests.swift @@ -0,0 +1,15 @@ +import XCTest +@testable import BraintreeShopperInsights + +final class BTShopperInsightsAnalytics_Tests: XCTestCase { + + func test_recommendedPaymentAnalyticEvents_sendExpectedEventNames() { + XCTAssertEqual(BTShopperInsightsAnalytics.paypalPresented, "shopper-insights:paypal-presented") + XCTAssertEqual(BTShopperInsightsAnalytics.paypalSelected, "shopper-insights:paypal-selected") + XCTAssertEqual(BTShopperInsightsAnalytics.venmoPresented, "shopper-insights:venmo-presented") + XCTAssertEqual(BTShopperInsightsAnalytics.venmoSelected, "shopper-insights:venmo-selected") + XCTAssertEqual(BTShopperInsightsAnalytics.recommendedPaymentsStarted, "shopper-insights:get-recommended-payments:started") + XCTAssertEqual(BTShopperInsightsAnalytics.recommendedPaymentsSucceeded, "shopper-insights:get-recommended-payments:succeeded") + XCTAssertEqual(BTShopperInsightsAnalytics.recommendedPaymentsFailed, "shopper-insights:get-recommended-payments:failed") + } +} diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index bf4bf7238c..2fb9bef732 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -41,6 +41,8 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertTrue(result!.isPayPalRecommended) XCTAssertTrue(result!.isVenmoRecommended) + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents[mockAPIClient.postedAnalyticsEvents.count-2], "shopper-insights:get-recommended-payments:started") + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "shopper-insights:get-recommended-payments:succeeded") } func testGetRecommendedPaymentMethods_whenAppsNotInstalled_callsEligiblePaymentsAPI() async { From 071d4803f6449d10bb5dec697c406864b869d795 Mon Sep 17 00:00:00 2001 From: ageddam Date: Mon, 22 Jan 2024 10:11:15 -0600 Subject: [PATCH 22/57] more analytic updates --- .../BTShopperInsightsClient.swift | 1 + .../BTShopperInsightsClient_Tests.swift | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index a9917745c6..50d73e00a6 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -56,6 +56,7 @@ public class BTShopperInsightsClient { throw error } } + self.notifySuccess(with: BTShopperInsightsResult()) // TODO: - Handle API Response. DTBTSDK-3388 } diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 2fb9bef732..089342cc79 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -28,6 +28,8 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertNotNil(result!.isPayPalRecommended) XCTAssertNotNil(result!.isVenmoRecommended) + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents[mockAPIClient.postedAnalyticsEvents.count-2], "shopper-insights:get-recommended-payments:started") + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "shopper-insights:get-recommended-payments:succeeded") } func testGetRecommendedPaymentMethods_whenBothAppsInstalled_returnsTrue() async { @@ -75,6 +77,16 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertEqual(amount["currency_code"], "USD") } + func testGetRecommendedPaymentMethods_whenAppsNotInstalled_callsEligiblePaymentsAPI_returnsError() async { + mockAPIClient.cannedResponseBody = nil + do { + let _ = try await sut.getRecommendedPaymentMethods(request: request) + } catch { + let error = error as NSError + XCTAssertNotNil(error) + } + } + // MARK: - Analytics func testSendPayPalPresentedEvent_sendsAnalytic() { From 539c8e8401020ae33ec70388422f790c13648569 Mon Sep 17 00:00:00 2001 From: ageddam Date: Mon, 22 Jan 2024 10:35:28 -0600 Subject: [PATCH 23/57] cleanup --- .../BTShopperInsightsClient_Tests.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 089342cc79..121e82fd9a 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -77,16 +77,6 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertEqual(amount["currency_code"], "USD") } - func testGetRecommendedPaymentMethods_whenAppsNotInstalled_callsEligiblePaymentsAPI_returnsError() async { - mockAPIClient.cannedResponseBody = nil - do { - let _ = try await sut.getRecommendedPaymentMethods(request: request) - } catch { - let error = error as NSError - XCTAssertNotNil(error) - } - } - // MARK: - Analytics func testSendPayPalPresentedEvent_sendsAnalytic() { From 4d3d2d6a704c28bf816e3d89b108dcc99ca32468 Mon Sep 17 00:00:00 2001 From: ageddam Date: Mon, 22 Jan 2024 10:48:05 -0600 Subject: [PATCH 24/57] revert podfile.lock update --- Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile.lock b/Podfile.lock index fcfb8736fa..e185e7e5d0 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -37,4 +37,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 224cc2d6666b79d3d6adb45cccd6dfbc6fe74d18 -COCOAPODS: 1.14.2 +COCOAPODS: 1.12.1 From 849b17c014f735ea87a99d3947dc6f8f24b6112f Mon Sep 17 00:00:00 2001 From: ageddam Date: Mon, 22 Jan 2024 10:53:32 -0600 Subject: [PATCH 25/57] address pr feedback --- .../BTShopperInsightsClient_Tests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 121e82fd9a..6ac5fe42b7 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -28,7 +28,7 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertNotNil(result!.isPayPalRecommended) XCTAssertNotNil(result!.isVenmoRecommended) - XCTAssertEqual(mockAPIClient.postedAnalyticsEvents[mockAPIClient.postedAnalyticsEvents.count-2], "shopper-insights:get-recommended-payments:started") + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:get-recommended-payments:started") XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "shopper-insights:get-recommended-payments:succeeded") } @@ -43,7 +43,7 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertTrue(result!.isPayPalRecommended) XCTAssertTrue(result!.isVenmoRecommended) - XCTAssertEqual(mockAPIClient.postedAnalyticsEvents[mockAPIClient.postedAnalyticsEvents.count-2], "shopper-insights:get-recommended-payments:started") + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:get-recommended-payments:started") XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "shopper-insights:get-recommended-payments:succeeded") } From a29756160275030f202b97edc5f4f2bc276d920b Mon Sep 17 00:00:00 2001 From: ageddam Date: Mon, 22 Jan 2024 11:55:16 -0600 Subject: [PATCH 26/57] cleanup --- .../BraintreeShopperInsights/BTShopperInsightsClient.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 50d73e00a6..3146d56c99 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -55,8 +55,10 @@ public class BTShopperInsightsClient { self.notifyFailure(with: error) throw error } + let result = BTShopperInsightsResult() + self.notifySuccess(with: result) + return result } - self.notifySuccess(with: BTShopperInsightsResult()) // TODO: - Handle API Response. DTBTSDK-3388 } From 235c6994f3c4b5373db25f0f1cd46770e6a38fdd Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:04:41 -0600 Subject: [PATCH 27/57] Drop -m suffix from `api.paypal.com` URL (#1175) --- .../Analytics/BTAnalyticsService.swift | 7 ++----- Sources/BraintreeCore/BTAPIClient.swift | 4 ++-- Sources/BraintreeCore/BTCoreConstants.swift | 4 ++++ Sources/BraintreeCore/URL+IsPayPal.swift | 3 ++- .../Analytics/BTAnalyticsService_Tests.swift | 2 +- .../BraintreeCoreTests/BTAPIClient_Tests.swift | 16 ++++++++-------- UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift | 4 ++-- 7 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index 1c78d834dd..89b1c1788f 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -28,9 +28,6 @@ class BTAnalyticsService: Equatable { /// are sent from only one session. In practice, BTAPIClient.metadata.sessionID should never change, so this /// is defensive. var analyticsSessions: [String: BTAnalyticsSession] = [:] - - /// The FPTI URL to post all analytic events. - static let url = URL(string: "https://api-m.paypal.com")! private let apiClient: BTAPIClient @@ -81,9 +78,9 @@ class BTAnalyticsService: Equatable { // TODO: - Refactor to make HTTP non-optional property and instantiate in init() if self.http == nil { if let clientToken = self.apiClient.clientToken { - self.http = BTHTTP(url: BTAnalyticsService.url, authorizationFingerprint: clientToken.authorizationFingerprint) + self.http = BTHTTP(url: BTCoreConstants.payPalProductionURL, authorizationFingerprint: clientToken.authorizationFingerprint) } else if let tokenizationKey = self.apiClient.tokenizationKey { - self.http = BTHTTP(url: BTAnalyticsService.url, tokenizationKey: tokenizationKey) + self.http = BTHTTP(url: BTCoreConstants.payPalProductionURL, tokenizationKey: tokenizationKey) } else { completion(BTAnalyticsServiceError.invalidAPIClient) return diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 6b36cf4d83..71364b6462 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -484,9 +484,9 @@ import Foundation func payPalAPIURL(forEnvironment environment: String) -> URL? { if environment.caseInsensitiveCompare("sandbox") == .orderedSame || environment.caseInsensitiveCompare("development") == .orderedSame { - return URL(string: "https://api-m.sandbox.paypal.com") + return BTCoreConstants.payPalSandboxURL } else { - return URL(string: "https://api-m.paypal.com") + return BTCoreConstants.payPalProductionURL } } diff --git a/Sources/BraintreeCore/BTCoreConstants.swift b/Sources/BraintreeCore/BTCoreConstants.swift index b5ee1a6bb2..d282f85621 100644 --- a/Sources/BraintreeCore/BTCoreConstants.swift +++ b/Sources/BraintreeCore/BTCoreConstants.swift @@ -14,6 +14,10 @@ import Foundation static let graphQLVersion: String = "2018-03-06" + static let payPalProductionURL = URL(string: "https://api.paypal.com")! + + static let payPalSandboxURL = URL(string: "https://api.sandbox.paypal.com")! + // MARK: - BTHTTPError Constants /// :nodoc: This property is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. diff --git a/Sources/BraintreeCore/URL+IsPayPal.swift b/Sources/BraintreeCore/URL+IsPayPal.swift index 2470b54745..2bf7461210 100644 --- a/Sources/BraintreeCore/URL+IsPayPal.swift +++ b/Sources/BraintreeCore/URL+IsPayPal.swift @@ -4,6 +4,7 @@ extension URL { /// Used to determine if the URL is a paypal.com domain in order to format API requests accordingly var isPayPalURL: Bool { - absoluteString.contains("api-m.paypal.com") || absoluteString.contains("api-m.sandbox.paypal.com") + absoluteString.contains(BTCoreConstants.payPalProductionURL.absoluteString) || + absoluteString.contains(BTCoreConstants.payPalSandboxURL.absoluteString) } } diff --git a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift index 6c13214be2..73ec74129b 100644 --- a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift @@ -20,7 +20,7 @@ final class BTAnalyticsService_Tests: XCTestCase { let expectation = expectation(description: "Sends analytics event") analyticsService.sendAnalyticsEvent("any.analytics.event") { error in XCTAssertNil(error) - XCTAssertEqual(analyticsService.http?.baseURL.absoluteString, "https://api-m.paypal.com") + XCTAssertEqual(analyticsService.http?.baseURL.absoluteString, "https://api.paypal.com") expectation.fulfill() } diff --git a/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift b/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift index 70ef8d2a44..137909d8f7 100644 --- a/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift +++ b/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift @@ -587,24 +587,24 @@ class BTAPIClient_Tests: XCTestCase { } func testPayPalBaseURLForEnvironment_returnsSandboxURL() { - let apiClientSand = BTAPIClient(authorization: "development_tokenization_key") + let apiClientSand = BTAPIClient(authorization: "sandbox_tokenization_key") let baseURLSand: URL? = apiClientSand?.payPalAPIURL(forEnvironment: "sandbox") - XCTAssertEqual(baseURLSand?.absoluteString, "https://api-m.sandbox.paypal.com") + XCTAssertEqual(baseURLSand?.absoluteString, "https://api.sandbox.paypal.com") - let apiClientDev = BTAPIClient(authorization: "development_tokenization_key") + let apiClientDev = BTAPIClient(authorization: "sandbox_tokenization_key") let baseURLDev: URL? = apiClientDev?.payPalAPIURL(forEnvironment: "development") - XCTAssertEqual(baseURLDev?.absoluteString, "https://api-m.sandbox.paypal.com") + XCTAssertEqual(baseURLDev?.absoluteString, "https://api.sandbox.paypal.com") } func testPayPalBaseURLForEnvironment_returnsProductionURL() { - let apiClientSand = BTAPIClient(authorization: "development_tokenization_key") + let apiClientSand = BTAPIClient(authorization: "production_tokenization_key") let baseURLSand: URL? = apiClientSand?.payPalAPIURL(forEnvironment: "production") - XCTAssertEqual(baseURLSand?.absoluteString, "https://api-m.paypal.com") + XCTAssertEqual(baseURLSand?.absoluteString, "https://api.paypal.com") } func testPayPalBaseURLForEnvironment_returnsProductionURL_asDefault() { - let apiClient = BTAPIClient(authorization: "development_tokenization_key") + let apiClient = BTAPIClient(authorization: "production_tokenization_key") let baseURL: URL? = apiClient?.payPalAPIURL(forEnvironment: "unknown") - XCTAssertEqual(baseURL?.absoluteString, "https://api-m.paypal.com") + XCTAssertEqual(baseURL?.absoluteString, "https://api.paypal.com") } } diff --git a/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift b/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift index da65fb6f72..7b0b337a80 100644 --- a/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift +++ b/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift @@ -488,7 +488,7 @@ final class BTHTTP_Tests: XCTestCase { func testPOSTRequests_whenBTHTTPInitializedWithPayPalAPIURL_doesNotSendAuthorizationInBody() { let expectation = expectation(description: "POST callback") - let http = BTHTTP(url: URL(string: "https://api-m.paypal.com")!, authorizationFingerprint: "test-authorization-fingerprint") + let http = BTHTTP(url: URL(string: "https://api.paypal.com")!, authorizationFingerprint: "test-authorization-fingerprint") http.session = testURLSession http.post("200.json") { body, response, error in @@ -507,7 +507,7 @@ final class BTHTTP_Tests: XCTestCase { func testPOSTRequests_whenBTHTTPInitializedWithPayPalAPIURL_sendsAuthorizationInHeader() { let expectation = expectation(description: "POST callback") - let http = BTHTTP(url: URL(string: "https://api-m.paypal.com")!, authorizationFingerprint: "some-fingerprint") + let http = BTHTTP(url: URL(string: "https://api.paypal.com")!, authorizationFingerprint: "some-fingerprint") http.session = testURLSession http.post("200.json") { body, response, error in From 5c16b60b274c998158563c2f6a4080e2aa991891 Mon Sep 17 00:00:00 2001 From: ageddam Date: Tue, 23 Jan 2024 11:18:25 -0600 Subject: [PATCH 28/57] address pr comments --- Braintree.xcodeproj/project.pbxproj | 2 +- .../BraintreeShopperInsights/BTShopperInsightsClient.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 73f5607aeb..b8515d4b3a 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -1330,8 +1330,8 @@ 8046982A2B27C4E80090878E /* BraintreeShopperInsightsTests */ = { isa = PBXGroup; children = ( - 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */, 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */, + 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */, 804698482B27C71C0090878E /* Info.plist */, ); path = BraintreeShopperInsightsTests; diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 3146d56c99..22709144fd 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -35,8 +35,7 @@ public class BTShopperInsightsClient { if isVenmoAppInstalled() && isPayPalAppInstalled() { let result = BTShopperInsightsResult(isPayPalRecommended: true, isVenmoRecommended: true) - notifySuccess(with: result) - return result + return notifySuccess(with: result) } else { // TODO: - Fill in appropriate merchantID (or ppClientID) from config once API team decides what we need to send let postParameters = BTEligiblePaymentsRequest( @@ -104,8 +103,9 @@ public class BTShopperInsightsClient { // MARK: - Analytics Helper Methods - private func notifySuccess(with result: BTShopperInsightsResult) { + private func notifySuccess(with result: BTShopperInsightsResult) -> BTShopperInsightsResult { apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsSucceeded) + return result } private func notifyFailure(with error: Error) { From d6be4805c5ac4da974f01efde119cacf15547d78 Mon Sep 17 00:00:00 2001 From: ageddam Date: Tue, 23 Jan 2024 11:48:28 -0600 Subject: [PATCH 29/57] fix warning --- Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 22709144fd..91150f8d9f 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -55,8 +55,7 @@ public class BTShopperInsightsClient { throw error } let result = BTShopperInsightsResult() - self.notifySuccess(with: result) - return result + return self.notifySuccess(with: result) } // TODO: - Handle API Response. DTBTSDK-3388 } From 6c379bed90d41b5d74d91f4bcff661803013e592 Mon Sep 17 00:00:00 2001 From: ageddam Date: Tue, 23 Jan 2024 13:05:44 -0600 Subject: [PATCH 30/57] cleanup --- .../BraintreeShopperInsights/BTShopperInsightsClient.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 91150f8d9f..c52366acd2 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -51,8 +51,7 @@ public class BTShopperInsightsClient { ) { json, _, error in Task { if let error { - self.notifyFailure(with: error) - throw error + try self.notifyFailure(with: error) } let result = BTShopperInsightsResult() return self.notifySuccess(with: result) @@ -107,7 +106,8 @@ public class BTShopperInsightsClient { return result } - private func notifyFailure(with error: Error) { + private func notifyFailure(with error: Error) throws { apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsFailed, errorDescription: error.localizedDescription) + throw error } } From 90e015c5b73e698f74743a62a588f0a2c6446ac7 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Fri, 26 Jan 2024 05:08:34 -0600 Subject: [PATCH 31/57] [Shopper Insights] Fix return flow bug (#1180) --- Sources/BraintreeCore/BTAPIClient.swift | 25 ++++++++++++++++++ .../BTShopperInsightsClient.swift | 25 +++++++----------- .../BTShopperInsightsClient_Tests.swift | 26 +++++++++++++++++++ 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 71364b6462..b2ea1b1be2 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -346,6 +346,31 @@ import Foundation } } + /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. + /// + /// Perfom an HTTP POST on a URL composed of the configured from environment and the given path. + /// - Parameters: + /// - path: The endpoint URI path. + /// - parameters: Optional set of query parameters to be encoded with the request. + /// - httpType: The underlying `BTAPIClientHTTPService` of the HTTP request. Defaults to `.gateway`. + /// - Returns: On success, `(BTJSON?, HTTPURLResponse?)` will contain the JSON body response and the HTTP response. + @_documentation(visibility: private) + public func post( + _ path: String, + parameters: Encodable, + httpType: BTAPIClientHTTPService = .gateway + ) async throws -> (BTJSON?, HTTPURLResponse?) { + try await withCheckedThrowingContinuation { continuation in + post(path, parameters: parameters, httpType: httpType) { json, httpResonse, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: (json, httpResonse)) + } + } + } + } + /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. @_documentation(visibility: private) public func sendAnalyticsEvent(_ eventName: String, errorDescription: String? = nil, correlationID: String? = nil) { diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index c52366acd2..3687f902d7 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -44,22 +44,15 @@ public class BTShopperInsightsClient { merchantID: "TODO-merchant-id-type" ) - apiClient.post( - "/v2/payments/find-eligible-methods", - parameters: postParameters, - httpType: .payPalAPI - ) { json, _, error in - Task { - if let error { - try self.notifyFailure(with: error) - } - let result = BTShopperInsightsResult() - return self.notifySuccess(with: result) - } + do { + let (_, _) = try await apiClient.post("/v2/payments/find-eligible-methods", parameters: postParameters, httpType: .payPalAPI) + // TODO: - Handle API Response. DTBTSDK-3388 + let result = BTShopperInsightsResult() + return self.notifySuccess(with: result) + } catch { + throw self.notifyFailure(with: error) } - - return BTShopperInsightsResult() } } @@ -106,8 +99,8 @@ public class BTShopperInsightsClient { return result } - private func notifyFailure(with error: Error) throws { + private func notifyFailure(with error: Error) -> Error { apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsFailed, errorDescription: error.localizedDescription) - throw error + return error } } diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 6ac5fe42b7..4a60a439d9 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -77,6 +77,32 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertEqual(amount["currency_code"], "USD") } + func testGetRecommendedPaymentMethods_whenAPIError_throws() async { + mockAPIClient.cannedResponseError = NSError(domain: "fake-error-domain", code: 123, userInfo: [NSLocalizedDescriptionKey:"fake-error-description"]) + + do { + _ = try await sut.getRecommendedPaymentMethods(request: request) + XCTFail("Expected error to be thrown.") + } catch let error as NSError { + XCTAssertEqual(error.code, 123) + XCTAssertEqual(error.localizedDescription, "fake-error-description") + XCTAssertEqual(error.domain, "fake-error-domain") + + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last, "shopper-insights:get-recommended-payments:failed") + } + } + + func testGetRecommendedPaymentMethods_whenAPISuccess_returnsResult() async { + // TODO: - Elaborate test once parsing logic added + do { + let result = try await sut.getRecommendedPaymentMethods(request: request) + XCTAssertNotNil(result) + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last, "shopper-insights:get-recommended-payments:succeeded") + } catch let error as NSError { + XCTFail("An error was not expected.") + } + } + // MARK: - Analytics func testSendPayPalPresentedEvent_sendsAnalytic() { From 61d9d958112fe6c090f7e90b5aa4378f899f172b Mon Sep 17 00:00:00 2001 From: agedd <105314544+agedd@users.noreply.github.com> Date: Wed, 31 Jan 2024 13:27:42 -0600 Subject: [PATCH 32/57] [ShopperInsights] Add Email & Phone Text Fields to Demo App (#1178) * create shopper insights demo feature (fields for email and phone number) * address pr comments + remove unused button helper method * address pr comments --- .../Features/Helpers/TextFieldWithLabel.swift | 55 +++++++++++++ .../ShopperInsightsViewController.swift | 79 +++++++++++++++++-- Demo/Demo.xcodeproj/project.pbxproj | 4 + 3 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 Demo/Application/Features/Helpers/TextFieldWithLabel.swift diff --git a/Demo/Application/Features/Helpers/TextFieldWithLabel.swift b/Demo/Application/Features/Helpers/TextFieldWithLabel.swift new file mode 100644 index 0000000000..653dd9bebb --- /dev/null +++ b/Demo/Application/Features/Helpers/TextFieldWithLabel.swift @@ -0,0 +1,55 @@ +import UIKit + +class TextFieldWithLabel: UIView { + + // MARK: - Views + + lazy var label: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.lineBreakMode = .byWordWrapping + label.numberOfLines = 0 + return label + }() + + lazy var textField: UITextField = { + let textField = UITextField() + textField.translatesAutoresizingMaskIntoConstraints = false + textField.borderStyle = .roundedRect + textField.clearButtonMode = .never + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + return textField + }() + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + configureStackView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Layout + + func configureStackView() { + let stackView = UIStackView(arrangedSubviews: [label, textField]) + stackView.axis = .vertical + stackView.spacing = 5 + stackView.distribution = .fillEqually + stackView.translatesAutoresizingMaskIntoConstraints = false + + addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + stackView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + stackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + ]) + } +} + diff --git a/Demo/Application/Features/ShopperInsightsViewController.swift b/Demo/Application/Features/ShopperInsightsViewController.swift index f62332e2e7..4cb48c30ec 100644 --- a/Demo/Application/Features/ShopperInsightsViewController.swift +++ b/Demo/Application/Features/ShopperInsightsViewController.swift @@ -10,14 +10,81 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { lazy var paypalClient = BTPayPalClient(apiClient: apiClient) lazy var venmoClient = BTVenmoClient(apiClient: apiClient) - lazy var shopperInsightsButton = createButton(title: "Fetch shopper insights", action: #selector(shopperInsightsButtonTapped)) lazy var payPalCheckoutButton = createButton(title: "PayPal Checkout", action: #selector(payPalCheckoutButtonTapped)) lazy var payPalVaultButton = createButton(title: "PayPal Vault", action: #selector(payPalVaultButtonTapped)) lazy var venmoButton = createButton(title: "Venmo", action: #selector(venmoButtonTapped)) + lazy var emailView: TextFieldWithLabel = { + let view = TextFieldWithLabel() + view.label.text = "Email" + view.textField.placeholder = "Email" + return view + }() + + lazy var countryCodeView: TextFieldWithLabel = { + let view = TextFieldWithLabel() + view.label.text = "Country Code" + view.textField.placeholder = "Country Code" + return view + }() + + lazy var nationalNumberView: TextFieldWithLabel = { + let view = TextFieldWithLabel() + view.label.text = "National Number" + view.textField.placeholder = "National Number" + return view + }() + + lazy var shopperInsightsButton = createButton(title: "Fetch Shopper Insights", action: #selector(shopperInsightsButtonTapped)) + + lazy var shopperInsightsInputView: UIStackView = { + let stackView = UIStackView( + arrangedSubviews: [ + emailView, + countryCodeView, + nationalNumberView, + ] + ) + stackView.axis = .vertical + stackView.spacing = 10 + stackView.distribution = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.isLayoutMarginsRelativeArrangement = true + stackView.layoutMargins = UIEdgeInsets( + top: 16.0, + left: 16.0, + bottom: 16.0, + right: 16.0 + ) + stackView.insetsLayoutMarginsFromSafeArea = false + return stackView + }() + + override func viewDidLoad() { + super.viewDidLoad() + + view.addSubview(shopperInsightsInputView) + view.addSubview(shopperInsightsButton) + + NSLayoutConstraint.activate( + [ + shopperInsightsInputView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + shopperInsightsInputView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + shopperInsightsInputView.topAnchor.constraint(equalTo: view.topAnchor), + shopperInsightsInputView.widthAnchor.constraint(equalTo: view.widthAnchor), + shopperInsightsButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40), + shopperInsightsButton.topAnchor.constraint(equalTo: shopperInsightsInputView.bottomAnchor, constant: 10), + shopperInsightsButton.widthAnchor.constraint( + equalTo: shopperInsightsInputView.widthAnchor, + multiplier: 0.8 + ), + ] + ) + } + override func createPaymentButton() -> UIView { - let buttons = [shopperInsightsButton, payPalCheckoutButton, payPalVaultButton, venmoButton] - buttons[1...3].forEach { $0.isEnabled = false } + let buttons = [payPalCheckoutButton, payPalVaultButton, venmoButton] + buttons.forEach { $0.isEnabled = false } let stackView = UIStackView(arrangedSubviews: buttons) stackView.axis = .vertical stackView.alignment = .center @@ -31,10 +98,10 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { self.progressBlock("Fetching shopper insights...") let request = BTShopperInsightsRequest( - email: "my-email@gmail.com", + email: emailView.textField.text ?? "", phone: Phone( - countryCode: "1", - nationalNumber: "1234567" + countryCode: countryCodeView.textField.text ?? "", + nationalNumber: nationalNumberView.textField.text ?? "" ) ) Task { diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index c81251b3d8..6916b48b87 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 57108A152832E789004EB870 /* PayPalNativeCheckoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57108A142832E789004EB870 /* PayPalNativeCheckoutViewController.swift */; }; 57108A172832EA04004EB870 /* BraintreePayPalNativeCheckout.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57108A162832EA04004EB870 /* BraintreePayPalNativeCheckout.framework */; }; 57108A182832EA04004EB870 /* BraintreePayPalNativeCheckout.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 57108A162832EA04004EB870 /* BraintreePayPalNativeCheckout.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 62240F5A2B67FE1100ECE5C9 /* TextFieldWithLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62240F592B67FE1100ECE5C9 /* TextFieldWithLabel.swift */; }; 8028B9762B28C9E100C88CE8 /* ShopperInsightsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8028B9752B28C9E100C88CE8 /* ShopperInsightsViewController.swift */; }; 8028B9782B28D42400C88CE8 /* BraintreeShopperInsights.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8028B9772B28D42400C88CE8 /* BraintreeShopperInsights.framework */; }; 8028B9792B28D42400C88CE8 /* BraintreeShopperInsights.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8028B9772B28D42400C88CE8 /* BraintreeShopperInsights.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -137,6 +138,7 @@ 570B93D32853A6D30041BAFE /* BraintreeCoreSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BraintreeCoreSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57108A142832E789004EB870 /* PayPalNativeCheckoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalNativeCheckoutViewController.swift; sourceTree = ""; }; 57108A162832EA04004EB870 /* BraintreePayPalNativeCheckout.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BraintreePayPalNativeCheckout.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 62240F592B67FE1100ECE5C9 /* TextFieldWithLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldWithLabel.swift; sourceTree = ""; }; 6D23244B5E9EE59BAB3F3003 /* Pods_Demo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Demo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 73498B4265CA7D315E2FBF38 /* Pods-Demo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.debug.xcconfig"; path = "Target Support Files/Pods-Demo/Pods-Demo.debug.xcconfig"; sourceTree = ""; }; 8028B9752B28C9E100C88CE8 /* ShopperInsightsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShopperInsightsViewController.swift; sourceTree = ""; }; @@ -277,6 +279,7 @@ 803FB9DD26D93146002BF92D /* BTCardFormView.swift */, BEAAAD042970A70D000BD296 /* BTSEPADirectDebitTestHelper.swift */, BED461822AD072D9001B0DDF /* CardHelpers.swift */, + 62240F592B67FE1100ECE5C9 /* TextFieldWithLabel.swift */, ); path = Helpers; sourceTree = ""; @@ -659,6 +662,7 @@ 8028B9762B28C9E100C88CE8 /* ShopperInsightsViewController.swift in Sources */, BEAAAA342A98E5E6001ECA63 /* IdealViewController.swift in Sources */, BE994B0D2AD838A500470773 /* PaymentButtonBaseViewController.swift in Sources */, + 62240F5A2B67FE1100ECE5C9 /* TextFieldWithLabel.swift in Sources */, BED7C4CA2ABDD35700EF8550 /* PayPalWebCheckoutViewController.swift in Sources */, BEBD52872AABA513005D6687 /* ThreeDSecureViewController.swift in Sources */, 803FB9DE26D93146002BF92D /* BTCardFormView.swift in Sources */, From 8e474092ac3b15db0f8d07e2e32d3cd4531e7018 Mon Sep 17 00:00:00 2001 From: agedd <105314544+agedd@users.noreply.github.com> Date: Thu, 1 Feb 2024 14:08:16 -0600 Subject: [PATCH 33/57] [DTBTSDK 3415] Parse Shopper Insights API Response (#1184) * add api handling logic and update unit test * add dummy test data * fix failing test * cleanup * fix build error * revert podfile changes * cleanup * fix typo * cleanup and update unit test * fix build errors * fix typo --- Braintree.xcodeproj/project.pbxproj | 8 +++++ .../BTEligiblePaymentMethods.swift | 29 +++++++++++++++++++ .../BTShopperInsightsClient.swift | 28 ++++++++++++++---- .../BTShopperInsightsError.swift | 22 ++++++++++++++ .../BTShopperInsightsClient_Tests.swift | 24 +++++++++++---- 5 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 Sources/BraintreeShopperInsights/BTEligiblePaymentMethods.swift create mode 100644 Sources/BraintreeShopperInsights/BTShopperInsightsError.swift diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index b8515d4b3a..d83bba9579 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -72,7 +72,9 @@ 57D9436E2968A8080079EAB1 /* BTPayPalLocaleCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9436D2968A8080079EAB1 /* BTPayPalLocaleCode.swift */; }; 57D94370296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */; }; 57D94372296CCA2F0079EAB1 /* String+NonceValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */; }; + 624B27F72B6AE0C2000AC08A /* BTShopperInsightsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */; }; 62970D102B5AF2C000BAB584 /* BTShopperInsightsAnalytics_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */; }; + 62EA90492B63071800DD79BC /* BTEligiblePaymentMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */; }; 800E78C429E0DD5300D1B0FC /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */; }; 800ED7832B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */; }; 800FC544257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */; }; @@ -736,7 +738,9 @@ 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BraintreePayPal_IntegrationTests.swift; sourceTree = ""; }; 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+NonceValidation.swift"; sourceTree = ""; }; 59A4135427B90CD7AE94D5CC /* Pods-Tests-IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests-IntegrationTests.debug.xcconfig"; path = "Target Support Files/Pods-Tests-IntegrationTests/Pods-Tests-IntegrationTests.debug.xcconfig"; sourceTree = ""; }; + 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsError.swift; sourceTree = ""; }; 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsAnalytics_Tests.swift; sourceTree = ""; }; + 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentMethods.swift; sourceTree = ""; }; 800E23DC22206A8300C5D22E /* BTThreeDSecureAuthenticateJWT_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAuthenticateJWT_Tests.swift; sourceTree = ""; }; 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentsRequest.swift; sourceTree = ""; }; @@ -1323,6 +1327,8 @@ 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */, 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */, 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */, + 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */, + 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */, ); path = BraintreeShopperInsights; sourceTree = ""; @@ -2958,9 +2964,11 @@ buildActionMask = 2147483647; files = ( 804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */, + 624B27F72B6AE0C2000AC08A /* BTShopperInsightsError.swift in Sources */, 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */, 800ED7832B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */, 8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */, + 62EA90492B63071800DD79BC /* BTEligiblePaymentMethods.swift in Sources */, 804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/BraintreeShopperInsights/BTEligiblePaymentMethods.swift b/Sources/BraintreeShopperInsights/BTEligiblePaymentMethods.swift new file mode 100644 index 0000000000..90ce49cfef --- /dev/null +++ b/Sources/BraintreeShopperInsights/BTEligiblePaymentMethods.swift @@ -0,0 +1,29 @@ +#if canImport(BraintreeCore) +import BraintreeCore +#endif + +struct BTEligiblePaymentMethods { + var paypal: BTEligiblePaymentMethodDetails? + var venmo: BTEligiblePaymentMethodDetails? + + init(json: BTJSON?) { + if let eligibileMethodsJSON = json?["eligible_methods"] { + self.paypal = BTEligiblePaymentMethodDetails(json: eligibileMethodsJSON["paypal"]) + self.venmo = BTEligiblePaymentMethodDetails(json: eligibileMethodsJSON["venmo"]) + } + } +} + +struct BTEligiblePaymentMethodDetails { + let canBeVaulted: Bool + let eligibleInPaypalNetwork: Bool + let recommended: Bool + let recommendedPriority: Int + + init(json: BTJSON) { + self.canBeVaulted = json["can_be_vaulted"].asBool() ?? false + self.eligibleInPaypalNetwork = json["eligible_in_paypal_network"].asBool() ?? false + self.recommended = json["recommended"].asBool() ?? false + self.recommendedPriority = json["recommended_priority"].asIntegerOrZero() + } +} diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 3687f902d7..e8248845b1 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -41,14 +41,20 @@ public class BTShopperInsightsClient { let postParameters = BTEligiblePaymentsRequest( email: request.email, phone: request.phone, - merchantID: "TODO-merchant-id-type" + merchantID: "MXSJ4F5BADVNS" ) do { - let (_, _) = try await apiClient.post("/v2/payments/find-eligible-methods", parameters: postParameters, httpType: .payPalAPI) - - // TODO: - Handle API Response. DTBTSDK-3388 - let result = BTShopperInsightsResult() + let (json, _) = try await apiClient.post("/v2/payments/find-eligible-methods", parameters: postParameters, httpType: .payPalAPI) + guard let eligibleMethodsJSON = json?["eligible_methods"].asDictionary(), + eligibleMethodsJSON.count != 0 else { + throw self.notifyFailure(with: BTShopperInsightsError.emptyBodyReturned) + } + let eligiblePaymentMethods = BTEligiblePaymentMethods(json: json) + let result = BTShopperInsightsResult( + isPayPalRecommended: isPaymentRecommended(eligiblePaymentMethods.paypal), + isVenmoRecommended: isPaymentRecommended(eligiblePaymentMethods.venmo) + ) return self.notifySuccess(with: result) } catch { throw self.notifyFailure(with: error) @@ -56,6 +62,18 @@ public class BTShopperInsightsClient { } } + /// This method determines whether a payment source is recommended + /// - Parameters: + /// - paymentMethodDetail: a `BTEligiblePaymentMethodDetails` containing the payment source's information + /// - Returns: `true` if both `eligibleInPPNetwork` and `recommended` are enabled, otherwise returns false. + private func isPaymentRecommended(_ paymentMethodDetail: BTEligiblePaymentMethodDetails?) -> Bool { + if let eligibleInPPNetwork = paymentMethodDetail?.eligibleInPaypalNetwork, + let recommended = paymentMethodDetail?.recommended { + return eligibleInPPNetwork && recommended + } + return false + } + /// Call this method when the PayPal button has been successfully displayed to the buyer. /// This method sends analytics to help improve the Shopper Insights feature experience. public func sendPayPalPresentedEvent() { diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsError.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsError.swift new file mode 100644 index 0000000000..712d2995ff --- /dev/null +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsError.swift @@ -0,0 +1,22 @@ +import Foundation + +public enum BTShopperInsightsError: Int, Error, CustomNSError, LocalizedError, Equatable { + + /// 1. A nil body was returned from the payment method request and no error was returned. + case emptyBodyReturned + + public static var errorDomain: String { + "com.braintreepayments.BTShopperInsightsErrorDomain" + } + + public var errorCode: Int { + rawValue + } + + public var errorDescription: String? { + switch self { + case .emptyBodyReturned: + return "An empty body was returned from the Eligible Payments API during the request." + } + } +} diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 4a60a439d9..bb9f62df9e 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -2,6 +2,7 @@ import Foundation import XCTest @testable import BraintreeTestShared @testable import BraintreeShopperInsights +@testable import BraintreeCore class BTShopperInsightsClient_Tests: XCTestCase { @@ -26,10 +27,9 @@ class BTShopperInsightsClient_Tests: XCTestCase { func testGetRecommendedPaymentMethods_returnsDefaultRecommendations() async { let result = try? await sut.getRecommendedPaymentMethods(request: request) - XCTAssertNotNil(result!.isPayPalRecommended) - XCTAssertNotNil(result!.isVenmoRecommended) + XCTAssertNil(result) XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:get-recommended-payments:started") - XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "shopper-insights:get-recommended-payments:succeeded") + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "shopper-insights:get-recommended-payments:failed") } func testGetRecommendedPaymentMethods_whenBothAppsInstalled_returnsTrue() async { @@ -72,7 +72,7 @@ class BTShopperInsightsClient_Tests: XCTestCase { let purchaseUnits = lastPostParameters["purchase_units"] as! [[String: Any]] let payee = purchaseUnits.first?["payee"] as! [String: String] - XCTAssertEqual(payee["merchant_id"], "TODO-merchant-id-type") + XCTAssertEqual(payee["merchant_id"], "MXSJ4F5BADVNS") let amount = purchaseUnits.first?["amount"] as! [String: String] XCTAssertEqual(amount["currency_code"], "USD") } @@ -93,10 +93,24 @@ class BTShopperInsightsClient_Tests: XCTestCase { } func testGetRecommendedPaymentMethods_whenAPISuccess_returnsResult() async { - // TODO: - Elaborate test once parsing logic added do { + let mockEligiblePaymentMethodResponse = BTJSON( + value: [ + "eligible_methods": [ + "venmo": [ + "can_be_vaulted": true, + "eligible_in_paypal_network": true, + "recommended": true, + "recommended_priority": 1 + ] + ] + ] + ) + mockAPIClient.cannedResponseBody = mockEligiblePaymentMethodResponse let result = try await sut.getRecommendedPaymentMethods(request: request) XCTAssertNotNil(result) + XCTAssertTrue(result.isVenmoRecommended) + XCTAssertFalse(result.isPayPalRecommended) XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last, "shopper-insights:get-recommended-payments:succeeded") } catch let error as NSError { XCTFail("An error was not expected.") From c3da6e1fe4c465ec5249363e417355b70d841362 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Tue, 6 Feb 2024 11:31:58 -0600 Subject: [PATCH 34/57] [ShopperInsights] Add default email & phone values to ShopperInsights Demo (#1185) --- .../ShopperInsightsViewController.swift | 20 ++++--------------- .../BTShopperInsightsRequest.swift | 2 +- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/Demo/Application/Features/ShopperInsightsViewController.swift b/Demo/Application/Features/ShopperInsightsViewController.swift index 4cb48c30ec..13c59f9bc7 100644 --- a/Demo/Application/Features/ShopperInsightsViewController.swift +++ b/Demo/Application/Features/ShopperInsightsViewController.swift @@ -10,7 +10,6 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { lazy var paypalClient = BTPayPalClient(apiClient: apiClient) lazy var venmoClient = BTVenmoClient(apiClient: apiClient) - lazy var payPalCheckoutButton = createButton(title: "PayPal Checkout", action: #selector(payPalCheckoutButtonTapped)) lazy var payPalVaultButton = createButton(title: "PayPal Vault", action: #selector(payPalVaultButtonTapped)) lazy var venmoButton = createButton(title: "Venmo", action: #selector(venmoButtonTapped)) @@ -18,6 +17,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { let view = TextFieldWithLabel() view.label.text = "Email" view.textField.placeholder = "Email" + view.textField.text = "PR1_merchantname@personal.example.com" return view }() @@ -25,6 +25,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { let view = TextFieldWithLabel() view.label.text = "Country Code" view.textField.placeholder = "Country Code" + view.textField.text = "1" return view }() @@ -32,6 +33,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { let view = TextFieldWithLabel() view.label.text = "National Number" view.textField.placeholder = "National Number" + view.textField.text = "4082321001" return view }() @@ -83,7 +85,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { } override func createPaymentButton() -> UIView { - let buttons = [payPalCheckoutButton, payPalVaultButton, venmoButton] + let buttons = [payPalVaultButton, venmoButton] buttons.forEach { $0.isEnabled = false } let stackView = UIStackView(arrangedSubviews: buttons) stackView.axis = .vertical @@ -108,7 +110,6 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { do { let result = try await shopperInsightsClient.getRecommendedPaymentMethods(request: request) self.progressBlock("PayPal Recommended: \(result.isPayPalRecommended)\nVenmo Recommended: \(result.isVenmoRecommended)") - self.payPalCheckoutButton.isEnabled = result.isPayPalRecommended self.payPalVaultButton.isEnabled = result.isPayPalRecommended self.venmoButton.isEnabled = result.isVenmoRecommended } catch { @@ -116,19 +117,6 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { } } } - - @objc func payPalCheckoutButtonTapped(_ button: UIButton) { - self.progressBlock("Tapped PayPal Checkout") - - button.setTitle("Processing...", for: .disabled) - button.isEnabled = false - - let paypalRequest = BTPayPalCheckoutRequest(amount: "4.30") - paypalClient.tokenize(paypalRequest) { (nonce, error) in - button.isEnabled = true - self.displayResultDetails(nonce: nonce, error: error) - } - } @objc func payPalVaultButtonTapped(_ button: UIButton) { self.progressBlock("Tapped PayPal Vault") diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift index 5d7f872a2a..e06eb6e873 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift @@ -53,7 +53,7 @@ public struct Phone: Encodable { /// Initialize a `BTShopperInsightsRequest.Phone`. /// - Parameters: /// - countryCode: The buyer's country code prefix to the national telephone number. An identifier for a specific country. Must not contain special characters. - /// - nationalNumber: The buyer's national phone number. Must not contain special characters. Must not contain special characters. + /// - nationalNumber: The buyer's national phone number. Must not contain special characters. /// - Note: This feature is in beta. It's public API may change or be removed in future releases. public init(countryCode: String, nationalNumber: String) { self.countryCode = countryCode From a86cd4d838f9a3e9aaeb9fd741aba689d57a3aa0 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Wed, 7 Feb 2024 03:49:55 -0600 Subject: [PATCH 35/57] Remove app installed checks for ShopperInsightsClient (#1186) --- .../BTShopperInsightsClient.swift | 57 +++++++------------ .../BTShopperInsightsClient_Tests.swift | 25 +------- 2 files changed, 21 insertions(+), 61 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index e8248845b1..56748ce4d4 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -33,32 +33,27 @@ public class BTShopperInsightsClient { public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult { apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsStarted) - if isVenmoAppInstalled() && isPayPalAppInstalled() { - let result = BTShopperInsightsResult(isPayPalRecommended: true, isVenmoRecommended: true) - return notifySuccess(with: result) - } else { - // TODO: - Fill in appropriate merchantID (or ppClientID) from config once API team decides what we need to send - let postParameters = BTEligiblePaymentsRequest( - email: request.email, - phone: request.phone, - merchantID: "MXSJ4F5BADVNS" - ) - - do { - let (json, _) = try await apiClient.post("/v2/payments/find-eligible-methods", parameters: postParameters, httpType: .payPalAPI) - guard let eligibleMethodsJSON = json?["eligible_methods"].asDictionary(), - eligibleMethodsJSON.count != 0 else { - throw self.notifyFailure(with: BTShopperInsightsError.emptyBodyReturned) - } - let eligiblePaymentMethods = BTEligiblePaymentMethods(json: json) - let result = BTShopperInsightsResult( - isPayPalRecommended: isPaymentRecommended(eligiblePaymentMethods.paypal), - isVenmoRecommended: isPaymentRecommended(eligiblePaymentMethods.venmo) - ) - return self.notifySuccess(with: result) - } catch { - throw self.notifyFailure(with: error) + // TODO: - Fill in appropriate merchantID (or ppClientID) from config once API team decides what we need to send + let postParameters = BTEligiblePaymentsRequest( + email: request.email, + phone: request.phone, + merchantID: "MXSJ4F5BADVNS" + ) + + do { + let (json, _) = try await apiClient.post("/v2/payments/find-eligible-methods", parameters: postParameters, httpType: .payPalAPI) + guard let eligibleMethodsJSON = json?["eligible_methods"].asDictionary(), + eligibleMethodsJSON.count != 0 else { + throw self.notifyFailure(with: BTShopperInsightsError.emptyBodyReturned) } + let eligiblePaymentMethods = BTEligiblePaymentMethods(json: json) + let result = BTShopperInsightsResult( + isPayPalRecommended: isPaymentRecommended(eligiblePaymentMethods.paypal), + isVenmoRecommended: isPaymentRecommended(eligiblePaymentMethods.venmo) + ) + return self.notifySuccess(with: result) + } catch { + throw self.notifyFailure(with: error) } } @@ -98,18 +93,6 @@ public class BTShopperInsightsClient { apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.venmoSelected) } - // MARK: - Private Methods - - private func isVenmoAppInstalled() -> Bool { - let venmoURL = URL(string: "com.venmo.touch.v2://")! - return application.canOpenURL(venmoURL) - } - - private func isPayPalAppInstalled() -> Bool { - let paypalURL = URL(string: "paypal://")! - return application.canOpenURL(paypalURL) - } - // MARK: - Analytics Helper Methods private func notifySuccess(with result: BTShopperInsightsResult) -> BTShopperInsightsResult { diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index bb9f62df9e..806ff1098d 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -24,30 +24,7 @@ class BTShopperInsightsClient_Tests: XCTestCase { // MARK: - getRecommendedPaymentMethods() - func testGetRecommendedPaymentMethods_returnsDefaultRecommendations() async { - let result = try? await sut.getRecommendedPaymentMethods(request: request) - - XCTAssertNil(result) - XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:get-recommended-payments:started") - XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "shopper-insights:get-recommended-payments:failed") - } - - func testGetRecommendedPaymentMethods_whenBothAppsInstalled_returnsTrue() async { - let fakeApplication = FakeApplication() - fakeApplication.cannedCanOpenURL = false - fakeApplication.canOpenURLWhitelist.append(URL(string: "com.venmo.touch.v2://")!) - fakeApplication.canOpenURLWhitelist.append(URL(string: "paypal://")!) - sut.application = fakeApplication - - let result = try? await sut.getRecommendedPaymentMethods(request: request) - - XCTAssertTrue(result!.isPayPalRecommended) - XCTAssertTrue(result!.isVenmoRecommended) - XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:get-recommended-payments:started") - XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, "shopper-insights:get-recommended-payments:succeeded") - } - - func testGetRecommendedPaymentMethods_whenAppsNotInstalled_callsEligiblePaymentsAPI() async { + func testGetRecommendedPaymentMethods_callsEligiblePaymentsAPI() async { _ = try? await sut.getRecommendedPaymentMethods(request: request) XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v2/payments/find-eligible-methods") From 55188957e796947f2d236bc5a0cc1fd7900c63ed Mon Sep 17 00:00:00 2001 From: agedd <105314544+agedd@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:44:24 -0600 Subject: [PATCH 36/57] [DTBTSDK-3403] Send PayPal-Client-Metadata-Id to PaymentReady API Call (#1187) * add paypal-client-metadata-id to SI api call Co-authored-by: Sammy Cannillo * fix failing unit test * cleanup and address pr comments --------- Co-authored-by: Sammy Cannillo --- Sources/BraintreeCore/BTAPIClient.swift | 6 ++++-- Sources/BraintreeCore/BTGraphQLHTTP.swift | 2 +- Sources/BraintreeCore/BTHTTP.swift | 18 +++++++++++++----- .../BTShopperInsightsClient.swift | 7 ++++++- .../BTShopperInsightsClient_Tests.swift | 1 + UnitTests/BraintreeTestShared/FakeHTTP.swift | 6 ++++-- .../BraintreeTestShared/MockAPIClient.swift | 4 +++- 7 files changed, 32 insertions(+), 12 deletions(-) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index eea25eee68..105683b699 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -327,6 +327,7 @@ import Foundation public func post( _ path: String, parameters: Encodable, + headers: [String: String]? = nil, httpType: BTAPIClientHTTPService = .gateway, completion: @escaping RequestCompletion ) { @@ -342,7 +343,7 @@ import Foundation } let postParameters = BTAPIRequest(requestBody: parameters, metadata: metadata, httpType: httpType) - http(for: httpType)?.post(path, parameters: postParameters, completion: completion) + http(for: httpType)?.post(path, parameters: postParameters, headers: headers, completion: completion) } } @@ -358,10 +359,11 @@ import Foundation public func post( _ path: String, parameters: Encodable, + headers: [String: String]? = nil, httpType: BTAPIClientHTTPService = .gateway ) async throws -> (BTJSON?, HTTPURLResponse?) { try await withCheckedThrowingContinuation { continuation in - post(path, parameters: parameters, httpType: httpType) { json, httpResonse, error in + post(path, parameters: parameters, headers: headers, httpType: httpType) { json, httpResonse, error in if let error { continuation.resume(throwing: error) } else { diff --git a/Sources/BraintreeCore/BTGraphQLHTTP.swift b/Sources/BraintreeCore/BTGraphQLHTTP.swift index 99e534b87a..3faa3ad11d 100644 --- a/Sources/BraintreeCore/BTGraphQLHTTP.swift +++ b/Sources/BraintreeCore/BTGraphQLHTTP.swift @@ -14,7 +14,7 @@ class BTGraphQLHTTP: BTHTTP { NSException(name: exceptionName, reason: "GET is unsupported").raise() } - override func post(_ path: String, parameters: [String: Any]? = nil, completion: @escaping RequestCompletion) { + override func post(_ path: String, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: @escaping RequestCompletion) { httpRequest(method: "POST", parameters: parameters, completion: completion) } diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index 0251623d7a..2a340817ef 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -114,14 +114,14 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate { } // TODO: - Remove when all POST bodies use Codable, instead of BTJSON/raw dictionaries - func post(_ path: String, parameters: [String: Any]? = nil, completion: @escaping RequestCompletion) { - httpRequest(method: "POST", path: path, parameters: parameters, completion: completion) + func post(_ path: String, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: @escaping RequestCompletion) { + httpRequest(method: "POST", path: path, parameters: parameters, headers: headers, completion: completion) } - func post(_ path: String, parameters: Encodable, completion: @escaping RequestCompletion) { + func post(_ path: String, parameters: Encodable, headers: [String: String]? = nil, completion: @escaping RequestCompletion) { do { let dict = try parameters.toDictionary() - post(path, parameters: dict, completion: completion) + post(path, parameters: dict, headers: headers, completion: completion) } catch let error { completion(nil, nil, error) } @@ -179,9 +179,10 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate { method: String, path: String, parameters: [String: Any]? = [:], + headers: [String: String]? = nil, completion: RequestCompletion? ) { - createRequest(method: method, path: path, parameters: parameters) { request, error in + createRequest(method: method, path: path, parameters: parameters, headers: headers) { request, error in guard let request = request else { self.handleRequestCompletion(data: nil, request: nil, shouldCache: false, response: nil, error: error, completion: completion) return @@ -202,6 +203,7 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate { method: String, path: String, parameters: [String: Any]? = [:], + headers: [String: String]? = nil, completion: @escaping (URLRequest?, Error?) -> Void ) { let hasHTTPPrefix: Bool = path.hasPrefix("http") @@ -251,6 +253,7 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate { method: method, url: fullPathURL, parameters: mutableParameters, + headers: headers, isDataURL: isDataURL ) { request, error in completion(request, error) @@ -261,6 +264,7 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate { method: String, url: URL, parameters: NSMutableDictionary? = [:], + headers additionalHeaders: [String: String]? = nil, isDataURL: Bool, completion: @escaping (URLRequest?, Error?) -> Void ) { @@ -283,6 +287,10 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate { headers["Client-Key"] = key } } + + if let additionalHeaders { + headers = headers.merging(additionalHeaders) { $1 } + } if method == "GET" || method == "DELETE" { if !isDataURL { diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 56748ce4d4..0a28bc568f 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -41,7 +41,12 @@ public class BTShopperInsightsClient { ) do { - let (json, _) = try await apiClient.post("/v2/payments/find-eligible-methods", parameters: postParameters, httpType: .payPalAPI) + let (json, _) = try await apiClient.post( + "/v2/payments/find-eligible-methods", + parameters: postParameters, + headers: ["PayPal-Client-Metadata-Id": apiClient.metadata.sessionID], + httpType: .payPalAPI + ) guard let eligibleMethodsJSON = json?["eligible_methods"].asDictionary(), eligibleMethodsJSON.count != 0 else { throw self.notifyFailure(with: BTShopperInsightsError.emptyBodyReturned) diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 806ff1098d..f9eae05071 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -29,6 +29,7 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertEqual(mockAPIClient.lastPOSTPath, "/v2/payments/find-eligible-methods") XCTAssertEqual(mockAPIClient.lastPOSTAPIClientHTTPType, .payPalAPI) + XCTAssertEqual(mockAPIClient.lastPOSTAdditionalHeaders?["PayPal-Client-Metadata-Id"], mockAPIClient.metadata.sessionID) guard let lastPostParameters = mockAPIClient.lastPOSTParameters else { XCTFail() diff --git a/UnitTests/BraintreeTestShared/FakeHTTP.swift b/UnitTests/BraintreeTestShared/FakeHTTP.swift index 516e9accff..3075821909 100644 --- a/UnitTests/BraintreeTestShared/FakeHTTP.swift +++ b/UnitTests/BraintreeTestShared/FakeHTTP.swift @@ -6,6 +6,7 @@ import Foundation @objc public var POSTRequestCount: Int = 0 @objc public var lastRequestEndpoint: String? public var lastRequestMethod: String? + public var lastPOSTRequestHeaders: [String: String]? = [:] @objc public var lastRequestParameters: [String: Any]? var stubMethod: String? var stubEndpoint: String? @@ -57,11 +58,12 @@ import Foundation } } - public override func post(_ path: String, parameters: [String: Any]? = nil, completion: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { + public override func post(_ path: String, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { POSTRequestCount += 1 lastRequestEndpoint = path lastRequestParameters = parameters lastRequestMethod = "POST" + lastPOSTRequestHeaders = headers if cannedError != nil { dispatchQueue.async { completion?(nil, nil, self.cannedError) @@ -88,7 +90,7 @@ import Foundation self.init(url: URL(string: "http://fake.com")!) } - public override func post(_ path: String, parameters: [String: Any]?, completion: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { + public override func post(_ path: String, parameters: [String: Any]?, headers: [String: String]? = nil, completion: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { POSTRequestCount += 1 lastRequestParameters = parameters completion?(self.cannedConfiguration, nil, nil) diff --git a/UnitTests/BraintreeTestShared/MockAPIClient.swift b/UnitTests/BraintreeTestShared/MockAPIClient.swift index 2a91a0950a..ef040906d0 100644 --- a/UnitTests/BraintreeTestShared/MockAPIClient.swift +++ b/UnitTests/BraintreeTestShared/MockAPIClient.swift @@ -5,6 +5,7 @@ public class MockAPIClient: BTAPIClient { public var lastPOSTPath = "" public var lastPOSTParameters = [:] as [AnyHashable: Any]? public var lastPOSTAPIClientHTTPType: BTAPIClientHTTPService? + public var lastPOSTAdditionalHeaders: [String: String]? = [:] public var lastGETPath = "" public var lastGETParameters = [:] as [String: Any]? @@ -50,10 +51,11 @@ public class MockAPIClient: BTAPIClient { completionBlock(cannedResponseBody, cannedHTTPURLResponse, cannedResponseError) } - public override func post(_ path: String, parameters: Encodable, httpType: BTAPIClientHTTPService = .gateway, completion completionBlock: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { + public override func post(_ path: String, parameters: Encodable, headers: [String: String]? = nil, httpType: BTAPIClientHTTPService = .gateway, completion completionBlock: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { lastPOSTPath = path lastPOSTParameters = try? parameters.toDictionary() lastPOSTAPIClientHTTPType = httpType + lastPOSTAdditionalHeaders = headers guard let completionBlock = completionBlock else { return From a5a3ad210fe5d6d7506489969ea0060c040f0c16 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:25:24 -0600 Subject: [PATCH 37/57] Add merchant-triggered analytic events to Demo app (#1189) --- .../Application/Features/ShopperInsightsViewController.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Demo/Application/Features/ShopperInsightsViewController.swift b/Demo/Application/Features/ShopperInsightsViewController.swift index 13c59f9bc7..b4aee096be 100644 --- a/Demo/Application/Features/ShopperInsightsViewController.swift +++ b/Demo/Application/Features/ShopperInsightsViewController.swift @@ -93,6 +93,9 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { stackView.distribution = .fillEqually stackView.translatesAutoresizingMaskIntoConstraints = false + shopperInsightsClient.sendPayPalPresentedEvent() + shopperInsightsClient.sendVenmoPresentedEvent() + return stackView } @@ -120,6 +123,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { @objc func payPalVaultButtonTapped(_ button: UIButton) { self.progressBlock("Tapped PayPal Vault") + shopperInsightsClient.sendPayPalSelectedEvent() button.setTitle("Processing...", for: .disabled) button.isEnabled = false @@ -133,6 +137,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { @objc func venmoButtonTapped(_ button: UIButton) { self.progressBlock("Tapped Venmo") + shopperInsightsClient.sendVenmoSelectedEvent() button.setTitle("Processing...", for: .disabled) button.isEnabled = false From 613401488fa524505e7ab299c864ad3c24850b37 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:17:20 -0600 Subject: [PATCH 38/57] [ShopperInsights] Accept email in `BTPayPalVaultRequest` (#1188) --- CHANGELOG.md | 1 + Sources/BraintreePayPal/BTPayPalVaultRequest.swift | 9 ++++++++- .../BTPayPalVaultRequest_Tests.swift | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be8cafac5..b477368c85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## unreleased * Add BraintreeShopperInsights module (BETA) * Send `paypal_context_id` in `batch_params` to PayPal's analytics service (FPTI) when available +* Add `BTPayPalRequest.userAuthenticationEmail` optional property ## 6.12.0 (2024-01-18) * BraintreePayPal diff --git a/Sources/BraintreePayPal/BTPayPalVaultRequest.swift b/Sources/BraintreePayPal/BTPayPalVaultRequest.swift index 02f4d62d3f..ea377930ba 100644 --- a/Sources/BraintreePayPal/BTPayPalVaultRequest.swift +++ b/Sources/BraintreePayPal/BTPayPalVaultRequest.swift @@ -11,6 +11,9 @@ import BraintreeCore /// Optional: Offers PayPal Credit if the customer qualifies. Defaults to `false`. public var offerCredit: Bool + + /// Optional: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email. + public var userAuthenticationEmail: String? // MARK: - Initializer @@ -30,9 +33,13 @@ import BraintreeCore let baseParameters = super.parameters(with: configuration) var vaultParameters: [String: Any] = ["offer_paypal_credit": offerCredit] - if billingAgreementDescription != nil { + if let billingAgreementDescription { vaultParameters["description"] = billingAgreementDescription } + + if let userAuthenticationEmail { + vaultParameters["payer_email"] = userAuthenticationEmail + } if let shippingAddressOverride { let shippingAddressParameters: [String: String?] = [ diff --git a/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift index 7735c26a96..c2baf2407a 100644 --- a/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift +++ b/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift @@ -47,11 +47,13 @@ class BTPayPalVaultRequest_Tests: XCTestCase { request.shippingAddressOverride = shippingAddress request.isShippingAddressEditable = true request.offerCredit = true + request.userAuthenticationEmail = "fake@email.com" let parameters = request.parameters(with: configuration) XCTAssertEqual(parameters["description"] as? String, "desc") XCTAssertEqual(parameters["offer_paypal_credit"] as? Bool, true) + XCTAssertEqual(parameters["payer_email"] as? String, "fake@email.com") guard let shippingParams = parameters["shipping_address"] as? [String:String] else { XCTFail(); return } From bacbe0c3a41b9cd4862c32c4d710188f2661a16f Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Mon, 26 Feb 2024 15:19:54 -0600 Subject: [PATCH 39/57] Pass email from text field into PayPalVaultRequest (#1191) --- Demo/Application/Features/ShopperInsightsViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Demo/Application/Features/ShopperInsightsViewController.swift b/Demo/Application/Features/ShopperInsightsViewController.swift index b4aee096be..b0338c1da0 100644 --- a/Demo/Application/Features/ShopperInsightsViewController.swift +++ b/Demo/Application/Features/ShopperInsightsViewController.swift @@ -129,6 +129,8 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { button.isEnabled = false let paypalRequest = BTPayPalVaultRequest() + paypalRequest.userAuthenticationEmail = emailView.textField.text ?? nil + paypalClient.tokenize(paypalRequest) { (nonce, error) in button.isEnabled = true self.displayResultDetails(nonce: nonce, error: error) From 662fec13a35ce692eb6e40c8458f89c8bc55042d Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Tue, 27 Feb 2024 21:26:45 -0800 Subject: [PATCH 40/57] docStrings on country restrictions --- Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 0a28bc568f..6bf76b6ef7 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -30,6 +30,8 @@ public class BTShopperInsightsClient { /// - request: A `BTShopperInsightsRequest` containing the buyer's user information /// - Returns: A `BTShopperInsightsResult` instance /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + /// Shopper Insights for PayPal payment recommendation is only available in US and AU, FR, DE, ITA, NED, ESP, Switzerland and UK. + /// Venmo recommendation is only available for US. public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult { apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsStarted) From d1e0961eefd1794be54e1ede44564c46f17605c3 Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Tue, 27 Feb 2024 21:47:34 -0800 Subject: [PATCH 41/57] revert docStrings on country restrictions --- Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 6bf76b6ef7..0a28bc568f 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -30,8 +30,6 @@ public class BTShopperInsightsClient { /// - request: A `BTShopperInsightsRequest` containing the buyer's user information /// - Returns: A `BTShopperInsightsResult` instance /// - Note: This feature is in beta. It's public API may change or be removed in future releases. - /// Shopper Insights for PayPal payment recommendation is only available in US and AU, FR, DE, ITA, NED, ESP, Switzerland and UK. - /// Venmo recommendation is only available for US. public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult { apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsStarted) From d57b773c54cd95bc3d73786baabe6059ee1cfc46 Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Wed, 28 Feb 2024 09:48:20 -0800 Subject: [PATCH 42/57] DocStrings on Country Restrictions (#1197) * docStrings on country restrictions * fix typo in docStrings * Sammy PR feedback * pr feedback 2 --- .../BraintreeShopperInsights/BTShopperInsightsClient.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 0a28bc568f..29f096401b 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -29,7 +29,9 @@ public class BTShopperInsightsClient { /// - Parameters: /// - request: A `BTShopperInsightsRequest` containing the buyer's user information /// - Returns: A `BTShopperInsightsResult` instance - /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + /// - Note: This feature is in beta. Its public API may change or be removed in future releases. + /// PayPal recommendation is only available for US, AU, FR, DE, ITA, NED, ESP, Switzerland and UK merchants. + /// Venmo recommendation is only available for US merchants. public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult { apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsStarted) From 94e09f03002b46140b539d9783e0bfd04c0cfdb5 Mon Sep 17 00:00:00 2001 From: agedd <105314544+agedd@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:24:20 -0600 Subject: [PATCH 43/57] [DTBTSDK-3544] Remove merchant_id from the Payment Ready API Request (#1195) * remove merchantID param * cleanup * address pr feedback --- .../BTEligiblePaymentsRequest.swift | 16 ++++------------ .../BTShopperInsightsClient.swift | 4 +--- .../BTShopperInsightsClient_Tests.swift | 2 -- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift index 7ea1159571..ea88f34927 100644 --- a/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift +++ b/Sources/BraintreeShopperInsights/BTEligiblePaymentsRequest.swift @@ -6,7 +6,7 @@ struct BTEligiblePaymentsRequest: Encodable { private let customer: Customer private let purchaseUnits: [PurchaseUnit] - private let preferences = Preferences() + private let preferences: Preferences enum CodingKeys: String, CodingKey { case customer = "customer" @@ -27,7 +27,6 @@ struct BTEligiblePaymentsRequest: Encodable { } struct PurchaseUnit: Encodable { - let payee: Payee let amount = Amount() struct Amount: Encodable { @@ -37,14 +36,6 @@ struct BTEligiblePaymentsRequest: Encodable { case currencyCode = "currency_code" } } - - struct Payee: Encodable { - let merchantID: String - - enum CodingKeys: String, CodingKey { - case merchantID = "merchant_id" - } - } } struct Preferences: Encodable { @@ -67,8 +58,9 @@ struct BTEligiblePaymentsRequest: Encodable { } } - init(email: String?, phone: Phone?, merchantID: String) { + init(email: String?, phone: Phone?) { self.customer = Customer(email: email, phone: phone) - self.purchaseUnits = [PurchaseUnit(payee: PurchaseUnit.Payee(merchantID: merchantID))] + self.purchaseUnits = [PurchaseUnit()] + self.preferences = Preferences() } } diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 29f096401b..a8a9604286 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -35,11 +35,9 @@ public class BTShopperInsightsClient { public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult { apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsStarted) - // TODO: - Fill in appropriate merchantID (or ppClientID) from config once API team decides what we need to send let postParameters = BTEligiblePaymentsRequest( email: request.email, - phone: request.phone, - merchantID: "MXSJ4F5BADVNS" + phone: request.phone ) do { diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index f9eae05071..2e6c18fa37 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -49,8 +49,6 @@ class BTShopperInsightsClient_Tests: XCTestCase { XCTAssertEqual(paymentSourceConstraint["payment_sources"] as! [String], ["PAYPAL", "VENMO"]) let purchaseUnits = lastPostParameters["purchase_units"] as! [[String: Any]] - let payee = purchaseUnits.first?["payee"] as! [String: String] - XCTAssertEqual(payee["merchant_id"], "MXSJ4F5BADVNS") let amount = purchaseUnits.first?["amount"] as! [String: String] XCTAssertEqual(amount["currency_code"], "USD") } From 0dc2b5fd0a0c95f9f4a91376dd355d4d6f187b4e Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Fri, 1 Mar 2024 11:31:01 -0800 Subject: [PATCH 44/57] fix merge conflict res marker --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b477368c85..ac5ae2205a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ ## 6.12.0 (2024-01-18) * BraintreePayPal * Add `imageURL`, `upcCode`, and `upcType` to `BTPayPalLineItem` ->>>>>>> main ## 6.11.0 (2023-12-20) * Update all SDK errors to be public and [Equatable](https://developer.apple.com/documentation/swift/equatable) (fixes #1152 and #1080) From 5313b4d2d5af6400f5e8fcba92dd5b7c18df4bdf Mon Sep 17 00:00:00 2001 From: Victoria Park Date: Thu, 7 Mar 2024 07:23:30 -0800 Subject: [PATCH 45/57] Add Privacy Manifest to ShopperInsights Module (#1200) * Add Privacy Manifest to ShopperInsights Module * CHANGELOG entry * Sammy PR feedback * Sammy and Alekhya PR feedback * Alekhya PR feedback * Jax PR feedback --- Braintree.podspec | 1 + Braintree.xcodeproj/project.pbxproj | 4 +++ CHANGELOG.md | 2 ++ Package.swift | 3 +- .../PrivacyInfo.xcprivacy | 33 +++++++++++++++++++ 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 Sources/BraintreeShopperInsights/PrivacyInfo.xcprivacy diff --git a/Braintree.podspec b/Braintree.podspec index 43d83d0eb9..22ab883b70 100644 --- a/Braintree.podspec +++ b/Braintree.podspec @@ -68,6 +68,7 @@ Pod::Spec.new do |s| s.subspec "ShopperInsights" do |s| s.source_files = "Sources/BraintreeShopperInsights/*.swift" s.dependency "Braintree/Core" + s.resource_bundle = { "BraintreeShopperInsights_PrivacyInfo" => "Sources/BraintreeShopperInsights/PrivacyInfo.xcprivacy" } end s.subspec "PayPalNativeCheckout" do |s| diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 0d753625c1..1a14898c85 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 3B14BFDF29B647AF0047426A /* BTCardAnalytics_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B14BFDE29B647AF0047426A /* BTCardAnalytics_Tests.swift */; }; 3B1C529429D5EB4E00B68A58 /* BTVenmoAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C529329D5EB4E00B68A58 /* BTVenmoAnalytics.swift */; }; 3B1C529729D638D400B68A58 /* BTVenmoAnalytics_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C529529D637F400B68A58 /* BTVenmoAnalytics_Tests.swift */; }; + 3B29C3952B90F12F0077741D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3B29C3942B90F12F0077741D /* PrivacyInfo.xcprivacy */; }; 3B57E9EA29ECC1AF00245174 /* BTLocalPaymentAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B57E9E929ECC1AF00245174 /* BTLocalPaymentAnalytics.swift */; }; 3B7A261129C0CAA40087059D /* BTPayPalAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7A261029C0CAA40087059D /* BTPayPalAnalytics.swift */; }; 3B7A261429C35BD00087059D /* BTPayPalAnalytics_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7A261229C35B670087059D /* BTPayPalAnalytics_Tests.swift */; }; @@ -668,6 +669,7 @@ 3B14BFDE29B647AF0047426A /* BTCardAnalytics_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTCardAnalytics_Tests.swift; sourceTree = ""; }; 3B1C529329D5EB4E00B68A58 /* BTVenmoAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTVenmoAnalytics.swift; sourceTree = ""; }; 3B1C529529D637F400B68A58 /* BTVenmoAnalytics_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTVenmoAnalytics_Tests.swift; sourceTree = ""; }; + 3B29C3942B90F12F0077741D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3B57E9E929ECC1AF00245174 /* BTLocalPaymentAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTLocalPaymentAnalytics.swift; sourceTree = ""; }; 3B7A261029C0CAA40087059D /* BTPayPalAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalAnalytics.swift; sourceTree = ""; }; 3B7A261229C35B670087059D /* BTPayPalAnalytics_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalAnalytics_Tests.swift; sourceTree = ""; }; @@ -1335,6 +1337,7 @@ 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */, 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */, 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */, + 3B29C3942B90F12F0077741D /* PrivacyInfo.xcprivacy */, ); path = BraintreeShopperInsights; sourceTree = ""; @@ -2632,6 +2635,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3B29C3952B90F12F0077741D /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5ae2205a..e040e31027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Add BraintreeShopperInsights module (BETA) * Send `paypal_context_id` in `batch_params` to PayPal's analytics service (FPTI) when available * Add `BTPayPalRequest.userAuthenticationEmail` optional property +* ShoppperInsights + * Add PrivacyInfo.xcprivacy file ## 6.12.0 (2024-01-18) * BraintreePayPal diff --git a/Package.swift b/Package.swift index aad34a0ed1..8bfdfe5428 100644 --- a/Package.swift +++ b/Package.swift @@ -104,7 +104,8 @@ let package = Package( ), .target( name: "BraintreeShopperInsights", - dependencies: ["BraintreeCore"] + dependencies: ["BraintreeCore"], + resources: [.copy("PrivacyInfo.xcprivacy")] ), .target( name: "BraintreeThreeDSecure", diff --git a/Sources/BraintreeShopperInsights/PrivacyInfo.xcprivacy b/Sources/BraintreeShopperInsights/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..82c81b43c8 --- /dev/null +++ b/Sources/BraintreeShopperInsights/PrivacyInfo.xcprivacy @@ -0,0 +1,33 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeEmailAddress + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypePhoneNumber + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + + From fd25f936dc495441c4e687042caadae9dcc54832 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 13 Mar 2024 16:11:38 -0500 Subject: [PATCH 46/57] Revert to Podfile.lock from main --- Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile.lock b/Podfile.lock index 086a065f08..fcfb8736fa 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -37,4 +37,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 224cc2d6666b79d3d6adb45cccd6dfbc6fe74d18 -COCOAPODS: 1.14.3 +COCOAPODS: 1.14.2 From 297b9821df8825a8a3f2a98c275402ffb28d0d87 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Fri, 19 Apr 2024 13:45:52 -0500 Subject: [PATCH 47/57] Resolve accidental merge conflict commits in Braintree.xcodeproj/project.pbxproj --- Braintree.xcodeproj/project.pbxproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 5e0cdb542d..da30f45d29 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -756,11 +756,9 @@ 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BraintreePayPal_IntegrationTests.swift; sourceTree = ""; }; 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+NonceValidation.swift"; sourceTree = ""; }; 59A4135427B90CD7AE94D5CC /* Pods-Tests-IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests-IntegrationTests.debug.xcconfig"; path = "Target Support Files/Pods-Tests-IntegrationTests/Pods-Tests-IntegrationTests.debug.xcconfig"; sourceTree = ""; }; -<<<<<<< HEAD 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsError.swift; sourceTree = ""; }; 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsAnalytics_Tests.swift; sourceTree = ""; }; 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentMethods.swift; sourceTree = ""; }; -======= 620C3C642BA21E5700C40A03 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 62236E192B98CFB000CDCC37 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 6298A1982B91010600E46EDF /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -768,7 +766,6 @@ 62A746402B9255AC003D32FF /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; 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 = ""; }; ->>>>>>> main 800E23DC22206A8300C5D22E /* BTThreeDSecureAuthenticateJWT_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAuthenticateJWT_Tests.swift; sourceTree = ""; }; 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentsRequest.swift; sourceTree = ""; }; @@ -792,14 +789,11 @@ 80581A8B25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BTConfiguration+ThreeDSecure_Tests.swift"; sourceTree = ""; }; 80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTGraphQLHTTP_SSLPinning_IntegrationTests.swift; sourceTree = ""; }; 805FD35B2331780F0000B514 /* BTPostalAddress_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPostalAddress_Tests.swift; sourceTree = ""; }; -<<<<<<< HEAD 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsClient.swift; sourceTree = ""; }; 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsResult.swift; sourceTree = ""; }; 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsClient_Tests.swift; sourceTree = ""; }; 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsRequest.swift; sourceTree = ""; }; -======= 806C85622B90EBED00A2754C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; ->>>>>>> main 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAPIRequest.swift; sourceTree = ""; }; 80842DA62B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 80A1EE3D2236AAC600F6218B /* BTThreeDSecureAdditionalInformation_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAdditionalInformation_Tests.swift; sourceTree = ""; }; From d88c578f3de8999751f79ebf7ad842f365431509 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Mon, 22 Apr 2024 11:15:50 -0500 Subject: [PATCH 48/57] Fix merge conflict issue, drop '-m' from paypal api URL --- Sources/BraintreeCore/Analytics/BTAnalyticsService.swift | 2 +- .../BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index a933325c0d..dfe834edba 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -8,7 +8,7 @@ class BTAnalyticsService: Equatable { var http: BTHTTP? /// The FPTI URL to post all analytic events. - static let url = URL(string: "https://api-m.paypal.com")! + static let url = URL(string: "https://api.paypal.com")! private let apiClient: BTAPIClient diff --git a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift index 3d65c81e1f..1e69995d4c 100644 --- a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift @@ -19,7 +19,7 @@ final class BTAnalyticsService_Tests: XCTestCase { await analyticsService.performEventRequest("any.analytics.event") - XCTAssertEqual(analyticsService.http?.baseURL.absoluteString, "https://api-m.paypal.com") + XCTAssertEqual(analyticsService.http?.baseURL.absoluteString, "https://api.paypal.com") } func testSendAnalyticsEvent_sendsAnalyticsEvent() async { From 3ab86238e40ef96d1c0156e2ac0efa12772cc261 Mon Sep 17 00:00:00 2001 From: agedd <105314544+agedd@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:45:58 -0500 Subject: [PATCH 49/57] Update Beta Docstrings to `Warning` For Shopper-Insights Feature (#1280) * update note to warning for shopper-insights feature * address pr feedback --- .../BTShopperInsightsClient.swift | 6 +++--- .../BTShopperInsightsRequest.swift | 12 ++++++------ .../BTShopperInsightsResult.swift | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index a8a9604286..ace9dfcd90 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -6,7 +6,7 @@ import BraintreeCore /// Use `BTShopperInsightsClient` to optimize your checkout experience by prioritizing the customer’s preferred payment methods in your UI. /// By customizing each customer’s checkout experience, you can improve conversion, increase sales/repeat buys and boost user retention/loyalty. -/// - Note: This feature is in beta. It's public API may change or be removed in future releases. +/// - Warning: This feature is in beta. It's public API may change or be removed in future releases. public class BTShopperInsightsClient { // MARK: - Internal Properties @@ -20,7 +20,7 @@ public class BTShopperInsightsClient { /// Creates a `BTShopperInsightsClient` /// - Parameter apiClient: A `BTAPIClient` instance. - /// - Note: This features only works with a client token. + /// - Warning: This features only works with a client token. public init(apiClient: BTAPIClient) { self.apiClient = apiClient } @@ -29,7 +29,7 @@ public class BTShopperInsightsClient { /// - Parameters: /// - request: A `BTShopperInsightsRequest` containing the buyer's user information /// - Returns: A `BTShopperInsightsResult` instance - /// - Note: This feature is in beta. Its public API may change or be removed in future releases. + /// - Warning: This feature is in beta. Its public API may change or be removed in future releases. /// PayPal recommendation is only available for US, AU, FR, DE, ITA, NED, ESP, Switzerland and UK merchants. /// Venmo recommendation is only available for US merchants. public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult { diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift index e06eb6e873..acf79086f5 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsRequest.swift @@ -1,7 +1,7 @@ import Foundation /// Buyer data required to use the Shopper Insights feature. -/// - Note: This feature is in beta. It's public API may change or be removed in future releases. +/// - Warning: This feature is in beta. It's public API may change or be removed in future releases. public struct BTShopperInsightsRequest { // MARK: - Internal Properties @@ -15,7 +15,7 @@ public struct BTShopperInsightsRequest { /// - Parameters: /// - email: The buyer's email address. /// - phone: The buyer's phone number details. - /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + /// - Warning: This feature is in beta. It's public API may change or be removed in future releases. public init(email: String, phone: Phone) { self.email = email self.phone = phone @@ -24,7 +24,7 @@ public struct BTShopperInsightsRequest { /// Initialize a `BTShopperInsightsRequest` /// - Parameters: /// - email: The buyer's email address. - /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + /// - Warning: This feature is in beta. It's public API may change or be removed in future releases. public init(email: String) { self.email = email } @@ -32,14 +32,14 @@ public struct BTShopperInsightsRequest { /// Initialize a `BTShopperInsightsRequest` /// - Parameters: /// - phone: The buyer's phone number details. - /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + /// - Warning: This feature is in beta. It's public API may change or be removed in future releases. public init(phone: Phone) { self.phone = phone } } /// Buyer's phone number details for use with the Shopper Insights feature. -/// - Note: This feature is in beta. It's public API may change or be removed in future releases. +/// - Warning: This feature is in beta. It's public API may change or be removed in future releases. public struct Phone: Encodable { private let countryCode: String @@ -54,7 +54,7 @@ public struct Phone: Encodable { /// - Parameters: /// - countryCode: The buyer's country code prefix to the national telephone number. An identifier for a specific country. Must not contain special characters. /// - nationalNumber: The buyer's national phone number. Must not contain special characters. - /// - Note: This feature is in beta. It's public API may change or be removed in future releases. + /// - Warning: This feature is in beta. It's public API may change or be removed in future releases. public init(countryCode: String, nationalNumber: String) { self.countryCode = countryCode self.nationalNumber = nationalNumber diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsResult.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsResult.swift index 7700177b54..de0a24ee9c 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsResult.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsResult.swift @@ -1,7 +1,7 @@ import Foundation /// A summary of the buyer's recommended payment methods. -/// - Note: This feature is in beta. It's public API may change or be removed in future releases. +/// - Warning: This feature is in beta. It's public API may change or be removed in future releases. public struct BTShopperInsightsResult { /// If true, display the PayPal button with high priority. From b5ad5eaa575fa19bbd7dd8cdd86adc311134c890 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:08:27 -0500 Subject: [PATCH 50/57] Add `userAuthenticationEmail` to 1-Time Checkout (#1276) --- CHANGELOG.md | 2 +- .../PaymentButtonBaseViewController.swift | 2 +- .../PayPalWebCheckoutViewController.swift | 85 +++++++++++++------ .../BTPayPalCheckoutRequest.swift | 7 ++ .../BTPayPalNativeCheckoutRequest.swift | 5 -- .../BTPayPalCheckoutRequest_Tests.swift | 2 + 6 files changed, 70 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d06c298332..c0e231d2f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * Add BraintreeShopperInsights module (BETA) * Add PrivacyInfo.xcprivacy file * BraintreePayPal - * Add `BTPayPalRequest.userAuthenticationEmail` optional property + * Add `BTPayPalCheckoutRequest.userAuthenticationEmail` optional property ## 5.25.0 (2024-04-10) * Require Xcode 15.0+ and Swift 5.9+ (per [Apple App Store requirements](https://developer.apple.com/news/upcoming-requirements/?id=04292024a)) diff --git a/Demo/Application/Base/PaymentButtonBaseViewController.swift b/Demo/Application/Base/PaymentButtonBaseViewController.swift index ba48e1a77b..3c3dcc167a 100644 --- a/Demo/Application/Base/PaymentButtonBaseViewController.swift +++ b/Demo/Application/Base/PaymentButtonBaseViewController.swift @@ -29,7 +29,7 @@ class PaymentButtonBaseViewController: BaseViewController { paymentButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), paymentButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), paymentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor), - paymentButton.heightAnchor.constraint(equalToConstant: 100) + paymentButton.heightAnchor.constraint(equalToConstant: 300) ]) } diff --git a/Demo/Application/Features/PayPalWebCheckoutViewController.swift b/Demo/Application/Features/PayPalWebCheckoutViewController.swift index cfa0b98a17..a794314b7f 100644 --- a/Demo/Application/Features/PayPalWebCheckoutViewController.swift +++ b/Demo/Application/Features/PayPalWebCheckoutViewController.swift @@ -5,21 +5,50 @@ import BraintreePayPal class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { lazy var payPalClient = BTPayPalClient(apiClient: apiClient) + + lazy var emailLabel: UILabel = { + let label = UILabel() + label.text = "Buyer email:" + return label + }() + + lazy var emailTextField: UITextField = { + let textField = UITextField() + textField.placeholder = "placeholder@email.com" + textField.backgroundColor = .systemBackground + return textField + }() + + lazy var payLaterToggleLabel: UILabel = { + let label = UILabel() + label.text = "Offer Pay Later" + label.font = .preferredFont(forTextStyle: .footnote) + return label + }() + + let payLaterToggle = UISwitch() override func createPaymentButton() -> UIView { let payPalCheckoutButton = createButton(title: "PayPal Checkout", action: #selector(tappedPayPalCheckout)) let payPalVaultButton = createButton(title: "PayPal Vault", action: #selector(tappedPayPalVault)) - let payPalPayLaterButton = createButton(title: "PayPal with Pay Later Offered", action: #selector(tappedPayPalPayLater)) - let buttons = [payPalCheckoutButton, payPalVaultButton, payPalPayLaterButton] - let stackView = UIStackView(arrangedSubviews: buttons) + let stackView = UIStackView(arrangedSubviews: [ + UIStackView(arrangedSubviews: [emailLabel, emailTextField]), + buttonsStackView(label: "1-Time Checkout", views: [ + UIStackView(arrangedSubviews: [payLaterToggleLabel, payLaterToggle]), + payPalCheckoutButton + ]), + buttonsStackView(label: "Vault",views: [payPalVaultButton]) + ]) + stackView.axis = .vertical - stackView.alignment = .center - stackView.distribution = .fillEqually + stackView.distribution = .fillProportionally + stackView.spacing = 25 stackView.translatesAutoresizingMaskIntoConstraints = false - return stackView } + + // MARK: - 1-Time Checkout Flows @objc func tappedPayPalCheckout(_ sender: UIButton) { progressBlock("Tapped PayPal - Checkout using BTPayPalClient") @@ -27,11 +56,15 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { sender.isEnabled = false let request = BTPayPalCheckoutRequest(amount: "5.00") + request.userAuthenticationEmail = emailTextField.text + let lineItem = BTPayPalLineItem(quantity: "1", unitAmount: "5.00", name: "item one 1234567", kind: .debit) lineItem.upcCode = "123456789" lineItem.upcType = .UPC_A lineItem.imageURL = URL(string: "https://www.example.com/example.jpg") request.lineItems = [lineItem] + + request.offerPayLater = payLaterToggle.isOn payPalClient.tokenize(request) { nonce, error in sender.isEnabled = true @@ -44,13 +77,16 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { self.completionBlock(nonce) } } - + + // MARK: - Vault Flows + @objc func tappedPayPalVault(_ sender: UIButton) { progressBlock("Tapped PayPal - Vault using BTPayPalClient") sender.setTitle("Processing...", for: .disabled) sender.isEnabled = false let request = BTPayPalVaultRequest() + request.userAuthenticationEmail = emailTextField.text payPalClient.tokenize(request) { nonce, error in sender.isEnabled = true @@ -63,24 +99,21 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { self.completionBlock(nonce) } } - - @objc func tappedPayPalPayLater(_ sender: UIButton) { - progressBlock("Tapped PayPal - initiating with Pay Later offered") - sender.setTitle("Processing...", for: .disabled) - sender.isEnabled = false - - let request = BTPayPalCheckoutRequest(amount: "4.30") - request.offerPayLater = true - - payPalClient.tokenize(request) { nonce, error in - sender.isEnabled = true - - guard let nonce else { - self.progressBlock(error?.localizedDescription) - return - } - - self.completionBlock(nonce) - } + + // MARK: - Helpers + + private func buttonsStackView(label: String, views: [UIView]) -> UIStackView { + let titleLabel = UILabel() + titleLabel.text = label + titleLabel.font = .preferredFont(forTextStyle: .title3) + + let buttonsStackView = UIStackView(arrangedSubviews: [titleLabel] + views) + buttonsStackView.axis = .vertical + buttonsStackView.distribution = .fillProportionally + buttonsStackView.backgroundColor = .systemGray6 + buttonsStackView.layoutMargins = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) + buttonsStackView.isLayoutMarginsRelativeArrangement = true + + return buttonsStackView } } diff --git a/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift b/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift index 53b01c46f6..2ad8ac8ae1 100644 --- a/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift +++ b/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift @@ -81,6 +81,9 @@ import BraintreeCore /// Optional: If set to `true`, this enables the Checkout with Vault flow, where the customer will be prompted to consent to a billing agreement during checkout. Defaults to `false`. public var requestBillingAgreement: Bool + + /// Optional: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email. + public var userAuthenticationEmail: String? // MARK: - Initializer @@ -130,6 +133,10 @@ import BraintreeCore if currencyCode != nil { checkoutParameters["currency_iso_code"] = currencyCode } + + if let userAuthenticationEmail { + checkoutParameters["payer_email"] = userAuthenticationEmail + } if userAction != .none, var experienceProfile = baseParameters["experience_profile"] as? [String: Any] { experienceProfile["user_action"] = userAction.stringValue diff --git a/Sources/BraintreePayPalNativeCheckout/BTPayPalNativeCheckoutRequest.swift b/Sources/BraintreePayPalNativeCheckout/BTPayPalNativeCheckoutRequest.swift index bd78ba097e..f333a61fd7 100644 --- a/Sources/BraintreePayPalNativeCheckout/BTPayPalNativeCheckoutRequest.swift +++ b/Sources/BraintreePayPalNativeCheckout/BTPayPalNativeCheckoutRequest.swift @@ -10,11 +10,6 @@ import BraintreePayPal /// Options for the PayPal Checkout flow. @objcMembers public class BTPayPalNativeCheckoutRequest: BTPayPalCheckoutRequest { - - // MARK: - Public Properties - - /// Optional: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email. - public var userAuthenticationEmail: String? // MARK: - Initializer diff --git a/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift index e35d584508..5bf58927eb 100644 --- a/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift +++ b/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift @@ -86,6 +86,7 @@ class BTPayPalCheckoutRequest_Tests: XCTestCase { request.requestBillingAgreement = true request.billingAgreementDescription = "description" request.userAction = .payNow + request.userAuthenticationEmail = "fake@email.com" let shippingAddress = BTPostalAddress() shippingAddress.streetAddress = "123 Main" @@ -111,6 +112,7 @@ class BTPayPalCheckoutRequest_Tests: XCTestCase { XCTAssertEqual(parameters["postal_code"] as? String, "11111") XCTAssertEqual(parameters["country_code"] as? String, "US") 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 billingAgreementDetails = parameters["billing_agreement_details"] as? [String : String] else { From 77737bc8aad653fce27632f20cda904d14121f95 Mon Sep 17 00:00:00 2001 From: Rich Herrera Date: Wed, 24 Apr 2024 11:35:28 -0600 Subject: [PATCH 51/57] Do not embed Core on Shopper Insights (#1281) --- Braintree.xcodeproj/project.pbxproj | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index da30f45d29..c6a6f140ab 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -75,16 +75,16 @@ 57D9436E2968A8080079EAB1 /* BTPayPalLocaleCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9436D2968A8080079EAB1 /* BTPayPalLocaleCode.swift */; }; 57D94370296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */; }; 57D94372296CCA2F0079EAB1 /* String+NonceValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */; }; - 624B27F72B6AE0C2000AC08A /* BTShopperInsightsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */; }; - 62970D102B5AF2C000BAB584 /* BTShopperInsightsAnalytics_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */; }; - 62EA90492B63071800DD79BC /* BTEligiblePaymentMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */; }; 620C3C652BA21E5700C40A03 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 620C3C642BA21E5700C40A03 /* PrivacyInfo.xcprivacy */; }; 62236E1A2B98CFB000CDCC37 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62236E192B98CFB000CDCC37 /* PrivacyInfo.xcprivacy */; }; + 624B27F72B6AE0C2000AC08A /* BTShopperInsightsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */; }; + 62970D102B5AF2C000BAB584 /* BTShopperInsightsAnalytics_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */; }; 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 */; }; 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 */; }; 800E78C429E0DD5300D1B0FC /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */; }; 800ED7832B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */; }; 800FC544257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */; }; @@ -113,7 +113,6 @@ 80A6C6192B21205900416D50 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */; }; 80AB424F2B27CAC200249218 /* BraintreeTestShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A903E1A624F9D34000C314E1 /* BraintreeTestShared.framework */; platformFilter = ios; }; 80AB42542B27DF5B00249218 /* BraintreeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; platformFilter = ios; }; - 80AB42552B27DF5B00249218 /* BraintreeCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 80B59A132B27C7FC004C2FA3 /* BTShopperInsightsClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */; }; 80BA3C292B23892700900BBB /* FakeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA3C282B23892700900BBB /* FakeRequest.swift */; }; 80BA64AC29D788E000E15264 /* BTLocalPaymentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64AB29D788E000E15264 /* BTLocalPaymentResult.swift */; }; @@ -639,17 +638,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - 80AB42582B27DF5B00249218 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 80AB42552B27DF5B00249218 /* BraintreeCore.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; A9E80AA024FEF45400196BD3 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -756,16 +744,16 @@ 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BraintreePayPal_IntegrationTests.swift; sourceTree = ""; }; 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+NonceValidation.swift"; sourceTree = ""; }; 59A4135427B90CD7AE94D5CC /* Pods-Tests-IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests-IntegrationTests.debug.xcconfig"; path = "Target Support Files/Pods-Tests-IntegrationTests/Pods-Tests-IntegrationTests.debug.xcconfig"; sourceTree = ""; }; - 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsError.swift; sourceTree = ""; }; - 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsAnalytics_Tests.swift; sourceTree = ""; }; - 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentMethods.swift; sourceTree = ""; }; 620C3C642BA21E5700C40A03 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 62236E192B98CFB000CDCC37 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsError.swift; sourceTree = ""; }; + 62970D0E2B5AE1E400BAB584 /* BTShopperInsightsAnalytics_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsAnalytics_Tests.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; 800E23DC22206A8300C5D22E /* BTThreeDSecureAuthenticateJWT_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAuthenticateJWT_Tests.swift; sourceTree = ""; }; 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentsRequest.swift; sourceTree = ""; }; @@ -2064,7 +2052,6 @@ 8046982C2B27C5340090878E /* Sources */, 8046982D2B27C5340090878E /* Frameworks */, 8046982E2B27C5340090878E /* Resources */, - 80AB42582B27DF5B00249218 /* Embed Frameworks */, ); buildRules = ( ); From d784552705deda5b2d5839a11e894cdfffb74c1e Mon Sep 17 00:00:00 2001 From: Stephanie <127455800+stechiu@users.noreply.github.com> Date: Tue, 14 May 2024 12:37:31 -0700 Subject: [PATCH 52/57] Added "Origami Checkout" to Web Checkout Flow (#1298) * Added Origami Checkout * Changed label text * Update PayPalWebCheckoutViewController.swift --- Demo/Application/Base/AppDelegate.swift | 2 ++ .../Application/Base/ContainmentViewController.swift | 5 +++++ .../Base/Settings/BraintreeDemoSettings.swift | 1 + .../Base/Settings/Settings.bundle/Root.plist | 3 +++ .../Features/PayPalWebCheckoutViewController.swift | 12 ++++++++++++ 5 files changed, 23 insertions(+) diff --git a/Demo/Application/Base/AppDelegate.swift b/Demo/Application/Base/AppDelegate.swift index ea3f9a92ce..2881da263e 100644 --- a/Demo/Application/Base/AppDelegate.swift +++ b/Demo/Application/Base/AppDelegate.swift @@ -28,6 +28,8 @@ import BraintreeCore userDefaults.set(BraintreeDemoAuthType.clientToken.rawValue, forKey: BraintreeDemoSettings.AuthorizationTypeDefaultsKey) } else if processInfoArgs.contains("-TokenizationKey") { userDefaults.set(BraintreeDemoAuthType.tokenizationKey.rawValue, forKey: BraintreeDemoSettings.AuthorizationTypeDefaultsKey) + } else if processInfoArgs.contains("-NewPayPalCheckoutTokenizationKey") { + userDefaults.set(BraintreeDemoAuthType.newPayPalCheckoutTokenizationKey.rawValue, forKey: BraintreeDemoSettings.AuthorizationTypeDefaultsKey) } else if processInfoArgs.contains("-MockedPayPalTokenizationKey") { userDefaults.set(BraintreeDemoAuthType.mockedPayPalTokenizationKey.rawValue, forKey: BraintreeDemoSettings.AuthorizationTypeDefaultsKey) } else if processInfoArgs.contains("-UITestHardcodedClientToken") { diff --git a/Demo/Application/Base/ContainmentViewController.swift b/Demo/Application/Base/ContainmentViewController.swift index 93b08b28eb..3f7a9ca7cd 100644 --- a/Demo/Application/Base/ContainmentViewController.swift +++ b/Demo/Application/Base/ContainmentViewController.swift @@ -176,6 +176,11 @@ class ContainmentViewController: UIViewController { } } + case .newPayPalCheckoutTokenizationKey: + updateStatus("Fetching new checkout token...") + let newPayPalCheckoutTokenizationKey = "sandbox_rz48bqvw_jcyycfw6f9j4nj9c" + currentViewController = instantiateViewController(with: newPayPalCheckoutTokenizationKey) + case .mockedPayPalTokenizationKey: let tokenizationKey = "sandbox_q7v35n9n_555d2htrfsnnmfb3" currentViewController = instantiateViewController(with: tokenizationKey) diff --git a/Demo/Application/Base/Settings/BraintreeDemoSettings.swift b/Demo/Application/Base/Settings/BraintreeDemoSettings.swift index 42f53c7250..48188ef41c 100644 --- a/Demo/Application/Base/Settings/BraintreeDemoSettings.swift +++ b/Demo/Application/Base/Settings/BraintreeDemoSettings.swift @@ -11,6 +11,7 @@ enum BraintreeDemoEnvironment: Int { enum BraintreeDemoAuthType: Int { case clientToken case tokenizationKey + case newPayPalCheckoutTokenizationKey case mockedPayPalTokenizationKey case uiTestHardcodedClientToken } diff --git a/Demo/Application/Base/Settings/Settings.bundle/Root.plist b/Demo/Application/Base/Settings/Settings.bundle/Root.plist index 6e88cab14c..4627481686 100644 --- a/Demo/Application/Base/Settings/Settings.bundle/Root.plist +++ b/Demo/Application/Base/Settings/Settings.bundle/Root.plist @@ -91,6 +91,7 @@ Client Token Tokenization Key + New PayPal Checkout Tokenization Key Key BraintreeDemoSettingsAuthorizationTypeKey @@ -100,11 +101,13 @@ Client Token Tokenization Key + New PayPal Checkout Tokenization Key Values 0 1 + 2 diff --git a/Demo/Application/Features/PayPalWebCheckoutViewController.swift b/Demo/Application/Features/PayPalWebCheckoutViewController.swift index a794314b7f..58e05d9cc9 100644 --- a/Demo/Application/Features/PayPalWebCheckoutViewController.swift +++ b/Demo/Application/Features/PayPalWebCheckoutViewController.swift @@ -28,6 +28,15 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { let payLaterToggle = UISwitch() + lazy var newPayPalCheckoutToggleLabel: UILabel = { + let label = UILabel() + label.text = "New PayPal Checkout Experience" + label.font = .preferredFont(forTextStyle: .footnote) + return label + }() + + let newPayPalCheckoutToggle = UISwitch() + override func createPaymentButton() -> UIView { let payPalCheckoutButton = createButton(title: "PayPal Checkout", action: #selector(tappedPayPalCheckout)) let payPalVaultButton = createButton(title: "PayPal Vault", action: #selector(tappedPayPalVault)) @@ -36,6 +45,7 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { UIStackView(arrangedSubviews: [emailLabel, emailTextField]), buttonsStackView(label: "1-Time Checkout", views: [ UIStackView(arrangedSubviews: [payLaterToggleLabel, payLaterToggle]), + UIStackView(arrangedSubviews: [newPayPalCheckoutToggleLabel, newPayPalCheckoutToggle]), payPalCheckoutButton ]), buttonsStackView(label: "Vault",views: [payPalVaultButton]) @@ -66,6 +76,8 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { request.offerPayLater = payLaterToggle.isOn + request.intent = newPayPalCheckoutToggle.isOn ? .sale : .authorize + payPalClient.tokenize(request) { nonce, error in sender.isEnabled = true From 30009798bde3c907d5100404b7c06f2c6947ddcb Mon Sep 17 00:00:00 2001 From: agedd <105314544+agedd@users.noreply.github.com> Date: Wed, 15 May 2024 10:45:17 -0500 Subject: [PATCH 53/57] don't allow empty payer email in one-time checkout request (#1301) --- Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift | 2 +- .../BTPayPalCheckoutRequest_Tests.swift | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift b/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift index 2ad8ac8ae1..ca6d364792 100644 --- a/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift +++ b/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift @@ -134,7 +134,7 @@ import BraintreeCore checkoutParameters["currency_iso_code"] = currencyCode } - if let userAuthenticationEmail { + if let userAuthenticationEmail, !userAuthenticationEmail.isEmpty { checkoutParameters["payer_email"] = userAuthenticationEmail } diff --git a/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift index 5bf58927eb..eb59465928 100644 --- a/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift +++ b/UnitTests/BraintreePayPalTests/BTPayPalCheckoutRequest_Tests.swift @@ -159,4 +159,13 @@ class BTPayPalCheckoutRequest_Tests: XCTestCase { XCTAssertNil(parameters["request_billing_agreement"]) XCTAssertNil(parameters["billing_agreement_details"]) } + + func testParametersWithConfiguration_whenUserAuthenticationEmailNotSet_doesNotSetPayerEmailInRequest() { + let request = BTPayPalCheckoutRequest(amount: "1") + request.userAuthenticationEmail = "" + + let parameters = request.parameters(with: configuration) + + XCTAssertNil(parameters["payer_email"]) + } } From 431c5cbbcbbf09e7a4da83bcad8b02f205e53b7e Mon Sep 17 00:00:00 2001 From: agedd <105314544+agedd@users.noreply.github.com> Date: Tue, 21 May 2024 16:48:41 -0500 Subject: [PATCH 54/57] Add isEligibleInPayPalNetwork Check to ShopperInsightsResult (#1315) * add iseligibleinpaypalnetwork check * cleanup logic * code cleanup and add unit tests * address pr comments --- .../ShopperInsightsViewController.swift | 2 +- .../BTShopperInsightsClient.swift | 17 +--- .../BTShopperInsightsResult.swift | 3 + .../BTShopperInsightsClient_Tests.swift | 79 +++++++++++++++++++ 4 files changed, 86 insertions(+), 15 deletions(-) diff --git a/Demo/Application/Features/ShopperInsightsViewController.swift b/Demo/Application/Features/ShopperInsightsViewController.swift index b0338c1da0..7fa66c1fde 100644 --- a/Demo/Application/Features/ShopperInsightsViewController.swift +++ b/Demo/Application/Features/ShopperInsightsViewController.swift @@ -112,7 +112,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { Task { do { let result = try await shopperInsightsClient.getRecommendedPaymentMethods(request: request) - self.progressBlock("PayPal Recommended: \(result.isPayPalRecommended)\nVenmo Recommended: \(result.isVenmoRecommended)") + self.progressBlock("PayPal Recommended: \(result.isPayPalRecommended)\nVenmo Recommended: \(result.isVenmoRecommended)\nEligible in PayPal Network: \(result.isEligibleInPayPalNetwork)") self.payPalVaultButton.isEnabled = result.isPayPalRecommended self.venmoButton.isEnabled = result.isVenmoRecommended } catch { diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index ace9dfcd90..0d1ad9979e 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -53,26 +53,15 @@ public class BTShopperInsightsClient { } let eligiblePaymentMethods = BTEligiblePaymentMethods(json: json) let result = BTShopperInsightsResult( - isPayPalRecommended: isPaymentRecommended(eligiblePaymentMethods.paypal), - isVenmoRecommended: isPaymentRecommended(eligiblePaymentMethods.venmo) + isPayPalRecommended: eligiblePaymentMethods.paypal?.recommended ?? false, + isVenmoRecommended: eligiblePaymentMethods.venmo?.recommended ?? false, + isEligibleInPayPalNetwork: eligiblePaymentMethods.paypal?.eligibleInPaypalNetwork ?? false || eligiblePaymentMethods.venmo?.eligibleInPaypalNetwork ?? false ) return self.notifySuccess(with: result) } catch { throw self.notifyFailure(with: error) } } - - /// This method determines whether a payment source is recommended - /// - Parameters: - /// - paymentMethodDetail: a `BTEligiblePaymentMethodDetails` containing the payment source's information - /// - Returns: `true` if both `eligibleInPPNetwork` and `recommended` are enabled, otherwise returns false. - private func isPaymentRecommended(_ paymentMethodDetail: BTEligiblePaymentMethodDetails?) -> Bool { - if let eligibleInPPNetwork = paymentMethodDetail?.eligibleInPaypalNetwork, - let recommended = paymentMethodDetail?.recommended { - return eligibleInPPNetwork && recommended - } - return false - } /// Call this method when the PayPal button has been successfully displayed to the buyer. /// This method sends analytics to help improve the Shopper Insights feature experience. diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsResult.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsResult.swift index de0a24ee9c..cb65ec6e0b 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsResult.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsResult.swift @@ -9,4 +9,7 @@ public struct BTShopperInsightsResult { /// If true, dislpay the Venmo button with high priority. public var isVenmoRecommended = false + + /// If true, buyer is a member of the PayPal Inc. (PayPal, Venmo, Honey) network. + public var isEligibleInPayPalNetwork = false } diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift index 2e6c18fa37..c35d2b7fa9 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsClient_Tests.swift @@ -93,6 +93,85 @@ class BTShopperInsightsClient_Tests: XCTestCase { } } + func testGetRecommendedPaymentMethods_whenEligibleInPayPalNetworkTrue_returnsOnlyPayPalRecommended() async { + do { + let mockPayPalRecommendedResponse = BTJSON( + value: [ + "eligible_methods": [ + "paypal": [ + "can_be_vaulted": true, + "eligible_in_paypal_network": true, + "recommended": true, + "recommended_priority": 1 + ] + ] + ] + ) + mockAPIClient.cannedResponseBody = mockPayPalRecommendedResponse + let result = try await sut.getRecommendedPaymentMethods(request: request) + XCTAssertTrue(result.isPayPalRecommended) + XCTAssertFalse(result.isVenmoRecommended) + XCTAssertTrue(result.isEligibleInPayPalNetwork) + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last, "shopper-insights:get-recommended-payments:succeeded") + } catch { + XCTFail("An error was not expected.") + } + } + + func testGetRecommendedPaymentMethods_whenEligibleInPayPalNetworkTrue_returnsOnlyVenmoRecommended() async { + do { + let mockVenmoRecommendedResponse = BTJSON( + value: [ + "eligible_methods": [ + "venmo": [ + "can_be_vaulted": true, + "eligible_in_paypal_network": true, + "recommended": true, + "recommended_priority": 1 + ] + ] + ] + ) + mockAPIClient.cannedResponseBody = mockVenmoRecommendedResponse + let result = try await sut.getRecommendedPaymentMethods(request: request) + XCTAssertFalse(result.isPayPalRecommended) + XCTAssertTrue(result.isVenmoRecommended) + XCTAssertTrue(result.isEligibleInPayPalNetwork) + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last, "shopper-insights:get-recommended-payments:succeeded") + } catch { + XCTFail("An error was not expected.") + } + } + + func testGetRecommendedPaymentMethods_whenBothEligibleInPayPalNetworkFalse_returnsResult() async { + do { + let mockPayPalRecommendedResponse = BTJSON( + value: [ + "eligible_methods": [ + "paypal": [ + "can_be_vaulted": true, + "eligible_in_paypal_network": false, + "recommended": false, + ], + "venmo": [ + "can_be_vaulted": true, + "eligible_in_paypal_network": false, + "recommended": false, + ] + ] + ] + ) + mockAPIClient.cannedResponseBody = mockPayPalRecommendedResponse + let result = try await sut.getRecommendedPaymentMethods(request: request) + XCTAssertFalse(result.isPayPalRecommended) + XCTAssertFalse(result.isVenmoRecommended) + XCTAssertFalse(result.isEligibleInPayPalNetwork) + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last, "shopper-insights:get-recommended-payments:succeeded") + } catch { + XCTFail("An error was not expected.") + } + } + // MARK: - Analytics func testSendPayPalPresentedEvent_sendsAnalytic() { From 60fa7da65a26fe782bba5bf30214b96c5be9e7cc Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 5 Jun 2024 16:14:20 -0500 Subject: [PATCH 55/57] Address merge conflict issues in UnitTest scheme build --- .../xcshareddata/xcschemes/UnitTests.xcscheme | 20 +++++++++++++------ .../BraintreeCoreTests/BTHTTP_Tests.swift | 4 ++-- UnitTests/BraintreeTestShared/FakeHTTP.swift | 4 ++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme index 6bdced205d..93d91e3c01 100644 --- a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme +++ b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme @@ -14,15 +14,26 @@ buildForAnalyzing = "NO"> + + + + Void)? = nil) { + public override func post(_ path: String, configuration: BTConfiguration? = nil, parameters: [String: Any]? = nil, headers: [String: String]? = nil, completion: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { POSTRequestCount += 1 lastRequestEndpoint = path lastRequestParameters = parameters @@ -84,7 +84,7 @@ import Foundation return self.init(authorization: fakeTokenizationKey, customBaseURL: URL(string: "http://fake.com")!) } - public override func post(_ path: String, configuration: BTConfiguration? = nil, parameters: [String: Any]?, completion: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { + public override func post(_ path: String, configuration: BTConfiguration? = nil, parameters: [String: Any]?, headers: [String: String]? = nil, completion: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) { POSTRequestCount += 1 lastRequestParameters = parameters completion?(self.cannedConfiguration, nil, nil) From 3df870c26bc845fa9e818fbf7a3f7a88d9e58af4 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Wed, 5 Jun 2024 16:22:03 -0500 Subject: [PATCH 56/57] Remove no-longer-needed unit test; re-introduced from merge conflict --- .../BraintreeCoreTests/BTHTTP_Tests.swift | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift b/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift index ec5136e7fc..083bdfd063 100644 --- a/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift +++ b/UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift @@ -272,25 +272,6 @@ final class BTHTTP_Tests: XCTestCase { waitForExpectations(timeout: 2) } - func testPOSTRequests_whenBTHTTPInitializedWithPayPalAPIURL_doesNotSendAuthorizationInBody() { - let expectation = expectation(description: "POST callback") - - let http = BTHTTP(authorization: fakeClientToken, customBaseURL: URL(string: "https://api-m.paypal.com")!) - http.session = testURLSession - - http.post("200.json") { body, response, error in - XCTAssertNotNil(body) - XCTAssertNotNil(response) - XCTAssertNil(error) - - let httpRequestBody = BTHTTPTestProtocol.parseRequestBodyFromTestResponseBody(body!) - XCTAssertFalse(httpRequestBody.contains("authorization_fingerprint")) - expectation.fulfill() - } - - waitForExpectations(timeout: 2) - } - func testPOSTRequests_whenBTHTTPInitializedWithPayPalAPIURL_sendsAuthorizationInHeader() { let expectation = expectation(description: "POST callback") From 55e5f0b3eb947b1c4e682cd7b86a94e55d830de3 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 6 Jun 2024 14:36:39 -0500 Subject: [PATCH 57/57] Shopper Insights Final Changes (#1332) * update release yml JSON output file name * update CHANGELOG * update spacing in PayPalWebCheckoutViewController * remove outdated BraintreeDemoPreferredPaymentMethodsViewController * cleanup syntax in ShopperInsightsViewController * sort files by name * update enum case * update spacing * update payPal casing in analytics * fix typos in BTEligiblePaymentMethods * move tests to UnitTest scheme * correct test to drop -m --- .github/workflows/release.yml | 2 +- Braintree.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/xcschemes/UnitTests.xcscheme | 10 ++ CHANGELOG.md | 3 +- .../PayPalWebCheckoutViewController.swift | 5 +- ...referredPaymentMethodsViewController.swift | 132 ------------------ .../ShopperInsightsViewController.swift | 22 +-- .../BTEligiblePaymentMethods.swift | 12 +- .../BTShopperInsightsAnalytics.swift | 4 +- .../BTShopperInsightsClient.swift | 10 +- .../BTShopperInsightsError.swift | 2 +- .../Analytics/BTAnalyticsService_Tests.swift | 2 +- .../BTShopperInsightsAnalytics_Tests.swift | 4 +- 13 files changed, 43 insertions(+), 173 deletions(-) delete mode 100644 Demo/Application/Features/Preferred Payment Methods/BraintreeDemoPreferredPaymentMethodsViewController.swift diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7e520ae48..9d4d199f70 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -111,7 +111,7 @@ jobs: sourcekitten doc -- -workspace Braintree.xcworkspace -scheme BraintreePayPal -destination 'name=iPhone 14,platform=iOS Simulator' > braintree-paypal.json sourcekitten doc -- -workspace Braintree.xcworkspace -scheme BraintreeVenmo -destination 'name=iPhone 14,platform=iOS Simulator' > braintree-venmo.json sourcekitten doc -- -workspace Braintree.xcworkspace -scheme BraintreePayPalMessaging -destination 'name=iPhone 14,platform=iOS Simulator' > braintree-paypal-messaging.json - sourcekitten doc -- -workspace Braintree.xcworkspace -scheme BraintreeShopperInsights -destination 'name=iPhone 14,platform=iOS Simulator' > braintree-paypal-messaging.json + sourcekitten doc -- -workspace Braintree.xcworkspace -scheme BraintreeShopperInsights -destination 'name=iPhone 14,platform=iOS Simulator' > braintree-shopper-insights.json # merge sourcekitten output jq -s '.[0] + .[1] + .[2] + .[3] + .[4] + .[5] + .[6] + .[7] + .[8] + .[9] + .[10] + .[11] + .[12]' braintree-core.json braintree-pay-pal-native-checkout.json braintree-sepa-direct-debit.json braintree-american-express.json braintree-data-collector.json braintree-apple-pay.json braintree-local-payment.json braintree-three-d-secure.json braintree-card.json braintree-paypal.json braintree-venmo.json braintree-paypal-messaging.json braintree-shopper-insights.json > swiftDoc.json diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 7f4ee66cca..9cc3e4b56e 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -114,9 +114,9 @@ 80A6C6192B21205900416D50 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */; }; 80AB424F2B27CAC200249218 /* BraintreeTestShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A903E1A624F9D34000C314E1 /* BraintreeTestShared.framework */; platformFilter = ios; }; 80AB42542B27DF5B00249218 /* BraintreeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; platformFilter = ios; }; - 80B59A132B27C7FC004C2FA3 /* BTShopperInsightsClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */; }; 80AD35F22BFBB1DD00BF890E /* TokenizationKeyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80AD35F12BFBB1DD00BF890E /* TokenizationKeyError.swift */; }; 80B207332BF6C0F100787E37 /* TokenizationKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B207322BF6C0F100787E37 /* TokenizationKey.swift */; }; + 80B59A132B27C7FC004C2FA3 /* BTShopperInsightsClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F3942B1E4FEB0059C4CB /* BTShopperInsightsClient_Tests.swift */; }; 80BA3C292B23892700900BBB /* FakeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA3C282B23892700900BBB /* FakeRequest.swift */; }; 80BA64AC29D788E000E15264 /* BTLocalPaymentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64AB29D788E000E15264 /* BTLocalPaymentResult.swift */; }; 80BA64B229D7937E00E15264 /* BTLocalPaymentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64B129D7937E00E15264 /* BTLocalPaymentRequest.swift */; }; @@ -1452,13 +1452,13 @@ 804698292B27C4D70090878E /* BraintreeShopperInsights */ = { isa = PBXGroup; children = ( + 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */, + 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */, 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */, 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */, + 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */, 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */, 8064F3902B1E49E10059C4CB /* BTShopperInsightsResult.swift */, - 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */, - 62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */, - 624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */, 3B29C3942B90F12F0077741D /* PrivacyInfo.xcprivacy */, ); path = BraintreeShopperInsights; diff --git a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme index 93d91e3c01..cf93ea140e 100644 --- a/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme +++ b/Braintree.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme @@ -173,6 +173,16 @@ ReferencedContainer = "container:Braintree.xcodeproj"> + + + + UIView { let buttons = [payPalVaultButton, venmoButton] buttons.forEach { $0.isEnabled = false } + let stackView = UIStackView(arrangedSubviews: buttons) stackView.axis = .vertical stackView.alignment = .center @@ -131,7 +121,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { let paypalRequest = BTPayPalVaultRequest() paypalRequest.userAuthenticationEmail = emailView.textField.text ?? nil - paypalClient.tokenize(paypalRequest) { (nonce, error) in + payPalClient.tokenize(paypalRequest) { nonce, error in button.isEnabled = true self.displayResultDetails(nonce: nonce, error: error) } @@ -145,7 +135,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { button.isEnabled = false let venmoRequest = BTVenmoRequest(paymentMethodUsage: .multiUse) - venmoClient.tokenize(venmoRequest) { (nonce, error) in + venmoClient.tokenize(venmoRequest) { nonce, error in button.isEnabled = true self.displayResultDetails(nonce: nonce, error: error) } diff --git a/Sources/BraintreeShopperInsights/BTEligiblePaymentMethods.swift b/Sources/BraintreeShopperInsights/BTEligiblePaymentMethods.swift index 90ce49cfef..4e64e6f91c 100644 --- a/Sources/BraintreeShopperInsights/BTEligiblePaymentMethods.swift +++ b/Sources/BraintreeShopperInsights/BTEligiblePaymentMethods.swift @@ -3,26 +3,26 @@ import BraintreeCore #endif struct BTEligiblePaymentMethods { - var paypal: BTEligiblePaymentMethodDetails? + var payPal: BTEligiblePaymentMethodDetails? var venmo: BTEligiblePaymentMethodDetails? init(json: BTJSON?) { - if let eligibileMethodsJSON = json?["eligible_methods"] { - self.paypal = BTEligiblePaymentMethodDetails(json: eligibileMethodsJSON["paypal"]) - self.venmo = BTEligiblePaymentMethodDetails(json: eligibileMethodsJSON["venmo"]) + if let eligibleMethodsJSON = json?["eligible_methods"] { + self.payPal = BTEligiblePaymentMethodDetails(json: eligibleMethodsJSON["paypal"]) + self.venmo = BTEligiblePaymentMethodDetails(json: eligibleMethodsJSON["venmo"]) } } } struct BTEligiblePaymentMethodDetails { let canBeVaulted: Bool - let eligibleInPaypalNetwork: Bool + let eligibleInPayPalNetwork: Bool let recommended: Bool let recommendedPriority: Int init(json: BTJSON) { self.canBeVaulted = json["can_be_vaulted"].asBool() ?? false - self.eligibleInPaypalNetwork = json["eligible_in_paypal_network"].asBool() ?? false + self.eligibleInPayPalNetwork = json["eligible_in_paypal_network"].asBool() ?? false self.recommended = json["recommended"].asBool() ?? false self.recommendedPriority = json["recommended_priority"].asIntegerOrZero() } diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsAnalytics.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsAnalytics.swift index 0f8d385405..fefbcd5fa1 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsAnalytics.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsAnalytics.swift @@ -4,8 +4,8 @@ enum BTShopperInsightsAnalytics { // MARK: - Merchant Triggered Events - static let paypalPresented = "shopper-insights:paypal-presented" - static let paypalSelected = "shopper-insights:paypal-selected" + static let payPalPresented = "shopper-insights:paypal-presented" + static let payPalSelected = "shopper-insights:paypal-selected" static let venmoPresented = "shopper-insights:venmo-presented" static let venmoSelected = "shopper-insights:venmo-selected" diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift index 0d1ad9979e..d3d43b14e2 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift @@ -47,15 +47,17 @@ public class BTShopperInsightsClient { headers: ["PayPal-Client-Metadata-Id": apiClient.metadata.sessionID], httpType: .payPalAPI ) + guard let eligibleMethodsJSON = json?["eligible_methods"].asDictionary(), eligibleMethodsJSON.count != 0 else { throw self.notifyFailure(with: BTShopperInsightsError.emptyBodyReturned) } + let eligiblePaymentMethods = BTEligiblePaymentMethods(json: json) let result = BTShopperInsightsResult( - isPayPalRecommended: eligiblePaymentMethods.paypal?.recommended ?? false, + isPayPalRecommended: eligiblePaymentMethods.payPal?.recommended ?? false, isVenmoRecommended: eligiblePaymentMethods.venmo?.recommended ?? false, - isEligibleInPayPalNetwork: eligiblePaymentMethods.paypal?.eligibleInPaypalNetwork ?? false || eligiblePaymentMethods.venmo?.eligibleInPaypalNetwork ?? false + isEligibleInPayPalNetwork: eligiblePaymentMethods.payPal?.eligibleInPayPalNetwork ?? false || eligiblePaymentMethods.venmo?.eligibleInPayPalNetwork ?? false ) return self.notifySuccess(with: result) } catch { @@ -66,13 +68,13 @@ public class BTShopperInsightsClient { /// Call this method when the PayPal button has been successfully displayed to the buyer. /// This method sends analytics to help improve the Shopper Insights feature experience. public func sendPayPalPresentedEvent() { - apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.paypalPresented) + apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.payPalPresented) } /// Call this method when the PayPal button has been selected/tapped by the buyer. /// This method sends analytics to help improve the Shopper Insights feature experience public func sendPayPalSelectedEvent() { - apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.paypalSelected) + apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.payPalSelected) } /// Call this method when the Venmo button has been successfully displayed to the buyer. diff --git a/Sources/BraintreeShopperInsights/BTShopperInsightsError.swift b/Sources/BraintreeShopperInsights/BTShopperInsightsError.swift index 712d2995ff..4a4ddd2d22 100644 --- a/Sources/BraintreeShopperInsights/BTShopperInsightsError.swift +++ b/Sources/BraintreeShopperInsights/BTShopperInsightsError.swift @@ -2,7 +2,7 @@ import Foundation public enum BTShopperInsightsError: Int, Error, CustomNSError, LocalizedError, Equatable { - /// 1. A nil body was returned from the payment method request and no error was returned. + /// 0. A nil body was returned from the payment method request and no error was returned. case emptyBodyReturned public static var errorDomain: String { diff --git a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift index 7e1a60f0f9..1df55c7d6b 100644 --- a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift @@ -19,7 +19,7 @@ final class BTAnalyticsService_Tests: XCTestCase { await analyticsService.performEventRequest("any.analytics.event") - XCTAssertEqual(analyticsService.http?.customBaseURL?.absoluteString, "https://api-m.paypal.com") + XCTAssertEqual(analyticsService.http?.customBaseURL?.absoluteString, "https://api.paypal.com") } func testSendAnalyticsEvent_sendsAnalyticsEvent() async { diff --git a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsAnalytics_Tests.swift b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsAnalytics_Tests.swift index 40599bf5a5..991a240b45 100644 --- a/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsAnalytics_Tests.swift +++ b/UnitTests/BraintreeShopperInsightsTests/BTShopperInsightsAnalytics_Tests.swift @@ -4,8 +4,8 @@ import XCTest final class BTShopperInsightsAnalytics_Tests: XCTestCase { func test_recommendedPaymentAnalyticEvents_sendExpectedEventNames() { - XCTAssertEqual(BTShopperInsightsAnalytics.paypalPresented, "shopper-insights:paypal-presented") - XCTAssertEqual(BTShopperInsightsAnalytics.paypalSelected, "shopper-insights:paypal-selected") + XCTAssertEqual(BTShopperInsightsAnalytics.payPalPresented, "shopper-insights:paypal-presented") + XCTAssertEqual(BTShopperInsightsAnalytics.payPalSelected, "shopper-insights:paypal-selected") XCTAssertEqual(BTShopperInsightsAnalytics.venmoPresented, "shopper-insights:venmo-presented") XCTAssertEqual(BTShopperInsightsAnalytics.venmoSelected, "shopper-insights:venmo-selected") XCTAssertEqual(BTShopperInsightsAnalytics.recommendedPaymentsStarted, "shopper-insights:get-recommended-payments:started")