Skip to content

Commit

Permalink
fix: improve linking (#129)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
reneaaron authored Sep 27, 2024
1 parent 25108f4 commit 52d07d2
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 108 deletions.
3 changes: 3 additions & 0 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
1 change: 0 additions & 1 deletion components/FocusableCamera.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export function FocusableCamera({ onScanned }: FocusableCameraProps) {
const handleBarCodeScanned = ({ data }: BarcodeScanningResult) => {
onScanned(data);
};

return (
<CameraView
onBarcodeScanned={handleBarCodeScanned}
Expand Down
35 changes: 18 additions & 17 deletions components/QRCodeScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,27 @@ import { Camera } from "expo-camera";
import { Text } from "~/components/ui/text";
import { CameraOff } from "./Icons";

type QRCodeScannerProps = {
interface QRCodeScannerProps {
onScanned: (data: string) => 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();
Expand All @@ -44,10 +47,10 @@ function QRCodeScanner({ onScanned }: QRCodeScannerProps) {
};

return (
<>
{isLoading && (
<View className="flex-1">
{(isLoading || (!isScanning && permissionStatus === PermissionStatus.UNDETERMINED)) && (
<View className="flex-1 justify-center items-center">
<Loading />
<Loading className="text-primary-foreground" />
</View>
)}
{!isLoading && <>
Expand All @@ -59,13 +62,11 @@ function QRCodeScanner({ onScanned }: QRCodeScannerProps) {
</View>
}
{isScanning && (
<>
<FocusableCamera onScanned={handleScanned} />
</>
<FocusableCamera onScanned={handleScanned} />
)}
</>
}
</>
</View>
);
}

Expand Down
29 changes: 29 additions & 0 deletions components/Receiver.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View className="flex flex-col gap-2">
<Text className="text-muted-foreground text-center font-semibold2">
To
</Text>
<Text className="text-center text-foreground text-2xl font-medium2">
{originalText.toLowerCase().replace("lightning:", "")}
</Text>
</View>
);
}
93 changes: 71 additions & 22 deletions hooks/useHandleLinking.ts
Original file line number Diff line number Diff line change
@@ -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<string | null>(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<string, string>,
).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]);
}
13 changes: 9 additions & 4 deletions pages/Wildcard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View className="flex-1 justify-center items-center flex flex-col gap-3">
Expand All @@ -17,7 +22,7 @@ export function Wildcard() {
}}
/>
<Loading />
<Text>Loading {pathname}</Text>
<Text>Loading</Text>
</View>
);
}
19 changes: 2 additions & 17 deletions pages/send/ConfirmPayment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -94,23 +95,7 @@ export function ConfirmPayment() {
</View>
)
)}
{
/* only show "To" for lightning addresses */ originalText !==
invoice &&
originalText
.toLowerCase()
.replace("lightning:", "")
.includes("@") && (
<View className="flex flex-col gap-2">
<Text className="text-muted-foreground text-center font-semibold2">
To
</Text>
<Text className="text-center text-foreground text-2xl font-medium2">
{originalText.toLowerCase().replace("lightning:", "")}
</Text>
</View>
)
}
<Receiver originalText={originalText} invoice={invoice} />
</View>
<View className="p-6">
<Button
Expand Down
31 changes: 11 additions & 20 deletions pages/send/LNURLPay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { errorToast } from "~/lib/errorToast";
import Loading from "~/components/Loading";
import { DualCurrencyInput } from "~/components/DualCurrencyInput";
import DismissableKeyboardView from "~/components/DismissableKeyboardView";
import { Receiver } from "~/components/Receiver";

export function LNURLPay() {
const { lnurlDetailsJSON, originalText } =
Expand Down Expand Up @@ -65,29 +66,19 @@ export function LNURLPay() {
readOnly={isAmountReadOnly}
autoFocus={!isAmountReadOnly}
/>
{lnurlDetails.commentAllowed &&
<View className="w-full">
<Text className="text-muted-foreground text-center font-semibold2">
Comment
</Text>
<Input
className="w-full border-transparent bg-transparent text-center native:text-2xl font-semibold2"
placeholder="Enter an optional comment"
value={comment}
onChangeText={setComment}
returnKeyType="done"
maxLength={lnurlDetails.commentAllowed}
/>
</View>
}
<View>
<View className="w-full">
<Text className="text-muted-foreground text-center font-semibold2">
To
</Text>
<Text className="text-center text-foreground text-2xl font-medium2">
{originalText}
Comment
</Text>
<Input
className="w-full border-transparent bg-transparent text-center native:text-2xl font-semibold2"
placeholder="Enter an optional comment"
value={comment}
onChangeText={setComment}
returnKeyType="done"
/>
</View>
<Receiver originalText={originalText} />
</View>
<View className="p-6">
<Button
Expand Down
12 changes: 2 additions & 10 deletions pages/send/PaymentSuccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Button } from "~/components/ui/button";
import { Text } from "~/components/ui/text";
import Screen from "~/components/Screen";
import { useGetFiatAmount } from "~/hooks/useGetFiatAmount";
import { Receiver } from "~/components/Receiver";

export function PaymentSuccess() {
const getFiatAmount = useGetFiatAmount();
Expand All @@ -30,16 +31,7 @@ export function PaymentSuccess() {
<Text className="text-2xl text-muted-foreground font-semibold2">{getFiatAmount(+amount)}</Text>
}
</View>
{originalText !== invoice &&
<View>
<Text className="text-muted-foreground text-center font-semibold2">
Sent to
</Text>
<Text className="text-foreground text-center text-2xl font-medium2">
{originalText}
</Text>
</View>
}
<Receiver originalText={originalText} invoice={invoice} />
</View>
<View className="p-6">
<Button
Expand Down
Loading

0 comments on commit 52d07d2

Please sign in to comment.