Skip to content

Commit ef3133e

Browse files
committed
✨ app: redesign send receiver screen
1 parent d2f875f commit ef3133e

14 files changed

Lines changed: 305 additions & 327 deletions

File tree

.changeset/red-pandas-delete.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@exactly/mobile": patch
3+
---
4+
5+
🚧 delete recent send contacts

src/components/send-funds/Contact.tsx

Lines changed: 0 additions & 61 deletions
This file was deleted.

src/components/send-funds/Contacts.tsx

Lines changed: 0 additions & 43 deletions
This file was deleted.

src/components/send-funds/QR.tsx

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1-
import React, { useCallback, useState } from "react";
1+
import React, { useCallback, useEffect, useRef, useState } from "react";
22
import { useTranslation } from "react-i18next";
33
import { Linking, StyleSheet } from "react-native";
44
import { useSafeAreaInsets } from "react-native-safe-area-context";
55

66
import { CameraView, useCameraPermissions } from "expo-camera";
7-
import { useFocusEffect, useRouter } from "expo-router";
7+
import { useFocusEffect, useLocalSearchParams, useRouter } from "expo-router";
88

99
import { ArrowLeft, BoxSelect, SwitchCamera } from "@tamagui/lucide-icons";
1010
import { useWindowDimensions, XStack, YStack } from "tamagui";
1111

12-
import { parse, safeParse } from "valibot";
13-
import { zeroAddress } from "viem";
12+
import { ChainType } from "@lifi/sdk";
13+
import { useQuery } from "@tanstack/react-query";
14+
import { safeParse } from "valibot";
1415

15-
import { Address } from "@exactly/common/validation";
16+
import chain from "@exactly/common/generated/chain";
1617

18+
import { lifiChainsOptions } from "../../utils/lifi";
19+
import receiverSchema from "../../utils/receiverSchema";
1720
import reportError from "../../utils/reportError";
1821
import IconButton from "../shared/IconButton";
1922
import Button from "../shared/StyledButton";
@@ -25,20 +28,33 @@ export default function QR() {
2528
const { height, width } = useWindowDimensions();
2629

2730
const router = useRouter();
31+
const { asset, fromChain, toChain, toToken, amount, fromAmount } = useLocalSearchParams();
32+
const destination = typeof toChain === "string" ? Number(toChain) : chain.id;
33+
const { data: chains } = useQuery(lifiChainsOptions);
34+
const destinationChain = chains?.find((item) => item.id === destination);
35+
const chainType = destinationChain?.chainType ?? (destination === chain.id ? ChainType.EVM : undefined);
2836

2937
const [active, setActive] = useState(true);
38+
const [invalid, setInvalid] = useState(false);
39+
const handledRef = useRef(false);
3040
const [cameraFacing, setCameraFacing] = useState<"back" | "front">("back");
3141
const [permission, requestPermission] = useCameraPermissions();
3242
const { t } = useTranslation();
3343

3444
useFocusEffect(
3545
useCallback(() => {
46+
handledRef.current = false;
3647
setActive(true);
3748
return () => {
3849
setActive(false);
3950
};
4051
}, []),
4152
);
53+
useEffect(() => {
54+
if (!invalid) return;
55+
const timer = setTimeout(() => setInvalid(false), 2000);
56+
return () => clearTimeout(timer);
57+
}, [invalid]);
4258

4359
if (!permission) return <View fullScreen backgroundColor="$backgroundSoft" />;
4460
if (!permission.granted) {
@@ -62,7 +78,7 @@ export default function QR() {
6278
}}
6379
gap="$s2"
6480
>
65-
<ArrowLeft size={24} color="white" />
81+
<ArrowLeft size={24} color="$uiNeutralPrimary" />
6682
<Text headline>{t("Back")}</Text>
6783
</XStack>
6884
<View padded>
@@ -106,7 +122,7 @@ export default function QR() {
106122
}}
107123
gap="$s2"
108124
>
109-
<ArrowLeft size={24} color="white" />
125+
<ArrowLeft size={24} color="$uiNeutralPrimary" />
110126
<Text headline>{t("Back")}</Text>
111127
</XStack>
112128
<View padded>
@@ -141,11 +157,18 @@ export default function QR() {
141157
{active && (
142158
<CameraView
143159
barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
144-
onBarcodeScanned={({ data: receiver }) => {
145-
const result = safeParse(Address, receiver);
146-
if (!result.success) return;
147-
if (result.output === parse(Address, zeroAddress)) return;
148-
router.dismissTo({ pathname: "/send-funds/asset", params: { receiver: result.output } });
160+
onBarcodeScanned={({ data }) => {
161+
if (chainType === undefined || handledRef.current) return;
162+
const result = safeParse(receiverSchema(chainType), scanned(data));
163+
if (!result.success) {
164+
setInvalid(true);
165+
return;
166+
}
167+
handledRef.current = true;
168+
router.dismissTo({
169+
pathname: "/send-funds/receiver",
170+
params: { receiver: result.output, asset, fromChain, toChain, toToken, amount, fromAmount },
171+
});
149172
}}
150173
facing={cameraFacing}
151174
style={styles.cameraView}
@@ -154,6 +177,21 @@ export default function QR() {
154177
<View position="absolute" fullScreen justifyContent="center" alignItems="center">
155178
<BoxSelect size={Math.min(width, height) * 0.5} color="white" />
156179
</View>
180+
{invalid && (
181+
<View
182+
position="absolute"
183+
bottom={bottom + 72}
184+
alignSelf="center"
185+
backgroundColor="$interactiveBaseErrorDefault"
186+
borderRadius="$r3"
187+
paddingHorizontal="$s4"
188+
paddingVertical="$s3"
189+
>
190+
<Text emphasized footnote color="$interactiveOnBaseErrorDefault">
191+
{t("Invalid {{chain}} address", { chain: (destinationChain ?? chain).name })}
192+
</Text>
193+
</View>
194+
)}
157195
<Button
158196
primary
159197
minHeight="auto"
@@ -189,4 +227,11 @@ export default function QR() {
189227
);
190228
}
191229

230+
function scanned(data: string) {
231+
const [locator = "", query] = data.split("?");
232+
const [target = "", action] = locator.slice(locator.lastIndexOf(":") + 1).split("/");
233+
if (action === undefined) return target.replace(/^pay-/, "").split("@")[0] ?? "";
234+
return action === "transfer" ? (new URLSearchParams(query).get("address") ?? "") : "";
235+
}
236+
192237
const styles = StyleSheet.create({ cameraView: { flex: 1 } });

0 commit comments

Comments
 (0)