mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-04-24 04:45:58 +08:00
refactor: btc send form
This commit is contained in:
@@ -1,13 +1,8 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { NetworkConfiguration } from '@shared/constants';
|
||||
|
||||
import { FormErrorMessages } from '@app/common/error-messages';
|
||||
import { fetchBtcNameOwner } from '@app/query/stacks/bns/bns.utils';
|
||||
import { StacksClient } from '@app/query/stacks/stacks-client';
|
||||
|
||||
import {
|
||||
btcAddressValidator,
|
||||
notCurrentAddressValidator,
|
||||
stxAddressNetworkValidator,
|
||||
stxAddressValidator,
|
||||
@@ -21,23 +16,3 @@ export function stxRecipientValidator(
|
||||
.concat(stxAddressNetworkValidator(currentNetwork))
|
||||
.concat(notCurrentAddressValidator(currentAddress || ''));
|
||||
}
|
||||
|
||||
export function btcRecipientAddressOrBnsNameValidator({ client }: { client: StacksClient }) {
|
||||
return yup.string().test({
|
||||
name: 'btcRecipientOrBnsName',
|
||||
message: FormErrorMessages.InvalidAddress,
|
||||
test: async value => {
|
||||
try {
|
||||
await btcAddressValidator().validate(value);
|
||||
return true;
|
||||
} catch (e) {}
|
||||
try {
|
||||
const btcAddress = await fetchBtcNameOwner(client, value ?? '');
|
||||
await btcAddressValidator().validate(btcAddress);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function FeeEstimateSelectLayout(props: FeeEstimateSelectLayoutProps) {
|
||||
position="absolute"
|
||||
ref={ref}
|
||||
style={styles}
|
||||
top="-100px"
|
||||
top="-35px"
|
||||
zIndex={9999}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -10,14 +10,25 @@ interface CollectibleAssetProps {
|
||||
}
|
||||
export function CollectibleAsset({ icon, name, symbol }: CollectibleAssetProps) {
|
||||
return (
|
||||
<SpaceBetween py="base" px="base" width="100%">
|
||||
<Flex alignItems="center">
|
||||
{icon}
|
||||
<Text ml="tight" mr="extra-tight">
|
||||
{name}
|
||||
</Text>
|
||||
{symbol && <Text>({symbol.toUpperCase()})</Text>}
|
||||
</Flex>
|
||||
</SpaceBetween>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
border="1px solid #DCDDE2"
|
||||
borderRadius="10px"
|
||||
minHeight="64px"
|
||||
mb="base"
|
||||
mt="loose"
|
||||
px="base"
|
||||
width="100%"
|
||||
>
|
||||
<SpaceBetween>
|
||||
<Flex alignItems="center">
|
||||
{icon}
|
||||
<Text ml="tight" mr="extra-tight">
|
||||
{name}
|
||||
</Text>
|
||||
{symbol && <Text>({symbol.toUpperCase()})</Text>}
|
||||
</Flex>
|
||||
</SpaceBetween>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Box, Button } from '@stacks/ui';
|
||||
import { Box, Button, Flex } from '@stacks/ui';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { Form, Formik } from 'formik';
|
||||
|
||||
@@ -16,8 +16,6 @@ import { OrdinalIcon } from '@app/components/icons/ordinal-icon';
|
||||
import { getNumberOfInscriptionOnUtxo } from '@app/query/bitcoin/ordinals/utils';
|
||||
|
||||
import { BtcSizeFeeEstimator } from '../../../common/transactions/bitcoin/fees/btc-size-fee-estimator';
|
||||
import { FormErrors } from '../send-crypto-asset-form/components/form-errors';
|
||||
import { FormFieldsLayout } from '../send-crypto-asset-form/components/form-fields.layout';
|
||||
import { RecipientField } from '../send-crypto-asset-form/components/recipient-field';
|
||||
import { CollectibleAsset } from './components/collectible-asset';
|
||||
import { CollectiblePreviewCard } from './components/collectible-preview-card';
|
||||
@@ -107,12 +105,15 @@ export function SendInscriptionForm() {
|
||||
<Box px="extra-loose">
|
||||
<CollectiblePreviewCard inscription={inscription} mt="extra-loose" />
|
||||
<Box mt={['base', 'extra-loose', '100px']}>
|
||||
<FormFieldsLayout>
|
||||
<Flex flexDirection="column" mt="loose" width="100%">
|
||||
<CollectibleAsset icon={<OrdinalIcon />} name="Ordinal inscription" />
|
||||
<RecipientField name={recipeintFieldName} placeholder="Address" />
|
||||
</FormFieldsLayout>
|
||||
<RecipientField
|
||||
name={recipeintFieldName}
|
||||
label="To"
|
||||
placeholder="Enter recipient address"
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
<FormErrors />
|
||||
{currentError && (
|
||||
<ErrorLabel textAlign="left" mb="base-loose">
|
||||
{currentError}
|
||||
@@ -120,6 +121,7 @@ export function SendInscriptionForm() {
|
||||
)}
|
||||
<Button
|
||||
mb="extra-loose"
|
||||
mt="tight"
|
||||
type="submit"
|
||||
borderRadius="10px"
|
||||
height="48px"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { FormErrorMessages } from '@app/common/error-messages';
|
||||
import {
|
||||
btcAddressNetworkValidator,
|
||||
btcAddressValidator,
|
||||
@@ -14,7 +15,7 @@ export function useOrdinalInscriptionFormValidationSchema() {
|
||||
return yup.object({
|
||||
[recipeintFieldName]: yup
|
||||
.string()
|
||||
.required('Please provide a recipient')
|
||||
.required(FormErrorMessages.AddressRequired)
|
||||
.concat(btcAddressValidator())
|
||||
.concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.network))
|
||||
.concat(btcTaprootAddressValidator()),
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Money } from '@shared/models/money.model';
|
||||
|
||||
import { convertAmountToBaseUnit } from '@app/common/money/calculate-money';
|
||||
import { Caption } from '@app/components/typography';
|
||||
|
||||
export function AvailableBalance(props: { availableBalance: Money }) {
|
||||
const { availableBalance } = props;
|
||||
return (
|
||||
<Caption>
|
||||
Balance: {convertAmountToBaseUnit(availableBalance).toFormat()}{' '}
|
||||
{availableBalance.symbol.toUpperCase()}
|
||||
</Caption>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// TODO: Remove with send form design refactor
|
||||
import { useEffect, useState } from 'react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
|
||||
import { Box, Flex } from '@stacks/ui';
|
||||
import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors';
|
||||
import { FormikContextType, useFormikContext } from 'formik';
|
||||
|
||||
import { ErrorLabel } from '@app/components/error-label';
|
||||
|
||||
function omitAmountErrorsAsDisplayedElsewhere([key]: [string, unknown]) {
|
||||
return key !== 'amount';
|
||||
}
|
||||
|
||||
function shouldDisplayErrors(form: FormikContextType<unknown>) {
|
||||
return (
|
||||
Object.entries(form.touched)
|
||||
.filter(omitAmountErrorsAsDisplayedElsewhere)
|
||||
.map(([_key, value]) => value)
|
||||
.includes(true) && Object.keys(form.errors).length
|
||||
);
|
||||
}
|
||||
|
||||
const closedHeight = 24;
|
||||
const openHeight = 56;
|
||||
|
||||
export function FormErrors() {
|
||||
const [showHide, setShowHide] = useState(closedHeight);
|
||||
const form = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldDisplayErrors(form)) {
|
||||
setShowHide(openHeight);
|
||||
return;
|
||||
}
|
||||
setShowHide(closedHeight);
|
||||
}, [form]);
|
||||
|
||||
const [firstError] =
|
||||
Object.entries(form.errors).filter(omitAmountErrorsAsDisplayedElsewhere) ?? [];
|
||||
|
||||
const [field, message] = firstError ?? [];
|
||||
|
||||
const isFirstErrorFieldTouched = (form.touched as any)[field];
|
||||
|
||||
return message && isFirstErrorFieldTouched && shouldDisplayErrors(form) ? (
|
||||
<AnimateHeight duration={400} easing="ease-out" height={showHide}>
|
||||
<Flex height={openHeight + 'px'}>
|
||||
<ErrorLabel
|
||||
alignSelf="center"
|
||||
data-testid={SendCryptoAssetSelectors.FormFieldInputErrorLabel}
|
||||
>
|
||||
{firstError?.[1]}
|
||||
</ErrorLabel>
|
||||
</Flex>
|
||||
</AnimateHeight>
|
||||
) : (
|
||||
<Box height={closedHeight + 'px'}></Box>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Flex } from '@stacks/ui';
|
||||
|
||||
interface FormFieldsProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export function FormFieldsLayout({ children }: FormFieldsProps) {
|
||||
return (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
flexDirection="column"
|
||||
maxHeight={['calc(100vh - 116px)', 'calc(85vh - 116px)']}
|
||||
overflowY="scroll"
|
||||
pb={['92px', 'unset']}
|
||||
pt={['base', '48px']}
|
||||
px="loose"
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { Box, color } from '@stacks/ui';
|
||||
|
||||
import { Money } from '@shared/models/money.model';
|
||||
@@ -8,19 +6,18 @@ import { formatMoney } from '@app/common/money/format-money';
|
||||
import { whenPageMode } from '@app/common/utils';
|
||||
import { SpaceBetween } from '@app/components/layout/space-between';
|
||||
import { Caption } from '@app/components/typography';
|
||||
import { HighFeeDrawer } from '@app/features/high-fee-drawer/high-fee-drawer';
|
||||
|
||||
import { PreviewButton } from './preview-button';
|
||||
|
||||
export function Footer(props: { balance: Money; url: string }) {
|
||||
const { balance, url } = props;
|
||||
export function FormFooter(props: { balance: Money }) {
|
||||
const { balance } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg={color('bg')}
|
||||
borderTop="1px solid #DCDDE2"
|
||||
bottom="0px"
|
||||
height={['128px', '116px']}
|
||||
height={['106px', '116px']}
|
||||
position={whenPageMode({
|
||||
full: 'unset',
|
||||
popup: 'absolute',
|
||||
@@ -35,8 +32,6 @@ export function Footer(props: { balance: Money; url: string }) {
|
||||
<Caption>{formatMoney(balance)}</Caption>
|
||||
</SpaceBetween>
|
||||
</Box>
|
||||
<HighFeeDrawer learnMoreUrl={url} />
|
||||
<Outlet />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { TextInputField } from './text-input-field';
|
||||
|
||||
interface RecipientFieldProps {
|
||||
isDisabled?: boolean;
|
||||
label?: string;
|
||||
labelAction?: string;
|
||||
name: string;
|
||||
onBlur?(): void;
|
||||
@@ -13,6 +14,7 @@ interface RecipientFieldProps {
|
||||
}
|
||||
export function RecipientField({
|
||||
isDisabled,
|
||||
label,
|
||||
labelAction,
|
||||
name,
|
||||
onBlur,
|
||||
@@ -24,6 +26,7 @@ export function RecipientField({
|
||||
<TextInputField
|
||||
dataTestId={SendCryptoAssetSelectors.RecipientFieldInput}
|
||||
isDisabled={isDisabled}
|
||||
label={label}
|
||||
labelAction={labelAction}
|
||||
minHeight="76px"
|
||||
name={name}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useCallback } from 'react';
|
||||
import { FiCopy } from 'react-icons/fi';
|
||||
|
||||
import { Box, Stack, color, useClipboard } from '@stacks/ui';
|
||||
import { Box, Text, color, useClipboard } from '@stacks/ui';
|
||||
import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors';
|
||||
|
||||
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
|
||||
import { AddressDisplayer } from '@app/components/address-displayer/address-displayer';
|
||||
import { SpaceBetween } from '@app/components/layout/space-between';
|
||||
import { Tooltip } from '@app/components/tooltip';
|
||||
|
||||
interface BnsAddressDisplayerProps {
|
||||
interface RecipientAddressDisplayerProps {
|
||||
address: string;
|
||||
}
|
||||
export function RecipientBnsAddressDisplayer({ address }: BnsAddressDisplayerProps) {
|
||||
export function RecipientAddressDisplayer({ address }: RecipientAddressDisplayerProps) {
|
||||
const analytics = useAnalytics();
|
||||
const { onCopy, hasCopied } = useClipboard(address);
|
||||
|
||||
@@ -21,16 +21,14 @@ export function RecipientBnsAddressDisplayer({ address }: BnsAddressDisplayerPro
|
||||
}, [analytics, onCopy]);
|
||||
|
||||
return (
|
||||
<Stack alignItems="center" isInline mb="base">
|
||||
<Box
|
||||
<SpaceBetween mb="base" width="100%">
|
||||
<Text
|
||||
color={color('text-caption')}
|
||||
data-testid={SendCryptoAssetSelectors.RecipientBnsAddressLabel}
|
||||
display="flex"
|
||||
flexWrap="wrap"
|
||||
fontSize={1}
|
||||
justifyContent="flex-start"
|
||||
fontSize={0}
|
||||
>
|
||||
<AddressDisplayer address={address} />
|
||||
</Box>
|
||||
{address}
|
||||
</Text>
|
||||
<Tooltip hideOnClick={false} label={hasCopied ? 'Copied!' : 'Copy address'} placement="right">
|
||||
<Box
|
||||
_hover={{ cursor: 'pointer' }}
|
||||
@@ -38,12 +36,11 @@ export function RecipientBnsAddressDisplayer({ address }: BnsAddressDisplayerPro
|
||||
color={color('text-caption')}
|
||||
data-testid={SendCryptoAssetSelectors.RecipientBnsAddressCopyToClipboard}
|
||||
onClick={copyToClipboard}
|
||||
size="16px"
|
||||
type="button"
|
||||
>
|
||||
<FiCopy />
|
||||
<FiCopy size="16px" />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</SpaceBetween>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
import { logger } from '@shared/logger';
|
||||
import { BitcoinSendFormValues, StacksSendFormValues } from '@shared/models/form.model';
|
||||
|
||||
import { FormErrorMessages } from '@app/common/error-messages';
|
||||
import { StacksClient } from '@app/query/stacks/stacks-client';
|
||||
import { useStacksClientUnanchored } from '@app/store/common/api-clients.hooks';
|
||||
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
|
||||
|
||||
// Handles validating the BNS name lookup
|
||||
export function useRecipientBnsName() {
|
||||
const { setFieldError, setFieldValue, values } = useFormikContext<
|
||||
BitcoinSendFormValues | StacksSendFormValues
|
||||
>();
|
||||
const [bnsAddress, setBnsAddress] = useState('');
|
||||
const currentNetwork = useCurrentNetworkState();
|
||||
const client = useStacksClientUnanchored();
|
||||
|
||||
const getBnsAddressAndValidate = useCallback(
|
||||
async (
|
||||
fetchFn: (client: StacksClient, name: string, isTestnet?: boolean) => Promise<string | null>
|
||||
) => {
|
||||
setBnsAddress('');
|
||||
if (!values.recipientBnsName) return;
|
||||
|
||||
try {
|
||||
const owner = await fetchFn(client, values.recipientBnsName, currentNetwork.isTestnet);
|
||||
if (owner) {
|
||||
setBnsAddress(owner);
|
||||
setFieldError('recipient', undefined);
|
||||
setFieldValue('recipient', owner);
|
||||
} else {
|
||||
setFieldError('recipientBnsName', FormErrorMessages.BnsAddressNotFound);
|
||||
}
|
||||
} catch (e) {
|
||||
setFieldError('recipientBnsName', FormErrorMessages.BnsAddressNotFound);
|
||||
logger.error('Error fetching bns address', e);
|
||||
}
|
||||
},
|
||||
[client, currentNetwork.isTestnet, setFieldError, setFieldValue, values.recipientBnsName]
|
||||
);
|
||||
|
||||
return { bnsAddress, getBnsAddressAndValidate, setBnsAddress };
|
||||
}
|
||||
@@ -3,19 +3,20 @@ import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
import { StacksSendFormValues } from '@shared/models/form.model';
|
||||
import { BitcoinSendFormValues, StacksSendFormValues } from '@shared/models/form.model';
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
|
||||
import { RecipientFieldType } from '@app/pages/send/send-crypto-asset-form/components/recipient-select/recipient-select';
|
||||
|
||||
import { useStacksRecipientBnsName } from './use-stacks-recipient-bns-name';
|
||||
import { useRecipientBnsName } from './use-recipient-bns-name';
|
||||
|
||||
export function useStacksRecipientField() {
|
||||
const { setFieldError, setFieldTouched, setFieldValue } =
|
||||
useFormikContext<StacksSendFormValues>();
|
||||
export function useRecipientSelectFields() {
|
||||
const { setFieldError, setFieldTouched, setFieldValue } = useFormikContext<
|
||||
BitcoinSendFormValues | StacksSendFormValues
|
||||
>();
|
||||
const [selectedRecipientField, setSelectedRecipientField] = useState(RecipientFieldType.Address);
|
||||
const [isSelectVisible, setIsSelectVisible] = useState(false);
|
||||
const { setBnsAddress } = useStacksRecipientBnsName();
|
||||
const { setBnsAddress } = useRecipientBnsName();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onClickLabelAction = useCallback(() => {
|
||||
@@ -2,27 +2,32 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
import { StacksSendFormValues } from '@shared/models/form.model';
|
||||
import { BitcoinSendFormValues, StacksSendFormValues } from '@shared/models/form.model';
|
||||
|
||||
import { RecipientField } from '@app/pages/send/send-crypto-asset-form/components/recipient-field';
|
||||
import { StacksClient } from '@app/query/stacks/stacks-client';
|
||||
|
||||
import { useStacksRecipientBnsName } from '../hooks/use-stacks-recipient-bns-name';
|
||||
import { RecipientBnsAddressDisplayer } from './recipient-bns-address-displayer';
|
||||
import { RecipientAddressDisplayer } from './components/recipient-address-displayer';
|
||||
import { useRecipientBnsName } from './hooks/use-recipient-bns-name';
|
||||
|
||||
interface RecipientFieldBnsNameProps {
|
||||
fetchFn(client: StacksClient, name: string, isTestnet?: boolean): Promise<string | null>;
|
||||
isSelectVisible: boolean;
|
||||
onClickLabelAction(): void;
|
||||
selectedRecipientField: number;
|
||||
topInputOverlay: JSX.Element;
|
||||
}
|
||||
export function RecipientFieldBnsName({
|
||||
fetchFn,
|
||||
isSelectVisible,
|
||||
onClickLabelAction,
|
||||
topInputOverlay,
|
||||
}: RecipientFieldBnsNameProps) {
|
||||
const [showBnsAddress, setShowBnsAddress] = useState(false);
|
||||
const { errors, setFieldError, values } = useFormikContext<StacksSendFormValues>();
|
||||
const { bnsAddress, getBnsAddressAndValidate } = useStacksRecipientBnsName();
|
||||
const { errors, setFieldError, values } = useFormikContext<
|
||||
BitcoinSendFormValues | StacksSendFormValues
|
||||
>();
|
||||
const { bnsAddress, getBnsAddressAndValidate } = useRecipientBnsName();
|
||||
|
||||
// Apply the recipient field validation to the bns name field
|
||||
// here so we don't need to validate the bns name on blur.
|
||||
@@ -46,12 +51,12 @@ export function RecipientFieldBnsName({
|
||||
isDisabled={isSelectVisible}
|
||||
labelAction="Select account"
|
||||
name="recipientBnsName"
|
||||
onBlur={getBnsAddressAndValidate}
|
||||
onBlur={() => getBnsAddressAndValidate(fetchFn)}
|
||||
onClickLabelAction={onClickLabelAction}
|
||||
placeholder="Enter recipient BNS name"
|
||||
topInputOverlay={topInputOverlay}
|
||||
/>
|
||||
{showBnsAddress ? <RecipientBnsAddressDisplayer address={bnsAddress} /> : null}
|
||||
{showBnsAddress ? <RecipientAddressDisplayer address={bnsAddress} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { FiChevronDown } from 'react-icons/fi';
|
||||
import { Box, Text, color } from '@stacks/ui';
|
||||
|
||||
const labels = ['Address', 'BNS Name'];
|
||||
const testLabels = ['address', 'bns-name'];
|
||||
|
||||
interface RecipientSelectItemProps {
|
||||
index: number;
|
||||
@@ -18,6 +19,7 @@ export function RecipientSelectItem(props: RecipientSelectItemProps) {
|
||||
alignItems="center"
|
||||
as="button"
|
||||
bg={color('bg')}
|
||||
data-testid={`recipient-select-field-${testLabels[index]}`}
|
||||
display="flex"
|
||||
height="32px"
|
||||
mb="0px !important"
|
||||
@@ -26,10 +28,16 @@ export function RecipientSelectItem(props: RecipientSelectItemProps) {
|
||||
pl={isVisible ? 'tight' : 'unset'}
|
||||
type="button"
|
||||
>
|
||||
<Text color={color('text-caption')} fontSize={1} fontWeight={500} ml="2px" mr="tight">
|
||||
<Text
|
||||
color={isVisible ? color('text-body') : color('accent')}
|
||||
fontSize={1}
|
||||
fontWeight={isVisible ? 400 : 500}
|
||||
ml="2px"
|
||||
mr="tight"
|
||||
>
|
||||
{labels[index]}
|
||||
</Text>
|
||||
{isVisible ? <></> : <FiChevronDown />}
|
||||
{isVisible ? <></> : <FiChevronDown color={color('accent')} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RecipientSelectItem } from './recipient-select-item';
|
||||
import { RecipientSelectLayout } from './recipient-select.layout';
|
||||
import { RecipientSelectItem } from './components/recipient-select-item';
|
||||
import { RecipientSelectLayout } from './components/recipient-select.layout';
|
||||
|
||||
export enum RecipientFieldType {
|
||||
Address,
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { Box } from '@stacks/ui';
|
||||
import { Flex } from '@stacks/ui';
|
||||
import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors';
|
||||
|
||||
interface SendCryptoAssetFormLayoutProps {
|
||||
children: JSX.Element;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export function SendCryptoAssetFormLayout({ children }: SendCryptoAssetFormLayoutProps) {
|
||||
return (
|
||||
<Box data-testid={SendCryptoAssetSelectors.SendForm} pb="base" width="100%">
|
||||
<Flex
|
||||
alignItems="center"
|
||||
data-testId={SendCryptoAssetSelectors.SendForm}
|
||||
flexDirection="column"
|
||||
maxHeight={['calc(100vh - 116px)', 'calc(85vh - 116px)']}
|
||||
overflowY="scroll"
|
||||
pb={['120px', '48px']}
|
||||
pt={['base', '48px']}
|
||||
px="loose"
|
||||
width="100%"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export function TextInputField({
|
||||
as="button"
|
||||
color={color('accent')}
|
||||
fontSize={1}
|
||||
fontWeight={500}
|
||||
onClick={onClickLabelAction}
|
||||
type="button"
|
||||
zIndex={999}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { RecipientFieldType } from '@app/pages/send/send-crypto-asset-form/components/recipient-select/recipient-select';
|
||||
import { fetchBtcNameOwner } from '@app/query/stacks/bns/bns.utils';
|
||||
|
||||
import { useRecipientSelectFields } from '../../../components/recipient-select-fields/hooks/use-recipient-select-fields';
|
||||
import { RecipientFieldAddress } from '../../../components/recipient-select-fields/recipient-field-address';
|
||||
import { RecipientFieldBnsName } from '../../../components/recipient-select-fields/recipient-field-bns-name';
|
||||
import { RecipientSelectOverlay } from '../../../components/recipient-select/components/recipient-select-overlay';
|
||||
|
||||
export function BitcoinRecipientField() {
|
||||
const {
|
||||
isSelectVisible,
|
||||
onClickLabelAction,
|
||||
onSelectRecipientFieldType,
|
||||
onSetIsSelectVisible,
|
||||
selectedRecipientField,
|
||||
} = useRecipientSelectFields();
|
||||
|
||||
const topInputOverlay = (
|
||||
<RecipientSelectOverlay
|
||||
isSelectVisible={isSelectVisible}
|
||||
onSelectRecipientFieldType={onSelectRecipientFieldType}
|
||||
onSetIsSelectVisible={onSetIsSelectVisible}
|
||||
selectedRecipientField={selectedRecipientField}
|
||||
/>
|
||||
);
|
||||
|
||||
const recipientFieldAddress = (
|
||||
<RecipientFieldAddress
|
||||
isSelectVisible={isSelectVisible}
|
||||
onClickLabelAction={onClickLabelAction}
|
||||
selectedRecipientField={selectedRecipientField}
|
||||
topInputOverlay={topInputOverlay}
|
||||
/>
|
||||
);
|
||||
|
||||
switch (selectedRecipientField) {
|
||||
case RecipientFieldType.Address:
|
||||
return recipientFieldAddress;
|
||||
case RecipientFieldType.BnsName:
|
||||
return (
|
||||
<RecipientFieldBnsName
|
||||
fetchFn={fetchBtcNameOwner}
|
||||
isSelectVisible={isSelectVisible}
|
||||
onClickLabelAction={onClickLabelAction}
|
||||
selectedRecipientField={selectedRecipientField}
|
||||
topInputOverlay={topInputOverlay}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return recipientFieldAddress;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { WarningLabel } from '@app/components/warning-label';
|
||||
|
||||
export function TestnetBtcMessage() {
|
||||
return (
|
||||
<WarningLabel mt="base-loose" width="100%">
|
||||
<WarningLabel mb="base">
|
||||
This is a Bitcoin testnet transaction. Funds have no value.{' '}
|
||||
<ExternalLink
|
||||
href="https://coinfaucet.eu/en/btc-testnet"
|
||||
@@ -1,9 +1,10 @@
|
||||
import { RecipientFieldType } from '@app/pages/send/send-crypto-asset-form/components/recipient-select/recipient-select';
|
||||
import { fetchNameOwner } from '@app/query/stacks/bns/bns.utils';
|
||||
|
||||
import { RecipientFieldAddress } from './components/recipient-field-address';
|
||||
import { RecipientFieldBnsName } from './components/recipient-field-bns-name';
|
||||
import { RecipientSelectOverlay } from './components/recipient-select-overlay';
|
||||
import { useStacksRecipientField } from './hooks/use-stacks-recipient-field';
|
||||
import { useRecipientSelectFields } from '../../../components/recipient-select-fields/hooks/use-recipient-select-fields';
|
||||
import { RecipientFieldAddress } from '../../../components/recipient-select-fields/recipient-field-address';
|
||||
import { RecipientFieldBnsName } from '../../../components/recipient-select-fields/recipient-field-bns-name';
|
||||
import { RecipientSelectOverlay } from '../../../components/recipient-select/components/recipient-select-overlay';
|
||||
|
||||
export function StacksRecipientField() {
|
||||
const {
|
||||
@@ -12,7 +13,7 @@ export function StacksRecipientField() {
|
||||
onSelectRecipientFieldType,
|
||||
onSetIsSelectVisible,
|
||||
selectedRecipientField,
|
||||
} = useStacksRecipientField();
|
||||
} = useRecipientSelectFields();
|
||||
|
||||
const topInputOverlay = (
|
||||
<RecipientSelectOverlay
|
||||
@@ -38,6 +39,7 @@ export function StacksRecipientField() {
|
||||
case RecipientFieldType.BnsName:
|
||||
return (
|
||||
<RecipientFieldBnsName
|
||||
fetchFn={fetchNameOwner}
|
||||
isSelectVisible={isSelectVisible}
|
||||
onClickLabelAction={onClickLabelAction}
|
||||
selectedRecipientField={selectedRecipientField}
|
||||
@@ -1,61 +0,0 @@
|
||||
// TODO: Remove with old recipient field in btc form
|
||||
import { useCallback } from 'react';
|
||||
import { FiCopy, FiInfo } from 'react-icons/fi';
|
||||
|
||||
import { Box, Stack, Text, Tooltip, color, useClipboard } from '@stacks/ui';
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors';
|
||||
|
||||
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
|
||||
|
||||
export function RecipientFieldBnsAddress(props: { bnsAddress: string }) {
|
||||
const { bnsAddress } = props;
|
||||
const analytics = useAnalytics();
|
||||
const { onCopy, hasCopied } = useClipboard(bnsAddress);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
void analytics.track('copy_resolved_address_to_clipboard');
|
||||
onCopy();
|
||||
}, [analytics, onCopy]);
|
||||
|
||||
const onHover = useCallback(
|
||||
() => analytics.track('view_resolved_recipient_address'),
|
||||
[analytics]
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack isInline spacing="tight" zIndex={999}>
|
||||
<Text
|
||||
color={color('text-caption')}
|
||||
data-testid={SendCryptoAssetSelectors.RecipientBnsAddressLabel}
|
||||
fontSize={0}
|
||||
>
|
||||
{truncateMiddle(bnsAddress, 4)}
|
||||
</Text>
|
||||
<Tooltip label={bnsAddress} maxWidth="none" placement="bottom">
|
||||
<Stack>
|
||||
<Box
|
||||
_hover={{ cursor: 'pointer' }}
|
||||
as={FiInfo}
|
||||
color={color('text-caption')}
|
||||
data-testid={SendCryptoAssetSelectors.RecipientBnsAddressInfoIcon}
|
||||
onMouseOver={onHover}
|
||||
size="12px"
|
||||
/>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
<Tooltip label={hasCopied ? 'Copied!' : 'Copy address'} placement="right">
|
||||
<Stack>
|
||||
<Box
|
||||
_hover={{ cursor: 'pointer' }}
|
||||
as={FiCopy}
|
||||
color={color('text-caption')}
|
||||
data-testid={SendCryptoAssetSelectors.RecipientBnsAddressCopyToClipboard}
|
||||
onClick={copyToClipboard}
|
||||
size="12px"
|
||||
/>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
import { logger } from '@shared/logger';
|
||||
import { StacksSendFormValues } from '@shared/models/form.model';
|
||||
|
||||
import { FormErrorMessages } from '@app/common/error-messages';
|
||||
import { fetchNameOwner } from '@app/query/stacks/bns/bns.utils';
|
||||
import { useStacksClientUnanchored } from '@app/store/common/api-clients.hooks';
|
||||
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
|
||||
|
||||
// Handles validating the BNS name lookup
|
||||
export function useStacksRecipientBnsName() {
|
||||
const { setFieldError, setFieldValue, values } = useFormikContext<StacksSendFormValues>();
|
||||
const [bnsAddress, setBnsAddress] = useState('');
|
||||
const currentNetwork = useCurrentNetworkState();
|
||||
const client = useStacksClientUnanchored();
|
||||
|
||||
const getBnsAddressAndValidate = useCallback(async () => {
|
||||
setBnsAddress('');
|
||||
if (!values.recipientBnsName) return;
|
||||
|
||||
try {
|
||||
const owner = await fetchNameOwner(client, values.recipientBnsName, currentNetwork.isTestnet);
|
||||
if (owner) {
|
||||
setBnsAddress(owner);
|
||||
setFieldError('recipient', undefined);
|
||||
setFieldValue('recipient', owner);
|
||||
} else {
|
||||
setFieldError('recipientBnsName', FormErrorMessages.BnsAddressNotFound);
|
||||
}
|
||||
} catch (e) {
|
||||
setFieldError('recipientBnsName', FormErrorMessages.BnsAddressNotFound);
|
||||
logger.error('Error fetching bns address', e);
|
||||
}
|
||||
}, [client, currentNetwork.isTestnet, setFieldError, setFieldValue, values.recipientBnsName]);
|
||||
|
||||
return { bnsAddress, getBnsAddressAndValidate, setBnsAddress };
|
||||
}
|
||||
@@ -12,19 +12,15 @@ import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/marke
|
||||
import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
|
||||
import { AmountField } from '../../components/amount-field';
|
||||
import { AvailableBalance } from '../../components/available-balance';
|
||||
import { FormErrors } from '../../components/form-errors';
|
||||
import { FormFieldsLayout } from '../../components/form-fields.layout';
|
||||
import { PreviewButton } from '../../components/preview-button';
|
||||
import { FormFooter } from '../../components/form-footer';
|
||||
import { SelectedAssetField } from '../../components/selected-asset-field';
|
||||
import { SendCryptoAssetFormLayout } from '../../components/send-crypto-asset-form.layout';
|
||||
import { SendFiatValue } from '../../components/send-fiat-value';
|
||||
import { SendMaxButton } from '../../components/send-max-button';
|
||||
import { useCalculateMaxBitcoinSpend } from '../../family/bitcoin/hooks/use-calculate-max-spend';
|
||||
import { BitcoinRecipientField } from '../../family/bitcoin/components/bitcoin-recipient-field';
|
||||
import { TestnetBtcMessage } from '../../family/bitcoin/components/testnet-btc-message';
|
||||
import { useSendFormRouteState } from '../../hooks/use-send-form-route-state';
|
||||
import { createDefaultInitialFormValues, defaultSendFormFormikProps } from '../../send-form.utils';
|
||||
import { BtcRecipientField } from './components/btc-recipient-field';
|
||||
import { TestnetBtcMessage } from './components/testnet-btc-message';
|
||||
import { useBtcSendForm } from './use-btc-send-form';
|
||||
|
||||
export function BtcSendForm() {
|
||||
@@ -34,17 +30,21 @@ export function BtcSendForm() {
|
||||
const currentAccountBtcAddress = useCurrentBtcNativeSegwitAccountAddressIndexZero();
|
||||
const btcBalance = useNativeSegwitBalance(currentAccountBtcAddress);
|
||||
|
||||
const calcMaxSpend = useCalculateMaxBitcoinSpend();
|
||||
|
||||
const { validationSchema, currentNetwork, formRef, previewTransaction, onFormStateChange } =
|
||||
useBtcSendForm();
|
||||
const {
|
||||
calcMaxSpend,
|
||||
currentNetwork,
|
||||
formRef,
|
||||
onFormStateChange,
|
||||
previewTransaction,
|
||||
validationSchema,
|
||||
} = useBtcSendForm();
|
||||
|
||||
return (
|
||||
<SendCryptoAssetFormLayout>
|
||||
<Box width="100%" pb="base">
|
||||
<Formik
|
||||
initialValues={createDefaultInitialFormValues({
|
||||
...routeState,
|
||||
recipientAddressOrBnsName: '',
|
||||
recipientBnsName: '',
|
||||
})}
|
||||
onSubmit={previewTransaction}
|
||||
validationSchema={validationSchema}
|
||||
@@ -55,36 +55,33 @@ export function BtcSendForm() {
|
||||
onFormStateChange(props.values);
|
||||
return (
|
||||
<Form>
|
||||
<AmountField
|
||||
balance={btcBalance.balance}
|
||||
switchableAmount={<SendFiatValue marketData={btcMarketData} assetSymbol={'BTC'} />}
|
||||
bottomInputOverlay={
|
||||
<SendMaxButton
|
||||
balance={btcBalance.balance}
|
||||
sendMaxBalance={
|
||||
calcMaxSpend(props.values.recipient)?.spendableBitcoin.toString() ?? '0'
|
||||
}
|
||||
/>
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FormFieldsLayout>
|
||||
<SendCryptoAssetFormLayout>
|
||||
<AmountField
|
||||
balance={btcBalance.balance}
|
||||
switchableAmount={
|
||||
<SendFiatValue marketData={btcMarketData} assetSymbol={'BTC'} />
|
||||
}
|
||||
bottomInputOverlay={
|
||||
<SendMaxButton
|
||||
balance={btcBalance.balance}
|
||||
sendMaxBalance={
|
||||
calcMaxSpend(props.values.recipient)?.spendableBitcoin.toString() ?? '0'
|
||||
}
|
||||
/>
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<SelectedAssetField icon={<BtcIcon />} name={btcBalance.asset.name} symbol="BTC" />
|
||||
{/* TODO: Implement new recipient field here */}
|
||||
<BtcRecipientField />
|
||||
</FormFieldsLayout>
|
||||
{currentNetwork.chain.bitcoin.network === 'testnet' && <TestnetBtcMessage />}
|
||||
<FormErrors />
|
||||
<PreviewButton />
|
||||
<Box my="base">
|
||||
<AvailableBalance availableBalance={btcBalance.balance} />
|
||||
</Box>
|
||||
<BitcoinRecipientField />
|
||||
{currentNetwork.chain.bitcoin.network === 'testnet' && <TestnetBtcMessage />}
|
||||
</SendCryptoAssetFormLayout>
|
||||
<FormFooter balance={btcBalance.balance} />
|
||||
<HighFeeDrawer learnMoreUrl={HIGH_FEE_WARNING_LEARN_MORE_URL_BTC} />
|
||||
<Outlet />
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</SendCryptoAssetFormLayout>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useField } from 'formik';
|
||||
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
|
||||
import { fetchBtcNameOwner } from '@app/query/stacks/bns/bns.utils';
|
||||
import { useStacksClientUnanchored } from '@app/store/common/api-clients.hooks';
|
||||
|
||||
import { RecipientField } from '../../../components/recipient-field';
|
||||
import { RecipientFieldBnsAddress } from '../../../family/stacks/components/stacks-recipient-field/components/recipient-bns-address';
|
||||
|
||||
export function BtcRecipientField() {
|
||||
const client = useStacksClientUnanchored();
|
||||
const [recipientAddressOrBnsNameField] = useField('recipientAddressOrBnsName');
|
||||
const [, _, recipientFieldHelpers] = useField('recipient');
|
||||
const navigate = useNavigate();
|
||||
const [bnsAddress, setBnsAddress] = useState('');
|
||||
const [lastValidatedInput, setLastValidatedInput] = useState('');
|
||||
|
||||
const getBtcAddressFromBns = useCallback(async () => {
|
||||
// Skip if this input was already handled
|
||||
if (lastValidatedInput === recipientAddressOrBnsNameField.value) return;
|
||||
|
||||
setBnsAddress('');
|
||||
setLastValidatedInput(recipientAddressOrBnsNameField.value);
|
||||
try {
|
||||
const btcFromBns = await fetchBtcNameOwner(client, recipientAddressOrBnsNameField.value);
|
||||
if (btcFromBns) {
|
||||
recipientFieldHelpers.setValue(btcFromBns);
|
||||
setBnsAddress(btcFromBns);
|
||||
} else {
|
||||
recipientFieldHelpers.setValue(recipientAddressOrBnsNameField.value);
|
||||
}
|
||||
} catch (error) {
|
||||
recipientFieldHelpers.setValue(recipientAddressOrBnsNameField.value);
|
||||
}
|
||||
}, [
|
||||
client,
|
||||
recipientAddressOrBnsNameField,
|
||||
recipientFieldHelpers,
|
||||
lastValidatedInput,
|
||||
setLastValidatedInput,
|
||||
]);
|
||||
|
||||
const onClickLabel = () => {
|
||||
setBnsAddress('');
|
||||
navigate(RouteUrls.SendCryptoAssetFormRecipientAccounts);
|
||||
};
|
||||
|
||||
return (
|
||||
<RecipientField
|
||||
labelAction="Choose account"
|
||||
name="recipientAddressOrBnsName"
|
||||
onBlur={getBtcAddressFromBns}
|
||||
onClickLabelAction={onClickLabel}
|
||||
placeholder="Address"
|
||||
topInputOverlay={
|
||||
!!bnsAddress ? <RecipientFieldBnsAddress bnsAddress={bnsAddress} /> : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -21,20 +21,17 @@ import {
|
||||
btcMinimumSpendValidator,
|
||||
} from '@app/common/validation/forms/amount-validators';
|
||||
import { btcAmountPrecisionValidator } from '@app/common/validation/forms/currency-validators';
|
||||
import { btcRecipientAddressOrBnsNameValidator } from '@app/common/validation/forms/recipient-validators';
|
||||
import { useUpdatePersistedSendFormValues } from '@app/features/popup-send-form-restoration/use-update-persisted-send-form-values';
|
||||
import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query';
|
||||
import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
import { useStacksClientUnanchored } from '@app/store/common/api-clients.hooks';
|
||||
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
|
||||
|
||||
import { useCalculateMaxBitcoinSpend } from '../../family/bitcoin/hooks/use-calculate-max-spend';
|
||||
import { useGenerateSignedBitcoinTx } from '../../family/bitcoin/hooks/use-generate-bitcoin-tx';
|
||||
import { useSendFormNavigate } from '../../hooks/use-send-form-navigate';
|
||||
import { useGenerateSignedBitcoinTx } from './use-generate-bitcoin-tx';
|
||||
|
||||
export function useBtcSendForm() {
|
||||
const formRef = useRef<FormikProps<BitcoinSendFormValues>>(null);
|
||||
|
||||
const currentNetwork = useCurrentNetwork();
|
||||
const currentAccountBtcAddress = useCurrentBtcNativeSegwitAccountAddressIndexZero();
|
||||
const btcCryptoCurrencyAssetBalance = useNativeSegwitBalance(currentAccountBtcAddress);
|
||||
@@ -44,14 +41,12 @@ export function useBtcSendForm() {
|
||||
const generateTx = useGenerateSignedBitcoinTx();
|
||||
const calcMaxSpend = useCalculateMaxBitcoinSpend();
|
||||
const { onFormStateChange } = useUpdatePersistedSendFormValues();
|
||||
const client = useStacksClientUnanchored();
|
||||
|
||||
return {
|
||||
formRef,
|
||||
|
||||
onFormStateChange,
|
||||
|
||||
calcMaxSpend,
|
||||
currentNetwork,
|
||||
formRef,
|
||||
onFormStateChange,
|
||||
|
||||
validationSchema: yup.object({
|
||||
amount: yup
|
||||
@@ -68,10 +63,6 @@ export function useBtcSendForm() {
|
||||
})
|
||||
)
|
||||
.concat(btcMinimumSpendValidator()),
|
||||
// TODO: Implement new recipient field here
|
||||
recipientAddressOrBnsName: btcRecipientAddressOrBnsNameValidator({
|
||||
client,
|
||||
}),
|
||||
recipient: yup
|
||||
.string()
|
||||
.concat(btcAddressValidator())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import { Navigate, Outlet, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Box } from '@stacks/ui';
|
||||
import { Form, Formik } from 'formik';
|
||||
|
||||
import { HIGH_FEE_WARNING_LEARN_MORE_URL_STX } from '@shared/constants';
|
||||
@@ -10,16 +11,16 @@ import { StxAvatar } from '@app/components/crypto-assets/stacks/components/stx-a
|
||||
import { EditNonceButton } from '@app/components/edit-nonce-button';
|
||||
import { FeesRow } from '@app/components/fees-row/fees-row';
|
||||
import { NonceSetter } from '@app/components/nonce-setter';
|
||||
import { HighFeeDrawer } from '@app/features/high-fee-drawer/high-fee-drawer';
|
||||
import { useUpdatePersistedSendFormValues } from '@app/features/popup-send-form-restoration/use-update-persisted-send-form-values';
|
||||
|
||||
import { AmountField } from '../../components/amount-field';
|
||||
import { Footer } from '../../components/footer';
|
||||
import { FormFieldsLayout } from '../../components/form-fields.layout';
|
||||
import { FormFooter } from '../../components/form-footer';
|
||||
import { MemoField } from '../../components/memo-field';
|
||||
import { SelectedAssetField } from '../../components/selected-asset-field';
|
||||
import { SendCryptoAssetFormLayout } from '../../components/send-crypto-asset-form.layout';
|
||||
import { SendMaxButton } from '../../components/send-max-button';
|
||||
import { StacksRecipientField } from '../../family/stacks/components/stacks-recipient-field/stacks-recipient-field';
|
||||
import { StacksRecipientField } from '../../family/stacks/components/stacks-recipient-field';
|
||||
import { defaultSendFormFormikProps } from '../../send-form.utils';
|
||||
import { useSip10SendForm } from './use-sip10-send-form';
|
||||
|
||||
@@ -42,7 +43,7 @@ export function StacksSip10FungibleTokenSendForm({}) {
|
||||
}
|
||||
|
||||
return (
|
||||
<SendCryptoAssetFormLayout>
|
||||
<Box width="100%" pb="base">
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={async (values, formikHelpers) => await previewTransaction(values, formikHelpers)}
|
||||
@@ -54,7 +55,7 @@ export function StacksSip10FungibleTokenSendForm({}) {
|
||||
return (
|
||||
<NonceSetter>
|
||||
<Form>
|
||||
<FormFieldsLayout>
|
||||
<SendCryptoAssetFormLayout>
|
||||
<AmountField
|
||||
balance={availableTokenBalance}
|
||||
bottomInputOverlay={
|
||||
@@ -70,17 +71,18 @@ export function StacksSip10FungibleTokenSendForm({}) {
|
||||
<FeesRow fees={stacksFtFees} isSponsored={false} mt="base" />
|
||||
<EditNonceButton
|
||||
alignSelf="flex-end"
|
||||
mb="extra-loose"
|
||||
mt="base"
|
||||
onEditNonce={() => navigate(RouteUrls.EditNonce)}
|
||||
/>
|
||||
</FormFieldsLayout>
|
||||
<Footer balance={availableTokenBalance} url={HIGH_FEE_WARNING_LEARN_MORE_URL_STX} />
|
||||
</SendCryptoAssetFormLayout>
|
||||
<FormFooter balance={availableTokenBalance} />
|
||||
<HighFeeDrawer learnMoreUrl={HIGH_FEE_WARNING_LEARN_MORE_URL_STX} />
|
||||
<Outlet />
|
||||
</Form>
|
||||
</NonceSetter>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</SendCryptoAssetFormLayout>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ import {
|
||||
useGenerateFtTokenTransferUnsignedTx,
|
||||
} from '@app/store/transactions/token-transfer.hooks';
|
||||
|
||||
import { useStacksFtRouteState } from '../../family/stacks/hooks/use-stacks-ft-params';
|
||||
import { useSendFormNavigate } from '../../hooks/use-send-form-navigate';
|
||||
import { useSendFormRouteState } from '../../hooks/use-send-form-route-state';
|
||||
import { createDefaultInitialFormValues } from '../../send-form.utils';
|
||||
import { useStacksFtRouteState } from './use-stacks-ft-params';
|
||||
|
||||
export function useSip10SendForm() {
|
||||
const [contractId, setContractId] = useState('');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Box } from '@stacks/ui';
|
||||
import { Form, Formik } from 'formik';
|
||||
|
||||
import { HIGH_FEE_WARNING_LEARN_MORE_URL_STX } from '@shared/constants';
|
||||
@@ -9,17 +10,17 @@ import { StxAvatar } from '@app/components/crypto-assets/stacks/components/stx-a
|
||||
import { EditNonceButton } from '@app/components/edit-nonce-button';
|
||||
import { FeesRow } from '@app/components/fees-row/fees-row';
|
||||
import { NonceSetter } from '@app/components/nonce-setter';
|
||||
import { HighFeeDrawer } from '@app/features/high-fee-drawer/high-fee-drawer';
|
||||
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
|
||||
|
||||
import { AmountField } from '../../components/amount-field';
|
||||
import { Footer } from '../../components/footer';
|
||||
import { FormFieldsLayout } from '../../components/form-fields.layout';
|
||||
import { FormFooter } from '../../components/form-footer';
|
||||
import { MemoField } from '../../components/memo-field';
|
||||
import { SelectedAssetField } from '../../components/selected-asset-field';
|
||||
import { SendCryptoAssetFormLayout } from '../../components/send-crypto-asset-form.layout';
|
||||
import { SendFiatValue } from '../../components/send-fiat-value';
|
||||
import { SendMaxButton } from '../../components/send-max-button';
|
||||
import { StacksRecipientField } from '../../family/stacks/components/stacks-recipient-field/stacks-recipient-field';
|
||||
import { StacksRecipientField } from '../../family/stacks/components/stacks-recipient-field';
|
||||
import { defaultSendFormFormikProps } from '../../send-form.utils';
|
||||
import { useStxSendForm } from './use-stx-send-form';
|
||||
|
||||
@@ -38,7 +39,7 @@ export function StxSendForm() {
|
||||
} = useStxSendForm();
|
||||
|
||||
return (
|
||||
<SendCryptoAssetFormLayout>
|
||||
<Box width="100%" pb="base">
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={previewTransaction}
|
||||
@@ -50,7 +51,7 @@ export function StxSendForm() {
|
||||
return (
|
||||
<NonceSetter>
|
||||
<Form>
|
||||
<FormFieldsLayout>
|
||||
<SendCryptoAssetFormLayout>
|
||||
<AmountField
|
||||
balance={availableStxBalance}
|
||||
switchableAmount={
|
||||
@@ -70,17 +71,18 @@ export function StxSendForm() {
|
||||
<FeesRow fees={stxFees} isSponsored={false} mt="tight" />
|
||||
<EditNonceButton
|
||||
alignSelf="flex-end"
|
||||
mb="extra-loose"
|
||||
mt="base"
|
||||
onEditNonce={() => navigate(RouteUrls.EditNonce)}
|
||||
/>
|
||||
</FormFieldsLayout>
|
||||
<Footer balance={availableStxBalance} url={HIGH_FEE_WARNING_LEARN_MORE_URL_STX} />
|
||||
</SendCryptoAssetFormLayout>
|
||||
<FormFooter balance={availableStxBalance} />
|
||||
<HighFeeDrawer learnMoreUrl={HIGH_FEE_WARNING_LEARN_MORE_URL_STX} />
|
||||
<Outlet />
|
||||
</Form>
|
||||
</NonceSetter>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</SendCryptoAssetFormLayout>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface BitcoinSendFormValues {
|
||||
feeType: string;
|
||||
memo: string;
|
||||
recipient: string;
|
||||
recipientBnsName: string;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors';
|
||||
import { SendCryptoAssetSelectors } from '@tests/selectors/send.selectors';
|
||||
import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors';
|
||||
import { createTestSelector } from '@tests/utils';
|
||||
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
|
||||
@@ -16,13 +14,13 @@ export class SendPage {
|
||||
readonly memoInput: Locator;
|
||||
readonly previewSendTxButton: Locator;
|
||||
readonly recipientChooseAccountButton: Locator;
|
||||
readonly recipientSelectFieldAddress: Locator;
|
||||
readonly recipientSelectFieldBnsName: Locator;
|
||||
readonly recipientInput: Locator;
|
||||
readonly recipientBnsAddressLabel: Locator;
|
||||
readonly recipientBnsAddressInfoIcon: Locator;
|
||||
readonly sendMaxButton: Locator;
|
||||
readonly feesRow: Locator;
|
||||
readonly memoRow: Locator;
|
||||
readonly feesSelector: string = createTestSelector(SharedComponentsSelectors.FeeRow);
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
@@ -40,13 +38,16 @@ export class SendPage {
|
||||
this.recipientChooseAccountButton = page.getByTestId(
|
||||
SendCryptoAssetSelectors.RecipientChooseAccountButton
|
||||
);
|
||||
this.recipientSelectFieldAddress = this.page.getByTestId(
|
||||
SendCryptoAssetSelectors.RecipientSelectFieldAddress
|
||||
);
|
||||
this.recipientSelectFieldBnsName = this.page.getByTestId(
|
||||
SendCryptoAssetSelectors.RecipientSelectFieldBnsName
|
||||
);
|
||||
this.recipientInput = this.page.getByTestId(SendCryptoAssetSelectors.RecipientFieldInput);
|
||||
this.recipientBnsAddressLabel = this.page.getByTestId(
|
||||
SendCryptoAssetSelectors.RecipientBnsAddressLabel
|
||||
);
|
||||
this.recipientBnsAddressInfoIcon = page.getByTestId(
|
||||
SendCryptoAssetSelectors.RecipientBnsAddressInfoIcon
|
||||
);
|
||||
this.feesRow = page.getByTestId(SendCryptoAssetSelectors.ConfirmationDetailsFee);
|
||||
this.memoRow = page.getByTestId(SendCryptoAssetSelectors.ConfirmationDetailsMemo);
|
||||
|
||||
@@ -70,8 +71,4 @@ export class SendPage {
|
||||
await this.page.waitForURL('**' + `${RouteUrls.SendCryptoAsset}/stx`);
|
||||
await this.page.getByTestId(SendCryptoAssetSelectors.SendForm).waitFor();
|
||||
}
|
||||
|
||||
async waitForFeesSelector() {
|
||||
await this.page.waitForSelector(this.feesSelector, { timeout: 30000 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ export enum SendCryptoAssetSelectors {
|
||||
MemoFieldInput = 'memo-field-input',
|
||||
PreviewSendTxBtn = 'preview-send-tx-btn',
|
||||
RecipientChooseAccountButton = 'recipient-choose-account-button',
|
||||
RecipientSelectFieldAddress = 'recipient-select-field-address',
|
||||
RecipientSelectFieldBnsName = 'recipient-select-field-bns-name',
|
||||
RecipientFieldInput = 'recipient-field-input',
|
||||
RecipientBnsAddressLabel = 'recipient-bns-address-label',
|
||||
RecipientBnsAddressInfoIcon = 'recipient-bns-address-info-icon',
|
||||
RecipientBnsAddressCopyToClipboard = 'recipient-bns-address-copy-to-clipboard',
|
||||
SendForm = 'send-form',
|
||||
SendMaxBtn = 'send-max-btn',
|
||||
|
||||
@@ -7,7 +7,6 @@ import { FormErrorMessages } from '@app/common/error-messages';
|
||||
import { test } from '../../fixtures/fixtures';
|
||||
|
||||
test.describe('send btc', () => {
|
||||
// TODO: Don't run these if we disable bitcoin sending?
|
||||
test.beforeEach(async ({ extensionId, globalPage, homePage, onboardingPage, sendPage }) => {
|
||||
await globalPage.setupAndUseApiCalls(extensionId);
|
||||
await onboardingPage.signInExistingUser();
|
||||
|
||||
@@ -31,26 +31,29 @@ test.describe('send stx', () => {
|
||||
|
||||
test('that recipient address matches bns name', async ({ page, sendPage }) => {
|
||||
await sendPage.amountInput.fill('.0001');
|
||||
await sendPage.recipientSelectFieldAddress.click();
|
||||
await sendPage.recipientSelectFieldBnsName.click();
|
||||
await sendPage.recipientInput.fill(TEST_BNS_NAME);
|
||||
await sendPage.recipientInput.blur();
|
||||
await sendPage.recipientBnsAddressLabel.waitFor();
|
||||
await sendPage.recipientBnsAddressInfoIcon.hover();
|
||||
const bnsResolvedAddress = await page.getByText(TEST_BNS_RESOLVED_ADDRESS).innerText();
|
||||
|
||||
test.expect(bnsResolvedAddress).toBeTruthy();
|
||||
});
|
||||
|
||||
test('that fee row defaults to middle fee estimation', async ({ page }) => {
|
||||
await page.getByTestId(SharedComponentsSelectors.FeeToBePaidLabel).scrollIntoViewIfNeeded();
|
||||
const feeToBePaid = await page
|
||||
.getByTestId(SharedComponentsSelectors.FeeToBePaidLabel)
|
||||
.innerText();
|
||||
const fee = Number(feeToBePaid.split(' ')[0]);
|
||||
// Using min/max fee caps
|
||||
const isMiddleFee = fee >= 0.003 && fee < 0.75;
|
||||
const isMiddleFee = fee >= 0.003 && fee <= 0.75;
|
||||
test.expect(isMiddleFee).toBeTruthy();
|
||||
});
|
||||
|
||||
test('that low fee estimate can be selected', async ({ page }) => {
|
||||
await page.getByTestId(SharedComponentsSelectors.FeeToBePaidLabel).scrollIntoViewIfNeeded();
|
||||
await page.getByTestId(SharedComponentsSelectors.MiddleFeeEstimateItem).click();
|
||||
await page.getByTestId(SharedComponentsSelectors.LowFeeEstimateItem).click();
|
||||
const feeToBePaid = await page
|
||||
@@ -58,7 +61,7 @@ test.describe('send stx', () => {
|
||||
.innerText();
|
||||
const fee = Number(feeToBePaid.split(' ')[0]);
|
||||
// Using min/max fee caps
|
||||
const isLowFee = fee >= 0.0025 && fee < 0.5;
|
||||
const isLowFee = fee >= 0.0025 && fee <= 0.5;
|
||||
test.expect(isLowFee).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -117,21 +120,22 @@ test.describe('send stx', () => {
|
||||
});
|
||||
|
||||
test.describe('send form preview', () => {
|
||||
test('that it shows preview of tx details to be confirmed', async ({ sendPage }) => {
|
||||
test('that it shows preview of tx details to be confirmed', async ({ page, sendPage }) => {
|
||||
await sendPage.amountInput.fill('0.000001');
|
||||
await sendPage.recipientInput.fill(TEST_ACCOUNT_2_STX_ADDRESS);
|
||||
await sendPage.waitForFeesSelector();
|
||||
await page.getByTestId(SharedComponentsSelectors.FeeToBePaidLabel).scrollIntoViewIfNeeded();
|
||||
await sendPage.previewSendTxButton.click();
|
||||
const details = await sendPage.confirmationDetails.allInnerTexts();
|
||||
test.expect(details).toBeTruthy();
|
||||
});
|
||||
|
||||
test('that it shows preview of tx details after validation error is resolved', async ({
|
||||
page,
|
||||
sendPage,
|
||||
}) => {
|
||||
await sendPage.amountInput.fill('0.0000001');
|
||||
await sendPage.recipientInput.fill(TEST_ACCOUNT_2_STX_ADDRESS);
|
||||
await sendPage.waitForFeesSelector();
|
||||
await page.getByTestId(SharedComponentsSelectors.FeeToBePaidLabel).scrollIntoViewIfNeeded();
|
||||
await sendPage.previewSendTxButton.click();
|
||||
const errorMsg = await sendPage.amountInputErrorLabel.innerText();
|
||||
test.expect(errorMsg).toEqual(FormErrorMessages.MustBePositive);
|
||||
@@ -143,6 +147,7 @@ test.describe('send stx', () => {
|
||||
});
|
||||
|
||||
test('that asset value, recipient, memo and fees on preview match input', async ({
|
||||
page,
|
||||
sendPage,
|
||||
}) => {
|
||||
const amount = '0.000001';
|
||||
@@ -151,7 +156,7 @@ test.describe('send stx', () => {
|
||||
await sendPage.amountInput.fill(amount);
|
||||
await sendPage.recipientInput.fill(TEST_ACCOUNT_2_STX_ADDRESS);
|
||||
await sendPage.memoInput.fill(memo);
|
||||
await sendPage.waitForFeesSelector();
|
||||
await page.getByTestId(SharedComponentsSelectors.FeeToBePaidLabel).scrollIntoViewIfNeeded();
|
||||
const fees = await sendPage.page
|
||||
.getByTestId(SharedComponentsSelectors.FeeToBePaidLabel)
|
||||
.innerText();
|
||||
@@ -177,13 +182,13 @@ test.describe('send stx', () => {
|
||||
test.expect(confirmationMemo).toEqual(memo);
|
||||
});
|
||||
|
||||
test('that empty memo on preview matches default empty value', async ({ sendPage }) => {
|
||||
test('that empty memo on preview matches default empty value', async ({ page, sendPage }) => {
|
||||
const amount = '0.000001';
|
||||
const emptyMemoPreviewValue = 'No memo';
|
||||
|
||||
await sendPage.amountInput.fill(amount);
|
||||
await sendPage.recipientInput.fill(TEST_ACCOUNT_2_STX_ADDRESS);
|
||||
await sendPage.waitForFeesSelector();
|
||||
await page.getByTestId(SharedComponentsSelectors.FeeToBePaidLabel).scrollIntoViewIfNeeded();
|
||||
await sendPage.previewSendTxButton.click();
|
||||
|
||||
const confirmationMemo = await sendPage.memoRow
|
||||
|
||||
Reference in New Issue
Block a user