From 6bb297cabe128d76eb85a0f30844c1c8b54a895b Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 10 Apr 2025 14:53:48 +0530 Subject: [PATCH 01/11] chore: remove fixed invoice handling logic in send --- pages/send/Send.tsx | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/pages/send/Send.tsx b/pages/send/Send.tsx index 02b3b06..2718fa1 100644 --- a/pages/send/Send.tsx +++ b/pages/send/Send.tsx @@ -110,32 +110,14 @@ export function Send() { return true; } - // Handle fixed amount LNURLs - if ( - lnurlDetails.minSendable === lnurlDetails.maxSendable && - !lnurlDetails.commentAllowed - ) { - const callback = new URL(lnurlDetails.callback); - callback.searchParams.append( - "amount", - lnurlDetails.minSendable.toString(), - ); - const lnurlPayInfo = await lnurl.getPayRequest(callback.toString()); - router.replace({ - pathname: "/send/confirm", - params: { invoice: lnurlPayInfo.pr, originalText }, - }); - } else { - router.replace({ - pathname: "/send/lnurl-pay", - params: { - lnurlDetailsJSON: JSON.stringify(lnurlDetails), - originalText, - amount, - }, - }); - } - + router.replace({ + pathname: "/send/lnurl-pay", + params: { + lnurlDetailsJSON: JSON.stringify(lnurlDetails), + originalText, + amount, + }, + }); return true; } else { // Check if this is a valid invoice From 8608c916f87d02fa3defdabbb9d15b2b621efc85 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 10 Apr 2025 14:54:33 +0530 Subject: [PATCH 02/11] fix: do not allow comment if not supported + show fixed-amount disclaimer --- pages/send/LNURLPay.tsx | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pages/send/LNURLPay.tsx b/pages/send/LNURLPay.tsx index 50bb0b9..0739982 100644 --- a/pages/send/LNURLPay.tsx +++ b/pages/send/LNURLPay.tsx @@ -1,6 +1,7 @@ import { router, useLocalSearchParams } from "expo-router"; import React, { useEffect } from "react"; import { View } from "react-native"; +import Toast from "react-native-toast-message"; import DismissableKeyboardView from "~/components/DismissableKeyboardView"; import { DualCurrencyInput } from "~/components/DualCurrencyInput"; import Loading from "~/components/Loading"; @@ -41,6 +42,11 @@ export function LNURLPay() { if (lnurlDetails.minSendable === lnurlDetails.maxSendable) { setAmount((lnurlDetails.minSendable / 1000).toString()); setAmountReadOnly(true); + Toast.show({ + type: "success", + text1: "You are paying a fixed amount invoice", + position: "top", + }); } }, [lnurlDetails.minSendable, lnurlDetails.maxSendable]); @@ -105,18 +111,20 @@ export function LNURLPay() { sats - - - Comment - - - + {!!lnurlDetails.commentAllowed && ( + + + Comment + + + + )} From 7df77e8f90e480b5d520988b647d2d1f70c8bf8c Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 10 Apr 2025 15:37:27 +0530 Subject: [PATCH 03/11] fix: send directly to lnurl pay screen --- lib/link.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/link.ts b/lib/link.ts index 0584e09..787d821 100644 --- a/lib/link.ts +++ b/lib/link.ts @@ -159,9 +159,10 @@ export const handleLink = async (url: string) => { if (lnurlDetails.tag === "payRequest") { router.push({ - pathname: "/send", + pathname: "/send/lnurl-pay", params: { - url: lnurl, + lnurlDetailsJSON: JSON.stringify(lnurlDetails), + receiver: lnurl, }, }); return; From 27600582e1e566a78a30310379b982d4f9804249 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Thu, 10 Apr 2025 15:49:18 +0530 Subject: [PATCH 04/11] chore: rename original text to receiver and name --- components/Receiver.tsx | 10 +++++----- pages/send/ConfirmPayment.tsx | 10 +++++----- pages/send/LNURLPay.tsx | 8 ++++---- pages/send/PaymentSuccess.tsx | 6 +++--- pages/send/ZeroAmount.tsx | 15 +++++++-------- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/components/Receiver.tsx b/components/Receiver.tsx index 7a4287c..0edcfe6 100644 --- a/components/Receiver.tsx +++ b/components/Receiver.tsx @@ -3,14 +3,14 @@ import { View } from "react-native"; import { Text } from "~/components/ui/text"; interface ReceiverProps { - originalText: string; + name: string; invoice?: string; } -export function Receiver({ originalText, invoice }: ReceiverProps) { +export function Receiver({ name, invoice }: ReceiverProps) { const shouldShowReceiver = - originalText !== invoice && - originalText.toLowerCase().replace("lightning:", "").includes("@"); + name !== invoice && + name.toLowerCase().replace("lightning:", "").includes("@"); if (!shouldShowReceiver) { return null; @@ -22,7 +22,7 @@ export function Receiver({ originalText, invoice }: ReceiverProps) { To - {originalText.toLowerCase().replace("lightning:", "")} + {name.toLowerCase().replace("lightning:", "")} ); diff --git a/pages/send/ConfirmPayment.tsx b/pages/send/ConfirmPayment.tsx index 8ac331f..3753260 100644 --- a/pages/send/ConfirmPayment.tsx +++ b/pages/send/ConfirmPayment.tsx @@ -19,10 +19,10 @@ import { useAppStore } from "~/lib/state/appStore"; export function ConfirmPayment() { const { data: transactions } = useTransactions(); - const { invoice, originalText, comment, successAction, amount } = + const { invoice, receiver, comment, successAction, amount } = useLocalSearchParams() as { invoice: string; - originalText: string; + receiver: string; comment: string; successAction: string; amount?: string; @@ -50,7 +50,7 @@ export function ConfirmPayment() { console.info("payInvoice Response", response); - if (originalText === ALBY_LIGHTNING_ADDRESS) { + if (receiver === ALBY_LIGHTNING_ADDRESS) { useAppStore.getState().updateLastAlbyPayment(); } @@ -59,7 +59,7 @@ export function ConfirmPayment() { pathname: "/send/success", params: { preimage: response.preimage, - originalText, + receiver, invoice, amount: amountToPaySats, successAction, @@ -112,7 +112,7 @@ export function ConfirmPayment() { ) )} - + diff --git a/pages/send/LNURLPay.tsx b/pages/send/LNURLPay.tsx index 0739982..ebf7d21 100644 --- a/pages/send/LNURLPay.tsx +++ b/pages/send/LNURLPay.tsx @@ -17,11 +17,11 @@ import { cn } from "~/lib/utils"; export function LNURLPay() { const { lnurlDetailsJSON, - originalText, + receiver, amount: amountParam, } = useLocalSearchParams() as unknown as { lnurlDetailsJSON: string; - originalText: string; + receiver: string; amount: string; }; const lnurlDetails: LNURLPayServiceResponse = JSON.parse(lnurlDetailsJSON); @@ -65,7 +65,7 @@ export function LNURLPay() { pathname: "/send/confirm", params: { invoice: lnurlPayInfo.pr, - originalText, + receiver, comment, successAction: lnurlPayInfo.successAction ? JSON.stringify(lnurlPayInfo.successAction) @@ -125,7 +125,7 @@ export function LNURLPay() { /> )} - + + + + + ); +} From 168d32d9a171562a4f5e5c28274b3ddef304d95f Mon Sep 17 00:00:00 2001 From: Shresth-Deorari Date: Fri, 11 Apr 2025 13:45:06 +0530 Subject: [PATCH 08/11] chore: update Send.tsx to use Manual screen --- pages/send/Send.tsx | 107 ++++++++++++++------------------------------ 1 file changed, 34 insertions(+), 73 deletions(-) diff --git a/pages/send/Send.tsx b/pages/send/Send.tsx index 3b6b9c2..297835d 100644 --- a/pages/send/Send.tsx +++ b/pages/send/Send.tsx @@ -2,13 +2,11 @@ import * as Clipboard from "expo-clipboard"; import { router, useLocalSearchParams } from "expo-router"; import React from "react"; import { View } from "react-native"; -import DismissableKeyboardView from "~/components/DismissableKeyboardView"; import { BookUserIcon, KeyboardIcon, PasteIcon } from "~/components/Icons"; import Loading from "~/components/Loading"; import QRCodeScanner from "~/components/QRCodeScanner"; import Screen from "~/components/Screen"; import { Button } from "~/components/ui/button"; -import { Input } from "~/components/ui/input"; import { Text } from "~/components/ui/text"; import { errorToast } from "~/lib/errorToast"; import { handleLink } from "~/lib/link"; @@ -21,8 +19,6 @@ 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); async function paste() { @@ -40,14 +36,6 @@ export function Send() { return loadPayment(data); }; - function openKeyboard() { - setKeyboardOpen(true); - } - - function submitKeyboardText() { - loadPayment(keyboardText); - } - const loadPayment = async (text: string, amount = "") => { if (!text) { errorToast(new Error("Your clipboard is empty.")); @@ -90,67 +78,40 @@ export function Send() { )} {!isLoading && ( <> - {!keyboardOpen && ( - <> - - - - - - - - )} - {keyboardOpen && ( - - - - - Type or paste a Lightning Address, lightning invoice or - LNURL. - - - - - - - )} + + + + + + )} From 0d72547635a707d711632c27a1f05f2580d44eef Mon Sep 17 00:00:00 2001 From: Shresth-Deorari Date: Fri, 11 Apr 2025 13:46:29 +0530 Subject: [PATCH 09/11] chore: add manual.js route for Manual screen --- app/(app)/send/manual.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/(app)/send/manual.js diff --git a/app/(app)/send/manual.js b/app/(app)/send/manual.js new file mode 100644 index 0000000..64b24a3 --- /dev/null +++ b/app/(app)/send/manual.js @@ -0,0 +1,5 @@ +import { Manual } from "../../../pages/send/Manual"; + +export default function Page() { + return ; +} From 661adff03b2d8f4b41d3cba915f9950b0bba5749 Mon Sep 17 00:00:00 2001 From: Shresth-Deorari Date: Fri, 11 Apr 2025 16:19:34 +0530 Subject: [PATCH 10/11] chore: disable Manual button when empty --- pages/send/Manual.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pages/send/Manual.tsx b/pages/send/Manual.tsx index 7673b0d..c615eef 100644 --- a/pages/send/Manual.tsx +++ b/pages/send/Manual.tsx @@ -12,10 +12,6 @@ export function Manual() { const [keyboardText, setKeyboardText] = useState(""); const submitKeyboardText = async () => { - if (!keyboardText) { - errorToast(new Error("Input is empty.")); - return; - } try { await processPayment(keyboardText, ""); } catch (error) { @@ -43,7 +39,11 @@ export function Manual() { returnKeyType="done" /> - From 1188c3ab16ba0214a19c1e907cb7d5ce36cf3f25 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Fri, 11 Apr 2025 18:30:49 +0530 Subject: [PATCH 11/11] chore: fix tests --- hooks/__tests__/useHandleLinking.ts | 86 +++++++++++++++++++---------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/hooks/__tests__/useHandleLinking.ts b/hooks/__tests__/useHandleLinking.ts index f957fbe..c1ed068 100644 --- a/hooks/__tests__/useHandleLinking.ts +++ b/hooks/__tests__/useHandleLinking.ts @@ -3,20 +3,38 @@ import { handleLink } from "../../lib/link"; jest.mock("expo-router"); +const mockLNURLPayResponse = { + tag: "payRequest", + callback: "https://getalby.com/callback", + commentAllowed: 255, + minSendable: 1000, + maxSendable: 10000000, + payerData: { + name: { mandatory: false }, + email: { mandatory: false }, + pubkey: { mandatory: false }, + }, +}; + +const mockLNURLWithdrawResponse = { + tag: "withdrawRequest", + callback: "https://getalby.com/callback", + k1: "unused", + defaultDescription: "withdrawal", + minWithdrawable: 21000, + maxWithdrawable: 21000, +}; + // Mock the lnurl module jest.mock("../../lib/lnurl", () => { const originalModule = jest.requireActual("../../lib/lnurl"); const mockGetDetails = jest.fn(async (lnurlString) => { + if (lnurlString === "hello@getalby.com") { + return mockLNURLPayResponse; + } if (lnurlString.startsWith("lnurlw")) { - return { - tag: "withdrawRequest", - callback: "https://getalby.com/callback", - k1: "unused", - defaultDescription: "withdrawal", - minWithdrawable: 21000, - maxWithdrawable: 21000, - }; + return mockLNURLWithdrawResponse; } return originalModule.lnurl.getDetails(lnurlString); }); @@ -30,53 +48,65 @@ jest.mock("../../lib/lnurl", () => { }; }); -const testVectors: Record = { +const testVectors: Record = { // Lightning Addresses "lightning:hello@getalby.com": { - url: "hello@getalby.com", - path: "/send", + path: "/send/lnurl-pay", + params: { + lnurlDetailsJSON: JSON.stringify(mockLNURLPayResponse), + receiver: "hello@getalby.com", + }, }, "lightning://hello@getalby.com": { - url: "hello@getalby.com", - path: "/send", + path: "/send/lnurl-pay", + params: { + lnurlDetailsJSON: JSON.stringify(mockLNURLPayResponse), + receiver: "hello@getalby.com", + }, }, "LIGHTNING://hello@getalby.com": { - url: "hello@getalby.com", - path: "/send", + path: "/send/lnurl-pay", + params: { + lnurlDetailsJSON: JSON.stringify(mockLNURLPayResponse), + receiver: "hello@getalby.com", + }, }, "LIGHTNING:hello@getalby.com": { - url: "hello@getalby.com", - path: "/send", + path: "/send/lnurl-pay", + params: { + lnurlDetailsJSON: JSON.stringify(mockLNURLPayResponse), + receiver: "hello@getalby.com", + }, }, // Lightning invoices "lightning:lnbc123": { - url: "lnbc123", path: "/send", + params: { url: "lnbc123" }, }, "lightning://lnbc123": { - url: "lnbc123", path: "/send", + params: { url: "lnbc123" }, }, // BIP21 "bitcoin:bitcoinaddress?lightning=lnbc123": { - url: "lnbc123", path: "/send", + params: { url: "lnbc123" }, }, "BITCOIN:bitcoinaddress?lightning=lnbc123": { - url: "lnbc123", path: "/send", + params: { url: "lnbc123" }, }, // LNURL-withdraw "lightning:lnurlw123": { - url: "lnurlw123", path: "/withdraw", + params: { url: "lnurlw123" }, }, "lightning://lnurlw123": { - url: "lnurlw123", path: "/withdraw", + params: { url: "lnurlw123" }, }, }; @@ -104,7 +134,7 @@ describe("handleLink", () => { "should parse the URL '%s' and navigate correctly", async (url, expectedOutput) => { await handleLink("exp://127.0.0.1:8081/--/" + url); - assertRedirect(expectedOutput.path, expectedOutput.url); + assertRedirect(expectedOutput.path, expectedOutput.params); }, ); }); @@ -114,17 +144,15 @@ describe("handleLink", () => { "should parse the URL '%s' and navigate correctly", async (url, expectedOutput) => { await handleLink(url); - assertRedirect(expectedOutput.path, expectedOutput.url); + assertRedirect(expectedOutput.path, expectedOutput.params); }, ); }); }); -const assertRedirect = (expectedPath: string, expectedUrl: string) => { +const assertRedirect = (expectedPath: string, expectedParams: any) => { expect(router.push).toHaveBeenCalledWith({ pathname: expectedPath, - params: { - url: expectedUrl, - }, + params: expectedParams, }); };