diff --git a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx
index 179ef8defdf..60dd457215d 100644
--- a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx
+++ b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx
@@ -11,6 +11,8 @@ import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask';
import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton';
import { SwapActionButton } from '@/__swaps__/screens/Swap/components/SwapActionButton';
import { SwapInput } from '@/__swaps__/screens/Swap/components/SwapInput';
+import { SwapNativeInput } from '@/__swaps__/screens/Swap/components/SwapNativeInput';
+import { SwapInputValuesCaret } from '@/__swaps__/screens/Swap/components/SwapInputValuesCaret';
import { TokenList } from '@/__swaps__/screens/Swap/components/TokenList/TokenList';
import { BASE_INPUT_WIDTH, INPUT_INNER_WIDTH, INPUT_PADDING, THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants';
@@ -50,7 +52,7 @@ function SwapInputActionButton() {
}
function SwapInputAmount() {
- const { focusedInput, SwapTextStyles, SwapInputController, AnimatedSwapStyles } = useSwapContext();
+ const { focusedInput, SwapTextStyles, SwapInputController } = useSwapContext();
return (
{SwapInputController.formattedInputAmount}
-
-
-
+
@@ -119,15 +119,7 @@ function InputAssetBalanceBadge() {
}
export function SwapInputAsset() {
- const {
- outputProgress,
- inputProgress,
- AnimatedSwapStyles,
- SwapTextStyles,
- SwapInputController,
- internalSelectedInputAsset,
- SwapNavigation,
- } = useSwapContext();
+ const { outputProgress, inputProgress, AnimatedSwapStyles, internalSelectedInputAsset, SwapNavigation } = useSwapContext();
return (
@@ -143,9 +135,7 @@ export function SwapInputAsset() {
-
- {SwapInputController.formattedInputNativeValue}
-
+
@@ -174,14 +164,6 @@ export const styles = StyleSheet.create({
backgroundOverlay: {
backgroundColor: 'rgba(0, 0, 0, 0.88)',
},
- caret: {
- height: 32,
- width: 2,
- },
- caretContainer: {
- flexGrow: 100,
- flexShrink: 0,
- },
flipButton: {
borderRadius: 15,
height: 30,
diff --git a/src/__swaps__/screens/Swap/components/SwapInputValuesCaret.tsx b/src/__swaps__/screens/Swap/components/SwapInputValuesCaret.tsx
new file mode 100644
index 00000000000..99c57675935
--- /dev/null
+++ b/src/__swaps__/screens/Swap/components/SwapInputValuesCaret.tsx
@@ -0,0 +1,95 @@
+import { Box, useColorMode } from '@/design-system';
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import Animated, { Easing, SharedValue, useAnimatedStyle, withRepeat, withSequence, withTiming } from 'react-native-reanimated';
+import { SLIDER_COLLAPSED_HEIGHT, SLIDER_HEIGHT, caretConfig } from '@/__swaps__/screens/Swap/constants';
+import { equalWorklet } from '@/__swaps__/safe-math/SafeMath';
+import { NavigationSteps } from '@/__swaps__/screens/Swap/hooks/useSwapNavigation';
+import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
+import { inputKeys } from '@/__swaps__/types/swap';
+import { getColorValueForThemeWorklet } from '@/__swaps__/utils/swaps';
+
+export function SwapInputValuesCaret({ inputCaretType, disabled }: { inputCaretType: inputKeys; disabled?: SharedValue }) {
+ const { isDarkMode } = useColorMode();
+ const {
+ configProgress,
+ focusedInput,
+ inputProgress,
+ internalSelectedInputAsset,
+ internalSelectedOutputAsset,
+ isQuoteStale,
+ outputProgress,
+ SwapInputController,
+ sliderPressProgress,
+ } = useSwapContext();
+
+ const inputMethod = SwapInputController.inputMethod;
+ const inputValues = SwapInputController.inputValues;
+
+ const caretStyle = useAnimatedStyle(() => {
+ const shouldShow =
+ !disabled?.value &&
+ configProgress.value === NavigationSteps.INPUT_ELEMENT_FOCUSED &&
+ focusedInput.value === inputCaretType &&
+ inputProgress.value === 0 &&
+ outputProgress.value === 0 &&
+ (inputMethod.value !== 'slider' ||
+ (inputMethod.value === 'slider' && equalWorklet(inputValues.value.inputAmount, 0)) ||
+ (sliderPressProgress.value === SLIDER_COLLAPSED_HEIGHT / SLIDER_HEIGHT && isQuoteStale.value === 0));
+
+ const opacity = shouldShow
+ ? withRepeat(
+ withSequence(
+ withTiming(1, { duration: 0 }),
+ withTiming(1, { duration: 400, easing: Easing.bezier(0.87, 0, 0.13, 1) }),
+ withTiming(0, caretConfig),
+ withTiming(1, caretConfig)
+ ),
+ -1,
+ true
+ )
+ : withTiming(0, caretConfig);
+
+ const isZero =
+ (inputMethod.value !== 'slider' && inputValues.value[inputCaretType] === 0) ||
+ (inputMethod.value === 'slider' && equalWorklet(inputValues.value.inputAmount, 0));
+
+ return {
+ display: shouldShow ? 'flex' : 'none',
+ opacity,
+ position: isZero ? 'absolute' : 'relative',
+ };
+ });
+
+ const assetCaretStyle = useAnimatedStyle(() => {
+ const selectedAsset =
+ inputCaretType === 'inputAmount' || inputCaretType === 'inputNativeValue' ? internalSelectedInputAsset : internalSelectedOutputAsset;
+ return {
+ backgroundColor: getColorValueForThemeWorklet(selectedAsset.value?.highContrastColor, isDarkMode, true),
+ };
+ });
+
+ const caretSizeStyle =
+ inputCaretType === 'inputNativeValue' || inputCaretType === 'outputNativeValue' ? styles.nativeCaret : styles.inputCaret;
+
+ return (
+
+
+
+ );
+}
+
+export const styles = StyleSheet.create({
+ nativeCaret: {
+ height: 19,
+ width: 1.5,
+ },
+ inputCaret: {
+ height: 32,
+ width: 2,
+ },
+ caretContainer: {
+ flexGrow: 100,
+ flexShrink: 0,
+ },
+});
diff --git a/src/__swaps__/screens/Swap/components/SwapNativeInput.tsx b/src/__swaps__/screens/Swap/components/SwapNativeInput.tsx
new file mode 100644
index 00000000000..7432d641750
--- /dev/null
+++ b/src/__swaps__/screens/Swap/components/SwapNativeInput.tsx
@@ -0,0 +1,82 @@
+import { AnimatedText, Box } from '@/design-system';
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import Animated, { runOnJS, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated';
+
+import { SwapInputValuesCaret } from '@/__swaps__/screens/Swap/components/SwapInputValuesCaret';
+import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton';
+import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
+import { equalWorklet } from '@/__swaps__/safe-math/SafeMath';
+
+export function SwapNativeInput({
+ nativeInputType,
+ handleTapWhileDisabled,
+}: {
+ nativeInputType: 'inputNativeValue' | 'outputNativeValue';
+ handleTapWhileDisabled?: () => void;
+}) {
+ const {
+ focusedInput,
+ internalSelectedInputAsset,
+ internalSelectedOutputAsset,
+ outputQuotesAreDisabled,
+ SwapTextStyles,
+ SwapInputController,
+ } = useSwapContext();
+
+ const formattedNativeInput =
+ nativeInputType === 'inputNativeValue' ? SwapInputController.formattedInputNativeValue : SwapInputController.formattedOutputNativeValue;
+
+ const textStyle = nativeInputType === 'inputNativeValue' ? SwapTextStyles.inputNativeValueStyle : SwapTextStyles.outputNativeValueStyle;
+
+ const nativeCurrencySymbol = formattedNativeInput.value.slice(0, 1);
+ const formattedNativeValue = useDerivedValue(() => {
+ return formattedNativeInput.value.slice(1);
+ });
+
+ const disabled = useDerivedValue(() => {
+ if (nativeInputType === 'outputNativeValue' && outputQuotesAreDisabled.value) return true;
+
+ // disable caret and pointer events for native inputs when corresponding asset is missing price
+ const asset = nativeInputType === 'inputNativeValue' ? internalSelectedInputAsset : internalSelectedOutputAsset;
+ const assetPrice = asset.value?.nativePrice || asset.value?.price?.value || 0;
+ return !assetPrice || equalWorklet(assetPrice, 0);
+ });
+
+ const pointerEventsStyle = useAnimatedStyle(() => {
+ return {
+ pointerEvents: disabled.value ? 'none' : 'box-only',
+ };
+ });
+
+ return (
+ {
+ 'worklet';
+ if (outputQuotesAreDisabled.value && handleTapWhileDisabled && nativeInputType === 'outputNativeValue') {
+ runOnJS(handleTapWhileDisabled)();
+ } else {
+ focusedInput.value = nativeInputType;
+ }
+ }}
+ >
+
+
+ {nativeCurrencySymbol}
+
+
+
+ {formattedNativeValue}
+
+
+
+
+
+ );
+}
+
+export const styles = StyleSheet.create({
+ nativeContainer: { alignItems: 'center', flexDirection: 'row', height: 17, pointerEvents: 'box-only' },
+ nativeRowContainer: { alignItems: 'center', flexDirection: 'row' },
+});
diff --git a/src/__swaps__/screens/Swap/components/SwapNumberPad.tsx b/src/__swaps__/screens/Swap/components/SwapNumberPad.tsx
index a90b5dace9d..b9b9a5286a4 100644
--- a/src/__swaps__/screens/Swap/components/SwapNumberPad.tsx
+++ b/src/__swaps__/screens/Swap/components/SwapNumberPad.tsx
@@ -11,9 +11,10 @@ import Animated, {
withDelay,
withTiming,
} from 'react-native-reanimated';
-
+import { supportedNativeCurrencies } from '@/references';
import { Bleed, Box, Columns, HitSlop, Separator, Text, useColorMode, useForegroundColor } from '@/design-system';
-import { stripCommas } from '@/__swaps__/utils/swaps';
+import { equalWorklet } from '@/__swaps__/safe-math/SafeMath';
+import { stripNonDecimalNumbers } from '@/__swaps__/utils/swaps';
import {
CUSTOM_KEYBOARD_HEIGHT,
LIGHT_SEPARATOR_COLOR,
@@ -30,6 +31,7 @@ import { colors } from '@/styles';
import { NavigationSteps, useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { IS_IOS } from '@/env';
import { inputKeys } from '@/__swaps__/types/swap';
+import { useAccountSettings } from '@/hooks';
type numberPadCharacter = number | 'backspace' | '.';
@@ -49,23 +51,81 @@ const getFormattedInputKey = (inputKey: inputKeys) => {
export const SwapNumberPad = () => {
const { isDarkMode } = useColorMode();
- const { focusedInput, isQuoteStale, SwapInputController, configProgress, outputQuotesAreDisabled } = useSwapContext();
+ const { nativeCurrency } = useAccountSettings();
+ const {
+ focusedInput,
+ internalSelectedInputAsset,
+ internalSelectedOutputAsset,
+ isQuoteStale,
+ SwapInputController,
+ configProgress,
+ outputQuotesAreDisabled,
+ } = useSwapContext();
const longPressTimer = useSharedValue(0);
- const addNumber = (number?: number) => {
+ const removeFormatting = (inputKey: inputKeys) => {
'worklet';
+ return stripNonDecimalNumbers(SwapInputController[getFormattedInputKey(inputKey)].value);
+ };
+
+ const ignoreChange = ({ currentValue, addingDecimal = false }: { currentValue?: string; addingDecimal?: boolean }) => {
+ 'worklet';
+ // ignore when: outputQuotesAreDisabled and we are updating the output amount or output native value
if ((focusedInput.value === 'outputAmount' || focusedInput.value === 'outputNativeValue') && outputQuotesAreDisabled.value) {
+ return true;
+ }
+
+ // ignore when: corresponding asset does not have a price and we are updating native inputs
+ const inputAssetPrice = internalSelectedInputAsset.value?.nativePrice || internalSelectedInputAsset.value?.price?.value || 0;
+ const outputAssetPrice = internalSelectedOutputAsset.value?.nativePrice || internalSelectedOutputAsset.value?.price?.value || 0;
+ const outputAssetHasNoPrice = !outputAssetPrice || equalWorklet(outputAssetPrice, 0);
+ const inputAssetHasNoPrice = !inputAssetPrice || equalWorklet(inputAssetPrice, 0);
+ if (
+ (focusedInput.value === 'outputNativeValue' && outputAssetHasNoPrice) ||
+ (focusedInput.value === 'inputNativeValue' && inputAssetHasNoPrice)
+ ) {
+ return true;
+ }
+
+ // ignore when: decimals exceed native currency decimals
+ if (currentValue) {
+ const currentValueDecimals = currentValue.split('.')?.[1]?.length ?? -1;
+ const nativeCurrencyDecimals = supportedNativeCurrencies[nativeCurrency].decimals;
+
+ const isNativePlaceholderValue = equalWorklet(currentValue, 0) && SwapInputController.inputMethod.value !== focusedInput.value;
+
+ if (addingDecimal && nativeCurrencyDecimals === 0) {
+ return true;
+ } else if (
+ (focusedInput.value === 'inputNativeValue' || focusedInput.value === 'outputNativeValue') &&
+ !isNativePlaceholderValue &&
+ currentValueDecimals >= nativeCurrencyDecimals
+ ) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ const addNumber = (number?: number) => {
+ 'worklet';
+ const inputKey = focusedInput.value;
+ const currentValue = removeFormatting(inputKey);
+
+ if (ignoreChange({ currentValue })) {
return;
}
// Immediately stop the quote fetching interval
SwapInputController.quoteFetchingInterval.stop();
- const inputKey = focusedInput.value;
- const currentValue = stripCommas(SwapInputController[getFormattedInputKey(inputKey)].value);
+ const inputMethod = SwapInputController.inputMethod.value;
- const newValue = currentValue === '0' ? `${number}` : `${currentValue}${number}`;
+ const isNativePlaceholderValue =
+ equalWorklet(currentValue, 0) && inputMethod !== inputKey && (inputKey === 'inputNativeValue' || inputKey === 'outputNativeValue');
+
+ const newValue = currentValue === '0' || isNativePlaceholderValue ? `${number}` : `${currentValue}${number}`;
// For a uint256, the maximum value is:
// 2e256 − 1 =115792089237316195423570985008687907853269984665640564039457584007913129639935
@@ -79,7 +139,7 @@ export const SwapNumberPad = () => {
isQuoteStale.value = 1;
}
- if (SwapInputController.inputMethod.value !== inputKey) {
+ if (inputMethod !== inputKey) {
SwapInputController.inputMethod.value = inputKey;
}
@@ -94,7 +154,11 @@ export const SwapNumberPad = () => {
const addDecimalPoint = () => {
'worklet';
const inputKey = focusedInput.value;
- const currentValue = stripCommas(SwapInputController[getFormattedInputKey(inputKey)].value);
+ const currentValue = removeFormatting(inputKey);
+
+ if (ignoreChange({ currentValue, addingDecimal: true })) {
+ return;
+ }
if (!currentValue.includes('.')) {
if (SwapInputController.inputMethod.value !== inputKey) {
@@ -115,7 +179,7 @@ export const SwapNumberPad = () => {
const deleteLastCharacter = () => {
'worklet';
- if ((focusedInput.value === 'outputAmount' || focusedInput.value === 'outputNativeValue') && outputQuotesAreDisabled.value) {
+ if (ignoreChange({})) {
return;
}
@@ -125,7 +189,7 @@ export const SwapNumberPad = () => {
SwapInputController.inputMethod.value = inputKey;
}
- const currentValue = stripCommas(SwapInputController[getFormattedInputKey(inputKey)].value);
+ const currentValue = removeFormatting(inputKey);
// Handle deletion, ensuring a placeholder zero remains if the entire number is deleted
const newValue = currentValue.length > 1 ? currentValue.slice(0, -1) : 0;
diff --git a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx
index 3ccde4f4748..e3a5557ead5 100644
--- a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx
+++ b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx
@@ -7,6 +7,8 @@ import { ScreenCornerRadius } from 'react-native-screen-corner-radius';
import { AnimatedSwapCoinIcon } from '@/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon';
import { BalanceBadge } from '@/__swaps__/screens/Swap/components/BalanceBadge';
+import { SwapNativeInput } from '@/__swaps__/screens/Swap/components/SwapNativeInput';
+import { SwapInputValuesCaret } from '@/__swaps__/screens/Swap/components/SwapInputValuesCaret';
import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask';
import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton';
import { SwapActionButton } from '@/__swaps__/screens/Swap/components/SwapActionButton';
@@ -20,7 +22,6 @@ import * as i18n from '@/languages';
import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
import { useSwapsStore } from '@/state/swaps/swapsStore';
-import { ethereumUtils } from '@/utils';
import Clipboard from '@react-native-clipboard/clipboard';
import { CopyPasteMenu } from './CopyPasteMenu';
@@ -51,27 +52,8 @@ function SwapOutputActionButton() {
);
}
-function SwapOutputAmount() {
- const { navigate } = useNavigation();
- const { focusedInput, SwapTextStyles, SwapInputController, AnimatedSwapStyles, outputQuotesAreDisabled } = useSwapContext();
-
- const handleTapWhileDisabled = useCallback(() => {
- const { inputAsset, outputAsset } = useSwapsStore.getState();
- const inputTokenSymbol = inputAsset?.symbol;
- const outputTokenSymbol = outputAsset?.symbol;
- const isCrosschainSwap = inputAsset?.chainId !== outputAsset?.chainId;
- const isBridgeSwap = inputTokenSymbol === outputTokenSymbol;
-
- navigate(Routes.EXPLAIN_SHEET, {
- inputToken: inputTokenSymbol,
- fromChainId: inputAsset?.chainId ?? ChainId.mainnet,
- toChainId: outputAsset?.chainId ?? ChainId.mainnet,
- isCrosschainSwap,
- isBridgeSwap,
- outputToken: outputTokenSymbol,
- type: 'output_disabled',
- });
- }, [navigate]);
+function SwapOutputAmount({ handleTapWhileDisabled }: { handleTapWhileDisabled: () => void }) {
+ const { focusedInput, SwapTextStyles, SwapInputController, outputQuotesAreDisabled } = useSwapContext();
const [isPasteEnabled, setIsPasteEnabled] = useState(() => !outputQuotesAreDisabled.value);
useAnimatedReaction(
@@ -115,9 +97,7 @@ function SwapOutputAmount() {
{SwapInputController.formattedOutputAmount}
-
-
-
+
@@ -147,15 +127,26 @@ function OutputAssetBalanceBadge() {
}
export function SwapOutputAsset() {
- const {
- outputProgress,
- inputProgress,
- AnimatedSwapStyles,
- SwapTextStyles,
- SwapInputController,
- internalSelectedOutputAsset,
- SwapNavigation,
- } = useSwapContext();
+ const { outputProgress, inputProgress, AnimatedSwapStyles, internalSelectedOutputAsset, SwapNavigation } = useSwapContext();
+ const { navigate } = useNavigation();
+
+ const handleTapWhileDisabled = useCallback(() => {
+ const { inputAsset, outputAsset } = useSwapsStore.getState();
+ const inputTokenSymbol = inputAsset?.symbol;
+ const outputTokenSymbol = outputAsset?.symbol;
+ const isCrosschainSwap = inputAsset?.chainId !== outputAsset?.chainId;
+ const isBridgeSwap = inputTokenSymbol === outputTokenSymbol;
+
+ navigate(Routes.EXPLAIN_SHEET, {
+ inputToken: inputTokenSymbol,
+ fromChainId: inputAsset?.chainId ?? ChainId.mainnet,
+ toChainId: outputAsset?.chainId ?? ChainId.mainnet,
+ isCrosschainSwap,
+ isBridgeSwap,
+ outputToken: outputTokenSymbol,
+ type: 'output_disabled',
+ });
+ }, [navigate]);
return (
@@ -165,15 +156,13 @@ export function SwapOutputAsset() {
-
+
-
- {SwapInputController.formattedOutputNativeValue}
-
+
@@ -203,14 +192,6 @@ export const styles = StyleSheet.create({
backgroundOverlay: {
backgroundColor: 'rgba(0, 0, 0, 0.88)',
},
- caret: {
- height: 32,
- width: 2,
- },
- caretContainer: {
- flexGrow: 100,
- flexShrink: 0,
- },
flipButton: {
borderRadius: 15,
height: 30,
diff --git a/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts b/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts
index 6e74ff1b4f1..244dd9f993e 100644
--- a/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts
+++ b/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts
@@ -212,18 +212,6 @@ export function useAnimatedSwapStyles({
};
});
- const assetToSellCaretStyle = useAnimatedStyle(() => {
- return {
- backgroundColor: getColorValueForThemeWorklet(internalSelectedInputAsset.value?.highContrastColor, isDarkMode, true),
- };
- });
-
- const assetToBuyCaretStyle = useAnimatedStyle(() => {
- return {
- backgroundColor: getColorValueForThemeWorklet(internalSelectedOutputAsset.value?.highContrastColor, isDarkMode, true),
- };
- });
-
const flipButtonFetchingStyle = useAnimatedStyle(() => {
if (IS_ANDROID) return { borderWidth: 0 };
return {
@@ -309,9 +297,7 @@ export function useAnimatedSwapStyles({
outputTokenListStyle,
swapActionWrapperStyle,
assetToSellIconStyle,
- assetToSellCaretStyle,
assetToBuyIconStyle,
- assetToBuyCaretStyle,
hideWhileReviewingOrConfiguringGas,
flipButtonFetchingStyle,
searchInputAssetButtonStyle,
diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts
index e82fa75fc39..030f7297458 100644
--- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts
+++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts
@@ -1,4 +1,11 @@
-import { divWorklet, equalWorklet, greaterThanWorklet, isNumberStringWorklet, mulWorklet } from '@/__swaps__/safe-math/SafeMath';
+import {
+ divWorklet,
+ equalWorklet,
+ greaterThanWorklet,
+ isNumberStringWorklet,
+ mulWorklet,
+ toFixedWorklet,
+} from '@/__swaps__/safe-math/SafeMath';
import { SCRUBBER_WIDTH, SLIDER_WIDTH, snappySpringConfig } from '@/__swaps__/screens/Swap/constants';
import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets';
import { ChainId } from '@/chains/types';
@@ -10,7 +17,14 @@ import {
convertRawAmountToDecimalFormat,
handleSignificantDecimalsWorklet,
} from '@/__swaps__/utils/numbers';
-import { addCommasToNumber, buildQuoteParams, clamp, getDefaultSlippageWorklet, trimTrailingZeros } from '@/__swaps__/utils/swaps';
+import {
+ addCommasToNumber,
+ addSymbolToNativeDisplayWorklet,
+ buildQuoteParams,
+ clamp,
+ getDefaultSlippageWorklet,
+ trimTrailingZeros,
+} from '@/__swaps__/utils/swaps';
import { analyticsV2 } from '@/analytics';
import { SPRING_CONFIGS } from '@/components/animations/animationConfigs';
import { useAccountSettings } from '@/hooks';
@@ -102,7 +116,7 @@ export function useSwapInputsController({
return addCommasToNumber(inputValues.value.inputAmount, '0');
}
- if (inputMethod.value === 'outputAmount') {
+ if (inputMethod.value === 'outputAmount' || inputMethod.value === 'inputNativeValue' || inputMethod.value === 'outputNativeValue') {
return valueBasedDecimalFormatter({
amount: inputValues.value.inputAmount,
nativePrice: inputNativePrice.value,
@@ -121,16 +135,19 @@ export function useSwapInputsController({
});
const formattedInputNativeValue = useDerivedValue(() => {
+ if (inputMethod.value === 'inputNativeValue') {
+ return addSymbolToNativeDisplayWorklet(inputValues.value.inputNativeValue, currentCurrency);
+ }
if (
(inputMethod.value === 'slider' && percentageToSwap.value === 0) ||
!inputValues.value.inputNativeValue ||
!isNumberStringWorklet(inputValues.value.inputNativeValue.toString()) ||
equalWorklet(inputValues.value.inputNativeValue, 0)
) {
- return convertAmountToNativeDisplayWorklet(0, currentCurrency);
+ return convertAmountToNativeDisplayWorklet(0, currentCurrency, false, true);
}
- return convertAmountToNativeDisplayWorklet(inputValues.value.inputNativeValue, currentCurrency);
+ return convertAmountToNativeDisplayWorklet(inputValues.value.inputNativeValue, currentCurrency, false, true);
});
const formattedOutputAmount = useDerivedValue(() => {
@@ -154,16 +171,19 @@ export function useSwapInputsController({
});
const formattedOutputNativeValue = useDerivedValue(() => {
+ if (inputMethod.value === 'outputNativeValue') {
+ return addSymbolToNativeDisplayWorklet(inputValues.value.outputNativeValue, currentCurrency);
+ }
if (
(inputMethod.value === 'slider' && percentageToSwap.value === 0) ||
!inputValues.value.outputNativeValue ||
!isNumberStringWorklet(inputValues.value.outputNativeValue.toString()) ||
equalWorklet(inputValues.value.outputNativeValue, 0)
) {
- return convertAmountToNativeDisplayWorklet(0, currentCurrency);
+ return convertAmountToNativeDisplayWorklet(0, currentCurrency, false, true);
}
- return convertAmountToNativeDisplayWorklet(inputValues.value.outputNativeValue, currentCurrency);
+ return convertAmountToNativeDisplayWorklet(inputValues.value.outputNativeValue, currentCurrency, false, true);
});
const updateNativePriceForAsset = useCallback(
@@ -206,7 +226,7 @@ export function useSwapInputsController({
}
// NOTE: if we encounter a quote error, let's make sure to update the outputAmount and inputAmount to 0 accordingly
- if (lastTypedInput.value === 'inputAmount') {
+ if (lastTypedInput.value === 'inputAmount' || lastTypedInput.value === 'inputNativeValue') {
inputValues.modify(prev => {
return {
...prev,
@@ -214,7 +234,7 @@ export function useSwapInputsController({
outputNativeValue: 0,
};
});
- } else if (lastTypedInput.value === 'outputAmount') {
+ } else if (lastTypedInput.value === 'outputAmount' || lastTypedInput.value === 'outputNativeValue') {
inputValues.modify(prev => {
return {
...prev,
@@ -246,7 +266,6 @@ export function useSwapInputsController({
quoteFetchingInterval: ReturnType;
}) => {
'worklet';
-
// Check whether the quote has been superseded by new user input so we don't introduce conflicting updates
const isLastTypedInputStillValid = originalQuoteParams.lastTypedInput === lastTypedInput.value;
@@ -259,7 +278,9 @@ export function useSwapInputsController({
const isInputAmountStillValid = originalQuoteParams.inputAmount === inputValues.value.inputAmount;
const isOutputAmountStillValid = originalQuoteParams.outputAmount === inputValues.value.outputAmount;
const areInputAmountsStillValid =
- originalQuoteParams.lastTypedInput === 'inputAmount' ? isInputAmountStillValid : isOutputAmountStillValid;
+ originalQuoteParams.lastTypedInput === 'inputAmount' || originalQuoteParams.lastTypedInput === 'inputNativeValue'
+ ? isInputAmountStillValid
+ : isOutputAmountStillValid;
// Set prices first regardless of the quote status, as long as the same assets are still selected
if (inputPrice && isInputUniqueIdStillValid) {
@@ -476,7 +497,7 @@ export function useSwapInputsController({
}
const quotedInputAmount =
- lastTypedInputParam === 'outputAmount'
+ lastTypedInputParam === 'outputAmount' || lastTypedInputParam === 'outputNativeValue'
? Number(
convertRawAmountToDecimalFormat(
quoteResponse.sellAmount.toString(),
@@ -486,7 +507,7 @@ export function useSwapInputsController({
: undefined;
const quotedOutputAmount =
- lastTypedInputParam === 'inputAmount'
+ lastTypedInputParam === 'inputAmount' || lastTypedInputParam === 'inputNativeValue'
? Number(
convertRawAmountToDecimalFormat(
quoteResponse.buyAmountMinusFees.toString(),
@@ -762,7 +783,7 @@ export function useSwapInputsController({
/**
* Observes value changes in the active inputMethod, which can be any of the following:
* - inputAmount
- * - inputNativeValue (TODO)
+ * - inputNativeValue
* - outputAmount
* - outputNativeValue (TODO)
* - sliderXPosition
@@ -870,6 +891,56 @@ export function useSwapInputsController({
};
});
+ runOnJS(debouncedFetchQuote)();
+ }
+ }
+ const inputMethodValue = inputMethod.value;
+ const isNativeInputMethod = inputMethodValue === 'inputNativeValue';
+ const isNativeOutputMethod = inputMethodValue === 'outputNativeValue';
+ if (
+ (isNativeInputMethod || isNativeOutputMethod) &&
+ !equalWorklet(current.values[inputMethodValue], previous.values[inputMethodValue])
+ ) {
+ // If the number in the native field changes
+ lastTypedInput.value = inputMethodValue;
+ if (equalWorklet(current.values[inputMethodValue], 0)) {
+ // If the native amount was set to 0
+ resetValuesToZeroWorklet({ updateSlider: true, inputKey: inputMethodValue });
+ } else {
+ // If the native amount was set to a non-zero value
+ if (isNativeInputMethod && !internalSelectedInputAsset.value) return;
+ if (isNativeOutputMethod && !internalSelectedOutputAsset.value) return;
+
+ // If the asset price is zero
+ if (isNativeInputMethod && equalWorklet(inputNativePrice.value, 0)) return;
+ if (isNativeOutputMethod && equalWorklet(outputNativePrice.value, 0)) return;
+
+ if (isQuoteStale.value !== 1) isQuoteStale.value = 1;
+ const nativePrice = isNativeInputMethod ? inputNativePrice.value : outputNativePrice.value;
+ const decimalPlaces = isNativeInputMethod
+ ? internalSelectedInputAsset.value?.decimals
+ : internalSelectedOutputAsset.value?.decimals;
+ const amount = toFixedWorklet(divWorklet(current.values[inputMethodValue], nativePrice), decimalPlaces || 18);
+ const amountKey = isNativeInputMethod ? 'inputAmount' : 'outputAmount';
+
+ inputValues.modify(values => {
+ return {
+ ...values,
+ [amountKey]: amount,
+ };
+ });
+
+ if (isNativeInputMethod) {
+ const inputAssetBalance = internalSelectedInputAsset.value?.maxSwappableAmount || '0';
+
+ if (equalWorklet(inputAssetBalance, 0)) {
+ sliderXPosition.value = withSpring(0, snappySpringConfig);
+ } else {
+ const updatedSliderPosition = clamp(Number(divWorklet(amount, inputAssetBalance)) * SLIDER_WIDTH, 0, SLIDER_WIDTH);
+ sliderXPosition.value = withSpring(updatedSliderPosition, snappySpringConfig);
+ }
+ }
+
runOnJS(debouncedFetchQuote)();
}
}
diff --git a/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts b/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts
index 92facea2c77..be633447abb 100644
--- a/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts
+++ b/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts
@@ -1,5 +1,4 @@
import {
- Easing,
SharedValue,
interpolateColor,
useAnimatedStyle,
@@ -11,45 +10,27 @@ import {
} from 'react-native-reanimated';
import { useColorMode, useForegroundColor } from '@/design-system';
-import {
- ETH_COLOR_DARK,
- ETH_COLOR_DARK_ACCENT,
- SLIDER_COLLAPSED_HEIGHT,
- SLIDER_HEIGHT,
- caretConfig,
- pulsingConfig,
-} from '@/__swaps__/screens/Swap/constants';
-import { inputKeys, inputMethods, inputValuesType } from '@/__swaps__/types/swap';
+import { ETH_COLOR_DARK, ETH_COLOR_DARK_ACCENT, pulsingConfig } from '@/__swaps__/screens/Swap/constants';
+import { inputMethods, inputValuesType } from '@/__swaps__/types/swap';
import { getColorValueForThemeWorklet, opacity } from '@/__swaps__/utils/swaps';
import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets';
import { equalWorklet } from '@/__swaps__/safe-math/SafeMath';
import { SPRING_CONFIGS, TIMING_CONFIGS } from '@/components/animations/animationConfigs';
-import { NavigationSteps } from './useSwapNavigation';
export function useSwapTextStyles({
- configProgress,
inputMethod,
inputValues,
internalSelectedInputAsset,
internalSelectedOutputAsset,
isFetching,
isQuoteStale,
- focusedInput,
- inputProgress,
- outputProgress,
- sliderPressProgress,
}: {
- configProgress: SharedValue;
inputMethod: SharedValue;
inputValues: SharedValue;
internalSelectedInputAsset: SharedValue;
internalSelectedOutputAsset: SharedValue;
isFetching: SharedValue;
isQuoteStale: SharedValue;
- focusedInput: SharedValue;
- inputProgress: SharedValue;
- outputProgress: SharedValue;
- sliderPressProgress: SharedValue;
}) {
const { isDarkMode } = useColorMode();
@@ -79,19 +60,30 @@ export function useSwapTextStyles({
});
const isInputZero = useDerivedValue(() => {
- const isZero =
- !internalSelectedInputAsset.value ||
- (inputValues.value.inputAmount === 0 && inputMethod.value !== 'slider') ||
- (inputMethod.value === 'slider' && equalWorklet(inputValues.value.inputAmount, 0));
- return isZero;
+ const isInputAmountZero = inputValues.value.inputAmount === 0;
+ if (!internalSelectedInputAsset.value) return true;
+
+ if (inputMethod.value === 'slider' && equalWorklet(inputValues.value.inputAmount, 0)) return true;
+
+ if (inputMethod.value === 'inputNativeValue' && isInputAmountZero) {
+ return inputValues.value.inputNativeValue === 0;
+ }
+
+ return isInputAmountZero;
});
const isOutputZero = useDerivedValue(() => {
- const isZero =
- !internalSelectedOutputAsset.value ||
- (inputValues.value.outputAmount === 0 && inputMethod.value !== 'slider') ||
- (inputMethod.value === 'slider' && equalWorklet(inputValues.value.outputAmount, 0));
- return isZero;
+ const isOutputAmountZero = inputValues.value.outputAmount === 0;
+
+ if (!internalSelectedOutputAsset.value) return true;
+
+ if (inputMethod.value === 'slider' && equalWorklet(inputValues.value.inputAmount, 0)) return true;
+
+ if (inputMethod.value === 'outputNativeValue' && isOutputAmountZero) {
+ return inputValues.value.outputNativeValue === 0;
+ }
+
+ return isOutputAmountZero;
});
const inputAssetColor = useDerivedValue(() => {
@@ -170,81 +162,10 @@ export function useSwapTextStyles({
};
});
- // TODO: Create a reusable InputCaret component
- const inputCaretStyle = useAnimatedStyle(() => {
- const shouldShow =
- configProgress.value === NavigationSteps.INPUT_ELEMENT_FOCUSED &&
- focusedInput.value === 'inputAmount' &&
- inputProgress.value === 0 &&
- outputProgress.value === 0 &&
- (inputMethod.value !== 'slider' ||
- (inputMethod.value === 'slider' && equalWorklet(inputValues.value.inputAmount, 0)) ||
- (sliderPressProgress.value === SLIDER_COLLAPSED_HEIGHT / SLIDER_HEIGHT && isQuoteStale.value === 0));
-
- const opacity = shouldShow
- ? withRepeat(
- withSequence(
- withTiming(1, { duration: 0 }),
- withTiming(1, { duration: 400, easing: Easing.bezier(0.87, 0, 0.13, 1) }),
- withTiming(0, caretConfig),
- withTiming(1, caretConfig)
- ),
- -1,
- true
- )
- : withTiming(0, caretConfig);
-
- const isZero =
- (inputMethod.value !== 'slider' && inputValues.value.inputAmount === 0) ||
- (inputMethod.value === 'slider' && equalWorklet(inputValues.value.inputAmount, 0));
-
- return {
- display: shouldShow ? 'flex' : 'none',
- opacity,
- position: isZero ? 'absolute' : 'relative',
- };
- });
-
- const outputCaretStyle = useAnimatedStyle(() => {
- const shouldShow =
- configProgress.value === NavigationSteps.INPUT_ELEMENT_FOCUSED &&
- focusedInput.value === 'outputAmount' &&
- inputProgress.value === 0 &&
- outputProgress.value === 0 &&
- (inputMethod.value !== 'slider' ||
- (inputMethod.value === 'slider' && equalWorklet(inputValues.value.inputAmount, 0)) ||
- (sliderPressProgress.value === SLIDER_COLLAPSED_HEIGHT / SLIDER_HEIGHT && isQuoteStale.value === 0));
-
- const opacity = shouldShow
- ? withRepeat(
- withSequence(
- withTiming(1, { duration: 0 }),
- withTiming(1, { duration: 400, easing: Easing.bezier(0.87, 0, 0.13, 1) }),
- withTiming(0, caretConfig),
- withTiming(1, caretConfig)
- ),
- -1,
- true
- )
- : withTiming(0, caretConfig);
-
- const isZero =
- (inputMethod.value !== 'slider' && inputValues.value.outputAmount === 0) ||
- (inputMethod.value === 'slider' && equalWorklet(inputValues.value.inputAmount, 0));
-
- return {
- display: shouldShow ? 'flex' : 'none',
- opacity,
- position: isZero ? 'absolute' : 'relative',
- };
- });
-
return {
inputAmountTextStyle,
- inputCaretStyle,
inputNativeValueStyle,
outputAmountTextStyle,
- outputCaretStyle,
outputNativeValueStyle,
};
}
diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx
index 7e1698d7f3d..abefed50f50 100644
--- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx
+++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx
@@ -439,17 +439,12 @@ export const SwapProvider = ({ children }: SwapProviderProps) => {
});
const SwapTextStyles = useSwapTextStyles({
- configProgress,
- focusedInput,
inputMethod: SwapInputController.inputMethod,
- inputProgress,
inputValues: SwapInputController.inputValues,
internalSelectedInputAsset,
internalSelectedOutputAsset,
isFetching,
isQuoteStale,
- outputProgress,
- sliderPressProgress,
});
const SwapNavigation = useSwapNavigation({
diff --git a/src/__swaps__/types/swap.ts b/src/__swaps__/types/swap.ts
index 3bf0134c6d3..6a513b68024 100644
--- a/src/__swaps__/types/swap.ts
+++ b/src/__swaps__/types/swap.ts
@@ -1,5 +1,4 @@
-import { ExtendedAnimatedAssetWithColors, UniqueId } from './assets';
-import { SearchAsset } from './search';
+import { ExtendedAnimatedAssetWithColors } from './assets';
export type inputKeys = 'inputAmount' | 'inputNativeValue' | 'outputAmount' | 'outputNativeValue';
export type inputMethods = inputKeys | 'slider';
diff --git a/src/__swaps__/utils/__tests__/numbers.test.ts b/src/__swaps__/utils/__tests__/numbers.test.ts
index 609ebc1186b..faf3be13c6b 100644
--- a/src/__swaps__/utils/__tests__/numbers.test.ts
+++ b/src/__swaps__/utils/__tests__/numbers.test.ts
@@ -13,7 +13,7 @@ const testCases = [
{ value: 1234.56, currency: supportedCurrencies.NZD, expected: 'NZ$1,234.56' },
{ value: 1234.56, currency: supportedCurrencies.GBP, expected: '£1,234.56' },
{ value: 1234.56, currency: supportedCurrencies.CNY, expected: '¥1,234.56' },
- { value: 1234.56, currency: supportedCurrencies.JPY, expected: '¥1,234.56' },
+ { value: 1234.56, currency: supportedCurrencies.JPY, expected: '¥1,235' },
{ value: 1234.56, currency: supportedCurrencies.INR, expected: '₹1,234.56' },
{ value: 1234.56, currency: supportedCurrencies.TRY, expected: '₺1,234.56' },
{ value: 1234.56, currency: supportedCurrencies.ZAR, expected: 'R1,234.56' },
diff --git a/src/__swaps__/utils/numbers.ts b/src/__swaps__/utils/numbers.ts
index 38969306888..c6c3e11a115 100644
--- a/src/__swaps__/utils/numbers.ts
+++ b/src/__swaps__/utils/numbers.ts
@@ -314,7 +314,8 @@ export const convertBipsToPercentage = (value: BigNumberish, decimals = 2): stri
export const convertAmountToNativeDisplayWorklet = (
value: number | string,
nativeCurrency: keyof nativeCurrencyType,
- useThreshold = false
+ useThreshold = false,
+ ignoreAlignment = false
) => {
'worklet';
@@ -338,7 +339,7 @@ export const convertAmountToNativeDisplayWorklet = (
maximumFractionDigits: decimals,
});
- const nativeDisplay = `${thresholdReached ? '<' : ''}${alignment === 'left' ? symbol : ''}${nativeValue}${alignment === 'right' ? symbol : ''}`;
+ const nativeDisplay = `${thresholdReached ? '<' : ''}${alignment === 'left' || ignoreAlignment ? symbol : ''}${nativeValue}${!ignoreAlignment && alignment === 'right' ? symbol : ''}`;
return nativeDisplay;
};
diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts
index 6b70afdf030..fc81b10cb35 100644
--- a/src/__swaps__/utils/swaps.ts
+++ b/src/__swaps__/utils/swaps.ts
@@ -17,7 +17,7 @@ import { TokenColors } from '@/graphql/__generated__/metadata';
import * as i18n from '@/languages';
import { RainbowConfig } from '@/model/remoteConfig';
import store from '@/redux/store';
-import { ETH_ADDRESS } from '@/references';
+import { ETH_ADDRESS, supportedNativeCurrencies } from '@/references';
import { userAssetsStore } from '@/state/assets/userAssets';
import { colors } from '@/styles';
import { BigNumberish } from '@ethersproject/bignumber';
@@ -197,6 +197,8 @@ export const findNiceIncrement = (availableBalance: string | number | undefined)
// /---- 🔵 Worklet utils 🔵 ----/ //
//
+type nativeCurrencyType = typeof supportedNativeCurrencies;
+
export function addCommasToNumber(number: string | number, fallbackValue: T = 0 as T): T | string {
'worklet';
if (isNaN(Number(number))) {
@@ -217,14 +219,25 @@ export function addCommasToNumber(number: string | n
}
}
+export const addSymbolToNativeDisplayWorklet = (value: number | string, nativeCurrency: keyof nativeCurrencyType): string => {
+ 'worklet';
+
+ const nativeSelected = supportedNativeCurrencies?.[nativeCurrency];
+ const { symbol } = nativeSelected;
+
+ const nativeValueWithCommas = addCommasToNumber(value, '0');
+
+ return `${symbol}${nativeValueWithCommas}`;
+};
+
export function clamp(value: number, lowerBound: number, upperBound: number) {
'worklet';
return Math.min(Math.max(lowerBound, value), upperBound);
}
-export function stripCommas(value: string) {
+export function stripNonDecimalNumbers(value: string) {
'worklet';
- return value.replace(/,/g, '');
+ return value.replace(/[^0-9.]/g, '');
}
export function trimTrailingZeros(value: string) {
@@ -628,7 +641,6 @@ export const buildQuoteParams = ({
fromAddress: currentAddress,
sellTokenAddress: inputAsset.isNativeAsset ? ETH_ADDRESS_AGGREGATOR : inputAsset.address,
buyTokenAddress: outputAsset.isNativeAsset ? ETH_ADDRESS_AGGREGATOR : outputAsset.address,
- // TODO: Handle native input cases below
sellAmount:
lastTypedInput === 'inputAmount' || lastTypedInput === 'inputNativeValue'
? convertAmountToRawAmount(inputAmount.toString(), inputAsset.decimals)
diff --git a/src/references/supportedCurrencies.ts b/src/references/supportedCurrencies.ts
index 9c356d87fd4..ef1588a377a 100644
--- a/src/references/supportedCurrencies.ts
+++ b/src/references/supportedCurrencies.ts
@@ -98,8 +98,8 @@ export const supportedCurrencies = {
emoji: '🇰🇷',
emojiName: 'south_korea',
label: i18n.t(i18n.l.settings.currency.KRW),
- mask: '[099999999999]{.}[00]',
- placeholder: '0.00',
+ mask: '[099999999999]',
+ placeholder: '0',
userAssetsSmallThreshold: 100,
smallThreshold: 1000,
symbol: '₩',
@@ -139,12 +139,12 @@ export const supportedCurrencies = {
alignment: 'left',
assetLimit: 1,
currency: 'JPY',
- decimals: 2,
+ decimals: 0,
emoji: '🇯🇵',
emojiName: 'japan',
label: i18n.t(i18n.l.settings.currency.JPY),
- mask: '[099999999999]{.}[00]',
- placeholder: '0.00',
+ mask: '[099999999999]',
+ placeholder: '0',
userAssetsSmallThreshold: 10,
smallThreshold: 100,
symbol: '¥',