From 4b169cc5d781f1362bb767825a0a8352a31880e6 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:52:12 +0100 Subject: [PATCH 1/7] Limit top holders to 3 on realunit page with dedicated holders subpage (#959) Show only the top 3 holders on the main realunit screen with a "More" button that navigates to /realunit/holders for the full paginated list. --- src/App.tsx | 5 + src/screens/realunit-holders.screen.tsx | 116 ++++++++++++++++++++++++ src/screens/realunit.screen.tsx | 42 ++------- 3 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 src/screens/realunit-holders.screen.tsx diff --git a/src/App.tsx b/src/App.tsx index 1efceba9e..dac973bc6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -60,6 +60,7 @@ const ComplianceKycFilesDetailsScreen = lazy(() => import('./screens/compliance- const ComplianceKycStatsScreen = lazy(() => import('./screens/compliance-kyc-stats.screen')); const ComplianceTransactionListScreen = lazy(() => import('./screens/compliance-transaction-list.screen')); const RealunitScreen = lazy(() => import('./screens/realunit.screen')); +const RealunitHoldersScreen = lazy(() => import('./screens/realunit-holders.screen')); const RealunitUserScreen = lazy(() => import('./screens/realunit-user.screen')); const PersonalIbanScreen = lazy(() => import('./screens/personal-iban.screen')); const BuyCryptoUpdateScreen = lazy(() => import('./screens/buy-crypto-update.screen')); @@ -360,6 +361,10 @@ export const Routes = [ index: true, element: withSuspense(), }, + { + path: 'holders', + element: withSuspense(), + }, { path: 'user/:address', element: withSuspense(), diff --git a/src/screens/realunit-holders.screen.tsx b/src/screens/realunit-holders.screen.tsx new file mode 100644 index 000000000..946acdd85 --- /dev/null +++ b/src/screens/realunit-holders.screen.tsx @@ -0,0 +1,116 @@ +import { + CopyButton, + IconColor, + SpinnerSize, + StyledButton, + StyledButtonWidth, + StyledLoadingSpinner, +} from '@dfx.swiss/react-components'; +import { useEffect } from 'react'; +import { useRealunitContext } from 'src/contexts/realunit.context'; +import { useSettingsContext } from 'src/contexts/settings.context'; +import { PaginationDirection } from 'src/dto/realunit.dto'; +import { useClipboard } from 'src/hooks/clipboard.hook'; +import { useAdminGuard } from 'src/hooks/guard.hook'; +import { useLayoutOptions } from 'src/hooks/layout-config.hook'; +import { useNavigation } from 'src/hooks/navigation.hook'; +import { blankedAddress } from 'src/util/utils'; + +export default function RealunitHoldersScreen(): JSX.Element { + useAdminGuard(); + + const { translate } = useSettingsContext(); + const { navigate } = useNavigation(); + const { copy } = useClipboard(); + + const { holders, totalCount, pageInfo, isLoading, fetchHolders } = useRealunitContext(); + + useLayoutOptions({ title: translate('screens/realunit', 'All Holders'), backButton: true }); + + useEffect(() => { + if (!holders.length) fetchHolders(); + }, [fetchHolders]); + + const handleAddressClick = (address: string) => { + const encodedAddress = encodeURIComponent(address); + navigate(`/realunit/user/${encodedAddress}`); + }; + + const changePage = (dir: PaginationDirection) => + fetchHolders(dir === PaginationDirection.NEXT ? pageInfo.endCursor : pageInfo.startCursor, dir); + + return ( + <> + {isLoading && !holders.length ? ( + + ) : ( +
+
+

+ {translate('screens/realunit', 'All Holders')} ({totalCount?.toLocaleString() ?? '0'}) +

+ + + + + + + + + + + {holders.map((holder) => ( + + + + + + ))} + +
+ {translate('screens/realunit', 'Address')} + + {translate('screens/realunit', 'Balance')} + + {translate('screens/realunit', 'Percentage')} +
+
+ + copy(holder.address)} /> +
+
{holder.balance}{holder.percentage.toFixed(2)}%
+
+ +
+
+ changePage(PaginationDirection.PREV)} + disabled={!pageInfo.hasPreviousPage} + width={StyledButtonWidth.MIN} + /> +
+ +
+ changePage(PaginationDirection.NEXT)} + disabled={!pageInfo.hasNextPage} + width={StyledButtonWidth.MIN} + /> +
+
+
+ )} + + ); +} diff --git a/src/screens/realunit.screen.tsx b/src/screens/realunit.screen.tsx index 47e7dfd80..9d2855097 100644 --- a/src/screens/realunit.screen.tsx +++ b/src/screens/realunit.screen.tsx @@ -10,7 +10,6 @@ import { useEffect } from 'react'; import { PriceHistoryChart } from 'src/components/realunit/price-history-chart'; import { useRealunitContext } from 'src/contexts/realunit.context'; import { useSettingsContext } from 'src/contexts/settings.context'; -import { PaginationDirection } from 'src/dto/realunit.dto'; import { useClipboard } from 'src/hooks/clipboard.hook'; import { useAdminGuard } from 'src/hooks/guard.hook'; import { useLayoutOptions } from 'src/hooks/layout-config.hook'; @@ -23,18 +22,8 @@ export default function RealunitScreen(): JSX.Element { const { navigate } = useNavigation(); const { copy } = useClipboard(); - const { - holders, - totalCount, - pageInfo, - tokenInfo, - isLoading, - priceHistory, - timeframe, - fetchHolders, - fetchPriceHistory, - fetchTokenInfo, - } = useRealunitContext(); + const { holders, totalCount, tokenInfo, isLoading, priceHistory, timeframe, fetchHolders, fetchPriceHistory, fetchTokenInfo } = + useRealunitContext(); useLayoutOptions({ backButton: true }); @@ -44,14 +33,13 @@ export default function RealunitScreen(): JSX.Element { if (!priceHistory.length) fetchPriceHistory(); }, [fetchHolders, fetchTokenInfo]); + const topHolders = holders.slice(0, 3); + const handleAddressClick = (address: string) => { const encodedAddress = encodeURIComponent(address); navigate(`/realunit/user/${encodedAddress}`); }; - const changePage = (dir: PaginationDirection) => - fetchHolders(dir === PaginationDirection.NEXT ? pageInfo.endCursor : pageInfo.startCursor, dir); - return ( <> {!holders.length && !tokenInfo ? ( @@ -144,7 +132,7 @@ export default function RealunitScreen(): JSX.Element { - {holders.map((holder) => ( + {topHolders.map((holder) => ( -
-
- changePage(PaginationDirection.PREV)} - disabled={!pageInfo.hasPreviousPage} - width={StyledButtonWidth.MIN} - /> -
- -
+ {holders.length > 3 && ( +
changePage(PaginationDirection.NEXT)} - disabled={!pageInfo.hasNextPage} + label={translate('general/actions', 'More')} + onClick={() => navigate('/realunit/holders')} width={StyledButtonWidth.MIN} />
-
+ )}
)} From 4f94beef79faaa8c5d0125298bd014c24e7cbf43 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:22:28 +0100 Subject: [PATCH 2/7] feat: add quotes and transactions sections to RealUnit admin page (#961) Display pending quotes and completed transactions tables below top holders on the /realunit admin page with offset-based pagination via More button. --- src/contexts/realunit.context.tsx | 48 ++++++++- src/dto/realunit.dto.ts | 28 +++++ src/hooks/realunit-api.hook.ts | 26 +++++ src/screens/realunit.screen.tsx | 163 +++++++++++++++++++++++++++++- 4 files changed, 260 insertions(+), 5 deletions(-) diff --git a/src/contexts/realunit.context.tsx b/src/contexts/realunit.context.tsx index 893a8e2b1..0bdbc0650 100644 --- a/src/contexts/realunit.context.tsx +++ b/src/contexts/realunit.context.tsx @@ -6,6 +6,8 @@ import { PageInfo, PaginationDirection, PriceHistoryEntry, + RealUnitQuote, + RealUnitTransaction, RealunitContextInterface, TokenInfo, TokenPrice, @@ -35,9 +37,21 @@ export function RealunitContextProvider({ children }: PropsWithChildren): JSX.El const [tokenPrice, setTokenPrice] = useState(); const [priceHistory, setPriceHistory] = useState([]); const [timeframe, setTimeframe] = useState(Timeframe.ALL); + const [quotes, setQuotes] = useState([]); + const [transactions, setTransactions] = useState([]); + const [quotesLoading, setQuotesLoading] = useState(false); + const [transactionsLoading, setTransactionsLoading] = useState(false); - const { getAccountSummary, getAccountHistory, getHolders, getPriceHistory, getTokenInfo, getTokenPrice } = - useRealunitApi(); + const { + getAccountSummary, + getAccountHistory, + getHolders, + getPriceHistory, + getTokenInfo, + getTokenPrice, + getAdminQuotes, + getAdminTransactions, + } = useRealunitApi(); const fetchAccountSummary = useCallback( (address: string) => { @@ -98,6 +112,24 @@ export function RealunitContextProvider({ children }: PropsWithChildren): JSX.El }); }, [setTokenPrice]); + const fetchQuotes = useCallback(() => { + setQuotesLoading(true); + getAdminQuotes(50, quotes.length) + .then((data) => { + setQuotes((prev) => [...prev, ...data]); + }) + .finally(() => setQuotesLoading(false)); + }, [quotes.length]); + + const fetchTransactions = useCallback(() => { + setTransactionsLoading(true); + getAdminTransactions(50, transactions.length) + .then((data) => { + setTransactions((prev) => [...prev, ...data]); + }) + .finally(() => setTransactionsLoading(false)); + }, [transactions.length]); + const context = useMemo( () => ({ accountSummary, @@ -110,12 +142,18 @@ export function RealunitContextProvider({ children }: PropsWithChildren): JSX.El tokenPrice, priceHistory, timeframe, + quotes, + transactions, + quotesLoading, + transactionsLoading, fetchAccountSummary, fetchAccountHistory, fetchHolders, fetchTokenInfo, fetchPriceHistory, fetchTokenPrice, + fetchQuotes, + fetchTransactions, }), [ accountSummary, @@ -128,12 +166,18 @@ export function RealunitContextProvider({ children }: PropsWithChildren): JSX.El tokenPrice, priceHistory, timeframe, + quotes, + transactions, + quotesLoading, + transactionsLoading, fetchAccountSummary, fetchAccountHistory, fetchHolders, fetchTokenInfo, fetchTokenPrice, fetchPriceHistory, + fetchQuotes, + fetchTransactions, ], ); diff --git a/src/dto/realunit.dto.ts b/src/dto/realunit.dto.ts index 03c61a3b6..6e5002a57 100644 --- a/src/dto/realunit.dto.ts +++ b/src/dto/realunit.dto.ts @@ -94,6 +94,28 @@ export enum PaginationDirection { PREV = 'prev', } +export interface RealUnitQuote { + id: number; + uid: string; + type: string; + status: string; + amount: number; + estimatedAmount: number; + created: string; + userAddress?: string; +} + +export interface RealUnitTransaction { + id: number; + uid: string; + type: string; + amountInChf: number; + assets: string; + created: string; + outputDate?: string; + userAddress?: string; +} + export interface RealunitContextInterface { accountSummary?: AccountSummary; history?: AccountHistory; @@ -105,10 +127,16 @@ export interface RealunitContextInterface { tokenPrice?: TokenPrice; priceHistory: PriceHistoryEntry[]; timeframe: Timeframe; + quotes: RealUnitQuote[]; + transactions: RealUnitTransaction[]; + quotesLoading: boolean; + transactionsLoading: boolean; fetchAccountSummary: (address: string) => void; fetchAccountHistory: (address: string, cursor?: string, direction?: PaginationDirection) => void; fetchHolders: (cursor?: string, direction?: PaginationDirection) => void; fetchTokenInfo: () => void; fetchPriceHistory: (timeframe?: Timeframe) => void; fetchTokenPrice: () => void; + fetchQuotes: () => void; + fetchTransactions: () => void; } diff --git a/src/hooks/realunit-api.hook.ts b/src/hooks/realunit-api.hook.ts index 147ec9ebe..c701ecacd 100644 --- a/src/hooks/realunit-api.hook.ts +++ b/src/hooks/realunit-api.hook.ts @@ -6,6 +6,8 @@ import { HoldersResponse, PaginationDirection, PriceHistoryEntry, + RealUnitQuote, + RealUnitTransaction, TokenInfo, TokenPrice, } from 'src/dto/realunit.dto'; @@ -70,6 +72,28 @@ export function useRealunitApi() { }); } + async function getAdminQuotes(limit?: number, offset?: number): Promise { + const params = new URLSearchParams(); + if (limit != null) params.set('limit', String(limit)); + if (offset != null) params.set('offset', String(offset)); + + return call({ + url: relativeUrl({ path: 'realunit/admin/quotes', params }), + method: 'GET', + }); + } + + async function getAdminTransactions(limit?: number, offset?: number): Promise { + const params = new URLSearchParams(); + if (limit != null) params.set('limit', String(limit)); + if (offset != null) params.set('offset', String(offset)); + + return call({ + url: relativeUrl({ path: 'realunit/admin/transactions', params }), + method: 'GET', + }); + } + return useMemo( () => ({ getAccountSummary, @@ -78,6 +102,8 @@ export function useRealunitApi() { getTokenInfo, getTokenPrice, getPriceHistory, + getAdminQuotes, + getAdminTransactions, }), [call], ); diff --git a/src/screens/realunit.screen.tsx b/src/screens/realunit.screen.tsx index 9d2855097..86d4e8601 100644 --- a/src/screens/realunit.screen.tsx +++ b/src/screens/realunit.screen.tsx @@ -22,8 +22,23 @@ export default function RealunitScreen(): JSX.Element { const { navigate } = useNavigation(); const { copy } = useClipboard(); - const { holders, totalCount, tokenInfo, isLoading, priceHistory, timeframe, fetchHolders, fetchPriceHistory, fetchTokenInfo } = - useRealunitContext(); + const { + holders, + totalCount, + tokenInfo, + isLoading, + priceHistory, + timeframe, + quotes, + transactions, + quotesLoading, + transactionsLoading, + fetchHolders, + fetchPriceHistory, + fetchTokenInfo, + fetchQuotes, + fetchTransactions, + } = useRealunitContext(); useLayoutOptions({ backButton: true }); @@ -31,7 +46,9 @@ export default function RealunitScreen(): JSX.Element { if (!holders.length) fetchHolders(); if (!tokenInfo) fetchTokenInfo(); if (!priceHistory.length) fetchPriceHistory(); - }, [fetchHolders, fetchTokenInfo]); + if (!quotes.length) fetchQuotes(); + if (!transactions.length) fetchTransactions(); + }, [fetchHolders, fetchTokenInfo, fetchQuotes, fetchTransactions]); const topHolders = holders.slice(0, 3); @@ -166,6 +183,146 @@ export default function RealunitScreen(): JSX.Element { /> )} + +
+

{translate('screens/realunit', 'Quotes')}

+ + + + + + + + + + + + + + {quotes.map((quote) => ( + + + + + + + + + + ))} + {!quotes.length && !quotesLoading && ( + + + + )} + +
+ {translate('screens/realunit', 'UID')} + + {translate('screens/realunit', 'Type')} + + {translate('screens/realunit', 'Status')} + + {translate('screens/realunit', 'Amount')} + + {translate('screens/realunit', 'Est. Amount')} + + {translate('screens/realunit', 'User')} + + {translate('screens/realunit', 'Created')} +
{quote.uid}{quote.type}{quote.status}{quote.amount?.toLocaleString()} + {quote.estimatedAmount?.toLocaleString()} + + {quote.userAddress ? blankedAddress(quote.userAddress, { displayLength: 12 }) : '-'} + + {new Date(quote.created).toLocaleString()} +
+ {translate('screens/realunit', 'No quotes found')} +
+ {quotesLoading ? ( +
+ +
+ ) : ( + quotes.length > 0 && ( +
+ +
+ ) + )} +
+ +
+

{translate('screens/realunit', 'Transactions')}

+ + + + + + + + + + + + + {transactions.map((tx) => ( + + + + + + + + + ))} + {!transactions.length && !transactionsLoading && ( + + + + )} + +
+ {translate('screens/realunit', 'UID')} + + {translate('screens/realunit', 'Type')} + + {translate('screens/realunit', 'Assets')} + + {translate('screens/realunit', 'Amount CHF')} + + {translate('screens/realunit', 'User')} + + {translate('screens/realunit', 'Date')} +
{tx.uid}{tx.type}{tx.assets} + {tx.amountInChf?.toLocaleString()} + + {tx.userAddress ? blankedAddress(tx.userAddress, { displayLength: 12 }) : '-'} + + {new Date(tx.outputDate ?? tx.created).toLocaleString()} +
+ {translate('screens/realunit', 'No transactions found')} +
+ {transactionsLoading ? ( +
+ +
+ ) : ( + transactions.length > 0 && ( +
+ +
+ ) + )} +
)} From cdc8ae34b3ad903c91c22e25ec76f9c838b93e2e Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:53:39 +0100 Subject: [PATCH 3/7] feat: add quote and transaction detail subpages (#963) Clicking a quote or transaction row now navigates to a dedicated detail page instead of the user account page. Both detail screens show all available fields as a key/value table with a clickable user address linking to the user account page. --- src/App.tsx | 20 ++ src/contexts/realunit.context.tsx | 3 + src/screens/realunit-quote-detail.screen.tsx | 117 +++++++++ src/screens/realunit-quotes.screen.tsx | 106 ++++++++ .../realunit-transaction-detail.screen.tsx | 111 +++++++++ src/screens/realunit-transactions.screen.tsx | 108 ++++++++ src/screens/realunit-user.screen.tsx | 2 +- src/screens/realunit.screen.tsx | 235 +++++++++--------- src/translations/languages/de.json | 21 +- src/translations/languages/fr.json | 21 +- src/translations/languages/it.json | 21 +- 11 files changed, 645 insertions(+), 120 deletions(-) create mode 100644 src/screens/realunit-quote-detail.screen.tsx create mode 100644 src/screens/realunit-quotes.screen.tsx create mode 100644 src/screens/realunit-transaction-detail.screen.tsx create mode 100644 src/screens/realunit-transactions.screen.tsx diff --git a/src/App.tsx b/src/App.tsx index dac973bc6..dd977871d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -61,6 +61,10 @@ const ComplianceKycStatsScreen = lazy(() => import('./screens/compliance-kyc-sta const ComplianceTransactionListScreen = lazy(() => import('./screens/compliance-transaction-list.screen')); const RealunitScreen = lazy(() => import('./screens/realunit.screen')); const RealunitHoldersScreen = lazy(() => import('./screens/realunit-holders.screen')); +const RealunitQuotesScreen = lazy(() => import('./screens/realunit-quotes.screen')); +const RealunitTransactionsScreen = lazy(() => import('./screens/realunit-transactions.screen')); +const RealunitQuoteDetailScreen = lazy(() => import('./screens/realunit-quote-detail.screen')); +const RealunitTransactionDetailScreen = lazy(() => import('./screens/realunit-transaction-detail.screen')); const RealunitUserScreen = lazy(() => import('./screens/realunit-user.screen')); const PersonalIbanScreen = lazy(() => import('./screens/personal-iban.screen')); const BuyCryptoUpdateScreen = lazy(() => import('./screens/buy-crypto-update.screen')); @@ -365,6 +369,22 @@ export const Routes = [ path: 'holders', element: withSuspense(), }, + { + path: 'quotes', + element: withSuspense(), + }, + { + path: 'quotes/:id', + element: withSuspense(), + }, + { + path: 'transactions', + element: withSuspense(), + }, + { + path: 'transactions/:id', + element: withSuspense(), + }, { path: 'user/:address', element: withSuspense(), diff --git a/src/contexts/realunit.context.tsx b/src/contexts/realunit.context.tsx index 0bdbc0650..d1d8b4ebc 100644 --- a/src/contexts/realunit.context.tsx +++ b/src/contexts/realunit.context.tsx @@ -60,6 +60,9 @@ export function RealunitContextProvider({ children }: PropsWithChildren): JSX.El .then((accountData) => { setAccountSummary(accountData); }) + .catch(() => { + setAccountSummary(undefined); + }) .finally(() => setIsLoading(false)); }, [setAccountSummary, setIsLoading], diff --git a/src/screens/realunit-quote-detail.screen.tsx b/src/screens/realunit-quote-detail.screen.tsx new file mode 100644 index 000000000..5f15a89e1 --- /dev/null +++ b/src/screens/realunit-quote-detail.screen.tsx @@ -0,0 +1,117 @@ +import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; +import { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { useRealunitContext } from 'src/contexts/realunit.context'; +import { useSettingsContext } from 'src/contexts/settings.context'; +import { useAdminGuard } from 'src/hooks/guard.hook'; +import { useLayoutOptions } from 'src/hooks/layout-config.hook'; +import { useNavigation } from 'src/hooks/navigation.hook'; +import { blankedAddress } from 'src/util/utils'; + +export default function RealunitQuoteDetailScreen(): JSX.Element { + useAdminGuard(); + + const { translate } = useSettingsContext(); + const { navigate } = useNavigation(); + const { id } = useParams<{ id: string }>(); + const { quotes, quotesLoading, fetchQuotes } = useRealunitContext(); + + useLayoutOptions({ title: translate('screens/realunit', 'Quote Detail'), backButton: true }); + + useEffect(() => { + if (!quotes.length) fetchQuotes(); + }, [fetchQuotes]); + + const quote = quotes.find((q) => q.id === Number(id)); + + const displayType = (type: string): string => { + switch (type) { + case 'BuyFiat': + return 'Sell'; + case 'BuyCrypto': + return 'Buy'; + default: + return type; + } + }; + + if (quotesLoading && !quotes.length) { + return ; + } + + if (!quote) { + return

{translate('screens/realunit', 'Quote not found')}

; + } + + return ( +
+

{translate('screens/realunit', 'Quote Detail')}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {translate('screens/realunit', 'Key')} + + {translate('screens/realunit', 'Value')} +
+ {translate('screens/realunit', 'Type')} + {displayType(quote.type)}
+ {translate('screens/realunit', 'Status')} + {quote.status}
+ {translate('screens/realunit', 'Amount')} + {quote.amount?.toLocaleString()}
+ {translate('screens/realunit', 'Estimated Amount')} + + {quote.estimatedAmount?.toLocaleString()} +
+ {translate('screens/realunit', 'User')} + + {quote.userAddress ? ( + + ) : ( + '-' + )} +
+ {translate('screens/realunit', 'Created')} + + {new Date(quote.created).toLocaleString()} +
+
+ ); +} diff --git a/src/screens/realunit-quotes.screen.tsx b/src/screens/realunit-quotes.screen.tsx new file mode 100644 index 000000000..24f90b8bb --- /dev/null +++ b/src/screens/realunit-quotes.screen.tsx @@ -0,0 +1,106 @@ +import { SpinnerSize, StyledButton, StyledButtonWidth, StyledLoadingSpinner } from '@dfx.swiss/react-components'; +import { useEffect } from 'react'; +import { useRealunitContext } from 'src/contexts/realunit.context'; +import { useSettingsContext } from 'src/contexts/settings.context'; +import { useAdminGuard } from 'src/hooks/guard.hook'; +import { useLayoutOptions } from 'src/hooks/layout-config.hook'; +import { useNavigation } from 'src/hooks/navigation.hook'; +import { blankedAddress } from 'src/util/utils'; + +export default function RealunitQuotesScreen(): JSX.Element { + useAdminGuard(); + + const { translate } = useSettingsContext(); + const { navigate } = useNavigation(); + const { quotes, quotesLoading, fetchQuotes } = useRealunitContext(); + + useLayoutOptions({ title: translate('screens/realunit', 'Pending Transactions'), backButton: true }); + + const displayType = (type: string): string => { + switch (type) { + case 'BuyFiat': + return 'Sell'; + case 'BuyCrypto': + return 'Buy'; + default: + return type; + } + }; + + useEffect(() => { + if (!quotes.length) fetchQuotes(); + }, [fetchQuotes]); + + return ( + <> + {quotesLoading && !quotes.length ? ( + + ) : ( +
+
+

{translate('screens/realunit', 'Pending Transactions')}

+ + + + + + + + + + + {quotes.map((quote) => ( + navigate(`/realunit/quotes/${quote.id}`)} + > + + + + + + ))} + {!quotes.length && !quotesLoading && ( + + + + )} + +
+ {translate('screens/realunit', 'Type')} + + {translate('screens/realunit', 'Amount')} + + {translate('screens/realunit', 'User')} + + {translate('screens/realunit', 'Created')} +
{displayType(quote.type)}{quote.amount?.toLocaleString()} + {quote.userAddress ? blankedAddress(quote.userAddress, { displayLength: 12 }) : '-'} + + {new Date(quote.created).toLocaleString()} +
+ {translate('screens/realunit', 'No pending transactions found')} +
+
+ + {quotes.length > 0 && ( +
+ +
+ )} + {quotesLoading && quotes.length > 0 && ( +
+ +
+ )} +
+ )} + + ); +} diff --git a/src/screens/realunit-transaction-detail.screen.tsx b/src/screens/realunit-transaction-detail.screen.tsx new file mode 100644 index 000000000..5584d0b26 --- /dev/null +++ b/src/screens/realunit-transaction-detail.screen.tsx @@ -0,0 +1,111 @@ +import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; +import { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { useRealunitContext } from 'src/contexts/realunit.context'; +import { useSettingsContext } from 'src/contexts/settings.context'; +import { useAdminGuard } from 'src/hooks/guard.hook'; +import { useLayoutOptions } from 'src/hooks/layout-config.hook'; +import { useNavigation } from 'src/hooks/navigation.hook'; +import { blankedAddress } from 'src/util/utils'; + +export default function RealunitTransactionDetailScreen(): JSX.Element { + useAdminGuard(); + + const { translate } = useSettingsContext(); + const { navigate } = useNavigation(); + const { id } = useParams<{ id: string }>(); + const { transactions, transactionsLoading, fetchTransactions } = useRealunitContext(); + + useLayoutOptions({ title: translate('screens/realunit', 'Transaction Detail'), backButton: true }); + + useEffect(() => { + if (!transactions.length) fetchTransactions(); + }, [fetchTransactions]); + + const transaction = transactions.find((t) => t.id === Number(id)); + + const displayType = (type: string): string => { + switch (type) { + case 'BuyFiat': + return 'Sell'; + case 'BuyCrypto': + return 'Buy'; + default: + return type; + } + }; + + if (transactionsLoading && !transactions.length) { + return ; + } + + if (!transaction) { + return

{translate('screens/realunit', 'Transaction not found')}

; + } + + return ( +
+

{translate('screens/realunit', 'Transaction Detail')}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {translate('screens/realunit', 'Key')} + + {translate('screens/realunit', 'Value')} +
+ {translate('screens/realunit', 'Type')} + {displayType(transaction.type)}
+ {translate('screens/realunit', 'Amount CHF')} + + {transaction.amountInChf?.toLocaleString()} +
+ {translate('screens/realunit', 'Assets')} + {transaction.assets}
+ {translate('screens/realunit', 'User')} + + {transaction.userAddress ? ( + + ) : ( + '-' + )} +
+ {translate('screens/realunit', 'Date')} + + {new Date(transaction.outputDate ?? transaction.created).toLocaleString()} +
+
+ ); +} diff --git a/src/screens/realunit-transactions.screen.tsx b/src/screens/realunit-transactions.screen.tsx new file mode 100644 index 000000000..5d36b36e4 --- /dev/null +++ b/src/screens/realunit-transactions.screen.tsx @@ -0,0 +1,108 @@ +import { SpinnerSize, StyledButton, StyledButtonWidth, StyledLoadingSpinner } from '@dfx.swiss/react-components'; +import { useEffect } from 'react'; +import { useRealunitContext } from 'src/contexts/realunit.context'; +import { useSettingsContext } from 'src/contexts/settings.context'; +import { useAdminGuard } from 'src/hooks/guard.hook'; +import { useLayoutOptions } from 'src/hooks/layout-config.hook'; +import { useNavigation } from 'src/hooks/navigation.hook'; +import { blankedAddress } from 'src/util/utils'; + +export default function RealunitTransactionsScreen(): JSX.Element { + useAdminGuard(); + + const { translate } = useSettingsContext(); + const { navigate } = useNavigation(); + const { transactions, transactionsLoading, fetchTransactions } = useRealunitContext(); + + useLayoutOptions({ title: translate('screens/realunit', 'Received Transactions'), backButton: true }); + + const displayType = (type: string): string => { + switch (type) { + case 'BuyFiat': + return 'Sell'; + case 'BuyCrypto': + return 'Buy'; + default: + return type; + } + }; + + useEffect(() => { + if (!transactions.length) fetchTransactions(); + }, [fetchTransactions]); + + return ( + <> + {transactionsLoading && !transactions.length ? ( + + ) : ( +
+
+

{translate('screens/realunit', 'Received Transactions')}

+ + + + + + + + + + + {transactions.map((tx) => ( + navigate(`/realunit/transactions/${tx.id}`)} + > + + + + + + ))} + {!transactions.length && !transactionsLoading && ( + + + + )} + +
+ {translate('screens/realunit', 'Type')} + + {translate('screens/realunit', 'Amount CHF')} + + {translate('screens/realunit', 'User')} + + {translate('screens/realunit', 'Date')} +
{displayType(tx.type)} + {tx.amountInChf?.toLocaleString()} + + {tx.userAddress ? blankedAddress(tx.userAddress, { displayLength: 12 }) : '-'} + + {new Date(tx.outputDate ?? tx.created).toLocaleString()} +
+ {translate('screens/realunit', 'No received transactions found')} +
+
+ + {transactions.length > 0 && ( +
+ +
+ )} + {transactionsLoading && transactions.length > 0 && ( +
+ +
+ )} +
+ )} + + ); +} diff --git a/src/screens/realunit-user.screen.tsx b/src/screens/realunit-user.screen.tsx index be71406a3..0985c28ae 100644 --- a/src/screens/realunit-user.screen.tsx +++ b/src/screens/realunit-user.screen.tsx @@ -61,7 +61,7 @@ export default function RealunitUserScreen(): JSX.Element { return ( <> - {!accountSummary ? ( + {isLoading && !accountSummary ? ( ) : !accountSummary ? (

{translate('screens/realunit', 'No data available')}

diff --git a/src/screens/realunit.screen.tsx b/src/screens/realunit.screen.tsx index 86d4e8601..def8aafe9 100644 --- a/src/screens/realunit.screen.tsx +++ b/src/screens/realunit.screen.tsx @@ -3,6 +3,7 @@ import { IconColor, SpinnerSize, StyledButton, + StyledButtonColor, StyledButtonWidth, StyledLoadingSpinner, } from '@dfx.swiss/react-components'; @@ -51,6 +52,19 @@ export default function RealunitScreen(): JSX.Element { }, [fetchHolders, fetchTokenInfo, fetchQuotes, fetchTransactions]); const topHolders = holders.slice(0, 3); + const topQuotes = quotes.slice(0, 3); + const topTransactions = transactions.slice(0, 3); + + const displayType = (type: string): string => { + switch (type) { + case 'BuyFiat': + return 'Sell'; + case 'BuyCrypto': + return 'Buy'; + default: + return type; + } + }; const handleAddressClick = (address: string) => { const encodedAddress = encodeURIComponent(address); @@ -64,65 +78,6 @@ export default function RealunitScreen(): JSX.Element { ) : (
- {isLoading ? ( -
- -
- ) : ( - tokenInfo && ( -
-

{translate('screens/realunit', 'RealUnit ')}

- - - - - - - - - - - - - - - - - - - - - - - - - - -
- {translate('screens/realunit', 'Overview')} - - {translate('screens/realunit', '')} -
- {translate('screens/realunit', 'Holders')} - - {totalCount?.toLocaleString() ?? '0'} -
- {translate('screens/realunit', 'Shares')} - - {Number(tokenInfo.totalShares.total).toLocaleString()} -
- {translate('screens/realunit', 'Total Supply')} - - {Number(tokenInfo.totalSupply.value).toLocaleString()} REALU -
- {translate('screens/realunit', 'Timestamp')} - - {new Date(tokenInfo.totalSupply.timestamp).toLocaleString()} -
-
- ) - )} -

{translate('screens/realunit', 'Price History')}

+ {isLoading ? ( +
+ +
+ ) : ( + tokenInfo && ( +
+

{translate('screens/realunit', 'RealUnit ')}

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {translate('screens/realunit', 'Overview')} + + {translate('screens/realunit', '')} +
+ {translate('screens/realunit', 'Holders')} + + {totalCount?.toLocaleString() ?? '0'} +
+ {translate('screens/realunit', 'Shares')} + + {Number(tokenInfo.totalShares.total).toLocaleString()} +
+ {translate('screens/realunit', 'Total Supply')} + + {Number(tokenInfo.totalSupply.value).toLocaleString()} REALU +
+ {translate('screens/realunit', 'Timestamp')} + + {new Date(tokenInfo.totalSupply.timestamp).toLocaleString()} +
+
+ ) + )} +

{translate('screens/realunit', 'Top Holders')}

@@ -179,31 +193,23 @@ export default function RealunitScreen(): JSX.Element { navigate('/realunit/holders')} - width={StyledButtonWidth.MIN} + width={StyledButtonWidth.FULL} + color={StyledButtonColor.STURDY_WHITE} /> )}
-

{translate('screens/realunit', 'Quotes')}

+

{translate('screens/realunit', 'Pending Transactions')}

- - - @@ -213,15 +219,14 @@ export default function RealunitScreen(): JSX.Element { - {quotes.map((quote) => ( - - - - + {topQuotes.map((quote) => ( + navigate(`/realunit/quotes/${quote.id}`)} + > + - @@ -232,44 +237,39 @@ export default function RealunitScreen(): JSX.Element { ))} {!quotes.length && !quotesLoading && ( - )}
- {translate('screens/realunit', 'UID')} - {translate('screens/realunit', 'Type')} - {translate('screens/realunit', 'Status')} - {translate('screens/realunit', 'Amount')} - {translate('screens/realunit', 'Est. Amount')} - {translate('screens/realunit', 'User')}
{quote.uid}{quote.type}{quote.status}
{displayType(quote.type)} {quote.amount?.toLocaleString()} - {quote.estimatedAmount?.toLocaleString()} - {quote.userAddress ? blankedAddress(quote.userAddress, { displayLength: 12 }) : '-'}
- {translate('screens/realunit', 'No quotes found')} + + {translate('screens/realunit', 'No pending transactions found')}
- {quotesLoading ? ( + {quotesLoading && !quotes.length && (
- ) : ( - quotes.length > 0 && ( -
- -
- ) )}
+ {quotes.length > 3 && ( +
+ navigate('/realunit/quotes')} + width={StyledButtonWidth.FULL} + color={StyledButtonColor.STURDY_WHITE} + /> +
+ )} +
-

{translate('screens/realunit', 'Transactions')}

+

{translate('screens/realunit', 'Received Transactions')}

- - @@ -282,11 +282,13 @@ export default function RealunitScreen(): JSX.Element { - {transactions.map((tx) => ( - - - - + {topTransactions.map((tx) => ( + navigate(`/realunit/transactions/${tx.id}`)} + > + @@ -300,29 +302,30 @@ export default function RealunitScreen(): JSX.Element { ))} {!transactions.length && !transactionsLoading && ( - )}
- {translate('screens/realunit', 'UID')} - {translate('screens/realunit', 'Type')} - {translate('screens/realunit', 'Assets')} - {translate('screens/realunit', 'Amount CHF')}
{tx.uid}{tx.type}{tx.assets}
{displayType(tx.type)} {tx.amountInChf?.toLocaleString()}
- {translate('screens/realunit', 'No transactions found')} + + {translate('screens/realunit', 'No received transactions found')}
- {transactionsLoading ? ( + {transactionsLoading && !transactions.length && (
- ) : ( - transactions.length > 0 && ( -
- -
- ) )}
+ + {transactions.length > 3 && ( +
+ navigate('/realunit/transactions')} + width={StyledButtonWidth.FULL} + color={StyledButtonColor.STURDY_WHITE} + /> +
+ )}
)} diff --git a/src/translations/languages/de.json b/src/translations/languages/de.json index ab393b439..4ef2178ad 100644 --- a/src/translations/languages/de.json +++ b/src/translations/languages/de.json @@ -5,6 +5,7 @@ "Start": "Starten", "Previous": "Zurück", "Next": "Weiter", + "More": "Mehr", "Continue": "Weiter", "Close": "Schliessen", "Ok": "Ok", @@ -1014,7 +1015,25 @@ "Details": "Details", "Tx Hash": "Tx Hash", "No transactions found": "Keine Transaktionen gefunden", - "Percentage": "Prozentsatz" + "Percentage": "Prozentsatz", + "RealUnit ": "RealUnit ", + "Overview": "Übersicht", + "Holders": "Inhaber", + "Shares": "Aktien", + "Total Supply": "Gesamtangebot", + "Price History": "Preisverlauf", + "Top Holders": "Top Inhaber", + "All Holders": "Alle Inhaber", + "Pending Transactions": "Pendente Transaktionen", + "No pending transactions found": "Keine pendenten Transaktionen gefunden", + "Received Transactions": "Erhaltene Transaktionen", + "No received transactions found": "Keine erhaltenen Transaktionen gefunden", + "Type": "Typ", + "Amount": "Betrag", + "User": "Benutzer", + "Created": "Erstellt", + "Amount CHF": "Betrag CHF", + "Date": "Datum" }, "screens/blockchain": { "Transaction signing": "Transaktionssignierung", diff --git a/src/translations/languages/fr.json b/src/translations/languages/fr.json index 08a851553..09f93ed88 100644 --- a/src/translations/languages/fr.json +++ b/src/translations/languages/fr.json @@ -5,6 +5,7 @@ "Start": "Lancer", "Previous": "Précédent", "Next": "Suivant", + "More": "Plus", "Continue": "Continuer", "Close": "Fermer", "Ok": "Ok", @@ -1013,7 +1014,25 @@ "Details": "Détails", "Tx Hash": "Tx Hash", "No transactions found": "Aucune transaction trouvée", - "Percentage": "Pourcentage" + "Percentage": "Pourcentage", + "RealUnit ": "RealUnit ", + "Overview": "Aperçu", + "Holders": "Détenteurs", + "Shares": "Actions", + "Total Supply": "Offre totale", + "Price History": "Historique des prix", + "Top Holders": "Principaux détenteurs", + "All Holders": "Tous les détenteurs", + "Pending Transactions": "Transactions en attente", + "No pending transactions found": "Aucune transaction en attente trouvée", + "Received Transactions": "Transactions reçues", + "No received transactions found": "Aucune transaction reçue trouvée", + "Type": "Type", + "Amount": "Montant", + "User": "Utilisateur", + "Created": "Créé", + "Amount CHF": "Montant CHF", + "Date": "Date" }, "screens/blockchain": { "Transaction signing": "Signature de la transaction", diff --git a/src/translations/languages/it.json b/src/translations/languages/it.json index c6fb0de23..9a914fe89 100644 --- a/src/translations/languages/it.json +++ b/src/translations/languages/it.json @@ -5,6 +5,7 @@ "Start": "Iniziare", "Previous": "Precedente", "Next": "Avanti", + "More": "Di più", "Continue": "Continua", "Close": "Chiudere", "Ok": "Ok", @@ -1013,7 +1014,25 @@ "Details": "Dettagli", "Tx Hash": "Tx Hash", "No transactions found": "Nessuna transazione trovata", - "Percentage": "Percentuale" + "Percentage": "Percentuale", + "RealUnit ": "RealUnit ", + "Overview": "Panoramica", + "Holders": "Titolari", + "Shares": "Azioni", + "Total Supply": "Offerta totale", + "Price History": "Storico dei prezzi", + "Top Holders": "Principali titolari", + "All Holders": "Tutti i titolari", + "Pending Transactions": "Transazioni in sospeso", + "No pending transactions found": "Nessuna transazione in sospeso trovata", + "Received Transactions": "Transazioni ricevute", + "No received transactions found": "Nessuna transazione ricevuta trovata", + "Type": "Tipo", + "Amount": "Importo", + "User": "Utente", + "Created": "Creato", + "Amount CHF": "Importo CHF", + "Date": "Data" }, "screens/blockchain": { "Transaction signing": "Firma della transazione", From 723fb107bdb04b38614756f691e7df858b972e98 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:09:27 +0100 Subject: [PATCH 4/7] feat: add confirm payment button to RealUnit quote detail (#964) * feat: add confirm payment button to RealUnit quote detail Add "Confirm Payment Received" button on quote detail screen, visible only for quotes in WaitingForPayment status. Includes confirmation overlay dialog and translations in DE/FR/IT. * fix: reset quotes cache after payment confirmation Clear quotes state after confirming payment so the quotes list re-fetches fresh data when navigating back, preventing stale WaitingForPayment status display. --- src/contexts/realunit.context.tsx | 9 +++++ src/dto/realunit.dto.ts | 2 ++ src/hooks/realunit-api.hook.ts | 8 +++++ src/screens/realunit-quote-detail.screen.tsx | 36 ++++++++++++++++++-- src/translations/languages/de.json | 5 ++- src/translations/languages/fr.json | 5 ++- src/translations/languages/it.json | 5 ++- 7 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/contexts/realunit.context.tsx b/src/contexts/realunit.context.tsx index d1d8b4ebc..4252871ed 100644 --- a/src/contexts/realunit.context.tsx +++ b/src/contexts/realunit.context.tsx @@ -51,6 +51,7 @@ export function RealunitContextProvider({ children }: PropsWithChildren): JSX.El getTokenPrice, getAdminQuotes, getAdminTransactions, + confirmPayment, } = useRealunitApi(); const fetchAccountSummary = useCallback( @@ -124,6 +125,10 @@ export function RealunitContextProvider({ children }: PropsWithChildren): JSX.El .finally(() => setQuotesLoading(false)); }, [quotes.length]); + const resetQuotes = useCallback(() => { + setQuotes([]); + }, []); + const fetchTransactions = useCallback(() => { setTransactionsLoading(true); getAdminTransactions(50, transactions.length) @@ -156,7 +161,9 @@ export function RealunitContextProvider({ children }: PropsWithChildren): JSX.El fetchPriceHistory, fetchTokenPrice, fetchQuotes, + resetQuotes, fetchTransactions, + confirmPayment, }), [ accountSummary, @@ -180,7 +187,9 @@ export function RealunitContextProvider({ children }: PropsWithChildren): JSX.El fetchTokenPrice, fetchPriceHistory, fetchQuotes, + resetQuotes, fetchTransactions, + confirmPayment, ], ); diff --git a/src/dto/realunit.dto.ts b/src/dto/realunit.dto.ts index 6e5002a57..9bae24d43 100644 --- a/src/dto/realunit.dto.ts +++ b/src/dto/realunit.dto.ts @@ -138,5 +138,7 @@ export interface RealunitContextInterface { fetchPriceHistory: (timeframe?: Timeframe) => void; fetchTokenPrice: () => void; fetchQuotes: () => void; + resetQuotes: () => void; fetchTransactions: () => void; + confirmPayment: (id: number) => Promise; } diff --git a/src/hooks/realunit-api.hook.ts b/src/hooks/realunit-api.hook.ts index c701ecacd..bbc2b51f1 100644 --- a/src/hooks/realunit-api.hook.ts +++ b/src/hooks/realunit-api.hook.ts @@ -94,6 +94,13 @@ export function useRealunitApi() { }); } + async function confirmPayment(id: number): Promise { + return call({ + url: `realunit/admin/quotes/${id}/confirm-payment`, + method: 'PUT', + }); + } + return useMemo( () => ({ getAccountSummary, @@ -104,6 +111,7 @@ export function useRealunitApi() { getPriceHistory, getAdminQuotes, getAdminTransactions, + confirmPayment, }), [call], ); diff --git a/src/screens/realunit-quote-detail.screen.tsx b/src/screens/realunit-quote-detail.screen.tsx index 5f15a89e1..de199583b 100644 --- a/src/screens/realunit-quote-detail.screen.tsx +++ b/src/screens/realunit-quote-detail.screen.tsx @@ -1,6 +1,7 @@ -import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; -import { useEffect } from 'react'; +import { SpinnerSize, StyledButton, StyledButtonWidth, StyledLoadingSpinner } from '@dfx.swiss/react-components'; +import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; +import { ConfirmationOverlay } from 'src/components/overlay/confirmation-overlay'; import { useRealunitContext } from 'src/contexts/realunit.context'; import { useSettingsContext } from 'src/contexts/settings.context'; import { useAdminGuard } from 'src/hooks/guard.hook'; @@ -14,7 +15,8 @@ export default function RealunitQuoteDetailScreen(): JSX.Element { const { translate } = useSettingsContext(); const { navigate } = useNavigation(); const { id } = useParams<{ id: string }>(); - const { quotes, quotesLoading, fetchQuotes } = useRealunitContext(); + const { quotes, quotesLoading, fetchQuotes, resetQuotes, confirmPayment } = useRealunitContext(); + const [showConfirmation, setShowConfirmation] = useState(false); useLayoutOptions({ title: translate('screens/realunit', 'Quote Detail'), backButton: true }); @@ -112,6 +114,34 @@ export default function RealunitQuoteDetailScreen(): JSX.Element { + + {quote.status === 'WaitingForPayment' && ( +
+ setShowConfirmation(true)} + width={StyledButtonWidth.FULL} + /> +
+ )} + + {showConfirmation && ( + setShowConfirmation(false)} + onConfirm={async () => { + await confirmPayment(quote.id); + resetQuotes(); + setShowConfirmation(false); + navigate(-1); + }} + /> + )} ); } diff --git a/src/translations/languages/de.json b/src/translations/languages/de.json index 4ef2178ad..ddad6bbfd 100644 --- a/src/translations/languages/de.json +++ b/src/translations/languages/de.json @@ -1033,7 +1033,10 @@ "User": "Benutzer", "Created": "Erstellt", "Amount CHF": "Betrag CHF", - "Date": "Datum" + "Date": "Datum", + "Confirm Payment Received": "Zahlungseingang bestätigen", + "Are you sure you want to confirm the payment receipt?": "Möchten Sie den Zahlungseingang wirklich bestätigen?", + "Payment confirmed successfully": "Zahlungseingang erfolgreich bestätigt" }, "screens/blockchain": { "Transaction signing": "Transaktionssignierung", diff --git a/src/translations/languages/fr.json b/src/translations/languages/fr.json index 09f93ed88..9740d4712 100644 --- a/src/translations/languages/fr.json +++ b/src/translations/languages/fr.json @@ -1032,7 +1032,10 @@ "User": "Utilisateur", "Created": "Créé", "Amount CHF": "Montant CHF", - "Date": "Date" + "Date": "Date", + "Confirm Payment Received": "Confirmer la réception du paiement", + "Are you sure you want to confirm the payment receipt?": "Êtes-vous sûr de vouloir confirmer la réception du paiement?", + "Payment confirmed successfully": "Réception du paiement confirmée avec succès" }, "screens/blockchain": { "Transaction signing": "Signature de la transaction", diff --git a/src/translations/languages/it.json b/src/translations/languages/it.json index 9a914fe89..606765cef 100644 --- a/src/translations/languages/it.json +++ b/src/translations/languages/it.json @@ -1032,7 +1032,10 @@ "User": "Utente", "Created": "Creato", "Amount CHF": "Importo CHF", - "Date": "Data" + "Date": "Data", + "Confirm Payment Received": "Conferma ricezione pagamento", + "Are you sure you want to confirm the payment receipt?": "Sei sicuro di voler confermare la ricezione del pagamento?", + "Payment confirmed successfully": "Ricezione del pagamento confermata con successo" }, "screens/blockchain": { "Transaction signing": "Firma della transazione", From f165b82f7c0c6e07bbec655e65515b7857e39b52 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:16:36 +0100 Subject: [PATCH 5/7] fix: remove flaky API integration tests (#962) * fix: remove flaky API integration tests Remove tests that call dev.api.dfx.swiss directly, causing intermittent CI failures due to network timeouts. * fix: clean up package.json references to removed API tests Remove test:api script, update test:all to use test instead, and remove __tests__/api/helpers/ from testPathIgnorePatterns. * fix: remove stale comments referencing deleted API tests --- e2e/buy-process.spec.ts | 2 - e2e/login-process.spec.ts | 2 - e2e/sell-process.spec.ts | 2 - e2e/swap-process.spec.ts | 2 - package.json | 6 +- src/__tests__/api/auth-api.test.ts | 121 --------- src/__tests__/api/buy-api.test.ts | 311 ---------------------- src/__tests__/api/helpers/api-client.ts | 174 ------------ src/__tests__/api/helpers/test-wallet.ts | 27 -- src/__tests__/api/sell-api.test.ts | 313 ---------------------- src/__tests__/api/swap-api.test.ts | 325 ----------------------- 11 files changed, 2 insertions(+), 1283 deletions(-) delete mode 100644 src/__tests__/api/auth-api.test.ts delete mode 100644 src/__tests__/api/buy-api.test.ts delete mode 100644 src/__tests__/api/helpers/api-client.ts delete mode 100644 src/__tests__/api/helpers/test-wallet.ts delete mode 100644 src/__tests__/api/sell-api.test.ts delete mode 100644 src/__tests__/api/swap-api.test.ts diff --git a/e2e/buy-process.spec.ts b/e2e/buy-process.spec.ts index 60a54f8ea..6cdf63ee6 100644 --- a/e2e/buy-process.spec.ts +++ b/e2e/buy-process.spec.ts @@ -1,8 +1,6 @@ import { test, expect } from '@playwright/test'; import { BlockchainType, getCachedAuth } from './helpers/auth-cache'; -// Note: API Integration tests have been moved to Jest (src/__tests__/api/buy-api.test.ts) -// This file now contains only UI Flow tests that require browser interaction test.describe('Buy Process - UI Flow', () => { async function getToken( diff --git a/e2e/login-process.spec.ts b/e2e/login-process.spec.ts index fd33f6489..befb84d65 100644 --- a/e2e/login-process.spec.ts +++ b/e2e/login-process.spec.ts @@ -1,8 +1,6 @@ import { test, expect } from '@playwright/test'; import { getCachedAuth } from './helpers/auth-cache'; -// Note: API Integration tests for authentication have been moved to Jest (src/__tests__/api/auth-api.test.ts) -// This file now contains only UI Flow tests that require browser interaction test.describe('Login Process - UI Flow', () => { let token: string; diff --git a/e2e/sell-process.spec.ts b/e2e/sell-process.spec.ts index e29a35ae3..e02cf0428 100644 --- a/e2e/sell-process.spec.ts +++ b/e2e/sell-process.spec.ts @@ -1,8 +1,6 @@ import { test, expect } from '@playwright/test'; import { getCachedAuth } from './helpers/auth-cache'; -// Note: API Integration tests have been moved to Jest (src/__tests__/api/sell-api.test.ts) -// This file now contains only UI Flow tests that require browser interaction test.describe('Sell Process - UI Flow', () => { let token: string; diff --git a/e2e/swap-process.spec.ts b/e2e/swap-process.spec.ts index e6fba6fc7..8edf1858f 100644 --- a/e2e/swap-process.spec.ts +++ b/e2e/swap-process.spec.ts @@ -1,8 +1,6 @@ import { test, expect } from '@playwright/test'; import { getCachedAuth } from './helpers/auth-cache'; -// Note: API Integration tests have been moved to Jest (src/__tests__/api/swap-api.test.ts) -// This file now contains only UI Flow tests that require browser interaction test.describe('Swap Process - UI Flow', () => { let token: string; diff --git a/package.json b/package.json index 32e4733eb..63cacb032 100644 --- a/package.json +++ b/package.json @@ -83,14 +83,13 @@ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --no-fix", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "react-app-rewired test --watchAll=false --passWithNoTests", - "test:api": "react-app-rewired test --watchAll=false --testPathPattern=__tests__/api", "test:e2e": "./scripts/e2e-test.sh", "test:e2e:local": "npx playwright test", "test:e2e:metamask": "npx playwright test --config=playwright.synpress.config.ts", "synpress:install-chrome": "npx @puppeteer/browsers install chrome@126.0.6478.0", "synpress:download-metamask": "mkdir -p .cache-synpress && curl -L https://github.com/MetaMask/metamask-extension/releases/download/v11.9.1/metamask-chrome-11.9.1.zip -o .cache-synpress/metamask.zip && unzip -o .cache-synpress/metamask.zip -d .cache-synpress/metamask-chrome-11.9.1", "synpress:setup": "npm run synpress:install-chrome && npm run synpress:download-metamask", - "test:all": "npm run test:api && npm run test:e2e", + "test:all": "npm run test && npm run test:e2e", "eject": "react-scripts eject", "serve": "serve build -l 4000", "analyze": "source-map-explorer 'build/static/js/*.js'", @@ -180,8 +179,7 @@ "node_modules/(?!(@dfx.swiss|@scure|@noble|@solana|bitcoinjs-lib|bitcoinjs-message|tronweb|tweetnacl)/)" ], "testPathIgnorePatterns": [ - "/node_modules/", - "__tests__/api/helpers/" + "/node_modules/" ], "setupFilesAfterEnv": [ "/src/setupTests.ts" diff --git a/src/__tests__/api/auth-api.test.ts b/src/__tests__/api/auth-api.test.ts deleted file mode 100644 index 9bcf384a0..000000000 --- a/src/__tests__/api/auth-api.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { createTestCredentials, TestCredentials } from './helpers/test-wallet'; - -const API_URL = 'https://dev.api.dfx.swiss/v1'; - -interface AuthResponse { - accessToken: string; -} - -interface SignInfoResponse { - message: string; - blockchains: string[]; -} - -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function authenticate( - credentials: TestCredentials, -): Promise<{ success: boolean; token?: string; error?: string }> { - try { - const response = await fetch(`${API_URL}/auth`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(credentials), - }); - - if (response.ok) { - const data: AuthResponse = await response.json(); - return { success: true, token: data.accessToken }; - } - - const errorBody = await response.json().catch(() => ({})); - return { success: false, error: (errorBody as { message?: string }).message || `HTTP ${response.status}` }; - } catch (e) { - return { success: false, error: String(e) }; - } -} - -async function getSignInfo( - address: string, - retries = 3, -): Promise { - for (let i = 0; i < retries; i++) { - try { - const response = await fetch(`${API_URL}/auth/signMessage?address=${address}`); - if (response.ok) { - return response.json(); - } - if (response.status === 429) { - await delay(1000 * (i + 1)); - continue; - } - return null; - } catch { - if (i < retries - 1) { - await delay(1000 * (i + 1)); - continue; - } - return null; - } - } - return null; -} - -// API Integration tests for Authentication (EVM only) -describe('Authentication - API Integration', () => { - describe('EVM Authentication', () => { - let evmCredentials: TestCredentials; - - beforeAll(async () => { - evmCredentials = await createTestCredentials(); - console.log(`EVM test address: ${evmCredentials.address}`); - }, 30000); - - test('should authenticate with Ethereum address', async () => { - const result = await authenticate(evmCredentials); - expect(result.success).toBeTruthy(); - expect(result.token).toBeTruthy(); - console.log('Ethereum login successful'); - }); - - test('should get correct sign info for EVM address', async () => { - const signInfo = await getSignInfo(evmCredentials.address); - expect(signInfo).toBeTruthy(); - if (!signInfo) return; - expect(signInfo.blockchains).toContain('Ethereum'); - expect(signInfo.blockchains).toContain('Polygon'); - expect(signInfo.blockchains).toContain('Arbitrum'); - expect(signInfo.blockchains).toContain('Optimism'); - expect(signInfo.blockchains).toContain('Base'); - expect(signInfo.blockchains).toContain('BinanceSmartChain'); - console.log(`EVM blockchains: ${signInfo.blockchains.join(', ')}`); - }); - - test('should reject invalid EVM signature', async () => { - const invalidCredentials = { - address: evmCredentials.address, - signature: '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - }; - - const result = await authenticate(invalidCredentials); - expect(result.success).toBeFalsy(); - console.log(`Invalid signature rejected: ${result.error}`); - }); - }); - - describe('Address Format Verification', () => { - test('should generate valid EVM address format', async () => { - const evmCreds = await createTestCredentials(); - expect(evmCreds.address).toMatch(/^0x[a-fA-F0-9]{40}$/); - console.log(`Generated EVM address: ${evmCreds.address}`); - }); - - test('should verify API recognizes EVM address', async () => { - const evmCreds = await createTestCredentials(); - const evmInfo = await getSignInfo(evmCreds.address); - expect(evmInfo?.blockchains).toContain('Ethereum'); - }); - }); -}); diff --git a/src/__tests__/api/buy-api.test.ts b/src/__tests__/api/buy-api.test.ts deleted file mode 100644 index b8872a80e..000000000 --- a/src/__tests__/api/buy-api.test.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { ApiClient, createApiClient } from './helpers/api-client'; -import { TestCredentials } from './helpers/test-wallet'; - -const API_URL = 'https://dev.api.dfx.swiss/v1'; - -// EVM-compatible blockchains for this test address -const EVM_BLOCKCHAINS = ['Ethereum', 'Arbitrum', 'Optimism', 'Polygon', 'Base', 'BinanceSmartChain', 'Gnosis']; - -interface Asset { - id: number; - name: string; - uniqueName: string; - blockchain: string; - buyable: boolean; -} - -interface Fiat { - id: number; - name: string; - sellable: boolean; -} - -interface BuyPaymentInfo { - id: number; - routeId: number; - amount: number; - currency: { id: number; name: string }; - asset: { id: number; name: string }; - estimatedAmount: number; - rate: number; - exchangeRate: number; - minVolume: number; - maxVolume: number; - fees: { total: number }; - isValid: boolean; - error?: string; - iban?: string; - remittanceInfo?: string; -} - -interface BuyQuote { - amount: number; - estimatedAmount: number; - rate: number; - exchangeRate: number; - minVolume: number; - maxVolume: number; - feeAmount: number; - isValid: boolean; - error?: string; -} - -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function getAssets(client: ApiClient): Promise { - const result = await client.get('/asset'); - expect(result.data).toBeTruthy(); - return result.data ?? []; -} - -async function getFiats(): Promise { - const response = await fetch(`${API_URL}/fiat`); - expect(response.ok).toBeTruthy(); - return response.json(); -} - -async function getBuyQuote( - params: { currency: { id: number }; asset: { id: number }; amount: number }, -): Promise { - const response = await fetch(`${API_URL}/buy/quote`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - currency: params.currency, - asset: params.asset, - amount: params.amount, - paymentMethod: 'Bank', - }), - }); - - expect(response.ok).toBeTruthy(); - return response.json(); -} - -async function createBuyPaymentInfo( - client: ApiClient, - params: { currency: { id: number }; asset: { id: number }; amount: number }, -): Promise<{ data: BuyPaymentInfo | null; error: string | null; status: number }> { - return client.put('/buy/paymentInfos', { - currency: params.currency, - asset: params.asset, - amount: params.amount, - paymentMethod: 'Bank', - }); -} - -// API Integration tests for Buy Process (EVM only) -describe('Buy Process - API Integration', () => { - let client: ApiClient; - let credentials: TestCredentials; - let buyableAssets: Asset[]; - let sellableFiats: Fiat[]; - - beforeAll(async () => { - const auth = await createApiClient(); - client = auth.client; - credentials = auth.credentials; - console.log(`Using EVM test address: ${credentials.address}`); - - const [assets, fiats] = await Promise.all([getAssets(client), getFiats()]); - - buyableAssets = assets.filter((a) => a.buyable && EVM_BLOCKCHAINS.includes(a.blockchain)); - sellableFiats = fiats.filter((f) => f.sellable); - - expect(buyableAssets.length).toBeGreaterThan(0); - expect(sellableFiats.length).toBeGreaterThan(0); - }, 60000); - - test('should authenticate with EVM credentials', async () => { - const response = await fetch(`${API_URL}/auth`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(credentials), - }); - - expect(response.ok).toBeTruthy(); - const data = await response.json(); - expect(data.accessToken).toBeTruthy(); - }); - - test('should fetch buyable assets', async () => { - const assets = await getAssets(client); - const buyable = assets.filter((a) => a.buyable); - - expect(buyable.length).toBeGreaterThan(0); - console.log(`Found ${buyable.length} buyable assets`); - }); - - test('should fetch sellable fiats', async () => { - const fiats = await getFiats(); - const sellable = fiats.filter((f) => f.sellable); - - expect(sellable.length).toBeGreaterThan(0); - console.log(`Found ${sellable.length} sellable fiats`); - - const eurExists = sellable.some((f) => f.name === 'EUR'); - const chfExists = sellable.some((f) => f.name === 'CHF'); - expect(eurExists || chfExists).toBeTruthy(); - }); - - test('should get buy quote for EUR -> ETH', async () => { - const eur = sellableFiats.find((f) => f.name === 'EUR'); - const eth = buyableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!eur || !eth) { - console.log('Skipping: EUR or ETH not available'); - return; - } - - const quote = await getBuyQuote({ - currency: { id: eur.id }, - asset: { id: eth.id }, - amount: 100, - }); - - expect(quote.amount).toBe(100); - expect(quote.estimatedAmount).toBeGreaterThan(0); - expect(quote.rate).toBeGreaterThan(0); - expect(quote.minVolume).toBeGreaterThan(0); - expect(quote.maxVolume).toBeGreaterThan(0); - - console.log(`Quote: 100 EUR -> ${quote.estimatedAmount} ETH (rate: ${quote.rate})`); - }); - - test('should get buy quote for CHF -> WBTC', async () => { - const chf = sellableFiats.find((f) => f.name === 'CHF'); - const wbtc = buyableAssets.find((a) => a.name === 'WBTC' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!chf || !wbtc) { - console.log('Skipping: CHF or WBTC not available'); - return; - } - - const quote = await getBuyQuote({ - currency: { id: chf.id }, - asset: { id: wbtc.id }, - amount: 200, - }); - - expect(quote.amount).toBe(200); - expect(quote.estimatedAmount).toBeGreaterThan(0); - expect(quote.rate).toBeGreaterThan(0); - - console.log(`Quote: 200 CHF -> ${quote.estimatedAmount} WBTC (rate: ${quote.rate})`); - }); - - test('should reject amount below minimum', async () => { - const eur = sellableFiats.find((f) => f.name === 'EUR'); - const eth = buyableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!eur || !eth) { - console.log('Skipping: EUR or ETH not available'); - return; - } - - const quote = await getBuyQuote({ - currency: { id: eur.id }, - asset: { id: eth.id }, - amount: 1, - }); - - if (!quote.isValid) { - expect(quote.error).toBeTruthy(); - console.log(`Amount too low error: ${quote.error}`); - } - }); - - test('should create buy payment info for EUR -> ETH', async () => { - const eur = sellableFiats.find((f) => f.name === 'EUR'); - const eth = buyableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!eur || !eth) { - console.log('Skipping: EUR or ETH not available'); - return; - } - - const result = await createBuyPaymentInfo(client, { - currency: { id: eur.id }, - asset: { id: eth.id }, - amount: 100, - }); - - if (result.error) { - console.log(`Payment info creation returned error: ${result.error} (status: ${result.status})`); - const expectedErrors = ['Trading not allowed', 'RecommendationRequired', 'EmailRequired', 'KYC required', 'KycRequired', 'User not found', 'Ident data incomplete']; - const isExpectedError = expectedErrors.some((e) => result.error?.includes(e)); - if (isExpectedError) { - console.log('Skipping test - account restriction'); - return; - } - expect(result.data).toBeTruthy(); - return; - } - - const paymentInfo = result.data; - if (!paymentInfo) return; - expect(paymentInfo.id).toBeGreaterThan(0); - expect(paymentInfo.amount).toBe(100); - expect(paymentInfo.currency.name).toBe('EUR'); - expect(paymentInfo.estimatedAmount).toBeGreaterThan(0); - expect(paymentInfo.rate).toBeGreaterThan(0); - - console.log(`Created payment info ID: ${paymentInfo.id}, Amount: ${paymentInfo.estimatedAmount} ETH`); - }); - - test('should handle multiple currencies', async () => { - const eth = buyableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!eth) { - console.log('Skipping: ETH not available'); - return; - } - - const currencies = ['EUR', 'CHF', 'USD'].map((name) => sellableFiats.find((f) => f.name === name)).filter(Boolean); - - for (const currency of currencies) { - if (!currency) continue; - - const quote = await getBuyQuote({ - currency: { id: currency.id }, - asset: { id: eth.id }, - amount: 100, - }); - - expect(quote.estimatedAmount).toBeGreaterThan(0); - console.log(`${currency.name} 100 -> ${quote.estimatedAmount} ETH`); - - await delay(500); - } - }, 15000); - - test('should handle multiple assets', async () => { - const eur = sellableFiats.find((f) => f.name === 'EUR'); - - if (!eur) { - console.log('Skipping: EUR not available'); - return; - } - - const assets = ['ETH', 'USDT', 'USDC'] - .map((name) => buyableAssets.find((a) => a.name === name)) - .filter(Boolean); - - for (const asset of assets) { - if (!asset) continue; - - const quote = await getBuyQuote({ - currency: { id: eur.id }, - asset: { id: asset.id }, - amount: 100, - }); - - expect(quote.estimatedAmount).toBeGreaterThan(0); - console.log(`EUR 100 -> ${quote.estimatedAmount} ${asset.name}`); - - await delay(500); - } - }, 15000); -}); diff --git a/src/__tests__/api/helpers/api-client.ts b/src/__tests__/api/helpers/api-client.ts deleted file mode 100644 index d41c8dafd..000000000 --- a/src/__tests__/api/helpers/api-client.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { createTestCredentials, TestCredentials } from './test-wallet'; - -const API_URL = 'https://dev.api.dfx.swiss/v1'; - -// Global cache for auth tokens to avoid rate limiting -const tokenCache: Map = new Map(); - -// Cache for credentials -let cachedCredentials: TestCredentials | null = null; - -// Mutex for serializing auth requests -let authMutex: Promise = Promise.resolve(); - -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function generateCredentials(): Promise { - if (cachedCredentials) { - return cachedCredentials; - } - - cachedCredentials = await createTestCredentials(); - return cachedCredentials; -} - -async function authenticateWithRetry( - credentials: TestCredentials, - maxRetries = 5, -): Promise { - let lastError: Error | null = null; - - for (let attempt = 0; attempt < maxRetries; attempt++) { - if (attempt > 0) { - const backoffMs = Math.pow(2, attempt) * 1000; - console.log(`Auth retry ${attempt}/${maxRetries}, waiting ${backoffMs}ms...`); - await delay(backoffMs); - } - - try { - const response = await fetch(`${API_URL}/auth`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(credentials), - }); - - if (response.ok) { - const data = await response.json(); - return data.accessToken; - } - - const status = response.status; - if (status === 429) { - console.log(`Rate limited (429), will retry...`); - lastError = new Error(`Rate limited: ${status}`); - continue; - } - - const body = await response.text().catch(() => 'unknown'); - throw new Error(`Auth failed with status ${status}: ${body}`); - } catch (e) { - if (e instanceof Error && e.message.includes('Rate limited')) { - lastError = e; - continue; - } - throw e; - } - } - - throw lastError || new Error('Authentication failed after retries'); -} - -async function getCachedAuth(): Promise<{ token: string; credentials: TestCredentials }> { - const credentials = await generateCredentials(); - const cacheKey = `evm:${credentials.address}`; - - const cached = tokenCache.get(cacheKey); - if (cached && cached.expiry > Date.now()) { - console.log(`Using cached token for evm`); - return { token: cached.token, credentials }; - } - - const currentMutex = authMutex; - let resolve: () => void = () => { /* noop */ }; - authMutex = new Promise((r) => (resolve = r)); - - try { - await currentMutex; - - const cachedAgain = tokenCache.get(cacheKey); - if (cachedAgain && cachedAgain.expiry > Date.now()) { - console.log(`Using cached token for evm (after mutex)`); - return { token: cachedAgain.token, credentials }; - } - - await delay(1000); - - console.log(`Authenticating evm address: ${credentials.address}`); - const token = await authenticateWithRetry(credentials); - - tokenCache.set(cacheKey, { - token, - expiry: Date.now() + 2 * 60 * 60 * 1000, - }); - - return { token, credentials }; - } finally { - resolve(); - } -} - -export { getTestIban } from './test-wallet'; - -export class ApiClient { - private token: string; - - constructor(token: string) { - this.token = token; - } - - private async request( - method: string, - path: string, - data?: unknown, - requireAuth = true, - ): Promise<{ data: T | null; error: string | null; status: number }> { - const headers: Record = { - 'Content-Type': 'application/json', - }; - - if (requireAuth) { - headers['Authorization'] = `Bearer ${this.token}`; - } - - const response = await fetch(`${API_URL}${path}`, { - method, - headers, - body: data ? JSON.stringify(data) : undefined, - }); - - if (response.ok) { - const json = await response.json(); - return { data: json, error: null, status: response.status }; - } - - const errorBody = await response.json().catch(() => ({})); - return { - data: null, - error: (errorBody as { message?: string }).message || 'Unknown error', - status: response.status, - }; - } - - async get(path: string, requireAuth = true): Promise<{ data: T | null; error: string | null; status: number }> { - return this.request('GET', path, undefined, requireAuth); - } - - async post(path: string, data?: unknown, requireAuth = true): Promise<{ data: T | null; error: string | null; status: number }> { - return this.request('POST', path, data, requireAuth); - } - - async put(path: string, data?: unknown, requireAuth = true): Promise<{ data: T | null; error: string | null; status: number }> { - return this.request('PUT', path, data, requireAuth); - } - - async delete(path: string, requireAuth = true): Promise<{ data: T | null; error: string | null; status: number }> { - return this.request('DELETE', path, undefined, requireAuth); - } -} - -export async function createApiClient(): Promise<{ client: ApiClient; credentials: TestCredentials }> { - const { token, credentials } = await getCachedAuth(); - return { client: new ApiClient(token), credentials }; -} diff --git a/src/__tests__/api/helpers/test-wallet.ts b/src/__tests__/api/helpers/test-wallet.ts deleted file mode 100644 index e772d4208..000000000 --- a/src/__tests__/api/helpers/test-wallet.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Pre-computed test credentials -// Generated from mnemonic: "below debris olive author enhance ankle drum angle buyer cruel school milk" -// WARNING: This is for testing only! Never use for real funds! -const PRECOMPUTED_EVM_ADDRESS = '0x4B33B90cFC38341Db2b9EC5cF3B737508801c617'; -const PRECOMPUTED_EVM_SIGNATURE = - '0x18e4049227f7006f6820233b5dd4ff9e76af0b2125d2d927efc5e6934db1837313ffa8ed80556c3bc558e44d6c6971bec8db12d3db9d99364ec2992d3e4a1f511c'; -const TEST_IBAN_DEFAULT = 'CH9300762011623852957'; - -export interface TestCredentials { - address: string; - signature: string; -} - -export function getTestIban(): string { - return TEST_IBAN_DEFAULT; -} - -/** - * Returns pre-computed EVM test credentials. - * Using pre-computed values to avoid webpack Buffer polyfill conflicts with ethers.js. - */ -export async function createTestCredentials(): Promise { - return { - address: PRECOMPUTED_EVM_ADDRESS, - signature: PRECOMPUTED_EVM_SIGNATURE, - }; -} diff --git a/src/__tests__/api/sell-api.test.ts b/src/__tests__/api/sell-api.test.ts deleted file mode 100644 index 7e8f2e571..000000000 --- a/src/__tests__/api/sell-api.test.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { ApiClient, createApiClient, getTestIban } from './helpers/api-client'; -import { TestCredentials } from './helpers/test-wallet'; - -const API_URL = 'https://dev.api.dfx.swiss/v1'; - -const EVM_BLOCKCHAINS = ['Ethereum', 'Arbitrum', 'Optimism', 'Polygon', 'Base', 'BinanceSmartChain', 'Gnosis']; - -interface Asset { - id: number; - name: string; - uniqueName: string; - blockchain: string; - sellable: boolean; -} - -interface Fiat { - id: number; - name: string; - buyable: boolean; -} - -interface SellPaymentInfo { - id: number; - routeId: number; - amount: number; - currency: { id: number; name: string }; - asset: { id: number; name: string }; - estimatedAmount: number; - rate: number; - exchangeRate: number; - minVolume: number; - maxVolume: number; - fees: { total: number }; - feesTarget: { total: number }; - isValid: boolean; - error?: string; - depositAddress?: string; -} - -interface SellQuote { - amount: number; - estimatedAmount: number; - rate: number; - exchangeRate: number; - minVolume: number; - maxVolume: number; - feeAmount: number; - isValid: boolean; - error?: string; -} - -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function getAssets(client: ApiClient): Promise { - const result = await client.get('/asset'); - expect(result.data).toBeTruthy(); - return result.data ?? []; -} - -async function getFiats(): Promise { - const response = await fetch(`${API_URL}/fiat`); - expect(response.ok).toBeTruthy(); - return response.json(); -} - -async function getSellQuote( - params: { asset: { id: number }; currency: { id: number }; amount: number }, -): Promise { - const response = await fetch(`${API_URL}/sell/quote`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - asset: params.asset, - currency: params.currency, - amount: params.amount, - }), - }); - - expect(response.ok).toBeTruthy(); - return response.json(); -} - -async function createSellPaymentInfo( - client: ApiClient, - params: { asset: { id: number }; currency: { id: number }; amount: number; iban: string }, -): Promise<{ data: SellPaymentInfo | null; error: string | null; status: number }> { - return client.put('/sell/paymentInfos', { - asset: params.asset, - currency: params.currency, - amount: params.amount, - iban: params.iban, - }); -} - -// API Integration tests for Sell Process (EVM only) -describe('Sell Process - API Integration', () => { - let client: ApiClient; - let credentials: TestCredentials; - let sellableAssets: Asset[]; - let buyableFiats: Fiat[]; - let testIban: string; - - beforeAll(async () => { - const auth = await createApiClient(); - client = auth.client; - credentials = auth.credentials; - testIban = getTestIban(); - console.log(`Using EVM test address: ${credentials.address}`); - - const [assets, fiats] = await Promise.all([getAssets(client), getFiats()]); - - sellableAssets = assets.filter((a) => a.sellable && EVM_BLOCKCHAINS.includes(a.blockchain)); - buyableFiats = fiats.filter((f) => f.buyable); - - expect(sellableAssets.length).toBeGreaterThan(0); - expect(buyableFiats.length).toBeGreaterThan(0); - }, 60000); - - test('should authenticate with EVM credentials', async () => { - const response = await fetch(`${API_URL}/auth`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(credentials), - }); - - expect(response.ok).toBeTruthy(); - const data = await response.json(); - expect(data.accessToken).toBeTruthy(); - }); - - test('should fetch sellable assets', async () => { - const assets = await getAssets(client); - const sellable = assets.filter((a) => a.sellable && EVM_BLOCKCHAINS.includes(a.blockchain)); - - expect(sellable.length).toBeGreaterThan(0); - console.log(`Found ${sellable.length} sellable EVM assets`); - }); - - test('should fetch buyable fiats', async () => { - const fiats = await getFiats(); - const buyable = fiats.filter((f) => f.buyable); - - expect(buyable.length).toBeGreaterThan(0); - console.log(`Found ${buyable.length} buyable fiats`); - - const eurExists = buyable.some((f) => f.name === 'EUR'); - const chfExists = buyable.some((f) => f.name === 'CHF'); - expect(eurExists || chfExists).toBeTruthy(); - }); - - test('should get sell quote for ETH -> EUR', async () => { - const eth = sellableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - const eur = buyableFiats.find((f) => f.name === 'EUR'); - - if (!eth || !eur) { - console.log('Skipping: ETH or EUR not available'); - return; - } - - const quote = await getSellQuote({ - asset: { id: eth.id }, - currency: { id: eur.id }, - amount: 0.1, - }); - - expect(quote.amount).toBe(0.1); - expect(quote.estimatedAmount).toBeGreaterThan(0); - expect(quote.rate).toBeGreaterThan(0); - expect(quote.minVolume).toBeGreaterThan(0); - expect(quote.maxVolume).toBeGreaterThan(0); - - console.log(`Quote: 0.1 ETH -> ${quote.estimatedAmount} EUR (rate: ${quote.rate})`); - }); - - test('should get sell quote for USDC -> CHF', async () => { - const usdc = sellableAssets.find((a) => a.name === 'USDC' && EVM_BLOCKCHAINS.includes(a.blockchain)); - const chf = buyableFiats.find((f) => f.name === 'CHF'); - - if (!usdc || !chf) { - console.log('Skipping: USDC or CHF not available'); - return; - } - - const quote = await getSellQuote({ - asset: { id: usdc.id }, - currency: { id: chf.id }, - amount: 100, - }); - - expect(quote.amount).toBe(100); - expect(quote.estimatedAmount).toBeGreaterThan(0); - expect(quote.rate).toBeGreaterThan(0); - - console.log(`Quote: 100 USDC -> ${quote.estimatedAmount} CHF (rate: ${quote.rate})`); - }); - - test('should reject amount below minimum', async () => { - const eth = sellableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - const eur = buyableFiats.find((f) => f.name === 'EUR'); - - if (!eth || !eur) { - console.log('Skipping: ETH or EUR not available'); - return; - } - - const quote = await getSellQuote({ - asset: { id: eth.id }, - currency: { id: eur.id }, - amount: 0.0001, - }); - - if (!quote.isValid) { - expect(quote.error).toBeTruthy(); - console.log(`Amount too low error: ${quote.error}`); - } - }); - - test('should create sell payment info for ETH -> EUR', async () => { - const eth = sellableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - const eur = buyableFiats.find((f) => f.name === 'EUR'); - - if (!eth || !eur) { - console.log('Skipping: ETH or EUR not available'); - return; - } - - const result = await createSellPaymentInfo(client, { - asset: { id: eth.id }, - currency: { id: eur.id }, - amount: 0.1, - iban: testIban, - }); - - if (result.error) { - console.log(`Payment info creation returned error: ${result.error} (status: ${result.status})`); - const expectedErrors = ['Trading not allowed', 'RecommendationRequired', 'EmailRequired', 'KYC required', 'KycRequired', 'User not found', 'Ident data incomplete']; - const isExpectedError = expectedErrors.some((e) => result.error?.includes(e)); - if (isExpectedError) { - console.log('Skipping test - account restriction'); - return; - } - expect(result.data).toBeTruthy(); - return; - } - - const paymentInfo = result.data; - if (!paymentInfo) return; - expect(paymentInfo.id).toBeGreaterThan(0); - expect(paymentInfo.amount).toBe(0.1); - expect(paymentInfo.estimatedAmount).toBeGreaterThan(0); - expect(paymentInfo.rate).toBeGreaterThan(0); - - console.log(`Created sell payment info ID: ${paymentInfo.id}, Amount: ${paymentInfo.estimatedAmount} EUR`); - }); - - test('should handle multiple fiat currencies', async () => { - const eth = sellableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!eth) { - console.log('Skipping: ETH not available'); - return; - } - - const currencies = ['EUR', 'CHF', 'USD'].map((name) => buyableFiats.find((f) => f.name === name)).filter(Boolean); - - for (const currency of currencies) { - if (!currency) continue; - - const quote = await getSellQuote({ - asset: { id: eth.id }, - currency: { id: currency.id }, - amount: 0.1, - }); - - expect(quote.estimatedAmount).toBeGreaterThan(0); - console.log(`0.1 ETH -> ${quote.estimatedAmount} ${currency.name}`); - - await delay(500); - } - }, 15000); - - test('should handle multiple assets', async () => { - const eur = buyableFiats.find((f) => f.name === 'EUR'); - - if (!eur) { - console.log('Skipping: EUR not available'); - return; - } - - const assets = ['ETH', 'USDT', 'USDC'] - .map((name) => sellableAssets.find((a) => a.name === name)) - .filter(Boolean); - - for (const asset of assets) { - if (!asset) continue; - - const amount = asset.name === 'ETH' ? 0.1 : 100; - - const quote = await getSellQuote({ - asset: { id: asset.id }, - currency: { id: eur.id }, - amount, - }); - - expect(quote.estimatedAmount).toBeGreaterThan(0); - console.log(`${amount} ${asset.name} -> ${quote.estimatedAmount} EUR`); - - await delay(500); - } - }, 15000); -}); diff --git a/src/__tests__/api/swap-api.test.ts b/src/__tests__/api/swap-api.test.ts deleted file mode 100644 index 7910d97be..000000000 --- a/src/__tests__/api/swap-api.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { ApiClient, createApiClient } from './helpers/api-client'; -import { TestCredentials } from './helpers/test-wallet'; - -const API_URL = 'https://dev.api.dfx.swiss/v1'; - -const EVM_BLOCKCHAINS = ['Ethereum', 'Arbitrum', 'Optimism', 'Polygon', 'Base', 'BinanceSmartChain', 'Gnosis']; - -interface Asset { - id: number; - name: string; - uniqueName: string; - blockchain: string; - buyable: boolean; - sellable: boolean; -} - -interface SwapQuote { - amount: number; - estimatedAmount: number; - exchangeRate: number; - minVolume: number; - maxVolume: number; - feeAmount: number; - fees: { total: number }; - feesTarget: { total: number }; - isValid: boolean; - error?: string; -} - -interface SwapPaymentInfo { - id: number; - routeId: number; - amount: number; - sourceAsset: { id: number; name: string }; - targetAsset: { id: number; name: string }; - estimatedAmount: number; - rate: number; - exchangeRate: number; - minVolume: number; - maxVolume: number; - fees: { total: number }; - feesTarget: { total: number }; - isValid: boolean; - error?: string; - depositAddress?: string; -} - -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function getAssets(client: ApiClient): Promise { - const result = await client.get('/asset'); - expect(result.data).toBeTruthy(); - return result.data ?? []; -} - -async function getSwapQuote( - params: { sourceAsset: { id: number }; targetAsset: { id: number }; amount: number }, -): Promise { - const response = await fetch(`${API_URL}/swap/quote`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - sourceAsset: params.sourceAsset, - targetAsset: params.targetAsset, - amount: params.amount, - }), - }); - - expect(response.ok).toBeTruthy(); - return response.json(); -} - -async function createSwapPaymentInfo( - client: ApiClient, - params: { sourceAsset: { id: number }; targetAsset: { id: number }; amount: number }, -): Promise<{ data: SwapPaymentInfo | null; error: string | null; status: number }> { - return client.put('/swap/paymentInfos', { - sourceAsset: params.sourceAsset, - targetAsset: params.targetAsset, - amount: params.amount, - }); -} - -// API Integration tests for Swap Process (EVM only) -describe('Swap Process - API Integration', () => { - let client: ApiClient; - let credentials: TestCredentials; - let swappableAssets: Asset[]; - - beforeAll(async () => { - const auth = await createApiClient(); - client = auth.client; - credentials = auth.credentials; - console.log(`Using test address: ${credentials.address}`); - - const assets = await getAssets(client); - - swappableAssets = assets.filter( - (a) => a.buyable && a.sellable && EVM_BLOCKCHAINS.includes(a.blockchain), - ); - - expect(swappableAssets.length).toBeGreaterThan(0); - console.log(`Found ${swappableAssets.length} swappable EVM assets`); - }, 60000); - - test('should authenticate with test credentials', async () => { - const response = await fetch(`${API_URL}/auth`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(credentials), - }); - - expect(response.ok).toBeTruthy(); - const data = await response.json(); - expect(data.accessToken).toBeTruthy(); - }); - - test('should fetch swappable assets', async () => { - const assets = await getAssets(client); - const swappable = assets.filter( - (a) => a.buyable && a.sellable && EVM_BLOCKCHAINS.includes(a.blockchain), - ); - - expect(swappable.length).toBeGreaterThan(0); - console.log(`Found ${swappable.length} swappable EVM assets`); - - const hasEth = swappable.some((a) => a.name === 'ETH'); - const hasUsdt = swappable.some((a) => a.name === 'USDT'); - const hasUsdc = swappable.some((a) => a.name === 'USDC'); - - expect(hasEth || hasUsdt || hasUsdc).toBeTruthy(); - }); - - test('should get swap quote for ETH -> USDT', async () => { - const eth = swappableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - const usdt = swappableAssets.find((a) => a.name === 'USDT' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!eth || !usdt) { - console.log('Skipping: ETH or USDT not available'); - return; - } - - const quote = await getSwapQuote({ - sourceAsset: { id: eth.id }, - targetAsset: { id: usdt.id }, - amount: 0.1, - }); - - expect(quote.amount).toBe(0.1); - expect(quote.estimatedAmount).toBeGreaterThan(0); - expect(quote.exchangeRate).toBeGreaterThan(0); - expect(quote.minVolume).toBeGreaterThan(0); - expect(quote.maxVolume).toBeGreaterThan(0); - - console.log(`Quote: 0.1 ETH -> ${quote.estimatedAmount} USDT (rate: ${quote.exchangeRate})`); - }); - - test('should get swap quote for USDC -> ETH', async () => { - const usdc = swappableAssets.find((a) => a.name === 'USDC' && EVM_BLOCKCHAINS.includes(a.blockchain)); - const eth = swappableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!usdc || !eth) { - console.log('Skipping: USDC or ETH not available'); - return; - } - - const quote = await getSwapQuote({ - sourceAsset: { id: usdc.id }, - targetAsset: { id: eth.id }, - amount: 100, - }); - - expect(quote.amount).toBe(100); - expect(quote.estimatedAmount).toBeGreaterThan(0); - expect(quote.exchangeRate).toBeGreaterThan(0); - - console.log(`Quote: 100 USDC -> ${quote.estimatedAmount} ETH (rate: ${quote.exchangeRate})`); - }); - - test('should get swap quote for USDT -> USDC (stablecoin swap)', async () => { - const usdt = swappableAssets.find((a) => a.name === 'USDT' && EVM_BLOCKCHAINS.includes(a.blockchain)); - const usdc = swappableAssets.find((a) => a.name === 'USDC' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!usdt || !usdc) { - console.log('Skipping: USDT or USDC not available'); - return; - } - - const quote = await getSwapQuote({ - sourceAsset: { id: usdt.id }, - targetAsset: { id: usdc.id }, - amount: 100, - }); - - expect(quote.amount).toBe(100); - expect(quote.estimatedAmount).toBeGreaterThan(0); - expect(quote.exchangeRate).toBeGreaterThan(0.9); - expect(quote.exchangeRate).toBeLessThan(1.1); - - console.log(`Quote: 100 USDT -> ${quote.estimatedAmount} USDC (rate: ${quote.exchangeRate})`); - }); - - test('should reject amount below minimum', async () => { - const eth = swappableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - const usdt = swappableAssets.find((a) => a.name === 'USDT' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!eth || !usdt) { - console.log('Skipping: ETH or USDT not available'); - return; - } - - const quote = await getSwapQuote({ - sourceAsset: { id: eth.id }, - targetAsset: { id: usdt.id }, - amount: 0.00001, - }); - - if (!quote.isValid) { - expect(quote.error).toBeTruthy(); - console.log(`Amount too low error: ${quote.error}`); - } - }); - - test.skip('should create swap payment info for ETH -> USDT', async () => { - const eth = swappableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - const usdt = swappableAssets.find((a) => a.name === 'USDT' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!eth || !usdt) { - console.log('Skipping: ETH or USDT not available'); - return; - } - - const result = await createSwapPaymentInfo(client, { - sourceAsset: { id: eth.id }, - targetAsset: { id: usdt.id }, - amount: 0.1, - }); - - if (result.error) { - console.log(`Payment info creation returned error: ${result.error} (status: ${result.status})`); - const expectedErrors = ['Trading not allowed', 'RecommendationRequired', 'EmailRequired', 'KYC required', 'KycRequired', 'User not found', 'Ident data incomplete']; - const isExpectedError = expectedErrors.some((e) => result.error?.includes(e)); - if (isExpectedError) { - console.log('Skipping test - account restriction'); - return; - } - expect(result.data).toBeTruthy(); - return; - } - - const paymentInfo = result.data; - if (!paymentInfo) return; - expect(paymentInfo.id).toBeGreaterThan(0); - expect(paymentInfo.amount).toBe(0.1); - expect(paymentInfo.estimatedAmount).toBeGreaterThan(0); - expect(paymentInfo.rate).toBeGreaterThan(0); - - console.log(`Created swap payment info ID: ${paymentInfo.id}, Amount: ${paymentInfo.estimatedAmount} USDT`); - }); - - test('should handle multiple swap pairs', async () => { - const eth = swappableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!eth) { - console.log('Skipping: ETH not available'); - return; - } - - const targetAssets = ['USDT', 'USDC'] - .map((name) => swappableAssets.find((a) => a.name === name && EVM_BLOCKCHAINS.includes(a.blockchain))) - .filter(Boolean); - - for (const targetAsset of targetAssets) { - if (!targetAsset) continue; - - const quote = await getSwapQuote({ - sourceAsset: { id: eth.id }, - targetAsset: { id: targetAsset.id }, - amount: 0.1, - }); - - expect(quote.estimatedAmount).toBeGreaterThan(0); - console.log(`0.1 ETH -> ${quote.estimatedAmount} ${targetAsset.name}`); - - await delay(500); - } - }); - - test('should handle reverse swap pairs', async () => { - const eth = swappableAssets.find((a) => a.name === 'ETH' && EVM_BLOCKCHAINS.includes(a.blockchain)); - const usdt = swappableAssets.find((a) => a.name === 'USDT' && EVM_BLOCKCHAINS.includes(a.blockchain)); - - if (!eth || !usdt) { - console.log('Skipping: ETH or USDT not available'); - return; - } - - const quote1 = await getSwapQuote({ - sourceAsset: { id: eth.id }, - targetAsset: { id: usdt.id }, - amount: 0.1, - }); - - await delay(500); - - const quote2 = await getSwapQuote({ - sourceAsset: { id: usdt.id }, - targetAsset: { id: eth.id }, - amount: 100, - }); - - expect(quote1.estimatedAmount).toBeGreaterThan(0); - expect(quote2.estimatedAmount).toBeGreaterThan(0); - - console.log(`0.1 ETH -> ${quote1.estimatedAmount} USDT`); - console.log(`100 USDT -> ${quote2.estimatedAmount} ETH`); - - const rate1 = quote1.estimatedAmount / 0.1; - const rate2 = 100 / quote2.estimatedAmount; - - expect(Math.abs(rate1 - rate2) / rate1).toBeLessThan(0.05); - }); -}); From 67a5a2043595e49e325738d5e2c4effd60ac5014 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:45:44 +0100 Subject: [PATCH 6/7] fix: replace non-null assertions with nullish coalescing (#965) Replace forbidden non-null assertions (!) with nullish coalescing (?? '') in RealUnit detail screens to fix widget build ESLint errors. --- src/screens/realunit-quote-detail.screen.tsx | 2 +- src/screens/realunit-transaction-detail.screen.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/screens/realunit-quote-detail.screen.tsx b/src/screens/realunit-quote-detail.screen.tsx index de199583b..2b4e07f8b 100644 --- a/src/screens/realunit-quote-detail.screen.tsx +++ b/src/screens/realunit-quote-detail.screen.tsx @@ -95,7 +95,7 @@ export default function RealunitQuoteDetailScreen(): JSX.Element { diff --git a/src/screens/realunit-transaction-detail.screen.tsx b/src/screens/realunit-transaction-detail.screen.tsx index 5584d0b26..63964a504 100644 --- a/src/screens/realunit-transaction-detail.screen.tsx +++ b/src/screens/realunit-transaction-detail.screen.tsx @@ -87,7 +87,7 @@ export default function RealunitTransactionDetailScreen(): JSX.Element { From bd42dfdfefead7f7060e0475220219a855c76fe1 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:42:46 +0100 Subject: [PATCH 7/7] feat: add RealUnit role guard for /realunit screens (#966) * feat: add useRealunitGuard for RealUnit role access Replace useAdminGuard with useRealunitGuard on all RealUnit screens so users with the RealUnit role can access the /realunit subpage. Admin users retain full access. * chore: update @dfx.swiss packages to 1.3.0-beta.237 --- package-lock.json | 16 ++++++++-------- package.json | 18 +++++++++--------- src/components/navigation.tsx | 2 +- src/hooks/guard.hook.ts | 4 ++++ src/screens/realunit-holders.screen.tsx | 4 ++-- src/screens/realunit-quote-detail.screen.tsx | 4 ++-- src/screens/realunit-quotes.screen.tsx | 4 ++-- .../realunit-transaction-detail.screen.tsx | 4 ++-- src/screens/realunit-transactions.screen.tsx | 4 ++-- src/screens/realunit-user.screen.tsx | 4 ++-- src/screens/realunit.screen.tsx | 4 ++-- 11 files changed, 36 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83ea91d10..dc8b0f1f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "1.0.4", "license": "MIT", "dependencies": { - "@dfx.swiss/react": "^1.3.0-beta.236", - "@dfx.swiss/react-components": "^1.3.0-beta.236", + "@dfx.swiss/react": "^1.3.0-beta.237", + "@dfx.swiss/react-components": "^1.3.0-beta.237", "@ledgerhq/hw-app-btc": "^6.24.1", "@ledgerhq/hw-app-eth": "^6.33.7", "@ledgerhq/hw-transport-webhid": "^6.27.19", @@ -2631,9 +2631,9 @@ } }, "node_modules/@dfx.swiss/react": { - "version": "1.3.0-beta.236", - "resolved": "https://registry.npmjs.org/@dfx.swiss/react/-/react-1.3.0-beta.236.tgz", - "integrity": "sha512-aNGss+yuu3kQBGnm7ifQ3LqEuXl4ScI2tFIeCltgP2P4nIs0BeioF02N8Myben4tGxgpayF6hiGLcvAy1pF5CQ==", + "version": "1.3.0-beta.237", + "resolved": "https://registry.npmjs.org/@dfx.swiss/react/-/react-1.3.0-beta.237.tgz", + "integrity": "sha512-VDVYw7wnUHxSy3DGN6rGS9UsYkn10GOienYcI5MGubKVGtkWY8GFRVQ9NeQBjIHFPHi9EJaDmM7GI0ZYDRfEew==", "license": "MIT", "dependencies": { "ibantools": "^4.2.1", @@ -2644,9 +2644,9 @@ } }, "node_modules/@dfx.swiss/react-components": { - "version": "1.3.0-beta.236", - "resolved": "https://registry.npmjs.org/@dfx.swiss/react-components/-/react-components-1.3.0-beta.236.tgz", - "integrity": "sha512-LTAE+YBKhNudxfAVuf+PlAGITlxJbtCIbP+67a4A/P5dm7IaZqUSDB96xbWLfl4o6qqOUGUizSPB1o0hk/fs/Q==", + "version": "1.3.0-beta.237", + "resolved": "https://registry.npmjs.org/@dfx.swiss/react-components/-/react-components-1.3.0-beta.237.tgz", + "integrity": "sha512-1xlqEB20rAfnAyuv4I+dQu1lsUyW4jXXo5Qxxn2hRAVaeBKddvv/vOWrQghSACxudwabn9aeLmicQQAQ67TXog==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.18.1", diff --git a/package.json b/package.json index 63cacb032..5cde4ccbe 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,14 @@ "access": "public" }, "dependencies": { - "@dfx.swiss/react": "^1.3.0-beta.236", - "@dfx.swiss/react-components": "^1.3.0-beta.236", + "@dfx.swiss/react": "^1.3.0-beta.237", + "@dfx.swiss/react-components": "^1.3.0-beta.237", "@ledgerhq/hw-app-btc": "^6.24.1", "@ledgerhq/hw-app-eth": "^6.33.7", "@ledgerhq/hw-transport-webhid": "^6.27.19", "@r2wc/react-to-web-component": "^2.0.2", + "@scure/bip32": "^2.0.1", + "@scure/bip39": "^2.0.1", "@solana/spl-token": "^0.4.13", "@solana/wallet-adapter-phantom": "^0.9.27", "@solana/wallet-adapter-trust": "^0.1.16", @@ -34,6 +36,8 @@ "apexcharts": "^4.7.0", "bech32": "^2.0.0", "bitbox-api": "^0.2.1", + "bitcoinjs-lib": "^7.0.0", + "bitcoinjs-message": "^2.2.0", "browser-lang": "^0.2.1", "buffer": "^6.0.3", "copy-to-clipboard": "^3.3.3", @@ -57,18 +61,14 @@ "react-scripts": "5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", + "tronweb": "^6.1.1", + "tweetnacl": "^1.0.3", "typescript": "^5.4.5", "url": "^0.11.1", "viem": "^2.13.3", "web-vitals": "^2.1.4", "web3": "^1.8.1", - "webln": "^0.3.2", - "@scure/bip32": "^2.0.1", - "@scure/bip39": "^2.0.1", - "bitcoinjs-lib": "^7.0.0", - "bitcoinjs-message": "^2.2.0", - "tronweb": "^6.1.1", - "tweetnacl": "^1.0.3" + "webln": "^0.3.2" }, "scripts": { "start": "react-app-rewired start", diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index b15677b76..5b92ab699 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -206,7 +206,7 @@ function NavigationMenu({ setIsNavigationOpen, small = false }: NavigationMenuCo onClose={() => setIsNavigationOpen(false)} /> )} - {session?.role && [UserRole.ADMIN].includes(session.role) && ( + {session?.role && [UserRole.ADMIN, UserRole.REALUNIT].includes(session.role) && (