Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persist GooglePayLauncherViewModel state #5226

Merged
merged 4 commits into from Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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