Skip to content

Commit

Permalink
Persist GooglePayLauncherViewModel state (#5226)
Browse files Browse the repository at this point in the history
  • Loading branch information
brnunes-stripe committed Jun 30, 2022
1 parent 74f4b15 commit 544fda1
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 28 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Expand Up @@ -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.
Expand Down
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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() {
Expand Down Expand Up @@ -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"
}
}
Expand Up @@ -35,7 +35,8 @@ internal class GooglePayLauncherActivity : AppCompatActivity() {
private val viewModel: GooglePayLauncherViewModel by viewModels {
GooglePayLauncherViewModel.Factory(
application,
args
args,
this
)
}

Expand Down Expand Up @@ -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(
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -35,16 +38,25 @@ 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,
private val args: GooglePayLauncherContract.Args,
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<Boolean>(HAS_LAUNCHED_KEY) == true
set(value) = savedStateHandle.set(HAS_LAUNCHED_KEY, value)

private val _googleResult = MutableLiveData<GooglePayLauncher.Result>()
internal val googlePayResult = _googleResult.distinctUntilChanged()
Expand Down Expand Up @@ -209,11 +221,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 <T : ViewModel> create(modelClass: Class<T>): T {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
val googlePayEnvironment = args.config.environment
val logger = Logger.getInstance(enableLogging)

Expand Down Expand Up @@ -265,8 +283,14 @@ internal class GooglePayLauncherViewModel(
googlePayConfig = GooglePayConfig(publishableKey, stripeAccountId),
isJcbEnabled = args.config.isJcbEnabled
),
googlePayRepository
googlePayRepository,
handle
) as T
}
}

companion object {
@VisibleForTesting
const val HAS_LAUNCHED_KEY = "has_launched"
}
}
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Boolean>(HAS_LAUNCHED_KEY)).isTrue()
}

@Test
fun `getResultFromConfirmation() using PaymentIntent should return expected result`() =
runTest {
Expand Down Expand Up @@ -218,7 +232,8 @@ class GooglePayLauncherViewModelTest {
stripeRepository,
paymentController,
googlePayJsonFactory,
googlePayRepository
googlePayRepository,
savedStateHandle
)

private class FakePaymentController : AbsPaymentController() {
Expand Down

0 comments on commit 544fda1

Please sign in to comment.