Skip to content

Card number

Learn about customisation options for the card number component.

Styling

val cardNumberComponentConfig = CardNumberComponentConfig(
    label = String,
    placeholder = String,
    onCardBrandChanged = ((CardBrand?) -> Unit)?,
    onCardBrandCannotRecognised = (() -> Unit)?,
    style = CardNumberStyle?,
    formatCardNumber = Boolean,
    validationEnabled = Boolean,
    validations = CardNumberValidations,
    showTrailingIcon = Boolean,
    showValidIcon = Boolean,
    validIcon = Int,
    invalidIcon = Int,
    showInvalidIcon = Boolean,
    useTransparentCardBrandImage = Boolean,
    acceptedCardBrands = List<CardBrand>,
    renderCardBrandSelector = Boolean,
    cardBrandSelectorComponent = CardBrandSelectorComponent?,
    cardCvcComponent = CardCvcComponent?,
    showTooltip = Boolean,
    showHintIcon = Boolean,
    hintIcon = Int,
    tooltipData = TooltipData,
    dynamicCardImageComponent = DynamicCardImageComponent?,
    focusOrder = Int,
    displayRequiredIcon = Boolean?,
    isRequired = Boolean
)
PropertyDescription
inputAttributes
InputAttributes?
Additional input field attributes and configurations.
isRequired
Boolean
Whether the card number field is required for submission.
placeholder
String
The placeholder text showing the card number format.
mask
String
The input mask pattern for formatting (e.g., #### #### #### ####).
componentStyles
ComponentStyles?
Custom styling for the component container.
inputStyles
StateStyles?
Styling for the input field in various states.
label
String?
The input label's text.
labelStyles
StateStyles?
The label styling based on the state.
labelPosition
LabelPosition
The position of the label relative to the input.

Possible values:
  • ABOVE
  • BELOW
  • START
  • END
isFloatingLabel
Boolean
Whether the label should float above the input when focused.
invalidTextStyles
ComponentStyle?
Styling for the validation message text.
invalidTextPosition
ErrorPosition
The position of the error messages relative to the input.

Possible values:
  • ABOVE
  • BELOW
  • START
  • END
guideText
String?
Helper text to display below the input field.
validations
CardNumberValidationRules?
Validation rules for card numbers.
invalidIcon
IconConfig?
Configuration for the invalid state icon.
validIcon
IconConfig?
Configuration for the valid state icon.
cardBrandIcon
IconConfig?
Configuration for the card brand icon display.
displayValidIcon
Boolean
Whether to show a success icon when valid.
displayInvalidIcon
Boolean
Whether to show an error icon when invalid.
displayCardBrandIcon
Boolean
Whether to show a detected card brand icon.
enableLuhnValidation
Boolean
Whether to enable Luhn algorithm validation.
enableCardBrandDetection
Boolean
Whether to auto-detect the card brand from the number.
supportedCardBrands
List<CardBrand>>
A list of supported card brands.
maskInput
Boolean
Whether to apply formatting mask to input.
autoFormat
Boolean
Whether to automatically format input with spaces.
maxLength
Int
The maximum length including formatting characters.
enableSecureInput
Boolean
Whether to enable secure input mode.
maskAfterInput
Boolean
Whether to mask numbers after input.
securityDelay
Long
The delay before masking input, in milliseconds.
contentDescription
String?
cardBrandContentDescription
String?
Accessibility description for card brand icon.
allowCopyPaste
Boolean
Whether to allow copy/paste. This is usually disabled for security reasons.
validationOnBlur
Boolean
Whether to validate when the input loses focus.
validationOnChange
Boolean
Whether to validate as the value changes.
focusOrder
Int
The tab order for accessibility navigation.
enableAnimations
Boolean
Whether to enable component animations.
cardBrandAnimationDuration
Long
The duration of the card brand change animations.
validationAnimationDuration
Long
The duration of the validation state animations.

Callbacks

data class CardNumberComponentConfig(
    val onCardBrandChanged: ((CardBrandChangeEvent) -> Unit)? = null,
    val onCardBrandCanNotRecognized: ((CardBrandChangeEvent) -> Unit)? = null,
    val onChange: ((CardNumberInputEvent) -> Unit)? = null,
    val onFocus: ((CardNumberFocusEvent) -> Unit)? = null,
    val onValidationPassed: ((List<ValidationResult>) -> Unit)? = null,
    val onValidationFailed: ((List<ValidationResult>) -> Unit)? = null
)
CallbackDescription
onCardBrandChanged: (CardBrandChangeEvent) -> UnitEvent handler for when the detected brand changes.
onCardBrandCanNotRecognized: () -> UnitEvent handler for when the card brand can't be recognised.
onChange: (CardNumberInputEvent) -> UnitEvent handler for when the card number value changes.
onFocus: (CardNumberFocusEvent) -> UnitEvent handler for when the input receives focus.
onValidationPassed: (List<ValidationResult>) -> UnitEvent handler for when validation passes.
onValidationFailed: (List<ValidationResult>) -> UnitEvent handler for when validation fails.

For more information about callbacks, see Events.

Example

class CardNumberComponentExample : ComponentActivity() {
    private lateinit var pxpCheckout: PxpCheckout
    private lateinit var cardNumberComponent: CardNumberComponent
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setupCardNumberComponent()
        
        setContent {
            MaterialTheme {
                CardNumberComponentScreen()
            }
        }
    }
    
    private fun setupCardNumberComponent() {
        val cardNumberConfig = CardNumberComponentConfig(
            // Basic configuration
            label = "Card Number",
            placeholder = "1234 5678 9012 3456",
            mask = "#### #### #### ####",
            isRequired = true,
            
            // Input attributes
            inputAttributes = InputAttributes(
                maxLength = 19,
                keyboardType = KeyboardType.Number,
                imeAction = ImeAction.Next,
                autoComplete = AutoCompleteType.CREDIT_CARD_NUMBER
            ),
            
            // Card number specific features
            enableLuhnValidation = true,
            enableCardBrandDetection = true,
            supportedCardBrands = listOf(
                CardBrand.VISA,
                CardBrand.MASTERCARD,
                CardBrand.AMERICAN_EXPRESS,
                CardBrand.DISCOVER
            ),
            maskInput = true,
            autoFormat = true,
            maxLength = 19,
            
            // Security configuration
            enableSecureInput = true,
            maskAfterInput = false,
            securityDelay = 3000L,
            
            // Component styling
            componentStyles = ComponentStyles(
                container = StateStyles(
                    base = ComponentStyle(
                        colors = ComponentColors(
                            background = Color.White,
                            border = Color(0xFFE0E0E0)
                        ),
                        dimensions = ComponentDimensions(
                            padding = PaddingValues(16.dp),
                            cornerRadius = 8.dp
                        ),
                        borders = ComponentBorders(
                            width = 1.dp,
                            style = BorderStyle.SOLID
                        )
                    ),
                    valid = ComponentStyle(
                        colors = ComponentColors(
                            border = Color(0xFF4CAF50),
                            background = Color(0xFFF8FFF8)
                        ),
                        animations = ComponentAnimations(
                            duration = 300.milliseconds,
                            easing = FastOutSlowInEasing
                        )
                    ),
                    invalid = ComponentStyle(
                        colors = ComponentColors(
                            border = Color(0xFFF44336),
                            background = Color(0xFFFFF8F8)
                        ),
                        animations = ComponentAnimations(
                            duration = 300.milliseconds,
                            easing = FastOutSlowInEasing
                        )
                    ),
                    focused = ComponentStyle(
                        colors = ComponentColors(
                            border = Color(0xFF2196F3)
                        ),
                        dimensions = ComponentDimensions(
                            elevation = 2.dp
                        )
                    )
                )
            ),
            
            // Input field styling
            inputStyles = StateStyles(
                base = ComponentStyle(
                    typography = ComponentTypography(
                        fontSize = 18.sp,
                        fontWeight = FontWeight.Medium,
                        color = Color(0xFF212121),
                        fontFamily = FontFamily.Monospace,
                        letterSpacing = 1.sp
                    ),
                    colors = ComponentColors(
                        background = Color.Transparent,
                        text = Color(0xFF212121),
                        placeholder = Color(0xFF9E9E9E)
                    ),
                    dimensions = ComponentDimensions(
                        padding = PaddingValues(horizontal = 16.dp, vertical = 12.dp)
                    )
                ),
                valid = ComponentStyle(
                    colors = ComponentColors(
                        text = Color(0xFF2E7D32)
                    )
                ),
                invalid = ComponentStyle(
                    colors = ComponentColors(
                        text = Color(0xFFD32F2F)
                    )
                ),
                focused = ComponentStyle(
                    colors = ComponentColors(
                        text = Color(0xFF1976D2)
                    )
                )
            ),
            
            // Label styling
            labelStyles = StateStyles(
                base = ComponentStyle(
                    typography = ComponentTypography(
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Medium,
                        color = Color(0xFF757575),
                        letterSpacing = 0.1.sp
                    ),
                    dimensions = ComponentDimensions(
                        padding = PaddingValues(bottom = 8.dp)
                    )
                ),
                invalid = ComponentStyle(
                    typography = ComponentTypography(
                        color = Color(0xFFD32F2F)
                    )
                ),
                focused = ComponentStyle(
                    typography = ComponentTypography(
                        color = Color(0xFF1976D2)
                    )
                )
            ),
            
            // Label configuration
            labelPosition = LabelPosition.ABOVE,
            isFloatingLabel = false,
            
            // Error message styling
            invalidTextStyles = ComponentStyle(
                typography = ComponentTypography(
                    fontSize = 12.sp,
                    color = Color(0xFFD32F2F),
                    fontStyle = FontStyle.Normal
                ),
                dimensions = ComponentDimensions(
                    padding = PaddingValues(top = 4.dp, start = 16.dp)
                )
            ),
            invalidTextPosition = ErrorPosition.BELOW,
            
            // Guide text
            guideText = "Enter your 16-digit card number",
            
            // Validation rules
            validations = CardNumberValidationRules(
                minLength = 13,
                maxLength = 19,
                enableLuhnCheck = true,
                allowedBrands = listOf(
                    CardBrand.VISA,
                    CardBrand.MASTERCARD,
                    CardBrand.AMERICAN_EXPRESS,
                    CardBrand.DISCOVER
                ),
                customValidator = { cardNumber ->
                    when {
                        cardNumber.isBlank() -> ValidationResult(
                            isValid = false,
                            errorMessage = "Card number is required",
                            errorCode = "REQUIRED"
                        )
                        cardNumber.replace(" ", "").length < 13 -> ValidationResult(
                            isValid = false,
                            errorMessage = "Card number is too short",
                            errorCode = "TOO_SHORT"
                        )
                        !isValidLuhn(cardNumber.replace(" ", "")) -> ValidationResult(
                            isValid = false,
                            errorMessage = "Invalid card number",
                            errorCode = "INVALID_LUHN"
                        )
                        else -> ValidationResult(isValid = true)
                    }
                }
            ),
            
            // Icon configuration
            validIcon = IconConfig(
                icon = Icons.Default.CheckCircle,
                tint = Color(0xFF4CAF50),
                size = 20.dp
            ),
            invalidIcon = IconConfig(
                icon = Icons.Default.Error,
                tint = Color(0xFFF44336),
                size = 20.dp
            ),
            cardBrandIcon = IconConfig(
                icon = Icons.Default.CreditCard,
                tint = Color(0xFF757575),
                size = 24.dp
            ),
            displayValidIcon = true,
            displayInvalidIcon = true,
            displayCardBrandIcon = true,
            
            // Accessibility
            contentDescription = "Credit card number input field",
            cardBrandContentDescription = "Detected card brand",
            
            // Input behaviour
            allowCopyPaste = false, // Security consideration
            validationOnBlur = true,
            validationOnChange = true,
            focusOrder = 1,
            
            // Animation configuration
            enableAnimations = true,
            cardBrandAnimationDuration = 300L,
            validationAnimationDuration = 200L,
            
            // Callback functions
            onChange = { event ->
                Log.d("CardNumber", "Card number changed: ${event.formattedValue}")
                Log.d("CardNumber", "Is valid: ${event.isValid}")
                Log.d("CardNumber", "Detected brand: ${event.detectedBrand?.displayName}")
                Log.d("CardNumber", "Luhn valid: ${event.luhnValid}")
                
                if (event.validationErrors.isNotEmpty()) {
                    Log.d("CardNumber", "Validation errors: ${event.validationErrors}")
                }
                
                handleCardNumberChange(event)
            },
            
            onFocus = { event ->
                Log.d("CardNumber", "Card number focused: ${event.isFocused}")
                handleCardNumberFocus(event)
            },
            
            onBlur = { event ->
                Log.d("CardNumber", "Card number blurred: ${event.isFocused}")
                handleCardNumberBlur(event)
            },
            
            onValidationPassed = { results ->
                Log.d("CardNumber", "Card number validation passed")
                handleValidationPassed(results)
            },
            
            onValidationFailed = { results ->
                Log.d("CardNumber", "Card number validation failed: $results")
                handleValidationFailed(results)
            },
            
            onCardBrandDetected = { brandEvent ->
                Log.d("CardNumber", "Card brand detected: ${brandEvent.cardBrand.displayName}")
                Log.d("CardNumber", "Detection confidence: ${brandEvent.confidence}")
                Log.d("CardNumber", "Is supported: ${brandEvent.isSupported}")
                
                handleCardBrandDetected(brandEvent)
            },
            
            onCardBrandChanged = { changeEvent ->
                Log.d("CardNumber", "Brand changed from ${changeEvent.previousBrand?.displayName} to ${changeEvent.newBrand?.displayName}")
                handleCardBrandChanged(changeEvent)
            },
            
            onLuhnValidation = { luhnEvent ->
                Log.d("CardNumber", "Luhn validation: ${luhnEvent.isValid}")
                Log.d("CardNumber", "Checksum: ${luhnEvent.checksum}")
                handleLuhnValidation(luhnEvent)
            }
        )
        
        cardNumberComponent = pxpCheckout.createComponent(
            ComponentType.CARD_NUMBER,
            cardNumberConfig
        )
    }
    
    @Composable
    private fun CardNumberComponentScreen() {
        var cardNumber by remember { mutableStateOf("") }
        var detectedBrand by remember { mutableStateOf<CardBrand?>(null) }
        var isValid by remember { mutableStateOf(false) }
        var validationErrors by remember { mutableStateOf<List<ValidationError>>(emptyList()) }
        var isLuhnValid by remember { mutableStateOf(false) }
        
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text(
                text = "Card Number Component",
                style = MaterialTheme.typography.headlineMedium,
                modifier = Modifier.padding(bottom = 8.dp)
            )
            
            // Card number component
            Card(
                modifier = Modifier.fillMaxWidth(),
                elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
            ) {
                Column(modifier = Modifier.padding(16.dp)) {
                    pxpCheckout.buildComponentView(
                        component = cardNumberComponent,
                        modifier = Modifier.fillMaxWidth()
                    )
                }
            }
            
            // Card brand display
            detectedBrand?.let { brand ->
                Card(
                    modifier = Modifier.fillMaxWidth(),
                    colors = CardDefaults.cardColors(
                        containerColor = Color(0xFFF3F4F6)
                    )
                ) {
                    Row(
                        modifier = Modifier.padding(16.dp),
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.spacedBy(12.dp)
                    ) {
                        Image(
                            painter = painterResource(brand.iconResId),
                            contentDescription = brand.displayName,
                            modifier = Modifier.size(32.dp)
                        )
                        
                        Column {
                            Text(
                                text = "Detected Card Brand",
                                style = MaterialTheme.typography.labelMedium,
                                color = Color(0xFF757575)
                            )
                            Text(
                                text = brand.displayName,
                                style = MaterialTheme.typography.bodyMedium,
                                fontWeight = FontWeight.Medium
                            )
                        }
                        
                        Spacer(modifier = Modifier.weight(1f))
                        
                        if (isValid && isLuhnValid) {
                            Icon(
                                imageVector = Icons.Default.CheckCircle,
                                contentDescription = "Valid",
                                tint = Color(0xFF4CAF50)
                            )
                        }
                    }
                }
            }
            
            // Validation status
            Card(
                modifier = Modifier.fillMaxWidth(),
                colors = CardDefaults.cardColors(
                    containerColor = when {
                        isValid && isLuhnValid -> Color(0xFFE8F5E8)
                        validationErrors.isNotEmpty() -> Color(0xFFFFEBEE)
                        else -> MaterialTheme.colorScheme.surfaceVariant
                    }
                )
            ) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Row(
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.spacedBy(8.dp)
                    ) {
                        when {
                            isValid && isLuhnValid -> {
                                Icon(
                                    imageVector = Icons.Default.CheckCircle,
                                    contentDescription = null,
                                    tint = Color(0xFF4CAF50)
                                )
                                Text(
                                    text = "Valid card number",
                                    style = MaterialTheme.typography.bodyMedium,
                                    fontWeight = FontWeight.Medium,
                                    color = Color(0xFF2E7D32)
                                )
                            }
                            validationErrors.isNotEmpty() -> {
                                Icon(
                                    imageVector = Icons.Default.Error,
                                    contentDescription = null,
                                    tint = Color(0xFFF44336)
                                )
                                Text(
                                    text = "Invalid card number",
                                    style = MaterialTheme.typography.bodyMedium,
                                    fontWeight = FontWeight.Medium,
                                    color = Color(0xFFD32F2F)
                                )
                            }
                            else -> {
                                Icon(
                                    imageVector = Icons.Default.Info,
                                    contentDescription = null,
                                    tint = Color(0xFF2196F3)
                                )
                                Text(
                                    text = "Enter card number",
                                    style = MaterialTheme.typography.bodyMedium,
                                    fontWeight = FontWeight.Medium
                                )
                            }
                        }
                    }
                    
                    if (cardNumber.isNotEmpty()) {
                        Spacer(modifier = Modifier.height(8.dp))
                        
                        Row(
                            horizontalArrangement = Arrangement.spacedBy(16.dp)
                        ) {
                            Column {
                                Text(
                                    text = "Length",
                                    style = MaterialTheme.typography.labelSmall,
                                    color = Color(0xFF757575)
                                )
                                Text(
                                    text = "${cardNumber.replace(" ", "").length} digits",
                                    style = MaterialTheme.typography.bodySmall
                                )
                            }
                            
                            Column {
                                Text(
                                    text = "Luhn Check",
                                    style = MaterialTheme.typography.labelSmall,
                                    color = Color(0xFF757575)
                                )
                                Text(
                                    text = if (isLuhnValid) "Valid" else "Invalid",
                                    style = MaterialTheme.typography.bodySmall,
                                    color = if (isLuhnValid) Color(0xFF4CAF50) else Color(0xFFF44336)
                                )
                            }
                        }
                    }
                    
                    if (validationErrors.isNotEmpty()) {
                        Spacer(modifier = Modifier.height(8.dp))
                        Column {
                            validationErrors.forEach { error ->
                                Text(
                                    text = "• ${error.message}",
                                    style = MaterialTheme.typography.bodySmall,
                                    color = Color(0xFFF44336)
                                )
                            }
                        }
                    }
                }
            }
        }
    }
    
    private fun handleCardNumberChange(event: CardNumberInputEvent) {
        // Update UI state
        // Validate card number
        // Update other components based on detected brand
    }
    
    private fun handleCardNumberFocus(event: CardNumberFocusEvent) {
        // Handle focus state changes
        // Show additional help if needed
    }
    
    private fun handleCardNumberBlur(event: CardNumberFocusEvent) {
        // Handle blur state changes
        // Trigger comprehensive validation
    }
    
    private fun handleValidationPassed(results: List<ValidationResult>) {
        // Handle successful validation
        // Enable next form field
        // Update form progress
    }
    
    private fun handleValidationFailed(results: List<ValidationResult>) {
        // Handle validation failures
        // Show specific error messages
        // Disable form submission
    }
    
    private fun handleCardBrandDetected(brandEvent: CardBrandEvent) {
        // Update UI to show detected brand
        // Update card brand icon
        // Configure other form fields based on brand
        
        if (!brandEvent.isSupported) {
            showUnsupportedCardBrandMessage(brandEvent.cardBrand)
        }
    }
    
    private fun handleCardBrandChanged(changeEvent: CardBrandChangeEvent) {
        // Handle brand change
        // Update form validation rules
        // Reset related field validations if needed
    }
    
    private fun handleLuhnValidation(luhnEvent: LuhnValidationEvent) {
        // Handle Luhn validation result
        // Update security indicators
        // Log validation for analytics
    }
    
    private fun showUnsupportedCardBrandMessage(cardBrand: CardBrand) {
        // Show message about unsupported card brand
        // Suggest supported alternatives
    }
    
    private fun isValidLuhn(cardNumber: String): Boolean {
        // Implement Luhn algorithm validation
        val digits = cardNumber.filter { it.isDigit() }.map { it.digitToInt() }
        if (digits.size < 13) return false
        
        val checksum = digits.asReversed().mapIndexed { index, digit ->
            if (index % 2 == 1) {
                val doubled = digit * 2
                if (doubled > 9) doubled - 9 else doubled
            } else {
                digit
            }
        }.sum()
        
        return checksum % 10 == 0
    }
}