diff --git a/apps/telegram-ecash-escrow/src/app/shopping/page.tsx b/apps/telegram-ecash-escrow/src/app/shopping/page.tsx index cc364869..33c9b1e3 100644 --- a/apps/telegram-ecash-escrow/src/app/shopping/page.tsx +++ b/apps/telegram-ecash-escrow/src/app/shopping/page.tsx @@ -184,7 +184,7 @@ export default function Shopping() {
- Goods & Services + Offers {isShowSortIcon && ( )} -
+
{!isLoadingFilter ? ( } - scrollableTarget="scrollableDiv" scrollThreshold={'100px'} > {dataFilter.map(item => { - return ; + return ; })} ) : ( diff --git a/apps/telegram-ecash-escrow/src/components/DetailInfo/OfferDetailInfo.tsx b/apps/telegram-ecash-escrow/src/components/DetailInfo/OfferDetailInfo.tsx index bf7cc435..0871c0cd 100644 --- a/apps/telegram-ecash-escrow/src/components/DetailInfo/OfferDetailInfo.tsx +++ b/apps/telegram-ecash-escrow/src/components/DetailInfo/OfferDetailInfo.tsx @@ -96,6 +96,17 @@ const OfferDetailInfo = ({ timelineItem, post, isShowBuyButton = false, isItemTi const isOwner = (postData ?? post)?.accountId === selectedAccountId; + // Format fiat price without decimals and with thousands separators for display + const formatFiatPrice = (price: number | string | undefined): string => { + if (price == null || price === '') return ''; + const num = typeof price === 'string' ? parseFloat(price) : price; + if (isNaN(num)) return String(price); + return new Intl.NumberFormat('en-GB', { + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }).format(Math.round(num)); + }; + const handleClickAction = e => { e.stopPropagation(); dispatch(openActionSheet('OfferActionSheet', { post: postData })); @@ -184,7 +195,7 @@ const OfferDetailInfo = ({ timelineItem, post, isShowBuyButton = false, isItemTi (offerData?.tickerPriceGoodsServices ?? DEFAULT_TICKER_GOODS_SERVICES) !== DEFAULT_TICKER_GOODS_SERVICES ? ( - ({offerData.priceGoodsServices} {offerData.tickerPriceGoodsServices ?? 'USD'}) + ({formatFiatPrice(offerData.priceGoodsServices)} {offerData.tickerPriceGoodsServices ?? 'USD'}) ) : null} diff --git a/apps/telegram-ecash-escrow/src/components/FilterList/ShoppingCurrencyModal.tsx b/apps/telegram-ecash-escrow/src/components/FilterList/ShoppingCurrencyModal.tsx new file mode 100644 index 00000000..932a0d35 --- /dev/null +++ b/apps/telegram-ecash-escrow/src/components/FilterList/ShoppingCurrencyModal.tsx @@ -0,0 +1,190 @@ +'use client'; + +import { LIST_CURRENCIES_USED } from '@bcpros/lixi-models'; +import { ChevronLeft } from '@mui/icons-material'; +import { + Box, + Button, + Dialog, + DialogContent, + DialogTitle, + IconButton, + Slide, + TextField, + Typography, + useMediaQuery, + useTheme +} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { TransitionProps } from '@mui/material/transitions'; +import React, { useId, useMemo, useState } from 'react'; +import { FilterCurrencyType } from '../../store/type/types'; + +interface ShoppingCurrencyModalProps { + isOpen: boolean; + onDismissModal?: (value: boolean) => void; + setSelectedItem?: (value: FilterCurrencyType) => void; +} + +const StyledDialog = styled(Dialog)(({ theme }) => ({ + '.MuiPaper-root': { + background: theme.palette.background.default, + backgroundRepeat: 'no-repeat', + backgroundSize: 'cover', + width: '500px', + height: '100vh', + maxHeight: '100%', + margin: 0, + [theme.breakpoints.down('sm')]: { + width: '100%' + } + }, + + '.MuiIconButton-root': { + width: 'fit-content', + svg: { + fontSize: '32px' + } + }, + + '.MuiDialogTitle-root': { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + + '.back-btn': { + position: 'absolute', + left: '10px' + }, + + '.btn-clear': { + color: '#FFF', + position: 'absolute', + right: '10px', + fontSize: '12px', + padding: '1px 5px' + } + }, + + '.MuiDialogContent-root': { + padding: '16px' + }, + + button: { + color: theme.palette.text.secondary + } +})); + +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref +) { + return ; +}); + +/** + * Simplified currency modal for Shopping tab + * Shows fiat currencies + XEC in a single list, sorted alphabetically + */ +const ShoppingCurrencyModal: React.FC = props => { + const { isOpen, onDismissModal, setSelectedItem } = props; + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('md')); + + const [searchTerm, setSearchTerm] = useState(''); + const titleId = useId(); + + // Build combined list of fiat currencies + XEC, sorted alphabetically by code + const currencyList = useMemo(() => { + // Add XEC as a currency option + const xecOption = { code: 'XEC', name: 'eCash' }; + + // Combine fiat currencies with XEC + const allCurrencies = [...LIST_CURRENCIES_USED, xecOption]; + + // Sort alphabetically by code + return allCurrencies.sort((a, b) => a.code.localeCompare(b.code)); + }, []); + + // Filter currencies based on search term + const filteredCurrencies = useMemo(() => { + if (!searchTerm) return currencyList; + + const lowerSearch = searchTerm.toLowerCase(); + return currencyList.filter( + option => option.code.toLowerCase().includes(lowerSearch) || option.name.toLowerCase().includes(lowerSearch) + ); + }, [currencyList, searchTerm]); + + const handleSelect = (currency: { code: string; name: string }) => { + const filterCurrency: FilterCurrencyType = { + paymentMethod: 5, // PAYMENT_METHOD.GOODS_SERVICES + value: currency.code + }; + setSelectedItem?.(filterCurrency); + onDismissModal?.(false); + setSearchTerm(''); + }; + + const handleClear = () => { + setSelectedItem?.({ paymentMethod: 5, value: '' }); + onDismissModal?.(false); + setSearchTerm(''); + }; + + const handleClose = () => { + onDismissModal?.(false); + setSearchTerm(''); + }; + + return ( + + + + + + Select currency + + + + setSearchTerm(e.target.value)} + value={searchTerm} + autoFocus + /> + + {filteredCurrencies.map(option => ( + + ))} + {filteredCurrencies.length === 0 && ( + No currencies found + )} + + + + ); +}; + +export default ShoppingCurrencyModal; diff --git a/apps/telegram-ecash-escrow/src/components/FilterOffer/ShoppingFilterComponent.tsx b/apps/telegram-ecash-escrow/src/components/FilterOffer/ShoppingFilterComponent.tsx index b3a21d1d..d7ff132e 100644 --- a/apps/telegram-ecash-escrow/src/components/FilterOffer/ShoppingFilterComponent.tsx +++ b/apps/telegram-ecash-escrow/src/components/FilterOffer/ShoppingFilterComponent.tsx @@ -18,7 +18,7 @@ import { styled } from '@mui/material/styles'; import { debounce } from 'lodash'; import React, { useCallback, useState } from 'react'; import { NumericFormat } from 'react-number-format'; -import FilterCurrencyModal from '../FilterList/FilterCurrencyModal'; +import ShoppingCurrencyModal from '../FilterList/ShoppingCurrencyModal'; const WrapFilter = styled('div')(({ theme }) => ({ marginBottom: '16px', @@ -228,7 +228,7 @@ const ShoppingFilterComponent: React.FC = ({ filte )}
- handleFilterCurrency(value)} onDismissModal={value => setOpenCurrencyList(value)} diff --git a/apps/telegram-ecash-escrow/src/components/OfferDetailInfo/OfferDetailInfo.tsx b/apps/telegram-ecash-escrow/src/components/OfferDetailInfo/OfferDetailInfo.tsx index c3c3a7d2..e07e398b 100644 --- a/apps/telegram-ecash-escrow/src/components/OfferDetailInfo/OfferDetailInfo.tsx +++ b/apps/telegram-ecash-escrow/src/components/OfferDetailInfo/OfferDetailInfo.tsx @@ -42,6 +42,17 @@ const OrderDetailInfo = ({ key, post }: { key: string; post: Post }) => { isGoodsServices: _isGoodsServices } = useOfferPrice({ paymentInfo: post?.offer, inputAmount: 1 }); + // Format fiat price without decimals and with thousands separators for display + const formatFiatPrice = (price: number | string | undefined): string => { + if (price == null || price === '') return ''; + const num = typeof price === 'string' ? parseFloat(price) : price; + if (isNaN(num)) return String(price); + return new Intl.NumberFormat('en-GB', { + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }).format(Math.round(num)); + }; + return ( @@ -60,7 +71,7 @@ const OrderDetailInfo = ({ key, post }: { key: string; post: Post }) => { (post.offer?.tickerPriceGoodsServices ?? DEFAULT_TICKER_GOODS_SERVICES) !== DEFAULT_TICKER_GOODS_SERVICES ? ( - ({post.offer.priceGoodsServices} {post.offer.tickerPriceGoodsServices ?? 'USD'}) + ({formatFiatPrice(post.offer.priceGoodsServices)} {post.offer.tickerPriceGoodsServices ?? 'USD'}) ) : null} diff --git a/apps/telegram-ecash-escrow/src/components/OfferItem/OfferItem.tsx b/apps/telegram-ecash-escrow/src/components/OfferItem/OfferItem.tsx index 0ba05cec..508d92ee 100644 --- a/apps/telegram-ecash-escrow/src/components/OfferItem/OfferItem.tsx +++ b/apps/telegram-ecash-escrow/src/components/OfferItem/OfferItem.tsx @@ -142,9 +142,10 @@ const OfferShowWrapItem = styled('div')(({ theme }) => ({ type OfferItemProps = { timelineItem?: TimelineQueryItem; + hidePaymentMethods?: boolean; }; -export default function OfferItem({ timelineItem }: OfferItemProps) { +export default function OfferItem({ timelineItem, hidePaymentMethods = false }: OfferItemProps) { const { status } = useSession(); const askAuthorization = useAuthorization(); const searchParams = useSearchParams(); @@ -164,6 +165,17 @@ export default function OfferItem({ timelineItem }: OfferItemProps) { const settingContext = useContext(SettingContext); const seedBackupTime = settingContext?.setting?.lastSeedBackupTime ?? lastSeedBackupTimeOnDevice ?? ''; + // Format fiat price without decimals and with thousands separators for display + const formatFiatPrice = (price: number | string | undefined): string => { + if (price == null || price === '') return ''; + const num = typeof price === 'string' ? parseFloat(price) : price; + if (isNaN(num)) return String(price); + return new Intl.NumberFormat('en-GB', { + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }).format(Math.round(num)); + }; + const { useGetAccountByAddressQuery } = accountsApi; const { currentData: accountQueryData } = useGetAccountByAddressQuery( { address: selectedWalletPath?.xAddress }, @@ -337,7 +349,7 @@ export default function OfferItem({ timelineItem }: OfferItemProps) { )} - {OfferItemPaymentMethod} + {!hidePaymentMethods && {OfferItemPaymentMethod}} @@ -350,7 +362,7 @@ export default function OfferItem({ timelineItem }: OfferItemProps) { (offerData?.tickerPriceGoodsServices ?? DEFAULT_TICKER_GOODS_SERVICES) !== DEFAULT_TICKER_GOODS_SERVICES ? ( - ({offerData.priceGoodsServices} {offerData.tickerPriceGoodsServices ?? 'USD'}) + ({formatFiatPrice(offerData.priceGoodsServices)} {offerData.tickerPriceGoodsServices ?? 'USD'}) ) : null} diff --git a/apps/telegram-ecash-escrow/src/hooks/useOfferPrice.tsx b/apps/telegram-ecash-escrow/src/hooks/useOfferPrice.tsx index 954e8ef8..895fcd78 100644 --- a/apps/telegram-ecash-escrow/src/hooks/useOfferPrice.tsx +++ b/apps/telegram-ecash-escrow/src/hooks/useOfferPrice.tsx @@ -187,7 +187,9 @@ export default function useOfferPrice({ paymentInfo, inputAmount = 1 }: UseOffer const priceOf1XECInLocalCurrency = xecRateEntry.rate; const priceOf1MXECInLocalCurrency = priceOf1XECInLocalCurrency * 1000000; - setAmountXECGoodsServices(1); // 1 XEC + // Round to 2 decimal places for XEC display + const roundedXEC = Math.round(1 * 100) / 100; + setAmountXECGoodsServices(roundedXEC); setAmountPer1MXEC( formatAmountFor1MXEC(priceOf1MXECInLocalCurrency, paymentInfo?.marginPercentage, coinCurrency, isBuyOffer) ); @@ -212,7 +214,10 @@ export default function useOfferPrice({ paymentInfo, inputAmount = 1 }: UseOffer ? paymentInfo.priceGoodsServices : 1; // Default to 1 XEC for legacy offers without price - setAmountXECGoodsServices(displayPrice); + // Round XEC to maximum 2 decimal places + const roundedPrice = Math.round(displayPrice * 100) / 100; + + setAmountXECGoodsServices(roundedPrice); setAmountPer1MXEC( formatAmountFor1MXEC(amountCoinOrCurrency, paymentInfo?.marginPercentage, coinCurrency, isBuyOffer) ); diff --git a/apps/telegram-ecash-escrow/src/store/constants.ts b/apps/telegram-ecash-escrow/src/store/constants.ts index bed05609..dc6174f5 100644 --- a/apps/telegram-ecash-escrow/src/store/constants.ts +++ b/apps/telegram-ecash-escrow/src/store/constants.ts @@ -98,6 +98,10 @@ export const LIST_TICKER_GOODS_SERVICES = [ { id: 2, name: 'USD' + }, + { + id: 3, + name: 'VND' } ]; export const DEFAULT_TICKER_GOODS_SERVICES = 'XEC'; diff --git a/apps/telegram-ecash-escrow/src/store/util.ts b/apps/telegram-ecash-escrow/src/store/util.ts index 450f1894..6e61ada2 100644 --- a/apps/telegram-ecash-escrow/src/store/util.ts +++ b/apps/telegram-ecash-escrow/src/store/util.ts @@ -132,17 +132,21 @@ export const getCoinRate = ({ tickerPriceGoodsServices, rateData }: GetCoinRateOptions): any | null => { - // For Goods & Services: priceGoodsServices is the PRICE (e.g., 1 USD) - // We need to find the USD (or tickerPriceGoodsServices) rate from rateData + // For Goods & Services: priceGoodsServices is the PRICE (e.g., 1 USD or 25000 VND) + // We need to find the currency rate from rateData if (isGoodsServicesConversion && tickerPriceGoodsServices) { - // Find the rate for the ticker currency (e.g., USD rate) const tickerPriceGoodsServicesUpper = tickerPriceGoodsServices.toUpperCase(); + + // Find the direct rate for the ticker currency (USD, VND, etc.) + // rateData contains inverted rates: 1 USD = X XEC, 1 VND = X XEC const tickerRate = rateData.find( (item: { coin?: string; rate?: number }) => item.coin?.toUpperCase() === tickerPriceGoodsServicesUpper )?.rate; + if (tickerRate && priceGoodsServices && priceGoodsServices > 0) { // Return the fiat currency rate multiplied by the price - // E.g., if 1 USD = 68027 XEC and item costs 1 USD, return 68027 + // E.g., if 1 USD = 68027 XEC and item costs 1 USD, return 68027 XEC + // E.g., if 1 VND = 4.35 XEC and item costs 25000 VND, return 108750 XEC return tickerRate * priceGoodsServices; } } @@ -279,7 +283,9 @@ export function formatAmountFor1MXEC(amount, marginPercentage = 0, coinCurrency } export function formatAmountForGoodsServices(amount) { - return `${formatNumber(amount)} XEC / ${GOODS_SERVICES_UNIT}`; + // Limit XEC to 2 decimal places + const roundedAmount = Math.round(amount * 100) / 100; + return `${formatNumber(roundedAmount)} XEC / ${GOODS_SERVICES_UNIT}`; } /**