diff --git a/src/components/avatar/ENSDomainIcon.tsx b/src/components/avatar/ENSDomainIcon.tsx new file mode 100644 index 0000000000..7f1081a4e3 --- /dev/null +++ b/src/components/avatar/ENSDomainIcon.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import Svg, {Defs, LinearGradient, Path, Stop} from 'react-native-svg'; + +interface ENSDomainIconProps { + size?: number; +} + +const ENSDomainIcon: React.FC = props => { + let {size = 20} = props; + + return ( + + + + + + + + + + + + ); +}; + +export default ENSDomainIcon; diff --git a/src/components/avatar/ProfileIcon.tsx b/src/components/avatar/ProfileIcon.tsx index 3ecd02818a..125b9667d9 100644 --- a/src/components/avatar/ProfileIcon.tsx +++ b/src/components/avatar/ProfileIcon.tsx @@ -1,7 +1,7 @@ import React from 'react'; import * as Svg from 'react-native-svg'; import {useTheme} from 'styled-components/native'; -import {Midnight, ProgressBlue} from '../../styles/colors'; +import {LinkBlue, Midnight, ProgressBlue} from '../../styles/colors'; interface ProfileIconProps { color?: Svg.Color; @@ -14,7 +14,7 @@ const ProfileIcon: React.FC = props => { const theme = useTheme(); size = size || 35; - color = color || (theme.dark ? '#4989FF' : '#9FAFF5'); + color = color || (theme.dark ? LinkBlue : '#9FAFF5'); background = background || (theme.dark ? Midnight : ProgressBlue); return ( diff --git a/src/components/avatar/UnstoppableDomainIcon.tsx b/src/components/avatar/UnstoppableDomainIcon.tsx new file mode 100644 index 0000000000..40f45cc822 --- /dev/null +++ b/src/components/avatar/UnstoppableDomainIcon.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import * as Svg from 'react-native-svg'; + +interface UnstoppableDomainIconProps { + size?: number; +} + +const UnstoppableDomainIcon: React.FC = props => { + const {size = 20} = props; + + return ( + <> + + + + + + ); +}; + +export default UnstoppableDomainIcon; diff --git a/src/components/home-card/HomeCard.tsx b/src/components/home-card/HomeCard.tsx index a220519090..f755a33a51 100644 --- a/src/components/home-card/HomeCard.tsx +++ b/src/components/home-card/HomeCard.tsx @@ -1,7 +1,14 @@ import * as React from 'react'; import {ReactElement, ReactNode} from 'react'; import styled, {useTheme} from 'styled-components/native'; -import {Action, LightBlack, Slate, SlateDark, White} from '../../styles/colors'; +import { + Action, + LightBlack, + Midnight, + Slate, + SlateDark, + White, +} from '../../styles/colors'; import Haptic from '../haptic-feedback/haptic'; import { ActiveOpacity, @@ -92,7 +99,7 @@ const FooterArrow = styled.TouchableOpacity` height: 35px; align-self: flex-end; border-radius: 50px; - background-color: ${({theme}) => (theme.dark ? '#0C204E' : '#ECEFFD')}; + background-color: ${({theme}) => (theme.dark ? Midnight : '#ECEFFD')}; align-items: center; justify-content: center; `; diff --git a/src/components/icons/copy-to-clipboard/CopyToClipboardIcon.tsx b/src/components/icons/copy-to-clipboard/CopyToClipboardIcon.tsx new file mode 100644 index 0000000000..4427a854eb --- /dev/null +++ b/src/components/icons/copy-to-clipboard/CopyToClipboardIcon.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import * as Svg from 'react-native-svg'; +import {Action} from '../../../styles/colors'; + +interface CopyToClipboardIconProps { + size?: number; + color?: Svg.Color; +} + +const CopyToClipboardIcon: React.FC = props => { + let {size = 20, color = Action} = props; + + return ( + + + + ); +}; + +export default CopyToClipboardIcon; diff --git a/src/components/icons/share/Share.tsx b/src/components/icons/share/Share.tsx index 6879db7413..accb872f31 100644 --- a/src/components/icons/share/Share.tsx +++ b/src/components/icons/share/Share.tsx @@ -9,32 +9,57 @@ import { } from '../../../styles/colors'; import styled from 'styled-components/native'; -const ShareSvg: React.FC<{isDark: boolean}> = ({isDark}) => { +interface StyleProps { + showBackground?: boolean; +} + +const ShareSvg: React.FC<{ + isDark: boolean; + width: number; + height: number; + fillColor?: string; +}> = ({isDark, width, height, fillColor}) => { return ( - + ); }; -const ShareContainer = styled.View` - height: 40px; - width: 40px; +const ShareContainer = styled.View` + height: ${({showBackground}) => (showBackground ? '40px' : '10px')}; + width: ${({showBackground}) => (showBackground ? '40px' : '10px')}; border-radius: 50px; - background: ${({theme: {dark}}) => (dark ? LightBlack : NeutralSlate)}; + background: ${({theme: {dark}, showBackground}) => + showBackground ? (dark ? LightBlack : NeutralSlate) : 'transparent'}; align-items: center; justify-content: center; `; -const ShareIcon = () => { +const ShareIcon = ({ + width = 14, + height = 16, + showBackground = true, + fillColor, +}: { + width?: number; + height?: number; + showBackground?: boolean; + fillColor?: string; +}) => { const theme = useTheme(); return ( - - + + ); }; diff --git a/src/components/list/ContactRow.tsx b/src/components/list/ContactRow.tsx index 5791bdeb52..a137a0be18 100644 --- a/src/components/list/ContactRow.tsx +++ b/src/components/list/ContactRow.tsx @@ -30,6 +30,8 @@ const RowContainer = styled.View` align-items: center; `; +export type DomainType = 'ens' | 'unstoppable'; + export interface ContactRowProps { address: string; coin: string; @@ -40,6 +42,7 @@ export interface ContactRowProps { destinationTag?: number; email?: string; domain?: string; + domainType?: DomainType; } interface Props { @@ -50,13 +53,27 @@ interface Props { const ContactRow = ({contact, onPress}: Props) => { const theme = useTheme(); const underlayColor = theme.dark ? '#121212' : '#fbfbff'; - const {coin: _coin, name, email, address, chain, domain} = contact; + const { + coin: _coin, + name, + email, + address, + chain, + domain, + domainType, + } = contact; const coin = getCurrencyAbbreviation(_coin, chain); return ( - +
{name}
diff --git a/src/components/modal/transact-menu/TransactMenuIcons.tsx b/src/components/modal/transact-menu/TransactMenuIcons.tsx index 6aa55143fb..eaf0da08d0 100644 --- a/src/components/modal/transact-menu/TransactMenuIcons.tsx +++ b/src/components/modal/transact-menu/TransactMenuIcons.tsx @@ -17,7 +17,7 @@ const BuyCrypto = () => { fill={theme.dark ? Midnight : Action} /> @@ -37,17 +37,17 @@ const BuyGiftCard = () => { fill={theme.dark ? Midnight : Action} /> @@ -86,11 +86,11 @@ const Exchange = () => { fill={theme.dark ? Midnight : Action} /> @@ -109,7 +109,7 @@ const Receive = () => { fill={theme.dark ? Midnight : Action} /> { return ( { fill={theme.dark ? Midnight : Action} /> ReactElement); badgeImg: string | ((props?: any) => ReactElement); size?: number; + domainType?: DomainType; } const ContactIconContainer = styled.View` @@ -39,10 +44,23 @@ const CoinBadgeContainer = styled.View` bottom: -1px; `; -const CoinBadge: React.FC = ({size = 20, img, badgeImg}) => { +const CoinBadge: React.FC = ({ + size = 20, + img, + badgeImg, + domainType, +}) => { return ( - + {domainType ? ( + domainType === 'ens' ? ( + + ) : ( + + ) + ) : ( + + )} ); }; @@ -53,6 +71,7 @@ const ContactIcon: React.FC = ({ size = 50, name, badge, + domainType, }) => { const tokenOptions = useAppSelector(({WALLET}: RootState) => { return { @@ -72,12 +91,12 @@ const ContactIcon: React.FC = ({ tokenOptions[getCurrencyAbbreviation(coin, chain)]?.logoURI ? (tokenOptions[getCurrencyAbbreviation(coin, chain)].logoURI as string) : ''); - const coinBadge = img ? ( ) : null; diff --git a/src/navigation/tabs/contacts/screens/ContactsAdd.tsx b/src/navigation/tabs/contacts/screens/ContactsAdd.tsx index 2fe3ea2c17..0c0dc3e39f 100644 --- a/src/navigation/tabs/contacts/screens/ContactsAdd.tsx +++ b/src/navigation/tabs/contacts/screens/ContactsAdd.tsx @@ -28,7 +28,10 @@ import { } from '../../../../components/styled/Containers'; import {ValidateCoinAddress} from '../../../../store/wallet/utils/validations'; import {GetCoinAndNetwork} from '../../../../store/wallet/effects/address/address'; -import {ContactRowProps} from '../../../../components/list/ContactRow'; +import { + ContactRowProps, + DomainType, +} from '../../../../components/list/ContactRow'; import {useNavigation} from '@react-navigation/core'; import {RootState} from '../../../../store'; import { @@ -74,6 +77,8 @@ import { getAddressByUnstoppableDomain, getENSDomainByAddress, } from '../../../../store/moralis/moralis.effects'; +import ENSDomainIcon from '../../../../components/avatar/ENSDomainIcon'; +import UnstoppableDomainIcon from '../../../../components/avatar/UnstoppableDomainIcon'; const InputContainer = styled.View<{hideInput?: boolean}>` display: ${({hideInput}) => (!hideInput ? 'flex' : 'none')}; @@ -97,6 +102,13 @@ const AddressBadge = styled.View` top: 50%; `; +const DomainBadge = styled.View` + position: absolute; + left: 5px; + top: 52%; + z-index: 1; +`; + const ScanButtonContainer = styled.TouchableOpacity` position: absolute; right: 5px; @@ -209,6 +221,7 @@ const ContactsAdd = ({ const [networkValue, setNetworkValue] = useState(''); const [chainValue, setChainValue] = useState(''); const [domainValue, setDomainValue] = useState(''); + const [domainTypeValue, setDomainTypeValue] = useState(); const [tokenModalVisible, setTokenModalVisible] = useState(false); const [currencyModalVisible, setCurrencyModalVisible] = useState(false); @@ -307,6 +320,7 @@ const ContactsAdd = ({ network: string, chain: string, domain: string, + domainType?: DomainType, ) => { setValidAddress(true); setAddressValue(address); @@ -315,6 +329,7 @@ const ContactsAdd = ({ setChainValue(chain); setValidDomain(true); setDomainValue(domain); + setDomainTypeValue(domainType); _setSelectedCurrency(coin); @@ -336,12 +351,14 @@ const ContactsAdd = ({ network, chain, domain, + domainType, }: { address?: string; coin?: string; network?: string; chain?: string; domain?: string; + domainType?: DomainType; }) => { if (address) { const coinAndNetwork = GetCoinAndNetwork(address, undefined, chain); @@ -358,6 +375,7 @@ const ContactsAdd = ({ network || coinAndNetwork.network, chain || coinAndNetwork.coin, domain || '', + domainType, ); } else { // try testnet @@ -373,6 +391,7 @@ const ContactsAdd = ({ network || 'testnet', chain || coinAndNetwork.coin, domain || '', + domainType, ); } } @@ -391,17 +410,24 @@ const ContactsAdd = ({ if (!domain) { return; } - const addressByENS = await dispatch(getAddressByENSDomain({domain})); const addressByUnstoppableDomain = await dispatch( getAddressByUnstoppableDomain({domain}), ); - if (addressByENS || addressByUnstoppableDomain) { + if (addressByENS) { + setValidDomain(true); + processAddressOrDomain({ + address: addressByENS, + domain, + domainType: 'ens', + }); + } else if (addressByUnstoppableDomain) { setValidDomain(true); processAddressOrDomain({ - address: addressByENS || addressByUnstoppableDomain, + address: addressByUnstoppableDomain, domain, + domainType: 'unstoppable', }); } else { resetValues(); @@ -421,6 +447,7 @@ const ContactsAdd = ({ setEvmValidAddress(false); setXrpValidAddress(false); setValidDomain(false); + setDomainTypeValue(undefined); }; const onSubmit = handleSubmit((contact: ContactRowProps) => { @@ -431,7 +458,7 @@ const ContactsAdd = ({ }); return; } - + if (!validDomain && domainValue) { setError('domain', { type: 'manual', @@ -440,9 +467,10 @@ const ContactsAdd = ({ return; } - if (addressValue && domainValue) { + if (addressValue && domainValue && domainTypeValue) { contact.address = addressValue; contact.domain = domainValue; + contact.domainType = domainTypeValue; } if (coinValue && chainValue && networkValue) { @@ -597,6 +625,7 @@ const ContactsAdd = ({ if (domain) { setValue('domain', domain); setDomainValue(domain); + setDomainTypeValue('ens'); setValidDomain(true); } } catch (err) { @@ -612,6 +641,7 @@ const ContactsAdd = ({ network: contact.network, chain: contact.chain, domain: contact.domain, + domainType: contact.domainType, }); setValue('address', contact.address!, {shouldDirty: true}); setValue('name', contact.name || ''); @@ -664,20 +694,36 @@ const ContactsAdd = ({ {!contact ? ( + {validDomain && domainTypeValue ? ( + domainTypeValue === 'ens' ? ( + + + + ) : ( + + + + ) + ) : null} ( - { - onChange(newValue); - processAddressOrDomain({address: newValue}); - }} - error={errors.address?.message} - value={value} - /> + <> + { + onChange(newValue); + processAddressOrDomain({address: newValue}); + }} + error={errors.address?.message} + value={value} + /> + )} name="address" defaultValue="" @@ -706,10 +752,28 @@ const ContactsAdd = ({ {evmValidAddress && domainValue ? ( + {validDomain && domainTypeValue ? ( + domainTypeValue === 'ens' ? ( + + + + ) : ( + + + + ) + ) : null} ( - + )} name="domain" defaultValue="" diff --git a/src/navigation/tabs/contacts/screens/ContactsDetails.tsx b/src/navigation/tabs/contacts/screens/ContactsDetails.tsx index 2b5d455dff..d3d393c3d7 100644 --- a/src/navigation/tabs/contacts/screens/ContactsDetails.tsx +++ b/src/navigation/tabs/contacts/screens/ContactsDetails.tsx @@ -293,6 +293,7 @@ const ContactsDetails = ({ size={100} name={contact.name} chain={contact.chain} + domainType={contact.domainType} />
diff --git a/src/navigation/tabs/home/components/CustomizeSvg.tsx b/src/navigation/tabs/home/components/CustomizeSvg.tsx index 1e13e940a6..b6b032f5c7 100644 --- a/src/navigation/tabs/home/components/CustomizeSvg.tsx +++ b/src/navigation/tabs/home/components/CustomizeSvg.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Circle, Path, Svg} from 'react-native-svg'; import {useTheme} from 'styled-components/native'; -import {Action, LinkBlue} from '../../../../styles/colors'; +import {Action, LinkBlue, Midnight} from '../../../../styles/colors'; const CustomizeSvg = () => { const theme = useTheme(); @@ -11,7 +11,7 @@ const CustomizeSvg = () => { cx="17.5" cy="17.5" r="17.5" - fill={theme.dark ? '#0C204E' : '#ECEFFD'} + fill={theme.dark ? Midnight : '#ECEFFD'} /> (dark ? '#0C204E' : Action)}; + background: ${({theme: {dark}}) => (dark ? Midnight : Action)}; margin: 11px 0 8px; `; @@ -49,7 +49,7 @@ const BuySvg = () => { fill-rule="evenodd" clip-rule="evenodd" d="M4.80223 0.805603L4.79692 0.805615L4.79275 0.805603C4.7812 0.805603 4.7697 0.80587 4.75828 0.806399C4.12091 0.829525 3.6113 1.35352 3.6113 1.99653L3.6113 3.65842C2.7443 3.82869 2.03432 4.20006 1.48137 4.77254C0.841615 5.435 0.521739 6.29936 0.521739 7.3656C0.521739 8.15425 0.68323 8.81355 1.00621 9.34352C1.3354 9.87349 1.79503 10.3183 2.38509 10.6779C2.97516 11.0375 3.67081 11.3593 4.47205 11.6432C5.28571 11.9334 5.85714 12.252 6.18634 12.599C6.51553 12.946 6.68012 13.3908 6.68012 13.9334C6.68012 14.4886 6.5 14.9397 6.13975 15.2867C5.7795 15.6337 5.25776 15.8072 4.57453 15.8072C4.20807 15.8072 3.85093 15.7347 3.50311 15.5896C3.15528 15.4382 2.86957 15.1826 2.64596 14.823C2.42236 14.4571 2.31056 13.9524 2.31056 13.3088H0C0 14.2994 0.186335 15.1038 0.559006 15.7221C0.931677 16.3404 1.41304 16.8072 2.00311 17.1227C2.51151 17.3891 3.04757 17.5664 3.6113 17.6548L3.6113 19.0035C3.6113 19.3705 3.77735 19.6988 4.03842 19.9173C4.17198 20.0861 4.37865 20.1944 4.61063 20.1944C4.639 20.1944 4.667 20.1928 4.69453 20.1896C4.73 20.1928 4.76593 20.1944 4.80223 20.1944C5.45997 20.1944 5.99316 19.6612 5.99316 19.0035V17.584C6.80457 17.4034 7.47455 17.0572 8.00311 16.5454C8.6677 15.8956 9 15.0186 9 13.9145C9 13.1259 8.83851 12.4697 8.51553 11.946C8.19876 11.4224 7.74534 10.9807 7.15528 10.6211C6.57143 10.2552 5.87888 9.92396 5.07764 9.62743C4.2205 9.31198 3.63043 8.98705 3.30745 8.65267C2.99068 8.31828 2.8323 7.88611 2.8323 7.35614C2.8323 6.81355 2.98137 6.3656 3.2795 6.01229C3.58385 5.65267 4.05901 5.47286 4.70497 5.47286C5.30745 5.47286 5.7795 5.69683 6.12112 6.14478C6.46894 6.59273 6.64286 7.21103 6.64286 7.99967H8.9441C8.9441 6.73153 8.63665 5.72207 8.02174 4.97128C7.50604 4.32986 6.82985 3.9058 5.99316 3.69912V1.99653C5.99316 1.3388 5.45997 0.805603 4.80223 0.805603Z" - fill={theme.dark ? '#4989FF' : White} + fill={theme.dark ? LinkBlue : White} /> ); @@ -61,11 +61,11 @@ const SwapSvg = () => { ); @@ -79,7 +79,7 @@ const ReceiveSvg = () => { fill-rule="evenodd" clip-rule="evenodd" d="M7.86536 12.7592L10.4752 10.1493C10.9423 9.68222 11.6997 9.68222 12.1668 10.1493C12.6339 10.6164 12.6339 11.3738 12.1668 11.8409L7.52223 16.4855C7.49556 16.5126 7.46761 16.5385 7.43848 16.5629C7.35839 16.6304 7.27154 16.6856 7.18038 16.7287C7.02539 16.802 6.8521 16.8431 6.66924 16.8431C6.49542 16.8431 6.33026 16.806 6.18124 16.7393C6.0654 16.6876 5.95609 16.6166 5.85822 16.5261C5.84522 16.5142 5.8325 16.5019 5.82004 16.4893L1.17162 11.8409C0.704511 11.3738 0.704511 10.6164 1.17162 10.1493C1.63874 9.68222 2.39608 9.68222 2.86319 10.1493L5.47312 12.7593V1.69154C5.47312 1.03094 6.00864 0.495422 6.66924 0.495422C7.32984 0.495422 7.86535 1.03094 7.86536 1.69154V12.7592Z" - fill={theme.dark ? '#4989FF' : White} + fill={theme.dark ? LinkBlue : White} /> ); @@ -93,7 +93,7 @@ const SendSvg = () => { fill-rule="evenodd" clip-rule="evenodd" d="M5.47327 4.57932L2.86339 7.1892C2.39627 7.65631 1.63893 7.65631 1.17182 7.1892C0.704706 6.72209 0.704706 5.96475 1.17182 5.49763L5.81631 0.853139C5.84375 0.825228 5.87254 0.798659 5.90259 0.773535C5.98103 0.707867 6.0659 0.653832 6.15494 0.611428C6.31077 0.537076 6.48522 0.495453 6.66938 0.495453C6.8432 0.495453 7.00836 0.532528 7.15738 0.599202C7.27322 0.650919 7.38253 0.721983 7.4804 0.812388C7.4934 0.824381 7.50613 0.836661 7.51858 0.849213L12.167 5.49763C12.6341 5.96475 12.6341 6.72208 12.167 7.1892C11.6999 7.65631 10.9425 7.65631 10.4754 7.1892L7.8655 4.57927L7.8655 15.647C7.8655 16.3076 7.32998 16.8431 6.66938 16.8431C6.00879 16.8431 5.47327 16.3076 5.47327 15.647L5.47327 4.57932Z" - fill={theme.dark ? '#4989FF' : White} + fill={theme.dark ? LinkBlue : White} /> ); diff --git a/src/navigation/wallet/components/DomainPill.tsx b/src/navigation/wallet/components/DomainPill.tsx new file mode 100644 index 0000000000..0bfefdc016 --- /dev/null +++ b/src/navigation/wallet/components/DomainPill.tsx @@ -0,0 +1,44 @@ +import React, {ReactElement, useState} from 'react'; +import styled from 'styled-components/native'; +import {LightBlack, Midnight, White} from '../../../styles/colors'; +import {H7} from '../../../components/styled/Text'; +import {ActiveOpacity} from '../../../components/styled/Containers'; + +interface Props { + icon?: ReactElement; + description: string; + onPress?: () => void; +} + +const PillContainer = styled.TouchableOpacity` + background-color: ${({theme: {dark}}) => (dark ? Midnight : '#ECEFFD')}; + flex-direction: row; + border-radius: 40px; + align-items: center; + justify-content: space-around; + padding: 0 10px 0 10px; + height: 28px; + max-width: 117px; +`; + +const PillText = styled(H7)` + color: ${({theme: {dark}}) => (dark ? White : LightBlack)}; + flex-direction: row; + margin: 0px 10px; +`; + +const DomainPill = ({icon, description, onPress}: Props) => { + return ( + + + {description} + + <>{icon} + + ); +}; + +export default DomainPill; diff --git a/src/navigation/wallet/components/ShareAddressModal.tsx b/src/navigation/wallet/components/ShareAddressModal.tsx new file mode 100644 index 0000000000..6235f0dde1 --- /dev/null +++ b/src/navigation/wallet/components/ShareAddressModal.tsx @@ -0,0 +1,216 @@ +import React, {useEffect, useState} from 'react'; +import styled from 'styled-components/native'; +import {BaseText} from '../../../components/styled/Text'; +import SheetModal from '../../../components/modal/base/sheet/SheetModal'; +import {SheetContainer} from '../../../components/styled/Containers'; +import {Black, White, SlateDark} from '../../../styles/colors'; +import {ScrollView, SafeAreaView, Share} from 'react-native'; +import {useTranslation} from 'react-i18next'; +import ENSDomainIcon from '../../../components/avatar/ENSDomainIcon'; +import CopyToClipboardIcon from '../../../components/icons/copy-to-clipboard/CopyToClipboardIcon'; +import {Wallet} from '../../../store/wallet/wallet.models'; +import {CurrencyImage} from '../../../components/currency-image/CurrencyImage'; +import {DomainType} from '../../../components/list/ContactRow'; +import UnstoppableDomainIcon from '../../../components/avatar/UnstoppableDomainIcon'; +import BitpaySvg from '../../../../assets/img/wallet/transactions/bitpay.svg'; +import haptic from '../../../components/haptic-feedback/haptic'; +import Clipboard from '@react-native-community/clipboard'; +import CopiedSvg from '../../../../assets/img/copied-success.svg'; + +const ShareAddressContainer = styled(SheetContainer)` + background-color: ${({theme: {dark}}) => (dark ? Black : White)}; +`; + +const ModalHeader = styled.View` + margin: 10px 0 20px 0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + position: relative; +`; + +const ModalHeaderText = styled(BaseText)` + font-size: 18px; + font-weight: bold; +`; + +const LabelTip = styled.View` + border: 1px solid ${SlateDark}; + border-radius: 12px; + padding: 16px; + height: 57px; + margin-bottom: 16px; +`; + +const Row = styled.TouchableOpacity` + flex-direction: row; + justify-content: space-between; + align-items: center; +`; + +const RowLabelContainer = styled.View` + max-width: 75%; + flex-direction: row; + align-items: center; + justify-content: flex-start; +`; + +const RowLabel = styled(BaseText)` + font-size: 14px; + padding-left: 8px; +`; + +const CopyToClipboardContainer = styled.TouchableOpacity` + align-items: center; + justify-content: flex-end; +`; + +interface Props { + isVisible: boolean; + closeModal: () => void; + wallet: Wallet; + domain?: string; + domainType?: DomainType; + email?: string; +} + +const ShareAddressModal = ({ + isVisible, + closeModal, + wallet, + domain, + domainType, + email, +}: Props) => { + const {t} = useTranslation(); + const [copiedEmail, setCopiedEmail] = useState(false); + const [copiedDomain, setCopiedDomain] = useState(false); + const [copiedAddress, setCopiedAddress] = useState(false); + const {receiveAddress, img, badgeImg} = wallet; + + const copyToClipboard = (text: string) => { + haptic('impactLight'); + Clipboard.setString(text); + }; + + useEffect(() => { + const timer = setTimeout(() => { + setCopiedEmail(false); + }, 3000); + return () => clearTimeout(timer); + }, [copiedEmail]); + + useEffect(() => { + const timer = setTimeout(() => { + setCopiedAddress(false); + }, 3000); + return () => clearTimeout(timer); + }, [copiedAddress]); + + useEffect(() => { + const timer = setTimeout(() => { + setCopiedDomain(false); + }, 3000); + return () => clearTimeout(timer); + }, [copiedDomain]); + + const shareAddress = async (text: string) => { + await Share.share({ + message: text, + }); + }; + + return ( + + + + + {t('Share Address')} + + + {email ? ( + + shareAddress(email)}> + + + + {email} + + + { + copyToClipboard(email); + setCopiedEmail(true); + }}> + {!copiedEmail ? ( + + ) : ( + + )} + + + + ) : null} + + {domain && domainType ? ( + + shareAddress(domain)}> + + {domainType === 'ens' ? ( + + ) : ( + + )} + + {domain} + + + { + copyToClipboard(domain); + setCopiedDomain(true); + }}> + {!copiedDomain ? ( + + ) : ( + + )} + + + + ) : null} + + {receiveAddress ? ( + + shareAddress(receiveAddress)}> + + + + {receiveAddress} + + + { + copyToClipboard(receiveAddress); + setCopiedAddress(true); + }}> + {!copiedAddress ? ( + + ) : ( + + )} + + + + ) : null} + + + + + ); +}; + +export default ShareAddressModal; diff --git a/src/navigation/wallet/screens/WalletDetails.tsx b/src/navigation/wallet/screens/WalletDetails.tsx index 30bb723d4f..304352cf4b 100644 --- a/src/navigation/wallet/screens/WalletDetails.tsx +++ b/src/navigation/wallet/screens/WalletDetails.tsx @@ -1,7 +1,7 @@ import {useNavigation, useTheme} from '@react-navigation/native'; import {StackScreenProps} from '@react-navigation/stack'; import i18next from 'i18next'; -import _ from 'lodash'; +import _, {debounce} from 'lodash'; import React, { ReactElement, useCallback, @@ -50,9 +50,11 @@ import { Wallet, } from '../../../store/wallet/wallet.models'; import { + Action, Air, Black, LightBlack, + LinkBlue, LuckySevens, SlateDark, White, @@ -127,6 +129,15 @@ import { import SentBadgeSvg from '../../../../assets/img/sent-badge.svg'; import {Analytics} from '../../../store/analytics/analytics.effects'; import {getGiftCardIcons} from '../../../lib/gift-cards/gift-card'; +import DomainPill from '../components/DomainPill'; +import ShareIcon from '../../../components/icons/share/Share'; +import {getENSDomainByAddress} from '../../../store/moralis/moralis.effects'; +import ShareAddressModal from '../components/ShareAddressModal'; +import CopyToClipboardIcon from '../../../components/icons/copy-to-clipboard/CopyToClipboardIcon'; +import haptic from '../../../components/haptic-feedback/haptic'; +import Clipboard from '@react-native-community/clipboard'; +import CopiedSvg from '../../../../assets/img/copied-success.svg'; +import {DomainType} from '../../../components/list/ContactRow'; export type WalletDetailsScreenParamList = { walletId: string; @@ -145,7 +156,7 @@ const WalletDetailsContainer = styled.View` `; const HeaderContainer = styled.View` - margin: 32px 0 24px; + margin: 10px 0 24px; `; const Row = styled.View` @@ -162,7 +173,7 @@ const TouchableRow = styled.TouchableOpacity` `; const BalanceContainer = styled.View` - padding: 0 15px 40px; + padding: 0 15px 29px; flex-direction: column; `; @@ -254,6 +265,12 @@ const TypeText = styled(BaseText)` color: ${({theme: {dark}}) => (dark ? LuckySevens : SlateDark)}; `; +const DomainPillContainer = styled.TouchableOpacity` + flex-direction: row; + justify-content: center; + margin-top: 12px; +`; + const getWalletType = ( key: Key, wallet: Wallet, @@ -298,6 +315,7 @@ const WalletDetails: React.FC = ({route}) => { const contactList = useAppSelector(({CONTACT}) => CONTACT.list); const defaultAltCurrency = useAppSelector(({APP}) => APP.defaultAltCurrency); const fullWalletObj = findWalletById(wallets, walletId) as Wallet; + const key = keys[fullWalletObj.keyId]; const uiFormattedWallet = buildUIFormattedWallet( fullWalletObj, @@ -452,6 +470,61 @@ const WalletDetails: React.FC = ({route}) => { const [needActionPendingTxps, setNeedActionPendingTxps] = useState([]); const [needActionUnsentTxps, setNeedActionUnsentTxps] = useState([]); + const user = useAppSelector(({BITPAY_ID}) => BITPAY_ID.user[network]); + const [domain, setDomain] = useState(); + const [domainType, setDomainType] = useState(); + const [showShareAddressModal, setShowShareAddressModal] = useState(false); + const [showShareAddressIcon, setShowShareAddressIcon] = useState(false); + const [copied, setCopied] = useState(false); + + const fetchENSDomainByAddress = useMemo( + () => + debounce(async ({address}: {address: string}) => { + try { + if (!address) { + return; + } + const _domain = await dispatch(getENSDomainByAddress({address})); + if (_domain) { + setDomain(_domain); + setDomainType('ens'); + } + } catch (err) { + console.error(err); + } + }, 300), + [], + ); + + if ( + fullWalletObj.receiveAddress && + SUPPORTED_EVM_COINS.includes(currencyAbbreviation.toLocaleLowerCase()) + ) { + fetchENSDomainByAddress({address: fullWalletObj.receiveAddress}); + } + + const copyToClipboard = (text: string) => { + haptic('impactLight'); + Clipboard.setString(text); + setCopied(true); + }; + + useEffect(() => { + if (!copied) { + return; + } + const timer = setTimeout(() => { + setCopied(false); + }, 3000); + + return () => clearTimeout(timer); + }, [copied]); + + useEffect(() => { + const _arr = _.compact([fullWalletObj.receiveAddress, domain, user?.email]); + setShowShareAddressIcon(_arr.length > 1 ? true : false); + }, [fullWalletObj.receiveAddress, domain]); + const setNeedActionTxps = (pendingTxps: TransactionProposal[]) => { const txpsPending: TransactionProposal[] = []; const txpsUnsent: TransactionProposal[] = []; @@ -916,6 +989,45 @@ const WalletDetails: React.FC = ({route}) => { return ( <> + + {walletType && ( + + {walletType.icon ? ( + {walletType.icon} + ) : null} + {walletType.title} + + )} + {protocolName ? ( + + + + + {protocolName} + + ) : null} + {IsShared(fullWalletObj) ? ( + + + Multisig {fullWalletObj.credentials.m}/ + {fullWalletObj.credentials.n} + + + ) : null} + {['xrp'].includes(fullWalletObj?.currencyAbbreviation) ? ( + setShowBalanceDetailsModal(true)}> + + + ) : null} + {['xrp'].includes(fullWalletObj?.currencyAbbreviation) && + Number(fullWalletObj?.balance?.cryptoConfirmedLocked) >= + 10 ? ( + + {t('Activated')} + + ) : null} + { @@ -954,45 +1066,35 @@ const WalletDetails: React.FC = ({route}) => { )} - - {walletType && ( - - {walletType.icon ? ( - {walletType.icon} - ) : null} - {walletType.title} - - )} - {protocolName ? ( - - - - - {protocolName} - - ) : null} - {IsShared(fullWalletObj) ? ( - - - Multisig {fullWalletObj.credentials.m}/ - {fullWalletObj.credentials.n} - - - ) : null} - {['xrp'].includes(fullWalletObj?.currencyAbbreviation) ? ( - setShowBalanceDetailsModal(true)}> - - - ) : null} - {['xrp'].includes(fullWalletObj?.currencyAbbreviation) && - Number(fullWalletObj?.balance?.cryptoConfirmedLocked) >= - 10 ? ( - - {t('Activated')} - - ) : null} - + {fullWalletObj.receiveAddress ? ( + + + ) : !copied ? ( + + ) : ( + + ) + } + onPress={() => { + showShareAddressIcon + ? setShowShareAddressModal(true) + : copyToClipboard(fullWalletObj.receiveAddress!); + }} + description={fullWalletObj.receiveAddress} + /> + + ) : null} {fullWalletObj ? ( @@ -1182,6 +1284,17 @@ const WalletDetails: React.FC = ({route}) => { wallet={fullWalletObj} /> ) : null} + + {fullWalletObj ? ( + setShowShareAddressModal(false)} + wallet={fullWalletObj} + domain={domain} + domainType={domainType} + email={user?.email} + /> + ) : null} ); }; diff --git a/src/navigation/wallet/screens/send/SendTo.tsx b/src/navigation/wallet/screens/send/SendTo.tsx index 6beca393de..e9ac34eb4b 100644 --- a/src/navigation/wallet/screens/send/SendTo.tsx +++ b/src/navigation/wallet/screens/send/SendTo.tsx @@ -346,7 +346,8 @@ const SendTo = () => { contact.chain === chain.toLowerCase() && contact.network === network && (contact.name.toLowerCase().includes(searchInput.toLowerCase()) || - contact.email?.toLowerCase().includes(searchInput.toLowerCase())), + contact.email?.toLowerCase().includes(searchInput.toLowerCase()) || + contact.domain?.toLowerCase().includes(searchInput.toLowerCase())), ); }, [allContacts, currencyAbbreviation, network, searchInput]);