diff --git a/src/renderer/families/solana/OptInFlowModal/Body.js b/src/renderer/families/solana/OptInFlowModal/Body.js new file mode 100644 index 0000000000..2af60e6cae --- /dev/null +++ b/src/renderer/families/solana/OptInFlowModal/Body.js @@ -0,0 +1,207 @@ +// @flow +import invariant from "invariant"; +import React, { useState, useCallback } from "react"; +import { compose } from "redux"; +import { connect, useDispatch } from "react-redux"; +import { Trans, withTranslation } from "react-i18next"; +import { createStructuredSelector } from "reselect"; +import { SyncSkipUnderPriority } from "@ledgerhq/live-common/lib/bridge/react"; +import Track from "~/renderer/analytics/Track"; + +import { UserRefusedOnDevice } from "@ledgerhq/errors"; + +import { getAccountBridge } from "@ledgerhq/live-common/lib/bridge"; +import useBridgeTransaction from "@ledgerhq/live-common/lib/bridge/useBridgeTransaction"; + +import type { StepId, StepProps, St } from "./types"; +import type { Account, Operation } from "@ledgerhq/live-common/lib/types"; +import type { TFunction } from "react-i18next"; +import type { Device } from "@ledgerhq/live-common/lib/hw/actions/types"; + +import { addPendingOperation } from "@ledgerhq/live-common/lib/account"; +import { updateAccountWithUpdater } from "~/renderer/actions/accounts"; + +import { getCurrentDevice } from "~/renderer/reducers/devices"; +import { closeModal, openModal } from "~/renderer/actions/modals"; + +import Stepper from "~/renderer/components/Stepper"; +import StepAsset, { StepAssetFooter } from "./steps/StepAsset"; +import GenericStepConnectDevice from "~/renderer/modals/Send/steps/GenericStepConnectDevice"; +import StepConfirmation, { StepConfirmationFooter } from "./steps/StepConfirmation"; +import logger from "~/logger/logger"; + +type OwnProps = {| + stepId: StepId, + onClose: () => void, + onChangeStepId: StepId => void, + params: { + account: Account, + parentAccount: ?Account, + }, + name: string, +|}; + +type StateProps = {| + t: TFunction, + device: ?Device, + accounts: Account[], + device: ?Device, + closeModal: string => void, + openModal: string => void, +|}; + +type Props = OwnProps & StateProps; + +const steps: Array = [ + { + id: "assets", + label: , + component: StepAsset, + noScroll: true, + footer: StepAssetFooter, + }, + { + id: "connectDevice", + label: , + component: GenericStepConnectDevice, + onBack: ({ transitionTo }: StepProps) => transitionTo("assets"), + }, + { + id: "confirmation", + label: , + component: StepConfirmation, + footer: StepConfirmationFooter, + }, +]; + +const mapStateToProps = createStructuredSelector({ + device: getCurrentDevice, +}); + +const mapDispatchToProps = { + closeModal, + openModal, +}; + +const Body = ({ + t, + stepId, + device, + closeModal, + openModal, + onChangeStepId, + params, + name, +}: Props) => { + const [optimisticOperation, setOptimisticOperation] = useState(null); + const [transactionError, setTransactionError] = useState(null); + const [signed, setSigned] = useState(false); + const dispatch = useDispatch(); + + const { + transaction, + setTransaction, + updateTransaction, + account, + parentAccount, + status, + bridgeError, + bridgePending, + } = useBridgeTransaction(() => { + const { account } = params; + + invariant(account, "solana: account required"); + + const bridge = getAccountBridge(account, undefined); + + const transaction = bridge.createTransaction(account); + + return { account, parentAccount: undefined, transaction }; + }); + + const handleCloseModal = useCallback(() => { + closeModal(name); + }, [closeModal, name]); + + const handleStepChange = useCallback(e => onChangeStepId(e.id), [onChangeStepId]); + + const handleRetry = useCallback(() => { + setTransactionError(null); + onChangeStepId("assets"); + }, [onChangeStepId]); + + const handleTransactionError = useCallback((error: Error) => { + if (!(error instanceof UserRefusedOnDevice)) { + logger.critical(error); + } + setTransactionError(error); + }, []); + + const handleOperationBroadcasted = useCallback( + (optimisticOperation: Operation) => { + if (!account) return; + dispatch( + updateAccountWithUpdater(account.id, account => + addPendingOperation(account, optimisticOperation), + ), + ); + setOptimisticOperation(optimisticOperation); + setTransactionError(null); + }, + [account, dispatch], + ); + + const error = transactionError || bridgeError || status.errors.amount; + const warning = status.warnings ? Object.values(status.warnings)[0] : null; + + const errorSteps = []; + + if (transactionError) { + errorSteps.push(2); + } else if (bridgeError) { + errorSteps.push(0); + } + + const stepperProps = { + title: t("solana.optIn.flow.title"), + device, + account, + parentAccount, + transaction, + signed, + stepId, + steps, + errorSteps, + disabledSteps: [], + hideBreadcrumb: !!error || !!warning, + onRetry: handleRetry, + onStepChange: handleStepChange, + onClose: handleCloseModal, + error, + warning, + status, + optimisticOperation, + openModal, + setSigned, + onChangeTransaction: setTransaction, + onUpdateTransaction: updateTransaction, + onOperationBroadcasted: handleOperationBroadcasted, + onTransactionError: handleTransactionError, + t, + bridgePending, + }; + + return ( + + + + + ); +}; + +const C: React$ComponentType = compose( + connect(mapStateToProps, mapDispatchToProps), + withTranslation(), +)(Body); + +export default C; diff --git a/src/renderer/families/solana/OptInFlowModal/fields/SplTokenSelector.js b/src/renderer/families/solana/OptInFlowModal/fields/SplTokenSelector.js new file mode 100644 index 0000000000..918365ee70 --- /dev/null +++ b/src/renderer/families/solana/OptInFlowModal/fields/SplTokenSelector.js @@ -0,0 +1,83 @@ +// @flow +import React, { useState, useMemo } from "react"; +import { Trans } from "react-i18next"; + +import type { TokenCurrency } from "@ledgerhq/live-common/lib/types"; + +import { listTokensForCryptoCurrency } from "@ledgerhq/live-common/lib/currencies"; + +import Box from "~/renderer/components/Box"; +import FirstLetterIcon from "~/renderer/components/FirstLetterIcon"; +import Select from "~/renderer/components/Select"; +import Text from "~/renderer/components/Text"; +import ToolTip from "~/renderer/components/Tooltip"; +import ExclamationCircleThin from "~/renderer/icons/ExclamationCircleThin"; + +const renderItem = ({ + data: { id, name }, + isDisabled, + data, +}: { + data: TokenCurrency, + isDisabled: boolean, +}) => { + // TODO: make a function for that in common + const tokenParts = id.split("/"); + const mintAddress = tokenParts[2]; + return ( + + + + {name} + + - Token {mintAddress} + + + {isDisabled && ( + }> + + + + + )} + + ); +}; + +export default function DelegationSelectorField({ account, transaction, t, onChange }: *) { + const [query, setQuery] = useState(""); + const subAccounts = account.subAccounts; + const options = listTokensForCryptoCurrency(account.currency); + const value = useMemo(() => options.find(({ id }) => id === transaction.assetId), [ + options, + transaction, + ]); + + return ( + +