Skip to content

Commit df84a51

Browse files
committed
Show supported card brands in the CardComponent
COSDK-660
1 parent 8d50588 commit df84a51

File tree

3 files changed

+170
-52
lines changed

3 files changed

+170
-52
lines changed

card/src/main/java/com/adyen/checkout/card/internal/ui/view/CardComponent.kt

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,13 @@ package com.adyen.checkout.card.internal.ui.view
1111
import androidx.compose.foundation.layout.Arrangement
1212
import androidx.compose.foundation.layout.Column
1313
import androidx.compose.foundation.layout.fillMaxWidth
14-
import androidx.compose.foundation.text.KeyboardOptions
15-
import androidx.compose.foundation.text.input.maxLength
1614
import androidx.compose.runtime.Composable
17-
import androidx.compose.runtime.remember
1815
import androidx.compose.ui.Modifier
19-
import androidx.compose.ui.focus.onFocusChanged
20-
import androidx.compose.ui.text.input.KeyboardType
2116
import androidx.compose.ui.tooling.preview.Preview
2217
import com.adyen.checkout.card.internal.ui.state.CardChangeListener
2318
import com.adyen.checkout.card.internal.ui.state.CardViewState
24-
import com.adyen.checkout.core.common.helper.CardNumberValidator
25-
import com.adyen.checkout.core.common.localization.CheckoutLocalizationKey
26-
import com.adyen.checkout.core.common.localization.internal.helper.resolveString
2719
import com.adyen.checkout.core.components.internal.ui.state.model.TextInputState
28-
import com.adyen.checkout.ui.internal.CheckoutTextField
2920
import com.adyen.checkout.ui.internal.ComponentScaffold
30-
import com.adyen.checkout.ui.internal.DigitOnlyInputTransformation
3121
import com.adyen.checkout.ui.internal.Dimensions
3222
import com.adyen.checkout.ui.internal.PayButton
3323

