Skip to content
This repository has been archived by the owner on Jun 16, 2022. It is now read-only.

Stellar: support non-native assets + custom fee #2223

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/const/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ export const ScreenName = {
SignSummary: "SignSummary",
SignValidationError: "SignValidationError",
SignValidationSuccess: "SignValidationSuccess",
StellarEditMemoType: "StellarEditMemoType",
StellarEditMemoValue: "StellarEditMemoValue",
Swap: "Swap",
SwapError: "SwapError",
SwapFormOrHistory: "SwapFormOrHistory",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -413,6 +422,9 @@ export const NavigatorName = {
PolkadotNominateFlow: "PolkadotNominateFlow",
PolkadotSimpleOperationFlow: "PolkadotSimpleOperationFlow",

// Stellar
StellarAddAssetFlow: "StellarAddAssetFlow",

NotificationCenter: "NotificationCenter",
Market: "Market",

Expand Down
274 changes: 274 additions & 0 deletions src/families/stellar/AddAssetFlow/01-SelectAsset.js
Original file line number Diff line number Diff line change
@@ -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 (
<TouchableOpacity
style={[styles.row]}
onPress={disabled ? onDisabledPress : onPress}
>
<FirstLetterIcon
label={item.name}
labelStyle={disabled ? { color: colors.grey } : {}}
/>
<LText
semiBold
style={[styles.name, disabled ? { color: colors.grey } : {}]}
>
{item.name}
</LText>
<LText style={styles.ticker} color="grey">
-
</LText>
<LText
style={styles.assetId}
color="grey"
numberOfLines={1}
ellipsizeMode="middle"
>
{assetIssuer}
</LText>
</TouchableOpacity>
);
};

const keyExtractor = token => token.id;

const renderEmptyList = () => (
<View style={styles.emptySearch}>
<LText style={styles.emptySearchText}>
<Trans i18nKey="common.noCryptoFound" />
</LText>
</View>
);

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 => (
<FlatList
data={list}
renderItem={({ item }: { item: TokenCurrency }) => (
<Row
item={item}
disabled={subAccounts.some(
(sub: SubAccount) =>
sub.type === "TokenAccount" &&
sub.token &&
sub.token.id === item.id,
)}
onPress={() => onNext(item.id)}
onDisabledPress={() => openModal(item.name)}
/>
)}
keyExtractor={keyExtractor}
/>
),
[subAccounts, onNext, openModal],
);

return (
<SafeAreaView style={[styles.root, { backgroundColor: colors.background }]}>
<TrackScreen category="DelegationFlow" name="Started" />
<KeyboardView style={styles.keyboardView}>
<View style={styles.searchContainer}>
<FilteredSearchBar
renderList={renderList}
inputWrapperStyle={styles.filteredSearchInputWrapperStyle}
renderEmptySearch={renderEmptyList}
keys={["name", "ticker"]}
list={options}
t={t}
/>
</View>
</KeyboardView>
<BottomModal isOpened={!!infoModalOpen} onClose={closeModal}>
<View style={styles.modal}>
<View style={styles.infoIcon}>
<InfoIcon bg={colors.lightLive}>
<Info size={30} color={colors.live} />
</InfoIcon>
</View>
<View style={styles.infoRow}>
<LText style={[styles.warnText, styles.title]} semiBold>
<Trans
i18nKey={`stellar.addAsset.flow.steps.selectToken.warning.title`}
/>
</LText>
<LText style={styles.warnText} color="grey">
<Trans
i18nKey={`stellar.addAsset.flow.steps.selectToken.warning.description`}
values={{ token: infoModalOpen }}
/>
</LText>
</View>
</View>
</BottomModal>
</SafeAreaView>
);
}

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",
},
});
85 changes: 85 additions & 0 deletions src/families/stellar/AddAssetFlow/02-ConnectDevice.js
Original file line number Diff line number Diff line change
@@ -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 (
<SafeAreaView style={[styles.root, { backgroundColor: colors.background }]}>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.scrollContainer}
>
<TrackScreen category="StellarAddAsset" name="ConnectDevice" />
<SelectDevice
onSelect={onSelectDevice}
steps={[connectingStep, accountApp(mainAccount)]}
/>
</ScrollView>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
root: {
flex: 1,
},
scroll: {
flex: 1,
},
scrollContainer: {
padding: 16,
},
});
Loading