Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(llm): πŸ“ add Stellar memo input on the recipient selection step #8178

Merged
merged 7 commits into from
Oct 30, 2024
Merged
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
7 changes: 7 additions & 0 deletions .changeset/chilled-points-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"live-mobile": minor
"@ledgerhq/coin-stellar": patch
"@ledgerhq/live-common": patch
---

Add Stellar memo input on the recipient selection step
65 changes: 65 additions & 0 deletions apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx
Original file line number Diff line number Diff line change
@@ -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<StellarTransaction>) => {
const { t } = useTranslation();

const [memoType, setMemoType] = useState<MemoType>("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 (
<>
<AnimatedInputSelect
placeholder={t("send.summary.memo.value")}
value={memoValue}
onChange={handleChangeValue}
selectProps={{
text: t(MEMO_TYPES.get(memoType) ?? "send.summary.memo.type"),
onPressSelect: () => setIsOpen(true),
}}
/>

<MemoTypeDrawer
isOpen={isOpen}
closeModal={() => setIsOpen(false)}
value={memoType}
onChange={handleChangeType}
/>
</>
);
};

type MemoType = Parameters<(typeof MEMO_TYPES)["get"]>[0];
68 changes: 68 additions & 0 deletions apps/ledger-live-mobile/src/families/stellar/MemoTypeDrawer.tsx
Original file line number Diff line number Diff line change
@@ -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<MemoType, string>([
["NO_MEMO", "stellar.memoType.NO_MEMO"],
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don't use enum for "NO_MEMO" etc? :)

More easier to use and to be sure we have all options!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also wanted to use an enum at first but then I realised that typescript enum can't really be iterated on (I think they lose their type with Object.values) without this the code becomes pretty verbose. Plus Mounir mentioned that it would be an anti pattern. Also IMO inferring the type from StellarMemoType (defined in Stellar's types/bridge.ts) provides a better type safety.

["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 (
<QueuedDrawer
title={
<Text variant="h4" textTransform="none">
{t("send.summary.memo.type")}
</Text>
}
isRequestingToBeOpened={isOpen}
onClose={closeModal}
>
{Array.from(MEMO_TYPES).map(([type, label]) => (
<Option
key={type}
label={t(label)}
selected={type === value}
onPress={() => onChange(type)}
/>
))}
</QueuedDrawer>
);
}

type OptionProps = TouchableOpacityProps & { label: string; selected?: boolean };
function Option({ label, selected = false, onPress }: OptionProps) {
return (
<TouchableOpacity
style={{ padding: 16, display: "flex", flexDirection: "row", alignItems: "center" }}
onPress={onPress}
>
<Text fontSize="body" fontWeight="semiBold" color={selected ? "primary.c80" : "neutral.c100"}>
{label}
</Text>
{selected && (
<Circle size={24} style={{ position: "absolute", right: 0 }}>
<Icons.CheckmarkCircleFill size="M" color="primary.c80" />
</Circle>
)}
</TouchableOpacity>
);
}

type MemoType = (typeof StellarMemoType)[number];
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ export const useMemoTagInput = (
null;

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

return Input && { Input, isEmpty, handleChange };
return Input && { Input, isEmpty, error, handleChange };
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ import type { AnimatedInputProps } from "@ledgerhq/native-ui/components/Form/Inp
export type MemoTagInputProps<T extends Transaction = Transaction> = Omit<
AnimatedInputProps,
"value" | "onChangeText" | "onChange"
> & { onChange: (update: { patch: Partial<T>; value: string }) => void };
> & { onChange: (update: { patch: Partial<T>; value: string; error?: Error }) => void };
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -277,7 +278,7 @@ export default function SendSelectRecipient({ navigation, route }: Props) {
},
]}
/>
<LText color="grey">{<Trans i18nKey="common.or" />}</LText>
<Text color="neutral.c70">{t("common.or")}</Text>
<View
style={[
styles.separatorLine,
Expand Down Expand Up @@ -316,6 +317,9 @@ export default function SendSelectRecipient({ navigation, route }: Props) {
placeholder={t("send.summary.memo.title")}
onChange={memoTag.handleChange}
/>
<Text mt={4} pl={2} color="alert">
<TranslatedError error={memoTag.error} />
</Text>
</View>
)}

Expand All @@ -337,7 +341,7 @@ export default function SendSelectRecipient({ navigation, route }: Props) {
testID="recipient-continue-button"
type="primary"
title={<Trans i18nKey="common.continue" />}
disabled={debouncedBridgePending || !!status.errors.recipient}
disabled={debouncedBridgePending || !!status.errors.recipient || memoTag?.error}
pending={debouncedBridgePending}
onPress={onPressContinue}
/>
Expand Down
8 changes: 7 additions & 1 deletion libs/coin-modules/coin-stellar/src/types/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +35 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe each elem could be part of an enum to be more accurate ? and more easy ti use in other component ? like in

const type = memoType === "NO_MEMO" && value ? "MEMO_TEXT" : memoType; ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah initially I created a type for this but it required more changes than I was confortable making to the Stellar code.
E.g in some places memoType is assigned a value.toString().
image

I'm not sure whether there's a good reason for this but rather than risking breaking things I decided not to touch the type of the Stellar transaction and just base my changes on this StellarMemoType list.


export type StellarTransactionMode = "send" | "changeTrust";

Expand Down
1 change: 1 addition & 0 deletions libs/ledger-live-common/.unimportedrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "@ledgerhq/coin-stellar/bridge/logic";
4 changes: 0 additions & 4 deletions libs/ledger-live-common/src/families/stellar/bridge/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
};

Expand Down
Loading