Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve linking #129

Merged
merged 14 commits into from
Sep 27, 2024
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>
);
}
36 changes: 21 additions & 15 deletions hooks/useHandleLinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,43 @@ import * as Linking from "expo-linking";
import { router, useRootNavigationState } from "expo-router";
import React from "react";

// TESTING: ["lightning:", "bitcoin:", "alby:", "exp:"]
const SUPPORTED_SCHEMES = ["lightning:", "bitcoin:", "alby:"];

export function useHandleLinking() {
const rootNavigationState = useRootNavigationState();
let url = Linking.useURL();
let hasNavigationState = !!rootNavigationState?.key;
const url = Linking.useURL();
const hasNavigationState = !!rootNavigationState?.key;

React.useEffect(() => {
if (!hasNavigationState) {
if (!hasNavigationState || !url) {
return;
}
console.log("Received linking URL", 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 (url.startsWith(scheme)) {
let currentUrl = url.startsWith(scheme + "//")
? url.replace(scheme + "//", scheme)
: url;

// TODO: it should not always navigate to send,
// but that's the only linking functionality supported right now
router.dismissAll();
router.navigate({
// TESTING:
// currentUrl = currentUrl.replace("exp:127.0.0.1:8081/--/", "lightning:");

// Instead of dismissing all screens, we'll use replace to avoid navigation stack issues
router.replace({
pathname: "/send",
params: {
url,
url: currentUrl,
},
});
break;
return;
}
}

// Redirect the user to the home screen
// if no match was found
router.replace({
pathname: "/",
});
}, [url, hasNavigationState]);
}
2 changes: 1 addition & 1 deletion pages/Wildcard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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
32 changes: 28 additions & 4 deletions pages/send/Send.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,22 @@ export function Send() {
const [isLoading, setLoading] = React.useState(false);
const [keyboardOpen, setKeyboardOpen] = React.useState(false);
const [keyboardText, setKeyboardText] = React.useState("");
const [startScanning, setStartScanning] = React.useState(false);

// Delay starting the QR scanner if url has no valid payment info
useEffect(() => {
if (url) {
loadPayment(url);
(async () => {
try {
const result = await loadPayment(url);
setStartScanning(!result);
reneaaron marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
console.error("failed to load payment", url, error);
errorToast(error);
}
})();
} else {
setStartScanning(true);
}
}, [url]);

Expand All @@ -53,11 +65,15 @@ export function Send() {
loadPayment(keyboardText);
}

async function loadPayment(text: string) {
async function loadPayment(text: string): Promise<boolean> {
if (!text) {
errorToast(new Error("Your clipboard is empty."));
return;
return false;
}

// Some apps use uppercased LIGHTNING: prefixes
text = text.toLowerCase();

console.log("loading payment", text);
const originalText = text;
setLoading(true);
Expand All @@ -74,6 +90,7 @@ export function Send() {
if (text.startsWith("lightning:")) {
text = text.substring("lightning:".length);
}

const lnurlValue = lnurl.findLnurl(text);
console.log("Checked lnurl value", text, lnurlValue);
if (lnurlValue) {
Expand Down Expand Up @@ -106,6 +123,8 @@ export function Send() {
},
});
}

return true;
} else {
// Check if this is a valid invoice
new Invoice({
Expand All @@ -116,12 +135,17 @@ export function Send() {
pathname: "/send/confirm",
params: { invoice: text, originalText },
});

return true;
}
} catch (error) {
console.error("failed to load payment", originalText, error);
errorToast(error);
} finally {
setLoading(false);
}

return false;
}

return (
Expand All @@ -136,7 +160,7 @@ export function Send() {
<>
{!keyboardOpen && (
<>
<QRCodeScanner onScanned={handleScanned} />
<QRCodeScanner onScanned={handleScanned} startScanning={startScanning} />
<View className="flex flex-row items-stretch justify-center gap-4 p-6">
<Button
onPress={openKeyboard}
Expand Down
Loading
Loading