diff --git a/identity/src/main/java/com/stripe/android/identity/IdentityActivity.kt b/identity/src/main/java/com/stripe/android/identity/IdentityActivity.kt index afaeb320ef3..8ccbaa72149 100644 --- a/identity/src/main/java/com/stripe/android/identity/IdentityActivity.kt +++ b/identity/src/main/java/com/stripe/android/identity/IdentityActivity.kt @@ -227,8 +227,8 @@ internal class IdentityActivity : ) } } - // Display cross icon on error fragment with failed reason, clicking it finishes the flow with Failed - isErrorFragmentWithFailedReason(destination, args) -> { + // Display cross icon on error fragment that should fail, clicking it finishes the flow with Failed + isErrorFragmentThatShouldFail(destination, args) -> { this.navigationIcon = AppCompatResources.getDrawable( this@IdentityActivity, R.drawable.ic_baseline_close_24 @@ -236,7 +236,7 @@ internal class IdentityActivity : this.setNavigationOnClickListener { val failedReason = requireNotNull( args?.getSerializable( - ErrorFragment.ARG_FAILED_REASON + ErrorFragment.ARG_CAUSE ) as? Throwable ) { "Failed to get failedReason from $args" @@ -299,11 +299,11 @@ internal class IdentityActivity : VerificationFlowResult.Canceled ) } - // On error fragment with failed reason, clicking back finishes the flow with Failed - isErrorFragmentWithFailedReason(destination, args) -> { + // On error fragment that should fail, clicking back finishes the flow with Failed + isErrorFragmentThatShouldFail(destination, args) -> { val failedReason = requireNotNull( args?.getSerializable( - ErrorFragment.ARG_FAILED_REASON + ErrorFragment.ARG_CAUSE ) as? Throwable ) { "Failed to get failedReason from $args" @@ -346,10 +346,14 @@ internal class IdentityActivity : private fun isConsentFragment(destination: NavDestination?) = destination?.id == R.id.consentFragment - private fun isErrorFragmentWithFailedReason( + /** + * Check if this is the final error fragment, which would fail the verification flow when + * back button is clicked. + */ + private fun isErrorFragmentThatShouldFail( destination: NavDestination?, args: Bundle? ) = destination?.id == R.id.errorFragment && - args?.containsKey(ErrorFragment.ARG_FAILED_REASON) == true + args?.getBoolean(ErrorFragment.ARG_SHOULD_FAIL, false) == true } } diff --git a/identity/src/main/java/com/stripe/android/identity/analytics/IdentityAnalyticsRequestFactory.kt b/identity/src/main/java/com/stripe/android/identity/analytics/IdentityAnalyticsRequestFactory.kt index 80eea5bb05a..996f2afa778 100644 --- a/identity/src/main/java/com/stripe/android/identity/analytics/IdentityAnalyticsRequestFactory.kt +++ b/identity/src/main/java/com/stripe/android/identity/analytics/IdentityAnalyticsRequestFactory.kt @@ -198,6 +198,17 @@ internal class IdentityAnalyticsRequestFactory @Inject constructor( ) ) + fun genericError( + message: String?, + stackTrace: String + ) = requestFactory.createRequest( + eventName = EVENT_GENERIC_ERROR, + additionalParams = additionalParamWithEventMetadata( + PARAM_MESSAGE to message, + PARAM_STACKTRACE to stackTrace + ) + ) + fun imageUpload( value: Long, compressionQuality: Float, @@ -264,6 +275,7 @@ internal class IdentityAnalyticsRequestFactory @Inject constructor( const val EVENT_MODEL_PERFORMANCE = "model_performance" const val EVENT_TIME_TO_SCREEN = "time_to_screen" const val EVENT_IMAGE_UPLOAD = "image_upload" + const val EVENT_GENERIC_ERROR = "generic_error" const val PARAM_EVENT_META_DATA = "event_metadata" const val PARAM_FROM_FALLBACK_URL = "from_fallback_url" @@ -294,6 +306,7 @@ internal class IdentityAnalyticsRequestFactory @Inject constructor( const val PARAM_NETWORK_TIME = "network_time" const val PARAM_FROM_SCREEN_NAME = "from_screen_name" const val PARAM_TO_SCREEN_NAME = "to_screen_name" + const val PARAM_MESSAGE = "message" const val PARAM_COMPRESSION_QUALITY = "compression_quality" const val PARAM_ID = "id" const val PARAM_FILE_NAME = "file_name" diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/ConfirmationFragment.kt b/identity/src/main/java/com/stripe/android/identity/navigation/ConfirmationFragment.kt index aa8ca67a0a5..98a5134b68b 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/ConfirmationFragment.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/ConfirmationFragment.kt @@ -79,7 +79,7 @@ internal class ConfirmationFragment( }, onFailure = { Log.e(TAG, "Failed to get VerificationPage") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } ) diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/DocSelectionFragment.kt b/identity/src/main/java/com/stripe/android/identity/navigation/DocSelectionFragment.kt index 47933e2b525..cf9e0d7341e 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/DocSelectionFragment.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/DocSelectionFragment.kt @@ -81,7 +81,7 @@ internal class DocSelectionFragment( } }, onFailure = { - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } ) lifecycleScope.launch(identityViewModel.workContext) { @@ -150,8 +150,10 @@ internal class DocSelectionFragment( } } ?: run { // Not possible for backend to send an empty list of allowed types. - Log.e(TAG, "Received an empty idDocumentTypeAllowlist.") - navigateToDefaultErrorFragment() + "Received an empty idDocumentTypeAllowlist.".let { msg -> + Log.e(TAG, msg) + navigateToDefaultErrorFragment(msg) + } } } @@ -252,8 +254,10 @@ internal class DocSelectionFragment( viewLifecycleOwner, onSuccess = { verificationPage -> if (verificationPage.documentCapture.requireLiveCapture) { - Log.e(TAG, "Can't access camera and client has required live capture.") - navigateToDefaultErrorFragment() + "Can't access camera and client has required live capture.".let { msg -> + Log.e(TAG, msg) + navigateToDefaultErrorFragment(msg) + } } else { navigateToUploadFragment( type.toUploadDestinationId(), @@ -263,7 +267,7 @@ internal class DocSelectionFragment( } }, onFailure = { - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } ) } @@ -289,7 +293,7 @@ internal class DocSelectionFragment( }, onFailure = { Log.e(TAG, "failed to observeForVerificationPage: $it") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } ) } diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/ErrorFragment.kt b/identity/src/main/java/com/stripe/android/identity/navigation/ErrorFragment.kt index a33104f3d36..9351e7a4634 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/ErrorFragment.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/ErrorFragment.kt @@ -31,39 +31,44 @@ internal class ErrorFragment( topButton.visibility = View.GONE - if (args.getInt(ARG_GO_BACK_BUTTON_DESTINATION) == UNSET_DESTINATION && - !args.containsKey(ARG_FAILED_REASON) - ) { - bottomButton.visibility = View.GONE - } else { - bottomButton.text = args[ARG_GO_BACK_BUTTON_TEXT] as String - bottomButton.visibility = View.VISIBLE + val cause = requireNotNull(args.getSerializable(ARG_CAUSE) as? Throwable) { + "cause of error is null" + } + + identityViewModel.sendAnalyticsRequest( + identityViewModel.identityAnalyticsRequestFactory.genericError( + message = cause.message, + stackTrace = cause.stackTraceToString() + ) + ) + + bottomButton.text = args[ARG_GO_BACK_BUTTON_TEXT] as String + bottomButton.visibility = View.VISIBLE - // If this is final destination, clicking bottom button and pressBack would end flow - (args.getSerializable(ARG_FAILED_REASON) as? Throwable)?.let { failedReason -> + // If ARG_SHOULD_FAIL is true, clicking bottom button and pressBack would end flow with Failed + if (args.getBoolean(ARG_SHOULD_FAIL, false)) { + identityViewModel.screenTracker.screenTransitionStart( + SCREEN_NAME_ERROR + ) + bottomButton.setOnClickListener { + verificationFlowFinishable.finishWithResult( + Failed(cause) + ) + } + } else { + bottomButton.setOnClickListener { identityViewModel.screenTracker.screenTransitionStart( SCREEN_NAME_ERROR ) - bottomButton.setOnClickListener { - verificationFlowFinishable.finishWithResult( - Failed(failedReason) - ) - } - } ?: run { - bottomButton.setOnClickListener { - identityViewModel.screenTracker.screenTransitionStart( - SCREEN_NAME_ERROR - ) - val destination = args[ARG_GO_BACK_BUTTON_DESTINATION] as Int - if (destination == UNEXPECTED_DESTINATION) { - findNavController().navigate(DEFAULT_BACK_BUTTON_NAVIGATION) - } else { - findNavController().let { navController -> - var shouldContinueNavigateUp = true - while (shouldContinueNavigateUp && navController.currentDestination?.id != destination) { - shouldContinueNavigateUp = - navController.navigateUpAndSetArgForUploadFragment() - } + val destination = args[ARG_GO_BACK_BUTTON_DESTINATION] as Int + if (destination == UNEXPECTED_DESTINATION) { + findNavController().navigate(DEFAULT_BACK_BUTTON_NAVIGATION) + } else { + findNavController().let { navController -> + var shouldContinueNavigateUp = true + while (shouldContinueNavigateUp && navController.currentDestination?.id != destination) { + shouldContinueNavigateUp = + navController.navigateUpAndSetArgForUploadFragment() } } } @@ -78,8 +83,10 @@ internal class ErrorFragment( // if set, shows go_back button, clicking it would navigate to the destination. const val ARG_GO_BACK_BUTTON_TEXT = "goBackButtonText" const val ARG_GO_BACK_BUTTON_DESTINATION = "goBackButtonDestination" - const val ARG_FAILED_REASON = "failedReason" - private const val UNSET_DESTINATION = 0 + + // if set to true, clicking bottom button and pressBack would end flow with Failed + const val ARG_SHOULD_FAIL = "shouldFail" + const val ARG_CAUSE = "cause" // Indicates the server returns a requirementError that doesn't match with current Fragment. // E.g ConsentFragment->DocSelectFragment could only have BIOMETRICCONSENT error but not IDDOCUMENTFRONT error. @@ -104,21 +111,26 @@ internal class ErrorFragment( } else { UNEXPECTED_DESTINATION }, - ARG_GO_BACK_BUTTON_TEXT to requirementError.backButtonText - // TODO(ccen) build continue button after backend behavior is finalized - // ARG_CONTINUE_BUTTON_TEXT to requirementError.continueButtonText, + ARG_GO_BACK_BUTTON_TEXT to requirementError.backButtonText, + ARG_SHOULD_FAIL to false, + ARG_CAUSE to IllegalStateException("VerificationPageDataRequirementError: $requirementError") ) ) } - fun NavController.navigateToErrorFragmentWithDefaultValues(context: Context) { + fun NavController.navigateToErrorFragmentWithDefaultValues( + context: Context, + cause: Throwable + ) { navigate( R.id.action_global_errorFragment, bundleOf( ARG_ERROR_TITLE to context.getString(R.string.error), ARG_ERROR_CONTENT to context.getString(R.string.unexpected_error_try_again), ARG_GO_BACK_BUTTON_DESTINATION to R.id.consentFragment, - ARG_GO_BACK_BUTTON_TEXT to context.getString(R.string.go_back) + ARG_GO_BACK_BUTTON_TEXT to context.getString(R.string.go_back), + ARG_SHOULD_FAIL to false, + ARG_CAUSE to cause ) ) } @@ -138,7 +150,8 @@ internal class ErrorFragment( ARG_ERROR_TITLE to context.getString(R.string.error), ARG_ERROR_CONTENT to context.getString(R.string.unexpected_error_try_again), ARG_GO_BACK_BUTTON_TEXT to context.getString(R.string.go_back), - ARG_FAILED_REASON to failedReason + ARG_SHOULD_FAIL to true, + ARG_CAUSE to failedReason ) ) } diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/IdentityCameraScanFragment.kt b/identity/src/main/java/com/stripe/android/identity/navigation/IdentityCameraScanFragment.kt index 28e0f554883..66cda88a211 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/IdentityCameraScanFragment.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/IdentityCameraScanFragment.kt @@ -139,7 +139,7 @@ internal abstract class IdentityCameraScanFragment( }, onFailure = { Log.e(TAG, "Fail to observeForVerificationPage: $it") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } ) stopScanning() diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/IdentityDocumentScanFragment.kt b/identity/src/main/java/com/stripe/android/identity/navigation/IdentityDocumentScanFragment.kt index 3929ec20747..3b61097322b 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/IdentityDocumentScanFragment.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/IdentityDocumentScanFragment.kt @@ -160,7 +160,7 @@ internal abstract class IdentityDocumentScanFragment( when { it.hasError() -> { Log.e(TAG, "Fail to upload files: ${it.getError()}") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it.getError()) } it.isAnyLoading() -> { continueButton.toggleToLoading() @@ -207,22 +207,21 @@ internal abstract class IdentityDocumentScanFragment( TAG, "fail to submit uploaded files: $throwable" ) - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(throwable) } } }, onFailure = { throwable -> Log.e(TAG, "Fail to observeForVerificationPage: $throwable") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(throwable) } ) } else -> { - Log.e( - TAG, - "observeAndUploadForBothSides reaches unexpected upload state: $it" - ) - navigateToDefaultErrorFragment() + "observeAndUploadForBothSides reaches unexpected upload state: $it".let { msg -> + Log.e(TAG, msg) + navigateToDefaultErrorFragment(msg) + } } } } diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/IdentityUploadFragment.kt b/identity/src/main/java/com/stripe/android/identity/navigation/IdentityUploadFragment.kt index f32aa10d45f..22c1176bd28 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/IdentityUploadFragment.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/IdentityUploadFragment.kt @@ -206,7 +206,7 @@ internal abstract class IdentityUploadFragment( identityViewModel.documentUploadState.collectLatest { latestState -> if (latestState.hasError()) { Log.e(TAG, "Fail to upload files: ${latestState.getError()}") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(latestState.getError()) } else { if (latestState.isFrontHighResUploaded()) { showFrontDone(latestState) @@ -315,7 +315,7 @@ internal abstract class IdentityUploadFragment( onSuccess(it.documentCapture) }, onFailure = { - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } ) } @@ -394,7 +394,7 @@ internal abstract class IdentityUploadFragment( ) }.onFailure { Log.d(TAG, "fail to submit uploaded files: $it") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } } } @@ -425,7 +425,7 @@ internal abstract class IdentityUploadFragment( }, onFailure = { Log.e(TAG, "Fail to observeForVerificationPage: $it") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } ) } diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/PassportScanFragment.kt b/identity/src/main/java/com/stripe/android/identity/navigation/PassportScanFragment.kt index 62948d94860..38170e58de7 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/PassportScanFragment.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/PassportScanFragment.kt @@ -55,7 +55,7 @@ internal class PassportScanFragment( identityViewModel.documentUploadState.collectLatest { when { it.hasError() -> { - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it.getError()) } it.isFrontLoading() -> { continueButton.toggleToLoading() @@ -98,22 +98,21 @@ internal class PassportScanFragment( TAG, "fail to submit uploaded files: $throwable" ) - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(throwable) } } }, onFailure = { throwable -> Log.e(TAG, "Fail to observeForVerificationPage: $throwable") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(throwable) } ) } else -> { - Log.d( - TAG, - "observeAndUploadForFrontSide reaches unexpected upload state: $it" - ) - navigateToDefaultErrorFragment() + "observeAndUploadForFrontSide reaches unexpected upload state: $it".let { msg -> + Log.d(TAG, msg) + navigateToDefaultErrorFragment(msg) + } } } } diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/PassportUploadFragment.kt b/identity/src/main/java/com/stripe/android/identity/navigation/PassportUploadFragment.kt index 7038d741b9c..a3d00a78f11 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/PassportUploadFragment.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/PassportUploadFragment.kt @@ -43,7 +43,7 @@ internal open class PassportUploadFragment( ) }.onFailure { Log.d(TAG, "fail to submit uploaded files: $it") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } } } diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/SelfieFragment.kt b/identity/src/main/java/com/stripe/android/identity/navigation/SelfieFragment.kt index fd8b4b3091b..a02f0ee7f41 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/SelfieFragment.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/SelfieFragment.kt @@ -198,8 +198,10 @@ internal class SelfieFragment( identityViewModel.selfieUploadState.collectLatest { when { it.hasError() -> { - Log.e(TAG, "Fail to upload files: ${it.getError()}") - navigateToDefaultErrorFragment() + "Fail to upload files: ${it.getError()}".let { msg -> + Log.e(TAG, msg) + navigateToDefaultErrorFragment(msg) + } } it.isAnyLoading() -> { continueButton.toggleToLoading() @@ -234,15 +236,14 @@ internal class SelfieFragment( TAG, "fail to submit uploaded files: $throwable" ) - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(throwable) } } else -> { - Log.e( - TAG, - "collectUploadedStateAndUploadForCollectedSelfies reaches unexpected upload state: $it" - ) - navigateToDefaultErrorFragment() + "collectUploadedStateAndUploadForCollectedSelfies reaches unexpected upload state: $it".let { msg -> + Log.e(TAG, msg) + navigateToDefaultErrorFragment(msg) + } } } } diff --git a/identity/src/main/java/com/stripe/android/identity/utils/NavigationUtils.kt b/identity/src/main/java/com/stripe/android/identity/utils/NavigationUtils.kt index 98109bb334b..0d79e4d04f8 100644 --- a/identity/src/main/java/com/stripe/android/identity/utils/NavigationUtils.kt +++ b/identity/src/main/java/com/stripe/android/identity/utils/NavigationUtils.kt @@ -85,14 +85,16 @@ internal suspend fun Fragment.postVerificationPageDataAndMaybeSubmit( .navigate(R.id.action_global_confirmationFragment) } else -> { - Log.e(TAG, "VerificationPage submit failed") - navigateToDefaultErrorFragment() + "VerificationPage submit failed".let { msg -> + Log.e(TAG, msg) + navigateToDefaultErrorFragment(msg) + } } } }, onFailure = { Log.e(TAG, "Failed to postVerificationPageSubmit: $it") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } ) } @@ -100,7 +102,7 @@ internal suspend fun Fragment.postVerificationPageDataAndMaybeSubmit( }, onFailure = { Log.e(TAG, "Failed to postVerificationPageData: $it") - navigateToDefaultErrorFragment() + navigateToDefaultErrorFragment(it) } ) } @@ -120,10 +122,20 @@ private fun Fragment.navigateToRequirementErrorFragment( } /** - * Navigate to [ErrorFragment] with default values. + * Navigate to [ErrorFragment] with default values and a cause. */ -internal fun Fragment.navigateToDefaultErrorFragment() { - findNavController().navigateToErrorFragmentWithDefaultValues(requireContext()) +internal fun Fragment.navigateToDefaultErrorFragment(cause: Throwable) { + findNavController().navigateToErrorFragmentWithDefaultValues(requireContext(), cause) +} + +/** + * Navigate to [ErrorFragment] with default values and a message. + */ +internal fun Fragment.navigateToDefaultErrorFragment(message: String) { + findNavController().navigateToErrorFragmentWithDefaultValues( + requireContext(), + IllegalStateException(message) + ) } /** diff --git a/identity/src/main/java/com/stripe/android/identity/viewmodel/IdentityViewModel.kt b/identity/src/main/java/com/stripe/android/identity/viewmodel/IdentityViewModel.kt index 299ee9af902..707cd9e95b2 100644 --- a/identity/src/main/java/com/stripe/android/identity/viewmodel/IdentityViewModel.kt +++ b/identity/src/main/java/com/stripe/android/identity/viewmodel/IdentityViewModel.kt @@ -594,7 +594,7 @@ internal class IdentityViewModel @Inject constructor( fun observeForVerificationPage( owner: LifecycleOwner, onSuccess: (VerificationPage) -> Unit, - onFailure: (Throwable?) -> Unit + onFailure: (Throwable) -> Unit ) { verificationPage.observe(owner) { resource -> when (resource.status) { @@ -603,7 +603,7 @@ internal class IdentityViewModel @Inject constructor( } Status.ERROR -> { Log.e(TAG, "Fail to get VerificationPage") - onFailure(resource.throwable) + onFailure(requireNotNull(resource.throwable)) } Status.LOADING -> {} // no-op } @@ -641,13 +641,18 @@ internal class IdentityViewModel @Inject constructor( } }, onFailure = { - _verificationPage.postValue( - Resource.error( - "Failed to retrieve verification page with " + - "sessionID: ${verificationArgs.verificationSessionId} and ephemeralKey: ${verificationArgs.ephemeralKeySecret}", - it - ) - ) + "Failed to retrieve verification page with " + + ( + "sessionID: ${verificationArgs.verificationSessionId} and ephemeralKey: " + + "${verificationArgs.ephemeralKeySecret}" + ).let { msg -> + _verificationPage.postValue( + Resource.error( + msg, + IllegalStateException(msg, it) + ) + ) + } } ) } diff --git a/identity/src/test/java/com/stripe/android/identity/IdentityActivityTest.kt b/identity/src/test/java/com/stripe/android/identity/IdentityActivityTest.kt index a563e11351e..c26af327d04 100644 --- a/identity/src/test/java/com/stripe/android/identity/IdentityActivityTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/IdentityActivityTest.kt @@ -161,7 +161,8 @@ internal class IdentityActivityTest { navController.navigate( R.id.errorFragment, bundleOf( - ErrorFragment.ARG_FAILED_REASON to failedReason + ErrorFragment.ARG_SHOULD_FAIL to true, + ErrorFragment.ARG_CAUSE to failedReason ) ) @@ -204,7 +205,8 @@ internal class IdentityActivityTest { navController.navigate( R.id.errorFragment, bundleOf( - ErrorFragment.ARG_FAILED_REASON to failedReason + ErrorFragment.ARG_SHOULD_FAIL to true, + ErrorFragment.ARG_CAUSE to failedReason ) ) diff --git a/identity/src/test/java/com/stripe/android/identity/navigation/ConfirmationFragmentTest.kt b/identity/src/test/java/com/stripe/android/identity/navigation/ConfirmationFragmentTest.kt index c1410f0659a..9c225460220 100644 --- a/identity/src/test/java/com/stripe/android/identity/navigation/ConfirmationFragmentTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/navigation/ConfirmationFragmentTest.kt @@ -49,6 +49,8 @@ class ConfirmationFragmentTest { ) on { uiContext } doReturn testDispatcher on { workContext } doReturn testDispatcher + on { screenTracker } doReturn mock() + on { analyticsState } doReturn mock() } private val verificationPage = mock().also { @@ -83,7 +85,7 @@ class ConfirmationFragmentTest { any(), failureCaptor.capture() ) - failureCaptor.firstValue(null) + failureCaptor.firstValue(mock()) } @Test diff --git a/identity/src/test/java/com/stripe/android/identity/navigation/ConsentFragmentTest.kt b/identity/src/test/java/com/stripe/android/identity/navigation/ConsentFragmentTest.kt index bb558fdec83..eb69c50af90 100644 --- a/identity/src/test/java/com/stripe/android/identity/navigation/ConsentFragmentTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/navigation/ConsentFragmentTest.kt @@ -176,7 +176,7 @@ internal class ConsentFragmentTest { any(), failureCaptor.capture() ) - failureCaptor.firstValue(null) + failureCaptor.firstValue(mock()) } private fun setUpSuccessVerificationPage( diff --git a/identity/src/test/java/com/stripe/android/identity/navigation/DocSelectionFragmentTest.kt b/identity/src/test/java/com/stripe/android/identity/navigation/DocSelectionFragmentTest.kt index c8632c694e2..d40c191ec40 100644 --- a/identity/src/test/java/com/stripe/android/identity/navigation/DocSelectionFragmentTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/navigation/DocSelectionFragmentTest.kt @@ -88,7 +88,7 @@ internal class DocSelectionFragmentTest { any(), failureCaptor.capture() ) - failureCaptor.firstValue(null) + failureCaptor.firstValue(mock()) } private fun setUpSuccessVerificationPage(times: Int = 1) { diff --git a/identity/src/test/java/com/stripe/android/identity/navigation/DriverLicenseScanFragmentTest.kt b/identity/src/test/java/com/stripe/android/identity/navigation/DriverLicenseScanFragmentTest.kt index 0d1099450c3..be93275fc76 100644 --- a/identity/src/test/java/com/stripe/android/identity/navigation/DriverLicenseScanFragmentTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/navigation/DriverLicenseScanFragmentTest.kt @@ -103,6 +103,7 @@ internal class DriverLicenseScanFragmentTest { private val errorDocumentUploadState = mock { on { hasError() } doReturn true + on { getError() } doReturn mock() } private val anyLoadingDocumentUploadState = mock { diff --git a/identity/src/test/java/com/stripe/android/identity/navigation/ErrorFragmentTest.kt b/identity/src/test/java/com/stripe/android/identity/navigation/ErrorFragmentTest.kt index ec67a674588..48407579be1 100644 --- a/identity/src/test/java/com/stripe/android/identity/navigation/ErrorFragmentTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/navigation/ErrorFragmentTest.kt @@ -11,9 +11,12 @@ import com.stripe.android.identity.IdentityVerificationSheet.VerificationFlowRes import com.stripe.android.identity.R import com.stripe.android.identity.VerificationFlowFinishable import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory +import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory.Companion.EVENT_GENERIC_ERROR import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory.Companion.EVENT_SCREEN_PRESENTED import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory.Companion.PARAM_EVENT_META_DATA +import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory.Companion.PARAM_MESSAGE import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory.Companion.PARAM_SCREEN_NAME +import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory.Companion.PARAM_STACKTRACE import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory.Companion.SCREEN_NAME_ERROR import com.stripe.android.identity.analytics.ScreenTracker import com.stripe.android.identity.databinding.BaseErrorFragmentBinding @@ -71,16 +74,6 @@ class ErrorFragmentTest { } } - @Test - fun `bottom button is hidden correctly when not set`() { - launchErrorFragment().onFragment { - val binding = BaseErrorFragmentBinding.bind(it.requireView()) - - assertThat(binding.topButton.visibility).isEqualTo(View.GONE) - assertThat(binding.bottomButton.visibility).isEqualTo(View.GONE) - } - } - @Test fun `bottom button is set correctly when set`() { launchErrorFragment(ErrorFragment.UNEXPECTED_DESTINATION).onFragment { @@ -213,16 +206,25 @@ class ErrorFragmentTest { ) = launchFragmentInContainer( bundleOf( ErrorFragment.ARG_ERROR_TITLE to TEST_ERROR_TITLE, - ErrorFragment.ARG_ERROR_CONTENT to TEST_ERROR_CONTENT + ErrorFragment.ARG_ERROR_CONTENT to TEST_ERROR_CONTENT, + ErrorFragment.ARG_CAUSE to TEST_CAUSE, + ErrorFragment.ARG_GO_BACK_BUTTON_TEXT to TEST_GO_BACK_BUTTON_TEXT ).also { bundle -> navigationDestination?.let { bundle.putInt(ErrorFragment.ARG_GO_BACK_BUTTON_DESTINATION, navigationDestination) - bundle.putString(ErrorFragment.ARG_GO_BACK_BUTTON_TEXT, TEST_GO_BACK_BUTTON_TEXT) } }, themeResId = R.style.Theme_MaterialComponents ) { ErrorFragment(mock(), viewModelFactoryFor(mockIdentityViewModel)) + }.onFragment { + verify(mockIdentityViewModel).sendAnalyticsRequest( + argThat { + eventName == EVENT_GENERIC_ERROR && + (params[PARAM_EVENT_META_DATA] as Map<*, *>)[PARAM_MESSAGE] == TEST_CAUSE.message && + (params[PARAM_EVENT_META_DATA] as Map<*, *>)[PARAM_STACKTRACE] == TEST_CAUSE.stackTraceToString() + } + ) } private fun launchErrorFragmentWithFailedReason( @@ -232,7 +234,8 @@ class ErrorFragmentTest { ErrorFragment.ARG_ERROR_TITLE to TEST_ERROR_TITLE, ErrorFragment.ARG_ERROR_CONTENT to TEST_ERROR_CONTENT, ErrorFragment.ARG_GO_BACK_BUTTON_TEXT to TEST_GO_BACK_BUTTON_TEXT, - ErrorFragment.ARG_FAILED_REASON to throwable + ErrorFragment.ARG_SHOULD_FAIL to true, + ErrorFragment.ARG_CAUSE to throwable ), themeResId = R.style.Theme_MaterialComponents ) { @@ -243,5 +246,6 @@ class ErrorFragmentTest { const val TEST_ERROR_TITLE = "test error title" const val TEST_ERROR_CONTENT = "test error content" const val TEST_GO_BACK_BUTTON_TEXT = "go back" + val TEST_CAUSE = IllegalStateException("error message") } } diff --git a/identity/src/test/java/com/stripe/android/identity/navigation/IDScanFragmentTest.kt b/identity/src/test/java/com/stripe/android/identity/navigation/IDScanFragmentTest.kt index 7d4dd55a2d8..b47d6e63ce0 100644 --- a/identity/src/test/java/com/stripe/android/identity/navigation/IDScanFragmentTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/navigation/IDScanFragmentTest.kt @@ -108,6 +108,7 @@ internal class IDScanFragmentTest { private val errorDocumentUploadState = mock { on { hasError() } doReturn true + on { getError() } doReturn mock() } private val anyLoadingDocumentUploadState = mock { diff --git a/identity/src/test/java/com/stripe/android/identity/navigation/IdentityUploadFragmentTest.kt b/identity/src/test/java/com/stripe/android/identity/navigation/IdentityUploadFragmentTest.kt index 3fd41b06d6f..a92eedee6ca 100644 --- a/identity/src/test/java/com/stripe/android/identity/navigation/IdentityUploadFragmentTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/navigation/IdentityUploadFragmentTest.kt @@ -92,6 +92,7 @@ class IdentityUploadFragmentTest { private val errorDocumentUploadState = mock { on { hasError() } doReturn true + on { getError() } doReturn mock() } private val mockScreenTracker = mock() diff --git a/identity/src/test/java/com/stripe/android/identity/navigation/PassportScanFragmentTest.kt b/identity/src/test/java/com/stripe/android/identity/navigation/PassportScanFragmentTest.kt index 3cb9ac5e863..34b3ca68fba 100644 --- a/identity/src/test/java/com/stripe/android/identity/navigation/PassportScanFragmentTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/navigation/PassportScanFragmentTest.kt @@ -102,6 +102,7 @@ class PassportScanFragmentTest { private val errorDocumentUploadState = mock { on { hasError() } doReturn true + on { getError() } doReturn mock() } private val frontLoadingDocumentUploadState = mock { diff --git a/identity/src/test/java/com/stripe/android/identity/navigation/PassportUploadFragmentTest.kt b/identity/src/test/java/com/stripe/android/identity/navigation/PassportUploadFragmentTest.kt index 8271ba7d055..3aeab5b71eb 100644 --- a/identity/src/test/java/com/stripe/android/identity/navigation/PassportUploadFragmentTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/navigation/PassportUploadFragmentTest.kt @@ -86,6 +86,7 @@ class PassportUploadFragmentTest { private val errorDocumentUploadState = mock { on { hasError() } doReturn true + on { getError() } doReturn mock() } private val mockScreenTracker = mock()