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',
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
moduleNameMapper: {
|
||||
'^checkout/(.*)': '<rootDir>/src/app/$1',
|
||||
'^checkout/(.*)$': '<rootDir>/src/common/$1',
|
||||
},
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ import { DestinationBankAccountInfo } from './DestinationBankAccountInfo';
|
||||
import { DestinationNotification } from './DestinationNotification';
|
||||
import { DestinationQRCodeAccountInfo } from './DestinationQRCodeAccountInfo';
|
||||
import { InfoItem } from './InfoItem';
|
||||
import { formatCardPan, formatPhoneNumber } from './utils';
|
||||
import { formatCardPan, formatPhoneNumber, normalizePhoneNumber, parseNationalNumber } from './utils';
|
||||
import { SBPIcon } from '../icons/SBPIcon';
|
||||
import { getGatewayIcon, mapGatewayName } from '../utils';
|
||||
|
||||
@ -58,11 +58,12 @@ export function DestinationInfo({ destination }: DestinationInfoProps) {
|
||||
{destination.destinationType === 'BankAccount' && <DestinationBankAccountInfo destination={destination} />}
|
||||
{(destination.destinationType === 'DestinationSBP' || destination.destinationType === 'SBP') && (
|
||||
<InfoItem
|
||||
copyValueFormatter={parseNationalNumber(['AZ'])}
|
||||
formatter={formatPhoneNumber}
|
||||
icon={currency === 'RUB' ? <SBPIcon /> : null}
|
||||
isCopyable={true}
|
||||
label={l['form.p2p.destination.spb.phone']}
|
||||
value={destination.phoneNumber}
|
||||
value={normalizePhoneNumber(destination.phoneNumber)}
|
||||
/>
|
||||
)}
|
||||
{destination?.bankName && (
|
||||
|
@ -12,11 +12,13 @@ export type InfoItemProps = {
|
||||
isCopyable?: boolean;
|
||||
isDivider?: boolean;
|
||||
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 { onCopy, hasCopied } = useClipboard(value);
|
||||
const [copiedValue, setCopiedValue] = useState(value);
|
||||
const { onCopy, hasCopied } = useClipboard(copiedValue);
|
||||
const [displayValue, setDisplayValue] = useState(value);
|
||||
const toast = useToast();
|
||||
|
||||
@ -32,6 +34,11 @@ export function InfoItem({ label, value, isCopyable, formatter, icon, isDivider
|
||||
formatter(value).then(setDisplayValue);
|
||||
}, [value, formatter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!copyValueFormatter) return;
|
||||
copyValueFormatter(value).then(setCopiedValue);
|
||||
}, [value, copyValueFormatter]);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayValue(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> => {
|
||||
try {
|
||||
const lib = await import('libphonenumber-js/min');
|
||||
return new lib.AsYouType().input(phoneNumber);
|
||||
const instance = new lib.AsYouType();
|
||||
return instance.input(phoneNumber);
|
||||
} catch (error) {
|
||||
console.error(`Error loading the libphonenumber-js library ${extractError(error)}`);
|
||||
console.error(`Format phone number failed: ${extractError(error)}`);
|
||||
return phoneNumber;
|
||||
}
|
||||
};
|
||||
|
@ -1,2 +1,4 @@
|
||||
export { formatCardPan } from './formatCardPan';
|
||||
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