@rango-dev/widget-embedded 0.42.3-next.3 → 0.42.3-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/components/Slippage/Slippage.d.ts.map +1 -1
  2. package/dist/containers/Inputs/Inputs.d.ts.map +1 -1
  3. package/dist/hooks/useSyncUrlAndStore/useSyncUrlAndStore.d.ts.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +4 -4
  6. package/dist/pages/LiquiditySourcePage.d.ts.map +1 -1
  7. package/dist/store/quote.d.ts +2 -0
  8. package/dist/store/quote.d.ts.map +1 -1
  9. package/dist/utils/colors.d.ts.map +1 -1
  10. package/dist/utils/numbers.d.ts +1 -0
  11. package/dist/utils/numbers.d.ts.map +1 -1
  12. package/dist/utils/sanitizers.d.ts +27 -0
  13. package/dist/utils/sanitizers.d.ts.map +1 -0
  14. package/dist/utils/sanitizers.test.d.ts +2 -0
  15. package/dist/utils/sanitizers.test.d.ts.map +1 -0
  16. package/dist/utils/validation.d.ts +26 -0
  17. package/dist/utils/validation.d.ts.map +1 -0
  18. package/dist/utils/validation.test.d.ts +2 -0
  19. package/dist/utils/validation.test.d.ts.map +1 -0
  20. package/dist/utils/wallets.d.ts.map +1 -1
  21. package/dist/widget-embedded.build.json +1 -1
  22. package/package.json +3 -3
  23. package/src/components/Slippage/Slippage.tsx +2 -2
  24. package/src/containers/Inputs/Inputs.tsx +2 -0
  25. package/src/hooks/useSyncUrlAndStore/useSyncUrlAndStore.ts +2 -0
  26. package/src/pages/LiquiditySourcePage.tsx +5 -3
  27. package/src/store/quote.ts +23 -4
  28. package/src/utils/colors.ts +3 -10
  29. package/src/utils/numbers.ts +11 -0
  30. package/src/utils/sanitizers.test.ts +122 -0
  31. package/src/utils/sanitizers.ts +41 -0
  32. package/src/utils/validation.test.ts +121 -0
  33. package/src/utils/validation.ts +45 -0
  34. package/src/utils/wallets.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rango-dev/widget-embedded",
3
- "version": "0.42.3-next.3",
3
+ "version": "0.42.3-next.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "source": "./src/index.ts",
@@ -30,7 +30,7 @@
30
30
  "@rango-dev/queue-manager-rango-preset": "^0.45.2-next.4",
31
31
  "@rango-dev/queue-manager-react": "^0.29.0",
32
32
  "@rango-dev/signer-solana": "^0.39.1-next.1",
33
- "@rango-dev/ui": "^0.46.2-next.5",
33
+ "@rango-dev/ui": "^0.46.2-next.6",
34
34
  "@rango-dev/wallets-core": "^0.43.1-next.3",
35
35
  "@rango-dev/wallets-react": "^0.30.2-next.4",
36
36
  "@rango-dev/wallets-shared": "^0.44.2-next.4",
@@ -54,4 +54,4 @@
54
54
  "publishConfig": {
55
55
  "access": "public"
56
56
  }
57
- }
57
+ }
@@ -14,6 +14,7 @@ import { MAX_SLIPPAGE, SLIPPAGES } from '../../constants/swapSettings';
14
14
  import { useAppStore } from '../../store/AppStore';
15
15
  import { getContainer } from '../../utils/common';
16
16
  import { getSlippageValidation } from '../../utils/settings';
17
+ import { isValidCurrencyFormat } from '../../utils/validation';
17
18
 
