diff --git a/AmericanExpress/build.gradle b/AmericanExpress/build.gradle index 8cd2a9fdb9..903e0dba70 100644 --- a/AmericanExpress/build.gradle +++ b/AmericanExpress/build.gradle @@ -39,6 +39,7 @@ dependencies { testImplementation libs.robolectric testImplementation libs.mockito.core testImplementation libs.json.assert + testImplementation libs.test.parameter.injector testImplementation project(':TestUtils') } diff --git a/BraintreeCore/build.gradle b/BraintreeCore/build.gradle index 180dedf6c8..cc5debecff 100644 --- a/BraintreeCore/build.gradle +++ b/BraintreeCore/build.gradle @@ -87,6 +87,7 @@ dependencies { testImplementation libs.mockito.core testImplementation libs.json.assert testImplementation libs.mockk + testImplementation libs.test.parameter.injector testImplementation libs.kotlin.test testImplementation project(':PayPal') testImplementation project(':TestUtils') diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d94d6c93..2cf11a56a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Braintree Android SDK Release Notes +## unreleased + +* PayPal + * Add PayPal App Switch for one time checkout flow (BETA) + * Add `enablePayPalAppSwitch` property to `PayPalCheckoutRequest` for App Switch support + * Require `PayPalCheckoutRequest.userAuthenticationEmail` for App Switch support + * Require `PayPalClient.appLinkReturnUrl` for App Switch support + * **Note:** This feature is currently in beta and may change or be removed in future releases. + ## 5.2.0 (2024-10-30) * GooglePay diff --git a/Card/build.gradle b/Card/build.gradle index 337fe77e33..7ff1504048 100644 --- a/Card/build.gradle +++ b/Card/build.gradle @@ -44,6 +44,7 @@ dependencies { testImplementation libs.androidx.core.ktx testImplementation libs.kotlin.stdlib testImplementation libs.kotlin.test + testImplementation libs.test.parameter.injector androidTestImplementation project(':TestUtils') androidTestImplementation libs.androidx.test.rules diff --git a/DataCollector/build.gradle b/DataCollector/build.gradle index f5ac385074..d7d55bcd1b 100644 --- a/DataCollector/build.gradle +++ b/DataCollector/build.gradle @@ -48,6 +48,7 @@ dependencies { testImplementation libs.json.assert testImplementation libs.mockito.core testImplementation libs.mockk + testImplementation libs.test.parameter.injector testImplementation project(':TestUtils') diff --git a/Demo/build.gradle b/Demo/build.gradle index 27a7e69a61..b1c0b6d8ce 100644 --- a/Demo/build.gradle +++ b/Demo/build.gradle @@ -106,6 +106,7 @@ dependencies { androidTestImplementation project(':TestUtils') testImplementation libs.junit + testImplementation libs.test.parameter.injector } task demoApkSize { diff --git a/Demo/src/main/java/com/braintreepayments/demo/PayPalRequestFactory.java b/Demo/src/main/java/com/braintreepayments/demo/PayPalRequestFactory.java index efdc0d8bd5..2318c09390 100644 --- a/Demo/src/main/java/com/braintreepayments/demo/PayPalRequestFactory.java +++ b/Demo/src/main/java/com/braintreepayments/demo/PayPalRequestFactory.java @@ -123,6 +123,10 @@ public static PayPalCheckoutRequest createPayPalCheckoutRequest( request.setUserPhoneNumber(new PayPalPhoneNumber(buyerPhoneCountryCode, buyerPhoneNationalNumber)); } + if (Settings.isPayPalAppSwithEnabled(context)) { + request.setEnablePayPalAppSwitch(true); + } + request.setDisplayName(Settings.getPayPalDisplayName(context)); String landingPageType = Settings.getPayPalLandingPageType(context); diff --git a/Demo/src/main/res/values/strings.xml b/Demo/src/main/res/values/strings.xml index d3272218a6..9815484262 100644 --- a/Demo/src/main/res/values/strings.xml +++ b/Demo/src/main/res/values/strings.xml @@ -8,7 +8,7 @@ Nonce: %s Device Data: %s Google Pay - PayPal Web + PayPal Visa Checkout Pay with iDEAL Local Payment diff --git a/GooglePay/build.gradle b/GooglePay/build.gradle index dc95ea28b4..918d79d222 100644 --- a/GooglePay/build.gradle +++ b/GooglePay/build.gradle @@ -55,6 +55,7 @@ dependencies { testImplementation libs.kotlin.stdlib testImplementation libs.coroutines.core testImplementation libs.mockk + testImplementation libs.test.parameter.injector testImplementation libs.androidx.test.core } diff --git a/LocalPayment/build.gradle b/LocalPayment/build.gradle index 36ffebe470..7871cb7635 100644 --- a/LocalPayment/build.gradle +++ b/LocalPayment/build.gradle @@ -41,6 +41,7 @@ dependencies { testImplementation libs.mockito.core testImplementation libs.json.assert testImplementation libs.mockk + testImplementation libs.test.parameter.injector testImplementation project(':TestUtils') androidTestImplementation project(':TestUtils') diff --git a/PayPal/build.gradle b/PayPal/build.gradle index 850bcfc88d..7065231883 100644 --- a/PayPal/build.gradle +++ b/PayPal/build.gradle @@ -42,6 +42,7 @@ dependencies { testImplementation libs.json.assert testImplementation libs.mockito.core testImplementation libs.mockk + testImplementation libs.test.parameter.injector testImplementation project(':TestUtils') } diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalCheckoutRequest.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalCheckoutRequest.kt index e95d708813..1ab0bec7cb 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalCheckoutRequest.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalCheckoutRequest.kt @@ -1,5 +1,6 @@ package com.braintreepayments.api.paypal +import android.os.Build import android.text.TextUtils import com.braintreepayments.api.core.Authorization import com.braintreepayments.api.core.ClientToken @@ -64,6 +65,7 @@ class PayPalCheckoutRequest @JvmOverloads constructor( var currencyCode: String? = null, var shouldRequestBillingAgreement: Boolean = false, var shouldOfferPayLater: Boolean = false, + override var enablePayPalAppSwitch: Boolean = false, override var localeCode: String? = null, override var billingAgreementDescription: String? = null, override var isShippingAddressRequired: Boolean = false, @@ -87,6 +89,7 @@ class PayPalCheckoutRequest @JvmOverloads constructor( displayName = displayName, merchantAccountId = merchantAccountId, riskCorrelationId = riskCorrelationId, + enablePayPalAppSwitch = enablePayPalAppSwitch, userAuthenticationEmail = userAuthenticationEmail, lineItems = lineItems ) { @@ -126,6 +129,13 @@ class PayPalCheckoutRequest @JvmOverloads constructor( userPhoneNumber?.let { parameters.put(PHONE_NUMBER_KEY, it.toJson()) } + if (enablePayPalAppSwitch && !appLink.isNullOrEmpty() && !userAuthenticationEmail.isNullOrEmpty()) { + parameters.put(ENABLE_APP_SWITCH_KEY, enablePayPalAppSwitch) + parameters.put(OS_VERSION_KEY, Build.VERSION.SDK_INT.toString()) + parameters.put(OS_TYPE_KEY, "Android") + parameters.put(MERCHANT_APP_RETURN_URL_KEY, appLink) + } + if (currencyCode == null) { currencyCode = configuration?.payPalCurrencyIsoCode } diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt index cfb9427d40..afd4c99704 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt @@ -44,9 +44,8 @@ internal class PayPalInternalClient( CREATE_SINGLE_PAYMENT_ENDPOINT } val url = "/v1/$endpoint" - val appLinkReturn = if (isBillingAgreement) appLink else null - if (isBillingAgreement && (payPalRequest as PayPalVaultRequest).enablePayPalAppSwitch) { + if (payPalRequest.enablePayPalAppSwitch) { payPalRequest.enablePayPalAppSwitch = isPayPalInstalled(context) } @@ -55,7 +54,7 @@ internal class PayPalInternalClient( authorization = merchantRepository.authorization, successUrl = successUrl, cancelUrl = cancelUrl, - appLink = appLinkReturn + appLink = appLink ) ?: throw JSONException("Error creating requestBody") sendPost( diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRequest.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRequest.kt index adfd68924c..8239d7588b 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRequest.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalRequest.kt @@ -68,6 +68,9 @@ import org.json.JSONException * tokenization. * @property riskCorrelationId A risk correlation ID created with Set Transaction Context on your * server. + * @property enablePayPalAppSwitch Used to determine if the customer will use the PayPal app switch flow. + * Defaults to `false`. + * - Warning: This property is currently in beta and may change or be removed in future releases. * @property userAuthenticationEmail User email to initiate a quicker authentication flow in cases * where the user has a PayPal Account with the same email. * @property userPhoneNumber User phone number used to initiate a quicker authentication flow in @@ -85,6 +88,7 @@ abstract class PayPalRequest internal constructor( open var displayName: String? = null, open var merchantAccountId: String? = null, open var riskCorrelationId: String? = null, + open var enablePayPalAppSwitch: Boolean = false, open var userAuthenticationEmail: String? = null, open var userPhoneNumber: PayPalPhoneNumber? = null, open var lineItems: List = emptyList() diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.kt index ea19bfb618..cb4493527a 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalVaultRequest.kt @@ -26,9 +26,6 @@ import org.json.JSONObject * @property shouldOfferCredit Offers PayPal Credit if the customer qualifies. Defaults to false. * @property recurringBillingDetails Optional: Recurring billing product details. * @property recurringBillingPlanType Optional: Recurring billing plan type, or charge pattern. - * @property enablePayPalAppSwitch Used to determine if the customer will use the PayPal app switch flow. - * Defaults to `false`. - * - Warning: This property is currently in beta and may change or be removed in future releases. */ @Parcelize class PayPalVaultRequest @@ -37,7 +34,7 @@ class PayPalVaultRequest var shouldOfferCredit: Boolean = false, var recurringBillingDetails: PayPalRecurringBillingDetails? = null, var recurringBillingPlanType: PayPalRecurringBillingPlanType? = null, - var enablePayPalAppSwitch: Boolean = false, + override var enablePayPalAppSwitch: Boolean = false, override var localeCode: String? = null, override var billingAgreementDescription: String? = null, override var isShippingAddressRequired: Boolean = false, @@ -61,6 +58,7 @@ class PayPalVaultRequest displayName = displayName, merchantAccountId = merchantAccountId, riskCorrelationId = riskCorrelationId, + enablePayPalAppSwitch = enablePayPalAppSwitch, userAuthenticationEmail = userAuthenticationEmail, lineItems = lineItems ) { diff --git a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalCheckoutRequestUnitTest.java b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalCheckoutRequestUnitTest.java index 8752cde5f0..4a3e91952e 100644 --- a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalCheckoutRequestUnitTest.java +++ b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalCheckoutRequestUnitTest.java @@ -5,11 +5,12 @@ import com.braintreepayments.api.core.Authorization; import com.braintreepayments.api.core.Configuration; import com.braintreepayments.api.core.PostalAddress; +import com.google.testing.junit.testparameterinjector.TestParameter; import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; +import org.robolectric.RobolectricTestParameterInjector; import java.util.ArrayList; @@ -20,7 +21,7 @@ import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.mock; -@RunWith(RobolectricTestRunner.class) +@RunWith(RobolectricTestParameterInjector.class) public class PayPalCheckoutRequestUnitTest { @Test @@ -37,11 +38,13 @@ public void newPayPalCheckoutRequest_setsDefaultValues() { assertNull(request.getLandingPageType()); assertNull(request.getBillingAgreementDescription()); assertFalse(request.getShouldOfferPayLater()); + assertFalse(request.getEnablePayPalAppSwitch()); + assertNull(request.getUserAuthenticationEmail()); assertFalse(request.getHasUserLocationConsent()); } @Test - public void setsValuesCorrectly() { + public void setsValuesCorrectly(@TestParameter boolean appSwitchEnabled) { PostalAddress postalAddress = new PostalAddress(); PayPalCheckoutRequest request = new PayPalCheckoutRequest("1.00", true); request.setCurrencyCode("USD"); @@ -56,6 +59,8 @@ public void setsValuesCorrectly() { request.setUserAction(PayPalPaymentUserAction.USER_ACTION_COMMIT); request.setDisplayName("Display Name"); request.setRiskCorrelationId("123-correlation"); + request.setEnablePayPalAppSwitch(appSwitchEnabled); + request.setUserAuthenticationEmail("test-email"); request.setLandingPageType(PayPalLandingPageType.LANDING_PAGE_TYPE_LOGIN); assertEquals("1.00", request.getAmount()); @@ -69,13 +74,15 @@ public void setsValuesCorrectly() { assertEquals(PayPalPaymentUserAction.USER_ACTION_COMMIT, request.getUserAction()); assertEquals("Display Name", request.getDisplayName()); assertEquals("123-correlation", request.getRiskCorrelationId()); + assertEquals(appSwitchEnabled, request.getEnablePayPalAppSwitch()); + assertEquals("test-email", request.getUserAuthenticationEmail()); assertEquals(PayPalLandingPageType.LANDING_PAGE_TYPE_LOGIN, request.getLandingPageType()); assertTrue(request.getShouldOfferPayLater()); assertTrue(request.getHasUserLocationConsent()); } @Test - public void parcelsCorrectly() { + public void parcelsCorrectly(@TestParameter boolean appSwitchEnabled) { PayPalCheckoutRequest request = new PayPalCheckoutRequest("12.34", true); request.setCurrencyCode("USD"); request.setLocaleCode("en-US"); @@ -93,6 +100,8 @@ public void parcelsCorrectly() { request.setDisplayName("Display Name"); request.setRiskCorrelationId("123-correlation"); request.setMerchantAccountId("merchant_account_id"); + request.setEnablePayPalAppSwitch(appSwitchEnabled); + request.setUserAuthenticationEmail("test-email"); ArrayList lineItems = new ArrayList<>(); lineItems.add(new PayPalLineItem(PayPalLineItemKind.DEBIT, "An Item", "1", "1")); @@ -110,6 +119,8 @@ public void parcelsCorrectly() { result.getBillingAgreementDescription()); assertTrue(result.isShippingAddressRequired()); assertTrue(result.isShippingAddressEditable()); + assertEquals(appSwitchEnabled, result.getEnablePayPalAppSwitch()); + assertEquals("test-email", result.getUserAuthenticationEmail()); assertEquals("Postal Address", result.getShippingAddressOverride().getRecipientName()); assertEquals(PayPalPaymentIntent.SALE, result.getIntent()); assertEquals(PayPalLandingPageType.LANDING_PAGE_TYPE_LOGIN, result.getLandingPageType()); @@ -171,4 +182,5 @@ public void createRequestBody_sets_userPhoneNumber_when_not_null() throws JSONEx assertTrue(requestBody.contains("\"phone_number\":{\"country_code\":\"1\",\"national_number\":\"1231231234\"}")); } + } \ No newline at end of file diff --git a/PayPalMessaging/build.gradle b/PayPalMessaging/build.gradle index a91188ccfe..6df18ade05 100644 --- a/PayPalMessaging/build.gradle +++ b/PayPalMessaging/build.gradle @@ -46,6 +46,7 @@ dependencies { testImplementation libs.androidx.junit testImplementation libs.mockk testImplementation libs.mockito.core + testImplementation libs.test.parameter.injector testImplementation project(':TestUtils') } diff --git a/SEPADirectDebit/build.gradle b/SEPADirectDebit/build.gradle index 2925ab895f..450039518c 100644 --- a/SEPADirectDebit/build.gradle +++ b/SEPADirectDebit/build.gradle @@ -40,6 +40,7 @@ dependencies { testImplementation libs.androidx.junit testImplementation libs.mockito.core testImplementation libs.mockk + testImplementation libs.test.parameter.injector testImplementation project(':TestUtils') androidTestImplementation project(':TestUtils') diff --git a/SharedUtils/build.gradle b/SharedUtils/build.gradle index 5688d60762..69760b7ba5 100644 --- a/SharedUtils/build.gradle +++ b/SharedUtils/build.gradle @@ -40,6 +40,7 @@ dependencies { testImplementation libs.androidx.test.core testImplementation libs.mockito.core testImplementation libs.robolectric + testImplementation libs.test.parameter.injector androidTestImplementation libs.androidx.test.runner androidTestImplementation libs.androidx.junit diff --git a/ShopperInsights/build.gradle b/ShopperInsights/build.gradle index f624ad77d9..f26f166142 100644 --- a/ShopperInsights/build.gradle +++ b/ShopperInsights/build.gradle @@ -40,6 +40,7 @@ dependencies { testImplementation libs.mockk testImplementation libs.robolectric testImplementation libs.androidx.junit + testImplementation libs.test.parameter.injector testImplementation project(':TestUtils') } diff --git a/TestUtils/build.gradle b/TestUtils/build.gradle index 0c5721dc8b..07c5aeaa86 100644 --- a/TestUtils/build.gradle +++ b/TestUtils/build.gradle @@ -37,6 +37,7 @@ dependencies { compileOnly libs.json.assert compileOnly libs.mockk compileOnly libs.mockito.core + compileOnly libs.test.parameter.injector implementation project(':BraintreeCore') implementation project(':ThreeDSecure') diff --git a/ThreeDSecure/build.gradle b/ThreeDSecure/build.gradle index 2329ce8fef..0aadacb43c 100644 --- a/ThreeDSecure/build.gradle +++ b/ThreeDSecure/build.gradle @@ -45,6 +45,7 @@ dependencies { testImplementation libs.mockito.core testImplementation libs.json.assert testImplementation libs.androidx.test.core + testImplementation libs.test.parameter.injector testImplementation project(':TestUtils') androidTestImplementation libs.androidx.test.rules diff --git a/Venmo/build.gradle b/Venmo/build.gradle index 67f374c627..e91e1a0581 100644 --- a/Venmo/build.gradle +++ b/Venmo/build.gradle @@ -44,6 +44,7 @@ dependencies { testImplementation libs.mockito.core testImplementation libs.json.assert testImplementation libs.mockk + testImplementation libs.test.parameter.injector testImplementation project(':TestUtils') } diff --git a/VisaCheckout/build.gradle b/VisaCheckout/build.gradle index a7912f8b9a..897574df77 100644 --- a/VisaCheckout/build.gradle +++ b/VisaCheckout/build.gradle @@ -49,6 +49,7 @@ dependencies { testImplementation libs.mockk testImplementation libs.mockito.core testImplementation libs.json.assert + testImplementation libs.test.parameter.injector } /* maven deploy + signing */ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3e00b4b217..700f7725e6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,8 @@ cardinal = "2.2.7-5" navigationSafeArgsGradlePlugin = "2.5.0" playServices = "19.4.0" junit = "4.13.2" -robolectric = "4.11.1" +testParameterInjector = "1.18" +robolectric = "4.14-beta-1" mockito = "5.7.0" jsonAssert = "1.5.1" mockk = "1.13.8" @@ -73,6 +74,7 @@ robolectric = { group = "org.robolectric", name = "robolectric", version.ref = " mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } json-assert = { group = "org.skyscreamer", name = "jsonassert", version.ref = "jsonAssert" } junit = { group = "junit", name = "junit", version.ref = "junit" } +test-parameter-injector = { group = "com.google.testparameterinjector", name = "test-parameter-injector", version.ref = "testParameterInjector" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } detekt-rules-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }