From 52d07d27ea27b1e342230d7ac48c46dfc41898f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:36:32 +0200 Subject: [PATCH] fix: improve linking (#129) * fix: improve linking * fix: improve linking * fix: cleanup * fix: wip * fix: receiver component * fix: loading state for qr scanner * fix: cleanup * fix: lowercase payment info * fix: improve handle linking hook * fix: reset navigation stack * fix: handle linking * fix: comment * fix: add exp scheme only for dev builds * fix: remove console log --- app/_layout.tsx | 3 + components/FocusableCamera.tsx | 1 - components/QRCodeScanner.tsx | 35 ++++---- components/Receiver.tsx | 29 +++++++ hooks/useHandleLinking.ts | 93 ++++++++++++++++----- pages/Wildcard.tsx | 13 ++- pages/send/ConfirmPayment.tsx | 19 +---- pages/send/LNURLPay.tsx | 31 +++---- pages/send/PaymentSuccess.tsx | 12 +-- pages/send/Send.tsx | 32 ++++++- pages/settings/wallets/WalletConnection.tsx | 16 +--- 11 files changed, 176 insertions(+), 108 deletions(-) create mode 100644 components/Receiver.tsx diff --git a/app/_layout.tsx b/app/_layout.tsx index 7cdc74f..963d573 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -26,6 +26,7 @@ import { usePathname } from "expo-router"; import { UserInactivityProvider } from "~/context/UserInactivity"; import { PortalHost } from '@rn-primitives/portal'; import { isBiometricSupported } from "~/lib/isBiometricSupported"; +import { useHandleLinking } from "~/hooks/useHandleLinking"; const LIGHT_THEME: Theme = { dark: false, @@ -54,6 +55,8 @@ export default function RootLayout() { const [checkedOnboarding, setCheckedOnboarding] = React.useState(false); const isUnlocked = useAppStore((store) => store.unlocked); const pathname = usePathname(); + + useHandleLinking(); useConnectionChecker(); const rootNavigationState = useRootNavigationState(); diff --git a/components/FocusableCamera.tsx b/components/FocusableCamera.tsx index 8407cbd..6721156 100644 --- a/components/FocusableCamera.tsx +++ b/components/FocusableCamera.tsx @@ -19,7 +19,6 @@ export function FocusableCamera({ onScanned }: FocusableCameraProps) { const handleBarCodeScanned = ({ data }: BarcodeScanningResult) => { onScanned(data); }; - return ( void; -}; + startScanning: boolean; +} -function QRCodeScanner({ onScanned }: QRCodeScannerProps) { - const [isScanning, setScanning] = React.useState(false); +function QRCodeScanner({ onScanned, startScanning = true }: QRCodeScannerProps) { + const [isScanning, setScanning] = React.useState(startScanning); const [isLoading, setLoading] = React.useState(false); const [permissionStatus, setPermissionStatus] = React.useState(PermissionStatus.UNDETERMINED); useEffect(() => { // Add some timeout to allow the screen transition to finish before // starting the camera to avoid stutters - setLoading(true); - window.setTimeout(async () => { - await scan(); - setLoading(false); - }, 200); - }, []); + if (startScanning) { + setLoading(true); + window.setTimeout(async () => { + await scan(); + setLoading(false); + }, 200); + } + }, [startScanning]); async function scan() { const { status } = await Camera.requestCameraPermissionsAsync(); @@ -44,10 +47,10 @@ function QRCodeScanner({ onScanned }: QRCodeScannerProps) { }; return ( - <> - {isLoading && ( + + {(isLoading || (!isScanning && permissionStatus === PermissionStatus.UNDETERMINED)) && ( - + )} {!isLoading && <> @@ -59,13 +62,11 @@ function QRCodeScanner({ onScanned }: QRCodeScannerProps) { } {isScanning && ( - <> - - + )} } - + ); } diff --git a/components/Receiver.tsx b/components/Receiver.tsx new file mode 100644 index 0000000..feb26c5 --- /dev/null +++ b/components/Receiver.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { View } from "react-native"; +import { Text } from "~/components/ui/text"; + +interface ReceiverProps { + originalText: string; + invoice?: string; +} + +export function Receiver({ originalText, invoice }: ReceiverProps) { + const shouldShowReceiver = + originalText !== invoice && + originalText.toLowerCase().replace("lightning:", "").includes("@"); + + if (!shouldShowReceiver) { + return null; + } + + return ( + + + To + + + {originalText.toLowerCase().replace("lightning:", "")} + + + ); +} \ No newline at end of file diff --git a/hooks/useHandleLinking.ts b/hooks/useHandleLinking.ts index 5cd823b..1a59438 100644 --- a/hooks/useHandleLinking.ts +++ b/hooks/useHandleLinking.ts @@ -1,38 +1,87 @@ import * as Linking from "expo-linking"; +import { getInitialURL } from "expo-linking"; import { router, useRootNavigationState } from "expo-router"; -import React from "react"; +import { useEffect, useCallback, useRef } from "react"; -const SUPPORTED_SCHEMES = ["lightning:", "bitcoin:", "alby:"]; +// TESTING: ["lightning:", "bitcoin:", "alby:", "exp:"] +const SUPPORTED_SCHEMES = ["lightning", "bitcoin", "alby"]; + +// Register exp scheme for testing during development +// https://docs.expo.dev/guides/linking/#creating-urls +if (process.env.NODE_ENV === "development") { + SUPPORTED_SCHEMES.push("exp"); +} export function useHandleLinking() { - const rootNavigationState = useRootNavigationState(); - let url = Linking.useURL(); - let hasNavigationState = !!rootNavigationState?.key; + const navigationState = useRootNavigationState(); + const pendingLinkRef = useRef(null); - React.useEffect(() => { - if (!hasNavigationState) { - return; - } - console.log("Received linking URL", url); + const handleLink = useCallback( + (url: string) => { + if (!url) return; + + const { hostname, path, queryParams, scheme } = Linking.parse(url); - for (const scheme of SUPPORTED_SCHEMES) { - if (url?.startsWith(scheme)) { - console.log("Linking URL matched scheme", url, scheme); - if (url.startsWith(scheme + "//")) { - url = url.replace(scheme + "//", scheme); + if (!scheme) return; + + if (SUPPORTED_SCHEMES.indexOf(scheme) > -1) { + let fullUrl = scheme === "exp" ? path : `${scheme}:${hostname}`; + + // Add query parameters to the URL if they exist + if (queryParams && Object.keys(queryParams).length > 0) { + const queryString = new URLSearchParams( + queryParams as Record, + ).toString(); + fullUrl += `?${queryString}`; } - // TODO: it should not always navigate to send, - // but that's the only linking functionality supported right now - router.dismissAll(); - router.navigate({ + if (router.canDismiss()) { + router.dismissAll(); + } + router.push({ pathname: "/send", params: { - url, + url: fullUrl, }, }); - break; + return; } + + // Redirect the user to the home screen + // if no match was found + router.replace({ + pathname: "/", + }); + }, + [navigationState?.key], + ); + + useEffect(() => { + const processInitialURL = async () => { + const url = await getInitialURL(); + if (url) pendingLinkRef.current = url; + }; + + processInitialURL(); + + const subscription = Linking.addEventListener( + "url", + (event: { url: string }) => { + if (navigationState?.key) { + handleLink(event.url); + } else { + pendingLinkRef.current = event.url; + } + }, + ); + + return () => subscription.remove(); + }, [handleLink]); + + useEffect(() => { + if (navigationState?.key && pendingLinkRef.current) { + handleLink(pendingLinkRef.current); + pendingLinkRef.current = null; } - }, [url, hasNavigationState]); + }, [navigationState?.key, handleLink]); } diff --git a/pages/Wildcard.tsx b/pages/Wildcard.tsx index 3cf5a18..6dc23d7 100644 --- a/pages/Wildcard.tsx +++ b/pages/Wildcard.tsx @@ -1,12 +1,17 @@ -import { Stack, usePathname } from "expo-router"; +import { router, Stack, useFocusEffect, usePathname } from "expo-router"; import { View } from "react-native"; import Loading from "~/components/Loading"; import { Text } from "~/components/ui/text"; -import { useHandleLinking } from "~/hooks/useHandleLinking"; export function Wildcard() { const pathname = usePathname(); - useHandleLinking(); + + // Should a user ever land on this page, redirect them to home + useFocusEffect(() => { + router.replace({ + pathname: "/" + }); + }); return ( @@ -17,7 +22,7 @@ export function Wildcard() { }} /> - Loading {pathname} + Loading ); } diff --git a/pages/send/ConfirmPayment.tsx b/pages/send/ConfirmPayment.tsx index 098730a..7e4642d 100644 --- a/pages/send/ConfirmPayment.tsx +++ b/pages/send/ConfirmPayment.tsx @@ -5,6 +5,7 @@ import React from "react"; import { View } from "react-native"; import { ZapIcon } from "~/components/Icons"; import Loading from "~/components/Loading"; +import { Receiver } from "~/components/Receiver"; import Screen from "~/components/Screen"; import { Button } from "~/components/ui/button"; import { Text } from "~/components/ui/text"; @@ -94,23 +95,7 @@ export function ConfirmPayment() { ) )} - { - /* only show "To" for lightning addresses */ originalText !== - invoice && - originalText - .toLowerCase() - .replace("lightning:", "") - .includes("@") && ( - - - To - - - {originalText.toLowerCase().replace("lightning:", "")} - - - ) - } +