diff --git a/mobile/__tests__/utils/formatXLM.test.ts b/mobile/__tests__/utils/formatXLM.test.ts
index eb19059..004b9e9 100644
--- a/mobile/__tests__/utils/formatXLM.test.ts
+++ b/mobile/__tests__/utils/formatXLM.test.ts
@@ -1,31 +1,40 @@
import { formatXLM } from '@/utils/stellar';
+function normalizeSpaces(value: string): string {
+ return value.replace(/\u00a0|\u202f/g, ' ');
+}
+
describe('formatXLM', () => {
it('formats an integer amount with thousand separators and decimals', () => {
- expect(formatXLM(1234)).toBe('1,234.00 XLM');
+ expect(formatXLM(1234, 2, 'en')).toBe('1,234.00 XLM');
});
it('formats a large amount with multiple separators', () => {
- expect(formatXLM(1000000.5)).toBe('1,000,000.50 XLM');
+ expect(formatXLM(1000000.5, 2, 'en')).toBe('1,000,000.50 XLM');
});
it('handles zero with fixed decimals', () => {
- expect(formatXLM(0)).toBe('0.00 XLM');
+ expect(formatXLM(0, 2, 'en')).toBe('0.00 XLM');
});
it('formats a decimal amount rounded to specified decimals', () => {
- expect(formatXLM(1.555, 2)).toBe('1.56 XLM');
- expect(formatXLM(1.5, 1)).toBe('1.5 XLM');
+ expect(formatXLM(1.555, 2, 'en')).toBe('1.56 XLM');
+ expect(formatXLM(1.5, 1, 'en')).toBe('1.5 XLM');
});
it('returns 0.00 XLM for NaN input', () => {
// @ts-ignore - testing runtime NaN
- expect(formatXLM(NaN)).toBe('0.00 XLM');
+ expect(formatXLM(NaN, 2, 'en')).toBe('0.00 XLM');
// @ts-ignore - testing runtime invalid string
- expect(formatXLM('abc' as any)).toBe('0.00 XLM');
+ expect(formatXLM('abc' as any, 2, 'en')).toBe('0.00 XLM');
});
it('handles negative numbers', () => {
- expect(formatXLM(-1234.5)).toBe('-1,234.50 XLM');
+ expect(formatXLM(-1234.5, 2, 'en')).toBe('-1,234.50 XLM');
+ });
+
+ it('uses a comma decimal separator for French locale', () => {
+ const formatted = normalizeSpaces(formatXLM(1234.5, 2, 'fr'));
+ expect(formatted).toMatch(/^1[ \u00a0\u202f]234,50 XLM$/);
});
});
diff --git a/mobile/app/(tabs)/groups/page.tsx b/mobile/app/(tabs)/groups/page.tsx
index e4ed95d..f4aed1c 100644
--- a/mobile/app/(tabs)/groups/page.tsx
+++ b/mobile/app/(tabs)/groups/page.tsx
@@ -3,7 +3,7 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { FlatList, RefreshControl, SafeAreaView, View, Text, Pressable, StyleSheet } from 'react-native';
import { useRouter } from 'expo-router';
-import { Badge, ErrorState, LoadingSkeleton, TextInput } from '../../../components/ui';
+import { Badge, EmptyState, ErrorState, LoadingSkeleton, TextInput } from '../../../components/ui';
import { useDebounce } from '../../../hooks/useDebounce';
import { useRefresh } from '../../../hooks/useRefresh';
import { formatXLM } from '../../../utils/stellar';
@@ -194,11 +194,11 @@ export default function GroupsPage() {
) : error ? (
) : (
-
data={filteredGroups}
- keyExtractor={(item) => item.id}
+ keyExtractor={(item: Group) => item.id}
renderItem={renderGroup}
- getItemLayout={(_, index) => ({ length: 110, offset: 110 * index, index })}
+ getItemLayout={(_: unknown, index: number) => ({ length: 110, offset: 110 * index, index })}
removeClippedSubviews
maxToRenderPerBatch={10}
windowSize={5}
@@ -211,10 +211,12 @@ export default function GroupsPage() {
/>
}
ListEmptyComponent={
-
- No groups to show
- Try another filter to see matching groups.
-
+
}
/>
)}
@@ -326,20 +328,4 @@ const styles = StyleSheet.create({
color: '#475569',
marginTop: 4,
},
- emptyState: {
- marginTop: 32,
- alignItems: 'center',
- },
- emptyTitle: {
- fontSize: 18,
- fontWeight: '700',
- color: '#0F172A',
- marginBottom: 8,
- },
- emptyMessage: {
- fontSize: 15,
- color: '#64748B',
- textAlign: 'center',
- maxWidth: 260,
- },
});
diff --git a/mobile/app/referrals/index.tsx b/mobile/app/referrals/index.tsx
index 9e767a1..a5fb28f 100644
--- a/mobile/app/referrals/index.tsx
+++ b/mobile/app/referrals/index.tsx
@@ -11,6 +11,8 @@ import {
} from 'react-native';
import * as Clipboard from 'expo-clipboard';
import AsyncStorage from '@react-native-async-storage/async-storage';
+import { EmptyState } from '../../components/ui';
+import { formatDate } from '../../utils/formatDate';
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -115,11 +117,7 @@ function StatsRow({ stats }: { stats: ReferralStats }) {
}
function ReferralRow({ item }: { item: Referral }) {
- const date = new Date(item.joinedAt).toLocaleDateString(undefined, {
- month: 'short',
- day: 'numeric',
- year: 'numeric',
- });
+ const date = formatDate(item.joinedAt, { month: 'short', day: 'numeric', year: 'numeric' });
return (
@@ -196,7 +194,7 @@ export default function ReferralsScreen({ publicKey }: ReferralsScreenProps) {
style={styles.screen}
contentContainerStyle={styles.content}
data={referrals}
- keyExtractor={(item) => item.id}
+ keyExtractor={(item: Referral) => item.id}
ListHeaderComponent={
<>
Invite & earn
@@ -208,11 +206,14 @@ export default function ReferralsScreen({ publicKey }: ReferralsScreenProps) {
{referrals.length > 0 && Your referrals}
>
}
- renderItem={({ item }) => }
+ renderItem={({ item }: { item: Referral }) => }
ListEmptyComponent={
-
- No referrals yet — share your code to get started!
-
+
}
/>
);
@@ -289,6 +290,4 @@ const styles = StyleSheet.create({
badgeTextConfirmed: { color: '#065F46' },
badgeTextPending: { color: '#92400E' },
- emptyList: { alignItems: 'center', paddingVertical: 32 },
- emptyText: { fontSize: 14, color: '#94A3B8', textAlign: 'center' },
-});
\ No newline at end of file
+});
diff --git a/mobile/app/transaction/history.tsx b/mobile/app/transaction/history.tsx
index 7b3540e..0055c04 100644
--- a/mobile/app/transaction/history.tsx
+++ b/mobile/app/transaction/history.tsx
@@ -11,6 +11,7 @@ import {
} from 'react-native';
import { useRouter } from 'expo-router';
import { TransactionItem, TransactionType } from '../../components/transactions/TransactionItem';
+import { EmptyState } from '../../components/ui';
import { useRefresh } from '../../hooks/useRefresh';
type TabKey = 'All' | 'Contributions' | 'Payouts';
@@ -142,9 +143,12 @@ export default function TransactionHistoryScreen() {
contentContainerStyle={styles.list}
refreshControl={}
ListEmptyComponent={
-
- No {activeTab.toLowerCase()} to show.
-
+
}
/>
@@ -190,6 +194,4 @@ const styles = StyleSheet.create({
backgroundColor: '#6366F1',
},
list: { paddingHorizontal: 16, paddingTop: 8 },
- empty: { marginTop: 48, alignItems: 'center' },
- emptyText: { color: '#64748B', fontSize: 15 },
});
diff --git a/mobile/app/transactions/index.tsx b/mobile/app/transactions/index.tsx
index d446ca9..e241d80 100644
--- a/mobile/app/transactions/index.tsx
+++ b/mobile/app/transactions/index.tsx
@@ -121,7 +121,8 @@ export default function TransactionHistory() {
onEndReachedThreshold={0.3}
ListEmptyComponent={
diff --git a/mobile/components/groups/ContributionHistory.tsx b/mobile/components/groups/ContributionHistory.tsx
index 18866bc..573e4ff 100644
--- a/mobile/components/groups/ContributionHistory.tsx
+++ b/mobile/components/groups/ContributionHistory.tsx
@@ -5,6 +5,7 @@ import { View, Text, StyleSheet, FlatList } from 'react-native';
import { SectionHeader } from '../ui/SectionHeader';
import { EmptyState } from '../ui/EmptyState';
import { formatXLM } from '../../utils/stellar';
+import { formatDate } from '../../utils/formatDate';
type Transaction = {
contributor: string;
@@ -22,11 +23,6 @@ export function ContributionHistory({ transactions }: ContributionHistoryProps)
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
- const formatDate = (dateString: string) => {
- const date = new Date(dateString);
- return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
- };
-
const renderTransaction = ({ item }: { item: Transaction }) => (
@@ -38,7 +34,9 @@ export function ContributionHistory({ transactions }: ContributionHistoryProps)
- {formatDate(item.date)}
+
+ {formatDate(item.date, { month: 'short', day: 'numeric', year: 'numeric' })}
+
@@ -52,7 +50,8 @@ export function ContributionHistory({ transactions }: ContributionHistoryProps)
@@ -67,7 +66,7 @@ export function ContributionHistory({ transactions }: ContributionHistoryProps)
`${item.contributor}-${item.round}-${index}`}
+ keyExtractor={(item: Transaction, index: number) => `${item.contributor}-${item.round}-${index}`}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={() => }
/>
diff --git a/mobile/components/groups/PayoutSchedule.tsx b/mobile/components/groups/PayoutSchedule.tsx
index 8ff42af..9fa9cd4 100644
--- a/mobile/components/groups/PayoutSchedule.tsx
+++ b/mobile/components/groups/PayoutSchedule.tsx
@@ -3,6 +3,7 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
+import { formatDate } from '../../utils/formatDate';
type RoundStatus = 'upcoming' | 'completed' | 'current';
@@ -22,11 +23,6 @@ export function PayoutSchedule({ rounds }: PayoutScheduleProps) {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
- const formatDate = (dateString: string) => {
- const date = new Date(dateString);
- return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
- };
-
const getStatusIcon = (status: RoundStatus) => {
switch (status) {
case 'completed':
@@ -94,7 +90,7 @@ export function PayoutSchedule({ rounds }: PayoutScheduleProps) {
styles.dateText,
round.status === 'current' && styles.currentText
]}>
- {formatDate(round.date)}
+ {formatDate(round.date, { month: 'short', day: 'numeric', year: 'numeric' })}
diff --git a/mobile/components/ui/EmptyState.tsx b/mobile/components/ui/EmptyState.tsx
index 856d797..9eb38d1 100644
--- a/mobile/components/ui/EmptyState.tsx
+++ b/mobile/components/ui/EmptyState.tsx
@@ -1,47 +1,2 @@
-import React from 'react';
-import { View, Text, Pressable, StyleSheet } from 'react-native';
-import { getIllustration, type IllustrationType } from './Illustration';
-
-interface Props {
- icon?: string;
- illustration?: IllustrationType;
- title: string;
- message: string;
- actionLabel?: string;
- onAction?: () => void;
-}
-
-export function EmptyState({ icon, illustration, title, message, actionLabel, onAction }: Props) {
- const selected = illustration ? getIllustration(illustration) : getIllustration('default');
- const useFallbackIcon = Boolean(icon);
-
- return (
-
-
- {useFallbackIcon ? {icon} : selected.render()}
-
- {title}
- {message}
- {actionLabel && (
-
- {actionLabel}
-
- )}
-
- );
-}
-
-const styles = StyleSheet.create({
- container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
- illustration: { marginBottom: 14 },
- icon: { fontSize: 48 },
- title: { fontSize: 18, fontWeight: '600', color: '#F1F5F9', marginBottom: 8 },
- message: { fontSize: 14, color: '#94A3B8', textAlign: 'center', marginBottom: 20 },
- button: {
- backgroundColor: '#334155',
- borderRadius: 8,
- paddingVertical: 10,
- paddingHorizontal: 20,
- },
- buttonText: { color: '#F1F5F9', fontWeight: '600', fontSize: 14 },
-});
+export { EmptyState } from './empty';
+export type { EmptyStateIllustration } from './empty';
diff --git a/mobile/components/ui/empty.tsx b/mobile/components/ui/empty.tsx
new file mode 100644
index 0000000..61781a4
--- /dev/null
+++ b/mobile/components/ui/empty.tsx
@@ -0,0 +1,143 @@
+import React from 'react';
+import { View, Text, Pressable, StyleSheet } from 'react-native';
+import Svg, { Circle, Path, Rect, Ellipse, Line } from 'react-native-svg';
+import { useTheme } from '../../context/ThemeContext';
+
+export type EmptyStateIllustration = 'groups' | 'transactions' | 'notifications' | 'default';
+
+function GroupsIllustration() {
+ return (
+
+ );
+}
+
+function TransactionsIllustration() {
+ return (
+
+ );
+}
+
+function NotificationsIllustration() {
+ return (
+
+ );
+}
+
+function DefaultIllustration() {
+ return (
+
+ );
+}
+
+function Illustration({ type }: { type: EmptyStateIllustration }) {
+ switch (type) {
+ case 'groups':
+ return ;
+ case 'transactions':
+ return ;
+ case 'notifications':
+ return ;
+ default:
+ return ;
+ }
+}
+
+type Tone = 'light' | 'dark' | 'auto';
+
+interface Props {
+ title: string;
+ message: string;
+ illustration?: EmptyStateIllustration;
+ icon?: string;
+ actionLabel?: string;
+ onAction?: () => void;
+ tone?: Tone;
+}
+
+export function EmptyState({ title, message, illustration, icon, actionLabel, onAction, tone = 'auto' }: Props) {
+ const { resolvedColorScheme, colors } = useTheme();
+ const effectiveTone = tone === 'auto' ? resolvedColorScheme : tone;
+ const titleColor = effectiveTone === 'dark' ? '#F1F5F9' : '#0F172A';
+ const messageColor = effectiveTone === 'dark' ? '#94A3B8' : '#64748B';
+ const buttonBg = effectiveTone === 'dark' ? '#334155' : colors.accent;
+ const buttonText = effectiveTone === 'dark' ? '#F1F5F9' : '#FFFFFF';
+
+ return (
+
+
+ {illustration ? : icon ? {icon} : }
+
+ {title}
+ {message}
+ {actionLabel ? (
+
+ {actionLabel}
+
+ ) : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
+ illustration: { marginBottom: 18, opacity: 0.98 },
+ icon: { fontSize: 48 },
+ title: { fontSize: 18, fontWeight: '700', marginBottom: 8, textAlign: 'center' },
+ message: { fontSize: 14, textAlign: 'center', marginBottom: 18, lineHeight: 20 },
+ button: { borderRadius: 10, paddingVertical: 10, paddingHorizontal: 18 },
+ buttonText: { fontWeight: '700', fontSize: 14 },
+});
+
diff --git a/mobile/screens/NotificationsScreen.tsx b/mobile/screens/NotificationsScreen.tsx
index 019b6ec..3e3becb 100644
--- a/mobile/screens/NotificationsScreen.tsx
+++ b/mobile/screens/NotificationsScreen.tsx
@@ -2,6 +2,8 @@ import React, { useEffect } from 'react';
import { View, FlatList } from 'react-native';
import { useNotificationsStore } from '../stores/notificationsStore';
import { NotificationItem } from '../components/NotificationItem';
+import { EmptyState } from '../components/ui';
+import type { Notification } from '../types/notification';
export const NotificationsScreen = () => {
const { notifications, setNotifications } = useNotificationsStore();
@@ -32,9 +34,18 @@ export const NotificationsScreen = () => {
item.id}
- renderItem={({ item }) => }
+ keyExtractor={(item: Notification) => item.id}
+ renderItem={({ item }: { item: Notification }) => }
+ contentContainerStyle={notifications.length === 0 ? { flexGrow: 1 } : undefined}
+ ListEmptyComponent={
+
+ }
/>
);
-};
\ No newline at end of file
+};
diff --git a/mobile/services/api/groupsApi.ts b/mobile/services/api/groupsApi.ts
index df894ed..1023a52 100644
--- a/mobile/services/api/groupsApi.ts
+++ b/mobile/services/api/groupsApi.ts
@@ -3,6 +3,8 @@
* Handles REST API calls for group-related operations
*/
+import { logger } from '../logger';
+
export interface Group {
id: string;
name: string;
@@ -37,7 +39,7 @@ class GroupsApiService {
*/
async getUserGroups(userAddress: string): Promise> {
try {
- console.log(`Fetching groups for user: ${userAddress}`);
+ logger.debug('GroupsApi', 'Fetching user groups');
// Mock API call - replace with actual implementation
await new Promise(resolve => setTimeout(resolve, 1000));
@@ -76,7 +78,7 @@ class GroupsApiService {
data: mockGroups,
};
} catch (error) {
- console.error('Failed to fetch user groups:', error);
+ logger.error('GroupsApi', 'Failed to fetch user groups', error);
return {
success: false,
error: 'Failed to fetch groups',
@@ -89,7 +91,7 @@ class GroupsApiService {
*/
async getGroupById(groupId: string): Promise> {
try {
- console.log(`Fetching group details for: ${groupId}`);
+ logger.debug('GroupsApi', 'Fetching group details', { groupId });
// Mock API call - replace with actual implementation
await new Promise(resolve => setTimeout(resolve, 800));
@@ -113,7 +115,7 @@ class GroupsApiService {
data: mockGroup,
};
} catch (error) {
- console.error('Failed to fetch group details:', error);
+ logger.error('GroupsApi', 'Failed to fetch group details', error);
return {
success: false,
error: 'Failed to fetch group details',
diff --git a/mobile/services/api/notificationsApi.ts b/mobile/services/api/notificationsApi.ts
index 23d608d..e4a9914 100644
--- a/mobile/services/api/notificationsApi.ts
+++ b/mobile/services/api/notificationsApi.ts
@@ -5,6 +5,7 @@
import { ApiResponse } from './groupsApi';
import { Notification } from '../../types/notification';
+import { logger } from '../logger';
class NotificationsApiService {
private baseUrl: string;
@@ -18,7 +19,7 @@ class NotificationsApiService {
*/
async getUserNotifications(userAddress: string): Promise> {
try {
- console.log(`Fetching notifications for user: ${userAddress}`);
+ logger.debug('NotificationsApi', 'Fetching user notifications');
// Mock API call - replace with actual implementation
await new Promise(resolve => setTimeout(resolve, 800));
@@ -45,7 +46,7 @@ class NotificationsApiService {
data: mockNotifications,
};
} catch (error) {
- console.error('Failed to fetch notifications:', error);
+ logger.error('NotificationsApi', 'Failed to fetch notifications', error);
return {
success: false,
error: 'Failed to fetch notifications',
diff --git a/mobile/services/api/transactionsApi.ts b/mobile/services/api/transactionsApi.ts
index 6c21d64..0a26b1f 100644
--- a/mobile/services/api/transactionsApi.ts
+++ b/mobile/services/api/transactionsApi.ts
@@ -4,6 +4,7 @@
*/
import { ApiResponse } from './groupsApi';
+import { logger } from '../logger';
export interface Transaction {
id: string;
@@ -32,7 +33,7 @@ class TransactionsApiService {
limit: number = 20
): Promise> {
try {
- console.log(`Fetching transactions for user: ${userAddress}`);
+ logger.debug('TransactionsApi', 'Fetching user transactions');
// Mock API call - replace with actual implementation
await new Promise(resolve => setTimeout(resolve, 1000));
@@ -65,7 +66,7 @@ class TransactionsApiService {
data: mockTransactions,
};
} catch (error) {
- console.error('Failed to fetch transactions:', error);
+ logger.error('TransactionsApi', 'Failed to fetch transactions', error);
return {
success: false,
error: 'Failed to fetch transactions',
diff --git a/mobile/services/logger/index.ts b/mobile/services/logger/index.ts
index 64172e5..baf9732 100644
--- a/mobile/services/logger/index.ts
+++ b/mobile/services/logger/index.ts
@@ -1,45 +1,2 @@
-/**
- * Standardized logger for the mobile app.
- * Non-error logs are disabled in production.
- */
-
-const IS_DEV = __DEV__;
-
-type LogLevel = 'debug' | 'info' | 'warn' | 'error';
-
-function shouldLog(level: LogLevel): boolean {
- return IS_DEV || level === 'error';
-}
-
-function write(level: LogLevel, tag: string, message: string, ...args: unknown[]) {
- if (!shouldLog(level)) return;
-
- const timestamp = new Date().toISOString();
- const prefix = `[${timestamp}][${level.toUpperCase()}][${tag}]`;
-
- switch (level) {
- case 'debug':
- console.debug(prefix, message, ...args);
- break;
- case 'info':
- console.info(prefix, message, ...args);
- break;
- case 'warn':
- console.warn(prefix, message, ...args);
- break;
- case 'error':
- console.error(prefix, message, ...args);
- break;
- }
-}
-
-export const logger = {
- debug: (tag: string, message: string, ...args: unknown[]) =>
- write('debug', tag, message, ...args),
- info: (tag: string, message: string, ...args: unknown[]) =>
- write('info', tag, message, ...args),
- warn: (tag: string, message: string, ...args: unknown[]) =>
- write('warn', tag, message, ...args),
- error: (tag: string, message: string, ...args: unknown[]) =>
- write('error', tag, message, ...args),
-};
+export { logger } from '../services/logger';
+export type { LogLevel } from '../services/logger';
diff --git a/mobile/services/notifications.ts b/mobile/services/notifications.ts
index efab2a7..363fe90 100644
--- a/mobile/services/notifications.ts
+++ b/mobile/services/notifications.ts
@@ -1,6 +1,7 @@
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
+import { logger } from './logger';
Notifications.setNotificationHandler({
handleNotification: async () => ({
@@ -12,7 +13,7 @@ Notifications.setNotificationHandler({
export async function registerForPushNotificationsAsync(): Promise {
if (!Device.isDevice) {
- console.warn('[notifications] Push tokens require a physical device');
+ logger.warn('Notifications', 'Push tokens require a physical device');
return null;
}
@@ -23,7 +24,7 @@ export async function registerForPushNotificationsAsync(): Promise Localization.locale ?? 'en';
diff --git a/mobile/utils/logger.ts b/mobile/utils/logger.ts
index af06e8a..baf9732 100644
--- a/mobile/utils/logger.ts
+++ b/mobile/utils/logger.ts
@@ -1 +1,2 @@
export { logger } from '../services/logger';
+export type { LogLevel } from '../services/logger';
diff --git a/mobile/utils/stellar.ts b/mobile/utils/stellar.ts
index cd3d13a..21023bf 100644
--- a/mobile/utils/stellar.ts
+++ b/mobile/utils/stellar.ts
@@ -1,3 +1,7 @@
+import * as Localization from 'expo-localization';
+
+const getLocale = (): string => Localization.locale ?? 'en';
+
/**
* Truncates a Stellar address to the format GXXX...XXXX.
* Returns the original string if it is too short to truncate.
@@ -13,17 +17,14 @@ export function truncateAddress(address: string, leading = 4, trailing = 4): str
*
* @param amount The numeric amount to format
* @param decimals The number of decimal places (default 2)
+ * @param locale Optional locale override (defaults to device locale)
* @returns A formatted string e.g., "1,234.50 XLM"
*/
-export function formatXLM(amount: number, decimals = 2): string {
- const value = isNaN(amount) || amount === null || amount === undefined ? 0 : amount;
-
- const sign = value >= 0 ? 1 : -1;
- const factor = Math.pow(10, decimals);
- const rounded = sign * (Math.round(Math.abs(value) * factor) / factor);
-
- const parts = rounded.toFixed(decimals).split('.');
- parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
-
- return `${parts.join('.')} XLM`;
+export function formatXLM(amount: number, decimals = 2, locale?: string): string {
+ const value = typeof amount === 'number' && Number.isFinite(amount) ? amount : 0;
+ const formatter = new Intl.NumberFormat(locale ?? getLocale(), {
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals,
+ });
+ return `${formatter.format(value)} XLM`;
}