Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ internal class CardScanGoogleLauncher @VisibleForTesting constructor(
private val _loadingState = MutableStateFlow(CardScanLoadingState.Loading)
override val loadingState: StateFlow<CardScanLoadingState> = _loadingState.asStateFlow()

@VisibleForTesting
lateinit var activityLauncher: ActivityResultLauncher<IntentSenderRequest>
private var deferredIntentSenderRequest: IntentSenderRequest? = null

private lateinit var activityLauncher: ActivityResultLauncher<IntentSenderRequest>

init {
paymentCardRecognitionClient.fetchIntent(
Expand All @@ -55,15 +56,37 @@ internal class CardScanGoogleLauncher @VisibleForTesting constructor(
paymentCardRecognitionClient.fetchIntent(
context = context,
onFailure = { e ->
_isLaunching = false
eventsReporter.onCardScanFailed(implementation, e)
},
onSuccess = { intentSenderRequest ->
eventsReporter.onCardScanStarted("google_pay")
activityLauncher.launch(intentSenderRequest, options)
launchIntentSenderOrDefer(intentSenderRequest)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not a blocker, but I wonder if we could fix this with coroutines. We'd pass CoroutineScope from rememberCardScanGoogleLauncher to this class and await an activityLauncher on the main dispatcher

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

In general, we should make this request in the view model scope rather than the composition scope, something I'll look to fix separately.

}
)
}

fun setLauncher(launcher: ActivityResultLauncher<IntentSenderRequest>) {
activityLauncher = launcher

val pending = deferredIntentSenderRequest ?: return

try {
launcher.launch(pending, options)
deferredIntentSenderRequest = null
} catch (_: IllegalStateException) {
_isLaunching = false
}
}

private fun launchIntentSenderOrDefer(intentSenderRequest: IntentSenderRequest) {
try {
activityLauncher.launch(intentSenderRequest, options)
} catch (_: IllegalStateException) {
deferredIntentSenderRequest = intentSenderRequest
}
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

There are a ton of other issues with this scanner is built that I need to be resolved. A lot of the work (fetching intent, checking availability) is being done in the composition scope of the Scan card button that should not be happening.

This PR fixes the initial crash but a good amount of follow-up work should be done. I'll open up tickets for these.


internal fun parseActivityResult(result: ActivityResult): CardScanResult {
val scanResult = when {
result.resultCode == Activity.RESULT_OK && result.data != null -> {
Expand Down Expand Up @@ -125,7 +148,7 @@ internal class CardScanGoogleLauncher @VisibleForTesting constructor(
onResult(launcher.parseActivityResult(result))
}
return remember(activityLauncher) {
launcher.apply { this.activityLauncher = activityLauncher }
launcher.apply { setLauncher(activityLauncher) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,36 @@ class CardScanGoogleLauncherTest {
assertThat(fakeEventsReporter.scanStartedCalls.awaitItem().implementation).isEqualTo("google_pay")
}

@Test
fun `deferred intent launches when setLauncher is called after unregistered launcher failure`() = runTest {
val failingLauncher = FakeActivityLauncher<IntentSenderRequest>(
throwOnNextLaunch = IllegalStateException(
"Attempting to launch an unregistered ActivityResultLauncher"
)
)
val succeedingLauncher = FakeActivityLauncher<IntentSenderRequest>()
val fakeEventsReporter = FakeCardScanEventsReporter()
val launcher = CardScanGoogleLauncher(
context = ApplicationProvider.getApplicationContext(),
eventsReporter = fakeEventsReporter,
options = null,
paymentCardRecognitionClient = FakePaymentCardRecognitionClient(true)
)
launcher.setLauncher(failingLauncher)
launcher.launch(ApplicationProvider.getApplicationContext())
assertThat(failingLauncher.launchCall.awaitItem()).isEqualTo(Unit)

launcher.setLauncher(succeedingLauncher)
assertThat(succeedingLauncher.launchCall.awaitItem()).isEqualTo(Unit)

assertThat(fakeEventsReporter.apiCheckSucceededCalls.awaitItem()).isNotNull()
assertThat(fakeEventsReporter.scanStartedCalls.awaitItem().implementation).isEqualTo("google_pay")

failingLauncher.validate()
succeedingLauncher.validate()
fakeEventsReporter.validate()
}

@Test
fun `card scan launcher should not be available when fetchIntent fails`() = runScenario(
cardScanLoadingState = CardScanLoadingState.Unavailable,
Expand Down Expand Up @@ -172,7 +202,7 @@ class CardScanGoogleLauncherTest {
options = null,
paymentCardRecognitionClient = FakePaymentCardRecognitionClient(cardScanLoadingState)
).apply {
this.activityLauncher = activityLauncher
setLauncher(activityLauncher)
}

val scenario = Scenario(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ import androidx.core.app.ActivityOptionsCompat
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.Turbine

internal class FakeActivityLauncher<I> : ActivityResultLauncher<I>() {
internal class FakeActivityLauncher<I>(
private var throwOnNextLaunch: Throwable? = null,
) : ActivityResultLauncher<I>() {
private val _launchCall = Turbine<Unit>()
val launchCall: ReceiveTurbine<Unit> = _launchCall
override val contract: ActivityResultContract<I, *>
get() = throw NotImplementedError("Not implemented!")

override fun launch(input: I, options: ActivityOptionsCompat?) {
_launchCall.add(Unit)

throwOnNextLaunch?.let { error ->
throwOnNextLaunch = null
throw error
}
}

override fun unregister() {
Expand Down
Loading