Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add icons to the IBAN, credit card and CVC fields. #4359

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a639793
Icons added for text fields.
michelleb-stripe Aug 12, 2021
034cd36
Merge with credit-card-compose
michelleb-stripe Nov 3, 2021
b24cdad
Add cardnumber and cvc icons
michelleb-stripe Nov 3, 2021
7fcce43
Remove the vertical divider.
michelleb-stripe Nov 4, 2021
ef4033f
Merge with master
michelleb-stripe Nov 5, 2021
b0c419c
apiDump ktlintFormat
michelleb-stripe Nov 9, 2021
06c7a20
Fix failing test
michelleb-stripe Nov 9, 2021
8371d52
Merge with michelleb/credit-card-compose
michelleb-stripe Nov 9, 2021
4173db4
Merge with michelleb/credit-card-compose
michelleb-stripe Nov 9, 2021
b4f6aeb
Merge branch 'michelleb/credit-card-compose' into michelleb/credit-ca…
michelleb-stripe Nov 9, 2021
9253523
Add the IBAN elements in
michelleb-stripe Nov 9, 2021
837ac22
Undos
michelleb-stripe Nov 9, 2021
9701d16
Merge branch 'michelleb/credit-card-compose' into michelleb/credit-ca…
michelleb-stripe Nov 9, 2021
e44a18a
Still need to get the bank color correct.
michelleb-stripe Nov 9, 2021
ab65f08
Merge with michelleb/credit-card-compose
michelleb-stripe Nov 9, 2021
1083a13
Icons now all sorted right.
michelleb-stripe Nov 9, 2021
5c8b849
ktformat
michelleb-stripe Nov 9, 2021
35e24b0
Merge branch 'michelleb/credit-card-compose' into michelleb/credit-ca…
michelleb-stripe Nov 11, 2021
75ea5ec
Merge branch 'michelleb/credit-card-compose' into michelleb/credit-ca…
michelleb-stripe Nov 17, 2021
94c9a2f
Merge branch 'michelleb/credit-card-compose' into michelleb/credit-ca…
michelleb-stripe Nov 29, 2021
7497066
Merging with master
michelleb-stripe Feb 7, 2022
08b6502
Merge with michelleb/credit-card-compose
michelleb-stripe Feb 7, 2022
b4cbe03
Merge with michelleb/credit-card-compose
michelleb-stripe Feb 7, 2022
ea84001
Merge with michelleb/credit-card-compose
michelleb-stripe Feb 7, 2022
fd62b27
Merge branch 'michelleb/credit-card-compose' into michelleb/credit-ca…
michelleb-stripe Feb 8, 2022
c2eb24b
Cleanup
michelleb-stripe Feb 8, 2022
78ebcf7
Vertical line not working
michelleb-stripe Feb 8, 2022
7370c49
Vertical row divider working with trailing icon
michelleb-stripe Feb 8, 2022
ff78568
Vertical row divider working with trailing icon supporting multiple f…
michelleb-stripe Feb 8, 2022
650d350
Merge with michelleb/credit-card-compose
michelleb-stripe Feb 10, 2022
e259d88
apiDump ktlintFormat
michelleb-stripe Feb 10, 2022
3f8fe3e
Merge branch 'michelleb/credit-card-compose' into michelleb/credit-ca…
michelleb-stripe Feb 11, 2022
011e626
Fix up
michelleb-stripe Feb 11, 2022
fea6499
ktlintFormat
michelleb-stripe Feb 11, 2022
72c3533
Merge branch 'michelleb/credit-card-compose' into michelleb/credit-ca…
michelleb-stripe Feb 14, 2022
3c18f83
Merge branch 'michelleb/credit-card-compose' into michelleb/credit-ca…
michelleb-stripe Feb 15, 2022
565fbf2
Fix failing test.
michelleb-stripe Feb 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions payments-ui-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
implementation project(":stripe-core")
implementation project(":payments-core")

implementation 'androidx.constraintlayout:constraintlayout-compose:1.0.0'
implementation 'androidx.core:core-ktx:1.7.0'
implementation libraries.androidx.appcompat
implementation libraries.androidx.lifecycle.livedata.ktx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ internal class CardNumberController constructor(
CardBrand.getCardBrands(it).firstOrNull() ?: CardBrand.Unknown
}

override val trailingIcon: Flow<TextFieldIcon?> = cardBrandFlow.map {
TextFieldIcon(it.icon, isIcon = false)
}

