From 966e607a9d58b49e569e550ab9c16f0e2eaf67c8 Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Tue, 28 Jun 2022 13:57:20 -0700 Subject: [PATCH 1/4] Persist GooglePayLauncherViewModel state --- .../GooglePayLauncherIntegrationActivity.kt | 45 ++++++++++++------- .../GooglePayLauncherActivity.kt | 6 +-- .../GooglePayLauncherViewModel.kt | 36 ++++++++++++--- 3 files changed, 61 insertions(+), 26 deletions(-) diff --git a/example/src/main/java/com/stripe/example/activity/GooglePayLauncherIntegrationActivity.kt b/example/src/main/java/com/stripe/example/activity/GooglePayLauncherIntegrationActivity.kt index d75d3c80f24..eef1616ad33 100644 --- a/example/src/main/java/com/stripe/example/activity/GooglePayLauncherIntegrationActivity.kt +++ b/example/src/main/java/com/stripe/example/activity/GooglePayLauncherIntegrationActivity.kt @@ -28,20 +28,24 @@ class GooglePayLauncherIntegrationActivity : StripeIntentActivity() { super.onCreate(savedInstanceState) setContentView(viewBinding.root) - viewBinding.progressBar.isVisible = true - viewBinding.googlePayButton.isEnabled = false - - viewModel.createPaymentIntent(COUNTRY_CODE) - .observe(this) { result -> - result.fold( - onSuccess = ::onPaymentIntentCreated, - onFailure = { error -> - snackbarController.show( - "Could not create PaymentIntent. ${error.message}" - ) - } - ) - } + // If the activity is being recreated, load the client secret if it has already been fetched + savedInstanceState?.let { + clientSecret = it.getString(SAVED_CLIENT_SECRET, "") + } + + if (clientSecret.isBlank()) { + viewModel.createPaymentIntent(COUNTRY_CODE) + .observe(this) { result -> + result.fold( + onSuccess = ::onPaymentIntentCreated, + onFailure = { error -> + snackbarController.show( + "Could not create PaymentIntent. ${error.message}" + ) + } + ) + } + } val googlePayLauncher = GooglePayLauncher( activity = this, @@ -52,9 +56,9 @@ class GooglePayLauncherIntegrationActivity : StripeIntentActivity() { billingAddressConfig = GooglePayLauncher.BillingAddressConfig( isRequired = true, format = GooglePayLauncher.BillingAddressConfig.Format.Full, - isPhoneNumberRequired = true + isPhoneNumberRequired = false ), - existingPaymentMethodRequired = true + existingPaymentMethodRequired = false ), readyCallback = ::onGooglePayReady, resultCallback = ::onGooglePayResult @@ -64,6 +68,13 @@ class GooglePayLauncherIntegrationActivity : StripeIntentActivity() { viewBinding.progressBar.isVisible = true googlePayLauncher.presentForPaymentIntent(clientSecret) } + + updateUi() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(SAVED_CLIENT_SECRET, clientSecret) } private fun updateUi() { @@ -98,10 +109,12 @@ class GooglePayLauncherIntegrationActivity : StripeIntentActivity() { } }.let { snackbarController.show(it) + googlePayButton.isEnabled = false } } private companion object { private const val COUNTRY_CODE = "US" + private const val SAVED_CLIENT_SECRET = "client_secret" } } diff --git a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherActivity.kt b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherActivity.kt index 98d926a4a45..79289724c0a 100644 --- a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherActivity.kt +++ b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherActivity.kt @@ -35,7 +35,8 @@ internal class GooglePayLauncherActivity : AppCompatActivity() { private val viewModel: GooglePayLauncherViewModel by viewModels { GooglePayLauncherViewModel.Factory( application, - args + args, + this ) } @@ -68,14 +69,13 @@ internal class GooglePayLauncherActivity : AppCompatActivity() { } if (!viewModel.hasLaunched) { - viewModel.hasLaunched = true - lifecycleScope.launch { runCatching { viewModel.createLoadPaymentDataTask() }.fold( onSuccess = { payWithGoogle(it) + viewModel.hasLaunched = true }, onFailure = { viewModel.updateResult( diff --git a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt index d45f5c98548..02b097815f1 100644 --- a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt +++ b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt @@ -2,12 +2,15 @@ package com.stripe.android.googlepaylauncher import android.app.Application import android.content.Intent +import android.os.Bundle import androidx.annotation.VisibleForTesting +import androidx.lifecycle.AbstractSavedStateViewModelFactory import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.viewModelScope +import androidx.savedstate.SavedStateRegistryOwner import com.google.android.gms.tasks.Task import com.google.android.gms.wallet.PaymentData import com.google.android.gms.wallet.PaymentDataRequest @@ -42,9 +45,17 @@ internal class GooglePayLauncherViewModel( private val stripeRepository: StripeRepository, private val paymentController: PaymentController, private val googlePayJsonFactory: GooglePayJsonFactory, - private val googlePayRepository: GooglePayRepository + private val googlePayRepository: GooglePayRepository, + private val savedStateHandle: SavedStateHandle ) : ViewModel() { - var hasLaunched: Boolean = false + /** + * [hasLaunched] indicates whether Google Pay has already been launched, and must be persisted + * across process death in case the Activity and ViewModel are destroyed while the user is + * interacting with Google Pay. + */ + var hasLaunched: Boolean + get() = savedStateHandle.get(HAS_LAUNCHED_KEY) == true + set(value) = savedStateHandle.set(HAS_LAUNCHED_KEY, value) private val _googleResult = MutableLiveData() internal val googlePayResult = _googleResult.distinctUntilChanged() @@ -209,11 +220,17 @@ internal class GooglePayLauncherViewModel( internal class Factory( private val application: Application, private val args: GooglePayLauncherContract.Args, + owner: SavedStateRegistryOwner, private val enableLogging: Boolean = false, - private val workContext: CoroutineContext = Dispatchers.IO - ) : ViewModelProvider.Factory { + private val workContext: CoroutineContext = Dispatchers.IO, + defaultArgs: Bundle? = null + ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { + override fun create( + key: String, + modelClass: Class, + handle: SavedStateHandle + ): T { val googlePayEnvironment = args.config.environment val logger = Logger.getInstance(enableLogging) @@ -265,8 +282,13 @@ internal class GooglePayLauncherViewModel( googlePayConfig = GooglePayConfig(publishableKey, stripeAccountId), isJcbEnabled = args.config.isJcbEnabled ), - googlePayRepository + googlePayRepository, + handle ) as T } } + + companion object { + private const val HAS_LAUNCHED_KEY = "has_launched" + } } From 79b4cad365c04f6e4ec8dca04869cb5ef5e02a8a Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Tue, 28 Jun 2022 14:05:14 -0700 Subject: [PATCH 2/4] LongParameterList --- .../android/googlepaylauncher/GooglePayLauncherViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt index 02b097815f1..9a5e67b0434 100644 --- a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt +++ b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.launch import org.json.JSONObject import kotlin.coroutines.CoroutineContext +@Suppress("LongParameterList") internal class GooglePayLauncherViewModel( private val paymentsClient: PaymentsClient, private val requestOptions: ApiRequest.Options, From 824b645aeda31278e050b36c8ff69ee5a9786b4c Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Tue, 28 Jun 2022 14:13:00 -0700 Subject: [PATCH 3/4] CHANGELOG --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a187f4a422..69f7b183ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,10 @@ ## X.X.X ### PaymentSheet -* [Fixed][5215](https://github.com/stripe/stripe-android/pull/5215) Fix issue with us_bank_account appearing in payment sheet when Financial Connections SDK is not available +* [Fixed] [5215](https://github.com/stripe/stripe-android/pull/5215) Fix issue with us_bank_account appearing in payment sheet when Financial Connections SDK is not available + +### Payments +* [FIXED] [5226](https://github.com/stripe/stripe-android/pull/5226) Persist `GooglePayLauncherViewModel` state across process death ## 20.6.2 - 2022-06-23 This release contains several bug fixes for Payments, reduces the size of StripeCardScan, and adds new `rememberFinancialConnections` features for Financial Connections. From 8ddacd498339026baf3a3a8779bfe169b154928f Mon Sep 17 00:00:00 2001 From: "Bruno R. Nunes" Date: Tue, 28 Jun 2022 14:24:15 -0700 Subject: [PATCH 4/4] test --- .../GooglePayLauncherViewModel.kt | 3 ++- .../GooglePayLauncherViewModelTest.kt | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt index 9a5e67b0434..5f8d9003374 100644 --- a/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt +++ b/payments-core/src/main/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModel.kt @@ -290,6 +290,7 @@ internal class GooglePayLauncherViewModel( } companion object { - private const val HAS_LAUNCHED_KEY = "has_launched" + @VisibleForTesting + const val HAS_LAUNCHED_KEY = "has_launched" } } diff --git a/payments-core/src/test/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModelTest.kt b/payments-core/src/test/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModelTest.kt index 1a805d61f0b..06ce89a797d 100644 --- a/payments-core/src/test/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModelTest.kt +++ b/payments-core/src/test/java/com/stripe/android/googlepaylauncher/GooglePayLauncherViewModelTest.kt @@ -1,6 +1,7 @@ package com.stripe.android.googlepaylauncher import android.content.Intent +import androidx.lifecycle.SavedStateHandle import com.google.android.gms.tasks.Task import com.google.android.gms.wallet.PaymentData import com.google.android.gms.wallet.PaymentsClient @@ -14,6 +15,7 @@ import com.stripe.android.SetupIntentResult import com.stripe.android.StripePaymentController import com.stripe.android.core.exception.StripeException import com.stripe.android.core.networking.ApiRequest +import com.stripe.android.googlepaylauncher.GooglePayLauncherViewModel.Companion.HAS_LAUNCHED_KEY import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmSetupIntentParams import com.stripe.android.model.ConfirmStripeIntentParams @@ -42,6 +44,7 @@ import kotlin.test.assertFailsWith @RunWith(RobolectricTestRunner::class) class GooglePayLauncherViewModelTest { private val stripeRepository = FakeStripeRepository() + private val savedStateHandle = SavedStateHandle() private val googlePayJsonFactory = GooglePayJsonFactory( googlePayConfig = GooglePayConfig( ApiKeyFixtures.FAKE_PUBLISHABLE_KEY, @@ -124,6 +127,17 @@ class GooglePayLauncherViewModelTest { ) } + @Test + fun `hasLaunched is stored in savedStateHandle`() { + val viewModel = createViewModel() + + assertThat(viewModel.hasLaunched).isFalse() + + viewModel.hasLaunched = true + + assertThat(savedStateHandle.get(HAS_LAUNCHED_KEY)).isTrue() + } + @Test fun `getResultFromConfirmation() using PaymentIntent should return expected result`() = runTest { @@ -218,7 +232,8 @@ class GooglePayLauncherViewModelTest { stripeRepository, paymentController, googlePayJsonFactory, - googlePayRepository + googlePayRepository, + savedStateHandle ) private class FakePaymentController : AbsPaymentController() {