Skip to content

Commit

Permalink
[Identity] Show alert dialog to cancel when falling back to document …
Browse files Browse the repository at this point in the history
…consent (#7051)
  • Loading branch information
ccen-stripe authored Jul 24, 2023
1 parent 39388dd commit 08e9164
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 8 deletions.
2 changes: 2 additions & 0 deletions identity/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<ID>LongMethod:IDNumberSection.kt$@Composable internal fun IDNumberSection( enabled: Boolean, idNumberCountries: List&lt;Country>, countryNotListedText: String, navController: NavController, onIdNumberCollected: (Resource&lt;IdNumberParam>) -> Unit )</ID>
<ID>LongMethod:IdentityActivity.kt$IdentityActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
<ID>LongMethod:IdentityNavGraph.kt$@Composable internal fun IdentityNavGraph( navController: NavHostController = rememberNavController(), identityViewModel: IdentityViewModel, fallbackUrlLauncher: FallbackUrlLauncher, appSettingsOpenable: AppSettingsOpenable, cameraPermissionEnsureable: CameraPermissionEnsureable, verificationFlowFinishable: VerificationFlowFinishable, identityScanViewModelFactory: IdentityScanViewModel.IdentityScanViewModelFactory, onTopBarNavigationClick: () -> Unit, topBarState: IdentityTopBarState, onNavControllerCreated: (NavController) -> Unit )</ID>
<ID>LongMethod:IdentityOnBackPressedHandler.kt$IdentityOnBackPressedHandler$override fun handleOnBackPressed()</ID>
<ID>LongMethod:IdentityViewModel.kt$IdentityViewModel$internal fun uploadScanResult( result: IdentityAggregator.FinalResult, verificationPage: VerificationPage, targetScanType: IdentityScanState.ScanType? )</ID>
<ID>LongMethod:IdentityViewModel.kt$IdentityViewModel$private fun uploadDocumentImagesAndNotify( imageFile: File, filePurpose: StripeFilePurpose, uploadMethod: UploadMethod, scores: List&lt;Float>? = null, isHighRes: Boolean, isFront: Boolean, scanType: IdentityScanState.ScanType, compressionQuality: Float )</ID>
<ID>LongMethod:IdentityViewModel.kt$IdentityViewModel$suspend fun postVerificationPageDataForDocSelection( type: CollectedDataParam.Type, navController: NavController, viewLifecycleOwner: LifecycleOwner, cameraPermissionEnsureable: CameraPermissionEnsureable )</ID>
Expand Down Expand Up @@ -56,6 +57,7 @@
<ID>MagicNumber:IDNumberSection.kt$USIDConfig.&lt;no name provided>$4</ID>
<ID>MagicNumber:OTPScreen.kt$4</ID>
<ID>MagicNumber:RoundToMaxDecimals.kt$10</ID>
<ID>MatchingDeclarationName:AlertUtils.kt$AlertButton</ID>
<ID>MatchingDeclarationName:ComposeLoadingButton.kt$LoadingButtonState</ID>
<ID>MaxLineLength:DefaultIdentityRepositoryTest.kt$DefaultIdentityRepositoryTest$fun</ID>
<ID>MaxLineLength:IDDetectorTransitionerTest.kt$IDDetectorTransitionerTest$fun</ID>
Expand Down
4 changes: 4 additions & 0 deletions identity/res/values/totranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@

<string name="stripe_upload_a_photo">Upload a photo</string>
<string name="stripe_upload_your_photo_id">Upload your photo ID</string>

<string name="stripe_identity_confirm_cancel">Are you sure you want to cancel?</string>
<string name="stripe_identity_yes">Yes</string>
<string name="stripe_identity_no">No</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -38,6 +37,8 @@ import com.stripe.android.identity.navigation.IndividualWelcomeDestination
import com.stripe.android.identity.navigation.navigateToFinalErrorScreen
import com.stripe.android.identity.ui.IdentityTheme
import com.stripe.android.identity.ui.IdentityTopBarState
import com.stripe.android.identity.utils.AlertButton
import com.stripe.android.identity.utils.showAlertDialog
import com.stripe.android.identity.viewmodel.IdentityViewModel
import javax.inject.Inject
import javax.inject.Provider
Expand Down Expand Up @@ -195,6 +196,7 @@ internal class IdentityActivity :
this.navController = it
onBackPressedCallback =
IdentityOnBackPressedHandler(
this,
this,
navController,
identityViewModel
Expand Down Expand Up @@ -230,12 +232,13 @@ internal class IdentityActivity :
* [CameraPermissionCheckingActivity.requestCameraPermission].
*/
override fun showPermissionRationaleDialog() {
val builder = AlertDialog.Builder(this)
builder.setMessage(R.string.stripe_camera_permission_rationale)
.setPositiveButton(R.string.stripe_ok) { _, _ ->
showAlertDialog(
this,
R.string.stripe_camera_permission_rationale,
positiveButton = AlertButton(R.string.stripe_ok) { _, _ ->
requestCameraPermission()
}
builder.show()
)
}

// This should have neve been invoked as PERMISSION_RATIONALE_SHOWN is never written.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.identity

import android.content.Context
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.navigation.NavController
Expand All @@ -13,12 +14,15 @@ import com.stripe.android.identity.navigation.InitialLoadingDestination
import com.stripe.android.identity.navigation.clearDataAndNavigateUp
import com.stripe.android.identity.navigation.routeToScreenName
import com.stripe.android.identity.networking.models.VerificationPage.Companion.requireSelfie
import com.stripe.android.identity.utils.AlertButton
import com.stripe.android.identity.utils.showAlertDialog
import com.stripe.android.identity.viewmodel.IdentityViewModel

/**
* Handles back button behavior based on current navigation status.
*/
internal class IdentityOnBackPressedHandler(
private val context: Context,
private val verificationFlowFinishable: VerificationFlowFinishable,
private val navController: NavController,
private val identityViewModel: IdentityViewModel
Expand All @@ -37,15 +41,30 @@ internal class IdentityOnBackPressedHandler(
return
}
if (navController.previousBackStackEntry?.destination?.route == InitialLoadingDestination.ROUTE.route ||
navController.previousBackStackEntry?.destination?.route == DebugDestination.ROUTE.route ||
destination?.route == ConsentDestination.ROUTE.route
navController.previousBackStackEntry?.destination?.route == DebugDestination.ROUTE.route
) {
finishWithCancelResult(
identityViewModel,
verificationFlowFinishable,
destination?.route?.routeToScreenName()
?: IdentityAnalyticsRequestFactory.SCREEN_NAME_UNKNOWN
)
} else if (destination?.route == ConsentDestination.ROUTE.route) {
showAlertDialog(
context,
R.string.stripe_identity_confirm_cancel,
positiveButton = AlertButton(
R.string.stripe_identity_yes
) { _, _ ->
finishWithCancelResult(
identityViewModel,
verificationFlowFinishable,
destination?.route?.routeToScreenName()
?: IdentityAnalyticsRequestFactory.SCREEN_NAME_UNKNOWN
)
},
negativeButton = AlertButton(R.string.stripe_identity_no)
)
} else {
when (destination?.route) {
ConfirmationDestination.ROUTE.route -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.stripe.android.identity.utils

import android.content.Context
import android.content.DialogInterface.OnClickListener
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog

internal data class AlertButton(
@StringRes val buttonRes: Int,
val onClickListener: OnClickListener? = null
)

internal fun showAlertDialog(
context: Context,
@StringRes titleRes: Int,
positiveButton: AlertButton? = null,
negativeButton: AlertButton? = null
) {
val builder = AlertDialog.Builder(context)
builder.setMessage(titleRes)
positiveButton?.let {
builder.setPositiveButton(positiveButton.buttonRes, positiveButton.onClickListener)
}
negativeButton?.let {
builder.setNegativeButton(negativeButton.buttonRes, negativeButton.onClickListener)
}
builder.show()
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.stripe.android.identity

import android.os.Build
import androidx.core.os.bundleOf
import androidx.lifecycle.MutableLiveData
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.test.core.app.ApplicationProvider
import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory
import com.stripe.android.identity.navigation.ConfirmationDestination
import com.stripe.android.identity.navigation.ConsentDestination
Expand All @@ -13,6 +15,7 @@ import com.stripe.android.identity.navigation.DocSelectionDestination
import com.stripe.android.identity.navigation.ErrorDestination
import com.stripe.android.identity.navigation.ErrorDestination.Companion.ARG_SHOULD_FAIL
import com.stripe.android.identity.navigation.InitialLoadingDestination
import com.stripe.android.identity.navigation.OTPDestination
import com.stripe.android.identity.navigation.routeToScreenName
import com.stripe.android.identity.viewmodel.IdentityViewModel
import org.junit.Test
Expand All @@ -24,10 +27,15 @@ import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowAlertDialog
import kotlin.test.assertNotNull

@RunWith(RobolectricTestRunner::class)
@Config(application = TestApplication::class, sdk = [Build.VERSION_CODES.Q])
class IdentityOnBackPressedHandlerTest {
private val mockFlowFinishable = mock<VerificationFlowFinishable>()
private val mockNavController = mock<NavController>()
Expand All @@ -40,6 +48,7 @@ class IdentityOnBackPressedHandlerTest {
}

private val handler = IdentityOnBackPressedHandler(
ApplicationProvider.getApplicationContext(),
mockFlowFinishable,
mockNavController,
mockIdentityViewModel
Expand Down Expand Up @@ -106,7 +115,17 @@ class IdentityOnBackPressedHandlerTest {
}

@Test
fun testBackPressOnConsentPage() {
fun testBackPressOnConsentPageWithTypeDocument() {
val initialNavBackStackEntry = mock<NavBackStackEntry> {
on { destination } doReturn NavDestination("").also {
it.route = InitialLoadingDestination.ROUTE.route
}
}

whenever(mockNavController.previousBackStackEntry).thenReturn(
initialNavBackStackEntry
)

val mockDestination = mock<NavDestination> {
on { route } doReturn ConsentDestination.ROUTE.route
}
Expand All @@ -129,6 +148,35 @@ class IdentityOnBackPressedHandlerTest {
)
}

@Test
fun testBackPressOnConsentPageWithTypePhoneV() {
val initialNavBackStackEntry = mock<NavBackStackEntry> {
on { destination } doReturn NavDestination("").also {
it.route = OTPDestination.ROUTE.route
}
}

whenever(mockNavController.previousBackStackEntry).thenReturn(
initialNavBackStackEntry
)

val mockDestination = mock<NavDestination> {
on { route } doReturn ConsentDestination.ROUTE.route
}

handler.updateState(
destination = mockDestination,
args = null
)

handler.handleOnBackPressed()

verifyNoMoreInteractions(mockFlowFinishable)

val cancelDialog = ShadowAlertDialog.getShownDialogs().first()
assertNotNull(cancelDialog)
}

@Test
fun testBackPressOnErrorPageWithArgShouldFail() {
val mockDestination = mock<NavDestination> {
Expand Down

0 comments on commit 08e9164

Please sign in to comment.