Skip to content

Commit 8540a98

Browse files
authored
Merge pull request Expensify#65079 from samranahm/64608/expiration-date-input-format
feat: improve expiration date input format by auto-inserting slash after month
2 parents da0805a + 01591fa commit 8540a98

12 files changed

Lines changed: 183 additions & 13 deletions

File tree

src/components/AddPaymentCard/PaymentCardForm.tsx

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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}>

src/languages/de.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1803,7 +1803,7 @@ const translations = {
18031803
nameOnCard: 'Name auf der Karte',
18041804
paymentCardNumber: 'Kartennummer',
18051805
expiration: 'Ablaufdatum',
1806-
expirationDate: 'MMYY',
1806+
expirationDate: 'MM/YY',
18071807
cvv: 'CVV',
18081808
billingAddress: 'Rechnungsadresse',
18091809
growlMessageOnSave: 'Ihre Zahlungskarte wurde erfolgreich hinzugefügt',

src/languages/en.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1780,7 +1780,7 @@ const translations = {
17801780
nameOnCard: 'Name on card',
17811781
paymentCardNumber: 'Card number',
17821782
expiration: 'Expiration date',
1783-
expirationDate: 'MMYY',
1783+
expirationDate: 'MM/YY',
17841784
cvv: 'CVV',
17851785
billingAddress: 'Billing address',
17861786
growlMessageOnSave: 'Your payment card was successfully added',

src/languages/es.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1780,7 +1780,7 @@ const translations = {
17801780
nameOnCard: 'Nombre en la tarjeta',
17811781
paymentCardNumber: 'Número de la tarjeta',
17821782
expiration: 'Fecha de vencimiento',
1783-
expirationDate: 'MMAA',
1783+
expirationDate: 'MM/AA',
17841784
cvv: 'CVV',
17851785
billingAddress: 'Dirección de envio',
17861786
growlMessageOnSave: 'Tu tarjeta de pago se añadió correctamente',

src/languages/fr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1805,7 +1805,7 @@ const translations = {
18051805
nameOnCard: 'Nom sur la carte',
18061806
paymentCardNumber: 'Numéro de carte',
18071807
expiration: "Date d'expiration",
1808-
expirationDate: 'MMYY',
1808+
expirationDate: 'MM/YY',
18091809
cvv: 'CVV',
18101810
billingAddress: 'Adresse de facturation',
18111811
growlMessageOnSave: 'Votre carte de paiement a été ajoutée avec succès',

src/languages/it.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1796,7 +1796,7 @@ const translations = {
17961796
nameOnCard: 'Nome sulla carta',
17971797
paymentCardNumber: 'Numero di carta',
17981798
expiration: 'Data di scadenza',
1799-
expirationDate: 'MMYY',
1799+
expirationDate: 'MM/YY',
18001800
cvv: 'CVV',
18011801
billingAddress: 'Indirizzo di fatturazione',
18021802
growlMessageOnSave: 'La tua carta di pagamento è stata aggiunta con successo',

src/languages/ja.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1794,7 +1794,7 @@ const translations = {
17941794
nameOnCard: 'カード名義人',
17951795
paymentCardNumber: 'カード番号',
17961796
expiration: '有効期限',
1797-
expirationDate: 'MMYY',
1797+
expirationDate: 'MM/YY',
17981798
cvv: 'CVV',
17991799
billingAddress: '請求先住所',
18001800
growlMessageOnSave: 'お支払いカードが正常に追加されました',

src/languages/nl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1796,7 +1796,7 @@ const translations = {
17961796
nameOnCard: 'Naam op kaart',
17971797
paymentCardNumber: 'Kaartnummer',
17981798
expiration: 'Vervaldatum',
1799-
expirationDate: 'MMYY',
1799+
expirationDate: 'MM/YY',
18001800
cvv: 'CVV',
18011801
billingAddress: 'Factuuradres',
18021802
growlMessageOnSave: 'Uw betaalkaart is succesvol toegevoegd',

src/languages/pl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1792,7 +1792,7 @@ const translations = {
17921792
nameOnCard: 'Imię na karcie',
17931793
paymentCardNumber: 'Numer karty',
17941794
expiration: 'Data wygaśnięcia',
1795-
expirationDate: 'MMYY',
1795+
expirationDate: 'MM/YY',
17961796
cvv: 'CVV',
17971797
billingAddress: 'Adres rozliczeniowy',
17981798
growlMessageOnSave: 'Twoja karta płatnicza została pomyślnie dodana',

src/languages/pt-BR.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1794,7 +1794,7 @@ const translations = {
17941794
nameOnCard: 'Nome no cartão',
17951795
paymentCardNumber: 'Número do cartão',
17961796
expiration: 'Data de validade',
1797-
expirationDate: 'MMYY',
1797+
expirationDate: 'MM/YY',
17981798
cvv: 'CVV',
17991799
billingAddress: 'Endereço de cobrança',
18001800
growlMessageOnSave: 'Seu cartão de pagamento foi adicionado com sucesso',

0 commit comments

Comments
 (0)