Skip to content
Open
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

This file was deleted.

Copy link
Collaborator

Choose a reason for hiding this comment

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

just an FYI -- I think that if you use the git mv command (e.g. to move BridgeError to BridgeException) then you'll get a much smaller and nicer diff

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.stripe.android.challenge.confirmation

internal class BridgeException(
override val message: String?,
val type: String?,
val code: String?,
override val cause: Throwable? = null,
) : Throwable() {
constructor(cause: Throwable?) : this(
message = cause?.message,
type = null,
code = null,
cause = cause
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package com.stripe.android.challenge.confirmation
internal sealed interface ConfirmationChallengeBridgeEvent {
data object Ready : ConfirmationChallengeBridgeEvent
data class Success(val clientSecret: String) : ConfirmationChallengeBridgeEvent
data class Error(val cause: Throwable) : ConfirmationChallengeBridgeEvent
data class Error(val error: BridgeException) : ConfirmationChallengeBridgeEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ internal class DefaultConfirmationChallengeBridgeHandler @Inject constructor(
UnexpectedErrorEvent.INTENT_CONFIRMATION_CHALLENGE_FAILED_TO_PARSE_SUCCESS_CALLBACK_PARAMS,
stripeException = StripeException.create(error)
)
_event.tryEmit(ConfirmationChallengeBridgeEvent.Error(error))
_event.tryEmit(ConfirmationChallengeBridgeEvent.Error(BridgeException(error)))
}
}

Expand All @@ -65,7 +65,7 @@ internal class DefaultConfirmationChallengeBridgeHandler @Inject constructor(
runCatching {
val jsonObject = JSONObject(errorMessage)
val errorParams = errorParamsParser.parse(jsonObject)
val bridgeError = BridgeError(
val bridgeError = BridgeException(
message = errorParams?.message,
type = errorParams?.type,
code = errorParams?.code
Expand All @@ -77,7 +77,7 @@ internal class DefaultConfirmationChallengeBridgeHandler @Inject constructor(
stripeException = StripeException.create(error)
)
_event.tryEmit(
ConfirmationChallengeBridgeEvent.Error(error)
ConfirmationChallengeBridgeEvent.Error(BridgeException(error))
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal class IntentConfirmationChallengeActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)

listenForActivityResult()
lifecycle.addObserver(viewModel)

setContent {
var showProgressIndicator by remember { mutableStateOf(true) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.stripe.android.challenge.confirmation

import android.app.Application
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
Expand All @@ -9,6 +11,7 @@ import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.stripe.android.challenge.confirmation.analytics.IntentConfirmationChallengeAnalyticsEventReporter
import com.stripe.android.challenge.confirmation.di.DaggerIntentConfirmationChallengeComponent
import com.stripe.android.core.injection.UIContext
import kotlinx.coroutines.flow.Flow
Expand All @@ -21,8 +24,9 @@ import kotlin.coroutines.CoroutineContext

internal class IntentConfirmationChallengeViewModel @Inject constructor(
val bridgeHandler: ConfirmationChallengeBridgeHandler,
@UIContext private val workContext: CoroutineContext
) : ViewModel() {
@UIContext private val workContext: CoroutineContext,
private val analyticsEventReporter: IntentConfirmationChallengeAnalyticsEventReporter
) : ViewModel(), DefaultLifecycleObserver {

private val _bridgeReady = MutableSharedFlow<Unit>()
val bridgeReady: Flow<Unit> = _bridgeReady
Expand All @@ -36,7 +40,17 @@ internal class IntentConfirmationChallengeViewModel @Inject constructor(
}
}

override fun onStart(owner: LifecycleOwner) {
analyticsEventReporter.start()
super.onStart(owner)
}

fun handleWebViewError(error: WebViewError) {
analyticsEventReporter.error(
errorType = error.webViewErrorType,
errorCode = error.errorCode.toString(),
fromBridge = false,
)
viewModelScope.launch {
_result.emit(IntentConfirmationChallengeActivityResult.Failed(error))
}
Expand All @@ -46,19 +60,26 @@ internal class IntentConfirmationChallengeViewModel @Inject constructor(
bridgeHandler.event.collectLatest { event ->
when (event) {
is ConfirmationChallengeBridgeEvent.Ready -> {
analyticsEventReporter.webViewLoaded()
_bridgeReady.emit(Unit)
}
is ConfirmationChallengeBridgeEvent.Success -> {
analyticsEventReporter.success()
_result.emit(
IntentConfirmationChallengeActivityResult.Success(
clientSecret = event.clientSecret
)
)
}
is ConfirmationChallengeBridgeEvent.Error -> {
analyticsEventReporter.error(
errorType = event.error.type,
errorCode = event.error.code,
fromBridge = true
)
_result.emit(
IntentConfirmationChallengeActivityResult.Failed(
error = event.cause
error = event.error
)
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.stripe.android.challenge.confirmation.analytics

import com.stripe.android.core.networking.AnalyticsRequestExecutor
import com.stripe.android.core.networking.AnalyticsRequestFactory
import com.stripe.android.core.utils.DurationProvider
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.DurationUnit

internal class DefaultIntentConfirmationChallengeAnalyticsEventReporter @Inject constructor(
private val analyticsRequestExecutor: AnalyticsRequestExecutor,
private val analyticsRequestFactory: AnalyticsRequestFactory,
private val durationProvider: DurationProvider,
) : IntentConfirmationChallengeAnalyticsEventReporter {

override fun start() {
durationProvider.start(DurationProvider.Key.IntentConfirmationChallenge)
durationProvider.start(DurationProvider.Key.IntentConfirmationChallengeWebViewLoaded)
fireEvent(IntentConfirmationChallengeAnalyticsEvent.Start())
}

override fun success() {
val duration = durationProvider.end(DurationProvider.Key.IntentConfirmationChallenge)
fireEvent(IntentConfirmationChallengeAnalyticsEvent.Success(durationInMs(duration)))
}

override fun error(
errorType: String?,
errorCode: String?,
fromBridge: Boolean
) {
val duration = durationProvider.end(DurationProvider.Key.IntentConfirmationChallenge)
fireEvent(
event = IntentConfirmationChallengeAnalyticsEvent.Error(
duration = durationInMs(duration),
errorType = errorType,
errorCode = errorCode,
fromBridge = fromBridge,
)
)
}

override fun webViewLoaded() {
val duration = durationProvider.end(DurationProvider.Key.IntentConfirmationChallengeWebViewLoaded)
fireEvent(IntentConfirmationChallengeAnalyticsEvent.WebViewLoaded(durationInMs(duration)))
}

private fun fireEvent(event: IntentConfirmationChallengeAnalyticsEvent) {
analyticsRequestExecutor.executeAsync(
analyticsRequestFactory.createRequest(
event = event,
additionalParams = event.params
)
)
}

private fun durationInMs(duration: Duration?) = duration?.toDouble(DurationUnit.MILLISECONDS)?.toFloat() ?: 0f
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.stripe.android.challenge.confirmation.analytics

import com.stripe.android.core.networking.AnalyticsEvent

internal sealed interface IntentConfirmationChallengeAnalyticsEvent : AnalyticsEvent {
val params: Map<String, Any?>

class Start : IntentConfirmationChallengeAnalyticsEvent {
override val params: Map<String, Any?>
get() = emptyMap()
override val eventName = "elements.intent_confirmation_challenge.start"
}

class Success(val duration: Float) : IntentConfirmationChallengeAnalyticsEvent {
override val params: Map<String, Any?>
get() = mapOf(FIELD_DURATION to duration)
override val eventName = "elements.intent_confirmation_challenge.success"
}

class Error(
val duration: Float,
val errorType: String?,
val errorCode: String?,
val fromBridge: Boolean
) : IntentConfirmationChallengeAnalyticsEvent {
override val params: Map<String, Any?>
get() = mapOf(
FIELD_DURATION to duration,
FIELD_ERROR_TYPE to errorType,
FIELD_ERROR_CODE to errorCode,
FIELD_FROM_BRIDGE to fromBridge
)
override val eventName = "elements.intent_confirmation_challenge.error"
}
Comment on lines +20 to +34
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd like to log the error message as well, but there's PII concerns


class WebViewLoaded(val duration: Float) : IntentConfirmationChallengeAnalyticsEvent {
override val params: Map<String, Any?>
get() = mapOf(FIELD_DURATION to duration)
override val eventName = "elements.intent_confirmation_challenge.web_view_loaded"
}

companion object {
internal const val FIELD_DURATION = "duration"
internal const val FIELD_ERROR_TYPE = "error_type"
internal const val FIELD_ERROR_CODE = "error_code"
internal const val FIELD_FROM_BRIDGE = "from_bridge"
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

In EventReporter, we name these functions on<event>, so e.g. start would be onStart. Can you update this to match?

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.stripe.android.challenge.confirmation.analytics

internal interface IntentConfirmationChallengeAnalyticsEventReporter {
fun start()

fun success()

fun error(
errorType: String?,
errorCode: String?,
fromBridge: Boolean
)

fun webViewLoaded()
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import com.stripe.android.challenge.confirmation.BridgeSuccessParamsJsonParser
import com.stripe.android.challenge.confirmation.ConfirmationChallengeBridgeHandler
import com.stripe.android.challenge.confirmation.DefaultConfirmationChallengeBridgeHandler
import com.stripe.android.challenge.confirmation.IntentConfirmationChallengeArgs
import com.stripe.android.challenge.confirmation.analytics.DefaultIntentConfirmationChallengeAnalyticsEventReporter
import com.stripe.android.challenge.confirmation.analytics.IntentConfirmationChallengeAnalyticsEventReporter
import com.stripe.android.core.injection.ENABLE_LOGGING
import com.stripe.android.core.injection.PUBLISHABLE_KEY
import com.stripe.android.core.model.parsers.ModelJsonParser
import com.stripe.android.core.networking.AnalyticsRequestExecutor
import com.stripe.android.core.networking.AnalyticsRequestFactory
import com.stripe.android.core.networking.DefaultAnalyticsRequestExecutor
import com.stripe.android.core.utils.DefaultDurationProvider
import com.stripe.android.core.utils.DurationProvider
import com.stripe.android.networking.PaymentAnalyticsRequestFactory
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.payments.core.analytics.RealErrorReporter
Expand All @@ -22,6 +26,7 @@ import dagger.Binds
import dagger.Module
import dagger.Provides
import javax.inject.Named
import javax.inject.Singleton

@Module
internal interface IntentConfirmationChallengeModule {
Expand All @@ -42,23 +47,34 @@ internal interface IntentConfirmationChallengeModule {
): ModelJsonParser<BridgeErrorParams>

@Binds
fun bindsAnalyticsRequestFactory(
paymentAnalyticsRequestFactory: PaymentAnalyticsRequestFactory
): AnalyticsRequestFactory
fun bindsErrorReporter(errorReporter: RealErrorReporter): ErrorReporter

@Binds
fun bindsErrorReporter(errorReporter: RealErrorReporter): ErrorReporter
fun bindAnalyticsReporter(
analyticsReporter: DefaultIntentConfirmationChallengeAnalyticsEventReporter
): IntentConfirmationChallengeAnalyticsEventReporter

@Binds
fun bindAnalyticsRequestExecutor(
analyticsRequestExecutor: DefaultAnalyticsRequestExecutor
): AnalyticsRequestExecutor

@Binds
fun bindAnalyticsRequestFactory(
paymentAnalyticsRequestFactory: PaymentAnalyticsRequestFactory
): AnalyticsRequestFactory

companion object {
@Provides
@Named(ENABLE_LOGGING)
fun provideEnableLogging(): Boolean = BuildConfig.DEBUG

@Provides
@Singleton
fun provideDurationProvider(): DurationProvider {
return DefaultDurationProvider.instance
}

@Provides
@Named(PUBLISHABLE_KEY)
fun providePublishableKey(args: IntentConfirmationChallengeArgs): () -> String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal class DefaultConfirmationChallengeBridgeHandlerTest {
private val testArgs = IntentConfirmationChallengeArgs(
publishableKey = "pk_test_123",
intent = PaymentIntentFixtures.PI_SUCCEEDED,
productUsage = listOf("PaymentSheet")
productUsage = listOf("PaymentSheet"),
)

@Test
Expand Down Expand Up @@ -115,8 +115,8 @@ internal class DefaultConfirmationChallengeBridgeHandlerTest {
val event = awaitItem()
assertThat(event).isInstanceOf(ConfirmationChallengeBridgeEvent.Error::class.java)
val errorEvent = event as ConfirmationChallengeBridgeEvent.Error
assertThat(errorEvent.cause).isInstanceOf(IllegalArgumentException::class.java)
assertThat(errorEvent.cause.message).isEqualTo("Missing client secret")
assertThat(errorEvent.error.cause).isInstanceOf(IllegalArgumentException::class.java)
assertThat(errorEvent.error.message).isEqualTo("Missing client secret")
}
}

Expand Down Expand Up @@ -146,8 +146,8 @@ internal class DefaultConfirmationChallengeBridgeHandlerTest {
val event = awaitItem()
assertThat(event).isInstanceOf(ConfirmationChallengeBridgeEvent.Error::class.java)
val errorEvent = event as ConfirmationChallengeBridgeEvent.Error
assertThat(errorEvent.cause).isInstanceOf(BridgeError::class.java)
assertThat(errorEvent.cause.message).isEqualTo("Payment declined")
assertThat(errorEvent.error).isInstanceOf(BridgeException::class.java)
assertThat(errorEvent.error.message).isEqualTo("Payment declined")
}
}

Expand All @@ -165,7 +165,7 @@ internal class DefaultConfirmationChallengeBridgeHandlerTest {
val event = awaitItem()
assertThat(event).isInstanceOf(ConfirmationChallengeBridgeEvent.Error::class.java)
val errorEvent = event as ConfirmationChallengeBridgeEvent.Error
assertThat(errorEvent.cause).isEqualTo(parsingException)
assertThat(errorEvent.error.cause).isEqualTo(parsingException)
}
}

Expand All @@ -183,7 +183,7 @@ internal class DefaultConfirmationChallengeBridgeHandlerTest {
val event = awaitItem()
assertThat(event).isInstanceOf(ConfirmationChallengeBridgeEvent.Error::class.java)
val errorEvent = event as ConfirmationChallengeBridgeEvent.Error
assertThat(errorEvent.cause).isEqualTo(parsingException)
assertThat(errorEvent.error.cause).isEqualTo(parsingException)
}
}

Expand Down
Loading
Loading