1- import React , { useCallback , useState } from "react" ;
1+ import React , { useCallback , useEffect , useRef , useState } from "react" ;
22import { useTranslation } from "react-i18next" ;
33import { Linking , StyleSheet } from "react-native" ;
44import { useSafeAreaInsets } from "react-native-safe-area-context" ;
55
66import { CameraView , useCameraPermissions } from "expo-camera" ;
7- import { useFocusEffect , useRouter } from "expo-router" ;
7+ import { useFocusEffect , useLocalSearchParams , useRouter } from "expo-router" ;
88
99import { ArrowLeft , BoxSelect , SwitchCamera } from "@tamagui/lucide-icons" ;
1010import { 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" ;
1720import reportError from "../../utils/reportError" ;
1821import IconButton from "../shared/IconButton" ;
1922import 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 ( / ^ p a y - / , "" ) . split ( "@" ) [ 0 ] ?? "" ;
234+ return action === "transfer" ? ( new URLSearchParams ( query ) . get ( "address" ) ?? "" ) : "" ;
235+ }
236+
192237const styles = StyleSheet . create ( { cameraView : { flex : 1 } } ) ;
0 commit comments