diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts index 72858e0a6bf..12001a15299 100644 --- a/suite-native/intl/src/en.ts +++ b/suite-native/intl/src/en.ts @@ -938,6 +938,22 @@ export const en = { title: 'Send from', }, outputs: { + correctNetworkMessage: + 'Make sure that you’re sending to an address on\u00A0{networkName} network. Learn more', + tokenOfNetworkSheet: { + title: 'You’re about to Send {tokenSymbol} that runs on {networkName} network.', + body: { + self: { + subtitle: 'Sending to yourself?', + text: 'Make sure your exchange or wallet supports this token on {networkName} network.', + }, + outside: { + subtitle: 'Sending to someone else?', + text: 'Check with them if they’re alright with receiving this token on {networkName} network.', + }, + }, + warning: 'Sending to a wrong network might result in loss of funds.', + }, recipients: { title: 'Amount & recipients', addressLabel: 'Recipient address', diff --git a/suite-native/link/src/components/Link.tsx b/suite-native/link/src/components/Link.tsx index 035797460ea..7bca1b03d24 100644 --- a/suite-native/link/src/components/Link.tsx +++ b/suite-native/link/src/components/Link.tsx @@ -9,7 +9,7 @@ import Animated, { import { RequireExactlyOne } from 'type-fest'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { Color } from '@trezor/theme'; +import { Color, TypographyStyle } from '@trezor/theme'; import { useOpenLink } from '../useOpenLink'; @@ -21,18 +21,22 @@ type LinkProps = RequireExactlyOne< isUnderlined?: boolean; textColor?: Color; textPressedColor?: Color; + textVariant?: TypographyStyle; }, 'href' | 'onPress' >; -const textStyle = prepareNativeStyle<{ isUnderlined: boolean }>((_, { isUnderlined }) => ({ - extend: { - condition: isUnderlined, - style: { - textDecorationLine: 'underline', +const textStyle = prepareNativeStyle<{ isUnderlined: boolean; textVariant: TypographyStyle }>( + (utils, { isUnderlined, textVariant }) => ({ + ...utils.typography[textVariant], + extend: { + condition: isUnderlined, + style: { + textDecorationLine: 'underline', + }, }, - }, -})); + }), +); const ANIMATION_DURATION = 100; const IS_NOT_PRESSED_VALUE = 0; @@ -44,6 +48,7 @@ export const Link = ({ isUnderlined = false, textColor = 'textPrimaryDefault', textPressedColor = 'textPrimaryPressed', + textVariant = 'body', onPress, }: LinkProps) => { const { utils, applyStyle } = useNativeStyles(); @@ -80,7 +85,7 @@ export const Link = ({ onPressIn={handlePressIn} onPress={handlePress} onPressOut={handlePressOut} - style={[applyStyle(textStyle, { isUnderlined }), animatedTextColorStyle]} + style={[applyStyle(textStyle, { isUnderlined, textVariant }), animatedTextColorStyle]} suppressHighlighting > {label} diff --git a/suite-native/module-send/package.json b/suite-native/module-send/package.json index 9663dcafeca..d5bacf62be9 100644 --- a/suite-native/module-send/package.json +++ b/suite-native/module-send/package.json @@ -35,6 +35,7 @@ "@suite-native/helpers": "workspace:*", "@suite-native/icons": "workspace:*", "@suite-native/intl": "workspace:*", + "@suite-native/link": "workspace:*", "@suite-native/navigation": "workspace:*", "@suite-native/qr-code": "workspace:*", "@suite-native/settings": "workspace:*", diff --git a/suite-native/module-send/src/components/AddressInput.tsx b/suite-native/module-send/src/components/AddressInput.tsx index c783f3a6056..f7ca519b5d3 100644 --- a/suite-native/module-send/src/components/AddressInput.tsx +++ b/suite-native/module-send/src/components/AddressInput.tsx @@ -19,6 +19,7 @@ import { isDebugEnv } from '@suite-native/config'; import { QrCodeBottomSheetIcon } from './QrCodeBottomSheetIcon'; import { getOutputFieldName } from '../utils'; import { SendOutputsFormValues } from '../sendOutputsFormSchema'; +import { useTokenOfNetworkAlert } from '../hooks/useTokenOfNetworkAlert'; type AddressInputProps = { index: number; @@ -36,6 +37,8 @@ export const AddressInput = ({ index, accountKey }: AddressInputProps) => { selectFreshAccountAddress(state, accountKey), ); + useTokenOfNetworkAlert({ inputIndex: index }); + const handleScanAddressQRCode = (qrCodeData: string) => { setValue(addressFieldName, qrCodeData, { shouldValidate: true }); if (networkSymbol && isAddressValid(qrCodeData, networkSymbol)) { diff --git a/suite-native/module-send/src/components/CorrectNetworkMessageCard.tsx b/suite-native/module-send/src/components/CorrectNetworkMessageCard.tsx new file mode 100644 index 00000000000..86487e4526f --- /dev/null +++ b/suite-native/module-send/src/components/CorrectNetworkMessageCard.tsx @@ -0,0 +1,55 @@ +import { CryptoIcon } from '@suite-native/icons'; +import { networks, NetworkSymbol } from '@suite-common/wallet-config'; +import { Card, HStack, Text } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; +import { Link } from '@suite-native/link'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { isCoinWithTokens } from '@suite-native/tokens'; + +const cardStyle = prepareNativeStyle(utils => ({ + backgroundColor: utils.colors.backgroundTertiaryDefaultOnElevation0, + borderColor: utils.colors.borderElevation0, + borderWidth: utils.borders.widths.small, + paddingVertical: utils.spacings.sp12, + + ...utils.boxShadows.none, +})); + +type CorrectNetworkMessageCardProps = { + networkSymbol: NetworkSymbol; +}; + +const LINK_URL = 'https://trezor.io/learn/a/how-to-choose-the-right-network'; + +export const CorrectNetworkMessageCard = ({ networkSymbol }: CorrectNetworkMessageCardProps) => { + const { applyStyle } = useNativeStyles(); + + if (!isCoinWithTokens(networkSymbol)) return null; + + const networkName = networks[networkSymbol].name; + + return ( + + + + + ( + + ), + }} + /> + + + + ); +}; diff --git a/suite-native/module-send/src/components/SendOutputFields.tsx b/suite-native/module-send/src/components/SendOutputFields.tsx index 397296c4636..f2689444cc7 100644 --- a/suite-native/module-send/src/components/SendOutputFields.tsx +++ b/suite-native/module-send/src/components/SendOutputFields.tsx @@ -1,13 +1,16 @@ import { useFieldArray } from 'react-hook-form'; +import { useSelector } from 'react-redux'; import { Card, Text, VStack } from '@suite-native/atoms'; import { useFormContext } from '@suite-native/forms'; import { AccountKey } from '@suite-common/wallet-types'; import { Translation } from '@suite-native/intl'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { AccountsRootState, selectAccountNetworkSymbol } from '@suite-common/wallet-core'; import { RecipientInputs } from './RecipientInputs'; import { SendOutputsFormValues } from '../sendOutputsFormSchema'; +import { CorrectNetworkMessageCard } from './CorrectNetworkMessageCard'; type SendOutputFieldsProps = { accountKey: AccountKey; @@ -21,6 +24,9 @@ const cardStyle = prepareNativeStyle(utils => ({ export const SendOutputFields = ({ accountKey }: SendOutputFieldsProps) => { const { applyStyle } = useNativeStyles(); const { control } = useFormContext(); + const networkSymbol = useSelector((state: AccountsRootState) => + selectAccountNetworkSymbol(state, accountKey), + ); const outputsFieldArray = useFieldArray({ control, name: 'outputs' }); return ( @@ -28,6 +34,7 @@ export const SendOutputFields = ({ accountKey }: SendOutputFieldsProps) => { + {networkSymbol && } {outputsFieldArray.fields.map((output, index) => ( diff --git a/suite-native/module-send/src/hooks/useTokenOfNetworkAlert.tsx b/suite-native/module-send/src/hooks/useTokenOfNetworkAlert.tsx new file mode 100644 index 00000000000..baad1434481 --- /dev/null +++ b/suite-native/module-send/src/hooks/useTokenOfNetworkAlert.tsx @@ -0,0 +1,156 @@ +import { ReactNode, useEffect, useRef } from 'react'; +import { useSelector } from 'react-redux'; + +import { useRoute, RouteProp } from '@react-navigation/native'; + +import { getNetwork } from '@suite-common/wallet-config'; +import { Box, VStack, Text, AlertBox } from '@suite-native/atoms'; +import { SendStackParamList, SendStackRoutes } from '@suite-native/navigation'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { Translation } from '@suite-native/intl'; +import { selectAccountTokenSymbol, TokensRootState } from '@suite-native/tokens'; +import { CryptoIcon } from '@suite-native/icons'; +import { useAlert } from '@suite-native/alerts'; +import { useFormContext } from '@suite-native/forms'; +import { isAddressValid } from '@suite-common/wallet-utils'; +import { AccountsRootState, selectAccountNetworkSymbol } from '@suite-common/wallet-core'; +import { AccountKey, TokenAddress } from '@suite-common/wallet-types'; + +import { getOutputFieldName } from '../utils'; + +type UseTokenOfNetworkAlertArgs = { + inputIndex: number; +}; + +const iconWrapperStyle = prepareNativeStyle(() => ({ + overflow: 'visible', + height: 90, + width: 90, +})); + +const networkIconWrapperStyle = prepareNativeStyle(utils => ({ + position: 'absolute', + backgroundColor: utils.colors.backgroundSurfaceElevation1, + padding: 3, + borderRadius: utils.borders.radii.round, + right: 0, + bottom: 0, + overflow: 'visible', +})); + +type ParagraphProps = { + header: ReactNode; + body: ReactNode; +}; + +const Paragraph = ({ header, body }: ParagraphProps) => ( + + {header} + {body} + +); + +const TokenOfNetworkAlertBody = ({ + accountKey, + tokenContract, +}: { + accountKey: AccountKey; + tokenContract?: TokenAddress; +}) => { + const { applyStyle } = useNativeStyles(); + const tokenSymbol = useSelector((state: TokensRootState) => + selectAccountTokenSymbol(state, accountKey, tokenContract), + ); + const networkSymbol = useSelector((state: AccountsRootState) => + selectAccountNetworkSymbol(state, accountKey), + ); + + if (!tokenContract || !networkSymbol) return null; + + const networkName = getNetwork(networkSymbol).name; + + return ( + + + + + + + + + + + + } + body={ + + } + /> + + } + body={ + + } + /> + + + + } + variant="warning" + borderRadius="r12" + /> + + ); +}; + +export const useTokenOfNetworkAlert = ({ inputIndex }: UseTokenOfNetworkAlertArgs) => { + const wasAlertShown = useRef(false); + const { showAlert } = useAlert(); + const { + params: { tokenContract, accountKey }, + } = useRoute>(); + + const tokenSymbol = useSelector((state: TokensRootState) => + selectAccountTokenSymbol(state, accountKey, tokenContract), + ); + const networkSymbol = useSelector((state: AccountsRootState) => + selectAccountNetworkSymbol(state, accountKey), + ); + + const { watch } = useFormContext(); + + const addressValue = watch(getOutputFieldName(inputIndex, 'address')); + + const isFilledValidAddress = + addressValue && networkSymbol && isAddressValid(addressValue, networkSymbol); + + useEffect(() => { + if (tokenContract && isFilledValidAddress && !wasAlertShown.current) { + showAlert({ + appendix: ( + + ), + primaryButtonTitle: , + }); + wasAlertShown.current = true; + } + }, [isFilledValidAddress, showAlert, tokenContract, tokenSymbol, accountKey]); +}; diff --git a/suite-native/module-send/tsconfig.json b/suite-native/module-send/tsconfig.json index 3e0cb278cc5..2d5ef4741e8 100644 --- a/suite-native/module-send/tsconfig.json +++ b/suite-native/module-send/tsconfig.json @@ -38,6 +38,7 @@ { "path": "../helpers" }, { "path": "../icons" }, { "path": "../intl" }, + { "path": "../link" }, { "path": "../navigation" }, { "path": "../qr-code" }, { "path": "../settings" }, diff --git a/yarn.lock b/yarn.lock index b985ce6bbf1..3315327ddaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10235,6 +10235,7 @@ __metadata: "@suite-native/helpers": "workspace:*" "@suite-native/icons": "workspace:*" "@suite-native/intl": "workspace:*" + "@suite-native/link": "workspace:*" "@suite-native/navigation": "workspace:*" "@suite-native/qr-code": "workspace:*" "@suite-native/settings": "workspace:*"