Skip to content

fix: prevent crash when Google Pay is presented after activity is backgrounded#2414

Open
simpson-peter wants to merge 1 commit into
stripe:masterfrom
simpson-peter:fix/android-google-pay-lifecycle-crash
Open

fix: prevent crash when Google Pay is presented after activity is backgrounded#2414
simpson-peter wants to merge 1 commit into
stripe:masterfrom
simpson-peter:fix/android-google-pay-lifecycle-crash

Conversation

@simpson-peter
Copy link
Copy Markdown

Summary

This PR adds a lifecycle check to GooglePayLauncherManager.onGooglePayReady which surfaces an error if the lifecycle is not at least STARTED.

I have read through the contribution guidelines and done my best to adhere to them. Please let me know if there are any changes to this PR that you would like to see with respect to the guidelines. Thank you for your time.

Motivation

I have been encountering a crash via the flutter-stripe package which occurs when a google pay transaction is initiated and then the app is briefly (~1 second) backgrounded. I have manually confirmed that this change prevents the crash in the described situation.

Testing

  • I tested this manually
  • I added automated tests

Documentation

Select one:

  • I have added relevant documentation for my changes.
  • This PR does not result in any developer-facing changes.

@simpson-peter simpson-peter requested review from a team as code owners April 27, 2026 18:47
@cla-assistant
Copy link
Copy Markdown

cla-assistant Bot commented Apr 27, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Copy Markdown
Contributor

@jaynewstrom-stripe jaynewstrom-stripe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this fix! The lifecycle check makes sense. I'd suggest adding an automated test to cover this behavior. Here's what I'd recommend:

1. Make onGooglePayReady testable

Change the visibility of onGooglePayReady from private to @VisibleForTesting internal:

import androidx.annotation.VisibleForTesting

@VisibleForTesting
internal fun onGooglePayReady(isReady: Boolean) {

2. Add test file

Create android/src/test/java/com/reactnativestripesdk/GooglePayLauncherManagerTest.kt:

package com.reactnativestripesdk

import androidx.lifecycle.Lifecycle
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableMap
import com.reactnativestripesdk.utils.GooglePayErrorType
import com.reactnativestripesdk.utils.readableMapOf
import com.stripe.android.googlepaylauncher.GooglePayLauncher
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.android.controller.ActivityController
import androidx.fragment.app.FragmentActivity

@RunWith(RobolectricTestRunner::class)
class GooglePayLauncherManagerTest {

  private lateinit var activityController: ActivityController<FragmentActivity>
  private lateinit var context: ReactApplicationContext

  private var callbackResult: GooglePayLauncher.Result? = null
  private var callbackError: WritableMap? = null

  private val callback: (GooglePayLauncher.Result?, WritableMap?) -> Unit = { result, error ->
    callbackResult = result
    callbackError = error
  }

  @Before
  fun setup() {
    activityController = Robolectric.buildActivity(FragmentActivity::class.java)
    callbackResult = null
    callbackError = null
  }

  private fun createManager(activity: FragmentActivity): GooglePayLauncherManager {
    context = mock(ReactApplicationContext::class.java)
    org.mockito.Mockito.`when`(context.currentActivity).thenReturn(activity)

    val googlePayParams = readableMapOf(
      "testEnv" to true,
      "merchantCountryCode" to "US",
      "merchantName" to "Test",
      "currencyCode" to "USD",
    )

    return GooglePayLauncherManager(
      context = context,
      clientSecret = "pi_test_secret",
      mode = GooglePayLauncherManager.Mode.ForPayment,
      googlePayParams = googlePayParams,
      callback = callback,
    )
  }

  @Test
  fun onGooglePayReady_whenLifecycleNotStarted_callsCallbackWithCanceledError() {
    val activity = activityController.create().get()
    // Activity is in CREATED state, not STARTED
    val manager = createManager(activity)

    manager.onGooglePayReady(true)

    assertNull(callbackResult)
    assertNotNull(callbackError)
    val errorMap = callbackError!!.getMap("error")
    assertNotNull(errorMap)
    assertEquals(GooglePayErrorType.Canceled.toString(), errorMap!!.getString("code"))
    assertEquals("Google Pay has been canceled", errorMap.getString("message"))
  }

  @Test
  fun onGooglePayReady_whenNotReady_callsCallbackWithFailedError() {
    val activity = activityController.create().start().get()
    val manager = createManager(activity)

    manager.onGooglePayReady(false)

    assertNull(callbackResult)
    assertNotNull(callbackError)
    val errorMap = callbackError!!.getMap("error")
    assertNotNull(errorMap)
    assertEquals(GooglePayErrorType.Failed.toString(), errorMap!!.getString("code"))
  }
}

The key test (onGooglePayReady_whenLifecycleNotStarted_callsCallbackWithCanceledError) verifies that when the activity is in CREATED state (simulating being backgrounded), the callback receives a canceled error instead of crashing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants