diff --git a/package.json b/package.json index 6e700cec74..15a7f7d7f8 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@ledgerhq/hw-transport-http": "6.24.1", "@ledgerhq/live-common": "https://github.com/LedgerHQ/ledger-live-common.git#develop", "@ledgerhq/logs": "6.10.0", - "@ledgerhq/native-ui": "^0.7.5", + "@ledgerhq/native-ui": "^0.7.6", "@ledgerhq/react-native-hid": "6.24.1", "@ledgerhq/react-native-hw-transport-ble": "6.25.1", "@ledgerhq/react-native-ledger-core": "4.19.1", diff --git a/src/components/AccountCard.tsx b/src/components/AccountCard.tsx index 8c3c67c1fb..3c3759b280 100644 --- a/src/components/AccountCard.tsx +++ b/src/components/AccountCard.tsx @@ -43,10 +43,7 @@ const AccountCard = ({ getTagDerivationMode(currency as CryptoCurrency, account.derivationMode); return ( - + { const { colors } = useTheme(); - const c = color || colors.live; + const c = color || colors.primary.c80; return ( void; + children?: React.ReactNode; + confirmLabel?: React.ReactNode; + confirmProps?: any; +}; + +const InfoModal = ({ + isOpened, + onClose, + id, + title, + desc, + bullets, + Icon = Icons.InfoMedium, + withCancel, + onContinue, + children, + confirmLabel, + confirmProps, + style, + containerStyle, +}: InfoModalProps) => ( + + + + {title ? ( + + {title} + + ) : null} + + {desc ? ( + + {desc} + + ) : null} + {bullets ? ( + + {bullets.map(b => ( + {b.val} + ))} + + ) : null} + + {children} + + + + + {withCancel ? ( + + ) : null} + + + +); + +function BulletLine({ children }: { children: any }) { + const { colors } = useTheme(); + return ( + + + + {children} + + + ); +} + +const styles = StyleSheet.create({ + modal: { + paddingHorizontal: 16, + paddingTop: 24, + alignItems: "center", + }, + modalTitle: { + marginVertical: 16, + fontSize: 14, + lineHeight: 21, + }, + modalDesc: { + textAlign: "center", + + marginBottom: 24, + }, + bulletsContainer: { + alignSelf: "flex-start", + }, + bulletLine: { + flexDirection: "row", + alignItems: "center", + marginBottom: 8, + }, + bulletLineText: { + marginLeft: 4, + textAlign: "left", + }, + childrenContainer: { + paddingTop: 24, + }, + footer: { + alignSelf: "stretch", + paddingTop: 24, + flexDirection: "row", + }, +}); + +export default memo(InfoModal); diff --git a/src/components/RootNavigator/AccountSettingsNavigator.js b/src/components/RootNavigator/AccountSettingsNavigator.js index 6a49fc95b8..90e545bc60 100644 --- a/src/components/RootNavigator/AccountSettingsNavigator.js +++ b/src/components/RootNavigator/AccountSettingsNavigator.js @@ -31,7 +31,7 @@ export default function AccountSettingsNavigator() { component={AccountSettingsMain} options={{ title: t("account.settings.header"), - headerRight: closableNavconfig.headerRight, + headerRight: null, }} /> void; + onViewDetails?: () => void; + title?: React.ReactNode; + description?: React.ReactNode; + primaryButton?: React.ReactNode; + secondaryButton?: React.ReactNode; + icon?: React.ReactNode; + iconColor?: string; + iconBoxSize: number; + iconSize: number; + info?: React.ReactNode; + onLearnMore?: () => void; +}; + +function ValidateSuccess({ + onClose, + onViewDetails, + title, + description, + primaryButton, + secondaryButton, + icon = Icons.CheckAloneMedium, + iconColor = "success.c100", + iconBoxSize = 64, + iconSize = 24, + info, + onLearnMore, +}: Props) { + return ( + + + + + {title || } + + + {description || } + + {info && ( + + {info} + + )} + + + {primaryButton || + (onViewDetails && ( + + ))} + {secondaryButton || + (onClose && ( + + ))} + + + ); +} + +export default memo(ValidateSuccess); diff --git a/src/families/tezos/AccountHeader.tsx b/src/families/tezos/AccountHeader.tsx index 1e314d1aa6..3646db0814 100644 --- a/src/families/tezos/AccountHeader.tsx +++ b/src/families/tezos/AccountHeader.tsx @@ -31,7 +31,7 @@ const styles = StyleSheet.create({ }, btn: { marginTop: 16, - width: 140, + width: 160, }, }); diff --git a/src/families/tezos/DelegationFlow/Started.tsx b/src/families/tezos/DelegationFlow/Started.tsx new file mode 100644 index 0000000000..641f97392e --- /dev/null +++ b/src/families/tezos/DelegationFlow/Started.tsx @@ -0,0 +1,77 @@ +import React, { useCallback } from "react"; +import { Linking } from "react-native"; +import { Trans } from "react-i18next"; +import { Flex, Text, Icons, List, Link, Button, Log } from "@ledgerhq/native-ui"; +import { ScreenName } from "../../../const"; +import { TrackScreen } from "../../../analytics"; +import { urls } from "../../../config/urls"; +import Illustration from "../../../images/illustration/Illustration"; +import EarnLight from "../../../images/illustration/Earn.light.png"; +import EarnDark from "../../../images/illustration/Earn.dark.png"; + +type Props = { + navigation: any; + route: { params: any }; +}; + +const Check = ; + +export default function DelegationStarted({ navigation, route }: Props) { + const onNext = useCallback(() => { + navigation.navigate(ScreenName.DelegationSummary, { + ...route.params, + }); + }, [navigation, route.params]); + + const howDelegationWorks = useCallback(() => { + Linking.openURL(urls.delegation); + }, []); + + return ( + + + + + + + + + + + + + + + , + , + , + ].map(wording => ({ title: wording, bullet: Check }))} + itemContainerProps={{ + alignItems: "center", + }} + my={8} + /> + + + + + + + + + ); +} diff --git a/src/families/tezos/DelegationFlow/Summary.tsx b/src/families/tezos/DelegationFlow/Summary.tsx new file mode 100644 index 0000000000..4e15a9d43a --- /dev/null +++ b/src/families/tezos/DelegationFlow/Summary.tsx @@ -0,0 +1,467 @@ +/* @flow */ +import React, { useCallback, useEffect, useState } from "react"; +import { View, StyleSheet, Animated } from "react-native"; +import SafeAreaView from "react-native-safe-area-view"; +import { useSelector } from "react-redux"; +import { Trans } from "react-i18next"; +import invariant from "invariant"; +import Icon from "react-native-vector-icons/dist/Feather"; +import { getAccountBridge } from "@ledgerhq/live-common/lib/bridge"; +import { + getAccountCurrency, + getAccountName, + getAccountUnit, + shortAddressPreview, +} from "@ledgerhq/live-common/lib/account"; +import { getCurrencyColor } from "@ledgerhq/live-common/lib/currencies"; +import useBridgeTransaction from "@ledgerhq/live-common/lib/bridge/useBridgeTransaction"; +import { + useDelegation, + useBaker, + useBakers, + useRandomBaker, +} from "@ledgerhq/live-common/lib/families/tezos/bakers"; +import whitelist from "@ledgerhq/live-common/lib/families/tezos/bakers.whitelist-default"; +import type { AccountLike } from "@ledgerhq/live-common/lib/types"; +import { useTheme } from "@react-navigation/native"; +import { Alert } from "@ledgerhq/native-ui"; +import { accountScreenSelector } from "../../../reducers/accounts"; +import { rgba } from "../../../colors"; +import { ScreenName } from "../../../const"; +import { TrackScreen } from "../../../analytics"; +import { useTransactionChangeFromNavigation } from "../../../logic/screenTransactionHooks"; +import Button from "../../../components/Button"; +import LText from "../../../components/LText"; +import Circle from "../../../components/Circle"; +import CurrencyIcon from "../../../components/CurrencyIcon"; +import CurrencyUnitValue from "../../../components/CurrencyUnitValue"; +import Touchable from "../../../components/Touchable"; +import DelegatingContainer from "../DelegatingContainer"; +import BakerImage from "../BakerImage"; + +const forceInset = { bottom: "always" }; + +type Props = { + navigation: any, + route: { params: RouteParams }, +}; + +type RouteParams = { + mode?: "delegate" | "undelegate", + accountId: string, + parentId?: string, +}; + +const AccountBalanceTag = ({ account }: { account: AccountLike }) => { + const unit = getAccountUnit(account); + const { colors } = useTheme(); + return ( + + + + + + ); +}; + +const ChangeDelegator = () => { + const { colors } = useTheme(); + return ( + + + + ); +}; + +const Line = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +const Words = ({ + children, + highlighted, + style, +}: { + children: React.ReactNode, + highlighted?: boolean, + style?: any, +}) => ( + + {children} + +); + +const BakerSelection = ({ + name, + readOnly, +}: { + name: string, + readOnly?: boolean, +}) => { + const { colors } = useTheme(); + return ( + + + {name} + + {readOnly ? null : ( + + + + )} + + ); +}; + +export default function DelegationSummary({ navigation, route }: Props) { + const { colors } = useTheme(); + const { account, parentAccount } = useSelector(accountScreenSelector(route)); + const bakers = useBakers(whitelist); + const randomBaker = useRandomBaker(bakers); + + const { + transaction, + setTransaction, + status, + bridgePending, + bridgeError, + } = useBridgeTransaction(() => ({ + account, + parentAccount, + })); + + invariant(account, "account must be defined"); + invariant(transaction, "transaction must be defined"); + invariant(transaction.family === "tezos", "transaction tezos"); + + // make sure tx is in sync + useEffect(() => { + if (!transaction || !account) return; + invariant(transaction.family === "tezos", "tezos tx"); + + // make sure the mode is in sync (an account changes can reset it) + const patch: Object = { + mode: route.params?.mode ?? "delegate", + }; + + // make sure that in delegate mode, a transaction recipient is set (random pick) + if (patch.mode === "delegate" && !transaction.recipient && randomBaker) { + patch.recipient = randomBaker.address; + } + + // when changes, we set again + if (patch.mode !== transaction.mode || "recipient" in patch) { + setTransaction( + getAccountBridge(account, parentAccount).updateTransaction( + transaction, + patch, + ), + ); + } + }, [ + account, + randomBaker, + navigation, + parentAccount, + setTransaction, + transaction, + route.params, + ]); + + const [rotateAnim] = useState(() => new Animated.Value(0)); + useEffect(() => { + Animated.loop( + Animated.sequence([ + Animated.timing(rotateAnim, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(rotateAnim, { + toValue: -1, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(rotateAnim, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + Animated.delay(1000), + ]), + ).start(); + return () => { + rotateAnim.setValue(0); + }; + }, [rotateAnim]); + + const rotate = rotateAnim.interpolate({ + inputRange: [0, 1], + // $FlowFixMe + outputRange: ["0deg", "30deg"], + }); + + const onChangeDelegator = useCallback(() => { + rotateAnim.setValue(0); + navigation.navigate(ScreenName.DelegationSelectValidator, { + ...route.params, + transaction, + }); + }, [rotateAnim, navigation, transaction, route.params]); + + const delegation = useDelegation(account); + const addr = + transaction.mode === "undelegate" + ? (delegation && delegation.address) || "" + : transaction.recipient; + const baker = useBaker(addr); + const bakerName = baker ? baker.name : shortAddressPreview(addr); + const currency = getAccountCurrency(account); + const color = getCurrencyColor(currency); + const accountName = getAccountName(account); + + // handle any edit screen changes + useTransactionChangeFromNavigation(setTransaction); + + const onContinue = useCallback(async () => { + navigation.navigate(ScreenName.DelegationSelectDevice, { + accountId: account.id, + parentId: parentAccount && parentAccount.id, + transaction, + status, + }); + }, [status, account, parentAccount, navigation, transaction]); + + return ( + + + + + + + + + + + } + right={ + transaction.mode === "delegate" ? ( + + + + + + + + + ) : ( + + ) + } + /> + + + + + {transaction.mode === "delegate" ? ( + + ) : ( + + )} + + + {accountName} + + + + {transaction.mode === "delegate" ? ( + + + + + + + + + ) : ( + + + + + + + )} + + {baker && transaction.mode === "delegate" ? ( + baker.capacityStatus === "full" ? null : ( + /* + + + + + + + */ + + + + + + + + ) + ) : null} + + + + + {transaction.mode === "undelegate" ? ( + } /> + ) : ( + } /> + )} +