@@ -45,11 +35,13 @@ internal fun CardComponent(
4535
},
4636
) {
4737
Column(
48-
modifier = modifier.fillMaxWidth(),
38+
modifier = Modifier.fillMaxWidth(),
4939
verticalArrangement = Arrangement.spacedBy(Dimensions.Large),
5040
) {
5141
CardNumberField(
5242
cardNumberState = viewState.cardNumber,
43+
supportedCardBrands = viewState.supportedCardBrands,
44+
isSupportedCardBrandsShown = viewState.isSupportedCardBrandsShown,
5345
isAmex = viewState.isAmex,
5446
onCardNumberChanged = changeListener::onCardNumberChanged,
5547
onCardNumberFocusChanged = changeListener::onCardNumberFocusChanged,
@@ -59,47 +51,6 @@ internal fun CardComponent(
5951
// TODO - Card Full UI
6052
}
6153

62-
@Composable
63-
private fun CardNumberField(
64-
cardNumberState: TextInputState,
65-
isAmex: Boolean,
66-
onCardNumberChanged: (String) -> Unit,
67-
onCardNumberFocusChanged: (Boolean) -> Unit,
68-
) {
69-
val showCardNumberError =
70-
cardNumberState.errorMessage != null && cardNumberState.showError
71-
val supportingTextCardNumber = if (showCardNumberError) {
72-
cardNumberState.errorMessage?.let { resolveString(it) }
73-
} else {
74-
null
75-
}
76-
77-
val outputTransformation = remember(isAmex) {
78-
CardNumberOutputTransformation(isAmex = isAmex)
79-
}
80-
81-
CheckoutTextField(
82-
modifier = Modifier
83-
.fillMaxWidth()
84-
.onFocusChanged { focusState ->
85-
onCardNumberFocusChanged(focusState.isFocused)
86-
},
87-
label = resolveString(CheckoutLocalizationKey.CARD_NUMBER),
88-
initialValue = cardNumberState.text,
89-
isError = showCardNumberError,
90-
supportingText = supportingTextCardNumber,
91-
onValueChange = { value ->
92-
onCardNumberChanged(value)
93-
},
94-
inputTransformation = DigitOnlyInputTransformation().maxLength(
95-
maxLength = CardNumberValidator.MAXIMUM_CARD_NUMBER_LENGTH,
96-
),
97-
outputTransformation = outputTransformation,
98-
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
99-
shouldFocus = cardNumberState.isFocused,
100-
)
101-
}
102-
10354
@Preview(showBackground = true)
10455
@Composable
10556
private fun CardComponentPreview() {
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
* Copyright (c) 2025 Adyen N.V.
3+
*
4+
* This file is open source and available under the MIT license. See the LICENSE file for more info.
5+
*
6+
* Created by ararat on 7/11/2025.
7+
*/
8+
9+
package com.adyen.checkout.card.internal.ui.view
10+
11+
import androidx.compose.animation.AnimatedVisibility
12+
import androidx.compose.foundation.layout.Arrangement
13+
import androidx.compose.foundation.layout.Column
14+
import androidx.compose.foundation.layout.FlowRow
15+
import androidx.compose.foundation.layout.fillMaxWidth
16+
import androidx.compose.foundation.layout.padding
17+
import androidx.compose.foundation.layout.size
18+
import androidx.compose.foundation.shape.RoundedCornerShape
19+
import androidx.compose.foundation.text.KeyboardOptions
20+
import androidx.compose.foundation.text.input.maxLength
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.remember
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.draw.clip
25+
import androidx.compose.ui.draw.dropShadow
26+
import androidx.compose.ui.focus.onFocusChanged
27+
import androidx.compose.ui.graphics.shadow.Shadow
28+
import androidx.compose.ui.text.input.KeyboardType
29+
import androidx.compose.ui.tooling.preview.Preview
30+
import androidx.compose.ui.unit.DpOffset
31+
import androidx.compose.ui.unit.dp
32+
import com.adyen.checkout.core.common.CardBrand
33+
import com.adyen.checkout.core.common.CardType
34+
import com.adyen.checkout.core.common.helper.CardNumberValidator
35+
import com.adyen.checkout.core.common.internal.ui.CheckoutNetworkLogo
36+
import com.adyen.checkout.core.common.localization.CheckoutLocalizationKey
37+
import com.adyen.checkout.core.common.localization.internal.helper.resolveString
38+
import com.adyen.checkout.core.components.internal.ui.state.model.TextInputState
39+
import com.adyen.checkout.ui.internal.CheckoutTextField
40+
import com.adyen.checkout.ui.internal.CheckoutThemeProvider
41+
import com.adyen.checkout.ui.internal.DigitOnlyInputTransformation
42+
import com.adyen.checkout.ui.internal.Dimensions
43+
44+
@Composable
45+
internal fun CardNumberField(
46+
cardNumberState: TextInputState,
47+
supportedCardBrands: List<CardBrand>,
48+
isSupportedCardBrandsShown: Boolean,
49+
isAmex: Boolean,
50+
onCardNumberChanged: (String) -> Unit,
51+
onCardNumberFocusChanged: (Boolean) -> Unit,
52+
modifier: Modifier = Modifier,
53+
) {
54+
Column(
55+
modifier = modifier,
56+
) {
57+
CardNumberInputField(
58+
cardNumberState = cardNumberState,
59+
isAmex = isAmex,
60+
onCardNumberChanged = onCardNumberChanged,
61+
onCardNumberFocusChanged = onCardNumberFocusChanged,
62+
)
63+
64+
CardBrandsList(
65+
cardBrands = supportedCardBrands,
66+
visible = isSupportedCardBrandsShown,
67+
)
68+
}
69+
}
70+
71+
@Composable
72+
private fun CardNumberInputField(
73+
cardNumberState: TextInputState,
74+
isAmex: Boolean,
75+
onCardNumberChanged: (String) -> Unit,
76+
onCardNumberFocusChanged: (Boolean) -> Unit,
77+
modifier: Modifier = Modifier,
78+
) {
79+
val showCardNumberError =
80+
cardNumberState.errorMessage != null && cardNumberState.showError
81+
val supportingTextCardNumber = if (showCardNumberError) {
82+
cardNumberState.errorMessage?.let { resolveString(it) }
83+
} else {
84+
null
85+
}
86+
87+
val outputTransformation = remember(isAmex) {
88+
CardNumberOutputTransformation(isAmex = isAmex)
89+
}
90+
91+
CheckoutTextField(
92+
modifier = modifier
93+
.fillMaxWidth()
94+
.onFocusChanged { focusState ->
95+
onCardNumberFocusChanged(focusState.isFocused)
96+
},
97+
label = resolveString(CheckoutLocalizationKey.CARD_NUMBER),
98+
initialValue = cardNumberState.text,
99+
isError = showCardNumberError,
100+
supportingText = supportingTextCardNumber,
101+
onValueChange = { value ->
102+
onCardNumberChanged(value)
103+
},
104+
inputTransformation = DigitOnlyInputTransformation().maxLength(
105+
maxLength = CardNumberValidator.MAXIMUM_CARD_NUMBER_LENGTH,
106+
),
107+
outputTransformation = outputTransformation,
108+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
109+
shouldFocus = cardNumberState.isFocused,
110+
)
111+
}
112+
113+
@Composable
114+
private fun CardBrandsList(
115+
cardBrands: List<CardBrand>,
116+
visible: Boolean,
117+
modifier: Modifier = Modifier,
118+
) {
119+
AnimatedVisibility(
120+
modifier = modifier.fillMaxWidth(),
121+
visible = visible,
122+
) {
123+
FlowRow(
124+
modifier = Modifier.padding(top = Dimensions.ExtraSmall),
125+
horizontalArrangement = Arrangement.spacedBy(Dimensions.ExtraSmall),
126+
verticalArrangement = Arrangement.spacedBy(Dimensions.ExtraSmall),
127+
) {
128+
for (cardBrand in cardBrands) {
129+
CheckoutNetworkLogo(
130+
modifier = Modifier
131+
.size(24.dp, 16.dp)
132+
.dropShadow(
133+
shape = RoundedCornerShape(Dimensions.CornerRadius),
134+
shadow = Shadow(
135+
radius = 1.dp,
136+
offset = DpOffset(x = 0.dp, 2.dp),
137+
color = CheckoutThemeProvider.colors.container,
138+
),
139+
)
140+
.clip(RoundedCornerShape(Dimensions.CornerRadius)),
141+
txVariant = cardBrand.txVariant,
142+
)
143+
}
144+
}
145+
}
146+
}
147+
148+
@Preview(showBackground = true)
149+
@Composable
150+
private fun CardNumberFieldPreview() {
151+
CardNumberField(
152+
cardNumberState = TextInputState(
153+
"5555444433331111",
154+
),
155+
supportedCardBrands = listOf(
156+
CardBrand(CardType.MASTERCARD.txVariant),
157+
CardBrand(CardType.VISA.txVariant),
158+
CardBrand(CardType.AMERICAN_EXPRESS.txVariant),
159+
),
160+
isSupportedCardBrandsShown = true,
161+
isAmex = false,
162+
onCardNumberChanged = {},
163+
onCardNumberFocusChanged = {},
164+
)
165+
}

ui/src/main/java/com/adyen/checkout/ui/internal/Dimensions.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@ object Dimensions {
2929
val TripleExtraLarge = 48.dp
3030

3131
val MinTouchTarget = 48.dp
32+
33+
val CornerRadius = 4.dp
3234
}

0 commit comments

Comments
 (0)