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

feat: modify receive screen for wallets without ln address and set up #224

Merged
merged 10 commits into from
Feb 7, 2025
5 changes: 5 additions & 0 deletions app/(app)/receive/alby-account.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AlbyAccount } from "../../../pages/receive/AlbyAccount";

export default function Page() {
return <AlbyAccount />;
}
5 changes: 5 additions & 0 deletions app/(app)/receive/invoice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Invoice } from "../../../pages/receive/Invoice";

export default function Page() {
return <Invoice />;
}
5 changes: 5 additions & 0 deletions app/(app)/receive/lightning-address.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LightningAddress } from "../../../pages/receive/LightningAddress";

export default function Page() {
return <LightningAddress />;
}
5 changes: 5 additions & 0 deletions app/(app)/receive/withdraw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Withdraw } from "../../../pages/receive/Withdraw";

export default function Page() {
return <Withdraw />;
}
5 changes: 0 additions & 5 deletions app/(app)/withdraw/index.js

This file was deleted.

Binary file added assets/alby-account.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
218 changes: 218 additions & 0 deletions components/CreateInvoice.tsx
Original file line number Diff line number Diff line change
@@ -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 ? (
<>
<View className="flex-1 justify-center items-center gap-8">
<View className="justify-center">
<QRCode value={invoice} />
<View className="absolute self-center p-2 rounded-2xl bg-white">
<Image
source={require("../assets/icon.png")}
className="w-20 h-20 rounded-xl"
resizeMode="contain"
/>
</View>
</View>
<View className="flex flex-col items-center justify-center gap-2">
<View className="flex flex-row items-end">
<Text className="text-foreground text-3xl font-semibold2">
{new Intl.NumberFormat().format(+amount)}{" "}
</Text>
<Text className="text-muted-foreground text-2xl font-semibold2">
sats
</Text>
</View>
{getFiatAmount && (
<Text className="text-muted-foreground text-2xl font-medium2">
{getFiatAmount(+amount)}
</Text>
)}
</View>
<View className="flex flex-row justify-center items-center gap-3">
<Loading />
<Text className="text-xl">Waiting for payment</Text>
</View>
</View>
<View className="flex flex-row gap-3 p-6">
<Button
onPress={share}
variant="secondary"
className="flex-1 flex flex-col gap-2"
>
<ShareIcon className="text-muted-foreground" />
<Text>Share</Text>
</Button>
<Button
variant="secondary"
onPress={copy}
className="flex-1 flex flex-col gap-2"
>
<CopyIcon className="text-muted-foreground" />
<Text>Copy</Text>
</Button>
</View>
</>
) : (
<DismissableKeyboardView>
<View className="flex-1 flex flex-col">
<View className="flex-1 h-full flex flex-col justify-center gap-5 p-3">
<DualCurrencyInput
amount={amount}
setAmount={setAmount}
autoFocus
/>
<View>
<Text className="text-muted-foreground text-center mt-6">
Description (optional)
</Text>
<Input
className="w-full text-center border-transparent bg-transparent native:text-2xl font-semibold2"
placeholder="No description"
value={comment}
onChangeText={setComment}
returnKeyType="done"
/>
</View>
</View>
<View className="m-6">
<Button
size="lg"
className="flex flex-row gap-2"
onPress={() => generateInvoice(+amount)}
disabled={isLoading}
>
{isLoading && <Loading className="text-primary-foreground" />}
<Text>Create Invoice</Text>
</Button>
</View>
</View>
</DismissableKeyboardView>
)}
</>
);
}
12 changes: 12 additions & 0 deletions components/Icons.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
PopiconsAtSymbolSolid as AddressIcon,
PopiconsCircleExclamationLine as AlertCircleIcon,
PopiconsBitcoinSolid as BitcoinIcon,
PopiconsAddressBookSolid as BookUserIcon,
Expand All @@ -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,
Expand Down Expand Up @@ -45,6 +49,7 @@ function interopIcon(icon: React.FunctionComponent<SvgProps>) {
});
}

interopIcon(AddressIcon);
interopIcon(AlertCircleIcon);
interopIcon(BitcoinIcon);
interopIcon(BookUserIcon);
Expand All @@ -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);
Expand All @@ -77,6 +85,7 @@ interopIcon(XIcon);
interopIcon(ZapIcon);

export {
AddressIcon,
AlertCircleIcon,
BitcoinIcon,
BookUserIcon,
Expand All @@ -88,13 +97,16 @@ export {
ExportIcon,
FingerprintIcon,
HelpCircleIcon,
LinkIcon,
MoveDownIcon,
MoveUpIcon,
NotificationIcon,
OnboardingIcon,
PasteIcon,
QRIcon,
RefreshIcon,
ResetIcon,
ScanIcon,
SettingsIcon,
ShareIcon,
SignOutIcon,
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
60 changes: 60 additions & 0 deletions pages/receive/AlbyAccount.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View className="flex-1 flex flex-col">
<Screen title="Get Alby Account" />
<ScrollView contentContainerClassName="flex items-center gap-3 p-6">
<Image
source={require("../../assets/alby-account.png")}
className="my-4"
style={{ width: imageWidth, height: imageWidth }}
/>
<View className="flex-1 flex mt-4 gap-6">
<Text className="font-semibold2 text-3xl text-center text-foreground">
Get your lightning address with Alby Account
</Text>
<View className="flex flex-col gap-2 mb-4">
<Text className="text-xl text-foreground">
{"\u2022 "}Lightning address & Nostr identifier,
</Text>
<Text className="text-xl text-foreground">
{"\u2022 "}Personal tipping page,
</Text>
<Text className="text-xl text-foreground">
{"\u2022 "}Access to podcasting 2.0 apps,
</Text>
<Text className="text-xl text-foreground">
{"\u2022 "}Buy bitcoin directly to your wallet,
</Text>
<Text className="text-xl text-foreground">
{"\u2022 "}Useful email wallet notifications,
</Text>
<Text className="text-xl text-foreground">
{"\u2022 "}Priority support.
</Text>
</View>
</View>
</ScrollView>
<View className="m-6">
<Button
size="lg"
className="flex flex-row gap-2"
onPress={() => openURL("https://getalby.com/")}
>
<Text>Get Alby Account</Text>
<LinkIcon className="text-primary-foreground" />
</Button>
</View>
</View>
);
}
Loading