Skip to content

Commit 7cf01df

Browse files
committed
feat(LIVE-29447): add mobile swap status drawer
1 parent e1bebc0 commit 7cf01df

21 files changed

Lines changed: 1051 additions & 13 deletions

apps/ledger-live-mobile/src/GlobalDrawers/registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import ReceiveDrawerWrapper from "LLM/features/Receive/drawers/ReceiveFundsOptio
44
import RebornBuyDeviceDrawer from "LLM/features/Reborn/drawers/RebornBuyDeviceDrawer";
55
import { DeeplinkInstallAppDrawer } from "LLM/features/DeeplinkInstallApp";
66
import { NotificationsPromptWrapper } from "LLM/features/NotificationsPrompt";
7+
import { GenericAwarenessModalDrawer } from "LLM/features/GenericAwarenessModal/screens/GenericAwarenessModalDrawer";
8+
import { SwapTransactionStatusDrawerWrapper } from "LLM/features/SwapTransactionStatus/SwapTransactionStatusDrawerWrapper";
79

810
/**
911
* Registry of all global drawers in the application.
@@ -33,6 +35,12 @@ export const DRAWER_REGISTRY = {
3335
notificationsPrompt: {
3436
component: NotificationsPromptWrapper,
3537
},
38+
genericAwarenessModal: {
39+
component: GenericAwarenessModalDrawer,
40+
},
41+
transactionStatus: {
42+
component: SwapTransactionStatusDrawerWrapper,
43+
},
3644
} as const satisfies Record<string, DrawerRegistryEntry>;
3745

3846
/**

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4117,6 +4117,42 @@
41174117
"history": {
41184118
"title": "Swap history"
41194119
},
4120+
"modals": {
4121+
"transactionStatus": {
4122+
"title": "Swap {{sendTicker}} → {{receiveTicker}}",
4123+
"sections": {
4124+
"status": {
4125+
"heading": "Status",
4126+
"sendPending": "Sending {{ticker}}",
4127+
"sendCompleted": "Sent {{ticker}}",
4128+
"receivePending": "Receiving {{ticker}}",
4129+
"receiveCompleted": "Received {{ticker}}"
4130+
},
4131+
"details": {
4132+
"networkFees": "Network fees",
4133+
"receiveAccount": "Receive account",
4134+
"provider": "Provider",
4135+
"swapId": "Swap ID"
4136+
}
4137+
},
4138+
"statusLabels": {
4139+
"pending": "Pending",
4140+
"onhold": "On hold",
4141+
"expired": "Expired",
4142+
"finished": "Completed",
4143+
"refunded": "Refunded",
4144+
"unknown": "Unknown"
4145+
},
4146+
"actions": {
4147+
"viewInExplorer": "View in explorer",
4148+
"copied": "Copied"
4149+
},
4150+
"accessibility": {
4151+
"close": "Close",
4152+
"copySwapId": "Copy swap ID"
4153+
}
4154+
}
4155+
},
41204156
"wrongDevice": {
41214157
"title": "Ledger Nano S™ is not compatible with {{provider}}",
41224158
"description": "Ledger Nano S™ is not compatible with {{provider}}. You can use Ledger Nano S Plus™, Ledger Nano X™, Ledger Stax™, or Ledger Flex™ to experience cross-chain swaps on {{provider}} via Ledger Wallet",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
import { Modal } from "react-native";
3+
import Clipboard from "@react-native-clipboard/clipboard";
4+
import { Box, Pressable, Text } from "@ledgerhq/lumen-ui-rnative";
5+
import { Copy } from "@ledgerhq/lumen-ui-rnative/symbols";
6+
import { useTranslation } from "~/context/Locale";
7+
8+
type CopyIconButtonProps = {
9+
text: string;
10+
};
11+
12+
export function CopyIconButton({ text }: CopyIconButtonProps) {
13+
const { t } = useTranslation();
14+
const [isCopied, setIsCopied] = useState(false);
15+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
16+
17+
useEffect(() => {
18+
return () => {
19+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
20+
};
21+
}, []);
22+
23+
const copyToClipboard = () => {
24+
Clipboard.setString(text);
25+
setIsCopied(true);
26+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
27+
timeoutRef.current = setTimeout(() => setIsCopied(false), 1_000);
28+
};
29+
30+
return (
31+
<>
32+
<Pressable
33+
onPress={copyToClipboard}
34+
accessibilityRole="button"
35+
accessibilityLabel={t("transfer.swap2.modals.transactionStatus.accessibility.copySwapId")}
36+
>
37+
<Copy size={16} color="base" />
38+
</Pressable>
39+
<Modal transparent visible={isCopied} animationType="fade">
40+
<Box lx={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
41+
<Box
42+
lx={{
43+
backgroundColor: "black",
44+
borderRadius: "sm",
45+
paddingHorizontal: "s16",
46+
paddingVertical: "s8",
47+
}}
48+
>
49+
<Text typography="body2SemiBold" lx={{ color: "white" }}>
50+
{t("transfer.swap2.modals.transactionStatus.actions.copied")}
51+
</Text>
52+
</Box>
53+
</Box>
54+
</Modal>
55+
</>
56+
);
57+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from "react";
2+
import { Box, Text } from "@ledgerhq/lumen-ui-rnative";
3+
4+
type DetailRowProps = {
5+
label: string;
6+
value: React.ReactNode;
7+
};
8+
9+
export function DetailRow({ label, value }: DetailRowProps) {
10+
return (
11+
<Box
12+
lx={{
13+
flexDirection: "row",
14+
alignItems: "center",
15+
justifyContent: "space-between",
16+
gap: "s16",
17+
}}
18+
>
19+
<Text typography="body3" lx={{ color: "muted" }}>
20+
{label}
21+
</Text>
22+
{typeof value === "string" ? (
23+
<Text typography="body3" lx={{ color: "base", textAlign: "right", flexShrink: 1 }}>
24+
{value}
25+
</Text>
26+
) : (
27+
value
28+
)}
29+
</Box>
30+
);
31+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React from "react";
2+
import { Linking } from "react-native";
3+
import { getProviderName } from "@ledgerhq/live-common/exchange/swap/utils/index";
4+
import type { AdditionalProviderConfig } from "@ledgerhq/live-common/exchange/providers/swap";
5+
import type { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets";
6+
import { Box, Pressable, Skeleton, Text } from "@ledgerhq/lumen-ui-rnative";
7+
import CurrencyIcon from "~/components/CurrencyIcon";
8+
import { useTranslation } from "~/context/Locale";
9+
import { CopyIconButton } from "./CopyIconButton";
10+
import { DetailRow } from "./DetailRow";
11+
import { ProviderIcon } from "./ProviderIcon";
12+
import { truncateMiddle } from "../utils";
13+
14+
type DetailsSectionProps = {
15+
feesAmount?: string;
16+
receiveAccountName?: string;
17+
provider?: string;
18+
providerData?: AdditionalProviderConfig;
19+
receiveCurrency?: CryptoOrTokenCurrency;
20+
swapId: string;
21+
};
22+
23+
export function DetailsSection({
24+
feesAmount,
25+
receiveAccountName,
26+
provider,
27+
providerData,
28+
receiveCurrency,
29+
swapId,
30+
}: DetailsSectionProps) {
31+
const { t } = useTranslation();
32+
const providerName = provider ? getProviderName(provider) : undefined;
33+
34+
return (
35+
<Box lx={{ gap: "s12" }}>
36+
<DetailRow
37+
label={t("transfer.swap2.modals.transactionStatus.sections.details.networkFees")}
38+
value={feesAmount ?? <Skeleton lx={{ height: "s16", width: "s96" }} />}
39+
/>
40+
<DetailRow
41+
label={t("transfer.swap2.modals.transactionStatus.sections.details.receiveAccount")}
42+
value={
43+
receiveAccountName ? (
44+
<Box lx={{ flexDirection: "row", alignItems: "center", gap: "s6", flexShrink: 1 }}>
45+
<Text typography="body3" lx={{ color: "base", textAlign: "right", flexShrink: 1 }}>
46+
{receiveAccountName}
47+
</Text>
48+
{receiveCurrency ? <CurrencyIcon currency={receiveCurrency} size={16} /> : null}
49+
</Box>
50+
) : (
51+
<Skeleton lx={{ height: "s16", width: "s112" }} />
52+
)
53+
}
54+
/>
55+
{provider && providerName ? (
56+
<DetailRow
57+
label={t("transfer.swap2.modals.transactionStatus.sections.details.provider")}
58+
value={
59+
providerData?.mainUrl ? (
60+
<Pressable
61+
onPress={() => Linking.openURL(providerData.mainUrl!).catch(() => {})}
62+
accessibilityRole="link"
63+
>
64+
<ProviderValue provider={provider} providerName={providerName} />
65+
</Pressable>
66+
) : (
67+
<ProviderValue provider={provider} providerName={providerName} />
68+
)
69+
}
70+
/>
71+
) : null}
72+
<DetailRow
73+
label={t("transfer.swap2.modals.transactionStatus.sections.details.swapId")}
74+
value={
75+
<Box lx={{ flexDirection: "row", alignItems: "center", gap: "s6" }}>
76+
<Text typography="body3SemiBold" lx={{ color: "base" }}>
77+
{truncateMiddle(swapId)}
78+
</Text>
79+
<CopyIconButton text={swapId} />
80+
</Box>
81+
}
82+
/>
83+
</Box>
84+
);
85+
}
86+
87+
function ProviderValue({ provider, providerName }: { provider: string; providerName: string }) {
88+
return (
89+
<Box lx={{ flexDirection: "row", alignItems: "center", gap: "s6" }}>
90+
<Text typography="body3SemiBold" lx={{ color: "base", textAlign: "right" }}>
91+
{providerName}
92+
</Text>
93+
<ProviderIcon name={provider} />
94+
</Box>
95+
);
96+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from "react";
2+
import { SvgUri } from "react-native-svg";
3+
import { getProviderIconUrl } from "@ledgerhq/live-common/icons/providers/providers";
4+
5+
type ProviderIconProps = {
6+
name: string;
7+
size?: number;
8+
};
9+
10+
export function ProviderIcon({ name, size = 16 }: ProviderIconProps) {
11+
return <SvgUri width={size} height={size} uri={getProviderIconUrl({ name, boxed: false })} />;
12+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from "react";
2+
import { Box } from "@ledgerhq/lumen-ui-rnative";
3+
4+
type StatusLineProps = {
5+
status: "success" | "pending" | "error" | "unknown";
6+
};
7+
8+
export function StatusLine({ status }: StatusLineProps) {
9+
return (
10+
<Box
11+
lx={{
12+
backgroundColor:
13+
status === "success" ? "successStrong" : status === "error" ? "errorStrong" : "muted",
14+
borderRadius: "full",
15+
height: "s32",
16+
width: "s4",
17+
marginTop: "s4",
18+
}}
19+
/>
20+
);
21+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from "react";
2+
import { Box, Text } from "@ledgerhq/lumen-ui-rnative";
3+
import { CheckmarkCircleFill, Clock, Warning } from "@ledgerhq/lumen-ui-rnative/symbols";
4+
import { StatusLine } from "./StatusLine";
5+
6+
type StatusRowProps = {
7+
status: "success" | "pending" | "error" | "unknown";
8+
title: string;
9+
subtitle: string;
10+
value: React.ReactNode;
11+
isLast?: boolean;
12+
};
13+
14+
export function StatusRow({ status, title, subtitle, value, isLast }: StatusRowProps) {
15+
return (
16+
<Box lx={{ flexDirection: "row", gap: "s12" }}>
17+
<Box lx={{ alignItems: "center", width: "s20" }}>
18+
{status === "success" ? (
19+
<CheckmarkCircleFill size={20} color="success" />
20+
) : status === "error" ? (
21+
<Warning size={20} color="error" />
22+
) : (
23+
<Clock size={20} color="muted" />
24+
)}
25+
{!isLast ? <StatusLine status={status} /> : null}
26+
</Box>
27+
<Box lx={{ flex: 1, gap: "s2" }}>
28+
<Box lx={{ flexDirection: "row", justifyContent: "space-between", gap: "s12" }}>
29+
<Text typography="body2SemiBold" lx={{ color: "base", flexShrink: 1 }}>
30+
{title}
31+
</Text>
32+
{typeof value === "string" ? (
33+
<Text typography="body2SemiBold" lx={{ color: "base", textAlign: "right" }}>
34+
{value}
35+
</Text>
36+
) : (
37+
value
38+
)}
39+
</Box>
40+
<Text
41+
typography="body3"
42+
lx={{
43+
color: status === "success" ? "success" : status === "error" ? "error" : "muted",
44+
}}
45+
>
46+
{subtitle}
47+
</Text>
48+
</Box>
49+
</Box>
50+
);
51+
}

0 commit comments

Comments
 (0)