Fix NFC pay of cashu PR

This commit is contained in:
minibits-cash
2026-01-07 15:09:46 +01:00
parent a639c05517
commit bd3f436b2b
7 changed files with 286 additions and 351 deletions
+1 -1
View File
@@ -1 +1 @@
019b4070-f889-7238-85b0-d02966f5a2f8
019b9547-7a93-74f0-b2df-a22f9c095db9
+2 -1
View File
@@ -8,4 +8,5 @@
# Customize the NODE_BINARY variable here.
# For example, to use nvm with brew, add the following line
# . "$(brew --prefix nvm)/nvm.sh" --no-use
export NODE_BINARY=$(command -v node)
export NODE_BINARY=/Users/michal/.nvm/versions/node/v24.12.0/bin/node
@@ -31,7 +31,7 @@
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Release"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "minibits_wallet",
"version": "0.3.4-beta.12",
"version": "0.3.4-beta.13",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
+1 -1
View File
@@ -34,7 +34,7 @@ import RNExitApp from 'react-native-exit-app'
import { TransactionStatus } from '../models/Transaction'
import { maxTransactionsInHistory } from '../models/TransactionsStore'
import { StaticScreenProps, useNavigation } from '@react-navigation/native'
// refresh // refresh
// refresh
type Props = StaticScreenProps<undefined>
+64 -129
View File
@@ -126,9 +126,9 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
const [encodedTokenToSend, setEncodedTokenToSend] = useState<string | undefined>()
const [amountToPay, setAmountToPay] = useState<string | undefined>()
const [amountToReceive, setAmountToReceive] = useState<string | undefined>()
const [encodedCashuPaymentRequest, setEncodedCashuPaymentRequest] = useState<string | undefined>()
const [decodedCashuPaymentRequest, setDecodedCashuPaymentRequest] = useState<CashuPaymentRequest | undefined>()
const [encodedInvoice, setEncodedInvoice] = useState<string>('')
// const [encodedCashuPaymentRequest, setEncodedCashuPaymentRequest] = useState<string | undefined>()
// const [decodedCashuPaymentRequest, setDecodedCashuPaymentRequest] = useState<CashuPaymentRequest | undefined>()
// const [encodedInvoice, setEncodedInvoice] = useState<string>('')
const [invoice, setInvoice] = useState<DecodedLightningInvoice | undefined>()
const [invoiceExpiry, setInvoiceExpiry] = useState<Date | undefined>()
const [meltQuote, setMeltQuote] = useState<MeltQuoteResponse | undefined>()
@@ -148,6 +148,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
const [isNfcSupported, setIsNfcSupported] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [isPaid, setIsPaid] = useState(false)
const [isError, setIsError] = useState(false)
const [nfcInfo, setNfcInfo] = useState<string | undefined>()
const [readNfcData, setReadNfcData] = useState<string | undefined>()
const [mint, setMint] = useState<Mint | undefined>()
@@ -310,11 +311,11 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
showSuccessModal()
}
if(currentTx?.status === TransactionStatus.PENDING) {
if(currentTx?.status === TransactionStatus.PENDING && !isError) { // avoid showing pending if we've got an error later
showPendingModal()
}
return
// If still PENDING or ERROR → do nothing, user can close manually
// If ERROR → do nothing, user can close manually
}
const eventName = `ev_${SYNC_STATE_WITH_MINT_TASK}_result`
@@ -408,13 +409,19 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
await handleIncomingData(decoded)
} catch (e: any) {
log.error('[NfcPayScreen] handlePaymentRequest failed', {
message: e?.message,
stack: e?.stack,
//decoded: decoded?.slice?.(0,200)
log.error(e.message, {params: e.params || {}})
setResultModalInfo({
status: TransactionStatus.ERROR,
title: e.params?.message ? e.message : 'NFC Payment failed',
message: e.params?.message || e.message || 'Unknown error during NFC payment processing.',
})
handleError(e)
setNfcInfo(e.message)
setIsError(true)
toggleResultModal()
} finally {
await NfcManager.cancelTechnologyRequest().catch(() => {})
setIsProcessing(false)
}
}
@@ -422,7 +429,6 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
const handleIncomingData = async (data: string) => {
// Alert.alert(data)
log.info('[handlePaymentRequest] received data via NFC', { data })
const result = IncomingParser.findAndExtract(data)
@@ -453,29 +459,26 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
const transports = pr.transport
if(transports && transports.length > 0 && !isOnline.current) {
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Payment failed',
message: 'Wallet can not pay this payment request while offline.',
})
toggleResultModal()
setNfcInfo('Can not pay requested amount')
return
throw new AppError(Err.VALIDATION_ERROR, 'Payment failed',
{ message: 'Wallet can not pay this payment request while offline.' }
)
}
const unit = validateAndNormalizeUnit(pr.unit)
unitRef.current = unit
// Store
setDecodedCashuPaymentRequest(pr)
setEncodedCashuPaymentRequest(encoded)
// setDecodedCashuPaymentRequest(pr)
// setEncodedCashuPaymentRequest(encoded)
if (pr.description) setMemo(pr.description)
const requiredAmount = pr.amount
const eligibleBalances = await getEligibleMintBalancesForCashu(pr, requiredAmount, unit)
if (eligibleBalances.length === 0) {
throw new AppError(Err.NOTFOUND_ERROR, 'Wallet has no mint with enough balance accepted by this payment request')
throw new AppError(Err.NOTFOUND_ERROR, 'Payment failed', {
message: 'Wallet has no mint with enough balance accepted by this payment request.'
})
}
const { selectedBalance, selectedProofs } = await selectMintBalance(
@@ -485,14 +488,9 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
)
if(!selectedBalance) { // we might not find suitable balance if offline
setResultModalInfo({
status: (transaction?.status || TransactionStatus.ERROR) as TransactionStatus,
title: 'Offline payment failed',
message: 'Wallet is offline and does not have ecash denominations matching requested amount',
});
toggleResultModal()
setNfcInfo('Can not pay requested amount')
return
throw new AppError(Err.NOTFOUND_ERROR, 'Offline payment failed', {
message: 'Wallet is offline and does not have ecash denominations matching requested amount.'
})
}
setMintBalanceToSendFrom(selectedBalance)
@@ -508,18 +506,19 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
undefined
)
await handleSendTaskResult(result)
await handleSendTaskResult(encoded, result)
// await handleSendTaskResult(result)
}
const handleSendTaskResult = async function (result: TransactionTaskResult) {
const handleSendTaskResult = async function (encodedCashuPaymentRequest: string, result: TransactionTaskResult) {
log.debug('[NfcScreen] handleSendTaskResult start', {transactionStatus: result.transaction?.status})
try {
const { transaction, error, encodedTokenToSend } = result
const decodedCashuPaymentRequest = decodePaymentRequest(encodedCashuPaymentRequest)
// Always update core transaction state
// update tx with pr info
if (transaction) {
setTransactionStatus(transaction.status)
setTransaction(transaction)
@@ -530,35 +529,22 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
transaction.update({
paymentId: decodedCashuPaymentRequest?.id,
paymentRequest: encodedCashuPaymentRequest,
});
})
}
}
// ———————— Error Cases ————————
if (error || !transaction) {
const message = error?.params?.message || error?.message || 'Unknown error'
const title = error ? 'Payment failed' : 'Internal error'
setResultModalInfo({
status: (transaction?.status || TransactionStatus.ERROR) as TransactionStatus,
title,
message,
});
toggleResultModal()
setNfcInfo('Payment failed')
return
throw new AppError(Err.NOTFOUND_ERROR, 'Payment failed', {
message: error?.params?.message || error?.message || 'Unknown error'
})
}
// ———————— Missing Token or PR ————————
if (!encodedTokenToSend || !decodedCashuPaymentRequest) {
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Internal error',
message: 'Failed to retrieve Payment request or ecash token',
});
toggleResultModal()
setNfcInfo('Payment failed')
return
throw new AppError(Err.NOTFOUND_ERROR, 'Internal error', {
message: 'Failed to retrieve Payment request or ecash token'
})
}
// ———————— Success Path ————————
@@ -567,6 +553,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
const transports = decodedCashuPaymentRequest.transport
// NFC write back only if no transport methods specified (POS device)
if(!transports || transports.length === 0) {
setNfcInfo('Prepared ecash token to be sent via NFC.')
@@ -606,8 +593,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
relays = NostrClient.getAllRelays()
}
log.debug('[NfcScreen] handleSendTaskResult: Sending ecash token via Nostr NIP-17 to payee.', { pubkey, relays })
const messageContent = JSON.stringify({
id: decodedCashuPaymentRequest.id,
mint: decodedTokenToSend.mint,
@@ -653,44 +639,26 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
} else {
// Error if neither transport is supported
throw new AppError(Err.VALIDATION_ERROR, 'Payment request only supports NOSTR or POST transports, but neither is available.')
throw new AppError(Err.NOTFOUND_ERROR, 'Payment failed', {
message: 'Payment request only supports NOSTR or POST transports, but neither is available.'
})
}
}
setNfcInfo('Ecash token sent successfully.')
setIsPaid(true)
expandHeader()
} catch (err: any) {
log.error('Error in handleSendTaskResult', {error: err, caller: 'NfcScreen.handleSendTaskResult'})
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Unexpected error',
message: err.message || 'Something went wrong',
})
toggleResultModal()
}
}
const handleLightningInvoice = async (encodedInvoice: string) => {
try {
log.trace('[handleLightningInvoice] start', {isOnline: isOnline.current})
if (!isOnline.current) {
// setInfo('Can not use Lightning payment method while offline.')
setResultModalInfo({
status: TransactionStatus.ERROR,
title: translate('commonOfflinePretty'),
message: 'Can not use Lightning payment method while offline.',
throw new AppError(Err.NOTFOUND_ERROR, translate('commonOfflinePretty'), {
message: 'Can not use Lightning payment method while offline.'
})
setIsResultModalVisible(true)
return
}
log.trace('[handleLightningInvoice] decoding invoice')
@@ -708,17 +676,12 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
const balances = proofsStore.getMintBalancesWithEnoughBalance(totalRequired, unitRef.current)
if (balances.length === 0) {
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Not enough funds',
throw new AppError(Err.NOTFOUND_ERROR, 'Not enough funds', {
message: translate('transferScreen_insufficientFunds', {
currency: getCurrency(unitRef.current).code,
amount: totalRequired,
}),
})
toggleResultModal()
return
})
}
const selectedBalance = balances[0]
@@ -735,7 +698,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
)
// Update UI
setEncodedInvoice(encodedInvoice)
// setEncodedInvoice(encodedInvoice)
setInvoice(decoded)
setInvoiceExpiry(expiresAt)
setMeltQuote(quote)
@@ -757,16 +720,9 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
const { transaction, message, error: txError } = result
if (!transaction && txError) {
setTransactionStatus(TransactionStatus.ERROR)
setResultModalInfo({
status: TransactionStatus.ERROR,
title: translate('payCommon_failed'),
message: txError.message || 'Lightning payment failed',
throw new AppError(Err.NOTFOUND_ERROR, translate('payCommon_failed'), {
message: txError.message || 'Lightning payment failed.'
})
toggleResultModal()
return;
}
// ——— Transaction exists ———
@@ -783,10 +739,8 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
message,
})
} else {
setResultModalInfo({
status,
title: txError.params?.message ? txError.message : translate('payCommon_failed'),
message: txError.params?.message || txError.message || 'Lightning payment failed',
throw new AppError(Err.NOTFOUND_ERROR, txError.params?.message ? txError.message : translate('payCommon_failed'), {
message: txError.params?.message || txError.message || 'Lightning payment failed'
})
}
} else {
@@ -799,29 +753,20 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
setIsPaid(true)
expandHeader()
setNfcInfo(transaction.memo || 'Lightning payment settled.')
}
} else {
// Fallback (shouldn't happen)
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Unknown error',
message: 'No transaction data received',
throw new AppError(Err.UNKNOWN_ERROR, 'Internal error', {
message: 'No transaction data received.'
})
}
toggleResultModal()
} catch (e: any) {
log.error(e.message, {error: String(e)})
handleError(e)
} finally {
await NfcManager.cancelTechnologyRequest().catch(() => {})
}
}
const handleCashuToken = async function (encodedToken: string) {
try {
const decoded = getDecodedToken(encodedToken)
const amount = CashuUtils.getProofsAmount(decoded.proofs)
const memo = decoded.memo || 'Received over NFC'
@@ -847,10 +792,8 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
message,
})
} else {
setResultModalInfo({
status,
title: txError.params?.message ? txError.message : 'Receive failed',
message: txError.params?.message || txError.message || 'Could not receive cashu token',
throw new AppError(Err.NOTFOUND_ERROR, txError.params?.message ? txError.message : 'Receive failed', {
message: txError.params?.message || txError.message || 'Could not receive cashu token'
})
}
} else {
@@ -867,21 +810,12 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
}
} else {
// Fallback (shouldn't happen)
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Unknown error',
message: 'No transaction data received',
throw new AppError(Err.UNKNOWN_ERROR, 'Internal error', {
message: 'No transaction data received.'
})
}
setIsResultModalVisible(true)
} catch(e: any) {
log.error(e.message, {error: String(e)})
handleError(e)
} finally {
await NfcManager.cancelTechnologyRequest().catch(() => {})
}
}
@@ -971,7 +905,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
}
const resetState = function () {
setEncodedInvoice('')
//ßsetEncodedInvoice('')
setInvoice(undefined)
setInvoiceExpiry(undefined)
setMeltQuote(undefined)
@@ -1208,7 +1142,8 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
</>
)}
{resultModalInfo &&
transactionStatus === TransactionStatus.PENDING && (
transactionStatus === TransactionStatus.PENDING &&
resultModalInfo.status !== TransactionStatus.ERROR && (
<>
<ResultModalInfo
icon="faTriangleExclamation"
+1 -2
View File
@@ -19,7 +19,6 @@ import AppError, { Err } from "../utils/AppError"
const { userSettingsStore } = rootStoreInstance
// refresh
if (!__DEV__) {
Sentry.init({
@@ -125,7 +124,7 @@ const customSentryTransport: transportFunctionType<TransportOptions> = async (pr
}
// For native JS Errors: preserve stack, don't override
if ('stack' in rawMessage) {
params.stack = rawMessage.stack
params.stack = rawMessage.stack?.slice(0, 200)
}
} else if (rawMessage && typeof rawMessage === 'object') {
message = '[Object logged]'