diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts
index a3f1bac360c..a918051cdfd 100644
--- a/suite-native/intl/src/en.ts
+++ b/suite-native/intl/src/en.ts
@@ -517,6 +517,8 @@ export const en = {
receiveTitle: 'Receive',
screenTitle: '{coinSymbol} Receive address',
deviceCancelError: 'Address confirmation canceled.',
+ xrpDestinationTag:
+ 'When sending XRP to Trezor, your online exchange may require a destination tag, but Trezor doesn’t. Enter any random number to proceed. Learn more.',
receiveAddressCard: {
alert: {
success: 'Receive address has been confirmed on your Trezor.',
@@ -1130,7 +1132,13 @@ export const en = {
addressQrLabel: 'Scan recipient address',
amountLabel: 'Amount to be sent',
maxButton: 'Send max',
- destinationTagLabel: 'Destination tag',
+ destinationTag: {
+ label: 'Destination tag',
+ warning:
+ 'Online exchanges require this to identify your account. Get your destination tag from your Ripple account. Make sure you really don’t need it.',
+ info: 'Online exchanges require this to identify your account. Get your destination tag from your exchange.',
+ linkText: 'What’s this?',
+ },
},
},
fees: {
diff --git a/suite-native/module-send/src/components/DestinationTagInput.tsx b/suite-native/module-send/src/components/DestinationTagInput.tsx
index e583f779126..84d7a7be25a 100644
--- a/suite-native/module-send/src/components/DestinationTagInput.tsx
+++ b/suite-native/module-send/src/components/DestinationTagInput.tsx
@@ -1,78 +1,59 @@
import { useRef, useState } from 'react';
import { TextInput, View, findNodeHandle } from 'react-native';
-import Animated, {
- FadeIn,
- FadeOut,
- LinearTransition,
- useSharedValue,
-} from 'react-native-reanimated';
-import { useSelector } from 'react-redux';
+import Animated, { FadeIn, FadeOut, useSharedValue } from 'react-native-reanimated';
-import { useRoute } from '@react-navigation/native';
-
-import { Box, IconButton, Text } from '@suite-native/atoms';
+import { AlertBox, AnimatedVStack, HStack, Switch, Text, VStack } from '@suite-native/atoms';
import { TextInputField, useFormContext } from '@suite-native/forms';
+import { Icon } from '@suite-native/icons';
import { Translation } from '@suite-native/intl';
-import {
- SendStackParamList,
- SendStackRoutes,
- StackProps,
- useScrollView,
-} from '@suite-native/navigation';
+import { Link } from '@suite-native/link';
+import { useScrollView } from '@suite-native/navigation';
import { useDebounce } from '@trezor/react-utils';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
import { integerTransformer } from '../hooks/useSendAmountTransformers';
-import { NativeSendRootState, selectRippleDestinationTagFromDraft } from '../sendFormSlice';
import { SendFieldName, SendOutputsFormValues } from '../sendOutputsFormSchema';
-const inputWrapperStyle = prepareNativeStyle<{ isInputDisplayed: boolean }>(
- (utils, { isInputDisplayed }) => ({
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
+const titleTextStyle = prepareNativeStyle(utils => ({
+ flex: 1,
+ gap: utils.spacings.sp12,
+}));
- extend: {
- condition: isInputDisplayed,
- style: {
- alignItems: undefined,
- flexDirection: 'column',
- gap: utils.spacings.sp12,
- },
- },
- }),
-);
+const inputWrapperStyle = prepareNativeStyle(utils => ({
+ justifyContent: 'space-between',
+ flexDirection: 'column',
+ gap: utils.spacings.sp12,
+}));
const SCROLL_TO_DELAY = 200;
-type RouteProps = StackProps['route'];
-
export const DestinationTagInput = () => {
- const route = useRoute();
- const { accountKey, tokenContract } = route.params;
const inputWrapperRef = useRef(null);
const inputRef = useRef(null);
const inputHeight = useSharedValue(null);
const scrollView = useScrollView();
const { applyStyle } = useNativeStyles();
- const isDestinationTagPresentInDraft = !!useSelector((state: NativeSendRootState) =>
- selectRippleDestinationTagFromDraft(state, accountKey, tokenContract),
- );
-
- const [isInputDisplayed, setIsInputDisplayed] = useState(isDestinationTagPresentInDraft);
+ const [isInputDisplayed, setIsInputDisplayed] = useState(true);
const destinationTagFieldName: SendFieldName = 'rippleDestinationTag';
- const debounce = useDebounce();
-
- const { trigger } = useFormContext();
+ const isRippleDestinationTagEnabledFieldName: SendFieldName = 'isRippleDestinationTagEnabled';
- const handleShowInput = () => {
- setIsInputDisplayed(true);
+ const debounce = useDebounce();
- // Wait for input element to be mounted.
- setTimeout(() => {
- inputRef.current?.focus();
- });
+ const { trigger, setValue } = useFormContext();
+
+ const handleShowInputChange = () => {
+ if (!isInputDisplayed) {
+ setValue(isRippleDestinationTagEnabledFieldName, true);
+ // Wait for input element to be mounted.
+ setTimeout(() => {
+ inputRef.current?.focus();
+ });
+ } else {
+ setValue(isRippleDestinationTagEnabledFieldName, false);
+ }
+ trigger(destinationTagFieldName);
+ setIsInputDisplayed(!isInputDisplayed);
};
const handleInputFocus = () => {
@@ -101,36 +82,60 @@ export const DestinationTagInput = () => {
};
return (
-
-
-
+
+
+
-
+
-
- {!isInputDisplayed && (
-
-
-
- )}
- {isInputDisplayed && (
-
-
+ (
+
+ ),
+ }}
/>
-
- )}
-
-
+
+
+
+
+ {isInputDisplayed ? (
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+ }
+ />
+
+ )}
+
);
};
diff --git a/suite-native/module-send/src/screens/SendOutputsScreen.tsx b/suite-native/module-send/src/screens/SendOutputsScreen.tsx
index d67eb96aee0..7b3b6b252b3 100644
--- a/suite-native/module-send/src/screens/SendOutputsScreen.tsx
+++ b/suite-native/module-send/src/screens/SendOutputsScreen.tsx
@@ -49,10 +49,13 @@ import { constructFormDraft } from '../utils';
const getDefaultValues = ({
tokenContract,
+ isRippleDestinationTagEnabled,
}: {
tokenContract?: TokenAddress;
+ isRippleDestinationTagEnabled: boolean;
}): Readonly =>
({
+ isRippleDestinationTagEnabled,
outputs: [
{
amount: '',
@@ -113,7 +116,10 @@ export const SendOutputsScreen = ({
decimals: tokenInfo?.decimals ?? network?.decimals,
isTaprootAvailable: !deviceUnavailableCapabilities?.taproot,
},
- defaultValues: getDefaultValues({ tokenContract }),
+ defaultValues: getDefaultValues({
+ tokenContract,
+ isRippleDestinationTagEnabled: network?.networkType === 'ripple',
+ }),
});
const {
diff --git a/suite-native/module-send/src/sendOutputsFormSchema.ts b/suite-native/module-send/src/sendOutputsFormSchema.ts
index 38fa1fab856..fb0a1b69b0e 100644
--- a/suite-native/module-send/src/sendOutputsFormSchema.ts
+++ b/suite-native/module-send/src/sendOutputsFormSchema.ts
@@ -194,10 +194,36 @@ export const sendOutputsFormValidationSchema = yup.object({
}),
)
.required(),
+ isRippleDestinationTagEnabled: yup.boolean(),
rippleDestinationTag: yup
.string()
- .optional()
+ .when('isRippleDestinationTagEnabled', {
+ is: true,
+ then: schema => schema.required('Destination Tag is required'),
+ otherwise: schema => schema.notRequired(),
+ })
.matches(/^\d*$/, 'You can only use positive numbers for the destination tag.')
+ .test(
+ 'is-destination-tag-required',
+ 'Destination tag was not set.',
+ (
+ value,
+ {
+ options: { context },
+ schema: { isRippleDestinationTagEnabled },
+ }: yup.TestContext,
+ ) => {
+ const { symbol } = context!;
+
+ if (!symbol) return true;
+ if (getNetworkType(symbol) !== 'ripple') return true;
+
+ // isRippleDestinationTagEnabled is enabled, tag should be set
+ if (!value && isRippleDestinationTagEnabled) return false;
+
+ return true;
+ },
+ )
.test(
'is-destination-tag-in-range',
'Destination tag is too high.',
diff --git a/suite-native/receive/src/screens/ReceiveAddressScreen.tsx b/suite-native/receive/src/screens/ReceiveAddressScreen.tsx
index 6073fbb2152..e46df23c349 100644
--- a/suite-native/receive/src/screens/ReceiveAddressScreen.tsx
+++ b/suite-native/receive/src/screens/ReceiveAddressScreen.tsx
@@ -10,12 +10,13 @@ import {
} from '@suite-common/wallet-core';
import { AccountKey, TokenAddress } from '@suite-common/wallet-types';
import { AccountDetailsCard } from '@suite-native/accounts';
-import { Box, ErrorMessage, VStack } from '@suite-native/atoms';
+import { AlertBox, Box, ErrorMessage, VStack } from '@suite-native/atoms';
import {
ConfirmOnTrezorImage,
selectHasFirmwareAuthenticityCheckHardFailed,
} from '@suite-native/device';
import { Translation } from '@suite-native/intl';
+import { Link } from '@suite-native/link';
import { CloseActionType, Screen } from '@suite-native/navigation';
import { ReceiveBlockedDeviceCompromisedScreen } from './ReceiveBlockedDeviceCompromisedScreen';
@@ -66,6 +67,8 @@ export const ReceiveAddressScreen = ({
const isConfirmOnTrezorReady =
isUnverifiedAddressRevealed && !isReceiveApproved && hasReceiveButtonRequest;
+ const showXrpInfo = account.networkType === 'ripple';
+
return (
+ {showXrpInfo && (
+ (
+
+ ),
+ }}
+ />
+ }
+ />
+ )}
{isAccountDetailVisible && (
)}