diff --git a/src/const/navigation.js b/src/const/navigation.js index 03a7dc51af..b317a0a289 100644 --- a/src/const/navigation.js +++ b/src/const/navigation.js @@ -130,8 +130,6 @@ export const ScreenName = { SignSummary: "SignSummary", SignValidationError: "SignValidationError", SignValidationSuccess: "SignValidationSuccess", - StellarEditMemoType: "StellarEditMemoType", - StellarEditMemoValue: "StellarEditMemoValue", Swap: "Swap", SwapError: "SwapError", SwapFormOrHistory: "SwapFormOrHistory", @@ -243,6 +241,17 @@ export const ScreenName = { PolkadotSimpleOperationValidationSuccess: "PolkadotSimpleOperationValidationSuccess", + // Stellar + StellarEditMemoType: "StellarEditMemoType", + StellarEditMemoValue: "StellarEditMemoValue", + StellarEditCustomFees: "StellarEditCustomFees", + StellarAddAssetSelectAsset: "StellarAddAssetSelectAsset", + StellarAddAssetSelectDevice: "StellarAddAssetSelectDevice", + StellarAddAssetConnectDevice: "StellarAddAssetConnectDevice", + StellarAddAssetValidation: "StellarAddAssetValidation", + StellarAddAssetValidationError: "StellarAddAssetValidationError", + StellarAddAssetValidationSuccess: "StellarAddAssetValidationSuccess", + LendingDashboard: "LendingDashboard", LendingClosedLoans: "LendingClosedLoans", LendingHistory: "LendingHistory", @@ -413,6 +422,9 @@ export const NavigatorName = { PolkadotNominateFlow: "PolkadotNominateFlow", PolkadotSimpleOperationFlow: "PolkadotSimpleOperationFlow", + // Stellar + StellarAddAssetFlow: "StellarAddAssetFlow", + NotificationCenter: "NotificationCenter", Market: "Market", diff --git a/src/families/stellar/AddAssetFlow/01-SelectAsset.js b/src/families/stellar/AddAssetFlow/01-SelectAsset.js new file mode 100644 index 0000000000..bca1e5efd2 --- /dev/null +++ b/src/families/stellar/AddAssetFlow/01-SelectAsset.js @@ -0,0 +1,274 @@ +// @flow +import invariant from "invariant"; +import React, { useCallback, useState } from "react"; +import { + View, + StyleSheet, + SafeAreaView, + FlatList, + TouchableOpacity, +} from "react-native"; +import { Trans, useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; + +import { getMainAccount } from "@ledgerhq/live-common/lib/account/helpers"; +import { getAccountBridge } from "@ledgerhq/live-common/lib/bridge/impl"; +import useBridgeTransaction from "@ledgerhq/live-common/lib/bridge/useBridgeTransaction"; +import { listTokensForCryptoCurrency } from "@ledgerhq/live-common/lib/currencies"; + +import type { + TokenCurrency, + SubAccount, +} from "@ledgerhq/live-common/lib/types"; + +import { useTheme } from "@react-navigation/native"; +import { ScreenName } from "../../../const"; +import LText from "../../../components/LText"; +import { accountScreenSelector } from "../../../reducers/accounts"; +import { TrackScreen } from "../../../analytics"; +import FilteredSearchBar from "../../../components/FilteredSearchBar"; +import FirstLetterIcon from "../../../components/FirstLetterIcon"; +import KeyboardView from "../../../components/KeyboardView"; +import InfoIcon from "../../../components/InfoIcon"; +import Info from "../../../icons/Info"; +import BottomModal from "../../../components/BottomModal"; + +const Row = ({ + item, + onPress, + onDisabledPress, + disabled, +}: { + item: TokenCurrency, + onPress: () => void, + onDisabledPress: () => void, + disabled: boolean, +}) => { + const { colors } = useTheme(); + const tokenId = item.id.split("/")[2]; + const assetIssuer = tokenId.split(":")[1]; + return ( + + + + {item.name} + + + - + + + {assetIssuer} + + + ); +}; + +const keyExtractor = token => token.id; + +const renderEmptyList = () => ( + + + + + +); + +type RouteParams = { + accountId: string, +}; + +type Props = { + navigation: any, + route: { params: RouteParams }, +}; + +export default function DelegationStarted({ navigation, route }: Props) { + const { colors } = useTheme(); + const { account } = useSelector(accountScreenSelector(route)); + const { t } = useTranslation(); + + invariant(account, "Account required"); + + const mainAccount = getMainAccount(account); + const bridge = getAccountBridge(mainAccount); + + invariant(mainAccount, "stellar Account required"); + + const { transaction } = useBridgeTransaction(() => { + const t = bridge.createTransaction(mainAccount); + + return { + account, + transaction: bridge.updateTransaction(t, { + operationType: "changeTrust", + }), + }; + }); + + const onNext = useCallback( + (assetId: string) => { + const tokenId = assetId.split("/")[2]; + const [assetCode, assetIssuer] = tokenId.split(":"); + + navigation.navigate(ScreenName.StellarAddAssetSelectDevice, { + ...route.params, + transaction: bridge.updateTransaction(transaction, { + assetCode, + assetIssuer, + }), + }); + }, + [navigation, route.params, bridge, transaction], + ); + + const subAccounts = mainAccount.subAccounts || []; + const options = listTokensForCryptoCurrency(mainAccount.currency); + + const [infoModalOpen, setInfoModalOpen] = useState(false); + + const openModal = useCallback(token => setInfoModalOpen(token), [ + setInfoModalOpen, + ]); + const closeModal = useCallback(() => setInfoModalOpen(false), [ + setInfoModalOpen, + ]); + + const renderList = useCallback( + list => ( + ( + + sub.type === "TokenAccount" && + sub.token && + sub.token.id === item.id, + )} + onPress={() => onNext(item.id)} + onDisabledPress={() => openModal(item.name)} + /> + )} + keyExtractor={keyExtractor} + /> + ), + [subAccounts, onNext, openModal], + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, + keyboardView: { flex: 1 }, + searchContainer: { + paddingTop: 16, + flex: 1, + }, + filteredSearchInputWrapperStyle: { + marginHorizontal: 16, + }, + row: { + flexDirection: "row", + alignItems: "center", + padding: 16, + }, + name: { + marginLeft: 10, + fontSize: 14, + }, + ticker: { + marginHorizontal: 5, + fontSize: 12, + }, + assetId: { + fontSize: 12, + flex: 1, + }, + emptySearch: { + paddingHorizontal: 16, + }, + emptySearchText: { + textAlign: "center", + }, + infoIcon: { + width: 80, + marginVertical: 16, + }, + title: { + lineHeight: 24, + fontSize: 16, + }, + warnText: { + textAlign: "center", + fontSize: 14, + lineHeight: 16, + marginVertical: 8, + }, + infoRow: { + paddingHorizontal: 16, + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + }, + modal: { + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + }, +}); diff --git a/src/families/stellar/AddAssetFlow/02-ConnectDevice.js b/src/families/stellar/AddAssetFlow/02-ConnectDevice.js new file mode 100644 index 0000000000..0b63aa0fdb --- /dev/null +++ b/src/families/stellar/AddAssetFlow/02-ConnectDevice.js @@ -0,0 +1,85 @@ +// @flow +import invariant from "invariant"; +import React, { useCallback } from "react"; +import { StyleSheet, ScrollView, SafeAreaView } from "react-native"; +import { useSelector } from "react-redux"; +import type { Transaction } from "@ledgerhq/live-common/lib/types"; +import useBridgeTransaction from "@ledgerhq/live-common/lib/bridge/useBridgeTransaction"; +import { getMainAccount } from "@ledgerhq/live-common/lib/account"; +import { useTheme } from "@react-navigation/native"; +import { accountScreenSelector } from "../../../reducers/accounts"; +import { ScreenName } from "../../../const"; +import { TrackScreen } from "../../../analytics"; +import SelectDevice from "../../../components/SelectDevice"; +import { + connectingStep, + accountApp, +} from "../../../components/DeviceJob/steps"; + +type RouteParams = { + accountId: string, + transaction: Transaction, +}; + +type Props = { + navigation: any, + route: { params: RouteParams }, +}; + +export default function ConnectDevice({ navigation, route }: Props) { + const { colors } = useTheme(); + const { account } = useSelector(accountScreenSelector(route)); + + invariant( + account, + "account is required", + ); + + const mainAccount = getMainAccount(account, undefined); + + const { transaction, status } = useBridgeTransaction(() => { + const transaction = route.params.transaction; + return { account, transaction }; + }); + + const onSelectDevice = useCallback( + (meta: any) => { + navigation.replace(ScreenName.StellarAddAssetValidation, { + ...route.params, + ...meta, + transaction, + status, + }); + }, + [navigation, status, transaction, route.params], + ); + + if (!account) return null; + + return ( + + + + + + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, + scroll: { + flex: 1, + }, + scrollContainer: { + padding: 16, + }, +}); diff --git a/src/families/stellar/AddAssetFlow/03-Validation.js b/src/families/stellar/AddAssetFlow/03-Validation.js new file mode 100644 index 0000000000..c0a1954288 --- /dev/null +++ b/src/families/stellar/AddAssetFlow/03-Validation.js @@ -0,0 +1,107 @@ +/* @flow */ +import React, { useMemo } from "react"; +import { View, StyleSheet, ActivityIndicator } from "react-native"; +import { useDispatch, useSelector } from "react-redux"; +import SafeAreaView from "react-native-safe-area-view"; +import invariant from "invariant"; +import type { + Transaction, + TransactionStatus, +} from "@ledgerhq/live-common/lib/types"; +import type { DeviceModelId } from "@ledgerhq/devices"; +import { useTheme } from "@react-navigation/native"; +import { useSignWithDevice } from "../../../logic/screenTransactionHooks"; +import { updateAccountWithUpdater } from "../../../actions/accounts"; +import { accountScreenSelector } from "../../../reducers/accounts"; +import { TrackScreen } from "../../../analytics"; +import PreventNativeBack from "../../../components/PreventNativeBack"; +import ValidateOnDevice from "../../../components/ValidateOnDevice"; +import SkipLock from "../../../components/behaviour/SkipLock"; + +const forceInset = { bottom: "always" }; + +type RouteParams = { + accountId: string, + deviceId: string, + modelId: DeviceModelId, + wired: boolean, + transaction: Transaction, + status: TransactionStatus, +}; + +type Props = { + navigation: any, + route: { params: RouteParams }, +}; + +export default function Validation({ navigation, route }: Props) { + const { colors } = useTheme(); + const { account } = useSelector(accountScreenSelector(route)); + invariant(account, "account is required"); + const dispatch = useDispatch(); + + const [signing, signed] = useSignWithDevice({ + context: "StellarAddAsset", + account, + parentAccount: undefined, + navigation, + updateAccountWithUpdater: (...args) => + dispatch(updateAccountWithUpdater(...args)), + }); + + const { status, transaction, modelId, wired, deviceId } = route.params; + + const device = useMemo( + () => ({ + deviceId, + modelId, + wired, + }), + [modelId, wired, deviceId], + ); + + return ( + + + {signing && ( + <> + + + + )} + + {signed ? ( + + + + ) : ( + + )} + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, + center: { + flex: 1, + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + }, +}); diff --git a/src/families/stellar/AddAssetFlow/03-ValidationError.js b/src/families/stellar/AddAssetFlow/03-ValidationError.js new file mode 100644 index 0000000000..5e2cd6ea40 --- /dev/null +++ b/src/families/stellar/AddAssetFlow/03-ValidationError.js @@ -0,0 +1,60 @@ +/* @flow */ +import React, { useCallback } from "react"; +import { StyleSheet, Linking } from "react-native"; +import SafeAreaView from "react-native-safe-area-view"; +import { useTheme } from "@react-navigation/native"; +import { TrackScreen } from "../../../analytics"; +import ValidateError from "../../../components/ValidateError"; +import { urls } from "../../../config/urls"; + +const forceInset = { bottom: "always" }; + +type RouteParams = { + accountId: string, + deviceId: string, + transaction: any, + error: Error, +}; + +type Props = { + navigation: any, + route: { params: RouteParams }, +}; + +export default function ValidationError({ navigation, route }: Props) { + const { colors } = useTheme(); + const onClose = useCallback(() => { + navigation.getParent().pop(); + }, [navigation]); + + const contactUs = useCallback(() => { + Linking.openURL(urls.contact); + }, []); + + const retry = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + const error = route.params.error; + + return ( + + + + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, +}); diff --git a/src/families/stellar/AddAssetFlow/03-ValidationSuccess.js b/src/families/stellar/AddAssetFlow/03-ValidationSuccess.js new file mode 100644 index 0000000000..0397eef687 --- /dev/null +++ b/src/families/stellar/AddAssetFlow/03-ValidationSuccess.js @@ -0,0 +1,84 @@ +/* @flow */ +import React, { useCallback, useMemo } from "react"; +import { View, StyleSheet } from "react-native"; +import { useSelector } from "react-redux"; +import { Trans } from "react-i18next"; +import type { Operation } from "@ledgerhq/live-common/lib/types"; +import { listTokensForCryptoCurrency } from "@ledgerhq/live-common/lib/currencies"; +import { useTheme } from "@react-navigation/native"; +import { accountScreenSelector } from "../../../reducers/accounts"; +import { TrackScreen } from "../../../analytics"; +import { ScreenName } from "../../../const"; +import PreventNativeBack from "../../../components/PreventNativeBack"; +import ValidateSuccess from "../../../components/ValidateSuccess"; + +type Props = { + navigation: any, + route: { params: RouteParams }, +}; + +type RouteParams = { + accountId: string, + deviceId: string, + transaction: any, + result: Operation, +}; + +export default function ValidationSuccess({ navigation, route }: Props) { + const { colors } = useTheme(); + const { account } = useSelector(accountScreenSelector(route)); + const { transaction } = route.params; + + const onClose = useCallback(() => { + navigation.getParent().pop(); + }, [navigation]); + + const goToOperationDetails = useCallback(() => { + if (!account) return; + + const result = route.params?.result; + if (!result) return; + + navigation.navigate(ScreenName.OperationDetails, { + accountId: account.id, + operation: result, + }); + }, [account, route.params, navigation]); + + const token = useMemo(() => { + const options = + account && account.type === "Account" + ? listTokensForCryptoCurrency(account.currency) + : []; + return options.find(({ id }) => id === transaction.assetId); + }, [account, transaction]); + + return ( + + + + + } + description={ + + } + /> + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, +}); diff --git a/src/families/stellar/AddAssetFlow/index.js b/src/families/stellar/AddAssetFlow/index.js new file mode 100644 index 0000000000..c98fe67323 --- /dev/null +++ b/src/families/stellar/AddAssetFlow/index.js @@ -0,0 +1,132 @@ +// @flow +import React, { useMemo } from "react"; +import { Platform } from "react-native"; +import { useTranslation } from "react-i18next"; +import { createStackNavigator } from "@react-navigation/stack"; +import { useTheme } from "@react-navigation/native"; +import { + getStackNavigatorConfig, + defaultNavigationOptions, +} from "../../../navigation/navigatorConfig"; +import StepHeader from "../../../components/StepHeader"; +import { ScreenName } from "../../../const"; +import SelectAsset from "./01-SelectAsset"; +import SelectDevice from "../../../screens/SelectDevice"; +import ConnectDevice from "../../../screens/ConnectDevice"; +import Validation from "./03-Validation"; +import ValidationError from "./03-ValidationError"; +import ValidationSuccess from "./03-ValidationSuccess"; + +function AddAssetFlow() { + const { t } = useTranslation(); + const { colors } = useTheme(); + const stackNavigationConfig = useMemo( + () => getStackNavigatorConfig(colors, true), + [colors], + ); + + return ( + + ( + + ), + headerLeft: () => null, + headerStyle: { + ...defaultNavigationOptions.headerStyle, + elevation: 0, + shadowOpacity: 0, + borderBottomWidth: 0, + }, + gestureEnabled: false, + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + headerLeft: null, + headerRight: null, + gestureEnabled: false, + }} + /> + + + + ); +} + +const options = { + headerShown: false, +}; + +export { AddAssetFlow as component, options }; + +const Stack = createStackNavigator(); diff --git a/src/families/stellar/ScreenEditCustomFees.js b/src/families/stellar/ScreenEditCustomFees.js new file mode 100644 index 0000000000..0bbf237d91 --- /dev/null +++ b/src/families/stellar/ScreenEditCustomFees.js @@ -0,0 +1,186 @@ +/* @flow */ +import { BigNumber } from "bignumber.js"; +import invariant from "invariant"; +import React, { useState, useCallback } from "react"; +import { useTranslation, Trans } from "react-i18next"; +import { + Keyboard, + StyleSheet, + View, + SafeAreaView, + ScrollView, +} from "react-native"; +import { useTheme } from "@react-navigation/native"; +import { getAccountBridge } from "@ledgerhq/live-common/lib/bridge"; +import { getMainAccount } from "@ledgerhq/live-common/lib/account"; +import { useSelector } from "react-redux"; +import type { Transaction } from "@ledgerhq/live-common/lib/families/stellar/types"; +import Button from "../../components/Button"; +import KeyboardView from "../../components/KeyboardView"; +import LText from "../../components/LText"; +import { accountScreenSelector } from "../../reducers/accounts"; +import CurrencyInput from "../../components/CurrencyInput"; + +const options = { + title: , + headerLeft: null, +}; + +const forceInset = { bottom: "always" }; + +type RouteParams = { + accountId: string, + transaction: Transaction, + currentNavigation: string, +}; + +type Props = { + navigation: any, + route: { params: RouteParams }, +}; + +function StellarEditCustomFees({ navigation, route }: Props) { + const { colors } = useTheme(); + const { t } = useTranslation(); + const { transaction } = route.params; + const { account, parentAccount } = useSelector(accountScreenSelector(route)); + invariant(transaction.family === "stellar", "not stellar family"); + invariant(account, "no account found"); + + const mainAccount = getMainAccount(account, parentAccount); + const { networkCongestionLevel } = transaction?.networkInfo || {}; + const [customFee, setCustomFee] = useState(transaction.fees); + + const onChange = fee => { + setCustomFee(fee); + }; + + const onSubmit = useCallback(() => { + Keyboard.dismiss(); + + setCustomFee(BigNumber(customFee || 0)); + const bridge = getAccountBridge(account, parentAccount); + const { currentNavigation } = route.params; + + navigation.navigate(currentNavigation, { + ...route.params, + accountId: account.id, + transaction: bridge.updateTransaction(transaction, { + fees: BigNumber(customFee || 0), + }), + }); + }, [ + customFee, + account, + parentAccount, + route.params, + navigation, + transaction, + ]); + + return ( + + + + + + + {mainAccount.unit.code} + + } + /> + + + + {networkCongestionLevel ? ( + + + {`${t( + `stellar.networkCongestionLevel.${networkCongestionLevel}`, + )} ${t("stellar.networkCongestion")}`} + + + ) : null} + + +