Skip to content

Commit

Permalink
feat(wallet-dashboard): update send flow after clustering coins by co…
Browse files Browse the repository at this point in the history
…in type (#839)

* feat(wallet-dashboard): improve coins portfolio by clustering them by coin type

* feat(wallet-dashboard): remove disabled rule

* feat(wallet-dashboard): update types and improve code

* feat(wallet-dashboard): fix build

* feat(wallet-dashboard): remove useCoinMetadata from filterAndSortTokenBalances

* feat(wallet-dashboard): cleanup

* feat(wallet-dashboard): sort by coinType

* feat(wallet-dashboard): add dropdown component

* feat(wallet-dashboard): rename component

* feat(wallet-dashboard): udpate popup logic

* feat(core): add info to function

* feat(core): move useSortedCoinsByCategories hook to core and use it in dashboard

* feat(wallet-dashboard): add comment and link to issue

* feat(wallet-dashboard): cleanup

* fix(wallet-dashboard): add conditional and use another format for documentation

* fix(dashboard): improvements

* feat(dashboard): add disabled feature to dropdown component

* fix(dashboard): rename Dropdown fields

* feat(wallet-dashboard): allow sending coins with any coin type (#929)

* feat(wallet-dashboard): show unrecognized coins

* feat(wallet-dashboard): allow sending coins with any coin type

---------

Co-authored-by: Begoña Álvarez de la Cruz <[email protected]>
Co-authored-by: cpl121 <[email protected]>
Co-authored-by: cpl121 <[email protected]>
  • Loading branch information
4 people authored Jul 5, 2024
1 parent c22590d commit 73507de
Show file tree
Hide file tree
Showing 19 changed files with 237 additions and 115 deletions.
5 changes: 5 additions & 0 deletions apps/core/src/constants/coins.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export const COINS_QUERY_REFETCH_INTERVAL = 20_000;
export const COINS_QUERY_STALE_TIME = 20_000;
2 changes: 2 additions & 0 deletions apps/core/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
// SPDX-License-Identifier: Apache-2.0

export * from './staking.constants';
export * from './recognizedPackages.constants';
export * from './coins.constants';
6 changes: 6 additions & 0 deletions apps/core/src/constants/recognizedPackages.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { IOTA_FRAMEWORK_ADDRESS, IOTA_SYSTEM_ADDRESS } from '@iota/iota.js/utils';

export const DEFAULT_RECOGNIZED_PACKAGES = [IOTA_FRAMEWORK_ADDRESS, IOTA_SYSTEM_ADDRESS];
1 change: 1 addition & 0 deletions apps/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export * from './useKioskClient';
export * from './useQueryTransactionsByAddress';
export * from './useGetTransaction';
export * from './useExtendedTransactionSummary';
export * from './useSortedCoinsByCategories';
export * from './useGetNFTMeta';
export * from './useIotaAddressValidation';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { usePinnedCoinTypes } from '_app/hooks/usePinnedCoinTypes';
import { useRecognizedPackages } from '_app/hooks/useRecognizedPackages';
import { type CoinBalance as CoinBalanceType } from '@iota/iota.js/client';
import { type CoinBalance } from '@iota/iota.js/client';
import { IOTA_TYPE_ARG } from '@iota/iota.js/utils';
import { useMemo } from 'react';
import { DEFAULT_RECOGNIZED_PACKAGES } from '../constants';

function sortCoins(balances: CoinBalanceType[]) {
function sortCoins(balances: CoinBalance[]) {
return balances.sort((a, b) => {
if (a.coinType === IOTA_TYPE_ARG) {
return -1;
Expand All @@ -18,26 +17,30 @@ function sortCoins(balances: CoinBalanceType[]) {
});
}

export function useSortedCoinsByCategories(coinBalances: CoinBalanceType[]) {
const recognizedPackages = useRecognizedPackages();
const [pinnedCoinTypes] = usePinnedCoinTypes();
export function useSortedCoinsByCategories(coinBalances: CoinBalance[]) {
const recognizedPackages = DEFAULT_RECOGNIZED_PACKAGES; // previous: useRecognizedPackages();

// Commented out pinnedCoinTypes until https://github.com/iotaledger/iota/issues/832 is resolved
// const [pinnedCoinTypes] = usePinnedCoinTypes();

return useMemo(() => {
const reducedCoinBalances = coinBalances?.reduce(
(acc, coinBalance) => {
if (recognizedPackages.includes(coinBalance.coinType.split('::')[0])) {
acc.recognized.push(coinBalance);
} else if (pinnedCoinTypes.includes(coinBalance.coinType)) {
acc.pinned.push(coinBalance);
} else {
}
// else if (pinnedCoinTypes.includes(coinBalance.coinType)) {
// acc.pinned.push(coinBalance);
// }
else {
acc.unrecognized.push(coinBalance);
}
return acc;
},
{
recognized: [] as CoinBalanceType[],
pinned: [] as CoinBalanceType[],
unrecognized: [] as CoinBalanceType[],
recognized: [] as CoinBalance[],
pinned: [] as CoinBalance[],
unrecognized: [] as CoinBalance[],
},
) ?? { recognized: [], pinned: [], unrecognized: [] };

Expand All @@ -46,5 +49,5 @@ export function useSortedCoinsByCategories(coinBalances: CoinBalanceType[]) {
pinned: sortCoins(reducedCoinBalances.pinned),
unrecognized: sortCoins(reducedCoinBalances.unrecognized),
};
}, [coinBalances, recognizedPackages, pinnedCoinTypes]);
}, [coinBalances, recognizedPackages /*pinnedCoinTypes*/]);
}
18 changes: 6 additions & 12 deletions apps/core/src/utils/filterAndSortTokenBalances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import { type CoinBalance } from '@iota/iota.js/client';
import { getCoinSymbol } from '../hooks';
import { IOTA_TYPE_ARG } from '@iota/iota.js/utils';

// Move this to the API backend https://github.com/iotaledger/iota/issues/922
/**
* Filter and sort token balances by symbol and total balance.
* IOTA tokens are always sorted first.
Expand All @@ -14,15 +14,9 @@ import { IOTA_TYPE_ARG } from '@iota/iota.js/utils';
export function filterAndSortTokenBalances(tokens: CoinBalance[]) {
return tokens
.filter((token) => Number(token.totalBalance) > 0)
.sort((a, b) => {
if (a.coinType === IOTA_TYPE_ARG && b.coinType !== IOTA_TYPE_ARG) {
return -1;
} else if (a.coinType !== IOTA_TYPE_ARG && b.coinType === IOTA_TYPE_ARG) {
return 1;
}

return (getCoinSymbol(a.coinType) + Number(a.totalBalance)).localeCompare(
getCoinSymbol(a.coinType) + Number(b.totalBalance),
);
});
.sort((a, b) =>
(getCoinSymbol(a.coinType) + Number(a.totalBalance)).localeCompare(
getCoinSymbol(b.coinType) + Number(b.totalBalance),
),
);
}
4 changes: 2 additions & 2 deletions apps/wallet-dashboard/app/dashboard/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
'use client';

import { AccountBalance, AllCoins, Button, NewStakePopup } from '@/components';
import { AccountBalance, MyCoins, Button, NewStakePopup } from '@/components';
import { usePopups } from '@/hooks';
import { useCurrentAccount, useCurrentWallet } from '@iota/dapp-kit';

Expand All @@ -23,7 +23,7 @@ function HomeDashboardPage(): JSX.Element {
<h1>Welcome</h1>
<div>Address: {account.address}</div>
<AccountBalance />
<AllCoins />
<MyCoins />
<Button onClick={addNewStake}>New Stake</Button>
</div>
)}
Expand Down
44 changes: 0 additions & 44 deletions apps/wallet-dashboard/components/Coins/AllCoins.tsx

This file was deleted.

74 changes: 74 additions & 0 deletions apps/wallet-dashboard/components/Coins/MyCoins.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import React from 'react';
import { useCurrentAccount, useIotaClientQuery } from '@iota/dapp-kit';
import { CoinItem, SendCoinPopup } from '@/components';
import { usePopups } from '@/hooks';
import { CoinBalance } from '@iota/iota.js/client';
import {
COINS_QUERY_REFETCH_INTERVAL,
COINS_QUERY_STALE_TIME,
filterAndSortTokenBalances,
useSortedCoinsByCategories,
} from '@iota/core';

function MyCoins(): React.JSX.Element {
const { openPopup, closePopup } = usePopups();
const account = useCurrentAccount();
const activeAccountAddress = account?.address;

const { data: coinBalances } = useIotaClientQuery(
'getAllBalances',
{ owner: activeAccountAddress! },
{
enabled: !!activeAccountAddress,
staleTime: COINS_QUERY_STALE_TIME,
refetchInterval: COINS_QUERY_REFETCH_INTERVAL,
select: filterAndSortTokenBalances,
},
);
const { recognized, unrecognized } = useSortedCoinsByCategories(coinBalances ?? []);

function openSendTokenPopup(coin: CoinBalance, address: string): void {
if (coinBalances) {
openPopup(
<SendCoinPopup
coin={coin}
senderAddress={address}
onClose={closePopup}
coins={coinBalances}
/>,
);
}
}

return (
<div className="flex w-2/3 flex-col items-center space-y-2">
<h3>My Coins:</h3>
{recognized?.map((coin, index) => {
return (
<CoinItem
key={index}
coinType={coin.coinType}
balance={BigInt(coin.totalBalance)}
onClick={() => openSendTokenPopup(coin, account?.address ?? '')}
/>
);
})}
<span>Unrecognized coins</span>
{unrecognized?.map((coin, index) => {
return (
<CoinItem
key={index}
coinType={coin.coinType}
balance={BigInt(coin.totalBalance)}
onClick={() => openSendTokenPopup(coin, account?.address ?? '')}
/>
);
})}
</div>
);
}

export default MyCoins;
2 changes: 1 addition & 1 deletion apps/wallet-dashboard/components/Coins/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export { default as AllCoins } from './AllCoins';
export { default as MyCoins } from './MyCoins';
export { default as CoinItem } from './CoinItem';
53 changes: 53 additions & 0 deletions apps/wallet-dashboard/components/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import React from 'react';

interface DropdownProps<T> {
options: T[];
selectedOption: T | null | undefined;
onChange: (selectedOption: T) => void;
placeholder?: string;
disabled?: boolean;
getOptionId: (option: T) => string | number;
}

function Dropdown<T>({
options,
selectedOption,
onChange,
placeholder,
disabled = false,
getOptionId,
}: DropdownProps<T>): JSX.Element {
function handleSelectionChange(e: React.ChangeEvent<HTMLSelectElement>): void {
const selectedKey = e.target.value;
const selectedOption = options.find((option) => getOptionId(option) === selectedKey);
if (selectedOption) {
onChange(selectedOption);
}
}

return (
<select
value={selectedOption ? getOptionId(selectedOption) : ''}
onChange={handleSelectionChange}
className="px-2 py-3"
disabled={disabled}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}

{options.map((option, index) => (
<option key={index} value={getOptionId(option)}>
{getOptionId(option)}
</option>
))}
</select>
);
}

export default Dropdown;
Loading

0 comments on commit 73507de

Please sign in to comment.