From 915403ee648f9843d40d3e676046330001754ef0 Mon Sep 17 00:00:00 2001 From: Sara Vasquez Date: Tue, 9 Jun 2026 17:13:16 -0700 Subject: [PATCH 1/3] fix: change functionality to remove deprecated method calls in google pay --- GooglePay/src/main/AndroidManifest.xml | 4 +- .../api/googlepay/GooglePayActivity.kt | 71 ------------ .../GooglePayActivityResultContract.kt | 49 --------- .../api/googlepay/GooglePayClient.kt | 20 ---- .../api/googlepay/GooglePayInternalClient.kt | 9 ++ .../api/googlepay/GooglePayLauncher.kt | 46 ++++++-- GooglePay/src/main/res/values/styles.xml | 16 --- ...GooglePayActivityResultContractUnitTest.kt | 102 ------------------ .../api/googlepay/GooglePayClientUnitTest.kt | 76 ------------- .../googlepay/GooglePayLauncherUnitTest.kt | 28 +++-- .../MockkGooglePayInternalClientBuilder.kt | 28 +++++ 11 files changed, 93 insertions(+), 356 deletions(-) delete mode 100644 GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayActivity.kt delete mode 100644 GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayActivityResultContract.kt delete mode 100644 GooglePay/src/main/res/values/styles.xml delete mode 100644 GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayActivityResultContractUnitTest.kt diff --git a/GooglePay/src/main/AndroidManifest.xml b/GooglePay/src/main/AndroidManifest.xml index 3cb6a1e553..6537ec12ba 100644 --- a/GooglePay/src/main/AndroidManifest.xml +++ b/GooglePay/src/main/AndroidManifest.xml @@ -1,7 +1,5 @@ - - + \ No newline at end of file diff --git a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayActivity.kt b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayActivity.kt deleted file mode 100644 index 5df2a0202e..0000000000 --- a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayActivity.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.braintreepayments.api.googlepay - -import android.content.Intent -import android.os.Build -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import com.braintreepayments.api.sharedutils.IntentExtensions.parcelable -import com.google.android.gms.wallet.AutoResolveHelper -import com.google.android.gms.wallet.PaymentDataRequest -import com.google.android.gms.wallet.Wallet -import com.google.android.gms.wallet.Wallet.WalletOptions -import com.google.android.gms.wallet.WalletConstants - -internal class GooglePayActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (savedInstanceState != null && savedInstanceState.getBoolean(EXTRA_RECREATING)) { - return - } - - val paymentsClient = Wallet.getPaymentsClient( - this, WalletOptions.Builder() - .setEnvironment( - intent.getIntExtra( - EXTRA_ENVIRONMENT, - WalletConstants.ENVIRONMENT_TEST - ) - ) - .build() - ) - - val request = intent.parcelable(EXTRA_PAYMENT_DATA_REQUEST) - if (request != null) { - AutoResolveHelper.resolveTask(paymentsClient.loadPaymentData(request), this, REQUEST_CODE) - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putBoolean(EXTRA_RECREATING, true) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - setResult(resultCode, data) - finish() - } - - override fun finish() { - super.finish() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, android.R.anim.fade_in, android.R.anim.fade_out) - } else { - @Suppress("DEPRECATION") - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) - } - } - - companion object { - const val EXTRA_ENVIRONMENT: String = - "com.braintreepayments.api.EXTRA_ENVIRONMENT" - const val EXTRA_PAYMENT_DATA_REQUEST: String = - "com.braintreepayments.api.EXTRA_PAYMENT_DATA_REQUEST" - - private const val EXTRA_RECREATING = "com.braintreepayments.api.EXTRA_RECREATING" - - private const val REQUEST_CODE = 1 - } -} diff --git a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayActivityResultContract.kt b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayActivityResultContract.kt deleted file mode 100644 index ee1f2e8fba..0000000000 --- a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayActivityResultContract.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.braintreepayments.api.googlepay - -import android.app.Activity -import android.content.Context -import android.content.Intent -import androidx.activity.result.contract.ActivityResultContract -import com.braintreepayments.api.core.BraintreeException -import com.braintreepayments.api.core.UserCanceledException -import com.google.android.gms.wallet.AutoResolveHelper -import com.google.android.gms.wallet.PaymentData - -internal class GooglePayActivityResultContract : - ActivityResultContract() { - override fun createIntent(context: Context, input: GooglePayPaymentAuthRequestParams): Intent { - return Intent(context, GooglePayActivity::class.java) - .putExtra(GooglePayClient.EXTRA_ENVIRONMENT, input.googlePayEnvironment) - .putExtra(GooglePayClient.EXTRA_PAYMENT_DATA_REQUEST, input.paymentDataRequest) - } - - override fun parseResult(resultCode: Int, intent: Intent?): GooglePayPaymentAuthResult { - when (resultCode) { - Activity.RESULT_OK -> { - if (intent != null) { - return GooglePayPaymentAuthResult(PaymentData.getFromIntent(intent), null) - } - } - Activity.RESULT_CANCELED -> { - return GooglePayPaymentAuthResult( - null, - UserCanceledException("User canceled Google Pay.") - ) - } - AutoResolveHelper.RESULT_ERROR -> { - if (intent != null) { - return GooglePayPaymentAuthResult( - null, - GooglePayException( - "An error was encountered during the Google Pay " + - "flow. See the status object in this exception for more details.", - AutoResolveHelper.getStatusFromIntent(intent) - ) - ) - } - } - } - - return GooglePayPaymentAuthResult(null, BraintreeException("An unexpected error occurred.")) - } -} diff --git a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt index 0863bce870..871789ef04 100644 --- a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt +++ b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt @@ -207,15 +207,6 @@ class GooglePayClient internal constructor( analyticsParamRepository.reset() braintreeClient.sendAnalyticsEvent(GooglePayAnalytics.PAYMENT_REQUEST_STARTED) - if (!validateManifest()) { - return GooglePayPaymentAuthRequest.Failure( - BraintreeException( - "GooglePayActivity was not found in the Android " + - "manifest, or did not have a theme of R.style.bt_transparent_activity" - ) - ) - } - try { val configuration = braintreeClient.getConfiguration() if (configuration.isGooglePayEnabled) { @@ -584,13 +575,6 @@ class GooglePayClient internal constructor( request.setEnvironment(configuration.googlePayEnvironment) } - private fun validateManifest(): Boolean { - val activityInfo = - braintreeClient.getManifestActivityInfo(GooglePayActivity::class.java) - return activityInfo != null && - activityInfo.themeResource == R.style.bt_transparent_activity - } - private fun callbackPaymentRequestSuccess( request: GooglePayPaymentAuthRequest.ReadyToLaunch, callback: GooglePayPaymentAuthRequestCallback @@ -635,10 +619,6 @@ class GooglePayClient internal constructor( } companion object { - const val EXTRA_ENVIRONMENT: String = "com.braintreepayments.api.EXTRA_ENVIRONMENT" - const val EXTRA_PAYMENT_DATA_REQUEST: String = - "com.braintreepayments.api.EXTRA_PAYMENT_DATA_REQUEST" - private const val VISA_NETWORK = "visa" private const val MASTERCARD_NETWORK = "mastercard" private const val AMEX_NETWORK = "amex" diff --git a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayInternalClient.kt b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayInternalClient.kt index 69290c2515..6a8ae35b58 100644 --- a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayInternalClient.kt +++ b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayInternalClient.kt @@ -6,6 +6,7 @@ import com.braintreepayments.api.googlepay.GooglePayReadinessResult.NotReadyToPa import com.google.android.gms.common.api.ApiException import com.google.android.gms.tasks.Task import com.google.android.gms.wallet.IsReadyToPayRequest +import com.google.android.gms.wallet.PaymentData import com.google.android.gms.wallet.Wallet import com.google.android.gms.wallet.Wallet.WalletOptions import com.google.android.gms.wallet.WalletConstants @@ -41,6 +42,14 @@ internal class GooglePayInternalClient { } } + fun loadPaymentData(context: Context, params: GooglePayPaymentAuthRequestParams): Task { + val paymentsClient = Wallet.getPaymentsClient( + context, + WalletOptions.Builder().setEnvironment(params.googlePayEnvironment).build() + ) + return paymentsClient.loadPaymentData(params.paymentDataRequest) + } + private fun getGooglePayEnvironment(configuration: Configuration): Int { return if ("production" == configuration.googlePayEnvironment) { WalletConstants.ENVIRONMENT_PRODUCTION diff --git a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayLauncher.kt b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayLauncher.kt index 32f321b77c..2d4186a172 100644 --- a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayLauncher.kt +++ b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayLauncher.kt @@ -1,10 +1,16 @@ package com.braintreepayments.api.googlepay +import android.content.Context import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultRegistry import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner +import com.braintreepayments.api.core.UserCanceledException +import com.google.android.gms.tasks.Task +import com.google.android.gms.wallet.PaymentData +import com.google.android.gms.wallet.contract.ApiTaskResult +import com.google.android.gms.wallet.contract.TaskResultContracts /** * Responsible for launching the Google Pay payment sheet @@ -12,16 +18,31 @@ import androidx.lifecycle.LifecycleOwner class GooglePayLauncher internal constructor( registry: ActivityResultRegistry, lifecycleOwner: LifecycleOwner, + private val context: Context, + private val internalGooglePayClient: GooglePayInternalClient = GooglePayInternalClient(), callback: GooglePayLauncherCallback ) { - private val activityLauncher: ActivityResultLauncher = registry.register( + private val activityLauncher: ActivityResultLauncher> = registry.register( GOOGLE_PAY_RESULT, lifecycleOwner, - GooglePayActivityResultContract() - ) { googlePayPaymentAuthResult: GooglePayPaymentAuthResult -> - callback.onGooglePayLauncherResult( - googlePayPaymentAuthResult - ) + TaskResultContracts.GetPaymentDataResult() + ) { apiTaskResult: ApiTaskResult -> + val result = when { + apiTaskResult.status.isSuccess -> + GooglePayPaymentAuthResult(apiTaskResult.result, null) + apiTaskResult.status.isCanceled -> + GooglePayPaymentAuthResult(null, UserCanceledException("User canceled Google Pay.")) + else -> + GooglePayPaymentAuthResult( + null, + GooglePayException( + "An error was encountered during the Google Pay " + + "flow. See the status object in this exception for more details.", + apiTaskResult.status + ) + ) + } + callback.onGooglePayLauncherResult(result) } /** @@ -36,8 +57,10 @@ class GooglePayLauncher internal constructor( fragment: Fragment, callback: GooglePayLauncherCallback ) : this( - fragment.requireActivity().activityResultRegistry, fragment.viewLifecycleOwner, - callback + fragment.requireActivity().activityResultRegistry, + fragment.viewLifecycleOwner, + fragment.requireContext(), + callback = callback ) /** @@ -51,7 +74,7 @@ class GooglePayLauncher internal constructor( constructor( activity: ComponentActivity, callback: GooglePayLauncherCallback - ) : this(activity.activityResultRegistry, activity, callback) + ) : this(activity.activityResultRegistry, activity, activity, callback = callback) /** * Launches the Google Pay payment sheet. This method cannot be called until the lifecycle of @@ -62,7 +85,10 @@ class GooglePayLauncher internal constructor( * received from invoking [GooglePayClient.createPaymentAuthRequest] */ fun launch(paymentAuthRequest: GooglePayPaymentAuthRequest.ReadyToLaunch) { - activityLauncher.launch(paymentAuthRequest.requestParams) + internalGooglePayClient.loadPaymentData(context, paymentAuthRequest.requestParams) + .addOnCompleteListener { completedTask -> + activityLauncher.launch(completedTask) + } } companion object { diff --git a/GooglePay/src/main/res/values/styles.xml b/GooglePay/src/main/res/values/styles.xml deleted file mode 100644 index f62686308f..0000000000 --- a/GooglePay/src/main/res/values/styles.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayActivityResultContractUnitTest.kt b/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayActivityResultContractUnitTest.kt deleted file mode 100644 index 549a42e726..0000000000 --- a/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayActivityResultContractUnitTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.braintreepayments.api.googlepay - -import android.app.Activity -import android.content.Context -import android.content.Intent -import androidx.test.core.app.ApplicationProvider -import com.braintreepayments.api.core.BraintreeException -import com.braintreepayments.api.core.UserCanceledException -import com.braintreepayments.api.sharedutils.IntentExtensions.parcelable -import com.google.android.gms.wallet.AutoResolveHelper -import com.google.android.gms.wallet.PaymentData -import com.google.android.gms.wallet.PaymentDataRequest -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull -import junit.framework.TestCase.assertNull -import junit.framework.TestCase.assertSame -import junit.framework.TestCase.assertTrue -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class GooglePayActivityResultContractUnitTest { - - @Test - fun `createIntent returns Intent with extras`() { - val googlePayRequest = GooglePayRequest("USD", "1.00", GooglePayTotalPriceStatus.TOTAL_PRICE_STATUS_FINAL) - - val paymentDataRequest = PaymentDataRequest.fromJson(googlePayRequest.toJson()) - - val intentData = GooglePayPaymentAuthRequestParams(1, paymentDataRequest) - - val context = ApplicationProvider.getApplicationContext() - - val sut = GooglePayActivityResultContract() - val intent = sut.createIntent(context, intentData) - - assertEquals(1, intent.getIntExtra(GooglePayClient.EXTRA_ENVIRONMENT, 0)) - assertSame( - paymentDataRequest, - intent.parcelable(GooglePayClient.EXTRA_PAYMENT_DATA_REQUEST) - ) - } - - @Test - fun `parseResult when result is OK and payment data exists returns Google Pay result with PaymentData`() { - val paymentData = PaymentData.fromJson("{}") - val data = Intent() - paymentData.putIntoIntent(data) - - val sut = GooglePayActivityResultContract() - - val result = sut.parseResult(Activity.RESULT_OK, data) - assertNotNull(result.paymentData) - assertNull(result.error) - } - - @Test - fun `parseResult when result is canceled returns Google Pay result with error`() { - val data = Intent() - - val sut = GooglePayActivityResultContract() - - val result = sut.parseResult(Activity.RESULT_CANCELED, data) - - val error = result.error - assertTrue(error is UserCanceledException) - assertEquals("User canceled Google Pay.", error!!.message) - assertNull(result.paymentData) - } - - @Test - fun `parseResult when RESULT_ERROR returns Google Pay result with error`() { - val data = Intent() - - val sut = GooglePayActivityResultContract() - - val result = sut.parseResult(AutoResolveHelper.RESULT_ERROR, data) - - val error = result.error - assertTrue(error is GooglePayException) - assertEquals( - "An error was encountered during the Google Pay " + - "flow. See the status object in this exception for more details.", error!!.message - ) - assertNull(result.paymentData) - } - - @Test - fun `parseResult when result is unexpected returns Google Pay result with error`() { - val data = Intent() - - val sut = GooglePayActivityResultContract() - - val result = sut.parseResult(2, data) - - val error = result.error - assertTrue(error is BraintreeException) - assertEquals("An unexpected error occurred.", error!!.message) - assertNull(result.paymentData) - } -} diff --git a/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayClientUnitTest.kt b/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayClientUnitTest.kt index 8cd31f7179..92124fab5e 100644 --- a/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayClientUnitTest.kt +++ b/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayClientUnitTest.kt @@ -1,6 +1,5 @@ package com.braintreepayments.api.googlepay -import android.content.pm.ActivityInfo import androidx.fragment.app.FragmentActivity import com.braintreepayments.api.core.AnalyticsEventParams import com.braintreepayments.api.core.AnalyticsParamRepository @@ -53,7 +52,6 @@ class GooglePayClientUnitTest { private lateinit var readyToPayCallback: GooglePayIsReadyToPayCallback private lateinit var intentDataCallback: GooglePayPaymentAuthRequestCallback private lateinit var activityResultCallback: GooglePayTokenizeCallback - private lateinit var activityInfo: ActivityInfo private lateinit var analyticsParamRepository: AnalyticsParamRepository private lateinit var merchantRepository: MerchantRepository private lateinit var testScope: TestScope @@ -64,13 +62,11 @@ class GooglePayClientUnitTest { readyToPayCallback = mockk(relaxed = true) activityResultCallback = mockk(relaxed = true) intentDataCallback = mockk(relaxed = true) - activityInfo = mockk(relaxed = true) analyticsParamRepository = mockk(relaxed = true) merchantRepository = mockk(relaxed = true) testScope = TestScope(testDispatcher) baseRequest = GooglePayRequest("USD", "1.00", GooglePayTotalPriceStatus.TOTAL_PRICE_STATUS_FINAL) - every { activityInfo.themeResource } returns R.style.bt_transparent_activity } @Test @@ -177,7 +173,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString(Fixtures.TOKENIZATION_KEY) @@ -245,7 +240,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString(Fixtures.TOKENIZATION_KEY) @@ -287,7 +281,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { @@ -427,7 +420,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString(Fixtures.TOKENIZATION_KEY) @@ -479,7 +471,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN) @@ -532,7 +523,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -564,7 +554,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN) @@ -617,7 +606,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -665,7 +653,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -713,7 +700,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -762,7 +748,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -810,7 +795,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -862,7 +846,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -916,7 +899,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -971,7 +953,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -1036,7 +1017,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -1087,7 +1067,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -1126,54 +1105,6 @@ class GooglePayClientUnitTest { assertEquals("ELO_DEBIT", allowedCardNetworks.getString(1)) } - @Test - fun createPaymentAuthRequest_whenManifestInvalid_forwardsExceptionToListener() = runTest(testDispatcher) { - val configuration = Configuration.fromJson(TestConfigurationBuilder() - .googlePay( - TestConfigurationBuilder.TestGooglePayConfigurationBuilder() - .environment("sandbox") - .googleAuthorizationFingerprint("google-auth-fingerprint") - .paypalClientId("paypal-client-id-for-google-payment") - .supportedNetworks(arrayOf("visa", "mastercard", "amex", "discover")) - .enabled(true) - ) - .withAnalytics() - .build()) - - val braintreeClient = MockkBraintreeClientBuilder() - .configurationSuccess(configuration) - .build() - - every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") - - val internalGooglePayClient = MockkGooglePayInternalClientBuilder().build() - - val sut = GooglePayClient( - braintreeClient, - internalGooglePayClient, - analyticsParamRepository, - merchantRepository, - testDispatcher, - testScope - ) - sut.createPaymentAuthRequest(baseRequest, intentDataCallback) - advanceUntilIdle() - - val captor = slot() - verify { intentDataCallback.onGooglePayPaymentAuthRequest(capture(captor)) } - - val request = captor.captured - assertTrue(request is GooglePayPaymentAuthRequest.Failure) - val exception = request.error - assertTrue(exception is BraintreeException) - assertEquals( - "GooglePayActivity was not found in the Android manifest, " + - "or did not have a theme of R.style.bt_transparent_activity", - exception.message - ) - verify { braintreeClient.sendAnalyticsEvent(GooglePayAnalytics.PAYMENT_REQUEST_FAILED, any()) } - } - @Test fun tokenize_withCardToken_returnsGooglePayNonce() { val paymentDataJson = Fixtures.RESPONSE_GOOGLE_PAY_CARD @@ -1192,7 +1123,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -1235,7 +1165,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -1359,7 +1288,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString("sandbox_tokenization_string") @@ -1395,7 +1323,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() val authorization = Authorization.fromString(Fixtures.TOKENIZATION_KEY) @@ -1437,7 +1364,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() val authorization = Authorization.fromString(Fixtures.BASE64_CLIENT_TOKEN) @@ -1468,7 +1394,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() val authorization = Authorization.fromString(Fixtures.TOKENIZATION_KEY) @@ -1523,7 +1448,6 @@ class GooglePayClientUnitTest { val braintreeClient = MockkBraintreeClientBuilder() .configurationSuccess(configuration) - .activityInfo(activityInfo) .build() every { merchantRepository.authorization } returns Authorization.fromString(Fixtures.TOKENIZATION_KEY) diff --git a/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayLauncherUnitTest.kt b/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayLauncherUnitTest.kt index c5a7944b50..56e93e6a45 100644 --- a/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayLauncherUnitTest.kt +++ b/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayLauncherUnitTest.kt @@ -1,10 +1,15 @@ package com.braintreepayments.api.googlepay +import android.content.Context import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContract import androidx.fragment.app.FragmentActivity +import androidx.test.core.app.ApplicationProvider +import com.google.android.gms.tasks.Task +import com.google.android.gms.wallet.PaymentData import com.google.android.gms.wallet.PaymentDataRequest +import com.google.android.gms.wallet.contract.TaskResultContracts import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -16,8 +21,7 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class GooglePayLauncherUnitTest { - private val activityResultLauncher = - mockk>(relaxed = true) + private val activityResultLauncher = mockk>>(relaxed = true) private val callback = mockk(relaxed = true) private val activityResultRegistry = mockk(relaxed = true) @@ -27,7 +31,7 @@ class GooglePayLauncherUnitTest { activityResultRegistry.register( any(), any(), - any>(), + any, Any>>(), any() ) } returns activityResultLauncher @@ -37,25 +41,31 @@ class GooglePayLauncherUnitTest { fun constructor_createsActivityLauncher() { val expectedKey = "com.braintreepayments.api.GooglePay.RESULT" val lifecycleOwner = FragmentActivity() + val context = ApplicationProvider.getApplicationContext() val registry = mockk(relaxed = true) - GooglePayLauncher(registry, lifecycleOwner, callback) + GooglePayLauncher(registry, lifecycleOwner, context, callback = callback) verify { registry.register( eq(expectedKey), eq(lifecycleOwner), - any>(), + any(), any() ) } } @Test - fun launch_launchesActivity() { + fun launch_launchesTask() { val lifecycleOwner = FragmentActivity() + val context = ApplicationProvider.getApplicationContext() + val mockTask = mockk>(relaxed = true) + val internalGooglePayClient = MockkGooglePayInternalClientBuilder() + .loadPaymentDataTask(mockTask) + .build() + val sut = GooglePayLauncher( - activityResultRegistry, lifecycleOwner, - callback + activityResultRegistry, lifecycleOwner, context, internalGooglePayClient, callback ) val googlePayRequest = GooglePayRequest("USD", "1.00", GooglePayTotalPriceStatus.TOTAL_PRICE_STATUS_FINAL) @@ -63,6 +73,6 @@ class GooglePayLauncherUnitTest { val intentData = GooglePayPaymentAuthRequestParams(1, paymentDataRequest) sut.launch(GooglePayPaymentAuthRequest.ReadyToLaunch(intentData)) - verify { activityResultLauncher.launch(intentData) } + verify { activityResultLauncher.launch(mockTask) } } } diff --git a/GooglePay/src/test/java/com/braintreepayments/api/googlepay/MockkGooglePayInternalClientBuilder.kt b/GooglePay/src/test/java/com/braintreepayments/api/googlepay/MockkGooglePayInternalClientBuilder.kt index 2121af9632..8d211ea815 100644 --- a/GooglePay/src/test/java/com/braintreepayments/api/googlepay/MockkGooglePayInternalClientBuilder.kt +++ b/GooglePay/src/test/java/com/braintreepayments/api/googlepay/MockkGooglePayInternalClientBuilder.kt @@ -1,9 +1,14 @@ package com.braintreepayments.api.googlepay +import android.content.Context import androidx.fragment.app.FragmentActivity import com.braintreepayments.api.core.Configuration +import com.google.android.gms.tasks.OnCompleteListener +import com.google.android.gms.tasks.Task import com.google.android.gms.wallet.IsReadyToPayRequest +import com.google.android.gms.wallet.PaymentData import io.mockk.coEvery +import io.mockk.every import io.mockk.mockk @Suppress("MagicNumber") @@ -11,6 +16,7 @@ internal class MockkGooglePayInternalClientBuilder { private var isReadyToPay: Boolean = false private var isReadyToPayError: Exception? = null + private var loadPaymentDataTask: Task = buildCompletingTask(mockk(relaxed = true)) fun isReadyToPay(isReadyToPay: Boolean): MockkGooglePayInternalClientBuilder { this.isReadyToPay = isReadyToPay @@ -22,6 +28,11 @@ internal class MockkGooglePayInternalClientBuilder { return this } + fun loadPaymentDataTask(task: Task): MockkGooglePayInternalClientBuilder { + this.loadPaymentDataTask = buildCompletingTask(task) + return this + } + fun build(): GooglePayInternalClient { val googlePayInternalClient = mockk(relaxed = true) @@ -39,6 +50,23 @@ internal class MockkGooglePayInternalClientBuilder { ?: let { GooglePayReadinessResult.ReadyToPay } } + every { + googlePayInternalClient.loadPaymentData( + any(), + any() + ) + } returns loadPaymentDataTask + return googlePayInternalClient } + + companion object { + fun buildCompletingTask(task: Task): Task { + every { task.addOnCompleteListener(any()) } answers { + firstArg>().onComplete(task) + task + } + return task + } + } } From 755a9bbd3513218d7f179bb843a4d2711ea88167 Mon Sep 17 00:00:00 2001 From: Sara Vasquez Date: Wed, 10 Jun 2026 16:53:50 -0700 Subject: [PATCH 2/3] fix: resolve possible memory leak, add changelog, and new tests --- CHANGELOG.md | 5 ++ .../api/googlepay/GooglePayClient.kt | 7 ++ .../api/googlepay/GooglePayLauncher.kt | 6 +- .../googlepay/GooglePayLauncherUnitTest.kt | 90 +++++++++++++++++++ 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8956a9531..74dc8d6459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Braintree Android SDK Release Notes +## unreleased + +* GooglePay + * Remove requirement for `GooglePayActivity` to be declared in the Android manifest + ## 5.28.1 (2026-06-01) * PayPal diff --git a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt index 871789ef04..0ba349f962 100644 --- a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt +++ b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt @@ -619,6 +619,13 @@ class GooglePayClient internal constructor( } companion object { + @Deprecated("This constant is no longer used and will be removed in a future release.") + const val EXTRA_ENVIRONMENT: String = "com.braintreepayments.api.EXTRA_ENVIRONMENT" + + @Deprecated("This constant is no longer used and will be removed in a future release.") + const val EXTRA_PAYMENT_DATA_REQUEST: String = + "com.braintreepayments.api.EXTRA_PAYMENT_DATA_REQUEST" + private const val VISA_NETWORK = "visa" private const val MASTERCARD_NETWORK = "mastercard" private const val AMEX_NETWORK = "amex" diff --git a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayLauncher.kt b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayLauncher.kt index 2d4186a172..32e5f33b34 100644 --- a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayLauncher.kt +++ b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayLauncher.kt @@ -18,11 +18,13 @@ import com.google.android.gms.wallet.contract.TaskResultContracts class GooglePayLauncher internal constructor( registry: ActivityResultRegistry, lifecycleOwner: LifecycleOwner, - private val context: Context, + context: Context, private val internalGooglePayClient: GooglePayInternalClient = GooglePayInternalClient(), callback: GooglePayLauncherCallback ) { + private val appContext: Context = context.applicationContext + private val activityLauncher: ActivityResultLauncher> = registry.register( GOOGLE_PAY_RESULT, lifecycleOwner, TaskResultContracts.GetPaymentDataResult() @@ -85,7 +87,7 @@ class GooglePayLauncher internal constructor( * received from invoking [GooglePayClient.createPaymentAuthRequest] */ fun launch(paymentAuthRequest: GooglePayPaymentAuthRequest.ReadyToLaunch) { - internalGooglePayClient.loadPaymentData(context, paymentAuthRequest.requestParams) + internalGooglePayClient.loadPaymentData(appContext, paymentAuthRequest.requestParams) .addOnCompleteListener { completedTask -> activityLauncher.launch(completedTask) } diff --git a/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayLauncherUnitTest.kt b/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayLauncherUnitTest.kt index 56e93e6a45..f57d35c558 100644 --- a/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayLauncherUnitTest.kt +++ b/GooglePay/src/test/java/com/braintreepayments/api/googlepay/GooglePayLauncherUnitTest.kt @@ -1,18 +1,26 @@ package com.braintreepayments.api.googlepay import android.content.Context +import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContract import androidx.fragment.app.FragmentActivity import androidx.test.core.app.ApplicationProvider +import com.braintreepayments.api.core.UserCanceledException +import com.google.android.gms.common.api.Status import com.google.android.gms.tasks.Task import com.google.android.gms.wallet.PaymentData import com.google.android.gms.wallet.PaymentDataRequest +import com.google.android.gms.wallet.contract.ApiTaskResult import com.google.android.gms.wallet.contract.TaskResultContracts import io.mockk.every import io.mockk.mockk +import io.mockk.slot import io.mockk.verify +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -75,4 +83,86 @@ class GooglePayLauncherUnitTest { sut.launch(GooglePayPaymentAuthRequest.ReadyToLaunch(intentData)) verify { activityResultLauncher.launch(mockTask) } } + + @Test + fun `callback_whenSuccess_passesPaymentDataToCallback`() { + val callbackSlot = slot>>() + val registry = mockk(relaxed = true) + every { + registry.register(any(), any(), any(), capture(callbackSlot)) + } returns mockk(relaxed = true) + val lifecycleOwner = FragmentActivity() + val context = ApplicationProvider.getApplicationContext() + GooglePayLauncher(registry, lifecycleOwner, context, callback = callback) + + val paymentData = mockk(relaxed = true) + val status = mockk(relaxed = true) + every { status.isSuccess } returns true + val apiTaskResult = mockk>(relaxed = true) + every { apiTaskResult.status } returns status + every { apiTaskResult.result } returns paymentData + + callbackSlot.captured.onActivityResult(apiTaskResult) + + val resultSlot = slot() + verify { callback.onGooglePayLauncherResult(capture(resultSlot)) } + assertEquals(paymentData, resultSlot.captured.paymentData) + assertNull(resultSlot.captured.error) + } + + @Test + fun `callback_whenCanceled_passesUserCanceledExceptionToCallback`() { + val callbackSlot = slot>>() + val registry = mockk(relaxed = true) + every { + registry.register(any(), any(), any(), capture(callbackSlot)) + } returns mockk(relaxed = true) + val lifecycleOwner = FragmentActivity() + val context = ApplicationProvider.getApplicationContext() + GooglePayLauncher(registry, lifecycleOwner, context, callback = callback) + + val status = mockk(relaxed = true) + every { status.isSuccess } returns false + every { status.isCanceled } returns true + val apiTaskResult = mockk>(relaxed = true) + every { apiTaskResult.status } returns status + + callbackSlot.captured.onActivityResult(apiTaskResult) + + val resultSlot = slot() + verify { callback.onGooglePayLauncherResult(capture(resultSlot)) } + assertNull(resultSlot.captured.paymentData) + assertTrue(resultSlot.captured.error is UserCanceledException) + assertEquals("User canceled Google Pay.", resultSlot.captured.error!!.message) + } + + @Test + fun `callback_whenError_passesGooglePayExceptionToCallback`() { + val callbackSlot = slot>>() + val registry = mockk(relaxed = true) + every { + registry.register(any(), any(), any(), capture(callbackSlot)) + } returns mockk(relaxed = true) + val lifecycleOwner = FragmentActivity() + val context = ApplicationProvider.getApplicationContext() + GooglePayLauncher(registry, lifecycleOwner, context, callback = callback) + + val status = mockk(relaxed = true) + every { status.isSuccess } returns false + every { status.isCanceled } returns false + val apiTaskResult = mockk>(relaxed = true) + every { apiTaskResult.status } returns status + + callbackSlot.captured.onActivityResult(apiTaskResult) + + val resultSlot = slot() + verify { callback.onGooglePayLauncherResult(capture(resultSlot)) } + assertNull(resultSlot.captured.paymentData) + assertTrue(resultSlot.captured.error is GooglePayException) + assertEquals( + "An error was encountered during the Google Pay " + + "flow. See the status object in this exception for more details.", + resultSlot.captured.error!!.message + ) + } } From f75360d4a706f1d295b129fbee94c92210d29860 Mon Sep 17 00:00:00 2001 From: Sara Vasquez Date: Mon, 15 Jun 2026 16:43:47 -0700 Subject: [PATCH 3/3] fix: address PR comments --- CHANGELOG.md | 4 +++- .../com/braintreepayments/api/googlepay/GooglePayClient.kt | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74dc8d6459..6980096c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ ## unreleased * GooglePay - * Remove requirement for `GooglePayActivity` to be declared in the Android manifest + * Remove requirement for `GooglePayActivity` to be declared in the Android manifest (fixes #1572) + * Note: If upgrading from v4, any manual `GooglePayActivity` declaration in your app's manifest should be removed + * Deprecate unused `GooglePayClient.EXTRA_ENVIRONMENT` and `GooglePayClient.EXTRA_PAYMENT_DATA_REQUEST` constants ## 5.28.1 (2026-06-01) diff --git a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt index 0ba349f962..52346d7f8c 100644 --- a/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt +++ b/GooglePay/src/main/java/com/braintreepayments/api/googlepay/GooglePayClient.kt @@ -619,10 +619,12 @@ class GooglePayClient internal constructor( } companion object { - @Deprecated("This constant is no longer used and will be removed in a future release.") + // NEXT_MAJOR_VERSION: Remove the following constants that are no longer used in the Google Pay flow + @Deprecated("This constant is no longer used in the Google Pay flow and will be removed in a future release.") const val EXTRA_ENVIRONMENT: String = "com.braintreepayments.api.EXTRA_ENVIRONMENT" - @Deprecated("This constant is no longer used and will be removed in a future release.") + // NEXT_MAJOR_VERSION: Remove the following constants that are no longer used in the Google Pay flow + @Deprecated("This constant is no longer used in the Google Pay flow and will be removed in a future release.") const val EXTRA_PAYMENT_DATA_REQUEST: String = "com.braintreepayments.api.EXTRA_PAYMENT_DATA_REQUEST"