From c2df85ee7962ac2d6beeee2789c92b1eb0c53722 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Fri, 29 Mar 2024 11:08:25 -0300 Subject: [PATCH] [REF] utxo selection and multisend flow refactor --- src/components/checkbox/Checkbox.spec.tsx | 4 +- src/navigation/wallet/WalletGroup.tsx | 7 - .../wallet/components/SendToAddress.tsx | 351 -------- .../wallet/components/SendToContact.tsx | 171 ---- .../wallet/screens/SendToOptions.tsx | 311 ------- src/navigation/wallet/screens/send/SendTo.tsx | 811 +++++++++++++----- yarn.lock | 5 + 7 files changed, 620 insertions(+), 1040 deletions(-) delete mode 100644 src/navigation/wallet/components/SendToAddress.tsx delete mode 100644 src/navigation/wallet/components/SendToContact.tsx delete mode 100644 src/navigation/wallet/screens/SendToOptions.tsx diff --git a/src/components/checkbox/Checkbox.spec.tsx b/src/components/checkbox/Checkbox.spec.tsx index bb3f913a0..ddc1ba7e9 100644 --- a/src/components/checkbox/Checkbox.spec.tsx +++ b/src/components/checkbox/Checkbox.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Checkbox from './Checkbox'; import {fireEvent, render} from '@test/render'; -import {SlateDark, Action} from '../../styles/colors'; +import {Action} from '../../styles/colors'; it('renders correctly', async () => { const mockFn = jest.fn(); @@ -9,7 +9,7 @@ it('renders correctly', async () => { , ); const checkbox = await getByTestId('checkbox'); - expect(getByTestId('checkboxBorder')).toHaveStyle({borderColor: SlateDark}); + expect(getByTestId('checkboxBorder')).toHaveStyle({borderColor: '#E5E5F2'}); fireEvent(checkbox, 'press'); expect(mockFn).toHaveBeenCalled(); diff --git a/src/navigation/wallet/WalletGroup.tsx b/src/navigation/wallet/WalletGroup.tsx index 7835a0725..f270534cc 100644 --- a/src/navigation/wallet/WalletGroup.tsx +++ b/src/navigation/wallet/WalletGroup.tsx @@ -68,7 +68,6 @@ import PayProConfirmTwoFactor, { PayProConfirmTwoFactorParamList, } from './screens/send/confirm/PayProConfirmTwoFactor'; import {useTranslation} from 'react-i18next'; -import SendToOptions, {SendToOptionsParamList} from './screens/SendToOptions'; import SelectInputs, {SelectInputsParamList} from './screens/SelectInputs'; import CurrencyTokenSelectionScreen, { CurrencyTokenSelectionScreenParamList, @@ -155,7 +154,6 @@ export type WalletGroupParamList = { AllAddresses: AllAddressesParamList; PriceCharts: PriceChartsParamList; ClearEncryptPassword: ClearEncryptPasswordParamList; - SendToOptions: SendToOptionsParamList; SelectInputs: SelectInputsParamList; EnterBuyerProvidedEmail: {data: string}; ExportTransactionHistory: {wallet: WalletModel}; @@ -208,7 +206,6 @@ export enum WalletScreens { ALL_ADDRESSES = 'AllAddresses', PRICE_CHARTS = 'PriceCharts', CLEAR_ENCRYPT_PASSWORD = 'ClearEncryptPassword', - SEND_TO_OPTIONS = 'SendToOptions', SELECT_INPUTS = 'SelectInputs', ENTER_BUYER_PROVIDED_EMAIL = 'EnterBuyerProvidedEmail', CLEAR_TRANSACTION_HISTORY_CACHE = 'ClearTransactionHistoryCache', @@ -430,10 +427,6 @@ const WalletGroup: React.FC = ({Wallet}) => { name={WalletScreens.CLEAR_ENCRYPT_PASSWORD} component={ClearEncryptPassword} /> - { - const dispatch = useAppDispatch(); - const {t} = useTranslation(); - const theme = useTheme(); - const placeHolderTextColor = theme.dark ? NeutralSlate : '#6F7782'; - const [searchInput, setSearchInput] = useState(''); - const [errorMessage, setErrorMessage] = useState(''); - const {defaultAltCurrency, hideAllBalances} = useAppSelector(({APP}) => APP); - const {keys} = useAppSelector(({WALLET}: RootState) => WALLET); - const {rates} = useAppSelector(({RATE}) => RATE); - const { - recipientList, - setRecipientListContext, - setRecipientAmountContext, - goToConfirmView, - goToSelectInputsView, - } = useContext(SendToOptionsContext); - const navigation = useNavigation(); - const route = useRoute>(); - const {wallet, context} = route.params; - const {currencyAbbreviation, id, network, chain} = wallet; - - const keyWallets: KeyWalletsRowProps[] = BuildKeyWalletRow( - keys, - id, - currencyAbbreviation, - chain, - network, - defaultAltCurrency.isoCode, - searchInput, - rates, - dispatch, - ); - - const onErrorMessageDismiss = () => { - setSearchInput(''); - }; - - const BchLegacyAddressInfoDismiss = (searchText: string) => { - try { - const cashAddr = TranslateToBchCashAddress( - searchText.replace(/^(bitcoincash:|bchtest:)/, ''), - ); - setSearchInput(cashAddr); - validateData(cashAddr); - } catch (error) { - dispatch(showBottomNotificationModal(Mismatch(onErrorMessageDismiss))); - } - }; - - const checkCoinAndNetwork = - (data: any): Effect => - dispatch => { - const addrData = GetCoinAndNetwork(data, network, chain); - const isValid = - chain === addrData?.coin.toLowerCase() && addrData?.network === network; - - if (isValid) { - return true; - } else { - // @ts-ignore - if (currencyAbbreviation === 'bch' && network === addrData?.network) { - const isLegacy = CheckIfLegacyBCH(data); - if (isLegacy) { - const appName = APP_NAME_UPPERCASE; - - dispatch( - showBottomNotificationModal( - BchLegacyAddressInfo(appName, () => { - BchLegacyAddressInfoDismiss(data); - }), - ), - ); - } else { - dispatch( - showBottomNotificationModal(Mismatch(onErrorMessageDismiss)), - ); - } - } else { - dispatch( - showBottomNotificationModal(Mismatch(onErrorMessageDismiss)), - ); - } - } - return false; - }; - - const validateData = async (text: string) => { - const data = ValidateURI(text); - if (ValidDataTypes.includes(data?.type)) { - if (dispatch(checkCoinAndNetwork(text))) { - setErrorMessage(''); - setSearchInput(''); - const extractedAmount = ExtractUriAmount(data.data); - const addr = ExtractBitPayUriAddress(text); - context === 'selectInputs' - ? goToSelectInputsView({address: addr}) - : addRecipient({ - address: addr, - amount: extractedAmount ? Number(extractedAmount[1]) : undefined, - }); - } - } else { - setErrorMessage(text.length > 15 ? 'Invalid Address' : ''); - } - }; - - const onSearchInputChange = debounce((text: string) => { - validateData(text); - }, 300); - - const addRecipient = (newRecipient: Recipient) => { - setRecipientAmountContext(newRecipient); - }; - - const onSendToWallet = async (selectedWallet: KeyWallet) => { - try { - const { - credentials, - id: walletId, - keyId, - walletName, - receiveAddress, - } = selectedWallet; - - let address = receiveAddress; - - if (!address) { - dispatch(startOnGoingProcessModal('GENERATING_ADDRESS')); - address = (await dispatch( - createWalletAddress({wallet: selectedWallet, newAddress: false}), - )) as string; - dispatch(dismissOnGoingProcessModal()); - await sleep(500); - } - - const newRecipient = { - type: 'wallet', - name: walletName || credentials.walletName, - walletId, - keyId, - address, - }; - - context === 'selectInputs' - ? goToSelectInputsView(newRecipient) - : addRecipient(newRecipient); - } catch (err) { - const e = err instanceof Error ? err.message : JSON.stringify(err); - dispatch(LogActions.error('[SendToWallet] ', e)); - } - }; - - const renderItem = useCallback( - ({item, index}) => { - return ( - setRecipientListContext(item, index, true)} - setAmount={() => setRecipientAmountContext(item, index, true)} - context={context} - /> - ); - }, - [wallet, setRecipientListContext, setRecipientAmountContext], - ); - - return ( - <> - - - { - setSearchInput(text); - onSearchInputChange(text); - }} - /> - { - haptic('impactLight'); - dispatch( - Analytics.track('Open Scanner', { - context: 'SendTo', - }), - ); - navigation.navigate('ScanRoot', { - onScanComplete: data => { - try { - if (data) { - validateData(data); - } - } catch (err) { - const e = - err instanceof Error ? err.message : JSON.stringify(err); - dispatch( - LogActions.error('[OpenScanner SendToAddress] ', e), - ); - } - }, - }); - }}> - - - - {errorMessage ? {errorMessage} : null} - - -
- {recipientList?.length > 1 - ? t('Recipients') + ` (${recipientList?.length})` - : t('Recipient')} -
-
- {recipientList && recipientList.length ? ( - - index.toString()} - renderItem={({item, index}: {item: Recipient; index: number}) => - renderItem({item, index}) - } - /> - - ) : ( - <> - - - {t( - 'To get started, you’ll need to enter a valid address or select an existing contact or wallet.', - )} - - -
- - )} -
-
- - - { - onSendToWallet(selectedWallet); - }} - /> - - - - {context !== 'selectInputs' ? ( - - - - ) : null} - - ); -}; - -export default SendToAddress; diff --git a/src/navigation/wallet/components/SendToContact.tsx b/src/navigation/wallet/components/SendToContact.tsx deleted file mode 100644 index d36946ac1..000000000 --- a/src/navigation/wallet/components/SendToContact.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React, {useCallback, useContext, useMemo, useState} from 'react'; -import { - CtaContainer as _CtaContainer, - Hr, - SearchContainer, - SearchInput, -} from '../../../components/styled/Containers'; -import Button from '../../../components/button/Button'; -import styled, {useTheme} from 'styled-components/native'; -import {H5, SubText} from '../../../components/styled/Text'; -import {NeutralSlate} from '../../../styles/colors'; -import {useRoute} from '@react-navigation/native'; -import {RouteProp} from '@react-navigation/core'; -import {WalletGroupParamList} from '../WalletGroup'; -import {RootState} from '../../../store'; -import {useTranslation} from 'react-i18next'; -import haptic from '../../../components/haptic-feedback/haptic'; -import {ContactTitle, ContactTitleContainer} from '../screens/send/SendTo'; -import ContactsSvg from '../../../../assets/img/tab-icons/contacts.svg'; -import {useAppSelector} from '../../../utils/hooks'; -import {FlatList, View} from 'react-native'; -import { - RecipientList, - RecipientRowContainer, - SendToOptionsContext, -} from '../screens/SendToOptions'; -import {Recipient} from '../../../store/wallet/wallet.models'; -import ContactRow from '../../../components/list/ContactRow'; - -const ScrollViewContainer = styled.ScrollView` - margin-top: 20px; - padding: 0 15px; -`; - -const SendToContactContainer = styled.View` - margin-top: 20px; - padding: 0 15px; -`; - -const CtaContainer = styled(_CtaContainer)` - padding: 10px 16px; -`; - -const SendToContact = () => { - const {t} = useTranslation(); - const theme = useTheme(); - const allContacts = useAppSelector(({CONTACT}: RootState) => CONTACT.list); - const placeHolderTextColor = theme.dark ? NeutralSlate : '#6F7782'; - const [searchInput, setSearchInput] = useState(''); - const route = useRoute>(); - const {wallet, context} = route.params; - const { - recipientList, - setRecipientListContext, - setRecipientAmountContext, - goToConfirmView, - goToSelectInputsView, - } = useContext(SendToOptionsContext); - const {currencyAbbreviation, network} = wallet; - - const contacts = useMemo(() => { - return allContacts.filter( - contact => - contact.coin === currencyAbbreviation.toLowerCase() && - contact.network === network && - (contact.name.toLowerCase().includes(searchInput.toLowerCase()) || - contact.email?.toLowerCase().includes(searchInput.toLowerCase())), - ); - }, [allContacts, searchInput, network, currencyAbbreviation]); - - const renderItem = useCallback( - ({item, index}) => { - return ( - setRecipientListContext(item, index, true)} - setAmount={() => setRecipientAmountContext(item, index, true)} - context={context} - /> - ); - }, - [wallet, setRecipientListContext, setRecipientAmountContext], - ); - - return ( - <> - - - { - setSearchInput(text); - }} - /> - - -
- {recipientList?.length > 1 ? t('Recipients') : t('Recipient')} -
-
- {recipientList && recipientList.length ? ( - index.toString()} - renderItem={({item, index}: {item: Recipient; index: number}) => - renderItem({item, index}) - } - /> - ) : ( - <> - - - {t( - 'To get started, you’ll need to enter a valid address or select an existing contact or wallet.', - )} - - -
- - )} -
-
- - {contacts.length > 0 ? ( - <> - - {ContactsSvg({})} - {'Contacts'} - - {contacts.map((item, index) => { - return ( - - { - haptic('impactLight'); - context === 'selectInputs' - ? goToSelectInputsView({...item, type: 'contact'}) - : setRecipientAmountContext({ - ...item, - type: 'contact', - }); - }} - /> - - ); - })} - - ) : null} - - {context !== 'selectInputs' ? ( - - - - ) : null} - - ); -}; - -export default SendToContact; diff --git a/src/navigation/wallet/screens/SendToOptions.tsx b/src/navigation/wallet/screens/SendToOptions.tsx deleted file mode 100644 index a288e1619..000000000 --- a/src/navigation/wallet/screens/SendToOptions.tsx +++ /dev/null @@ -1,311 +0,0 @@ -import React, {useLayoutEffect, useState} from 'react'; -import styled from 'styled-components/native'; -import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs'; -import {ScreenOptions} from '../../../styles/tabNavigator'; -import {H5, H7, HeaderTitle} from '../../../components/styled/Text'; -import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'; -import {useTranslation} from 'react-i18next'; -import {WalletGroupParamList} from '../WalletGroup'; -import SendToAddress from '../components/SendToAddress'; -import SendToContact from '../components/SendToContact'; -import { - Recipient, - TransactionOptionsContext, - TxDetailsSendingTo, - Wallet, -} from '../../../store/wallet/wallet.models'; -import {CurrencyImage} from '../../../components/currency-image/CurrencyImage'; -import {ActiveOpacity, Hr} from '../../../components/styled/Containers'; -import {TouchableOpacity} from 'react-native'; -import WalletIcons from '../components/WalletIcons'; -import _ from 'lodash'; -import AmountModal from '../../../components/amount/AmountModal'; -import { - createProposalAndBuildTxDetails, - handleCreateTxProposalError, -} from '../../../store/wallet/effects/send/send'; -import {formatCurrencyAbbreviation, sleep} from '../../../utils/helper-methods'; -import { - dismissOnGoingProcessModal, - showBottomNotificationModal, -} from '../../../store/app/app.actions'; -import {useAppDispatch} from '../../../utils/hooks'; -import {startOnGoingProcessModal} from '../../../store/app/app.effects'; - -export type SendToOptionsParamList = { - title: string; - wallet: Wallet; - context: string; -}; - -export const RecipientRowContainer = styled.View` - align-items: center; - flex-direction: row; - justify-content: space-between; - height: 55px; -`; - -export const RecipientContainer = styled.View` - flex-direction: row; - flex: 1; -`; - -const RecipientOptionsContainer = styled.View` - justify-content: flex-end; - flex-direction: row; - align-items: center; - flex: 1; -`; - -interface RecipientListProps { - recipient: Recipient; - wallet: Wallet; - deleteRecipient: () => void; - setAmount: () => void; - context: string; -} - -export const RecipientList: React.FC = ({ - recipient, - wallet, - deleteRecipient, - setAmount, - context, -}) => { - let recipientData: TxDetailsSendingTo; - - if (recipient?.type === 'contact') { - recipientData = { - recipientName: recipient?.name, - recipientAddress: recipient?.address, - img: recipient?.type, - }; - } else { - recipientData = { - recipientName: recipient.name, - recipientAddress: recipient.address, - img: wallet?.img || wallet?.currencyAbbreviation, - }; - } - - return ( - <> - - - - - {recipientData.recipientName || recipientData.recipientAddress} - - - - {context === 'multisend' ? ( - { - setAmount(); - }}> -
- {recipient.amount + - ' ' + - formatCurrencyAbbreviation(wallet.currencyAbbreviation)} -
-
- ) : null} - - deleteRecipient()}> - - -
-
-
- - ); -}; - -const ImportContainer = styled.SafeAreaView` - flex: 1; - margin-top: 10px; -`; - -interface SendToOptionsContextProps { - recipientList: Recipient[]; - setRecipientListContext: ( - recipient: Recipient, - index?: number, - removeRecipient?: boolean, - updateRecipient?: boolean, - amount?: number, - ) => void; - setRecipientAmountContext: ( - recipient: Recipient, - index?: number, - updateRecipient?: boolean, - ) => void; - goToConfirmView: () => void; - goToSelectInputsView: (recipient: Recipient) => void; -} - -export const SendToOptionsContext = - React.createContext( - {} as SendToOptionsContextProps, - ); - -const SendToOptions = () => { - const {t} = useTranslation(); - const dispatch = useAppDispatch(); - const Tab = createMaterialTopTabNavigator(); - const navigation = useNavigation(); - const {params} = useRoute>(); - const {wallet} = params; - const [recipientList, setRecipientList] = useState([]); - const [recipientAmount, setRecipientAmount] = useState<{ - showModal: boolean; - recipient?: Recipient; - index?: number; - updateRecipient?: boolean; - }>({showModal: false}); - - const setRecipientListContext = ( - recipient: Recipient, - index?: number, - removeRecipient?: boolean, - updateRecipient?: boolean, - ) => { - let newRecipientList: Recipient[] = _.cloneDeep(recipientList); - if (removeRecipient) { - newRecipientList.splice(index!, 1); - } else if (updateRecipient) { - newRecipientList[index!] = recipient; - } else { - newRecipientList = [...newRecipientList, recipient]; - } - - setRecipientList(newRecipientList); - }; - - const setRecipientAmountContext = ( - recipient: Recipient, - index?: number, - updateRecipient?: boolean, - ) => { - if (recipient.amount && !updateRecipient) { - setRecipientListContext(recipient); - } else { - setRecipientAmount({showModal: true, recipient, index, updateRecipient}); - } - }; - - const goToConfirmView = async () => { - try { - dispatch(startOnGoingProcessModal('LOADING')); - const amount = _.sumBy(recipientList, 'amount'); - const tx = { - wallet, - recipient: recipientList[0], - recipientList, - amount, - context: 'multisend' as TransactionOptionsContext, - }; - const {txDetails, txp} = (await dispatch( - createProposalAndBuildTxDetails(tx), - )) as any; - dispatch(dismissOnGoingProcessModal()); - await sleep(500); - navigation.navigate('Confirm', { - wallet, - recipient: recipientList[0], - recipientList, - txp, - txDetails, - amount, - }); - } catch (err: any) { - const errorMessageConfig = ( - await Promise.all([ - dispatch(handleCreateTxProposalError(err)), - sleep(500), - ]) - )[0]; - dispatch(dismissOnGoingProcessModal()); - await sleep(500); - dispatch( - showBottomNotificationModal({ - ...errorMessageConfig, - enableBackdropDismiss: false, - actions: [ - { - text: t('OK'), - action: () => {}, - }, - ], - }), - ); - } - }; - - useLayoutEffect(() => { - navigation.setOptions({ - headerTitle: () => {params.title}, - headerTitleAlign: 'center', - }); - }, [navigation, t, params.title]); - - const goToSelectInputsView = (recipient: Recipient) => { - navigation.navigate('SelectInputs', { - recipient, - wallet, - }); - }; - - return ( - - - - - - - - - { - setRecipientAmount({showModal: false}); - }} - onSubmit={amount => { - setRecipientAmount({showModal: false}); - setRecipientListContext( - {...recipientAmount.recipient!, amount}, - recipientAmount.index, - false, - recipientAmount.updateRecipient, - ); - }} - /> - - ); -}; - -export default SendToOptions; diff --git a/src/navigation/wallet/screens/send/SendTo.tsx b/src/navigation/wallet/screens/send/SendTo.tsx index b530df024..d992b0262 100644 --- a/src/navigation/wallet/screens/send/SendTo.tsx +++ b/src/navigation/wallet/screens/send/SendTo.tsx @@ -1,13 +1,25 @@ -import React, {useEffect, useLayoutEffect, useMemo, useState} from 'react'; +import React, { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from 'react'; import { BaseText, + H5, + H7, HeaderTitle, Paragraph, + SubText, } from '../../../../components/styled/Text'; import {useNavigation, useRoute, useTheme} from '@react-navigation/native'; import styled from 'styled-components/native'; import { ActiveOpacity, + Column, + HeaderRightContainer, + Hr, ScreenGutter, SearchContainer, SearchInput, @@ -18,6 +30,8 @@ import ContactsSvg from '../../../../../assets/img/tab-icons/contacts.svg'; import { LightBlack, Midnight, + NotificationPrimary, + Slate, Slate30, SlateDark, White, @@ -32,7 +46,13 @@ import { getErrorString, sleep, } from '../../../../utils/helper-methods'; -import {Key} from '../../../../store/wallet/wallet.models'; +import { + Key, + Recipient, + TransactionOptionsContext, + TxDetailsSendingTo, + Wallet, +} from '../../../../store/wallet/wallet.models'; import {Rates} from '../../../../store/rate/rate.models'; import debounce from 'lodash.debounce'; import { @@ -40,11 +60,15 @@ import { ValidDataTypes, ValidateURI, } from '../../../../store/wallet/utils/validations'; -import {TouchableOpacity, View} from 'react-native'; +import {FlatList, TouchableOpacity, View} from 'react-native'; import haptic from '../../../../components/haptic-feedback/haptic'; import merge from 'lodash.merge'; import cloneDeep from 'lodash.clonedeep'; -import {GetPayProUrl} from '../../../../store/wallet/utils/decode-uri'; +import { + ExtractBitPayUriAddress, + ExtractUriAmount, + GetPayProUrl, +} from '../../../../store/wallet/utils/decode-uri'; import KeyWalletsRow, { KeyWallet, KeyWalletsRowProps, @@ -83,21 +107,164 @@ import {IsUtxoCoin} from '../../../../store/wallet/utils/currency'; import {goToAmount, incomingData} from '../../../../store/scan/scan.effects'; import {useTranslation} from 'react-i18next'; import {toFiat} from '../../../../store/wallet/utils/wallet'; -import Settings from '../../../../components/settings/Settings'; -import OptionsSheet, {Option} from '../../components/OptionsSheet'; -import Icons from '../../components/WalletIcons'; -import ContactRow from '../../../../components/list/ContactRow'; +import ContactRow, { + ContactRowProps, +} from '../../../../components/list/ContactRow'; import {ReceivingAddress} from '../../../../store/bitpay-id/bitpay-id.models'; import {BitPayIdEffects} from '../../../../store/bitpay-id'; import {getCurrencyCodeFromCoinAndChain} from '../../../bitpay-id/utils/bitpay-id-utils'; import {Analytics} from '../../../../store/analytics/analytics.effects'; import {LogActions} from '../../../../store/log'; +import Checkbox from '../../../../components/checkbox/Checkbox'; +import _ from 'lodash'; +import AmountModal from '../../../../components/amount/AmountModal'; +import {CurrencyImage} from '../../../../components/currency-image/CurrencyImage'; +import WalletIcons from '../../components/WalletIcons'; +import Button from '../../../../components/button/Button'; +import { + createProposalAndBuildTxDetails, + handleCreateTxProposalError, +} from '../../../../store/wallet/effects/send/send'; +import ContactIcon from '../../../../navigation/tabs/contacts/components/ContactIcon'; + +const AdvancedOptionsButton = styled.TouchableOpacity` + height: 40px; + flex-direction: row; + align-items: center; +`; + +const AdvancedOptionsButtonText = styled(BaseText)` + font-size: 14px; + color: ${({theme: {dark}}) => (dark ? White : NotificationPrimary)}; + margin-bottom: 5px; +`; + +const CheckBoxContainer = styled.View` + padding-left: 10px; +`; + +const CheckBoxWrapper = styled.View` + display: flex; + flex-direction: row; + align-items: center; + padding: 5px; +`; + +const CheckBoxCol = styled.View` + display: flex; + flex-direction: column; +`; + +const CheckboxText = styled(BaseText)` + color: ${({theme: {dark}}) => (dark ? Slate : SlateDark)}; + font-size: 12px; + font-weight: 400; + line-height: 20px; +`; + +const RecipientContainer = styled.View` + align-items: center; + flex-direction: row; + padding: 15px 0px; +`; + +const RecipientOptionsContainer = styled.View` + justify-content: flex-end; + flex-direction: row; + align-items: center; + flex: 1; +`; +const ContactImageContainer = styled.View` + height: 35px; + width: 35px; + display: flex; + justify-content: center; +`; + +const RecipientNameColumn = styled(Column)` + margin-left: 20px; + margin-right: 24px; +`; + +export const RecipientList: React.FC = ({ + recipient, + wallet, + deleteRecipient, + setAmount, +}) => { + let recipientData: TxDetailsSendingTo; + + if (recipient?.type === 'contact') { + recipientData = { + recipientName: recipient?.name, + recipientAddress: recipient?.address, + img: recipient?.type, + }; + } else { + recipientData = { + recipientName: recipient.name, + recipientAddress: recipient.address, + img: wallet?.img || wallet?.currencyAbbreviation, + }; + } + + return ( + + + {recipient?.type === 'contact' ? ( + + ) : ( + + )} + + + {recipientData.recipientName ? ( +
+ {recipientData.recipientName} +
+ ) : ( + + {recipientData.recipientAddress} + + )} +
+ + { + setAmount(); + }}> +
{recipient.amount}
+
+ deleteRecipient()}> + + +
+
+ ); +}; + +interface RecipientListProps { + recipient: Recipient; + wallet: Wallet; + deleteRecipient: () => void; + setAmount: () => void; +} const SafeAreaView = styled.SafeAreaView` flex: 1; `; -const ScrollView = styled.ScrollView` +const SendToContainer = styled.FlatList` flex: 1; margin-top: 20px; padding: 0 ${ScreenGutter}; @@ -148,6 +315,15 @@ const InfoSheetMessage = styled.View` padding: 20px 0; `; +const SelectInputContainer = styled.TouchableOpacity` + margin: 0 0 0 20px; +`; + +const SelectOptionText = styled(BaseText)` + color: ${({theme: {dark}}) => (dark ? White : NotificationPrimary)}; + font-size: 13px; +`; + const isEmailAddress = (text: string) => { if (!text.includes('@')) { return false; @@ -239,6 +415,9 @@ const SendTo = () => { const dispatch = useAppDispatch(); const logger = useLogger(); const route = useRoute>(); + const [selectInputOption, setSelectInputOption] = useState(false); + const [multiSendOption, setMultiSendOption] = useState(false); + const [showAdvancedOptions, setShowAdvancedOptions] = useState(true); const {keys} = useAppSelector(({WALLET}: RootState) => WALLET); const {rates} = useAppSelector(({RATE}) => RATE); @@ -248,63 +427,59 @@ const SendTo = () => { const theme = useTheme(); const placeHolderTextColor = theme.dark ? LightBlack : Slate30; const [searchInput, setSearchInput] = useState(''); - const [showWalletOptions, setShowWalletOptions] = useState(false); + const [recipientList, setRecipientList] = useState([]); const [searchIsEmailAddress, setSearchIsEmailAddress] = useState(false); const [emailAddressSearchPromise, setEmailAddressSearchPromise] = useState< Promise >(Promise.resolve([])); + const [recipientAmount, setRecipientAmount] = useState<{ + showModal: boolean; + recipient?: Recipient; + index?: number; + updateRecipient?: boolean; + }>({showModal: false}); const {wallet} = route.params; const {currencyAbbreviation, id, chain, network} = wallet; const isUtxo = IsUtxoCoin(wallet?.currencyAbbreviation); - const selectInputOption: Option = { - img: , - title: t('Select Inputs for this Transaction'), - description: t("Choose which inputs you'd like to use to send crypto."), - onPress: async () => { - await sleep(500); - navigation.navigate('SendToOptions', { - title: t('Select Inputs'), - wallet, - context: 'selectInputs', - }); - }, - }; - - const multisendOption: Option = { - img: , - title: t('Transfer to Multiple Recipients'), - description: t('Send crypto to multiple contacts or addresses.'), - onPress: async () => { - await sleep(500); - navigation.navigate('SendToOptions', { - title: t('Multiple Recipients'), - wallet, - context: 'multisend', - }); - }, - }; - - const assetOptions: Array