Skip to content

Commit 8d8ebf2

Browse files
authored
Merge pull request #14339 from LedgerHQ/feat/send-flow-orchestrator-lwm
feat(lwm): new send flow layout
2 parents 467706c + abeb96b commit 8d8ebf2

File tree

14 files changed

+673
-8
lines changed

14 files changed

+673
-8
lines changed

.changeset/stale-students-rush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"live-mobile": minor
3+
---
4+
5+
feat(lwm): create send flow orchestrator

apps/ledger-live-mobile/src/locales/en/common.json

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4156,6 +4156,81 @@
41564156
"loading": "Searching devices..."
41574157
},
41584158
"send": {
4159+
"newSendFlow": {
4160+
"title": "Send {{currency}}",
4161+
"available": "Available {{amount}}",
4162+
"placeholder": "Enter address or ENS",
4163+
"placeholderNoENS": "Enter address",
4164+
"editRecipientAccessibilityLabel": "Edit recipient",
4165+
"recent": "Recent",
4166+
"myAccounts": "My accounts",
4167+
"sendTo": "Send to {{address}}",
4168+
"addressMatched": "Address matched",
4169+
"addressNotFound": "Address not found",
4170+
"alreadyUsed": "Already used · {{date}}",
4171+
"recentSendWillAppear": "Your recent send will appear here",
4172+
"relativeDate": {
4173+
"justNow": "Just now",
4174+
"minutesAgo_one": "1 min ago",
4175+
"minutesAgo_other": "{{count}} min ago",
4176+
"hoursAgo_one": "1 hour ago",
4177+
"hoursAgo_other": "{{count}} hours ago",
4178+
"daysAgo_one": "1 day ago",
4179+
"daysAgo_other": "{{count}} days ago"
4180+
},
4181+
"remove": "Remove",
4182+
"enterTag": "Enter {{tag}}",
4183+
"firstInteraction": {
4184+
"title": "You’re sending to a new address",
4185+
"description": "Verify that the address is correct. In case of doubt send a small amount first."
4186+
},
4187+
"sanctioned": {
4188+
"title": "Flagged address",
4189+
"description": "This address has been flagged and Ledger doesn’t allow sending to it",
4190+
"helpCenter": "Help center"
4191+
},
4192+
"errors": {
4193+
"incorrectFormat": "Incorrect address format",
4194+
"incompatibleAsset": "Address isn’t compatible with the asset selected",
4195+
"walletNotExist": "Wallet doesn’t exist"
4196+
},
4197+
"skipMemo": {
4198+
"title": "Skip adding {{tag}}?",
4199+
"warning": "Your asset may be lost if a {{tag}} is required by your recipient"
4200+
},
4201+
"tagHelp": {
4202+
"title": "What’s a {{tag}}?",
4203+
"description": "A {{tag}} is required when sending {{currency}} to a crypto exchange. It’s a feature used to identify the recipient of the transaction. Not including the {{tag}} memo may result in a loss of funds."
4204+
},
4205+
"networkFeesInFiat": "Network fees in {{currency}}",
4206+
"feesAmount": "Fees amount ({{unit}})",
4207+
"gasLimit": "Gas limit ({{unit}})",
4208+
"feesPaid": "Fees paid to the network to proceed your transaction",
4209+
"insufficientBalanceFees": "Insufficient balance to cover the network fees",
4210+
"amountToSend": "Amount to send in ({{currency}})",
4211+
"coinToSend": "Coin to send",
4212+
"coinControlCustom": "Custom coin selection",
4213+
"quickActions": {
4214+
"quarter": "25%",
4215+
"half": "50%",
4216+
"threeQuarters": "75%",
4217+
"max": "Max"
4218+
},
4219+
"reviewCta": "Review",
4220+
"getCta": "Get {{currency}}",
4221+
"switchInputMode": "Switch amount input mode",
4222+
"aboveMaximum": "Above maximum balance",
4223+
"transactionSigned": "Transaction signed",
4224+
"processingTransaction": "Your transaction is being processed",
4225+
"sign": {
4226+
"title": "Continue on your Ledger {{wording}}",
4227+
"description": "Follow the instructions displayed on your Secure Touchscreen."
4228+
},
4229+
"gasOptionsSync": {
4230+
"warningTitle": "Unable to refresh network fees",
4231+
"warningDescription": "Fee presets may be outdated."
4232+
}
4233+
},
41594234
"pendingTxWarning": "You have incoming transactions pending. Please beware, pending transactions can be reversed.",
41604235
"tooMuchUTXOBottomModal": {
41614236
"cta": "Continue",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from "react";
2+
3+
import { SendFlowLayoutView } from "./SendFlowLayoutView";
4+
import type { SendFlowLayoutProps } from "./types";
5+
6+
export function SendFlowLayout(props: SendFlowLayoutProps) {
7+
return <SendFlowLayoutView {...props} />;
8+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from "react";
2+
import { View, ScrollView } from "react-native";
3+
import { SafeAreaView } from "react-native-safe-area-context";
4+
import { useStyleSheet } from "@ledgerhq/lumen-ui-rnative/styles";
5+
6+
import type { SendFlowLayoutProps } from "./types";
7+
import { SendHeader } from "./SendHeader";
8+
9+
export function SendFlowLayoutView({ headerRight, headerContent, children }: SendFlowLayoutProps) {
10+
const styles = useStyleSheet(
11+
theme => ({
12+
container: {
13+
flex: 1,
14+
backgroundColor: theme.colors.bg.base,
15+
},
16+
headerContent: {
17+
marginTop: theme.spacings.s12,
18+
paddingHorizontal: theme.spacings.s16,
19+
},
20+
bodyContent: {
21+
padding: theme.spacings.s16,
22+
flexGrow: 1,
23+
},
24+
}),
25+
[],
26+
);
27+
28+
return (
29+
<SafeAreaView style={styles.container} edges={["top"]}>
30+
<SendHeader headerRight={headerRight} />
31+
{headerContent ? <View style={styles.headerContent}>{headerContent}</View> : null}
32+
<ScrollView contentContainerStyle={styles.bodyContent}>{children}</ScrollView>
33+
</SafeAreaView>
34+
);
35+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React from "react";
2+
import { View, Pressable } from "react-native";
3+
import {
4+
AddressInput,
5+
IconButton,
6+
NavBar,
7+
NavBarBackButton,
8+
NavBarContent,
9+
NavBarDescription,
10+
NavBarTitle,
11+
NavBarTrailing,
12+
} from "@ledgerhq/lumen-ui-rnative";
13+
import { useStyleSheet } from "@ledgerhq/lumen-ui-rnative/styles";
14+
import { Close } from "@ledgerhq/lumen-ui-rnative/symbols";
15+
16+
import { useTranslation } from "~/context/Locale";
17+
18+
import { useSendHeaderViewModel } from "../hooks/useSendHeaderViewModel";
19+
20+
type SendHeaderProps = Readonly<{
21+
headerRight?: React.ReactNode;
22+
}>;
23+
24+
export function SendHeader({ headerRight }: SendHeaderProps) {
25+
const { t } = useTranslation();
26+
const viewModel = useSendHeaderViewModel();
27+
const styles = useStyleSheet(
28+
theme => ({
29+
addressInputContainer: {
30+
marginTop: theme.spacings.s8,
31+
paddingHorizontal: theme.spacings.s16,
32+
position: "relative" as const,
33+
},
34+
absoluteOverlay: {
35+
position: "absolute" as const,
36+
top: 0,
37+
left: 0,
38+
right: 0,
39+
bottom: 0,
40+
},
41+
}),
42+
[],
43+
);
44+
45+
return (
46+
<>
47+
<NavBar appearance="compact">
48+
{viewModel.canGoBack ? (
49+
<NavBarBackButton
50+
onPress={viewModel.handleBackPress}
51+
accessibilityLabel={t("common.back")}
52+
/>
53+
) : null}
54+
<NavBarContent>
55+
{viewModel.showTitle && viewModel.title ? (
56+
<NavBarTitle>{viewModel.title}</NavBarTitle>
57+
) : null}
58+
{viewModel.descriptionText ? (
59+
<NavBarDescription>{viewModel.descriptionText}</NavBarDescription>
60+
) : null}
61+
</NavBarContent>
62+
{viewModel.showHeaderRight && (
63+
<NavBarTrailing>
64+
{headerRight ?? (
65+
<IconButton
66+
appearance="no-background"
67+
size="md"
68+
icon={Close}
69+
accessibilityLabel={t("common.close")}
70+
onPress={viewModel.handleClose}
71+
/>
72+
)}
73+
</NavBarTrailing>
74+
)}
75+
</NavBar>
76+
{viewModel.showRecipientInput ? (
77+
<View style={styles.addressInputContainer}>
78+
{viewModel.isRecipientStep ? (
79+
<AddressInput
80+
value={viewModel.recipientSearchValue}
81+
onChangeText={viewModel.setRecipientSearchValue}
82+
onClear={viewModel.clearRecipientSearch}
83+
onQrCodeClick={viewModel.handleQrCodeClick}
84+
placeholder={viewModel.recipientPlaceholder}
85+
/>
86+
) : (
87+
<>
88+
<AddressInput
89+
value={viewModel.formattedAddress}
90+
editable={false}
91+
hideClearButton
92+
placeholder={viewModel.recipientPlaceholder}
93+
/>
94+
<Pressable
95+
style={styles.absoluteOverlay}
96+
accessibilityRole="button"
97+
accessibilityLabel={t("send.newSendFlow.editRecipientAccessibilityLabel")}
98+
onPress={viewModel.handleRecipientInputPress}
99+
/>
100+
</>
101+
)}
102+
</View>
103+
) : null}
104+
</>
105+
);
106+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type React from "react";
2+
3+
export type SendFlowLayoutProps = Readonly<{
4+
headerRight?: React.ReactNode;
5+
headerContent?: React.ReactNode;
6+
children: React.ReactNode;
7+
}>;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { SEND_FLOW_STEP, type SendFlowStep } from "@ledgerhq/live-common/flows/send/types";
2+
import type { SendStepConfig, SendFlowConfig } from "./types";
3+
import { ScreenName } from "~/const";
4+
import TransparentHeaderNavigationOptions from "~/navigation/TransparentHeaderNavigationOptions";
5+
6+
export const SEND_FLOW_STEP_ORDER: readonly SendFlowStep[] = [
7+
SEND_FLOW_STEP.RECIPIENT,
8+
SEND_FLOW_STEP.AMOUNT,
9+
SEND_FLOW_STEP.SIGNATURE,
10+
SEND_FLOW_STEP.CONFIRMATION,
11+
];
12+
13+
export const SEND_STEP_CONFIGS: Record<SendFlowStep, SendStepConfig> = {
14+
[SEND_FLOW_STEP.RECIPIENT]: {
15+
id: SEND_FLOW_STEP.RECIPIENT,
16+
canGoBack: true,
17+
addressInput: true,
18+
screenName: ScreenName.SendFlowRecipient,
19+
showHeaderRight: false,
20+
screenOptions: {
21+
...TransparentHeaderNavigationOptions,
22+
title: "",
23+
},
24+
},
25+
[SEND_FLOW_STEP.AMOUNT]: {
26+
id: SEND_FLOW_STEP.AMOUNT,
27+
canGoBack: true,
28+
addressInput: true,
29+
screenName: ScreenName.SendFlowAmount,
30+
showHeaderRight: false,
31+
screenOptions: {
32+
...TransparentHeaderNavigationOptions,
33+
title: "",
34+
},
35+
},
36+
[SEND_FLOW_STEP.SIGNATURE]: {
37+
id: SEND_FLOW_STEP.SIGNATURE,
38+
canGoBack: false,
39+
showTitle: false,
40+
showHeaderRight: false,
41+
screenName: ScreenName.SendFlowSignature,
42+
screenOptions: {
43+
...TransparentHeaderNavigationOptions,
44+
title: "",
45+
gestureEnabled: false,
46+
},
47+
},
48+
[SEND_FLOW_STEP.CONFIRMATION]: {
49+
id: SEND_FLOW_STEP.CONFIRMATION,
50+
canGoBack: true,
51+
showTitle: false,
52+
screenName: ScreenName.SendFlowConfirmation,
53+
screenOptions: {
54+
...TransparentHeaderNavigationOptions,
55+
title: "",
56+
},
57+
},
58+
};
59+
60+
export const SEND_FLOW_CONFIG: SendFlowConfig = {
61+
stepOrder: SEND_FLOW_STEP_ORDER,
62+
stepConfigs: SEND_STEP_CONFIGS,
63+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useMemo } from "react";
2+
import { useSelector } from "~/context/hooks";
3+
import { counterValueCurrencySelector } from "~/reducers/settings";
4+
import { useMaybeAccountUnit } from "LLM/hooks/useAccountUnit";
5+
import { getAccountCurrency } from "@ledgerhq/live-common/account/index";
6+
import { formatCurrencyUnit } from "@ledgerhq/live-common/currencies/index";
7+
import { useCalculate } from "@ledgerhq/live-countervalues-react";
8+
import { AccountLike } from "@ledgerhq/types-live";
9+
import { BigNumber } from "bignumber.js";
10+
import { useLocale } from "~/context/Locale";
11+
12+
export function useAvailableBalance(account?: AccountLike | null) {
13+
const { locale } = useLocale();
14+
const counterValueCurrency = useSelector(counterValueCurrencySelector);
15+
const unit = useMaybeAccountUnit(account ?? undefined);
16+
17+
const accountCurrency = useMemo(
18+
() => (account ? getAccountCurrency(account) : undefined),
19+
[account],
20+
);
21+
22+
const counterValue = useCalculate({
23+
from: accountCurrency ?? counterValueCurrency,
24+
to: counterValueCurrency,
25+
value: account?.balance.toNumber() ?? 0,
26+
disableRounding: true,
27+
});
28+
29+
const availableBalanceFormatted = useMemo(() => {
30+
if (!account || !unit) return "";
31+
return formatCurrencyUnit(unit, account.balance, {
32+
showCode: true,
33+
locale,
34+
});
35+
}, [account, unit, locale]);
36+
37+
const counterValueFormatted = useMemo(() => {
38+
if (typeof counterValue !== "number" || !counterValueCurrency) return "";
39+
return formatCurrencyUnit(counterValueCurrency.units[0], new BigNumber(counterValue), {
40+
showCode: true,
41+
locale,
42+
});
43+
}, [counterValue, counterValueCurrency, locale]);
44+
45+
return useMemo(() => {
46+
if (!account) return "";
47+
return counterValueFormatted || availableBalanceFormatted || "";
48+
}, [account, counterValueFormatted, availableBalanceFormatted]);
49+
}

0 commit comments

Comments
 (0)