+
{!hideListActions ? (
- }
- onPress={onCreateAccount}
- text={t('NEW_ACCOUNT')}
- transparent
+ onClick={onCreateAccount}
+ title={t('NEW_ACCOUNT')}
+ variant="secondary"
/>
- }
- onPress={onImportLedgerAccount}
- text={t('LEDGER_ACCOUNT')}
- transparent
+ onClick={onImportLedgerAccount}
+ title={t('LEDGER_ACCOUNT')}
+ variant="secondary"
/>
) : null}
diff --git a/src/app/screens/backupWallet/index.tsx b/src/app/screens/backupWallet/index.tsx
index 58483a5b1..7751a10ec 100644
--- a/src/app/screens/backupWallet/index.tsx
+++ b/src/app/screens/backupWallet/index.tsx
@@ -10,8 +10,8 @@ import styled from 'styled-components';
const Container = styled.div((props) => ({
flex: 1,
- paddingLeft: props.theme.spacing(8),
- paddingRight: props.theme.spacing(8),
+ paddingLeft: props.theme.space.m,
+ paddingRight: props.theme.space.m,
display: 'flex',
flexDirection: 'column',
}));
@@ -27,7 +27,7 @@ const ContentContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
- marginBottom: props.theme.spacing(32),
+ marginBottom: props.theme.space.xxxl,
}));
const Title = styled.h1((props) => ({
@@ -46,7 +46,7 @@ const BackupActionsContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
- marginTop: props.theme.spacing(20),
+ marginTop: props.theme.space.xxl,
width: '100%',
columnGap: props.theme.space.xs,
}));
@@ -55,7 +55,7 @@ const LoadingContainer = styled.div((props) => ({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
- marginTop: props.theme.spacing(20),
+ marginTop: props.theme.space.xxl,
width: '100%',
}));
diff --git a/src/app/screens/btcSendRequest/index.tsx b/src/app/screens/btcSendRequest/index.tsx
index 50db52f51..b01680774 100644
--- a/src/app/screens/btcSendRequest/index.tsx
+++ b/src/app/screens/btcSendRequest/index.tsx
@@ -1,20 +1,12 @@
-import { makeRPCError, sendRpcResponse } from '@common/utils/rpc/helpers';
+import { makeRPCError, makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers';
import ConfirmBtcTransaction from '@components/confirmBtcTransaction';
import useBtcFeeRate from '@hooks/useBtcFeeRate';
-import useHasFeature from '@hooks/useHasFeature';
import useSelectedAccount from '@hooks/useSelectedAccount';
import useTransactionContext from '@hooks/useTransactionContext';
import useWalletSelector from '@hooks/useWalletSelector';
import { RpcErrorCode } from '@sats-connect/core';
import type { TransactionSummary } from '@screens/sendBtc/helpers';
-import {
- AnalyticsEvents,
- btcTransaction,
- FeatureId,
- parseSummaryForRunes,
- type RuneSummary,
- type Transport,
-} from '@secretkeylabs/xverse-core';
+import { AnalyticsEvents, btcTransaction, type Transport } from '@secretkeylabs/xverse-core';
import Spinner from '@ui-library/spinner';
import { BITCOIN_DUST_AMOUNT_SATS } from '@utils/constants';
import { trackMixPanel } from '@utils/mixpanel';
@@ -50,8 +42,6 @@ function BtcSendRequest() {
const [feeRate, setFeeRate] = useState('');
const [transaction, setTransaction] = useState();
const [summary, setSummary] = useState();
- const [runeSummary, setRuneSummary] = useState();
- const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT);
const [isLoading, setIsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -74,7 +64,6 @@ function BtcSendRequest() {
useEffect(() => {
if (!payload || !transactionContext || !feeRate) {
setSummary(undefined);
- setRuneSummary(undefined);
return;
}
const generateTxnAndSummary = async () => {
@@ -84,13 +73,6 @@ function BtcSendRequest() {
const newSummary = await newTransaction.getSummary();
setTransaction(newTransaction);
setSummary(newSummary);
- if (newSummary && hasRunesSupport) {
- setRuneSummary(
- await parseSummaryForRunes(transactionContext, newSummary, transactionContext.network, {
- separateTransfersOnNoExternalInputs: true,
- }),
- );
- }
} catch (e) {
setTransaction(undefined);
setSummary(undefined);
@@ -123,23 +105,35 @@ function BtcSendRequest() {
useEffect(() => {
const checkIfMismatch = () => {
+ let errorTitle = '';
+ let error = '';
+ let textAlignment = 'center';
+
if (payload.senderAddress !== selectedAccount.btcAddress) {
- navigate('/tx-status', {
- state: {
- txid: '',
- currency: 'STX',
- error: t('CONFIRM_TRANSACTION.ADDRESS_MISMATCH'),
- browserTx: true,
- },
- });
+ if (
+ selectedAccount.btcAddresses.native?.address === payload.senderAddress ||
+ selectedAccount.btcAddresses.nested?.address === payload.senderAddress
+ ) {
+ errorTitle = t('REQUEST_ERRORS.ADDRESS_TYPE_MISMATCH_TITLE');
+ error = t('REQUEST_ERRORS.ADDRESS_TYPE_MISMATCH');
+ } else {
+ errorTitle = t('REQUEST_ERRORS.ADDRESS_MISMATCH_TITLE');
+ error = t('REQUEST_ERRORS.ADDRESS_MISMATCH');
+ }
+ textAlignment = 'left';
+ } else if (payload.network.type !== network.type) {
+ error = t('REQUEST_ERRORS.NETWORK_MISMATCH');
}
- if (payload.network.type !== network.type) {
+
+ if (error) {
navigate('/tx-status', {
state: {
txid: '',
currency: 'BTC',
- error: t('CONFIRM_TRANSACTION.NETWORK_MISMATCH'),
+ errorTitle,
+ error,
browserTx: true,
+ textAlignment,
},
});
}
@@ -166,6 +160,15 @@ function BtcSendRequest() {
checkIfValidAmount();
}, [payload]);
+ const handleCancel = () => {
+ const response = makeRPCError(requestId, {
+ code: RpcErrorCode.USER_REJECTION,
+ message: 'User rejected request to send transfer',
+ });
+ sendRpcResponse(+tabId, response);
+ window.close();
+ };
+
const handleSubmit = async (ledgerTransport?: Transport) => {
try {
setIsSubmitting(true);
@@ -175,16 +178,26 @@ function BtcSendRequest() {
action: 'transfer',
wallet_type: selectedAccount.accountType || 'software',
});
- navigate('/tx-status', {
- state: {
+ if (txnId) {
+ const sendTransferResponse = makeRpcSuccessResponse<'sendTransfer'>(requestId, {
txid: txnId,
- currency: 'BTC',
- error: '',
- browserTx: true,
- },
- });
+ });
+ sendRpcResponse(+tabId, sendTransferResponse);
+ navigate('/tx-status', {
+ state: {
+ txid: txnId,
+ currency: 'BTC',
+ error: '',
+ browserTx: true,
+ },
+ });
+ }
} catch (e) {
- console.error(e);
+ const response = makeRPCError(requestId, {
+ code: RpcErrorCode.INTERNAL_ERROR,
+ message: (e as any).message,
+ });
+ sendRpcResponse(+tabId, response);
navigate('/tx-status', {
state: {
txid: '',
@@ -212,11 +225,10 @@ function BtcSendRequest() {
return (
window.close()}
+ onCancel={handleCancel}
onConfirm={handleSubmit}
getFeeForFeeRate={calculateFeeForFeeRate}
onFeeRateSet={(newFeeRate) => setFeeRate(newFeeRate.toString())}
@@ -224,6 +236,7 @@ function BtcSendRequest() {
isSubmitting={isSubmitting}
isBroadcast
hideBottomBar
+ showAccountHeader
/>
);
}
diff --git a/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts b/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts
index 3486a7e7d..6ee2bb7b2 100644
--- a/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts
+++ b/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts
@@ -10,7 +10,7 @@ import { useLocation } from 'react-router-dom';
const useBtcSendRequestPayload = (btcAddress: string, network: SettingsNetwork) => {
const { search } = useLocation();
- const params = new URLSearchParams(search);
+ const params = useMemo(() => new URLSearchParams(search), [search]);
const tabId = params.get('tabId') ?? '0';
const requestId = params.get('requestId') ?? '';
@@ -38,7 +38,7 @@ const useBtcSendRequestPayload = (btcAddress: string, network: SettingsNetwork)
payload: rpcPayload,
requestToken: null,
};
- }, []);
+ }, [params, btcAddress, network]);
return { payload, tabId, requestToken, requestId };
};
diff --git a/src/app/screens/buy/index.tsx b/src/app/screens/buy/index.tsx
index ef302015b..4753fd87f 100644
--- a/src/app/screens/buy/index.tsx
+++ b/src/app/screens/buy/index.tsx
@@ -10,7 +10,7 @@ import useWalletSelector from '@hooks/useWalletSelector';
import { FeatureId, getMoonPaySignedUrl } from '@secretkeylabs/xverse-core';
import Spinner from '@ui-library/spinner';
import { MOON_PAY_API_KEY, MOON_PAY_URL, TRANSAC_API_KEY, TRANSAC_URL } from '@utils/constants';
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import styled from 'styled-components';
diff --git a/src/app/screens/coinDashboard/coinHeader.styled.ts b/src/app/screens/coinDashboard/coinHeader.styled.ts
index f5a0721d6..01b91c7cb 100644
--- a/src/app/screens/coinDashboard/coinHeader.styled.ts
+++ b/src/app/screens/coinDashboard/coinHeader.styled.ts
@@ -44,6 +44,7 @@ export const CoinBalanceText = styled.p((props) => ({
color: props.theme.colors.white_0,
textAlign: 'center',
wordBreak: 'break-all',
+ cursor: 'pointer',
}));
export const FiatAmountText = styled.p((props) => ({
@@ -52,6 +53,7 @@ export const FiatAmountText = styled.p((props) => ({
fontSize: '0.875rem',
marginTop: props.theme.spacing(2),
textAlign: 'center',
+ cursor: 'pointer',
}));
export const BalanceTitleText = styled.p((props) => ({
diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx
index bc82e7e52..2cb59a979 100644
--- a/src/app/screens/coinDashboard/coinHeader.tsx
+++ b/src/app/screens/coinDashboard/coinHeader.tsx
@@ -8,12 +8,13 @@ import BottomModal from '@components/bottomModal';
import ActionButton from '@components/button';
import SmallActionButton from '@components/smallActionButton';
import TokenImage from '@components/tokenImage';
-import useBtcWalletData from '@hooks/queries/useBtcWalletData';
-import useCoinRates from '@hooks/queries/useCoinRates';
+import useSelectedAccountBtcBalance from '@hooks/queries/useSelectedAccountBtcBalance';
import useStxWalletData from '@hooks/queries/useStxWalletData';
+import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates';
import useHasFeature from '@hooks/useHasFeature';
import useSelectedAccount from '@hooks/useSelectedAccount';
import useWalletSelector from '@hooks/useWalletSelector';
+import { getTrackingIdentifier, isMotherToken } from '@screens/swap/utils';
import {
AnalyticsEvents,
FeatureId,
@@ -22,7 +23,9 @@ import {
microstacksToStx,
type FungibleToken,
} from '@secretkeylabs/xverse-core';
+import { setBalanceHiddenToggleAction } from '@stores/wallet/actions/actionCreators';
import type { CurrencyTypes } from '@utils/constants';
+import { HIDDEN_BALANCE_LABEL } from '@utils/constants';
import { isInOptions, isLedgerAccount } from '@utils/helper';
import { trackMixPanel } from '@utils/mixpanel';
import { getBalanceAmount, getFtTicker } from '@utils/tokens';
@@ -30,6 +33,7 @@ import BigNumber from 'bignumber.js';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { NumericFormat } from 'react-number-format';
+import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import {
AvailableStxContainer,
@@ -57,18 +61,25 @@ type Props = {
export default function CoinHeader({ currency, fungibleToken }: Props) {
const selectedAccount = useSelectedAccount();
- const { fiatCurrency, network } = useWalletSelector();
- const { data: btcBalance } = useBtcWalletData();
+ const { fiatCurrency, network, balanceHidden } = useWalletSelector();
+
+ // TODO: this should be a dumb component, move the logic to the parent
+ // TODO: currently, we get btc and stx balances here for all currencies and FTs, but we should get them in
+ // TODO: the relevant parent and pass them as props
+ const { confirmedPaymentBalance: btcBalance } = useSelectedAccountBtcBalance();
const { data: stxData } = useStxWalletData();
- const { btcFiatRate, stxBtcRate } = useCoinRates();
+ const { btcFiatRate, stxBtcRate } = useSupportedCoinRates();
const navigate = useNavigate();
const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' });
+ const { t: commonT } = useTranslation('translation', { keyPrefix: 'COMMON' });
const [openReceiveModal, setOpenReceiveModal] = useState(false);
const isReceivingAddressesVisible = !isLedgerAccount(selectedAccount);
+ const dispatch = useDispatch();
const showRunesListing =
(useHasFeature(FeatureId.RUNES_LISTING) || process.env.NODE_ENV === 'development') &&
- network.type === 'Mainnet';
+ network.type === 'Mainnet' &&
+ fungibleToken?.protocol === 'runes';
const handleReceiveModalOpen = () => {
setOpenReceiveModal(true);
@@ -101,7 +112,12 @@ export default function CoinHeader({ currency, fungibleToken }: Props) {
value={microstacksToStx(new BigNumber(stxData?.locked ?? '0')).toString()}
displayType="text"
thousandSeparator
- renderText={(value: string) => {`${value} STX`}}
+ renderText={(value: string) => (
+
+ {balanceHidden && HIDDEN_BALANCE_LABEL}
+ {!balanceHidden && `${value} STX`}
+
+ )}
/>
@@ -110,7 +126,12 @@ export default function CoinHeader({ currency, fungibleToken }: Props) {
value={microstacksToStx(new BigNumber(stxData?.availableBalance ?? 0)).toString()}
displayType="text"
thousandSeparator
- renderText={(value: string) => {`${value} STX`}}
+ renderText={(value: string) => (
+
+ {balanceHidden && HIDDEN_BALANCE_LABEL}
+ {!balanceHidden && `${value} STX`}
+
+ )}
/>
@@ -150,33 +171,49 @@ export default function CoinHeader({ currency, fungibleToken }: Props) {
const getDashboardTitle = () => {
if (fungibleToken?.name) {
- return `${fungibleToken.name} ${t('BALANCE')}`;
+ return fungibleToken.name;
}
-
if (!currency) {
return '';
}
-
if (currency === 'STX') {
- return `Stacks ${t('BALANCE')}`;
+ if (new BigNumber(stxData?.locked ?? 0).gt(0)) {
+ return `${commonT('STACKS')} ${commonT('BALANCE')}`;
+ }
+ return commonT('STACKS');
}
if (currency === 'BTC') {
- return `Bitcoin ${t('BALANCE')}`;
+ return commonT('BITCOIN');
}
- return `${currency} ${t('BALANCE')}`;
+ return `${currency}`;
};
const isCrossChainSwapsEnabled = useHasFeature(FeatureId.CROSS_CHAIN_SWAPS);
- const showRunesSwap =
- (currency === 'FT' && fungibleToken?.protocol === 'runes') || currency === 'BTC';
- const showSwaps = isCrossChainSwapsEnabled && showRunesSwap;
+ const isStacksSwapsEnabled = useHasFeature(FeatureId.STACKS_SWAPS);
+
+ const isSwapEligibleCurrency =
+ (currency === 'FT' &&
+ (fungibleToken?.protocol === 'runes' ||
+ (isStacksSwapsEnabled && fungibleToken?.protocol === 'stacks'))) ||
+ currency === 'BTC' ||
+ (currency === 'STX' && isStacksSwapsEnabled);
+ const showSwaps = isCrossChainSwapsEnabled && isSwapEligibleCurrency;
+
+ const fiatValue = getFiatEquivalent(
+ Number(getBalanceAmount(currency, fungibleToken, stxData, btcBalance)),
+ currency,
+ BigNumber(stxBtcRate),
+ BigNumber(btcFiatRate),
+ fungibleToken,
+ );
const navigateToSwaps = () => {
if (!showSwaps) {
return;
}
trackMixPanel(AnalyticsEvents.InitiateSwapFlow, {
- selectedToken: fungibleToken ? fungibleToken.name ?? fungibleToken.principal : currency,
+ selectedToken: fungibleToken ? getTrackingIdentifier(fungibleToken) : currency,
+ principal: isMotherToken(fungibleToken) ? getTrackingIdentifier(fungibleToken) : undefined,
});
navigate(`/swap?from=${fungibleToken ? fungibleToken.principal : currency}`);
};
@@ -194,6 +231,9 @@ export default function CoinHeader({ currency, fungibleToken }: Props) {
handleReceiveModalOpen();
};
+ const toggleHideBalance = () =>
+ dispatch(setBalanceHiddenToggleAction({ toggle: !balanceHidden }));
+
return (
@@ -218,23 +258,27 @@ export default function CoinHeader({ currency, fungibleToken }: Props) {
displayType="text"
thousandSeparator
renderText={(value: string) => (
- {`${value} ${getTokenTicker()}`}
+
+ {balanceHidden && HIDDEN_BALANCE_LABEL}
+ {!balanceHidden && `${value} ${getTokenTicker()}`}
+
)}
/>
- {value}}
- />
+ {fiatValue && (
+ (
+
+ {balanceHidden && HIDDEN_BALANCE_LABEL}
+ {!balanceHidden && value}
+
+ )}
+ />
+ )}
{renderStackingBalances()}
@@ -244,7 +288,7 @@ export default function CoinHeader({ currency, fungibleToken }: Props) {
{showSwaps && (
)}
- {showRunesListing && fungibleToken?.protocol === 'runes' && (
+ {showRunesListing && (
({
+ display: 'flex',
+ flexDirection: 'row',
+ width: '100%',
+ marginBottom: props.theme.space.m,
+}));
+
+const IconOuterContainer = styled.div((_) => ({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+}));
+
+const IconContainer = styled.div((_) => ({
+ position: 'relative',
+}));
+
+const TitleContainer = styled.div((props) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ margin: `${props.theme.space.s} ${props.theme.space.m}`,
+}));
+
+const AddressTypeContainer = styled.div((_) => ({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: '4px',
+}));
+
+const BalanceContainer = styled.div((props) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-end',
+ margin: `${props.theme.space.s} ${props.theme.space.m}`,
+ flexGrow: 1,
+}));
+
+const StarIconContainer = styled.div((props) => ({
+ position: 'absolute',
+ right: '-8px',
+ bottom: '4px',
+ backgroundColor: props.theme.colors.elevation0,
+ borderRadius: '50%',
+ padding: '2.2px',
+ display: 'flex',
+ alignItems: 'center',
+}));
+
+type AddressBalanceProps = {
+ balance: number | undefined;
+ addressType: BtcAddressType | undefined;
+ totalBalance: number | undefined;
+};
+
+export default function AddressBalance({
+ balance,
+ addressType,
+ totalBalance,
+}: AddressBalanceProps) {
+ const { fiatCurrency, btcPaymentAddressType, balanceHidden } = useWalletSelector();
+ const { btcFiatRate } = useSupportedCoinRates();
+
+ if (balance === undefined) {
+ return null;
+ }
+
+ const balancePercentage =
+ balance && totalBalance
+ ? BigNumber(balance).div(totalBalance).multipliedBy(100).toFixed(2)
+ : '0.00';
+
+ const isCurrentPaymentAddress = addressType === btcPaymentAddressType;
+
+ return (
+
+
+
+
+ {isCurrentPaymentAddress && (
+
+
+
+ )}
+
+
+
+
+ BTC
+ {addressType && }
+
+ {`${balancePercentage}%`}
+
+
+
+ {balanceHidden ? HIDDEN_BALANCE_LABEL : satsToBtc(BigNumber(balance)).toString()}
+
+
+ {balanceHidden ? (
+ HIDDEN_BALANCE_LABEL
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/screens/coinDashboard/coins/btc/balanceBreakdown.tsx b/src/app/screens/coinDashboard/coins/btc/balanceBreakdown.tsx
new file mode 100644
index 000000000..dba37bf35
--- /dev/null
+++ b/src/app/screens/coinDashboard/coins/btc/balanceBreakdown.tsx
@@ -0,0 +1,53 @@
+import useSelectedAccountBtcBalance from '@hooks/queries/useSelectedAccountBtcBalance';
+import useWalletSelector from '@hooks/useWalletSelector';
+import Spinner from '@ui-library/spinner';
+import styled from 'styled-components';
+import { SecondaryContainer } from '../../index.styled';
+import AddressBalance from './addressBalance';
+
+const SpinnerContainer = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+export default function BalanceBreakdown() {
+ const { btcPaymentAddressType } = useWalletSelector();
+ const { confirmedPaymentBalance, nativeBalance, nestedBalance, taprootBalance, isLoading } =
+ useSelectedAccountBtcBalance();
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const showNested = btcPaymentAddressType === 'nested' || nestedBalance?.confirmedBalance !== 0;
+
+ return (
+
+
+ {showNested && (
+
+ )}
+
+
+ );
+}
diff --git a/src/app/screens/coinDashboard/coins/btc/index.tsx b/src/app/screens/coinDashboard/coins/btc/index.tsx
new file mode 100644
index 000000000..fd8f20044
--- /dev/null
+++ b/src/app/screens/coinDashboard/coins/btc/index.tsx
@@ -0,0 +1,79 @@
+import { GlobalPreferredBtcAddressSheet } from '@components/preferredBtcAddress';
+import BottomBar from '@components/tabBar';
+import TopRow from '@components/topRow';
+import useCanUserSwitchPaymentType from '@hooks/useCanUserSwitchPaymentType';
+import { broadcastResetUserFlow, useResetUserFlow } from '@hooks/useResetUserFlow';
+import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed';
+import { Tabs, type TabProp } from '@ui-library/tabs';
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useSearchParams } from 'react-router-dom';
+import CoinHeader from '../../coinHeader';
+import { Container, FtInfoContainer } from '../../index.styled';
+import TransactionsHistoryList from '../../transactionsHistoryList';
+import BalanceBreakdown from './balanceBreakdown';
+
+export default function CoinDashboard() {
+ const [searchParams] = useSearchParams();
+ const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' });
+ const fromSecondaryTab = searchParams.get('secondaryTab') === 'true' ? 'secondary' : 'primary';
+
+ const [currentTab, setCurrentTab] = useState<'primary' | 'secondary'>(fromSecondaryTab);
+ const [showPreferredBtcAddressSheet, setShowPreferredBtcAddressSheet] = useState(false);
+
+ useResetUserFlow('/coinDashboard');
+
+ const showBtcAddressTypeSelector = useCanUserSwitchPaymentType();
+
+ const handleGoBack = () => broadcastResetUserFlow();
+ const handleChangeAddressTypeClick = showBtcAddressTypeSelector
+ ? () => setShowPreferredBtcAddressSheet(true)
+ : undefined;
+
+ useTrackMixPanelPageViewed({});
+
+ const onCancelAddressType = () => setShowPreferredBtcAddressSheet(false);
+
+ const tabs: TabProp<'primary' | 'secondary'>[] = [
+ {
+ label: t('TRANSACTIONS'),
+ value: 'primary',
+ },
+ {
+ label: t('BREAKDOWN'),
+ value: 'secondary',
+ },
+ ];
+
+ return (
+ <>
+
+
+
+
+ setCurrentTab(tabClicked)}
+ />
+
+ {currentTab === 'primary' && (
+
+ )}
+ {currentTab === 'secondary' && }
+
+
+
+ >
+ );
+}
diff --git a/src/app/screens/coinDashboard/coins/other.tsx b/src/app/screens/coinDashboard/coins/other.tsx
new file mode 100644
index 000000000..b6d5c09ad
--- /dev/null
+++ b/src/app/screens/coinDashboard/coins/other.tsx
@@ -0,0 +1,248 @@
+import linkIcon from '@assets/img/linkIcon.svg';
+import CopyButton from '@components/copyButton';
+import OptionsDialog from '@components/optionsDialog/optionsDialog';
+import BottomBar from '@components/tabBar';
+import TopRow from '@components/topRow';
+import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens';
+import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery';
+import useRuneUtxosQuery from '@hooks/queries/runes/useRuneUtxosQuery';
+import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens';
+import useSpamTokens from '@hooks/queries/useSpamTokens';
+import { broadcastResetUserFlow, useResetUserFlow } from '@hooks/useResetUserFlow';
+import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed';
+import { Flag } from '@phosphor-icons/react';
+import RuneBundleRow from '@screens/coinDashboard/runes/bundleRow';
+import type { FungibleToken } from '@secretkeylabs/xverse-core';
+import { mapRareSatsAPIResponseToBundle } from '@secretkeylabs/xverse-core';
+import {
+ setBrc20ManageTokensAction,
+ setRunesManageTokensAction,
+ setSip10ManageTokensAction,
+ setSpamTokenAction,
+} from '@stores/wallet/actions/actionCreators';
+import { SPAM_OPTIONS_WIDTH, type CurrencyTypes } from '@utils/constants';
+import { ftDecimals, getExplorerUrl, getTruncatedAddress } from '@utils/helper';
+import { getFullTxId, getTxIdFromFullTxId, getVoutFromFullTxId } from '@utils/runes';
+import BigNumber from 'bignumber.js';
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+import { useParams, useSearchParams } from 'react-router-dom';
+import Theme from 'theme';
+import CoinHeader from '../coinHeader';
+import {
+ Button,
+ ButtonRow,
+ Container,
+ ContractAddressCopyButton,
+ ContractDeploymentButton,
+ CopyButtonContainer,
+ FtInfoContainer,
+ RuneBundlesContainer,
+ SecondaryContainer,
+ ShareIcon,
+ TokenContractAddress,
+ TokenText,
+} from '../index.styled';
+import TransactionsHistoryList from '../transactionsHistoryList';
+
+// TODO: This should be refactored into separate components for each protocol/coin as needed
+// TODO: with a shared component for the common parts
+
+export default function CoinDashboard() {
+ const [searchParams] = useSearchParams();
+ const ftKey = searchParams.get('ftKey') ?? '';
+ const protocol = searchParams.get('protocol') ?? '';
+ const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' });
+ const fromSecondaryTab = searchParams.get('secondaryTab') === 'true' ? 'secondary' : 'primary';
+ const [currentTab, setCurrentTab] = useState<'primary' | 'secondary'>(fromSecondaryTab);
+ const [showOptionsDialog, setShowOptionsDialog] = useState(false);
+ const [optionsDialogIndents, setOptionsDialogIndents] = useState<
+ { top: string; left: string } | undefined
+ >();
+ const { addToSpamTokens } = useSpamTokens();
+ const dispatch = useDispatch();
+ const { currency } = useParams();
+ const { data: runesCoinsList } = useVisibleRuneFungibleTokens();
+ const { data: sip10CoinsList } = useVisibleSip10FungibleTokens();
+ const { data: brc20CoinsList } = useVisibleBrc20FungibleTokens();
+
+ let selectedFt: FungibleToken | undefined;
+
+ if (ftKey && protocol) {
+ switch (protocol) {
+ case 'stacks':
+ selectedFt = sip10CoinsList.find((ft) => ft.principal === ftKey);
+ break;
+ case 'brc-20':
+ selectedFt = brc20CoinsList.find((ft) => ft.principal === ftKey);
+ break;
+ case 'runes':
+ selectedFt = runesCoinsList.find((ft) => ft.principal === ftKey);
+ break;
+ default:
+ selectedFt = undefined;
+ }
+ }
+ const { data: runeUtxos } = useRuneUtxosQuery(
+ selectedFt?.protocol === 'runes' ? selectedFt?.name : '',
+ );
+
+ const showTxHistory = currentTab === 'primary';
+ const displayTabs = ['stacks', 'runes'].includes(protocol);
+ const showStxContract = currentTab === 'secondary' && selectedFt && protocol === 'stacks';
+ const showRuneBundles = currentTab === 'secondary' && selectedFt && protocol === 'runes';
+
+ useResetUserFlow('/coinDashboard');
+
+ const handleGoBack = () => broadcastResetUserFlow();
+
+ useTrackMixPanelPageViewed(
+ protocol
+ ? {
+ protocol,
+ }
+ : {},
+ );
+
+ const openOptionsDialog = (event: React.MouseEvent) => {
+ setShowOptionsDialog(true);
+ setOptionsDialogIndents({
+ top: `${(event.target as HTMLElement).parentElement?.getBoundingClientRect().top}px`,
+ left: `calc(100% - ${SPAM_OPTIONS_WIDTH}px)`,
+ });
+ };
+
+ const closeOptionsDialog = () => setShowOptionsDialog(false);
+
+ return (
+ <>
+
+ {showOptionsDialog && (
+
+ {
+ if (!selectedFt) {
+ handleGoBack();
+ return;
+ }
+ // set the visibility to false
+ const payload = {
+ principal: selectedFt.principal,
+ isEnabled: false,
+ };
+ if (protocol === 'runes') {
+ dispatch(setRunesManageTokensAction(payload));
+ } else if (protocol === 'stacks') {
+ dispatch(setSip10ManageTokensAction(payload));
+ } else if (protocol === 'brc-20') {
+ dispatch(setBrc20ManageTokensAction(payload));
+ }
+
+ addToSpamTokens(selectedFt.principal);
+ dispatch(setSpamTokenAction(selectedFt));
+
+ handleGoBack();
+ }}
+ >
+
+
+ {t('HIDE_AND_REPORT')}
+
+
+
+ )}
+
+
+ {/* TODO: import { Tabs } from ui-library/tabs.tsx */}
+ {displayTabs && (
+
+
+
+
+ )}
+ {showTxHistory && (
+
+ )}
+ {showStxContract && (
+
+ {t('FT_CONTRACT_PREFIX')}
+ navigator.clipboard.writeText(selectedFt?.principal as string)}
+ >
+
+ {getTruncatedAddress(selectedFt?.principal as string, 20)}
+
+
+
+
+
+ window.open(getExplorerUrl(selectedFt?.principal as string), '_blank')}
+ >
+ {t('OPEN_FT_CONTRACT_DEPLOYMENT')}
+ {t('STACKS_EXPLORER')}
+
+
+
+ )}
+ {showRuneBundles && (
+
+
+ {runeUtxos?.map((utxo) => {
+ const fullTxId = getFullTxId(utxo);
+ const runeAmount = utxo.runes?.filter((rune) => rune[0] === selectedFt?.name)[0][1]
+ .amount;
+ return (
+
+ );
+ })}
+
+
+ )}
+
+
+ >
+ );
+}
diff --git a/src/app/screens/coinDashboard/index.styled.ts b/src/app/screens/coinDashboard/index.styled.ts
new file mode 100644
index 000000000..d77300c26
--- /dev/null
+++ b/src/app/screens/coinDashboard/index.styled.ts
@@ -0,0 +1,120 @@
+import { StyledP } from '@ui-library/common.styled';
+import styled from 'styled-components';
+
+export const Container = styled.div((props) => ({
+ display: 'flex',
+ flex: 1,
+ marginTop: props.theme.space.xs,
+ flexDirection: 'column',
+ overflowY: 'auto',
+ '&::-webkit-scrollbar': {
+ display: 'none',
+ },
+}));
+
+export const SecondaryContainer = styled.div((props) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ paddingLeft: props.theme.space.m,
+ paddingRight: props.theme.space.m,
+ marginTop: props.theme.space.m,
+ marginBottom: props.theme.space.xl,
+ h1: {
+ ...props.theme.typography.body_medium_m,
+ color: props.theme.colors.white_400,
+ },
+}));
+
+export const ContractAddressCopyButton = styled.button((props) => ({
+ display: 'flex',
+ marginTop: props.theme.space.xxs,
+ background: 'transparent',
+}));
+
+export const TokenContractAddress = styled.p((props) => ({
+ ...props.theme.typography.body_medium_l,
+ color: props.theme.colors.white_0,
+ textAlign: 'left',
+ overflowWrap: 'break-word',
+ width: 300,
+}));
+
+export const FtInfoContainer = styled.div((props) => ({
+ display: 'flex',
+ flexDirection: 'row',
+ borderTop: `1px solid ${props.theme.colors.elevation2}`,
+ paddingTop: props.theme.space.l,
+ marginTop: props.theme.space.xl,
+ paddingLeft: props.theme.space.m,
+}));
+
+export const ShareIcon = styled.img({
+ width: 18,
+ height: 18,
+});
+
+export const CopyButtonContainer = styled.div((props) => ({
+ marginRight: props.theme.space.xxs,
+}));
+
+export const ContractDeploymentButton = styled.button((props) => ({
+ ...props.theme.typography.body_m,
+ display: 'flex',
+ alignItems: 'center',
+ marginTop: props.theme.space.l,
+ background: 'none',
+ color: props.theme.colors.white_400,
+ span: {
+ color: props.theme.colors.white_0,
+ marginLeft: props.theme.space.xs,
+ },
+ img: {
+ marginLeft: props.theme.space.xs,
+ },
+}));
+
+export const Button = styled.button<{
+ isSelected: boolean;
+}>((props) => ({
+ ...props.theme.typography.body_bold_l,
+ fontSize: 11,
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: 31,
+ paddingLeft: props.theme.space.s,
+ paddingRight: props.theme.space.s,
+ marginRight: props.theme.space.xxs,
+ borderRadius: 44,
+ background: props.isSelected ? props.theme.colors.elevation2 : 'transparent',
+ color: props.theme.colors.white_0,
+ opacity: props.isSelected ? 1 : 0.6,
+}));
+
+export const ButtonRow = styled.button`
+ display: flex;
+ align-items: center;
+ background-color: transparent;
+ flex-direction: row;
+ padding-left: ${(props) => props.theme.space.m};
+ padding-right: ${(props) => props.theme.space.m};
+ padding-top: ${(props) => props.theme.space.s};
+ padding-bottom: ${(props) => props.theme.space.s};
+ transition: background-color 0.2s ease;
+ :hover {
+ background-color: ${(props) => props.theme.colors.elevation3};
+ }
+ :active {
+ background-color: ${(props) => props.theme.colors.elevation3};
+ }
+`;
+
+export const TokenText = styled(StyledP)`
+ margin-left: ${(props) => props.theme.space.m};
+`;
+
+export const RuneBundlesContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${(props) => props.theme.space.s};
+`;
diff --git a/src/app/screens/coinDashboard/index.tsx b/src/app/screens/coinDashboard/index.tsx
index 35d8a17b9..a89395893 100644
--- a/src/app/screens/coinDashboard/index.tsx
+++ b/src/app/screens/coinDashboard/index.tsx
@@ -1,319 +1,15 @@
-import linkIcon from '@assets/img/linkIcon.svg';
-import CopyButton from '@components/copyButton';
-import OptionsDialog from '@components/optionsDialog/optionsDialog';
-import BottomBar from '@components/tabBar';
-import TopRow from '@components/topRow';
-import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens';
-import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery';
-import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens';
-import useBtcWalletData from '@hooks/queries/useBtcWalletData';
-import useSpamTokens from '@hooks/queries/useSpamTokens';
-import { broadcastResetUserFlow, useResetUserFlow } from '@hooks/useResetUserFlow';
-import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed';
-import { Flag } from '@phosphor-icons/react';
-import type { FungibleToken } from '@secretkeylabs/xverse-core';
-import {
- setBrc20ManageTokensAction,
- setRunesManageTokensAction,
- setSip10ManageTokensAction,
- setSpamTokenAction,
-} from '@stores/wallet/actions/actionCreators';
-import { StyledP } from '@ui-library/common.styled';
-import { SPAM_OPTIONS_WIDTH, type CurrencyTypes } from '@utils/constants';
-import { getExplorerUrl } from '@utils/helper';
-import { useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useDispatch } from 'react-redux';
-import { useParams, useSearchParams } from 'react-router-dom';
-import styled from 'styled-components';
-import Theme from 'theme';
-import CoinHeader from './coinHeader';
-import TransactionsHistoryList from './transactionsHistoryList';
-
-const Container = styled.div((props) => ({
- display: 'flex',
- flex: 1,
- marginTop: props.theme.spacing(4),
- flexDirection: 'column',
- overflowY: 'auto',
- '&::-webkit-scrollbar': {
- display: 'none',
- },
-}));
-
-const TokenContractContainer = styled.div((props) => ({
- display: 'flex',
- flexDirection: 'column',
- paddingLeft: props.theme.spacing(8),
- paddingRight: props.theme.spacing(8),
- marginTop: props.theme.spacing(16),
- h1: {
- ...props.theme.typography.body_medium_m,
- color: props.theme.colors.white_400,
- },
-}));
-
-const ContractAddressCopyButton = styled.button((props) => ({
- display: 'flex',
- marginTop: props.theme.spacing(2),
- background: 'transparent',
-}));
-
-const TokenContractAddress = styled.p((props) => ({
- ...props.theme.typography.body_medium_l,
- color: props.theme.colors.white_0,
- textAlign: 'left',
- overflowWrap: 'break-word',
- width: 300,
-}));
-
-const FtInfoContainer = styled.div((props) => ({
- display: 'flex',
- flexDirection: 'row',
- borderTop: `1px solid ${props.theme.colors.elevation2}`,
- paddingTop: props.theme.spacing(12),
- marginTop: props.theme.spacing(16),
- paddingLeft: props.theme.spacing(8),
- paddingRight: props.theme.spacing(14),
-}));
-
-const ShareIcon = styled.img({
- width: 18,
- height: 18,
-});
-
-const CopyButtonContainer = styled.div((props) => ({
- marginRight: props.theme.spacing(2),
-}));
-
-const ContractDeploymentButton = styled.button((props) => ({
- ...props.theme.typography.body_m,
- display: 'flex',
- alignItems: 'center',
- marginTop: props.theme.spacing(12),
- background: 'none',
- color: props.theme.colors.white_400,
- span: {
- color: props.theme.colors.white_0,
- marginLeft: props.theme.spacing(3),
- },
- img: {
- marginLeft: props.theme.spacing(3),
- },
-}));
-
-const Button = styled.button<{
- isSelected: boolean;
-}>((props) => ({
- ...props.theme.typography.body_bold_l,
- fontSize: 11,
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- height: 31,
- paddingLeft: props.theme.spacing(6),
- paddingRight: props.theme.spacing(6),
- marginRight: props.theme.spacing(2),
- borderRadius: 44,
- background: props.isSelected ? props.theme.colors.elevation2 : 'transparent',
- color: props.theme.colors.white_0,
- opacity: props.isSelected ? 1 : 0.6,
-}));
-
-const ButtonRow = styled.button`
- display: flex;
- align-items: center;
- background-color: transparent;
- flex-direction: row;
- padding-left: ${(props) => props.theme.space.m};
- padding-right: ${(props) => props.theme.space.m};
- padding-top: ${(props) => props.theme.space.s};
- padding-bottom: ${(props) => props.theme.space.s};
- transition: background-color 0.2s ease;
- :hover {
- background-color: ${(props) => props.theme.colors.elevation3};
- }
- :active {
- background-color: ${(props) => props.theme.colors.elevation3};
- }
-`;
-
-const TokenText = styled(StyledP)`
- margin-left: ${(props) => props.theme.space.m};
-`;
+import { useParams } from 'react-router-dom';
+import Btc from './coins/btc';
+import Other from './coins/other';
export default function CoinDashboard() {
- const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' });
- const [showFtContractDetails, setShowFtContractDetails] = useState(false);
- const [showOptionsDialog, setShowOptionsDialog] = useState(false);
- const [optionsDialogIndents, setOptionsDialogIndents] = useState<
- { top: string; left: string } | undefined
- >();
- const [searchParams] = useSearchParams();
- const { addToSpamTokens } = useSpamTokens();
- const dispatch = useDispatch();
const { currency } = useParams();
- const { visible: runesCoinsList } = useVisibleRuneFungibleTokens();
- const { visible: sip10CoinsList } = useVisibleSip10FungibleTokens();
- const { visible: brc20CoinsList } = useVisibleBrc20FungibleTokens();
-
- const ftKey = searchParams.get('ftKey');
- const protocol = searchParams.get('protocol');
- let selectedFt: FungibleToken | undefined;
- if (ftKey && protocol) {
- switch (protocol) {
- case 'stacks':
- selectedFt = sip10CoinsList.find((ft) => ft.principal === ftKey);
- break;
- case 'brc-20':
- selectedFt = brc20CoinsList.find((ft) => ft.principal === ftKey);
- break;
- case 'runes':
- selectedFt = runesCoinsList.find((ft) => ft.principal === ftKey);
- break;
- default:
- selectedFt = undefined;
- }
+ switch (currency) {
+ case 'BTC':
+ return ;
+ default:
+ // TODO: split this more. Other is currently doing too much
+ return ;
}
-
- useResetUserFlow('/coinDashboard');
- useBtcWalletData();
-
- const handleGoBack = () => broadcastResetUserFlow();
-
- useTrackMixPanelPageViewed(
- protocol
- ? {
- protocol,
- }
- : {},
- );
-
- const openOptionsDialog = (event: React.MouseEvent) => {
- setShowOptionsDialog(true);
-
- setOptionsDialogIndents({
- top: `${(event.target as HTMLElement).parentElement?.getBoundingClientRect().top}px`,
- left: `calc(100% - ${SPAM_OPTIONS_WIDTH}px)`,
- });
- };
-
- const closeOptionsDialog = () => {
- setShowOptionsDialog(false);
- };
-
- const openContractDeployment = () =>
- window.open(getExplorerUrl(selectedFt?.principal as string), '_blank');
-
- const onContractClick = () => setShowFtContractDetails(true);
-
- const handleCopyContractAddress = () =>
- navigator.clipboard.writeText(selectedFt?.principal as string);
-
- const onTransactionsClick = () => setShowFtContractDetails(false);
-
- const formatAddress = (addr: string): string =>
- addr ? `${addr.substring(0, 20)}...${addr.substring(addr.length - 20, addr.length)}` : '';
-
- return (
- <>
-
- {showOptionsDialog && (
-
- {
- if (!selectedFt) {
- handleGoBack();
- return;
- }
- // set the visibility to false
- const payload = {
- principal: selectedFt.principal,
- isEnabled: false,
- };
- if (protocol === 'runes') {
- dispatch(setRunesManageTokensAction(payload));
- } else if (protocol === 'stacks') {
- dispatch(setSip10ManageTokensAction(payload));
- } else if (protocol === 'brc-20') {
- dispatch(setBrc20ManageTokensAction(payload));
- }
-
- addToSpamTokens(selectedFt.principal);
- dispatch(setSpamTokenAction(selectedFt));
-
- handleGoBack();
- }}
- >
-
-
- {t('HIDE_AND_REPORT')}
-
-
-
- )}
-
-
- {protocol === 'stacks' && (
-
-
-
-
- )}
- {selectedFt && protocol === 'stacks' && showFtContractDetails && (
-
- {t('FT_CONTRACT_PREFIX')}
-
-
- {formatAddress(selectedFt?.principal as string)}
-
-
-
-
-
-
- {t('OPEN_FT_CONTRACT_DEPLOYMENT')}
- {t('STACKS_EXPLORER')}
-
-
-
- )}
- {!showFtContractDetails && (
-
- )}
-
-
- >
- );
}
diff --git a/src/app/screens/coinDashboard/runes/bundleRow.tsx b/src/app/screens/coinDashboard/runes/bundleRow.tsx
new file mode 100644
index 000000000..8492595e5
--- /dev/null
+++ b/src/app/screens/coinDashboard/runes/bundleRow.tsx
@@ -0,0 +1,160 @@
+import RareSatIcon from '@components/rareSatIcon/rareSatIcon';
+import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer';
+import useWalletSelector from '@hooks/useWalletSelector';
+import type { Bundle } from '@secretkeylabs/xverse-core';
+import { StyledP } from '@ui-library/common.styled';
+import { HIDDEN_BALANCE_LABEL } from '@utils/constants';
+import { getTruncatedAddress } from '@utils/helper';
+import { useTranslation } from 'react-i18next';
+import { NumericFormat } from 'react-number-format';
+import { useNavigate } from 'react-router-dom';
+import styled from 'styled-components';
+
+const Container = styled.div`
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ justify-content: space-between;
+ flex: 1;
+ padding: ${(props) => props.theme.space.s};
+ padding-left ${(props) => props.theme.space.m};
+ border-radius: ${(props) => props.theme.space.xs};
+ border: 1px solid 'transparent';
+ background-color: ${(props) => props.theme.colors.elevation1};
+ gap: ${(props) => props.theme.space.s};
+ :hover {
+ cursor: pointer;
+ },
+`;
+
+const InfoContainer = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ gap: ${(props) => props.theme.space.xxxs};
+`;
+
+const SubContainer = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: row;
+ justify-content: space-between;
+`;
+
+const RuneTitle = styled(StyledP)`
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+ text-align: left;
+`;
+
+const StyledBundleSub = styled(StyledP)`
+ width: 100%;
+ text-align: left;
+`;
+
+const SizeInfoContainer = styled.div`
+ display: flex;
+ align-items: center;
+ column-gap: ${(props) => props.theme.space.xxs};
+`;
+
+const RangeContainer = styled.div``;
+
+const Range = styled.div`
+ display: flex;
+ border-radius: 6px;
+ border: 1px solid ${(props) => props.theme.colors.white_800};
+ padding: 1px;
+ flex-wrap: wrap;
+ flex-direction: row;
+ align-items: center;
+`;
+
+type Props = {
+ runeAmount: string;
+ runeSymbol: string;
+ runeId: string;
+ txId: string;
+ vout: string;
+ satAmount: number;
+ bundle: Bundle;
+};
+
+function RuneBundleRow({ runeAmount, runeSymbol, runeId, txId, vout, satAmount, bundle }: Props) {
+ const { t } = useTranslation('translation', { keyPrefix: 'COMMON' });
+ const { balanceHidden } = useWalletSelector();
+ const navigate = useNavigate();
+ const satributesArr = bundle.satributes.flatMap((item) => item);
+ const { setSelectedSatBundleDetails } = useSatBundleDataReducer();
+
+ const handleOnClick = () => {
+ // exotics v1 wont show range details only bundle details
+ setSelectedSatBundleDetails(bundle);
+ navigate('/nft-dashboard/rare-sats-bundle', { state: { source: 'RuneBundlesTab', runeId } });
+ };
+
+ const onKeyDown = (event) => {
+ if (event.key === 'Enter') {
+ handleOnClick();
+ }
+ };
+
+ return (
+
+
+
+ {satributesArr.map((satribute) => (
+
+ ))}
+
+
+
+ {balanceHidden ? (
+
+ {HIDDEN_BALANCE_LABEL}
+
+ ) : (
+ (
+
+ {value}
+
+ )}
+ />
+ )}
+
+
+ {`${getTruncatedAddress(txId, 6)}:${vout}`}
+
+
+ (
+
+ {value}
+
+ )}
+ />
+
+
+
+
+ );
+}
+
+export default RuneBundleRow;
diff --git a/src/app/screens/coinDashboard/transactionsHistoryList.tsx b/src/app/screens/coinDashboard/transactionsHistoryList.tsx
index f78c73e08..ccc976e4e 100644
--- a/src/app/screens/coinDashboard/transactionsHistoryList.tsx
+++ b/src/app/screens/coinDashboard/transactionsHistoryList.tsx
@@ -18,7 +18,7 @@ import type {
} from '@stacks/stacks-blockchain-api-types';
import Spinner from '@ui-library/spinner';
import type { CurrencyTypes } from '@utils/constants';
-import { formatDate } from '@utils/date';
+import { formatDate, formatDateKey } from '@utils/date';
import { isLedgerAccount } from '@utils/helper';
import {
isAddressTransactionWithTransfers,
@@ -34,18 +34,18 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
-const ListItemsContainer = styled.div({
+const ListItemsContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
flex: 1,
-});
+ marginTop: props.theme.space.l,
+}));
const ListHeader = styled.h1((props) => ({
- marginTop: props.theme.spacing(20),
- marginBottom: props.theme.spacing(12),
- marginLeft: props.theme.spacing(8),
- marginRight: props.theme.spacing(8),
- ...props.theme.headline_s,
+ ...props.theme.typography.headline_xs,
+ margin: props.theme.space.m,
+ marginTop: 0,
+ marginBottom: props.theme.space.l,
}));
const LoadingContainer = styled.div({
@@ -65,7 +65,7 @@ const NoTransactionsContainer = styled.div((props) => ({
}));
const GroupContainer = styled(animated.div)((props) => ({
- marginBottom: props.theme.spacing(8),
+ marginBottom: props.theme.space.m,
}));
const SectionHeader = styled.div((props) => ({
@@ -73,8 +73,8 @@ const SectionHeader = styled.div((props) => ({
flexDirection: 'row',
alignItems: 'center',
marginBottom: props.theme.spacing(7),
- paddingLeft: props.theme.spacing(8),
- paddingRight: props.theme.spacing(8),
+ paddingLeft: props.theme.space.m,
+ paddingRight: props.theme.space.m,
}));
const SectionSeparator = styled.div((props) => ({
@@ -86,87 +86,161 @@ const SectionSeparator = styled.div((props) => ({
const SectionTitle = styled.p((props) => ({
...props.theme.body_xs,
color: props.theme.colors.white_200,
- marginRight: props.theme.spacing(4),
+ marginRight: props.theme.space.xs,
}));
-interface TransactionsHistoryListProps {
- coin: CurrencyTypes;
- stxTxFilter: string | null;
- brc20Token: string | null;
- runeToken: string | null;
- runeSymbol: string | null;
-}
-
-const sortTransactionsByBlockHeight = (transactions: BtcTransactionData[]) =>
- transactions.sort((txA, txB) => {
- if (txB.blockHeight > txA.blockHeight) {
- return 1;
- }
- return -1;
- });
-
const groupBtcTxsByDate = (
transactions: BtcTransactionData[],
-): { [x: string]: BtcTransactionData[] } => {
+): [Date, string, BtcTransactionData[]][] => {
const pendingTransactions: BtcTransactionData[] = [];
const processedTransactions: { [x: string]: BtcTransactionData[] } = {};
transactions.forEach((transaction) => {
- const txDate = formatDate(new Date(transaction.seenTime));
if (transaction.txStatus === 'pending') {
pendingTransactions.push(transaction);
} else {
- if (!processedTransactions[txDate]) processedTransactions[txDate] = [transaction];
- else processedTransactions[txDate].push(transaction);
-
- sortTransactionsByBlockHeight(processedTransactions[txDate]);
+ const txDateKey = formatDateKey(new Date(transaction.seenTime));
+ if (!processedTransactions[txDateKey]) {
+ processedTransactions[txDateKey] = [];
+ }
+ processedTransactions[txDateKey].push(transaction);
}
});
- sortTransactionsByBlockHeight(pendingTransactions);
+
+ const result: [Date, string, BtcTransactionData[]][] = [];
+
if (pendingTransactions.length > 0) {
- const result = { Pending: pendingTransactions, ...processedTransactions };
- return result;
+ result.push([new Date(), 'Pending', pendingTransactions]);
}
- return processedTransactions;
+
+ Object.values(processedTransactions).forEach((grp) => {
+ if (grp.length === 0) {
+ return;
+ }
+
+ grp.sort((txA, txB) => {
+ // sort by block height first
+ const blockHeightDiff = txB.blockHeight - txA.blockHeight;
+ if (blockHeightDiff !== 0) {
+ return blockHeightDiff;
+ }
+
+ // if block height is the same, sort by txid for consistency
+ return txB.txid.localeCompare(txA.txid);
+ });
+
+ result.push([new Date(grp[0].seenTime), formatDate(new Date(grp[0].seenTime)), grp]);
+ });
+
+ result.sort((a, b) => b[0].getTime() - a[0].getTime());
+
+ return result;
};
const groupRuneTxsByDate = (
transactions: GetRunesActivityForAddressEvent[],
-): Record => {
- const mappedTransactions = {};
+): [Date, string, GetRunesActivityForAddressEvent[]][] => {
+ const processedTransactions: { [x: string]: GetRunesActivityForAddressEvent[] } = {};
+
transactions.forEach((transaction) => {
- const txDate = formatDate(new Date(transaction.blockTimestamp));
- if (!mappedTransactions[txDate]) {
- mappedTransactions[txDate] = [transaction];
- } else {
- mappedTransactions[txDate].push(transaction);
+ const txDateKey = formatDateKey(new Date(transaction.blockTimestamp));
+ if (!processedTransactions[txDateKey]) {
+ processedTransactions[txDateKey] = [];
}
+ processedTransactions[txDateKey].push(transaction);
});
- return mappedTransactions;
+
+ const result: [Date, string, GetRunesActivityForAddressEvent[]][] = [];
+
+ Object.values(processedTransactions).forEach((grp) => {
+ if (grp.length === 0) {
+ return;
+ }
+
+ grp.sort((txA, txB) => {
+ // sort by block height first
+ const blockHeightDiff = txB.blockHeight - txA.blockHeight;
+ if (blockHeightDiff !== 0) {
+ return blockHeightDiff;
+ }
+
+ // if block height is the same, sort by txid for consistency
+ return txB.txid.localeCompare(txA.txid);
+ });
+
+ result.push([
+ new Date(grp[0].blockTimestamp),
+ formatDate(new Date(grp[0].blockTimestamp)),
+ grp,
+ ]);
+ });
+
+ result.sort((a, b) => b[0].getTime() - a[0].getTime());
+
+ return result;
};
-const groupedTxsByDateMap = (txs: (AddressTransactionWithTransfers | MempoolTransaction)[]) =>
- txs.reduce(
- (
- all: { [x: string]: (AddressTransactionWithTransfers | Tx)[] },
- transaction: AddressTransactionWithTransfers | Tx,
- ) => {
- const date = formatDate(
- new Date(
- isAddressTransactionWithTransfers(transaction) && transaction.tx?.burn_block_time_iso
- ? transaction.tx.burn_block_time_iso
- : Date.now(),
- ),
- );
- if (!all[date]) {
- all[date] = [transaction];
- } else {
- all[date].push(transaction);
+const groupedTxsByDateMap = (
+ transactions: (AddressTransactionWithTransfers | MempoolTransaction)[],
+): [Date, string, (AddressTransactionWithTransfers | Tx)[]][] => {
+ const getBlockTimestamp = (tx: AddressTransactionWithTransfers | Tx): Date => {
+ let dateStr: string;
+
+ if (isAddressTransactionWithTransfers(tx)) {
+ dateStr = tx.tx.burn_block_time_iso;
+ } else if ('receipt_time_iso' in tx) {
+ dateStr = tx.receipt_time_iso;
+ } else {
+ dateStr = '';
+ }
+
+ return dateStr ? new Date(dateStr) : new Date();
+ };
+
+ const getTxid = (tx: AddressTransactionWithTransfers | Tx): string => {
+ if (isAddressTransactionWithTransfers(tx)) {
+ return tx.tx.tx_id;
+ }
+ return tx.tx_id;
+ };
+
+ const processedTransactions: { [x: string]: (AddressTransactionWithTransfers | Tx)[] } = {};
+
+ transactions.forEach((transaction) => {
+ const txDate = getBlockTimestamp(transaction);
+ const txDateKey = formatDateKey(txDate);
+
+ if (!processedTransactions[txDateKey]) {
+ processedTransactions[txDateKey] = [];
+ }
+ processedTransactions[txDateKey].push(transaction);
+ });
+
+ const result: [Date, string, (AddressTransactionWithTransfers | Tx)[]][] = [];
+
+ Object.values(processedTransactions).forEach((grp) => {
+ if (grp.length === 0) {
+ return;
+ }
+
+ grp.sort((txA, txB) => {
+ // sort by block height first
+ const blockHeightDiff = getBlockTimestamp(txB).getTime() - getBlockTimestamp(txA).getTime();
+ if (blockHeightDiff !== 0) {
+ return blockHeightDiff;
}
- return all;
- },
- {},
- );
+
+ // if block height is the same, sort by txid for consistency
+ return getTxid(txB).localeCompare(getTxid(txA));
+ });
+
+ result.push([getBlockTimestamp(grp[0]), formatDate(new Date(getBlockTimestamp(grp[0]))), grp]);
+ });
+
+ result.sort((a, b) => b[0].getTime() - a[0].getTime());
+
+ return result;
+};
const filterStxTxs = (
txs: (AddressTransactionWithTransfers | MempoolTransaction)[],
@@ -189,13 +263,29 @@ const filterStxTxs = (
);
});
-export default function TransactionsHistoryList(props: TransactionsHistoryListProps) {
- const { coin, stxTxFilter, brc20Token, runeToken, runeSymbol } = props;
+type Props = {
+ coin: CurrencyTypes;
+ stxTxFilter: string | null;
+ brc20Token: string | null;
+ runeToken: string | null;
+ runeSymbol: string | null;
+ withTitle?: boolean;
+};
+
+function TransactionsHistoryList({
+ coin,
+ stxTxFilter,
+ brc20Token,
+ runeToken,
+ runeSymbol,
+ withTitle = true,
+}: Props) {
+ const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' });
const selectedAccount = useSelectedAccount();
const { network, selectedAccountType } = useWalletSelector();
const btcClient = useBtcClient();
const seedVault = useSeedVault();
- const { data, isLoading, isFetching, error } = useTransactions(
+ const { data, isLoading, error } = useTransactions(
(coin as CurrencyTypes) || 'STX',
brc20Token,
runeToken,
@@ -209,7 +299,6 @@ export default function TransactionsHistoryList(props: TransactionsHistoryListPr
},
});
- const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' });
const wallet = selectedAccount
? {
...selectedAccount,
@@ -245,20 +334,20 @@ export default function TransactionsHistoryList(props: TransactionsHistoryListPr
return groupedTxsByDateMap(filteredTxs);
}
return groupedTxsByDateMap(data as (AddressTransactionWithTransfers | MempoolTransaction)[]);
- }, [data, isLoading, isFetching]);
+ }, [data, coin, stxTxFilter]);
return (
- {t('TRANSACTION_HISTORY_TITLE')}
+ {withTitle && {t('TRANSACTION_HISTORY_TITLE')}}
{groupedTxs &&
!isLoading &&
- Object.keys(groupedTxs).map((group) => (
+ groupedTxs.map(([, group, items]) => (
{group}
- {groupedTxs[group].map((transaction) => {
+ {items.map((transaction) => {
if (wallet && isRuneTransaction(transaction)) {
return (
);
}
+
+export default TransactionsHistoryList;
diff --git a/src/app/screens/confirmBrc20Transaction/index.tsx b/src/app/screens/confirmBrc20Transaction/index.tsx
index 1439d3ba5..3fe59c373 100644
--- a/src/app/screens/confirmBrc20Transaction/index.tsx
+++ b/src/app/screens/confirmBrc20Transaction/index.tsx
@@ -2,7 +2,7 @@ import InfoContainer from '@components/infoContainer';
import BottomBar from '@components/tabBar';
import TopRow from '@components/topRow';
import TransactionDetailComponent from '@components/transactionDetailComponent';
-import useCoinRates from '@hooks/queries/useCoinRates';
+import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates';
import useBtcFeeRate from '@hooks/useBtcFeeRate';
import useDebounce from '@hooks/useDebounce';
import { useResetUserFlow } from '@hooks/useResetUserFlow';
@@ -53,7 +53,7 @@ function ConfirmBrc20Transaction() {
const { network, fiatCurrency, feeMultipliers } = useWalletSelector();
const selectedAccount = useSelectedAccount();
const { btcAddress, ordinalsAddress } = selectedAccount;
- const { btcFiatRate } = useCoinRates();
+ const { btcFiatRate } = useSupportedCoinRates();
const navigate = useNavigate();
const {
recipientAddress,
diff --git a/src/app/screens/confirmFtTransaction/index.tsx b/src/app/screens/confirmFtTransaction/index.tsx
index 9e9f010ba..0a7efc666 100644
--- a/src/app/screens/confirmFtTransaction/index.tsx
+++ b/src/app/screens/confirmFtTransaction/index.tsx
@@ -1,4 +1,4 @@
-import type { ConfirmStxTransactionState, LedgerTransactionType } from '@common/types/ledger';
+import type { ConfirmStxTransactionState } from '@common/types/ledger';
import ConfirmStxTransactionComponent from '@components/confirmStxTransactionComponent';
import TransferMemoView from '@components/confirmStxTransactionComponent/transferMemoView';
import RecipientComponent from '@components/recipientComponent';
@@ -82,15 +82,13 @@ function ConfirmFtTransaction() {
const handleOnConfirmClick = (txs: StacksTransaction[]) => {
if (isLedgerAccount(selectedAccount)) {
- const type: LedgerTransactionType = 'STX';
const state: ConfirmStxTransactionState = {
unsignedTx: Buffer.from(unsignedTx.serialize()),
- type,
recipients: [{ address: recipientAddress, amountMicrostacks: new BigNumber(amount) }],
fee: new BigNumber(unsignedTx.auth.spendingCondition.fee.toString()),
};
- navigate('/confirm-ledger-tx', { state });
+ navigate('/confirm-ledger-stx-tx', { state });
return;
}
diff --git a/src/app/screens/confirmNftTransaction/index.tsx b/src/app/screens/confirmNftTransaction/index.tsx
index 82e4f3350..3c39993f8 100644
--- a/src/app/screens/confirmNftTransaction/index.tsx
+++ b/src/app/screens/confirmNftTransaction/index.tsx
@@ -1,5 +1,5 @@
import AssetIcon from '@assets/img/transactions/Assets.svg';
-import type { ConfirmStxTransactionState, LedgerTransactionType } from '@common/types/ledger';
+import type { ConfirmStxTransactionState } from '@common/types/ledger';
import AccountHeaderComponent from '@components/accountHeader';
import ConfirmStxTransactionComponent from '@components/confirmStxTransactionComponent';
import RecipientComponent from '@components/recipientComponent';
@@ -21,12 +21,14 @@ import {
type StacksTransaction,
} from '@secretkeylabs/xverse-core';
import { deserializeTransaction } from '@stacks/transactions';
+import { removeAccountAvatarAction } from '@stores/wallet/actions/actionCreators';
import { useMutation } from '@tanstack/react-query';
import { isLedgerAccount } from '@utils/helper';
import { trackMixPanel } from '@utils/mixpanel';
import BigNumber from 'bignumber.js';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import styled from 'styled-components';
@@ -71,9 +73,12 @@ const ReviewTransactionText = styled.h1((props) => ({
}));
function ConfirmNftTransaction() {
+ const dispatch = useDispatch();
const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' });
const isGalleryOpen: boolean = document.documentElement.clientWidth > 360;
const selectedAccount = useSelectedAccount();
+ const { avatarIds, network } = useWalletSelector();
+ const selectedAvatar = avatarIds[selectedAccount.ordinalsAddress];
const [fee, setFee] = useState();
const navigate = useNavigate();
const location = useLocation();
@@ -84,7 +89,6 @@ function ConfirmNftTransaction() {
const { unsignedTx: unsignedTxHex, recipientAddress } = location.state;
const unsignedTx = useMemo(() => deserializeTransaction(unsignedTxHex), [unsignedTxHex]);
- const { network } = useWalletSelector();
const { refetch } = useStxWalletData();
const selectedNetwork = useNetworkSelector();
const {
@@ -107,11 +111,24 @@ function ConfirmNftTransaction() {
isNft: true,
},
});
+
setTimeout(() => {
refetch();
}, 1000);
+
+ if (selectedAvatar?.type === 'stacks' && selectedAvatar.nft?.token_id === nft?.token_id) {
+ dispatch(removeAccountAvatarAction({ address: selectedAccount.ordinalsAddress }));
+ }
}
- }, [stxTxBroadcastData]);
+ }, [
+ dispatch,
+ navigate,
+ refetch,
+ stxTxBroadcastData,
+ selectedAccount.ordinalsAddress,
+ nft,
+ selectedAvatar,
+ ]);
useEffect(() => {
if (txError) {
@@ -128,10 +145,8 @@ function ConfirmNftTransaction() {
const handleOnConfirmClick = (txs: StacksTransaction[]) => {
if (isLedgerAccount(selectedAccount)) {
- const type: LedgerTransactionType = 'STX';
const state: ConfirmStxTransactionState = {
unsignedTx: Buffer.from(unsignedTx.serialize()),
- type,
recipients: [
{
address: recipientAddress,
@@ -148,7 +163,7 @@ function ConfirmNftTransaction() {
),
};
- navigate('/confirm-ledger-tx', { state });
+ navigate('/confirm-ledger-stx-tx', { state });
return;
}
diff --git a/src/app/screens/confirmStxTransaction/index.tsx b/src/app/screens/confirmStxTransaction/index.tsx
index 92efa41af..b85630963 100644
--- a/src/app/screens/confirmStxTransaction/index.tsx
+++ b/src/app/screens/confirmStxTransaction/index.tsx
@@ -1,5 +1,5 @@
import IconStacks from '@assets/img/dashboard/stx_icon.svg';
-import type { ConfirmStxTransactionState, LedgerTransactionType } from '@common/types/ledger';
+import type { ConfirmStxTransactionState } from '@common/types/ledger';
import {
sendInternalErrorMessage,
sendUserRejectionMessage,
@@ -55,6 +55,13 @@ const SpendDelegatedStxWarning = styled(Callout)((props) => ({
marginBottom: props.theme.space.m,
}));
+const Subtitle = styled.p`
+ ${(props) => props.theme.typography.body_medium_m};
+ color: ${(props) => props.theme.colors.white_200};
+ margin-top: ${(props) => props.theme.space.s};
+ margin-bottom: ${(props) => props.theme.space.xs};
+`;
+
function ConfirmStxTransaction() {
const { t } = useTranslation('translation');
const [hasTabClosed, setHasTabClosed] = useState(false);
@@ -211,16 +218,14 @@ function ConfirmStxTransaction() {
const handleConfirmClick = (txs: StacksTransaction[]) => {
if (isLedgerAccount(selectedAccount)) {
- const type: LedgerTransactionType = 'STX';
const fee = new BigNumber(txs[0].auth.spendingCondition.fee.toString());
const state: ConfirmStxTransactionState = {
unsignedTx: Buffer.from(unsignedTx.serialize()),
- type,
recipients: [{ address: recipient, amountMicrostacks: amount }],
fee,
};
- navigate('/confirm-ledger-tx', { state });
+ navigate('/confirm-ledger-stx-tx', { state });
return;
}
const rawTx = buf2hex(txs[0].serialize());
@@ -350,6 +355,7 @@ function ConfirmStxTransaction() {
currencyType="STX"
title={t('CONFIRM_TRANSACTION.AMOUNT')}
/>
+ {t('CONFIRM_TRANSACTION.TRANSACTION_DETAILS')}
{memo && }
{hasTabClosed && (
diff --git a/src/app/screens/connect/authenticationRequest/index.tsx b/src/app/screens/connect/authenticationRequest/index.tsx
index 0a6bae09f..e13471afd 100644
--- a/src/app/screens/connect/authenticationRequest/index.tsx
+++ b/src/app/screens/connect/authenticationRequest/index.tsx
@@ -3,7 +3,7 @@ import stxIcon from '@assets/img/dashboard/stx_icon.svg';
import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg';
import ledgerConnectStxIcon from '@assets/img/ledger/ledger_import_connect_stx.svg';
import { MESSAGE_SOURCE } from '@common/types/message-types';
-import { delay } from '@common/utils/ledger';
+import { delay } from '@common/utils/promises';
import BottomModal from '@components/bottomModal';
import ActionButton from '@components/button';
import LedgerConnectionView from '@components/ledger/connectLedgerView';
diff --git a/src/app/screens/connect/selectAccount.tsx b/src/app/screens/connect/selectAccount.tsx
index ba98ddab3..dcdb969e3 100644
--- a/src/app/screens/connect/selectAccount.tsx
+++ b/src/app/screens/connect/selectAccount.tsx
@@ -74,7 +74,7 @@ type Props = {
};
function SelectAccount({ account, handlePressAccount }: Props) {
- const gradient = getAccountGradient(account?.stxAddress || account?.btcAddress!);
+ const gradient = getAccountGradient(account);
const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' });
const theme = useTheme();
diff --git a/src/app/screens/createInscription/index.tsx b/src/app/screens/createInscription/index.tsx
index 5e46b157b..b2c3d55fa 100644
--- a/src/app/screens/createInscription/index.tsx
+++ b/src/app/screens/createInscription/index.tsx
@@ -27,10 +27,9 @@ import ConfirmScreen from '@components/confirmScreen';
import useWalletSelector from '@hooks/useWalletSelector';
import { isLedgerAccount } from '@utils/helper';
-import InscribeSection from '@components/confirmBtcTransaction/inscribeSection';
import useBtcClient from '@hooks/apiClients/useBtcClient';
-import useCoinRates from '@hooks/queries/useCoinRates';
import useConfirmedBtcBalance from '@hooks/queries/useConfirmedBtcBalance';
+import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates';
import useSelectedAccount from '@hooks/useSelectedAccount';
import useTransactionContext from '@hooks/useTransactionContext';
import Button from '@ui-library/button';
@@ -38,6 +37,7 @@ import { StyledP } from '@ui-library/common.styled';
import Sheet from '@ui-library/sheet';
import Spinner from '@ui-library/spinner';
import { trackMixPanel } from '@utils/mixpanel';
+import InscribeSection from 'app/components/confirmBtcTransaction/sections/inscribeSection';
import CompleteScreen from './CompleteScreen';
import EditFee from './EditFee';
import ErrorModal from './ErrorModal';
@@ -108,7 +108,7 @@ function CreateInscription() {
const selectedAccount = useSelectedAccount();
const { ordinalsAddress, btcAddress } = selectedAccount;
const { network, fiatCurrency } = useWalletSelector();
- const { btcFiatRate } = useCoinRates();
+ const { btcFiatRate } = useSupportedCoinRates();
const transactionContext = useTransactionContext();
@@ -481,7 +481,6 @@ function CreateInscription() {
/>
)}
diff --git a/src/app/screens/createPassword/index.tsx b/src/app/screens/createPassword/index.tsx
index 572c78b7e..29b01c37e 100644
--- a/src/app/screens/createPassword/index.tsx
+++ b/src/app/screens/createPassword/index.tsx
@@ -101,7 +101,6 @@ function CreatePassword(): JSX.Element {
handleContinue={handleContinuePasswordCreation}
handleBack={handleNewPasswordBack}
checkPasswordStrength
- createPasswordFlow
autoFocus
/>
) : (
diff --git a/src/app/screens/etchRune/index.tsx b/src/app/screens/etchRune/index.tsx
index 9ad6169d9..61eb7ecb9 100644
--- a/src/app/screens/etchRune/index.tsx
+++ b/src/app/screens/etchRune/index.tsx
@@ -67,18 +67,7 @@ function EtchRune() {
{orderTx && orderTx.summary && !etchError && (
- input.extendedUtxo.address !== btcAddress &&
- input.extendedUtxo.address !== ordinalsAddress,
- ),
- }}
+ runeEtchDetails={etchRequest}
feeRate={+feeRate}
confirmText={t('CONFIRM')}
cancelText={t('CANCEL')}
diff --git a/src/app/screens/executeBrc20Transaction/index.tsx b/src/app/screens/executeBrc20Transaction/index.tsx
index b5882c0d6..3bd44db13 100644
--- a/src/app/screens/executeBrc20Transaction/index.tsx
+++ b/src/app/screens/executeBrc20Transaction/index.tsx
@@ -165,7 +165,7 @@ function ExecuteBrc20Transaction() {
loadingPercentage={loadingPercentageAwareOfStatus}
/>
)}
-
+
theme.typography.body_medium_m};
display: flex;
align-items: center;
+ align-self: flex-start;
column-gap: ${({ theme }) => theme.space.xs};
color: ${({ theme }) => theme.colors.white_0};
margin-top: ${({ theme }) => theme.space.s};
@@ -58,7 +59,7 @@ const LoaderContainer = styled.div((props) => ({
flex: 1,
justifyContent: 'center',
alignItems: 'center',
- marginTop: props.theme.spacing(12),
+ marginTop: props.theme.space.l,
}));
function ExploreScreen() {
diff --git a/src/app/screens/forgotPassword/index.tsx b/src/app/screens/forgotPassword/index.tsx
index 93e2c5303..860719d34 100644
--- a/src/app/screens/forgotPassword/index.tsx
+++ b/src/app/screens/forgotPassword/index.tsx
@@ -1,7 +1,7 @@
-import ActionButton from '@components/button';
-import CheckBox from '@components/checkBox';
import TopRow from '@components/topRow';
import useWalletReducer from '@hooks/useWalletReducer';
+import Button from '@ui-library/button';
+import Checkbox from '@ui-library/checkbox';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@@ -12,14 +12,14 @@ const Container = styled.div((props) => ({
flexDirection: 'column',
height: '100%',
backgroundColor: props.theme.colors.elevation0,
- padding: `0 ${props.theme.spacing(8)}px 0 ${props.theme.spacing(8)}px`,
+ padding: `0 ${props.theme.space.m}`,
}));
const Paragraph = styled.p((props) => ({
- ...props.theme.body_l,
+ ...props.theme.typography.body_l,
color: props.theme.colors.white_200,
textAlign: 'left',
- marginTop: props.theme.spacing(12),
+ marginTop: props.theme.space.l,
}));
const BottomContainer = styled.div((props) => ({
@@ -30,8 +30,8 @@ const ButtonsContainer = styled.div((props) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
- marginTop: props.theme.spacing(16),
- columnGap: props.theme.spacing(8),
+ marginTop: props.theme.space.xl,
+ columnGap: props.theme.space.xs,
}));
const StyledTopRow = styled(TopRow)({
@@ -54,7 +54,7 @@ function ForgotPassword(): JSX.Element {
const handleResetWallet = async () => {
await resetWallet();
- navigate('/');
+ onBack();
};
return (
@@ -63,19 +63,19 @@ function ForgotPassword(): JSX.Element {
{t('PARAGRAPH1')}
{t('PARAGRAPH2')}
-
-
-
+
diff --git a/src/app/screens/home/announcementModal/index.tsx b/src/app/screens/home/announcementModal/index.tsx
new file mode 100644
index 000000000..47d2fa1b2
--- /dev/null
+++ b/src/app/screens/home/announcementModal/index.tsx
@@ -0,0 +1,43 @@
+import { useSessionStorage } from '@hooks/useStorage';
+import { markAlertSeen, shouldShowAlert, type AnnouncementKey } from '@utils/alertTracker';
+import { useEffect, useRef, useState } from 'react';
+import NativeSegWit from './nativeSegWit';
+
+const getAnnouncementToShow = () => {
+ if (shouldShowAlert('native_segwit_intro')) {
+ return 'native_segwit_intro';
+ }
+ return undefined;
+};
+
+export default function AnnouncementModal() {
+ const [hasShown, setHasShown] = useSessionStorage('announceShown', false);
+ const stickyShown = useRef(hasShown);
+ const [announcementToShow, setAnnouncementToShow] = useState();
+ const [isVisible, setIsVisible] = useState(true);
+
+ useEffect(() => {
+ setHasShown(true);
+
+ if (stickyShown.current) {
+ // we've already shown the announcement modal on this load, so don't show it again
+ return;
+ }
+
+ const announcement = getAnnouncementToShow();
+ setAnnouncementToShow(announcement);
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to run this once
+ }, []);
+
+ const onClose = (announcementKey: AnnouncementKey) => () => {
+ setIsVisible(false);
+ markAlertSeen(announcementKey);
+ };
+
+ switch (announcementToShow) {
+ case 'native_segwit_intro':
+ return ;
+ default:
+ return null;
+ }
+}
diff --git a/src/app/screens/home/announcementModal/nativeSegWit.tsx b/src/app/screens/home/announcementModal/nativeSegWit.tsx
new file mode 100644
index 000000000..9cb60f664
--- /dev/null
+++ b/src/app/screens/home/announcementModal/nativeSegWit.tsx
@@ -0,0 +1,76 @@
+import BtcLogo from '@assets/img/btcFlashy.svg';
+import Button from '@ui-library/button';
+import Sheet from '@ui-library/sheet';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+import styled from 'styled-components';
+
+const LogoContainer = styled.div(() => ({
+ display: 'flex',
+ justifyContent: 'center',
+}));
+
+const LogoImg = styled.img(() => ({
+ height: '135px',
+}));
+
+const DescriptionItem = styled.div<{ $bottomSpacer?: boolean }>((props) => ({
+ ...props.theme.typography.body_m,
+ color: props.theme.colors.white_200,
+ marginBottom: props.$bottomSpacer ? props.theme.space.m : 0,
+}));
+
+const HighlightSpan = styled.span`
+ ${(props) => props.theme.typography.body_medium_m};
+ color: ${(props) => props.theme.colors.white_0};
+`;
+
+const ButtonContainer = styled.div((props) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ gap: props.theme.space.s,
+ marginTop: props.theme.space.xl,
+}));
+
+type Props = {
+ isVisible: boolean;
+ onClose: () => void;
+};
+
+export default function NativeSegWit({ isVisible, onClose }: Props) {
+ const { t } = useTranslation('translation', {
+ keyPrefix: 'DASHBOARD_SCREEN.ANNOUNCEMENTS.NATIVE_SEGWIT',
+ });
+ const navigate = useNavigate();
+
+ const onNavigate = () => {
+ navigate('/preferred-address');
+ onClose();
+ };
+
+ return (
+
+
+
+ }
+ onClose={onClose}
+ visible={isVisible}
+ >
+
+ {t('DESCRIPTION_1a')}
+ {t('DESCRIPTION_1b')}
+ {t('DESCRIPTION_1c')}
+
+ {t('DESCRIPTION_2')}
+ {t('DESCRIPTION_3')}
+
+
+
+
+
+ );
+}
diff --git a/src/app/screens/home/balanceCard/index.tsx b/src/app/screens/home/balanceCard/index.tsx
index cf240a745..302eb7d7c 100644
--- a/src/app/screens/home/balanceCard/index.tsx
+++ b/src/app/screens/home/balanceCard/index.tsx
@@ -3,18 +3,21 @@ import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc
import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery';
import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens';
import useAccountBalance from '@hooks/queries/useAccountBalance';
-import useBtcWalletData from '@hooks/queries/useBtcWalletData';
-import useCoinRates from '@hooks/queries/useCoinRates';
+import useSelectedAccountBtcBalance from '@hooks/queries/useSelectedAccountBtcBalance';
import useStxWalletData from '@hooks/queries/useStxWalletData';
+import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates';
import useSelectedAccount from '@hooks/useSelectedAccount';
import useWalletSelector from '@hooks/useWalletSelector';
import { currencySymbolMap } from '@secretkeylabs/xverse-core';
+import { setBalanceHiddenToggleAction } from '@stores/wallet/actions/actionCreators';
import Spinner from '@ui-library/spinner';
-import { LoaderSize } from '@utils/constants';
-import { calculateTotalBalance } from '@utils/helper';
+import { HIDDEN_BALANCE_LABEL, LoaderSize } from '@utils/constants';
+import { calculateTotalBalance, getAccountBalanceKey } from '@utils/helper';
+import BigNumber from 'bignumber.js';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { NumericFormat } from 'react-number-format';
+import { useDispatch } from 'react-redux';
import styled from 'styled-components';
const RowContainer = styled.div((props) => ({
@@ -61,8 +64,10 @@ const BalanceContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start',
+ width: 'fit-content',
alignItems: 'center',
gap: props.theme.spacing(5),
+ cursor: 'pointer',
}));
interface BalanceCardProps {
@@ -73,20 +78,23 @@ interface BalanceCardProps {
function BalanceCard(props: BalanceCardProps) {
const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN' });
const selectedAccount = useSelectedAccount();
- const { fiatCurrency, hideStx, accountBalances } = useWalletSelector();
- const { data: btcBalance } = useBtcWalletData();
+ const dispatch = useDispatch();
+ const { fiatCurrency, hideStx, accountBalances, balanceHidden } = useWalletSelector();
+ const { confirmedPaymentBalance: btcBalance, isLoading: btcBalanceLoading } =
+ useSelectedAccountBtcBalance();
const { data: stxData } = useStxWalletData();
- const { btcFiatRate, stxBtcRate } = useCoinRates();
+ const { btcFiatRate, stxBtcRate } = useSupportedCoinRates();
const { setAccountBalance } = useAccountBalance();
const { isLoading, isRefetching } = props;
- const oldTotalBalance = accountBalances[selectedAccount.btcAddress];
- const { visible: sip10CoinsList } = useVisibleSip10FungibleTokens();
- const { visible: brc20CoinsList } = useVisibleBrc20FungibleTokens();
- const { visible: runesCoinList } = useVisibleRuneFungibleTokens();
+ // TODO: refactor this into a hook
+ const oldTotalBalance = accountBalances[getAccountBalanceKey(selectedAccount)];
+ const { data: sip10CoinsList } = useVisibleSip10FungibleTokens();
+ const { data: brc20CoinsList } = useVisibleBrc20FungibleTokens();
+ const { data: runesCoinList } = useVisibleRuneFungibleTokens();
const balance = calculateTotalBalance({
- stxBalance: stxData?.balance.toString() ?? '0',
- btcBalance: btcBalance?.toString() ?? '0',
+ stxBalance: BigNumber(stxData?.balance ?? 0).toString(),
+ btcBalance: (btcBalance ?? 0).toString(),
sipCoinsList: sip10CoinsList,
brcCoinsList: brc20CoinsList,
runesCoinList,
@@ -96,14 +104,14 @@ function BalanceCard(props: BalanceCardProps) {
});
useEffect(() => {
- if (!balance || !selectedAccount || isLoading || isRefetching) {
+ if (!balance || !selectedAccount || isLoading || btcBalanceLoading || isRefetching) {
return;
}
if (oldTotalBalance !== balance) {
setAccountBalance(selectedAccount, balance);
}
- }, [balance, oldTotalBalance, selectedAccount, isLoading, isRefetching]);
+ }, [balance, oldTotalBalance, selectedAccount, isLoading, isRefetching, btcBalanceLoading]);
useEffect(() => {
(() => {
@@ -125,6 +133,12 @@ function BalanceCard(props: BalanceCardProps) {
})();
});
+ const onClickBalance = () => dispatch(setBalanceHiddenToggleAction({ toggle: !balanceHidden }));
+
+ const onKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') onClickBalance();
+ };
+
return (
<>
@@ -138,20 +152,29 @@ function BalanceCard(props: BalanceCardProps) {
) : (
-
- (
- {value}
- )}
- />
- {isRefetching && (
-
-
-
+
+ {balanceHidden && (
+
+ {HIDDEN_BALANCE_LABEL}
+
+ )}
+ {!balanceHidden && (
+ <>
+ (
+ {value}
+ )}
+ />
+ {isRefetching && (
+
+
+
+ )}
+ >
)}
)}
diff --git a/src/app/screens/home/banner.tsx b/src/app/screens/home/banner.tsx
index bab3a7d79..056493311 100644
--- a/src/app/screens/home/banner.tsx
+++ b/src/app/screens/home/banner.tsx
@@ -1,27 +1,25 @@
-import { XCircle } from '@phosphor-icons/react';
import { AnalyticsEvents, type NotificationBanner } from '@secretkeylabs/xverse-core';
-import { setNotificationBannersAction } from '@stores/wallet/actions/actionCreators';
-import { CrossButton } from '@ui-library/sheet';
import { trackMixPanel } from '@utils/mixpanel';
-import { useDispatch } from 'react-redux';
import styled from 'styled-components';
-import Theme from 'theme';
const Container = styled.div`
position: relative;
width: 100%;
color: ${({ theme }) => theme.colors.white_0};
- padding-top: ${({ theme }) => theme.space.xxs};
- padding-bottom: ${({ theme }) => theme.space.m};
`;
-const BannerContent = styled.div`
+const BannerContent = styled.button`
cursor: pointer;
+ width: 100%;
+ min-height: 60px;
display: flex;
align-items: center;
- column-gap: ${({ theme }) => theme.space.m};
+ column-gap: ${({ theme }) => theme.space.s};
padding-left: ${({ theme }) => theme.space.m};
padding-right: ${({ theme }) => theme.space.m};
+ color: ${({ theme }) => theme.colors.white_0};
+ background-color: transparent;
+ text-align: left;
transition: opacity 0.1s ease;
&:hover {
@@ -52,25 +50,9 @@ const BannerText = styled.div`
overflow: hidden;
`;
-const StyledCrossButton = styled(CrossButton)`
- z-index: 1;
- position: absolute;
- top: -${(props) => props.theme.space.xs};
- right: -${(props) => props.theme.space.xxs};
-`;
-
-function Banner({ id, name, url, icon, description }: NotificationBanner) {
- const dispatch = useDispatch();
-
- const dismissBanner = () => {
- dispatch(setNotificationBannersAction({ id, isDismissed: true }));
- };
-
+function Banner({ name, url, icon, description }: NotificationBanner) {
return (
-
-
-
{
trackMixPanel(
diff --git a/src/app/screens/home/bannerCarousel.tsx b/src/app/screens/home/bannerCarousel.tsx
new file mode 100644
index 000000000..47d978c99
--- /dev/null
+++ b/src/app/screens/home/bannerCarousel.tsx
@@ -0,0 +1,142 @@
+import { CaretLeft, CaretRight } from '@phosphor-icons/react';
+import type { NotificationBanner } from '@secretkeylabs/xverse-core';
+import { setNotificationBannersAction } from '@stores/wallet/actions/actionCreators';
+import CrossButton from '@ui-library/crossButton';
+import { useState } from 'react';
+import { useDispatch } from 'react-redux';
+import styled from 'styled-components';
+import 'swiper/css';
+import 'swiper/css/pagination';
+import { Navigation, Pagination } from 'swiper/modules';
+import { Swiper, SwiperSlide } from 'swiper/react';
+import Banner from './banner';
+
+const CarouselContainer = styled.div`
+ position: relative;
+ margin-top: ${({ theme }) => theme.space.xxs};
+ margin-bottom: ${({ theme }) => theme.space.xxs};
+
+ .swiper {
+ padding: 0;
+ z-index: 0;
+ }
+
+ .swiper-wrapper {
+ align-items: center;
+ }
+
+ .swiper-pagination {
+ position: absolute;
+ bottom: 0px;
+ }
+
+ .swiper-pagination-bullet {
+ width: 6px;
+ height: 6px;
+ background: ${(props) => props.theme.colors.white_600};
+ opacity: 1;
+ border-radius: 50px;
+ transition: width 0.2s ease, background 0.1s ease;
+ }
+
+ .swiper-pagination-bullet-active {
+ width: 18px;
+ background: ${(props) => props.theme.colors.white_0};
+ }
+`;
+
+const Button = styled.button`
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ background-color: transparent;
+ width: 18px;
+ height: 18px;
+ z-index: 1;
+
+ svg {
+ color: ${({ theme }) => theme.colors.white_0};
+ transition: color 0.1s ease;
+ }
+
+ &:hover {
+ svg {
+ color: ${({ theme }) => theme.colors.white_200};
+ }
+ }
+
+ &:disabled {
+ cursor: default;
+ svg {
+ color: ${({ theme }) => theme.colors.white_600};
+ }
+ }
+
+ &.swiper-button-next {
+ right: -6px;
+ }
+
+ &.swiper-button-prev {
+ left: -8px;
+ }
+`;
+
+const PaginationContainer = styled.div`
+ margin-bottom: -40px;
+ z-index: 1;
+`;
+
+const StyledCrossButton = styled(CrossButton)`
+ top: -8px;
+ right: -6px;
+ z-index: 1;
+`;
+
+type Props = {
+ items: NotificationBanner[];
+};
+
+function BannerCarousel({ items }: Props) {
+ const [activeIndex, setActiveIndex] = useState(0);
+ const dispatch = useDispatch();
+
+ const dismissBanner = () => {
+ dispatch(setNotificationBannersAction({ id: items[activeIndex].id, isDismissed: true }));
+ };
+
+ return (
+
+
+ {
+ setActiveIndex(swiper.activeIndex);
+ }}
+ allowTouchMove
+ >
+ {items.map((item) => (
+
+
+
+ ))}
+
+ {items.length > 1 && (
+ <>
+
+
+ >
+ )}
+
+
+ );
+}
+
+export default BannerCarousel;
diff --git a/src/app/screens/home/index.styled.ts b/src/app/screens/home/index.styled.ts
index 5f34e3d64..bebc02d64 100644
--- a/src/app/screens/home/index.styled.ts
+++ b/src/app/screens/home/index.styled.ts
@@ -1,4 +1,5 @@
import TokenTile from '@components/tokenTile';
+import Callout from '@ui-library/callout';
import Divider from '@ui-library/divider';
import styled from 'styled-components';
@@ -15,16 +16,13 @@ export const ColumnContainer = styled.div((props) => ({
flexDirection: 'column',
alignItems: 'space-between',
justifyContent: 'space-between',
- gap: props.theme.space.s,
- marginTop: props.theme.space.l,
+ marginTop: props.theme.space.xs,
marginBottom: props.theme.space.s,
}));
export const ReceiveContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
- marginTop: props.theme.spacing(12),
- marginBottom: props.theme.spacing(16),
gap: props.theme.space.m,
}));
@@ -64,12 +62,12 @@ export const TokenListButtonContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
- marginTop: props.theme.spacing(6),
+ marginTop: props.theme.space.s,
marginBottom: props.theme.spacing(22),
}));
export const StyledTokenTile = styled(TokenTile)`
- background-color: ${(props) => props.theme.colors.elevation1};
+ background-color: transparent;
`;
export const Icon = styled.img({
@@ -83,7 +81,7 @@ export const MergedOrdinalsIcon = styled.img({
});
export const VerifyOrViewContainer = styled.div((props) => ({
- marginTop: props.theme.spacing(16),
+ marginTop: props.theme.space.xl,
marginBottom: props.theme.spacing(20),
}));
@@ -92,9 +90,9 @@ export const VerifyButtonContainer = styled.div((props) => ({
}));
export const ModalContent = styled.div((props) => ({
- padding: props.theme.spacing(8),
+ padding: props.theme.space.m,
paddingTop: 0,
- paddingBottom: props.theme.spacing(16),
+ paddingBottom: props.theme.space.xl,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
@@ -106,14 +104,14 @@ export const ModalIcon = styled.img((props) => ({
export const ModalTitle = styled.div((props) => ({
...props.theme.typography.body_bold_l,
- marginBottom: props.theme.spacing(4),
+ marginBottom: props.theme.space.xs,
textAlign: 'center',
}));
export const ModalDescription = styled.div((props) => ({
...props.theme.typography.body_m,
color: props.theme.colors.white_200,
- marginBottom: props.theme.spacing(16),
+ marginBottom: props.theme.space.xl,
textAlign: 'center',
}));
@@ -140,7 +138,7 @@ export const StacksIcon = styled.img({
export const MergedIcon = styled.div((props) => ({
position: 'relative',
- marginBottom: props.theme.spacing(12),
+ marginBottom: props.theme.space.l,
}));
export const IconBackground = styled.div((props) => ({
@@ -157,9 +155,23 @@ export const IconBackground = styled.div((props) => ({
alignItems: 'center',
}));
-export const StyledDivider = styled(Divider)`
+export const StyledDivider = styled(Divider)<{ $noMarginBottom?: boolean }>`
flex: 1 0 auto;
width: calc(100% + ${(props) => props.theme.space.xl});
margin-left: -${(props) => props.theme.space.m};
margin-right: -${(props) => props.theme.space.m};
+ transition: margin-bottom 0.1s ease;
+ ${(props) =>
+ props.$noMarginBottom &&
+ `
+ margin-bottom: 0;
+ `}
`;
+
+export const StyledDividerSingle = styled(StyledDivider)`
+ margin-bottom: 0;
+`;
+
+export const SpacedCallout = styled(Callout)((props) => ({
+ marginTop: props.theme.space.s,
+}));
diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx
index fce35f393..60a78c5e6 100644
--- a/src/app/screens/home/index.tsx
+++ b/src/app/screens/home/index.tsx
@@ -1,32 +1,42 @@
import dashboardIcon from '@assets/img/dashboard-icon.svg';
-import BitcoinToken from '@assets/img/dashboard/bitcoin_token.svg';
import ListDashes from '@assets/img/dashboard/list_dashes.svg';
-import ordinalsIcon from '@assets/img/dashboard/ordinalBRC20.svg';
-import stacksIcon from '@assets/img/dashboard/stx_icon.svg';
import ArrowSwap from '@assets/img/icons/ArrowSwap.svg';
import AccountHeaderComponent from '@components/accountHeader';
import BottomModal from '@components/bottomModal';
-import ReceiveCardComponent from '@components/receiveCardComponent';
-import ShowBtcReceiveAlert from '@components/showBtcReceiveAlert';
-import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert';
import BottomBar from '@components/tabBar';
-import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens';
-import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery';
-import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens';
+import {
+ useGetBrc20FungibleTokens,
+ useVisibleBrc20FungibleTokens,
+} from '@hooks/queries/ordinals/useGetBrc20FungibleTokens';
+import {
+ useRuneFungibleTokensQuery,
+ useVisibleRuneFungibleTokens,
+} from '@hooks/queries/runes/useRuneFungibleTokensQuery';
+import {
+ useGetSip10FungibleTokens,
+ useVisibleSip10FungibleTokens,
+} from '@hooks/queries/stx/useGetSip10FungibleTokens';
import useAppConfig from '@hooks/queries/useAppConfig';
-import useBtcWalletData from '@hooks/queries/useBtcWalletData';
-import useCoinRates from '@hooks/queries/useCoinRates';
import useFeeMultipliers from '@hooks/queries/useFeeMultipliers';
+import useSelectedAccountBtcBalance from '@hooks/queries/useSelectedAccountBtcBalance';
import useSpamTokens from '@hooks/queries/useSpamTokens';
import useStxWalletData from '@hooks/queries/useStxWalletData';
+import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates';
+import useAvatarCleanup from '@hooks/useAvatarCleanup';
import useHasFeature from '@hooks/useHasFeature';
import useNotificationBanners from '@hooks/useNotificationBanners';
import useSelectedAccount from '@hooks/useSelectedAccount';
import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed';
import useWalletSelector from '@hooks/useWalletSelector';
import { ArrowDown, ArrowUp, Plus } from '@phosphor-icons/react';
+import { animated, useTransition } from '@react-spring/web';
import CoinSelectModal from '@screens/home/coinSelectModal';
-import { AnalyticsEvents, FeatureId, type FungibleToken } from '@secretkeylabs/xverse-core';
+import {
+ AnalyticsEvents,
+ FeatureId,
+ type FungibleToken,
+ type FungibleTokenWithStates,
+} from '@secretkeylabs/xverse-core';
import {
changeShowDataCollectionAlertAction,
setBrc20ManageTokensAction,
@@ -35,13 +45,11 @@ import {
setSpamTokenAction,
} from '@stores/wallet/actions/actionCreators';
import Button from '@ui-library/button';
-import Sheet from '@ui-library/sheet';
import SnackBar from '@ui-library/snackBar';
import type { CurrencyTypes } from '@utils/constants';
import { isInOptions, isLedgerAccount } from '@utils/helper';
import { optInMixPanel, optOutMixPanel, trackMixPanel } from '@utils/mixpanel';
import { sortFtByFiatBalance } from '@utils/tokens';
-import BigNumber from 'bignumber.js';
import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
@@ -49,88 +57,72 @@ import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { useTheme } from 'styled-components';
import SquareButton from '../../components/squareButton';
+import AnnouncementModal from './announcementModal';
import BalanceCard from './balanceCard';
-import Banner from './banner';
+import BannerCarousel from './bannerCarousel';
import {
ButtonImage,
ButtonText,
ColumnContainer,
Container,
- Icon,
- IconBackground,
- MergedIcon,
- MergedOrdinalsIcon,
ModalButtonContainer,
ModalContent,
ModalControlsContainer,
ModalDescription,
ModalIcon,
ModalTitle,
- ReceiveContainer,
RowButtonContainer,
- StacksIcon,
StyledDivider,
+ StyledDividerSingle,
StyledTokenTile,
TokenListButton,
TokenListButtonContainer,
- VerifyButtonContainer,
- VerifyOrViewContainer,
} from './index.styled';
+import ReceiveSheet from './receiveSheet';
function Home() {
const { t } = useTranslation('translation', {
keyPrefix: 'DASHBOARD_SCREEN',
});
const selectedAccount = useSelectedAccount();
- const { stxAddress, btcAddress, ordinalsAddress } = selectedAccount;
- const {
- showBtcReceiveAlert,
- showOrdinalReceiveAlert,
- showDataCollectionAlert,
- network,
- hideStx,
- spamToken,
- notificationBanners,
- } = useWalletSelector();
+ const { stxAddress, btcAddress } = selectedAccount;
+ const { showDataCollectionAlert, hideStx, spamToken, notificationBanners } = useWalletSelector();
const theme = useTheme();
const navigate = useNavigate();
const dispatch = useDispatch();
const [openReceiveModal, setOpenReceiveModal] = useState(false);
const [openSendModal, setOpenSendModal] = useState(false);
const [openBuyModal, setOpenBuyModal] = useState(false);
- const [isBtcReceiveAlertVisible, setIsBtcReceiveAlertVisible] = useState(false);
- const [isOrdinalReceiveAlertVisible, setIsOrdinalReceiveAlertVisible] = useState(false);
- const [areReceivingAddressesVisible, setAreReceivingAddressesVisible] = useState(
- !isLedgerAccount(selectedAccount),
- );
- const [choseToVerifyAddresses, setChoseToVerifyAddresses] = useState(false);
- const { isInitialLoading: loadingBtcWalletData, isRefetching: refetchingBtcWalletData } =
- useBtcWalletData();
+ const { isLoading: loadingBtcWalletData, isRefetching: refetchingBtcWalletData } =
+ useSelectedAccountBtcBalance();
const { isInitialLoading: loadingStxWalletData, isRefetching: refetchingStxWalletData } =
useStxWalletData();
- const { btcFiatRate, stxBtcRate } = useCoinRates();
- const { data: notificationBannersArr } = useNotificationBanners();
+ const { btcFiatRate, stxBtcRate } = useSupportedCoinRates();
+ const { data: notificationBannersArr, isFetching: isFetchingNotificationBannersArr } =
+ useNotificationBanners();
+
+ const { data: fullSip10CoinsList } = useGetSip10FungibleTokens();
+ const { data: fullBrc20CoinsList } = useGetBrc20FungibleTokens();
+ const { data: fullRunesCoinsList } = useRuneFungibleTokensQuery();
const {
- unfilteredData: fullSip10CoinsList,
- visible: sip10CoinsList,
+ data: sip10CoinsList,
isInitialLoading: loadingStxCoinData,
isRefetching: refetchingStxCoinData,
} = useVisibleSip10FungibleTokens();
const {
- unfilteredData: fullBrc20CoinsList,
- visible: brc20CoinsList,
+ data: brc20CoinsList,
isInitialLoading: loadingBrcCoinData,
isRefetching: refetchingBrcCoinData,
} = useVisibleBrc20FungibleTokens();
const {
- unfilteredData: fullRunesCoinsList,
- visible: runesCoinsList,
+ data: runesCoinsList,
isInitialLoading: loadingRunesData,
isRefetching: refetchingRunesData,
} = useVisibleRuneFungibleTokens();
useFeeMultipliers();
useAppConfig();
+ useAvatarCleanup();
useTrackMixPanelPageViewed();
const { removeFromSpamTokens } = useSpamTokens();
@@ -159,11 +151,23 @@ function Home() {
isEnabled: true,
};
- if (fullRunesCoinsList?.find((ft) => ft.principal === spamToken.principal)) {
+ if (
+ fullRunesCoinsList?.find(
+ (ft: FungibleTokenWithStates) => ft.principal === spamToken.principal,
+ )
+ ) {
dispatch(setRunesManageTokensAction(payload));
- } else if (fullSip10CoinsList?.find((ft) => ft.principal === spamToken.principal)) {
+ } else if (
+ fullSip10CoinsList?.find(
+ (ft: FungibleTokenWithStates) => ft.principal === spamToken.principal,
+ )
+ ) {
dispatch(setSip10ManageTokensAction(payload));
- } else if (fullBrc20CoinsList?.find((ft) => ft.principal === spamToken.principal)) {
+ } else if (
+ fullBrc20CoinsList?.find(
+ (ft: FungibleTokenWithStates) => ft.principal === spamToken.principal,
+ )
+ ) {
dispatch(setBrc20ManageTokensAction(payload));
}
@@ -177,29 +181,23 @@ function Home() {
}
}, [spamToken]);
- const combinedFtList = sip10CoinsList
- .concat(brc20CoinsList)
- .concat(runesCoinsList)
- .sort((a, b) => sortFtByFiatBalance(a, b, stxBtcRate, btcFiatRate));
+ const combinedFtList = (sip10CoinsList ?? [])
+ .concat(brc20CoinsList ?? [])
+ .concat(runesCoinsList ?? [])
+ .sort((a: FungibleTokenWithStates, b: FungibleTokenWithStates) =>
+ sortFtByFiatBalance(a, b, stxBtcRate, btcFiatRate),
+ );
- const showNotificationBanner =
- notificationBannersArr?.length &&
- notificationBannersArr.length > 0 &&
- !notificationBanners[notificationBannersArr[0].id];
+ const filteredNotificationBannersArr = notificationBannersArr
+ ? notificationBannersArr.filter((banner) => !notificationBanners[banner.id])
+ : [];
+ const showBannerCarousel =
+ !isFetchingNotificationBannersArr && !!filteredNotificationBannersArr?.length;
const onReceiveModalOpen = () => {
setOpenReceiveModal(true);
};
- const onReceiveModalClose = () => {
- setOpenReceiveModal(false);
-
- if (isLedgerAccount(selectedAccount)) {
- setAreReceivingAddressesVisible(false);
- setChoseToVerifyAddresses(false);
- }
- };
-
const onSendModalOpen = () => {
setOpenSendModal(true);
};
@@ -240,14 +238,6 @@ function Home() {
navigate('/send-btc');
};
- const onBTCReceiveSelect = () => {
- navigate('/receive/BTC');
- };
-
- const onSTXReceiveSelect = () => {
- navigate('/receive/STX');
- };
-
const onSendFtSelect = async (fungibleToken: FungibleToken) => {
let route = '';
switch (fungibleToken?.protocol) {
@@ -281,22 +271,6 @@ function Home() {
navigate('/buy/BTC');
};
- const onOrdinalReceiveAlertOpen = () => {
- if (showOrdinalReceiveAlert) setIsOrdinalReceiveAlertVisible(true);
- };
-
- const onOrdinalReceiveAlertClose = () => {
- setIsOrdinalReceiveAlertVisible(false);
- };
-
- const onReceiveAlertClose = () => {
- setIsBtcReceiveAlertVisible(false);
- };
-
- const onReceiveAlertOpen = () => {
- if (showBtcReceiveAlert) setIsBtcReceiveAlertVisible(true);
- };
-
const handleTokenPressed = (currency: CurrencyTypes, fungibleToken?: FungibleToken) => {
if (fungibleToken) {
navigate(
@@ -307,99 +281,11 @@ function Home() {
}
};
- const onOrdinalsReceivePress = () => {
- navigate('/receive/ORD');
- };
-
const onSwapPressed = () => {
trackMixPanel(AnalyticsEvents.InitiateSwapFlow, {});
navigate('/swap');
};
- const receiveContent = (
-
-
-
-
-
-
-
-
-
- {stxAddress && (
-
-
-
-
-
-
-
-
- )}
-
- {isLedgerAccount(selectedAccount) && !stxAddress && (
- }
- title={t('ADD_STACKS_ADDRESS')}
- onClick={async () => {
- if (!isInOptions()) {
- await chrome.tabs.create({
- url: chrome.runtime.getURL(`options.html#/add-stx-address-ledger`),
- });
- } else {
- navigate('/add-stx-address-ledger');
- }
- }}
- />
- )}
-
- );
-
- const verifyOrViewAddresses = (
-
-
-
-
- );
-
const handleDataCollectionDeny = () => {
optOutMixPanel();
dispatch(changeShowDataCollectionAlertAction(false));
@@ -413,15 +299,24 @@ function Home() {
const isCrossChainSwapsEnabled = useHasFeature(FeatureId.CROSS_CHAIN_SWAPS);
const showSwaps = isCrossChainSwapsEnabled;
+ const transitions = useTransition(showBannerCarousel, {
+ from: { maxHeight: '1000px', opacity: 0.5 },
+ enter: { maxHeight: '1000px', opacity: 1 },
+ leave: { maxHeight: '0px', opacity: 0 },
+ config: (item, index, phase) =>
+ phase === 'leave'
+ ? {
+ duration: 300,
+ easing: (progress) => 1 - (1 - progress) ** 4,
+ }
+ : {
+ duration: 200,
+ },
+ });
+
return (
<>
- {isBtcReceiveAlertVisible && (
-
- )}
- {isOrdinalReceiveAlertVisible && (
-
- )}
- {showNotificationBanner && (
- <>
-
-
-
-
- >
+ {transitions((style, item) =>
+ item ? (
+
+
+
+
+
+
+ ) : (
+
+
+
+ ),
)}
@@ -478,7 +383,7 @@ function Home() {
onPress={handleTokenPressed}
/>
)}
- {combinedFtList.map((coin) => {
+ {combinedFtList.map((coin: FungibleTokenWithStates) => {
const isLoading = loadingStxCoinData || loadingBrcCoinData || loadingRunesData;
return (
-
- {areReceivingAddressesVisible ? receiveContent : verifyOrViewAddresses}
-
+
+ setOpenReceiveModal(false)} />
+
new BigNumber(ft.balance).gt(0))}
+ coins={combinedFtList}
title={t('SEND')}
loadingWalletData={loadingStxWalletData || loadingBtcWalletData}
/>
@@ -523,6 +428,7 @@ function Home() {
title={t('BUY')}
loadingWalletData={loadingStxWalletData || loadingBtcWalletData}
/>
+
diff --git a/src/app/screens/home/receiveSheet.tsx b/src/app/screens/home/receiveSheet.tsx
new file mode 100644
index 000000000..2d7630f5a
--- /dev/null
+++ b/src/app/screens/home/receiveSheet.tsx
@@ -0,0 +1,204 @@
+import BitcoinToken from '@assets/img/dashboard/bitcoin_token.svg';
+import ordinalsIcon from '@assets/img/dashboard/ordinalBRC20.svg';
+import stacksIcon from '@assets/img/dashboard/stx_icon.svg';
+import ReceiveCardComponent from '@components/receiveCardComponent';
+import ShowBtcReceiveAlert from '@components/showBtcReceiveAlert';
+import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert';
+import useSelectedAccount from '@hooks/useSelectedAccount';
+import useWalletSelector from '@hooks/useWalletSelector';
+import { Plus } from '@phosphor-icons/react';
+import Button from '@ui-library/button';
+import Sheet from '@ui-library/sheet';
+import { markAlertSeen, shouldShowAlert } from '@utils/alertTracker';
+import { isInOptions, isLedgerAccount } from '@utils/helper';
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+import {
+ Icon,
+ IconBackground,
+ MergedIcon,
+ MergedOrdinalsIcon,
+ ReceiveContainer,
+ SpacedCallout,
+ StacksIcon,
+ VerifyButtonContainer,
+ VerifyOrViewContainer,
+} from './index.styled';
+
+type Props = {
+ visible: boolean;
+ onClose: () => void;
+};
+
+function ReceiveSheet({ visible, onClose }: Props) {
+ const { t } = useTranslation('translation', {
+ keyPrefix: 'DASHBOARD_SCREEN',
+ });
+
+ const selectedAccount = useSelectedAccount();
+ const { stxAddress, btcAddress, ordinalsAddress } = selectedAccount;
+ const { showBtcReceiveAlert, showOrdinalReceiveAlert, btcPaymentAddressType } =
+ useWalletSelector();
+ const navigate = useNavigate();
+
+ const [isBtcReceiveAlertVisible, setIsBtcReceiveAlertVisible] = useState(false);
+ const [isOrdinalReceiveAlertVisible, setIsOrdinalReceiveAlertVisible] = useState(false);
+ const [areReceivingAddressesVisible, setAreReceivingAddressesVisible] = useState(
+ !isLedgerAccount(selectedAccount),
+ );
+ const [choseToVerifyAddresses, setChoseToVerifyAddresses] = useState(false);
+ const [showNativeSegWitCallout, setShowNativeSegWitCallout] = useState(
+ shouldShowAlert('co:panel:address_changed_to_native'),
+ );
+
+ const onReceiveModalClose = () => {
+ onClose();
+
+ if (isLedgerAccount(selectedAccount)) {
+ setAreReceivingAddressesVisible(false);
+ setChoseToVerifyAddresses(false);
+ }
+ };
+
+ const onBTCReceiveSelect = () => {
+ navigate('/receive/BTC');
+ };
+
+ const onSTXReceiveSelect = () => {
+ navigate('/receive/STX');
+ };
+
+ const onOrdinalReceiveAlertOpen = () => {
+ if (showOrdinalReceiveAlert) setIsOrdinalReceiveAlertVisible(true);
+ };
+
+ const onOrdinalReceiveAlertClose = () => {
+ setIsOrdinalReceiveAlertVisible(false);
+ };
+
+ const onReceiveAlertClose = () => {
+ setIsBtcReceiveAlertVisible(false);
+ };
+
+ const onReceiveAlertOpen = () => {
+ if (showBtcReceiveAlert) setIsBtcReceiveAlertVisible(true);
+ };
+
+ const onOrdinalsReceivePress = () => {
+ navigate('/receive/ORD');
+ };
+
+ const onNativeSegWitPanelClose = () => {
+ setShowNativeSegWitCallout(false);
+ markAlertSeen('co:panel:address_changed_to_native');
+ };
+
+ const receiveContent = (
+
+ }
+ >
+ {showNativeSegWitCallout && btcPaymentAddressType === 'native' && (
+
+ )}
+
+
+ }
+ />
+
+ {stxAddress && (
+
+
+
+
+
+
+ }
+ />
+ )}
+
+ {isLedgerAccount(selectedAccount) && !stxAddress && (
+ }
+ title={t('ADD_STACKS_ADDRESS')}
+ onClick={async () => {
+ if (!isInOptions()) {
+ await chrome.tabs.create({
+ url: chrome.runtime.getURL(`options.html#/add-stx-address-ledger`),
+ });
+ } else {
+ navigate('/add-stx-address-ledger');
+ }
+ }}
+ />
+ )}
+
+ );
+
+ const verifyOrViewAddresses = (
+
+
+
+
+ );
+
+ return (
+ <>
+ {isBtcReceiveAlertVisible && (
+
+ )}
+ {isOrdinalReceiveAlertVisible && (
+
+ )}
+
+ {areReceivingAddressesVisible ? receiveContent : verifyOrViewAddresses}
+
+ >
+ );
+}
+
+export default ReceiveSheet;
diff --git a/src/app/screens/landing/index.tsx b/src/app/screens/landing/index.tsx
index 41e317546..5eb6936cb 100644
--- a/src/app/screens/landing/index.tsx
+++ b/src/app/screens/landing/index.tsx
@@ -131,13 +131,13 @@ const InitialTransitionLandingSectionContainer = styled(LandingSectionContainer)
animation: ${() => slideY} 0.2s ease-out;
`;
-const TransitionLeftLandingSectionContainer = styled(LandingSectionContainer)`
- animation: ${slideLeftAnimation} 0.3s cubic-bezier(0, 0, 0.58, 1) forwards;
-`;
-
-const TransitionRightLandingSectionContainer = styled(LandingSectionContainer)`
- animation: ${() => slideRightAnimation} 0.3s cubic-bezier(0, 0, 0.58, 1) forwards;
-`;
+const TransitionLandingSectionContainer = styled(LandingSectionContainer)<{
+ $direction: 'left' | 'right';
+}>((props) => ({
+ animation: `${
+ props.$direction === 'left' ? slideLeftAnimation : slideRightAnimation
+ } 0.3s cubic-bezier(0, 0, 0.58, 1) forwards`,
+}));
const Logo = styled.img`
width: 135px;
@@ -155,13 +155,13 @@ const OnboardingContainer = styled.div((props) => ({
marginBottom: props.theme.space.l,
}));
-const TransitionLeftOnboardingContainer = styled(OnboardingContainer)`
- animation: ${() => slideLeftAnimation} 0.3s cubic-bezier(0, 0, 0.58, 1) forwards;
-`;
-
-const TransitionRightOnboardingContainer = styled(OnboardingContainer)`
- animation: ${() => slideRightAnimation} 0.3s cubic-bezier(0, 0, 0.58, 1) forwards;
-`;
+const TransitionOnboardingContainer = styled(OnboardingContainer)<{
+ $direction: 'left' | 'right';
+}>((props) => ({
+ animation: `${
+ props.$direction === 'left' ? slideLeftAnimation : slideRightAnimation
+ } 0.3s cubic-bezier(0, 0, 0.58, 1) forwards`,
+}));
const OnBoardingImage = styled.img(() => ({
marginTop: -26,
@@ -201,7 +201,9 @@ const RestoreButton = styled(Button)((props) => ({
function Landing() {
const { t } = useTranslation('translation', { keyPrefix: 'LANDING_SCREEN' });
const [currentStepIndex, setCurrentStepIndex] = useState(0);
- const [animationComplete, setAnimationComplete] = useState(false);
+ const [animationComplete, setAnimationComplete] = useState(
+ process.env.SKIP_ANIMATION_WALLET_STARTUP === 'true',
+ );
const [slideTransitions, setSlideTransitions] = useState(false);
const [transitionDirection, setTransitionDirection] = useState<'left' | 'right'>('right');
const navigate = useNavigate();
@@ -305,24 +307,19 @@ function Landing() {
if (slideTransitions) {
switch (currentStepIndex) {
case 0:
- return transitionDirection === 'left' ? (
-
- {renderLandingSection()}
-
- ) : (
-
+ return (
+
{renderLandingSection()}
-
+
);
default:
- return transitionDirection === 'left' ? (
-
- {renderOnboardingContent(currentStepIndex)}
-
- ) : (
-
+ return (
+
{renderOnboardingContent(currentStepIndex)}
-
+
);
}
} else {
diff --git a/src/app/screens/ledger/addStxAddress/index.tsx b/src/app/screens/ledger/addStxAddress/index.tsx
index 04f6b67e8..4c2b3ee2c 100644
--- a/src/app/screens/ledger/addStxAddress/index.tsx
+++ b/src/app/screens/ledger/addStxAddress/index.tsx
@@ -1,7 +1,7 @@
import stxIcon from '@assets/img/dashboard/stx_icon.svg';
import checkCircleIcon from '@assets/img/ledger/check_circle.svg';
import ledgerConnectStxIcon from '@assets/img/ledger/ledger_import_connect_stx.svg';
-import { delay } from '@common/utils/ledger';
+import { delay } from '@common/utils/promises';
import ActionButton from '@components/button';
import LedgerConnectionView from '@components/ledger/connectLedgerView';
import LedgerFailView from '@components/ledger/failLedgerView';
@@ -56,7 +56,7 @@ function AddStxAddress(): JSX.Element {
const [isAddressRejected, setIsAddressRejected] = useState(false);
const [stacksCredentials, setStacksCredentials] = useState(undefined);
const selectedAccount = useSelectedAccount();
- const { network } = useWalletSelector();
+ const { network, ledgerAccountsList } = useWalletSelector();
const { updateLedgerAccounts } = useWalletReducer();
const { search } = useLocation();
const params = new URLSearchParams(search);
@@ -74,8 +74,14 @@ function AddStxAddress(): JSX.Element {
return;
}
+ const existingAccount = ledgerAccountsList.find((account) => account.id === selectedAccount.id);
+
+ if (!existingAccount) {
+ return;
+ }
+
const ledgerAccount: Account = {
- ...selectedAccount,
+ ...existingAccount,
stxAddress: stacksCreds?.address || '',
stxPublicKey: stacksCreds?.publicKey || '',
};
diff --git a/src/app/screens/ledger/confirmLedgerTransaction/index.styled.ts b/src/app/screens/ledger/confirmLedgerStxTransaction/index.styled.ts
similarity index 77%
rename from src/app/screens/ledger/confirmLedgerTransaction/index.styled.ts
rename to src/app/screens/ledger/confirmLedgerStxTransaction/index.styled.ts
index c2be25a13..ac60de4bd 100644
--- a/src/app/screens/ledger/confirmLedgerTransaction/index.styled.ts
+++ b/src/app/screens/ledger/confirmLedgerStxTransaction/index.styled.ts
@@ -61,22 +61,6 @@ export const ConnectLedgerTitle = styled.h1<{ textAlign?: 'left' | 'center' }>((
marginBottom: props.theme.spacing(6),
}));
-export const ConnectLedgerTextAdvanced = styled.p<{
- isCompleted?: boolean;
-}>((props) => ({
- ...props.theme.body_m,
- display: 'flex',
- alignItems: 'flex-start',
- color: props.isCompleted ? props.theme.colors.white_400 : props.theme.colors.white_200,
- textAlign: 'center',
- marginBottom: props.theme.spacing(16),
-}));
-
-export const InfoContainerWrapper = styled.div((props) => ({
- textAlign: 'left',
- marginTop: props.theme.spacing(8),
-}));
-
export const TxDetails = styled.div((props) => ({
marginTop: props.theme.spacing(36),
width: '100%',
@@ -98,9 +82,3 @@ export const RecipientsWrapper = styled.div({
display: 'flex',
flexDirection: 'column',
});
-
-export const ConfirmTxIconBig = styled.img((props) => ({
- width: 32,
- height: 32,
- marginBottom: props.theme.spacing(8),
-}));
diff --git a/src/app/screens/ledger/confirmLedgerStxTransaction/index.tsx b/src/app/screens/ledger/confirmLedgerStxTransaction/index.tsx
new file mode 100644
index 000000000..6fdcb74cd
--- /dev/null
+++ b/src/app/screens/ledger/confirmLedgerStxTransaction/index.tsx
@@ -0,0 +1,314 @@
+import checkCircleIcon from '@assets/img/ledger/check_circle.svg';
+import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg';
+import ledgerConnectStxIcon from '@assets/img/ledger/ledger_import_connect_stx.svg';
+import { delay } from '@common/utils/promises';
+import ActionButton from '@components/button';
+import LedgerConnectionView from '@components/ledger/connectLedgerView';
+import FullScreenHeader from '@components/ledger/fullScreenHeader';
+import useNetworkSelector from '@hooks/useNetwork';
+import useWalletSelector from '@hooks/useWalletSelector';
+import Transport from '@ledgerhq/hw-transport-webusb';
+import { useTransition } from '@react-spring/web';
+import {
+ broadcastSignedTransaction,
+ microstacksToStx,
+ signLedgerStxTransaction,
+ type StacksRecipient,
+} from '@secretkeylabs/xverse-core';
+import { DEFAULT_TRANSITION_OPTIONS } from '@utils/constants';
+import { getStxTxStatusUrl, getTruncatedAddress } from '@utils/helper';
+import BigNumber from 'bignumber.js';
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useLocation } from 'react-router-dom';
+
+import useSelectedAccount from '@hooks/useSelectedAccount';
+import {
+ Container,
+ OnBoardingContentContainer,
+ RecipientsWrapper,
+ SuccessActionsContainer,
+ TxConfirmedContainer,
+ TxConfirmedDescription,
+ TxConfirmedTitle,
+ TxDetails,
+ TxDetailsRow,
+ TxDetailsTitle,
+} from './index.styled';
+
+enum Steps {
+ ConnectLedger = 0,
+ ConfirmTransaction = 1,
+ TransactionConfirmed = 2,
+}
+
+function ConfirmLedgerStxTransaction(): JSX.Element {
+ const [currentStep, setCurrentStep] = useState(Steps.ConnectLedger);
+ const [txId, setTxId] = useState(undefined);
+
+ const [isButtonDisabled, setIsButtonDisabled] = useState(false);
+ const [isConnectSuccess, setIsConnectSuccess] = useState(false);
+ const [isConnectFailed, setIsConnectFailed] = useState(false);
+ const [isWrongDevice, setIsWrongDevice] = useState(false);
+ const [isFinalTxApproved, setIsFinalTxApproved] = useState(false);
+ const [isTxRejected, setIsTxRejected] = useState(false);
+
+ const { t } = useTranslation('translation', { keyPrefix: 'LEDGER_CONFIRM_TRANSACTION_SCREEN' });
+ const location = useLocation();
+ const selectedNetwork = useNetworkSelector();
+
+ const selectedAccount = useSelectedAccount();
+ const { network } = useWalletSelector();
+
+ const {
+ recipients,
+ unsignedTx,
+ fee,
+ tabId,
+ tabMessageId: messageId,
+ }: {
+ amount: BigNumber;
+ recipients: StacksRecipient[];
+ unsignedTx: Buffer;
+ fee?: BigNumber;
+ messageId?: string;
+ tabId?: number;
+ tabMessageId?: string;
+ } = location.state;
+
+ const transition = useTransition(currentStep, DEFAULT_TRANSITION_OPTIONS);
+
+ const signAndBroadcastStxTx = async (transport: Transport, addressIndex: number) => {
+ try {
+ const result = await signLedgerStxTransaction({
+ transport,
+ transactionBuffer: unsignedTx,
+ addressIndex,
+ });
+ setIsFinalTxApproved(true);
+ await delay(1500);
+ const transactionHash = await broadcastSignedTransaction(result, selectedNetwork);
+ setTxId(transactionHash);
+ setCurrentStep(Steps.TransactionConfirmed);
+ } catch (err) {
+ console.error(err);
+ setIsTxRejected(true);
+ setIsButtonDisabled(false);
+ } finally {
+ transport.close();
+ }
+ };
+
+ const handleConnectAndConfirm = async () => {
+ if (!selectedAccount) {
+ console.error('No account selected');
+ return;
+ }
+
+ try {
+ setIsButtonDisabled(true);
+
+ const transport = await Transport.create();
+
+ if (!transport) {
+ setIsConnectSuccess(false);
+ setIsConnectFailed(true);
+ setIsButtonDisabled(false);
+ return;
+ }
+
+ const addressIndex = selectedAccount.deviceAccountIndex;
+
+ if (addressIndex === undefined) {
+ setIsConnectSuccess(false);
+ setIsConnectFailed(true);
+ setIsWrongDevice(true);
+ setIsButtonDisabled(false);
+ return;
+ }
+
+ setIsConnectSuccess(true);
+ await delay(1500);
+
+ if (currentStep !== Steps.ConfirmTransaction) {
+ setCurrentStep(Steps.ConfirmTransaction);
+ }
+
+ await signAndBroadcastStxTx(transport as Transport, addressIndex);
+ await transport.close();
+ } catch (err) {
+ setIsConnectSuccess(false);
+ setIsConnectFailed(true);
+ } finally {
+ setIsButtonDisabled(false);
+ }
+ };
+
+ const goToConfirmationStep = () => {
+ setCurrentStep(Steps.ConfirmTransaction);
+
+ handleConnectAndConfirm();
+ };
+
+ const handleRetry = async () => {
+ setIsTxRejected(false);
+ setIsConnectSuccess(false);
+ setIsConnectFailed(false);
+ setIsWrongDevice(false);
+ setCurrentStep(Steps.ConnectLedger);
+ };
+
+ const handleClose = () => {
+ if (typeof window !== 'undefined') {
+ window.close();
+ }
+ };
+
+ const handleSeeTransaction = () => {
+ if (!txId) {
+ console.error('No txId found');
+ return;
+ }
+
+ window.open(getStxTxStatusUrl(txId, network), '_blank', 'noopener,noreferrer');
+ };
+
+ const renderTxDetails = () => (
+
+
+ Recipient{recipients.length > 1 ? 's' : ''}
+
+ {recipients.map((recipient) => (
+ {getTruncatedAddress(recipient.address)}
+ ))}
+
+
+
+ {t('AMOUNT')}
+
+ {microstacksToStx((recipients[0] as StacksRecipient).amountMicrostacks).toString()} STX
+
+
+ {fee && (
+
+ {t('FEES')}
+ {microstacksToStx(fee).toString()} STX
+
+ )}
+
+ );
+
+ const renderLedgerConfirmationView = () => {
+ switch (currentStep) {
+ case Steps.ConnectLedger:
+ return (
+
+
+
+ );
+ case Steps.ConfirmTransaction:
+ return (
+
+
+ {renderTxDetails()}
+
+ );
+ case Steps.TransactionConfirmed:
+ return (
+
+
+ {t('SUCCESS.TITLE')}
+ {t('SUCCESS.SUBTITLE')}
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ const renderLedgerConfirmationControls = () => {
+ switch (currentStep) {
+ case Steps.ConfirmTransaction:
+ return (
+
+
+
+
+ );
+ case Steps.TransactionConfirmed:
+ return (
+
+
+
+
+ );
+ default:
+ return (
+
+
+
+
+ );
+ }
+ };
+
+ return (
+
+
+ {transition((style) => (
+ <>
+
+ {renderLedgerConfirmationView()}
+
+ {renderLedgerConfirmationControls()}
+ >
+ ))}
+
+ );
+}
+
+export default ConfirmLedgerStxTransaction;
diff --git a/src/app/screens/ledger/confirmLedgerTransaction/index.tsx b/src/app/screens/ledger/confirmLedgerTransaction/index.tsx
deleted file mode 100644
index 5b608790c..000000000
--- a/src/app/screens/ledger/confirmLedgerTransaction/index.tsx
+++ /dev/null
@@ -1,575 +0,0 @@
-import InfoIcon from '@assets/img/info.svg';
-import ledgerConfirmBtcIcon from '@assets/img/ledger/btc_icon.svg';
-import checkCircleIcon from '@assets/img/ledger/check_circle.svg';
-import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg';
-import ledgerConnectStxIcon from '@assets/img/ledger/ledger_import_connect_stx.svg';
-import ledgerConfirmOrdinalsIcon from '@assets/img/ledger/ordinals_icon_big.svg';
-import { type LedgerTransactionType } from '@common/types/ledger';
-import { delay } from '@common/utils/ledger';
-import ActionButton from '@components/button';
-import InfoContainer from '@components/infoContainer';
-import LedgerConnectionView, {
- ConnectLedgerContainer,
- ConnectLedgerText,
-} from '@components/ledger/connectLedgerView';
-import LedgerFailView from '@components/ledger/failLedgerView';
-import FullScreenHeader from '@components/ledger/fullScreenHeader';
-import Stepper from '@components/stepper';
-import useBtcClient from '@hooks/apiClients/useBtcClient';
-import useNetworkSelector from '@hooks/useNetwork';
-import useWalletSelector from '@hooks/useWalletSelector';
-import Transport from '@ledgerhq/hw-transport-webusb';
-import { useTransition } from '@react-spring/web';
-import {
- broadcastSignedTransaction,
- microstacksToStx,
- satsToBtc,
- signLedgerMixedBtcTransaction,
- signLedgerNativeSegwitBtcTransaction,
- signLedgerStxTransaction,
- type Recipient,
- type StacksRecipient,
- type UTXO,
-} from '@secretkeylabs/xverse-core';
-import { DEFAULT_TRANSITION_OPTIONS } from '@utils/constants';
-import { getBtcTxStatusUrl, getStxTxStatusUrl, getTruncatedAddress } from '@utils/helper';
-import BigNumber from 'bignumber.js';
-import { useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useLocation } from 'react-router-dom';
-
-import { makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers';
-import useSelectedAccount from '@hooks/useSelectedAccount';
-import {
- ConfirmTxIconBig,
- ConnectLedgerTextAdvanced,
- ConnectLedgerTitle,
- Container,
- InfoContainerWrapper,
- InfoImage,
- OnBoardingContentContainer,
- RecipientsWrapper,
- SuccessActionsContainer,
- TxConfirmedContainer,
- TxConfirmedDescription,
- TxConfirmedTitle,
- TxDetails,
- TxDetailsRow,
- TxDetailsTitle,
-} from './index.styled';
-
-enum Steps {
- ConnectLedger = 0,
- ExternalInputs = 0.5,
- ConfirmTransaction = 1,
- ConfirmFees = 1.5,
- TransactionConfirmed = 2,
-}
-
-function ConfirmLedgerTransaction(): JSX.Element {
- const [currentStep, setCurrentStep] = useState(Steps.ConnectLedger);
- const [txId, setTxId] = useState(undefined);
-
- const [isButtonDisabled, setIsButtonDisabled] = useState(false);
- const [isConnectSuccess, setIsConnectSuccess] = useState(false);
- const [isConnectFailed, setIsConnectFailed] = useState(false);
- const [isWrongDevice, setIsWrongDevice] = useState(false);
- const [isTxApproved, setIsTxApproved] = useState(false);
- const [isFinalTxApproved, setIsFinalTxApproved] = useState(false);
- const [isTxRejected, setIsTxRejected] = useState(false);
-
- const { t } = useTranslation('translation', { keyPrefix: 'LEDGER_CONFIRM_TRANSACTION_SCREEN' });
- const location = useLocation();
- const selectedNetwork = useNetworkSelector();
-
- const selectedAccount = useSelectedAccount();
- const { network } = useWalletSelector();
-
- const btcClient = useBtcClient();
-
- const {
- recipients,
- type,
- unsignedTx,
- ordinalUtxo,
- feeRateInput,
- fee,
- tabId,
- tabMessageId: messageId,
- }: {
- amount: BigNumber;
- recipients: Recipient[] | StacksRecipient[];
- type: LedgerTransactionType;
- unsignedTx: Buffer;
- ordinalUtxo?: UTXO;
- feeRateInput?: string;
- fee?: BigNumber;
- messageId?: string;
- tabId?: number;
- tabMessageId?: string;
- } = location.state;
-
- const transition = useTransition(currentStep, DEFAULT_TRANSITION_OPTIONS);
-
- const signAndBroadcastOrdinalsTx = async (transport: Transport, addressIndex: number) => {
- try {
- const result = await signLedgerMixedBtcTransaction({
- transport,
- esploraProvider: btcClient,
- network: network.type,
- addressIndex,
- recipients: recipients as Recipient[],
- feeRate: feeRateInput?.toString(),
- ordinalUtxo,
- });
- const { value: psbtCreatedValue } = await result.next();
-
- const { value: taprootSignedValue } = await result.next();
- setIsTxApproved(true);
- setCurrentStep(Steps.ConfirmFees);
-
- const { value: txHex } = await result.next();
- setIsFinalTxApproved(true);
- await delay(1500);
- const transactionId = await btcClient.sendRawTransaction(txHex || taprootSignedValue);
- setTxId(transactionId.tx.hash);
- setCurrentStep(Steps.TransactionConfirmed);
- if (tabId) {
- const response = makeRpcSuccessResponse(messageId, { txid: transactionId.tx.hash });
- sendRpcResponse(tabId, response);
- }
- } catch (err) {
- console.error(err);
- setIsTxRejected(true);
- setIsButtonDisabled(false);
- } finally {
- transport.close();
- }
- };
-
- const signAndBroadcastBtcTx = async (transport: Transport, addressIndex: number) => {
- try {
- const result = await signLedgerNativeSegwitBtcTransaction({
- transport,
- esploraProvider: btcClient,
- network: network.type,
- addressIndex,
- recipients: recipients as Recipient[],
- feeRate: feeRateInput?.toString(),
- });
- setIsFinalTxApproved(true);
- await delay(1500);
- const transactionId = await btcClient.sendRawTransaction(result);
- setTxId(transactionId.tx.hash);
- setCurrentStep(Steps.TransactionConfirmed);
- if (tabId) {
- const response = makeRpcSuccessResponse(messageId, { txid: transactionId.tx.hash });
- sendRpcResponse(tabId, response);
- }
- } catch (err) {
- console.error(err);
- setIsTxRejected(true);
- setIsButtonDisabled(false);
- } finally {
- transport.close();
- }
- };
-
- const signAndBroadcastStxTx = async (transport: Transport, addressIndex: number) => {
- try {
- const result = await signLedgerStxTransaction({
- transport,
- transactionBuffer: unsignedTx,
- addressIndex,
- });
- setIsFinalTxApproved(true);
- await delay(1500);
- const transactionHash = await broadcastSignedTransaction(result, selectedNetwork);
- setTxId(transactionHash);
- setCurrentStep(Steps.TransactionConfirmed);
- } catch (err) {
- console.error(err);
- setIsTxRejected(true);
- setIsButtonDisabled(false);
- } finally {
- transport.close();
- }
- };
-
- const handleConnectAndConfirm = async () => {
- if (!selectedAccount) {
- console.error('No account selected');
- return;
- }
-
- try {
- setIsButtonDisabled(true);
-
- const transport = await Transport.create();
-
- if (!transport) {
- setIsConnectSuccess(false);
- setIsConnectFailed(true);
- setIsButtonDisabled(false);
- return;
- }
-
- const addressIndex = selectedAccount.deviceAccountIndex;
-
- if (addressIndex === undefined) {
- setIsConnectSuccess(false);
- setIsConnectFailed(true);
- setIsWrongDevice(true);
- setIsButtonDisabled(false);
- return;
- }
-
- setIsConnectSuccess(true);
- await delay(1500);
-
- if (
- type === 'ORDINALS' &&
- currentStep !== Steps.ExternalInputs &&
- currentStep !== Steps.ConfirmTransaction
- ) {
- setCurrentStep(Steps.ExternalInputs);
- return;
- }
-
- if (currentStep !== Steps.ConfirmTransaction) {
- setCurrentStep(Steps.ConfirmTransaction);
- }
-
- switch (type) {
- case 'BTC':
- case 'BRC-20':
- await signAndBroadcastBtcTx(transport as Transport, addressIndex);
- break;
- case 'STX':
- await signAndBroadcastStxTx(transport as Transport, addressIndex);
- break;
- case 'ORDINALS':
- await signAndBroadcastOrdinalsTx(transport as Transport, addressIndex);
- break;
- default:
- break;
- }
- await transport.close();
- } catch (err) {
- setIsConnectSuccess(false);
- setIsConnectFailed(true);
- } finally {
- setIsButtonDisabled(false);
- }
- };
-
- const goToConfirmationStep = () => {
- setCurrentStep(Steps.ConfirmTransaction);
-
- handleConnectAndConfirm();
- };
-
- const handleRetry = async () => {
- setIsTxRejected(false);
- setIsConnectSuccess(false);
- setIsConnectFailed(false);
- setIsWrongDevice(false);
- setIsTxApproved(false);
- setCurrentStep(Steps.ConnectLedger);
- };
-
- const handleClose = () => {
- if (typeof window !== 'undefined') {
- window.close();
- }
- };
-
- const handleSeeTransaction = () => {
- if (!txId) {
- console.error('No txId found');
- return;
- }
-
- switch (type) {
- case 'BTC':
- case 'BRC-20':
- window.open(getBtcTxStatusUrl(txId, network), '_blank', 'noopener,noreferrer');
- break;
- case 'STX':
- window.open(getStxTxStatusUrl(txId, network), '_blank', 'noopener,noreferrer');
- break;
- case 'ORDINALS':
- window.open(getBtcTxStatusUrl(txId, network), '_blank', 'noopener,noreferrer');
- break;
- default:
- break;
- }
- };
-
- const renderTxDetails = () => (
-
-
- Recipient{recipients.length > 1 ? 's' : ''}
-
- {recipients.map((recipient) => (
- {getTruncatedAddress(recipient.address)}
- ))}
-
-
-
- {ordinalUtxo?.value ? 'Ordinal value' : 'Amount'}
- {type === 'STX' ? (
-
- {microstacksToStx((recipients[0] as StacksRecipient).amountMicrostacks).toString()} STX
-
- ) : (
-
- {ordinalUtxo?.value
- ? satsToBtc(new BigNumber(ordinalUtxo?.value)).toString()
- : satsToBtc((recipients[0] as Recipient).amountSats).toString()}{' '}
- BTC
-
- )}
-
- {fee && (
-
- {t('FEES')}
- {type === 'STX' ? (
- {microstacksToStx(fee).toString()} STX
- ) : (
- {satsToBtc(fee).toString()} BTC
- )}
-
- )}
-
- );
-
- const renderOrdinalTxDetails = () => (
- <>
-
- {renderTxDetails()}
- >
- );
-
- const connectErrSubtitle =
- type === 'STX' ? 'CONNECT.STX_ERROR_SUBTITLE' : 'CONNECT.BTC_ERROR_SUBTITLE';
-
- const renderLedgerConfirmationView = () => {
- switch (currentStep) {
- case Steps.ConnectLedger:
- return (
-
-
-
- );
- case Steps.ExternalInputs:
- if (isTxRejected || isConnectFailed) {
- return (
-
- );
- }
-
- return (
-
-
-
- {t('EXTERNAL_INPUTS.TITLE')}
- {t('EXTERNAL_INPUTS.SUBTITLE')}
-
-
- );
- case Steps.ConfirmTransaction:
- if (type === 'ORDINALS') {
- if (isTxRejected || isConnectFailed) {
- return (
-
- );
- }
-
- return (
-
-
- {t('CONFIRM.ORDINAL_TX.ORDINAL_TITLE')}
-
- {t('CONFIRM.ORDINAL_TX.ORDINAL_SUBTITLE')}
-
- {renderOrdinalTxDetails()}
-
-
- );
- }
-
- return (
-
-
- {renderTxDetails()}
-
- );
- case Steps.ConfirmFees:
- if (type === 'ORDINALS') {
- if (isTxRejected || isConnectFailed) {
- return (
-
- );
- }
-
- return (
-
-
- {t('CONFIRM.TITLE')}
-
- {t('CONFIRM.ORDINAL_TX.BTC_SUBTITLE')}
-
- {renderOrdinalTxDetails()}
-
-
- );
- }
- return null;
- case Steps.TransactionConfirmed:
- return (
-
-
- {t('SUCCESS.TITLE')}
- {t('SUCCESS.SUBTITLE')}
- {type === 'BRC-20' && (
-
-
-
- )}
-
- );
- default:
- return null;
- }
- };
-
- const renderLedgerConfirmationControls = () => {
- switch (currentStep) {
- case Steps.ExternalInputs:
- if (isTxRejected || isConnectFailed) {
- return (
-
-
-
-
- );
- }
-
- return (
-
-
-
- );
- case Steps.ConfirmTransaction:
- case Steps.ConfirmFees:
- if (type === 'ORDINALS' && !isTxRejected && !isConnectFailed) {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
-
- );
- case Steps.TransactionConfirmed:
- return (
-
-
-
-
- );
- default:
- return (
-
-
-
-
- );
- }
- };
-
- return (
-
-
- {transition((style) => (
- <>
-
- {renderLedgerConfirmationView()}
-
- {renderLedgerConfirmationControls()}
- >
- ))}
-
- );
-}
-
-export default ConfirmLedgerTransaction;
diff --git a/src/app/screens/ledger/importLedgerAccount/index.tsx b/src/app/screens/ledger/importLedgerAccount/index.tsx
index 8de7969e9..713a2285b 100644
--- a/src/app/screens/ledger/importLedgerAccount/index.tsx
+++ b/src/app/screens/ledger/importLedgerAccount/index.tsx
@@ -1,4 +1,5 @@
-import { delay, getDeviceNewAccountIndex, getNewAccountId } from '@common/utils/ledger';
+import { getDeviceNewAccountIndex, getNewAccountId } from '@common/utils/ledger';
+import { delay } from '@common/utils/promises';
import FullScreenHeader from '@components/ledger/fullScreenHeader';
import useWalletReducer from '@hooks/useWalletReducer';
import useWalletSelector from '@hooks/useWalletSelector';
@@ -200,12 +201,8 @@ function ImportLedger(): JSX.Element {
const ledgerAccount: Account = {
id: newAccountId,
stxAddress: stacksCreds?.address || '',
- btcAddress: btcCreds?.address || '',
- ordinalsAddress: ordinalsCreds?.address || '',
masterPubKey: masterPubKey || masterFingerPrint || '',
stxPublicKey: stacksCreds?.publicKey || '',
- btcPublicKey: btcCreds?.publicKey || '',
- ordinalsPublicKey: ordinalsCreds?.publicKey || '',
accountType: 'ledger',
accountName: `Ledger Account ${newAccountId + 1}`,
deviceAccountIndex: getDeviceNewAccountIndex(
@@ -213,6 +210,16 @@ function ImportLedger(): JSX.Element {
network.type,
masterPubKey || masterFingerPrint,
),
+ btcAddresses: {
+ native: {
+ address: btcCreds?.address || '',
+ publicKey: btcCreds?.publicKey || '',
+ },
+ taproot: {
+ address: ordinalsCreds?.address || '',
+ publicKey: ordinalsCreds?.publicKey || '',
+ },
+ },
};
await addLedgerAccount(ledgerAccount);
await delay(1000);
@@ -224,10 +231,16 @@ function ImportLedger(): JSX.Element {
if (currentAccount && isBitcoinSelected) {
const ledgerAccount: Account = {
...currentAccount,
- btcAddress: btcCreds?.address || '',
- btcPublicKey: btcCreds?.publicKey || '',
- ordinalsAddress: ordinalsCreds?.address || '',
- ordinalsPublicKey: ordinalsCreds?.publicKey || '',
+ btcAddresses: {
+ native: {
+ address: btcCreds?.address || '',
+ publicKey: btcCreds?.publicKey || '',
+ },
+ taproot: {
+ address: ordinalsCreds?.address || '',
+ publicKey: ordinalsCreds?.publicKey || '',
+ },
+ },
};
await updateLedgerAccounts(ledgerAccount);
await delay(1000);
@@ -383,8 +396,8 @@ function ImportLedger(): JSX.Element {
setCurrentStep(ImportLedgerSteps.START);
};
- const handleAssetSelect = (e: React.ChangeEvent) => {
- if (e.target.id === 'stx_select_card') {
+ const handleAssetSelect = (selectedAsset: 'Bitcoin' | 'Stacks') => {
+ if (selectedAsset === 'Stacks') {
setIsStacksSelected(!isStacksSelected);
}
};
diff --git a/src/app/screens/ledger/importLedgerAccount/stepControls.tsx b/src/app/screens/ledger/importLedgerAccount/stepControls.tsx
index 4e7b80a0a..e55085649 100644
--- a/src/app/screens/ledger/importLedgerAccount/stepControls.tsx
+++ b/src/app/screens/ledger/importLedgerAccount/stepControls.tsx
@@ -1,16 +1,16 @@
-import ActionButton from '@components/button';
+import Button from '@ui-library/button';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { ImportLedgerSteps } from './types';
-const ButtonContainer = styled.div((props) => ({
- marginLeft: 3,
- marginRight: 3,
- marginTop: props.theme.spacing(4),
+const VerticalButtonContainer = styled.div((props) => ({
+ display: 'flex',
+ gap: props.theme.space.s,
+ flexDirection: 'column',
width: '100%',
}));
-interface Props {
+type Props = {
isBitcoinSelected: boolean;
isStacksSelected: boolean;
isTogglerChecked: boolean;
@@ -29,7 +29,7 @@ interface Props {
isStxAddressRejected: boolean;
accountNameError?: string;
};
-}
+};
function StepControls({
isBitcoinSelected,
@@ -62,63 +62,59 @@ function StepControls({
switch (currentStep) {
case ImportLedgerSteps.START:
- return ;
+ return ;
case ImportLedgerSteps.SELECT_ASSET:
return (
-
);
case ImportLedgerSteps.BEFORE_START:
return (
-
);
case ImportLedgerSteps.IMPORTANT_WARNING:
return (
-
);
case ImportLedgerSteps.CONNECT_LEDGER:
return (
-
);
case ImportLedgerSteps.ADD_MULTIPLE_ACCOUNTS:
return (
- <>
-
-
-
-
-
-
- >
+
+
+
+
);
case ImportLedgerSteps.ADD_ADDRESS:
case ImportLedgerSteps.ADD_ORDINALS_ADDRESS:
@@ -129,33 +125,40 @@ function StepControls({
isStxAddressRejected
) {
return (
-
);
}
break;
case ImportLedgerSteps.ADDRESS_ADDED:
- return ;
+ return (
+
+ );
case ImportLedgerSteps.ADD_ACCOUNT_NAME:
return (
-
);
case ImportLedgerSteps.IMPORT_END:
return (
-
);
default:
diff --git a/src/app/screens/ledger/importLedgerAccount/steps/index.styled.ts b/src/app/screens/ledger/importLedgerAccount/steps/index.styled.ts
index b791f721b..edfbca5b1 100644
--- a/src/app/screens/ledger/importLedgerAccount/steps/index.styled.ts
+++ b/src/app/screens/ledger/importLedgerAccount/steps/index.styled.ts
@@ -1,4 +1,3 @@
-import Switch from 'react-switch';
import styled from 'styled-components';
export const ImportStartImage = styled.img((props) => ({
@@ -17,7 +16,7 @@ export const ImportBeforeStartContainer = styled.div((props) => ({
flexDirection: 'column',
alignItems: 'center',
maxWidth: '328px',
- paddingTop: props.theme.spacing(16),
+ paddingTop: props.theme.space.xl,
}));
export const ImportStartTitle = styled.h1((props) => ({
@@ -28,8 +27,8 @@ export const ImportStartTitle = styled.h1((props) => ({
export const ImportStartText = styled.p((props) => ({
...props.theme.body_m,
textAlign: 'center',
- marginTop: props.theme.spacing(6),
- color: props.theme.colors.white[200],
+ marginTop: props.theme.space.s,
+ color: props.theme.colors.white_200,
}));
export const ImportBeforeStartTitle = styled.h1((props) => ({
@@ -41,22 +40,22 @@ export const ImportBeforeStartTitle = styled.h1((props) => ({
export const ImportBeforeStartText = styled.p((props) => ({
...props.theme.body_m,
textAlign: 'left',
- marginTop: props.theme.spacing(6),
- color: props.theme.colors.white[200],
+ marginTop: props.theme.space.s,
+ color: props.theme.colors.white_200,
}));
export const ImportCardContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
- gap: props.theme.spacing(6),
+ gap: props.theme.space.s,
}));
export const SelectAssetTextContainer = styled.div((props) => ({
- marginTop: props.theme.spacing(20),
- marginBottom: props.theme.spacing(16),
+ marginTop: props.theme.space.xxl,
+ marginBottom: props.theme.space.xl,
display: 'flex',
flexDirection: 'column',
- gap: props.theme.spacing(6),
+ gap: props.theme.space.s,
}));
export const SelectAssetTitle = styled.h1((props) => ({
@@ -66,40 +65,40 @@ export const SelectAssetTitle = styled.h1((props) => ({
export const SelectAssetText = styled.p<{
centered?: boolean;
}>((props) => ({
- ...props.theme.body_m,
- color: props.theme.colors.white[200],
+ ...props.theme.typography.body_m,
+ color: props.theme.colors.white_200,
textAlign: props.centered ? 'center' : 'left',
}));
export const SelectAssetFootNote = styled.p((props) => ({
...props.theme.body_xs,
- color: props.theme.colors.white[200],
- marginTop: props.theme.spacing(6),
+ color: props.theme.colors.white_200,
+ marginTop: props.theme.space.s,
}));
export const AddAddressHeaderContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
- gap: props.theme.spacing(8),
- marginTop: props.theme.spacing(20),
- marginBottom: props.theme.spacing(8),
+ gap: props.theme.space.m,
+ marginTop: props.theme.space.xxl,
+ marginBottom: props.theme.space.m,
}));
export const CreateAnotherAccountContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
- gap: props.theme.spacing(8),
+ gap: props.theme.space.m,
paddingTop: props.theme.spacing(90),
- marginBottom: props.theme.spacing(16),
+ marginBottom: props.theme.space.xl,
}));
export const AddAddressDetailsContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
- gap: props.theme.spacing(20),
+ gap: props.theme.space.xxl,
}));
export const AddressAddedContainer = styled.div`
@@ -115,11 +114,11 @@ export const AddressAddedContainer = styled.div`
`;
export const AddAccountNameContainer = styled.div((props) => ({
- marginTop: props.theme.spacing(20),
+ marginTop: props.theme.space.xxl,
width: '100%',
display: 'flex',
flexDirection: 'column',
- gap: props.theme.spacing(6),
+ gap: props.theme.space.s,
}));
export const AddAccountNameTitleContainer = styled.div((props) => ({
@@ -146,8 +145,8 @@ export const EndScreenTextContainer = styled.div((props) => ({
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
- gap: props.theme.spacing(6),
- marginBottom: props.theme.spacing(20),
+ gap: props.theme.space.s,
+ marginBottom: props.theme.space.xxl,
}));
export const ConfirmationText = styled.p((props) => ({
@@ -160,32 +159,34 @@ export const ConfirmationText = styled.p((props) => ({
export const ConfirmationStepsContainer = styled.div((props) => ({
display: 'flex',
justifyContent: 'center',
- marginTop: props.theme.spacing(12),
+ marginTop: props.theme.space.l,
}));
export const OptionsContainer = styled.div((props) => ({
width: '100%',
- marginTop: props.theme.spacing(16),
+ marginTop: props.theme.space.xl,
}));
interface OptionProps {
- selected?: boolean;
+ $selected?: boolean;
}
-export const Option = styled.div((props) => ({
+export const Option = styled.button((props) => ({
+ ...props.theme.typography.body_medium_m,
width: '100%',
- backgroundColor: props.theme.colors.elevation3,
- padding: props.theme.spacing(8),
+ color: props.theme.colors.white_0,
+ backgroundColor: props.theme.colors.elevation1,
+ padding: props.theme.space.m,
paddingTop: props.theme.spacing(7),
paddingBottom: props.theme.spacing(7),
+ marginBottom: props.theme.space.s,
+ border: `1px solid ${props.$selected ? '#7383ff4d' : props.theme.colors.elevation1}`,
borderRadius: props.theme.radius(2),
- fontSize: '0.75rem',
- marginBottom: props.theme.spacing(6),
- border: `1px solid ${props.selected ? props.theme.colors.elevation6 : 'transparent'}`,
cursor: 'pointer',
userSelect: 'none',
display: 'flex',
alignItems: 'center',
transition: 'border 0.2s ease',
+ textAlign: 'left',
}));
// TODO create radio button in ui-library
@@ -196,41 +197,32 @@ export const OptionIcon = styled.div((props) => ({
width: 16,
height: 16,
borderRadius: '50%',
- border: `1px solid ${props.theme.colors.white[0]}`,
+ border: `1px solid ${props.theme.colors.white_0}`,
marginRight: props.theme.spacing(10),
flex: 'none',
'&::after': {
content: '""',
- display: props.selected ? 'block' : 'none',
- width: 10,
- height: 10,
+ display: props.$selected ? 'block' : 'none',
+ width: 8,
+ height: 8,
borderRadius: 100,
- backgroundColor: props.theme.colors.orange_main,
+ backgroundColor: props.theme.colors.white_0,
},
}));
export const ConfirmationStep = styled.div<{
- isCompleted: boolean;
+ $isCompleted: boolean;
}>((props) => ({
width: 32,
height: 4,
- backgroundColor: props.isCompleted ? props.theme.colors.white[0] : props.theme.colors.white[900],
+ backgroundColor: props.$isCompleted ? props.theme.colors.white_0 : props.theme.colors.white_900,
borderRadius: props.theme.radius(1),
transition: 'background-color 0.2s ease',
':first-child': {
- marginRight: props.theme.spacing(4),
+ marginRight: props.theme.space.xs,
},
}));
-// TODO create custom switch in ui-library
-export const CustomSwitch = styled(Switch)`
- .react-switch-handle {
- background-color: ${({ checked }) =>
- checked ? '#FFFFFF' : 'rgba(255, 255, 255, 0.2)'} !important;
- border: ${({ checked }) => (checked ? '' : '4px solid rgba(255, 255, 255, 0.2)')} !important;
- }
-`;
-
export const TogglerContainer = styled.div((props) => ({
display: 'flex',
alignItems: 'center',
@@ -238,7 +230,7 @@ export const TogglerContainer = styled.div((props) => ({
}));
export const TogglerText = styled.p((props) => ({
- marginLeft: props.theme.spacing(8),
+ marginLeft: props.theme.space.m,
fontWeight: 500,
fontSize: '0.875rem',
lineHeight: '140%',
@@ -255,8 +247,8 @@ export const WarningIcon = styled.img({
});
export const LedgerFailViewContainer = styled.div((props) => ({
- paddingLeft: props.theme.spacing(8),
- paddingRight: props.theme.spacing(8),
+ paddingLeft: props.theme.space.m,
+ paddingRight: props.theme.space.m,
margin: 'auto',
}));
@@ -267,6 +259,6 @@ export const LedgerFailButtonsContainer = styled.div((props) => ({
export const ActionButtonContainer = styled.div((props) => ({
'&:not(:last-of-type)': {
- marginBottom: props.theme.spacing(8),
+ marginBottom: props.theme.space.m,
},
}));
diff --git a/src/app/screens/ledger/importLedgerAccount/steps/index.tsx b/src/app/screens/ledger/importLedgerAccount/steps/index.tsx
index f5210b0ea..4a22e8ba0 100644
--- a/src/app/screens/ledger/importLedgerAccount/steps/index.tsx
+++ b/src/app/screens/ledger/importLedgerAccount/steps/index.tsx
@@ -13,10 +13,10 @@ import LedgerAddressComponent from '@components/ledger/ledgerAddressComponent';
import LedgerAssetSelectCard from '@components/ledger/ledgerAssetSelectCard';
import LedgerInput from '@components/ledger/ledgerInput';
import { useTranslation } from 'react-i18next';
-import { useTheme } from 'styled-components';
import LedgerConnectionView from '../../../../components/ledger/connectLedgerView';
import { ImportLedgerSteps, LedgerLiveOptions } from '../types';
+import Toggle from '@ui-library/toggle';
import {
AddAccountNameContainer,
AddAccountNameTitleContainer,
@@ -29,7 +29,6 @@ import {
CreateAnotherAccountContainer,
CreateMultipleAccountsText,
CustomLink,
- CustomSwitch,
EndScreenContainer,
EndScreenTextContainer,
ImportBeforeStartContainer,
@@ -53,11 +52,11 @@ import {
} from './index.styled';
const LINK_TO_LEDGER_ACCOUNT_ISSUE_GUIDE =
- 'https://support.xverse.app/hc/en-us/articles/17901278165773';
+ 'https://support.xverse.app/hc/en-us/articles/17898446492557';
const LINK_TO_LEDGER_PASSPHRASE_GUIDE =
'https://support.xverse.app/hc/en-us/articles/17901278165773';
-interface Props {
+type Props = {
isConnectSuccess: boolean;
isBitcoinSelected: boolean;
isStacksSelected: boolean;
@@ -68,7 +67,7 @@ interface Props {
accountName: string;
accountId: number;
selectedLedgerLiveOption: LedgerLiveOptions | null;
- handleAssetSelect: (event: React.ChangeEvent) => void;
+ handleAssetSelect: (selectedAsset: 'Bitcoin' | 'Stacks') => void;
setSelectedLedgerLiveOption: (option: LedgerLiveOptions) => void;
setIsTogglerChecked: (checked: boolean) => void;
setAccountName: (name: string) => void;
@@ -84,7 +83,7 @@ interface Props {
isStxAddressRejected: boolean;
accountNameError?: string;
};
-}
+};
function Steps({
isConnectSuccess,
@@ -105,7 +104,6 @@ function Steps({
errors,
}: Props) {
const { t } = useTranslation('translation', { keyPrefix: 'LEDGER_IMPORT_SCREEN' });
- const theme = useTheme();
const { bitcoinCredentials, ordinalsCredentials, stacksCredentials } = creds;
const {
isConnectFailed,
@@ -137,18 +135,18 @@ function Steps({
icon={btcOrdinalsIcon}
title={t('LEDGER_IMPORT_2_SELECT.BTC_TITLE')}
text={t('LEDGER_IMPORT_2_SELECT.BTC_SUBTITLE')}
- id="btc_select_card"
+ name="Bitcoin"
isChecked={isBitcoinSelected}
- onChange={handleAssetSelect}
+ onClick={handleAssetSelect}
/>
{t('LEDGER_IMPORT_2_FOOTNOTE')}
@@ -167,16 +165,16 @@ function Steps({
@@ -239,13 +237,9 @@ function Steps({
)}
- setIsTogglerChecked(!isTogglerChecked)}
checked={isTogglerChecked}
- uncheckedIcon={false}
- checkedIcon={false}
/>
{selectedLedgerLiveOption === LedgerLiveOptions.USING ? (
@@ -346,8 +340,8 @@ function Steps({
{t('LEDGER_ADD_ADDRESS.CONFIRM_TO_CONTINUE')}
{isBitcoinSelected && (
-
-
+
+
)}
>
@@ -385,8 +379,8 @@ function Steps({
{t('LEDGER_ADD_ADDRESS.CONFIRM_TO_CONTINUE')}
-
-
+
+
>
);
diff --git a/src/app/screens/ledger/verifyLedgerAccountAddress/index.tsx b/src/app/screens/ledger/verifyLedgerAccountAddress/index.tsx
index cf730ec4f..bafacfa91 100644
--- a/src/app/screens/ledger/verifyLedgerAccountAddress/index.tsx
+++ b/src/app/screens/ledger/verifyLedgerAccountAddress/index.tsx
@@ -1,4 +1,4 @@
-import { delay } from '@common/utils/ledger';
+import { delay } from '@common/utils/promises';
import ActionButton from '@components/button';
import InfoContainer from '@components/infoContainer';
import FullScreenHeader from '@components/ledger/fullScreenHeader';
diff --git a/src/app/screens/legal/index.tsx b/src/app/screens/legal/index.tsx
index f4496a08e..80f88ad07 100644
--- a/src/app/screens/legal/index.tsx
+++ b/src/app/screens/legal/index.tsx
@@ -1,8 +1,8 @@
import LinkIcon from '@assets/img/linkIcon.svg';
import Separator from '@components/separator';
import useWalletReducer from '@hooks/useWalletReducer';
-import { CustomSwitch } from '@screens/ledger/importLedgerAccount/steps/index.styled';
import Button from '@ui-library/button';
+import Toggle from '@ui-library/toggle';
import { PRIVACY_POLICY_LINK, TERMS_LINK } from '@utils/constants';
import { saveIsTermsAccepted } from '@utils/localStorage';
import { optInMixPanel, optOutMixPanel } from '@utils/mixpanel';
@@ -116,14 +116,7 @@ function Legal() {
{t('AUTHORIZE_DATA_COLLECTION.TITLE')}
-
+
diff --git a/src/app/screens/listRune/index.styled.ts b/src/app/screens/listRune/index.styled.ts
index 3e7e79b35..c9454d423 100644
--- a/src/app/screens/listRune/index.styled.ts
+++ b/src/app/screens/listRune/index.styled.ts
@@ -134,6 +134,7 @@ export const RightAlignStyledP = styled(StyledP)`
text-align: right;
`;
-export const NoItemsContainer = styled.div((props) => ({
- padding: `${props.theme.space.m} ${props.theme.space.s}`,
+export const NoItemsContainer = styled(StyledP)((props) => ({
+ marginTop: props.theme.space.xxxl,
+ textAlign: 'center',
}));
diff --git a/src/app/screens/listRune/index.tsx b/src/app/screens/listRune/index.tsx
index d719c99f0..a405011cb 100644
--- a/src/app/screens/listRune/index.tsx
+++ b/src/app/screens/listRune/index.tsx
@@ -1,10 +1,11 @@
+import RequestsRoutes from '@common/utils/route-urls';
import BottomBar from '@components/tabBar';
import TopRow from '@components/topRow';
-import useRuneFloorPriceQuery from '@hooks/queries/runes/useRuneFloorPriceQuery';
+import useRuneFloorPricePerMarketplaceQuery from '@hooks/queries/runes/useRuneFloorPricePerMarketplaceQuery';
import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery';
-import useRuneSellPsbt from '@hooks/queries/runes/useRuneSellPsbt';
-import useRuneUtxosQuery from '@hooks/queries/runes/useRuneUtxosQuery';
-import useCoinRates from '@hooks/queries/useCoinRates';
+import useRuneSellPsbtPerMarketplace from '@hooks/queries/runes/useRuneSellPsbtPerMarketplace';
+import useRuneUtxosQueryPerMarketplace from '@hooks/queries/runes/useRuneUtxosQueryPerMarketplace';
+import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates';
import useHasFeature from '@hooks/useHasFeature';
import { useResetUserFlow } from '@hooks/useResetUserFlow';
import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed';
@@ -18,14 +19,17 @@ import {
currencySymbolMap,
getBtcFiatEquivalent,
satsToBtc,
+ type FungibleToken,
+ type GetListedUtxosResponseUtxo,
+ type Marketplace,
} from '@secretkeylabs/xverse-core';
import Button, { LinkButton } from '@ui-library/button';
import { StickyButtonContainer, StyledP } from '@ui-library/common.styled';
import Spinner from '@ui-library/spinner';
import { formatToXDecimalPlaces, ftDecimals } from '@utils/helper';
-import { getFullTxId, getTxIdFromFullTxId, getVoutFromFullTxId } from '@utils/runes';
+import { getTxIdFromFullTxId, getVoutFromFullTxId } from '@utils/runes';
import BigNumber from 'bignumber.js';
-import { useEffect, useReducer } from 'react';
+import { useEffect, useMemo, useReducer, useState } from 'react';
import toast from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { NumericFormat } from 'react-number-format';
@@ -52,17 +56,25 @@ import {
TabButtonsContainer,
TabContainer,
} from './index.styled';
+import ListMarketplaceItem from './listMarketplaceItem';
import WrapperComponent from './listRuneWrapper';
import SetCustomPriceModal from './setCustomPriceModal';
+const joinedSelectedMarketplaces = (selectedMarketplaces: Marketplace[]) => {
+ const last = selectedMarketplaces.pop();
+ return [selectedMarketplaces.join(', '), last].filter((s) => s).join(' and ');
+};
+
+const getFullTxId = (item: GetListedUtxosResponseUtxo) => `${item.txid}:${item.vout.toString()}`;
+
export default function ListRuneScreen() {
const { t } = useTranslation('translation', { keyPrefix: 'LIST_RUNE_SCREEN' });
const navigate = useNavigate();
const { runeId } = useParams();
- const { visible: runesCoinsList } = useVisibleRuneFungibleTokens(false);
+ const { data: runesCoinsList } = useVisibleRuneFungibleTokens(false);
const selectedRune = runesCoinsList.find((ft) => ft.principal === runeId);
const { fiatCurrency } = useWalletSelector();
- const { btcFiatRate } = useCoinRates();
+ const { btcFiatRate } = useSupportedCoinRates();
const location = useLocation();
const params = new URLSearchParams(location.search);
const locationFrom = params.get('from');
@@ -76,19 +88,38 @@ export default function ListRuneScreen() {
useTrackMixPanelPageViewed();
const {
- data: listItemsResponse,
+ data,
isInitialLoading: listItemsLoading,
isRefetching: listItemsRefetching,
refetch,
- } = useRuneUtxosQuery(selectedRune?.name ?? '', 'unlisted', false);
+ } = useRuneUtxosQueryPerMarketplace(selectedRune as FungibleToken, false);
+ const listItemsResponse = data?.unlistedItems?.filter((i) => i.runes.length === 1) || [];
+ const supportedMarketplaces: Marketplace[] = ['Unisat', 'Magic Eden', 'OKX'];
const {
- data: runeFloorPrice,
+ data: floorPriceData,
isInitialLoading: floorPriceLoading,
isRefetching: floorPriceRefetching,
- } = useRuneFloorPriceQuery(selectedRune?.name ?? '', false);
+ } = useRuneFloorPricePerMarketplaceQuery(
+ {
+ name: selectedRune?.assetName ?? '',
+ id: selectedRune?.principal ?? '',
+ },
+ supportedMarketplaces,
+ false,
+ );
- const noFloorPrice = runeFloorPrice === 0;
+ const [selectedMarketplaces, setSelectedMarketplaces] = useState(new Set
());
+ const runeFloorPrice = useMemo(
+ () =>
+ Math.min(
+ ...(floorPriceData
+ ?.filter((d) => selectedMarketplaces.has(d.marketplace.name))
+ .map((d) => d.floorPrice) || [0]),
+ ),
+ [floorPriceData, selectedMarketplaces],
+ );
+ const noFloorPrice = useMemo(() => runeFloorPrice === 0, [runeFloorPrice]);
const isLoading =
listItemsLoading || listItemsRefetching || floorPriceLoading || floorPriceRefetching;
@@ -112,7 +143,9 @@ export default function ListRuneScreen() {
signPsbtPayload,
loading: psbtLoading,
error: psbtError,
- } = useRuneSellPsbt(selectedRune?.name ?? '', listItemsMap);
+ } = useRuneSellPsbtPerMarketplace(selectedRune as FungibleToken, listItemsMap, [
+ ...selectedMarketplaces,
+ ]);
const selectedListItems = Object.values(listItemsMap).filter((item) => item.selected);
@@ -141,7 +174,7 @@ export default function ListRuneScreen() {
const curAmount = currentItem.amount;
return curAmount > maxAmount ? curAmount : maxAmount;
}, 0);
- const maxGlobalPriceSats = 1_000_000_000 / highestSelectedRuneAmount;
+ const maxGlobalPriceSats = 500_000_000 / highestSelectedRuneAmount; // Unisat's maximum
const invalidListings: boolean = selectedListItems.some(
(item) => item.amount * item.priceSats < 10000 || item.amount * item.priceSats > 1000000000,
@@ -157,8 +190,23 @@ export default function ListRuneScreen() {
if (section === 'SELECT_RUNES') {
navigate(`/coinDashboard/FT?ftKey=${selectedRune?.principal}&protocol=runes`);
- } else {
+ } else if (section === 'SELECT_MARKETPLACES') {
dispatch({ type: 'SET_SECTION', payload: 'SELECT_RUNES' });
+ } else {
+ dispatch({ type: 'SET_SECTION', payload: 'SELECT_MARKETPLACES' });
+ }
+ };
+
+ const getTitle = () => {
+ switch (section) {
+ case 'SELECT_RUNES':
+ return t('SELECT_RUNES');
+ case 'SELECT_MARKETPLACES':
+ return t('SELECT_MARKETPLACES');
+ case 'SET_PRICES':
+ return t('LIST_RUNES');
+ default:
+ return '';
}
};
@@ -166,6 +214,8 @@ export default function ListRuneScreen() {
switch (section) {
case 'SELECT_RUNES':
return t('SELECT_RUNES_SECTION');
+ case 'SELECT_MARKETPLACES':
+ return t('SELECT_MARKETPLACES_SECTION');
case 'SET_PRICES':
return t('SET_PRICES_SECTION');
default:
@@ -181,15 +231,13 @@ export default function ListRuneScreen() {
dispatch({ type: 'UPDATE_ONE_LIST_ITEM', key, payload: updatedSelectedListItem });
if (
Object.values(listItemsMap).filter((listItem) => listItem.selected).length ===
- listItemsResponse?.length ??
- 0
+ listItemsResponse?.length
) {
dispatch({ type: 'SET_SELECT_ALL_TOGGLE', payload: true });
}
if (
Object.values(listItemsMap).filter((listItem) => !listItem.selected).length ===
- listItemsResponse?.length ??
- 0
+ listItemsResponse?.length
) {
dispatch({ type: 'SET_SELECT_ALL_TOGGLE', payload: false });
}
@@ -237,15 +285,16 @@ export default function ListRuneScreen() {
),
});
}
- }, [listItemsResponse, runeFloorPrice, location.state, selectedRune]);
+ }, [listItemsResponse.length, runeFloorPrice, location.state, selectedRune?.decimals]);
useEffect(() => {
if (signPsbtPayload) {
- navigate(`/psbt-signing-request?magicEdenPsbt=true&runeId=${runeId}`, {
+ navigate(RequestsRoutes.RuneListingBatchSigning, {
state: {
payload: signPsbtPayload,
+ utxos: listItemsMap,
+ minPriceSats: Math.min(...Object.values(listItemsMap).map((item) => item.priceSats)),
selectedRune,
- listRunesState,
},
});
}
@@ -278,7 +327,9 @@ export default function ListRuneScreen() {
selectedRuneId={selectedRune?.principal ?? ''}
getDesc={getDesc}
>
- {t('NO_UNLISTED_ITEMS')}
+
+ {t('NO_UNLISTED_ITEMS')}
+
);
}
@@ -293,7 +344,7 @@ export default function ListRuneScreen() {
-
+
{getDesc()}
@@ -361,8 +412,10 @@ export default function ListRuneScreen() {
>
)}
+ {section === 'SELECT_MARKETPLACES' && (
+ <>
+
+
+
+ {floorPriceData?.map((runeMarketInfo) => {
+ const { name } = runeMarketInfo.marketplace;
+ const selected = selectedMarketplaces.has(name);
+
+ return (
+
+ setSelectedMarketplaces((prev) => {
+ if (selected) {
+ prev.delete(name);
+ } else {
+ prev.add(name);
+ }
+ return new Set([...prev]);
+ })
+ }
+ />
+ );
+ })}
+
+
+
+
+
+
+
+ >
+ )}
{section === 'SET_PRICES' && (
<>
@@ -418,7 +514,13 @@ export default function ListRuneScreen() {
- dispatch({ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', payload: true })
+ dispatch({
+ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL',
+ payload: {
+ title: t('SET_PRICES'),
+ visible: true,
+ },
+ })
}
variant={
runePriceOption === 'custom' && !individualCustomPriceUsed
@@ -430,9 +532,10 @@ export default function ListRuneScreen() {
{noFloorPrice
? t('NO_FLOOR_PRICE', { symbol: selectedRune?.runeSymbol })
- : t('MAGIC_EDEN_FLOOR_PRICE', {
+ : t('MARKETPLACE_FLOOR_PRICE', {
sats: formatToXDecimalPlaces(runeFloorPrice, 5),
symbol: selectedRune?.runeSymbol,
+ marketplaces: joinedSelectedMarketplaces([...selectedMarketplaces]),
})}
@@ -458,7 +561,13 @@ export default function ListRuneScreen() {
}
handleShowCustomPriceModal={() => {
dispatch({ type: 'SET_INDIVIDUAL_CUSTOM_ITEM', payload: fullTxId });
- dispatch({ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', payload: true });
+ dispatch({
+ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL',
+ payload: {
+ title: t('EDIT_PRICE'),
+ visible: true,
+ },
+ });
}}
/>
))}
@@ -539,8 +648,8 @@ export default function ListRuneScreen() {
{
dispatch({ type: 'SET_INDIVIDUAL_CUSTOM_ITEM', payload: null });
- dispatch({ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', payload: false });
+ dispatch({
+ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL',
+ payload: {
+ ...toggleCustomPriceModal,
+ visible: false,
+ },
+ });
}}
onApplyPrice={(priceSats: number) => {
if (individualCustomItem) {
@@ -565,7 +680,13 @@ export default function ListRuneScreen() {
dispatch({ type: 'SET_RUNE_PRICE_OPTION', payload: 'custom' });
}
dispatch({ type: 'SET_INDIVIDUAL_CUSTOM_ITEM', payload: null });
- dispatch({ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL', payload: false });
+ dispatch({
+ type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL',
+ payload: {
+ ...toggleCustomPriceModal,
+ visible: false,
+ },
+ });
}}
/>
>
diff --git a/src/app/screens/listRune/listMarketplaceItem.tsx b/src/app/screens/listRune/listMarketplaceItem.tsx
new file mode 100644
index 000000000..98250941a
--- /dev/null
+++ b/src/app/screens/listRune/listMarketplaceItem.tsx
@@ -0,0 +1,92 @@
+import TokenImage from '@components/tokenImage';
+import type { FungibleToken, ListingProvider } from '@secretkeylabs/xverse-core';
+import Checkbox from '@ui-library/checkbox';
+import { StyledP } from '@ui-library/common.styled';
+import { formatNumber } from '@utils/helper';
+import { useTranslation } from 'react-i18next';
+import styled from 'styled-components';
+import theme from 'theme';
+
+const Container = styled.div<{ $selected: boolean }>`
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ justify-content: space-between;
+ flex: 1;
+ padding: ${(props) => props.theme.space.s};
+ padding-right: ${(props) => props.theme.space.m};
+ border-radius: ${(props) => props.theme.space.xs};
+ border: 1px solid;
+ border-color: ${(props) => (props.$selected ? props.theme.colors.white_900 : 'transparent')};
+ background-color: ${(props) =>
+ props.$selected ? props.theme.colors.elevation2 : props.theme.colors.elevation1};
+ transition: background-color 0.1s ease, border-color 0.1s ease;
+`;
+
+const RowCenter = styled.div({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+});
+
+const InfoContainer = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ gap: ${(props) => props.theme.space.xxxs};
+`;
+
+const SubtitleContainer = styled.div((props) => ({
+ marginTop: props.theme.space.xxs,
+}));
+
+const CheckBoxContainer = styled.div((props) => ({
+ marginRight: props.theme.space.s,
+}));
+
+type Props = {
+ runeMarketInfo: {
+ floorPrice: number;
+ marketplace: ListingProvider;
+ };
+ rune: FungibleToken;
+ selected: boolean;
+ onToggle: () => void;
+};
+
+function ListMarketplaceItem({ runeMarketInfo, rune, selected, onToggle }: Props) {
+ const { t } = useTranslation('translation', { keyPrefix: 'LIST_RUNE_SCREEN' });
+ const { floorPrice: rawFloorPrice, marketplace } = runeMarketInfo;
+ const floorPrice = formatNumber(rawFloorPrice);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {marketplace.name}
+
+
+
+
+
+ {t('FLOOR_PRICE', {
+ floor_price: floorPrice,
+ symbol: rune.runeSymbol || rune.name,
+ })}
+
+
+
+
+
+ );
+}
+
+export default ListMarketplaceItem;
diff --git a/src/app/screens/listRune/reducer.tsx b/src/app/screens/listRune/reducer.tsx
index 3867f8d45..7796ef994 100644
--- a/src/app/screens/listRune/reducer.tsx
+++ b/src/app/screens/listRune/reducer.tsx
@@ -1,13 +1,18 @@
import type { RuneItem } from '@utils/runes';
+type SupportedSections = 'SELECT_RUNES' | 'SELECT_MARKETPLACES' | 'SET_PRICES';
+
interface ListRunesState {
listItemsMap: Record;
- section: 'SELECT_RUNES' | 'SET_PRICES';
+ section: SupportedSections;
selectAllToggle: boolean;
runePriceOption: 1 | 1.05 | 1.1 | 1.2 | 'custom';
customRunePrice: number | null;
individualCustomItem: string | null;
- toggleCustomPriceModal: boolean;
+ toggleCustomPriceModal: {
+ title: string;
+ visible: boolean;
+ };
}
type Action =
@@ -15,12 +20,12 @@ type Action =
| { type: 'TOGGLE_ALL_LIST_ITEMS'; payload: boolean }
| { type: 'SET_ALL_LIST_ITEMS_PRICES'; payload: number }
| { type: 'UPDATE_ONE_LIST_ITEM'; key: string; payload: RuneItem }
- | { type: 'SET_SECTION'; payload: 'SELECT_RUNES' | 'SET_PRICES' }
+ | { type: 'SET_SECTION'; payload: SupportedSections }
| { type: 'SET_SELECT_ALL_TOGGLE'; payload: boolean }
| { type: 'SET_RUNE_PRICE_OPTION'; payload: 1 | 1.05 | 1.1 | 1.2 | 'custom' }
| { type: 'SET_CUSTOM_RUNE_PRICE'; payload: number | null }
| { type: 'SET_INDIVIDUAL_CUSTOM_ITEM'; payload: string | null }
- | { type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL'; payload: boolean }
+ | { type: 'SET_TOGGLE_CUSTOM_PRICE_MODAL'; payload: { title: string; visible: boolean } }
| { type: 'RESTORE_STATE_FROM_PSBT'; payload: ListRunesState };
export const initialListRunesState: ListRunesState = {
@@ -30,7 +35,10 @@ export const initialListRunesState: ListRunesState = {
runePriceOption: 1,
customRunePrice: null,
individualCustomItem: null,
- toggleCustomPriceModal: false,
+ toggleCustomPriceModal: {
+ title: '',
+ visible: false,
+ },
};
export function ListRunesReducer(state: ListRunesState, action: Action): ListRunesState {
diff --git a/src/app/screens/listRune/setCustomPriceModal.tsx b/src/app/screens/listRune/setCustomPriceModal.tsx
index 09e1853ee..bbd8f094d 100644
--- a/src/app/screens/listRune/setCustomPriceModal.tsx
+++ b/src/app/screens/listRune/setCustomPriceModal.tsx
@@ -4,7 +4,7 @@ import { StyledP } from '@ui-library/common.styled';
import Input from '@ui-library/input';
import Sheet from '@ui-library/sheet';
import { formatToXDecimalPlaces } from '@utils/helper';
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
@@ -61,6 +61,17 @@ function SetCustomPriceModal({
const lowError: boolean = priceSats.length !== 0 && Number(priceSats) < minPriceSats;
const highError: boolean = priceSats.length !== 0 && Number(priceSats) > maxPriceSats;
+ const inputFeedback = useMemo(() => {
+ if (lowError || highError) {
+ return [
+ {
+ variant: 'danger' as const,
+ message: '',
+ },
+ ];
+ }
+ }, [highError, lowError]);
+
return (
}
+ feedback={inputFeedback}
hideClear
autoFocus
/>
diff --git a/src/app/screens/listRune/setRunePriceItem.tsx b/src/app/screens/listRune/setRunePriceItem.tsx
index 34e94c65f..67c301c1b 100644
--- a/src/app/screens/listRune/setRunePriceItem.tsx
+++ b/src/app/screens/listRune/setRunePriceItem.tsx
@@ -1,4 +1,4 @@
-import useCoinRates from '@hooks/queries/useCoinRates';
+import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates';
import useWalletSelector from '@hooks/useWalletSelector';
import { PencilSimple } from '@phosphor-icons/react';
import FloorComparisonLabel from '@screens/listRune/floorComparisonLabel';
@@ -61,6 +61,11 @@ const InfoRowContainer = styled.div`
gap: ${(props) => props.theme.space.xs};
`;
+const RightAlignContainer = styled.div(() => ({
+ display: 'flex',
+ justifyContent: 'end',
+}));
+
type Props = {
runeAmount: number;
runeSymbol: string;
@@ -79,7 +84,7 @@ function SetRunePriceItem({
handleShowCustomPriceModal,
}: Props) {
const { t } = useTranslation('translation');
- const { btcFiatRate } = useCoinRates();
+ const { btcFiatRate } = useSupportedCoinRates();
const { fiatCurrency } = useWalletSelector();
return (
@@ -150,13 +155,15 @@ function SetRunePriceItem({
- 1000000000}
- typography="body_medium_s"
- />
+
+ 1000000000}
+ typography="body_medium_s"
+ />
+
>
);
}
diff --git a/src/app/screens/login/index.tsx b/src/app/screens/login/index.tsx
index a65047dfb..1cc685dce 100644
--- a/src/app/screens/login/index.tsx
+++ b/src/app/screens/login/index.tsx
@@ -1,7 +1,6 @@
import Eye from '@assets/img/createPassword/Eye.svg';
import EyeSlash from '@assets/img/createPassword/EyeSlash.svg';
import logo from '@assets/img/xverse_logo.svg';
-import useSanityCheck from '@hooks/useSanityCheck';
import useSeedVault from '@hooks/useSeedVault';
import useSeedVaultMigration from '@hooks/useSeedVaultMigration';
import useWalletReducer from '@hooks/useWalletReducer';
@@ -46,7 +45,7 @@ const AppVersion = styled.p((props) => ({
...props.theme.typography.body_s,
color: props.theme.colors.white_0,
textAlign: 'right',
- marginTop: props.theme.spacing(8),
+ marginTop: props.theme.space.m,
}));
const TopSectionContainer = styled.div((props) => ({
@@ -69,10 +68,10 @@ const PasswordInputContainer = styled.div((props) => ({
alignItems: 'center',
width: '100%',
border: `1px solid ${props.theme.colors.elevation3}`,
- paddingLeft: props.theme.spacing(8),
- paddingRight: props.theme.spacing(8),
+ paddingLeft: props.theme.space.m,
+ paddingRight: props.theme.space.m,
borderRadius: props.theme.radius(1),
- marginTop: props.theme.spacing(4),
+ marginTop: props.theme.space.xs,
}));
const PasswordInput = styled.input((props) => ({
@@ -94,7 +93,7 @@ const LandingTitle = styled.h1((props) => ({
}));
const ButtonContainer = styled.div((props) => ({
- marginTop: props.theme.spacing(8),
+ marginTop: props.theme.space.m,
width: '100%',
}));
@@ -102,15 +101,19 @@ const ErrorMessage = styled.h2((props) => ({
...props.theme.typography.body_medium_m,
textAlign: 'left',
color: props.theme.colors.feedback.error,
- marginTop: props.theme.spacing(4),
+ marginTop: props.theme.space.xs,
}));
-const ForgotPasswordButton = styled.a((props) => ({
+const ForgotPasswordButton = styled.button((props) => ({
...props.theme.typography.body_m,
textAlign: 'center',
- marginTop: props.theme.spacing(12),
+ marginTop: props.theme.space.l,
color: props.theme.colors.white_0,
textDecoration: 'underline',
+ backgroundColor: 'transparent',
+ ':hover': {
+ textDecoration: 'none',
+ },
}));
function Login(): JSX.Element {
@@ -118,13 +121,13 @@ function Login(): JSX.Element {
const navigate = useNavigate();
const { unlockWallet } = useWalletReducer();
const { hasSeed } = useSeedVault();
- const { getSanityCheck } = useSanityCheck();
const { migrateCachedStorage, isVaultUpdated } = useSeedVaultMigration();
- const [isPasswordVisible, setIsPasswordVisible] = useState(false);
- const [password, setPassword] = useState('');
- const [error, setError] = useState('');
+ const [isPasswordVisible, setIsPasswordVisible] = useState(false);
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
const [isVerifying, setIsVerifying] = useState(false);
const [showMigration, setShowMigration] = useState(false);
+
const styles = useSpring({
from: {
opacity: 0,
@@ -168,8 +171,7 @@ function Login(): JSX.Element {
// Check for SeedVault Migrations
try {
const hasMigrated = await isVaultUpdated();
- const sanityCheck = await getSanityCheck('X-Current-Version');
- if (!hasMigrated && sanityCheck) {
+ if (!hasMigrated) {
setShowMigration(true);
} else {
setIsVerifying(false);
@@ -194,7 +196,7 @@ function Login(): JSX.Element {
useEffect(() => {
const keyDownHandler = async (event) => {
- if (event.key === 'Enter') {
+ if (event.key === 'Enter' && !!password && document.activeElement?.id === 'password-input') {
event.preventDefault();
await handleVerifyPassword();
}
@@ -214,7 +216,7 @@ function Login(): JSX.Element {
<>
{!showMigration ? (
- Beta
+ {t('BETA_VERSION')}
@@ -223,6 +225,7 @@ function Login(): JSX.Element {
{t('PASSWORD_INPUT_LABEL')}
- checked ? '#FFFFFF' : 'rgba(255, 255, 255, 0.2)'} !important;
- border: ${({ checked }) => (checked ? '' : '4px solid rgba(255, 255, 255, 0.2)')} !important;
- }
-`;
-
const CoinTitleText = styled.p<{ isEnabled?: boolean }>((props) => ({
...props.theme.typography[props.isEnabled ? 'body_bold_m' : 'body_m'],
color: props.theme.colors[props.isEnabled ? 'white_0' : 'white_400'],
@@ -91,15 +82,7 @@ function CoinItem({
{name}
-
+
);
}
diff --git a/src/app/screens/manageTokens/index.tsx b/src/app/screens/manageTokens/index.tsx
index f9356edd9..54072b2d5 100644
--- a/src/app/screens/manageTokens/index.tsx
+++ b/src/app/screens/manageTokens/index.tsx
@@ -1,21 +1,18 @@
import stacksIcon from '@assets/img/dashboard/stx_icon.svg';
-import runesComingSoon from '@assets/img/manageTokens/runes_coming_soon.svg';
import OptionsDialog from '@components/optionsDialog/optionsDialog';
import BottomBar from '@components/tabBar';
import TopRow from '@components/topRow';
import { useGetBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens';
import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery';
import { useGetSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens';
-import useHasFeature from '@hooks/useHasFeature';
import useSelectedAccount from '@hooks/useSelectedAccount';
import useWalletReducer from '@hooks/useWalletReducer';
import useWalletSelector from '@hooks/useWalletSelector';
import { Eye, EyeSlash } from '@phosphor-icons/react';
import CoinItem from '@screens/manageTokens/coinItem';
import {
- FeatureId,
- type FungibleToken,
type FungibleTokenProtocol,
+ type FungibleTokenWithStates,
} from '@secretkeylabs/xverse-core';
import {
setBrc20ManageTokensAction,
@@ -25,7 +22,6 @@ import {
} from '@stores/wallet/actions/actionCreators';
import { StyledP } from '@ui-library/common.styled';
import { SPAM_OPTIONS_WIDTH } from '@utils/constants';
-import BigNumber from 'bignumber.js';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
@@ -97,14 +93,6 @@ const Description = styled.h1((props) => ({
marginBottom: props.theme.spacing(16),
}));
-const RunesContainer = styled.div((props) => ({
- marginTop: props.theme.spacing(24),
- marginRight: props.theme.spacing(5),
- display: 'flex',
- flexDirection: 'row',
- justifyContent: 'center',
-}));
-
const ErrorsText = styled.p((props) => ({
...props.theme.typography.body_bold_m,
color: props.theme.colors.white_200,
@@ -113,10 +101,6 @@ const ErrorsText = styled.p((props) => ({
textAlign: 'center',
}));
-const RunesComingSoon = styled.img({
- width: '70%',
-});
-
const ButtonRow = styled.button`
display: flex;
align-items: center;
@@ -161,16 +145,20 @@ function ManageTokens() {
const { t } = useTranslation('translation', { keyPrefix: 'TOKEN_SCREEN' });
const selectedAccount = useSelectedAccount();
- const { sip10ManageTokens, brc20ManageTokens, runesManageTokens, showSpamTokens } =
- useWalletSelector();
- const { data: runesList, isError: runeError } = useRuneFungibleTokensQuery();
- const { data: sip10List, isError: sip10Error } = useGetSip10FungibleTokens();
- const { data: brc20List, isError: brc20Error } = useGetBrc20FungibleTokens();
+ const { showSpamTokens } = useWalletSelector();
+ const { data: runesList, isError: runeError } = useRuneFungibleTokensQuery((data) =>
+ data.filter((ft) => ft.showToggle),
+ );
+ const { data: sip10List, isError: sip10Error } = useGetSip10FungibleTokens((data) =>
+ data.filter((ft) => ft.showToggle),
+ );
+ const { data: brc20List, isError: brc20Error } = useGetBrc20FungibleTokens((data) =>
+ data.filter((ft) => ft.showToggle),
+ );
const [selectedProtocol, setSelectedProtocol] = useState(
selectedAccount?.stxAddress ? 'stacks' : 'brc-20',
);
- const showRunes = useHasFeature(FeatureId.RUNES_SUPPORT);
const [showOptionsDialog, setShowOptionsDialog] = useState(false);
const [optionsDialogIndents, setOptionsDialogIndents] = useState<
@@ -193,9 +181,9 @@ function ManageTokens() {
};
const toggled = (isEnabled: boolean, _coinName: string, coinKey: string) => {
- const runeFt = runesList?.find((ft) => ft.principal === coinKey);
- const sip10Ft = sip10List?.find((ft) => ft.principal === coinKey);
- const brc20Ft = brc20List?.find((ft) => ft.principal === coinKey);
+ const runeFt = runesList?.find((ft: FungibleTokenWithStates) => ft.principal === coinKey);
+ const sip10Ft = sip10List?.find((ft: FungibleTokenWithStates) => ft.principal === coinKey);
+ const brc20Ft = brc20List?.find((ft: FungibleTokenWithStates) => ft.principal === coinKey);
const payload = { principal: coinKey, isEnabled };
@@ -211,29 +199,19 @@ function ManageTokens() {
const handleBackButtonClick = () => navigate('/');
const getCoinsList = () => {
- let coins: FungibleToken[];
+ let coins: FungibleTokenWithStates[];
let error: boolean;
switch (selectedProtocol) {
case 'stacks':
- coins = (sip10List ?? []).map((ft) => ({
- ...ft,
- visible:
- sip10ManageTokens[ft.principal] ?? (ft.supported && new BigNumber(ft.balance).gt(0)),
- }));
+ coins = sip10List ?? [];
error = sip10Error;
break;
case 'brc-20':
- coins = (brc20List ?? []).map((ft) => ({
- ...ft,
- visible: brc20ManageTokens[ft.principal] ?? new BigNumber(ft.balance).gt(0),
- }));
+ coins = brc20List ?? [];
error = brc20Error;
break;
case 'runes':
- coins = (runesList ?? []).map((ft) => ({
- ...ft,
- visible: runesManageTokens[ft.principal] ?? new BigNumber(ft.balance).gt(0),
- }));
+ coins = runesList ?? [];
error = runeError;
break;
default:
@@ -246,7 +224,7 @@ function ManageTokens() {
return (
<>
{selectedProtocol === 'stacks' && }
- {coins.map((coin: FungibleToken) => (
+ {coins.map((coin) => (
))}
{!coins.length && {t('NO_COINS')}}
@@ -316,15 +294,7 @@ function ManageTokens() {
RUNES
-
- {selectedProtocol === 'runes' && !showRunes ? (
-
-
-
- ) : (
- getCoinsList()
- )}
-
+ {getCoinsList()}
diff --git a/src/app/screens/mintRune/index.tsx b/src/app/screens/mintRune/index.tsx
index 69c43d161..5d118d152 100644
--- a/src/app/screens/mintRune/index.tsx
+++ b/src/app/screens/mintRune/index.tsx
@@ -1,6 +1,5 @@
import ConfirmBtcTransaction from '@components/confirmBtcTransaction';
import RequestError from '@components/requests/requestError';
-import useSelectedAccount from '@hooks/useSelectedAccount';
import { RUNE_DISPLAY_DEFAULTS, type Transport } from '@secretkeylabs/xverse-core';
import Spinner from '@ui-library/spinner';
import { useCallback, useEffect } from 'react';
@@ -18,7 +17,6 @@ const LoaderContainer = styled.div(() => ({
function MintRune() {
const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' });
- const { btcAddress, ordinalsAddress } = useSelectedAccount();
const navigate = useNavigate();
const {
mintRequest,
@@ -70,28 +68,18 @@ function MintRune() {
{orderTx && orderTx.summary && runeInfo && !mintError && (
- input.extendedUtxo.address !== btcAddress &&
- input.extendedUtxo.address !== ordinalsAddress,
- ),
- receipts: [],
- transfers: [],
- mint: {
- runeName: runeInfo.entry.spaced_rune,
- amount: BigInt(runeInfo.entry.terms.amount?.toNumber() ?? 0),
- divisibility: runeInfo.entry.divisibility.toNumber(),
- symbol: runeInfo.entry.symbol,
- inscriptionId: runeInfo.parent ?? '',
- runeIsOpen: runeInfo.mintable,
- runeIsMintable: runeInfo.mintable,
- destinationAddress: mintRequest.destinationAddress,
- repeats: mintRequest.repeats,
- runeSize: RUNE_DISPLAY_DEFAULTS.size,
- },
+ runeMintDetails={{
+ runeId: runeInfo.id,
+ runeName: runeInfo.entry.spaced_rune,
+ amount: BigInt(runeInfo.entry.terms.amount?.toNumber() ?? 0),
+ divisibility: runeInfo.entry.divisibility.toNumber(),
+ symbol: runeInfo.entry.symbol,
+ inscriptionId: runeInfo.parent ?? '',
+ runeIsOpen: runeInfo.mintable,
+ runeIsMintable: runeInfo.mintable,
+ destinationAddress: mintRequest.destinationAddress,
+ repeats: mintRequest.repeats,
+ runeSize: RUNE_DISPLAY_DEFAULTS.size,
}}
feeRate={+feeRate}
confirmText={t('CONFIRM')}
diff --git a/src/app/screens/mintRune/useMintRequest.ts b/src/app/screens/mintRune/useMintRequest.ts
index 52c203847..0c5a2266c 100644
--- a/src/app/screens/mintRune/useMintRequest.ts
+++ b/src/app/screens/mintRune/useMintRequest.ts
@@ -1,4 +1,5 @@
import { makeRPCError, makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers';
+import useBtcClient from '@hooks/apiClients/useBtcClient';
import useOrdinalsServiceApi from '@hooks/apiClients/useOrdinalsServiceApi';
import useRunesApi from '@hooks/apiClients/useRunesApi';
import useTransactionContext from '@hooks/useTransactionContext';
@@ -40,6 +41,7 @@ const useMintRequest = (): {
const txContext = useTransactionContext();
const ordinalsServiceApi = useOrdinalsServiceApi();
const runesApi = useRunesApi();
+ const btcClient = useBtcClient();
const [mintError, setMintError] = useState<{
code: number | undefined;
@@ -139,11 +141,8 @@ const useMintRequest = (): {
const payAndConfirmMintRequest = async (ledgerTransport?: Transport) => {
try {
setIsExecuting(true);
- const txid = await orderTx?.transaction.broadcast({
- ledgerTransport,
- rbfEnabled: false,
- });
- if (!txid) {
+
+ if (!orderTx) {
const response = makeRPCError(requestId, {
code: RpcErrorCode.INTERNAL_ERROR,
message: 'Failed to broadcast transaction',
@@ -151,6 +150,21 @@ const useMintRequest = (): {
sendRpcResponse(+tabId, response);
return;
}
+
+ // TODO: make enhancedTransaction class use a passed in btcClient and use:
+ /*
+ orderTx.transaction.broadcast({
+ ledgerTransport,
+ rbfEnabled: false,
+ });
+ */
+
+ const { hex: transactionHex, id: txid } = await orderTx.transaction.getTransactionHexAndId({
+ ledgerTransport,
+ rbfEnabled: false,
+ });
+ await btcClient.sendRawTransaction(transactionHex);
+
await ordinalsServiceApi.executeMint(orderId, txid);
const mintRequestResponse = makeRpcSuccessResponse<'runes_mint'>(requestId, {
fundingAddress: txContext.paymentAddress.address,
diff --git a/src/app/screens/nftCollection/index.styled.ts b/src/app/screens/nftCollection/index.styled.ts
new file mode 100644
index 000000000..552e0c923
--- /dev/null
+++ b/src/app/screens/nftCollection/index.styled.ts
@@ -0,0 +1,99 @@
+import { GridContainer } from '@screens/nftDashboard/collectiblesTabs/index.styled';
+import styled from 'styled-components';
+
+interface Props {
+ $isGalleryOpen?: boolean;
+}
+
+export const Container = styled.div((props) => ({
+ ...props.theme.scrollbar,
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+}));
+
+export const NoCollectiblesText = styled.p((props) => ({
+ ...props.theme.typography.body_bold_m,
+ color: props.theme.colors.white_200,
+ marginTop: props.theme.space.xl,
+ marginBottom: 'auto',
+ textAlign: 'center',
+}));
+
+export const HeadingText = styled.p((props) => ({
+ ...props.theme.typography.body_bold_m,
+ color: props.theme.colors.white_400,
+}));
+
+export const CollectionText = styled.p((props) => ({
+ ...props.theme.typography.headline_s,
+ color: props.theme.colors.white_0,
+ marginTop: props.theme.space.xxxs,
+ marginBottom: props.theme.space.xs,
+ wordBreak: 'break-word',
+}));
+
+export const BottomBarContainer = styled.div({
+ marginTop: 'auto',
+});
+
+export const PageHeader = styled.div`
+ padding: ${(props) => props.theme.space.xs};
+ padding-top: 0;
+ max-width: 1224px;
+ margin-top: ${(props) => (props.$isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)};
+ margin-left: auto;
+ margin-right: auto;
+ width: 100%;
+`;
+
+export const PageHeaderContent = styled.div`
+ display: flex;
+ flex-direction: ${(props) => (props.$isGalleryOpen ? 'row' : 'column')};
+ justify-content: ${(props) => (props.$isGalleryOpen ? 'space-between' : 'initial')};
+ row-gap: ${(props) => props.theme.space.xl};
+`;
+
+export const NftContainer = styled.div`
+ display: flex;
+ flex-direction: ${(props) => (props.$isGalleryOpen ? 'column' : 'row')};
+ justify-content: ${(props) => (props.$isGalleryOpen ? 'space-between' : 'initial')};
+ column-gap: ${(props) => props.theme.space.m};
+`;
+
+export const BackButtonContainer = styled.div((props) => ({
+ display: 'flex',
+ marginBottom: props.theme.space.xxl,
+}));
+
+export const BackButton = styled.button((props) => ({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ background: 'transparent',
+ marginBottom: props.theme.space.l,
+}));
+
+export const AssetDetailButtonText = styled.div((props) => ({
+ ...props.theme.typography.body_m,
+ marginLeft: props.theme.space.xxxs,
+ color: props.theme.colors.white_0,
+ textAlign: 'center',
+}));
+
+export const StyledGridContainer = styled(GridContainer)`
+ margin-top: ${(props) => props.theme.space.s};
+ padding: 0 ${(props) => props.theme.space.xs};
+ padding-bottom: ${(props) => props.theme.space.xl};
+ max-width: 1224px;
+ margin-left: auto;
+ margin-right: auto;
+ width: 100%;
+`;
+
+export const CollectionNameDiv = styled.div`
+ display: flex;
+ align-items: center;
+ gap: ${(props) => props.theme.space.s};
+`;
diff --git a/src/app/screens/nftCollection/index.tsx b/src/app/screens/nftCollection/index.tsx
index 3d4f8a067..5eb7d96f0 100644
--- a/src/app/screens/nftCollection/index.tsx
+++ b/src/app/screens/nftCollection/index.tsx
@@ -1,121 +1,55 @@
import AccountHeaderComponent from '@components/accountHeader';
import CollectibleCollectionGridItem from '@components/collectibleCollectionGridItem';
import CollectibleDetailTile from '@components/collectibleDetailTile';
+import SquareButton from '@components/squareButton';
import BottomTabBar from '@components/tabBar';
import { StyledBarLoader, TilesSkeletonLoader } from '@components/tilesSkeletonLoader';
import TopRow from '@components/topRow';
import WebGalleryButton from '@components/webGalleryButton';
import WrenchErrorMessage from '@components/wrenchErrorMessage';
import useNftDetail from '@hooks/queries/useNftDetail';
-import { ArrowLeft } from '@phosphor-icons/react';
-import { GridContainer } from '@screens/nftDashboard/collectiblesTabs';
+import useSelectedAccount from '@hooks/useSelectedAccount';
+import useWalletSelector from '@hooks/useWalletSelector';
+import { ArchiveTray, ArrowLeft, DotsThreeVertical, Star } from '@phosphor-icons/react';
import Nft from '@screens/nftDashboard/nft';
import NftImage from '@screens/nftDashboard/nftImage';
+import { StyledButton } from '@screens/ordinalsCollection/index.styled';
import type { NonFungibleToken, StacksCollectionData } from '@secretkeylabs/xverse-core';
-import { EMPTY_LABEL } from '@utils/constants';
+import {
+ addToHideCollectiblesAction,
+ addToStarCollectiblesAction,
+ removeAccountAvatarAction,
+ removeFromHideCollectiblesAction,
+ removeFromStarCollectiblesAction,
+} from '@stores/wallet/actions/actionCreators';
+import Sheet from '@ui-library/sheet';
+import SnackBar from '@ui-library/snackBar';
+import { EMPTY_LABEL, LONG_TOAST_DURATION } from '@utils/constants';
import { getFullyQualifiedKey, getNftCollectionsGridItemId, isBnsCollection } from '@utils/nfts';
-import { useRef, type PropsWithChildren } from 'react';
+import { useRef, useState, type PropsWithChildren } from 'react';
import toast from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useIsVisible } from 'react-is-visible';
+import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
-import styled from 'styled-components';
+import Theme from 'theme';
+import {
+ AssetDetailButtonText,
+ BackButton,
+ BackButtonContainer,
+ BottomBarContainer,
+ CollectionNameDiv,
+ CollectionText,
+ Container,
+ HeadingText,
+ NftContainer,
+ NoCollectiblesText,
+ PageHeader,
+ PageHeaderContent,
+ StyledGridContainer,
+} from './index.styled';
import useNftCollection from './useNftCollection';
-interface Props {
- isGalleryOpen?: boolean;
-}
-const Container = styled.div((props) => ({
- ...props.theme.scrollbar,
- display: 'flex',
- flexDirection: 'column',
- flex: 1,
-}));
-
-const NoCollectiblesText = styled.p((props) => ({
- ...props.theme.typography.body_bold_m,
- color: props.theme.colors.white_200,
- marginTop: props.theme.spacing(16),
- marginBottom: 'auto',
- textAlign: 'center',
-}));
-
-const HeadingText = styled.p((props) => ({
- ...props.theme.typography.body_bold_m,
- color: props.theme.colors.white_400,
-}));
-
-const CollectionText = styled.p((props) => ({
- ...props.theme.typography.headline_s,
- color: props.theme.colors.white_0,
- marginTop: props.theme.spacing(1),
- marginBottom: props.theme.spacing(4),
- wordBreak: 'break-word',
-}));
-
-const BottomBarContainer = styled.div({
- marginTop: 'auto',
-});
-
-const PageHeader = styled.div`
- padding: ${(props) => props.theme.space.xs};
- padding-top: 0;
- max-width: 1224px;
- margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)};
- margin-left: auto;
- margin-right: auto;
- width: 100%;
-`;
-
-const PageHeaderContent = styled.div`
- display: flex;
- flex-direction: ${(props) => (props.isGalleryOpen ? 'row' : 'column')};
- justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')};
- row-gap: ${(props) => props.theme.space.xl};
-`;
-
-const NftContainer = styled.div`
- display: flex;
- flex-direction: ${(props) => (props.isGalleryOpen ? 'column' : 'row')};
- justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')};
- column-gap: ${(props) => props.theme.space.m};
-`;
-
-const BackButtonContainer = styled.div((props) => ({
- display: 'flex',
- flexDirection: 'row',
- width: 800,
- marginTop: props.theme.spacing(40),
-}));
-
-const BackButton = styled.button((props) => ({
- display: 'flex',
- flexDirection: 'row',
- justifyContent: 'flex-start',
- alignItems: 'center',
- background: 'transparent',
- marginBottom: props.theme.spacing(12),
-}));
-
-const AssetDetailButtonText = styled.div((props) => ({
- ...props.theme.typography.body_m,
- fontWeight: 400,
- fontSize: 14,
- marginLeft: 2,
- color: props.theme.colors.white_0,
- textAlign: 'center',
-}));
-
-const StyledGridContainer = styled(GridContainer)`
- margin-top: ${(props) => props.theme.space.s};
- padding: 0 ${(props) => props.theme.space.xs};
- padding-bottom: ${(props) => props.theme.space.xl};
- max-width: 1224px;
- margin-left: auto;
- margin-right: auto;
- width: 100%;
-`;
-
/*
* component to virtualise the grid item if not in window
* placeholder is required to match grid item size, in order to negate scroll jank
@@ -176,6 +110,12 @@ function CollectionGridItemWithData({
function NftCollection() {
const { t } = useTranslation('translation', { keyPrefix: 'COLLECTIBLE_COLLECTION_SCREEN' });
+ const { t: commonT } = useTranslation('translation', { keyPrefix: 'COMMON' });
+ const selectedAccount = useSelectedAccount();
+ const { starredCollectibleIds, hiddenCollectibleIds, avatarIds } = useWalletSelector();
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const [isOptionsModalVisible, setIsOptionsModalVisible] = useState(false);
const {
collectionData,
portfolioValue,
@@ -186,37 +126,193 @@ function NftCollection() {
handleBackButtonClick,
openInGalleryView,
} = useNftCollection();
+ const currentAvatar = avatarIds[selectedAccount.btcAddress];
+
+ const openOptionsDialog = () => {
+ setIsOptionsModalVisible(true);
+ };
+
+ const closeOptionsDialog = () => {
+ setIsOptionsModalVisible(false);
+ };
+
+ const collectionStarred = starredCollectibleIds[selectedAccount.stxAddress]?.some(
+ ({ id }) => id === collectionData?.collection_id,
+ );
+ const collectionHidden = Object.keys(hiddenCollectibleIds[selectedAccount.stxAddress] ?? {}).some(
+ (id) => id === collectionData?.collection_id,
+ );
+
+ const handleUnHideCollection = () => {
+ const isLastHiddenItem =
+ Object.keys(hiddenCollectibleIds[selectedAccount.stxAddress] ?? {}).length === 1;
+ dispatch(
+ removeFromHideCollectiblesAction({
+ address: selectedAccount.stxAddress,
+ id: collectionData?.collection_id ?? '',
+ }),
+ );
+ closeOptionsDialog();
+ toast.custom();
+ navigate(`/nft-dashboard/${isLastHiddenItem ? '' : 'hidden'}?tab=nfts`);
+ };
+
+ const handleClickUndoHiding = (toastId: string) => {
+ dispatch(
+ removeFromHideCollectiblesAction({
+ address: selectedAccount.stxAddress,
+ id: collectionData?.collection_id ?? '',
+ }),
+ );
+ toast.remove(toastId);
+ toast.custom(, {
+ duration: LONG_TOAST_DURATION,
+ });
+ };
+
+ const handleHideCollection = () => {
+ dispatch(
+ addToHideCollectiblesAction({
+ address: selectedAccount.stxAddress,
+ id: collectionData?.collection_id ?? '',
+ }),
+ );
+
+ if (currentAvatar?.type === 'stacks') {
+ const isHidingUsedAvatar = collectionData?.all_nfts.some(
+ (nft) =>
+ `${nft.asset_identifier}:${nft.identifier.tokenId}` ===
+ currentAvatar.nft.fully_qualified_token_id,
+ );
+
+ if (isHidingUsedAvatar) {
+ dispatch(removeAccountAvatarAction({ address: selectedAccount.btcAddress }));
+ }
+ }
+
+ closeOptionsDialog();
+ navigate('/nft-dashboard?tab=nfts');
+ const toastId = toast.custom(
+ handleClickUndoHiding(toastId),
+ }}
+ />,
+ { duration: LONG_TOAST_DURATION },
+ );
+ };
+
+ const handleClickUndoStarring = (toastId: string) => {
+ dispatch(
+ removeFromStarCollectiblesAction({
+ address: selectedAccount.stxAddress,
+ id: collectionData?.collection_id ?? '',
+ }),
+ );
+ toast.remove(toastId);
+ toast.custom();
+ };
+
+ const handleStarClick = () => {
+ if (collectionStarred) {
+ dispatch(
+ removeFromStarCollectiblesAction({
+ address: selectedAccount.stxAddress,
+ id: collectionData?.collection_id ?? '',
+ }),
+ );
+ toast.custom(, {
+ duration: LONG_TOAST_DURATION,
+ });
+ } else {
+ dispatch(
+ addToStarCollectiblesAction({
+ address: selectedAccount.stxAddress,
+ id: collectionData?.collection_id ?? '',
+ }),
+ );
+ const toastId = toast.custom(
+ handleClickUndoStarring(toastId),
+ }}
+ />,
+ { duration: LONG_TOAST_DURATION },
+ );
+ }
+ };
return (
<>
{isGalleryOpen ? (
) : (
-
+
)}
-
+
{isGalleryOpen && (
<>
- {t('BACK_TO_GALLERY')}
+ {t(collectionHidden ? 'BACK_TO_HIDDEN_COLLECTIBLES' : 'BACK_TO_GALLERY')}
>
)}
-
+
{t('COLLECTION')}
-
- {collectionData?.collection_name || }
-
+
+
+ {collectionData?.collection_name || }
+
+ {isGalleryOpen && (
+ <>
+ {collectionHidden ? null : (
+
+ ) : (
+
+ )
+ }
+ onPress={handleStarClick}
+ isTransparent
+ size={44}
+ radiusSize={12}
+ />
+ )}
+
+ }
+ onPress={openOptionsDialog}
+ isTransparent
+ size={44}
+ radiusSize={12}
+ />
+ >
+ )}
+
{!isGalleryOpen && }
-
+
{isEmpty && {t('NO_COLLECTIBLES')}}
{!!isError && }
-
+
{isLoading ? (
) : (
- collectionData?.all_nfts
- .sort((a, b) => (a.value.repr > b.value.repr ? 1 : -1))
- .map((nft) => (
-
-
-
- ))
+ collectionData?.all_nfts.map((nft) => (
+
+
+
+ ))
)}
>
@@ -266,6 +360,29 @@ function NftCollection() {
)}
+ {isOptionsModalVisible && (
+
+ {collectionHidden ? (
+ }
+ title={t('UNHIDE_COLLECTION')}
+ onClick={handleUnHideCollection}
+ />
+ ) : (
+ }
+ title={t('HIDE_COLLECTION')}
+ onClick={handleHideCollection}
+ />
+ )}
+
+ )}
>
);
}
diff --git a/src/app/screens/nftCollection/useNftCollection.ts b/src/app/screens/nftCollection/useNftCollection.ts
index 37e64bc07..cf74fa48b 100644
--- a/src/app/screens/nftCollection/useNftCollection.ts
+++ b/src/app/screens/nftCollection/useNftCollection.ts
@@ -1,5 +1,7 @@
import useStacksCollectibles from '@hooks/queries/useStacksCollectibles';
import { useResetUserFlow } from '@hooks/useResetUserFlow';
+import useSelectedAccount from '@hooks/useSelectedAccount';
+import useWalletSelector from '@hooks/useWalletSelector';
import { useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
@@ -7,12 +9,18 @@ export default function useNftCollection() {
const navigate = useNavigate();
useResetUserFlow('/nft-collection');
- const { id: collectionId } = useParams();
- const { data, isLoading, error } = useStacksCollectibles();
+ const { id: collectionId, from } = useParams();
+ const comesFromHidden = from === 'hidden';
+ const { data, isLoading, error } = useStacksCollectibles(comesFromHidden);
+ const { hiddenCollectibleIds } = useWalletSelector();
+ const { stxAddress } = useSelectedAccount();
const collectionData = data?.results.find(
(collection) => collection.collection_id === collectionId,
);
+ const collectionHidden = Object.keys(hiddenCollectibleIds[stxAddress] ?? {}).some(
+ (id) => id === collectionId,
+ );
const portfolioValue =
collectionData?.floor_price && !Number.isNaN(collectionData?.all_nfts?.length)
@@ -21,13 +29,16 @@ export default function useNftCollection() {
const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []);
- const handleBackButtonClick = () => {
- navigate('/nft-dashboard?tab=nfts');
- };
+ const handleBackButtonClick = () =>
+ navigate(`/nft-dashboard${comesFromHidden || collectionHidden ? '/hidden' : ''}?tab=nfts`);
const openInGalleryView = async () => {
await chrome.tabs.create({
- url: chrome.runtime.getURL(`options.html#/nft-dashboard/nft-collection/${collectionId}`),
+ url: chrome.runtime.getURL(
+ `options.html#/nft-dashboard/nft-collection/${collectionId}${
+ comesFromHidden || collectionHidden ? '/hidden' : ''
+ }`,
+ ),
});
};
diff --git a/src/app/screens/nftDashboard/collectiblesTabs/index.styled.ts b/src/app/screens/nftDashboard/collectiblesTabs/index.styled.ts
new file mode 100644
index 000000000..ec0bd8425
--- /dev/null
+++ b/src/app/screens/nftDashboard/collectiblesTabs/index.styled.ts
@@ -0,0 +1,103 @@
+import WrenchErrorMessage from '@components/wrenchErrorMessage';
+import Button from '@ui-library/button';
+import { StyledP, StyledTabList } from '@ui-library/common.styled';
+import styled from 'styled-components';
+
+export const GridContainer = styled.div<{
+ $isGalleryOpen: boolean;
+}>((props) => ({
+ display: 'grid',
+ columnGap: props.$isGalleryOpen ? props.theme.space.xl : props.theme.space.m,
+ rowGap: props.$isGalleryOpen ? props.theme.space.xl : props.theme.space.l,
+ marginTop: props.theme.space.l,
+ gridTemplateColumns: props.$isGalleryOpen
+ ? 'repeat(auto-fill,minmax(220px,1fr))'
+ : 'repeat(auto-fill,minmax(150px,1fr))',
+}));
+
+export const RareSatsTabContainer = styled.div((props) => ({
+ marginTop: props.theme.space.l,
+}));
+
+export const StickyStyledTabList = styled(StyledTabList)`
+ position: sticky;
+ background: ${(props) => props.theme.colors.elevation0};
+ top: -1px;
+ z-index: 1;
+ padding: ${(props) => props.theme.space.m} 0;
+`;
+
+export const StyledTotalItems = styled(StyledP)`
+ margin-top: ${(props) => props.theme.space.s};
+`;
+
+export const NoticeContainer = styled.div((props) => ({
+ marginTop: props.theme.space.m,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+}));
+
+export const StyledWrenchErrorMessage = styled(WrenchErrorMessage)`
+ margin-top: ${(props) => props.theme.space.xxl};
+`;
+
+export const NoCollectiblesText = styled.div((props) => ({
+ ...props.theme.typography.body_bold_m,
+ color: props.theme.colors.white_200,
+ marginTop: props.theme.space.xl,
+ textAlign: 'center',
+}));
+
+export const LoadMoreButtonContainer = styled.div((props) => ({
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: props.theme.spacing(30),
+ marginTop: props.theme.space.xl,
+ button: {
+ width: 156,
+ },
+}));
+
+export const LoaderContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+export const CountLoaderContainer = styled.div((props) => ({
+ marginTop: props.theme.space.s,
+ marginBottom: props.theme.space.l,
+}));
+
+export const StyledButton = styled(Button)`
+ &.tertiary {
+ color: ${(props) => props.theme.colors.white_200};
+ padding: 0;
+ width: auto;
+ min-height: 20px;
+
+ &:hover:enabled {
+ opacity: 0.8;
+ }
+ }
+`;
+
+export const StyledSheetButton = styled(Button)`
+ &.tertiary {
+ justify-content: flex-start;
+ color: ${(props) => props.theme.colors.white_200};
+ padding-left: 0;
+
+ &:hover:enabled {
+ opacity: 0.8;
+ }
+ }
+`;
+
+export const TopBarContainer = styled.div((props) => ({
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginTop: props.theme.space.s,
+}));
diff --git a/src/app/screens/nftDashboard/collectiblesTabs.tsx b/src/app/screens/nftDashboard/collectiblesTabs/index.tsx
similarity index 55%
rename from src/app/screens/nftDashboard/collectiblesTabs.tsx
rename to src/app/screens/nftDashboard/collectiblesTabs/index.tsx
index 41af28166..4a8eec173 100644
--- a/src/app/screens/nftDashboard/collectiblesTabs.tsx
+++ b/src/app/screens/nftDashboard/collectiblesTabs/index.tsx
@@ -1,105 +1,39 @@
-import ActionButton from '@components/button';
-import { StyledBarLoader, TilesSkeletonLoader } from '@components/tilesSkeletonLoader';
-import WrenchErrorMessage from '@components/wrenchErrorMessage';
import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed';
+import { ArchiveTray } from '@phosphor-icons/react';
import { mapRareSatsAPIResponseToBundle, type Bundle } from '@secretkeylabs/xverse-core';
-import { StyledP, StyledTab, StyledTabList } from '@ui-library/common.styled';
+import Button from '@ui-library/button';
+import { StyledP } from '@ui-library/common.styled';
+import { TabItem } from '@ui-library/tabs';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { TabPanel, Tabs } from 'react-tabs';
-import styled from 'styled-components';
-import Notice from './notice';
-import RareSatsTabGridItem from './rareSatsTabGridItem';
-import type { NftDashboardState } from './useNftDashboard';
+import Theme from 'theme';
+import Notice from '../notice';
+import RareSatsTabGridItem from '../rareSatsTabGridItem';
+import type { NftDashboardState } from '../useNftDashboard';
+
+import {
+ LoadMoreButtonContainer,
+ NoCollectiblesText,
+ NoticeContainer,
+ RareSatsTabContainer,
+ StickyStyledTabList,
+ StyledButton,
+ StyledTotalItems,
+ StyledWrenchErrorMessage,
+ TopBarContainer,
+} from './index.styled';
+import SkeletonLoader from './skeletonLoader';
const MAX_SATS_ITEMS_EXTENSION = 5;
const MAX_SATS_ITEMS_GALLERY = 20;
-export const GridContainer = styled.div<{
- isGalleryOpen: boolean;
-}>((props) => ({
- display: 'grid',
- columnGap: props.isGalleryOpen ? props.theme.space.xl : props.theme.space.m,
- rowGap: props.isGalleryOpen ? props.theme.space.xl : props.theme.space.l,
- marginTop: props.theme.space.l,
- gridTemplateColumns: props.isGalleryOpen
- ? 'repeat(auto-fill,minmax(220px,1fr))'
- : 'repeat(auto-fill,minmax(150px,1fr))',
-}));
-
-const RareSatsTabContainer = styled.div<{
- isGalleryOpen: boolean;
-}>((props) => ({
- marginTop: props.theme.space.l,
-}));
-
-const StickyStyledTabList = styled(StyledTabList)`
- position: sticky;
- background: ${(props) => props.theme.colors.elevation0};
- top: -1px;
- z-index: 1;
- padding: ${(props) => props.theme.space.m} 0;
-`;
-
-const StyledTotalItems = styled(StyledP)`
- margin-top: ${(props) => props.theme.space.s};
-`;
-
-const NoticeContainer = styled.div((props) => ({
- marginTop: props.theme.spacing(8),
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
-}));
-
-const StyledWrenchErrorMessage = styled(WrenchErrorMessage)`
- margin-top: ${(props) => props.theme.space.xxl};
-`;
-
-const NoCollectiblesText = styled.div((props) => ({
- ...props.theme.typography.body_bold_m,
- color: props.theme.colors.white_200,
- marginTop: props.theme.spacing(16),
- textAlign: 'center',
-}));
-
-const LoadMoreButtonContainer = styled.div((props) => ({
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: props.theme.spacing(30),
- marginTop: props.theme.space.xl,
- button: {
- width: 156,
- },
-}));
-
-const LoaderContainer = styled.div({
- display: 'flex',
- flexDirection: 'column',
-});
-
-const CountLoaderContainer = styled.div((props) => ({
- marginTop: props.theme.spacing(6),
- marginBottom: props.theme.spacing(12),
-}));
-
-function SkeletonLoader({ isGalleryOpen }: { isGalleryOpen: boolean }) {
- return (
-
-
-
-
-
-
- );
-}
-
type TabButton = {
key: string;
label: string;
};
+
const tabs: TabButton[] = [
{
key: 'inscriptions',
@@ -120,21 +54,24 @@ const tabKeyToIndex = (key?: string | null) => {
return tabs.findIndex((tab) => tab.key === key);
};
+type Props = {
+ className?: string;
+ nftListView: React.ReactNode;
+ inscriptionListView: React.ReactNode;
+ nftDashboard: NftDashboardState;
+};
+
export default function CollectiblesTabs({
className,
nftListView,
inscriptionListView,
nftDashboard,
-}: {
- className?: string;
- nftListView: React.ReactNode;
- inscriptionListView: React.ReactNode;
- nftDashboard: NftDashboardState;
-}) {
+}: Props) {
const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' });
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [tabIndex, setTabIndex] = useState(tabKeyToIndex(searchParams?.get('tab')));
+
const {
isGalleryOpen,
rareSatsQuery,
@@ -155,18 +92,15 @@ export default function CollectiblesTabs({
[tabIndex],
);
- const handleSelectTab = (index: number) => {
- setTabIndex(index);
- };
+ const handleSelectTab = (index: number) => setTabIndex(index);
useEffect(() => {
setSearchParams({ tab: tabs[tabIndex]?.key });
- }, [tabIndex, setSearchParams]);
+ }, [tabIndex]);
const ordinalBundleCount = rareSatsQuery?.data?.pages?.[0]?.total || 0;
const showNoBundlesNotice =
ordinalBundleCount === 0 && !rareSatsQuery.isLoading && !rareSatsQuery.error;
-
const visibleTabButtons = tabs.filter((tab: TabButton) => {
if (tab.key === 'rareSats' && !hasActivatedRareSatsKey) {
return false;
@@ -179,33 +113,48 @@ export default function CollectiblesTabs({
return (
+ {/* TODO: replace with Tabs component from `src/app/ui-library/tabs.tsx` */}
{visibleTabButtons.length > 1 && (
{visibleTabButtons.map(({ key, label }) => (
- {t(label)}
+ handleSelectTab(tabKeyToIndex(key))}
+ >
+ {t(label)}
+
))}
)}
{hasActivatedOrdinalsKey && (
- {inscriptionsQuery.isInitialLoading ? (
-
- ) : (
- <>
- {totalInscriptions > 0 && (
-
- {totalInscriptions === 1
- ? t('TOTAL_ITEMS_ONE')
- : t('TOTAL_ITEMS', { count: totalInscriptions })}
-
- )}
- {inscriptionListView}
- >
- )}
+
+ {inscriptionsQuery.isInitialLoading ? (
+
+ ) : (
+ <>
+ {totalInscriptions > 0 && (
+
+
+ {totalInscriptions === 1
+ ? t('TOTAL_ITEMS_ONE')
+ : t('TOTAL_ITEMS', { count: totalInscriptions })}
+
+ }
+ title={t('HIDDEN_COLLECTIBLES')}
+ onClick={() => {
+ navigate(`/nft-dashboard/hidden?tab=${tabs[tabIndex]?.key}`);
+ }}
+ />
+
+ )}
+ {inscriptionListView}
+ >
+ )}
+
)}
@@ -214,13 +163,19 @@ export default function CollectiblesTabs({
) : (
<>
{totalNfts > 0 && (
-
- {totalNfts === 1 ? t('TOTAL_ITEMS_ONE') : t('TOTAL_ITEMS', { count: totalNfts })}
-
+
+
+ {totalNfts === 1 ? t('TOTAL_ITEMS_ONE') : t('TOTAL_ITEMS', { count: totalNfts })}
+
+ }
+ title={t('HIDDEN_COLLECTIBLES')}
+ onClick={() => {
+ navigate('/nft-dashboard/hidden?tab=nfts');
+ }}
+ />
+
)}
{nftListView}
>
@@ -239,7 +194,6 @@ export default function CollectiblesTabs({
: t('TOTAL_ITEMS', { count: ordinalBundleCount })}
)}
-
{!rareSatsQuery.isLoading && showNoticeAlert && (
) : (
-
+
{!rareSatsQuery.error &&
!rareSatsQuery.isLoading &&
rareSatsQuery.data?.pages
@@ -278,12 +232,12 @@ export default function CollectiblesTabs({
)}
{rareSatsQuery.hasNextPage && (
- rareSatsQuery.fetchNextPage()}
+ onClick={() => rareSatsQuery.fetchNextPage()}
/>
)}
diff --git a/src/app/screens/nftDashboard/collectiblesTabs/skeletonLoader.tsx b/src/app/screens/nftDashboard/collectiblesTabs/skeletonLoader.tsx
new file mode 100644
index 000000000..faed4ea84
--- /dev/null
+++ b/src/app/screens/nftDashboard/collectiblesTabs/skeletonLoader.tsx
@@ -0,0 +1,15 @@
+import { StyledBarLoader, TilesSkeletonLoader } from '@components/tilesSkeletonLoader';
+import { CountLoaderContainer, LoaderContainer } from './index.styled';
+
+function SkeletonLoader({ isGalleryOpen }: { isGalleryOpen: boolean }) {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default SkeletonLoader;
diff --git a/src/app/screens/nftDashboard/hidden/index.tsx b/src/app/screens/nftDashboard/hidden/index.tsx
new file mode 100644
index 000000000..abc8942ab
--- /dev/null
+++ b/src/app/screens/nftDashboard/hidden/index.tsx
@@ -0,0 +1,329 @@
+import AccountHeaderComponent from '@components/accountHeader';
+import BottomTabBar from '@components/tabBar';
+import TopRow from '@components/topRow';
+import useSelectedAccount from '@hooks/useSelectedAccount';
+import useWalletSelector from '@hooks/useWalletSelector';
+import { ArrowLeft, TrayArrowUp } from '@phosphor-icons/react';
+import {
+ removeAllFromHideCollectiblesAction,
+ setHiddenCollectiblesAction,
+} from '@stores/wallet/actions/actionCreators';
+import { StyledHeading, StyledP } from '@ui-library/common.styled';
+import Sheet from '@ui-library/sheet';
+import SnackBar from '@ui-library/snackBar';
+import { TabItem } from '@ui-library/tabs';
+import { LONG_TOAST_DURATION } from '@utils/constants';
+import { useState } from 'react';
+import toast from 'react-hot-toast';
+import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+import { TabPanel, Tabs } from 'react-tabs';
+import styled from 'styled-components';
+import Theme from 'theme';
+import {
+ StickyStyledTabList,
+ StyledButton,
+ StyledSheetButton,
+} from '../collectiblesTabs/index.styled';
+import SkeletonLoader from '../collectiblesTabs/skeletonLoader';
+import useNftDashboard from '../useNftDashboard';
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ overflow-y: auto;
+ ${(props) => props.theme.scrollbar}
+`;
+
+const PageHeader = styled.div`
+ padding: ${(props) => props.theme.space.s};
+ padding-bottom: ${(props) => props.theme.space.l};
+ border-bottom: 0.5px solid ${(props) => props.theme.colors.elevation3};
+ max-width: 1224px;
+ margin-left: auto;
+ margin-right: auto;
+ width: 100%;
+`;
+
+const CollectiblesContainer = styled.div`
+ padding: 0 ${(props) => props.theme.space.s};
+ padding-bottom: ${(props) => props.theme.space.xl};
+ max-width: 1224px;
+ margin-left: auto;
+ margin-right: auto;
+ width: 100%;
+`;
+
+const RowCenterSpaceBetween = styled.div<{ addMarginBottom?: boolean }>`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: ${(props) => (props.addMarginBottom ? props.theme.space.m : 0)};
+`;
+
+const BackButton = styled.button`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: ${(props) => props.theme.space.xxs};
+ background: transparent;
+ margin-bottom: ${(props) => props.theme.space.xl};
+ color: ${(props) => props.theme.colors.white_0};
+`;
+
+const ItemCountContainer = styled.div<{ $isGalleryOpen: boolean }>`
+ margin-top: ${(props) => (props.$isGalleryOpen ? props.theme.space.l : props.theme.space.m)};
+`;
+
+type TabButton = {
+ key: string;
+ label: string;
+};
+
+const tabs: TabButton[] = [
+ {
+ key: 'inscriptions',
+ label: 'INSCRIPTIONS',
+ },
+ {
+ key: 'nfts',
+ label: 'NFTS',
+ },
+];
+
+const tabKeyToIndex = (key?: string | null) => {
+ if (!key) return 0;
+ return tabs.findIndex((tab) => tab.key === key);
+};
+
+function NftDashboardHidden() {
+ const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' });
+ const { t: tCommon } = useTranslation('translation', { keyPrefix: 'COMMON' });
+ const { t: tCollectibles } = useTranslation('translation', {
+ keyPrefix: 'COLLECTIBLE_COLLECTION_SCREEN',
+ });
+ const [searchParams] = useSearchParams();
+ const tab = searchParams?.get('tab');
+ const {
+ isGalleryOpen,
+ hiddenInscriptionsQuery,
+ totalHiddenInscriptions,
+ HiddenInscriptionListView,
+ hiddenStacksNftsQuery,
+ totalHiddenNfts,
+ HiddenNftListView,
+ hasActivatedOrdinalsKey,
+ } = useNftDashboard();
+ const { ordinalsAddress, stxAddress } = useSelectedAccount();
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const { hiddenCollectibleIds } = useWalletSelector();
+ const [isOptionsModalVisible, setIsOptionsModalVisible] = useState(false);
+ const [tabIndex, setTabIndex] = useState(tabKeyToIndex(tab));
+
+ const visibleTabButtons = tabs.filter((tabItem: TabButton) => {
+ if (tabItem.key === 'inscriptions' && !hasActivatedOrdinalsKey) {
+ return false;
+ }
+ return true;
+ });
+
+ const handleBackButtonClick = () => {
+ navigate(`/nft-dashboard?tab=${tab}`);
+ };
+
+ const handleClickUndoHidingAll = ({
+ toastId,
+ currentInscriptionsHidden,
+ currentStacksNftsHidden,
+ }: {
+ toastId: string;
+ currentInscriptionsHidden: Record;
+ currentStacksNftsHidden: Record;
+ }) => {
+ dispatch(
+ setHiddenCollectiblesAction({
+ collectibleIds: {
+ [ordinalsAddress]: currentInscriptionsHidden,
+ [stxAddress]: currentStacksNftsHidden,
+ },
+ }),
+ );
+ toast.remove(toastId);
+ toast.custom(, {
+ duration: LONG_TOAST_DURATION,
+ });
+ };
+
+ const handleUnHideAll = () => {
+ const currentInscriptionsHidden = {
+ ...(hiddenCollectibleIds[ordinalsAddress] ?? {}),
+ };
+ const currentStacksNftsHidden = {
+ ...(hiddenCollectibleIds[stxAddress] ?? {}),
+ };
+
+ dispatch(removeAllFromHideCollectiblesAction({ address: ordinalsAddress }));
+ dispatch(removeAllFromHideCollectiblesAction({ address: stxAddress }));
+ handleBackButtonClick();
+
+ const toastId = toast.custom(
+
+ handleClickUndoHidingAll({
+ toastId,
+ currentInscriptionsHidden,
+ currentStacksNftsHidden,
+ }),
+ }}
+ />,
+ { duration: LONG_TOAST_DURATION },
+ );
+ };
+
+ const handleSelectTab = (index: number) => setTabIndex(index);
+
+ const openOptionsDialog = () => {
+ setIsOptionsModalVisible(true);
+ };
+
+ const closeOptionsDialog = () => {
+ setIsOptionsModalVisible(false);
+ };
+
+ return (
+ <>
+ {isGalleryOpen ? (
+
+ ) : (
+ 0 || totalHiddenNfts > 0 ? openOptionsDialog : undefined
+ }
+ />
+ )}
+
+
+ {isGalleryOpen && (
+
+
+
+ {tCollectibles('BACK_TO_GALLERY')}
+
+
+ )}
+
+
+ {t('HIDDEN_COLLECTIBLES')}
+
+ {isGalleryOpen && (totalHiddenInscriptions > 0 || totalHiddenNfts > 0) ? (
+ }
+ title={t('UNHIDE_ALL')}
+ onClick={handleUnHideAll}
+ />
+ ) : null}
+
+
+
+ {/* TODO: replace with Tabs component from `src/app/ui-library/tabs.tsx` */}
+
+ {visibleTabButtons.length > 1 && (
+
+ {visibleTabButtons.map(({ key, label }) => (
+ handleSelectTab(tabKeyToIndex(key))}
+ >
+ {t(label)}
+
+ ))}
+
+ )}
+ {hasActivatedOrdinalsKey && (
+
+
+
+ {hiddenInscriptionsQuery.isInitialLoading ? (
+
+ ) : (
+ <>
+
+ {totalHiddenInscriptions > 0 ? (
+
+ {totalHiddenInscriptions === 1
+ ? t('TOTAL_ITEMS_ONE')
+ : t('TOTAL_ITEMS', { count: totalHiddenInscriptions })}
+
+ ) : (
+
+ )}
+
+
+ >
+ )}
+
+
+
+ )}
+
+ {hiddenStacksNftsQuery.isInitialLoading ? (
+
+ ) : (
+ <>
+
+ {totalHiddenNfts > 0 ? (
+
+ {totalHiddenNfts === 1
+ ? t('TOTAL_ITEMS_ONE')
+ : t('TOTAL_ITEMS', { count: totalHiddenNfts })}
+
+ ) : (
+
+ )}
+
+
+ >
+ )}
+
+
+
+
+ {!isGalleryOpen && }
+ {isOptionsModalVisible && (
+
+ }
+ title={t('UNHIDE_ALL')}
+ onClick={handleUnHideAll}
+ />
+
+ )}
+ >
+ );
+}
+
+export default NftDashboardHidden;
diff --git a/src/app/screens/nftDashboard/index.tsx b/src/app/screens/nftDashboard/index.tsx
index fade3b948..a9b98eac3 100644
--- a/src/app/screens/nftDashboard/index.tsx
+++ b/src/app/screens/nftDashboard/index.tsx
@@ -1,10 +1,10 @@
import FeatureIcon from '@assets/img/nftDashboard/rareSats/NewFeature.svg';
import AccountHeaderComponent from '@components/accountHeader';
-import ActionButton from '@components/button';
import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert';
import BottomTabBar from '@components/tabBar';
import WebGalleryButton from '@components/webGalleryButton';
import { ArrowDown } from '@phosphor-icons/react';
+import Button from '@ui-library/button';
import { StyledHeading } from '@ui-library/common.styled';
import Dialog from '@ui-library/dialog';
import { useTranslation } from 'react-i18next';
@@ -31,7 +31,7 @@ const PageHeader = styled.div`
width: 100%;
`;
-const StyledCollectiblesTabs = styled(CollectiblesTabs)`
+const CollectiblesContainer = styled.div`
padding: 0 ${(props) => props.theme.space.s};
padding-bottom: ${(props) => props.theme.space.xl};
max-width: 1224px;
@@ -54,7 +54,7 @@ const ReceiveNftContainer = styled.div((props) => ({
}));
const CollectibleContainer = styled.div((props) => ({
- marginBottom: props.theme.spacing(12),
+ marginBottom: props.theme.space.l,
}));
const ButtonContainer = styled.div({
@@ -90,7 +90,6 @@ function NftDashboard() {
onActivateRareSatsAlertEnablePress,
isGalleryOpen,
} = nftDashboard;
-
return (
<>
{isOrdinalReceiveAlertVisible && (
@@ -124,10 +123,10 @@ function NftDashboard() {
- }
- text={t('RECEIVE')}
- onPress={onReceiveModalOpen}
+ title={t('RECEIVE')}
+ onClick={onReceiveModalOpen}
/>
{openReceiveModal && (
@@ -142,11 +141,13 @@ function NftDashboard() {
)}
- }
- inscriptionListView={}
- nftDashboard={nftDashboard}
- />
+
+ }
+ inscriptionListView={}
+ nftDashboard={nftDashboard}
+ />
+
{!isGalleryOpen && }
>
diff --git a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx
index c83077702..74d384f0b 100644
--- a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx
+++ b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx
@@ -1,4 +1,7 @@
import CollectibleCollage from '@components/collectibleCollage/collectibleCollage';
+import useSelectedAccount from '@hooks/useSelectedAccount';
+import useWalletSelector from '@hooks/useWalletSelector';
+import { Star } from '@phosphor-icons/react';
import OrdinalImage from '@screens/ordinals/ordinalImage';
import type { InscriptionCollectionsData } from '@secretkeylabs/xverse-core';
import { StyledP } from '@ui-library/common.styled';
@@ -10,6 +13,7 @@ import {
} from '@utils/inscriptions';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
+import Theme from '../../../theme';
const CollectionContainer = styled.div((props) => ({
display: 'flex',
@@ -28,6 +32,16 @@ const InfoContainer = styled.div`
width: 100%;
`;
+const StyledItemIdContainer = styled.div`
+ display: flex;
+ gap: ${(props) => props.theme.space.xxs};
+ width: 100%;
+`;
+
+const StyledStar = styled(Star)`
+ margin-top: ${(props) => props.theme.space.xxxs};
+`;
+
const StyledItemId = styled(StyledP)`
text-align: left;
text-wrap: nowrap;
@@ -46,15 +60,30 @@ const StyledItemSub = styled(StyledP)`
function InscriptionsTabGridItem({ item: collection }: { item: InscriptionCollectionsData }) {
const navigate = useNavigate();
+ const { starredCollectibleIds, hiddenCollectibleIds } = useWalletSelector();
+ const { ordinalsAddress } = useSelectedAccount();
+ const collectionStarred = starredCollectibleIds[ordinalsAddress]?.some(
+ ({ id }) => id === collection.collection_id,
+ );
+ const standaloneInscriptionStarred =
+ !collection.collection_id &&
+ collection.total_inscriptions === 1 &&
+ starredCollectibleIds[ordinalsAddress]?.some(
+ ({ id }) => id === collection.thumbnail_inscriptions?.[0].id,
+ );
- const handleClickCollectionId = (e: React.MouseEvent) => {
+ const isItemHidden = Object.keys(hiddenCollectibleIds[ordinalsAddress] ?? {}).some(
+ (id) => id === getCollectionKey(collection),
+ );
+
+ const handleClickCollection = (e: React.MouseEvent) => {
const collectionId = e.currentTarget.value;
- navigate(`/nft-dashboard/ordinals-collection/${collectionId}`);
+ navigate(`/nft-dashboard/ordinals-collection/${collectionId}/${isItemHidden ? 'hidden' : ''}`);
};
- const handleClickInscriptionId = (e: React.MouseEvent) => {
+ const handleClickInscription = (e: React.MouseEvent) => {
const inscriptionId = e.currentTarget.value;
- navigate(`/nft-dashboard/ordinal-detail/${inscriptionId}`);
+ navigate(`/nft-dashboard/ordinal-detail/${inscriptionId}/${isItemHidden ? 'hidden' : ''}`);
};
const itemId = getInscriptionsTabGridItemId(collection);
@@ -66,7 +95,7 @@ function InscriptionsTabGridItem({ item: collection }: { item: InscriptionCollec
data-testid="inscription-container"
type="button"
value={getCollectionKey(collection)}
- onClick={isCollection(collection) ? handleClickCollectionId : handleClickInscriptionId}
+ onClick={isCollection(collection) ? handleClickCollection : handleClickInscription}
>
{!collection.thumbnail_inscriptions ? ( // eslint-disable-line no-nested-ternary
@@ -79,9 +108,15 @@ function InscriptionsTabGridItem({ item: collection }: { item: InscriptionCollec
)}
-
- {itemId}
-
+
+ {(collectionStarred || standaloneInscriptionStarred) && (
+
+ )}
+
+ {itemId}
+
+
+
((props) => ({
+const NftImageContainer = styled.div<{
+ $isGalleryView: boolean;
+}>((props) => ({
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
@@ -25,7 +19,7 @@ const NftImageContainer = styled.div((props) => ({
'> img': {
width: '100%',
},
- flexGrow: props.isGalleryView ? 1 : 'initial',
+ flexGrow: props.$isGalleryView ? 1 : 'initial',
}));
const GridItemContainer = styled.div((props) => ({
@@ -43,11 +37,17 @@ const BnsImage = styled.img({
height: '100%',
});
+type Props = {
+ asset: NonFungibleToken;
+ isGalleryOpen: boolean;
+};
+
function Nft({ asset, isGalleryOpen }: Props) {
const { data } = useNftDetail(asset.identifier);
+
return (
-
+
{isBnsContract(asset?.asset_identifier) ? (
) : (
@@ -57,4 +57,5 @@ function Nft({ asset, isGalleryOpen }: Props) {
);
}
+
export default Nft;
diff --git a/src/app/screens/nftDashboard/nftImage.tsx b/src/app/screens/nftDashboard/nftImage.tsx
index 07aa50521..c3249d26e 100644
--- a/src/app/screens/nftDashboard/nftImage.tsx
+++ b/src/app/screens/nftDashboard/nftImage.tsx
@@ -94,6 +94,10 @@ function NftImage({ metadata, isInCollage = false }: Props) {
playsInline
controls
preload="auto"
+ onClick={(event) => {
+ // Prevent playback when clicking anywhere other than the controls
+ event.preventDefault();
+ }}
/>
);
}
diff --git a/src/app/screens/nftDashboard/nftTabGridItem.tsx b/src/app/screens/nftDashboard/nftTabGridItem.tsx
index ee3ee663f..5fd1c8c15 100644
--- a/src/app/screens/nftDashboard/nftTabGridItem.tsx
+++ b/src/app/screens/nftDashboard/nftTabGridItem.tsx
@@ -1,9 +1,13 @@
import CollectibleCollage from '@components/collectibleCollage/collectibleCollage';
+import useSelectedAccount from '@hooks/useSelectedAccount';
+import useWalletSelector from '@hooks/useWalletSelector';
+import { Star } from '@phosphor-icons/react';
import type { StacksCollectionData } from '@secretkeylabs/xverse-core';
import { StyledP } from '@ui-library/common.styled';
import { getNftsTabGridItemSubText } from '@utils/nfts';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
+import Theme from 'theme';
import Nft from './nft';
import NftImage from './nftImage';
@@ -24,6 +28,12 @@ const InfoContainer = styled.div`
width: 100%;
`;
+const TitleContainer = styled.div`
+ display: flex;
+ gap: 4px;
+ width: 100%;
+`;
+
const StyledItemId = styled(StyledP)`
text-align: left;
text-wrap: nowrap;
@@ -40,17 +50,32 @@ const StyledItemSub = styled(StyledP)`
width: 100%;
`;
-function NftTabGridItem({
- item: collection,
- isLoading = false,
-}: {
+const StyledStar = styled(Star)`
+ margin-top: ${(props) => props.theme.space.xxxs};
+`;
+
+type Props = {
item: StacksCollectionData;
isLoading?: boolean;
-}) {
+};
+
+function NftTabGridItem({ item: collection, isLoading = false }: Props) {
const navigate = useNavigate();
+ const { stxAddress } = useSelectedAccount();
+ const { hiddenCollectibleIds, starredCollectibleIds } = useWalletSelector();
+
+ const isItemHidden = Object.keys(hiddenCollectibleIds[stxAddress] ?? {}).some(
+ (id) => id === collection.collection_id,
+ );
+
+ const collectionStarred = starredCollectibleIds[stxAddress]?.some(
+ ({ id }) => id === collection.collection_id,
+ );
const handleClickCollection = () => {
- navigate(`nft-collection/${collection.collection_id}`);
+ navigate(
+ `/nft-dashboard/nft-collection/${collection.collection_id}/${isItemHidden ? 'hidden' : ''}`,
+ );
};
const itemId = collection.collection_name;
@@ -68,9 +93,12 @@ function NftTabGridItem({
)}
-
- {itemId}
-
+
+ {collectionStarred && }
+
+ {itemId}
+
+
{itemSubText}
diff --git a/src/app/screens/nftDashboard/receiveNft/index.tsx b/src/app/screens/nftDashboard/receiveNft/index.tsx
index 9d7d72432..16a5d4c91 100644
--- a/src/app/screens/nftDashboard/receiveNft/index.tsx
+++ b/src/app/screens/nftDashboard/receiveNft/index.tsx
@@ -3,24 +3,26 @@ import plusIcon from '@assets/img/dashboard/plus.svg';
import stacksIcon from '@assets/img/dashboard/stx_icon.svg';
import ordinalsIcon from '@assets/img/nftDashboard/ordinals_icon.svg';
import ActionButton from '@components/button';
-import UpdatedBottomModal from '@components/updatedBottomModal';
+import ReceiveCardComponent from '@components/receiveCardComponent';
import useSelectedAccount from '@hooks/useSelectedAccount';
import useWalletSelector from '@hooks/useWalletSelector';
import { Plus } from '@phosphor-icons/react';
+import Sheet from '@ui-library/sheet';
import { isInOptions, isLedgerAccount } from '@utils/helper';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
-import ReceiveCardComponent from '../../../components/receiveCardComponent';
+
+const GalleryModalContainer = styled.div((props) => ({
+ padding: `0 ${props.theme.space.m}`,
+}));
const ColumnContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
marginTop: props.theme.space.l,
marginBottom: props.theme.space.xl,
- paddingLeft: props.theme.space.m,
- paddingRight: props.theme.space.m,
gap: props.theme.space.m,
}));
@@ -80,12 +82,12 @@ const VerifyButtonContainer = styled.div((props) => ({
marginBottom: props.theme.spacing(6),
}));
-interface Props {
+type Props = {
visible: boolean;
onClose: () => void;
setOrdinalReceiveAlert: () => void;
isGalleryOpen: boolean;
-}
+};
function ReceiveNftModal({ visible, onClose, isGalleryOpen, setOrdinalReceiveAlert }: Props) {
const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' });
@@ -140,14 +142,15 @@ function ReceiveNftModal({ visible, onClose, isGalleryOpen, setOrdinalReceiveAle
onQrAddressClick={onOrdinalsReceivePress}
showVerifyButton={choseToVerifyAddresses}
currency="ORD"
- >
-
-
-
-
-
-
-
+ icon={
+
+
+
+
+
+
+ }
+ />
)}
{stxAddress && (
@@ -157,14 +160,15 @@ function ReceiveNftModal({ visible, onClose, isGalleryOpen, setOrdinalReceiveAle
onQrAddressClick={onReceivePress}
showVerifyButton={choseToVerifyAddresses}
currency="STX"
- >
-
-
-
-
-
-
-
+ icon={
+
+
+
+
+
+
+ }
+ />
)}
{isLedgerAccount(selectedAccount) && !stxAddress && (
@@ -214,16 +218,14 @@ function ReceiveNftModal({ visible, onClose, isGalleryOpen, setOrdinalReceiveAle
- {isReceivingAddressesVisible ? receiveContent : verifyOrViewAddresses}
+
+ {isReceivingAddressesVisible ? receiveContent : verifyOrViewAddresses}
+
>
) : (
-
+
{isReceivingAddressesVisible ? receiveContent : verifyOrViewAddresses}
-
+
);
}
diff --git a/src/app/screens/nftDashboard/useNftDashboard.tsx b/src/app/screens/nftDashboard/useNftDashboard.tsx
index b2cbd6971..67427cb19 100644
--- a/src/app/screens/nftDashboard/useNftDashboard.tsx
+++ b/src/app/screens/nftDashboard/useNftDashboard.tsx
@@ -1,5 +1,4 @@
-import ActionButton from '@components/button';
-import useAddressInscriptionCollections from '@hooks/queries/ordinals/useAddressInscriptionCollections';
+import useAddressInscriptions from '@hooks/queries/ordinals/useAddressInscriptions';
import { useAddressRareSats } from '@hooks/queries/ordinals/useAddressRareSats';
import useStacksCollectibles from '@hooks/queries/useStacksCollectibles';
import useWalletSelector from '@hooks/useWalletSelector';
@@ -10,6 +9,7 @@ import {
ChangeActivateRareSatsAction,
SetRareSatsNoticeDismissedAction,
} from '@stores/wallet/actions/actionCreators';
+import Button from '@ui-library/button';
import { getCollectionKey } from '@utils/inscriptions';
import { InvalidParamsError } from '@utils/query';
import { useCallback, useEffect, useMemo, useRef, useState, type PropsWithChildren } from 'react';
@@ -17,27 +17,27 @@ import { useTranslation } from 'react-i18next';
import { useIsVisible } from 'react-is-visible';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
-import { GridContainer } from './collectiblesTabs';
+import { GridContainer } from './collectiblesTabs/index.styled';
import InscriptionsTabGridItem from './inscriptionsTabGridItem';
import NftTabGridItem from './nftTabGridItem';
const NoCollectiblesText = styled.h1((props) => ({
...props.theme.typography.body_bold_m,
color: props.theme.colors.white_200,
- marginTop: props.theme.spacing(16),
+ marginTop: props.theme.space.xl,
marginBottom: 'auto',
textAlign: 'center',
}));
const ErrorContainer = styled.div((props) => ({
- marginTop: props.theme.spacing(20),
+ marginTop: props.theme.space.xxl,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}));
const ErrorTextContainer = styled.div((props) => ({
- marginTop: props.theme.spacing(8),
+ marginTop: props.theme.space.m,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
@@ -75,7 +75,9 @@ export type NftDashboardState = {
showNewFeatureAlert: boolean;
isOrdinalReceiveAlertVisible: boolean;
stacksNftsQuery: ReturnType;
- inscriptionsQuery: ReturnType;
+ inscriptionsQuery: ReturnType;
+ hiddenInscriptionsQuery: ReturnType;
+ hiddenStacksNftsQuery: ReturnType;
rareSatsQuery: ReturnType;
openInGalleryView: () => void;
onReceiveModalOpen: () => void;
@@ -83,7 +85,9 @@ export type NftDashboardState = {
onOrdinalReceiveAlertOpen: () => void;
onOrdinalReceiveAlertClose: () => void;
InscriptionListView: () => JSX.Element;
+ HiddenInscriptionListView: () => JSX.Element;
NftListView: () => JSX.Element;
+ HiddenNftListView: () => JSX.Element;
onActivateRareSatsAlertCrossPress: () => void;
onActivateRareSatsAlertDenyPress: () => void;
onActivateRareSatsAlertEnablePress: () => void;
@@ -93,7 +97,9 @@ export type NftDashboardState = {
hasActivatedRareSatsKey?: boolean;
showNoticeAlert?: boolean;
totalNfts: number;
+ totalHiddenNfts: number;
totalInscriptions: number;
+ totalHiddenInscriptions: number;
};
const useNftDashboard = (): NftDashboardState => {
@@ -106,11 +112,16 @@ const useNftDashboard = (): NftDashboardState => {
const [showNoticeAlert, setShowNoticeAlert] = useState(false);
const [isOrdinalReceiveAlertVisible, setIsOrdinalReceiveAlertVisible] = useState(false);
const stacksNftsQuery = useStacksCollectibles();
- const inscriptionsQuery = useAddressInscriptionCollections();
+ const hiddenStacksNftsQuery = useStacksCollectibles(true);
+ const inscriptionsQuery = useAddressInscriptions();
+ const hiddenInscriptionsQuery = useAddressInscriptions(true);
const rareSatsQuery = useAddressRareSats();
const totalInscriptions = inscriptionsQuery.data?.pages?.[0]?.total_inscriptions ?? 0;
+ const totalHiddenInscriptions = hiddenInscriptionsQuery.data?.pages?.[0]?.total_inscriptions ?? 0;
+
const totalNfts = stacksNftsQuery.data?.total_nfts ?? 0;
+ const totalHiddenNfts = hiddenStacksNftsQuery.data?.total_nfts ?? 0;
const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []);
@@ -168,7 +179,7 @@ const useNftDashboard = (): NftDashboardState => {
return (
<>
-
+
{inscriptionsQuery.data?.pages
?.map((page) => page?.results)
.flat()
@@ -178,12 +189,12 @@ const useNftDashboard = (): NftDashboardState => {
{inscriptionsQuery.hasNextPage && (
- inscriptionsQuery.fetchNextPage()}
+ onClick={() => inscriptionsQuery.fetchNextPage()}
/>
)}
@@ -191,6 +202,51 @@ const useNftDashboard = (): NftDashboardState => {
);
}, [inscriptionsQuery, isGalleryOpen, totalInscriptions, t]);
+ const HiddenInscriptionListView = useCallback(() => {
+ if (
+ hiddenInscriptionsQuery.error &&
+ !(hiddenInscriptionsQuery.error instanceof InvalidParamsError)
+ ) {
+ return (
+
+
+
+ {t('ERROR_RETRIEVING')}
+ {t('TRY_AGAIN')}
+
+
+ );
+ }
+
+ if (totalHiddenInscriptions === 0) {
+ return {t('NO_COLLECTIBLES')};
+ }
+
+ return (
+ <>
+
+ {hiddenInscriptionsQuery.data?.pages
+ ?.map((page) => page?.results)
+ .flat()
+ .map((collection: InscriptionCollectionsData) => (
+
+ ))}
+
+ {hiddenInscriptionsQuery.hasNextPage && (
+
+
+ )}
+ >
+ );
+ }, [hiddenInscriptionsQuery, isGalleryOpen, totalHiddenInscriptions, t]);
+
const NftListView = useCallback(() => {
if (stacksNftsQuery.error && !(stacksNftsQuery.error instanceof InvalidParamsError)) {
return (
@@ -209,7 +265,7 @@ const useNftDashboard = (): NftDashboardState => {
}
return (
-
+
{stacksNftsQuery.data?.results.map((collection: StacksCollectionData) => (
@@ -219,6 +275,37 @@ const useNftDashboard = (): NftDashboardState => {
);
}, [stacksNftsQuery, isGalleryOpen, totalNfts, t]);
+ const HiddenNftListView = useCallback(() => {
+ if (
+ hiddenStacksNftsQuery.error &&
+ !(hiddenStacksNftsQuery.error instanceof InvalidParamsError)
+ ) {
+ return (
+
+
+
+ {t('ERROR_RETRIEVING')}
+ {t('TRY_AGAIN')}
+
+
+ );
+ }
+
+ if (totalHiddenNfts === 0) {
+ return {t('NO_COLLECTIBLES')};
+ }
+
+ return (
+
+ {hiddenStacksNftsQuery.data?.results.map((collection: StacksCollectionData) => (
+
+
+
+ ))}
+
+ );
+ }, [hiddenStacksNftsQuery, isGalleryOpen, totalHiddenNfts, t]);
+
const onActivateRareSatsAlertCrossPress = () => {
setShowNewFeatureAlert(false);
};
@@ -245,14 +332,18 @@ const useNftDashboard = (): NftDashboardState => {
showNewFeatureAlert,
isOrdinalReceiveAlertVisible,
stacksNftsQuery,
+ hiddenStacksNftsQuery,
inscriptionsQuery,
+ hiddenInscriptionsQuery,
openInGalleryView,
onReceiveModalOpen,
onReceiveModalClose,
onOrdinalReceiveAlertOpen,
onOrdinalReceiveAlertClose,
InscriptionListView,
+ HiddenInscriptionListView,
NftListView,
+ HiddenNftListView,
onActivateRareSatsAlertCrossPress,
onActivateRareSatsAlertDenyPress,
onActivateRareSatsAlertEnablePress,
@@ -263,7 +354,9 @@ const useNftDashboard = (): NftDashboardState => {
showNoticeAlert,
rareSatsQuery,
totalNfts,
+ totalHiddenNfts,
totalInscriptions,
+ totalHiddenInscriptions,
};
};
diff --git a/src/app/screens/nftDetail/index.styled.ts b/src/app/screens/nftDetail/index.styled.ts
index 19be80ec7..49865ab83 100644
--- a/src/app/screens/nftDetail/index.styled.ts
+++ b/src/app/screens/nftDetail/index.styled.ts
@@ -1,22 +1,19 @@
-import { BetterBarLoader } from '@components/barLoader';
-import Separator from '@components/separator';
import { Tooltip } from 'react-tooltip';
import styled from 'styled-components';
+interface DetailSectionProps {
+ $isGallery: boolean;
+}
+
export const ExtensionContainer = styled.div((props) => ({
...props.theme.scrollbar,
display: 'flex',
flexDirection: 'column',
- marginTop: props.theme.spacing(4),
+ marginTop: props.theme.space.xs,
alignItems: 'center',
flex: 1,
- paddingLeft: props.theme.spacing(4),
- paddingRight: props.theme.spacing(4),
-}));
-
-export const GalleryReceiveButtonContainer = styled.div((props) => ({
- marginRight: props.theme.spacing(3),
- width: '100%',
+ paddingLeft: props.theme.space.xs,
+ paddingRight: props.theme.space.xs,
}));
export const BackButtonContainer = styled.div((props) => ({
@@ -32,8 +29,8 @@ export const ButtonContainer = styled.div((props) => ({
justifyContent: 'center',
columnGap: props.theme.spacing(11),
paddingBottom: props.theme.spacing(16),
- marginBottom: props.theme.spacing(4),
- marginTop: props.theme.spacing(4),
+ marginBottom: props.theme.space.xs,
+ marginTop: props.theme.space.xs,
width: '100%',
borderBottom: `1px solid ${props.theme.colors.elevation3}`,
}));
@@ -52,7 +49,7 @@ export const NftContainer = styled.div((props) => ({
justifyContent: 'flex-start',
alignItems: 'flex-start',
borderRadius: 8,
- marginBottom: props.theme.spacing(12),
+ marginBottom: props.theme.space.l,
}));
export const ExtensionNftContainer = styled.div((props) => ({
@@ -63,13 +60,14 @@ export const ExtensionNftContainer = styled.div((props) => ({
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
- marginBottom: props.theme.spacing(12),
+ marginBottom: props.theme.space.l,
}));
export const NftTitleText = styled.h1((props) => ({
...props.theme.typography.headline_m,
color: props.theme.colors.white_0,
textAlign: 'center',
+ wordBreak: 'break-word',
}));
export const CollectibleText = styled.p((props) => ({
@@ -81,7 +79,7 @@ export const CollectibleText = styled.p((props) => ({
export const NftGalleryTitleText = styled.h1((props) => ({
...props.theme.typography.headline_l,
color: props.theme.colors.white_0,
- marginBottom: props.theme.spacing(4),
+ marginBottom: props.theme.space.xs,
}));
export const NftOwnedByText = styled.p((props) => ({
@@ -109,20 +107,15 @@ export const GridContainer = styled.div((props) => ({
display: 'grid',
width: '100%',
marginTop: props.theme.spacing(6),
- columnGap: props.theme.spacing(4),
- rowGap: props.theme.spacing(4),
+ columnGap: props.theme.space.xs,
+ rowGap: props.theme.space.xs,
gridTemplateColumns: 'repeat(auto-fit,minmax(150px,1fr))',
- marginBottom: props.theme.spacing(8),
-}));
-
-export const ShareButtonContainer = styled.div((props) => ({
- marginLeft: props.theme.spacing(3),
- width: '100%',
+ marginBottom: props.theme.space.m,
}));
export const DescriptionContainer = styled.div((props) => ({
display: 'flex',
- marginLeft: props.theme.spacing(20),
+ marginLeft: props.theme.space.xxl,
flexDirection: 'column',
marginBottom: props.theme.spacing(30),
}));
@@ -140,8 +133,8 @@ export const WebGalleryButton = styled.button((props) => ({
borderRadius: props.theme.radius(1),
backgroundColor: 'transparent',
width: '100%',
- marginTop: props.theme.spacing(4),
- marginBottom: props.theme.spacing(12),
+ marginTop: props.theme.space.xs,
+ marginBottom: props.theme.space.l,
}));
export const WebGalleryButtonText = styled.div((props) => ({
@@ -162,20 +155,12 @@ export const BackButton = styled.button((props) => ({
justifyContent: 'flex-start',
alignItems: 'center',
background: 'transparent',
- marginBottom: props.theme.spacing(12),
+ marginBottom: props.theme.space.l,
}));
-export const ExtensionLoaderContainer = styled.div({
- height: '100%',
- display: 'flex',
- flexDirection: 'column',
- justifyContent: 'space-around',
- alignItems: 'center',
-});
-
export const SeeDetailsButtonContainer = styled.div((props) => ({
marginBottom: props.theme.spacing(27),
- marginTop: props.theme.spacing(4),
+ marginTop: props.theme.space.xs,
}));
export const Button = styled.button((props) => ({
@@ -184,7 +169,7 @@ export const Button = styled.button((props) => ({
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
- width: props.isGallery ? 400 : 328,
+ width: props.$isGallery ? 400 : 328,
height: 44,
padding: 12,
borderRadius: 12,
@@ -220,19 +205,19 @@ export const GalleryScrollContainer = styled.div((props) => ({
export const ButtonHiglightedText = styled.p((props) => ({
...props.theme.typography.body_m,
color: props.theme.colors.white_0,
- marginLeft: props.theme.spacing(2),
- marginRight: props.theme.spacing(2),
+ marginLeft: props.theme.space.xxs,
+ marginRight: props.theme.space.xxs,
}));
export const GalleryRowContainer = styled.div<{
- withGap?: boolean;
+ $withGap?: boolean;
}>((props) => ({
display: 'flex',
alignItems: 'flex-start',
- marginTop: props.theme.spacing(8),
- marginBottom: props.theme.spacing(12),
+ marginTop: props.theme.space.m,
+ marginBottom: props.theme.space.l,
flexDirection: 'row',
- columnGap: props.withGap ? props.theme.spacing(20) : 0,
+ columnGap: props.$withGap ? props.theme.space.m : 0,
}));
export const StyledTooltip = styled(Tooltip)`
@@ -245,14 +230,6 @@ export const StyledTooltip = styled(Tooltip)`
}
`;
-export const StyledBarLoader = styled(BetterBarLoader)<{
- withMarginBottom?: boolean;
-}>((props) => ({
- padding: 0,
- borderRadius: props.theme.radius(1),
- marginBottom: props.withMarginBottom ? props.theme.spacing(6) : 0,
-}));
-
export const GalleryContainer = styled.div({
marginLeft: 'auto',
marginRight: 'auto',
@@ -264,57 +241,20 @@ export const GalleryContainer = styled.div({
maxWidth: 1224,
});
-export const ActionButtonLoader = styled.div((props) => ({
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- rowGap: props.theme.spacing(4),
-}));
-
-export const ActionButtonsLoader = styled.div((props) => ({
- display: 'flex',
- justifyContent: 'center',
- columnGap: props.theme.spacing(11),
-}));
-
-export const GalleryLoaderContainer = styled.div({
- display: 'flex',
- flexDirection: 'column',
-});
-
-export const StyledSeparator = styled(Separator)`
- width: 100%;
-`;
-
-export const TitleLoader = styled.div`
- display: flex;
- flex-direction: column;
-`;
-interface DetailSectionProps {
- isGallery: boolean;
-}
-
export const NftDetailsContainer = styled.div((props) => ({
display: 'flex',
alignItems: 'flex-start',
flexDirection: 'column',
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
- width: props.isGallery ? 400 : '100%',
- marginTop: props.theme.spacing(8),
+ width: props.$isGallery ? 400 : '100%',
+ marginTop: props.theme.space.m,
}));
export const DetailSection = styled.div((props) => ({
display: 'flex',
- flexDirection: !props.isGallery ? 'row' : 'column',
+ flexDirection: !props.$isGallery ? 'row' : 'column',
justifyContent: 'center',
width: '100%',
- columnGap: props.theme.spacing(8),
-}));
-
-export const InfoContainer = styled.div((props) => ({
- width: '100%',
- display: 'flex',
- justifyContent: 'space-between',
- padding: `0 ${props.theme.spacing(8)}px`,
+ columnGap: props.theme.space.m,
}));
diff --git a/src/app/screens/nftDetail/index.tsx b/src/app/screens/nftDetail/index.tsx
index c4b4b3ec0..9b19f7d60 100644
--- a/src/app/screens/nftDetail/index.tsx
+++ b/src/app/screens/nftDetail/index.tsx
@@ -1,18 +1,44 @@
import SquaresFour from '@assets/img/nftDashboard/squares_four.svg';
import AccountHeaderComponent from '@components/accountHeader';
-import ActionButton from '@components/button';
import CollectibleDetailTile from '@components/collectibleDetailTile';
import SquareButton from '@components/squareButton';
import BottomTabBar from '@components/tabBar';
import TopRow from '@components/topRow';
-import { ArrowLeft, ArrowUp, Share } from '@phosphor-icons/react';
+import useOptionsSheet from '@hooks/useOptionsSheet';
+import useSelectedAccount from '@hooks/useSelectedAccount';
+import useWalletSelector from '@hooks/useWalletSelector';
+import {
+ ArrowLeft,
+ ArrowUp,
+ DotsThreeVertical,
+ Share,
+ Star,
+ UserCircleCheck,
+ UserCircleMinus,
+} from '@phosphor-icons/react';
import NftImage from '@screens/nftDashboard/nftImage';
+import { StyledButton } from '@screens/ordinalsCollection/index.styled';
import type { Attribute } from '@secretkeylabs/xverse-core';
-import { EMPTY_LABEL } from '@utils/constants';
+import {
+ addToStarCollectiblesAction,
+ removeAccountAvatarAction,
+ removeFromStarCollectiblesAction,
+ setAccountAvatarAction,
+} from '@stores/wallet/actions/actionCreators';
+import ActionButton from '@ui-library/button';
+import Sheet from '@ui-library/sheet';
+import SnackBar from '@ui-library/snackBar';
+import { EMPTY_LABEL, LONG_TOAST_DURATION } from '@utils/constants';
+import { useCallback } from 'react';
+import toast from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+import Theme from '../../../theme';
+import { ExtensionLoader, GalleryLoader } from './loaders';
+import NftAttribute from './nftAttribute';
+import useNftDetailScreen from './useNftDetail';
+
import {
- ActionButtonLoader,
- ActionButtonsLoader,
AssetDeatilButtonText,
AttributeText,
BackButton,
@@ -28,16 +54,12 @@ import {
DescriptionContainer,
DetailSection,
ExtensionContainer,
- ExtensionLoaderContainer,
ExtensionNftContainer,
GalleryCollectibleText,
GalleryContainer,
- GalleryLoaderContainer,
- GalleryReceiveButtonContainer,
GalleryRowContainer,
GalleryScrollContainer,
GridContainer,
- InfoContainer,
NftContainer,
NftDetailsContainer,
NftGalleryTitleText,
@@ -46,18 +68,16 @@ import {
OwnerAddressText,
RowContainer,
SeeDetailsButtonContainer,
- ShareButtonContainer,
- StyledBarLoader,
- StyledSeparator,
StyledTooltip,
- TitleLoader,
WebGalleryButton,
WebGalleryButtonText,
} from './index.styled';
-import NftAttribute from './nftAttribute';
-import useNftDetailScreen from './useNftDetail';
function NftDetailScreen() {
+ const dispatch = useDispatch();
+ const optionsSheet = useOptionsSheet();
+ const { t: optionsDialogT } = useTranslation('translation', { keyPrefix: 'OPTIONS_DIALOG' });
+ const { t: commonT } = useTranslation('translation', { keyPrefix: 'COMMON' });
const { t } = useTranslation('translation', { keyPrefix: 'NFT_DETAIL_SCREEN' });
const {
nft,
@@ -74,6 +94,99 @@ function NftDetailScreen() {
handleOnSendClick,
galleryTitle,
} = useNftDetailScreen();
+ const { ordinalsAddress } = useSelectedAccount();
+ const { avatarIds, hiddenCollectibleIds, starredCollectibleIds } = useWalletSelector();
+ const selectedAvatar = avatarIds[ordinalsAddress];
+ const isNftSelectedAsAvatar =
+ selectedAvatar?.type === 'stacks' && selectedAvatar.nft.token_id === nftData?.token_id;
+
+ const handleSetAvatar = useCallback(() => {
+ if (ordinalsAddress && nftData?.token_id) {
+ dispatch(
+ setAccountAvatarAction({
+ address: ordinalsAddress,
+ avatar: { type: 'stacks', nft: nftData },
+ }),
+ );
+
+ const toastId = toast.custom(
+ {
+ if (selectedAvatar?.type) {
+ dispatch(
+ setAccountAvatarAction({ address: ordinalsAddress, avatar: selectedAvatar }),
+ );
+ } else {
+ dispatch(removeAccountAvatarAction({ address: ordinalsAddress }));
+ }
+
+ toast.remove(toastId);
+ toast.custom();
+ },
+ }}
+ />,
+ );
+ }
+
+ optionsSheet.close();
+ }, [dispatch, optionsDialogT, commonT, ordinalsAddress, nftData, optionsSheet, selectedAvatar]);
+
+ const handleRemoveAvatar = useCallback(() => {
+ dispatch(removeAccountAvatarAction({ address: ordinalsAddress }));
+ toast.custom();
+ optionsSheet.close();
+ }, [dispatch, optionsDialogT, ordinalsAddress, optionsSheet]);
+
+ const isNftCollectionHidden = Object.keys(hiddenCollectibleIds[stxAddress] ?? {}).some(
+ (id) => id === collection?.collection_id,
+ );
+ const nftId = nftData?.fully_qualified_token_id ?? '';
+ const nftStarred = starredCollectibleIds[stxAddress]?.some(({ id }) => id === nftId);
+
+ const handleClickUndoStarring = (toastId: string) => {
+ dispatch(
+ removeFromStarCollectiblesAction({
+ address: stxAddress,
+ id: nftId,
+ }),
+ );
+ toast.remove(toastId);
+ toast.custom();
+ };
+
+ const handleStarClick = () => {
+ if (nftStarred) {
+ dispatch(
+ removeFromStarCollectiblesAction({
+ address: stxAddress,
+ id: nftId,
+ }),
+ );
+ toast.custom();
+ } else {
+ dispatch(
+ addToStarCollectiblesAction({
+ address: stxAddress,
+ id: nftId,
+ }),
+ );
+ const toastId = toast.custom(
+ handleClickUndoStarring(toastId),
+ }}
+ />,
+ { duration: LONG_TOAST_DURATION },
+ );
+ }
+ };
const nftAttributes = nftData?.nft_token_attributes?.length !== 0 && (
<>
@@ -90,9 +203,9 @@ function NftDetailScreen() {
>
);
const nftDetails = (
-
+
{collection?.collection_name && (
-
+
)}
{!isGalleryOpen && nftAttributes}
-
+
{nftData?.rarity_score && (
@@ -117,35 +230,7 @@ function NftDetailScreen() {
);
const extensionView = isLoading ? (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
) : (
{t('COLLECTIBLE')}
@@ -182,11 +267,11 @@ function NftDetailScreen() {
{nftDetails}
-