diff --git a/package.json b/package.json index e19178d1de..60a757be57 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "mock-android": "yarn android:mock && yarn android:install", "staging-android": "yarn android:staging && yarn android:install", "prettier": "prettier --write \"src/**/*.js\"", - "lint": "eslint src", + "lint": "eslint src --fix", "flow": "flow", "test": "yarn check --integrity && ./scripts/check-no-dups.sh && yarn lint --quiet", "sync-flowtyped": "NODE_OPTIONS=--max_old_space_size=10000 flow-typed install -s -o && rm flow-typed/npm/axios_*", diff --git a/src/actions/general.js b/src/actions/general.js index e65ab2f7c2..1d41998dd7 100644 --- a/src/actions/general.js +++ b/src/actions/general.js @@ -6,12 +6,14 @@ import { sortAccountsComparatorFromOrder, } from "@ledgerhq/live-common/lib/account"; import type { FlattenAccountsOptions } from "@ledgerhq/live-common/lib/account"; +import type { TrackingPair } from "@ledgerhq/live-common/lib/countervalues/types"; import { useCalculateCountervalueCallback as useCalculateCountervalueCallbackCommon, useCountervaluesPolling, useTrackingPairForAccounts, } from "@ledgerhq/live-common/lib/countervalues/react"; import { useDistribution as useDistributionCommon } from "@ledgerhq/live-common/lib/portfolio/v2/react"; +import { BehaviorSubject } from "rxjs"; import { reorderAccounts } from "./accounts"; import { accountsSelector } from "../reducers/accounts"; import { @@ -22,6 +24,10 @@ import { clearBridgeCache } from "../bridge/cache"; import clearLibcore from "../helpers/clearLibcore"; import { flushAll } from "../components/DBSave"; +const extraSessionTrackingPairsChanges: BehaviorSubject< + TrackingPair[], +> = new BehaviorSubject([]); + export function useDistribution() { const accounts = useSelector(accountsSelector); const to = useSelector(counterValueCurrencySelector); @@ -116,8 +122,36 @@ export function useUserSettings() { ); } -export function useTrackingPairs() { +export function addExtraSessionTrackingPair(trackingPair: TrackingPair) { + const value = extraSessionTrackingPairsChanges.value; + if ( + !value.some( + tp => tp.from === trackingPair.from && tp.to === trackingPair.to, + ) + ) + extraSessionTrackingPairsChanges.next(value.concat(trackingPair)); +} + +export function useExtraSessionTrackingPair() { + const [extraSessionTrackingPair, setExtraSessionTrackingPair] = useState([]); + + useEffect(() => { + const sub = extraSessionTrackingPairsChanges.subscribe( + setExtraSessionTrackingPair, + ); + return () => sub && sub.unsubscribe(); + }, []); + + return extraSessionTrackingPair; +} + +export function useTrackingPairs(): TrackingPair[] { const accounts = useSelector(accountsSelector); const countervalue = useSelector(counterValueCurrencySelector); - return useTrackingPairForAccounts(accounts, countervalue); + const trPairs = useTrackingPairForAccounts(accounts, countervalue); + const extraSessionTrackingPairs = useExtraSessionTrackingPair(); + return useMemo(() => extraSessionTrackingPairs.concat(trPairs), [ + extraSessionTrackingPairs, + trPairs, + ]); } diff --git a/src/components/ConfirmationModal.js b/src/components/ConfirmationModal.js index 37b35bc412..ab5d071d92 100644 --- a/src/components/ConfirmationModal.js +++ b/src/components/ConfirmationModal.js @@ -17,6 +17,7 @@ type Props = {| confirmationTitle?: React$Node, confirmationDesc?: React$Node, Icon?: React$ComponentType<*>, + iconColor?: string, image?: number, confirmButtonText?: React$Node, rejectButtonText?: React$Node, @@ -41,12 +42,16 @@ class ConfirmationModal extends PureComponent { rejectButtonText, onConfirm, Icon, + iconColor, image, alert, hideRejectButton, colors, ...rest } = this.props; + + const iColor = iconColor || colors.live; + return ( { {...rest} > {Icon && ( - - + + )} {image && ( diff --git a/src/components/CounterValue.js b/src/components/CounterValue.js index 53b28ed009..c9930a1be5 100644 --- a/src/components/CounterValue.js +++ b/src/components/CounterValue.js @@ -1,13 +1,20 @@ // @flow -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback, useMemo, useEffect } from "react"; import { BigNumber } from "bignumber.js"; import { useSelector } from "react-redux"; import type { Currency } from "@ledgerhq/live-common/lib/types"; -import { useCalculate } from "@ledgerhq/live-common/lib/countervalues/react"; +import { + useCalculate, + useCountervaluesPolling, +} from "@ledgerhq/live-common/lib/countervalues/react"; import { TouchableOpacity, StyleSheet } from "react-native"; import { Trans } from "react-i18next"; import { useTheme } from "@react-navigation/native"; import { counterValueCurrencySelector } from "../reducers/settings"; +import { + useTrackingPairs, + addExtraSessionTrackingPair, +} from "../actions/general"; import CurrencyUnitValue from "./CurrencyUnitValue"; import LText from "./LText"; import Circle from "./Circle"; @@ -67,6 +74,36 @@ export default function CounterValue({ const value = valueProp instanceof BigNumber ? valueProp.toNumber() : valueProp; const counterValueCurrency = useSelector(counterValueCurrencySelector); + + const trackingPairs = useTrackingPairs(); + const cvPolling = useCountervaluesPolling(); + const hasTrackingPair = useMemo( + () => + trackingPairs.some( + tp => tp.from === currency && tp.to === counterValueCurrency, + ), + [counterValueCurrency, currency, trackingPairs], + ); + + useEffect(() => { + let t; + if (!hasTrackingPair) { + addExtraSessionTrackingPair({ from: currency, to: counterValueCurrency }); + t = setTimeout(cvPolling.poll, 2000); // poll after 2s to ensure debounced CV userSettings are effective after this update + } + + return () => { + if (t) clearTimeout(t); + }; + }, [ + counterValueCurrency, + currency, + cvPolling, + cvPolling.poll, + hasTrackingPair, + trackingPairs, + ]); + const countervalue = useCalculate({ from: currency, to: counterValueCurrency, diff --git a/src/components/CurrencyInput.js b/src/components/CurrencyInput.js index 1e98ccd558..42b2a8a07d 100644 --- a/src/components/CurrencyInput.js +++ b/src/components/CurrencyInput.js @@ -48,6 +48,7 @@ type Props = { style?: *, inputStyle?: *, colors: *, + dynamicFontRatio?: number, }; type State = { @@ -68,6 +69,7 @@ class CurrencyInput extends PureComponent { hasWarning: false, autoFocus: false, editable: true, + dynamicFontRatio: 0.75, }; state = { @@ -150,11 +152,12 @@ class CurrencyInput extends PureComponent { editable, placeholder, colors, + dynamicFontRatio = 0.75, } = this.props; const { displayValue } = this.state; // calculating an approximative font size - const screenWidth = Dimensions.get("window").width * 0.75; + const screenWidth = Dimensions.get("window").width * dynamicFontRatio; const dynamicFontSize = Math.round( clamp( Math.sqrt((screenWidth * 32) / displayValue.length), diff --git a/src/components/FabActions.js b/src/components/FabActions.js index c8d33aca69..cfb4073b59 100644 --- a/src/components/FabActions.js +++ b/src/components/FabActions.js @@ -1,4 +1,4 @@ -// @flow; +// @flow import React from "react"; import { useTheme } from "@react-navigation/native"; @@ -12,8 +12,8 @@ import type { AccountLike, Account } from "@ledgerhq/live-common/lib/types"; import { isCurrencySupported } from "../screens/Exchange/coinifyConfig"; import { - swapSelectableCurrenciesSelector, readOnlyModeEnabledSelector, + swapSelectableCurrenciesSelector, } from "../reducers/settings"; import { accountsCountSelector } from "../reducers/accounts"; import { NavigatorName, ScreenName } from "../const"; @@ -37,9 +37,11 @@ function FabAccountActions({ account, parentAccount }: FabAccountActionsProps) { const { colors } = useTheme(); const currency = getAccountCurrency(account); - const availableOnSwap = useSelector(state => - swapSelectableCurrenciesSelector(state), + const swapSelectableCurrencies = useSelector( + swapSelectableCurrenciesSelector, ); + const availableOnSwap = + swapSelectableCurrencies.includes(currency.id) && account.balance.gt(0); const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector); const canBeBought = isCurrencySupported(currency, "buy"); @@ -69,7 +71,7 @@ function FabAccountActions({ account, parentAccount }: FabAccountActionsProps) { }, ] : []), - ...(availableOnSwap.includes(currency.id) + ...(availableOnSwap ? [ { navigationParams: [ diff --git a/src/components/InfoModal.js b/src/components/InfoModal.js index 002d0cf0b9..3440cea34c 100644 --- a/src/components/InfoModal.js +++ b/src/components/InfoModal.js @@ -21,14 +21,14 @@ type BulletItem = { type InfoModalProps = ModalProps & { id?: string, - title?: string | React$Element<*>, - desc?: string | React$Element<*>, + title?: React$Node, + desc?: React$Node, bullets?: BulletItem[], Icon?: React$ComponentType<*>, withCancel?: boolean, onContinue?: () => void, children?: React$Node, - confirmLabel?: string | React$Element<*>, + confirmLabel?: React$Node, confirmProps?: *, }; diff --git a/src/components/KeyboardView.js b/src/components/KeyboardView.js index e761664597..18c65de483 100644 --- a/src/components/KeyboardView.js +++ b/src/components/KeyboardView.js @@ -1,6 +1,12 @@ // @flow import React from "react"; -import { KeyboardAvoidingView, Platform, NativeModules } from "react-native"; +import { + KeyboardAvoidingView, + Platform, + NativeModules, + StatusBar, +} from "react-native"; +import { useHeaderHeight } from "@react-navigation/elements"; import { HEIGHT as ExperimentalHeaderHeight } from "../screens/Settings/Experimental/ExperimentalHeader"; import useExperimental from "../screens/Settings/Experimental/useExperimental"; @@ -14,17 +20,21 @@ type Props = { const KeyboardView = React.memo( ({ style = { flex: 1 }, children }: *) => { const isExperimental = useExperimental(); + const headerHeight = useHeaderHeight(); + let behavior; let keyboardVerticalOffset = isExperimental ? ExperimentalHeaderHeight : 0; if (Platform.OS === "ios") { keyboardVerticalOffset += DeviceInfo.isIPhoneX_deprecated ? 88 : 64; - behavior = "padding"; + behavior = "height"; } return ( diff --git a/src/components/RootNavigator/BaseNavigator.js b/src/components/RootNavigator/BaseNavigator.js index 954729e100..3f41daf154 100644 --- a/src/components/RootNavigator/BaseNavigator.js +++ b/src/components/RootNavigator/BaseNavigator.js @@ -63,6 +63,11 @@ import RequestAccountNavigator from "./RequestAccountNavigator"; import VerifyAccount from "../../screens/VerifyAccount"; import PlatformApp from "../../screens/Platform/App"; +import SwapFormSelectAccount from "../../screens/Swap/FormSelection/SelectAccountScreen"; +import SwapFormSelectCurrency from "../../screens/Swap/FormSelection/SelectCurrencyScreen"; +import SwapFormSelectFees from "../../screens/Swap/FormSelection/SelectFeesScreen"; +import SwapFormSelectProviderRate from "../../screens/Swap/FormSelection/SelectProviderRateScreen"; + export default function BaseNavigator() { const { t } = useTranslation(); const { colors } = useTheme(); @@ -118,7 +123,56 @@ export default function BaseNavigator() { + ({ + headerTitle: () => ( + + ), + headerRight: null, + })} + /> + , + headerRight: null, + }} + /> + ( + + ), + headerRight: null, + }} + /> + ( + + ), + headerRight: null, + }} /> { - const { params: routeParams } = route; +export default function SwapFormNavigator({ + route, +}: { + route: { params: RouteParams }, +}) { const { t } = useTranslation(); const { colors } = useTheme(); + const { params: routeParams } = route; return ( { ( + /** width has to be a little bigger to accomodate the switch in size between semibold to regular */ {t("transfer.swap.form.tab")} ), }} > - {_props =>
} + {_props => } { /> ); -}; +} const Tab = createMaterialTopTabNavigator(); - -export default SwapFormOrHistory; diff --git a/src/components/RootNavigator/SwapNavigator.js b/src/components/RootNavigator/SwapNavigator.js index 073194f752..93f49f5528 100644 --- a/src/components/RootNavigator/SwapNavigator.js +++ b/src/components/RootNavigator/SwapNavigator.js @@ -2,53 +2,34 @@ import React, { useMemo } from "react"; import { createStackNavigator } from "@react-navigation/stack"; + import { useTranslation } from "react-i18next"; import { useTheme } from "@react-navigation/native"; -import useEnv from "@ledgerhq/live-common/lib/hooks/useEnv"; import { ScreenName } from "../../const"; -import SwapFormOrHistory from "../../screens/Swap/FormOrHistory"; -import SwapSummary from "../../screens/Swap/FormOrHistory/Form/Summary"; -import SwapError from "../../screens/Swap/FormOrHistory/Form/Error"; -import SwapFormAmount from "../../screens/Swap/FormOrHistory/Form/Amount"; +import SwapError from "../../screens/Swap/Error"; import SwapKYC from "../../screens/Swap/KYC"; import SwapKYCStates from "../../screens/Swap/KYC/StateSelect"; -import Swap from "../../screens/Swap"; -import Swap2 from "../../screens/Swap2"; -import SwapOperationDetails from "../../screens/Swap/FormOrHistory/OperationDetails"; -import { BackButton } from "../../screens/OperationDetails"; -import SwapPendingOperation from "../../screens/Swap/FormOrHistory/Form/PendingOperation"; -import SwapFormSelectCrypto from "../../screens/Swap/FormOrHistory/Form/SelectAccount/01-SelectCrypto"; -import SwapFormSelectAccount from "../../screens/Swap/FormOrHistory/Form/SelectAccount/02-SelectAccount"; +import Swap from "../../screens/Swap/SwapEntry"; +import SwapFormNavigator from "./SwapFormNavigator"; import { getStackNavigatorConfig } from "../../navigation/navigatorConfig"; import styles from "../../navigation/styles"; import StepHeader from "../StepHeader"; +import SwapOperationDetails from "../../screens/Swap/OperationDetails"; +import { BackButton } from "../../screens/OperationDetails"; +import SwapPendingOperation from "../../screens/Swap/PendingOperation"; export default function SwapNavigator() { const { t } = useTranslation(); const { colors } = useTheme(); - const isSwapV2Enabled = useEnv("EXPERIMENTAL_SWAP") && __DEV__; const stackNavigationConfig = useMemo( () => getStackNavigatorConfig(colors, true), [colors], ); - if (isSwapV2Enabled) { - return ( - - - - ); - } - return ( - + - , - headerRight: null, - }} - /> - , - headerRight: null, - }} - /> - , - headerRight: null, - }} - /> - , - headerRight: null, + title: t("transfer.swap.form.tab"), }} /> , }; const CVWrapper = ({ children }: { children: * }) => ( @@ -55,6 +56,7 @@ export default function SelectFeesStrategy({ onStrategySelect, onCustomFeesPress, forceUnitLabel, + disabledStrategies, }: Props) { const { t } = useTranslation(); @@ -85,6 +87,9 @@ export default function SelectFeesStrategy({ const renderItem = ({ item }) => ( onPressStrategySelect(item)} + disabled={ + disabledStrategies ? disabledStrategies.includes(item.label) : false + } style={[ styles.feeButton, { @@ -95,7 +100,12 @@ export default function SelectFeesStrategy({ }, ]} > - + { const mainAccount = getMainAccount(account, parentAccount); const C = perFamily[mainAccount.currency.family]; // FIXME: looks like a hack, need to find how to handle networkInfo properly return C && transaction?.networkInfo ? ( { @@ -43,6 +44,7 @@ export default function EthereumFeesStrategy({ setTransaction, navigation, route, + ...props }: Props) { const defaultStrategies = useFeesStrategy(transaction); const [customStrategy, setCustomStrategy] = useState( @@ -89,6 +91,7 @@ export default function EthereumFeesStrategy({ return ( ( + + + +); + +export default Lock; diff --git a/src/icons/Unlock.js b/src/icons/Unlock.js new file mode 100644 index 0000000000..5141c0a97c --- /dev/null +++ b/src/icons/Unlock.js @@ -0,0 +1,15 @@ +// @flow + +import React from "react"; +import Svg, { Path } from "react-native-svg"; + +const Unlock = ({ size, color }: { size: number, color: string }) => ( + + + +); + +export default Unlock; diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 79cb00f5b3..617f474c70 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -1845,8 +1845,8 @@ "subtitle": "Your information is collected by LEDGER on behalf of and transferred to WYRE for KYC purposes. For more information, please check our Privacy Policy", "pending": { "cta": "Continue", - "title": "KYC submitted for approval", - "subtitle": "Your KYC has been submitted and is pending approval.", + "title": "Your information has been submitted for approval", + "subtitle": "It usually takes only a few minutes before you can start swapping", "link": "Learn more about KYC" }, "approved": { @@ -1901,9 +1901,9 @@ "cta": "Close" }, "pendingOperation": { - "description": "Your Swap operation has been sent to the network for confirmation. It may take up to an hour before you receive your {{targetCurrency}}.", + "description": "Your Swap operation has been sent to the network for confirmation. It may take up to an hour before you receive your swapped {{targetCurrency}} assets", "label": "Your Swap ID:", - "title": "Swap broadcast successfully ", + "title": "Pending operation", "disclaimer": "Take note of your Swap ID number in case you’d need assistance from {{provider}} support.", "cta": "See details" }, @@ -1923,14 +1923,23 @@ "button": "Continue", "from": "From", "to": "To", + "source": "Source", + "target": "Target account", + "noAccount": "You don’t have {{currency}} account", "balance": "Balance <0>123", "fromAccount": "Select account", "toAccount": "Select account", "paraswapCTA": "Looking for Paraswap? It has been moved to the Discover tab!", + "quote": "Quote", + "receive": "Receive", "amount": { "useMax": "Use max", "available": "Total available" }, + "noAsset": { + "title": "You have no asset to swap", + "desc": "Buy some and come back to swap" + }, "noApp": { "title": "{{appName}} app not installed", "desc": "Please go to Manager to install the {{appName}} app.", @@ -2011,6 +2020,22 @@ } } }, + "swapv2": { + "form": { + "summary": { + "from": "From", + "to": "To", + "send": "Send", + "payoutNetworkFees": "Payout fees", + "payoutNetworkFeesTooltip": "This amount will not be shown on your device", + "receive": "Amount", + "receiveFloat": "Amount to receive before service fees", + "provider": "Provider", + "method": "Rate", + "fees": "Fees" + } + } + }, "lending": { "title": "Lend crypto", "titleTransferTab": "Lend", diff --git a/src/screens/AddAccounts/01-SelectCrypto.js b/src/screens/AddAccounts/01-SelectCrypto.js index 904be338e7..0746de2de8 100644 --- a/src/screens/AddAccounts/01-SelectCrypto.js +++ b/src/screens/AddAccounts/01-SelectCrypto.js @@ -26,6 +26,7 @@ const SEARCH_KEYS = ["name", "ticker"]; type Props = { devMode: boolean, navigation: any, + route: { params: { filterCurrencyIds?: string[] } }, }; const keyExtractor = currency => currency.id; @@ -41,17 +42,27 @@ const renderEmptyList = () => ( const listSupportedTokens = () => listTokens().filter(t => isCurrencySupported(t.parentCurrency)); -export default function AddAccountsSelectCrypto({ navigation }: Props) { +export default function AddAccountsSelectCrypto({ navigation, route }: Props) { const { colors } = useTheme(); + const { filterCurrencyIds = [] } = route.params || {}; const cryptoCurrencies = useMemo( - () => listSupportedCurrencies().concat(listSupportedTokens()), - [], + () => + listSupportedCurrencies() + .concat(listSupportedTokens()) + .filter( + ({ id }) => + filterCurrencyIds.length <= 0 || filterCurrencyIds.includes(id), + ), + [filterCurrencyIds], ); const sortedCryptoCurrencies = useCurrenciesByMarketcap(cryptoCurrencies); const onPressCurrency = (currency: CryptoCurrency) => { - navigation.navigate(ScreenName.AddAccountsSelectDevice, { currency }); + navigation.navigate(ScreenName.AddAccountsSelectDevice, { + ...(route?.params ?? {}), + currency, + }); }; const onPressToken = (token: TokenCurrency) => { diff --git a/src/screens/Swap/FormOrHistory/Confirmation.js b/src/screens/Swap/Confirmation.js similarity index 77% rename from src/screens/Swap/FormOrHistory/Confirmation.js rename to src/screens/Swap/Confirmation.js index 7224564c9b..453b3f9afc 100644 --- a/src/screens/Swap/FormOrHistory/Confirmation.js +++ b/src/screens/Swap/Confirmation.js @@ -1,6 +1,6 @@ // @flow -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { StyleSheet, View } from "react-native"; import { useTranslation } from "react-i18next"; @@ -27,23 +27,31 @@ import { getAccountCurrency, } from "@ledgerhq/live-common/lib/account"; -import { renderLoading } from "../../../components/DeviceAction/rendering"; -import { ScreenName } from "../../../const"; -import { updateAccountWithUpdater } from "../../../actions/accounts"; -import DeviceAction from "../../../components/DeviceAction"; -import BottomModal from "../../../components/BottomModal"; -import ModalBottomAction from "../../../components/ModalBottomAction"; -import { useBroadcast } from "../../../components/useBroadcast"; -import { swapKYCSelector } from "../../../reducers/settings"; +import type { DeviceInfo } from "@ledgerhq/live-common/lib/types/manager"; +import type { Device } from "@ledgerhq/live-common/lib/hw/actions/types"; -import type { DeviceMeta } from "./Form"; +import { renderLoading } from "../../components/DeviceAction/rendering"; +import { ScreenName } from "../../const"; +import { updateAccountWithUpdater } from "../../actions/accounts"; +import DeviceAction from "../../components/DeviceAction"; +import BottomModal from "../../components/BottomModal"; +import ModalBottomAction from "../../components/ModalBottomAction"; +import { useBroadcast } from "../../components/useBroadcast"; +import { swapKYCSelector } from "../../reducers/settings"; const silentSigningAction = createAction(connectApp); const swapAction = initSwapCreateAction(connectApp, initSwap); +export type DeviceMeta = { + result: { installed: any }, + device: Device, + deviceInfo: DeviceInfo, +}; + type Props = { - exchange: Exchange, - exchangeRate: ExchangeRate, + swap: Exchange, + rate: ExchangeRate, + provider: string, transaction: Transaction, deviceMeta: DeviceMeta, onError: (error: Error) => void, @@ -51,17 +59,32 @@ type Props = { status: TransactionStatus, }; const Confirmation = ({ - exchange, - exchangeRate, + swap, + rate, + provider, transaction, onError, onCancel, deviceMeta, status, }: Props) => { - const { fromAccount, fromParentAccount, toAccount } = exchange; + const { + from: { account: fromAccount, parentAccount: fromParentAccount }, + to: { account: toAccount, parentAccount: toParentAccount }, + } = swap; + + const exchange = useMemo( + () => ({ + fromAccount, + fromParentAccount, + toAccount, + toParentAccount, + }), + [fromAccount, fromParentAccount, toAccount, toParentAccount], + ); + const swapKYC = useSelector(swapKYCSelector); - const providerKYC = swapKYC[exchangeRate.provider]; + const providerKYC = swapKYC[provider]; const [swapData, setSwapData] = useState(null); const [signedOperation, setSignedOperation] = useState(null); @@ -90,7 +113,10 @@ const Confirmation = ({ account, operation, transaction, - swap: { exchange, exchangeRate }, + swap: { + exchange, + exchangeRate: rate, + }, swapId, }), operation, @@ -99,7 +125,7 @@ const Confirmation = ({ ); navigation.replace(ScreenName.SwapPendingOperation, { swapId, - provider: exchangeRate.provider, + provider: rate.provider, targetCurrency: targetCurrency.name, operation, fromAccount, @@ -107,14 +133,14 @@ const Confirmation = ({ }); }, [ - dispatch, - exchange, - exchangeRate, fromAccount, fromParentAccount, + dispatch, navigation, + rate, + targetCurrency.name, transaction, - targetCurrency, + exchange, ], ); @@ -156,7 +182,7 @@ const Confirmation = ({ onError={onError} request={{ exchange, - exchangeRate, + exchangeRate: rate, transaction, userId: providerKYC?.id, requireLatestFirmware: true, diff --git a/src/screens/Swap/Connect.js b/src/screens/Swap/Connect.js index 8a93542f0e..52e418322c 100644 --- a/src/screens/Swap/Connect.js +++ b/src/screens/Swap/Connect.js @@ -1,19 +1,23 @@ // @flow import React, { useState, useCallback } from "react"; -import { Trans } from "react-i18next"; import { View, StyleSheet } from "react-native"; import connectManager from "@ledgerhq/live-common/lib/hw/connectManager"; import { createAction } from "@ledgerhq/live-common/lib/hw/actions/manager"; import { useTheme } from "@react-navigation/native"; import SelectDevice from "../../components/SelectDevice"; import DeviceActionModal from "../../components/DeviceActionModal"; -import LText from "../../components/LText"; import { TrackScreen } from "../../analytics"; const action = createAction(connectManager); -const Connect = ({ setResult }: { setResult: (result: any) => void }) => { +const Connect = ({ + setResult, + provider, +}: { + setResult: (result: any) => void, + provider?: string, +}) => { const [device, setDevice] = useState(null); const [result, setLocalResult] = useState(); @@ -28,10 +32,11 @@ const Connect = ({ setResult }: { setResult: (result: any) => void }) => { const { colors } = useTheme(); return ( - - - - + { - const { exchange, providers, provider } = route.params; - const { fromAccount, fromParentAccount, toAccount } = exchange; - - const swapKYC = useSelector(swapKYCSelector); - const dispatch = useDispatch(); - const providerKYC = swapKYC[provider]; - const fromCurrency = getAccountCurrency(fromAccount); - const toCurrency = getAccountCurrency(toAccount); - const fromUnit = getAccountUnit(fromAccount); - const toUnit = getAccountUnit(toAccount); - const [error, setError] = useState(null); - const [rate, setRate] = useState(null); - const [ - dismissedUnauthorizedRatesModal, - setDismissedUnauthorizedRatesModal, - ] = useState(false); - const [showUnauthorizedRates, setShowUnauthorizedRates] = useState(false); - const [rateExpiration, setRateExpiration] = useState(null); - const [useAllAmount, setUseAllAmount] = useState(false); - const [maxSpendable, setMaxSpendable] = useState(BigNumber(0)); - - const onResetKYC = useCallback(() => { - dispatch(setSwapKYCStatus({ provider: "wyre" })); - navigation.replace(ScreenName.Swap); - setDismissedUnauthorizedRatesModal(true); - setShowUnauthorizedRates(false); - }, [dispatch, navigation]); - - const enabledTradeMethods = useMemo( - () => - getEnabledTradingMethods({ - providers, - provider, - fromCurrency, - toCurrency, - }), - [fromCurrency, provider, providers, toCurrency], - ); - const [tradeMethod, setTradeMethod] = useState<"fixed" | "float">( - enabledTradeMethods[0] || "float", - ); - - const { - status, - transaction, - setTransaction, - bridgePending, - } = useBridgeTransaction(() => ({ - account: fromAccount, - parentAccount: fromParentAccount, - })); - - invariant(transaction, "transaction must be defined"); - const onTradeMethodChange = useCallback(method => { - if (method === "fixed" || method === "float") { - setTradeMethod(method); - setRate(null); - setRateExpiration(null); - } - }, []); - const onAmountChange = useCallback( - amount => { - if (!amount.eq(transaction.amount)) { - const bridge = getAccountBridge(fromAccount, fromParentAccount); - const fromCurrency = getAccountCurrency( - getMainAccount(fromAccount, fromParentAccount), - ); - setTransaction( - bridge.updateTransaction(transaction, { - amount, - recipient: getAbandonSeedAddress(fromCurrency.id), - }), - ); - setRate(null); - setRateExpiration(null); - - if (maxSpendable && maxSpendable.gt(0) && amount.gt(maxSpendable)) { - setError(new NotEnoughBalance()); - } else { - setError(null); - } - } - }, - [fromAccount, fromParentAccount, maxSpendable, setTransaction, transaction], - ); - - useEffect(() => { - let ignore = false; - async function getEstimatedMaxSpendable() { - const bridge = getAccountBridge(fromAccount, fromParentAccount); - const max = await bridge.estimateMaxSpendable({ - account: fromAccount, - parentAccount: fromParentAccount, - transaction, - }); - setMaxSpendable(max); - } - if (!ignore) { - getEstimatedMaxSpendable(); - } - - return () => { - ignore = true; - }; - }, [transaction, fromAccount, fromParentAccount]); - - useEffect(() => { - let ignore = false; - const KYCUserId = Config.SWAP_OVERRIDE_KYC_USER_ID || providerKYC?.id; - async function getRates() { - try { - // $FlowFixMe No idea how to pass this - const rates = await getExchangeRates(exchange, transaction, KYCUserId); - if (ignore) return; - const rate = rates.find( - rate => - rate.tradeMethod === tradeMethod && rate.provider === provider, - ); - if (rate?.error) { - if (rate?.error && rate.error instanceof AccessDeniedError) { - setShowUnauthorizedRates(true); - } - setError(rate.error); - } else { - setRate(rate); // FIXME when we have multiple providers this will not be enough - setRateExpiration(new Date(Date.now() + 60000)); - } - } catch (error) { - if (ignore) return; - setError(error); - } - } - if (!ignore && !error && transaction.amount.gt(0) && !rate) { - getRates(); - } - - return () => { - ignore = true; - }; - }, [ - exchange, - fromAccount, - toAccount, - error, - transaction, - tradeMethod, - rate, - providerKYC?.id, - provider, - ]); - - const onContinue = useCallback(() => { - navigation.navigate(ScreenName.SwapSummary, { - ...route.params, - exchange, - exchangeRate: rate, - transaction, - status, - rateExpiration, - }); - }, [ - navigation, - route.params, - exchange, - rate, - transaction, - status, - rateExpiration, - ]); - - const toggleSendMax = useCallback(() => { - const newUseAllAmount = !useAllAmount; - setUseAllAmount(newUseAllAmount); - onAmountChange(newUseAllAmount ? maxSpendable : BigNumber(0)); - }, [useAllAmount, setUseAllAmount, onAmountChange, maxSpendable]); - - const hasErrors = Object.keys(status.errors).length; - const canContinue = !bridgePending && !hasErrors && rate; - - const amountError = - transaction.amount.gt(0) && - (error || status.errors?.gasPrice || status.errors?.amount); - const hideError = - bridgePending || - (useAllAmount && amountError && amountError instanceof AmountRequired); - - const options = [ - { - value: "float", - label: , - disabled: !enabledTradeMethods.includes("float"), - }, - { - value: "fixed", - label: , - disabled: !enabledTradeMethods.includes("fixed"), - }, - ]; - - const toValue = rate - ? transaction.amount - .times(rate.magnitudeAwareRate) - .minus(rate.payoutNetworkFees || 0) - : null; - - const actualRate = toValue ? toValue.dividedBy(transaction.amount) : null; - - return ( - - {dismissedUnauthorizedRatesModal ? ( - - - - ) : null} - - - - - {actualRate && rateExpiration ? ( - - ) : ( - - )} - - - - {fromUnit.code} - - } - hasError={!hideError && !!error} - /> - - - - - - - - {toUnit.code} - - } - /> - - - - - - - - - - - - - - - - - - - -