From 40af61b3a6455651791045be9fe289672f0a9200 Mon Sep 17 00:00:00 2001 From: Timothy Chow Date: Fri, 22 Nov 2024 14:47:30 -0600 Subject: [PATCH 1/3] Add deep link support as a fallback to app links --- .../api/core/GetReturnLinkTypeUseCase.kt | 32 +++++++++++++++++++ .../demo/PayPalFragment.java | 7 ++-- .../api/paypal/PayPalClient.kt | 19 +++++++++-- .../api/paypal/PayPalInternalClient.kt | 29 ++++++++++++++--- 4 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCase.kt diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCase.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCase.kt new file mode 100644 index 0000000000..de4d6f08ab --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCase.kt @@ -0,0 +1,32 @@ +package com.braintreepayments.api.core + +import android.content.Intent +import android.content.pm.PackageManager +import androidx.annotation.RestrictTo +import com.braintreepayments.api.core.GetReturnLinkTypeUseCase.ReturnLinkType + +/** + * Use case that returns which link type should be used for navigating from App Switch / CCT back into the merchant app. + * + * If a user unchecks the "Open supported links" checkbox in the Android OS settings for the merchant's app. If this + * setting is unchecked, this use case will return [ReturnLinkType.DEEP_LINK], otherwise [ReturnLinkType.APP_LINK] + * will be returned. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class GetReturnLinkTypeUseCase(private val merchantRepository: MerchantRepository) { + + enum class ReturnLinkType { APP_LINK, DEEP_LINK } + + operator fun invoke(): ReturnLinkType { + val context = merchantRepository.applicationContext + val intent = Intent(Intent.ACTION_VIEW, merchantRepository.appLinkReturnUri).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + } + val resolvedActivity = context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) + return if (resolvedActivity?.activityInfo?.packageName == context.packageName) { + ReturnLinkType.APP_LINK + } else { + ReturnLinkType.DEEP_LINK + } + } +} diff --git a/Demo/src/main/java/com/braintreepayments/demo/PayPalFragment.java b/Demo/src/main/java/com/braintreepayments/demo/PayPalFragment.java index fa90df0b62..a913af7526 100644 --- a/Demo/src/main/java/com/braintreepayments/demo/PayPalFragment.java +++ b/Demo/src/main/java/com/braintreepayments/demo/PayPalFragment.java @@ -75,9 +75,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c }); payPalClient = new PayPalClient( - requireContext(), - super.getAuthStringArg(), - Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments") + requireContext(), + super.getAuthStringArg(), + Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments"), + null ); payPalLauncher = new PayPalLauncher(); diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt index ccdfbd46ad..8a7ef07802 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt @@ -9,6 +9,7 @@ import com.braintreepayments.api.core.BraintreeClient import com.braintreepayments.api.core.BraintreeException import com.braintreepayments.api.core.BraintreeRequestCodes import com.braintreepayments.api.core.Configuration +import com.braintreepayments.api.core.GetReturnLinkTypeUseCase import com.braintreepayments.api.core.LinkType import com.braintreepayments.api.core.MerchantRepository import com.braintreepayments.api.core.UserCanceledException @@ -24,6 +25,7 @@ class PayPalClient internal constructor( private val braintreeClient: BraintreeClient, private val internalPayPalClient: PayPalInternalClient = PayPalInternalClient(braintreeClient), private val merchantRepository: MerchantRepository = MerchantRepository.instance, + private val getReturnLinkTypeUseCase: GetReturnLinkTypeUseCase = GetReturnLinkTypeUseCase(merchantRepository) ) { /** @@ -54,8 +56,9 @@ class PayPalClient internal constructor( constructor( context: Context, authorization: String, - appLinkReturnUrl: Uri - ) : this(BraintreeClient(context, authorization, null, appLinkReturnUrl)) + appLinkReturnUrl: Uri, + returnUrlScheme: String? = null + ) : this(BraintreeClient(context, authorization, returnUrlScheme, appLinkReturnUrl)) /** * Starts the PayPal payment flow by creating a [PayPalPaymentAuthRequestParams] to be @@ -152,10 +155,20 @@ class PayPalClient internal constructor( return BrowserSwitchOptions() .requestCode(BraintreeRequestCodes.PAYPAL.code) - .appLinkUri(merchantRepository.appLinkReturnUri) .url(Uri.parse(paymentAuthRequest.approvalUrl)) .launchAsNewTask(braintreeClient.launchesBrowserSwitchAsNewTask()) .metadata(metadata) + .apply { + when (getReturnLinkTypeUseCase()) { + GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK -> { + appLinkUri(merchantRepository.appLinkReturnUri) + } + + GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK -> { + returnUrlScheme(merchantRepository.returnUrlScheme) + } + } + } } /** 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..aa56ce6040 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt @@ -7,6 +7,7 @@ import com.braintreepayments.api.core.BraintreeClient import com.braintreepayments.api.core.BraintreeException import com.braintreepayments.api.core.Configuration import com.braintreepayments.api.core.DeviceInspector +import com.braintreepayments.api.core.GetReturnLinkTypeUseCase import com.braintreepayments.api.core.MerchantRepository import com.braintreepayments.api.datacollector.DataCollector import com.braintreepayments.api.datacollector.DataCollectorInternalRequest @@ -20,9 +21,8 @@ internal class PayPalInternalClient( private val apiClient: ApiClient = ApiClient(braintreeClient), private val deviceInspector: DeviceInspector = DeviceInspector(), private val merchantRepository: MerchantRepository = MerchantRepository.instance, + private val getReturnLinkTypeUseCase: GetReturnLinkTypeUseCase = GetReturnLinkTypeUseCase(merchantRepository) ) { - private val cancelUrl = "${merchantRepository.appLinkReturnUri}://onetouch/v1/cancel" - private val successUrl = "${merchantRepository.appLinkReturnUri}://onetouch/v1/success" private val appLink = merchantRepository.appLinkReturnUri?.toString() fun sendRequest( @@ -50,12 +50,28 @@ internal class PayPalInternalClient( payPalRequest.enablePayPalAppSwitch = isPayPalInstalled(context) } + val returnLinkType = getReturnLinkTypeUseCase() + val navigationLink = when (returnLinkType) { + GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK -> merchantRepository.appLinkReturnUri + GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK -> merchantRepository.returnUrlScheme + } + val appLinkParam = if ( + navigationLink == GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK && isBillingAgreement + ) { + merchantRepository.appLinkReturnUri?.toString() + } else { + null + } + + val cancelUrl = "$navigationLink://onetouch/v1/cancel" + val successUrl = "$navigationLink://onetouch/v1/success" + val requestBody = payPalRequest.createRequestBody( configuration = configuration, authorization = merchantRepository.authorization, successUrl = successUrl, cancelUrl = cancelUrl, - appLink = appLinkReturn + appLink = appLinkParam ) ?: throw JSONException("Error creating requestBody") sendPost( @@ -123,12 +139,17 @@ internal class PayPalInternalClient( ) } + val returnLink = when (getReturnLinkTypeUseCase()) { + GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK -> merchantRepository.appLinkReturnUri + GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK -> merchantRepository.returnUrlScheme + } + val paymentAuthRequest = PayPalPaymentAuthRequestParams( payPalRequest = payPalRequest, browserSwitchOptions = null, clientMetadataId = clientMetadataId, pairingId = pairingId, - successUrl = successUrl + successUrl = "$returnLink://onetouch/v1/success" ) if (isAppSwitchEnabled(payPalRequest) && isPayPalInstalled(context)) { From 739c4c0b79883d8ddc385d11222870fabadc94da Mon Sep 17 00:00:00 2001 From: Timothy Chow Date: Mon, 2 Dec 2024 15:34:55 -0600 Subject: [PATCH 2/3] Add unit tests and kdoc --- .../core/GetReturnLinkTypeUseCaseUnitTest.kt | 53 ++++++ .../api/paypal/PayPalClient.kt | 5 +- .../api/paypal/PayPalClientUnitTest.java | 75 +++++++-- .../paypal/PayPalInternalClientUnitTest.java | 158 +++++++++++++++--- 4 files changed, 248 insertions(+), 43 deletions(-) create mode 100644 BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCaseUnitTest.kt diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCaseUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCaseUnitTest.kt new file mode 100644 index 0000000000..bb10b2c96c --- /dev/null +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCaseUnitTest.kt @@ -0,0 +1,53 @@ +package com.braintreepayments.api.core + +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ResolveInfo +import io.mockk.every +import io.mockk.mockk +import org.junit.Before +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.Test +import kotlin.test.assertEquals + +@RunWith(RobolectricTestRunner::class) +class GetReturnLinkTypeUseCaseUnitTest { + + private val merchantRepository: MerchantRepository = mockk(relaxed = true) + private val context: Context = mockk(relaxed = true) + private val resolveInfo = ResolveInfo() + private val activityInfo = ActivityInfo() + private val contextPackageName = "context.package.name" + + lateinit var subject: GetReturnLinkTypeUseCase + + @Before + fun setUp() { + every { merchantRepository.applicationContext } returns context + every { context.packageName } returns contextPackageName + resolveInfo.activityInfo = activityInfo + every { context.packageManager.resolveActivity(any(), any()) } returns resolveInfo + + subject = GetReturnLinkTypeUseCase(merchantRepository) + } + + @Test + fun `when invoke is called and app link is available, APP_LINK is returned`() { + activityInfo.packageName = "context.package.name" + + val result = subject() + + assertEquals(GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK, result) + } + + @Test + fun `when invoke is called and app link is not available, DEEP_LINK is returned`() { + activityInfo.packageName = "different.package.name" + + val result = subject() + + assertEquals(GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK, result) + } +} diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt index 8a7ef07802..7f12516d41 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt @@ -50,8 +50,9 @@ class PayPalClient internal constructor( * @param context an Android Context * @param authorization a Tokenization Key or Client Token used to authenticate * @param appLinkReturnUrl A [Uri] containing the Android App Link website associated with - * your application to be used to return to your app from the PayPal - * payment flows. + * your application to be used to return to your app from the PayPal payment flows. + * @param returnUrlScheme A return url scheme that will be used as a deep link fallback when returning to your app + * via App Link is not available (buyer unchecks the "Open supported links" setting). */ constructor( context: Context, diff --git a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java index ea68b5b6ed..9c742cd80e 100644 --- a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java +++ b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java @@ -19,6 +19,7 @@ import com.braintreepayments.api.core.BraintreeClient; import com.braintreepayments.api.core.BraintreeRequestCodes; import com.braintreepayments.api.core.Configuration; +import com.braintreepayments.api.core.GetReturnLinkTypeUseCase; import com.braintreepayments.api.core.MerchantRepository; import com.braintreepayments.api.testutils.Fixtures; import com.braintreepayments.api.testutils.MockBraintreeClientBuilder; @@ -43,6 +44,7 @@ public class PayPalClientUnitTest { private PayPalPaymentAuthCallback paymentAuthCallback; private MerchantRepository merchantRepository; + private GetReturnLinkTypeUseCase getReturnLinkTypeUseCase; @Before public void beforeEach() throws JSONException { @@ -55,6 +57,10 @@ public void beforeEach() throws JSONException { paymentAuthCallback = mock(PayPalPaymentAuthCallback.class); merchantRepository = mock(MerchantRepository.class); + getReturnLinkTypeUseCase = mock(GetReturnLinkTypeUseCase.class); + + when(merchantRepository.getReturnUrlScheme()).thenReturn("com.braintreepayments.demo"); + when(getReturnLinkTypeUseCase.invoke()).thenReturn(GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK); } @Test @@ -77,7 +83,7 @@ public void createPaymentAuthRequest_callsBackPayPalResponse_sendsStartedAnalyti BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalEnabledConfig).build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); ArgumentCaptor captor = @@ -131,7 +137,7 @@ public void createPaymentAuthRequest_whenLaunchesBrowserSwitchAsNewTaskEnabled_s new MockBraintreeClientBuilder().configuration(payPalEnabledConfig) .launchesBrowserSwitchAsNewTask(true).build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); ArgumentCaptor captor = @@ -166,7 +172,7 @@ public void createPaymentAuthRequest_setsAppLinkReturnUrl() { BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalEnabledConfig) .build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); ArgumentCaptor captor = @@ -179,6 +185,43 @@ public void createPaymentAuthRequest_setsAppLinkReturnUrl() { ((PayPalPaymentAuthRequest.ReadyToLaunch) request).getRequestParams().getBrowserSwitchOptions().getAppLinkUri()); } + @Test + public void createPaymentAuthRequest_setsDeepLinkReturnUrlScheme() { + when(getReturnLinkTypeUseCase.invoke()).thenReturn(GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK); + PayPalVaultRequest payPalVaultRequest = new PayPalVaultRequest(true); + payPalVaultRequest.setMerchantAccountId("sample-merchant-account-id"); + + PayPalPaymentAuthRequestParams paymentAuthRequest = new PayPalPaymentAuthRequestParams( + payPalVaultRequest, + null, + "https://example.com/approval/url", + "sample-client-metadata-id", + null, + "https://example.com/success/url" + ); + + PayPalInternalClient payPalInternalClient = + new MockPayPalInternalClientBuilder().sendRequestSuccess(paymentAuthRequest) + .build(); + + when(merchantRepository.getAppLinkReturnUri()).thenReturn(Uri.parse("www.example.com")); + + BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalEnabledConfig) + .build(); + + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PayPalPaymentAuthRequest.class); + verify(paymentAuthCallback).onPayPalPaymentAuthRequest(captor.capture()); + + PayPalPaymentAuthRequest request = captor.getValue(); + assertTrue(request instanceof PayPalPaymentAuthRequest.ReadyToLaunch); + assertEquals(merchantRepository.getReturnUrlScheme(), + ((PayPalPaymentAuthRequest.ReadyToLaunch) request).getRequestParams().getBrowserSwitchOptions().getReturnUrlScheme()); + } + @Test public void createPaymentAuthRequest_whenPayPalNotEnabled_returnsError() { PayPalInternalClient payPalInternalClient = new MockPayPalInternalClientBuilder().build(); @@ -186,7 +229,7 @@ public void createPaymentAuthRequest_whenPayPalNotEnabled_returnsError() { BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalDisabledConfig).build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.createPaymentAuthRequest(activity, new PayPalCheckoutRequest("1.00", true), paymentAuthCallback); @@ -215,7 +258,7 @@ public void createPaymentAuthRequest_whenCheckoutRequest_whenConfigError_forward .configurationError(authError) .build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.createPaymentAuthRequest(activity, new PayPalCheckoutRequest("1.00", true), paymentAuthCallback); ArgumentCaptor captor = @@ -240,7 +283,7 @@ public void requestBillingAgreement_whenConfigError_forwardsErrorToListener() { .configurationError(authError) .build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.createPaymentAuthRequest(activity, new PayPalVaultRequest(true), paymentAuthCallback); ArgumentCaptor captor = @@ -266,7 +309,7 @@ public void createPaymentAuthRequest_whenVaultRequest_sendsPayPalRequestViaInter PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.createPaymentAuthRequest(activity, payPalRequest, paymentAuthCallback); verify(payPalInternalClient).sendRequest(same(activity), same(payPalRequest), @@ -282,7 +325,7 @@ public void createPaymentAuthRequest_whenCheckoutRequest_sendsPayPalRequestViaIn PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.createPaymentAuthRequest(activity, payPalRequest, paymentAuthCallback); verify(payPalInternalClient).sendRequest(same(activity), same(payPalRequest), @@ -314,7 +357,7 @@ public void createPaymentAuthRequest_whenVaultRequest_sendsAppSwitchStartedEvent BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalEnabledConfig).build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); ArgumentCaptor captor = @@ -359,7 +402,7 @@ public void tokenize_withBillingAgreement_tokenizesResponseOnSuccess() throws JS PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -403,7 +446,7 @@ public void tokenize_withOneTimePayment_tokenizesResponseOnSuccess() throws JSON PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -448,7 +491,7 @@ public void tokenize_whenCancelUriReceived_notifiesCancellationAndSendsAnalytics PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -488,7 +531,7 @@ public void tokenize_whenPayPalInternalClientTokenizeResult_callsBackResult() PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -530,7 +573,7 @@ public void tokenize_whenPayPalInternalClientTokenizeResult_sendsAppSwitchSuccee PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -569,7 +612,7 @@ public void tokenize_whenPayPalNotEnabled_sendsAppSwitchFailedEvents() throws JS PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -601,7 +644,7 @@ public void tokenize_whenCancelUriReceived_sendsAppSwitchCanceledEvents() PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); diff --git a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalInternalClientUnitTest.java b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalInternalClientUnitTest.java index d743584131..d86ead76d0 100644 --- a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalInternalClientUnitTest.java +++ b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalInternalClientUnitTest.java @@ -23,6 +23,7 @@ import com.braintreepayments.api.core.ClientToken; import com.braintreepayments.api.core.Configuration; import com.braintreepayments.api.core.DeviceInspector; +import com.braintreepayments.api.core.GetReturnLinkTypeUseCase; import com.braintreepayments.api.core.MerchantRepository; import com.braintreepayments.api.core.PostalAddress; import com.braintreepayments.api.core.TokenizationKey; @@ -63,6 +64,7 @@ public class PayPalInternalClientUnitTest { PayPalInternalClientCallback payPalInternalClientCallback; private MerchantRepository merchantRepository = mock(MerchantRepository.class); + private GetReturnLinkTypeUseCase getReturnLinkTypeUseCase = mock(GetReturnLinkTypeUseCase.class); @Before public void beforeEach() throws JSONException { @@ -75,6 +77,8 @@ public void beforeEach() throws JSONException { apiClient = mock(ApiClient.class); deviceInspector = mock(DeviceInspector.class); payPalInternalClientCallback = mock(PayPalInternalClientCallback.class); + + when(getReturnLinkTypeUseCase.invoke()).thenReturn(GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK); } @Test @@ -92,7 +96,8 @@ public void sendRequest_withPayPalVaultRequest_sendsAllParameters() throws JSONE dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PostalAddress shippingAddressOverride = new PostalAddress(); @@ -153,6 +158,85 @@ public void sendRequest_withPayPalVaultRequest_sendsAllParameters() throws JSONE JSONAssert.assertEquals(expected, actual, true); } + @Test + public void sendRequest_withPayPalVaultRequest_sendsAllParameters_with_deep_link() throws JSONException { + when(getReturnLinkTypeUseCase.invoke()).thenReturn(GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK); + + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .configuration(configuration) + .build(); + when(clientToken.getBearer()).thenReturn("client-token-bearer"); + + when(merchantRepository.getAuthorization()).thenReturn(clientToken); + when(merchantRepository.getReturnUrlScheme()).thenReturn("com.braintreepayments.demo"); + + PayPalInternalClient sut = new PayPalInternalClient( + braintreeClient, + dataCollector, + apiClient, + deviceInspector, + merchantRepository, + getReturnLinkTypeUseCase + ); + + PostalAddress shippingAddressOverride = new PostalAddress(); + shippingAddressOverride.setRecipientName("Brianna Tree"); + shippingAddressOverride.setStreetAddress("123 Fake St."); + shippingAddressOverride.setExtendedAddress("Apt. v.0"); + shippingAddressOverride.setLocality("Oakland"); + shippingAddressOverride.setRegion("CA"); + shippingAddressOverride.setPostalCode("12345"); + shippingAddressOverride.setCountryCodeAlpha2("US"); + + PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); + payPalRequest.setBillingAgreementDescription("Billing Agreement Description"); + payPalRequest.setMerchantAccountId("sample-merchant-account-id"); + payPalRequest.setLandingPageType(PayPalLandingPageType.LANDING_PAGE_TYPE_BILLING); + payPalRequest.setDisplayName("sample-display-name"); + payPalRequest.setLocaleCode("US"); + payPalRequest.setShippingAddressRequired(true); + payPalRequest.setShippingAddressEditable(true); + payPalRequest.setShouldOfferCredit(true); + payPalRequest.setShippingAddressOverride(shippingAddressOverride); + + sut.sendRequest(context, payPalRequest, payPalInternalClientCallback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(braintreeClient).sendPOST( + eq("/v1/paypal_hermes/setup_billing_agreement"), + captor.capture(), + anyMap(), + any(HttpResponseCallback.class) + ); + + String result = captor.getValue(); + JSONObject actual = new JSONObject(result); + + JSONObject expected = new JSONObject() + .put("authorization_fingerprint", "client-token-bearer") + .put("return_url", "com.braintreepayments.demo://onetouch/v1/success") + .put("cancel_url", "com.braintreepayments.demo://onetouch/v1/cancel") + .put("offer_paypal_credit", true) + .put("description", "Billing Agreement Description") + .put("experience_profile", new JSONObject() + .put("no_shipping", false) + .put("landing_page_type", "billing") + .put("brand_name", "sample-display-name") + .put("locale_code", "US") + .put("address_override", false)) + .put("shipping_address", new JSONObject() + .put("line1", "123 Fake St.") + .put("line2", "Apt. v.0") + .put("city", "Oakland") + .put("state", "CA") + .put("postal_code", "12345") + .put("country_code", "US") + .put("recipient_name", "Brianna Tree")) + .put("merchant_account_id", "sample-merchant-account-id"); + + JSONAssert.assertEquals(expected, actual, true); + } + @Test public void sendRequest_withPayPalCheckoutRequest_sendsAllParameters() throws JSONException { BraintreeClient braintreeClient = new MockBraintreeClientBuilder() @@ -168,7 +252,8 @@ public void sendRequest_withPayPalCheckoutRequest_sendsAllParameters() throws JS dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PostalAddress shippingAddressOverride = new PostalAddress(); shippingAddressOverride.setRecipientName("Brianna Tree"); @@ -269,7 +354,8 @@ public void sendRequest_withTokenizationKey_sendsClientKeyParam() throws JSONExc dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -304,7 +390,8 @@ public void sendRequest_withEmptyDisplayName_fallsBackToPayPalConfigurationDispl dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(false); @@ -339,7 +426,8 @@ public void sendRequest_withLocaleNotSpecified_omitsLocale() throws JSONExceptio dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -374,7 +462,8 @@ public void sendRequest_withMerchantAccountIdNotSpecified_omitsMerchantAccountId dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -409,7 +498,8 @@ public void sendRequest_withShippingAddressOverrideNotSpecified_sendsAddressOver dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -445,7 +535,8 @@ public void sendRequest_withShippingAddressSpecified_sendsAddressOverrideBasedOn dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -482,7 +573,8 @@ public void sendRequest_withPayPalVaultRequest_omitsEmptyBillingAgreementDescrip dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -517,7 +609,8 @@ public void sendRequest_withPayPalCheckoutRequest_fallsBackToPayPalConfiguration dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -550,7 +643,8 @@ public void sendRequest_withPayPalCheckoutRequest_omitsEmptyLineItems() throws J dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -587,7 +681,8 @@ public void sendRequest_whenRiskCorrelationIdNotNull_setsClientMetadataIdToRiskC dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -619,7 +714,8 @@ public void sendRequest_whenRiskCorrelationIdNull_setsClientMetadataIdFromPayPal dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -649,7 +745,8 @@ public void sendRequest_withPayPalCheckoutRequest_whenRequestBillingAgreementFal dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -689,7 +786,8 @@ public void sendRequest_withPayPalVaultRequest_callsBackPayPalResponseOnSuccess( dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -728,7 +826,8 @@ public void sendRequest_withPayPalVaultRequest_callsBackPayPalResponseOnSuccess_ dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -770,7 +869,8 @@ public void sendRequest_withPayPalVaultRequest_callsBackPayPalResponseOnSuccess_ dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -805,7 +905,8 @@ public void sendRequest_withPayPalCheckoutRequest_callsBackPayPalResponseOnSucce dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -847,7 +948,8 @@ public void sendRequest_propagatesHttpErrors() { dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -870,7 +972,8 @@ public void sendRequest_propagatesMalformedJSONResponseErrors() { dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -894,7 +997,8 @@ public void sendRequest_onConfigurationFailure_forwardsError() { dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -914,7 +1018,8 @@ public void tokenize_tokenizesWithApiClient() { dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); sut.tokenize(payPalAccount, callback); @@ -937,7 +1042,8 @@ public void tokenize_onTokenizeResult_returnsAccountNonceToCallback() throws JSO dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); sut.tokenize(payPalAccount, callback); @@ -967,7 +1073,8 @@ public void tokenize_onTokenizeError_returnsErrorToCallback() { dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); sut.tokenize(payPalAccount, callback); @@ -991,7 +1098,8 @@ public void payPalDataCollector_passes_correct_arguments_to_getClientMetadataId( dataCollector, apiClient, deviceInspector, - merchantRepository + merchantRepository, + getReturnLinkTypeUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); From b65f5385f9f14ccb9d1c77aeb723b497188e041d Mon Sep 17 00:00:00 2001 From: Timothy Chow Date: Tue, 3 Dec 2024 13:36:53 -0600 Subject: [PATCH 3/3] Add a null check for both app link and deep link urls --- .../api/core/BraintreeClient.kt | 8 +- .../api/core/GetReturnLinkTypeUseCase.kt | 32 ------- .../api/core/GetReturnLinkUseCase.kt | 48 ++++++++++ .../api/core/MerchantRepository.kt | 2 + .../api/core/BraintreeClientUnitTest.kt | 8 +- .../core/GetReturnLinkTypeUseCaseUnitTest.kt | 53 ----------- .../api/core/GetReturnLinkUseCaseUnitTest.kt | 87 +++++++++++++++++ .../demo/PayPalFragment.java | 2 +- .../api/paypal/PayPalClient.kt | 52 +++++++---- .../api/paypal/PayPalInternalClient.kt | 30 +++--- .../api/paypal/PayPalClientUnitTest.java | 88 +++++++++++++----- .../paypal/PayPalInternalClientUnitTest.java | 93 ++++++++++++------- 12 files changed, 320 insertions(+), 183 deletions(-) delete mode 100644 BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCase.kt create mode 100644 BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkUseCase.kt delete mode 100644 BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCaseUnitTest.kt create mode 100644 BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkUseCaseUnitTest.kt diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/BraintreeClient.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/BraintreeClient.kt index efc3d56cab..737e00bbdd 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/core/BraintreeClient.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/core/BraintreeClient.kt @@ -7,7 +7,6 @@ import androidx.annotation.RestrictTo import com.braintreepayments.api.sharedutils.HttpResponseCallback import com.braintreepayments.api.sharedutils.HttpResponseTiming import com.braintreepayments.api.sharedutils.ManifestValidator -import com.braintreepayments.api.sharedutils.Time import org.json.JSONException import org.json.JSONObject @@ -22,12 +21,12 @@ class BraintreeClient internal constructor( authorization: Authorization, returnUrlScheme: String, appLinkReturnUri: Uri?, + deepLinkFallbackUrlScheme: String? = null, sdkComponent: SdkComponent = SdkComponent.create(applicationContext), private val httpClient: BraintreeHttpClient = BraintreeHttpClient(), private val graphQLClient: BraintreeGraphQLClient = BraintreeGraphQLClient(), private val configurationLoader: ConfigurationLoader = ConfigurationLoader.instance, private val manifestValidator: ManifestValidator = ManifestValidator(), - private val time: Time = Time(), private val merchantRepository: MerchantRepository = MerchantRepository.instance, private val analyticsClient: AnalyticsClient = AnalyticsClient(), ) { @@ -47,6 +46,7 @@ class BraintreeClient internal constructor( returnUrlScheme: String? = null, appLinkReturnUri: Uri? = null, integrationType: IntegrationType? = null, + deepLinkFallbackUrlScheme: String? = null, ) : this( applicationContext = context.applicationContext, authorization = Authorization.fromString(authorization), @@ -54,6 +54,7 @@ class BraintreeClient internal constructor( ?: "${getAppPackageNameWithoutUnderscores(context.applicationContext)}.braintree", appLinkReturnUri = appLinkReturnUri, integrationType = integrationType ?: IntegrationType.CUSTOM, + deepLinkFallbackUrlScheme = deepLinkFallbackUrlScheme ) init { @@ -73,6 +74,9 @@ class BraintreeClient internal constructor( if (appLinkReturnUri != null) { it.appLinkReturnUri = appLinkReturnUri } + if (deepLinkFallbackUrlScheme != null) { + it.deepLinkFallbackUrlScheme = deepLinkFallbackUrlScheme + } } } diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCase.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCase.kt deleted file mode 100644 index de4d6f08ab..0000000000 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCase.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.braintreepayments.api.core - -import android.content.Intent -import android.content.pm.PackageManager -import androidx.annotation.RestrictTo -import com.braintreepayments.api.core.GetReturnLinkTypeUseCase.ReturnLinkType - -/** - * Use case that returns which link type should be used for navigating from App Switch / CCT back into the merchant app. - * - * If a user unchecks the "Open supported links" checkbox in the Android OS settings for the merchant's app. If this - * setting is unchecked, this use case will return [ReturnLinkType.DEEP_LINK], otherwise [ReturnLinkType.APP_LINK] - * will be returned. - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -class GetReturnLinkTypeUseCase(private val merchantRepository: MerchantRepository) { - - enum class ReturnLinkType { APP_LINK, DEEP_LINK } - - operator fun invoke(): ReturnLinkType { - val context = merchantRepository.applicationContext - val intent = Intent(Intent.ACTION_VIEW, merchantRepository.appLinkReturnUri).apply { - addCategory(Intent.CATEGORY_BROWSABLE) - } - val resolvedActivity = context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) - return if (resolvedActivity?.activityInfo?.packageName == context.packageName) { - ReturnLinkType.APP_LINK - } else { - ReturnLinkType.DEEP_LINK - } - } -} diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkUseCase.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkUseCase.kt new file mode 100644 index 0000000000..7fc8d7f3e1 --- /dev/null +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/core/GetReturnLinkUseCase.kt @@ -0,0 +1,48 @@ +package com.braintreepayments.api.core + +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import androidx.annotation.RestrictTo +import com.braintreepayments.api.core.GetReturnLinkUseCase.ReturnLinkResult + +/** + * Use case that returns a return link that should be used for navigating from App Switch / CCT back into the merchant + * app. It handles both App Links and Deep Links. + * + * If a user unchecks the "Open supported links" checkbox in the Android OS settings for the merchant's app. If this + * setting is unchecked, this use case will return [ReturnLinkResult.DeepLink], otherwise [ReturnLinkResult.AppLink] + * will be returned. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class GetReturnLinkUseCase(private val merchantRepository: MerchantRepository) { + + sealed class ReturnLinkResult { + data class AppLink(val appLinkReturnUri: Uri) : ReturnLinkResult() + + data class DeepLink(val deepLinkFallbackUrlScheme: String) : ReturnLinkResult() + + data class Failure(val exception: Exception) : ReturnLinkResult() + } + + operator fun invoke(): ReturnLinkResult { + val context = merchantRepository.applicationContext + val intent = Intent(Intent.ACTION_VIEW, merchantRepository.appLinkReturnUri).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + } + val resolvedActivity = context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) + return if (resolvedActivity?.activityInfo?.packageName == context.packageName) { + merchantRepository.appLinkReturnUri?.let { + ReturnLinkResult.AppLink(it) + } ?: run { + ReturnLinkResult.Failure(BraintreeException("App Link Return Uri is null")) + } + } else { + merchantRepository.deepLinkFallbackUrlScheme?.let { + ReturnLinkResult.DeepLink(it) + } ?: run { + ReturnLinkResult.Failure(BraintreeException("Deep Link fallback return url is null")) + } + } + } +} diff --git a/BraintreeCore/src/main/java/com/braintreepayments/api/core/MerchantRepository.kt b/BraintreeCore/src/main/java/com/braintreepayments/api/core/MerchantRepository.kt index d8ad972c5c..ce0fccb246 100644 --- a/BraintreeCore/src/main/java/com/braintreepayments/api/core/MerchantRepository.kt +++ b/BraintreeCore/src/main/java/com/braintreepayments/api/core/MerchantRepository.kt @@ -16,6 +16,8 @@ class MerchantRepository { lateinit var returnUrlScheme: String var appLinkReturnUri: Uri? = null + var deepLinkFallbackUrlScheme: String? = null + companion object { /** diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/core/BraintreeClientUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/core/BraintreeClientUnitTest.kt index ecba21eace..c7d6d5cbf9 100644 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/core/BraintreeClientUnitTest.kt +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/core/BraintreeClientUnitTest.kt @@ -10,7 +10,6 @@ import com.braintreepayments.api.BrowserSwitchClient import com.braintreepayments.api.sharedutils.HttpResponseCallback import com.braintreepayments.api.sharedutils.ManifestValidator import com.braintreepayments.api.sharedutils.NetworkResponseCallback -import com.braintreepayments.api.sharedutils.Time import com.braintreepayments.api.testutils.Fixtures import io.mockk.* import org.json.JSONException @@ -319,10 +318,7 @@ class BraintreeClientUnitTest { .configuration(configuration) .build() - val time: Time = mockk() - every { time.currentTime } returns 123 - - val sut = createBraintreeClient(configurationLoader, time) + val sut = createBraintreeClient(configurationLoader) sut.sendAnalyticsEvent("event.started") verify { @@ -432,7 +428,6 @@ class BraintreeClientUnitTest { private fun createBraintreeClient( configurationLoader: ConfigurationLoader = mockk(), - time: Time = Time(), appLinkReturnUri: Uri? = Uri.parse("https://example.com"), merchantRepository: MerchantRepository = MerchantRepository.instance ) = BraintreeClient( @@ -446,7 +441,6 @@ class BraintreeClientUnitTest { analyticsClient = analyticsClient, manifestValidator = manifestValidator, configurationLoader = configurationLoader, - time = time, merchantRepository = merchantRepository, ) } diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCaseUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCaseUnitTest.kt deleted file mode 100644 index bb10b2c96c..0000000000 --- a/BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkTypeUseCaseUnitTest.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.braintreepayments.api.core - -import android.content.Context -import android.content.Intent -import android.content.pm.ActivityInfo -import android.content.pm.ResolveInfo -import io.mockk.every -import io.mockk.mockk -import org.junit.Before -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import kotlin.test.Test -import kotlin.test.assertEquals - -@RunWith(RobolectricTestRunner::class) -class GetReturnLinkTypeUseCaseUnitTest { - - private val merchantRepository: MerchantRepository = mockk(relaxed = true) - private val context: Context = mockk(relaxed = true) - private val resolveInfo = ResolveInfo() - private val activityInfo = ActivityInfo() - private val contextPackageName = "context.package.name" - - lateinit var subject: GetReturnLinkTypeUseCase - - @Before - fun setUp() { - every { merchantRepository.applicationContext } returns context - every { context.packageName } returns contextPackageName - resolveInfo.activityInfo = activityInfo - every { context.packageManager.resolveActivity(any(), any()) } returns resolveInfo - - subject = GetReturnLinkTypeUseCase(merchantRepository) - } - - @Test - fun `when invoke is called and app link is available, APP_LINK is returned`() { - activityInfo.packageName = "context.package.name" - - val result = subject() - - assertEquals(GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK, result) - } - - @Test - fun `when invoke is called and app link is not available, DEEP_LINK is returned`() { - activityInfo.packageName = "different.package.name" - - val result = subject() - - assertEquals(GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK, result) - } -} diff --git a/BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkUseCaseUnitTest.kt b/BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkUseCaseUnitTest.kt new file mode 100644 index 0000000000..81ed10d334 --- /dev/null +++ b/BraintreeCore/src/test/java/com/braintreepayments/api/core/GetReturnLinkUseCaseUnitTest.kt @@ -0,0 +1,87 @@ +package com.braintreepayments.api.core + +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ResolveInfo +import android.net.Uri +import io.mockk.every +import io.mockk.mockk +import org.junit.Before +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@RunWith(RobolectricTestRunner::class) +class GetReturnLinkUseCaseUnitTest { + + private val merchantRepository: MerchantRepository = mockk(relaxed = true) + private val context: Context = mockk(relaxed = true) + private val resolveInfo = ResolveInfo() + private val activityInfo = ActivityInfo() + private val contextPackageName = "context.package.name" + private val appLinkReturnUri = Uri.parse("https://example.com") + private val deepLinkFallbackUrlScheme = "com.braintreepayments.demo" + + lateinit var subject: GetReturnLinkUseCase + + @Before + fun setUp() { + every { merchantRepository.applicationContext } returns context + every { merchantRepository.appLinkReturnUri } returns appLinkReturnUri + every { merchantRepository.deepLinkFallbackUrlScheme } returns deepLinkFallbackUrlScheme + every { context.packageName } returns contextPackageName + resolveInfo.activityInfo = activityInfo + every { context.packageManager.resolveActivity(any(), any()) } returns resolveInfo + + subject = GetReturnLinkUseCase(merchantRepository) + } + + @Test + fun `when invoke is called and app link is available, APP_LINK is returned`() { + activityInfo.packageName = "context.package.name" + + val result = subject() + + assertEquals(GetReturnLinkUseCase.ReturnLinkResult.AppLink(appLinkReturnUri), result) + } + + @Test + fun `when invoke is called and app link is not available, DEEP_LINK is returned`() { + activityInfo.packageName = "different.package.name" + + val result = subject() + + assertEquals(GetReturnLinkUseCase.ReturnLinkResult.DeepLink(deepLinkFallbackUrlScheme), result) + } + + @Test + fun `when invoke is called and deep link is available but null, Failure is returned`() { + activityInfo.packageName = "different.package.name" + every { merchantRepository.deepLinkFallbackUrlScheme } returns null + + val result = subject() + + assertTrue { result is GetReturnLinkUseCase.ReturnLinkResult.Failure } + assertEquals( + "Deep Link fallback return url is null", + (result as GetReturnLinkUseCase.ReturnLinkResult.Failure).exception.message + ) + } + + @Test + fun `when invoke is called and app link is available but null, Failure is returned`() { + activityInfo.packageName = "context.package.name" + every { merchantRepository.appLinkReturnUri } returns null + + val result = subject() + + assertTrue { result is GetReturnLinkUseCase.ReturnLinkResult.Failure } + assertEquals( + "App Link Return Uri is null", + (result as GetReturnLinkUseCase.ReturnLinkResult.Failure).exception.message + ) + } +} diff --git a/Demo/src/main/java/com/braintreepayments/demo/PayPalFragment.java b/Demo/src/main/java/com/braintreepayments/demo/PayPalFragment.java index a913af7526..86c067e408 100644 --- a/Demo/src/main/java/com/braintreepayments/demo/PayPalFragment.java +++ b/Demo/src/main/java/com/braintreepayments/demo/PayPalFragment.java @@ -78,7 +78,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c requireContext(), super.getAuthStringArg(), Uri.parse("https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments"), - null + "com.braintreepayments.demo.braintree" ); payPalLauncher = new PayPalLauncher(); diff --git a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt index 7f12516d41..5d00d97370 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalClient.kt @@ -9,7 +9,7 @@ import com.braintreepayments.api.core.BraintreeClient import com.braintreepayments.api.core.BraintreeException import com.braintreepayments.api.core.BraintreeRequestCodes import com.braintreepayments.api.core.Configuration -import com.braintreepayments.api.core.GetReturnLinkTypeUseCase +import com.braintreepayments.api.core.GetReturnLinkUseCase import com.braintreepayments.api.core.LinkType import com.braintreepayments.api.core.MerchantRepository import com.braintreepayments.api.core.UserCanceledException @@ -25,7 +25,7 @@ class PayPalClient internal constructor( private val braintreeClient: BraintreeClient, private val internalPayPalClient: PayPalInternalClient = PayPalInternalClient(braintreeClient), private val merchantRepository: MerchantRepository = MerchantRepository.instance, - private val getReturnLinkTypeUseCase: GetReturnLinkTypeUseCase = GetReturnLinkTypeUseCase(merchantRepository) + private val getReturnLinkUseCase: GetReturnLinkUseCase = GetReturnLinkUseCase(merchantRepository) ) { /** @@ -51,15 +51,22 @@ class PayPalClient internal constructor( * @param authorization a Tokenization Key or Client Token used to authenticate * @param appLinkReturnUrl A [Uri] containing the Android App Link website associated with * your application to be used to return to your app from the PayPal payment flows. - * @param returnUrlScheme A return url scheme that will be used as a deep link fallback when returning to your app - * via App Link is not available (buyer unchecks the "Open supported links" setting). + * @param deepLinkFallbackUrlScheme A return url scheme that will be used as a deep link fallback when returning to + * your app via App Link is not available (buyer unchecks the "Open supported links" setting). */ constructor( context: Context, authorization: String, appLinkReturnUrl: Uri, - returnUrlScheme: String? = null - ) : this(BraintreeClient(context, authorization, returnUrlScheme, appLinkReturnUrl)) + deepLinkFallbackUrlScheme: String? = null + ) : this( + BraintreeClient( + context = context, + authorization = authorization, + deepLinkFallbackUrlScheme = deepLinkFallbackUrlScheme, + appLinkReturnUri = appLinkReturnUrl + ) + ) /** * Starts the PayPal payment flow by creating a [PayPalPaymentAuthRequestParams] to be @@ -93,6 +100,7 @@ class PayPalClient internal constructor( } } + @Suppress("TooGenericExceptionCaught") private fun sendPayPalRequest( context: Context, payPalRequest: PayPalRequest, @@ -116,14 +124,16 @@ class PayPalClient internal constructor( braintreeClient.sendAnalyticsEvent(PayPalAnalytics.APP_SWITCH_STARTED, analyticsParams) } - callback.onPayPalPaymentAuthRequest( - PayPalPaymentAuthRequest.ReadyToLaunch(payPalResponse) - ) - } catch (exception: JSONException) { - callbackCreatePaymentAuthFailure( - callback, - PayPalPaymentAuthRequest.Failure(exception) - ) + callback.onPayPalPaymentAuthRequest(PayPalPaymentAuthRequest.ReadyToLaunch(payPalResponse)) + } catch (exception: Exception) { + when (exception) { + is JSONException, + is BraintreeException -> { + callbackCreatePaymentAuthFailure(callback, PayPalPaymentAuthRequest.Failure(exception)) + } + + else -> throw exception + } } } else { callbackCreatePaymentAuthFailure( @@ -134,7 +144,7 @@ class PayPalClient internal constructor( } } - @Throws(JSONException::class) + @Throws(JSONException::class, BraintreeException::class) private fun buildBrowserSwitchOptions( paymentAuthRequest: PayPalPaymentAuthRequestParams ): BrowserSwitchOptions { @@ -160,14 +170,16 @@ class PayPalClient internal constructor( .launchAsNewTask(braintreeClient.launchesBrowserSwitchAsNewTask()) .metadata(metadata) .apply { - when (getReturnLinkTypeUseCase()) { - GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK -> { - appLinkUri(merchantRepository.appLinkReturnUri) + when (val returnLinkResult = getReturnLinkUseCase()) { + is GetReturnLinkUseCase.ReturnLinkResult.AppLink -> { + appLinkUri(returnLinkResult.appLinkReturnUri) } - GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK -> { - returnUrlScheme(merchantRepository.returnUrlScheme) + is GetReturnLinkUseCase.ReturnLinkResult.DeepLink -> { + returnUrlScheme(returnLinkResult.deepLinkFallbackUrlScheme) } + + is GetReturnLinkUseCase.ReturnLinkResult.Failure -> throw returnLinkResult.exception } } } 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 aa56ce6040..38746419bf 100644 --- a/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt +++ b/PayPal/src/main/java/com/braintreepayments/api/paypal/PayPalInternalClient.kt @@ -7,7 +7,7 @@ import com.braintreepayments.api.core.BraintreeClient import com.braintreepayments.api.core.BraintreeException import com.braintreepayments.api.core.Configuration import com.braintreepayments.api.core.DeviceInspector -import com.braintreepayments.api.core.GetReturnLinkTypeUseCase +import com.braintreepayments.api.core.GetReturnLinkUseCase import com.braintreepayments.api.core.MerchantRepository import com.braintreepayments.api.datacollector.DataCollector import com.braintreepayments.api.datacollector.DataCollectorInternalRequest @@ -21,9 +21,8 @@ internal class PayPalInternalClient( private val apiClient: ApiClient = ApiClient(braintreeClient), private val deviceInspector: DeviceInspector = DeviceInspector(), private val merchantRepository: MerchantRepository = MerchantRepository.instance, - private val getReturnLinkTypeUseCase: GetReturnLinkTypeUseCase = GetReturnLinkTypeUseCase(merchantRepository) + private val getReturnLinkUseCase: GetReturnLinkUseCase = GetReturnLinkUseCase(merchantRepository) ) { - private val appLink = merchantRepository.appLinkReturnUri?.toString() fun sendRequest( context: Context, @@ -44,19 +43,22 @@ internal class PayPalInternalClient( CREATE_SINGLE_PAYMENT_ENDPOINT } val url = "/v1/$endpoint" - val appLinkReturn = if (isBillingAgreement) appLink else null if (isBillingAgreement && (payPalRequest as PayPalVaultRequest).enablePayPalAppSwitch) { payPalRequest.enablePayPalAppSwitch = isPayPalInstalled(context) } - val returnLinkType = getReturnLinkTypeUseCase() - val navigationLink = when (returnLinkType) { - GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK -> merchantRepository.appLinkReturnUri - GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK -> merchantRepository.returnUrlScheme + val returnLinkResult = getReturnLinkUseCase() + val navigationLink: String = when (returnLinkResult) { + is GetReturnLinkUseCase.ReturnLinkResult.AppLink -> returnLinkResult.appLinkReturnUri.toString() + is GetReturnLinkUseCase.ReturnLinkResult.DeepLink -> returnLinkResult.deepLinkFallbackUrlScheme + is GetReturnLinkUseCase.ReturnLinkResult.Failure -> { + callback.onResult(null, returnLinkResult.exception) + return@getConfiguration + } } val appLinkParam = if ( - navigationLink == GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK && isBillingAgreement + returnLinkResult is GetReturnLinkUseCase.ReturnLinkResult.AppLink && isBillingAgreement ) { merchantRepository.appLinkReturnUri?.toString() } else { @@ -139,9 +141,13 @@ internal class PayPalInternalClient( ) } - val returnLink = when (getReturnLinkTypeUseCase()) { - GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK -> merchantRepository.appLinkReturnUri - GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK -> merchantRepository.returnUrlScheme + val returnLink: String = when (val returnLinkResult = getReturnLinkUseCase()) { + is GetReturnLinkUseCase.ReturnLinkResult.AppLink -> returnLinkResult.appLinkReturnUri.toString() + is GetReturnLinkUseCase.ReturnLinkResult.DeepLink -> returnLinkResult.deepLinkFallbackUrlScheme + is GetReturnLinkUseCase.ReturnLinkResult.Failure -> { + callback.onResult(null, returnLinkResult.exception) + return@sendPOST + } } val paymentAuthRequest = PayPalPaymentAuthRequestParams( diff --git a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java index 9c742cd80e..3bb81af02a 100644 --- a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java +++ b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalClientUnitTest.java @@ -17,9 +17,10 @@ import com.braintreepayments.api.BrowserSwitchOptions; import com.braintreepayments.api.core.AnalyticsEventParams; import com.braintreepayments.api.core.BraintreeClient; +import com.braintreepayments.api.core.BraintreeException; import com.braintreepayments.api.core.BraintreeRequestCodes; import com.braintreepayments.api.core.Configuration; -import com.braintreepayments.api.core.GetReturnLinkTypeUseCase; +import com.braintreepayments.api.core.GetReturnLinkUseCase; import com.braintreepayments.api.core.MerchantRepository; import com.braintreepayments.api.testutils.Fixtures; import com.braintreepayments.api.testutils.MockBraintreeClientBuilder; @@ -44,7 +45,7 @@ public class PayPalClientUnitTest { private PayPalPaymentAuthCallback paymentAuthCallback; private MerchantRepository merchantRepository; - private GetReturnLinkTypeUseCase getReturnLinkTypeUseCase; + private GetReturnLinkUseCase getReturnLinkUseCase; @Before public void beforeEach() throws JSONException { @@ -57,10 +58,12 @@ public void beforeEach() throws JSONException { paymentAuthCallback = mock(PayPalPaymentAuthCallback.class); merchantRepository = mock(MerchantRepository.class); - getReturnLinkTypeUseCase = mock(GetReturnLinkTypeUseCase.class); + getReturnLinkUseCase = mock(GetReturnLinkUseCase.class); when(merchantRepository.getReturnUrlScheme()).thenReturn("com.braintreepayments.demo"); - when(getReturnLinkTypeUseCase.invoke()).thenReturn(GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK); + when(getReturnLinkUseCase.invoke()).thenReturn(new GetReturnLinkUseCase.ReturnLinkResult.AppLink( + Uri.parse("www.example.com") + )); } @Test @@ -83,7 +86,7 @@ public void createPaymentAuthRequest_callsBackPayPalResponse_sendsStartedAnalyti BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalEnabledConfig).build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); ArgumentCaptor captor = @@ -137,7 +140,7 @@ public void createPaymentAuthRequest_whenLaunchesBrowserSwitchAsNewTaskEnabled_s new MockBraintreeClientBuilder().configuration(payPalEnabledConfig) .launchesBrowserSwitchAsNewTask(true).build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); ArgumentCaptor captor = @@ -172,7 +175,7 @@ public void createPaymentAuthRequest_setsAppLinkReturnUrl() { BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalEnabledConfig) .build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); ArgumentCaptor captor = @@ -187,7 +190,9 @@ public void createPaymentAuthRequest_setsAppLinkReturnUrl() { @Test public void createPaymentAuthRequest_setsDeepLinkReturnUrlScheme() { - when(getReturnLinkTypeUseCase.invoke()).thenReturn(GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK); + when(getReturnLinkUseCase.invoke()).thenReturn(new GetReturnLinkUseCase.ReturnLinkResult.DeepLink( + "com.braintreepayments.demo" + )); PayPalVaultRequest payPalVaultRequest = new PayPalVaultRequest(true); payPalVaultRequest.setMerchantAccountId("sample-merchant-account-id"); @@ -204,12 +209,10 @@ public void createPaymentAuthRequest_setsDeepLinkReturnUrlScheme() { new MockPayPalInternalClientBuilder().sendRequestSuccess(paymentAuthRequest) .build(); - when(merchantRepository.getAppLinkReturnUri()).thenReturn(Uri.parse("www.example.com")); - BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalEnabledConfig) .build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); ArgumentCaptor captor = @@ -218,10 +221,45 @@ public void createPaymentAuthRequest_setsDeepLinkReturnUrlScheme() { PayPalPaymentAuthRequest request = captor.getValue(); assertTrue(request instanceof PayPalPaymentAuthRequest.ReadyToLaunch); - assertEquals(merchantRepository.getReturnUrlScheme(), + assertEquals("com.braintreepayments.demo", ((PayPalPaymentAuthRequest.ReadyToLaunch) request).getRequestParams().getBrowserSwitchOptions().getReturnUrlScheme()); } + @Test + public void createPaymentAuthRequest_returnsAnErrorWhen_getReturnLinkUseCase_returnsAFailure() { + BraintreeException exception = new BraintreeException(); + when(getReturnLinkUseCase.invoke()).thenReturn(new GetReturnLinkUseCase.ReturnLinkResult.Failure(exception)); + + PayPalVaultRequest payPalVaultRequest = new PayPalVaultRequest(true); + payPalVaultRequest.setMerchantAccountId("sample-merchant-account-id"); + + PayPalPaymentAuthRequestParams paymentAuthRequest = new PayPalPaymentAuthRequestParams( + payPalVaultRequest, + null, + "https://example.com/approval/url", + "sample-client-metadata-id", + null, + "https://example.com/success/url" + ); + + PayPalInternalClient payPalInternalClient = + new MockPayPalInternalClientBuilder().sendRequestSuccess(paymentAuthRequest) + .build(); + + BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalEnabledConfig) + .build(); + + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); + sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PayPalPaymentAuthRequest.class); + verify(paymentAuthCallback).onPayPalPaymentAuthRequest(captor.capture()); + + PayPalPaymentAuthRequest request = captor.getValue(); + assertTrue(request instanceof PayPalPaymentAuthRequest.Failure); + assertEquals(exception, ((PayPalPaymentAuthRequest.Failure) request).getError()); + } + @Test public void createPaymentAuthRequest_whenPayPalNotEnabled_returnsError() { PayPalInternalClient payPalInternalClient = new MockPayPalInternalClientBuilder().build(); @@ -229,7 +267,7 @@ public void createPaymentAuthRequest_whenPayPalNotEnabled_returnsError() { BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalDisabledConfig).build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.createPaymentAuthRequest(activity, new PayPalCheckoutRequest("1.00", true), paymentAuthCallback); @@ -258,7 +296,7 @@ public void createPaymentAuthRequest_whenCheckoutRequest_whenConfigError_forward .configurationError(authError) .build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.createPaymentAuthRequest(activity, new PayPalCheckoutRequest("1.00", true), paymentAuthCallback); ArgumentCaptor captor = @@ -283,7 +321,7 @@ public void requestBillingAgreement_whenConfigError_forwardsErrorToListener() { .configurationError(authError) .build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.createPaymentAuthRequest(activity, new PayPalVaultRequest(true), paymentAuthCallback); ArgumentCaptor captor = @@ -309,7 +347,7 @@ public void createPaymentAuthRequest_whenVaultRequest_sendsPayPalRequestViaInter PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.createPaymentAuthRequest(activity, payPalRequest, paymentAuthCallback); verify(payPalInternalClient).sendRequest(same(activity), same(payPalRequest), @@ -325,7 +363,7 @@ public void createPaymentAuthRequest_whenCheckoutRequest_sendsPayPalRequestViaIn PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.createPaymentAuthRequest(activity, payPalRequest, paymentAuthCallback); verify(payPalInternalClient).sendRequest(same(activity), same(payPalRequest), @@ -357,7 +395,7 @@ public void createPaymentAuthRequest_whenVaultRequest_sendsAppSwitchStartedEvent BraintreeClient braintreeClient = new MockBraintreeClientBuilder().configuration(payPalEnabledConfig).build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.createPaymentAuthRequest(activity, payPalVaultRequest, paymentAuthCallback); ArgumentCaptor captor = @@ -402,7 +440,7 @@ public void tokenize_withBillingAgreement_tokenizesResponseOnSuccess() throws JS PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -446,7 +484,7 @@ public void tokenize_withOneTimePayment_tokenizesResponseOnSuccess() throws JSON PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -491,7 +529,7 @@ public void tokenize_whenCancelUriReceived_notifiesCancellationAndSendsAnalytics PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -531,7 +569,7 @@ public void tokenize_whenPayPalInternalClientTokenizeResult_callsBackResult() PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -573,7 +611,7 @@ public void tokenize_whenPayPalInternalClientTokenizeResult_sendsAppSwitchSuccee PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -612,7 +650,7 @@ public void tokenize_whenPayPalNotEnabled_sendsAppSwitchFailedEvents() throws JS PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); @@ -644,7 +682,7 @@ public void tokenize_whenCancelUriReceived_sendsAppSwitchCanceledEvents() PayPalPaymentAuthResult.Success payPalPaymentAuthResult = new PayPalPaymentAuthResult.Success( browserSwitchResult); BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); - PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkTypeUseCase); + PayPalClient sut = new PayPalClient(braintreeClient, payPalInternalClient, merchantRepository, getReturnLinkUseCase); sut.tokenize(payPalPaymentAuthResult, payPalTokenizeCallback); diff --git a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalInternalClientUnitTest.java b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalInternalClientUnitTest.java index d86ead76d0..0de7604dc5 100644 --- a/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalInternalClientUnitTest.java +++ b/PayPal/src/test/java/com/braintreepayments/api/paypal/PayPalInternalClientUnitTest.java @@ -20,10 +20,11 @@ import com.braintreepayments.api.core.ApiClient; import com.braintreepayments.api.core.BraintreeClient; +import com.braintreepayments.api.core.BraintreeException; import com.braintreepayments.api.core.ClientToken; import com.braintreepayments.api.core.Configuration; import com.braintreepayments.api.core.DeviceInspector; -import com.braintreepayments.api.core.GetReturnLinkTypeUseCase; +import com.braintreepayments.api.core.GetReturnLinkUseCase; import com.braintreepayments.api.core.MerchantRepository; import com.braintreepayments.api.core.PostalAddress; import com.braintreepayments.api.core.TokenizationKey; @@ -64,7 +65,7 @@ public class PayPalInternalClientUnitTest { PayPalInternalClientCallback payPalInternalClientCallback; private MerchantRepository merchantRepository = mock(MerchantRepository.class); - private GetReturnLinkTypeUseCase getReturnLinkTypeUseCase = mock(GetReturnLinkTypeUseCase.class); + private GetReturnLinkUseCase getReturnLinkUseCase = mock(GetReturnLinkUseCase.class); @Before public void beforeEach() throws JSONException { @@ -78,7 +79,9 @@ public void beforeEach() throws JSONException { deviceInspector = mock(DeviceInspector.class); payPalInternalClientCallback = mock(PayPalInternalClientCallback.class); - when(getReturnLinkTypeUseCase.invoke()).thenReturn(GetReturnLinkTypeUseCase.ReturnLinkType.APP_LINK); + when(getReturnLinkUseCase.invoke()).thenReturn(new GetReturnLinkUseCase.ReturnLinkResult.AppLink( + Uri.parse("https://example.com") + )); } @Test @@ -97,7 +100,7 @@ public void sendRequest_withPayPalVaultRequest_sendsAllParameters() throws JSONE apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PostalAddress shippingAddressOverride = new PostalAddress(); @@ -160,7 +163,9 @@ public void sendRequest_withPayPalVaultRequest_sendsAllParameters() throws JSONE @Test public void sendRequest_withPayPalVaultRequest_sendsAllParameters_with_deep_link() throws JSONException { - when(getReturnLinkTypeUseCase.invoke()).thenReturn(GetReturnLinkTypeUseCase.ReturnLinkType.DEEP_LINK); + when(getReturnLinkUseCase.invoke()).thenReturn(new GetReturnLinkUseCase.ReturnLinkResult.DeepLink( + "com.braintreepayments.demo" + )); BraintreeClient braintreeClient = new MockBraintreeClientBuilder() .configuration(configuration) @@ -176,7 +181,7 @@ public void sendRequest_withPayPalVaultRequest_sendsAllParameters_with_deep_link apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PostalAddress shippingAddressOverride = new PostalAddress(); @@ -245,7 +250,6 @@ public void sendRequest_withPayPalCheckoutRequest_sendsAllParameters() throws JS when(clientToken.getBearer()).thenReturn("client-token-bearer"); when(merchantRepository.getAuthorization()).thenReturn(clientToken); - when(merchantRepository.getAppLinkReturnUri()).thenReturn(Uri.parse("https://example.com")); PayPalInternalClient sut = new PayPalInternalClient( braintreeClient, @@ -253,7 +257,7 @@ public void sendRequest_withPayPalCheckoutRequest_sendsAllParameters() throws JS apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PostalAddress shippingAddressOverride = new PostalAddress(); shippingAddressOverride.setRecipientName("Brianna Tree"); @@ -355,7 +359,7 @@ public void sendRequest_withTokenizationKey_sendsClientKeyParam() throws JSONExc apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -391,7 +395,7 @@ public void sendRequest_withEmptyDisplayName_fallsBackToPayPalConfigurationDispl apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(false); @@ -427,7 +431,7 @@ public void sendRequest_withLocaleNotSpecified_omitsLocale() throws JSONExceptio apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -463,7 +467,7 @@ public void sendRequest_withMerchantAccountIdNotSpecified_omitsMerchantAccountId apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -499,7 +503,7 @@ public void sendRequest_withShippingAddressOverrideNotSpecified_sendsAddressOver apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -536,7 +540,7 @@ public void sendRequest_withShippingAddressSpecified_sendsAddressOverrideBasedOn apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -574,7 +578,7 @@ public void sendRequest_withPayPalVaultRequest_omitsEmptyBillingAgreementDescrip apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -610,7 +614,7 @@ public void sendRequest_withPayPalCheckoutRequest_fallsBackToPayPalConfiguration apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -644,7 +648,7 @@ public void sendRequest_withPayPalCheckoutRequest_omitsEmptyLineItems() throws J apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -682,7 +686,7 @@ public void sendRequest_whenRiskCorrelationIdNotNull_setsClientMetadataIdToRiskC apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -715,7 +719,7 @@ public void sendRequest_whenRiskCorrelationIdNull_setsClientMetadataIdFromPayPal apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -746,7 +750,7 @@ public void sendRequest_withPayPalCheckoutRequest_whenRequestBillingAgreementFal apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -787,7 +791,7 @@ public void sendRequest_withPayPalVaultRequest_callsBackPayPalResponseOnSuccess( apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -827,7 +831,7 @@ public void sendRequest_withPayPalVaultRequest_callsBackPayPalResponseOnSuccess_ apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -870,7 +874,7 @@ public void sendRequest_withPayPalVaultRequest_callsBackPayPalResponseOnSuccess_ apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalVaultRequest payPalRequest = new PayPalVaultRequest(true); @@ -906,7 +910,7 @@ public void sendRequest_withPayPalCheckoutRequest_callsBackPayPalResponseOnSucce apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -949,7 +953,7 @@ public void sendRequest_propagatesHttpErrors() { apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -973,7 +977,7 @@ public void sendRequest_propagatesMalformedJSONResponseErrors() { apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -998,7 +1002,7 @@ public void sendRequest_onConfigurationFailure_forwardsError() { apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); @@ -1007,6 +1011,33 @@ public void sendRequest_onConfigurationFailure_forwardsError() { verify(payPalInternalClientCallback).onResult(null, configurationError); } + @Test + public void sendRequest_returnLinkResultFailure_forwardsError() { + BraintreeException exception = new BraintreeException(); + + when(getReturnLinkUseCase.invoke()).thenReturn(new GetReturnLinkUseCase.ReturnLinkResult.Failure(exception)); + BraintreeClient braintreeClient = new MockBraintreeClientBuilder() + .configuration(configuration) + .sendPOSTSuccessfulResponse("{bad:") + .build(); + + when(merchantRepository.getAuthorization()).thenReturn(clientToken); + + PayPalInternalClient sut = new PayPalInternalClient( + braintreeClient, + dataCollector, + apiClient, + deviceInspector, + merchantRepository, + getReturnLinkUseCase + ); + + PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true); + sut.sendRequest(context, payPalRequest, payPalInternalClientCallback); + + verify(payPalInternalClientCallback).onResult(isNull(), eq(exception)); + } + @Test public void tokenize_tokenizesWithApiClient() { BraintreeClient braintreeClient = new MockBraintreeClientBuilder().build(); @@ -1019,7 +1050,7 @@ public void tokenize_tokenizesWithApiClient() { apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); sut.tokenize(payPalAccount, callback); @@ -1043,7 +1074,7 @@ public void tokenize_onTokenizeResult_returnsAccountNonceToCallback() throws JSO apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); sut.tokenize(payPalAccount, callback); @@ -1074,7 +1105,7 @@ public void tokenize_onTokenizeError_returnsErrorToCallback() { apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); sut.tokenize(payPalAccount, callback); @@ -1099,7 +1130,7 @@ public void payPalDataCollector_passes_correct_arguments_to_getClientMetadataId( apiClient, deviceInspector, merchantRepository, - getReturnLinkTypeUseCase + getReturnLinkUseCase ); PayPalCheckoutRequest payPalRequest = new PayPalCheckoutRequest("1.00", true);