From 6d55bda3aafaece9b9a6f1760a0b60c3e95a3e7f Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 17 Jun 2026 14:30:49 +0530 Subject: [PATCH 1/2] feat: add paste option in dual currency input --- components/DualCurrencyInput.tsx | 289 +++++++++++++++++++++---------- package.json | 1 + yarn.lock | 5 + 3 files changed, 201 insertions(+), 94 deletions(-) diff --git a/components/DualCurrencyInput.tsx b/components/DualCurrencyInput.tsx index d17fa69..6f8189f 100644 --- a/components/DualCurrencyInput.tsx +++ b/components/DualCurrencyInput.tsx @@ -5,6 +5,8 @@ import { BottomSheetTextInput, BottomSheetView, } from "@gorhom/bottom-sheet"; +import * as Clipboard from "expo-clipboard"; +import * as Haptics from "expo-haptics"; import React, { useCallback, useRef, useState } from "react"; import { Keyboard, @@ -214,6 +216,7 @@ export function DualCurrencyInput({ ); const [text, setText] = React.useState(amount); const [inputMode, setInputMode] = React.useState<"sats" | "fiat">("sats"); + const [showPasteAction, setShowPasteAction] = React.useState(false); const formatPlaceholderNumber = React.useCallback((value: number) => { const suffixes = { M: 1_000_000, k: 1_000 }; @@ -283,6 +286,73 @@ export function DualCurrencyInput({ ); }, [bitcoinDisplayFormat]); + const applyPastedValue = useCallback( + (rawValue: string) => { + if (inputMode === "sats") { + const [integerPart = ""] = rawValue.split("."); + const next = integerPart.replace(/\D/g, "").replace(/^0+/, ""); + if (!next) { + errorToast( + new Error("Your clipboard does not contain a valid amount."), + ); + return; + } + if (Number(next) > MAX_SATS_THRESHOLD) { + showSatsThresholdToast(); + return; + } + setText(next); + setAmount(next); + return; + } + + const sanitized = rawValue.replace(/,/g, "").replace(/[^\d.]/g, ""); + + const [integerPart = "", ...decimalParts] = sanitized.split("."); + const hasDecimal = decimalParts.length > 0; + + const normalizedInteger = integerPart.replace(/^0+(?=\d)/, ""); + const normalizedDecimal = decimalParts.join("").slice(0, 2); + + const next = hasDecimal + ? `${normalizedInteger || "0"}.${normalizedDecimal}` + : normalizedInteger; + + if (!next) { + errorToast( + new Error("Your clipboard does not contain a valid amount."), + ); + return; + } + + const satsAmount = getSatsAmount?.(+next); + if (satsAmount !== undefined && satsAmount > MAX_SATS_THRESHOLD) { + showSatsThresholdToast(); + return; + } + + setText(next); + if (getSatsAmount) { + setAmount(satsAmount?.toString() || ""); + } + }, + [getSatsAmount, inputMode, setAmount, showSatsThresholdToast], + ); + + const paste = useCallback(async () => { + let clipboardText; + try { + clipboardText = await Clipboard.getStringAsync(); + } catch (error) { + console.error("Failed to read clipboard", error); + errorToast(new Error("Failed to read clipboard.")); + return; + } + + applyPastedValue(clipboardText); + setShowPasteAction(false); + }, [applyPastedValue]); + const handleKeyPress = (key: string) => { if (inputMode === "sats") { let next; @@ -357,6 +427,12 @@ export function DualCurrencyInput({ return ( + {showPasteAction && ( + setShowPasteAction(false)} + /> + )} {validationMessage !== "" && ( @@ -364,102 +440,127 @@ export function DualCurrencyInput({ )} - - {(inputMode === "fiat" || - (inputMode === "sats" && bitcoinDisplayFormat === "bip177")) && ( - 11 - ? "ios:text-4xl" - : "ios:text-5xl", - displayCharacterCount <= 14 && - displayCharacterCount >= 11 && - "ios:sm:text-5xl", - ), - android: cn( - displayCharacterCount > 11 - ? "android:text-4xl" - : "android:text-[42px]", - displayCharacterCount <= 14 && - displayCharacterCount >= 11 && - "sm:android:text-[42px]", - ), - }), - "text-secondary-foreground font-bold2 !leading-[1.5]", - !text && "text-muted", - )} - > - {inputMode === "sats" && bitcoinDisplayFormat === "bip177" - ? "₿" - : symbol} - - )} - 11 - ? "ios:text-4xl" - : "ios:text-5xl", - displayCharacterCount <= 14 && - displayCharacterCount >= 11 && - "ios:sm:text-5xl", - ), - android: cn( - displayCharacterCount > 11 - ? "android:text-4xl" - : "android:text-[42px]", - displayCharacterCount <= 14 && - displayCharacterCount >= 11 && - "sm:android:text-[42px]", - ), - }), - "font-semibold2 !leading-[1.5]", - !text && "text-muted", - validationMessage && "text-destructive", - )} + + { + if (!text) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + setShowPasteAction(true); + } + }} + delayLongPress={300} > - {text - ? formattedText - : inputMode === "sats" - ? min - ? max - ? `${formatPlaceholderNumber(min)}-${formatPlaceholderNumber(max)}` - : `Min ${formatPlaceholderNumber(min)}` - : max - ? `Max ${formatPlaceholderNumber(max)}` - : "0" - : "0.00"} - - {inputMode === "sats" && bitcoinDisplayFormat === "sats" && ( - 11 - ? "ios:text-4xl" - : "ios:text-5xl", - displayCharacterCount <= 14 && - displayCharacterCount >= 11 && - "ios:sm:text-5xl", - ), - android: cn( - displayCharacterCount > 11 - ? "android:text-4xl" - : "android:text-[42px]", - displayCharacterCount <= 14 && - displayCharacterCount >= 11 && - "sm:android:text-[42px]", - ), - }), - "text-secondary-foreground font-semibold2 !leading-[1.5]", - !text && "text-muted", + + {(inputMode === "fiat" || + (inputMode === "sats" && + bitcoinDisplayFormat === "bip177")) && ( + 11 + ? "ios:text-4xl" + : "ios:text-5xl", + displayCharacterCount <= 14 && + displayCharacterCount >= 11 && + "ios:sm:text-5xl", + ), + android: cn( + displayCharacterCount > 11 + ? "android:text-4xl" + : "android:text-[42px]", + displayCharacterCount <= 14 && + displayCharacterCount >= 11 && + "sm:android:text-[42px]", + ), + }), + "text-secondary-foreground font-bold2 !leading-[1.5]", + !text && "text-muted", + )} + > + {inputMode === "sats" && bitcoinDisplayFormat === "bip177" + ? "₿" + : symbol} + )} - > - {+amount === 1 ? "sat" : "sats"} - + 11 + ? "ios:text-4xl" + : "ios:text-5xl", + displayCharacterCount <= 14 && + displayCharacterCount >= 11 && + "ios:sm:text-5xl", + ), + android: cn( + displayCharacterCount > 11 + ? "android:text-4xl" + : "android:text-[42px]", + displayCharacterCount <= 14 && + displayCharacterCount >= 11 && + "sm:android:text-[42px]", + ), + }), + "font-semibold2 !leading-[1.5]", + !text && "text-muted", + validationMessage && "text-destructive", + )} + > + {text + ? formattedText + : inputMode === "sats" + ? min + ? max + ? `${formatPlaceholderNumber(min)}-${formatPlaceholderNumber(max)}` + : `Min ${formatPlaceholderNumber(min)}` + : max + ? `Max ${formatPlaceholderNumber(max)}` + : "0" + : "0.00"} + + {inputMode === "sats" && bitcoinDisplayFormat === "sats" && ( + 11 + ? "ios:text-4xl" + : "ios:text-5xl", + displayCharacterCount <= 14 && + displayCharacterCount >= 11 && + "ios:sm:text-5xl", + ), + android: cn( + displayCharacterCount > 11 + ? "android:text-4xl" + : "android:text-[42px]", + displayCharacterCount <= 14 && + displayCharacterCount >= 11 && + "sm:android:text-[42px]", + ), + }), + "text-secondary-foreground font-semibold2 !leading-[1.5]", + !text && "text-muted", + )} + > + {+amount === 1 ? "sat" : "sats"} + + )} + + + {showPasteAction && ( + + + + Paste + + )} {fiatCurrency && ( diff --git a/package.json b/package.json index 69c0b29..804ea24 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "expo-constants": "~18.0.10", "expo-device": "~8.0.9", "expo-font": "~14.0.9", + "expo-haptics": "~15.0.8", "expo-image-picker": "~17.0.8", "expo-linear-gradient": "~15.0.7", "expo-linking": "~8.0.8", diff --git a/yarn.lock b/yarn.lock index 2eff62a..e6c027e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5042,6 +5042,11 @@ expo-font@~14.0.9: dependencies: fontfaceobserver "^2.1.0" +expo-haptics@~15.0.8: + version "15.0.8" + resolved "https://registry.yarnpkg.com/expo-haptics/-/expo-haptics-15.0.8.tgz#f93f895ac5d76fe0c5ac26b3644e1dbb097833f3" + integrity sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g== + expo-image-loader@~6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-6.0.0.tgz#15230442cbb90e101c080a4c81e37d974e43e072" From d2bc84bd778fe0dbe60966d2e2841f5d96dcc428 Mon Sep 17 00:00:00 2001 From: Adithya Vardhan Date: Wed, 17 Jun 2026 20:21:47 +0530 Subject: [PATCH 2/2] chore: remove unnecessary bg-card class --- components/DualCurrencyInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/DualCurrencyInput.tsx b/components/DualCurrencyInput.tsx index 6f8189f..0e9a2af 100644 --- a/components/DualCurrencyInput.tsx +++ b/components/DualCurrencyInput.tsx @@ -556,7 +556,7 @@ export function DualCurrencyInput({ Paste