Skip to content

Commit

Permalink
feat: redesign send rpc transfer flow
Browse files Browse the repository at this point in the history
  • Loading branch information
alter-eggo committed Dec 18, 2024
1 parent e618ccf commit 716f685
Show file tree
Hide file tree
Showing 47 changed files with 5,689 additions and 2,798 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
"@leather.io/query": "2.26.1",
"@leather.io/stacks": "1.4.0",
"@leather.io/tokens": "0.12.1",
"@leather.io/ui": "1.39.0",
"@leather.io/ui": "1.44.4",
"@leather.io/utils": "0.21.1",
"@ledgerhq/hw-transport-webusb": "6.27.19",
"@noble/hashes": "1.5.0",
Expand Down Expand Up @@ -268,7 +268,7 @@
"@btckit/types": "0.0.19",
"@chromatic-com/storybook": "3.2.2",
"@leather.io/eslint-config": "0.7.0",
"@leather.io/panda-preset": "0.5.3",
"@leather.io/panda-preset": "0.8.0",
"@leather.io/prettier-config": "0.6.0",
"@leather.io/rpc": "2.4.0",
"@ls-lint/ls-lint": "2.2.3",
Expand Down
5,993 changes: 3,976 additions & 2,017 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions src/app/common/fees/use-fees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect, useMemo, useState } from 'react';

import type { MarketData } from '@leather.io/models';

import type { RawFee } from '@app/components/bitcoin-fees-list/bitcoin-fees.utils';

export type FeeType = 'slow' | 'standard' | 'fast' | 'custom';

export interface FeeDisplayInfo {
feeType: FeeType;
baseUnitsValue: number;
feeRate: number;
titleLeft: string;
captionLeft: string;
titleRight?: string;
captionRight?: string;
}

export interface FormatFeeForDisplayArgs {
rawFee: RawFee;
marketData: MarketData;
}

interface UseFeesProps {
defaultFeeType?: FeeType;
fees: FeesRawData;
getCustomFeeData(rate: number): RawFee;
marketData: MarketData;
formatFeeForDisplay({ rawFee, marketData }: FormatFeeForDisplayArgs): FeeDisplayInfo;
}

export type FeesRawData = Record<Exclude<FeeType, 'custom'>, RawFee>;

