From 50ef579276aa927f668bcf7d35969cd03bf1f3e5 Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Mon, 1 May 2023 16:57:07 -0700 Subject: [PATCH 1/7] Support Identity test mode M1 --- identity/api/identity.api | 2 - identity/res/values/totranslate.xml | 4 +- .../identity/navigation/ErrorDestination.kt | 2 + .../identity/navigation/IdentityNavGraph.kt | 5 + .../networking/DefaultIdentityRepository.kt | 36 ++- .../identity/networking/IdentityRepository.kt | 20 ++ .../identity/networking/NetworkConstants.kt | 7 + .../models/VerificationReportRequirement.kt | 35 +++ .../stripe/android/identity/ui/DebugScreen.kt | 232 +++++++++++++++--- .../identity/ui/StyledClickableText.kt | 75 ++++++ .../identity/viewmodel/IdentityViewModel.kt | 108 +++++--- .../DefaultIdentityRepositoryTest.kt | 81 +++++- .../android/identity/ui/DebugScreenTest.kt | 73 +++++- 13 files changed, 608 insertions(+), 72 deletions(-) create mode 100644 identity/src/main/java/com/stripe/android/identity/networking/models/VerificationReportRequirement.kt create mode 100644 identity/src/main/java/com/stripe/android/identity/ui/StyledClickableText.kt diff --git a/identity/api/identity.api b/identity/api/identity.api index 46ce89c2c1b..3e111e75510 100644 --- a/identity/api/identity.api +++ b/identity/api/identity.api @@ -291,12 +291,10 @@ public final class com/stripe/android/identity/ui/ComposableSingletons$DebugScre public static field lambda-1 Lkotlin/jvm/functions/Function3; public static field lambda-2 Lkotlin/jvm/functions/Function3; public static field lambda-3 Lkotlin/jvm/functions/Function3; - public static field lambda-4 Lkotlin/jvm/functions/Function3; public fun ()V public final fun getLambda-1$identity_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-2$identity_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-3$identity_release ()Lkotlin/jvm/functions/Function3; - public final fun getLambda-4$identity_release ()Lkotlin/jvm/functions/Function3; } public final class com/stripe/android/identity/ui/ComposableSingletons$SelfieScreenKt { diff --git a/identity/res/values/totranslate.xml b/identity/res/values/totranslate.xml index 4d36521c92e..fad13724aac 100644 --- a/identity/res/values/totranslate.xml +++ b/identity/res/values/totranslate.xml @@ -5,7 +5,7 @@ You\'re currently in testmode This page is only shown in testmode. Complete with test data - Completed.]]> + Save time by choosing a desired result and completing instantly with that outcome. The mobile SDK flow will return with result Completed. Verification success Verification failure @@ -14,7 +14,7 @@ SUBMIT Terminate mobile SDK flow - Completed, Canceled or Failed without changing the verification session on server.]]> + Terminate the mobile SDK flow locally with Cancelled or Failed. Failure from test mode COMPLETED diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/ErrorDestination.kt b/identity/src/main/java/com/stripe/android/identity/navigation/ErrorDestination.kt index c31505151cd..d23f6d8711f 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/ErrorDestination.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/ErrorDestination.kt @@ -40,6 +40,8 @@ internal class ErrorDestination( // If this happens, set the back button destination to [DEFAULT_BACK_BUTTON_DESTINATION] const val UNEXPECTED_ROUTE = "UnexpectedRoute" + val TAG = ErrorDestination::class.java.simpleName + fun errorTitle(backStackEntry: NavBackStackEntry) = backStackEntry.getStringArgument(ARG_ERROR_TITLE) diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/IdentityNavGraph.kt b/identity/src/main/java/com/stripe/android/identity/navigation/IdentityNavGraph.kt index 8b1f1046819..7ace8718689 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/IdentityNavGraph.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/IdentityNavGraph.kt @@ -1,5 +1,6 @@ package com.stripe.android.identity.navigation +import android.util.Log import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable @@ -321,6 +322,10 @@ internal fun IdentityNavGraph( ) } screen(ErrorDestination.ROUTE) { + Log.d( + ErrorDestination.TAG, + "About to show error screen with error caused by ${identityViewModel.errorCause.value}" + ) ErrorScreen( identityViewModel = identityViewModel, title = ErrorDestination.errorTitle(it), diff --git a/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt b/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt index 8b6eba9b72d..4e774e22270 100644 --- a/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt +++ b/identity/src/main/java/com/stripe/android/identity/networking/DefaultIdentityRepository.kt @@ -94,6 +94,40 @@ internal class DefaultIdentityRepository @Inject constructor( VerificationPageData.serializer() ) + override suspend fun verifyTestVerificationSession( + id: String, + ephemeralKey: String, + simulateDelay: Boolean + ) = executeRequestWithKSerializer( + apiRequestFactory.createPost( + url = "$BASE_URL/$IDENTITY_VERIFICATION_PAGES/${urlEncode(id)}/$TESTING/$VERIFY", + options = ApiRequest.Options( + apiKey = ephemeralKey + ), + params = mapOf( + SIMULATE_DELAY to simulateDelay + ) + ), + VerificationPageData.serializer() + ) + + override suspend fun unverifyTestVerificationSession( + id: String, + ephemeralKey: String, // todo - need to add this to the request + simulateDelay: Boolean + ) = executeRequestWithKSerializer( + apiRequestFactory.createPost( + url = "$BASE_URL/$IDENTITY_VERIFICATION_PAGES/${urlEncode(id)}/$TESTING/$UNVERIFY", + options = ApiRequest.Options( + apiKey = ephemeralKey + ), + params = mapOf( + SIMULATE_DELAY to simulateDelay + ) + ), + VerificationPageData.serializer() + ) + override suspend fun uploadImage( verificationId: String, ephemeralKey: String, @@ -252,8 +286,6 @@ internal class DefaultIdentityRepository @Inject constructor( } internal companion object { - const val SUBMIT = "submit" - const val DATA = "data" val TAG: String = DefaultIdentityRepository::class.java.simpleName } } diff --git a/identity/src/main/java/com/stripe/android/identity/networking/IdentityRepository.kt b/identity/src/main/java/com/stripe/android/identity/networking/IdentityRepository.kt index 881513cddc4..d428971e20e 100644 --- a/identity/src/main/java/com/stripe/android/identity/networking/IdentityRepository.kt +++ b/identity/src/main/java/com/stripe/android/identity/networking/IdentityRepository.kt @@ -44,6 +44,26 @@ internal interface IdentityRepository { ephemeralKey: String ): VerificationPageData + @Throws( + APIConnectionException::class, + APIException::class + ) + suspend fun verifyTestVerificationSession( + id: String, + ephemeralKey: String, + simulateDelay: Boolean + ): VerificationPageData + + @Throws( + APIConnectionException::class, + APIException::class + ) + suspend fun unverifyTestVerificationSession( + id: String, + ephemeralKey: String, + simulateDelay: Boolean + ): VerificationPageData + @Throws( APIConnectionException::class, APIException::class diff --git a/identity/src/main/java/com/stripe/android/identity/networking/NetworkConstants.kt b/identity/src/main/java/com/stripe/android/identity/networking/NetworkConstants.kt index 2279f02f741..daa2d45e43e 100644 --- a/identity/src/main/java/com/stripe/android/identity/networking/NetworkConstants.kt +++ b/identity/src/main/java/com/stripe/android/identity/networking/NetworkConstants.kt @@ -4,3 +4,10 @@ internal const val BASE_URL = "https://api.stripe.com/v1" internal const val IDENTITY_VERIFICATION_PAGES = "identity/verification_pages" internal const val IDENTITY_STRIPE_API_VERSION_WITH_BETA_HEADER = "2020-08-27;identity_client_api=v3" + +internal const val SUBMIT = "submit" +internal const val DATA = "data" +internal const val TESTING = "testing" +internal const val VERIFY = "verify" +internal const val UNVERIFY = "unverify" +internal const val SIMULATE_DELAY = "simulate_delay" diff --git a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationReportRequirement.kt b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationReportRequirement.kt new file mode 100644 index 00000000000..2cb5d961b62 --- /dev/null +++ b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationReportRequirement.kt @@ -0,0 +1,35 @@ +package com.stripe.android.identity.networking.models + +import kotlinx.serialization.SerialName + +internal enum class VerificationReportRequirement(val value: String) { + @SerialName("generic") + GENERIC("generic"), + + @SerialName("address") + ADDRESS("address"), + + @SerialName("dob") + DOB("dob"), + + @SerialName("document") + DOCUMENT("document"), + + @SerialName("email") + EMAIL("email"), + + @SerialName("id_number") + ID_NUMBER("id_number"), + + @SerialName("phone") + PHONE("phone"), + + @SerialName("phone_otp") + PHONE_OTP("phone_otp"), + + @SerialName("phone_records") + PHONE_RECORDS("phone_records"), + + @SerialName("selfie") + SELFIE("selfie") +} diff --git a/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt b/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt index 209f5b9f907..20136e52f95 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt @@ -13,8 +13,14 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme +import androidx.compose.material.RadioButton import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -23,15 +29,26 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.stripe.android.identity.IdentityVerificationSheet import com.stripe.android.identity.R import com.stripe.android.identity.VerificationFlowFinishable +import com.stripe.android.identity.navigation.DebugDestination import com.stripe.android.identity.navigation.navigateTo import com.stripe.android.identity.networking.models.Requirement.Companion.nextDestination +import com.stripe.android.identity.ui.CompleteOption.FAILURE +import com.stripe.android.identity.ui.CompleteOption.FAILURE_ASYNC +import com.stripe.android.identity.ui.CompleteOption.SUCCESS +import com.stripe.android.identity.ui.CompleteOption.SUCCESS_ASYNC import com.stripe.android.identity.viewmodel.IdentityViewModel import com.stripe.android.uicore.text.Html +import kotlinx.coroutines.launch + +internal enum class CompleteOption { + SUCCESS, FAILURE, SUCCESS_ASYNC, FAILURE_ASYNC +} /** * Screen to show debug options for test mode verification. @@ -58,15 +75,71 @@ internal fun DebugScreen( ) ) { val context = LocalContext.current + var proceedState by remember { + mutableStateOf(LoadingButtonState.Idle) + } + + val coroutineScope = rememberCoroutineScope() TitleSection() + + Divider( + modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) + ) + + CompleteWithTestDataSection { completeOption -> + proceedState = LoadingButtonState.Disabled + + when (completeOption) { + SUCCESS -> { + coroutineScope.launch { + identityViewModel.verifySessionAndTransition( + fromRoute = DebugDestination.ROUTE.route, + simulateDelay = false, + navController = navController + ) + } + } + FAILURE -> { + coroutineScope.launch { + identityViewModel.unverifySessionAndTransition( + fromRoute = DebugDestination.ROUTE.route, + simulateDelay = false, + navController = navController + ) + } + } + SUCCESS_ASYNC -> { + coroutineScope.launch { + identityViewModel.verifySessionAndTransition( + fromRoute = DebugDestination.ROUTE.route, + simulateDelay = true, + navController = navController + ) + } + } + FAILURE_ASYNC -> { + coroutineScope.launch { + identityViewModel.unverifySessionAndTransition( + fromRoute = DebugDestination.ROUTE.route, + simulateDelay = true, + navController = navController + ) + } + } + } + } + Divider( modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) ) + FinishMobileFlowWithResultSection(verificationFlowFinishable) + Divider( modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) ) + PreviewUserExperienceSection { navController.navigateTo( verificationPage.requirements.missing.nextDestination( @@ -105,6 +178,79 @@ private fun TitleSection() { } } +@Composable +internal fun CompleteWithTestDataSection( + onClickSubmit: (CompleteOption) -> Unit +) { + var submitState: LoadingButtonState by remember { + mutableStateOf(LoadingButtonState.Disabled) + } + + var completeOption: CompleteOption? by remember { + mutableStateOf(null) + } + + Text( + text = stringResource(id = R.string.stripe_complete_with_test_data), + style = MaterialTheme.typography.h4 + ) + Html( + html = stringResource(id = R.string.stripe_complete_with_test_data_details), + modifier = Modifier.padding(vertical = 8.dp) + ) + + CompleteOptionRow( + content = stringResource(id = R.string.stripe_verification_success), + selected = completeOption == SUCCESS, + enabled = submitState != LoadingButtonState.Loading, + testTag = TEST_TAG_SUCCESS, + onClick = { + completeOption = SUCCESS + submitState = LoadingButtonState.Idle + } + ) + CompleteOptionRow( + content = stringResource(id = R.string.stripe_verification_failure), + selected = completeOption == FAILURE, + enabled = submitState != LoadingButtonState.Loading, + testTag = TEST_TAG_FAILURE, + onClick = { + completeOption = FAILURE + submitState = LoadingButtonState.Idle + } + ) + CompleteOptionRow( + content = stringResource(id = R.string.stripe_verification_success_async), + selected = completeOption == SUCCESS_ASYNC, + enabled = submitState != LoadingButtonState.Loading, + testTag = TEST_TAG_SUCCESS_ASYNC, + onClick = { + completeOption = SUCCESS_ASYNC + submitState = LoadingButtonState.Idle + } + ) + CompleteOptionRow( + content = stringResource(id = R.string.stripe_verification_failure_async), + selected = completeOption == FAILURE_ASYNC, + enabled = submitState != LoadingButtonState.Loading, + testTag = TEST_TAG_FAILURE_ASYNC, + onClick = { + completeOption = FAILURE_ASYNC + submitState = LoadingButtonState.Idle + } + ) + + LoadingButton( + text = stringResource(id = R.string.stripe_submit), + state = submitState, + modifier = Modifier.testTag(TEST_TAG_SUBMIT_BUTTON), + onClick = { + submitState = LoadingButtonState.Loading + onClickSubmit(requireNotNull(completeOption)) + } + ) +} + @Composable private fun FinishMobileFlowWithResultSection( finishable: VerificationFlowFinishable @@ -119,41 +265,37 @@ private fun FinishMobileFlowWithResultSection( modifier = Modifier.padding(vertical = 8.dp) ) - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TEST_TAG_COMPLETE_BUTTON), - onClick = { - finishable.finishWithResult(IdentityVerificationSheet.VerificationFlowResult.Completed) - } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically ) { - Text(text = stringResource(id = R.string.stripe_completed)) - } - - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TEST_TAG_CANCELLED_BUTTON), - onClick = { - finishable.finishWithResult(IdentityVerificationSheet.VerificationFlowResult.Canceled) + Button( + modifier = Modifier + .weight(1f) + .padding(horizontal = 10.dp) + .testTag(TEST_TAG_CANCELLED_BUTTON), + onClick = { + finishable.finishWithResult(IdentityVerificationSheet.VerificationFlowResult.Canceled) + } + ) { + Text(text = stringResource(id = R.string.stripe_cancelled)) } - ) { - Text(text = stringResource(id = R.string.stripe_cancelled)) - } - Button( - modifier = Modifier - .fillMaxWidth() - .testTag(TEST_TAG_FAILED_BUTTON), - onClick = { - finishable.finishWithResult( - IdentityVerificationSheet.VerificationFlowResult.Failed( - Exception(failureExceptionMessage) + Button( + modifier = Modifier + .weight(1f) + .padding(horizontal = 10.dp) + .testTag(TEST_TAG_FAILED_BUTTON), + onClick = { + finishable.finishWithResult( + IdentityVerificationSheet.VerificationFlowResult.Failed( + Exception(failureExceptionMessage) + ) ) - ) + } + ) { + Text(text = stringResource(id = R.string.stripe_failed)) } - ) { - Text(text = stringResource(id = R.string.stripe_failed)) } } @@ -179,7 +321,35 @@ private fun PreviewUserExperienceSection( } } -internal const val TEST_TAG_COMPLETE_BUTTON = "Completed" +@Composable +private fun CompleteOptionRow( + content: String, + selected: Boolean, + enabled: Boolean, + testTag: String, + onClick: () -> Unit +) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = selected, + enabled = enabled, + modifier = Modifier.testTag(testTag), + onClick = onClick + ) + StyledClickableText( + text = AnnotatedString(content), + modifier = Modifier.padding(start = 8.dp), + enabled = enabled, + onClick = { onClick() } + ) + } +} + internal const val TEST_TAG_CANCELLED_BUTTON = "Cancelled" internal const val TEST_TAG_FAILED_BUTTON = "Failed" internal const val TEST_TAG_PROCEED_BUTTON = "Proceed" +internal const val TEST_TAG_SUBMIT_BUTTON = "Submit" +internal const val TEST_TAG_SUCCESS = "success" +internal const val TEST_TAG_SUCCESS_ASYNC = "success_async" +internal const val TEST_TAG_FAILURE = "failure" +internal const val TEST_TAG_FAILURE_ASYNC = "failure_async" diff --git a/identity/src/main/java/com/stripe/android/identity/ui/StyledClickableText.kt b/identity/src/main/java/com/stripe/android/identity/ui/StyledClickableText.kt new file mode 100644 index 00000000000..9296fdc5697 --- /dev/null +++ b/identity/src/main/java/com/stripe/android/identity/ui/StyledClickableText.kt @@ -0,0 +1,75 @@ +package com.stripe.android.identity.ui + +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.material.LocalTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.TextUnit + +@Composable +internal fun StyledClickableText( + text: AnnotatedString, + modifier: Modifier = Modifier, + enabled: Boolean = true, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + onClick: (Int) -> Unit +) { + val textColor = color.takeOrElse { + style.color.takeOrElse { + LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + } + } + // NOTE(text-perf-review): It might be worthwhile writing a bespoke merge implementation that + // will avoid reallocating if all of the options here are the defaults + val mergedStyle = style.merge( + TextStyle( + color = textColor, + fontSize = fontSize, + fontWeight = fontWeight, + textAlign = textAlign, + lineHeight = lineHeight, + fontFamily = fontFamily, + textDecoration = textDecoration, + fontStyle = fontStyle, + letterSpacing = letterSpacing + ) + ) + + ClickableText( + text = text, + modifier = modifier, + style = mergedStyle, + softWrap = softWrap, + maxLines = maxLines, + onTextLayout = onTextLayout, + onClick = if (enabled) { + onClick + } else { + { } + } + ) +} 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 444a691e240..ced3bea2ff6 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 @@ -854,6 +854,85 @@ internal class IdentityViewModel constructor( } } + /** + * Invoke the verify endpoint and navigate to the [ConfirmationDestination] if successful. + */ + suspend fun verifySessionAndTransition( + fromRoute: String, + simulateDelay: Boolean, + navController: NavController + ) { + runCatching { + identityRepository.verifyTestVerificationSession( + id = verificationArgs.verificationSessionId, + ephemeralKey = verificationArgs.ephemeralKeySecret, + simulateDelay = simulateDelay + ) + }.navigateToConfirmIfSubmitted(fromRoute, navController) + } + + /** + * Invoke the unverify endpoint and navigate to the [ConfirmationDestination] if successful. + */ + suspend fun unverifySessionAndTransition( + fromRoute: String, + simulateDelay: Boolean, + navController: NavController + ) { + runCatching { + identityRepository.unverifyTestVerificationSession( + id = verificationArgs.verificationSessionId, + ephemeralKey = verificationArgs.ephemeralKeySecret, + simulateDelay = simulateDelay + ) + }.navigateToConfirmIfSubmitted(fromRoute, navController) + } + + /** + * Check the [Result] of [VerificationPageData] and try to navigate to [ConfirmationDestination]. + * + * If Result is success, check the value of VerificationPageData + * If VerificationPageData has error, navigate to [ErrorDestination] with the error. + * If VerificationPageData is submitted, navigate to [ConfirmationDestination]. + * If VerificationPageData is not submitted, navigate to [ErrorDestination]. + * + * If Result is failed, navigate to [ErrorDestination] with the failure information. + * + */ + private fun Result.navigateToConfirmIfSubmitted( + fromRoute: String, + navController: NavController + ) { + this.onSuccess { submittedVerificationPageData -> + verificationPageSubmit.updateStateAndSave { + Resource.success(DUMMY_RESOURCE) + } + when { + submittedVerificationPageData.hasError() -> { + submittedVerificationPageData.requirements.errors[0].let { requirementError -> + errorCause.postValue( + IllegalStateException("VerificationPageDataRequirementError: $requirementError") + ) + navController.navigateToErrorScreenWithRequirementError( + fromRoute, + requirementError + ) + } + } + submittedVerificationPageData.submitted -> { + navController.navigateTo(ConfirmationDestination) + } + else -> { + errorCause.postValue(IllegalStateException("VerificationPage submit failed")) + navController.navigateToErrorScreenWithDefaultValues(getApplication()) + } + } + }.onFailure { + errorCause.postValue(it) + navController.navigateToErrorScreenWithDefaultValues(getApplication()) + } + } + /** * Download an ML model and post its value to [target]. */ @@ -1048,34 +1127,7 @@ internal class IdentityViewModel constructor( verificationArgs.verificationSessionId, verificationArgs.ephemeralKeySecret ) - }.onSuccess { submittedVerificationPageData -> - verificationPageSubmit.updateStateAndSave { - Resource.success(DUMMY_RESOURCE) - } - when { - submittedVerificationPageData.hasError() -> { - submittedVerificationPageData.requirements.errors[0].let { requirementError -> - errorCause.postValue( - IllegalStateException("VerificationPageDataRequirementError: $requirementError") - ) - navController.navigateToErrorScreenWithRequirementError( - fromRoute, - requirementError - ) - } - } - submittedVerificationPageData.submitted -> { - navController.navigateTo(ConfirmationDestination) - } - else -> { - errorCause.postValue(IllegalStateException("VerificationPage submit failed")) - navController.navigateToErrorScreenWithDefaultValues(getApplication()) - } - } - }.onFailure { - errorCause.postValue(it) - navController.navigateToErrorScreenWithDefaultValues(getApplication()) - } + }.navigateToConfirmIfSubmitted(fromRoute, navController) } /** diff --git a/identity/src/test/java/com/stripe/android/identity/networking/DefaultIdentityRepositoryTest.kt b/identity/src/test/java/com/stripe/android/identity/networking/DefaultIdentityRepositoryTest.kt index 2165190cd99..f3b1fec1599 100644 --- a/identity/src/test/java/com/stripe/android/identity/networking/DefaultIdentityRepositoryTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/networking/DefaultIdentityRepositoryTest.kt @@ -12,8 +12,6 @@ import com.stripe.android.core.networking.HEADER_AUTHORIZATION import com.stripe.android.core.networking.StripeNetworkClient import com.stripe.android.core.networking.StripeRequest import com.stripe.android.core.networking.StripeResponse -import com.stripe.android.identity.networking.DefaultIdentityRepository.Companion.DATA -import com.stripe.android.identity.networking.DefaultIdentityRepository.Companion.SUBMIT import com.stripe.android.identity.networking.models.ClearDataParam import com.stripe.android.identity.networking.models.ClearDataParam.Companion.createCollectedDataParamEntry import com.stripe.android.identity.networking.models.CollectedDataParam @@ -274,6 +272,38 @@ class DefaultIdentityRepositoryTest { } } + @Test + fun testVerifyTestVerificationSessionWithoutDelay() { + testVerifyEndpoint( + verify = true, + simulateDelay = false + ) + } + + @Test + fun testVerifyTestVerificationSessionWithDelay() { + testVerifyEndpoint( + verify = true, + simulateDelay = true + ) + } + + @Test + fun testUnverifyTestVerificationSessionWithoutDelay() { + testVerifyEndpoint( + verify = false, + simulateDelay = false + ) + } + + @Test + fun testUnverifyTestVerificationSessionWithDelay() { + testVerifyEndpoint( + verify = false, + simulateDelay = true + ) + } + @Test fun `retrieveVerificationPage with error response throws APIException from executeRequestWithKSerializer`() { runBlocking { @@ -479,6 +509,53 @@ class DefaultIdentityRepositoryTest { } } + private fun testVerifyEndpoint( + verify: Boolean, + simulateDelay: Boolean + ) { + runBlocking { + whenever(mockStripeNetworkClient.executeRequest(any())).thenReturn( + StripeResponse( + code = HTTP_OK, + body = VERIFICATION_PAGE_DATA_JSON_STRING + ) + ) + val verificationPage = + if (verify) { + identityRepository.verifyTestVerificationSession( + id = TEST_ID, + ephemeralKey = TEST_EPHEMERAL_KEY, + simulateDelay = simulateDelay + ) + } else { + identityRepository.unverifyTestVerificationSession( + id = TEST_ID, + ephemeralKey = TEST_EPHEMERAL_KEY, + simulateDelay = simulateDelay + ) + } + + assertThat(verificationPage).isInstanceOf(VerificationPageData::class.java) + verify(mockStripeNetworkClient).executeRequest(requestCaptor.capture()) + + val request = requestCaptor.firstValue + + assertThat(request).isInstanceOf(ApiRequest::class.java) + assertThat(request.method).isEqualTo(StripeRequest.Method.POST) + if (verify) { + assertThat(request.url).isEqualTo("$BASE_URL/$IDENTITY_VERIFICATION_PAGES/$TEST_ID/$TESTING/$VERIFY") + } else { + assertThat(request.url).isEqualTo("$BASE_URL/$IDENTITY_VERIFICATION_PAGES/$TEST_ID/$TESTING/$UNVERIFY") + } + assertThat(request.headers[HEADER_AUTHORIZATION]).isEqualTo("Bearer $TEST_EPHEMERAL_KEY") + assertThat((request as ApiRequest).params).isEqualTo( + mapOf( + SIMULATE_DELAY to simulateDelay + ) + ) + } + } + private companion object { const val TEST_EPHEMERAL_KEY = "ek_test_12345" const val TEST_ID = "vs_12345" diff --git a/identity/src/test/java/com/stripe/android/identity/ui/DebugScreenTest.kt b/identity/src/test/java/com/stripe/android/identity/ui/DebugScreenTest.kt index 84c5737ea87..710788f6f0a 100644 --- a/identity/src/test/java/com/stripe/android/identity/ui/DebugScreenTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/ui/DebugScreenTest.kt @@ -14,12 +14,14 @@ import com.stripe.android.identity.IdentityVerificationSheet import com.stripe.android.identity.TestApplication import com.stripe.android.identity.VerificationFlowFinishable import com.stripe.android.identity.navigation.ConsentDestination +import com.stripe.android.identity.navigation.DebugDestination import com.stripe.android.identity.navigation.IndividualWelcomeDestination import com.stripe.android.identity.networking.Resource import com.stripe.android.identity.networking.models.Requirement import com.stripe.android.identity.networking.models.VerificationPage import com.stripe.android.identity.networking.models.VerificationPageRequirements import com.stripe.android.identity.viewmodel.IdentityViewModel +import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -28,6 +30,7 @@ import org.mockito.kotlin.argWhere import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.same import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner @@ -74,18 +77,77 @@ class DebugScreenTest { } @Test - fun testCompleteButton() { + fun testSubmitWithSuccess() { setComposeTestRuleWith(verificationPageForDoc) { - onNodeWithTag(TEST_TAG_COMPLETE_BUTTON).performClick() - verify(mockVerificationFlowFinishable).finishWithResult( - eq(IdentityVerificationSheet.VerificationFlowResult.Completed) - ) + runBlocking { + onNodeWithTag(TEST_TAG_SUBMIT_BUTTON).performScrollTo() + onNodeWithTag(TEST_TAG_SUCCESS).performClick() + onNodeWithTag(TEST_TAG_SUBMIT_BUTTON).performClick() + + verify(mockIdentityViewModel).verifySessionAndTransition( + fromRoute = eq(DebugDestination.ROUTE.route), + simulateDelay = eq(false), + navController = same(mockNavController) + ) + } + } + } + + @Test + fun testSubmitWithSuccessAsync() { + setComposeTestRuleWith(verificationPageForDoc) { + runBlocking { + onNodeWithTag(TEST_TAG_SUBMIT_BUTTON).performScrollTo() + onNodeWithTag(TEST_TAG_SUCCESS_ASYNC).performClick() + onNodeWithTag(TEST_TAG_SUBMIT_BUTTON).performClick() + + verify(mockIdentityViewModel).verifySessionAndTransition( + fromRoute = eq(DebugDestination.ROUTE.route), + simulateDelay = eq(true), + navController = same(mockNavController) + ) + } + } + } + + @Test + fun testSubmitWithFailure() { + setComposeTestRuleWith(verificationPageForDoc) { + runBlocking { + onNodeWithTag(TEST_TAG_SUBMIT_BUTTON).performScrollTo() + onNodeWithTag(TEST_TAG_FAILURE).performClick() + onNodeWithTag(TEST_TAG_SUBMIT_BUTTON).performClick() + + verify(mockIdentityViewModel).unverifySessionAndTransition( + fromRoute = eq(DebugDestination.ROUTE.route), + simulateDelay = eq(false), + navController = same(mockNavController) + ) + } + } + } + + @Test + fun testSubmitWithFailureAsync() { + setComposeTestRuleWith(verificationPageForDoc) { + runBlocking { + onNodeWithTag(TEST_TAG_SUBMIT_BUTTON).performScrollTo() + onNodeWithTag(TEST_TAG_FAILURE_ASYNC).performClick() + onNodeWithTag(TEST_TAG_SUBMIT_BUTTON).performClick() + + verify(mockIdentityViewModel).unverifySessionAndTransition( + fromRoute = eq(DebugDestination.ROUTE.route), + simulateDelay = eq(true), + navController = same(mockNavController) + ) + } } } @Test fun testCancelledButton() { setComposeTestRuleWith(verificationPageForDoc) { + onNodeWithTag(TEST_TAG_CANCELLED_BUTTON).performScrollTo() onNodeWithTag(TEST_TAG_CANCELLED_BUTTON).performClick() verify(mockVerificationFlowFinishable).finishWithResult( eq(IdentityVerificationSheet.VerificationFlowResult.Canceled) @@ -96,6 +158,7 @@ class DebugScreenTest { @Test fun testFailedButton() { setComposeTestRuleWith(verificationPageForDoc) { + onNodeWithTag(TEST_TAG_FAILED_BUTTON).performScrollTo() onNodeWithTag(TEST_TAG_FAILED_BUTTON).performClick() verify(mockVerificationFlowFinishable).finishWithResult( argWhere { From 1f6351bada905ad3abd5d474400bf513161873a6 Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Tue, 2 May 2023 15:16:02 -0700 Subject: [PATCH 2/7] fix detekt --- identity/detekt-baseline.xml | 1 + .../stripe/android/identity/ui/DebugScreen.kt | 50 ++++--------------- 2 files changed, 12 insertions(+), 39 deletions(-) diff --git a/identity/detekt-baseline.xml b/identity/detekt-baseline.xml index 2fe0ac98f92..0ac5dbb9761 100644 --- a/identity/detekt-baseline.xml +++ b/identity/detekt-baseline.xml @@ -14,6 +14,7 @@ LongMethod:CameraScreenLaunchedEffect.kt$@Composable internal fun CameraScreenLaunchedEffect( identityViewModel: IdentityViewModel, identityScanViewModel: IdentityScanViewModel, verificationPage: VerificationPage, navController: NavController, cameraManager: IdentityCameraManager, onCameraReady: () -> Unit ) LongMethod:ConfirmationScreen.kt$@Composable internal fun ConfirmationScreen( navController: NavController, identityViewModel: IdentityViewModel, verificationFlowFinishable: VerificationFlowFinishable ) LongMethod:ConsentScreen.kt$@Composable private fun SuccessUI( merchantLogoUri: Uri, verificationPage: VerificationPage, onConsentAgreed: () -> Unit, onConsentDeclined: () -> Unit ) + LongMethod:DebugScreen.kt$@Composable internal fun DebugScreen( navController: NavController, identityViewModel: IdentityViewModel, verificationFlowFinishable: VerificationFlowFinishable ) LongMethod:DocSelectionScreen.kt$@Composable internal fun DocSelectionScreen( navController: NavController, identityViewModel: IdentityViewModel, cameraPermissionEnsureable: CameraPermissionEnsureable ) LongMethod:DocumenetScanScreen.kt$@Composable internal fun DocumentScanScreen( navController: NavController, identityViewModel: IdentityViewModel, identityScanViewModel: IdentityScanViewModel, frontScanType: IdentityScanState.ScanType, backScanType: IdentityScanState.ScanType?, shouldStartFromBack: Boolean, messageRes: DocumentScanMessageRes, collectedDataParamType: CollectedDataParam.Type, route: String ) LongMethod:ErrorScreen.kt$@Composable internal fun ErrorScreen( identityViewModel: IdentityViewModel, title: String, modifier: Modifier = Modifier, message1: String? = null, message2: String? = null, topButton: ErrorScreenButton? = null, bottomButton: ErrorScreenButton? = null, ) diff --git a/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt b/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt index 20136e52f95..d258f98e704 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt @@ -46,10 +46,6 @@ import com.stripe.android.identity.viewmodel.IdentityViewModel import com.stripe.android.uicore.text.Html import kotlinx.coroutines.launch -internal enum class CompleteOption { - SUCCESS, FAILURE, SUCCESS_ASYNC, FAILURE_ASYNC -} - /** * Screen to show debug options for test mode verification. */ @@ -75,21 +71,12 @@ internal fun DebugScreen( ) ) { val context = LocalContext.current - var proceedState by remember { - mutableStateOf(LoadingButtonState.Idle) - } - + var proceedState by remember { mutableStateOf(LoadingButtonState.Idle) } val coroutineScope = rememberCoroutineScope() - TitleSection() - - Divider( - modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) - ) - + Divider(modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin))) CompleteWithTestDataSection { completeOption -> proceedState = LoadingButtonState.Disabled - when (completeOption) { SUCCESS -> { coroutineScope.launch { @@ -129,17 +116,9 @@ internal fun DebugScreen( } } } - - Divider( - modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) - ) - + Divider(modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin))) FinishMobileFlowWithResultSection(verificationFlowFinishable) - - Divider( - modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) - ) - + Divider(modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin))) PreviewUserExperienceSection { navController.navigateTo( verificationPage.requirements.missing.nextDestination( @@ -182,23 +161,13 @@ private fun TitleSection() { internal fun CompleteWithTestDataSection( onClickSubmit: (CompleteOption) -> Unit ) { - var submitState: LoadingButtonState by remember { - mutableStateOf(LoadingButtonState.Disabled) - } - - var completeOption: CompleteOption? by remember { - mutableStateOf(null) - } - - Text( - text = stringResource(id = R.string.stripe_complete_with_test_data), - style = MaterialTheme.typography.h4 - ) + var submitState: LoadingButtonState by remember { mutableStateOf(LoadingButtonState.Disabled) } + var completeOption: CompleteOption? by remember { mutableStateOf(null) } + Text(text = stringResource(id = R.string.stripe_complete_with_test_data), style = MaterialTheme.typography.h4) Html( html = stringResource(id = R.string.stripe_complete_with_test_data_details), modifier = Modifier.padding(vertical = 8.dp) ) - CompleteOptionRow( content = stringResource(id = R.string.stripe_verification_success), selected = completeOption == SUCCESS, @@ -239,7 +208,6 @@ internal fun CompleteWithTestDataSection( submitState = LoadingButtonState.Idle } ) - LoadingButton( text = stringResource(id = R.string.stripe_submit), state = submitState, @@ -345,6 +313,10 @@ private fun CompleteOptionRow( } } +internal enum class CompleteOption { + SUCCESS, FAILURE, SUCCESS_ASYNC, FAILURE_ASYNC +} + internal const val TEST_TAG_CANCELLED_BUTTON = "Cancelled" internal const val TEST_TAG_FAILED_BUTTON = "Failed" internal const val TEST_TAG_PROCEED_BUTTON = "Proceed" From 9683b40da787795966c9b6e910a961a4e51167bb Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Thu, 4 May 2023 20:48:19 -0700 Subject: [PATCH 3/7] move strings --- identity/res/values/totranslate.xml | 18 ------------------ identity/res/values/untranslatable.xml | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/identity/res/values/totranslate.xml b/identity/res/values/totranslate.xml index fad13724aac..5686cd72dd8 100644 --- a/identity/res/values/totranslate.xml +++ b/identity/res/values/totranslate.xml @@ -6,22 +6,4 @@ This page is only shown in testmode. Complete with test data Save time by choosing a desired result and completing instantly with that outcome. The mobile SDK flow will return with result Completed. - - Verification success - Verification failure - Verification success async - Verification failure async - SUBMIT - - Terminate mobile SDK flow - Terminate the mobile SDK flow locally with Cancelled or Failed. - - Failure from test mode - COMPLETED - CANCELLED - FAILED - - Preview user experience - Proceed to preview as an end user. Information provided will not be verified. - PROCEED \ No newline at end of file diff --git a/identity/res/values/untranslatable.xml b/identity/res/values/untranslatable.xml index 311b80bf0a2..2248651ede9 100644 --- a/identity/res/values/untranslatable.xml +++ b/identity/res/values/untranslatable.xml @@ -1,4 +1,20 @@ MM / DD / YYYY + Verification success + Verification failure + Verification success async + Verification failure async + SUBMIT + + Terminate mobile SDK flow + Terminate the mobile SDK flow locally with Cancelled or Failed. + + Failure from test mode + CANCELLED + FAILED + + Preview user experience + Proceed to preview as an end user. Information provided will not be verified. + PROCEED \ No newline at end of file From 9ddc9a75aa9289b297f91770a4ed62b892e6aa93 Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Thu, 4 May 2023 21:16:03 -0700 Subject: [PATCH 4/7] resolve comments --- .../identity/navigation/ErrorDestination.kt | 2 +- .../models/VerificationReportRequirement.kt | 35 --------- .../stripe/android/identity/ui/DebugScreen.kt | 24 ++++-- .../identity/ui/StyledClickableText.kt | 75 ------------------- 4 files changed, 18 insertions(+), 118 deletions(-) delete mode 100644 identity/src/main/java/com/stripe/android/identity/networking/models/VerificationReportRequirement.kt delete mode 100644 identity/src/main/java/com/stripe/android/identity/ui/StyledClickableText.kt diff --git a/identity/src/main/java/com/stripe/android/identity/navigation/ErrorDestination.kt b/identity/src/main/java/com/stripe/android/identity/navigation/ErrorDestination.kt index d23f6d8711f..393cf589626 100644 --- a/identity/src/main/java/com/stripe/android/identity/navigation/ErrorDestination.kt +++ b/identity/src/main/java/com/stripe/android/identity/navigation/ErrorDestination.kt @@ -40,7 +40,7 @@ internal class ErrorDestination( // If this happens, set the back button destination to [DEFAULT_BACK_BUTTON_DESTINATION] const val UNEXPECTED_ROUTE = "UnexpectedRoute" - val TAG = ErrorDestination::class.java.simpleName + const val TAG = "ErrorDestination" fun errorTitle(backStackEntry: NavBackStackEntry) = backStackEntry.getStringArgument(ARG_ERROR_TITLE) diff --git a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationReportRequirement.kt b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationReportRequirement.kt deleted file mode 100644 index 2cb5d961b62..00000000000 --- a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationReportRequirement.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.stripe.android.identity.networking.models - -import kotlinx.serialization.SerialName - -internal enum class VerificationReportRequirement(val value: String) { - @SerialName("generic") - GENERIC("generic"), - - @SerialName("address") - ADDRESS("address"), - - @SerialName("dob") - DOB("dob"), - - @SerialName("document") - DOCUMENT("document"), - - @SerialName("email") - EMAIL("email"), - - @SerialName("id_number") - ID_NUMBER("id_number"), - - @SerialName("phone") - PHONE("phone"), - - @SerialName("phone_otp") - PHONE_OTP("phone_otp"), - - @SerialName("phone_records") - PHONE_RECORDS("phone_records"), - - @SerialName("selfie") - SELFIE("selfie") -} diff --git a/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt b/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt index d258f98e704..63f7ad30727 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt @@ -1,6 +1,7 @@ package com.stripe.android.identity.ui import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -163,7 +164,10 @@ internal fun CompleteWithTestDataSection( ) { var submitState: LoadingButtonState by remember { mutableStateOf(LoadingButtonState.Disabled) } var completeOption: CompleteOption? by remember { mutableStateOf(null) } - Text(text = stringResource(id = R.string.stripe_complete_with_test_data), style = MaterialTheme.typography.h4) + Text( + text = stringResource(id = R.string.stripe_complete_with_test_data), + style = MaterialTheme.typography.h4 + ) Html( html = stringResource(id = R.string.stripe_complete_with_test_data_details), modifier = Modifier.padding(vertical = 8.dp) @@ -297,18 +301,24 @@ private fun CompleteOptionRow( testTag: String, onClick: () -> Unit ) { - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Row( + modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxWidth() + .clickable(enabled = enabled, onClick = onClick), + verticalAlignment = Alignment.CenterVertically + ) { RadioButton( selected = selected, enabled = enabled, modifier = Modifier.testTag(testTag), - onClick = onClick + onClick = null ) - StyledClickableText( - text = AnnotatedString(content), + + Text( + text = content, modifier = Modifier.padding(start = 8.dp), - enabled = enabled, - onClick = { onClick() } + style = MaterialTheme.typography.button ) } } diff --git a/identity/src/main/java/com/stripe/android/identity/ui/StyledClickableText.kt b/identity/src/main/java/com/stripe/android/identity/ui/StyledClickableText.kt deleted file mode 100644 index 9296fdc5697..00000000000 --- a/identity/src/main/java/com/stripe/android/identity/ui/StyledClickableText.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.stripe.android.identity.ui - -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.LocalContentAlpha -import androidx.compose.material.LocalContentColor -import androidx.compose.material.LocalTextStyle -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.takeOrElse -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.unit.TextUnit - -@Composable -internal fun StyledClickableText( - text: AnnotatedString, - modifier: Modifier = Modifier, - enabled: Boolean = true, - color: Color = Color.Unspecified, - fontSize: TextUnit = TextUnit.Unspecified, - fontStyle: FontStyle? = null, - fontWeight: FontWeight? = null, - fontFamily: FontFamily? = null, - letterSpacing: TextUnit = TextUnit.Unspecified, - textDecoration: TextDecoration? = null, - textAlign: TextAlign? = null, - lineHeight: TextUnit = TextUnit.Unspecified, - softWrap: Boolean = true, - maxLines: Int = Int.MAX_VALUE, - onTextLayout: (TextLayoutResult) -> Unit = {}, - style: TextStyle = LocalTextStyle.current, - onClick: (Int) -> Unit -) { - val textColor = color.takeOrElse { - style.color.takeOrElse { - LocalContentColor.current.copy(alpha = LocalContentAlpha.current) - } - } - // NOTE(text-perf-review): It might be worthwhile writing a bespoke merge implementation that - // will avoid reallocating if all of the options here are the defaults - val mergedStyle = style.merge( - TextStyle( - color = textColor, - fontSize = fontSize, - fontWeight = fontWeight, - textAlign = textAlign, - lineHeight = lineHeight, - fontFamily = fontFamily, - textDecoration = textDecoration, - fontStyle = fontStyle, - letterSpacing = letterSpacing - ) - ) - - ClickableText( - text = text, - modifier = modifier, - style = mergedStyle, - softWrap = softWrap, - maxLines = maxLines, - onTextLayout = onTextLayout, - onClick = if (enabled) { - onClick - } else { - { } - } - ) -} From 5ce32746d17f5442f362aec88da733eacaaa727a Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Thu, 4 May 2023 21:18:27 -0700 Subject: [PATCH 5/7] detekt --- identity/detekt-baseline.xml | 1 + .../src/main/java/com/stripe/android/identity/ui/DebugScreen.kt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/identity/detekt-baseline.xml b/identity/detekt-baseline.xml index 0ac5dbb9761..bf3467bf14d 100644 --- a/identity/detekt-baseline.xml +++ b/identity/detekt-baseline.xml @@ -14,6 +14,7 @@ LongMethod:CameraScreenLaunchedEffect.kt$@Composable internal fun CameraScreenLaunchedEffect( identityViewModel: IdentityViewModel, identityScanViewModel: IdentityScanViewModel, verificationPage: VerificationPage, navController: NavController, cameraManager: IdentityCameraManager, onCameraReady: () -> Unit ) LongMethod:ConfirmationScreen.kt$@Composable internal fun ConfirmationScreen( navController: NavController, identityViewModel: IdentityViewModel, verificationFlowFinishable: VerificationFlowFinishable ) LongMethod:ConsentScreen.kt$@Composable private fun SuccessUI( merchantLogoUri: Uri, verificationPage: VerificationPage, onConsentAgreed: () -> Unit, onConsentDeclined: () -> Unit ) + LongMethod:DebugScreen.kt$@Composable internal fun CompleteWithTestDataSection( onClickSubmit: (CompleteOption) -> Unit ) LongMethod:DebugScreen.kt$@Composable internal fun DebugScreen( navController: NavController, identityViewModel: IdentityViewModel, verificationFlowFinishable: VerificationFlowFinishable ) LongMethod:DocSelectionScreen.kt$@Composable internal fun DocSelectionScreen( navController: NavController, identityViewModel: IdentityViewModel, cameraPermissionEnsureable: CameraPermissionEnsureable ) LongMethod:DocumenetScanScreen.kt$@Composable internal fun DocumentScanScreen( navController: NavController, identityViewModel: IdentityViewModel, identityScanViewModel: IdentityScanViewModel, frontScanType: IdentityScanState.ScanType, backScanType: IdentityScanState.ScanType?, shouldStartFromBack: Boolean, messageRes: DocumentScanMessageRes, collectedDataParamType: CollectedDataParam.Type, route: String ) diff --git a/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt b/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt index 63f7ad30727..6e26800ecdd 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.stripe.android.identity.IdentityVerificationSheet From bb6173068943d8fe711d85d25d2b1030261425cd Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Thu, 4 May 2023 21:31:25 -0700 Subject: [PATCH 6/7] add changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef102c5336a..eb0ecfe1b25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ * [CHANGED][6635](https://github.com/stripe/stripe-android/pull/6635) Use non transitive R classes. * [CHANGED][6676](https://github.com/stripe/stripe-android/pull/6676) Updated Compose BOM to 2023.05.00. +### Identity +* [ADDED][6642](https://github.com/stripe/stripe-android/pull/6642) Support Test mode M1. + ## 20.24.2 - 2023-05-03 ### Payments From 74613dfe71c7738c76f52c8e33341cc7a3a3e988 Mon Sep 17 00:00:00 2001 From: Chen Cen Date: Fri, 5 May 2023 10:32:52 -0700 Subject: [PATCH 7/7] fix test --- .../main/java/com/stripe/android/identity/ui/DebugScreen.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt b/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt index 6e26800ecdd..a3a0f7be6b7 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/DebugScreen.kt @@ -87,6 +87,7 @@ internal fun DebugScreen( ) } } + FAILURE -> { coroutineScope.launch { identityViewModel.unverifySessionAndTransition( @@ -96,6 +97,7 @@ internal fun DebugScreen( ) } } + SUCCESS_ASYNC -> { coroutineScope.launch { identityViewModel.verifySessionAndTransition( @@ -105,6 +107,7 @@ internal fun DebugScreen( ) } } + FAILURE_ASYNC -> { coroutineScope.launch { identityViewModel.unverifySessionAndTransition( @@ -302,6 +305,7 @@ private fun CompleteOptionRow( ) { Row( modifier = Modifier + .testTag(testTag) .padding(vertical = 8.dp) .fillMaxWidth() .clickable(enabled = enabled, onClick = onClick), @@ -310,7 +314,6 @@ private fun CompleteOptionRow( RadioButton( selected = selected, enabled = enabled, - modifier = Modifier.testTag(testTag), onClick = null )