From 19a4c5f6e11a64329b30186eb5ea951c0cc9854d Mon Sep 17 00:00:00 2001 From: LFBarreto Date: Thu, 26 Aug 2021 15:44:06 +0200 Subject: [PATCH 01/64] LL-6623 integrate swap form v2 tab navigation --- src/components/RootNavigator/BaseNavigator.js | 7 ++- .../RootNavigator/SwapFormNavigator.js | 32 +++++++++++ src/components/RootNavigator/SwapNavigator.js | 53 +++++++++++++++---- 3 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 src/components/RootNavigator/SwapFormNavigator.js diff --git a/src/components/RootNavigator/BaseNavigator.js b/src/components/RootNavigator/BaseNavigator.js index efb82cca96..fd6f24e7d9 100644 --- a/src/components/RootNavigator/BaseNavigator.js +++ b/src/components/RootNavigator/BaseNavigator.js @@ -111,7 +111,12 @@ export default function BaseNavigator() { getStackNavigatorConfig(colors, true), + [colors], + ); + + return ( + + + + ); +} + +const Stack = createStackNavigator(); diff --git a/src/components/RootNavigator/SwapNavigator.js b/src/components/RootNavigator/SwapNavigator.js index 073194f752..b455721000 100644 --- a/src/components/RootNavigator/SwapNavigator.js +++ b/src/components/RootNavigator/SwapNavigator.js @@ -2,6 +2,7 @@ import React, { useMemo } from "react"; import { createStackNavigator } from "@react-navigation/stack"; +import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs"; import { useTranslation } from "react-i18next"; import { useTheme } from "@react-navigation/native"; import useEnv from "@ledgerhq/live-common/lib/hooks/useEnv"; @@ -13,7 +14,7 @@ import SwapFormAmount from "../../screens/Swap/FormOrHistory/Form/Amount"; import SwapKYC from "../../screens/Swap/KYC"; import SwapKYCStates from "../../screens/Swap/KYC/StateSelect"; import Swap from "../../screens/Swap"; -import Swap2 from "../../screens/Swap2"; +import SwapFormNavigator from "./SwapFormNavigator"; import SwapOperationDetails from "../../screens/Swap/FormOrHistory/OperationDetails"; import { BackButton } from "../../screens/OperationDetails"; import SwapPendingOperation from "../../screens/Swap/FormOrHistory/Form/PendingOperation"; @@ -22,11 +23,18 @@ import SwapFormSelectAccount from "../../screens/Swap/FormOrHistory/Form/SelectA import { getStackNavigatorConfig } from "../../navigation/navigatorConfig"; import styles from "../../navigation/styles"; import StepHeader from "../StepHeader"; +import LText from "../LText"; +import History from "../../screens/Swap/FormOrHistory/History"; + +type TabLabelProps = { + focused: boolean, + color: string, +}; export default function SwapNavigator() { const { t } = useTranslation(); const { colors } = useTheme(); - const isSwapV2Enabled = useEnv("EXPERIMENTAL_SWAP") && __DEV__; + const isSwapV2Enabled = __DEV__; const stackNavigationConfig = useMemo( () => getStackNavigatorConfig(colors, true), [colors], @@ -34,21 +42,47 @@ export default function SwapNavigator() { if (isSwapV2Enabled) { return ( - - + ( + /** width has to be a little bigger to accomodate the switch in size between semibold to regular */ + + {t("transfer.swap.form.tab")} + + ), + }} + /> + ( + + {t("transfer.swap.history.tab")} + + ), }} /> - + ); } return ( - + Date: Fri, 27 Aug 2021 16:50:27 +0200 Subject: [PATCH 02/64] LL-6626 LL-6627 (SwapV2): integrate account selection --- src/components/RootNavigator/BaseNavigator.js | 20 +- src/const/navigation.js | 3 + src/locales/en/common.json | 2 + src/screens/AddAccounts/01-SelectCrypto.js | 8 +- .../Form/SelectAccount/02-SelectAccount.js | 2 + .../Swap2/FormSelection/AccountAmountRow.js | 78 ++++++ .../Swap2/FormSelection/AccountSelect.js | 135 ++++++++++ .../FormSelection/SelectAccountScreen.js | 238 ++++++++++++++++++ src/screens/Swap2/index.js | 119 ++++++++- 9 files changed, 597 insertions(+), 8 deletions(-) create mode 100644 src/screens/Swap2/FormSelection/AccountAmountRow.js create mode 100644 src/screens/Swap2/FormSelection/AccountSelect.js create mode 100644 src/screens/Swap2/FormSelection/SelectAccountScreen.js diff --git a/src/components/RootNavigator/BaseNavigator.js b/src/components/RootNavigator/BaseNavigator.js index fd6f24e7d9..ff037e1070 100644 --- a/src/components/RootNavigator/BaseNavigator.js +++ b/src/components/RootNavigator/BaseNavigator.js @@ -61,6 +61,8 @@ import RequestAccountNavigator from "./RequestAccountNavigator"; import VerifyAccount from "../../screens/VerifyAccount"; import PlatformApp from "../../screens/Platform/App"; +import SwapFormSelectAccount from "../../screens/Swap2/FormSelection/SelectAccountScreen"; + export default function BaseNavigator() { const { t } = useTranslation(); const { colors } = useTheme(); @@ -115,9 +117,25 @@ export default function BaseNavigator() { ...stackNavigationConfig, headerStyle: styles.headerNoShadow, headerLeft: null, - title: t("transfer.swap.exchange"), + title: t("transfer.swap.form.tab"), }} /> + ({ + headerTitle: () => ( + + ), + headerRight: null, + })} + /> 123", "fromAccount": "Select account", "toAccount": "Select account", diff --git a/src/screens/AddAccounts/01-SelectCrypto.js b/src/screens/AddAccounts/01-SelectCrypto.js index fed0ee3308..10fadee4c8 100644 --- a/src/screens/AddAccounts/01-SelectCrypto.js +++ b/src/screens/AddAccounts/01-SelectCrypto.js @@ -25,6 +25,7 @@ const SEARCH_KEYS = ["name", "ticker"]; type Props = { devMode: boolean, navigation: any, + route: { params: * }, }; const keyExtractor = currency => currency.id; @@ -37,7 +38,7 @@ const renderEmptyList = () => ( ); -export default function AddAccountsSelectCrypto({ navigation }: Props) { +export default function AddAccountsSelectCrypto({ navigation, route }: Props) { const { colors } = useTheme(); const cryptoCurrencies = useMemo( () => listSupportedCurrencies().concat(listTokens()), @@ -47,7 +48,10 @@ export default function AddAccountsSelectCrypto({ navigation }: Props) { 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/Form/SelectAccount/02-SelectAccount.js b/src/screens/Swap/FormOrHistory/Form/SelectAccount/02-SelectAccount.js index b6f3f67f0a..1c687a425f 100644 --- a/src/screens/Swap/FormOrHistory/Form/SelectAccount/02-SelectAccount.js +++ b/src/screens/Swap/FormOrHistory/Form/SelectAccount/02-SelectAccount.js @@ -116,6 +116,8 @@ export default function SelectAccount({ navigation, route }: Props) { : account.currency.id) === selectedCurrency.id, ); + console.log(allAccounts, elligibleAccountsForSelectedCurrency); + const renderList = useCallback( items => { // $FlowFixMe diff --git a/src/screens/Swap2/FormSelection/AccountAmountRow.js b/src/screens/Swap2/FormSelection/AccountAmountRow.js new file mode 100644 index 0000000000..0c4ab990f0 --- /dev/null +++ b/src/screens/Swap2/FormSelection/AccountAmountRow.js @@ -0,0 +1,78 @@ +// @flow +import React from "react"; +import { View, StyleSheet } from "react-native"; +import type { AccountLike } from "@ledgerhq/live-common/lib/types"; +import type { + Exchange, + SwapTransaction, +} from "@ledgerhq/live-common/lib/exchange/swap/types"; + +import { Trans } from "react-i18next"; +import { useTheme } from "@react-navigation/native"; +import LText from "../../../components/LText"; +import AccountSelect from "./AccountSelect"; + +type Props = { + navigation: *, + exchange: Exchange, + onChange: (value: Exchange) => void, + transaction: SwapTransaction, + onUpdateTransaction: (transaction: SwapTransaction) => void, +}; + +export default function AccountAmountRow({ + navigation, + exchange, + onChange, + transaction, + onUpdateTransaction, +}: Props) { + const { colors } = useTheme(); + const { fromAccount, fromParentAccount, toAccount, toParentAccount } = exchange; + + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + root: { + flexDirection: "row", + paddingVertical: 16, + }, + label: { + fontSize: 12, + lineHeight: 15, + }, + divider: { + width: "100%", + height: 1, + marginVertical: 16, + }, +}); diff --git a/src/screens/Swap2/FormSelection/AccountSelect.js b/src/screens/Swap2/FormSelection/AccountSelect.js new file mode 100644 index 0000000000..4539df1918 --- /dev/null +++ b/src/screens/Swap2/FormSelection/AccountSelect.js @@ -0,0 +1,135 @@ +// @flow +import React, { useCallback, useMemo } from "react"; +import { View, StyleSheet, TouchableOpacity } from "react-native"; +import { Trans } from "react-i18next"; +import { useTheme } from "@react-navigation/native"; + +import type { + CryptoCurrency, + TokenCurrency, +} from "@ledgerhq/live-common/lib/types"; +import type { Exchange } from "@ledgerhq/live-common/lib/exchange/swap/types"; +import { + getAccountCurrency, + getAccountName, +} from "@ledgerhq/live-common/lib/account"; + +import SearchIcon from "../../../icons/Search"; +import LText from "../../../components/LText"; +import { ScreenName } from "../../../const"; +import Chevron from "../../../icons/Chevron"; +import CurrencyIcon from "../../../components/CurrencyIcon"; + +type Props = { + navigation: *, + exchange: Exchange, + target: "from" | "to", +}; + +export default function AccountSelect({ navigation, exchange, target }: Props) { + const { colors } = useTheme(); + + const value = useMemo( + () => (target === "from" ? exchange.fromAccount : exchange.toAccount), + [target, exchange], + ); + + const currency = useMemo(() => value && getAccountCurrency(value), [value]); + const name = useMemo(() => value && getAccountName(value), [value]); + + const onPressItem = useCallback( + (currencyOrToken: CryptoCurrency | TokenCurrency) => { + if (target === "from") { + // NB Clear toAccount only if it will collide with the selected currency + const toAccount = + exchange.toAccount && + getAccountCurrency(exchange.toAccount).id === currencyOrToken.id + ? undefined + : exchange.toAccount; + + navigation.navigate(ScreenName.SwapV2FormSelectAccount, { + exchange: { + ...exchange, + fromAccount: null, + toAccount, + }, + selectedCurrency: currencyOrToken, + target, + }); + } else { + navigation.navigate(ScreenName.SwapV2FormSelectAccount, { + exchange: { + ...exchange, + toAccount: null, + }, + selectedCurrency: currencyOrToken, + target, + }); + } + }, + [exchange, navigation, target], + ); + + return ( + + + {value ? ( + <> + + + + + + {name} + + + {currency.ticker} + + + + ) : ( + <> + + + + + + + + )} + + + + + + ); +} + +const styles = StyleSheet.create({ + root: { + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + }, + label: { + fontSize: 16, + lineHeight: 19, + }, + chevron: { + marginLeft: 8, + }, + iconContainer: { + marginRight: 8, + }, + accountColumn: { + flexDirection: "column", + }, + accountTicker: { + fontSize: 13, + lineHeight: 16, + }, +}); diff --git a/src/screens/Swap2/FormSelection/SelectAccountScreen.js b/src/screens/Swap2/FormSelection/SelectAccountScreen.js new file mode 100644 index 0000000000..e97e768506 --- /dev/null +++ b/src/screens/Swap2/FormSelection/SelectAccountScreen.js @@ -0,0 +1,238 @@ +/* @flow */ + +import React, { useCallback, useMemo } from "react"; +import { + View, + StyleSheet, + FlatList, + SafeAreaView, + TouchableOpacity, +} from "react-native"; +import { useSelector } from "react-redux"; +import { Trans } from "react-i18next"; +import { useTheme } from "@react-navigation/native"; + +import type { + Account, + AccountLikeArray, +} from "@ledgerhq/live-common/lib/types"; +import { + accountWithMandatoryTokens, + flattenAccounts, +} from "@ledgerhq/live-common/lib/account/helpers"; + +import type { SearchResult } from "../../../helpers/formatAccountSearchResults"; +import { accountsSelector } from "../../../reducers/accounts"; +import { TrackScreen } from "../../../analytics"; +import LText from "../../../components/LText"; +import FilteredSearchBar from "../../../components/FilteredSearchBar"; +import AccountCard from "../../../components/AccountCard"; +import KeyboardView from "../../../components/KeyboardView"; +import { formatSearchResults } from "../../../helpers/formatAccountSearchResults"; +import { NavigatorName, ScreenName } from "../../../const"; + +import type { SwapRouteParams } from ".."; +import AddIcon from "../../../icons/Plus"; + +const SEARCH_KEYS = ["name", "unit.code", "token.name", "token.ticker"]; + +type Props = { + accounts: Account[], + allAccounts: AccountLikeArray, + navigation: any, + route: { params: SwapRouteParams }, +}; + +export default function SelectAccount({ navigation, route }: Props) { + const { colors } = useTheme(); + const { exchange, target } = route.params; + const accounts = useSelector(accountsSelector); + + const enhancedAccounts = useMemo( + () => accounts.map(acc => accountWithMandatoryTokens(acc, [])), + [accounts], + ); + + const allAccounts = flattenAccounts(enhancedAccounts); + + const keyExtractor = item => item.account.id; + const isFrom = target === "from"; + + const renderItem = useCallback( + ({ item: result }: { item: SearchResult }) => { + const { account } = result; + const parentAccount = + account.type === "TokenAccount" + ? accounts.find(a => a.id === account.parentId) + : null; + const accountParams = isFrom + ? { + fromAccount: account, + fromParentAccount: parentAccount, + } + : { + toAccount: account, + toParentAccount: parentAccount, + }; + + return ( + + { + navigation.navigate(ScreenName.SwapForm, { + ...route.params, + exchange: { + ...exchange, + ...accountParams, + }, + }); + }} + /> + + ); + }, + [accounts, isFrom, colors.fog, navigation, route.params, exchange], + ); + + const elligibleAccountsForSelectedCurrency = allAccounts.filter(account => + isFrom + ? account.balance.gt(0) && exchange?.toAccount?.id !== account.id + : exchange?.fromAccount?.id !== account.id, + ); + + const onAddAccount = useCallback(() => { + navigation.navigate(NavigatorName.AddAccounts, { + screen: ScreenName.AddAccountsSelectCrypto, + params: { + returnToSwap: true, + onSuccess: () => + navigation.navigate(ScreenName.SwapV2FormSelectAccount, { + params: route.params, + }), + }, + }); + }, [navigation, route.params]); + + const renderList = useCallback( + items => { + // $FlowFixMe + const formatedList = formatSearchResults(items, enhancedAccounts); + return ( + ( + + + + + + + + + )} + /> + ); + }, + [colors.lightLive, colors.live, enhancedAccounts, onAddAccount, renderItem], + ); + + return ( + + + + + ( + + + + + + )} + /> + + + + ); +} + +const styles = StyleSheet.create({ + addAccountButton: { + flex: 1, + flexDirection: "row", + paddingVertical: 16, + alignItems: "center", + }, + root: { + flex: 1, + }, + tokenCardStyle: { + marginLeft: 26, + paddingLeft: 7, + borderLeftWidth: 1, + }, + card: { + paddingHorizontal: 16, + backgroundColor: "transparent", + }, + searchContainer: { + paddingTop: 18, + flex: 1, + }, + list: { + paddingTop: 8, + }, + emptyResults: { + flex: 1, + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + }, + label: { + fontSize: 16, + lineHeight: 19, + }, + button: { + flex: 1, + }, + iconContainer: { + borderRadius: 26, + height: 26, + width: 26, + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + buttonContainer: { + paddingTop: 24, + paddingLeft: 16, + paddingRight: 16, + marginBottom: 24, + flexDirection: "row", + }, +}); diff --git a/src/screens/Swap2/index.js b/src/screens/Swap2/index.js index 3dffc5d5af..45fc5a54d5 100644 --- a/src/screens/Swap2/index.js +++ b/src/screens/Swap2/index.js @@ -1,16 +1,116 @@ // @flow import { useTheme } from "@react-navigation/native"; -import React from "react"; -import { SafeAreaView, StyleSheet } from "react-native"; -import LText from "../../components/LText"; +import React, { useMemo, useCallback } from "react"; +import { SafeAreaView, StyleSheet, View } from "react-native"; -export default function SwapEntrypoint() { +import type { + CryptoCurrency, + TokenCurrency, + Transaction, + TransactionStatus, +} from "@ledgerhq/live-common/lib/types"; +import type { + Account, + AccountLike, +} from "@ledgerhq/live-common/lib/types/account"; + +import type { + Exchange, + ExchangeRate, +} from "@ledgerhq/live-common/lib/exchange/swap/types"; +import type { CurrenciesStatus } from "@ledgerhq/live-common/lib/exchange/swap/logic"; + +// import { getAccountCurrency } from "@ledgerhq/live-common/lib/account"; +import useBridgeTransaction from "@ledgerhq/live-common/lib/bridge/useBridgeTransaction"; + +import { Trans } from "react-i18next"; +import AccountAmountRow from "./FormSelection/AccountAmountRow"; +import Button from "../../components/Button"; + +export type SwapRouteParams = { + exchange: Exchange, + exchangeRate: ExchangeRate, + currenciesStatus: CurrenciesStatus, + selectableCurrencies: (CryptoCurrency | TokenCurrency)[], + transaction?: Transaction, + status?: TransactionStatus, + selectedCurrency: CryptoCurrency | TokenCurrency, + providers: any, + provider: any, + installedApps: any, + target: "from" | "to", + rateExpiration?: Date, +}; + +type Props = { + route: { params: SwapRouteParams }, + navigation: *, + defaultAccount: ?AccountLike, + defaultParentAccount: ?Account, +}; + +export default function SwapEntrypoint({ + route, + navigation, + defaultAccount, + defaultParentAccount, +}: Props) { const { colors } = useTheme(); + const exchange = useMemo( + () => + route.params?.exchange || { + fromAccount: defaultAccount?.balance.gt(0) ? defaultAccount : undefined, + fromParentAccount: defaultAccount?.balance.gt(0) + ? defaultParentAccount + : undefined, + }, + [defaultAccount, defaultParentAccount, route.params], + ); + + const onExchangeUpdate = useCallback( + (exchange: Exchange) => { + navigation.setParams({ ...route.params, exchange }); + }, + [route.params], + ); + + const { fromAccount, fromParentAccount /* , toAccount */ } = exchange; + // const fromCurrency = fromAccount ? getAccountCurrency(fromAccount) : null; + // const toCurrency = toAccount ? getAccountCurrency(toAccount) : null; + + const { + // status, + transaction, + // setTransaction, + updateTransaction, + bridgePending, + } = useBridgeTransaction(() => ({ + account: fromAccount, + parentAccount: fromParentAccount, + })); return ( - Swap V2 💸 + + +