diff --git a/app/(app)/receive/alby-account.js b/app/(app)/receive/alby-account.js
new file mode 100644
index 00000000..1f98ad4c
--- /dev/null
+++ b/app/(app)/receive/alby-account.js
@@ -0,0 +1,5 @@
+import { AlbyAccount } from "../../../pages/receive/AlbyAccount";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/(app)/receive/invoice.js b/app/(app)/receive/invoice.js
new file mode 100644
index 00000000..86451448
--- /dev/null
+++ b/app/(app)/receive/invoice.js
@@ -0,0 +1,5 @@
+import { Invoice } from "../../../pages/receive/Invoice";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/(app)/receive/lightning-address.js b/app/(app)/receive/lightning-address.js
new file mode 100644
index 00000000..ec533ff2
--- /dev/null
+++ b/app/(app)/receive/lightning-address.js
@@ -0,0 +1,5 @@
+import { LightningAddress } from "../../../pages/receive/LightningAddress";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/(app)/receive/withdraw.js b/app/(app)/receive/withdraw.js
new file mode 100644
index 00000000..1412dff1
--- /dev/null
+++ b/app/(app)/receive/withdraw.js
@@ -0,0 +1,5 @@
+import { Withdraw } from "../../../pages/receive/Withdraw";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/(app)/withdraw/index.js b/app/(app)/withdraw/index.js
deleted file mode 100644
index c5149757..00000000
--- a/app/(app)/withdraw/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Withdraw } from "../../../pages/withdraw/Withdraw";
-
-export default function Page() {
- return ;
-}
diff --git a/assets/alby-account.png b/assets/alby-account.png
new file mode 100644
index 00000000..bb890a74
Binary files /dev/null and b/assets/alby-account.png differ
diff --git a/components/CreateInvoice.tsx b/components/CreateInvoice.tsx
new file mode 100644
index 00000000..b11faf92
--- /dev/null
+++ b/components/CreateInvoice.tsx
@@ -0,0 +1,218 @@
+import { Nip47Transaction } from "@getalby/sdk/dist/NWCClient";
+import * as Clipboard from "expo-clipboard";
+import { router } from "expo-router";
+import React from "react";
+import { Image, Share, View } from "react-native";
+import Toast from "react-native-toast-message";
+import DismissableKeyboardView from "~/components/DismissableKeyboardView";
+import { DualCurrencyInput } from "~/components/DualCurrencyInput";
+import { CopyIcon, ShareIcon } from "~/components/Icons";
+import Loading from "~/components/Loading";
+import QRCode from "~/components/QRCode";
+import { Button } from "~/components/ui/button";
+import { Input } from "~/components/ui/input";
+import { Text } from "~/components/ui/text";
+import { useGetFiatAmount } from "~/hooks/useGetFiatAmount";
+import { errorToast } from "~/lib/errorToast";
+import { useAppStore } from "~/lib/state/appStore";
+
+export function CreateInvoice() {
+ const getFiatAmount = useGetFiatAmount();
+ const [isLoading, setLoading] = React.useState(false);
+ const [invoice, setInvoice] = React.useState("");
+ const [amount, setAmount] = React.useState("");
+ const [comment, setComment] = React.useState("");
+
+ function generateInvoice(amount?: number) {
+ if (!amount) {
+ errorToast(new Error("0-amount invoices are currently not supported"));
+ return;
+ }
+ (async () => {
+ setLoading(true);
+ try {
+ const nwcClient = useAppStore.getState().nwcClient;
+ if (!nwcClient) {
+ throw new Error("NWC client not connected");
+ }
+ const response = await nwcClient.makeInvoice({
+ amount: amount * 1000 /*FIXME: allow 0-amount invoices */,
+ ...(comment ? { description: comment } : {}),
+ });
+
+ console.info("makeInvoice Response", response);
+
+ setInvoice(response.invoice);
+ } catch (error) {
+ console.error(error);
+ errorToast(error);
+ }
+ setLoading(false);
+ })();
+ }
+
+ function copy() {
+ const text = invoice;
+ if (!text) {
+ errorToast(new Error("Nothing to copy"));
+ return;
+ }
+ Clipboard.setStringAsync(text);
+ Toast.show({
+ type: "success",
+ text1: "Copied to clipboard",
+ });
+ }
+
+ async function share() {
+ const message = invoice;
+ try {
+ if (!message) {
+ throw new Error("no lightning address set");
+ }
+ await Share.share({
+ message,
+ });
+ } catch (error) {
+ console.error("Error sharing:", error);
+ errorToast(error);
+ }
+ }
+
+ React.useEffect(() => {
+ let polling = true;
+ let pollCount = 0;
+ let prevTransaction: Nip47Transaction | undefined;
+ (async () => {
+ while (polling) {
+ try {
+ const transactions = await useAppStore
+ .getState()
+ .nwcClient?.listTransactions({
+ limit: 1,
+ type: "incoming",
+ });
+ const receivedTransaction = transactions?.transactions[0];
+ if (receivedTransaction) {
+ if (
+ polling &&
+ pollCount > 0 &&
+ receivedTransaction.payment_hash !== prevTransaction?.payment_hash
+ ) {
+ if (invoice && receivedTransaction.invoice === invoice) {
+ router.dismissAll();
+ router.navigate({
+ pathname: "/receive/success",
+ params: { invoice: receivedTransaction.invoice },
+ });
+ } else {
+ console.info("Received another payment");
+ }
+ }
+ prevTransaction = receivedTransaction;
+ }
+ ++pollCount;
+ } catch (error) {
+ console.error("Failed to poll for incoming transaction", error);
+ }
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
+ })();
+ return () => {
+ polling = false;
+ };
+ }, [invoice]);
+
+ return (
+ <>
+ {invoice ? (
+ <>
+
+
+
+
+
+
+
+
+
+
+ {new Intl.NumberFormat().format(+amount)}{" "}
+
+
+ sats
+
+
+ {getFiatAmount && (
+
+ {getFiatAmount(+amount)}
+
+ )}
+
+
+
+ Waiting for payment
+
+
+
+
+
+
+ >
+ ) : (
+
+
+
+
+
+
+ Description (optional)
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/components/Icons.tsx b/components/Icons.tsx
index c6907390..89d8100c 100644
--- a/components/Icons.tsx
+++ b/components/Icons.tsx
@@ -1,4 +1,5 @@
import {
+ PopiconsAtSymbolSolid as AddressIcon,
PopiconsCircleExclamationLine as AlertCircleIcon,
PopiconsBitcoinSolid as BitcoinIcon,
PopiconsAddressBookSolid as BookUserIcon,
@@ -10,13 +11,16 @@ import {
PopiconsUploadSolid as ExportIcon,
PopiconsTouchIdSolid as FingerprintIcon,
PopiconsCircleInfoLine as HelpCircleIcon,
+ PopiconsLinkExternalSolid as LinkIcon,
PopiconsArrowDownLine as MoveDownIcon,
PopiconsArrowUpLine as MoveUpIcon,
PopiconsNotificationSquareSolid as NotificationIcon,
PopiconsLifebuoySolid as OnboardingIcon,
PopiconsClipboardTextSolid as PasteIcon,
+ PopiconsQrCodeMinimalSolid as QRIcon,
PopiconsReloadLine as RefreshIcon,
PopiconsReloadSolid as ResetIcon,
+ PopiconsFullscreenSolid as ScanIcon,
PopiconsSettingsMinimalSolid as SettingsIcon,
PopiconsShareSolid as ShareIcon,
PopiconsLogoutSolid as SignOutIcon,
@@ -45,6 +49,7 @@ function interopIcon(icon: React.FunctionComponent) {
});
}
+interopIcon(AddressIcon);
interopIcon(AlertCircleIcon);
interopIcon(BitcoinIcon);
interopIcon(BookUserIcon);
@@ -56,13 +61,16 @@ interopIcon(EditIcon);
interopIcon(ExportIcon);
interopIcon(FingerprintIcon);
interopIcon(HelpCircleIcon);
+interopIcon(LinkIcon);
interopIcon(MoveDownIcon);
interopIcon(MoveUpIcon);
interopIcon(NotificationIcon);
interopIcon(OnboardingIcon);
interopIcon(PasteIcon);
+interopIcon(QRIcon);
interopIcon(RefreshIcon);
interopIcon(ResetIcon);
+interopIcon(ScanIcon);
interopIcon(SettingsIcon);
interopIcon(ShareIcon);
interopIcon(SignOutIcon);
@@ -77,6 +85,7 @@ interopIcon(XIcon);
interopIcon(ZapIcon);
export {
+ AddressIcon,
AlertCircleIcon,
BitcoinIcon,
BookUserIcon,
@@ -88,13 +97,16 @@ export {
ExportIcon,
FingerprintIcon,
HelpCircleIcon,
+ LinkIcon,
MoveDownIcon,
MoveUpIcon,
NotificationIcon,
OnboardingIcon,
PasteIcon,
+ QRIcon,
RefreshIcon,
ResetIcon,
+ ScanIcon,
SettingsIcon,
ShareIcon,
SignOutIcon,
diff --git a/package.json b/package.json
index 12560abd..aba14060 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,6 @@
"@noble/curves": "^1.6.0",
"@popicons/react-native": "^0.0.22",
"@react-native-async-storage/async-storage": "1.23.1",
- "@react-navigation/native-stack": "^7.2.0",
"@rn-primitives/dialog": "^1.0.3",
"@rn-primitives/portal": "^1.0.3",
"@rn-primitives/switch": "^1.0.3",
diff --git a/pages/receive/AlbyAccount.tsx b/pages/receive/AlbyAccount.tsx
new file mode 100644
index 00000000..12a99815
--- /dev/null
+++ b/pages/receive/AlbyAccount.tsx
@@ -0,0 +1,60 @@
+import { openURL } from "expo-linking";
+import React from "react";
+import { Dimensions, Image, ScrollView, View } from "react-native";
+import { LinkIcon } from "~/components/Icons";
+import Screen from "~/components/Screen";
+import { Button } from "~/components/ui/button";
+import { Text } from "~/components/ui/text";
+
+export function AlbyAccount() {
+ const dimensions = Dimensions.get("window");
+ const imageWidth = Math.round((dimensions.width * 3) / 5);
+
+ return (
+
+
+
+
+
+
+ Get your lightning address with Alby Account
+
+
+
+ {"\u2022 "}Lightning address & Nostr identifier,
+
+
+ {"\u2022 "}Personal tipping page,
+
+
+ {"\u2022 "}Access to podcasting 2.0 apps,
+
+
+ {"\u2022 "}Buy bitcoin directly to your wallet,
+
+
+ {"\u2022 "}Useful email wallet notifications,
+
+
+ {"\u2022 "}Priority support.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/pages/receive/Invoice.tsx b/pages/receive/Invoice.tsx
new file mode 100644
index 00000000..edb2f7ff
--- /dev/null
+++ b/pages/receive/Invoice.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import { CreateInvoice } from "~/components/CreateInvoice";
+import Screen from "~/components/Screen";
+
+export function Invoice() {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/pages/receive/LightningAddress.tsx b/pages/receive/LightningAddress.tsx
new file mode 100644
index 00000000..93d7df68
--- /dev/null
+++ b/pages/receive/LightningAddress.tsx
@@ -0,0 +1,43 @@
+import { router } from "expo-router";
+import React from "react";
+import { View } from "react-native";
+import { QRIcon } from "~/components/Icons";
+import Screen from "~/components/Screen";
+import { Button } from "~/components/ui/button";
+import { Text } from "~/components/ui/text";
+import { useAppStore } from "~/lib/state/appStore";
+
+export function LightningAddress() {
+ const walletId = useAppStore((store) => store.selectedWalletId);
+
+ return (
+
+
+
+
+
+ satoshi
+
+ @getalby.com
+
+
+
+ Attach your lightning address to this wallet to display it as QR code
+ for fast face-to-face transactions
+
+
+
+
+
+ );
+}
diff --git a/pages/receive/Receive.tsx b/pages/receive/Receive.tsx
index cbe2303d..829d3b1a 100644
--- a/pages/receive/Receive.tsx
+++ b/pages/receive/Receive.tsx
@@ -1,71 +1,30 @@
-import { Nip47Transaction } from "@getalby/sdk/dist/NWCClient";
import * as Clipboard from "expo-clipboard";
import { router } from "expo-router";
import React from "react";
import { Share, TouchableOpacity, View } from "react-native";
import Toast from "react-native-toast-message";
-import DismissableKeyboardView from "~/components/DismissableKeyboardView";
-import { DualCurrencyInput } from "~/components/DualCurrencyInput";
-import { CopyIcon, ShareIcon, WithdrawIcon, ZapIcon } from "~/components/Icons";
-import Loading from "~/components/Loading";
+import { CreateInvoice } from "~/components/CreateInvoice";
+import {
+ AddressIcon,
+ ScanIcon,
+ ShareIcon,
+ WithdrawIcon,
+ ZapIcon,
+} from "~/components/Icons";
import QRCode from "~/components/QRCode";
import Screen from "~/components/Screen";
import { Button } from "~/components/ui/button";
-import { Input } from "~/components/ui/input";
import { Text } from "~/components/ui/text";
-import { useGetFiatAmount } from "~/hooks/useGetFiatAmount";
import { errorToast } from "~/lib/errorToast";
import { useAppStore } from "~/lib/state/appStore";
export function Receive() {
- const getFiatAmount = useGetFiatAmount();
- const [isLoading, setLoading] = React.useState(false);
- const [invoice, _setInvoice] = React.useState("");
- const invoiceRef = React.useRef("");
- const [amount, setAmount] = React.useState("");
- const [comment, setComment] = React.useState("");
- const [enterCustomAmount, setEnterCustomAmount] = React.useState(false);
const selectedWalletId = useAppStore((store) => store.selectedWalletId);
const wallets = useAppStore((store) => store.wallets);
const lightningAddress = wallets[selectedWalletId].lightningAddress;
- const nwcCapabilities = wallets[selectedWalletId].nwcCapabilities;
-
- function setInvoice(invoice: string) {
- _setInvoice(invoice);
- invoiceRef.current = invoice;
- }
-
- function generateInvoice(amount?: number) {
- if (!amount) {
- console.error("0-amount invoices are currently not supported");
- return;
- }
- (async () => {
- setLoading(true);
- try {
- const nwcClient = useAppStore.getState().nwcClient;
- if (!nwcClient) {
- throw new Error("NWC client not connected");
- }
- const response = await nwcClient.makeInvoice({
- amount: amount * 1000 /*FIXME: allow 0-amount invoices */,
- ...(comment ? { description: comment } : {}),
- });
-
- console.info("makeInvoice Response", response);
-
- setInvoice(response.invoice);
- setEnterCustomAmount(false);
- } catch (error) {
- console.error(error);
- errorToast(error);
- }
- setLoading(false);
- })();
- }
function copy() {
- const text = invoice || lightningAddress;
+ const text = lightningAddress;
if (!text) {
errorToast(new Error("Nothing to copy"));
return;
@@ -77,92 +36,11 @@ export function Receive() {
});
}
- // TODO: move this somewhere else to have app-wide notifications of incoming payments
- React.useEffect(() => {
- if (!nwcCapabilities || nwcCapabilities.indexOf("notifications") < 0) {
- // TODO: we do not check if the wallet supports listTransactions,
- // and could also use lookupInvoice if it's a custom invoice
- let polling = true;
- let pollCount = 0;
- let prevTransaction: Nip47Transaction | undefined;
- (async () => {
- while (polling) {
- try {
- const transactions = await useAppStore
- .getState()
- .nwcClient?.listTransactions({
- limit: 1,
- type: "incoming",
- });
- const receivedTransaction = transactions?.transactions[0];
- if (receivedTransaction) {
- if (
- polling &&
- pollCount > 0 &&
- receivedTransaction.payment_hash !==
- prevTransaction?.payment_hash
- ) {
- if (
- !invoiceRef.current ||
- receivedTransaction.invoice === invoiceRef.current
- ) {
- router.dismissAll();
- router.navigate({
- pathname: "/receive/success",
- params: { invoice: receivedTransaction.invoice },
- });
- } else {
- console.info("Received another payment");
- }
- }
- prevTransaction = receivedTransaction;
- }
- ++pollCount;
- } catch (error) {
- console.error("Failed to list transactions", error);
- }
- await new Promise((resolve) => setTimeout(resolve, 1000));
- }
- })();
- return () => {
- polling = false;
- };
- }
-
- const nwcClient = useAppStore.getState().nwcClient;
- if (!nwcClient) {
- throw new Error("NWC client not connected");
- }
- let unsub: (() => void) | undefined = undefined;
- (async () => {
- unsub = await nwcClient.subscribeNotifications((notification) => {
- console.info("RECEIVED notification", notification);
- if (notification.notification_type === "payment_received") {
- if (
- !invoiceRef.current ||
- notification.notification.invoice === invoiceRef.current
- ) {
- router.dismissAll();
- router.navigate({
- pathname: "/receive/success",
- params: { invoice: notification.notification.invoice },
- });
- } else {
- console.info("Received another payment");
- }
- }
- });
- })();
- return () => {
- unsub?.();
- };
- }, [nwcCapabilities]);
-
async function share() {
- const message = invoice || lightningAddress;
+ const message = lightningAddress;
try {
if (!message) {
- throw new Error("no invoice or lightning address set");
+ throw new Error("no lightning address set");
}
await Share.share({
message,
@@ -175,164 +53,87 @@ export function Receive() {
return (
<>
-
- {!enterCustomAmount && !invoice && !lightningAddress && (
+
+ !lightningAddress && (
+ <>
+ {
+ router.push("/receive/lightning-address");
+ }}
+ >
+
+
+ {
+ router.push("receive/withdraw");
+ }}
+ >
+
+
+ >
+ )
+ }
+ />
+ {!lightningAddress ? (
+
+ ) : (
<>
-
- {/* TODO: re-add when we have a way to create a lightning address for new users */}
- {/*
-
- Receive quickly with a Lightning Address
-
-
-
- */}
+
+
+
+
+
+ {lightningAddress}
+
+
+
+
-
- >
- )}
- {!enterCustomAmount && (invoice.length || lightningAddress) && (
- <>
-
-
-
- {invoice ? (
-
-
- {new Intl.NumberFormat().format(+amount)}{" "}
-
-
- sats
-
-
- ) : (
- lightningAddress && (
-
-
- {lightningAddress}
-
-
- )
- )}
- {invoice && getFiatAmount && (
-
- {getFiatAmount(+amount)}
-
- )}
-
- {invoice && (
-
-
- Waiting for payment
-
- )}
-
-
-
- {!enterCustomAmount && invoice && (
-
- )}
- {!enterCustomAmount && !invoice && (
-
- )}
- {!enterCustomAmount && !invoice && (
-
- )}
>
)}
- {/* TODO: move to one place - this is all copied from LNURL-Pay */}
- {!invoice && enterCustomAmount && (
-
-
-
-
-
-
- Description (optional)
-
-
-
-
-
-
-
-
-
- )}
>
);
}
diff --git a/pages/receive/ReceiveSuccess.tsx b/pages/receive/ReceiveSuccess.tsx
index 6c43f0ac..f3c9b924 100644
--- a/pages/receive/ReceiveSuccess.tsx
+++ b/pages/receive/ReceiveSuccess.tsx
@@ -16,7 +16,7 @@ export function ReceiveSuccess() {
});
return (
-
+
diff --git a/pages/withdraw/Withdraw.tsx b/pages/receive/Withdraw.tsx
similarity index 99%
rename from pages/withdraw/Withdraw.tsx
rename to pages/receive/Withdraw.tsx
index 31f7f536..830acbe4 100644
--- a/pages/withdraw/Withdraw.tsx
+++ b/pages/receive/Withdraw.tsx
@@ -160,7 +160,7 @@ export function Withdraw() {
return (
<>
-
+
{isLoading && (
diff --git a/pages/settings/wallets/EditWallet.tsx b/pages/settings/wallets/EditWallet.tsx
index 9773a4c8..65febc93 100644
--- a/pages/settings/wallets/EditWallet.tsx
+++ b/pages/settings/wallets/EditWallet.tsx
@@ -5,11 +5,11 @@ import { Pressable, Alert as RNAlert, View } from "react-native";
import Toast from "react-native-toast-message";
import Alert from "~/components/Alert";
import {
+ AddressIcon,
ExportIcon,
TrashIcon,
TriangleAlertIcon,
WalletIcon,
- ZapIcon,
} from "~/components/Icons";
import Loading from "~/components/Loading";
import Screen from "~/components/Screen";
@@ -105,7 +105,7 @@ export function EditWallet() {
-
+
Lightning Address
diff --git a/pages/settings/wallets/LightningAddress.tsx b/pages/settings/wallets/LightningAddress.tsx
index 3832af2d..be453818 100644
--- a/pages/settings/wallets/LightningAddress.tsx
+++ b/pages/settings/wallets/LightningAddress.tsx
@@ -1,15 +1,15 @@
-import { LightningAddress } from "@getalby/lightning-tools";
import { router, useLocalSearchParams } from "expo-router";
import React from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
+import Alert from "~/components/Alert";
import DismissableKeyboardView from "~/components/DismissableKeyboardView";
+import { AlertCircleIcon } from "~/components/Icons";
import Loading from "~/components/Loading";
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 { useAppStore } from "~/lib/state/appStore";
export function SetLightningAddress() {
@@ -24,51 +24,27 @@ export function SetLightningAddress() {
const updateLightningAddress = async () => {
setLoading(true);
- try {
- if (lightningAddress) {
- const nwcClient = useAppStore.getState().getNWCClient(walletId);
- if (!nwcClient) {
- throw new Error("NWC client not connected");
- }
-
- // by generating an invoice from the lightning address and checking
- // we own it via lookup_invoice, we can prove we own the lightning address
- const _lightningAddress = new LightningAddress(lightningAddress);
- await _lightningAddress.fetch();
- const invoiceFromLightningAddress =
- await _lightningAddress.requestInvoice({ satoshi: 1 });
- let found = false;
- try {
- const transaction = await nwcClient.lookupInvoice({
- payment_hash: invoiceFromLightningAddress.paymentHash,
- });
- found =
- transaction?.invoice === invoiceFromLightningAddress.paymentRequest;
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- } catch (_ /* transaction is not found */) {}
-
- if (!found) {
- throw new Error(
- "Could not verify you are the owner of this lightning address.",
- );
- }
- }
-
- useAppStore.getState().updateWallet({ lightningAddress }, walletId);
- Toast.show({
- type: "success",
- text1: "Lightning address updated",
- });
- router.back();
- } catch (error) {
- errorToast(error);
- }
+ useAppStore.getState().updateWallet({ lightningAddress }, walletId);
+ Toast.show({
+ type: "success",
+ text1: "Lightning address updated",
+ });
+ router.back();
setLoading(false);
};
return (
+
+
+
@@ -92,6 +68,7 @@ export function SetLightningAddress() {