mirror of
https://github.com/valitydev/checkout.git
synced 2024-11-06 02:25:18 +00:00
EMP-97: Add copy value formatter. Add parseNationalNumber (#360)
Some checks are pending
Master / Build (push) Waiting to run
Some checks are pending
Master / Build (push) Waiting to run
This commit is contained in:
parent
088f181c6e
commit
edc38a1230
@ -3,6 +3,6 @@ module.exports = {
|
|||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'jest-environment-jsdom',
|
testEnvironment: 'jest-environment-jsdom',
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^checkout/(.*)': '<rootDir>/src/app/$1',
|
'^checkout/(.*)$': '<rootDir>/src/common/$1',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,7 @@ import { DestinationBankAccountInfo } from './DestinationBankAccountInfo';
|
|||||||
import { DestinationNotification } from './DestinationNotification';
|
import { DestinationNotification } from './DestinationNotification';
|
||||||
import { DestinationQRCodeAccountInfo } from './DestinationQRCodeAccountInfo';
|
import { DestinationQRCodeAccountInfo } from './DestinationQRCodeAccountInfo';
|
||||||
import { InfoItem } from './InfoItem';
|
import { InfoItem } from './InfoItem';
|
||||||
import { formatCardPan, formatPhoneNumber } from './utils';
|
import { formatCardPan, formatPhoneNumber, normalizePhoneNumber, parseNationalNumber } from './utils';
|
||||||
import { SBPIcon } from '../icons/SBPIcon';
|
import { SBPIcon } from '../icons/SBPIcon';
|
||||||
import { getGatewayIcon, mapGatewayName } from '../utils';
|
import { getGatewayIcon, mapGatewayName } from '../utils';
|
||||||
|
|
||||||
@ -58,11 +58,12 @@ export function DestinationInfo({ destination }: DestinationInfoProps) {
|
|||||||
{destination.destinationType === 'BankAccount' && <DestinationBankAccountInfo destination={destination} />}
|
{destination.destinationType === 'BankAccount' && <DestinationBankAccountInfo destination={destination} />}
|
||||||
{(destination.destinationType === 'DestinationSBP' || destination.destinationType === 'SBP') && (
|
{(destination.destinationType === 'DestinationSBP' || destination.destinationType === 'SBP') && (
|
||||||
<InfoItem
|
<InfoItem
|
||||||
|
copyValueFormatter={parseNationalNumber(['AZ'])}
|
||||||
formatter={formatPhoneNumber}
|
formatter={formatPhoneNumber}
|
||||||
icon={currency === 'RUB' ? <SBPIcon /> : null}
|
icon={currency === 'RUB' ? <SBPIcon /> : null}
|
||||||
isCopyable={true}
|
isCopyable={true}
|
||||||
label={l['form.p2p.destination.spb.phone']}
|
label={l['form.p2p.destination.spb.phone']}
|
||||||
value={destination.phoneNumber}
|
value={normalizePhoneNumber(destination.phoneNumber)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{destination?.bankName && (
|
{destination?.bankName && (
|
||||||
|
@ -12,11 +12,13 @@ export type InfoItemProps = {
|
|||||||
isCopyable?: boolean;
|
isCopyable?: boolean;
|
||||||
isDivider?: boolean;
|
isDivider?: boolean;
|
||||||
formatter?: (value: string) => Promise<string>;
|
formatter?: (value: string) => Promise<string>;
|
||||||
|
copyValueFormatter?: (value: string) => Promise<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InfoItem({ label, value, isCopyable, formatter, icon, isDivider }: InfoItemProps) {
|
export function InfoItem({ label, value, isCopyable, formatter, icon, isDivider, copyValueFormatter }: InfoItemProps) {
|
||||||
const { l } = useContext(LocaleContext);
|
const { l } = useContext(LocaleContext);
|
||||||
const { onCopy, hasCopied } = useClipboard(value);
|
const [copiedValue, setCopiedValue] = useState(value);
|
||||||
|
const { onCopy, hasCopied } = useClipboard(copiedValue);
|
||||||
const [displayValue, setDisplayValue] = useState(value);
|
const [displayValue, setDisplayValue] = useState(value);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@ -32,6 +34,11 @@ export function InfoItem({ label, value, isCopyable, formatter, icon, isDivider
|
|||||||
formatter(value).then(setDisplayValue);
|
formatter(value).then(setDisplayValue);
|
||||||
}, [value, formatter]);
|
}, [value, formatter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!copyValueFormatter) return;
|
||||||
|
copyValueFormatter(value).then(setCopiedValue);
|
||||||
|
}, [value, copyValueFormatter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisplayValue(value);
|
setDisplayValue(value);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
import { formatCard } from 'checkout/utils';
|
||||||
|
|
||||||
|
import { formatCardPan } from '../formatCardPan';
|
||||||
|
|
||||||
|
// Mock the dependencies
|
||||||
|
jest.mock('checkout/utils', () => ({
|
||||||
|
extractError: jest.fn((error) => error.message),
|
||||||
|
formatCard: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('formatCardPan', () => {
|
||||||
|
let consoleErrorSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
(formatCard as jest.Mock).mockReset();
|
||||||
|
// Setup console.error spy and silence it
|
||||||
|
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format card pan successfully', async () => {
|
||||||
|
const pan = '4111111111111111';
|
||||||
|
const mockValidationResult = {
|
||||||
|
card: {
|
||||||
|
niceType: 'Visa',
|
||||||
|
type: 'visa',
|
||||||
|
patterns: [4],
|
||||||
|
gaps: [4, 8, 12],
|
||||||
|
lengths: [16, 18, 19],
|
||||||
|
code: {
|
||||||
|
name: 'CVV',
|
||||||
|
size: 3,
|
||||||
|
},
|
||||||
|
matchStrength: 1,
|
||||||
|
},
|
||||||
|
isValid: true,
|
||||||
|
isPotentiallyValid: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock successful dynamic import
|
||||||
|
jest.mock('card-validator', () => ({
|
||||||
|
number: jest.fn().mockReturnValue(mockValidationResult),
|
||||||
|
}));
|
||||||
|
|
||||||
|
(formatCard as jest.Mock).mockReturnValue('4111 1111 1111 1111');
|
||||||
|
|
||||||
|
const result = await formatCardPan(pan);
|
||||||
|
|
||||||
|
expect(result).toBe('4111 1111 1111 1111');
|
||||||
|
expect(formatCard).toHaveBeenCalledWith(pan, mockValidationResult);
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return original pan when card-validator fails to load', async () => {
|
||||||
|
const pan = '4111111111111111';
|
||||||
|
|
||||||
|
// Force the dynamic import to fail
|
||||||
|
jest.mock('card-validator', () => {
|
||||||
|
throw new Error('Failed to load card-validator');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset formatCard mock to ensure it's not returning a value from previous test
|
||||||
|
(formatCard as jest.Mock).mockImplementation(() => pan);
|
||||||
|
|
||||||
|
const result = await formatCardPan(pan);
|
||||||
|
|
||||||
|
expect(result).toBe(pan);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
|
'Error loading the card-validator library Failed to load card-validator',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,51 @@
|
|||||||
|
import { formatPhoneNumber } from '../formatPhoneNumber';
|
||||||
|
|
||||||
|
let mockThrowError = false;
|
||||||
|
|
||||||
|
jest.mock('libphonenumber-js/min', () => ({
|
||||||
|
AsYouType: jest.fn().mockImplementation(() => {
|
||||||
|
if (mockThrowError) {
|
||||||
|
throw new Error('Failed to load library');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
input: jest.fn().mockImplementation((number) => {
|
||||||
|
if (number === '1234567890') {
|
||||||
|
return '(123) 456-7890';
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('formatPhoneNumber', () => {
|
||||||
|
let consoleErrorSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockThrowError = false;
|
||||||
|
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format a valid phone number', async () => {
|
||||||
|
const result = await formatPhoneNumber('1234567890');
|
||||||
|
expect(result).toBe('(123) 456-7890');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return original number when formatting fails', async () => {
|
||||||
|
mockThrowError = true;
|
||||||
|
const originalNumber = '1234567890';
|
||||||
|
const result = await formatPhoneNumber(originalNumber);
|
||||||
|
expect(result).toBe(originalNumber);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith('Format phone number failed: Error: Failed to load library');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty string', async () => {
|
||||||
|
const result = await formatPhoneNumber('');
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,17 @@
|
|||||||
|
import { normalizePhoneNumber } from '../normalizePhoneNumber';
|
||||||
|
|
||||||
|
describe('normalizePhoneNumber', () => {
|
||||||
|
it('should return the same number when it already starts with +', () => {
|
||||||
|
expect(normalizePhoneNumber('+12345678900')).toBe('+12345678900');
|
||||||
|
expect(normalizePhoneNumber('+44123456789')).toBe('+44123456789');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add + prefix when number does not start with +', () => {
|
||||||
|
expect(normalizePhoneNumber('12345678900')).toBe('+12345678900');
|
||||||
|
expect(normalizePhoneNumber('44123456789')).toBe('+44123456789');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty string', () => {
|
||||||
|
expect(normalizePhoneNumber('')).toBe('+');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,57 @@
|
|||||||
|
import { parseNationalNumber } from '../parseNationalNumber';
|
||||||
|
|
||||||
|
// Mock the libphonenumber-js import
|
||||||
|
jest.mock('libphonenumber-js/min', () => ({
|
||||||
|
parsePhoneNumber: jest.fn((number) => {
|
||||||
|
if (number === '+994501234567') {
|
||||||
|
return {
|
||||||
|
country: 'AZ',
|
||||||
|
nationalNumber: '501234567',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (number === '+79123456789') {
|
||||||
|
return {
|
||||||
|
country: 'RU',
|
||||||
|
nationalNumber: '9123456789',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error('Invalid phone number');
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('parseNationalNumber', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
console.error = jest.fn(); // Mock console.error
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return national number for matching country', async () => {
|
||||||
|
const parser = parseNationalNumber(['AZ']);
|
||||||
|
const result = await parser('+994501234567');
|
||||||
|
expect(result).toBe('501234567');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return original number for non-matching country', async () => {
|
||||||
|
const parser = parseNationalNumber(['RU']);
|
||||||
|
const result = await parser('+994501234567');
|
||||||
|
expect(result).toBe('+994501234567');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple countries', async () => {
|
||||||
|
const parser = parseNationalNumber(['RU', 'AZ']);
|
||||||
|
const result1 = await parser('+994501234567');
|
||||||
|
const result2 = await parser('+79123456789');
|
||||||
|
|
||||||
|
expect(result1).toBe('501234567');
|
||||||
|
expect(result2).toBe('9123456789');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return original number on parsing error', async () => {
|
||||||
|
const parser = parseNationalNumber(['AZ']);
|
||||||
|
const invalidNumber = 'invalid';
|
||||||
|
const result = await parser(invalidNumber);
|
||||||
|
|
||||||
|
expect(result).toBe(invalidNumber);
|
||||||
|
expect(console.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -3,9 +3,10 @@ import { extractError } from 'checkout/utils';
|
|||||||
export const formatPhoneNumber = async (phoneNumber: string): Promise<string> => {
|
export const formatPhoneNumber = async (phoneNumber: string): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const lib = await import('libphonenumber-js/min');
|
const lib = await import('libphonenumber-js/min');
|
||||||
return new lib.AsYouType().input(phoneNumber);
|
const instance = new lib.AsYouType();
|
||||||
|
return instance.input(phoneNumber);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error loading the libphonenumber-js library ${extractError(error)}`);
|
console.error(`Format phone number failed: ${extractError(error)}`);
|
||||||
return phoneNumber;
|
return phoneNumber;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
export { formatCardPan } from './formatCardPan';
|
export { formatCardPan } from './formatCardPan';
|
||||||
export { formatPhoneNumber } from './formatPhoneNumber';
|
export { formatPhoneNumber } from './formatPhoneNumber';
|
||||||
|
export { normalizePhoneNumber } from './normalizePhoneNumber';
|
||||||
|
export { parseNationalNumber } from './parseNationalNumber';
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export const normalizePhoneNumber = (phoneNumber: string): string =>
|
||||||
|
phoneNumber.startsWith('+') ? phoneNumber : `+${phoneNumber}`;
|
@ -0,0 +1,17 @@
|
|||||||
|
import { extractError } from 'checkout/utils';
|
||||||
|
|
||||||
|
export const parseNationalNumber =
|
||||||
|
(forCountries: string[]) =>
|
||||||
|
async (phoneNumber: string): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const lib = await import('libphonenumber-js/min');
|
||||||
|
const parsed = lib.parsePhoneNumber(phoneNumber);
|
||||||
|
if (forCountries.includes(parsed.country)) {
|
||||||
|
return parsed.nationalNumber;
|
||||||
|
}
|
||||||
|
return phoneNumber;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Parse national number failed: ${extractError(error)}`);
|
||||||
|
return phoneNumber;
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user