Skip to content

fix: normalize inputAmount by stripping leading zeros after first non… #1118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions widget/embedded/src/components/Slippage/Slippage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { MAX_SLIPPAGE, SLIPPAGES } from '../../constants/swapSettings';
import { useAppStore } from '../../store/AppStore';
import { getContainer } from '../../utils/common';
import { getSlippageValidation } from '../../utils/settings';
import { isValidCurrencyFormat } from '../../utils/validation';

import {
BaseContainer,
Expand Down Expand Up @@ -54,9 +55,8 @@ export function Slippage() {

const onInput = (event: React.FormEvent<HTMLInputElement>) => {
const input = event.target as HTMLInputElement;
const regex = /^(0|[1-9]\d*)(\.\d{1,2})?$/;
const value = input.value;
if (!regex.test(value)) {
if (!isValidCurrencyFormat(value)) {
input.value = value.slice(0, -1);
}
};
Expand Down
2 changes: 2 additions & 0 deletions widget/embedded/src/containers/Inputs/Inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function Inputs(props: PropTypes) {
toToken,
toBlockchain,
setInputAmount,
sanitizeInputAmount,
inputAmount,
inputUsdValue,
outputAmount,
Expand Down Expand Up @@ -79,6 +80,7 @@ export function Inputs(props: PropTypes) {
label={i18n.t('From')}
mode="From"
onInputChange={setInputAmount}
onInputBlur={sanitizeInputAmount}
balance={fromTokenFormattedBalance}
chain={{
displayName: fromBlockchain?.displayName || '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export function useSyncUrlAndStore() {
useEffect(() => {
const { autoConnect, clientUrl, utmQueryParams, blockchain } =
getUrlSearchParams();

if (isInRouterContext && fetchMetaStatus === 'success') {
updateUrlSearchParams({
[SearchParams.FROM_BLOCKCHAIN]: fromBlockchain?.name,
Expand All @@ -107,6 +108,7 @@ export function useSyncUrlAndStore() {
toBlockchain,
toToken,
campaignMode,
fetchMetaStatus,
]);

useEffect(() => {
Expand Down
8 changes: 5 additions & 3 deletions widget/embedded/src/pages/LiquiditySourcePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '../components/SettingsContainer';
import { useAppStore } from '../store/AppStore';
import { containsText } from '../utils/numbers';
import { replaceSpacesWithDash } from '../utils/sanitizers';
import { getUniqueSwappersGroups } from '../utils/settings';

interface PropTypes {
Expand Down Expand Up @@ -70,9 +71,10 @@ export function LiquiditySourcePage({ sourceType }: PropTypes) {
const list = liquiditySources.map((sourceItem) => {
const { selected, groupTitle, logo, id, ...restSourceItem } = sourceItem;
return {
id: `widget-setting-liquidity-source-${id
.toLowerCase()
.replace(/\s+/g, '-')}-item-btn`,
id: `widget-setting-liquidity-source-${replaceSpacesWithDash(
id.toLowerCase()
)}-item-btn`,

start: <Image src={logo} size={22} type="circular" />,
onClick: () => {
if (!campaignMode) {
Expand Down
27 changes: 24 additions & 3 deletions widget/embedded/src/store/quote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ import {
WidgetEvents,
} from '../types';
import { isPositiveNumber } from '../utils/numbers';
import {
ensureLeadingZeroForDecimal,
removeLeadingZeros,
} from '../utils/sanitizers';
import { getUsdInputFrom, getUsdOutputFrom } from '../utils/swap';
import { isZeroValue } from '../utils/validation';

import createSelectors from './selectors';

Expand Down Expand Up @@ -87,6 +92,7 @@ export interface QuoteState {
value: SomeQuoteState[K]
) => void;
setInputAmount: (amount: string) => void;
sanitizeInputAmount: (amount: string) => void;
setSelectedQuote: (quote: SelectedQuote | null) => void;
retry: (retryQuote: RetryQuote) => void;
switchFromAndTo: () => void;
Expand Down Expand Up @@ -223,16 +229,31 @@ export const useQuoteStore = createSelectors(
}),
}));
},
sanitizeInputAmount: (amount) => {
let sanitized = amount;
if (isZeroValue(sanitized)) {
sanitized = '0';
}
set(() => ({
inputAmount: sanitized,
}));
},
setInputAmount: (amount) => {
let sanitized = amount;
if (!isZeroValue(amount)) {
// sanitize once a meaningful digit is entered (e.g. "00001" β†’ "1")
sanitized = removeLeadingZeros(sanitized);
sanitized = ensureLeadingZeroForDecimal(sanitized);
}
set((state) => ({
inputAmount: amount,
...(!amount && {
inputAmount: sanitized,
...(!sanitized && {
outputAmount: null,
outputUsdValue: new BigNumber(0),
selectedQuote: null,
}),
...(!!state.fromToken && {
inputUsdValue: getUsdValue(state.fromToken, amount),
inputUsdValue: getUsdValue(state.fromToken, sanitized),
}),
}));
},
Expand Down
13 changes: 3 additions & 10 deletions widget/embedded/src/utils/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Types
import type { ThemeColors, WidgetColors, WidgetColorsKeys } from '../types';

import { isColorKeyOverridden } from './validation';

type RGB = {
red: number;
green: number;
Expand Down Expand Up @@ -40,15 +42,6 @@ function expandShortHexColor(hexColor: string) {
return `#${hexColor}`;
}

/*
* We letting users to override some specific colors (e.g. `primary550`, `secondary100`).
* So we are generating a range of colors if `primary` (or other keys) has passed but if user is passing a specific color,
* we will override the user color to generated range.
*/
function isOverridingColor(colorKey: string): boolean {
return /[0-9]+$/.test(colorKey);
}

// pad a hexadecimal string with zeros if it needs it
function pad(number: string, length: number) {
return number.padStart(length, '0');
Expand Down Expand Up @@ -171,7 +164,7 @@ export function expandToGenerateThemeColors(
*/
const isSingleColor = ['background', 'foreground'].includes(colorKey);

if (!isSingleColor && !isOverridingColor(colorKey)) {
if (!isSingleColor && !isColorKeyOverridden(colorKey)) {
const expandedHexColor = expandShortHexColor(expandColor);
Object.assign(
output,
Expand Down
86 changes: 86 additions & 0 deletions widget/embedded/src/utils/sanitizers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { faker } from '@faker-js/faker';
import { describe, expect, test } from 'vitest';

import {
ensureLeadingZeroForDecimal,
formatThousandsWithCommas,
removeLeadingZeros,
replaceSpacesWithDash,
} from './sanitizers';

const WORD_COUNT = 5;
const FAKER_SEED = 12;
faker.seed(FAKER_SEED);

describe('check sanitization behaviors', () => {
describe('check leading zero removal', () => {
test('should remove zeros at start before digits', () => {
expect(removeLeadingZeros('000123')).toBe('123');
expect(removeLeadingZeros('00123')).toBe('123');
});

test('should preserve lone zero or non-digit prefixes', () => {
expect(removeLeadingZeros('0')).toBe('0');
expect(removeLeadingZeros('0000')).toBe('0');
expect(removeLeadingZeros('00.1')).toBe('0.1');
expect(removeLeadingZeros('00a')).toBe('0a');
});

test('should leave strings with no leading zeros unchanged', () => {
const val = faker.number.int({ min: 1, max: 999 }).toString();
expect(removeLeadingZeros(val)).toBe(val);
});
});

describe('check decimal leading zero insertion', () => {
test('should add a zero before lone decimal points', () => {
expect(ensureLeadingZeroForDecimal('.5')).toBe('0.5');
expect(ensureLeadingZeroForDecimal('.000')).toBe('0.000');
expect(ensureLeadingZeroForDecimal('0.001')).toBe('0.001');
expect(ensureLeadingZeroForDecimal('000.000')).toBe('000.000');
});

test('should not alter other inputs', () => {
expect(ensureLeadingZeroForDecimal('0.5')).toBe('0.5');
expect(ensureLeadingZeroForDecimal('2.')).toBe('2.');
expect(ensureLeadingZeroForDecimal('')).toBe('');
});
});

describe('check thousand separator formatting', () => {
test('should insert commas every three digits for 4–6 digit numbers', () => {
expect(formatThousandsWithCommas('1000')).toBe('1,000');
expect(formatThousandsWithCommas('12345')).toBe('12,345');
expect(formatThousandsWithCommas('123456')).toBe('123,456');
});

test('should handle large numbers correctly', () => {
expect(formatThousandsWithCommas('1234567')).toBe('1,234,567');
expect(formatThousandsWithCommas('1234567890')).toBe('1,234,567,890');
});

test('should not add commas for numbers below 1000', () => {
expect(formatThousandsWithCommas('0')).toBe('0');
expect(formatThousandsWithCommas('999')).toBe('999');
});
});

describe('check space-to-dash replacement', () => {
test('should convert any spaces to a single dash', () => {
const words = faker.lorem.words(WORD_COUNT).split(' ');
const spaced = words.join(' ');
expect(replaceSpacesWithDash(spaced)).toBe(words.join('-'));
});

test('should convert tabs and newlines into dashes', () => {
const words = faker.lorem.words(WORD_COUNT).split(' ');
expect(replaceSpacesWithDash(words.join('\t'))).toBe(words.join('-'));
expect(replaceSpacesWithDash(words.join('\n'))).toBe(words.join('-'));
});

test('should handle empty or single-word strings', () => {
expect(replaceSpacesWithDash('')).toBe('');
expect(replaceSpacesWithDash('nospace')).toBe('nospace');
});
});
});
31 changes: 31 additions & 0 deletions widget/embedded/src/utils/sanitizers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Remove leading zeros when followed by another digit.
* @example "000123" β†’ "123"
*/
export function removeLeadingZeros(input: string): string {
return input.replace(/^0+(?=\d)/g, '');
}

/**
* Ensure a leading zero before a decimal point.
* @example ".45" β†’ "0.45"
*/
export function ensureLeadingZeroForDecimal(input: string): string {
return input.replace(/^\.(\d+)/, '0.$1');
}

/**
* Insert commas as thousand separators.
* @example "1234567" β†’ "1,234,567"
*/
export function formatThousandsWithCommas(input: string): string {
return input.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

/**
* Replace spaces (one or more) with a single dash.
* @example "a b c" β†’ "a-b-c"
*/
export function replaceSpacesWithDash(input: string): string {
return input.replace(/\s+/g, '-');
}
Loading