private val _fieldState = combine(cardBrandFlow, _fieldValue) { brand, fieldValue ->
cardTextFieldConfig.determineState(brand, fieldValue)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ internal class CvcController constructor(
FormFieldEntry(value, complete)
}

override val trailingIcon: Flow<TextFieldIcon?> = cardBrandFlow.map {
TextFieldIcon(it.cvcIcon, isIcon = false)
}

init {
onValueChange("")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import androidx.compose.ui.text.input.KeyboardType
import com.stripe.android.ui.core.R
import com.stripe.android.ui.core.elements.TextFieldStateConstants.Error
import com.stripe.android.ui.core.elements.TextFieldStateConstants.Valid
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.Calendar

internal class DateConfig : TextFieldConfig {
Expand All @@ -17,6 +19,7 @@ internal class DateConfig : TextFieldConfig {
override val label = R.string.stripe_paymentsheet_expiration_date_hint
override val keyboard = KeyboardType.NumberPassword
override val visualTransformation = ExpiryDateVisualTransformation()
override val trailingIcon: StateFlow<TextFieldIcon?> = MutableStateFlow(null)

override fun filter(userTyped: String) = userTyped.filter { it.isDigit() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import com.stripe.android.ui.core.R
import com.stripe.android.ui.core.elements.TextFieldStateConstants.Error
import com.stripe.android.ui.core.elements.TextFieldStateConstants.Valid
import kotlinx.coroutines.flow.MutableStateFlow
import java.util.regex.Pattern

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Expand All @@ -19,6 +20,7 @@ class EmailConfig : TextFieldConfig {
override val label = R.string.email
override val keyboard = KeyboardType.Email
override val visualTransformation: VisualTransformation? = null
override val trailingIcon: MutableStateFlow<TextFieldIcon?> = MutableStateFlow(null)

/**
* This will allow all characters, but will show as invalid if it doesn't match
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import com.stripe.android.ui.core.R
import kotlinx.coroutines.flow.MutableStateFlow
import java.math.BigInteger
import java.util.Locale

Expand All @@ -27,6 +28,13 @@ class IbanConfig : TextFieldConfig {
override val label = R.string.iban
override val keyboard = KeyboardType.Ascii

override val trailingIcon: MutableStateFlow<TextFieldIcon?> = MutableStateFlow(
TextFieldIcon(
R.drawable.stripe_ic_bank_generic,
isIcon = true
)
)

// Displays the IBAN in groups of 4 characters with spaces added between them
override val visualTransformation: VisualTransformation = VisualTransformation { text ->
val output = StringBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import com.stripe.android.ui.core.R
import com.stripe.android.ui.core.elements.TextFieldStateConstants.Error
import com.stripe.android.ui.core.elements.TextFieldStateConstants.Valid
import kotlinx.coroutines.flow.MutableStateFlow

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class NameConfig : TextFieldConfig {
Expand All @@ -17,6 +18,7 @@ class NameConfig : TextFieldConfig {
override val debugLabel = "name"
override val keyboard = KeyboardType.Text
override val visualTransformation: VisualTransformation? = null
override val trailingIcon: MutableStateFlow<TextFieldIcon?> = MutableStateFlow(null)

override fun determineState(input: String): TextFieldState {
return when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ package com.stripe.android.ui.core.elements

import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension

@Composable
internal fun RowElementUI(
Expand All @@ -22,45 +18,53 @@ internal fun RowElementUI(
hiddenIdentifiers: List<IdentifierSpec>
) {
val fields = controller.fields
Row(
Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val cardStyle = CardStyle(isSystemInDarkTheme())

// An attempt was made to do this with a row, and a vertical divider created with a box.
// The row had a height of IntrinsicSize.Min, and the box/vertical divider filled the height
// when adding in the trailing icon this broke and caused the overall height of the row to
// increase. By using the constraint layout the vertical divider does not negatively effect
// the size of the row.
ConstraintLayout {
// Create references for the composables to constrain
val fieldRefs = fields.map { createRef() }
val dividerRefs = fields.map { createRef() }

fields.forEachIndexed { index, field ->
SectionFieldElementUI(
enabled,
field,
Modifier.fillMaxWidth(
(1f / fields.size.toFloat()).takeIf { index != (fields.size - 1) } ?: 1f
),
Modifier
.constrainAs(fieldRefs[index]) {
if (index == 0) {
start.linkTo(parent.start)
} else {
start.linkTo(dividerRefs[index - 1].end)
}
top.linkTo(parent.top)
}
.fillMaxWidth(
(1f / fields.size.toFloat()).takeIf { index != (fields.size - 1) } ?: 1f
),
hiddenIdentifiers
)

if (index != (fields.size - 1)) {
val cardStyle = CardStyle(isSystemInDarkTheme())
VeriticalDivider(
color = cardStyle.cardBorderColor,
thickness = cardStyle.cardBorderWidth,
Divider(
modifier = Modifier
.constrainAs(dividerRefs[index]) {
start.linkTo(fieldRefs[index].end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
height = (Dimension.fillToConstraints)
}
.padding(
horizontal = cardStyle.cardBorderWidth
)
.width(cardStyle.cardBorderWidth)
.background(cardStyle.cardBorderColor)
)
}
}
}
}

@Composable
internal fun VeriticalDivider(
color: Color,
modifier: Modifier = Modifier,
thickness: Dp = 1.dp,
) {
Box(
modifier = Modifier
.fillMaxHeight()
.width(thickness)
.background(color)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.annotation.StringRes
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import kotlinx.coroutines.flow.MutableStateFlow

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class SimpleTextFieldConfig(
Expand All @@ -14,6 +15,7 @@ class SimpleTextFieldConfig(
) : TextFieldConfig {
override val debugLabel: String = "generic_text"
override val visualTransformation: VisualTransformation? = null
override val trailingIcon: MutableStateFlow<TextFieldIcon?> = MutableStateFlow(null)

override fun determineState(input: String): TextFieldState = object : TextFieldState {
override fun shouldShowError(hasFocus: Boolean) = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.annotation.RestrictTo
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import kotlinx.coroutines.flow.StateFlow

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
sealed interface TextFieldConfig {
Expand All @@ -22,6 +23,8 @@ sealed interface TextFieldConfig {
/** Transformation for changing visual output of the input field. */
val visualTransformation: VisualTransformation?

val trailingIcon: StateFlow<TextFieldIcon?>

/** This will determine the state of the field based on the text */
fun determineState(input: String): TextFieldState

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.stripe.android.ui.core.elements

import androidx.annotation.DrawableRes
import androidx.annotation.RestrictTo
import androidx.annotation.StringRes
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
Expand All @@ -17,6 +19,7 @@ interface TextFieldController : InputController {
fun onFocusChange(newHasFocus: Boolean)

val debugLabel: String
val trailingIcon: Flow<TextFieldIcon?>
val capitalization: KeyboardCapitalization
val keyboardType: KeyboardType
override val label: Flow<Int>
Expand All @@ -27,17 +30,29 @@ interface TextFieldController : InputController {
val visibleError: Flow<Boolean>
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
data class TextFieldIcon(
@DrawableRes
val idRes: Int,
@StringRes
val contentDescription: Int? = null,

/** If it is an icon that should be tinted to match the text the value should be true */
val isIcon: Boolean
)

/**
* This class will provide the onValueChanged and onFocusChanged functionality to the field's
* composable. These functions will update the observables as needed. It is responsible for
* exposing immutable observers for its data
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class SimpleTextFieldController constructor(
private val textFieldConfig: TextFieldConfig,
override val showOptionalLabel: Boolean = false,
initialValue: String? = null
) : TextFieldController, SectionFieldErrorController {
override val trailingIcon: Flow<TextFieldIcon?> = textFieldConfig.trailingIcon
override val capitalization: KeyboardCapitalization = textFieldConfig.capitalization
override val keyboardType: KeyboardType = textFieldConfig.keyboard
override val visualTransformation =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.stripe.android.ui.core.elements

import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
Expand All @@ -23,6 +25,7 @@ import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import com.stripe.android.ui.core.R
Expand Down Expand Up @@ -58,6 +61,7 @@ internal fun TextField(

val focusManager = LocalFocusManager.current
val value by textFieldController.fieldValue.collectAsState("")
val trailingIcon by textFieldController.trailingIcon.collectAsState(null)
val shouldShowError by textFieldController.visibleError.collectAsState(false)

var hasFocus by rememberSaveable { mutableStateOf(false) }
Expand Down Expand Up @@ -137,7 +141,10 @@ internal fun TextField(
colors = colors,
maxLines = 1,
singleLine = true,
enabled = enabled
enabled = enabled,
trailingIcon = trailingIcon?.let {
{ TrailingIcon(it, colors) }
}
)
}

Expand All @@ -148,3 +155,25 @@ internal fun nextFocus(focusManager: FocusManager) {
}
}
}

@Composable
internal fun TrailingIcon(
trailingIcon: TextFieldIcon,
colors: androidx.compose.material.TextFieldColors
) {
if (trailingIcon.isIcon) {
Icon(
painter = painterResource(id = trailingIcon.idRes),
contentDescription = trailingIcon.contentDescription?.let {
stringResource(trailingIcon.contentDescription)
}
)
} else {
Image(
painter = painterResource(id = trailingIcon.idRes),
contentDescription = trailingIcon.contentDescription?.let {
stringResource(trailingIcon.contentDescription)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ internal class FormViewModel @Inject internal constructor(
}
}

private val creditBillingElement = elements
private val cardBillingElement = elements
.map { elementsList ->
elementsList
?.filterIsInstance<SectionElement>()
Expand All @@ -134,11 +134,11 @@ internal class FormViewModel @Inject internal constructor(
saveForFutureUseElement.map {
it?.controller?.hiddenIdentifiers ?: flowOf(emptyList())
}.flattenConcat(),
creditBillingElement.map {
cardBillingElement.map {
it?.hiddenIdentifiers ?: flowOf(emptyList())
}.flattenConcat()
) { showFutureUse, saveFutureUseIdentifiers, creditBillingIdentifiers ->
val hiddenIdentifiers = saveFutureUseIdentifiers.plus(creditBillingIdentifiers)
) { showFutureUse, saveFutureUseIdentifiers, cardBillingIdentifiers ->
val hiddenIdentifiers = saveFutureUseIdentifiers.plus(cardBillingIdentifiers)
// For hidden *section* identifiers, list of identifiers of elements in the section
val identifiers = sectionToFieldIdentifierMap
.filter { idControllerPair ->
Expand Down
Loading