diff --git a/.changeset/chilled-points-argue.md b/.changeset/chilled-points-argue.md new file mode 100644 index 000000000000..71b02c5b50a4 --- /dev/null +++ b/.changeset/chilled-points-argue.md @@ -0,0 +1,7 @@ +--- +"live-mobile": minor +"@ledgerhq/coin-stellar": patch +"@ledgerhq/live-common": patch +--- + +Add Stellar memo input on the recipient selection step diff --git a/apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx new file mode 100644 index 000000000000..6c58457e5910 --- /dev/null +++ b/apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx @@ -0,0 +1,65 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { isMemoValid } from "@ledgerhq/live-common/families/stellar/bridge/logic"; +import { + StellarWrongMemoFormat, + type Transaction as StellarTransaction, +} from "@ledgerhq/live-common/families/stellar/types"; +import { AnimatedInputSelect } from "@ledgerhq/native-ui"; +import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; +import { MemoTypeDrawer, MEMO_TYPES } from "./MemoTypeDrawer"; + +export default ({ onChange }: MemoTagInputProps) => { + const { t } = useTranslation(); + + const [memoType, setMemoType] = useState("NO_MEMO"); + const [memoValue, setMemoValue] = React.useState(""); + const [isOpen, setIsOpen] = useState(false); + + const handleChange = (type: MemoType, value: string) => { + const error = isMemoValid(type, value) ? undefined : new StellarWrongMemoFormat(); + const patch = { memoType: type, memoValue: value }; + onChange({ value, patch, error }); + }; + + const handleChangeType = (type: MemoType) => { + const value = type === "NO_MEMO" ? "" : memoValue; + handleChange(type, value); + + setMemoType(type); + if (value !== memoValue) setMemoValue(value); + setIsOpen(false); + }; + + const handleChangeValue = (value: string) => { + const type = memoType === "NO_MEMO" && value ? "MEMO_TEXT" : memoType; + handleChange(type, value); + + setMemoValue(value); + if (type !== memoType) setMemoType(type); + }; + + return ( + <> + setIsOpen(true), + }} + /> + + setIsOpen(false)} + value={memoType} + onChange={handleChangeType} + /> + + ); +}; + +type MemoType = Parameters<(typeof MEMO_TYPES)["get"]>[0]; diff --git a/apps/ledger-live-mobile/src/families/stellar/MemoTypeDrawer.tsx b/apps/ledger-live-mobile/src/families/stellar/MemoTypeDrawer.tsx new file mode 100644 index 000000000000..f6168494ef34 --- /dev/null +++ b/apps/ledger-live-mobile/src/families/stellar/MemoTypeDrawer.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { TouchableOpacity, TouchableOpacityProps } from "react-native"; + +import type { StellarMemoType } from "@ledgerhq/live-common/families/stellar/types"; +import { Icons, Text } from "@ledgerhq/native-ui"; +import Circle from "~/components/Circle"; +import QueuedDrawer from "~/components/QueuedDrawer"; + +export const MEMO_TYPES = new Map([ + ["NO_MEMO", "stellar.memoType.NO_MEMO"], + ["MEMO_TEXT", "stellar.memoType.MEMO_TEXT"], + ["MEMO_ID", "stellar.memoType.MEMO_ID"], + ["MEMO_HASH", "stellar.memoType.MEMO_HASH"], + ["MEMO_RETURN", "stellar.memoType.MEMO_RETURN"], +]); + +type Props = { + isOpen: boolean; + closeModal: () => void; + value: MemoType; + onChange: (value: MemoType) => void; +}; + +export function MemoTypeDrawer({ isOpen, closeModal, value, onChange }: Props) { + const { t } = useTranslation(); + return ( + + {t("send.summary.memo.type")} + + } + isRequestingToBeOpened={isOpen} + onClose={closeModal} + > + {Array.from(MEMO_TYPES).map(([type, label]) => ( + + ); +} + +type OptionProps = TouchableOpacityProps & { label: string; selected?: boolean }; +function Option({ label, selected = false, onPress }: OptionProps) { + return ( + + + {label} + + {selected && ( + + + + )} + + ); +} + +type MemoType = (typeof StellarMemoType)[number]; diff --git a/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts b/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts index 5e4daf1b278e..17c816609869 100644 --- a/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts +++ b/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts @@ -18,13 +18,15 @@ export const useMemoTagInput = ( null; const [isEmpty, setIsEmpty] = useState(true); + const [error, setError] = useState(); const handleChange = useCallback( - ({ patch, value }) => { + ({ patch, value, error }) => { setIsEmpty(!value); + setError(error); updateTransaction(patch); }, [updateTransaction], ); - return Input && { Input, isEmpty, handleChange }; + return Input && { Input, isEmpty, error, handleChange }; }; diff --git a/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts b/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts index 9687b43d5631..7fbbecc42c43 100644 --- a/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts +++ b/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts @@ -4,4 +4,4 @@ import type { AnimatedInputProps } from "@ledgerhq/native-ui/components/Form/Inp export type MemoTagInputProps = Omit< AnimatedInputProps, "value" | "onChangeText" | "onChange" -> & { onChange: (update: { patch: Partial; value: string }) => void }; +> & { onChange: (update: { patch: Partial; value: string; error?: Error }) => void }; diff --git a/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx b/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx index 42f9e64b5b3d..d42000658c03 100644 --- a/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx +++ b/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx @@ -1,5 +1,6 @@ import { isConfirmedOperation } from "@ledgerhq/coin-framework/operation"; import { RecipientRequired } from "@ledgerhq/errors"; +import { Text } from "@ledgerhq/native-ui"; import { getAccountCurrency, getMainAccount } from "@ledgerhq/live-common/account/helpers"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; import { @@ -27,11 +28,11 @@ import CancelButton from "~/components/CancelButton"; import { EditOperationCard } from "~/components/EditOperationCard"; import GenericErrorBottomModal from "~/components/GenericErrorBottomModal"; import KeyboardView from "~/components/KeyboardView"; -import LText from "~/components/LText"; import NavigationScrollView from "~/components/NavigationScrollView"; import RetryButton from "~/components/RetryButton"; import { SendFundsNavigatorStackParamList } from "~/components/RootNavigator/types/SendFundsNavigator"; import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; +import TranslatedError from "~/components/TranslatedError"; import { ScreenName } from "~/const"; import { accountScreenSelector } from "~/reducers/accounts"; import { currencySettingsForAccountSelector } from "~/reducers/settings"; @@ -277,7 +278,7 @@ export default function SendSelectRecipient({ navigation, route }: Props) { }, ]} /> - {} + {t("common.or")} + + + )} @@ -337,7 +341,7 @@ export default function SendSelectRecipient({ navigation, route }: Props) { testID="recipient-continue-button" type="primary" title={} - disabled={debouncedBridgePending || !!status.errors.recipient} + disabled={debouncedBridgePending || !!status.errors.recipient || memoTag?.error} pending={debouncedBridgePending} onPress={onPressContinue} /> diff --git a/libs/coin-modules/coin-stellar/src/types/bridge.ts b/libs/coin-modules/coin-stellar/src/types/bridge.ts index ed0a5f21c03b..93b5487ba362 100644 --- a/libs/coin-modules/coin-stellar/src/types/bridge.ts +++ b/libs/coin-modules/coin-stellar/src/types/bridge.ts @@ -32,7 +32,13 @@ export enum NetworkCongestionLevel { HIGH = "HIGH", } -export const StellarMemoType = ["NO_MEMO", "MEMO_TEXT", "MEMO_ID", "MEMO_HASH", "MEMO_RETURN"]; +export const StellarMemoType = [ + "NO_MEMO", + "MEMO_TEXT", + "MEMO_ID", + "MEMO_HASH", + "MEMO_RETURN", +] as const; export type StellarTransactionMode = "send" | "changeTrust"; diff --git a/libs/ledger-live-common/.unimportedrc.json b/libs/ledger-live-common/.unimportedrc.json index 534d9060f42d..58226330923f 100644 --- a/libs/ledger-live-common/.unimportedrc.json +++ b/libs/ledger-live-common/.unimportedrc.json @@ -309,6 +309,7 @@ "src/exchange/swap/const/blockchain.ts", "src/families/cardano/logic.ts", "src/families/cardano/staking.ts", + "src/families/stellar/bridge/logic.ts", "src/families/stellar/logic.ts", "src/families/tezos/logic.ts", "src/families/tezos/react.ts", diff --git a/libs/ledger-live-common/src/families/stellar/bridge/logic.ts b/libs/ledger-live-common/src/families/stellar/bridge/logic.ts new file mode 100644 index 000000000000..81f4484f6401 --- /dev/null +++ b/libs/ledger-live-common/src/families/stellar/bridge/logic.ts @@ -0,0 +1 @@ +export * from "@ledgerhq/coin-stellar/bridge/logic"; diff --git a/libs/ledger-live-common/src/families/stellar/bridge/mock.ts b/libs/ledger-live-common/src/families/stellar/bridge/mock.ts index 87346e99678a..8aed62809ca9 100644 --- a/libs/ledger-live-common/src/families/stellar/bridge/mock.ts +++ b/libs/ledger-live-common/src/families/stellar/bridge/mock.ts @@ -59,10 +59,6 @@ const createTransaction = (): Transaction => ({ }); const updateTransaction = (t, patch) => { - if ("recipient" in patch && patch.recipient !== t.recipient) { - return { ...t, ...patch, memoType: null }; - } - return { ...t, ...patch }; };