Skip to content

Commit

Permalink
feat(llm): 🏷️ add memo/tag input in some coins send flow (#8155)
Browse files Browse the repository at this point in the history
* feat(llm): add the memo tag input

* feat(llm): add the memo tag drawer

* feat(llm): use coin specific memo tag input via useMemoTagInput

* feat(llm): add a TON "comment" field in the recipient step

* feat(llm): add an ICP memo input in the recipient step

* feat(llm): add XRP tag input to the recipient step

* chore(llm): simplify each coin specific MemoTagInput

* feat(llm): add the memo tag input on a bunch of coins

* chore: update change log

* chore(llm): create a GenericMemoTagInput component to reduce the boilerplate on each coin

* chore(llm): store the memo tag drawer state in an enum
  • Loading branch information
thesan authored Oct 23, 2024
1 parent c4f102d commit bb6dfe4
Show file tree
Hide file tree
Showing 19 changed files with 338 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/strong-spiders-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"live-mobile": minor
---

Add memo/tag input in some coins send flow
1 change: 1 addition & 0 deletions apps/ledger-live-mobile/scripts/sync-families-dispatch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const targets = [
"ConnectDevice",
"NoAssociatedAccounts",
"EditOperationPanel",
"MemoTagInput",
];

async function genTarget(target) {
Expand Down
12 changes: 12 additions & 0 deletions apps/ledger-live-mobile/src/families/algorand/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from "react";

import type { Transaction as AlgorandTransaction } from "@ledgerhq/live-common/families/algorand/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => (
<GenericMemoTagInput<AlgorandTransaction>
{...props}
valueToTxPatch={value => ({ memo: value || undefined })}
/>
);
12 changes: 12 additions & 0 deletions apps/ledger-live-mobile/src/families/cardano/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from "react";

import type { Transaction as CardanoTransaction } from "@ledgerhq/live-common/families/cardano/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => (
<GenericMemoTagInput<CardanoTransaction>
{...props}
valueToTxPatch={value => ({ memo: value || undefined })}
/>
);
18 changes: 18 additions & 0 deletions apps/ledger-live-mobile/src/families/casper/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";
import { useTranslation } from "react-i18next";

import type { Transaction as CasperTransaction } from "@ledgerhq/live-common/families/casper/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => {
const { t } = useTranslation();
return (
<GenericMemoTagInput<CasperTransaction>
{...props}
textToValue={text => text.replace(/\D/g, "")}
valueToTxPatch={value => ({ transferId: value || undefined })}
placeholder={t("send.summary.transferId")}
/>
);
};
12 changes: 12 additions & 0 deletions apps/ledger-live-mobile/src/families/cosmos/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from "react";

import type { Transaction as CosmosTransaction } from "@ledgerhq/live-common/families/cosmos/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => (
<GenericMemoTagInput<CosmosTransaction>
{...props}
valueToTxPatch={value => ({ memo: value || undefined })}
/>
);
12 changes: 12 additions & 0 deletions apps/ledger-live-mobile/src/families/crypto_org/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from "react";

import type { Transaction as CryptoOrgTransaction } from "@ledgerhq/live-common/families/crypto_org/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => (
<GenericMemoTagInput<CryptoOrgTransaction>
{...props}
valueToTxPatch={value => ({ memo: value || undefined })}
/>
);
12 changes: 12 additions & 0 deletions apps/ledger-live-mobile/src/families/hedera/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from "react";

import type { Transaction as HederaTransaction } from "@ledgerhq/live-common/families/hedera/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => (
<GenericMemoTagInput<HederaTransaction>
{...props}
valueToTxPatch={value => ({ memo: value || undefined })}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";

import { Transaction as ICPTransaction } from "@ledgerhq/live-common/families/internet_computer/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => (
<GenericMemoTagInput<ICPTransaction>
{...props}
textToValue={text => text.replace(/\D/g, "")}
valueToTxPatch={value => ({ memo: value || undefined })}
/>
);
12 changes: 12 additions & 0 deletions apps/ledger-live-mobile/src/families/solana/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from "react";

import { Transaction as SolanaTransaction } from "@ledgerhq/live-common/generated/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => (
<GenericMemoTagInput<SolanaTransaction>
{...props}
valueToTxPatch={value => ({ memo: value || undefined })}
/>
);
12 changes: 12 additions & 0 deletions apps/ledger-live-mobile/src/families/stacks/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from "react";

import type { Transaction as StacksTransaction } from "@ledgerhq/live-common/families/stacks/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => (
<GenericMemoTagInput<StacksTransaction>
{...props}
valueToTxPatch={value => ({ memo: value || undefined })}
/>
);
19 changes: 19 additions & 0 deletions apps/ledger-live-mobile/src/families/ton/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import { useTranslation } from "react-i18next";

