Surface a hint when the recipient picker is closed with no selection

The campaign donate flow opens HDSendBitcoinDialog with a prefilled
bitcoin:bc1q…?sp=sp1… URI. BitcoinRecipientInput auto-opens its
dropdown so the donor picks between the on-chain address and the
silent-payment code — privacy vs. compatibility, the explicit choice
the picker was designed around (92608f14).

In practice the donor's eye lands on the amount presets first. Tapping
$100 counts as an outside click, dismisses the popover, and leaves
`recipient` null. The Send button is disabled (correctly — no
destination resolved), the input still shows the prefilled URI, and
nothing on screen tells the donor what's missing. They eventually
discover that re-tapping the recipient input reopens the dropdown.

Add a small amber hint with a warning icon directly beneath the
recipient input whenever the input has parseable candidates but no
selection AND the popover is closed. The whole hint is a button that
reopens the popover and refocuses the input on tap, so the recovery
takes one click instead of a guessing game.

Gate the hint on a new `hasOpenedForQuery` flag that flips true the
first time the popover opens for the current query and resets when the
query clears. That keeps the hint from flashing for one paint frame
between mount and the auto-open effect on prefilled inputs.

Regression-of: 92608f14
This commit is contained in:
mkfain
2026-05-30 20:04:27 +02:00
parent c53e476dee
commit c54008cd3d
17 changed files with 73 additions and 17 deletions
+41 -1
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Bitcoin, EyeOff, QrCode, X } from 'lucide-react';
import { AlertTriangle, Bitcoin, EyeOff, QrCode, X } from 'lucide-react';
import { Input } from '@/components/ui/input';
import {
@@ -108,6 +108,11 @@ export function BitcoinRecipientInput({
// selection.
const [query, setQuery] = useState<string>(initialInput ?? '');
const [open, setOpen] = useState(false);
// Tracks whether the popover has been opened at least once for the
// current query. The "choose a payment method" hint suppresses on the
// very first render so callers prefilling the input don't see the hint
// flash for one frame before the auto-open effect runs.
const [hasOpenedForQuery, setHasOpenedForQuery] = useState(false);
const [scannerOpen, setScannerOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
@@ -161,11 +166,20 @@ export function BitcoinRecipientInput({
useEffect(() => {
if (trimmed.length === 0) {
setOpen(false);
setHasOpenedForQuery(false);
return;
}
if (hasSp || hasBtc) setOpen(true);
}, [trimmed, hasSp, hasBtc]);
// Track the first time the popover opens for the current query, so the
// "choose a payment method" hint only appears after the donor has had a
// chance to see (and dismiss) the dropdown — not flash for one paint
// frame between mount and the auto-open effect above.
useEffect(() => {
if (open) setHasOpenedForQuery(true);
}, [open]);
// ── Selection callbacks ───────────────────────────────────────────────
const selectBtc = useCallback(
(address: string) => {
@@ -331,6 +345,32 @@ export function BitcoinRecipientInput({
</PopoverContent>
</Popover>
{/* Picker-closed reminder. When the input holds parseable candidates
but the donor hasn't actually picked one yet — typically because
they tapped an amount preset, which counts as an outside-click
and dismisses the popover — the Send button is disabled with no
visible reason. Surface an actionable hint that re-opens the
dropdown so the donor doesn't have to guess that they're meant
to tap the recipient input again.
Gated on `hasOpenedForQuery` so the hint doesn't flash for one
paint frame between mount and the auto-open effect on prefilled
inputs (campaign donate flow). */}
{hasOpenedForQuery && !popoverOpen && totalItems > 0 && (
<button
type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
setOpen(true);
inputRef.current?.focus();
}}
className="flex items-center gap-1.5 text-xs text-amber-600 dark:text-amber-500 hover:text-amber-700 dark:hover:text-amber-400 motion-safe:transition-colors text-left"
>
<AlertTriangle className="size-3.5 shrink-0" />
<span>{t('walletSend.recipient.choosePaymentMethod')}</span>
</button>
)}
<QrScannerDialog
isOpen={scannerOpen}
onClose={() => setScannerOpen(false)}
+2 -1
View File
@@ -1158,7 +1158,8 @@
"bitcoinAddress": "عنوان بيتكوين",
"silentPayment": "عنوان دفع صامت",
"toLabel": "إلى",
"clear": "مسح المستلم"
"clear": "مسح المستلم",
"choosePaymentMethod": "اختر طريقة الدفع للمتابعة"
},
"feeSpeed": {
"fastest": "~10 دقائق",
+2 -1
View File
@@ -1672,7 +1672,8 @@
"bitcoinAddress": "Bitcoin address",
"silentPayment": "Silent payment address",
"toLabel": "To",
"clear": "Clear recipient"
"clear": "Clear recipient",
"choosePaymentMethod": "Choose a payment method to continue"
},
"feeSpeed": {
"fastest": "~10 min",
+2 -1
View File
@@ -1174,7 +1174,8 @@
"bitcoinAddress": "Dirección de Bitcoin",
"silentPayment": "Dirección de pago silencioso",
"toLabel": "Para",
"clear": "Borrar destinatario"
"clear": "Borrar destinatario",
"choosePaymentMethod": "Elige un método de pago para continuar"
},
"feeSpeed": {
"fastest": "~10 min",
+2 -1
View File
@@ -1174,7 +1174,8 @@
"bitcoinAddress": "نشانی بیت‌کوین",
"silentPayment": "نشانی پرداخت خاموش",
"toLabel": "به",
"clear": "پاک کردن گیرنده"
"clear": "پاک کردن گیرنده",
"choosePaymentMethod": "برای ادامه یک روش پرداخت انتخاب کنید"
},
"feeSpeed": {
"fastest": "~۱۰ دقیقه",
+2 -1
View File
@@ -1597,7 +1597,8 @@
"bitcoinAddress": "Adresse Bitcoin",
"silentPayment": "Adresse de paiement silencieux",
"toLabel": "À",
"clear": "Effacer le destinataire"
"clear": "Effacer le destinataire",
"choosePaymentMethod": "Choisissez un mode de paiement pour continuer"
},
"feeSpeed": {
"fastest": "~10 min",
+2 -1
View File
@@ -1543,7 +1543,8 @@
"bitcoinAddress": "Bitcoin एड्रेस",
"silentPayment": "साइलेंट पेमेंट एड्रेस",
"toLabel": "किसे",
"clear": "प्राप्तकर्ता हटाएँ"
"clear": "प्राप्तकर्ता हटाएँ",
"choosePaymentMethod": "जारी रखने के लिए भुगतान विधि चुनें"
},
"feeSpeed": {
"fastest": "~10 मिनट",
+2 -1
View File
@@ -1543,7 +1543,8 @@
"bitcoinAddress": "Alamat Bitcoin",
"silentPayment": "Alamat silent payment",
"toLabel": "Kepada",
"clear": "Hapus penerima"
"clear": "Hapus penerima",
"choosePaymentMethod": "Pilih metode pembayaran untuk melanjutkan"
},
"feeSpeed": {
"fastest": "~10 menit",
+2 -1
View File
@@ -1174,7 +1174,8 @@
"bitcoinAddress": "អាសយដ្ឋាន Bitcoin",
"silentPayment": "អាសយដ្ឋានទូទាត់ស្ងាត់",
"toLabel": "ជូន",
"clear": "សម្អាតអ្នកទទួល"
"clear": "សម្អាតអ្នកទទួល",
"choosePaymentMethod": "ជ្រើសរើសវិធីសាស្ត្រទូទាត់ដើម្បីបន្ត"
},
"feeSpeed": {
"fastest": "~១០ នាទី",
+2 -1
View File
@@ -1174,7 +1174,8 @@
"bitcoinAddress": "د بټکوین پته",
"silentPayment": "د چوپې ورکړې پته",
"toLabel": "ته",
"clear": "ترلاسه‌کوونکی پاک کړئ"
"clear": "ترلاسه‌کوونکی پاک کړئ",
"choosePaymentMethod": "د دوام لپاره د ورکړې طریقه وټاکئ"
},
"feeSpeed": {
"fastest": "~۱۰ دقیقې",
+2 -1
View File
@@ -1607,7 +1607,8 @@
"bitcoinAddress": "Endereço Bitcoin",
"silentPayment": "Endereço de pagamento silencioso",
"toLabel": "Para",
"clear": "Limpar destinatário"
"clear": "Limpar destinatário",
"choosePaymentMethod": "Escolha um método de pagamento para continuar"
},
"feeSpeed": {
"fastest": "~10 min",
+2 -1
View File
@@ -1607,7 +1607,8 @@
"bitcoinAddress": "Bitcoin-адрес",
"silentPayment": "Адрес тихого платежа",
"toLabel": "Кому",
"clear": "Очистить получателя"
"clear": "Очистить получателя",
"choosePaymentMethod": "Выберите способ оплаты, чтобы продолжить"
},
"feeSpeed": {
"fastest": "~10 мин",
+2 -1
View File
@@ -1174,7 +1174,8 @@
"bitcoinAddress": "Kero yeBitcoin",
"silentPayment": "Kero yemubhadharo wakanyararira",
"toLabel": "Kuna",
"clear": "Bvisa mugamuchiri"
"clear": "Bvisa mugamuchiri",
"choosePaymentMethod": "Sarudza nzira yokubhadhara kuti uenderere"
},
"feeSpeed": {
"fastest": "~10 maminitsi",
+2 -1
View File
@@ -1502,7 +1502,8 @@
"bitcoinAddress": "Anwani ya Bitcoin",
"silentPayment": "Anwani ya malipo ya kimya",
"toLabel": "Kwa",
"clear": "Futa mpokeaji"
"clear": "Futa mpokeaji",
"choosePaymentMethod": "Chagua njia ya malipo ili kuendelea"
},
"feeSpeed": {
"fastest": "~dakika 10",
+2 -1
View File
@@ -1542,7 +1542,8 @@
"bitcoinAddress": "Bitcoin adresi",
"silentPayment": "Sessiz ödeme adresi",
"toLabel": "Alıcı",
"clear": "Alıcıyı temizle"
"clear": "Alıcıyı temizle",
"choosePaymentMethod": "Devam etmek için bir ödeme yöntemi seçin"
},
"feeSpeed": {
"fastest": "~10 dk",
+2 -1
View File
@@ -1110,7 +1110,8 @@
"bitcoinAddress": "比特幣地址",
"silentPayment": "靜默支付地址",
"toLabel": "收件人",
"clear": "清除收件人"
"clear": "清除收件人",
"choosePaymentMethod": "請選擇付款方式以繼續"
},
"feeSpeed": {
"fastest": "~10 分鐘",
+2 -1
View File
@@ -1174,7 +1174,8 @@
"bitcoinAddress": "比特币地址",
"silentPayment": "静默支付地址",
"toLabel": "收件人",
"clear": "清除收件人"
"clear": "清除收件人",
"choosePaymentMethod": "请选择支付方式以继续"
},
"feeSpeed": {
"fastest": "~10 分钟",