From a8fc9fb889b5fe4a22ba7aeef4a8964a2a2fd498 Mon Sep 17 00:00:00 2001 From: raushan-skyflow Date: Fri, 4 Jul 2025 00:36:02 +0530 Subject: [PATCH 1/7] SK-2157 custom input formatting for collect elements --- src/components/CardNumberElement/index.tsx | 6 +- src/components/InputFieldElement/index.tsx | 3 +- src/core/CollectElement/index.ts | 6 +- src/core/constants/index.ts | 5 ++ src/utils/helpers/index.ts | 68 +++++++++++++++++++++- src/utils/logs/index.ts | 2 + 6 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/components/CardNumberElement/index.tsx b/src/components/CardNumberElement/index.tsx index de3b8b3..7500247 100644 --- a/src/components/CardNumberElement/index.tsx +++ b/src/components/CardNumberElement/index.tsx @@ -5,11 +5,12 @@ import React, { useEffect, useRef } from "react"; import { Image, Text, TextInput, View } from "react-native"; import type CollectElement from "../../core/CollectElement"; import { CARD_ENCODED_ICONS, CARD_NUMBER_MASK, CardType, CardTypeValues, DEFAULT_CARD_INPUT_MAX_LENGTH } from "../../core/constants"; -import { CollectElementProps, ElementType, ELEMENT_REQUIRED_ASTERISK, REQUIRED_MARK_DEFAULT_STYLE, ContainerType, CARD_NUMBER_ELEMENT_DEFAULT_STYLE, CARD_ICON_DEFAULT_STYLE, IListItem } from "../../utils/constants"; +import { CollectElementProps, ElementType, ELEMENT_REQUIRED_ASTERISK, REQUIRED_MARK_DEFAULT_STYLE, ContainerType, CARD_NUMBER_ELEMENT_DEFAULT_STYLE, CARD_ICON_DEFAULT_STYLE, IListItem, CollectElementOptions } from "../../utils/constants"; import SkyflowError from "../../utils/skyflow-error"; import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; import uuid from 'react-native-uuid'; import Dropdown from "../../core/Dropdown"; +import { formatCollectElementOptions } from "../../utils/helpers"; const CardNumberElement: React.FC = ({ container, options, ...rest }) => { @@ -33,7 +34,8 @@ const CardNumberElement: React.FC = ({ container, options, useEffect(() => { if (container) { - const element: CollectElement = container.create({ ...rest, type: ElementType.CARD_NUMBER, containerType: container.type }, mergedOptions); + const elementOptions: CollectElementOptions = formatCollectElementOptions(ElementType.CARD_NUMBER, options, container.getContext().logLevel); + const element: CollectElement = container.create({ ...rest, type: ElementType.CARD_NUMBER, containerType: container.type }, {...mergedOptions , ...elementOptions}); setElement(element); if (container.type === ContainerType.COLLECT) element.setMethods(setErrorText, { setInputStyles: setInputStyles, setLabelStyles: setLabelStyles }); diff --git a/src/components/InputFieldElement/index.tsx b/src/components/InputFieldElement/index.tsx index 0825173..19861cd 100644 --- a/src/components/InputFieldElement/index.tsx +++ b/src/components/InputFieldElement/index.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useRef } from "react"; import { Text, TextInput, View } from "react-native"; import type CollectElement from "../../core/CollectElement"; -import { CollectElementProps, ElementType, ELEMENT_REQUIRED_ASTERISK, REQUIRED_MARK_DEFAULT_STYLE, ContainerType } from "../../utils/constants"; +import { CollectElementProps, ElementType, ELEMENT_REQUIRED_ASTERISK, REQUIRED_MARK_DEFAULT_STYLE, ContainerType, CollectElementOptions } from "../../utils/constants"; import SkyflowError from "../../utils/skyflow-error"; import SKYFLOW_ERROR_CODE from "../../utils/skyflow-error-code"; import uuid from 'react-native-uuid'; @@ -51,6 +51,7 @@ const InputFieldElement: React.FC = ({ container, options = ref={textInputRef} value={elementValue} placeholder={rest.placeholder} + {...(options?.format ? { maxLength: options.format.length } : {})} onChangeText={(text) => { element?.onChangeElement(text); setElementValue(element.getInternalState().value) diff --git a/src/core/CollectElement/index.ts b/src/core/CollectElement/index.ts index 68f79bf..c7fcea0 100644 --- a/src/core/CollectElement/index.ts +++ b/src/core/CollectElement/index.ts @@ -32,6 +32,7 @@ import { appendZeroToOne, detectCardType, formatCardNumber, + formatInputFieldValue, formatExpirationDate, formatExpirationMonthValue, getReturnValue, @@ -186,7 +187,10 @@ class CollectElement extends SkyflowElement { break; case ElementType.CARD_NUMBER: this.#cardType = detectCardType(value); - this.updateElement(formatCardNumber(value, this.#cardType)); + this.updateElement(formatCardNumber(value, this.#cardType, this.#options.format)); + break; + case ElementType.INPUT_FIELD: + this.updateElement(formatInputFieldValue(value, this.#options.format)); break; default: this.updateElement(value); diff --git a/src/core/constants/index.ts b/src/core/constants/index.ts index 0924bcd..6db2b94 100644 --- a/src/core/constants/index.ts +++ b/src/core/constants/index.ts @@ -40,6 +40,7 @@ export const DEFAULT_EXPIRATION_YEAR_FORMAT = 'YY'; export const FOUR_DIGIT_YEAR_FORMAT = 'YYYY'; export const DEFAULT_EXPIRATION_DATE_FORMAT = 'MM/YY'; +export const DEFAULT_CARD_NUMBER_FORMAT = 'XXXX XXXX XXXX XXXX'; export const MONTH_FORMAT = 'MM'; @@ -178,6 +179,10 @@ export const ALLOWED_EXPIRY_DATE_FORMATS = [ 'YY/MM', 'MM/YYYY', ]; +export const ALLOWED_CARD_NUMBER_FORMATS = [ + DEFAULT_CARD_NUMBER_FORMAT, + 'XXXX-XXXX-XXXX-XXXX', +] export const DEFAULT_COLLECT_ELEMENT_REQUIRED_TEXT = 'Field is required'; diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 8c523de..98814e2 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -12,6 +12,8 @@ import { DEFAULT_EXPIRATION_DATE_FORMAT, ALLOWED_EXPIRY_DATE_FORMATS, ALLOWED_EXPIRY_YEAR_FORMATS, + ALLOWED_CARD_NUMBER_FORMATS, + DEFAULT_CARD_NUMBER_FORMAT, } from '../../core/constants'; import { ElementType, MessageType } from '../constants'; import logs from '../logs'; @@ -141,8 +143,42 @@ export const detectCardType = (cardNumber: string) => { return detectedType; }; -export const formatCardNumber = (cardNumber, type) => { - return applyMask(cardNumber, CARD_NUMBER_MASK[type]); +export const formatCardNumber = ( + cardNumber: string, + type: string | number, + format = DEFAULT_CARD_NUMBER_FORMAT +) => { + const mask = CARD_NUMBER_MASK[type] || CARD_NUMBER_MASK[CardType.DEFAULT]; + const maskedValue = applyMask(cardNumber, mask); + const separator = format.includes('-') ? '-' : ' '; + const formattedValue = maskedValue.replace(/[\s-]+/g, separator).trim(); + + return formattedValue; +}; + +export const formatInputFieldValue = (value: string, format: string): string => { + if (!value || value.length === 0) return ''; + if (!format || format.length === 0) return value; + + format = format.toUpperCase(); + const valueChars = value.replace(/[-\s]/g, '').split(''); + let formatted = ''; + let valueIndex = 0; + + for (let formatIndex = 0; formatIndex < format.length && valueIndex < valueChars.length; formatIndex++) { + const formatChar = format[formatIndex]; + + if (formatChar === 'X') { + formatted += valueChars[valueIndex]; + valueIndex++; + } else { + if (valueIndex < valueChars.length && valueIndex > 0) { + formatted += formatChar; + } + } + } + + return formatted; }; export const getReturnValue = ( @@ -204,6 +240,13 @@ export const isValidExpiryYearFormat = (format: string): boolean => { return false; }; +export const isValidCardNumberFormat = (format: string): boolean => { + if (format) { + return ALLOWED_CARD_NUMBER_FORMATS.includes(format); + } + return false; +}; + export const formatCollectElementOptions = ( elementType: ElementType, options, @@ -259,6 +302,27 @@ export const formatCollectElementOptions = ( ? formattedOptions.format.toUpperCase() : DEFAULT_EXPIRATION_YEAR_FORMAT, }; + } else if (elementType === ElementType.CARD_NUMBER) { + let isvalidFormat = false; + if (formattedOptions.format) { + isvalidFormat = isValidCardNumberFormat( + formattedOptions.format.toUpperCase() + ); + if (!isvalidFormat) { + printLog( + parameterizedString( + logs.warnLogs.INVALID_CARD_NUMBER_FORMAT, + ALLOWED_CARD_NUMBER_FORMATS.toString() + ), + MessageType.WARN, + logLevel + ); + } + } + formattedOptions = { + ...formattedOptions, + format: isvalidFormat ? formattedOptions.format : DEFAULT_CARD_NUMBER_FORMAT, + }; } return formattedOptions; }; diff --git a/src/utils/logs/index.ts b/src/utils/logs/index.ts index fb6a125..b140d95 100644 --- a/src/utils/logs/index.ts +++ b/src/utils/logs/index.ts @@ -226,6 +226,8 @@ const logs = { 'EXPIRATION_DATE format must be in one of %s1, the format is set to default MM/YY', INVALID_EXPIRATION_YEAR_FORMAT: 'EXPIRATION_YEAR format must be in one of %s1, the format is set to default YY', + INVALID_CARD_NUMBER_FORMAT: + `CARD_NUMBER format must be in one of %s1, the format is set to default XXXX XXXX XXXX XXXX`, }, }; From ac379497bfdd465bd227cd2e7c7ab49c83f37b61 Mon Sep 17 00:00:00 2001 From: raushan-skyflow Date: Fri, 4 Jul 2025 01:34:10 +0530 Subject: [PATCH 2/7] SK-2157 trimmed the format string --- src/components/InputFieldElement/index.tsx | 2 +- src/utils/helpers/index.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/InputFieldElement/index.tsx b/src/components/InputFieldElement/index.tsx index aab033d..8fd6177 100644 --- a/src/components/InputFieldElement/index.tsx +++ b/src/components/InputFieldElement/index.tsx @@ -52,7 +52,7 @@ const InputFieldElement: React.FC = ({ container, options = ref={textInputRef} value={elementValue} placeholder={rest.placeholder} - {...(options?.format ? { maxLength: options.format.length } : {})} + {...(options?.format ? { maxLength: options.format.trim().length } : {})} onChangeText={(text) => { element?.onChangeElement(text); setElementValue(element.getInternalState().value) diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 98814e2..968a2e8 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -148,6 +148,8 @@ export const formatCardNumber = ( type: string | number, format = DEFAULT_CARD_NUMBER_FORMAT ) => { + if (!cardNumber || cardNumber.length === 0) return ''; + const mask = CARD_NUMBER_MASK[type] || CARD_NUMBER_MASK[CardType.DEFAULT]; const maskedValue = applyMask(cardNumber, mask); const separator = format.includes('-') ? '-' : ' '; @@ -160,15 +162,17 @@ export const formatInputFieldValue = (value: string, format: string): string => if (!value || value.length === 0) return ''; if (!format || format.length === 0) return value; - format = format.toUpperCase(); + format = format.trim().toUpperCase(); + value = value.trim(); const valueChars = value.replace(/[-\s]/g, '').split(''); let formatted = ''; let valueIndex = 0; + const separator = 'X'; for (let formatIndex = 0; formatIndex < format.length && valueIndex < valueChars.length; formatIndex++) { const formatChar = format[formatIndex]; - if (formatChar === 'X') { + if (formatChar === separator) { formatted += valueChars[valueIndex]; valueIndex++; } else { From 154782a0b4314df03aca718cca0603cb2a6d0a8d Mon Sep 17 00:00:00 2001 From: raushan-skyflow Date: Fri, 4 Jul 2025 02:21:38 +0530 Subject: [PATCH 3/7] SK-2157 unit test for card number, input field element & helpers function --- __tests__/core/collectElement.test.js | 128 ++++++++++++++++- __tests__/utils/helper.test.js | 193 ++++++++++++++++++++++++++ 2 files changed, 317 insertions(+), 4 deletions(-) diff --git a/__tests__/core/collectElement.test.js b/__tests__/core/collectElement.test.js index 34433e8..ae81d11 100644 --- a/__tests__/core/collectElement.test.js +++ b/__tests__/core/collectElement.test.js @@ -257,7 +257,11 @@ describe('test Collect Element class', () => { new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_COLUMN_IN_COLLECT, [], true) ); - collecteElement = new CollectElement({ table: 'cards', column: 'test', skyflowID: '' }, {}, context); + collecteElement = new CollectElement( + { table: 'cards', column: 'test', skyflowID: '' }, + {}, + context + ); expect(isValid).toThrow( new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_SKYFLOW_ID_COLLECT, [], true) ); @@ -515,8 +519,10 @@ describe('test Collect Element class', () => { ); cardNumberElement.onDropdownSelect(CardType.DISCOVER); cardNumberElement.onChangeElement('', true); - expect(cardNumberElement.getClientState().selectedCardScheme).toEqual('DISCOVER'); - }) + expect(cardNumberElement.getClientState().selectedCardScheme).toEqual( + 'DISCOVER' + ); + }); it('should remove spaces from value', () => { const elementInput = { @@ -529,4 +535,118 @@ describe('test Collect Element class', () => { collectElement.updateValue('4111 1111 1111 1111'); expect(collectElement.getUnformattedValue()).toBe('4111111111111111'); }); -}); \ No newline at end of file + + it('test format option in card number element', () => { + const formatTestCases = [ + // Test different valid formats for the card number + { + format: 'XXXX-XXXX-XXXX-XXXX', + input: '4111111111111111', + expected: '4111-1111-1111-1111', + }, + { + format: 'XXXX XXXX XXXX XXXX', + input: '4111111111111111', + expected: '4111 1111 1111 1111', + }, + // Test partial input + { + format: 'XXXX-XXXX-XXXX-XXXX', + input: '411111', + expected: '4111-11', + }, + { + format: 'XXXX XXXX XXXX XXXX', + input: '411111', + expected: '4111 11', + }, + // Test input with leading, trailing spaces & small format + { + format: ' xxxx-xxxx-xxxx ', + input: ' 4111111111111111 ', + expected: '4111-1111-1111-1111', + }, + ]; + + formatTestCases.forEach((testCase) => { + const collectElement = new CollectElement( + { + table: 'cards', + column: 'card_number', + type: ElementType.CARD_NUMBER, + containerType: ContainerType.COLLECT, + }, + { + format: testCase.format, + }, + context + ); + + collectElement.onChangeElement(testCase.input); + expect(collectElement.getInternalState().value).toBe(testCase.expected); + }); + }); + + it('test format option in input field element', () => { + const formatTestCases = [ + { + format: 'XX-XX-XX', + input: '123456', + expected: '12-34-56', + }, + { + format: 'XXX XXX', + input: '123456', + expected: '123 456', + }, + { + format: 'XX/XX/XX', + input: '123456', + expected: '12/34/56', + }, + // Test partial input + { + format: 'XX-XX-XX', + input: '1234', + expected: '12-34', + }, + // Test with special characters + { + format: 'XX@XX#XX', + input: '123456', + expected: '12@34#56', + }, + // Test input longer than format + { + format: 'XX-XX', + input: '123456', + expected: '12-34', + }, + // Test input with leading, trailing spaces & small format + { + format: ' xx-xx-xxx ', + input: ' 1234567 ', + expected: '12-34-567', + }, + ]; + + formatTestCases.forEach((testCase) => { + const collectElement = new CollectElement( + { + table: 'table', + column: 'column', + type: ElementType.INPUT_FIELD, + containerType: ContainerType.COLLECT, + }, + { + format: testCase.format, + }, + context + ); + + collectElement.onChangeElement(testCase.input); + expect(collectElement.getInternalState().value).toBe(testCase.expected); + expect(collectElement.getInternalState().isValid).toBe(true); + }); + }); +}); diff --git a/__tests__/utils/helper.test.js b/__tests__/utils/helper.test.js index c1a2a36..65e74ab 100644 --- a/__tests__/utils/helper.test.js +++ b/__tests__/utils/helper.test.js @@ -16,6 +16,7 @@ import { getYearAndMonthBasedOnFormat, getMetaObject, getDeviceModel, + formatInputFieldValue, } from '../../src/utils/helpers'; import { CardType, @@ -300,4 +301,196 @@ describe('test getYearAndMonthBasedOnFormat function', () => { year: '2032', }); }); + + it('should format expiration date options correctly', () => { + const validFormats = ['MM/YY', 'MM/YYYY', 'YYYY/MM', 'YY/MM']; + validFormats.forEach((format) => { + const result = formatCollectElementOptions( + ElementType.EXPIRATION_DATE, + { format }, + LogLevel.WARN + ); + expect(result).toEqual({ + format, + required: false, + }); + }); + }); + + it('should use default format for invalid expiration date format', () => { + const result = formatCollectElementOptions( + ElementType.EXPIRATION_DATE, + { format: 'INVALID' }, + LogLevel.WARN + ); + expect(result).toEqual({ + format: DEFAULT_EXPIRATION_DATE_FORMAT, + required: false, + }); + expect(printLog).toBeCalled(); + }); + + it('should format expiration year options correctly', () => { + const validFormats = ['YY', 'YYYY']; + validFormats.forEach((format) => { + const result = formatCollectElementOptions( + ElementType.EXPIRATION_YEAR, + { format }, + LogLevel.WARN + ); + expect(result).toEqual({ + format, + required: false, + }); + }); + }); + + it('should use default format for invalid expiration year format', () => { + const result = formatCollectElementOptions( + ElementType.EXPIRATION_YEAR, + { format: 'INVALID' }, + LogLevel.WARN + ); + expect(result).toEqual({ + format: DEFAULT_EXPIRATION_YEAR_FORMAT, + required: false, + }); + expect(printLog).toBeCalled(); + }); +}); + +describe('test formatCardNumber with different formats', () => { + const testCases = [ + { + input: '4111111111111111', + type: CardType.VISA, + format: 'XXXX-XXXX-XXXX-XXXX', + expected: '4111-1111-1111-1111', + }, + { + input: '378282246310005', + type: CardType.AMEX, + format: 'XXXX-XXXXXX-XXXXX', + expected: '3782-822463-10005', + }, + { + input: '6011111111111117', + type: CardType.DISCOVER, + format: 'XXXX XXXX XXXX XXXX', + expected: '6011 1111 1111 1117', + }, + // Test partial inputs + { + input: '411111', + type: CardType.VISA, + format: 'XXXX-XXXX-XXXX-XXXX', + expected: '4111-11', + }, + // Test empty input + { + input: '', + type: CardType.DEFAULT, + format: 'XXXX-XXXX-XXXX-XXXX', + expected: '', + }, + ]; + + testCases.forEach((testCase) => { + it(`should format ${testCase.type} card number correctly with format ${testCase.format}`, () => { + expect( + formatCardNumber(testCase.input, testCase.type, testCase.format) + ).toBe(testCase.expected); + }); + }); +}); + +describe('test formatInputFieldValue with different formats', () => { + const testCases = [ + { + input: '123456', + format: 'XX-XX-XX', + expected: '12-34-56', + }, + { + input: 'ABCDEF', + format: 'XXX XXX', + expected: 'ABC DEF', + }, + { + input: '12345', + format: 'XX/XX/X', + expected: '12/34/5', + }, + // Test with special characters in format + { + input: '123456', + format: 'XX@XX#XX', + expected: '12@34#56', + }, + // Test partial input + { + input: '123', + format: 'XX-XX-XX', + expected: '12-3', + }, + // Test input longer than format + { + input: '1234567', + format: 'XX-XX', + expected: '12-34', + }, + // Test empty input + { + input: '', + format: 'XX-XX', + expected: '', + }, + // Test with no format + { + input: '123456', + format: '', + expected: '123456', + }, + ]; + + testCases.forEach((testCase) => { + it(`should format input '${testCase.input}' with format '${testCase.format}' correctly`, () => { + expect(formatInputFieldValue(testCase.input, testCase.format)).toBe( + testCase.expected + ); + }); + }); +}); + +describe('test formatCollectElementOptions for different element types', () => { + it('should format card number options correctly', () => { + const options = { + format: 'XXXX-XXXX-XXXX-XXXX', + required: true, + }; + const result = formatCollectElementOptions( + ElementType.CARD_NUMBER, + options, + LogLevel.WARN + ); + expect(result).toEqual({ + format: 'XXXX-XXXX-XXXX-XXXX', + required: true, + }); + }); + + it('should preserve other options while formatting', () => { + const options = { + format: 'XXXX-XXXX-XXXX-XXXX', + required: true, + enableCardIcon: true, + customOption: 'value', + }; + const result = formatCollectElementOptions( + ElementType.CARD_NUMBER, + options, + LogLevel.WARN + ); + expect(result).toEqual(options); + }); }); From ec4abb88d5df545cddbc32e52342d5c1c4f33c58 Mon Sep 17 00:00:00 2001 From: raushan-skyflow Date: Fri, 4 Jul 2025 23:33:42 +0530 Subject: [PATCH 4/7] SK-2157 update card number formatter & input field formatter --- __tests__/core/collectElement.test.js | 6 +++--- src/utils/helpers/index.ts | 29 ++++++++++++++++++--------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/__tests__/core/collectElement.test.js b/__tests__/core/collectElement.test.js index ae81d11..403b859 100644 --- a/__tests__/core/collectElement.test.js +++ b/__tests__/core/collectElement.test.js @@ -560,11 +560,11 @@ describe('test Collect Element class', () => { input: '411111', expected: '4111 11', }, - // Test input with leading, trailing spaces & small format + // Test for invalid format, It will fallback to default format { format: ' xxxx-xxxx-xxxx ', input: ' 4111111111111111 ', - expected: '4111-1111-1111-1111', + expected: '4111 1111 1111 1111', }, ]; @@ -626,7 +626,7 @@ describe('test Collect Element class', () => { { format: ' xx-xx-xxx ', input: ' 1234567 ', - expected: '12-34-567', + expected: ' xx-xx-xxx ', }, ]; diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 968a2e8..007aa59 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -149,6 +149,13 @@ export const formatCardNumber = ( format = DEFAULT_CARD_NUMBER_FORMAT ) => { if (!cardNumber || cardNumber.length === 0) return ''; + const isvalidFormat = isValidCardNumberFormat( + format + ); + + if (!isvalidFormat) { + format = DEFAULT_CARD_NUMBER_FORMAT; + }; const mask = CARD_NUMBER_MASK[type] || CARD_NUMBER_MASK[CardType.DEFAULT]; const maskedValue = applyMask(cardNumber, mask); @@ -162,22 +169,26 @@ export const formatInputFieldValue = (value: string, format: string): string => if (!value || value.length === 0) return ''; if (!format || format.length === 0) return value; - format = format.trim().toUpperCase(); - value = value.trim(); - const valueChars = value.replace(/[-\s]/g, '').split(''); + const valueChars = value.replace(/\D/g, '').split(''); + const valueCharsLength = valueChars.length; let formatted = ''; let valueIndex = 0; const separator = 'X'; - for (let formatIndex = 0; formatIndex < format.length && valueIndex < valueChars.length; formatIndex++) { + for (let formatIndex = 0; formatIndex < format.length; formatIndex++) { const formatChar = format[formatIndex]; - + if (formatChar === separator) { - formatted += valueChars[valueIndex]; - valueIndex++; + if (valueIndex < valueCharsLength) { + formatted += valueChars[valueIndex++]; + } else { + break; + } } else { - if (valueIndex < valueChars.length && valueIndex > 0) { + if (valueIndex < valueCharsLength) { formatted += formatChar; + } else { + break; } } } @@ -311,7 +322,7 @@ export const formatCollectElementOptions = ( if (formattedOptions.format) { isvalidFormat = isValidCardNumberFormat( formattedOptions.format.toUpperCase() - ); + ); if (!isvalidFormat) { printLog( parameterizedString( From 29382d5ef97b0013c57168703bd7731c0a0ced94 Mon Sep 17 00:00:00 2001 From: raushan-skyflow Date: Fri, 4 Jul 2025 23:52:37 +0530 Subject: [PATCH 5/7] SK-2157 fix unit test --- __tests__/utils/helper.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/__tests__/utils/helper.test.js b/__tests__/utils/helper.test.js index 65e74ab..9bd9aa4 100644 --- a/__tests__/utils/helper.test.js +++ b/__tests__/utils/helper.test.js @@ -370,7 +370,7 @@ describe('test formatCardNumber with different formats', () => { { input: '378282246310005', type: CardType.AMEX, - format: 'XXXX-XXXXXX-XXXXX', + format: 'XXXX-XXXX-XXXX-XXXX', expected: '3782-822463-10005', }, { @@ -412,9 +412,9 @@ describe('test formatInputFieldValue with different formats', () => { expected: '12-34-56', }, { - input: 'ABCDEF', + input: '123456', format: 'XXX XXX', - expected: 'ABC DEF', + expected: '123 456', }, { input: '12345', From 0c1b2bb71979132f6a0e5cf60ac52241fcee604a Mon Sep 17 00:00:00 2001 From: raushan-skyflow Date: Mon, 7 Jul 2025 14:15:24 +0530 Subject: [PATCH 6/7] SK-2157 update formatter for smoothly deletion --- src/utils/helpers/index.ts | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 007aa59..ac6b552 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -166,33 +166,26 @@ export const formatCardNumber = ( }; export const formatInputFieldValue = (value: string, format: string): string => { - if (!value || value.length === 0) return ''; if (!format || format.length === 0) return value; + if (!value || value.length === 0) return ''; - const valueChars = value.replace(/\D/g, '').split(''); - const valueCharsLength = valueChars.length; - let formatted = ''; - let valueIndex = 0; - const separator = 'X'; + const formatter= 'X'; - for (let formatIndex = 0; formatIndex < format.length; formatIndex++) { - const formatChar = format[formatIndex]; + const inputChars = format.includes(formatter) ? value.replace(/\D/g, '').split('') : value.split(''); + let formatted = ''; + let inputIndex = 0; - if (formatChar === separator) { - if (valueIndex < valueCharsLength) { - formatted += valueChars[valueIndex++]; - } else { - break; - } + for (let idx = 0; idx < format.length && inputIndex < inputChars.length; idx++) { + if (format[idx] === formatter) { + formatted += inputChars[inputIndex]; + inputIndex++; } else { - if (valueIndex < valueCharsLength) { - formatted += formatChar; - } else { - break; + formatted += format[idx]; + if (inputIndex < inputChars.length && inputChars[inputIndex] === format[idx]) { + inputIndex++; } } } - return formatted; }; From 1c17d7f013a35ee1684d3f58c46ab5bd1ca84dd3 Mon Sep 17 00:00:00 2001 From: raushan-skyflow Date: Tue, 8 Jul 2025 15:40:49 +0530 Subject: [PATCH 7/7] SK-2168 input formatting with combination of constants & dynamic input in InputFieldElement --- __tests__/utils/helper.test.js | 35 +++++++++++++++-- src/core/CollectElement/index.ts | 2 +- src/core/constants/index.ts | 3 ++ src/utils/helpers/index.ts | 64 ++++++++++++++++++++++++-------- 4 files changed, 83 insertions(+), 21 deletions(-) diff --git a/__tests__/utils/helper.test.js b/__tests__/utils/helper.test.js index 9bd9aa4..13b2ca4 100644 --- a/__tests__/utils/helper.test.js +++ b/__tests__/utils/helper.test.js @@ -421,6 +421,24 @@ describe('test formatInputFieldValue with different formats', () => { format: 'XX/XX/X', expected: '12/34/5', }, + { + input: '1567C89', + format: 'XX/XYX/X', + translation: { X: '[5-9]', Y: '[A-C]' }, + expected: '56/7C8/9', + }, + // Combination of constants and dynamic inputs + { + input: '1234121234', + format: '+91 XXXX-XX-XXXX', + expected: '+91 1234-12-1234', + }, + { + input: 'B5678978C7QAB97', + format: '91 YXX-XZX-XXY-XZYXX', + translation: { X: '[5-9]', Y: '[A-C]' }, + expected: '91 B56-7Z8-97C-7ZA97', + }, // Test with special characters in format { input: '123456', @@ -454,10 +472,19 @@ describe('test formatInputFieldValue with different formats', () => { ]; testCases.forEach((testCase) => { - it(`should format input '${testCase.input}' with format '${testCase.format}' correctly`, () => { - expect(formatInputFieldValue(testCase.input, testCase.format)).toBe( - testCase.expected - ); + const translation = testCase.translation || { + X: '[0-9]', + }; + it(`should format input '${testCase.input}' with format '${ + testCase.format + }' & translation '${JSON.stringify(translation)}' correctly`, () => { + expect( + formatInputFieldValue( + testCase.input, + testCase.format, + testCase?.translation + ) + ).toBe(testCase.expected); }); }); }); diff --git a/src/core/CollectElement/index.ts b/src/core/CollectElement/index.ts index fb2d5fc..13bf271 100644 --- a/src/core/CollectElement/index.ts +++ b/src/core/CollectElement/index.ts @@ -194,7 +194,7 @@ class CollectElement extends SkyflowElement { this.updateElement(formatCardNumber(value, this.#cardType, this.#options.format)); break; case ElementType.INPUT_FIELD: - this.updateElement(formatInputFieldValue(value, this.#options.format)); + this.updateElement(formatInputFieldValue(value, this.#options.format, this.#options.translation)); break; default: this.updateElement(value); diff --git a/src/core/constants/index.ts b/src/core/constants/index.ts index ee1e03d..3be2ea7 100644 --- a/src/core/constants/index.ts +++ b/src/core/constants/index.ts @@ -167,6 +167,9 @@ export const REVEAL_ELEMENT_ERROR_TEXT = 'Invalid Token'; export const DEFAULT_COLLECT_ELEMENT_ERROR_TEXT = 'Invalid value'; export const DEFAULT_VALIDATION_ERROR_TEXT = 'Validation Failed'; +export const DEFAULT_INPUT_FIELD_TRANSLATION: Record = { + 'X': '[0-9]', +}; export const ALLOWED_EXPIRY_YEAR_FORMATS = [ DEFAULT_EXPIRATION_YEAR_FORMAT, diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index ac6b552..4fe207c 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -14,6 +14,7 @@ import { ALLOWED_EXPIRY_YEAR_FORMATS, ALLOWED_CARD_NUMBER_FORMATS, DEFAULT_CARD_NUMBER_FORMAT, + DEFAULT_INPUT_FIELD_TRANSLATION, } from '../../core/constants'; import { ElementType, MessageType } from '../constants'; import logs from '../logs'; @@ -165,28 +166,59 @@ export const formatCardNumber = ( return formattedValue; }; -export const formatInputFieldValue = (value: string, format: string): string => { - if (!format || format.length === 0) return value; - if (!value || value.length === 0) return ''; +export const formatInputFieldValue = ( + input: string, + format: string, + translation: Record = DEFAULT_INPUT_FIELD_TRANSLATION +): string => { + if (!format || format.length === 0) return input; + if (!input || input.length === 0) return ''; - const formatter= 'X'; + const inputArray = Array.from(input); + const formatArray = Array.from(format); + let formattedOutput = ''; + let formatIndex = 0; - const inputChars = format.includes(formatter) ? value.replace(/\D/g, '').split('') : value.split(''); - let formatted = ''; - let inputIndex = 0; + for (let inputIndex = 0; inputIndex < inputArray.length; inputIndex++) { + const inputChar = inputArray[inputIndex]; - for (let idx = 0; idx < format.length && inputIndex < inputChars.length; idx++) { - if (format[idx] === formatter) { - formatted += inputChars[inputIndex]; - inputIndex++; - } else { - formatted += format[idx]; - if (inputIndex < inputChars.length && inputChars[inputIndex] === format[idx]) { - inputIndex++; + if (formatIndex < formatArray.length) { + const currentFormatChar = formatArray[formatIndex]; + + if (translation[currentFormatChar]) { + const regex = new RegExp(translation[currentFormatChar]); + if (regex.test(inputChar)) { + formattedOutput += inputChar; + formatIndex++; + } + } else { + if (inputChar === currentFormatChar) { + formattedOutput += inputChar; + formatIndex++; + } else { + for (let scanIndex = formatIndex; scanIndex < formatArray.length; scanIndex++) { + const nextFormatChar = formatArray[scanIndex]; + + if (translation[nextFormatChar]) { + const regex = new RegExp(translation[nextFormatChar]); + if (regex.test(inputChar)) { + formattedOutput += inputChar; + formatIndex = scanIndex + 1; + } + break; + } else { + formattedOutput += nextFormatChar; + formatIndex = scanIndex + 1; + } + } + } } + } else { + break; } } - return formatted; + + return formattedOutput; }; export const getReturnValue = (