export function useFeesHandler({
defaultFeeType = 'standard',
fees,
getCustomFeeData,
marketData,
formatFeeForDisplay,
}: UseFeesProps) {
const [selectedFeeType, setSelectedFeeType] = useState<FeeType>(defaultFeeType);
const [editFeeSelected, setEditFeeSelected] = useState<FeeType>(selectedFeeType);
const [customFeeRate, setCustomFeeRate] = useState<string>('');

const customFeeData = getCustomFeeData(Number(customFeeRate));

const selectedFeeData = useMemo(() => {
if (selectedFeeType === 'custom') {
return formatFeeForDisplay({ rawFee: customFeeData, marketData });
}

const rawFee = fees[selectedFeeType];

if (!rawFee) {
return {};
}

return formatFeeForDisplay({ rawFee, marketData });
}, [fees, selectedFeeType, customFeeData, marketData, formatFeeForDisplay]);

useEffect(() => {
if (customFeeRate === '' && selectedFeeType !== 'custom') {
const data = fees[selectedFeeType];
if (data && data.feeRate) {
setCustomFeeRate(data.feeRate.toString());
}
}
}, [fees, selectedFeeType, customFeeRate]);

return {
selectedFeeType,
setSelectedFeeType,
selectedFeeData,
editFeeSelected,
setEditFeeSelected,
customFeeRate,
setCustomFeeRate,
customFeeData,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SwitchAccountOutletContext } from './switch-account';
export function useSwitchAccountSheet() {
const { isShowingSwitchAccount, setIsShowingSwitchAccount } =
useOutletContext<SwitchAccountOutletContext>();

return {
isShowingSwitchAccount,
setIsShowingSwitchAccount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function determineUtxosForSpendAll({
throw new Error('Cannot calculate spend of invalid address type');
});
const filteredUtxos = filterUneconomicalUtxos({ utxos, feeRate, recipients });

if (!filteredUtxos.length) throw new InsufficientFundsError();
const sizeInfo = getSizeInfo({
inputLength: filteredUtxos.length,
isSendMax: true,
Expand Down
12 changes: 10 additions & 2 deletions src/app/common/transactions/bitcoin/use-generate-bitcoin-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ interface GenerateNativeSegwitTxValues {
recipients: TransferRecipient[];
}

export function useGenerateUnsignedNativeSegwitTx() {
interface UseGenerateUnsignedNativeSegwitTxProps {
throwError?: boolean;
}

// temp arg before refactoring all flows to new design
export function useGenerateUnsignedNativeSegwitTx({
throwError = false,
}: UseGenerateUnsignedNativeSegwitTxProps = {}) {
const signer = useCurrentAccountNativeSegwitIndexZeroSigner();

const networkMode = useBitcoinScureLibNetworkConfig();
Expand Down Expand Up @@ -87,9 +94,10 @@ export function useGenerateUnsignedNativeSegwitTx() {
} catch (e) {
// eslint-disable-next-line no-console
console.log('Error signing bitcoin transaction', e);
if (throwError) throw e;
return null;
}
},
[networkMode, signer.address, signer.publicKey]
[networkMode, signer.address, signer.publicKey, throwError]
);
}
16 changes: 16 additions & 0 deletions src/app/components/account/account-bitcoin-address.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Caption } from '@leather.io/ui';
import { truncateMiddle } from '@leather.io/utils';

import { BitcoinNativeSegwitAccountLoader } from '../loaders/bitcoin-account-loader';

interface AccountBitcoinAddressProps {
index: number;
}

export function AccountBitcoinAddress({ index }: AccountBitcoinAddressProps) {
return (
<BitcoinNativeSegwitAccountLoader index={index}>
{signer => <Caption>{truncateMiddle(signer.address, 4)}</Caption>}
</BitcoinNativeSegwitAccountLoader>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { styled } from 'leather-styles/jsx';

import { Caption, ItemLayout, Pressable, SkeletonLoader } from '@leather.io/ui';
import { formatDustUsdAmounts, formatMoneyPadded, i18nFormatCurrency } from '@leather.io/utils';

import { useAccountDisplayName } from '@app/common/hooks/account/use-account-names';
import { useConvertCryptoCurrencyToFiatAmount } from '@app/common/hooks/use-convert-to-fiat-amount';
import { useCurrentBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useStacksAccounts } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { AccountAvatarItem } from '@app/ui/components/account/account-avatar/account-avatar-item';

import { AccountBitcoinAddress } from '../account/account-bitcoin-address';
import { AccountNameLayout } from '../account/account-name';
import { ApproveTransactionSwitchAccount } from './approve-transaction-switch-account';

interface ApproveBitcoinTransactionSwitchAccountProps {
toggleSwitchAccount(): void;
}

export function ApproveBitcoinTransactionSwitchAccount({
toggleSwitchAccount,
}: ApproveBitcoinTransactionSwitchAccountProps) {
const index = useCurrentAccountIndex();
const stacksAccounts = useStacksAccounts();
const { balance, isLoading: isLoadingBalance } = useCurrentBtcCryptoAssetBalanceNativeSegwit();

const convertToFiatAmount = useConvertCryptoCurrencyToFiatAmount('BTC');
const fiatAmount = convertToFiatAmount(balance.availableBalance);

const stxAddress = stacksAccounts[index]?.address || '';
const { data: name = '', isLoading: isLoadingName } = useAccountDisplayName({
address: stxAddress,
index,
});

const titleRight = (
<SkeletonLoader isLoading={isLoadingBalance} width="96px">
<styled.span textStyle="label.02">{formatMoneyPadded(balance.availableBalance)}</styled.span>
</SkeletonLoader>
);

const captionRight = (
<SkeletonLoader isLoading={isLoadingBalance} width="48px">
<Caption>{formatDustUsdAmounts(i18nFormatCurrency(fiatAmount))}</Caption>
</SkeletonLoader>
);

return (
<ApproveTransactionSwitchAccount>
<Pressable onClick={toggleSwitchAccount}>
<ItemLayout
showChevron
img={<AccountAvatarItem index={0} publicKey="" name="" />}
titleLeft={<AccountNameLayout isLoading={isLoadingName}>{name}</AccountNameLayout>}
captionLeft={<AccountBitcoinAddress index={index} />}
titleRight={titleRight}
captionRight={captionRight}
/>
</Pressable>
</ApproveTransactionSwitchAccount>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FormError } from '@app/components/error/form-error';

interface ApproveTransactionErrorProps {
isLoading: boolean;
isInsufficientBalance: boolean;
}

export function ApproveTransactionError({
isLoading,
isInsufficientBalance,
}: ApproveTransactionErrorProps) {
if (isLoading) return null;
if (isInsufficientBalance) return <FormError text="Available balance insufficient" />;
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { styled } from 'leather-styles/jsx';

import { Approver, QuestionCircleIcon } from '@leather.io/ui';

interface ApproveTransactionHeaderProps {
title: string;
href?: string;
onPressRequestedByLink(e: React.MouseEvent<HTMLAnchorElement>): void;
}

export function ApproveTransactionHeader({
title,
href = 'https://leather.io/guides/connect-dapps',
onPressRequestedByLink,
}: ApproveTransactionHeaderProps) {
return (
<Approver.Header
title={title}
info={
<styled.a display="block" p="space.01" target="_blank" href={href}>
<QuestionCircleIcon variant="small" />
</styled.a>
}
onPressRequestedByLink={onPressRequestedByLink}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { HStack, styled } from 'leather-styles/jsx';

import { AddressDisplayer, Approver, BtcAvatarIcon, ItemLayout, UserIcon } from '@leather.io/ui';
import { formatDustUsdAmounts, formatMoneyPadded, i18nFormatCurrency } from '@leather.io/utils';

import type { TransferRecipient } from '@shared/models/form.model';

import { useConvertCryptoCurrencyToFiatAmount } from '@app/common/hooks/use-convert-to-fiat-amount';
import { IconWrapper } from '@app/components/icon-wrapper';
import { Divider } from '@app/components/layout/divider';

interface ApproveTransactionRecipientsProps {
recipients: TransferRecipient[];
}

export function ApproveTransactionRecipients({ recipients }: ApproveTransactionRecipientsProps) {
const convertToFiatAmount = useConvertCryptoCurrencyToFiatAmount('BTC');

return recipients.map(({ address, amount }) => {
const fiatAmount = convertToFiatAmount(amount);

const titleRight = formatMoneyPadded(amount);
const captionRight = formatDustUsdAmounts(i18nFormatCurrency(fiatAmount));

return (
<Approver.Section key={address}>
<Approver.Subheader>
<styled.span textStyle="label.01">You'll send</styled.span>
</Approver.Subheader>

<ItemLayout
img={<BtcAvatarIcon />}
titleLeft="Bitcoin"
captionLeft="Bitcoin blockchain"
titleRight={titleRight}
captionRight={captionRight}
/>

<Divider mt="space.05" mb="space.04" />

<Approver.Subheader>
<styled.span textStyle="label.01">To address</styled.span>
</Approver.Subheader>
<HStack key={address} alignItems="center" gap="space.04" pb="space.03">
<IconWrapper>
<UserIcon />
</IconWrapper>
<AddressDisplayer address={address} />
</HStack>
</Approver.Section>
);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Approver, ItemLayout, Pressable } from '@leather.io/ui';

import type { FeeDisplayInfo } from '@app/common/fees/use-fees';

import { CryptoAssetItemPlaceholder } from '../crypto-asset-item/crypto-asset-item-placeholder';
import { FeeItemIcon } from '../fees/fee-item-icon';

interface ApproveTransactionSelectedFeeProps {
isLoading: boolean;
selectedFeeData: FeeDisplayInfo;
onChooseTransferFee(): void;
}

export function ApproveTransactionSelectedFee({
isLoading,
selectedFeeData,
onChooseTransferFee,
}: ApproveTransactionSelectedFeeProps) {
return (
<Approver.Section>
<Approver.Subheader>Fee</Approver.Subheader>
<Pressable onClick={onChooseTransferFee} mb="space.02">
{isLoading || !selectedFeeData ? (
<CryptoAssetItemPlaceholder my="0" />
) : (
<ItemLayout
img={<FeeItemIcon feeType={selectedFeeData.feeType} />}
titleLeft={selectedFeeData.titleLeft}
captionLeft={selectedFeeData.captionLeft}
titleRight={selectedFeeData.titleRight}
captionRight={selectedFeeData.captionRight}
showChevron
/>
)}
</Pressable>
</Approver.Section>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Box } from 'leather-styles/jsx';

import { Approver } from '@leather.io/ui';

import type { HasChildren } from '@app/common/has-children';

export function ApproveTransactionSwitchAccount({ children }: HasChildren) {
return (
<Approver.Section>
<Approver.Subheader>With account</Approver.Subheader>
<Box mb="space.03">{children}</Box>
</Approver.Section>
);
}
Loading

0 comments on commit 716f685

Please sign in to comment.