18
19
  import {
19
20
  BaseContainer,
@@ -55,9 +56,8 @@ export function Slippage() {
55
56
 
56
57
  const onInput = (event: React.FormEvent<HTMLInputElement>) => {
57
58
  const input = event.target as HTMLInputElement;
58
- const regex = /^(0|[1-9]\d*)(\.\d{1,2})?$/;
59
59
  const value = input.value;
60
- if (!regex.test(value)) {
60
+ if (!isValidCurrencyFormat(value)) {
61
61
  input.value = value.slice(0, -1);
62
62
  }
63
63
  };
@@ -34,6 +34,7 @@ export function Inputs(props: PropTypes) {
34
34
  toToken,
35
35
  toBlockchain,
36
36
  setInputAmount,
37
+ sanitizeInputAmount,
37
38
  inputAmount,
38
39
  inputUsdValue,
39
40
  outputAmount,
@@ -79,6 +80,7 @@ export function Inputs(props: PropTypes) {
79
80
  label={i18n.t('From')}
80
81
  mode="From"
81
82
  onInputChange={setInputAmount}
83
+ onInputBlur={sanitizeInputAmount}
82
84
  balance={fromTokenFormattedBalance}
83
85
  chain={{
84
86
  displayName: fromBlockchain?.displayName || '',
@@ -83,6 +83,7 @@ export function useSyncUrlAndStore() {
83
83
  useEffect(() => {
84
84
  const { autoConnect, clientUrl, utmQueryParams, blockchain } =
85
85
  getUrlSearchParams();
86
+
86
87
  if (isInRouterContext && fetchMetaStatus === 'success') {
87
88
  updateUrlSearchParams({
88
89
  [SearchParams.FROM_BLOCKCHAIN]: fromBlockchain?.name,
@@ -107,6 +108,7 @@ export function useSyncUrlAndStore() {
107
108
  toBlockchain,
108
109
  toToken,
109
110
  campaignMode,
111
+ fetchMetaStatus,
110
112
  ]);
111
113
 
112
114
  useEffect(() => {
@@ -22,6 +22,7 @@ import {
22
22
  } from '../components/SettingsContainer';
23
23
  import { useAppStore } from '../store/AppStore';
24
24
  import { containsText } from '../utils/numbers';
25
+ import { replaceSpacesWithDash } from '../utils/sanitizers';
25
26
  import { getUniqueSwappersGroups } from '../utils/settings';
26
27
 
27
28
  interface PropTypes {
@@ -70,9 +71,10 @@ export function LiquiditySourcePage({ sourceType }: PropTypes) {
70
71
  const list = liquiditySources.map((sourceItem) => {
71
72
  const { selected, groupTitle, logo, id, ...restSourceItem } = sourceItem;
72
73
  return {
73
- id: `widget-setting-liquidity-source-${id
74
- .toLowerCase()
75
- .replace(/\s+/g, '-')}-item-btn`,
74
+ id: `widget-setting-liquidity-source-${replaceSpacesWithDash(
75
+ id.toLowerCase()
76
+ )}-item-btn`,
77
+
76
78
  start: <Image src={logo} size={22} type="circular" />,
77
79
  onClick: () => {
78
80
  if (!campaignMode) {
@@ -21,8 +21,13 @@ import {
21
21
  type Wallet,
22
22
  WidgetEvents,
23
23
  } from '../types';
24
- import { isPositiveNumber } from '../utils/numbers';
24
+ import { isPositiveNumber, sanitizeInputAmount } from '../utils/numbers';
25
+ import {
26
+ ensureLeadingZeroForDecimal,
27
+ removeLeadingZeros,
28
+ } from '../utils/sanitizers';
25
29
  import { getUsdInputFrom, getUsdOutputFrom } from '../utils/swap';
30
+ import { isZeroValue } from '../utils/validation';
26
31
 
27
32
  import createSelectors from './selectors';
28
33
 
@@ -87,6 +92,7 @@ export interface QuoteState {
87
92
  value: SomeQuoteState[K]
88
93
  ) => void;
89
94
  setInputAmount: (amount: string) => void;
95
+ sanitizeInputAmount: (amount: string) => void;
90
96
  setSelectedQuote: (quote: SelectedQuote | null) => void;
91
97
  retry: (retryQuote: RetryQuote) => void;
92
98
  switchFromAndTo: () => void;
@@ -223,16 +229,29 @@ export const useQuoteStore = createSelectors(
223
229
  }),
224
230
  }));
225
231
  },
232
+ sanitizeInputAmount: (amount) => {
233
+ const sanitized = sanitizeInputAmount(amount);
234
+
235
+ set(() => ({
236
+ inputAmount: sanitized,
237
+ }));
238
+ },
226
239
  setInputAmount: (amount) => {
240
+ let sanitized = amount;
241
+ if (!isZeroValue(amount)) {
242
+ // sanitize once a meaningful digit is entered (e.g. "00001" → "1")
243
+ sanitized = removeLeadingZeros(sanitized);
244
+ sanitized = ensureLeadingZeroForDecimal(sanitized);
245
+ }
227
246
  set((state) => ({
228
- inputAmount: amount,
229
- ...(!amount && {
247
+ inputAmount: sanitized,
248
+ ...(!sanitized && {
230
249
  outputAmount: null,
231
250
  outputUsdValue: new BigNumber(0),
232
251
  selectedQuote: null,
233
252
  }),
234
253
  ...(!!state.fromToken && {
235
- inputUsdValue: getUsdValue(state.fromToken, amount),
254
+ inputUsdValue: getUsdValue(state.fromToken, sanitized),
236
255
  }),
237
256
  }));
238
257
  },
@@ -2,6 +2,8 @@
2
2
  // Types
3
3
  import type { ThemeColors, WidgetColors, WidgetColorsKeys } from '../types';
4
4
 
5
+ import { isColorKeyOverridden } from './validation';
6
+
5
7
  type RGB = {
6
8
  red: number;
7
9
  green: number;
@@ -40,15 +42,6 @@ function expandShortHexColor(hexColor: string) {
40
42
  return `#${hexColor}`;
41
43
  }
42
44
 
43
- /*
44
- * We letting users to override some specific colors (e.g. `primary550`, `secondary100`).
45
- * So we are generating a range of colors if `primary` (or other keys) has passed but if user is passing a specific color,
46
- * we will override the user color to generated range.
47
- */
48
- function isOverridingColor(colorKey: string): boolean {
49
- return /[0-9]+$/.test(colorKey);
50
- }
51
-
52
45
  // pad a hexadecimal string with zeros if it needs it
53
46
  function pad(number: string, length: number) {
54
47
  return number.padStart(length, '0');
@@ -171,7 +164,7 @@ export function expandToGenerateThemeColors(
171
164
  */
172
165
  const isSingleColor = ['background', 'foreground'].includes(colorKey);
173
166
 
174
- if (!isSingleColor && !isOverridingColor(colorKey)) {
167
+ if (!isSingleColor && !isColorKeyOverridden(colorKey)) {
175
168
  const expandedHexColor = expandShortHexColor(expandColor);
176
169
  Object.assign(
177
170
  output,
@@ -4,6 +4,9 @@ import type { BestRouteResponse } from 'rango-sdk';
4
4
 
5
5
  import { BigNumber } from 'bignumber.js';
6
6
 
7
+ import { stripTrailingZeros } from './sanitizers';
8
+ import { isZeroValue } from './validation';
9
+
7
10
  /*
8
11
  * if time > 1h -> rounded with 5 minutes precision
9
12
  * if time < 1h -> rounded with 15 seconds precision
@@ -125,3 +128,11 @@ export const containsText = (text: string, searchText: string) =>
125
128
 
126
129
  export const isPositiveNumber = (text?: string) =>
127
130
  !!text && parseFloat(text) > 0;
131
+
132
+ export function sanitizeInputAmount(amount: string): string {
133
+ if (isZeroValue(amount)) {
134
+ return '0';
135
+ }
136
+
137
+ return stripTrailingZeros(amount);
138
+ }
@@ -0,0 +1,122 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { describe, expect, test } from 'vitest';
3
+
4
+ import {
5
+ ensureLeadingZeroForDecimal,
6
+ formatThousandsWithCommas,
7
+ removeLeadingZeros,
8
+ replaceSpacesWithDash,
9
+ stripTrailingZeros,
10
+ } from './sanitizers';
11
+
12
+ const WORD_COUNT = 5;
13
+ const FAKER_SEED = 12;
14
+ const LONG_NUMBER_LENGTH = 50;
15
+ faker.seed(FAKER_SEED);
16
+
17
+ describe('check sanitization behaviors', () => {
18
+ describe('check leading zero removal', () => {
19
+ test('should remove zeros at start before digits', () => {
20
+ expect(removeLeadingZeros('000123')).toBe('123');
21
+ expect(removeLeadingZeros('00123')).toBe('123');
22
+ });
23
+
24
+ test('should preserve lone zero or non-digit prefixes', () => {
25
+ expect(removeLeadingZeros('0')).toBe('0');
26
+ expect(removeLeadingZeros('0000')).toBe('0');
27
+ expect(removeLeadingZeros('00.1')).toBe('0.1');
28
+ expect(removeLeadingZeros('00a')).toBe('0a');
29
+ });
30
+
31
+ test('should leave strings with no leading zeros unchanged', () => {
32
+ const val = faker.number.int({ min: 1, max: 999 }).toString();
33
+ expect(removeLeadingZeros(val)).toBe(val);
34
+ });
35
+ });
36
+
37
+ describe('check decimal leading zero insertion', () => {
38
+ test('should add a zero before lone decimal points', () => {
39
+ expect(ensureLeadingZeroForDecimal('.5')).toBe('0.5');
40
+ expect(ensureLeadingZeroForDecimal('.000')).toBe('0.000');
41
+ expect(ensureLeadingZeroForDecimal('0.001')).toBe('0.001');
42
+ expect(ensureLeadingZeroForDecimal('000.000')).toBe('000.000');
43
+ });
44
+
45
+ test('should not alter other inputs', () => {
46
+ expect(ensureLeadingZeroForDecimal('0.5')).toBe('0.5');
47
+ expect(ensureLeadingZeroForDecimal('2.')).toBe('2.');
48
+ expect(ensureLeadingZeroForDecimal('')).toBe('');
49
+ });
50
+ });
51
+
52
+ describe('check thousand separator formatting', () => {
53
+ test('should insert commas every three digits for 4–6 digit numbers', () => {
54
+ expect(formatThousandsWithCommas('1000')).toBe('1,000');
55
+ expect(formatThousandsWithCommas('12345')).toBe('12,345');
56
+ expect(formatThousandsWithCommas('123456')).toBe('123,456');
57
+ });
58
+
59
+ test('should handle large numbers correctly', () => {
60
+ expect(formatThousandsWithCommas('1234567')).toBe('1,234,567');
61
+ expect(formatThousandsWithCommas('1234567890')).toBe('1,234,567,890');
62
+ });
63
+
64
+ test('should not add commas for numbers below 1000', () => {
65
+ expect(formatThousandsWithCommas('0')).toBe('0');
66
+ expect(formatThousandsWithCommas('999')).toBe('999');
67
+ });
68
+ });
69
+
70
+ describe('check space-to-dash replacement', () => {
71
+ test('should convert any spaces to a single dash', () => {
72
+ const words = faker.lorem.words(WORD_COUNT).split(' ');
73
+ const spaced = words.join(' ');
74
+ expect(replaceSpacesWithDash(spaced)).toBe(words.join('-'));
75
+ });
76
+
77
+ test('should convert tabs and newlines into dashes', () => {
78
+ const words = faker.lorem.words(WORD_COUNT).split(' ');
79
+ expect(replaceSpacesWithDash(words.join('\t'))).toBe(words.join('-'));
80
+ expect(replaceSpacesWithDash(words.join('\n'))).toBe(words.join('-'));
81
+ });
82
+
83
+ test('should handle empty or single-word strings', () => {
84
+ expect(replaceSpacesWithDash('')).toBe('');
85
+ expect(replaceSpacesWithDash('nospace')).toBe('nospace');
86
+ });
87
+ });
88
+
89
+ describe('check trailing zero stripping', () => {
90
+ test('should trim only zeros after last non-zero digit', () => {
91
+ expect(stripTrailingZeros('0.0010000')).toBe('0.001');
92
+ expect(stripTrailingZeros('123.45000')).toBe('123.45');
93
+ expect(stripTrailingZeros('5.1000')).toBe('5.1');
94
+ });
95
+
96
+ test('should remove entire fractional part if all zeros', () => {
97
+ expect(stripTrailingZeros('10.000')).toBe('10');
98
+ expect(stripTrailingZeros('0.000')).toBe('0');
99
+ });
100
+
101
+ test('should leave inputs without trailing zeros untouched', () => {
102
+ expect(stripTrailingZeros('42')).toBe('42');
103
+ expect(stripTrailingZeros('7.89')).toBe('7.89');
104
+ expect(stripTrailingZeros('0.123')).toBe('0.123');
105
+ });
106
+
107
+ test('edge: very long fractional zeros', () => {
108
+ const long =
109
+ '0.' +
110
+ '0'.repeat(LONG_NUMBER_LENGTH) +
111
+ '1' +
112
+ '0'.repeat(LONG_NUMBER_LENGTH);
113
+ expect(stripTrailingZeros(long)).toBe(
114
+ '0.' + '0'.repeat(LONG_NUMBER_LENGTH) + '1'
115
+ );
116
+ });
117
+
118
+ test('edge: no decimal point', () => {
119
+ expect(stripTrailingZeros('1000')).toBe('1000');
120
+ });
121
+ });
122
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Remove leading zeros when followed by another digit.
3
+ * @example "000123" → "123"
4
+ */
5
+ export function removeLeadingZeros(input: string): string {
6
+ return input.replace(/^0+(?=\d)/g, '');
7
+ }
8
+
9
+ /**
10
+ * Ensure a leading zero before a decimal point.
11
+ * @example ".45" → "0.45"
12
+ */
13
+ export function ensureLeadingZeroForDecimal(input: string): string {
14
+ return input.replace(/^\.(\d+)/, '0.$1');
15
+ }
16
+
17
+ /**
18
+ * Insert commas as thousand separators.
19
+ * @example "1234567" → "1,234,567"
20
+ */
21
+ export function formatThousandsWithCommas(input: string): string {
22
+ return input.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
23
+ }
24
+
25
+ /**
26
+ * Replace spaces (one or more) with a single dash.
27
+ * @example "a b c" → "a-b-c"
28
+ */
29
+ export function replaceSpacesWithDash(input: string): string {
30
+ return input.replace(/\s+/g, '-');
31
+ }
32
+
33
+ /**
34
+ * Strip any trailing zeros in the fractional part, and remove a dangling decimal point.
35
+ * @example "0.0010000" → "0.001"
36
+ * @example "10.000" → "10"
37
+ */
38
+ export function stripTrailingZeros(input: string): string {
39
+ const s = input.replace(/(\.\d*?[1-9])0+$/, '$1');
40
+ return s.replace(/\.0+$/, '');
41
+ }
@@ -0,0 +1,121 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { describe, expect, test } from 'vitest';
3
+
4
+ import {
5
+ isColorKeyOverridden,
6
+ isNumeric,
7
+ isValidCurrencyFormat,
8
+ isZeroValue,
9
+ } from './validation';
10
+
11
+ const FAKER_SEED = 14;
12
+ const THREE_MAX_DECIMAL = 3;
13
+ const FOUR_MAX_DECIMAL = 4;
14
+ const SMALL_INT = { min: 1, max: 9 };
15
+ const FLOAT_OPTS = { min: 0, max: 100, precision: 0.01 };
16
+ faker.seed(FAKER_SEED);
17
+
18
+ describe('check validation behaviors', () => {
19
+ describe('check zero-value detection', () => {
20
+ test('should detect strings of zeros', () => {
21
+ expect(isZeroValue('0')).toBe(true);
22
+ expect(isZeroValue('000')).toBe(true);
23
+ expect(isZeroValue('000.00')).toBe(true);
24
+ });
25
+ test('should reject non-zero or fractional values', () => {
26
+ expect(isZeroValue('0.0001')).toBe(false);
27
+ expect(isZeroValue('000.0001')).toBe(false);
28
+ expect(isZeroValue(faker.number.int(SMALL_INT).toString())).toBe(false);
29
+ });
30
+ test('should return false for empty or non-numeric', () => {
31
+ expect(isZeroValue('')).toBe(false);
32
+ expect(isZeroValue('abc')).toBe(false);
33
+ });
34
+ });
35
+
36
+ describe('check currency format validation', () => {
37
+ // Default behavior (maxDecimals = 2)
38
+ test('should accepts integers and up to 2 decimals', () => {
39
+ expect(isValidCurrencyFormat('0')).toBe(true);
40
+ expect(isValidCurrencyFormat('10.2')).toBe(true);
41
+ expect(isValidCurrencyFormat('10.25')).toBe(true);
42
+ });
43
+
44
+ test('should rejects more than 2 decimals', () => {
45
+ expect(isValidCurrencyFormat('1.234')).toBe(false);
46
+ expect(isValidCurrencyFormat('0.001')).toBe(false);
47
+ });
48
+
49
+ test('should rejects leading zero on non-zero integer', () => {
50
+ expect(isValidCurrencyFormat('01')).toBe(false);
51
+ expect(isValidCurrencyFormat('00.5')).toBe(false);
52
+ });
53
+
54
+ test('should rejects non-numeric and malformed strings', () => {
55
+ expect(isValidCurrencyFormat('')).toBe(false);
56
+ expect(isValidCurrencyFormat('.5')).toBe(false);
57
+ expect(isValidCurrencyFormat('1.2.3')).toBe(false);
58
+ expect(isValidCurrencyFormat('abc')).toBe(false);
59
+ expect(isValidCurrencyFormat('0.1a')).toBe(false);
60
+ });
61
+
62
+ test('should allows up to specified decimals', () => {
63
+ expect(isValidCurrencyFormat('1.234', THREE_MAX_DECIMAL)).toBe(true);
64
+ expect(isValidCurrencyFormat('0.1234', FOUR_MAX_DECIMAL)).toBe(true);
65
+ });
66
+
67
+ test('should rejects if decimals exceed custom limit', () => {
68
+ expect(isValidCurrencyFormat('2.1234', THREE_MAX_DECIMAL)).toBe(false);
69
+ expect(isValidCurrencyFormat('0.12345', FOUR_MAX_DECIMAL)).toBe(false);
70
+ });
71
+
72
+ test('should rejects trailing dot', () => {
73
+ expect(isValidCurrencyFormat('10.')).toBe(false);
74
+ });
75
+
76
+ test('should handles zero with custom decimals edge', () => {
77
+ expect(isValidCurrencyFormat('0.0', 1)).toBe(true);
78
+ expect(isValidCurrencyFormat('0.00', 1)).toBe(false);
79
+ });
80
+
81
+ test('check large values within limits', () => {
82
+ expect(isValidCurrencyFormat('1234567890.12')).toBe(true);
83
+ expect(isValidCurrencyFormat('1234567890.123')).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe('check numeric string validation', () => {
88
+ test('should accept integers and decimals', () => {
89
+ expect(isNumeric('0')).toBe(true);
90
+ expect(isNumeric('00.1')).toBe(true);
91
+ expect(isNumeric('123.456')).toBe(true);
92
+ expect(isNumeric(faker.number.float(FLOAT_OPTS).toString())).toBe(true);
93
+ });
94
+ test('should reject leading dot or letters', () => {
95
+ expect(isNumeric('.5')).toBe(false);
96
+ expect(isNumeric('abc')).toBe(false);
97
+ expect(isNumeric('1.2.3')).toBe(false);
98
+ });
99
+ test('should reject empty string', () => {
100
+ expect(isNumeric('')).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe('check color key override detection', () => {
105
+ test('should detect numeric suffix keys', () => {
106
+ expect(isColorKeyOverridden('primary550')).toBe(true);
107
+ expect(isColorKeyOverridden('secondary100')).toBe(true);
108
+ expect(
109
+ isColorKeyOverridden(
110
+ faker.word.sample() +
111
+ faker.number.int({ min: 1, max: 999 }).toString()
112
+ )
113
+ ).toBe(true);
114
+ });
115
+ test('should reject keys without digits', () => {
116
+ expect(isColorKeyOverridden('primary')).toBe(false);
117
+ expect(isColorKeyOverridden('colorKey')).toBe(false);
118
+ expect(isColorKeyOverridden('')).toBe(false);
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Check if a string is composed only of zeros, optionally with decimal zeros.
3
+ * @param input - string to test, e.g. "0", "000.00"
4
+ * @returns true when input represents zero, otherwise false
5
+ */
6
+ export function isZeroValue(input: string) {
7
+ const zeroPattern = /^0+(?:\.0+)?$/;
8
+ return zeroPattern.test(input);
9
+ }
10
+
11
+ /**
12
+ * Validate currency-style input: up to `maxDecimals` places after the decimal point.
13
+ * @param input - string to test, e.g. "0", "10.25"
14
+ * @param maxDecimals - maximum digits allowed after the decimal (default: 2)
15
+ * @returns true for valid money formats, false otherwise
16
+ */
17
+ export function isValidCurrencyFormat(
18
+ input: string,
19
+ maxDecimals: number = 2
20
+ ): boolean {
21
+ // construct pattern like ^(?:0|[1-9]\d*)(?:\.\d{1,2})?$
22
+ const pattern = `^(?:0|[1-9]\\d*)(?:\\.\\d{1,${maxDecimals}})?$`;
23
+ const regex = new RegExp(pattern);
24
+ return regex.test(input);
25
+ }
26
+
27
+ /**
28
+ * Test if a string is a numeric literal (integers or decimals), allows leading zeros.
29
+ * @param input - string to test, e.g. "00.5", "123"
30
+ * @returns true when string is numeric, false otherwise
31
+ */
32
+ export function isNumeric(input: string): boolean {
33
+ const numericPattern = /^\d+(?:\.\d+)?$/;
34
+ return numericPattern.test(input);
35
+ }
36
+
37
+ /**
38
+ * Detect if a color key name ends with digits (overriding default shades).
39
+ * @param key - color key, e.g. "primary", "secondary100"
40
+ * @returns true when key has numeric suffix, false otherwise
41
+ */
42
+ export function isColorKeyOverridden(key: string): boolean {
43
+ const overridePattern = /\d+$/;
44
+ return overridePattern.test(key);
45
+ }
@@ -41,6 +41,7 @@ import { EXCLUDED_WALLETS } from '../constants/wallets';
41
41
 
42
42
  import { isBlockchainTypeInCategory, removeDuplicateFrom } from './common';
43
43
  import { numberToString } from './numbers';
44
+ import { formatThousandsWithCommas } from './sanitizers';
44
45
 
45
46
  export type ExtendedModalWalletInfo = WalletInfoWithExtra &
46
47
  Pick<ExtendedWalletInfo, 'properties' | 'isHub'>;
@@ -295,7 +296,7 @@ export const calculateWalletUsdValue = (balances: BalanceState) => {
295
296
 
296
297
  function numberWithThousandSeparator(number: string | number): string {
297
298
  const parts = number.toString().split('.');
298
- parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
299
+ parts[0] = formatThousandsWithCommas(parts[0]);
299
300
  return parts.join('.');
300
301
  }
301
302