Fixes: Pay button not shown when donating for custom address, amount input unintentionally selected while typing.

This commit is contained in:
minibits-cash
2025-07-03 13:46:02 +02:00
parent 8e17305d84
commit 3b6e1e4461
9 changed files with 172 additions and 109 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "minibits_wallet",
"version": "0.2.2-beta.25",
"version": "0.2.2-beta.26",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
+92
View File
@@ -0,0 +1,92 @@
import React, { forwardRef, useState } from 'react'
import { TextInput, TextStyle } from 'react-native'
import { spacing, useThemeColor, typography } from '../theme'
import { verticalScale } from '@gocodingnow/rn-size-matters'
import { MintUnit, getCurrency } from '../services/wallet/currency'
import numbro from 'numbro'
interface AmountInputProps {
value: string
onChangeText: (text: string) => void
unit?: MintUnit
onEndEditing?: () => void
editable?: boolean
selectTextOnFocus?: boolean
onFocus?: () => void
onBlur?: () => void
style?: TextStyle
}
export const AmountInput = forwardRef<TextInput, AmountInputProps>(
(
{
value,
onChangeText,
unit = 'sat',
onEndEditing,
editable = true,
selectTextOnFocus,
onFocus,
onBlur,
style,
...rest
},
ref
) => {
const [hasAmountFocusedOnce, setHasAmountFocusedOnce] = useState(false)
const amountInputColor = useThemeColor('amountInput')
const handleFocus = () => {
setHasAmountFocusedOnce(true)
onFocus?.()
}
const handleBlur = () => {
setHasAmountFocusedOnce(false)
onBlur?.()
}
const handleEndEditing = () => {
if (onEndEditing) {
onEndEditing()
}
// Default formatting behavior
const formattedValue = numbro(value).format({
thousandSeparated: true,
mantissa: getCurrency(unit).mantissa
})
onChangeText(formattedValue)
}
const defaultStyle: TextStyle = {
borderRadius: spacing.small,
margin: 0,
padding: 0,
fontSize: verticalScale(48),
fontFamily: typography.primary?.medium,
textAlign: 'center',
color: amountInputColor,
}
return (
<TextInput
ref={ref}
value={value}
onChangeText={onChangeText}
onEndEditing={handleEndEditing}
onFocus={handleFocus}
onBlur={handleBlur}
style={[defaultStyle, style]}
maxLength={9}
keyboardType="numeric"
returnKeyType="done"
selectTextOnFocus={selectTextOnFocus !== undefined ? selectTextOnFocus : !hasAmountFocusedOnce}
editable={editable}
{...rest}
/>
)
}
)
AmountInput.displayName = 'AmountInput'
+1
View File
@@ -1,3 +1,4 @@
export * from "./AmountInput"
export * from "./AutoImage"
export * from "./Button"
export * from "./Card"
+10 -10
View File
@@ -22,7 +22,7 @@ import { LNURLPayParams, LnurlClient } from '../../services/lnurlService'
import { useNavigation } from '@react-navigation/native'
import { TransferOption } from '../TransferScreen'
const DEFAULT_DONATION_AMOUNT = 500
const DEFAULT_DONATION_AMOUNT = 10
const DONATION_LNURL_ADDRESS = 'minibits@minibits.cash'
export const OwnName = observer(function (props: {pubkey: string}) {
@@ -273,9 +273,9 @@ export const OwnName = observer(function (props: {pubkey: string}) {
const currentNameColor = colors.palette.primary200
const inputBg = useThemeColor('background')
const inputText = useThemeColor('text')
const small = 1000
const medium = 1500
const large = 2000
const small = DEFAULT_DONATION_AMOUNT * 2
const medium = DEFAULT_DONATION_AMOUNT * 3
const large = DEFAULT_DONATION_AMOUNT * 4
const invoiceBg = useThemeColor('background')
const invoiceTextColor = useThemeColor('textDim')
const domainText = useThemeColor('textDim')
@@ -366,28 +366,28 @@ export const OwnName = observer(function (props: {pubkey: string}) {
preset="secondary"
style={{marginRight: spacing.small}}
text={`${small.toLocaleString()}`}
onPress={() => setDonationAmount(1000)}
onPress={() => setDonationAmount(DEFAULT_DONATION_AMOUNT * 2)}
/>
<Button
preset="secondary"
style={{marginRight: spacing.small}}
text={`${medium.toLocaleString()}`}
onPress={() => setDonationAmount(1500)}
onPress={() => setDonationAmount(DEFAULT_DONATION_AMOUNT * 3)}
/>
<Button
preset="secondary"
text={`${large.toLocaleString()}`}
onPress={() => setDonationAmount(2000)}
onPress={() => setDonationAmount(DEFAULT_DONATION_AMOUNT * 4)}
/>
</View>
<View style={$buttonContainer}>
{(donationAmount === 1000) && (
{(donationAmount === DEFAULT_DONATION_AMOUNT * 2) && (
<Text text={`♥`} size='lg' />
)}
{(donationAmount === 1500) && (
{(donationAmount === DEFAULT_DONATION_AMOUNT * 3) && (
<Text text={`♥ ♥`} size='lg' />
)}
{(donationAmount === 2000) && (
{(donationAmount === DEFAULT_DONATION_AMOUNT * 4) && (
<Text text={`♥ ♥ ♥`} size='lg' />
)}
</View>
+4 -3
View File
@@ -14,6 +14,7 @@ import {
ListItem,
BottomModal,
Text,
AmountInput,
} from '../components'
import {Mint} from '../models/Mint'
import {Transaction, TransactionStatus} from '../models/Transaction'
@@ -325,10 +326,10 @@ export const ReceiveScreen = observer(function ReceiveScreen({ route }: Props) {
/>
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<View style={$amountContainer}>
<TextInput
<AmountInput
value={toNumber(receivedAmount) > 0 ? receivedAmount : amountToReceive}
style={[$amountInput, {color: amountInputColor}]}
maxLength={9}
onChangeText={() => {}}
unit={unit}
editable={false}
/>
</View>
+19 -23
View File
@@ -27,7 +27,8 @@ import {
InfoModal,
ErrorModal,
BottomModal,
Text,
Text,
AmountInput,
} from '../components'
import {TransactionStatus, Transaction} from '../models/Transaction'
import {useStores} from '../models'
@@ -567,10 +568,8 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
const onAmountEndEditing = function () {
try {
const precision = getCurrency(unitRef.current).precision
const mantissa = getCurrency(unitRef.current).mantissa
const amount = round(toNumber(amountToSend) * precision, 0)
//const amount = parseInt(amountToSend)
const precision = getCurrency(unitRef.current).precision
const amount = round(toNumber(amountToSend) * precision, 0)
log.trace('[onAmountEndEditing]', amount)
@@ -586,15 +585,16 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
return
}
LayoutAnimation.easeInEaseOut()
LayoutAnimation.easeInEaseOut()
setAmountToSend(`${numbro(amountToSend).format({thousandSeparated: true, mantissa: getCurrency(unitRef.current).mantissa})}`)
setAvailableMintBalances(availableBalances)
// Default mint if not set from route params is with the one with highest balance
if(!mintBalanceToSendFrom) {setMintBalanceToSendFrom(availableBalances[0])}
// We do not make memo focus mandatory
// Show mint selector
// Default mint if not set from route params is the one with the highest balance
if(!mintBalanceToSendFrom) {
setMintBalanceToSendFrom(availableBalances[0])
}
LayoutAnimation.easeInEaseOut()
setIsMintSelectorVisible(true)
} catch (e: any) {
@@ -1005,21 +1005,17 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
/>
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<View style={$amountContainer}>
<TextInput
<AmountInput
ref={amountInputRef}
onChangeText={amount => setAmountToSend(amount)}
onEndEditing={onAmountEndEditing}
value={amountToSend}
style={[$amountInput, {color: amountInputColor}]}
maxLength={9}
keyboardType="numeric"
selectTextOnFocus={true}
editable={
(transactionStatus === TransactionStatus.PENDING || isOfflineSend || isCashuPrWithAmount)
? false
: true
onChangeText={amount => setAmountToSend(amount)}
unit={unitRef.current}
onEndEditing={onAmountEndEditing}
editable={(transactionStatus === TransactionStatus.PENDING || isOfflineSend || isCashuPrWithAmount)
? false
: true
}
returnKeyType={'done'}
style={{color: amountInputColor}}
/>
{isConvertedAmountVisible() && (
<CurrencyAmount
+34 -50
View File
@@ -24,6 +24,7 @@ import {
ListItem,
BottomModal,
Text,
AmountInput,
} from '../components'
import {TransactionStatus, Transaction} from '../models/Transaction'
import {useStores} from '../models'
@@ -91,13 +92,13 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
} = useStores()
const amountInputRef = useRef<TextInput>(null)
const memoInputRef = useRef<TextInput>(null)
const unitRef = useRef<MintUnit>('sat')
// const tokenInputRef = useRef<TextInput>(null)
const [paymentOption, setPaymentOption] = useState<ReceiveOption>(
ReceiveOption.SHOW_INVOICE,
)
const [amountToTopup, setAmountToTopup] = useState<string>('0')
const [unit, setUnit] = useState<MintUnit>('sat')
const [amountToTopup, setAmountToTopup] = useState<string>('0')
const [contactToSendFrom, setContactToSendFrom] = useState<
Contact | undefined
>()
@@ -127,9 +128,6 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
const [info, setInfo] = useState('')
const [error, setError] = useState<AppError | undefined>()
const [isAmountEndEditing, setIsAmountEndEditing] = useState<boolean>(false)
const [isMemoEndEditing, setIsMemoEndEditing] = useState<boolean>(false)
const [resultModalInfo, setResultModalInfo] = useState<
{status: TransactionStatus; title?: string; message: string} | undefined
>()
@@ -172,7 +170,7 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
)
}
setUnit(unit)
unitRef.current = unit
if (mintUrl) {
const mintBalance = proofsStore.getMintBalance(mintUrl)
@@ -384,11 +382,11 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
}
try {
const precision = getCurrency(unit).precision
const mantissa = getCurrency(unit).mantissa
const precision = getCurrency(unitRef.current).precision
const mantissa = getCurrency(unitRef.current).mantissa
const amount = round(toNumber(amountToTopup) * precision, 0)
log.trace('[onAmountEndEditing]', {amount, unit})
log.trace('[onAmountEndEditing]', {amount, unit: unitRef.current})
if (!isInternetReachable) {
setInfo(translate('common.offlinePretty'))
@@ -412,7 +410,7 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
return
}
const availableBalances = proofsStore.getMintBalancesWithUnit(unit)
const availableBalances = proofsStore.getMintBalancesWithUnit(unitRef.current)
if (availableBalances.length === 0) {
infoMessage(
@@ -421,23 +419,15 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
)
return
}
setAmountToTopup(
`${numbro(amountToTopup).format({
thousandSeparated: true,
mantissa,
})}`
) // round amount based on currency format
setAvailableMintBalances(availableBalances)
// Default mint if not set from route params is the one with the highest balance to topup
if (!mintBalanceToTopup) {
setMintBalanceToTopup(availableBalances[0])
}
setIsAmountEndEditing(true)
// We do not make memo focus mandatory
LayoutAnimation.easeInEaseOut()
// Show mint selector
LayoutAnimation.easeInEaseOut()
setIsMintSelectorVisible(true)
} catch (e: any) {
handleError(e)
@@ -451,7 +441,6 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
if (availableMintBalances.length > 0) {
setIsMintSelectorVisible(true)
}
setIsMemoEndEditing(true)
}
const onMemoDone = function () {
@@ -483,14 +472,14 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
setIsTopupTaskSentToQueue(true)
const amountToTopupInt = round(
toNumber(amountToTopup) * getCurrency(unit).precision,
toNumber(amountToTopup) * getCurrency(unitRef.current).precision,
0,
)
WalletTask.topupQueue(
mintBalanceToTopup as MintBalance,
amountToTopupInt,
unit,
unitRef.current,
memo,
contactToSendTo,
)
@@ -510,7 +499,7 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
const message = translate('topup.nostrDMreceived', {
npub: walletProfileStore.npub,
amount: amountToTopup,
currency: getCurrency(unit).code
currency: getCurrency(unitRef.current).code
})
// invoice
let content = message + ' \n' + invoiceToPay + ' \n'
@@ -638,8 +627,6 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
// reset state so it does not interfere next payment
setAmountToTopup('')
setMemo('')
setIsAmountEndEditing(false)
setIsMemoEndEditing(false)
setIsMintSelectorVisible(false)
setIsNostrDMModalVisible(false)
setIsWithdrawModalVisible(false)
@@ -679,10 +666,10 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
return undefined
}
const precision = getCurrency(unit).precision
const precision = getCurrency(unitRef.current).precision
return convertToFromSats(
round(toNumber(amountToTopup) * precision, 0) || 0,
getCurrency(unit).code,
getCurrency(unitRef.current).code,
walletStore.exchangeRate
)
}
@@ -690,8 +677,8 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
const isConvertedAmountVisible = function () {
return (
walletStore.exchangeRate &&
(userSettingsStore.exchangeCurrency === getCurrency(unit).code ||
unit === 'sat') &&
(userSettingsStore.exchangeCurrency === getCurrency(unitRef.current).code ||
unitRef.current === 'sat') &&
getConvertedAmount() !== undefined
)
}
@@ -705,28 +692,25 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
? mintsStore.findByUrl(mintBalanceToTopup?.mintUrl)
: undefined
}
unit={unit}
unit={unitRef.current}
/>
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<View style={$amountContainer}>
<TextInput
ref={amountInputRef}
onChangeText={amount => setAmountToTopup(amount)}
onEndEditing={onAmountEndEditing}
value={amountToTopup}
style={[$amountInput, {color: amountInputColor}]}
maxLength={9}
keyboardType="numeric"
selectTextOnFocus={true}
editable={
transactionStatus === TransactionStatus.PENDING ? false : true
}
returnKeyType={'done'}
/>
<AmountInput
ref={amountInputRef}
value={amountToTopup}
onChangeText={amount => setAmountToTopup(amount)}
unit={unitRef.current}
onEndEditing={onAmountEndEditing}
editable={
transactionStatus === TransactionStatus.PENDING ? false : true
}
style={{color: amountInputColor}}
/>
{isConvertedAmountVisible() && (
<CurrencyAmount
amount={getConvertedAmount() ?? 0}
currencyCode={unit === 'sat' ? userSettingsStore.exchangeCurrency : CurrencyCode.SAT}
currencyCode={unitRef.current === 'sat' ? userSettingsStore.exchangeCurrency : CurrencyCode.SAT}
symbolStyle={{color: convertedAmountColor, marginTop: spacing.tiny, fontSize: verticalScale(10)}}
amountStyle={{color: convertedAmountColor, lineHeight: spacing.small}}
size='small'
@@ -787,7 +771,7 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
<MintBalanceSelector
mintBalances={availableMintBalances}
selectedMintBalance={mintBalanceToTopup as MintBalance}
unit={unit}
unit={unitRef.current}
title={translate("topup.mint")}
confirmTitle={translate("common.confirmCreateInvoice")}
onMintBalanceSelect={onMintBalanceSelect}
@@ -836,7 +820,7 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
<TranItem
label="transactionCommon.feePaid"
value={transaction.fee || 0}
unit={unit}
unit={unitRef.current}
isCurrency={true}
/>
<TranItem
@@ -878,7 +862,7 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
contactToSendTo={contactToSendTo as Contact}
relaysToShareTo={relaysToShareTo}
amountToTopup={amountToTopup}
unit={unit}
unit={unitRef.current}
sendAsNostrDM={sendAsNostrDM}
isNostrDMSending={isNostrDMSending}
/>
+9 -21
View File
@@ -22,7 +22,8 @@ import {
ErrorModal,
ListItem,
BottomModal,
Text,
Text,
AmountInput,
} from '../components'
import {Transaction, TransactionStatus} from '../models/Transaction'
import {useStores} from '../models'
@@ -94,7 +95,7 @@ export const TransferScreen = observer(function TransferScreen({ route }: Props)
const [amountToTransfer, setAmountToTransfer] = useState<string>('0')
const [invoiceExpiry, setInvoiceExpiry] = useState<Date | undefined>()
const [paymentHash, setPaymentHash] = useState<string | undefined>()
const [lnurlPayParams, setLnurlPayParams] = useState<LNURLPayParams & {address?: string} | undefined>()
const [lnurlPayParams, setLnurlPayParams] = useState<LNURLPayParams & {address?: string} | undefined>()
const [meltQuote, setMeltQuote] = useState<MeltQuoteResponse | undefined>()
const [finalFee, setFinalFee] = useState<number>(0)
const [memo, setMemo] = useState('')
@@ -496,15 +497,6 @@ const onMintBalanceSelect = function (balance: MintBalance) {
setMintBalanceToTransferFrom(balance) // this triggers effect to get melt quote
}
const onAmountEndEditing = function () {
setAmountToTransfer(
`${numbro(amountToTransfer).format({
thousandSeparated: true,
mantissa: getCurrency(unitRef.current).mantissa
})}`
)
}
// Amount is editable only in case of LNURL Pay, while invoice is not yet retrieved
const onRequestLnurlInvoice = async function () {
@@ -635,7 +627,7 @@ const onEncodedInvoice = async function (encoded: string) {
return
}
setMintBalanceToTransferFrom(balanceToTransferFrom)
setMintBalanceToTransferFrom({...balanceToTransferFrom}) // force to trigger effect to create melt quote
// continues in hook that handles other mint selection by user
} catch (e: any) {
@@ -757,17 +749,13 @@ const amountInputColor = useThemeColor('amountInput')
/>
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<View style={$amountContainer}>
<TextInput
<AmountInput
ref={amountInputRef}
onChangeText={amount => setAmountToTransfer(amount)}
onEndEditing={onAmountEndEditing}
value={amountToTransfer}
style={[$amountInput, {color: amountInputColor}]}
maxLength={9}
keyboardType="numeric"
selectTextOnFocus={true}
editable={isAmountEditable ? true : false}
returnKeyType={'done'}
onChangeText={amount => setAmountToTransfer(amount)}
unit={unitRef.current}
editable={isAmountEditable}
style={{color: amountInputColor}}
/>
{encodedInvoice && (meltQuote?.fee_reserve || finalFee) ? (
+2 -1
View File
@@ -126,7 +126,8 @@ export const topupTask = async function (
log.trace('[topupTask] invoice', {amount, paymentHash, expiry, timestamp, expiresAtDate})
transactionData.push({
status: TransactionStatus.PENDING,
status: TransactionStatus.PENDING,
quote: mintQuote,
createdAt: new Date()
})