mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
Fix NFC pay of cashu PR
This commit is contained in:
Vendored
+1
-1
@@ -1 +1 @@
|
||||
019b4070-f889-7238-85b0-d02966f5a2f8
|
||||
019b9547-7a93-74f0-b2df-a22f9c095db9
|
||||
+2
-1
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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]'
|
||||
|
||||
Reference in New Issue
Block a user