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:*"