From a10011ca2873e377ba282d6f65d9bf2b2ca6c762 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 17 Jun 2026 20:15:56 +0530 Subject: [PATCH] chore: add helpers for clipboard copy and paste --- components/CreateInvoice.tsx | 12 +++------- lib/utils.ts | 31 ++++++++++++++++++++++++++ pages/Transaction.tsx | 17 +++++++------- pages/receive/Receive.tsx | 11 +++------ pages/receive/Withdraw.tsx | 17 ++++---------- pages/send/Address.tsx | 13 ++++------- pages/send/Send.tsx | 17 ++++---------- pages/settings/wallets/EditWallet.tsx | 14 +++++------- pages/settings/wallets/SetupWallet.tsx | 14 ++++-------- 9 files changed, 67 insertions(+), 79 deletions(-) diff --git a/components/CreateInvoice.tsx b/components/CreateInvoice.tsx index 38afa1b..56983f5 100644 --- a/components/CreateInvoice.tsx +++ b/components/CreateInvoice.tsx @@ -1,9 +1,7 @@ import type { Nip47Transaction } from "@getalby/sdk"; -import * as Clipboard from "expo-clipboard"; import { router } from "expo-router"; import React from "react"; import { Platform, Share, View } from "react-native"; -import Toast from "react-native-toast-message"; import { DualCurrencyInput } from "~/components/DualCurrencyInput"; import { CopyIcon, ShareIcon } from "~/components/Icons"; import Loading from "~/components/Loading"; @@ -13,7 +11,7 @@ import { Text } from "~/components/ui/text"; import { useGetFiatAmount } from "~/hooks/useGetFiatAmount"; import { errorToast } from "~/lib/errorToast"; import { useAppStore } from "~/lib/state/appStore"; -import { cn, formatBitcoinAmount } from "~/lib/utils"; +import { cn, copyToClipboard, formatBitcoinAmount } from "~/lib/utils"; export function CreateInvoice() { const getFiatAmount = useGetFiatAmount(); @@ -53,17 +51,13 @@ export function CreateInvoice() { })(); } - function copy() { + async 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", - }); + await copyToClipboard(text); } async function share() { diff --git a/lib/utils.ts b/lib/utils.ts index 1f1c675..9045bf3 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -5,8 +5,11 @@ import { sha256 } from "@noble/hashes/sha2.js"; import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils.js"; import { Buffer } from "buffer"; import { clsx, type ClassValue } from "clsx"; +import * as Clipboard from "expo-clipboard"; import { getPublicKey, nip19 } from "nostr-tools"; +import Toast from "react-native-toast-message"; import { twMerge } from "tailwind-merge"; +import { errorToast } from "~/lib/errorToast"; import { BitcoinDisplayFormat } from "~/lib/state/appStore"; export function cn(...inputs: ClassValue[]) { @@ -39,6 +42,34 @@ export function safeNpubEncode(hex: string): string | undefined { } } +export async function copyToClipboard( + text: string, + successMessage = "Copied to clipboard", +) { + // Clipboard.setStringAsync always resolves to true in iOS and + // android so we don't have to add a catch block for errors + await Clipboard.setStringAsync(text); + Toast.show({ + type: "success", + text1: successMessage, + }); +} + +export async function readClipboardText() { + try { + const text = await Clipboard.getStringAsync(); + if (!text) { + errorToast(new Error("Your clipboard is empty.")); + return undefined; + } + return text; + } catch (error) { + console.error("Failed to read clipboard", error); + errorToast(error, "Failed to read clipboard"); + return undefined; + } +} + export function formatBitcoinAmount( amount: number, displayFormat: BitcoinDisplayFormat = "bip177", diff --git a/pages/Transaction.tsx b/pages/Transaction.tsx index cc45812..23f5cc9 100644 --- a/pages/Transaction.tsx +++ b/pages/Transaction.tsx @@ -1,7 +1,6 @@ import type { Nip47Transaction, Nip47TransactionMetadata } from "@getalby/sdk"; import { hexToBytes } from "@noble/hashes/utils.js"; import dayjs from "dayjs"; -import * as Clipboard from "expo-clipboard"; import { Link, useLocalSearchParams } from "expo-router"; import { nip19 } from "nostr-tools"; import React from "react"; @@ -12,7 +11,6 @@ import { TouchableOpacity, View, } from "react-native"; -import Toast from "react-native-toast-message"; import { LinkIcon } from "~/components/Icons"; import AcceptedTransactionIcon from "~/components/icons/AcceptedTransaction"; import FailedTransactionIcon from "~/components/icons/FailedTransaction"; @@ -23,7 +21,12 @@ import Screen from "~/components/Screen"; import { Text } from "~/components/ui/text"; import { useGetFiatAmount } from "~/hooks/useGetFiatAmount"; import { BitcoinDisplayFormat, useAppStore } from "~/lib/state/appStore"; -import { cn, formatBitcoinAmount, safeNpubEncode } from "~/lib/utils"; +import { + cn, + copyToClipboard, + formatBitcoinAmount, + safeNpubEncode, +} from "~/lib/utils"; type TLVRecord = { type: number; @@ -308,12 +311,8 @@ function TransactionDetailRow(props: { {props.title} { - Clipboard.setStringAsync(props.content); - Toast.show({ - type: "success", - text1: "Copied to clipboard", - }); + onPress={async () => { + await copyToClipboard(props.content); }} > diff --git a/pages/receive/Receive.tsx b/pages/receive/Receive.tsx index d3ae824..26c76bb 100644 --- a/pages/receive/Receive.tsx +++ b/pages/receive/Receive.tsx @@ -1,8 +1,6 @@ -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 { CreateInvoice } from "~/components/CreateInvoice"; import { AddressIcon, @@ -18,23 +16,20 @@ import { Button } from "~/components/ui/button"; import { Text } from "~/components/ui/text"; import { errorToast } from "~/lib/errorToast"; import { useAppStore } from "~/lib/state/appStore"; +import { copyToClipboard } from "~/lib/utils"; export function Receive() { const selectedWalletId = useAppStore((store) => store.selectedWalletId); const wallets = useAppStore((store) => store.wallets); const lightningAddress = wallets[selectedWalletId].lightningAddress; - function copy() { + async function copy() { const text = lightningAddress; if (!text) { errorToast(new Error("Nothing to copy")); return; } - Clipboard.setStringAsync(text); - Toast.show({ - type: "success", - text1: "Copied to clipboard", - }); + await copyToClipboard(text); } async function share() { diff --git a/pages/receive/Withdraw.tsx b/pages/receive/Withdraw.tsx index f29742f..c24b960 100644 --- a/pages/receive/Withdraw.tsx +++ b/pages/receive/Withdraw.tsx @@ -1,4 +1,3 @@ -import * as Clipboard from "expo-clipboard"; import { router, useLocalSearchParams } from "expo-router"; import { lnurl as lnurlLib, @@ -16,6 +15,7 @@ import { Text } from "~/components/ui/text"; import { WalletSwitcher } from "~/components/WalletSwitcher"; import { errorToast } from "~/lib/errorToast"; import { useAppStore } from "~/lib/state/appStore"; +import { readClipboardText } from "~/lib/utils"; export function Withdraw() { const { url } = useLocalSearchParams<{ url: string }>(); @@ -57,14 +57,10 @@ export function Withdraw() { }, [url]); async function paste() { - let clipboardText; - try { - clipboardText = await Clipboard.getStringAsync(); - } catch (error) { - console.error("Failed to read clipboard", error); - return; + const clipboardText = await readClipboardText(); + if (clipboardText) { + loadWithdrawal(clipboardText); } - loadWithdrawal(clipboardText); } const handleScanned = (data: string) => { @@ -72,11 +68,6 @@ export function Withdraw() { }; async function loadWithdrawal(text: string): Promise { - if (!text) { - errorToast(new Error("Your clipboard is empty.")); - return false; - } - text = text.toLowerCase(); console.info("loading withdrawal", text); diff --git a/pages/send/Address.tsx b/pages/send/Address.tsx index 8915b77..c07271e 100644 --- a/pages/send/Address.tsx +++ b/pages/send/Address.tsx @@ -6,7 +6,6 @@ import { BottomSheetTextInput, BottomSheetView, } from "@gorhom/bottom-sheet"; -import * as Clipboard from "expo-clipboard"; import React, { useCallback, useRef, useState } from "react"; import { Keyboard, @@ -27,7 +26,7 @@ import { Text } from "~/components/ui/text"; import { initiatePaymentFlow } from "~/lib/initiatePaymentFlow"; import { useAppStore } from "~/lib/state/appStore"; import { useThemeColor } from "~/lib/useThemeColor"; -import { cn } from "~/lib/utils"; +import { cn, readClipboardText } from "~/lib/utils"; interface ContactInputProps { lnAddress: string; @@ -204,14 +203,10 @@ export function Address() { }, [addressBookEntries, keyboardText]); const paste = async () => { - let clipboardText; - try { - clipboardText = await Clipboard.getStringAsync(); - } catch (error) { - console.error("Failed to read clipboard", error); - return; + const clipboardText = await readClipboardText(); + if (clipboardText) { + setKeyboardText(clipboardText); } - setKeyboardText(clipboardText); }; return ( diff --git a/pages/send/Send.tsx b/pages/send/Send.tsx index b82177f..c435416 100644 --- a/pages/send/Send.tsx +++ b/pages/send/Send.tsx @@ -1,6 +1,5 @@ import { NWAClient } from "@getalby/sdk/nwc"; import { Camera } from "expo-camera"; -import * as Clipboard from "expo-clipboard"; import * as ImagePicker from "expo-image-picker"; import { router, useLocalSearchParams } from "expo-router"; import React from "react"; @@ -13,6 +12,7 @@ import { Button } from "~/components/ui/button"; import { Text } from "~/components/ui/text"; import { errorToast } from "~/lib/errorToast"; import { initiatePaymentFlow } from "~/lib/initiatePaymentFlow"; +import { readClipboardText } from "~/lib/utils"; export function Send() { const { url, amount } = useLocalSearchParams<{ @@ -63,14 +63,10 @@ export function Send() { } async function paste() { - let clipboardText; - try { - clipboardText = await Clipboard.getStringAsync(); - } catch (error) { - console.error("Failed to read clipboard", error); - return; + const clipboardText = await readClipboardText(); + if (clipboardText) { + await loadPayment(clipboardText); } - await loadPayment(clipboardText); } const handleScanned = async (data: string) => { @@ -78,11 +74,6 @@ export function Send() { }; const loadPayment = async (text: string, amount = "") => { - if (!text) { - errorToast(new Error("Your clipboard is empty.")); - return false; - } - if (text.startsWith("nostr+walletauth") /* can have : or +alby: */) { const nwaOptions = NWAClient.parseWalletAuthUrl(text); router.replace({ diff --git a/pages/settings/wallets/EditWallet.tsx b/pages/settings/wallets/EditWallet.tsx index e3d3501..94f9550 100644 --- a/pages/settings/wallets/EditWallet.tsx +++ b/pages/settings/wallets/EditWallet.tsx @@ -1,4 +1,3 @@ -import * as Clipboard from "expo-clipboard"; import { Link, router, useLocalSearchParams } from "expo-router"; import React, { useState } from "react"; import { @@ -25,7 +24,7 @@ import { Text } from "~/components/ui/text"; import { IS_EXPO_GO, REQUIRED_CAPABILITIES } from "~/lib/constants"; import { deregisterWalletNotifications } from "~/lib/notifications"; import { useAppStore } from "~/lib/state/appStore"; -import { cn } from "~/lib/utils"; +import { cn, copyToClipboard } from "~/lib/utils"; export function EditWallet() { const { id } = useLocalSearchParams() as { id: string }; @@ -171,7 +170,7 @@ export function EditWallet() { }, { text: "Confirm", - onPress: () => { + onPress: async () => { const isSuperuser = useAppStore .getState() .wallets[ @@ -195,11 +194,10 @@ export function EditWallet() { if (!nwcUrl) { return; } - Clipboard.setStringAsync(nwcUrl); - Toast.show({ - type: "success", - text1: "Connection Secret copied to clipboard", - }); + await copyToClipboard( + nwcUrl, + "Connection Secret copied to clipboard", + ); }, }, ], diff --git a/pages/settings/wallets/SetupWallet.tsx b/pages/settings/wallets/SetupWallet.tsx index 04f9545..579fe5d 100644 --- a/pages/settings/wallets/SetupWallet.tsx +++ b/pages/settings/wallets/SetupWallet.tsx @@ -1,5 +1,4 @@ import { NWCClient, type Nip47Capability } from "@getalby/sdk/nwc"; -import * as Clipboard from "expo-clipboard"; import { router, useLocalSearchParams } from "expo-router"; import { useAppStore } from "lib/state/appStore"; import React from "react"; @@ -23,7 +22,7 @@ import { Text } from "~/components/ui/text"; import { IS_EXPO_GO, REQUIRED_CAPABILITIES } from "~/lib/constants"; import { errorToast } from "~/lib/errorToast"; import { registerWalletNotifications } from "~/lib/notifications"; -import { cn } from "~/lib/utils"; +import { cn, readClipboardText } from "~/lib/utils"; export function SetupWallet() { const { nwcUrl: nwcUrlFromSchemeLink } = useLocalSearchParams<{ @@ -48,15 +47,10 @@ export function SetupWallet() { }; async function paste() { - let nostrWalletConnectUrl; - try { - nostrWalletConnectUrl = await Clipboard.getStringAsync(); - } catch (error) { - console.error("Failed to read clipboard", error); - errorToast(error, "Failed to read clipboard"); - return; + const nostrWalletConnectUrl = await readClipboardText(); + if (nostrWalletConnectUrl) { + connect(nostrWalletConnectUrl); } - connect(nostrWalletConnectUrl); } const connect = React.useCallback(