From f42dbe784422416572bd43c39134460f1cc69647 Mon Sep 17 00:00:00 2001 From: Chen Cen <79880926+ccen-stripe@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:37:31 -0800 Subject: [PATCH] [Identity] Butter M1 UX tweaks (#7638) --- identity/api/identity.api | 14 ++++ .../res/drawable/stripe_ellipsis_icon.xml | 15 ++++ identity/res/values-night/colors.xml | 2 + identity/res/values/colors.xml | 2 + identity/res/values/strings.xml | 6 ++ .../stripe/android/identity/ui/BottomSheet.kt | 71 +++++++++++++++--- .../android/identity/ui/ConsentLines.kt | 51 +++++++++++-- .../android/identity/ui/ConsentScreen.kt | 8 +- .../identity/ui/ConsentWelcomeHeader.kt | 23 +++++- .../android/identity/ui/DocWarmupScreen.kt | 74 +++++++++---------- .../identity/ui/DocWarmupScreenTest.kt | 12 +-- 11 files changed, 209 insertions(+), 69 deletions(-) create mode 100644 identity/res/drawable/stripe_ellipsis_icon.xml diff --git a/identity/api/identity.api b/identity/api/identity.api index e54d043ed27..94df0399e99 100644 --- a/identity/api/identity.api +++ b/identity/api/identity.api @@ -346,6 +346,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$ConsentLinesKt { + public static final field INSTANCE Lcom/stripe/android/identity/ui/ComposableSingletons$ConsentLinesKt; + 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$ConsentScreenKt { public static final field INSTANCE Lcom/stripe/android/identity/ui/ComposableSingletons$ConsentScreenKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; @@ -353,6 +360,13 @@ public final class com/stripe/android/identity/ui/ComposableSingletons$ConsentSc public final fun getLambda-1$identity_release ()Lkotlin/jvm/functions/Function2; } +public final class com/stripe/android/identity/ui/ComposableSingletons$ConsentWelcomeHeaderKt { + public static final field INSTANCE Lcom/stripe/android/identity/ui/ComposableSingletons$ConsentWelcomeHeaderKt; + 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/res/drawable/stripe_ellipsis_icon.xml b/identity/res/drawable/stripe_ellipsis_icon.xml new file mode 100644 index 00000000000..16cca89cfbd --- /dev/null +++ b/identity/res/drawable/stripe_ellipsis_icon.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/identity/res/values-night/colors.xml b/identity/res/values-night/colors.xml index 18bc4ca9421..766bb0bff82 100644 --- a/identity/res/values-night/colors.xml +++ b/identity/res/values-night/colors.xml @@ -6,4 +6,6 @@ #F7FAFC #F7FAFC + + #D8DEE4 diff --git a/identity/res/values/colors.xml b/identity/res/values/colors.xml index ef73e668bb2..a9cd318dff0 100644 --- a/identity/res/values/colors.xml +++ b/identity/res/values/colors.xml @@ -13,4 +13,6 @@ #1A1B25 #1A1B25 + + #687385 diff --git a/identity/res/values/strings.xml b/identity/res/values/strings.xml index c80bbbf7236..0de4e14f756 100644 --- a/identity/res/values/strings.xml +++ b/identity/res/values/strings.xml @@ -2,6 +2,8 @@ Accepted forms of ID + + Accepted forms of ID include App Settings @@ -47,6 +49,8 @@ dispute_protection icon document icon + + ellipsis icon exclamation icon @@ -111,6 +115,8 @@ ID ID Number + + I\'m ready The ID number you entered is incomplete. diff --git a/identity/src/main/java/com/stripe/android/identity/ui/BottomSheet.kt b/identity/src/main/java/com/stripe/android/identity/ui/BottomSheet.kt index 879177b9ff3..4f1397b16ca 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/BottomSheet.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/BottomSheet.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -22,9 +23,11 @@ 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.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.lifecycle.viewmodel.compose.viewModel import com.stripe.android.identity.R import com.stripe.android.identity.networking.models.VerificationPageIconType @@ -34,6 +37,7 @@ import com.stripe.android.identity.networking.models.getContentDescriptionId import com.stripe.android.identity.networking.models.getResourceId import com.stripe.android.identity.viewmodel.BottomSheetViewModel import com.stripe.android.uicore.text.Html +import java.util.regex.Pattern @Composable @ExperimentalMaterialApi @@ -111,25 +115,62 @@ private fun BottomSheetLine(line: VerificationPageStaticContentBottomSheetLineCo ) { Text( text = line.title, - style = MaterialTheme.typography.h5, + fontWeight = FontWeight.Bold, modifier = Modifier.padding( bottom = 4.dp - ) - ) - Html( - html = line.content, - color = MaterialTheme.colors.onSurface.copy( - alpha = 0.6f ), - urlSpanStyle = SpanStyle( - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.secondary - ) ) + + line.content.tryParseUl()?.let { bulletStrings -> + // [HTML] uses HtmlCompat.fromHtml, which automatically adds an additional line break for
  • items. + // We would like to build our line break instead. + for (bulletString in bulletStrings) { + Text( + modifier = Modifier.padding(bottom = 2.dp), + text = "• $bulletString", + color = MaterialTheme.colors.onSurface, + style = LocalTextStyle.current.merge( + lineHeight = 20.sp + ) + ) + } + } ?: run { + Html( + html = line.content, + color = MaterialTheme.colors.onSurface, + style = LocalTextStyle.current.merge( + lineHeight = 20.sp + ), + urlSpanStyle = SpanStyle( + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colors.onSurface + ) + ) + } } } } +private fun String.tryParseUl(): List? { + val trimmed = this.trim() + // Detect if string is an unordered HTML list + val ulPattern = Pattern.compile("^
      .*
    $", Pattern.DOTALL) + val ulMatcher = ulPattern.matcher(trimmed) + if (!ulMatcher.find()) { + return null + } + + // Extract list items + val items: MutableList = mutableListOf() + val liPattern = Pattern.compile("
  • (.*?)
  • ", Pattern.DOTALL) + val liMatcher = liPattern.matcher(trimmed) + + while (liMatcher.find()) { + items.add(requireNotNull(liMatcher.group(1))) + } + return items +} + @Preview @Composable @ExperimentalMaterialApi @@ -153,8 +194,14 @@ internal fun ButtonSheetPreview() { VerificationPageStaticContentBottomSheetLineContent( icon = VerificationPageIconType.CLOUD, title = "cloud line", - content = "cloud line content with another " + + content = "Drivers license cloud line content with another " + "link, with multiline content" + ), + VerificationPageStaticContentBottomSheetLineContent( + icon = VerificationPageIconType.WALLET, + title = "bullets line", + content = "
    • Drivers license
    • Passport
    • National ID
    • " + + "
    • Valid government-issued identification that clearly shows your face
      • " ) ) ) 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 index 657f12746d8..8b9eb36fb05 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/ConsentLines.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/ConsentLines.kt @@ -1,23 +1,26 @@ package com.stripe.android.identity.ui import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column 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.colorResource 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.tooling.preview.Preview 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.VerificationPageIconType import com.stripe.android.identity.networking.models.VerificationPageStaticConsentLineContent import com.stripe.android.identity.networking.models.VerificationPageStaticContentBottomSheetContent import com.stripe.android.identity.networking.models.getContentDescriptionId @@ -33,7 +36,11 @@ internal fun ConsentLines( Row( modifier = Modifier .testTag(CONSENT_LINE_TAG) - .padding(top = dimensionResource(id = R.dimen.stripe_item_vertical_margin)) + .padding( + top = dimensionResource(id = R.dimen.stripe_item_vertical_margin), + start = dimensionResource(id = R.dimen.stripe_page_horizontal_margin), + end = dimensionResource(id = R.dimen.stripe_page_horizontal_margin) + ) ) { Image( painter = painterResource(id = line.icon.getResourceId()), @@ -44,14 +51,12 @@ internal fun ConsentLines( ) BottomSheetHTML( html = line.content, - color = MaterialTheme.colors.onSurface.copy( - alpha = 0.6f - ), + color = colorResource(id = R.color.stripe_html_line), style = LocalTextStyle.current.merge(fontSize = 16.sp), bottomSheets = bottomSheets, urlSpanStyle = SpanStyle( textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.secondary + color = colorResource(id = R.color.stripe_html_line) ) ) } @@ -59,3 +64,37 @@ internal fun ConsentLines( } internal const val CONSENT_LINE_TAG = "consentLineTag" + +@Preview +@Composable +@ExperimentalMaterialApi +internal fun ConsentLinePreview() { + IdentityPreview { + Column { + ConsentLines( + 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" + ) + ), + bottomSheets = mapOf() + ) + } + } +} 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 4d3addeda78..cff56fab681 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 @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -21,6 +20,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag @@ -168,10 +168,10 @@ private fun SuccessUI( .semantics { testTag = PRIVACY_POLICY_TAG }, - color = MaterialTheme.colors.onBackground, + color = colorResource(id = R.color.stripe_html_line), urlSpanStyle = SpanStyle( textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.secondary + color = colorResource(id = R.color.stripe_html_line) ) ) @@ -196,7 +196,7 @@ private fun SuccessUI( onConsentAgreed() } - LoadingButton( + LoadingTextButton( modifier = Modifier .semantics { testTag = DECLINE_BUTTON_TAG }, text = consentPage.declineButtonText.uppercase(), 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 index f72a1f5abe9..c959fd1c175 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/ConsentWelcomeHeader.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/ConsentWelcomeHeader.kt @@ -3,11 +3,13 @@ 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.Column 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.ExperimentalMaterialApi import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -21,6 +23,7 @@ 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.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.stripe.android.identity.R @@ -72,11 +75,11 @@ internal fun ConsentWelcomeHeader( ) } Image( - painter = painterResource(id = R.drawable.stripe_plus_icon), + painter = painterResource(id = R.drawable.stripe_ellipsis_icon), modifier = Modifier .width(16.dp) .height(16.dp), - contentDescription = stringResource(id = R.string.stripe_description_plus) + contentDescription = stringResource(id = R.string.stripe_description_ellipsis) ) Image( @@ -106,3 +109,19 @@ internal fun ConsentWelcomeHeader( ) } + +@Preview +@Composable +@ExperimentalMaterialApi +internal fun ConsentWelcomeHeaderPreview() { + IdentityPreview { + Column { + ConsentWelcomeHeader( + modifier = Modifier, + merchantLogoUri = Uri.EMPTY, + title = "TEST TITLE", + showLogos = true + ) + } + } +} diff --git a/identity/src/main/java/com/stripe/android/identity/ui/DocWarmupScreen.kt b/identity/src/main/java/com/stripe/android/identity/ui/DocWarmupScreen.kt index 8ef9afe2c25..9a8d9b5f152 100644 --- a/identity/src/main/java/com/stripe/android/identity/ui/DocWarmupScreen.kt +++ b/identity/src/main/java/com/stripe/android/identity/ui/DocWarmupScreen.kt @@ -1,7 +1,6 @@ package com.stripe.android.identity.ui import androidx.compose.foundation.Image -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -9,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme @@ -21,7 +19,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource @@ -29,6 +26,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign 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.camera.CameraPermissionEnsureable import com.stripe.android.identity.R @@ -101,55 +99,53 @@ internal fun DocWarmupView( top = dimensionResource(id = R.dimen.stripe_item_vertical_margin) ), style = MaterialTheme.typography.h4, + fontSize = 26.sp, textAlign = TextAlign.Center ) + + val driverLicense = stringResource(id = R.string.stripe_driver_license) + val governmentId = stringResource(id = R.string.stripe_government_id) + val passport = stringResource(id = R.string.stripe_passport) + val formsOfId = stringResource(id = R.string.stripe_accepted_forms_of_id_include) + + val allowedListString = remember(documentSelectPage) { + "$formsOfId " + documentSelectPage.idDocumentTypeAllowlist.keys.mapNotNull { + when (it) { + DRIVING_LICENSE_KEY -> driverLicense + ID_CARD_KEY -> governmentId + PASSPORT_KEY -> passport + else -> null + } + }.joinToString(", ") + "." + } + Text( - text = stringResource(id = R.string.stripe_doc_front_warmup_body), + text = allowedListString, modifier = Modifier .fillMaxWidth() .padding( top = dimensionResource(id = R.dimen.stripe_item_vertical_margin), - ), - style = MaterialTheme.typography.subtitle1, + ) + .testTag(DOC_FRONT_ACCEPTED_IDS_TAG), + style = MaterialTheme.typography.subtitle1.merge(lineHeight = 22.sp), textAlign = TextAlign.Center ) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 40.dp, bottom = 20.dp) - .border( - width = 2.dp, - color = Color.LightGray, - shape = RoundedCornerShape(10.dp) - ) - .padding(10.dp) - .testTag(DOC_FRONT_ACCEPTED_IDS_TAG) - ) { - Text( - text = stringResource(id = R.string.stripe_accepted_forms_of_id), - style = MaterialTheme.typography.subtitle1, - modifier = Modifier.padding(start = 4.dp, top = 4.dp, bottom = 4.dp), - ) - documentSelectPage.idDocumentTypeAllowlist.keys.mapNotNull { - when (it) { - DRIVING_LICENSE_KEY -> stringResource(id = R.string.stripe_driver_license) - ID_CARD_KEY -> stringResource(id = R.string.stripe_government_id) - PASSPORT_KEY -> stringResource(id = R.string.stripe_passport) - else -> null - } - }.forEach { idType -> - Text( - text = "• $idType", - modifier = Modifier.padding(start = 10.dp, top = 4.dp, bottom = 4.dp), - ) - } - } } + Text( + text = stringResource(id = R.string.stripe_doc_front_warmup_body), + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = dimensionResource(id = R.dimen.stripe_item_vertical_margin), + ), + style = MaterialTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + LoadingButton( modifier = Modifier.testTag(DOC_FRONT_CONTINUE_BUTTON_TAG), - text = stringResource(id = R.string.stripe_kontinue).uppercase(), + text = stringResource(id = R.string.stripe_im_ready).uppercase(), state = continueButtonState ) { continueButtonState = LoadingButtonState.Loading diff --git a/identity/src/test/java/com/stripe/android/identity/ui/DocWarmupScreenTest.kt b/identity/src/test/java/com/stripe/android/identity/ui/DocWarmupScreenTest.kt index 18affd3c7c5..932b61b464c 100644 --- a/identity/src/test/java/com/stripe/android/identity/ui/DocWarmupScreenTest.kt +++ b/identity/src/test/java/com/stripe/android/identity/ui/DocWarmupScreenTest.kt @@ -46,12 +46,12 @@ class DocWarmupScreenTest { } with(composeTestRule) { onNodeWithTag(DOC_FRONT_ACCEPTED_IDS_TAG).assertExists() - onNodeWithTag(DOC_FRONT_ACCEPTED_IDS_TAG).onChildAt(1) - .assertTextEquals("• " + context.getString(R.string.stripe_passport)) - onNodeWithTag(DOC_FRONT_ACCEPTED_IDS_TAG).onChildAt(2) - .assertTextEquals("• " + context.getString(R.string.stripe_driver_license)) - onNodeWithTag(DOC_FRONT_ACCEPTED_IDS_TAG).onChildAt(3) - .assertTextEquals("• " + context.getString(R.string.stripe_government_id)) + onNodeWithTag(DOC_FRONT_ACCEPTED_IDS_TAG).assertTextEquals( + "${context.getString(R.string.stripe_accepted_forms_of_id_include)} " + + "${context.getString(R.string.stripe_passport)}, " + + "${context.getString(R.string.stripe_driver_license)}, " + + "${context.getString(R.string.stripe_government_id)}." + ) onNodeWithTag(DOC_FRONT_CONTINUE_BUTTON_TAG).onChildAt(0).performClick() verify(onContinueClickMock).invoke() }