From 4f1dccdffe3b01698426c078c167c2d6ba2eee32 Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Wed, 8 May 2024 13:06:09 +0530 Subject: [PATCH 01/16] [feat]: add new api for masked contact number --- .../__tests__/getMaskedPhoneNumber.test.ts | 60 +++++++++++++++++++ .../phoneNumber/getMaskedPhoneNumber.ts | 42 +++++++++++++ .../src/modules/phoneNumber/index.ts | 1 + 3 files changed, 103 insertions(+) create mode 100644 packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts create mode 100644 packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts diff --git a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts new file mode 100644 index 00000000..0b59f7fd --- /dev/null +++ b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts @@ -0,0 +1,60 @@ +import { CountryCodeType } from '../../types'; +import { getMaskedPhoneNumber } from '../index'; + +describe('phoneNumber - getMaskedPhoneNumber', () => { + // Test data array for multiple countries + const testCasesWithDialCode = [ + { countryCode: 'US', expected: '+1 xxx-xxx-xxxx' }, + { countryCode: 'GB', expected: '+44 xxxx xxx xxx' }, + { countryCode: 'DE', expected: '+49 xxx xxxxxxxx' }, + { countryCode: 'IN', expected: '+91 xxxx xxxxxx' }, + { countryCode: 'JP', expected: '+81 xx xxxx xxxx' }, + ]; + + // Tests for valid inputs including dial code + testCasesWithDialCode.forEach(({ countryCode, expected }) => { + it(`should return the correct phone number format including dial code for ${countryCode}`, () => { + const result = getMaskedPhoneNumber(countryCode as CountryCodeType, true); + expect(result).toBe(expected); + }); + }); + + // Tests for valid inputs without dial code + testCasesWithDialCode.forEach(({ countryCode, expected }) => { + it(`should return the correct phone number format without dial code for ${countryCode}`, () => { + const result = getMaskedPhoneNumber( + countryCode as CountryCodeType, + false, + ); + // Remove the dial code and leading space from the expected string + const expectedWithoutDialCode = expected.substring( + expected.indexOf(' ') + 1, + ); + expect(result).toBe(expectedWithoutDialCode); + }); + }); + + // Test for invalid country code + it('should throw an error for an invalid country code', () => { + expect(() => { + // @ts-expect-error null is not a valid country code + getMaskedPhoneNumber('XYZ', true); + }).toThrow('Parameter "countryCode" is invalid: XYZ'); + }); + + // Test for missing country code + it('should throw an error when country code is undefined', () => { + expect(() => { + // @ts-expect-error null is not a valid country code + getMaskedPhoneNumber(undefined, true); + }).toThrow('Parameter "countryCode" is invalid: undefined'); + }); + + // Test for null country code + it('should throw an error when country code is null', () => { + expect(() => { + // @ts-expect-error null is not a valid country code + getMaskedPhoneNumber(null, true); + }).toThrow('Parameter "countryCode" is invalid: null'); + }); +}); diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts new file mode 100644 index 00000000..d942edc5 --- /dev/null +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -0,0 +1,42 @@ +import getDialCodeByCountryCode from './getDialCodeByCountryCode'; +import { withErrorBoundary } from '../../common/errorBoundary'; +import PHONE_FORMATTER_MAPPER from './data/phoneFormatterMapper.json'; +import { CountryCodeType } from '../types'; + +/** + * Returns a masked phone number based on the country code. + * It uses predefined mappings to format phone numbers according to the country standards. + * + * @param countryCode The ISO 3166-1 alpha-2 country code. + * @param withDialCode A boolean indicating whether to include the country's dial code in the result. It has a default value of "true" + * @returns The masked phone number as a string. + */ +const getMaskedPhoneNumber = ( + countryCode: CountryCodeType, + withDialCode: boolean = true, +): string => { + // Throw errors if countryCode is invalid + if (!countryCode) + throw new Error(`Parameter "countryCode" is invalid: ${countryCode}`); + + // Retrieve the template for the given country code + const formattingTemplate = PHONE_FORMATTER_MAPPER[countryCode]; + + // Check if the country code is valid and a template exists + if (!formattingTemplate) { + throw new Error(`Parameter "countryCode" is invalid: ${countryCode}`); + } + + // If including the dial code, prepend it to the template with a space + if (withDialCode) { + const dialCode = getDialCodeByCountryCode(countryCode); + return `${dialCode} ${formattingTemplate}`; + } + + // Return the template directly if not including the dial code + return formattingTemplate; +}; + +export default withErrorBoundary( + getMaskedPhoneNumber, +); diff --git a/packages/i18nify-js/src/modules/phoneNumber/index.ts b/packages/i18nify-js/src/modules/phoneNumber/index.ts index ff84eed1..ff56731d 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/index.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/index.ts @@ -3,3 +3,4 @@ export { default as formatPhoneNumber } from './formatPhoneNumber'; export { default as parsePhoneNumber } from './parsePhoneNumber'; export { default as getDialCodes } from './getDialCodes'; export { default as getDialCodeByCountryCode } from './getDialCodeByCountryCode'; +export { default as getMaskedPhoneNumber } from './getMaskedPhoneNumber'; From 21cdd20436680f4147fd902b495a33f566fdb7c7 Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Wed, 8 May 2024 13:12:38 +0530 Subject: [PATCH 02/16] [docs]: add README docs for getMaskedPhoneNumber --- packages/i18nify-js/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/i18nify-js/README.md b/packages/i18nify-js/README.md index 21b8923f..dfd52a57 100644 --- a/packages/i18nify-js/README.md +++ b/packages/i18nify-js/README.md @@ -541,6 +541,26 @@ console.log(getDialCodeByCountryCode('BR')); // Outputs the dial code for Brazil console.log(getDialCodeByCountryCode('DE')); // Outputs the dial code for Germany (+49) ``` +#### getMaskedPhoneNumber(countryCode, withDialCode = true) + +📱🌍 This function provides an easy way to generate a formatted, masked phone number for any specified country based on its ISO 3166-1 alpha-2 code. Ideal for applications that handle international phone data, it allows for displaying phone number formats in a user-friendly masked layout. If the country code is not recognized or if formatting data is missing, the function will clearly indicate an error, ensuring reliable and accurate use. + +##### Examples + +``` +// Displaying the masked phone number for India including the dial code +console.log(getMaskedPhoneNumber('IN')); // Outputs: "+91 xxxx xxxxxx" + +// Displaying the masked phone number for India without the dial code +console.log(getMaskedPhoneNumber('IN', false)); // Outputs: "xxxx xxxxxx" + +// Displaying the masked phone number for Malaysia including the dial code +console.log(getMaskedPhoneNumber('MY')); // Outputs: "+60 xx xxxxx xx" + +// Displaying the masked phone number for Malaysia without the dial code +console.log(getMaskedPhoneNumber('MY', false)); // Outputs: "xx xxxxx xx" +``` + ### Module 03: Geo Module 🌍 Dive into the digital atlas with the Geo Module 🌍, your ultimate toolkit for accessing geo contextual data from around the globe 🌐. Whether you're infusing your projects with national pride 🎉 or exploring different countries 🤔, this module is like a magic carpet ride 🧞‍♂️. With a range of functions at your disposal ✨, incorporating global data 🚩 into your app has never been easier. Let's explore these global gems 🌟: From 583401426a203ced11888140d85b113197e971e8 Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Wed, 8 May 2024 13:13:56 +0530 Subject: [PATCH 03/16] [chore]: remove extra comments --- .../i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts index d942edc5..0b36c310 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -5,7 +5,6 @@ import { CountryCodeType } from '../types'; /** * Returns a masked phone number based on the country code. - * It uses predefined mappings to format phone numbers according to the country standards. * * @param countryCode The ISO 3166-1 alpha-2 country code. * @param withDialCode A boolean indicating whether to include the country's dial code in the result. It has a default value of "true" From 51f0427c99e1f03317f08c65a833bb28800d246f Mon Sep 17 00:00:00 2001 From: Divyansh Singh Date: Wed, 8 May 2024 14:32:06 +0530 Subject: [PATCH 04/16] Create few-deers-agree.md --- .changeset/few-deers-agree.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/few-deers-agree.md diff --git a/.changeset/few-deers-agree.md b/.changeset/few-deers-agree.md new file mode 100644 index 00000000..5503c53b --- /dev/null +++ b/.changeset/few-deers-agree.md @@ -0,0 +1,5 @@ +--- +"@razorpay/i18nify-js": minor +--- + +add new api for masked contact number in phone number module From eb86090088f55a65c665c3495c711c596ccbf37c Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Mon, 13 May 2024 00:51:56 +0530 Subject: [PATCH 05/16] [chore]: update the logic for getMaskedPhoneNumber --- .../phoneNumber/getMaskedPhoneNumber.ts | 113 ++++++++++++++---- .../src/modules/phoneNumber/index.ts | 1 + .../src/modules/phoneNumber/types.ts | 15 +++ 3 files changed, 105 insertions(+), 24 deletions(-) create mode 100644 packages/i18nify-js/src/modules/phoneNumber/types.ts diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts index 0b36c310..aba914f5 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -1,39 +1,104 @@ import getDialCodeByCountryCode from './getDialCodeByCountryCode'; import { withErrorBoundary } from '../../common/errorBoundary'; import PHONE_FORMATTER_MAPPER from './data/phoneFormatterMapper.json'; -import { CountryCodeType } from '../types'; +import { cleanPhoneNumber, detectCountryAndDialCodeFromPhone } from './utils'; +import { GetMaskedPhoneNumberOptions } from './types'; /** - * Returns a masked phone number based on the country code. + * Generates a masked phone number based on provided options. + * This function handles the complexity of different phone number formats and + * masking preferences such as complete masking or partial masking of digits. * - * @param countryCode The ISO 3166-1 alpha-2 country code. - * @param withDialCode A boolean indicating whether to include the country's dial code in the result. It has a default value of "true" - * @returns The masked phone number as a string. + * @param {GetMaskedPhoneNumberOptions} options - Options for generating the masked phone number. + * @param {CountryCodeType} options.countryCode - The country code associated with the phone number. + * @param {boolean} options.withDialCode - Determines if the dial code should be included in the masked number. + * @param {string} options.phoneNumber - The actual phone number to mask. + * @param {MaskingOptions} options.maskingOptions - Options to specify how the masking should be performed. + * @returns {string} The masked phone number formatted as per the specified options. + * @throws {Error} Throws an error if both countryCode and phoneNumber are empty or if other input validations fail. */ -const getMaskedPhoneNumber = ( - countryCode: CountryCodeType, - withDialCode: boolean = true, -): string => { - // Throw errors if countryCode is invalid - if (!countryCode) - throw new Error(`Parameter "countryCode" is invalid: ${countryCode}`); - - // Retrieve the template for the given country code - const formattingTemplate = PHONE_FORMATTER_MAPPER[countryCode]; - - // Check if the country code is valid and a template exists - if (!formattingTemplate) { - throw new Error(`Parameter "countryCode" is invalid: ${countryCode}`); +const getMaskedPhoneNumber = ({ + countryCode, + withDialCode = true, + phoneNumber, + maskingOptions = { + completeMasking: true, + prefixMasking: true, + maskedDigitsCount: 0, + maskingChar: 'x', + }, +}: GetMaskedPhoneNumberOptions) => { + if (!countryCode && !phoneNumber) { + throw new Error('Both countryCode and phoneNumber cannot be empty.'); } - // If including the dial code, prepend it to the template with a space + let maskedContactNumber: string; + + if (phoneNumber) { + // Clean the phone number to remove any non-numeric characters, except the leading '+' + let updatedPhoneNumber = phoneNumber; + updatedPhoneNumber = updatedPhoneNumber.toString(); + updatedPhoneNumber = cleanPhoneNumber(updatedPhoneNumber); + + // Detect the country code and dial code from the cleaned phone number + const countryData = detectCountryAndDialCodeFromPhone(updatedPhoneNumber); + const updatedCountryCode = countryCode || countryData.countryCode; + + // Get the phone number formatting template based on the country code + const formattingTemplate = PHONE_FORMATTER_MAPPER[updatedCountryCode]; + + if (!formattingTemplate) { + throw new Error(`Parameter "phoneNumber" is invalid: ${phoneNumber}`); + } + + maskedContactNumber = formattingTemplate; + + // If not complete masking, calculate the masked phone number based on the masking options + if (!maskingOptions.completeMasking) { + const dialCode = countryData.dialCode; + const phoneNumberWithoutDialCode = updatedPhoneNumber.slice( + dialCode.toString().length, + ); + + // Validate the masked digits count against the phone number length + if ( + maskingOptions.maskedDigitsCount && + maskingOptions.maskedDigitsCount > phoneNumberWithoutDialCode.length + ) { + throw new Error( + `maskedDigitsCount exceeds phone number length. Value of "maskedDigitsCount" is ${maskingOptions.maskedDigitsCount}`, + ); + } + + // Apply the masking characters to the phone number based on prefix or suffix masking + if (maskingOptions.prefixMasking) { + maskedContactNumber = + maskingOptions.maskingChar.repeat(maskingOptions.maskedDigitsCount) + + phoneNumberWithoutDialCode.slice(maskingOptions.maskedDigitsCount); + } else { + maskedContactNumber = + phoneNumberWithoutDialCode.slice( + 0, + -maskingOptions.maskedDigitsCount, + ) + + maskingOptions.maskingChar.repeat(maskingOptions.maskedDigitsCount); + } + } + } else { + // Retrieve the phone number formatting template using the country code + maskedContactNumber = PHONE_FORMATTER_MAPPER[countryCode]; + if (!maskedContactNumber) { + throw new Error(`Parameter "countryCode" is invalid: ${countryCode}`); + } + } + + // Include the dial code in the masked phone number if requested if (withDialCode) { const dialCode = getDialCodeByCountryCode(countryCode); - return `${dialCode} ${formattingTemplate}`; + return `${dialCode} ${maskedContactNumber}`; + } else { + return maskedContactNumber; } - - // Return the template directly if not including the dial code - return formattingTemplate; }; export default withErrorBoundary( diff --git a/packages/i18nify-js/src/modules/phoneNumber/index.ts b/packages/i18nify-js/src/modules/phoneNumber/index.ts index ff56731d..b69e534c 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/index.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/index.ts @@ -4,3 +4,4 @@ export { default as parsePhoneNumber } from './parsePhoneNumber'; export { default as getDialCodes } from './getDialCodes'; export { default as getDialCodeByCountryCode } from './getDialCodeByCountryCode'; export { default as getMaskedPhoneNumber } from './getMaskedPhoneNumber'; +export type { GetMaskedPhoneNumberOptions } from './types'; diff --git a/packages/i18nify-js/src/modules/phoneNumber/types.ts b/packages/i18nify-js/src/modules/phoneNumber/types.ts new file mode 100644 index 00000000..83d37251 --- /dev/null +++ b/packages/i18nify-js/src/modules/phoneNumber/types.ts @@ -0,0 +1,15 @@ +import { CountryCodeType } from '../..'; + +export interface MaskingOptions { + completeMasking?: boolean; + prefixMasking?: boolean; + maskedDigitsCount?: number; + maskingChar?: string; +} + +export interface GetMaskedPhoneNumberOptions { + countryCode: CountryCodeType; + withDialCode?: boolean; + phoneNumber?: string; + maskingOptions?: MaskingOptions; +} From 07ae9e3803944623d763dd6fc2b809ba10180a5b Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Mon, 13 May 2024 01:35:18 +0530 Subject: [PATCH 06/16] [test]: add UTs for getMaskedPhoneNumber --- .../__tests__/getMaskedPhoneNumber.test.ts | 186 +++++++++++++----- .../phoneNumber/getMaskedPhoneNumber.ts | 28 ++- .../src/modules/phoneNumber/utils.ts | 60 ++++++ 3 files changed, 219 insertions(+), 55 deletions(-) diff --git a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts index 0b59f7fd..db788aa3 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts @@ -1,60 +1,156 @@ import { CountryCodeType } from '../../types'; -import { getMaskedPhoneNumber } from '../index'; +import { getMaskedPhoneNumber, GetMaskedPhoneNumberOptions } from '../index'; describe('phoneNumber - getMaskedPhoneNumber', () => { - // Test data array for multiple countries - const testCasesWithDialCode = [ - { countryCode: 'US', expected: '+1 xxx-xxx-xxxx' }, - { countryCode: 'GB', expected: '+44 xxxx xxx xxx' }, - { countryCode: 'DE', expected: '+49 xxx xxxxxxxx' }, - { countryCode: 'IN', expected: '+91 xxxx xxxxxx' }, - { countryCode: 'JP', expected: '+81 xx xxxx xxxx' }, - ]; - - // Tests for valid inputs including dial code - testCasesWithDialCode.forEach(({ countryCode, expected }) => { - it(`should return the correct phone number format including dial code for ${countryCode}`, () => { - const result = getMaskedPhoneNumber(countryCode as CountryCodeType, true); - expect(result).toBe(expected); - }); + it('should throw an error when both countryCode and phoneNumber are empty', () => { + expect(() => + getMaskedPhoneNumber({ + countryCode: '' as CountryCodeType, + phoneNumber: '', + }), + ).toThrow('Both countryCode and phoneNumber cannot be empty.'); }); - // Tests for valid inputs without dial code - testCasesWithDialCode.forEach(({ countryCode, expected }) => { - it(`should return the correct phone number format without dial code for ${countryCode}`, () => { - const result = getMaskedPhoneNumber( - countryCode as CountryCodeType, - false, + it.each([ + { + countryCode: 'US', + phoneNumber: '+1234567890', + result: '+1 xxx-xxx-xxxx', + completeMasking: true, + }, + { + countryCode: 'GB', + phoneNumber: '+447911123456', + result: '+44 xxxx xx3 456', + maskedDigitsCount: 6, + maskingChar: 'x', + prefixMasking: true, + }, + { + countryCode: 'IN', + phoneNumber: '+919876543210', + result: '+91 9876 54321#', + maskedDigitsCount: 1, + maskingChar: '#', + prefixMasking: false, + }, + ])( + 'should apply masking correctly for different countries and options', + ({ + countryCode, + phoneNumber, + result, + completeMasking, + maskedDigitsCount, + maskingChar, + prefixMasking, + }) => { + const options = { + countryCode, + phoneNumber, + maskingOptions: { + completeMasking: completeMasking ?? false, + prefixMasking: prefixMasking ?? true, + maskedDigitsCount: maskedDigitsCount ?? 0, + maskingChar: maskingChar ?? 'x', + }, + }; + const maskedPhoneNumber = getMaskedPhoneNumber( + options as GetMaskedPhoneNumberOptions, ); - // Remove the dial code and leading space from the expected string - const expectedWithoutDialCode = expected.substring( - expected.indexOf(' ') + 1, + expect(maskedPhoneNumber).toBe(result); + }, + ); + + it('should handle phone numbers with and without leading plus sign', () => { + const testCases = [ + { + countryCode: 'US', + phoneNumber: '+1234567890', + expected: '+1 xxx-xxx-xxxx', + }, + { + countryCode: 'US', + phoneNumber: '1234567890', + expected: '+1 xxx-xxx-xxxx', + }, + ]; + + testCases.forEach(({ countryCode, phoneNumber, expected }) => { + const options = { + countryCode, + phoneNumber, + maskingOptions: { completeMasking: true }, + }; + const result = getMaskedPhoneNumber( + options as GetMaskedPhoneNumberOptions, ); - expect(result).toBe(expectedWithoutDialCode); + expect(result).toBe(expected); }); }); - // Test for invalid country code - it('should throw an error for an invalid country code', () => { - expect(() => { - // @ts-expect-error null is not a valid country code - getMaskedPhoneNumber('XYZ', true); - }).toThrow('Parameter "countryCode" is invalid: XYZ'); + it('should throw error for invalid country code', () => { + const optionsWithPhoneNumber = { + countryCode: 'XX' as CountryCodeType, // Invalid country code + phoneNumber: '0123456789', + }; + const optionsWithoutPhoneNumber = { + countryCode: 'XX' as CountryCodeType, // Invalid country code + }; + expect(() => getMaskedPhoneNumber(optionsWithPhoneNumber)).toThrow( + 'Error: Parameter "phoneNumber" is invalid: 0123456789', + ); + expect(() => getMaskedPhoneNumber(optionsWithoutPhoneNumber)).toThrow( + 'Parameter "countryCode" is invalid: XX', + ); }); - // Test for missing country code - it('should throw an error when country code is undefined', () => { - expect(() => { - // @ts-expect-error null is not a valid country code - getMaskedPhoneNumber(undefined, true); - }).toThrow('Parameter "countryCode" is invalid: undefined'); - }); + it.each([ + { + countryCode: 'DE', + phoneNumber: '+4915212345678', + expected: '+49 xxx xxxxxxxx', + }, + { + countryCode: 'JP', + phoneNumber: '+819012345678', + expected: '+81 xx xxxx xxxx', + }, + { + countryCode: 'RU', + phoneNumber: '+79031234567', + expected: '+7 xxx xxx-xx-xx', + }, + { + countryCode: 'KZ', + phoneNumber: '+77011234567', + expected: '+7 xxx-xxx-xx-xx', + }, + ])( + 'should format correctly with dial code for different countries', + ({ countryCode, phoneNumber, expected }) => { + expect( + getMaskedPhoneNumber({ + countryCode: countryCode as CountryCodeType, + phoneNumber, + }), + ).toBe(expected); + }, + ); - // Test for null country code - it('should throw an error when country code is null', () => { - expect(() => { - // @ts-expect-error null is not a valid country code - getMaskedPhoneNumber(null, true); - }).toThrow('Parameter "countryCode" is invalid: null'); + it('should throw error when maskedDigitsCount exceeds the phone number length', () => { + const options = { + countryCode: 'US', + phoneNumber: '+1234567890', + maskingOptions: { + completeMasking: false, + maskedDigitsCount: 20, // Excessive count + }, + }; + expect(() => + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toThrow( + 'maskedDigitsCount exceeds phone number length. Value of "maskedDigitsCount" is 20', + ); }); }); diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts index aba914f5..739c6fee 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -1,7 +1,12 @@ import getDialCodeByCountryCode from './getDialCodeByCountryCode'; import { withErrorBoundary } from '../../common/errorBoundary'; import PHONE_FORMATTER_MAPPER from './data/phoneFormatterMapper.json'; -import { cleanPhoneNumber, detectCountryAndDialCodeFromPhone } from './utils'; +import { + cleanPhoneNumber, + detectCountryAndDialCodeFromPhone, + replaceFirstXsWithChars, + replaceLastXsWithChars, +} from './utils'; import { GetMaskedPhoneNumberOptions } from './types'; /** @@ -72,16 +77,19 @@ const getMaskedPhoneNumber = ({ // Apply the masking characters to the phone number based on prefix or suffix masking if (maskingOptions.prefixMasking) { - maskedContactNumber = - maskingOptions.maskingChar.repeat(maskingOptions.maskedDigitsCount) + - phoneNumberWithoutDialCode.slice(maskingOptions.maskedDigitsCount); + maskedContactNumber = replaceLastXsWithChars( + formattingTemplate, + phoneNumberWithoutDialCode, + phoneNumberWithoutDialCode.length - + (maskingOptions.maskedDigitsCount || 0), + ).replace(/x/g, maskingOptions.maskingChar || 'x'); } else { - maskedContactNumber = - phoneNumberWithoutDialCode.slice( - 0, - -maskingOptions.maskedDigitsCount, - ) + - maskingOptions.maskingChar.repeat(maskingOptions.maskedDigitsCount); + maskedContactNumber = replaceFirstXsWithChars( + formattingTemplate, + phoneNumberWithoutDialCode, + phoneNumberWithoutDialCode.length - + (maskingOptions.maskedDigitsCount || 0), + ).replace(/x/g, maskingOptions.maskingChar || 'x'); } } } else { diff --git a/packages/i18nify-js/src/modules/phoneNumber/utils.ts b/packages/i18nify-js/src/modules/phoneNumber/utils.ts index 6b760b16..923fb92d 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/utils.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/utils.ts @@ -75,3 +75,63 @@ export const cleanPhoneNumber = (phoneNumber: string) => { const cleanedPhoneNumber = phoneNumber.replace(regex, ''); return phoneNumber[0] === '+' ? `+${cleanedPhoneNumber}` : cleanedPhoneNumber; }; + +/** + * Replaces the first `n` occurrences of 'x' in a source string with the first `n` characters from a replacement string. + * + * @param source {string} - The original string where replacements are to be made. + * @param replacement {string} - The string from which replacement characters are taken. + * @param n {number} - The number of 'x' characters to replace. + * @returns {string} - The modified string after replacements. + */ +export const replaceFirstXsWithChars = ( + source: string, + replacement: string, + n: number, +): string => { + // Convert the source string into an array of characters for easy manipulation + let result: string[] = source.split(''); + let replaceIndex: number = 0; + let replacementsDone: number = 0; + + // Iterate over the result array to replace 'x' with characters from the replacement string + for (let i = 0; i < result.length && replacementsDone < n; i++) { + if (result[i] === 'x' && replaceIndex < replacement.length) { + result[i] = replacement[replaceIndex++]; + replacementsDone++; + } + } + + // Join the array back into a string and return the modified result + return result.join(''); +}; + +/** + * Replaces the last `n` occurrences of 'x' in a source string with the last `n` characters from a replacement string. + * + * @param source {string} - The original string where replacements are to be made. + * @param replacement {string} - The string from which replacement characters are taken. + * @param n {number} - The number of 'x' characters to replace from the end of the source string. + * @returns {string} - The modified string after replacements. + */ +export const replaceLastXsWithChars = ( + source: string, + replacement: string, + n: number, +): string => { + // Convert the source string into an array of characters for easy manipulation + let result: string[] = source.split(''); + let replaceIndex: number = replacement.length - 1; + let replacementsDone: number = 0; + + // Iterate from the end of the source string + for (let i = result.length - 1; i >= 0 && replacementsDone < n; i--) { + if (result[i] === 'x' && replaceIndex >= 0) { + result[i] = replacement[replaceIndex--]; + replacementsDone++; + } + } + + // Join the array back into a string and return the modified result + return result.join(''); +}; From b7c1b9c319cdabd6ca8e9ecb899f2bbdeba182bb Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Mon, 13 May 2024 01:41:29 +0530 Subject: [PATCH 07/16] [docs]: add README docs for getMaskedPhoneNumber --- packages/i18nify-js/README.md | 152 ++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 64 deletions(-) diff --git a/packages/i18nify-js/README.md b/packages/i18nify-js/README.md index dfd52a57..2a7a1914 100644 --- a/packages/i18nify-js/README.md +++ b/packages/i18nify-js/README.md @@ -96,26 +96,26 @@ console.log(convertToMinorUnit(50, { currency: 'GBP' })); // Converts 50 pounds ##### Examples -``` -console.log(formatNumber("1000.5", { currency: "USD" })); // $1,000.50 +```javascript +console.log(formatNumber('1000.5', { currency: 'USD' })); // $1,000.50 console.log( - formatNumber("1500", { - currency: "EUR", - locale: "fr-FR", + formatNumber('1500', { + currency: 'EUR', + locale: 'fr-FR', intlOptions: { - currencyDisplay: "code", + currencyDisplay: 'code', }, - }) + }), ); // 1 500,00 EUR console.log( - formatNumber("5000", { - currency: "JPY", + formatNumber('5000', { + currency: 'JPY', intlOptions: { - currencyDisplay: "narrowSymbol", + currencyDisplay: 'narrowSymbol', }, - }) + }), ); // ¥5,000 ``` @@ -125,7 +125,7 @@ console.log( ##### Examples -``` +```javascript console.log(getCurrencyList()); /* { AED: { symbol: 'د.إ', @@ -162,7 +162,7 @@ Picture this: it's like having a cool decoder ring for currency codes! 🔍💰 ##### Examples -``` +```javascript console.log(getCurrencySymbol('USD')); // $ console.log(getCurrencySymbol('UZS')); // so'm @@ -176,12 +176,12 @@ This slick function breaks down numbers into separate pieces using Intl.NumberFo ##### Examples -``` +```javascript console.log( formatNumberByParts(12345.67, { - currency: "USD", - locale: "en-US", - }) + currency: 'USD', + locale: 'en-US', + }), ); /* { "currency": "$", "integer": "12,345", @@ -218,9 +218,9 @@ console.log( console.log( formatNumberByParts(12345.67, { - currency: "XYZ", - locale: "en-US", - }) + currency: 'XYZ', + locale: 'en-US', + }), ); /* { "currency": "XYZ", "integer": "12,345", @@ -261,9 +261,9 @@ console.log( console.log( formatNumberByParts(12345.67, { - currency: "EUR", - locale: "fr-FR", - }) + currency: 'EUR', + locale: 'fr-FR', + }), ); /* { "integer": "12 345", "decimal": ",", @@ -304,9 +304,9 @@ console.log( console.log( formatNumberByParts(12345.67, { - currency: "JPY", - locale: "ja-JP", - }) + currency: 'JPY', + locale: 'ja-JP', + }), ); /* { "currency": "¥", "integer": "12,346", @@ -333,9 +333,9 @@ console.log( console.log( formatNumberByParts(12345.67, { - currency: "OMR", - locale: "ar-OM", - }) + currency: 'OMR', + locale: 'ar-OM', + }), ); /* { "integer": "١٢٬٣٤٥", "decimal": "٫", @@ -393,26 +393,26 @@ This module's your phone's best friend, handling all things phone number-related ##### Examples -``` ---> Basic Validation +```javascript +// Basic Validation console.log(isValidPhoneNumber('+14155552671')); // true ---> Specifying Country Code for Validation +// Specifying Country Code for Validation console.log(isValidPhoneNumber('0501234567', 'AE')); // true ---> Auto-Detecting Country Code +// Auto-Detecting Country Code console.log(isValidPhoneNumber('+447700900123')); // true ---> Handling Invalid Numbers +// Handling Invalid Numbers console.log(isValidPhoneNumber('123456789', 'US')); // false ---> Invalid Country Code +// Invalid Country Code console.log(isValidPhoneNumber('+123456789')); // false ---> Empty Phone Number +// Empty Phone Number console.log(isValidPhoneNumber('')); // false ---> Non-Standard Formatting +// Non-Standard Formatting console.log(isValidPhoneNumber('(555) 555-5555')); // true ``` @@ -422,26 +422,26 @@ console.log(isValidPhoneNumber('(555) 555-5555')); // true ##### Examples -``` ---> Basic Formatting +```javascript +// Basic Formatting console.log(formatPhoneNumber('+14155552671')); // '+1 415-555-2671' ---> Specifying Country Code for Formatting +// Specifying Country Code for Formatting console.log(formatPhoneNumber('0501234567', 'AE')); // '050 123 4567' ---> Auto-Detecting Country Code for Formatting +// Auto-Detecting Country Code for Formatting console.log(formatPhoneNumber('+447700900123')); // '+44 7700 900123' ---> Handling Invalid Numbers for Formatting +// Handling Invalid Numbers for Formatting console.log(formatPhoneNumber('123456789', 'US')); // '123456789' ---> Invalid Country Code for Formatting +// Invalid Country Code for Formatting console.log(formatPhoneNumber('+123456789')); // '+123456789' ---> Empty Phone Number +// Empty Phone Number console.log(formatPhoneNumber('')); // Throws an Error: 'Parameter `phoneNumber` is invalid!' ---> Non-Standard Formatting +// Non-Standard Formatting console.log(formatPhoneNumber('(555) 555-5555')); // '555 555 5555' ``` @@ -451,8 +451,8 @@ console.log(formatPhoneNumber('(555) 555-5555')); // '555 555 5555' ##### Examples -``` ---> Formatting a Phone Number +```javascript +// Formatting a Phone Number const phoneNumber = '+1 (555) 123-4567'; const parsedInfo = parsePhoneNumber(phoneNumber); console.log('Country Code:', parsedInfo.countryCode); // 'US' @@ -460,16 +460,18 @@ console.log('Formatted Number:', parsedInfo.formattedPhoneNumber); // '555-123-4 console.log('Dial Code:', parsedInfo.dialCode); // '+1' console.log('Format Template:', parsedInfo.formatTemplate); // 'xxx-xxx-xxxx' ---> Parsing a Phone Number with Specified Country Code +// Parsing a Phone Number with Specified Country Code const phoneNumber = '987654321'; // Phone number without country code const countryCode = 'IN'; // Specifying the country code (India) const parsedInfo = parsePhoneNumber(phoneNumber, countryCode); console.log('Country Code:', parsedInfo.countryCode); // 'IN' console.log('Formatted Number:', parsedInfo.formattedPhoneNumber); // '98-765-4321' -console.log('Dial Code:', parsedInfo.dialCode); '' -console.log('Format Template:', parsedInfo.formatTemplate); 'xxxx xxxxxx' +console.log('Dial Code:', parsedInfo.dialCode); +(''); +console.log('Format Template:', parsedInfo.formatTemplate); +('xxxx xxxxxx'); ---> Handling Invalid Phone Numbers +// Handling Invalid Phone Numbers try { const invalidPhoneNumber = ''; // Empty phone number // This will throw an error since the phone number is empty @@ -481,7 +483,7 @@ try { console.error('Error:', error.message); // 'Parameter `phoneNumber` is invalid!' } ---> Obtaining Format Information for a Country Code +// Obtaining Format Information for a Country Code const countryCode = 'JP'; // Country code for Japan // Get the format information without providing a phone number const parsedInfo = parsePhoneNumber('', countryCode); @@ -541,24 +543,46 @@ console.log(getDialCodeByCountryCode('BR')); // Outputs the dial code for Brazil console.log(getDialCodeByCountryCode('DE')); // Outputs the dial code for Germany (+49) ``` -#### getMaskedPhoneNumber(countryCode, withDialCode = true) +#### getMaskedPhoneNumber(options) -📱🌍 This function provides an easy way to generate a formatted, masked phone number for any specified country based on its ISO 3166-1 alpha-2 code. Ideal for applications that handle international phone data, it allows for displaying phone number formats in a user-friendly masked layout. If the country code is not recognized or if formatting data is missing, the function will clearly indicate an error, ensuring reliable and accurate use. +📞🔒 The getMaskedPhoneNumber function is a versatile tool designed to handle phone number formatting and masking based on the specific requirements of different countries. This function is ideal for applications that require the display of partially hidden phone numbers for security purposes or privacy concerns. It supports a wide range of configurations, including options to mask portions of the phone number, specify the number of digits to mask, and choose whether to mask digits from the beginning or end of the number. ##### Examples -``` -// Displaying the masked phone number for India including the dial code -console.log(getMaskedPhoneNumber('IN')); // Outputs: "+91 xxxx xxxxxx" - -// Displaying the masked phone number for India without the dial code -console.log(getMaskedPhoneNumber('IN', false)); // Outputs: "xxxx xxxxxx" +```javascript +// Masking a U.S. phone number completely +console.log( + getMaskedPhoneNumber({ + countryCode: 'US', + phoneNumber: '2025550125', + withDialCode: true, + }), +); +// Output: +1 xxx-xxx-xxxx -// Displaying the masked phone number for Malaysia including the dial code -console.log(getMaskedPhoneNumber('MY')); // Outputs: "+60 xx xxxxx xx" +// Partially masking an Indian phone number, hiding the last 6 digits +console.log( + getMaskedPhoneNumber({ + countryCode: 'IN', + phoneNumber: '9876543210', + maskingOptions: { + completeMasking: false, + prefixMasking: false, + maskedDigitsCount: 6, + maskingChar: '*', + }, + withDialCode: true, + }), +); +// Output: +91 9876**-**** -// Displaying the masked phone number for Malaysia without the dial code -console.log(getMaskedPhoneNumber('MY', false)); // Outputs: "xx xxxxx xx" +// Formatting and completely masking a phone number for Brazil without specifying a phone number +console.log( + getMaskedPhoneNumber({ + countryCode: 'BR', + }), +); +// Output: xx xxxx-xxxx ``` ### Module 03: Geo Module 🌍 From 516c14995020eede0d584f8a3ad5f4d450db654b Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Mon, 13 May 2024 01:49:59 +0530 Subject: [PATCH 08/16] [test]: add missing UT --- .../__tests__/getMaskedPhoneNumber.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts index db788aa3..7e65b29f 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts @@ -153,4 +153,17 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { 'maskedDigitsCount exceeds phone number length. Value of "maskedDigitsCount" is 20', ); }); + + it('should format the phone number without including the dial code when withDialCode is false', () => { + const options = { + countryCode: 'US', + withDialCode: false, + maskingOptions: { + completeMasking: true, + }, + }; + const expectedOutput = 'xxx-xxx-xxxx'; + const result = getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions); + expect(result).toBe(expectedOutput); + }); }); From 23384b4099168c7d9e08e1f7464c2ef96a49fa19 Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Mon, 13 May 2024 11:35:43 +0530 Subject: [PATCH 09/16] [test]: add missing UT --- .../__tests__/replaceFirstXsWithChars.test.ts | 40 +++++++++++++++++++ .../__tests__/replaceLastXsWithChars.test.ts | 34 ++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceFirstXsWithChars.test.ts create mode 100644 packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceLastXsWithChars.test.ts diff --git a/packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceFirstXsWithChars.test.ts b/packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceFirstXsWithChars.test.ts new file mode 100644 index 00000000..6b787a75 --- /dev/null +++ b/packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceFirstXsWithChars.test.ts @@ -0,0 +1,40 @@ +import { replaceFirstXsWithChars } from '../utils'; + +describe('phone number - utils - replaceFirstXsWithChars', () => { + test("replaces the first N x's with characters from the replacement string", () => { + expect(replaceFirstXsWithChars('xxxxxx', 'abc', 3)).toBe('abcxxx'); + }); + + test.each([ + ['no xs present', 'hello', 'a', 2, 'hello'], + ['replacement string shorter than count of xs', 'xxxxx', 'ab', 5, 'abxxx'], + [ + 'replacement string longer than needed', + 'xxxx', + 'abcdef', + 2, + 'abxx'.slice(0, 4), + ], + ['empty source string', '', 'abc', 3, ''], + ['empty replacement string', 'xxxx', '', 3, 'xxxx'], + ['count n is zero', 'xxxx', 'abc', 0, 'xxxx'], + ])('%s', (_, source, replacement, n, expected) => { + expect(replaceFirstXsWithChars(source, replacement, n)).toBe(expected); + }); + + test("replaces nothing when n is greater than the number of x's", () => { + expect(replaceFirstXsWithChars('xx', 'abc', 5)).toBe('abcx'.slice(0, 2)); + }); + + test('handles strings with mixed characters', () => { + expect(replaceFirstXsWithChars('ax1x2x3x', 'xyz', 3)).toBe('ax1y2z3x'); + }); + + test('negative n is treated as zero', () => { + expect(replaceFirstXsWithChars('xxxx', 'abc', -1)).toBe('xxxx'); + }); + + test('n larger than both strings only replaces up to the shortest', () => { + expect(replaceFirstXsWithChars('xxyyxx', 'abc', 10)).toBe('abyycx'); + }); +}); diff --git a/packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceLastXsWithChars.test.ts b/packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceLastXsWithChars.test.ts new file mode 100644 index 00000000..3b485ee4 --- /dev/null +++ b/packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceLastXsWithChars.test.ts @@ -0,0 +1,34 @@ +import { replaceLastXsWithChars } from '../utils'; + +describe('phone number - utils - replaceLastXsWithChars', () => { + test("replaces the last N x's with characters from the replacement string", () => { + expect(replaceLastXsWithChars('xxxxxx', 'abc', 3)).toBe('xxxabc'); + }); + + test.each([ + ['no xs present', 'hello', 'a', 2, 'hello'], + ['replacement string shorter than count of xs', 'xxxxx', 'ab', 5, 'xxxab'], + ['replacement string longer than needed', 'xxxx', 'abcdef', 2, 'xxef'], + ['empty source string', '', 'abc', 3, ''], + ['empty replacement string', 'xxxx', '', 3, 'xxxx'], + ['count n is zero', 'xxxx', 'abc', 0, 'xxxx'], + ])('%s', (_, source, replacement, n, expected) => { + expect(replaceLastXsWithChars(source, replacement, n)).toBe(expected); + }); + + test("replaces nothing when n is greater than the number of x's", () => { + expect(replaceLastXsWithChars('xx', 'abc', 5)).toBe('bc'); + }); + + test('handles strings with mixed characters', () => { + expect(replaceLastXsWithChars('ax1x2x3x', 'xyz', 3)).toBe('ax1x2y3z'); + }); + + test('negative n is treated as zero', () => { + expect(replaceLastXsWithChars('xxxx', 'abc', -1)).toBe('xxxx'); + }); + + test('n larger than both strings only replaces up to the last available xs', () => { + expect(replaceLastXsWithChars('xxyyxx', 'abc', 10)).toBe('xayybc'); + }); +}); From 418cd4c25c2e98c5c7130179b00d770ded1c1f78 Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Mon, 13 May 2024 20:25:01 +0530 Subject: [PATCH 10/16] [chore]: resolve review comments --- packages/i18nify-js/README.md | 3 +- .../__tests__/getMaskedPhoneNumber.test.ts | 250 ++++++++---------- .../phoneNumber/getMaskedPhoneNumber.ts | 78 ++++-- .../src/modules/phoneNumber/types.ts | 3 +- 4 files changed, 163 insertions(+), 171 deletions(-) diff --git a/packages/i18nify-js/README.md b/packages/i18nify-js/README.md index 2a7a1914..1a70ac93 100644 --- a/packages/i18nify-js/README.md +++ b/packages/i18nify-js/README.md @@ -566,8 +566,7 @@ console.log( countryCode: 'IN', phoneNumber: '9876543210', maskingOptions: { - completeMasking: false, - prefixMasking: false, + maskingStyle: 'suffix', maskedDigitsCount: 6, maskingChar: '*', }, diff --git a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts index 7e65b29f..44acf0e6 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts @@ -2,168 +2,138 @@ import { CountryCodeType } from '../../types'; import { getMaskedPhoneNumber, GetMaskedPhoneNumberOptions } from '../index'; describe('phoneNumber - getMaskedPhoneNumber', () => { - it('should throw an error when both countryCode and phoneNumber are empty', () => { + it('should throw error if no countryCode and phoneNumber are provided', () => { expect(() => - getMaskedPhoneNumber({ - countryCode: '' as CountryCodeType, - phoneNumber: '', - }), - ).toThrow('Both countryCode and phoneNumber cannot be empty.'); + getMaskedPhoneNumber({} as GetMaskedPhoneNumberOptions), + ).toThrow('Either countryCode and phoneNumber is mandatory.'); }); - it.each([ - { + it('should throw error for invalid country code when phone number is not provided', () => { + expect(() => + getMaskedPhoneNumber({ countryCode: 'ZZ' as CountryCodeType }), + ).toThrow('Parameter "countryCode" is invalid: ZZ'); + }); + + it('should throw error for invalid country code when phone number is provided', () => { + const options = { + countryCode: 'ZZ' as CountryCodeType, + phoneNumber: '7394926646', + }; + const expected = 'xxxxxxxxxx'; + expect(getMaskedPhoneNumber(options)).toEqual(expected); + }); + + it('should return full masked phone number with dial code', () => { + const options = { countryCode: 'US', - phoneNumber: '+1234567890', - result: '+1 xxx-xxx-xxxx', - completeMasking: true, - }, - { - countryCode: 'GB', - phoneNumber: '+447911123456', - result: '+44 xxxx xx3 456', - maskedDigitsCount: 6, - maskingChar: 'x', - prefixMasking: true, - }, - { - countryCode: 'IN', - phoneNumber: '+919876543210', - result: '+91 9876 54321#', - maskedDigitsCount: 1, - maskingChar: '#', - prefixMasking: false, - }, - ])( - 'should apply masking correctly for different countries and options', - ({ - countryCode, - phoneNumber, - result, - completeMasking, - maskedDigitsCount, - maskingChar, - prefixMasking, - }) => { - const options = { - countryCode, - phoneNumber, - maskingOptions: { - completeMasking: completeMasking ?? false, - prefixMasking: prefixMasking ?? true, - maskedDigitsCount: maskedDigitsCount ?? 0, - maskingChar: maskingChar ?? 'x', - }, - }; - const maskedPhoneNumber = getMaskedPhoneNumber( - options as GetMaskedPhoneNumberOptions, - ); - expect(maskedPhoneNumber).toBe(result); - }, - ); + phoneNumber: '+12345678901', + withDialCode: true, + maskingOptions: { maskingStyle: 'full' }, + }; + const expected = '+1 xxx-xxx-xxxx'; + expect( + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toEqual(expected); + }); - it('should handle phone numbers with and without leading plus sign', () => { - const testCases = [ - { - countryCode: 'US', - phoneNumber: '+1234567890', - expected: '+1 xxx-xxx-xxxx', - }, - { - countryCode: 'US', - phoneNumber: '1234567890', - expected: '+1 xxx-xxx-xxxx', - }, - ]; + it('should return phone number masked with prefix', () => { + const options = { + countryCode: 'US', + phoneNumber: '2345678901', + withDialCode: false, + maskingOptions: { maskingStyle: 'prefix', maskedDigitsCount: 6 }, + }; + const expected = 'xxx-xxx-8901'; + expect( + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toEqual(expected); + }); - testCases.forEach(({ countryCode, phoneNumber, expected }) => { - const options = { - countryCode, - phoneNumber, - maskingOptions: { completeMasking: true }, - }; - const result = getMaskedPhoneNumber( - options as GetMaskedPhoneNumberOptions, - ); - expect(result).toBe(expected); - }); + it('should return phone number masked with suffix', () => { + const options = { + countryCode: 'US', + phoneNumber: '2345678901', + withDialCode: false, + maskingOptions: { maskingStyle: 'suffix', maskedDigitsCount: 6 }, + }; + const expected = '234-5xx-xxxx'; + expect( + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toEqual(expected); }); - it('should throw error for invalid country code', () => { - const optionsWithPhoneNumber = { - countryCode: 'XX' as CountryCodeType, // Invalid country code - phoneNumber: '0123456789', + it('should handle alternate masking of digits', () => { + const options = { + countryCode: 'US', + phoneNumber: '2345678901', + withDialCode: false, + maskingOptions: { maskingStyle: 'alternate', maskingChar: '*' }, }; - const optionsWithoutPhoneNumber = { - countryCode: 'XX' as CountryCodeType, // Invalid country code + const expected = '2*4*6*8*0*'; + expect( + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toEqual(expected); + }); + + it('should handle no phoneNumber with just country code', () => { + const options = { + countryCode: 'US', + withDialCode: true, + }; + const expected = '+1 xxx-xxx-xxxx'; + expect( + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toEqual(expected); + }); + + it('should return phone number with full masking and dial code', () => { + const options = { + countryCode: 'US', + phoneNumber: '2345678901', + withDialCode: true, + maskingOptions: { maskingStyle: 'full', maskingChar: '#' }, }; - expect(() => getMaskedPhoneNumber(optionsWithPhoneNumber)).toThrow( - 'Error: Parameter "phoneNumber" is invalid: 0123456789', - ); - expect(() => getMaskedPhoneNumber(optionsWithoutPhoneNumber)).toThrow( - 'Parameter "countryCode" is invalid: XX', - ); + const expected = '+1 ###-###-####'; + expect( + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toEqual(expected); }); - it.each([ - { - countryCode: 'DE', - phoneNumber: '+4915212345678', - expected: '+49 xxx xxxxxxxx', - }, - { - countryCode: 'JP', - phoneNumber: '+819012345678', - expected: '+81 xx xxxx xxxx', - }, - { - countryCode: 'RU', - phoneNumber: '+79031234567', - expected: '+7 xxx xxx-xx-xx', - }, - { - countryCode: 'KZ', - phoneNumber: '+77011234567', - expected: '+7 xxx-xxx-xx-xx', - }, - ])( - 'should format correctly with dial code for different countries', - ({ countryCode, phoneNumber, expected }) => { - expect( - getMaskedPhoneNumber({ - countryCode: countryCode as CountryCodeType, - phoneNumber, - }), - ).toBe(expected); - }, - ); + it('should return formatted phone number with complete masking when no masking options provided', () => { + const options = { + countryCode: 'US', + phoneNumber: '2345678901', + withDialCode: true, + }; + const expected = '+1 xxx-xxx-xxxx'; + expect( + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toEqual(expected); + }); - it('should throw error when maskedDigitsCount exceeds the phone number length', () => { + it('should handle input with non-numeric characters in phoneNumber', () => { const options = { countryCode: 'US', - phoneNumber: '+1234567890', - maskingOptions: { - completeMasking: false, - maskedDigitsCount: 20, // Excessive count - }, + phoneNumber: '+1 (234) 567-8901', + withDialCode: false, + maskingOptions: { maskingStyle: 'full', maskingChar: '*' }, }; - expect(() => + const expected = '***-***-****'; + expect( getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), - ).toThrow( - 'maskedDigitsCount exceeds phone number length. Value of "maskedDigitsCount" is 20', - ); + ).toEqual(expected); }); - it('should format the phone number without including the dial code when withDialCode is false', () => { + it('should perform complete masking if maskedDigitsCount is larger than phoneNumber length', () => { const options = { countryCode: 'US', + phoneNumber: '12345', withDialCode: false, - maskingOptions: { - completeMasking: true, - }, + maskingOptions: { maskingStyle: 'suffix', maskedDigitsCount: 10 }, }; - const expectedOutput = 'xxx-xxx-xxxx'; - const result = getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions); - expect(result).toBe(expectedOutput); + const expected = 'xxx-xxx-xxxx'; + expect( + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toEqual(expected); }); }); diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts index 739c6fee..9aef440e 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -27,14 +27,13 @@ const getMaskedPhoneNumber = ({ withDialCode = true, phoneNumber, maskingOptions = { - completeMasking: true, - prefixMasking: true, + maskingStyle: 'full', maskedDigitsCount: 0, maskingChar: 'x', }, }: GetMaskedPhoneNumberOptions) => { if (!countryCode && !phoneNumber) { - throw new Error('Both countryCode and phoneNumber cannot be empty.'); + throw new Error('Either countryCode and phoneNumber is mandatory.'); } let maskedContactNumber: string; @@ -50,16 +49,20 @@ const getMaskedPhoneNumber = ({ const updatedCountryCode = countryCode || countryData.countryCode; // Get the phone number formatting template based on the country code - const formattingTemplate = PHONE_FORMATTER_MAPPER[updatedCountryCode]; + let formattingTemplate = PHONE_FORMATTER_MAPPER[updatedCountryCode]; + // In case phone number doesn't have dialCode masking should happen without formatting if (!formattingTemplate) { - throw new Error(`Parameter "phoneNumber" is invalid: ${phoneNumber}`); + return updatedPhoneNumber.replace( + /./g, + maskingOptions.maskingChar || 'x', + ); } maskedContactNumber = formattingTemplate; // If not complete masking, calculate the masked phone number based on the masking options - if (!maskingOptions.completeMasking) { + if (maskingOptions.maskingStyle !== 'full') { const dialCode = countryData.dialCode; const phoneNumberWithoutDialCode = updatedPhoneNumber.slice( dialCode.toString().length, @@ -70,26 +73,47 @@ const getMaskedPhoneNumber = ({ maskingOptions.maskedDigitsCount && maskingOptions.maskedDigitsCount > phoneNumberWithoutDialCode.length ) { - throw new Error( - `maskedDigitsCount exceeds phone number length. Value of "maskedDigitsCount" is ${maskingOptions.maskedDigitsCount}`, - ); - } - - // Apply the masking characters to the phone number based on prefix or suffix masking - if (maskingOptions.prefixMasking) { - maskedContactNumber = replaceLastXsWithChars( - formattingTemplate, - phoneNumberWithoutDialCode, - phoneNumberWithoutDialCode.length - - (maskingOptions.maskedDigitsCount || 0), - ).replace(/x/g, maskingOptions.maskingChar || 'x'); + maskedContactNumber = PHONE_FORMATTER_MAPPER[countryCode]; } else { - maskedContactNumber = replaceFirstXsWithChars( - formattingTemplate, - phoneNumberWithoutDialCode, - phoneNumberWithoutDialCode.length - - (maskingOptions.maskedDigitsCount || 0), - ).replace(/x/g, maskingOptions.maskingChar || 'x'); + // Apply the masking characters to the phone number based on prefix or suffix masking + if (maskingOptions.maskingStyle === 'prefix') { + // Example: 7394926646 --> xxxx 926646 + maskedContactNumber = replaceLastXsWithChars( + formattingTemplate, + String(phoneNumberWithoutDialCode), + phoneNumberWithoutDialCode.length - + (maskingOptions.maskedDigitsCount || 0), + ).replace(/x/g, maskingOptions.maskingChar || 'x'); + } else if (maskingOptions.maskingStyle === 'suffix') { + // Example: 7394926646 --> 7494 92xxxx + maskedContactNumber = replaceFirstXsWithChars( + formattingTemplate, + String(phoneNumberWithoutDialCode), + phoneNumberWithoutDialCode.length - + (maskingOptions.maskedDigitsCount || 0), + ).replace(/x/g, maskingOptions.maskingChar || 'x'); + } else if (maskingOptions.maskingStyle === 'alternate') { + // Example: 7394926646 --> 7x9x 9x6x4x + maskedContactNumber = String(phoneNumberWithoutDialCode) + .trim() + .split('') + .reduce( + (acc: any, char: string) => { + if (/\d/.test(char)) { + acc.numericCount % 2 !== 0 + ? acc.result.push('x') + : acc.result.push(char); + acc.numericCount++; + } else { + acc.result.push(char); + } + return acc; + }, + { result: [], numericCount: 0 }, + ) + .result.join('') + .replace(/x/g, maskingOptions.maskingChar || 'x'); + } } } } else { @@ -103,9 +127,9 @@ const getMaskedPhoneNumber = ({ // Include the dial code in the masked phone number if requested if (withDialCode) { const dialCode = getDialCodeByCountryCode(countryCode); - return `${dialCode} ${maskedContactNumber}`; + return `${dialCode} ${maskedContactNumber.replace(/x/g, maskingOptions.maskingChar || 'x')}`; } else { - return maskedContactNumber; + return maskedContactNumber.replace(/x/g, maskingOptions.maskingChar || 'x'); } }; diff --git a/packages/i18nify-js/src/modules/phoneNumber/types.ts b/packages/i18nify-js/src/modules/phoneNumber/types.ts index 83d37251..05c1c830 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/types.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/types.ts @@ -1,8 +1,7 @@ import { CountryCodeType } from '../..'; export interface MaskingOptions { - completeMasking?: boolean; - prefixMasking?: boolean; + maskingStyle?: 'full' | 'prefix' | 'suffix' | 'alternate'; maskedDigitsCount?: number; maskingChar?: string; } From 5983c45f0dade1115c4a75c7b981733b85b4015e Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Mon, 13 May 2024 20:30:29 +0530 Subject: [PATCH 11/16] [chore]: remove unnecessary condition --- .../i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts index 9aef440e..973c202c 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -104,8 +104,6 @@ const getMaskedPhoneNumber = ({ ? acc.result.push('x') : acc.result.push(char); acc.numericCount++; - } else { - acc.result.push(char); } return acc; }, From 1c53298f5884c3842fbfc693b7ee277eb5f9a475 Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Tue, 14 May 2024 10:27:40 +0530 Subject: [PATCH 12/16] [chore]: resolve review comments --- packages/i18nify-js/README.md | 47 ++++++++++++++++++- .../__tests__/getMaskedPhoneNumber.test.ts | 35 +++++++++----- .../src/modules/phoneNumber/constants.ts | 6 +++ .../phoneNumber/getMaskedPhoneNumber.ts | 15 +++--- .../src/modules/phoneNumber/index.ts | 1 + .../src/modules/phoneNumber/types.ts | 3 +- 6 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 packages/i18nify-js/src/modules/phoneNumber/constants.ts diff --git a/packages/i18nify-js/README.md b/packages/i18nify-js/README.md index 1a70ac93..e1f5b317 100644 --- a/packages/i18nify-js/README.md +++ b/packages/i18nify-js/README.md @@ -560,7 +560,7 @@ console.log( ); // Output: +1 xxx-xxx-xxxx -// Partially masking an Indian phone number, hiding the last 6 digits +// Partially masking an Indian phone number, hiding the last 6 digits with maskingStyle: suffix console.log( getMaskedPhoneNumber({ countryCode: 'IN', @@ -573,7 +573,50 @@ console.log( withDialCode: true, }), ); -// Output: +91 9876**-**** +// Output: +91 9876 ****** + +// Partially masking an Indian phone number, hiding the first 6 digits with maskingStyle: prefix +console.log( + getMaskedPhoneNumber({ + countryCode: 'IN', + phoneNumber: '9876543210', + maskingOptions: { + maskingStyle: 'prefix', + maskedDigitsCount: 6, + maskingChar: '*', + }, + withDialCode: true, + }), +); +// Output: +91 **** 543210 + +// Partially masking an Indian phone number, hiding the first 6 digits with maskingStyle: full +console.log( + getMaskedPhoneNumber({ + countryCode: 'IN', + phoneNumber: '9876543210', + maskingOptions: { + maskingStyle: 'full', + maskingChar: '*', + }, + withDialCode: true, + }), +); +// Output: +91 **** ****** + +// Partially masking an Indian phone number, hiding the first 6 digits with maskingStyle: alternate +console.log( + getMaskedPhoneNumber({ + countryCode: 'IN', + phoneNumber: '9876543210', + maskingOptions: { + maskingStyle: 'alternate', + maskingChar: '*', + }, + withDialCode: true, + }), +); +// Output: +91 9*7* 5*3*1* // Formatting and completely masking a phone number for Brazil without specifying a phone number console.log( diff --git a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts index 44acf0e6..185a81d0 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts @@ -1,11 +1,12 @@ import { CountryCodeType } from '../../types'; import { getMaskedPhoneNumber, GetMaskedPhoneNumberOptions } from '../index'; +import { MaskingStyle } from '../constants'; describe('phoneNumber - getMaskedPhoneNumber', () => { it('should throw error if no countryCode and phoneNumber are provided', () => { expect(() => getMaskedPhoneNumber({} as GetMaskedPhoneNumberOptions), - ).toThrow('Either countryCode and phoneNumber is mandatory.'); + ).toThrow('Either countryCode or phoneNumber is mandatory.'); }); it('should throw error for invalid country code when phone number is not provided', () => { @@ -14,7 +15,7 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { ).toThrow('Parameter "countryCode" is invalid: ZZ'); }); - it('should throw error for invalid country code when phone number is provided', () => { + it('should handle invalid country code when phone number is provided', () => { const options = { countryCode: 'ZZ' as CountryCodeType, phoneNumber: '7394926646', @@ -28,7 +29,7 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { countryCode: 'US', phoneNumber: '+12345678901', withDialCode: true, - maskingOptions: { maskingStyle: 'full' }, + maskingOptions: { maskingStyle: MaskingStyle.Full }, }; const expected = '+1 xxx-xxx-xxxx'; expect( @@ -41,7 +42,10 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { countryCode: 'US', phoneNumber: '2345678901', withDialCode: false, - maskingOptions: { maskingStyle: 'prefix', maskedDigitsCount: 6 }, + maskingOptions: { + maskingStyle: MaskingStyle.Prefix, + maskedDigitsCount: 6, + }, }; const expected = 'xxx-xxx-8901'; expect( @@ -54,7 +58,10 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { countryCode: 'US', phoneNumber: '2345678901', withDialCode: false, - maskingOptions: { maskingStyle: 'suffix', maskedDigitsCount: 6 }, + maskingOptions: { + maskingStyle: MaskingStyle.Suffix, + maskedDigitsCount: 6, + }, }; const expected = '234-5xx-xxxx'; expect( @@ -67,7 +74,10 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { countryCode: 'US', phoneNumber: '2345678901', withDialCode: false, - maskingOptions: { maskingStyle: 'alternate', maskingChar: '*' }, + maskingOptions: { + maskingStyle: MaskingStyle.Alternate, + maskingChar: '*', + }, }; const expected = '2*4*6*8*0*'; expect( @@ -75,7 +85,7 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { ).toEqual(expected); }); - it('should handle no phoneNumber with just country code', () => { + it('should handle masking with just countryCode', () => { const options = { countryCode: 'US', withDialCode: true, @@ -86,12 +96,12 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { ).toEqual(expected); }); - it('should return phone number with full masking and dial code', () => { + it('should return phone number with full masking and dial code when maskingChar is #', () => { const options = { countryCode: 'US', phoneNumber: '2345678901', withDialCode: true, - maskingOptions: { maskingStyle: 'full', maskingChar: '#' }, + maskingOptions: { maskingStyle: MaskingStyle.Full, maskingChar: '#' }, }; const expected = '+1 ###-###-####'; expect( @@ -116,7 +126,7 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { countryCode: 'US', phoneNumber: '+1 (234) 567-8901', withDialCode: false, - maskingOptions: { maskingStyle: 'full', maskingChar: '*' }, + maskingOptions: { maskingStyle: MaskingStyle.Full, maskingChar: '*' }, }; const expected = '***-***-****'; expect( @@ -129,7 +139,10 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { countryCode: 'US', phoneNumber: '12345', withDialCode: false, - maskingOptions: { maskingStyle: 'suffix', maskedDigitsCount: 10 }, + maskingOptions: { + maskingStyle: MaskingStyle.Suffix, + maskedDigitsCount: 10, + }, }; const expected = 'xxx-xxx-xxxx'; expect( diff --git a/packages/i18nify-js/src/modules/phoneNumber/constants.ts b/packages/i18nify-js/src/modules/phoneNumber/constants.ts new file mode 100644 index 00000000..b82d3e85 --- /dev/null +++ b/packages/i18nify-js/src/modules/phoneNumber/constants.ts @@ -0,0 +1,6 @@ +export enum MaskingStyle { + Full = 'full', + Prefix = 'prefix', + Suffix = 'suffix', + Alternate = 'alternate', +} diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts index 973c202c..b8eaaf27 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -8,6 +8,7 @@ import { replaceLastXsWithChars, } from './utils'; import { GetMaskedPhoneNumberOptions } from './types'; +import { MaskingStyle } from './constants'; /** * Generates a masked phone number based on provided options. @@ -27,13 +28,13 @@ const getMaskedPhoneNumber = ({ withDialCode = true, phoneNumber, maskingOptions = { - maskingStyle: 'full', + maskingStyle: MaskingStyle.Full, maskedDigitsCount: 0, maskingChar: 'x', }, }: GetMaskedPhoneNumberOptions) => { if (!countryCode && !phoneNumber) { - throw new Error('Either countryCode and phoneNumber is mandatory.'); + throw new Error('Either countryCode or phoneNumber is mandatory.'); } let maskedContactNumber: string; @@ -51,7 +52,7 @@ const getMaskedPhoneNumber = ({ // Get the phone number formatting template based on the country code let formattingTemplate = PHONE_FORMATTER_MAPPER[updatedCountryCode]; - // In case phone number doesn't have dialCode masking should happen without formatting + // Apply full masking to the phone number if it lacks a dialCode, without formatting the phone number. if (!formattingTemplate) { return updatedPhoneNumber.replace( /./g, @@ -62,7 +63,7 @@ const getMaskedPhoneNumber = ({ maskedContactNumber = formattingTemplate; // If not complete masking, calculate the masked phone number based on the masking options - if (maskingOptions.maskingStyle !== 'full') { + if (maskingOptions.maskingStyle !== MaskingStyle.Full) { const dialCode = countryData.dialCode; const phoneNumberWithoutDialCode = updatedPhoneNumber.slice( dialCode.toString().length, @@ -76,7 +77,7 @@ const getMaskedPhoneNumber = ({ maskedContactNumber = PHONE_FORMATTER_MAPPER[countryCode]; } else { // Apply the masking characters to the phone number based on prefix or suffix masking - if (maskingOptions.maskingStyle === 'prefix') { + if (maskingOptions.maskingStyle === MaskingStyle.Prefix) { // Example: 7394926646 --> xxxx 926646 maskedContactNumber = replaceLastXsWithChars( formattingTemplate, @@ -84,7 +85,7 @@ const getMaskedPhoneNumber = ({ phoneNumberWithoutDialCode.length - (maskingOptions.maskedDigitsCount || 0), ).replace(/x/g, maskingOptions.maskingChar || 'x'); - } else if (maskingOptions.maskingStyle === 'suffix') { + } else if (maskingOptions.maskingStyle === MaskingStyle.Suffix) { // Example: 7394926646 --> 7494 92xxxx maskedContactNumber = replaceFirstXsWithChars( formattingTemplate, @@ -92,7 +93,7 @@ const getMaskedPhoneNumber = ({ phoneNumberWithoutDialCode.length - (maskingOptions.maskedDigitsCount || 0), ).replace(/x/g, maskingOptions.maskingChar || 'x'); - } else if (maskingOptions.maskingStyle === 'alternate') { + } else if (maskingOptions.maskingStyle === MaskingStyle.Alternate) { // Example: 7394926646 --> 7x9x 9x6x4x maskedContactNumber = String(phoneNumberWithoutDialCode) .trim() diff --git a/packages/i18nify-js/src/modules/phoneNumber/index.ts b/packages/i18nify-js/src/modules/phoneNumber/index.ts index b69e534c..f3fd322f 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/index.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/index.ts @@ -5,3 +5,4 @@ export { default as getDialCodes } from './getDialCodes'; export { default as getDialCodeByCountryCode } from './getDialCodeByCountryCode'; export { default as getMaskedPhoneNumber } from './getMaskedPhoneNumber'; export type { GetMaskedPhoneNumberOptions } from './types'; +export { MaskingStyle } from './constants'; diff --git a/packages/i18nify-js/src/modules/phoneNumber/types.ts b/packages/i18nify-js/src/modules/phoneNumber/types.ts index 05c1c830..ece2bca6 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/types.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/types.ts @@ -1,7 +1,8 @@ import { CountryCodeType } from '../..'; +import { MaskingStyle } from './constants'; export interface MaskingOptions { - maskingStyle?: 'full' | 'prefix' | 'suffix' | 'alternate'; + maskingStyle?: MaskingStyle; maskedDigitsCount?: number; maskingChar?: string; } From a322fec2ac13868291ac485d45715e3209d226e8 Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Tue, 14 May 2024 11:22:13 +0530 Subject: [PATCH 13/16] [chore]: resolve review comments --- .../phoneNumber/getMaskedPhoneNumber.ts | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts index b8eaaf27..4dc212fc 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -27,12 +27,14 @@ const getMaskedPhoneNumber = ({ countryCode, withDialCode = true, phoneNumber, - maskingOptions = { - maskingStyle: MaskingStyle.Full, - maskedDigitsCount: 0, - maskingChar: 'x', - }, + maskingOptions = {}, }: GetMaskedPhoneNumberOptions) => { + const { + maskingStyle = MaskingStyle.Full, + maskedDigitsCount = 0, + maskingChar = 'x', + } = maskingOptions; + if (!countryCode && !phoneNumber) { throw new Error('Either countryCode or phoneNumber is mandatory.'); } @@ -54,16 +56,13 @@ const getMaskedPhoneNumber = ({ // Apply full masking to the phone number if it lacks a dialCode, without formatting the phone number. if (!formattingTemplate) { - return updatedPhoneNumber.replace( - /./g, - maskingOptions.maskingChar || 'x', - ); + return updatedPhoneNumber.replace(/./g, maskingChar); } maskedContactNumber = formattingTemplate; // If not complete masking, calculate the masked phone number based on the masking options - if (maskingOptions.maskingStyle !== MaskingStyle.Full) { + if (maskingStyle !== MaskingStyle.Full) { const dialCode = countryData.dialCode; const phoneNumberWithoutDialCode = updatedPhoneNumber.slice( dialCode.toString().length, @@ -71,29 +70,27 @@ const getMaskedPhoneNumber = ({ // Validate the masked digits count against the phone number length if ( - maskingOptions.maskedDigitsCount && - maskingOptions.maskedDigitsCount > phoneNumberWithoutDialCode.length + maskedDigitsCount && + maskedDigitsCount > phoneNumberWithoutDialCode.length ) { maskedContactNumber = PHONE_FORMATTER_MAPPER[countryCode]; } else { // Apply the masking characters to the phone number based on prefix or suffix masking - if (maskingOptions.maskingStyle === MaskingStyle.Prefix) { + if (maskingStyle === MaskingStyle.Prefix) { // Example: 7394926646 --> xxxx 926646 maskedContactNumber = replaceLastXsWithChars( formattingTemplate, String(phoneNumberWithoutDialCode), - phoneNumberWithoutDialCode.length - - (maskingOptions.maskedDigitsCount || 0), - ).replace(/x/g, maskingOptions.maskingChar || 'x'); - } else if (maskingOptions.maskingStyle === MaskingStyle.Suffix) { + phoneNumberWithoutDialCode.length - maskedDigitsCount, + ).replace(/x/g, maskingChar); + } else if (maskingStyle === MaskingStyle.Suffix) { // Example: 7394926646 --> 7494 92xxxx maskedContactNumber = replaceFirstXsWithChars( formattingTemplate, String(phoneNumberWithoutDialCode), - phoneNumberWithoutDialCode.length - - (maskingOptions.maskedDigitsCount || 0), - ).replace(/x/g, maskingOptions.maskingChar || 'x'); - } else if (maskingOptions.maskingStyle === MaskingStyle.Alternate) { + phoneNumberWithoutDialCode.length - maskedDigitsCount, + ).replace(/x/g, maskingChar); + } else if (maskingStyle === MaskingStyle.Alternate) { // Example: 7394926646 --> 7x9x 9x6x4x maskedContactNumber = String(phoneNumberWithoutDialCode) .trim() @@ -111,7 +108,7 @@ const getMaskedPhoneNumber = ({ { result: [], numericCount: 0 }, ) .result.join('') - .replace(/x/g, maskingOptions.maskingChar || 'x'); + .replace(/x/g, maskingChar); } } } @@ -126,9 +123,9 @@ const getMaskedPhoneNumber = ({ // Include the dial code in the masked phone number if requested if (withDialCode) { const dialCode = getDialCodeByCountryCode(countryCode); - return `${dialCode} ${maskedContactNumber.replace(/x/g, maskingOptions.maskingChar || 'x')}`; + return `${dialCode} ${maskedContactNumber.replace(/x/g, maskingChar)}`; } else { - return maskedContactNumber.replace(/x/g, maskingOptions.maskingChar || 'x'); + return maskedContactNumber.replace(/x/g, maskingChar); } }; From ebd1ebd6387291664356e52e94eea571506b051f Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Mon, 20 May 2024 02:17:23 +0530 Subject: [PATCH 14/16] [chore]: modularise getMaskedPhoneNumber for better readibility --- ...ithChars.test.ts => prefixMasking.test.ts} | 16 +-- ...ithChars.test.ts => suffixMasking.test.ts} | 16 +-- .../phoneNumber/getMaskedPhoneNumber.ts | 108 ++++++++---------- .../src/modules/phoneNumber/utils.ts | 35 +++++- 4 files changed, 94 insertions(+), 81 deletions(-) rename packages/i18nify-js/src/modules/phoneNumber/__tests__/{replaceLastXsWithChars.test.ts => prefixMasking.test.ts} (60%) rename packages/i18nify-js/src/modules/phoneNumber/__tests__/{replaceFirstXsWithChars.test.ts => suffixMasking.test.ts} (60%) diff --git a/packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceLastXsWithChars.test.ts b/packages/i18nify-js/src/modules/phoneNumber/__tests__/prefixMasking.test.ts similarity index 60% rename from packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceLastXsWithChars.test.ts rename to packages/i18nify-js/src/modules/phoneNumber/__tests__/prefixMasking.test.ts index 3b485ee4..1c94b147 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceLastXsWithChars.test.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/__tests__/prefixMasking.test.ts @@ -1,8 +1,8 @@ -import { replaceLastXsWithChars } from '../utils'; +import { prefixMasking } from '../utils'; -describe('phone number - utils - replaceLastXsWithChars', () => { +describe('phone number - utils - prefixMasking', () => { test("replaces the last N x's with characters from the replacement string", () => { - expect(replaceLastXsWithChars('xxxxxx', 'abc', 3)).toBe('xxxabc'); + expect(prefixMasking('xxxxxx', 'abc', 3)).toBe('xxxabc'); }); test.each([ @@ -13,22 +13,22 @@ describe('phone number - utils - replaceLastXsWithChars', () => { ['empty replacement string', 'xxxx', '', 3, 'xxxx'], ['count n is zero', 'xxxx', 'abc', 0, 'xxxx'], ])('%s', (_, source, replacement, n, expected) => { - expect(replaceLastXsWithChars(source, replacement, n)).toBe(expected); + expect(prefixMasking(source, replacement, n)).toBe(expected); }); test("replaces nothing when n is greater than the number of x's", () => { - expect(replaceLastXsWithChars('xx', 'abc', 5)).toBe('bc'); + expect(prefixMasking('xx', 'abc', 5)).toBe('bc'); }); test('handles strings with mixed characters', () => { - expect(replaceLastXsWithChars('ax1x2x3x', 'xyz', 3)).toBe('ax1x2y3z'); + expect(prefixMasking('ax1x2x3x', 'xyz', 3)).toBe('ax1x2y3z'); }); test('negative n is treated as zero', () => { - expect(replaceLastXsWithChars('xxxx', 'abc', -1)).toBe('xxxx'); + expect(prefixMasking('xxxx', 'abc', -1)).toBe('xxxx'); }); test('n larger than both strings only replaces up to the last available xs', () => { - expect(replaceLastXsWithChars('xxyyxx', 'abc', 10)).toBe('xayybc'); + expect(prefixMasking('xxyyxx', 'abc', 10)).toBe('xayybc'); }); }); diff --git a/packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceFirstXsWithChars.test.ts b/packages/i18nify-js/src/modules/phoneNumber/__tests__/suffixMasking.test.ts similarity index 60% rename from packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceFirstXsWithChars.test.ts rename to packages/i18nify-js/src/modules/phoneNumber/__tests__/suffixMasking.test.ts index 6b787a75..4fea6a31 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/__tests__/replaceFirstXsWithChars.test.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/__tests__/suffixMasking.test.ts @@ -1,8 +1,8 @@ -import { replaceFirstXsWithChars } from '../utils'; +import { suffixMasking } from '../utils'; -describe('phone number - utils - replaceFirstXsWithChars', () => { +describe('phone number - utils - suffixMasking', () => { test("replaces the first N x's with characters from the replacement string", () => { - expect(replaceFirstXsWithChars('xxxxxx', 'abc', 3)).toBe('abcxxx'); + expect(suffixMasking('xxxxxx', 'abc', 3)).toBe('abcxxx'); }); test.each([ @@ -19,22 +19,22 @@ describe('phone number - utils - replaceFirstXsWithChars', () => { ['empty replacement string', 'xxxx', '', 3, 'xxxx'], ['count n is zero', 'xxxx', 'abc', 0, 'xxxx'], ])('%s', (_, source, replacement, n, expected) => { - expect(replaceFirstXsWithChars(source, replacement, n)).toBe(expected); + expect(suffixMasking(source, replacement, n)).toBe(expected); }); test("replaces nothing when n is greater than the number of x's", () => { - expect(replaceFirstXsWithChars('xx', 'abc', 5)).toBe('abcx'.slice(0, 2)); + expect(suffixMasking('xx', 'abc', 5)).toBe('abcx'.slice(0, 2)); }); test('handles strings with mixed characters', () => { - expect(replaceFirstXsWithChars('ax1x2x3x', 'xyz', 3)).toBe('ax1y2z3x'); + expect(suffixMasking('ax1x2x3x', 'xyz', 3)).toBe('ax1y2z3x'); }); test('negative n is treated as zero', () => { - expect(replaceFirstXsWithChars('xxxx', 'abc', -1)).toBe('xxxx'); + expect(suffixMasking('xxxx', 'abc', -1)).toBe('xxxx'); }); test('n larger than both strings only replaces up to the shortest', () => { - expect(replaceFirstXsWithChars('xxyyxx', 'abc', 10)).toBe('abyycx'); + expect(suffixMasking('xxyyxx', 'abc', 10)).toBe('abyycx'); }); }); diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts index 4dc212fc..5383ce3c 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -4,8 +4,9 @@ import PHONE_FORMATTER_MAPPER from './data/phoneFormatterMapper.json'; import { cleanPhoneNumber, detectCountryAndDialCodeFromPhone, - replaceFirstXsWithChars, - replaceLastXsWithChars, + suffixMasking, + prefixMasking, + alternateMasking, } from './utils'; import { GetMaskedPhoneNumberOptions } from './types'; import { MaskingStyle } from './constants'; @@ -40,6 +41,7 @@ const getMaskedPhoneNumber = ({ } let maskedContactNumber: string; + let dialCode: string; if (phoneNumber) { // Clean the phone number to remove any non-numeric characters, except the leading '+' @@ -50,67 +52,51 @@ const getMaskedPhoneNumber = ({ // Detect the country code and dial code from the cleaned phone number const countryData = detectCountryAndDialCodeFromPhone(updatedPhoneNumber); const updatedCountryCode = countryCode || countryData.countryCode; - - // Get the phone number formatting template based on the country code - let formattingTemplate = PHONE_FORMATTER_MAPPER[updatedCountryCode]; - - // Apply full masking to the phone number if it lacks a dialCode, without formatting the phone number. - if (!formattingTemplate) { - return updatedPhoneNumber.replace(/./g, maskingChar); + try { + dialCode = getDialCodeByCountryCode(updatedCountryCode); + } catch (error) { + dialCode = countryData.dialCode; } - maskedContactNumber = formattingTemplate; + // Extract the phone number without dial code + const phoneNumberWithoutDialCode = + updatedPhoneNumber[0] === '+' + ? updatedPhoneNumber.slice(dialCode.toString().length) + : updatedPhoneNumber; - // If not complete masking, calculate the masked phone number based on the masking options - if (maskingStyle !== MaskingStyle.Full) { - const dialCode = countryData.dialCode; - const phoneNumberWithoutDialCode = updatedPhoneNumber.slice( - dialCode.toString().length, - ); + // Get the phone number formatting template based on the country code + let formattingTemplate = + PHONE_FORMATTER_MAPPER[updatedCountryCode] || + phoneNumber.replace(/\d/g, 'x'); - // Validate the masked digits count against the phone number length - if ( - maskedDigitsCount && - maskedDigitsCount > phoneNumberWithoutDialCode.length - ) { - maskedContactNumber = PHONE_FORMATTER_MAPPER[countryCode]; - } else { - // Apply the masking characters to the phone number based on prefix or suffix masking - if (maskingStyle === MaskingStyle.Prefix) { - // Example: 7394926646 --> xxxx 926646 - maskedContactNumber = replaceLastXsWithChars( - formattingTemplate, - String(phoneNumberWithoutDialCode), - phoneNumberWithoutDialCode.length - maskedDigitsCount, - ).replace(/x/g, maskingChar); - } else if (maskingStyle === MaskingStyle.Suffix) { - // Example: 7394926646 --> 7494 92xxxx - maskedContactNumber = replaceFirstXsWithChars( - formattingTemplate, - String(phoneNumberWithoutDialCode), - phoneNumberWithoutDialCode.length - maskedDigitsCount, - ).replace(/x/g, maskingChar); - } else if (maskingStyle === MaskingStyle.Alternate) { - // Example: 7394926646 --> 7x9x 9x6x4x - maskedContactNumber = String(phoneNumberWithoutDialCode) - .trim() - .split('') - .reduce( - (acc: any, char: string) => { - if (/\d/.test(char)) { - acc.numericCount % 2 !== 0 - ? acc.result.push('x') - : acc.result.push(char); - acc.numericCount++; - } - return acc; - }, - { result: [], numericCount: 0 }, - ) - .result.join('') - .replace(/x/g, maskingChar); + switch (maskingStyle) { + case MaskingStyle.Alternate: + // Example: 7394926646 --> 7x9x 9x6x4x + maskedContactNumber = alternateMasking(phoneNumberWithoutDialCode); + break; + case MaskingStyle.Prefix: + // Example: 7394926646 --> xxxx 926646 + maskedContactNumber = prefixMasking( + formattingTemplate, + String(phoneNumberWithoutDialCode), + phoneNumberWithoutDialCode.length - maskedDigitsCount, + ); + break; + case MaskingStyle.Suffix: + // Example: 7394926646 --> 7494 92xxxx + maskedContactNumber = suffixMasking( + formattingTemplate, + String(phoneNumberWithoutDialCode), + phoneNumberWithoutDialCode.length - maskedDigitsCount, + ); + break; + default: // Full Masking Condition + maskedContactNumber = formattingTemplate; + if (!maskedContactNumber) { + throw new Error( + `Either of "phoneNumber" or "countryCode" is invalid. countryCode: ${countryCode}, phoneNumber: ${phoneNumber}`, + ); } - } } } else { // Retrieve the phone number formatting template using the country code @@ -118,14 +104,14 @@ const getMaskedPhoneNumber = ({ if (!maskedContactNumber) { throw new Error(`Parameter "countryCode" is invalid: ${countryCode}`); } + dialCode = getDialCodeByCountryCode(countryCode); } // Include the dial code in the masked phone number if requested if (withDialCode) { - const dialCode = getDialCodeByCountryCode(countryCode); - return `${dialCode} ${maskedContactNumber.replace(/x/g, maskingChar)}`; + return `${dialCode} ${maskedContactNumber.replace(/x/g, maskingChar)}`.trim(); } else { - return maskedContactNumber.replace(/x/g, maskingChar); + return maskedContactNumber.trim().replace(/x/g, maskingChar); } }; diff --git a/packages/i18nify-js/src/modules/phoneNumber/utils.ts b/packages/i18nify-js/src/modules/phoneNumber/utils.ts index 923fb92d..bc6380de 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/utils.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/utils.ts @@ -81,10 +81,10 @@ export const cleanPhoneNumber = (phoneNumber: string) => { * * @param source {string} - The original string where replacements are to be made. * @param replacement {string} - The string from which replacement characters are taken. - * @param n {number} - The number of 'x' characters to replace. + * @param n {number} - The number of 'x' characters to replace (unmasked digit count). * @returns {string} - The modified string after replacements. */ -export const replaceFirstXsWithChars = ( +export const suffixMasking = ( source: string, replacement: string, n: number, @@ -111,10 +111,10 @@ export const replaceFirstXsWithChars = ( * * @param source {string} - The original string where replacements are to be made. * @param replacement {string} - The string from which replacement characters are taken. - * @param n {number} - The number of 'x' characters to replace from the end of the source string. + * @param n {number} - The number of 'x' characters to replace from the end of the source string (unmasked digit count). * @returns {string} - The modified string after replacements. */ -export const replaceLastXsWithChars = ( +export const prefixMasking = ( source: string, replacement: string, n: number, @@ -135,3 +135,30 @@ export const replaceLastXsWithChars = ( // Join the array back into a string and return the modified result return result.join(''); }; + +/** + * Replaces every alternate digit of phone number with 'x' in phoneNumberWithoutDialCode. + * + * @param phoneNumberWithoutDialCode {number | string} - The original phone number without dial code where replacements are to be made. + * @returns {string} - The modified string after replacements. + */ +export const alternateMasking = ( + phoneNumberWithoutDialCode: number | string, +): string => { + return String(phoneNumberWithoutDialCode) + .trim() + .split('') + .reduce( + (acc: any, char: string) => { + if (/\d/.test(char)) { + acc.numericCount % 2 !== 0 + ? acc.result.push('x') + : acc.result.push(char); + acc.numericCount++; + } + return acc; + }, + { result: [], numericCount: 0 }, + ) + .result.join(''); +}; From 84ef659aa924e095f07ea77626bb3d927f3c1840 Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Mon, 20 May 2024 02:18:35 +0530 Subject: [PATCH 15/16] [chore]: remove unnecesary exception --- .../src/modules/phoneNumber/getMaskedPhoneNumber.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts index 5383ce3c..f46bba26 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -92,11 +92,6 @@ const getMaskedPhoneNumber = ({ break; default: // Full Masking Condition maskedContactNumber = formattingTemplate; - if (!maskedContactNumber) { - throw new Error( - `Either of "phoneNumber" or "countryCode" is invalid. countryCode: ${countryCode}, phoneNumber: ${phoneNumber}`, - ); - } } } else { // Retrieve the phone number formatting template using the country code From e4b328b059a32515f1d1e278b16ef7b304947eaa Mon Sep 17 00:00:00 2001 From: RgnDunes Date: Mon, 20 May 2024 12:45:55 +0530 Subject: [PATCH 16/16] [test]: add missing UTs --- .../__tests__/getMaskedPhoneNumber.test.ts | 108 ++++++++++++++++++ .../phoneNumber/getMaskedPhoneNumber.ts | 2 +- 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts index 185a81d0..29aadd7d 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/__tests__/getMaskedPhoneNumber.test.ts @@ -149,4 +149,112 @@ describe('phoneNumber - getMaskedPhoneNumber', () => { getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), ).toEqual(expected); }); + + describe('should mask with just phone number (without dialcode) without countryCode', () => { + const phoneNumber = '7394926646'; + const testCases = [ + { + options: { + phoneNumber, + maskingOptions: { + maskingStyle: MaskingStyle.Prefix, + maskedDigitsCount: 4, + }, + }, + expected: 'xxxx926646', + description: 'Prefix style', + }, + { + options: { + phoneNumber, + maskingOptions: { + maskingStyle: MaskingStyle.Suffix, + maskedDigitsCount: 4, + }, + }, + expected: '739492xxxx', + description: 'Suffix style', + }, + { + options: { + phoneNumber, + maskingOptions: { + maskingStyle: MaskingStyle.Alternate, + }, + }, + expected: '7x9x9x6x4x', + description: 'Alternate style', + }, + { + options: { + phoneNumber, + maskingOptions: { + maskingStyle: MaskingStyle.Full, + }, + }, + expected: 'xxxxxxxxxx', + description: 'Full style', + }, + ]; + + test.each(testCases)('$description', ({ options, expected }) => { + expect( + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toEqual(expected); + }); + }); + + describe('should mask with just phone number (with dialcode) without countryCode', () => { + const phoneNumber = '+91 7394926646'; + const testCases = [ + { + options: { + phoneNumber, + maskingOptions: { + maskingStyle: MaskingStyle.Prefix, + maskedDigitsCount: 4, + }, + }, + expected: '+91 xxxx 926646', + description: 'Prefix style', + }, + { + options: { + phoneNumber, + maskingOptions: { + maskingStyle: MaskingStyle.Suffix, + maskedDigitsCount: 4, + }, + }, + expected: '+91 7394 92xxxx', + description: 'Suffix style', + }, + { + options: { + phoneNumber, + maskingOptions: { + maskingStyle: MaskingStyle.Alternate, + }, + }, + expected: '+91 7x9x9x6x4x', + description: 'Alternate style', + }, + { + options: { + phoneNumber, + maskingOptions: { + maskingStyle: MaskingStyle.Full, + }, + }, + expected: '+91 xxxx xxxxxx', + description: 'Full style', + }, + ]; + + test.each(testCases)('$description', ({ options, expected }) => { + expect( + getMaskedPhoneNumber(options as GetMaskedPhoneNumberOptions), + ).toEqual(expected); + }); + }); }); diff --git a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts index f46bba26..ef53d721 100644 --- a/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts +++ b/packages/i18nify-js/src/modules/phoneNumber/getMaskedPhoneNumber.ts @@ -71,7 +71,7 @@ const getMaskedPhoneNumber = ({ switch (maskingStyle) { case MaskingStyle.Alternate: - // Example: 7394926646 --> 7x9x 9x6x4x + // Example: 7394926646 --> 7x9x9x6x4x maskedContactNumber = alternateMasking(phoneNumberWithoutDialCode); break; case MaskingStyle.Prefix: