Skip to content

Commit 8c0bee5

Browse files
committed
refactor: canton offers mobile
1 parent d81ad17 commit 8c0bee5

19 files changed

+2028
-613
lines changed

.changeset/beige-readers-allow.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+
refactor canton offers mobile

apps/ledger-live-mobile/src/families/canton/PendingTransferProposals/DeviceAppModal.tsx

Lines changed: 32 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import React, { useCallback, useMemo, useState, useEffect, FC } from "react";
2-
import { useTranslation, Trans } from "~/context/Locale";
3-
import { useSelector } from "~/context/hooks";
4-
import { Flex, Text, InfiniteLoader, IconBox, IconsLegacy } from "@ledgerhq/native-ui";
1+
import { Flex, IconBox, IconsLegacy, InfiniteLoader, Text } from "@ledgerhq/native-ui";
2+
import React, { FC } from "react";
3+
import Button from "~/components/Button";
54
import DeviceActionModal from "~/components/DeviceActionModal";
6-
import { useAppDeviceAction } from "~/hooks/deviceActions";
7-
import QueuedDrawer from "~/components/QueuedDrawer";
85
import GenericErrorBottomModal from "~/components/GenericErrorBottomModal";
9-
import Button from "~/components/Button";
10-
import { AppResult } from "@ledgerhq/live-common/hw/actions/app";
11-
import { lastConnectedDeviceSelector } from "~/reducers/settings";
6+
import QueuedDrawer from "~/components/QueuedDrawer";
7+
import { Trans, useTranslation } from "~/context/Locale";
128
import { type TransferProposalAction } from "./types";
9+
import {
10+
useDeviceAppModalViewModel,
11+
type DeviceAppModalViewModel,
12+
} from "./useDeviceAppModalViewModel";
1313

1414
type Props = {
1515
isOpen: boolean;
@@ -19,8 +19,6 @@ type Props = {
1919
onClose?: () => void;
2020
};
2121

22-
type ConfirmationState = "pending" | "confirming" | "completed" | "error";
23-
2422
const translations = {
2523
title: {
2624
accept: "canton.pendingTransactions.deviceAppModal.success.accept.title",
@@ -34,61 +32,25 @@ const translations = {
3432
},
3533
};
3634

37-
const DeviceAppModal: FC<Props> = ({ isOpen, onConfirm, action, onClose, appName }) => {
38-
const { t } = useTranslation();
39-
const [confirmationState, setConfirmationState] = useState<ConfirmationState>("pending");
40-
const [error, setError] = useState<Error | null>(null);
41-
42-
const actionConnect = useAppDeviceAction();
43-
const device = useSelector(lastConnectedDeviceSelector);
44-
45-
const request = useMemo(
46-
() => ({
47-
appName,
48-
}),
49-
[appName],
50-
);
51-
52-
useEffect(() => {
53-
if (isOpen) {
54-
setConfirmationState("pending");
55-
setError(null);
56-
}
57-
}, [isOpen]);
58-
59-
const handleConfirm = useCallback(
60-
async (deviceId: string) => {
61-
try {
62-
setConfirmationState("confirming");
63-
await onConfirm(deviceId);
64-
setConfirmationState("completed");
65-
} catch (err) {
66-
setConfirmationState("error");
67-
setError(err instanceof Error ? err : new Error(String(err)));
68-
}
69-
},
70-
[onConfirm],
71-
);
72-
73-
const handleRetry = useCallback(() => {
74-
setConfirmationState("pending");
75-
setError(null);
76-
}, []);
77-
78-
const handleDeviceResult = useCallback(
79-
(deviceResult: AppResult) => {
80-
if (deviceResult?.device) {
81-
let deviceId = deviceResult.device.deviceId;
82-
83-
if (!deviceId || deviceId === "") {
84-
deviceId = deviceResult.device.wired ? "usb" : "ble";
85-
}
35+
type ViewProps = DeviceAppModalViewModel & {
36+
isOpen: boolean;
37+
action: TransferProposalAction;
38+
onClose?: () => void;
39+
};
8640

87-
handleConfirm(deviceId);
88-
}
89-
},
90-
[handleConfirm],
91-
);
41+
export function View({
42+
confirmationState,
43+
error,
44+
request,
45+
device,
46+
actionConnect,
47+
handleDeviceResult,
48+
handleRetry,
49+
isOpen,
50+
action,
51+
onClose,
52+
}: ViewProps) {
53+
const { t } = useTranslation();
9254

9355
if (confirmationState === "completed") {
9456
return (
@@ -158,6 +120,11 @@ const DeviceAppModal: FC<Props> = ({ isOpen, onConfirm, action, onClose, appName
158120
analyticsPropertyFlow="canton-pending"
159121
/>
160122
);
123+
}
124+
125+
const DeviceAppModal: FC<Props> = ({ isOpen, onConfirm, action, onClose, appName }) => {
126+
const viewModel = useDeviceAppModalViewModel({ isOpen, onConfirm, appName });
127+
return <View {...viewModel} isOpen={isOpen} action={action} onClose={onClose} />;
161128
};
162129

163130
export default DeviceAppModal;
Lines changed: 45 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,41 @@
1-
import React, { useCallback, useMemo } from "react";
2-
import { useTranslation, Trans } from "~/context/Locale";
3-
import { TouchableOpacity } from "react-native";
1+
import { Box, Button, Flex, Text } from "@ledgerhq/native-ui";
42
import { Account } from "@ledgerhq/types-live";
5-
import { BigNumber } from "bignumber.js";
6-
import { Flex, Text, Button, Box } from "@ledgerhq/native-ui";
7-
import Clipboard from "@react-native-clipboard/clipboard";
8-
import { useAccountUnit } from "LLM/hooks/useAccountUnit";
9-
import { CantonAccount } from "@ledgerhq/live-common/families/canton/types";
10-
import { isCantonAccount } from "@ledgerhq/coin-canton";
11-
import { useTimeRemaining } from "@ledgerhq/live-common/families/canton/react";
12-
import QueuedDrawer from "~/components/QueuedDrawer";
3+
import React from "react";
4+
import { TouchableOpacity } from "react-native";
135
import CurrencyUnitValue from "~/components/CurrencyUnitValue";
146
import FormatDate from "~/components/DateFormat/FormatDate";
15-
import { type TransferProposalAction } from "./types";
16-
17-
type PendingProposal = {
18-
contract_id: string;
19-
sender: string;
20-
receiver: string;
21-
amount: BigNumber;
22-
memo: string;
23-
expires_at_micros: number;
24-
isExpired: boolean;
25-
};
7+
import QueuedDrawer from "~/components/QueuedDrawer";
8+
import { Trans, useTranslation } from "~/context/Locale";
9+
import { type ProcessedProposal, type TransferProposalAction } from "./types";
10+
import {
11+
usePendingTransferProposalsDetailsViewModel,
12+
type PendingTransferProposalsDetailsViewModel,
13+
} from "./usePendingTransferProposalsDetailsViewModel";
2614

2715
type Props = {
2816
account: Account;
29-
parentAccount: Account;
30-
contractId: string;
17+
proposal: ProcessedProposal | null;
3118
onOpenModal: (contractId: string, action: TransferProposalAction) => void;
3219
isOpen: boolean;
3320
onClose?: () => void;
3421
};
3522

36-
const PendingTransferProposalsDetails: React.FC<Props> = ({
37-
account,
38-
parentAccount,
39-
contractId,
40-
onOpenModal,
23+
type ViewProps = PendingTransferProposalsDetailsViewModel & {
24+
proposal: ProcessedProposal | null;
25+
isOpen: boolean;
26+
onClose?: () => void;
27+
};
28+
29+
export function View({
30+
unit,
31+
timeRemaining,
32+
handleAction,
33+
handleCopy,
34+
proposal,
4135
isOpen,
4236
onClose,
43-
}) => {
37+
}: ViewProps) {
4438
const { t } = useTranslation();
45-
const unit = useAccountUnit(account);
46-
47-
const cantonAccount: CantonAccount | null = isCantonAccount(account) ? account : null;
48-
const proposal = useMemo<PendingProposal | null>(() => {
49-
const pendingTransferProposals = cantonAccount?.cantonResources?.pendingTransferProposals || [];
50-
const found = pendingTransferProposals.find(p => p.contract_id === contractId);
51-
if (!found) return null;
52-
53-
const now = Date.now();
54-
const isExpired = now > found.expires_at_micros / 1000;
55-
return {
56-
...found,
57-
isExpired,
58-
amount: new BigNumber(found.amount),
59-
};
60-
}, [cantonAccount, contractId]);
61-
62-
const handleAcceptOffer = useCallback(
63-
(contractId: string) => {
64-
onOpenModal(contractId, "accept");
65-
},
66-
[onOpenModal],
67-
);
68-
69-
const handleRejectOffer = useCallback(
70-
(contractId: string) => {
71-
onOpenModal(contractId, "reject");
72-
},
73-
[onOpenModal],
74-
);
75-
76-
const handleWithdrawOffer = useCallback(
77-
(contractId: string) => {
78-
onOpenModal(contractId, "withdraw");
79-
},
80-
[onOpenModal],
81-
);
82-
83-
const handleCopy = useCallback((text: string) => {
84-
Clipboard.setString(text);
85-
}, []);
86-
87-
const timeRemaining = useTimeRemaining(proposal?.expires_at_micros, proposal?.isExpired);
8839

8940
if (!proposal) {
9041
return (
@@ -96,7 +47,7 @@ const PendingTransferProposalsDetails: React.FC<Props> = ({
9647
);
9748
}
9849

99-
const isIncoming = proposal.sender !== parentAccount.xpub;
50+
const { isIncoming, isExpired } = proposal;
10051

10152
return (
10253
<QueuedDrawer
@@ -165,11 +116,11 @@ const PendingTransferProposalsDetails: React.FC<Props> = ({
165116
{t("canton.pendingTransactions.expiresAt")}
166117
</Text>
167118
<Text variant="body" color="neutral.c100">
168-
<FormatDate date={new Date(proposal.expires_at_micros / 1000)} />
119+
<FormatDate date={proposal.expiresAt} />
169120
</Text>
170121
</Flex>
171122

172-
{!proposal.isExpired && timeRemaining && (
123+
{!isExpired && timeRemaining && (
173124
<Flex mb={8}>
174125
<Text variant="paragraph" color="neutral.c70" mb={2}>
175126
{t("canton.pendingTransactions.expiresIn")}
@@ -180,7 +131,7 @@ const PendingTransferProposalsDetails: React.FC<Props> = ({
180131
</Flex>
181132
)}
182133

183-
{proposal.isExpired && (
134+
{isExpired && (
184135
<Flex mb={8}>
185136
<Text variant="paragraph" color="neutral.c70" mb={2}>
186137
{t("canton.pendingTransactions.status.label")}
@@ -195,20 +146,20 @@ const PendingTransferProposalsDetails: React.FC<Props> = ({
195146
<Text variant="paragraph" color="neutral.c70" mb={2}>
196147
{t("canton.pendingTransactions.deviceAppModal.contractId")}
197148
</Text>
198-
<TouchableOpacity onPress={() => handleCopy(proposal.contract_id)}>
149+
<TouchableOpacity onPress={() => handleCopy(proposal.contractId)}>
199150
<Text variant="body" color="neutral.c100" numberOfLines={1} ellipsizeMode="middle">
200-
{proposal.contract_id}
151+
{proposal.contractId}
201152
</Text>
202153
</TouchableOpacity>
203154
</Flex>
204155

205156
<Flex flexDirection="row" mt={4} justifyContent="center" columnGap={8}>
206157
{isIncoming ? (
207158
<>
208-
{!proposal.isExpired && (
159+
{!isExpired && (
209160
<Button
210161
type="shade"
211-
onPress={() => handleAcceptOffer(proposal.contract_id)}
162+
onPress={() => handleAction("accept")}
212163
iconName="CheckAlone"
213164
flex={1}
214165
>
@@ -218,7 +169,7 @@ const PendingTransferProposalsDetails: React.FC<Props> = ({
218169
<Button
219170
outline
220171
type="main"
221-
onPress={() => handleRejectOffer(proposal.contract_id)}
172+
onPress={() => handleAction("reject")}
222173
iconName="Close"
223174
flex={1}
224175
>
@@ -230,7 +181,7 @@ const PendingTransferProposalsDetails: React.FC<Props> = ({
230181
outline
231182
type="main"
232183
iconName="Close"
233-
onPress={() => handleWithdrawOffer(proposal.contract_id)}
184+
onPress={() => handleAction("withdraw")}
234185
flex={1}
235186
>
236187
<Trans i18nKey="canton.pendingTransactions.withdraw" />
@@ -240,6 +191,17 @@ const PendingTransferProposalsDetails: React.FC<Props> = ({
240191
</Flex>
241192
</QueuedDrawer>
242193
);
194+
}
195+
196+
const PendingTransferProposalsDetails: React.FC<Props> = ({
197+
account,
198+
proposal,
199+
onOpenModal,
200+
isOpen,
201+
onClose,
202+
}) => {
203+
const viewModel = usePendingTransferProposalsDetailsViewModel({ account, proposal, onOpenModal });
204+
return <View {...viewModel} proposal={proposal} isOpen={isOpen} onClose={onClose} />;
243205
};
244206

245207
export default PendingTransferProposalsDetails;

0 commit comments

Comments
 (0)