Skip to content

fix: improve send logic #308

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

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions app/(app)/send/manual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Manual } from "../../../pages/send/Manual";

export default function Page() {
return <Manual />;
}
3 changes: 3 additions & 0 deletions components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PopiconsUploadSolid as ExportIcon,
PopiconsTouchIdSolid as FingerprintIcon,
PopiconsCircleInfoSolid as HelpCircleIcon,
PopiconsKeyboardSolid as KeyboardIcon,
PopiconsLinkExternalSolid as LinkIcon,
PopiconsArrowDownLine as MoveDownIcon,
PopiconsArrowUpLine as MoveUpIcon,
Expand Down Expand Up @@ -65,6 +66,7 @@ interopIcon(EditIcon);
interopIcon(ExportIcon);
interopIcon(FingerprintIcon);
interopIcon(HelpCircleIcon);
interopIcon(KeyboardIcon);
interopIcon(LinkIcon);
interopIcon(MoveDownIcon);
interopIcon(MoveUpIcon);
Expand Down Expand Up @@ -103,6 +105,7 @@ export {
ExportIcon,
FingerprintIcon,
HelpCircleIcon,
KeyboardIcon,
LinkIcon,
MoveDownIcon,
MoveUpIcon,
Expand Down
10 changes: 5 additions & 5 deletions components/Receiver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,7 +22,7 @@ export function Receiver({ originalText, invoice }: ReceiverProps) {
To
</Text>
<Text className="text-center text-foreground text-2xl font-medium2">
{originalText.toLowerCase().replace("lightning:", "")}
{name.toLowerCase().replace("lightning:", "")}
</Text>
</View>
);
Expand Down
86 changes: 57 additions & 29 deletions hooks/__tests__/useHandleLinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 === "[email protected]") {
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);
});
Expand All @@ -30,53 +48,65 @@ jest.mock("../../lib/lnurl", () => {
};
});

const testVectors: Record<string, { url: string; path: string }> = {
const testVectors: Record<string, { path: string; params: any }> = {
// Lightning Addresses
"lightning:[email protected]": {
url: "[email protected]",
path: "/send",
path: "/send/lnurl-pay",
params: {
lnurlDetailsJSON: JSON.stringify(mockLNURLPayResponse),
receiver: "[email protected]",
},
},
"lightning://[email protected]": {
url: "[email protected]",
path: "/send",
path: "/send/lnurl-pay",
params: {
lnurlDetailsJSON: JSON.stringify(mockLNURLPayResponse),
receiver: "[email protected]",
},
},
"LIGHTNING://[email protected]": {
url: "[email protected]",
path: "/send",
path: "/send/lnurl-pay",
params: {
lnurlDetailsJSON: JSON.stringify(mockLNURLPayResponse),
receiver: "[email protected]",
},
},
"LIGHTNING:[email protected]": {
url: "[email protected]",
path: "/send",
path: "/send/lnurl-pay",
params: {
lnurlDetailsJSON: JSON.stringify(mockLNURLPayResponse),
receiver: "[email protected]",
},
},

// 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" },
},
};

Expand Down Expand Up @@ -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);
},
);
});
Expand All @@ -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,
});
};
5 changes: 3 additions & 2 deletions lib/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
97 changes: 97 additions & 0 deletions lib/processPayment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Invoice } from "@getalby/lightning-tools";
import { router } from "expo-router";
import { lnurl as lnurlLib } from "lib/lnurl";
import { errorToast } from "~/lib/errorToast";
import { convertMerchantQRToLightningAddress } from "~/lib/merchants";

export async function processPayment(
text: string,
amount: string,
): Promise<boolean> {
// Some apps use uppercased LIGHTNING: prefixes
text = text.toLowerCase();
console.info("loading payment", text);
const originalText = text;

try {
if (text.startsWith("bitcoin:")) {
const universalUrl = text.replace("bitcoin:", "http://");
const url = new URL(universalUrl);
const lightningParam = url.searchParams.get("lightning");
if (!lightningParam) {
throw new Error("No lightning param found in bitcoin payment link");
}
text = lightningParam;
}

if (text.startsWith("lightning:")) {
text = text.substring("lightning:".length);
}

// convert picknpay QRs to lighnting addresses
const merchantLightningAddress = convertMerchantQRToLightningAddress(text);
if (merchantLightningAddress) {
text = merchantLightningAddress;
}

const lnurl = lnurlLib.findLnurl(text);
console.info("Checked lnurl value", text, lnurl);

if (lnurl) {
const lnurlDetails = await lnurlLib.getDetails(lnurl);

if (
lnurlDetails.tag !== "payRequest" &&
lnurlDetails.tag !== "withdrawRequest"
) {
throw new Error("LNURL tag not supported");
}

if (lnurlDetails.tag === "withdrawRequest") {
router.replace({
pathname: "/withdraw",
params: { url: lnurl },
});
return true;
}

if (lnurlDetails.tag === "payRequest") {
router.replace({
pathname: "/send/lnurl-pay",
params: {
lnurlDetailsJSON: JSON.stringify(lnurlDetails),
receiver: lnurl,
amount,
},
});
return true;
}
} else {
// Check if this is a valid invoice
const invoice = new Invoice({ pr: text });

if (invoice.satoshi === 0) {
router.replace({
pathname: "/send/0-amount",
params: {
invoice: text,
receiver: originalText,
comment: invoice.description,
},
});
return true;
}

router.replace({
pathname: "/send/confirm",
params: { invoice: text, receiver: originalText },
});
return true;
}
} catch (error) {
console.error("failed to load payment", originalText, error);
errorToast(error);
}

return false;
}
10 changes: 5 additions & 5 deletions pages/send/ConfirmPayment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

Expand All @@ -59,7 +59,7 @@ export function ConfirmPayment() {
pathname: "/send/success",
params: {
preimage: response.preimage,
originalText,
receiver,
invoice,
amount: amountToPaySats,
successAction,
Expand Down Expand Up @@ -112,7 +112,7 @@ export function ConfirmPayment() {
</View>
)
)}
<Receiver originalText={originalText} invoice={invoice} />
<Receiver name={receiver} invoice={invoice} />
</View>
<View className="p-6 bg-background">
<WalletSwitcher selectedWalletId={selectedWalletId} wallets={wallets} />
Expand Down
Loading