import type { Transaction as TonTransaction } from "@ledgerhq/live-common/families/ton/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => {
const { t } = useTranslation();
return (
<GenericMemoTagInput<TonTransaction>
{...props}
valueToTxPatch={value =>
value ? { comment: { isEncrypted: false, text: value } } : { comment: undefined }
}
placeholder={t("send.summary.comment")}
/>
);
};
18 changes: 18 additions & 0 deletions apps/ledger-live-mobile/src/families/xrp/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";
import { useTranslation } from "react-i18next";

import type { Transaction as RippleTransaction } from "@ledgerhq/live-common/families/xrp/types";
import type { MemoTagInputProps } from "LLM/features/MemoTag/types";
import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput";

export default (props: MemoTagInputProps) => {
const { t } = useTranslation();
return (
<GenericMemoTagInput<RippleTransaction>
{...props}
textToValue={text => text.replace(/\D/g, "")}
valueToTxPatch={value => ({ tag: value ? Number(value) : undefined })}
placeholder={t("send.summary.tag")}
/>
);
};
8 changes: 8 additions & 0 deletions apps/ledger-live-mobile/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -3112,6 +3112,14 @@
"input": "Enter address",
"inputEns": "Enter address or ENS"
},
"memoTag": {
"input": "Enter Tag / Memo",
"title": "Don’t use Tag/Memo?",
"description": "Your funds will get lost if you don’t add a Tag/Memo when sending them to any centralized exchange or custodial wallet.",
"cta": "Add tag",
"ignore": "Don't add tag",
"learnMore": "Learn more about Tag/Memo"
},
"send": {
"title": "Send",
"description": "Send crypto to another wallet."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";

import type { Transaction } from "@ledgerhq/live-common/generated/types";
import { AnimatedInput } from "@ledgerhq/native-ui";
import { MemoTagInputProps } from "../types";

type Props<T extends Transaction = Transaction> = MemoTagInputProps<T> & {
textToValue?: (text: string) => string;
valueToTxPatch: (text: string) => Partial<T>;
};

export function GenericMemoTagInput<T extends Transaction>({
onChange,
valueToTxPatch,
textToValue,
...inputProps
}: Props<T>) {
const [value, setValue] = React.useState("");

const handleChange = (text: string) => {
const value = textToValue?.(text) ?? text;
const patch = valueToTxPatch(value);
setValue(value);
onChange({ value, patch });
};

return <AnimatedInput {...inputProps} value={value} onChangeText={handleChange} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useTheme } from "styled-components/native";
import React, { memo } from "react";
import { useTranslation } from "react-i18next";
import { Linking } from "react-native";
import { Flex, Icons, Text } from "@ledgerhq/native-ui";
import Button from "~/components/Button";
import Circle from "~/components/Circle";
import QueuedDrawer from "~/components/QueuedDrawer";
import { urls } from "~/utils/urls";

type Props = {
open: boolean;
onClose: () => void;
onNext: () => void;
};

export const MemoTagDrawer = memo(({ open, onClose, onNext }: Props) => {
const { colors } = useTheme();
const { t } = useTranslation();

return (
<QueuedDrawer isRequestingToBeOpened={open} onClose={onClose}>
<Flex alignItems="center" mb={7}>
<Circle size={72} bg={colors.opacityDefault.c05}>
<Icons.InformationFill size="L" color="primary.c80" />
</Circle>
</Flex>

<Text variant="h4" textAlign="center" mb={6}>
{t("transfer.receive.memoTag.title")}
</Text>

<Text variant="bodyLineHeight" textAlign="center" color="neutral.c80" mb={8}>
{t("transfer.receive.memoTag.description")}
</Text>

<Button type="primary" title={t("transfer.memoTag.cta")} onPress={onClose} mb={3} />

<Button
type="tertiary"
title={t("transfer.memoTag.ignore")}
onPress={onNext}
outline
mb={3}
/>

<Button
type="accent"
size="large"
Icon={() => <Icons.ExternalLink size="S" color="primary.c80" />}
onPress={() => Linking.openURL(urls.memoTag)}
>
{t("transfer.memoTag.learnMore")}
</Button>
</QueuedDrawer>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FC, useCallback, useState } from "react";

import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import { Transaction } from "@ledgerhq/live-common/generated/types";
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import perFamily from "~/generated/MemoTagInput";
import { MemoTagInputProps } from "../types";

export const useMemoTagInput = (
family: CryptoCurrency["family"],
updateTransaction: (patch: Partial<Transaction>) => void,
) => {
const featureMemoTag = useFeature("llmMemoTag");
const Input: FC<MemoTagInputProps> | null =
(featureMemoTag?.enabled &&
family in perFamily &&
perFamily[family as keyof typeof perFamily]) ||
null;

const [isEmpty, setIsEmpty] = useState(true);
const handleChange = useCallback<MemoTagInputProps["onChange"]>(
({ patch, value }) => {
setIsEmpty(!value);
updateTransaction(patch);
},
[updateTransaction],
);

return Input && { Input, isEmpty, handleChange };
};
7 changes: 7 additions & 0 deletions apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Transaction } from "@ledgerhq/live-common/generated/types";
import type { AnimatedInputProps } from "@ledgerhq/native-ui/components/Form/Input/AnimatedInput";

export type MemoTagInputProps<T extends Transaction = Transaction> = Omit<
AnimatedInputProps,
"value" | "onChangeText" | "onChange"
> & { onChange: (update: { patch: Partial<T>; value: string }) => void };
Loading

1 comment on commit bb6dfe4

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bot] Bitcoin on Staging ($0.00) ⏲ 51s

What is the bot and how does it work? Everything is documented here!

13 critical spec errors

Spec Bitcoin failed!

TransportStatusError: Ledger device: UNKNOWN_ERROR (0x6a86)

Spec Bitcoin Testnet failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec Bitcoin Cash failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec Bitcoin Gold failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec Dash failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec Digibyte failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec DogeCoin failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec Komodo failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec Litecoin failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec Qtum failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec ZCash failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec Horizen failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com

Spec Decred failed!

Error: getaddrinfo ENOTFOUND atlas-stg.ledger-test.com
Details of the 0 mutations

Spec Bitcoin (failed)


Spec Bitcoin Testnet (failed)


Spec Bitcoin Cash (failed)


Spec Bitcoin Gold (failed)


Spec Dash (failed)


Spec Digibyte (failed)


Spec DogeCoin (failed)


Spec Komodo (failed)


Spec Litecoin (failed)


Spec Qtum (failed)


Spec ZCash (failed)


Spec Horizen (failed)


Spec Decred (failed)


Details of the 65 uncovered mutations

Spec Bitcoin (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec Bitcoin Testnet (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec Bitcoin Cash (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec Bitcoin Gold (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec Dash (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec Digibyte (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec DogeCoin (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec Komodo (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec Litecoin (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec Qtum (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec ZCash (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec Horizen (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:

Spec Decred (5)

  • move ~50%:
  • optimize-size:
  • send 1 utxo:
  • send OP_RETURN transaction:
  • send max:
Portfolio ($0.00) – Details of the 13 currencies
Spec (accounts) State Remaining Runs (est) funds?
Bitcoin (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
Bitcoin Testnet (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
Bitcoin Cash (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
Bitcoin Gold (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
Dash (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
Digibyte (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
DogeCoin (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
Komodo (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
Litecoin (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
Qtum (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
ZCash (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
Horizen (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``
Decred (0) 0 ops , πŸ€·β€β™‚οΈ ❌ ``

Performance ⏲ 51s

Time spent for each spec: (total across mutations)

Spec (accounts) preload scan re-sync tx status sign op broadcast test destination test
TOTAL 4.98ms N/A N/A N/A N/A N/A N/A N/A
Bitcoin (0) 0.24ms N/A N/A N/A N/A N/A N/A N/A
Bitcoin Testnet (0) 0.86ms N/A N/A N/A N/A N/A N/A N/A
Bitcoin Cash (0) 2.05ms N/A N/A N/A N/A N/A N/A N/A
Bitcoin Gold (0) 0.18ms N/A N/A N/A N/A N/A N/A N/A
Dash (0) 0.20ms N/A N/A N/A N/A N/A N/A N/A
Digibyte (0) 0.16ms N/A N/A N/A N/A N/A N/A N/A
DogeCoin (0) 0.16ms N/A N/A N/A N/A N/A N/A N/A
Komodo (0) 0.17ms N/A N/A N/A N/A N/A N/A N/A
Litecoin (0) 0.16ms N/A N/A N/A N/A N/A N/A N/A
Qtum (0) 0.24ms N/A N/A N/A N/A N/A N/A N/A
ZCash (0) 0.23ms N/A N/A N/A N/A N/A N/A N/A
Horizen (0) 0.15ms N/A N/A N/A N/A N/A N/A N/A
Decred (0) 0.18ms N/A N/A N/A N/A N/A N/A N/A

What is the bot and how does it work? Everything is documented here!

Please sign in to comment.