From 971e015b911b88381edbbb7a359c9c66a2d338e4 Mon Sep 17 00:00:00 2001 From: Chen Cen <79880926+ccen-stripe@users.noreply.github.com> Date: Wed, 11 Oct 2023 11:04:39 -0700 Subject: [PATCH] [Identity] - support consent page for Butter 2.0 (#7412) --- identity/api/identity.api | 15 + identity/detekt-baseline.xml | 6 +- .../identity/networking/NetworkConstants.kt | 2 +- .../networking/models/VerificationPage.kt | 4 +- .../models/VerificationPageIconType.kt | 3 +- ...erificationPageStaticConsentLineContent.kt | 15 + ...erificationPageStaticContentConsentPage.kt | 8 +- ...nPageStaticContentIndividualWelcomePage.kt | 6 +- .../android/identity/ui/BottomsheetHTML.kt | 71 +++ .../android/identity/ui/ConfirmationScreen.kt | 11 +- .../android/identity/ui/ConsentLines.kt | 61 +++ .../android/identity/ui/ConsentScreen.kt | 230 +++------ .../identity/ui/ConsentWelcomeHeader.kt | 108 +++++ .../identity/ui/IndividualWelcomeScreen.kt | 214 +++----- .../identity/viewmodel/IdentityViewModel.kt | 14 + .../DefaultIdentityRepositoryTest.kt | 34 +- .../IdentityNetworkResponseFixtures.kt | 457 ++++++++++++++---- .../identity/ui/ConfirmationScreenTest.kt | 8 +- .../android/identity/ui/ConsentScreenTest.kt | 75 ++- .../ui/IndividualWelcomeScreenTest.kt | 25 +- 20 files changed, 884 insertions(+), 483 deletions(-) create mode 100644 identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticConsentLineContent.kt create mode 100644 identity/src/main/java/com/stripe/android/identity/ui/BottomsheetHTML.kt create mode 100644 identity/src/main/java/com/stripe/android/identity/ui/ConsentLines.kt create mode 100644 identity/src/main/java/com/stripe/android/identity/ui/ConsentWelcomeHeader.kt diff --git a/identity/api/identity.api b/identity/api/identity.api index f101ba51c83..30a34f374d0 100644 --- a/identity/api/identity.api +++ b/identity/api/identity.api @@ -221,6 +221,14 @@ public final class com/stripe/android/identity/networking/models/VerificationPag public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/stripe/android/identity/networking/models/VerificationPageStaticConsentLineContent$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/identity/networking/models/VerificationPageStaticConsentLineContent; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/stripe/android/identity/networking/models/VerificationPageStaticConsentLineContent; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/stripe/android/identity/networking/models/VerificationPageStaticContentBottomSheetContent$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/stripe/android/identity/networking/models/VerificationPageStaticContentBottomSheetContent; @@ -334,6 +342,13 @@ public final class com/stripe/android/identity/ui/ComposableSingletons$BottomShe public final fun getLambda-2$identity_release ()Lkotlin/jvm/functions/Function2; } +public final class com/stripe/android/identity/ui/ComposableSingletons$ConsentScreenKt { + public static final field INSTANCE Lcom/stripe/android/identity/ui/ComposableSingletons$ConsentScreenKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$identity_release ()Lkotlin/jvm/functions/Function2; +} + public final class com/stripe/android/identity/ui/ComposableSingletons$DebugScreenKt { public static final field INSTANCE Lcom/stripe/android/identity/ui/ComposableSingletons$DebugScreenKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; diff --git a/identity/detekt-baseline.xml b/identity/detekt-baseline.xml index 5b3a7f3d51f..25cfedbb98e 100644 --- a/identity/detekt-baseline.xml +++ b/identity/detekt-baseline.xml @@ -13,8 +13,8 @@ LongMethod:AddressSection.kt$@Composable internal fun AddressSection( enabled: Boolean, identityViewModel: IdentityViewModel, addressCountries: List<Country>, addressNotListedText: String, navController: NavController, onAddressCollected: (Resource<RequiredInternationalAddress>) -> Unit ) 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 ConsentHeader( merchantLogoUri: Uri, title: String, privacyPolicy: String, timeEstimate: String ) - LongMethod:ConsentScreen.kt$@Composable private fun SuccessUI( merchantLogoUri: Uri, verificationPage: VerificationPage, onConsentAgreed: () -> Unit, onConsentDeclined: () -> Unit ) + LongMethod:ConsentScreen.kt$@Composable private fun SuccessUI( merchantLogoUri: Uri, consentPage: VerificationPageStaticContentConsentPage, bottomSheets: Map<String, VerificationPageStaticContentBottomSheetContent>?, visitedIndividualWelcomePage: Boolean, onConsentAgreed: () -> Unit, onConsentDeclined: () -> Unit ) + LongMethod:ConsentWelcomeHeader.kt$@Composable internal fun ConsentWelcomeHeader( modifier: Modifier = Modifier, merchantLogoUri: Uri, title: String?, showLogos: Boolean = true ) 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 ) @@ -78,8 +78,6 @@ TooManyFunctions:IdentityRepository.kt$IdentityRepository TooManyFunctions:IdentityViewModel.kt$IdentityViewModel : AndroidViewModel TooManyFunctions:UploadDestinations.kt$DocumentUploadDestination$Companion - TopLevelPropertyNaming:ConfirmationScreen.kt$internal const val confirmationConfirmButtonTag = "ConfirmButton" - TopLevelPropertyNaming:ConfirmationScreen.kt$internal const val confirmationTitleTag = "ConfirmationTitle" TopLevelPropertyNaming:DocSelectionScreen.kt$internal const val docSelectionTitleTag = "Title" TopLevelPropertyNaming:DocSelectionScreen.kt$internal const val singleSelectionTag = "SingleSelection" UnnecessaryAbstractClass:IdentityCommonModule.kt$IdentityCommonModule$IdentityCommonModule 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 0eb383f92dd..f72f929a66c 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 @@ -3,7 +3,7 @@ package com.stripe.android.identity.networking 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=v4" + "2020-08-27;identity_client_api=v5" internal const val SUBMIT = "submit" internal const val DATA = "data" diff --git a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPage.kt b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPage.kt index 9c1f8b6a9a4..684f5844295 100644 --- a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPage.kt +++ b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPage.kt @@ -53,7 +53,9 @@ internal data class VerificationPage( @SerialName("unsupported_client") val unsupportedClient: Boolean, @SerialName("welcome") - val welcome: VerificationPageStaticContentTextPage? = null + val welcome: VerificationPageStaticContentTextPage? = null, + @SerialName("bottomsheet") + val bottomSheet: Map? = null ) : Parcelable { @Serializable internal enum class Status { diff --git a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageIconType.kt b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageIconType.kt index 4a2bd616dc2..aac5cfa144f 100644 --- a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageIconType.kt +++ b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageIconType.kt @@ -41,7 +41,8 @@ internal fun VerificationPageIconType.getResourceId() = when (this) { VerificationPageIconType.CLOUD -> R.drawable.stripe_cloud_icon VerificationPageIconType.DOCUMENT -> R.drawable.stripe_document_icon - VerificationPageIconType.CREATE_IDENTITY_VERIFICATION -> R.drawable.stripe_create_identity_verification_icon + VerificationPageIconType.CREATE_IDENTITY_VERIFICATION -> + R.drawable.stripe_create_identity_verification_icon VerificationPageIconType.LOCK -> R.drawable.stripe_lock_icon VerificationPageIconType.MOVED -> R.drawable.stripe_moved_icon VerificationPageIconType.WALLET -> R.drawable.stripe_wallet_icon diff --git a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticConsentLineContent.kt b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticConsentLineContent.kt new file mode 100644 index 00000000000..32d9fa74fa8 --- /dev/null +++ b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticConsentLineContent.kt @@ -0,0 +1,15 @@ +package com.stripe.android.identity.networking.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@Parcelize +internal data class VerificationPageStaticConsentLineContent( + @SerialName("icon") + val icon: VerificationPageIconType, + @SerialName("content") + val content: String +) : Parcelable diff --git a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticContentConsentPage.kt b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticContentConsentPage.kt index 670815fdb63..3629cfb1575 100644 --- a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticContentConsentPage.kt +++ b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticContentConsentPage.kt @@ -10,8 +10,6 @@ import kotlinx.serialization.Serializable internal data class VerificationPageStaticContentConsentPage( @SerialName("accept_button_text") val acceptButtonText: String, - @SerialName("body") - val body: String, @SerialName("decline_button_text") val declineButtonText: String, @SerialName("scroll_to_continue_button_text") @@ -19,7 +17,7 @@ internal data class VerificationPageStaticContentConsentPage( @SerialName("title") val title: String?, @SerialName("privacy_policy") - val privacyPolicy: String?, - @SerialName("time_estimate") - val timeEstimate: String? + val privacyPolicy: String, + @SerialName("lines") + val lines: List ) : Parcelable diff --git a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticContentIndividualWelcomePage.kt b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticContentIndividualWelcomePage.kt index fcc6f438b35..8eee1038a81 100644 --- a/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticContentIndividualWelcomePage.kt +++ b/identity/src/main/java/com/stripe/android/identity/networking/models/VerificationPageStaticContentIndividualWelcomePage.kt @@ -10,12 +10,10 @@ import kotlinx.serialization.Serializable internal data class VerificationPageStaticContentIndividualWelcomePage( @SerialName("get_started_button_text") val getStartedButtonText: String, - @SerialName("body") - val body: String, @SerialName("title") val title: String, @SerialName("privacy_policy") val privacyPolicy: String, - @SerialName("time_estimate") - val timeEstimate: String + @SerialName("lines") + val lines: List ) : Parcelable diff --git a/identity/src/main/java/com/stripe/android/identity/ui/BottomsheetHTML.kt b/identity/src/main/java/com/stripe/android/identity/ui/BottomsheetHTML.kt new file mode 100644 index 00000000000..35d2e7a9b13 --- /dev/null +++ b/identity/src/main/java/com/stripe/android/identity/ui/BottomsheetHTML.kt @@ -0,0 +1,71 @@ +package com.stripe.android.identity.ui + +import android.content.Intent +import android.net.Uri +import android.util.Log +import android.webkit.URLUtil +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextDecoration +import androidx.lifecycle.viewmodel.compose.viewModel +import com.stripe.android.identity.networking.STRIPE_BOTTOM_SHEET +import com.stripe.android.identity.networking.models.VerificationPageStaticContentBottomSheetContent +import com.stripe.android.identity.viewmodel.BottomSheetViewModel +import com.stripe.android.uicore.text.HtmlWithCustomOnClick + +/** + * Draw Html with the ability to open a web link or bottomsheet + */ +@Composable +@ExperimentalMaterialApi +internal fun BottomSheetHTML( + html: String, + modifier: Modifier = Modifier, + bottomSheets: Map?, + color: Color = Color.Unspecified, + style: TextStyle, + urlSpanStyle: SpanStyle = SpanStyle(textDecoration = TextDecoration.Underline) +) { + val context = LocalContext.current + val bottomSheetViewModel = viewModel() + HtmlWithCustomOnClick( + html = html, + modifier = modifier, + color = color, + style = style, + urlSpanStyle = urlSpanStyle + ) { annotatedStringRanges -> + annotatedStringRanges.firstOrNull()?.item?.let { urlString -> + when { + (URLUtil.isNetworkUrl(urlString)) -> { + val openURL = Intent(Intent.ACTION_VIEW) + openURL.data = Uri.parse(urlString) + context.startActivity(openURL) + } + + urlString.startsWith(STRIPE_BOTTOM_SHEET) -> { + val bottomSheetId = urlString.substringAfterLast('/') + bottomSheets?.get(bottomSheetId)?.let { bottomSheetContent -> + bottomSheetViewModel.showBottomSheet(bottomSheetContent) + } ?: run { + Log.e( + BottomSheetHTMLTAG, + "Fail to present buttomsheet with id $bottomSheetId" + ) + } + } + + else -> { + Log.e(BottomSheetHTMLTAG, "unknown url string: $urlString") + } + } + } + } +} + +internal const val BottomSheetHTMLTAG = "BottomSheetHTML" diff --git a/identity/src/main/java/com/stripe/android/identity/ui/ConfirmationScreen.kt b/identity/src/main/java/com/stripe/android/identity/ui/ConfirmationScreen.kt index 76101ce7a01..16caa95df93 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/ConfirmationScreen.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/ConfirmationScreen.kt @@ -102,7 +102,7 @@ internal fun ConfirmationScreen( vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin) ) .semantics { - testTag = confirmationTitleTag + testTag = CONFIRMATION_TITLE_TAG }, fontSize = 24.sp, fontWeight = FontWeight.Bold @@ -113,7 +113,7 @@ internal fun ConfirmationScreen( modifier = Modifier .padding(bottom = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) .semantics { - testTag = BODY_TAG + testTag = CONFIRMATION_BODY_TAG }, color = MaterialTheme.colors.onBackground, urlSpanStyle = SpanStyle( @@ -132,7 +132,7 @@ internal fun ConfirmationScreen( modifier = Modifier .fillMaxWidth() .semantics { - testTag = confirmationConfirmButtonTag + testTag = CONFIRMATION_BUTTON_TAG } ) { Text(text = successPage.buttonText.uppercase()) @@ -141,5 +141,6 @@ internal fun ConfirmationScreen( } } -internal const val confirmationTitleTag = "ConfirmationTitle" -internal const val confirmationConfirmButtonTag = "ConfirmButton" +internal const val CONFIRMATION_TITLE_TAG = "ConfirmationTitle" +internal const val CONFIRMATION_BUTTON_TAG = "ConfirmButton" +internal const val CONFIRMATION_BODY_TAG = "Body" diff --git a/identity/src/main/java/com/stripe/android/identity/ui/ConsentLines.kt b/identity/src/main/java/com/stripe/android/identity/ui/ConsentLines.kt new file mode 100644 index 00000000000..657f12746d8 --- /dev/null +++ b/identity/src/main/java/com/stripe/android/identity/ui/ConsentLines.kt @@ -0,0 +1,61 @@ +package com.stripe.android.identity.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.SpanStyle +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.stripe.android.identity.R +import com.stripe.android.identity.networking.models.VerificationPageStaticConsentLineContent +import com.stripe.android.identity.networking.models.VerificationPageStaticContentBottomSheetContent +import com.stripe.android.identity.networking.models.getContentDescriptionId +import com.stripe.android.identity.networking.models.getResourceId + +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun ConsentLines( + lines: List, + bottomSheets: Map? +) { + for (line in lines) { + Row( + modifier = Modifier + .testTag(CONSENT_LINE_TAG) + .padding(top = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) + ) { + Image( + painter = painterResource(id = line.icon.getResourceId()), + modifier = Modifier + .size(28.dp) + .padding(end = 8.dp), + contentDescription = stringResource(id = line.icon.getContentDescriptionId()) + ) + BottomSheetHTML( + html = line.content, + color = MaterialTheme.colors.onSurface.copy( + alpha = 0.6f + ), + style = LocalTextStyle.current.merge(fontSize = 16.sp), + bottomSheets = bottomSheets, + urlSpanStyle = SpanStyle( + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colors.secondary + ) + ) + } + } +} + +internal const val CONSENT_LINE_TAG = "consentLineTag" diff --git a/identity/src/main/java/com/stripe/android/identity/ui/ConsentScreen.kt b/identity/src/main/java/com/stripe/android/identity/ui/ConsentScreen.kt index d08d21157f1..4d3addeda78 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/ConsentScreen.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/ConsentScreen.kt @@ -1,21 +1,16 @@ package com.stripe.android.identity.ui import android.net.Uri -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -27,15 +22,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext 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.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.stripe.android.identity.R import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory.Companion.SCREEN_NAME_CONSENT @@ -43,24 +35,18 @@ import com.stripe.android.identity.navigation.ConsentDestination import com.stripe.android.identity.navigation.navigateToErrorScreenWithDefaultValues import com.stripe.android.identity.networking.Resource import com.stripe.android.identity.networking.models.CollectedDataParam -import com.stripe.android.identity.networking.models.VerificationPage import com.stripe.android.identity.networking.models.VerificationPage.Companion.requireSelfie -import com.stripe.android.identity.utils.isRemote -import com.stripe.android.identity.utils.urlWithoutQuery +import com.stripe.android.identity.networking.models.VerificationPageIconType +import com.stripe.android.identity.networking.models.VerificationPageStaticConsentLineContent +import com.stripe.android.identity.networking.models.VerificationPageStaticContentBottomSheetContent +import com.stripe.android.identity.networking.models.VerificationPageStaticContentConsentPage import com.stripe.android.identity.viewmodel.IdentityViewModel -import com.stripe.android.uicore.image.StripeImage -import com.stripe.android.uicore.image.StripeImageLoader -import com.stripe.android.uicore.image.getDrawableFromUri -import com.stripe.android.uicore.image.rememberDrawablePainter import com.stripe.android.uicore.text.Html import kotlinx.coroutines.launch internal const val TITLE_TAG = "Title" -internal const val TIME_ESTIMATE_TAG = "TimeEstimate" internal const val CONSENT_HEADER_TAG = "ConsentHeader" internal const val PRIVACY_POLICY_TAG = "PrivacyPolicy" -internal const val DIVIDER_TAG = "divider" -internal const val BODY_TAG = "Body" internal const val ACCEPT_BUTTON_TAG = "Accept" internal const val DECLINE_BUTTON_TAG = "Decline" internal const val LOADING_SCREEN_TAG = "Loading" @@ -83,6 +69,8 @@ internal fun ConsentScreen( } ) { val verificationPage = remember { it } + val visitedIndividualWelcomePage by + identityViewModel.visitedIndividualWelcomeScreen.collectAsState() LaunchedEffect(Unit) { identityViewModel.updateAnalyticsState { oldState -> oldState.copy( @@ -96,7 +84,9 @@ internal fun ConsentScreen( ) SuccessUI( identityViewModel.verificationArgs.brandLogo, - verificationPage, + verificationPage.biometricConsent, + verificationPage.bottomSheet, + visitedIndividualWelcomePage, onConsentAgreed = { coroutineScope.launch { identityViewModel.postVerificationPageDataAndMaybeNavigate( @@ -126,11 +116,12 @@ internal fun ConsentScreen( @Composable private fun SuccessUI( merchantLogoUri: Uri, - verificationPage: VerificationPage, + consentPage: VerificationPageStaticContentConsentPage, + bottomSheets: Map?, + visitedIndividualWelcomePage: Boolean, onConsentAgreed: () -> Unit, onConsentDeclined: () -> Unit ) { - val consentPage = verificationPage.biometricConsent Column( modifier = Modifier .fillMaxSize() @@ -139,7 +130,8 @@ private fun SuccessUI( end = dimensionResource(id = R.dimen.stripe_page_horizontal_margin), top = dimensionResource(id = R.dimen.stripe_page_vertical_margin), bottom = dimensionResource(id = R.dimen.stripe_page_vertical_margin) - ) + ), + horizontalAlignment = Alignment.CenterHorizontally ) { val scrollState = rememberScrollState() Column( @@ -150,28 +142,13 @@ private fun SuccessUI( testTag = SCROLLABLE_COLUMN_TAG } ) { - if (consentPage.title != null && consentPage.privacyPolicy != null && consentPage.timeEstimate != null) { - ConsentHeader( - merchantLogoUri = merchantLogoUri, - title = consentPage.title, - privacyPolicy = consentPage.privacyPolicy, - timeEstimate = consentPage.timeEstimate - ) - } - - Html( - html = consentPage.body, - modifier = Modifier - .padding(bottom = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) - .semantics { - testTag = BODY_TAG - }, - color = MaterialTheme.colors.onBackground, - urlSpanStyle = SpanStyle( - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.secondary - ) + ConsentWelcomeHeader( + modifier = Modifier.testTag(CONSENT_HEADER_TAG), + merchantLogoUri = merchantLogoUri, + title = consentPage.title, + showLogos = visitedIndividualWelcomePage.not() ) + ConsentLines(lines = consentPage.lines, bottomSheets = bottomSheets) } var acceptState by remember { mutableStateOf(LoadingButtonState.Idle) } @@ -184,6 +161,20 @@ private fun SuccessUI( } } + Html( + html = consentPage.privacyPolicy, + modifier = Modifier + .padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) + .semantics { + testTag = PRIVACY_POLICY_TAG + }, + color = MaterialTheme.colors.onBackground, + urlSpanStyle = SpanStyle( + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colors.secondary + ) + ) + LoadingButton( modifier = Modifier .padding(bottom = 10.dp) @@ -218,119 +209,46 @@ private fun SuccessUI( } } +@Preview @Composable -private fun ConsentHeader( - merchantLogoUri: Uri, - title: String, - privacyPolicy: String, - timeEstimate: String -) { - Row( - modifier = Modifier - .fillMaxWidth() - .testTag(CONSENT_HEADER_TAG), - verticalAlignment = Alignment.CenterVertically - ) { - if (merchantLogoUri.isRemote()) { - val localContext = LocalContext.current - val imageLoader = remember(merchantLogoUri) { - StripeImageLoader(localContext) - } - StripeImage( - url = merchantLogoUri.urlWithoutQuery(), - imageLoader = imageLoader, - contentDescription = stringResource(id = R.string.stripe_description_merchant_logo), - modifier = Modifier - .width(32.dp) - .height(32.dp) - ) - } else { - Image( - painter = rememberDrawablePainter( - LocalContext.current.getDrawableFromUri( - merchantLogoUri +@ExperimentalMaterialApi +internal fun ConsentPreview() { + IdentityPreview { + SuccessUI( + merchantLogoUri = Uri.EMPTY, + consentPage = VerificationPageStaticContentConsentPage( + acceptButtonText = "Accept", + declineButtonText = "Decline", + scrollToContinueButtonText = "scroll to button", + title = "Tora's cat food works with Stripe to verify your identity", + privacyPolicy = "Stripe Privacy Policy • " + + "Tora's cat food Privacy Policy", + lines = listOf( + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.PHONE, + content = "This is the line content with phone icon" + ), + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.CAMERA, + content = "This is the line content with camera icon" + ), + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.CLOUD, + content = "This is the line content with cloud icon and a " + + "web link" + ), + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.WALLET, + content = "This is the line content with wallet icon and a " + + "bottomsheet link" ) - ), - modifier = Modifier - .width(32.dp) - .height(32.dp), - contentDescription = stringResource(id = R.string.stripe_description_merchant_logo) - ) - } - Image( - painter = painterResource(id = R.drawable.stripe_plus_icon), - modifier = Modifier - .width(16.dp) - .height(16.dp), - contentDescription = stringResource(id = R.string.stripe_description_plus) - ) - - Image( - painter = painterResource(id = R.drawable.stripe_square), - modifier = Modifier - .width(32.dp) - .height(32.dp), - contentDescription = stringResource(id = R.string.stripe_description_stripe_logo) - ) - } - Text( - text = title, - modifier = Modifier - .fillMaxWidth() - .padding( - vertical = dimensionResource( - id = R.dimen.stripe_item_vertical_margin ) - ) - .semantics { - testTag = TITLE_TAG - }, - fontSize = 24.sp, - fontWeight = FontWeight.Bold - ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 12.dp) - ) { - Image( - painter = painterResource(id = R.drawable.stripe_time_estimate_icon), - contentDescription = stringResource(id = R.string.stripe_description_time_estimate) - ) - Html( - html = timeEstimate, - modifier = Modifier - .fillMaxWidth() - .padding(start = 6.dp) - .semantics { - testTag = TIME_ESTIMATE_TAG - }, - color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f), - urlSpanStyle = SpanStyle( - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.secondary - ) + ), + visitedIndividualWelcomePage = false, + bottomSheets = mapOf(), + onConsentAgreed = {}, + onConsentDeclined = {} ) } - Html( - html = privacyPolicy, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) - .semantics { - testTag = PRIVACY_POLICY_TAG - }, - color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f), - urlSpanStyle = SpanStyle( - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.secondary - ) - ) - Divider( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) - .semantics { - testTag = DIVIDER_TAG - } - ) } diff --git a/identity/src/main/java/com/stripe/android/identity/ui/ConsentWelcomeHeader.kt b/identity/src/main/java/com/stripe/android/identity/ui/ConsentWelcomeHeader.kt new file mode 100644 index 00000000000..f72a1f5abe9 --- /dev/null +++ b/identity/src/main/java/com/stripe/android/identity/ui/ConsentWelcomeHeader.kt @@ -0,0 +1,108 @@ +package com.stripe.android.identity.ui + +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.stripe.android.identity.R +import com.stripe.android.identity.utils.isRemote +import com.stripe.android.identity.utils.urlWithoutQuery +import com.stripe.android.uicore.image.StripeImage +import com.stripe.android.uicore.image.StripeImageLoader +import com.stripe.android.uicore.image.getDrawableFromUri +import com.stripe.android.uicore.image.rememberDrawablePainter + +@Composable +internal fun ConsentWelcomeHeader( + modifier: Modifier = Modifier, + merchantLogoUri: Uri, + title: String?, + showLogos: Boolean = true +) { + if (showLogos) { + Row( + modifier = modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + if (merchantLogoUri.isRemote()) { + val localContext = LocalContext.current + val imageLoader = remember(merchantLogoUri) { + StripeImageLoader(localContext) + } + StripeImage( + url = merchantLogoUri.urlWithoutQuery(), + imageLoader = imageLoader, + contentDescription = stringResource(id = R.string.stripe_description_merchant_logo), + modifier = Modifier + .width(64.dp) + .height(64.dp) + ) + } else { + Image( + painter = rememberDrawablePainter( + LocalContext.current.getDrawableFromUri( + merchantLogoUri + ) + ), + modifier = Modifier + .width(64.dp) + .height(64.dp), + contentDescription = stringResource(id = R.string.stripe_description_merchant_logo) + ) + } + Image( + painter = painterResource(id = R.drawable.stripe_plus_icon), + modifier = Modifier + .width(16.dp) + .height(16.dp), + contentDescription = stringResource(id = R.string.stripe_description_plus) + ) + + Image( + painter = painterResource(id = R.drawable.stripe_square), + modifier = Modifier + .width(64.dp) + .height(64.dp), + contentDescription = stringResource(id = R.string.stripe_description_stripe_logo) + ) + } + } + Text( + text = title ?: "", + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = dimensionResource( + id = R.dimen.stripe_item_vertical_margin + ) + ) + .semantics { + testTag = TITLE_TAG + }, + fontSize = 26.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + + ) +} diff --git a/identity/src/main/java/com/stripe/android/identity/ui/IndividualWelcomeScreen.kt b/identity/src/main/java/com/stripe/android/identity/ui/IndividualWelcomeScreen.kt index b094fce88e2..f796ff95a48 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/IndividualWelcomeScreen.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/IndividualWelcomeScreen.kt @@ -1,50 +1,33 @@ package com.stripe.android.identity.ui import android.net.Uri -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.stripe.android.identity.R import com.stripe.android.identity.analytics.IdentityAnalyticsRequestFactory.Companion.SCREEN_NAME_INDIVIDUAL_WELCOME import com.stripe.android.identity.navigation.IndividualDestination import com.stripe.android.identity.navigation.navigateTo +import com.stripe.android.identity.networking.models.VerificationPageStaticContentBottomSheetContent import com.stripe.android.identity.networking.models.VerificationPageStaticContentIndividualWelcomePage -import com.stripe.android.identity.utils.isRemote -import com.stripe.android.identity.utils.urlWithoutQuery import com.stripe.android.identity.viewmodel.IdentityViewModel -import com.stripe.android.uicore.image.StripeImage -import com.stripe.android.uicore.image.StripeImageLoader -import com.stripe.android.uicore.image.getDrawableFromUri -import com.stripe.android.uicore.image.rememberDrawablePainter import com.stripe.android.uicore.text.Html @Composable @@ -59,171 +42,92 @@ internal fun IndividualWelcomeScreen( val individualWelcomePage = verificationPage.individualWelcome val merchantLogoUri = identityViewModel.verificationArgs.brandLogo - var acceptState by remember { mutableStateOf(LoadingButtonState.Idle) } - val scrollState = rememberScrollState() ScreenTransitionLaunchedEffect( identityViewModel = identityViewModel, screenName = SCREEN_NAME_INDIVIDUAL_WELCOME ) - Column( - modifier = Modifier - .fillMaxSize() - .padding( - start = dimensionResource(id = R.dimen.stripe_page_horizontal_margin), - end = dimensionResource(id = R.dimen.stripe_page_horizontal_margin), - top = dimensionResource(id = R.dimen.stripe_page_vertical_margin), - bottom = dimensionResource(id = R.dimen.stripe_page_vertical_margin) - ) - ) { - Column( - modifier = Modifier - .weight(1f) - .verticalScroll(scrollState) - ) { - WelcomeHeader(merchantLogoUri, individualWelcomePage) - WelcomeBody(individualWelcomePage) - } - - LoadingButton( - modifier = Modifier - .semantics { testTag = INDIVIDUAL_WELCOME_GET_STARTED_BUTTON_TAG }, - text = individualWelcomePage.getStartedButtonText.uppercase(), - state = acceptState - ) { - acceptState = LoadingButtonState.Disabled - navController.navigateTo(IndividualDestination) - } + LaunchedEffect(Unit) { + identityViewModel.visitedIndividualWelcome() } + + SuccessUI( + merchantLogoUri = merchantLogoUri, + welcomePage = individualWelcomePage, + bottomSheets = verificationPage.bottomSheet, + navController = navController + ) } } @Composable -private fun WelcomeHeader( +private fun SuccessUI( merchantLogoUri: Uri, - individualWelcomePage: VerificationPageStaticContentIndividualWelcomePage + welcomePage: VerificationPageStaticContentIndividualWelcomePage, + bottomSheets: Map?, + navController: NavController, ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + Column( + modifier = Modifier + .fillMaxSize() + .padding( + start = dimensionResource(id = R.dimen.stripe_page_horizontal_margin), + end = dimensionResource(id = R.dimen.stripe_page_horizontal_margin), + top = dimensionResource(id = R.dimen.stripe_page_vertical_margin), + bottom = dimensionResource(id = R.dimen.stripe_page_vertical_margin) + ), + horizontalAlignment = Alignment.CenterHorizontally ) { - if (merchantLogoUri.isRemote()) { - val localContext = LocalContext.current - val imageLoader = remember(merchantLogoUri) { - StripeImageLoader(localContext) - } - StripeImage( - url = merchantLogoUri.urlWithoutQuery(), - imageLoader = imageLoader, - contentDescription = stringResource(id = R.string.stripe_description_merchant_logo), - modifier = Modifier - .width(32.dp) - .height(32.dp) - ) - } else { - Image( - painter = rememberDrawablePainter( - LocalContext.current.getDrawableFromUri(merchantLogoUri) - ), - modifier = Modifier - .width(32.dp) - .height(32.dp), - contentDescription = stringResource(id = R.string.stripe_description_merchant_logo) + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(scrollState) + .semantics { + testTag = SCROLLABLE_COLUMN_TAG + } + ) { + ConsentWelcomeHeader( + merchantLogoUri = merchantLogoUri, + title = welcomePage.title ) + ConsentLines(lines = welcomePage.lines, bottomSheets = bottomSheets) } - Image( - painter = painterResource(id = R.drawable.stripe_plus_icon), - modifier = Modifier - .width(16.dp) - .height(16.dp), - contentDescription = stringResource(id = R.string.stripe_description_plus) - ) - Image( - painter = painterResource(id = R.drawable.stripe_square), - modifier = Modifier - .width(32.dp) - .height(32.dp), - contentDescription = stringResource(id = R.string.stripe_description_stripe_logo) - ) - } - Text( - text = individualWelcomePage.title, - modifier = Modifier - .fillMaxWidth() - .padding( - vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin) - ) - .semantics { - testTag = INDIVIDUAL_WELCOME_TITLE_TAG - }, - fontSize = 24.sp, - fontWeight = FontWeight.Bold - ) -} + var scrolledToBottom by remember { mutableStateOf(false) } + LaunchedEffect(scrollState.value) { + if (!scrolledToBottom) { + scrolledToBottom = scrollState.value == scrollState.maxValue + } + } -@Composable -private fun WelcomeBody(individualWelcomePage: VerificationPageStaticContentIndividualWelcomePage) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 12.dp) - ) { - Image( - painter = painterResource(id = R.drawable.stripe_time_estimate_icon), - contentDescription = stringResource(id = R.string.stripe_description_time_estimate) - ) Html( - html = individualWelcomePage.timeEstimate, + html = welcomePage.privacyPolicy, modifier = Modifier - .fillMaxWidth() - .padding(start = 6.dp) + .padding(vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) .semantics { - testTag = INDIVIDUAL_WELCOME_TIME_ESTIMATE_TAG + testTag = PRIVACY_POLICY_TAG }, - color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f), + color = MaterialTheme.colors.onBackground, urlSpanStyle = SpanStyle( textDecoration = TextDecoration.Underline, color = MaterialTheme.colors.secondary ) ) - } - Html( - html = individualWelcomePage.privacyPolicy, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) - .semantics { - testTag = INDIVIDUAL_WELCOME_PRIVACY_POLICY_TAG - }, - color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f), - urlSpanStyle = SpanStyle( - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.secondary - ) - ) - Divider( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) - ) - Html( - html = individualWelcomePage.body, - modifier = Modifier - .padding(bottom = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) - .semantics { - testTag = INDIVIDUAL_WELCOME_BODY_TAG - }, - color = MaterialTheme.colors.onBackground, - urlSpanStyle = SpanStyle( - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.secondary - ) - ) + var acceptState by remember { mutableStateOf(LoadingButtonState.Idle) } + LoadingButton( + modifier = Modifier + .semantics { testTag = INDIVIDUAL_WELCOME_GET_STARTED_BUTTON_TAG }, + text = welcomePage.getStartedButtonText.uppercase(), + state = acceptState + ) { + acceptState = LoadingButtonState.Disabled + navController.navigateTo(IndividualDestination) + } + } } internal const val INDIVIDUAL_WELCOME_PRIVACY_POLICY_TAG = "PrivacyPolicy" -internal const val INDIVIDUAL_WELCOME_TIME_ESTIMATE_TAG = "TimeEstimate" internal const val INDIVIDUAL_WELCOME_TITLE_TAG = "Title" internal const val INDIVIDUAL_WELCOME_GET_STARTED_BUTTON_TAG = "GetStarted" -internal const val INDIVIDUAL_WELCOME_BODY_TAG = "Body" 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 f4964790fac..22314fec126 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 @@ -189,6 +189,13 @@ internal class IdentityViewModel constructor( ) val collectedData: StateFlow = _collectedData + private val _visitedIndividualWelcomeScreen = MutableStateFlow( + savedStateHandle[VISITED_INDIVIDUAL_WELCOME_PAGE] ?: run { + false + } + ) + val visitedIndividualWelcomeScreen: StateFlow = _visitedIndividualWelcomeScreen + /** * StateFlow to track request status of postVerificationPageData */ @@ -963,6 +970,7 @@ internal class IdentityViewModel constructor( submittedVerificationPageData.submittedAndClosed() -> { navController.navigateTo(ConfirmationDestination) } + else -> { errorCause.postValue(IllegalStateException("VerificationPage submit failed")) navController.navigateToErrorScreenWithDefaultValues(getApplication()) @@ -1700,6 +1708,10 @@ internal class IdentityViewModel constructor( ) } + fun visitedIndividualWelcome() { + _visitedIndividualWelcomeScreen.updateStateAndSave { true } + } + fun updateImageHandlerScanTypes( frontScanType: IdentityScanState.ScanType, backScanType: IdentityScanState.ScanType? @@ -1762,6 +1774,7 @@ internal class IdentityViewModel constructor( _missingRequirements -> MISSING_REQUIREMENTS verificationPageData -> VERIFICATION_PAGE_DATA verificationPageSubmit -> VERIFICATION_PAGE_SUBMIT + _visitedIndividualWelcomeScreen -> VISITED_INDIVIDUAL_WELCOME_PAGE else -> { throw IllegalStateException("Unexpected state flow: $this") } @@ -1814,5 +1827,6 @@ internal class IdentityViewModel constructor( private const val VERIFICATION_PAGE = "verification_page" private const val VERIFICATION_PAGE_DATA = "verification_page_data" private const val VERIFICATION_PAGE_SUBMIT = "verification_page_submit" + private const val VISITED_INDIVIDUAL_WELCOME_PAGE = "visited_individual_welcome_page" } } 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 e65191f1101..55271c5cba2 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 @@ -19,6 +19,8 @@ import com.stripe.android.identity.networking.models.CollectedDataParam.Companio import com.stripe.android.identity.networking.models.Requirement import com.stripe.android.identity.networking.models.VerificationPage import com.stripe.android.identity.networking.models.VerificationPageData +import com.stripe.android.identity.networking.models.VerificationPageIconType +import com.stripe.android.identity.networking.models.VerificationPageStaticConsentLineContent import com.stripe.android.identity.utils.IdentityIO import kotlinx.coroutines.runBlocking import org.json.JSONObject @@ -167,18 +169,34 @@ class DefaultIdentityRepositoryTest { VERIFICATION_PAGE_TYPE_ADDRESS_JSON_STRING ) { assertThat(it.individualWelcome.getStartedButtonText).isEqualTo("Get started") - assertThat(it.individualWelcome.body).isEqualTo( - "You’ll need to share some personal information to complete the " + - "verification. Learn more" - ) assertThat(it.individualWelcome.title).isEqualTo( - "Tora's catfood partners with Stripe for secure Identity verification" + "Andrew's Audio works with Stripe to verify your identity" ) assertThat(it.individualWelcome.privacyPolicy).isEqualTo( - "Data will be stored and may be used according to the " + - "Stripe Privacy Policy and Tora's catfood Privacy Policy." + "Stripe Privacy Policy • " + + "Andrew's Audio Privacy Policy" + ) + assertThat(it.individualWelcome.lines).isEqualTo( + listOf( + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.DOCUMENT, + content = "You'll provide personal information including your name and " + + "phone number." + ), + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.DISPUTE_PROTECTION, + content = "The information you provide Stripe will help us " + + "confirm your identity." + ), + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.LOCK, + content = "Andrew's Audio will only have access to this " + + "verification data." + ) + ) ) - assertThat(it.individualWelcome.timeEstimate).isEqualTo("Takes less than 1 minute.") } } diff --git a/identity/src/test/java/com/stripe/android/identity/networking/IdentityNetworkResponseFixtures.kt b/identity/src/test/java/com/stripe/android/identity/networking/IdentityNetworkResponseFixtures.kt index bf9915fc51c..f07e9b1e3bf 100644 --- a/identity/src/test/java/com/stripe/android/identity/networking/IdentityNetworkResponseFixtures.kt +++ b/identity/src/test/java/com/stripe/android/identity/networking/IdentityNetworkResponseFixtures.kt @@ -25,13 +25,82 @@ internal val VERIFICATION_PAGE_NOT_REQUIRE_LIVE_CAPTURE_JSON_STRING = """ "id": "vs_1KgNstEAjaOkiuGMpFXVTocU", "object": "identity.verification_page", "biometric_consent": { - "accept_button_text": "Accept and continue", - "body": "\u003Cp\u003E\u003Cb\u003EHow Stripe will verify your identity\u003C/b\u003E\u003C/p\u003E\u003Cp\u003EStripe will use biometric technology (on images of you and your IDs) and other data sources to confirm your identity and for fraud and security purposes. Stripe will store these images and the results of this check and share them with mlgb.band.\u003C/p\u003E\u003Cp\u003E\u003Ca href='https://stripe.com/about'\u003ELearn about Stripe\u003C/a\u003E\u003C/p\u003E\u003Cp\u003E\u003Ca href='https://stripe.com/privacy-center/legal#stripe-identity'\u003ELearn how Stripe Identity works\u003C/a\u003E\u003C/p\u003E", - "decline_button_text": "No, don't verify", - "privacy_policy": "Data will be stored and may be used according to the \u003Ca href='https://stripe.com/privacy'\u003EStripe Privacy Policy\u003C/a\u003E and mlgb.band Privacy Policy.", - "time_estimate": "Takes about 1–2 minutes.", - "title": "mlgb.band uses Stripe to verify your identity", - "scroll_to_continue_button_text": "Scroll to consent" + "accept_button_text": "Agree and continue", + "body": null, + "decline_button_text": "Decline", + "lines": [ + { + "content": "You'll scan a valid photo ID.", + "icon": "camera" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", + "scroll_to_continue_button_text": "Scroll to continue", + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" + }, + "bottomsheet": { + "consent_identity": { + "bottomsheet_id": "consent_identity", + "lines": [ + { + "content": "We leverage Stripe's own verification service to verify your identity through your document and selfie.", + "icon": "cloud", + "title": "Stripe technology" + }, + { + "content": "We also work with trusted partners, including document issuers and authorized record holders, to help us verify your identity.", + "icon": "moved", + "title": "Third party partners" + } + ], + "title": "How we verify you" + }, + "consent_photo_id": { + "bottomsheet_id": "consent_photo_id", + "lines": [ + { + "content": "
  • Drivers license
  • Passport
  • National ID
  • Valid government-issued identification that clearly shows your face
", + "icon": "wallet", + "title": "Accepted forms of identification" + } + ], + "title": "Types of photo ID" + }, + "consent_verification_data": { + "bottomsheet_id": "consent_verification_data", + "lines": [ + { + "content": "Stripe handles billions of dollars in payments annually. The same infrastructure keeps identity verification data safe as well.", + "icon": "lock", + "title": "Your data is encrypted" + }, + { + "content": "Stripe will use and store your data under Stripe’s privacy policy, including to manage loss and for legal compliance. Learn more.", + "icon": "lock", + "title": "Stripe data use" + }, + { + "content": "Andrew's Audio will have access to the information you submit and the status of your verification, and may use your information under its privacy policy.", + "icon": "moved", + "title": "Andrew's Audio access" + }, + { + "content": "You can delete your data by contacting Andrew's Audio.", + "icon": "document", + "title": "Manage your data" + } + ], + "title": "Your data" + } }, "country_not_listed": { "address_from_other_country_text_button_text": "Have an Address from another country?", @@ -116,11 +185,25 @@ internal val VERIFICATION_PAGE_NOT_REQUIRE_LIVE_CAPTURE_JSON_STRING = """ } }, "individual_welcome": { - "body": "You’ll need to share some personal information to complete the verification. Learn more", - "get_started_button_text": "Get started", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", - "time_estimate": "Takes less than 1 minute.", - "title": "Tora's catfood partners with Stripe for secure Identity verification" + "body": null, + "get_started_button_text": "Get started", + "lines": [ + { + "content": "You'll provide personal information including your name and phone number.", + "icon": "document" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "livemode": false, "requirements": { @@ -148,13 +231,27 @@ internal val VERIFICATION_PAGE_REQUIRE_LIVE_CAPTURE_JSON_STRING = """ "id": "vs_1KgNstEAjaOkiuGMpFXVTocU", "object": "identity.verification_page", "biometric_consent": { - "accept_button_text": "Accept and continue", - "body": "\u003Cp\u003E\u003Cb\u003EHow Stripe will verify your identity\u003C/b\u003E\u003C/p\u003E\u003Cp\u003EStripe will use biometric technology (on images of you and your IDs) and other data sources to confirm your identity and for fraud and security purposes. Stripe will store these images and the results of this check and share them with mlgb.band.\u003C/p\u003E\u003Cp\u003E\u003Ca href='https://stripe.com/about'\u003ELearn about Stripe\u003C/a\u003E\u003C/p\u003E\u003Cp\u003E\u003Ca href='https://stripe.com/privacy-center/legal#stripe-identity'\u003ELearn how Stripe Identity works\u003C/a\u003E\u003C/p\u003E", - "decline_button_text": "No, don't verify", - "privacy_policy": "Data will be stored and may be used according to the \u003Ca href='https://stripe.com/privacy'\u003EStripe Privacy Policy\u003C/a\u003E and mlgb.band Privacy Policy.", - "time_estimate": "Takes about 1–2 minutes.", - "title": "mlgb.band uses Stripe to verify your identity", - "scroll_to_continue_button_text": "Scroll to consent" + "accept_button_text": "Agree and continue", + "body": null, + "decline_button_text": "Decline", + "lines": [ + { + "content": "You'll scan a valid photo ID.", + "icon": "camera" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", + "scroll_to_continue_button_text": "Scroll to continue", + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "country_not_listed": { "address_from_other_country_text_button_text": "Have an Address from another country?", @@ -239,11 +336,25 @@ internal val VERIFICATION_PAGE_REQUIRE_LIVE_CAPTURE_JSON_STRING = """ } }, "individual_welcome": { - "body": "You’ll need to share some personal information to complete the verification. Learn more", - "get_started_button_text": "Get started", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", - "time_estimate": "Takes less than 1 minute.", - "title": "Tora's catfood partners with Stripe for secure Identity verification" + "body": null, + "get_started_button_text": "Get started", + "lines": [ + { + "content": "You'll provide personal information including your name and phone number.", + "icon": "document" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "livemode": false, "requirements": { @@ -271,13 +382,27 @@ internal val VERIFICATION_PAGE_REQUIRE_SELFIE_LIVE_CAPTURE_JSON_STRING = """ "id": "vs_1M8UU5GMZYGNxJkBN55D3nva", "object": "identity.verification_page", "biometric_consent": { - "accept_button_text": "Accept and continue", - "body": "

How Stripe will verify your identity

Stripe will use biometric technology (on images of you and your IDs), as well as other data sources and our service providers, to confirm your identity and for fraud and security purposes. Stripe will store these images and the results of this check and share them with Andrew's Audio. You can subsequently opt-out by contacting Stripe. Learn more

", - "decline_button_text": "No, don't verify", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Andrew's Audio Privacy Policy.", + "accept_button_text": "Agree and continue", + "body": null, + "decline_button_text": "Decline", + "lines": [ + { + "content": "You'll scan a valid photo ID.", + "icon": "camera" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", "scroll_to_continue_button_text": "Scroll to continue", - "time_estimate": "Takes about 1–2 minutes.", - "title": "Andrew's Audio uses Stripe to verify your identity" + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "country_not_listed": { "address_from_other_country_text_button_text": "Have an Address from another country?", @@ -367,11 +492,25 @@ internal val VERIFICATION_PAGE_REQUIRE_SELFIE_LIVE_CAPTURE_JSON_STRING = """ } }, "individual_welcome": { - "body": "You’ll need to share some personal information to complete the verification. Learn more", - "get_started_button_text": "Get started", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", - "time_estimate": "Takes less than 1 minute.", - "title": "Tora's catfood partners with Stripe for secure Identity verification" + "body": null, + "get_started_button_text": "Get started", + "lines": [ + { + "content": "You'll provide personal information including your name and phone number.", + "icon": "document" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "livemode": true, "requirements": { @@ -450,13 +589,27 @@ internal val VERIFICATION_PAGE_TYPE_DOCUMENT_REQUIRE_ID_NUMBER_JSON_STRING = """ "id": "vs_1MOrPwEGkPhabJTjzCzKF4DM", "object": "identity.verification_page", "biometric_consent": { - "accept_button_text": "Accept and continue", - "body": "

How Stripe will verify your identity

Stripe will use biometric technology (on images of you and your IDs), as well as other data sources and our service providers, to confirm your identity and for fraud and security purposes. Stripe will store these images and the results of this check and share them with Tora's catfood. You can subsequently opt-out by contacting Stripe. Learn more

", - "decline_button_text": "No, don't verify", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", + "accept_button_text": "Agree and continue", + "body": null, + "decline_button_text": "Decline", + "lines": [ + { + "content": "You'll scan a valid photo ID.", + "icon": "camera" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", "scroll_to_continue_button_text": "Scroll to continue", - "time_estimate": "Takes about 1–2 minutes.", - "title": "Tora's catfood uses Stripe to verify your identity" + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "country_not_listed": { "address_from_other_country_text_button_text": "Have an Address from another country?", @@ -546,11 +699,25 @@ internal val VERIFICATION_PAGE_TYPE_DOCUMENT_REQUIRE_ID_NUMBER_JSON_STRING = """ } }, "individual_welcome": { - "body": "You’ll need to share some personal information to complete the verification. Learn more", - "get_started_button_text": "Get started", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", - "time_estimate": "Takes less than 1 minute.", - "title": "Tora's catfood partners with Stripe for secure Identity verification" + "body": null, + "get_started_button_text": "Get started", + "lines": [ + { + "content": "You'll provide personal information including your name and phone number.", + "icon": "document" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "livemode": false, "requirements": { @@ -581,13 +748,27 @@ internal val VERIFICATION_PAGE_TYPE_DOCUMENT_REQUIRE_ADDRESS_JSON_STRING = """ "id": "vs_1MRjwTEGkPhabJTjIEKiUmmS", "object": "identity.verification_page", "biometric_consent": { - "accept_button_text": "Accept and continue", - "body": "

How Stripe will verify your identity

Stripe will use biometric technology (on images of you and your IDs), as well as other data sources and our service providers, to confirm your identity and for fraud and security purposes. Stripe will store these images and the results of this check and share them with Tora's catfood. You can subsequently opt-out by contacting Stripe. Learn more

", - "decline_button_text": "No, don't verify", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", + "accept_button_text": "Agree and continue", + "body": null, + "decline_button_text": "Decline", + "lines": [ + { + "content": "You'll scan a valid photo ID.", + "icon": "camera" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", "scroll_to_continue_button_text": "Scroll to continue", - "time_estimate": "Takes about 1–2 minutes.", - "title": "Tora's catfood uses Stripe to verify your identity" + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "country_not_listed": { "address_from_other_country_text_button_text": "Have an Address from another country?", @@ -677,11 +858,25 @@ internal val VERIFICATION_PAGE_TYPE_DOCUMENT_REQUIRE_ADDRESS_JSON_STRING = """ } }, "individual_welcome": { - "body": "You’ll need to share some personal information to complete the verification. Learn more", - "get_started_button_text": "Get started", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", - "time_estimate": "Takes less than 1 minute.", - "title": "Tora's catfood partners with Stripe for secure Identity verification" + "body": null, + "get_started_button_text": "Get started", + "lines": [ + { + "content": "You'll provide personal information including your name and phone number.", + "icon": "document" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "livemode": true, "requirements": { @@ -711,13 +906,27 @@ internal val VERIFICATION_PAGE_TYPE_DOCUMENT_REQUIRE_ADDRESS_AND_ID_NUMBER_JSON_ "id": "vs_1MTb71EGkPhabJTjK2kJQ5xI", "object": "identity.verification_page", "biometric_consent": { - "accept_button_text": "Accept and continue", - "body": "

How Stripe will verify your identity

Stripe will use biometric technology (on images of you and your IDs), as well as other data sources and our service providers, to confirm your identity and for fraud and security purposes. Stripe will store these images and the results of this check and share them with Tora's catfood. You can subsequently opt-out by contacting Stripe. Learn more

", - "decline_button_text": "No, don't verify", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", + "accept_button_text": "Agree and continue", + "body": null, + "decline_button_text": "Decline", + "lines": [ + { + "content": "You'll scan a valid photo ID.", + "icon": "camera" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", "scroll_to_continue_button_text": "Scroll to continue", - "time_estimate": "Takes about 1–2 minutes.", - "title": "Tora's catfood uses Stripe to verify your identity" + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "country_not_listed": { "address_from_other_country_text_button_text": "Have an Address from another country?", @@ -807,11 +1016,25 @@ internal val VERIFICATION_PAGE_TYPE_DOCUMENT_REQUIRE_ADDRESS_AND_ID_NUMBER_JSON_ } }, "individual_welcome": { - "body": "You’ll need to share some personal information to complete the verification. Learn more", - "get_started_button_text": "Get started", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", - "time_estimate": "Takes less than 1 minute.", - "title": "Tora's catfood partners with Stripe for secure Identity verification" + "body": null, + "get_started_button_text": "Get started", + "lines": [ + { + "content": "You'll provide personal information including your name and phone number.", + "icon": "document" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "livemode": false, "requirements": { @@ -842,13 +1065,27 @@ internal val VERIFICATION_PAGE_TYPE_ID_NUMBER_JSON_STRING = """ "id": "vs_1MOrMgEGkPhabJTjkbUNVfh6", "object": "identity.verification_page", "biometric_consent": { - "accept_button_text": "Accept and continue", - "body": "

How Stripe will verify your identity

Stripe will use biometric technology (on images of you and your IDs), as well as other data sources and our service providers, to confirm your identity and for fraud and security purposes. Stripe will store these images and the results of this check and share them with Tora's catfood. You can subsequently opt-out by contacting Stripe. Learn more

", - "decline_button_text": "No, don't verify", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", + "accept_button_text": "Agree and continue", + "body": null, + "decline_button_text": "Decline", + "lines": [ + { + "content": "You'll scan a valid photo ID.", + "icon": "camera" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", "scroll_to_continue_button_text": "Scroll to continue", - "time_estimate": "Takes about 1–2 minutes.", - "title": "Tora's catfood uses Stripe to verify your identity" + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "country_not_listed": { "address_from_other_country_text_button_text": "Have an Address from another country?", @@ -938,11 +1175,25 @@ internal val VERIFICATION_PAGE_TYPE_ID_NUMBER_JSON_STRING = """ } }, "individual_welcome": { - "body": "You’ll need to share some personal information to complete the verification. Learn more", - "get_started_button_text": "Get started", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", - "time_estimate": "Takes less than 1 minute.", - "title": "Tora's catfood partners with Stripe for secure Identity verification" + "body": null, + "get_started_button_text": "Get started", + "lines": [ + { + "content": "You'll provide personal information including your name and phone number.", + "icon": "document" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "livemode": false, "requirements": { @@ -970,13 +1221,27 @@ internal val VERIFICATION_PAGE_TYPE_ADDRESS_JSON_STRING = """ "id": "vs_1MOrKBEGkPhabJTjBA2ohFAW", "object": "identity.verification_page", "biometric_consent": { - "accept_button_text": "Accept and continue", - "body": "

How Stripe will verify your identity

Stripe will use biometric technology (on images of you and your IDs), as well as other data sources and our service providers, to confirm your identity and for fraud and security purposes. Stripe will store these images and the results of this check and share them with Tora's catfood. You can subsequently opt-out by contacting Stripe. Learn more

", - "decline_button_text": "No, don't verify", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", + "accept_button_text": "Agree and continue", + "body": null, + "decline_button_text": "Decline", + "lines": [ + { + "content": "You'll scan a valid photo ID.", + "icon": "camera" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", "scroll_to_continue_button_text": "Scroll to continue", - "time_estimate": "Takes about 1–2 minutes.", - "title": "Tora's catfood uses Stripe to verify your identity" + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "country_not_listed": { "address_from_other_country_text_button_text": "Have an Address from another country?", @@ -1066,11 +1331,25 @@ internal val VERIFICATION_PAGE_TYPE_ADDRESS_JSON_STRING = """ } }, "individual_welcome": { - "body": "You’ll need to share some personal information to complete the verification. Learn more", + "body": null, "get_started_button_text": "Get started", - "privacy_policy": "Data will be stored and may be used according to the Stripe Privacy Policy and Tora's catfood Privacy Policy.", - "time_estimate": "Takes less than 1 minute.", - "title": "Tora's catfood partners with Stripe for secure Identity verification" + "lines": [ + { + "content": "You'll provide personal information including your name and phone number.", + "icon": "document" + }, + { + "content": "The information you provide Stripe will help us confirm your identity.", + "icon": "dispute_protection" + }, + { + "content": "Andrew's Audio will only have access to this verification data.", + "icon": "lock" + } + ], + "privacy_policy": "Stripe Privacy PolicyAndrew's Audio Privacy Policy", + "time_estimate": null, + "title": "Andrew's Audio works with Stripe to verify your identity" }, "livemode": false, "requirements": { diff --git a/identity/src/test/java/com/stripe/android/identity/ui/ConfirmationScreenTest.kt b/identity/src/test/java/com/stripe/android/identity/ui/ConfirmationScreenTest.kt index 954770a4713..ce962c232c4 100644 --- a/identity/src/test/java/com/stripe/android/identity/ui/ConfirmationScreenTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/ui/ConfirmationScreenTest.kt @@ -51,12 +51,12 @@ class ConfirmationScreenTest { @Test fun verifyUIIsBoundAndButtonInteracts() { testConfirmationScreen { - onNodeWithTag(confirmationTitleTag).assertTextEquals(CONFIRMATION_TITLE) - onNodeWithTag(BODY_TAG).assertTextEquals(CONFIRMATION_BODY) - onNodeWithTag(confirmationConfirmButtonTag).assertTextEquals( + onNodeWithTag(CONFIRMATION_TITLE_TAG).assertTextEquals(CONFIRMATION_TITLE) + onNodeWithTag(CONFIRMATION_BODY_TAG).assertTextEquals(CONFIRMATION_BODY) + onNodeWithTag(CONFIRMATION_BUTTON_TAG).assertTextEquals( CONFIRMATION_BUTTON_TEXT.uppercase() ) - onNodeWithTag(confirmationConfirmButtonTag).performClick() + onNodeWithTag(CONFIRMATION_BUTTON_TAG).performClick() verify(mockIdentityViewModel).sendSucceededAnalyticsRequestForNative() verify(mockVerificationFlowFinishable).finishWithResult( eq(IdentityVerificationSheet.VerificationFlowResult.Completed) diff --git a/identity/src/test/java/com/stripe/android/identity/ui/ConsentScreenTest.kt b/identity/src/test/java/com/stripe/android/identity/ui/ConsentScreenTest.kt index 8c386408278..68d77fb1df2 100644 --- a/identity/src/test/java/com/stripe/android/identity/ui/ConsentScreenTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/ui/ConsentScreenTest.kt @@ -1,10 +1,12 @@ package com.stripe.android.identity.ui import android.os.Build +import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onChildAt import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -16,9 +18,13 @@ import com.stripe.android.identity.navigation.ConsentDestination 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.VerificationPageIconType import com.stripe.android.identity.networking.models.VerificationPageRequirements +import com.stripe.android.identity.networking.models.VerificationPageStaticConsentLineContent import com.stripe.android.identity.networking.models.VerificationPageStaticContentConsentPage import com.stripe.android.identity.viewmodel.IdentityViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test @@ -46,42 +52,35 @@ class ConsentScreenTest { private val mockVerificationArgs = mock { on { brandLogo } doReturn mock() } + + private val visitedIndividualWelcome = MutableStateFlow(false) + private val mockIdentityViewModel = mock { on { verificationPage } doReturn verificationPageLiveData on { verificationArgs } doReturn mockVerificationArgs + on { visitedIndividualWelcomeScreen } doReturn visitedIndividualWelcome } private val mockNavController = mock() - private val verificationPageWithTitleTimeAndPolicy = mock().also { + private val verificationPage = mock().also { whenever(it.biometricConsent).thenReturn( VerificationPageStaticContentConsentPage( acceptButtonText = CONSENT_ACCEPT_TEXT, title = CONSENT_TITLE, privacyPolicy = CONSENT_PRIVACY_POLICY, - timeEstimate = CONSENT_TIME_ESTIMATE, - body = CONSENT_BODY, - declineButtonText = CONSENT_DECLINE_TEXT, - scrollToContinueButtonText = SCROLL_TO_CONTINUE_TEXT - ) - ) - whenever(it.requirements).thenReturn( - VerificationPageRequirements( - missing = listOf(Requirement.BIOMETRICCONSENT) - ) - ) - } - - private val verificationPageWithOutTitleTimeAndPolicy = mock().also { - whenever(it.biometricConsent).thenReturn( - VerificationPageStaticContentConsentPage( - acceptButtonText = CONSENT_ACCEPT_TEXT, - title = null, - privacyPolicy = null, - timeEstimate = null, - body = CONSENT_BODY, declineButtonText = CONSENT_DECLINE_TEXT, - scrollToContinueButtonText = SCROLL_TO_CONTINUE_TEXT + scrollToContinueButtonText = SCROLL_TO_CONTINUE_TEXT, + lines = listOf( + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.CAMERA, + content = CONTENT_CAMERA_LINE + ), + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.CLOUD, + content = CONTENT_CLOUD_LINE + ) + ) ) ) whenever(it.requirements).thenReturn( @@ -92,16 +91,12 @@ class ConsentScreenTest { } @Test - fun `when VerificationPage with title time and policy UI is bound correctly`() { - setComposeTestRuleWith(Resource.success(verificationPageWithTitleTimeAndPolicy)) { + fun `when not visitedIndividualWelcomePage UI is bound correctly`() { + setComposeTestRuleWith(Resource.success(verificationPage)) { onNodeWithTag(LOADING_SCREEN_TAG).assertDoesNotExist() - onNodeWithTag(TITLE_TAG).assertTextEquals(CONSENT_TITLE) - onNodeWithTag(TIME_ESTIMATE_TAG).assertTextEquals(CONSENT_TIME_ESTIMATE) onNodeWithTag(PRIVACY_POLICY_TAG).assertTextEquals(CONSENT_PRIVACY_POLICY) - onNodeWithTag(DIVIDER_TAG).assertExists() - onNodeWithTag(BODY_TAG).assertTextEquals(CONSENT_BODY) - + onAllNodesWithTag(CONSENT_LINE_TAG).assertCountEquals(2) onNodeWithTag(ACCEPT_BUTTON_TAG).onChildAt(0) .assertTextEquals(SCROLL_TO_CONTINUE_TEXT.uppercase()) onNodeWithTag(ACCEPT_BUTTON_TAG).onChildAt(0).assertIsNotEnabled() @@ -113,17 +108,13 @@ class ConsentScreenTest { } @Test - fun `when VerificationPage without title time and policy UI is bound correctly`() { - setComposeTestRuleWith(Resource.success(verificationPageWithOutTitleTimeAndPolicy)) { + fun `when visitedIndividualWelcomePage UI is bound correctly`() { + visitedIndividualWelcome.update { true } + setComposeTestRuleWith(Resource.success(verificationPage)) { onNodeWithTag(LOADING_SCREEN_TAG).assertDoesNotExist() - onNodeWithTag(CONSENT_HEADER_TAG).assertDoesNotExist() - onNodeWithTag(TIME_ESTIMATE_TAG).assertDoesNotExist() - onNodeWithTag(PRIVACY_POLICY_TAG).assertDoesNotExist() - onNodeWithTag(DIVIDER_TAG).assertDoesNotExist() - - onNodeWithTag(BODY_TAG).assertTextEquals(CONSENT_BODY) - + onNodeWithTag(PRIVACY_POLICY_TAG).assertTextEquals(CONSENT_PRIVACY_POLICY) + onAllNodesWithTag(CONSENT_LINE_TAG).assertCountEquals(2) onNodeWithTag(ACCEPT_BUTTON_TAG).onChildAt(0) .assertTextEquals(SCROLL_TO_CONTINUE_TEXT.uppercase()) onNodeWithTag(ACCEPT_BUTTON_TAG).onChildAt(1).assertDoesNotExist() @@ -136,7 +127,7 @@ class ConsentScreenTest { @Test fun `when agreed button is clicked correctly navigates`() { - setComposeTestRuleWith(Resource.success(verificationPageWithTitleTimeAndPolicy)) { + setComposeTestRuleWith(Resource.success(verificationPage)) { onNodeWithTag(DECLINE_BUTTON_TAG).onChildAt(0).performClick() runBlocking { verify(mockIdentityViewModel).postVerificationPageDataAndMaybeNavigate( @@ -185,10 +176,10 @@ class ConsentScreenTest { private companion object { const val CONSENT_TITLE = "title" const val CONSENT_PRIVACY_POLICY = "privacy policy" - const val CONSENT_TIME_ESTIMATE = "time estimate" - const val CONSENT_BODY = "this is the consent body" const val CONSENT_ACCEPT_TEXT = "yes" const val CONSENT_DECLINE_TEXT = "no" const val SCROLL_TO_CONTINUE_TEXT = "scroll to continue" + const val CONTENT_CAMERA_LINE = "content for camera line" + const val CONTENT_CLOUD_LINE = "content for cloud line" } } diff --git a/identity/src/test/java/com/stripe/android/identity/ui/IndividualWelcomeScreenTest.kt b/identity/src/test/java/com/stripe/android/identity/ui/IndividualWelcomeScreenTest.kt index 62939d75a5b..c54de9d29a6 100644 --- a/identity/src/test/java/com/stripe/android/identity/ui/IndividualWelcomeScreenTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/ui/IndividualWelcomeScreenTest.kt @@ -1,9 +1,11 @@ package com.stripe.android.identity.ui import android.os.Build +import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onChildAt import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -15,6 +17,8 @@ import com.stripe.android.identity.TestApplication import com.stripe.android.identity.navigation.INDIVIDUAL import com.stripe.android.identity.networking.Resource import com.stripe.android.identity.networking.models.VerificationPage +import com.stripe.android.identity.networking.models.VerificationPageIconType +import com.stripe.android.identity.networking.models.VerificationPageStaticConsentLineContent import com.stripe.android.identity.networking.models.VerificationPageStaticContentIndividualWelcomePage import com.stripe.android.identity.viewmodel.IdentityViewModel import org.junit.Rule @@ -46,10 +50,18 @@ class IndividualWelcomeScreenTest { whenever(it.individualWelcome).thenReturn( VerificationPageStaticContentIndividualWelcomePage( getStartedButtonText = INDIVIDUAL_WELCOME_GET_STARTED_TEXT, - body = INDIVIDUAL_WELCOME_BODY, title = INDIVIDUAL_WELCOME_TITLE, privacyPolicy = INDIVIDUAL_WELCOME_PRIVACY_POLICY, - timeEstimate = INDIVIDUAL_WELCOME_TIME_ESTIMATE + lines = listOf( + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.CAMERA, + content = INDIVIDUAL_WELCOME_CAMERA_LINE + ), + VerificationPageStaticConsentLineContent( + icon = VerificationPageIconType.CLOUD, + content = INDIVIDUAL_WELCOME_CLOUD_LINE + ) + ) ) ) } @@ -65,13 +77,10 @@ class IndividualWelcomeScreenTest { fun verifyUIIsBoundCorrectly() { testIndividualWelcome { onNodeWithTag(INDIVIDUAL_WELCOME_TITLE_TAG).assertTextEquals(INDIVIDUAL_WELCOME_TITLE) - onNodeWithTag(INDIVIDUAL_WELCOME_BODY_TAG).assertTextEquals(INDIVIDUAL_WELCOME_BODY) onNodeWithTag(INDIVIDUAL_WELCOME_GET_STARTED_BUTTON_TAG).onChildAt(0).assertTextEquals( INDIVIDUAL_WELCOME_GET_STARTED_TEXT.uppercase() ) - onNodeWithTag(INDIVIDUAL_WELCOME_TIME_ESTIMATE_TAG).assertTextEquals( - INDIVIDUAL_WELCOME_TIME_ESTIMATE - ) + onAllNodesWithTag(CONSENT_LINE_TAG).assertCountEquals(2) onNodeWithTag(INDIVIDUAL_WELCOME_PRIVACY_POLICY_TAG).assertTextEquals( INDIVIDUAL_WELCOME_PRIVACY_POLICY ) @@ -105,8 +114,8 @@ class IndividualWelcomeScreenTest { private companion object { const val INDIVIDUAL_WELCOME_TITLE = "title" const val INDIVIDUAL_WELCOME_PRIVACY_POLICY = "privacy policy" - const val INDIVIDUAL_WELCOME_TIME_ESTIMATE = "time estimate" - const val INDIVIDUAL_WELCOME_BODY = "this is the consent body" const val INDIVIDUAL_WELCOME_GET_STARTED_TEXT = "get started" + const val INDIVIDUAL_WELCOME_CAMERA_LINE = "content for camera line" + const val INDIVIDUAL_WELCOME_CLOUD_LINE = "content for cloud line" } }