diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ecb92e13ca0e..bfaca9c1e580 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -527,7 +527,7 @@ const CONST = { // Prevents consecutive special characters or spaces like '--', '..', '((', '))', or ' '. REPEATED_SPECIAL_CHAR_PATTERN: /([-\s().])\1+/, - MERCHANT_NAME_MAX_LENGTH: 255, + MERCHANT_NAME_MAX_BYTES: 255, MASKED_PAN_PREFIX: 'XXXXXXXXXXXX', diff --git a/src/libs/StringUtils/index.ts b/src/libs/StringUtils/index.ts index 6d4f1c0ba1c6..f69163752402 100644 --- a/src/libs/StringUtils/index.ts +++ b/src/libs/StringUtils/index.ts @@ -139,6 +139,15 @@ function removePreCodeBlock(text = '') { return text.replace(/
]*>|<\/pre>/g, '');
}
+/**
+ * Returns the number of bytes required to encode a string in UTF-8.
+ */
+function getUTF8ByteLength(str: string) {
+ const encoder = new TextEncoder();
+ const bytes = encoder.encode(str);
+ return bytes.length;
+}
+
export default {
sanitizeString,
isEmptyString,
@@ -153,4 +162,5 @@ export default {
sortStringArrayByLength,
dedent,
hash,
+ getUTF8ByteLength,
};
diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts
index a550a8f2f049..69570619da2e 100644
--- a/src/libs/ValidationUtils.ts
+++ b/src/libs/ValidationUtils.ts
@@ -657,6 +657,15 @@ function isValidRegistrationNumber(registrationNumber: string, country: Country
}
}
+/**
+ * Checks if the `inputValue` byte length exceeds the specified byte length,
+ * returning `isValid` (boolean) and `byteLength` (number) to be used in dynamic error copy.
+ */
+function isValidInputLength(inputValue: string, byteLength: number) {
+ const valueByteLength = StringUtils.getUTF8ByteLength(inputValue);
+ return {isValid: valueByteLength <= byteLength, byteLength: valueByteLength};
+}
+
export {
meetsMinimumAgeRequirement,
meetsMaximumAgeRequirement,
@@ -708,4 +717,5 @@ export {
isValidZipCodeInternational,
isValidOwnershipPercentage,
isValidRegistrationNumber,
+ isValidInputLength,
};
diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx
index 4a519d94aefc..a1ebf1f45020 100644
--- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx
+++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersMerchantPage.tsx
@@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import {updateAdvancedFilters} from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
+import {isValidInputLength} from '@libs/ValidationUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -21,7 +22,7 @@ function SearchFiltersMerchantPage() {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
+ const [searchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM, {canBeMissing: true});
const merchant = searchAdvancedFiltersForm?.[FILTER_KEYS.MERCHANT];
const {inputCallbackRef} = useAutoFocusInput();
@@ -33,9 +34,10 @@ function SearchFiltersMerchantPage() {
const validate = (values: FormOnyxValues) => {
const errors: FormInputErrors = {};
const merchantValue = values.merchant.trim();
+ const {isValid, byteLength} = isValidInputLength(merchantValue, CONST.MERCHANT_NAME_MAX_BYTES);
- if (merchantValue.length > CONST.MERCHANT_NAME_MAX_LENGTH) {
- errors.merchant = translate('common.error.characterLimitExceedCounter', {length: merchantValue.length, limit: CONST.MERCHANT_NAME_MAX_LENGTH});
+ if (!isValid) {
+ errors.merchant = translate('common.error.characterLimitExceedCounter', {length: byteLength, limit: CONST.MERCHANT_NAME_MAX_BYTES});
}
return errors;
diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx
index 0d2980ac5fdc..af14bc01e941 100644
--- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx
+++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx
@@ -11,6 +11,7 @@ import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import {getTransactionDetails, isExpenseRequest, isPolicyExpenseChat} from '@libs/ReportUtils';
+import {isValidInputLength} from '@libs/ValidationUtils';
import {setDraftSplitTransaction, setMoneyRequestMerchant, updateMoneyRequestMerchant} from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -60,15 +61,16 @@ function IOURequestStepMerchant({
const validate = useCallback(
(value: FormOnyxValues) => {
const errors: FormInputErrors = {};
+ const {isValid, byteLength} = isValidInputLength(value.moneyRequestMerchant, CONST.MERCHANT_NAME_MAX_BYTES);
if (isMerchantRequired && !value.moneyRequestMerchant) {
errors.moneyRequestMerchant = translate('common.error.fieldRequired');
} else if (isMerchantRequired && value.moneyRequestMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT) {
errors.moneyRequestMerchant = translate('iou.error.invalidMerchant');
- } else if (value.moneyRequestMerchant.length > CONST.MERCHANT_NAME_MAX_LENGTH) {
+ } else if (!isValid) {
errors.moneyRequestMerchant = translate('common.error.characterLimitExceedCounter', {
- length: value.moneyRequestMerchant.length,
- limit: CONST.MERCHANT_NAME_MAX_LENGTH,
+ length: byteLength,
+ limit: CONST.MERCHANT_NAME_MAX_BYTES,
});
}
diff --git a/tests/unit/ValidationUtilsTest.ts b/tests/unit/ValidationUtilsTest.ts
index 343e05ef2e52..e9e10dc4f138 100644
--- a/tests/unit/ValidationUtilsTest.ts
+++ b/tests/unit/ValidationUtilsTest.ts
@@ -1,4 +1,5 @@
import {addDays, format, startOfDay, subYears} from 'date-fns';
+import {TextEncoder} from 'util';
import {translateLocal} from '@libs/Localize';
import CONST from '@src/CONST';
import {
@@ -7,6 +8,7 @@ import {
isValidAccountRoute,
isValidDate,
isValidExpirationDate,
+ isValidInputLength,
isValidLegalName,
isValidPastDate,
isValidPaymentZipCode,
@@ -18,6 +20,8 @@ import {
meetsMinimumAgeRequirement,
} from '@src/libs/ValidationUtils';
+global.TextEncoder = TextEncoder;
+
describe('ValidationUtils', () => {
describe('isValidDate', () => {
test('Should return true for a valid date within the range', () => {
@@ -396,4 +400,99 @@ describe('ValidationUtils', () => {
expect(isValid).toBe(false);
});
});
+
+ describe('isValidInputLength', () => {
+ // Test Latin alphabet characters (1 byte each in UTF-8)
+ describe('Latin alphabet characters', () => {
+ test('returns true and correct byte length when Latin string byte length exceeds limit', () => {
+ expect(isValidInputLength('abc', 2)).toEqual({isValid: false, byteLength: 3}); // 3 bytes > 2
+ });
+
+ test('returns false and correct byte length when Latin string byte length equals limit', () => {
+ expect(isValidInputLength('abc', 3)).toEqual({isValid: true, byteLength: 3}); // 3 bytes ≤ 3
+ });
+
+ test('returns false and correct byte length when Latin string byte length is less than limit', () => {
+ expect(isValidInputLength('ab', 3)).toEqual({isValid: true, byteLength: 2}); // 2 bytes ≤ 3
+ });
+ });
+
+ // Test Sanskrit characters (typically 3 bytes each in UTF-8)
+ describe('Sanskrit characters', () => {
+ test('returns true and correct byte length when Sanskrit string byte length exceeds limit', () => {
+ expect(isValidInputLength('कष', 5)).toEqual({isValid: false, byteLength: 6}); // 6 bytes > 5
+ });
+
+ test('returns false and correct byte length when Sanskrit string byte length equals limit', () => {
+ expect(isValidInputLength('कष', 6)).toEqual({isValid: true, byteLength: 6}); // 6 bytes ≤ 6
+ });
+
+ test('returns false and correct byte length when Sanskrit string byte length is less than limit', () => {
+ expect(isValidInputLength('क', 4)).toEqual({isValid: true, byteLength: 3}); // 3 bytes ≤ 4
+ });
+ });
+
+ // Test emojis (typically 4 bytes each in UTF-8)
+ describe('Emojis', () => {
+ test('returns true and correct byte length when emoji byte length exceeds limit', () => {
+ expect(isValidInputLength('😊', 3)).toEqual({isValid: false, byteLength: 4}); // 4 bytes > 3
+ });
+
+ test('returns false and correct byte length when emoji byte length equals limit', () => {
+ expect(isValidInputLength('😊', 4)).toEqual({isValid: true, byteLength: 4}); // 4 bytes ≤ 4
+ });
+
+ test('returns false and correct byte length when emoji byte length is less than limit', () => {
+ expect(isValidInputLength('😊', 5)).toEqual({isValid: true, byteLength: 4}); // 4 bytes ≤ 5
+ });
+ });
+
+ // Test empty strings and spaces
+ describe('Empty strings and spaces', () => {
+ test('returns false and correct byte length for empty string regardless of limit', () => {
+ expect(isValidInputLength('', 0)).toEqual({isValid: true, byteLength: 0}); // 0 bytes ≤ 0
+ expect(isValidInputLength('', 1)).toEqual({isValid: true, byteLength: 0}); // 0 bytes ≤ 1
+ });
+
+ test('returns true and correct byte length when space string byte length exceeds limit', () => {
+ expect(isValidInputLength(' ', 2)).toEqual({isValid: false, byteLength: 3}); // 3 bytes > 2
+ });
+
+ test('returns false and correct byte length when space string byte length equals limit', () => {
+ expect(isValidInputLength(' ', 2)).toEqual({isValid: true, byteLength: 2}); // 2 bytes ≤ 2
+ });
+ });
+
+ // Test mixed characters
+ describe('Mixed characters', () => {
+ test('returns true and correct byte length when mixed string byte length exceeds limit', () => {
+ expect(isValidInputLength('aक😊', 6)).toEqual({isValid: false, byteLength: 8}); // 1 + 3 + 4 = 8 bytes > 6
+ });
+
+ test('returns false and correct byte length when mixed string byte length equals limit', () => {
+ expect(isValidInputLength('aक😊', 8)).toEqual({isValid: true, byteLength: 8}); // 1 + 3 + 4 = 8 bytes ≤ 8
+ });
+
+ test('returns false and correct byte length when mixed string byte length is less than limit', () => {
+ expect(isValidInputLength('aक', 5)).toEqual({isValid: true, byteLength: 4}); // 1 + 3 = 4 bytes ≤ 5
+ });
+ });
+
+ // Test edge cases
+ describe('Edge cases', () => {
+ test('handles negative length parameter', () => {
+ expect(isValidInputLength('abc', -1)).toEqual({isValid: false, byteLength: 3}); // 3 bytes > -1
+ });
+
+ test('handles zero length parameter', () => {
+ expect(isValidInputLength('a', 0)).toEqual({isValid: false, byteLength: 1}); // 1 byte > 0
+ expect(isValidInputLength('', 0)).toEqual({isValid: true, byteLength: 0}); // 0 bytes ≤ 0
+ });
+
+ test('handles special characters (e.g., newlines, tabs)', () => {
+ expect(isValidInputLength('\n\t', 1)).toEqual({isValid: false, byteLength: 2}); // 2 bytes > 1
+ expect(isValidInputLength('\n\t', 2)).toEqual({isValid: true, byteLength: 2}); // 2 bytes ≤ 2
+ });
+ });
+ });
});