@@ -133,13 +133,55 @@ function PaymentCardForm({
133133 currencySelectorRoute,
134134} : PaymentCardFormProps ) {
135135 const styles = useThemeStyles ( ) ;
136- const [ data , metadata ] = useOnyx ( ONYXKEYS . FORMS . ADD_PAYMENT_CARD_FORM ) ;
136+ const [ data , metadata ] = useOnyx ( ONYXKEYS . FORMS . ADD_PAYMENT_CARD_FORM , { canBeMissing : true } ) ;
137137
138138 const { translate} = useLocalize ( ) ;
139139 const route = useRoute ( ) ;
140140 const label = CARD_LABELS [ isDebitCard ? CARD_TYPES . DEBIT_CARD : CARD_TYPES . PAYMENT_CARD ] ;
141141
142142 const cardNumberRef = useRef < AnimatedTextInputRef > ( null ) ;
143+ const [ expirationDate , setExpirationDate ] = useState ( data ?. expirationDate ) ;
144+
145+ const previousValueRef = useRef < string > ( '' ) ;
146+
147+ // Formats user input into a valid expiration date (MM/YY) and automatically adds slash after the month.
148+ // Ensures the month is always between 01 and 12 by correcting invalid value to match the proper format.
149+ const onChangeExpirationDate = useCallback ( ( newValue : string ) => {
150+ if ( typeof newValue !== 'string' ) {
151+ return ;
152+ }
153+
154+ let value = newValue . replace ( CONST . REGEX . NON_NUMERIC , '' ) ;
155+
156+ if ( value . length === 1 ) {
157+ const firstDigit = value . charAt ( 0 ) ;
158+ if ( parseInt ( firstDigit , 10 ) > 1 ) {
159+ value = `0${ firstDigit } ` ;
160+ }
161+ }
162+
163+ if ( value . length >= 2 ) {
164+ const month = parseInt ( value . slice ( 0 , 2 ) , 10 ) ;
165+ if ( value . startsWith ( '00' ) ) {
166+ value = '0' ;
167+ }
168+ if ( month > 12 ) {
169+ value = `0${ value . charAt ( 0 ) } ${ value . charAt ( 1 ) } ${ value . charAt ( 2 ) } ` ;
170+ }
171+ }
172+
173+ const prevValue = previousValueRef . current ?. replace ( CONST . REGEX . NON_NUMERIC , '' ) ?? '' ;
174+ let formattedValue = value ;
175+
176+ if ( value . length === 2 && prevValue . length < 2 ) {
177+ formattedValue = `${ value } /` ;
178+ } else if ( value . length > 2 ) {
179+ formattedValue = `${ value . slice ( 0 , 2 ) } /${ value . slice ( 2 , 4 ) } ` ;
180+ }
181+
182+ previousValueRef . current = formattedValue ;
183+ setExpirationDate ( formattedValue ) ;
184+ } , [ ] ) ;
143185
144186 const [ cardNumber , setCardNumber ] = useState ( '' ) ;
145187
@@ -154,7 +196,10 @@ function PaymentCardForm({
154196 errors . cardNumber = translate ( label . error . cardNumber ) ;
155197 }
156198
157- if ( values . expirationDate && ! isValidExpirationDate ( values . expirationDate ) ) {
199+ // When user pastes 5 digit value without slash, trim it to the first 4 digits before validation.
200+ const normalizedExpirationDate = values . expirationDate ?. length === 5 && ! values . expirationDate . includes ( '/' ) ? values . expirationDate . slice ( 0 , 4 ) : values . expirationDate ;
201+
202+ if ( normalizedExpirationDate && ! isValidExpirationDate ( normalizedExpirationDate ) ) {
158203 errors . expirationDate = translate ( label . error . expirationDate ) ;
159204 }
160205
@@ -251,14 +296,17 @@ function PaymentCardForm({
251296 < View style = { [ styles . mr2 , styles . flex1 ] } >
252297 < InputWrapper
253298 defaultValue = { data ?. expirationDate }
299+ value = { expirationDate }
300+ onChangeText = { onChangeExpirationDate }
254301 InputComponent = { TextInput }
255302 inputID = { INPUT_IDS . EXPIRATION_DATE }
256303 label = { translate ( label . defaults . expiration ) }
304+ testID = { label . defaults . expiration }
257305 aria-label = { translate ( label . defaults . expiration ) }
258306 role = { CONST . ROLE . PRESENTATION }
259307 placeholder = { translate ( label . defaults . expirationDate ) }
260308 inputMode = { CONST . INPUT_MODE . NUMERIC }
261- maxLength = { 4 }
309+ maxLength = { 5 }
262310 />
263311 </ View >
264312 < View style = { styles . flex1 } >
0 commit comments