From e4fef6d395406f752845c1d9a31a6ed764619e87 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sun, 12 May 2024 22:54:27 +0700 Subject: [PATCH 1/3] feat: address book --- app/receive/index.js | 2 +- app/receive/success.js | 2 +- app/send/address-book.js | 5 ++ app/send/confirm.js | 2 +- app/send/index.js | 2 +- app/send/lnurl-pay.js | 2 +- app/send/success.js | 2 +- app/settings/address-book/new.js | 5 ++ app/settings/lightning-address.js | 5 -- app/settings/wallet-connection.js | 5 -- .../wallets/[id]/lightning-address.js | 5 ++ .../wallets/[id]/wallet-connection.js | 5 ++ components/Icons.tsx | 9 +++ components/ToastConfig.tsx | 44 +++++++----- lib/state/appStore.ts | 39 +++++++++++ pages/Home.tsx | 5 +- pages/Transaction.tsx | 11 +-- pages/{ => receive}/Receive.tsx | 5 +- pages/{ => receive}/ReceiveSuccess.tsx | 0 pages/send/AddressBook.tsx | 62 ++++++++++++++++ pages/{ => send}/ConfirmPayment.tsx | 2 +- pages/{ => send}/LNURLPay.tsx | 0 pages/{ => send}/PaymentSuccess.tsx | 8 ++- pages/{ => send}/Send.tsx | 34 +++++---- .../address-book/NewAddressBookEntry.tsx | 70 +++++++++++++++++++ pages/settings/wallets/EditWallet.tsx | 10 ++- .../{ => wallets}/LightningAddress.tsx | 0 .../{ => wallets}/WalletConnection.tsx | 0 28 files changed, 281 insertions(+), 60 deletions(-) create mode 100644 app/send/address-book.js create mode 100644 app/settings/address-book/new.js delete mode 100644 app/settings/lightning-address.js delete mode 100644 app/settings/wallet-connection.js create mode 100644 app/settings/wallets/[id]/lightning-address.js create mode 100644 app/settings/wallets/[id]/wallet-connection.js rename pages/{ => receive}/Receive.tsx (98%) rename pages/{ => receive}/ReceiveSuccess.tsx (100%) create mode 100644 pages/send/AddressBook.tsx rename pages/{ => send}/ConfirmPayment.tsx (97%) rename pages/{ => send}/LNURLPay.tsx (100%) rename pages/{ => send}/PaymentSuccess.tsx (88%) rename pages/{ => send}/Send.tsx (87%) create mode 100644 pages/settings/address-book/NewAddressBookEntry.tsx rename pages/settings/{ => wallets}/LightningAddress.tsx (100%) rename pages/settings/{ => wallets}/WalletConnection.tsx (100%) diff --git a/app/receive/index.js b/app/receive/index.js index caf5512..67caa6e 100644 --- a/app/receive/index.js +++ b/app/receive/index.js @@ -1,4 +1,4 @@ -import { Receive } from "../../pages/Receive"; +import { Receive } from "../../pages/receive/Receive"; export default function Page() { return ; diff --git a/app/receive/success.js b/app/receive/success.js index 9f9d94d..4f575d3 100644 --- a/app/receive/success.js +++ b/app/receive/success.js @@ -1,4 +1,4 @@ -import { ReceiveSuccess } from "../../pages/ReceiveSuccess"; +import { ReceiveSuccess } from "../../pages/receive/ReceiveSuccess"; export default function Page() { return ; diff --git a/app/send/address-book.js b/app/send/address-book.js new file mode 100644 index 0000000..468154a --- /dev/null +++ b/app/send/address-book.js @@ -0,0 +1,5 @@ +import { AddressBook } from "../../pages/send/AddressBook"; + +export default function Page() { + return ; +} diff --git a/app/send/confirm.js b/app/send/confirm.js index 0e2c708..805f614 100644 --- a/app/send/confirm.js +++ b/app/send/confirm.js @@ -1,4 +1,4 @@ -import { ConfirmPayment } from "../../pages/ConfirmPayment"; +import { ConfirmPayment } from "../../pages/send/ConfirmPayment"; export default function Page() { return ; diff --git a/app/send/index.js b/app/send/index.js index b30e0c1..9728af5 100644 --- a/app/send/index.js +++ b/app/send/index.js @@ -1,4 +1,4 @@ -import { Send } from "../../pages/Send"; +import { Send } from "../../pages/send/Send"; export default function Page() { return ; diff --git a/app/send/lnurl-pay.js b/app/send/lnurl-pay.js index 87ae0b6..b4996f3 100644 --- a/app/send/lnurl-pay.js +++ b/app/send/lnurl-pay.js @@ -1,4 +1,4 @@ -import { LNURLPay } from "../../pages/LNURLPay"; +import { LNURLPay } from "../../pages/send/LNURLPay"; export default function Page() { return ; diff --git a/app/send/success.js b/app/send/success.js index ab12d89..2211d52 100644 --- a/app/send/success.js +++ b/app/send/success.js @@ -1,4 +1,4 @@ -import { PaymentSuccess } from "../../pages/PaymentSuccess"; +import { PaymentSuccess } from "../../pages/send/PaymentSuccess"; export default function Page() { return ; diff --git a/app/settings/address-book/new.js b/app/settings/address-book/new.js new file mode 100644 index 0000000..b5370f6 --- /dev/null +++ b/app/settings/address-book/new.js @@ -0,0 +1,5 @@ +import { NewAddressBookEntry } from "../../../pages/settings/address-book/NewAddressBookEntry"; + +export default function Page() { + return ; +} diff --git a/app/settings/lightning-address.js b/app/settings/lightning-address.js deleted file mode 100644 index 6f4defc..0000000 --- a/app/settings/lightning-address.js +++ /dev/null @@ -1,5 +0,0 @@ -import { LightningAddress } from "../../pages/settings/LightningAddress"; - -export default function Page() { - return ; -} diff --git a/app/settings/wallet-connection.js b/app/settings/wallet-connection.js deleted file mode 100644 index 198f129..0000000 --- a/app/settings/wallet-connection.js +++ /dev/null @@ -1,5 +0,0 @@ -import { WalletConnection } from "../../pages/settings/WalletConnection"; - -export default function Page() { - return ; -} diff --git a/app/settings/wallets/[id]/lightning-address.js b/app/settings/wallets/[id]/lightning-address.js new file mode 100644 index 0000000..48445ed --- /dev/null +++ b/app/settings/wallets/[id]/lightning-address.js @@ -0,0 +1,5 @@ +import { LightningAddress } from "../../../../pages/settings/wallets/LightningAddress"; + +export default function Page() { + return ; +} diff --git a/app/settings/wallets/[id]/wallet-connection.js b/app/settings/wallets/[id]/wallet-connection.js new file mode 100644 index 0000000..8cf5f24 --- /dev/null +++ b/app/settings/wallets/[id]/wallet-connection.js @@ -0,0 +1,5 @@ +import { WalletConnection } from "../../../../pages/settings/wallets/WalletConnection"; + +export default function Page() { + return ; +} diff --git a/components/Icons.tsx b/components/Icons.tsx index 4072259..15963db 100644 --- a/components/Icons.tsx +++ b/components/Icons.tsx @@ -16,6 +16,9 @@ import { ArrowLeftRight, PlusCircle, Cog, + ClipboardPaste, + Keyboard, + BookUser, } from "lucide-react-native"; import { cssInterop } from "nativewind"; @@ -47,6 +50,9 @@ interopIcon(Settings2); interopIcon(ArrowLeftRight); interopIcon(PlusCircle); interopIcon(Cog); +interopIcon(ClipboardPaste); +interopIcon(Keyboard); +interopIcon(BookUser); export { AlertCircle, @@ -65,4 +71,7 @@ export { ArrowLeftRight, PlusCircle, Cog, + ClipboardPaste, + Keyboard, + BookUser, }; diff --git a/components/ToastConfig.tsx b/components/ToastConfig.tsx index 7b6b353..1d89886 100644 --- a/components/ToastConfig.tsx +++ b/components/ToastConfig.tsx @@ -12,6 +12,7 @@ import { View } from "react-native"; import { CheckCircle, XCircle } from "./Icons"; import { Link } from "expo-router"; import { Button } from "./ui/button"; +import { useAppStore } from "~/lib/state/appStore"; export const toastConfig: ToastConfig = { success: ({ text1, text2 }) => ( @@ -39,22 +40,29 @@ export const toastConfig: ToastConfig = { ), - connectionError: ({ text1, text2, hide }) => ( - - - - - {text1} - - - - - - - - - - ), + connectionError: ({ text1, text2, hide }) => { + const selectedWalletId = useAppStore((store) => store.selectedWalletId); + return ( + + + + + {" "} + {text1} + + + + + + + + + + ); + }, }; diff --git a/lib/state/appStore.ts b/lib/state/appStore.ts index 85abbf6..bf273ed 100644 --- a/lib/state/appStore.ts +++ b/lib/state/appStore.ts @@ -2,12 +2,14 @@ import { create } from "zustand"; import { NWCClient, Nip47Capability } from "@getalby/sdk/dist/NWCClient"; import { nwc } from "@getalby/sdk"; import { secureStorage } from "lib/secureStorage"; +import { NewAddressBookEntry } from "~/pages/settings/address-book/NewAddressBookEntry"; interface AppState { readonly nwcClient: NWCClient | undefined; readonly fiatCurrency: string; readonly selectedWalletId: number; readonly wallets: Wallet[]; + readonly addressBookEntries: AddressBookEntry[]; setNWCClient: (nwcClient: NWCClient | undefined) => void; setNostrWalletConnectUrl(nostrWalletConnectUrl: string): void; removeNostrWalletConnectUrl(): void; @@ -15,9 +17,11 @@ interface AppState { setFiatCurrency(fiatCurrency: string): void; setSelectedWalletId(walletId: number): void; addWallet(wallet: Wallet): void; + addAddressBookEntry(entry: AddressBookEntry): void; } const walletKeyPrefix = "wallet"; +const addressBookEntryKeyPrefix = "addressBookEntry"; const selectedWalletIdKey = "selectedWalletId"; const fiatCurrencyKey = "fiatCurrency"; @@ -28,10 +32,19 @@ type Wallet = { nwcCapabilities?: Nip47Capability[]; }; +type AddressBookEntry = { + name?: string; + lightningAddress?: string; +}; + const getWalletKey = (walletId: number) => { return walletKeyPrefix + walletId; }; +const getAddressBookEntryKey = (addressBookEntryId: number) => { + return addressBookEntryKeyPrefix + addressBookEntryId; +}; + function loadWallets(): Wallet[] { // TODO: remove after a while - migrates from old single-wallet format ///////////////////////////// @@ -71,6 +84,20 @@ function loadWallets(): Wallet[] { return wallets; } +function loadAddressBookEntries(): AddressBookEntry[] { + const addressBookEntries: AddressBookEntry[] = []; + for (let i = 0; ; i++) { + const addressBookEntryJSON = secureStorage.getItem( + getAddressBookEntryKey(i) + ); + if (!addressBookEntryJSON) { + break; + } + addressBookEntries.push(JSON.parse(addressBookEntryJSON)); + } + return addressBookEntries; +} + export const useAppStore = create()((set, get) => { const updateCurrentWallet = (walletUpdate: Partial) => { const selectedWalletId = get().selectedWalletId; @@ -95,6 +122,7 @@ export const useAppStore = create()((set, get) => { ); const initialWallets = loadWallets(); return { + addressBookEntries: loadAddressBookEntries(), wallets: initialWallets, nwcClient: getNWCClient(initialSelectedWalletId), fiatCurrency: secureStorage.getItem(fiatCurrencyKey) || "", @@ -134,6 +162,17 @@ export const useAppStore = create()((set, get) => { nwcClient: undefined, }); }, + addAddressBookEntry: (addressBookEntry: AddressBookEntry) => { + const currentAddressBookEntries = get().addressBookEntries; + const newAddressBookEntryId = currentAddressBookEntries.length; + secureStorage.setItem( + getAddressBookEntryKey(newAddressBookEntryId), + JSON.stringify(addressBookEntry) + ); + set({ + addressBookEntries: [...currentAddressBookEntries, addressBookEntry], + }); + }, }; }); diff --git a/pages/Home.tsx b/pages/Home.tsx index 7e23169..3c8d9f5 100644 --- a/pages/Home.tsx +++ b/pages/Home.tsx @@ -9,7 +9,7 @@ import { import React from "react"; import { useBalance } from "hooks/useBalance"; import { useAppStore } from "lib/state/appStore"; -import { WalletConnection } from "pages/settings/WalletConnection"; +import { WalletConnection } from "~/pages/settings/wallets/WalletConnection"; import { useTransactions } from "hooks/useTransactions"; import { Link, Stack, router, useFocusEffect } from "expo-router"; import dayjs from "dayjs"; @@ -55,6 +55,9 @@ export function Home() { }, [allTransactions, transactions, refreshingTransactions]); const onRefresh = React.useCallback(() => { + if (refreshingTransactions) { + return; + } (async () => { setRefreshingTransactions(true); setPage(1); diff --git a/pages/Transaction.tsx b/pages/Transaction.tsx index 740fdc3..375f13c 100644 --- a/pages/Transaction.tsx +++ b/pages/Transaction.tsx @@ -1,11 +1,9 @@ import { Nip47Transaction } from "@getalby/sdk/dist/NWCClient"; import dayjs from "dayjs"; import { Stack, useLocalSearchParams } from "expo-router"; -import { Scroll } from "lucide-react-native"; import React from "react"; -import { ScrollView, View } from "react-native"; -import { CheckCircle, MoveDownLeft } from "~/components/Icons"; -import { Separator } from "~/components/ui/separator"; +import { View } from "react-native"; +import { CheckCircle } from "~/components/Icons"; import { Text } from "~/components/ui/text"; import { useGetFiatAmount } from "~/hooks/useGetFiatAmount"; import { cn } from "~/lib/utils"; @@ -67,7 +65,10 @@ export function Transaction() { title="Payment Hash" content={transaction.payment_hash} /> - + diff --git a/pages/Receive.tsx b/pages/receive/Receive.tsx similarity index 98% rename from pages/Receive.tsx rename to pages/receive/Receive.tsx index 66acd73..badb5dc 100644 --- a/pages/Receive.tsx +++ b/pages/receive/Receive.tsx @@ -180,7 +180,10 @@ export function Receive() { Receive Quickly with a Lightning Address - + diff --git a/pages/ReceiveSuccess.tsx b/pages/receive/ReceiveSuccess.tsx similarity index 100% rename from pages/ReceiveSuccess.tsx rename to pages/receive/ReceiveSuccess.tsx diff --git a/pages/send/AddressBook.tsx b/pages/send/AddressBook.tsx new file mode 100644 index 0000000..703a961 --- /dev/null +++ b/pages/send/AddressBook.tsx @@ -0,0 +1,62 @@ +import { Link, Stack, router } from "expo-router"; +import { Pressable, View } from "react-native"; +import { PlusCircle } from "~/components/Icons"; + +import { + Card, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { Text } from "~/components/ui/text"; +import { useAppStore } from "~/lib/state/appStore"; + +export function AddressBook() { + const addressBookEntries = useAppStore((store) => store.addressBookEntries); + return ( + + + {addressBookEntries.map((addressBookEntry, index) => ( + { + router.dismissAll(); + router.navigate({ + pathname: "/send", + params: { + url: addressBookEntry.lightningAddress, + }, + }); + }} + > + + + + {addressBookEntry.name || addressBookEntry.lightningAddress} + + + {addressBookEntry.lightningAddress} + + + + + ))} + + + + + + + Add Address + + + + + + + ); +} diff --git a/pages/ConfirmPayment.tsx b/pages/send/ConfirmPayment.tsx similarity index 97% rename from pages/ConfirmPayment.tsx rename to pages/send/ConfirmPayment.tsx index f219ff3..501cf9b 100644 --- a/pages/ConfirmPayment.tsx +++ b/pages/send/ConfirmPayment.tsx @@ -33,7 +33,7 @@ export function ConfirmPayment() { router.dismissAll(); router.replace({ pathname: "/send/success", - params: { preimage: response.preimage }, + params: { preimage: response.preimage, originalText }, }); } catch (error) { console.error(error); diff --git a/pages/LNURLPay.tsx b/pages/send/LNURLPay.tsx similarity index 100% rename from pages/LNURLPay.tsx rename to pages/send/LNURLPay.tsx diff --git a/pages/PaymentSuccess.tsx b/pages/send/PaymentSuccess.tsx similarity index 88% rename from pages/PaymentSuccess.tsx rename to pages/send/PaymentSuccess.tsx index 6506f8e..062fd09 100644 --- a/pages/PaymentSuccess.tsx +++ b/pages/send/PaymentSuccess.tsx @@ -8,7 +8,10 @@ import Toast from "react-native-toast-message"; import { Copy } from "~/components/Icons"; export function PaymentSuccess() { - const { preimage } = useLocalSearchParams() as { preimage: string }; + const { preimage, originalText } = useLocalSearchParams() as { + preimage: string; + originalText: string; + }; return ( + + Successful payment to {originalText} + { diff --git a/pages/Send.tsx b/pages/send/Send.tsx similarity index 87% rename from pages/Send.tsx rename to pages/send/Send.tsx index e7a3b26..e6f5efa 100644 --- a/pages/Send.tsx +++ b/pages/send/Send.tsx @@ -1,17 +1,16 @@ import { BarCodeScanningResult, Camera } from "expo-camera/legacy"; // TODO: check if Android camera detach bug is fixed and update camera import React, { useEffect } from "react"; -import { - ActivityIndicator, - Keyboard, - TouchableOpacity, - TouchableWithoutFeedback, - View, -} from "react-native"; +import { Keyboard, TouchableWithoutFeedback, View } from "react-native"; import * as Clipboard from "expo-clipboard"; import { lnurl } from "lib/lnurl"; import { Button } from "~/components/ui/button"; -import { Camera as CameraIcon } from "~/components/Icons"; -import { Stack, router, useLocalSearchParams } from "expo-router"; +import { + BookUser, + Camera as CameraIcon, + ClipboardPaste, + Keyboard as KeyboardIcon, +} from "~/components/Icons"; +import { Link, Stack, router, useLocalSearchParams } from "expo-router"; import { Text } from "~/components/ui/text"; import { Input } from "~/components/ui/input"; import { errorToast } from "~/lib/errorToast"; @@ -98,7 +97,7 @@ export function Send() { throw new Error("LNURL tag " + lnurlDetails.tag + " not supported"); } - router.push({ + router.replace({ pathname: "/send/lnurl-pay", params: { lnurlDetailsJSON: JSON.stringify(lnurlDetails), @@ -106,7 +105,7 @@ export function Send() { }, }); } else { - router.push({ + router.replace({ pathname: "/send/confirm", params: { invoice: text, originalText }, }); @@ -114,8 +113,8 @@ export function Send() { } catch (error) { console.error(error); errorToast(error as Error); + setLoading(false); } - setLoading(false); } return ( @@ -135,13 +134,18 @@ export function Send() { {isScanning && ( <> - + + + + )} diff --git a/pages/settings/address-book/NewAddressBookEntry.tsx b/pages/settings/address-book/NewAddressBookEntry.tsx new file mode 100644 index 0000000..fd44989 --- /dev/null +++ b/pages/settings/address-book/NewAddressBookEntry.tsx @@ -0,0 +1,70 @@ +import { Stack, router } from "expo-router"; +import React from "react"; +import { Keyboard, TouchableWithoutFeedback, View } from "react-native"; +import Toast from "react-native-toast-message"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Text } from "~/components/ui/text"; +import { useAppStore } from "~/lib/state/appStore"; + +export function NewAddressBookEntry() { + const [name, setName] = React.useState(""); + const [lightningAddress, setLightningAddress] = React.useState(""); + return ( + { + Keyboard.dismiss(); + }} + > + + + + + + + + + + + ); +} diff --git a/pages/settings/wallets/EditWallet.tsx b/pages/settings/wallets/EditWallet.tsx index d8401ba..c74a162 100644 --- a/pages/settings/wallets/EditWallet.tsx +++ b/pages/settings/wallets/EditWallet.tsx @@ -48,7 +48,10 @@ export function EditWallet() { - + @@ -60,7 +63,10 @@ export function EditWallet() { - + diff --git a/pages/settings/LightningAddress.tsx b/pages/settings/wallets/LightningAddress.tsx similarity index 100% rename from pages/settings/LightningAddress.tsx rename to pages/settings/wallets/LightningAddress.tsx diff --git a/pages/settings/WalletConnection.tsx b/pages/settings/wallets/WalletConnection.tsx similarity index 100% rename from pages/settings/WalletConnection.tsx rename to pages/settings/wallets/WalletConnection.tsx From 706a3359b1051666ee154c3dc9613ae07f1ac6f2 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sun, 12 May 2024 22:58:40 +0700 Subject: [PATCH 2/3] fix: migrate old wallet --- lib/state/appStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/state/appStore.ts b/lib/state/appStore.ts index bf273ed..d6e3683 100644 --- a/lib/state/appStore.ts +++ b/lib/state/appStore.ts @@ -52,10 +52,10 @@ function loadWallets(): Wallet[] { const oldNostrWalletConnectUrl = secureStorage.getItem( oldNostrWalletConnectUrlKey ); - const oldLightningAddressKey = "lightningAddressKey"; + const oldLightningAddressKey = "lightningAddress"; const oldLightningAddress = secureStorage.getItem(oldLightningAddressKey); - const oldNwcCapabilitiesKey = "nwcCapabilitiesKey"; + const oldNwcCapabilitiesKey = "nwcCapabilities"; const oldNwcCapabilities = secureStorage.getItem(oldNwcCapabilitiesKey); if (oldNostrWalletConnectUrl) { const wallet: Wallet = { From 90148a522b22b42d174355d87a758a1f15542aab Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Sun, 12 May 2024 22:59:38 +0700 Subject: [PATCH 3/3] chore: bump version --- app.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app.json b/app.json index 197a58f..52f4c33 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Alby Mobile", "slug": "alby-mobile", - "version": "1.1.0", + "version": "1.2.0", "scheme": ["bitcoin", "lightning", "alby"], "orientation": "portrait", "icon": "./assets/icon.png", diff --git a/package.json b/package.json index ce6e0a5..c0e8c7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alby-mobile", - "version": "1.1.0", + "version": "1.2.0", "main": "expo-router/entry", "scripts": { "start": "expo start",