mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
949 lines
31 KiB
TypeScript
949 lines
31 KiB
TypeScript
import {observer} from 'mobx-react-lite'
|
|
import React, {FC, useEffect, useState, useCallback, useRef} from 'react'
|
|
import {useFocusEffect} from '@react-navigation/native'
|
|
import {
|
|
UIManager,
|
|
Platform,
|
|
TextStyle,
|
|
View,
|
|
ViewStyle,
|
|
FlatList,
|
|
TextInput,
|
|
Keyboard,
|
|
} from 'react-native'
|
|
import {spacing, useThemeColor, colors, typography} from '../theme'
|
|
import {WalletStackScreenProps} from '../navigation'
|
|
import {
|
|
Button,
|
|
Icon,
|
|
Card,
|
|
Screen,
|
|
Loading,
|
|
InfoModal,
|
|
ErrorModal,
|
|
ListItem,
|
|
BottomModal,
|
|
Text,
|
|
} from '../components'
|
|
import {Mint} from '../models/Mint'
|
|
import {Transaction, TransactionStatus} from '../models/Transaction'
|
|
import {useStores} from '../models'
|
|
import {MintClient, TransactionTaskResult, WalletTask} from '../services'
|
|
import EventEmitter from '../utils/eventEmitter'
|
|
import {log} from '../services/logService'
|
|
import AppError, {Err} from '../utils/AppError'
|
|
import {MintBalance} from '../models/Mint'
|
|
import {MintListItem} from './Mints/MintListItem'
|
|
import {ResultModalInfo} from './Wallet/ResultModalInfo'
|
|
import {addSeconds} from 'date-fns'
|
|
import { PaymentRequestStatus } from '../models/PaymentRequest'
|
|
import { infoMessage } from '../utils/utils'
|
|
import { DecodedLightningInvoice, LightningUtils } from '../services/lightning/lightningUtils'
|
|
import { SendOption } from './SendOptionsScreen'
|
|
import { round, roundDown, roundUp, toNumber } from '../utils/number'
|
|
import { LnurlClient, LNURLPayParams } from '../services/lnurlService'
|
|
import { moderateVerticalScale } from '@gocodingnow/rn-size-matters'
|
|
import { Currencies, CurrencyCode, MintUnit, getCurrency } from "../services/wallet/currency"
|
|
import { FeeBadge } from './Wallet/FeeBadge'
|
|
import { MeltQuoteResponse } from '@cashu/cashu-ts'
|
|
import { MintHeader } from './Mints/MintHeader'
|
|
import { MintBalanceSelector } from './Mints/MintBalanceSelector'
|
|
import numbro from 'numbro'
|
|
import { TranItem } from './TranDetailScreen'
|
|
import useIsInternetReachable from '../utils/useIsInternetReachable'
|
|
import { translate } from '../i18n'
|
|
import { MemoInputCard } from '../components/MemoInputCard'
|
|
|
|
|
|
if (
|
|
Platform.OS === 'android' &&
|
|
UIManager.setLayoutAnimationEnabledExperimental
|
|
) {
|
|
UIManager.setLayoutAnimationEnabledExperimental(true)
|
|
}
|
|
|
|
export const TransferScreen: FC<WalletStackScreenProps<'Transfer'>> = observer(
|
|
function TransferScreen({route, navigation}) {
|
|
|
|
const amountInputRef = useRef<TextInput>(null)
|
|
const lnurlCommentInputRef = useRef<TextInput>(null)
|
|
|
|
const {proofsStore, mintsStore, paymentRequestsStore, transactionsStore} = useStores()
|
|
|
|
const isInternetReachable = useIsInternetReachable()
|
|
|
|
const [encodedInvoice, setEncodedInvoice] = useState<string>('')
|
|
const [invoice, setInvoice] = useState<DecodedLightningInvoice | undefined>()
|
|
const [amountToTransfer, setAmountToTransfer] = useState<string>('0')
|
|
const [unit, setUnit] = useState<MintUnit>('sat')
|
|
const [invoiceExpiry, setInvoiceExpiry] = useState<Date | undefined>()
|
|
const [paymentHash, setPaymentHash] = useState<string | undefined>()
|
|
const [lnurlPayParams, setLnurlPayParams] = useState<LNURLPayParams & {address?: string} | undefined>()
|
|
const [isWaitingForFees, setIsWaitingForFees] = useState<boolean>(false)
|
|
const [meltQuote, setMeltQuote] = useState<MeltQuoteResponse | undefined>()
|
|
const [finalFee, setFinalFee] = useState<number>(0)
|
|
const [memo, setMemo] = useState('')
|
|
const [lnurlDescription, setLnurlDescription] = useState('')
|
|
const [lnurlPayCommentAllowed, setLnurlPayCommentAllowed] = useState(0)
|
|
const [lnurlPayComment, setLnurlPayComment] = useState('')
|
|
const [availableMintBalances, setAvailableMintBalances] = useState<MintBalance[]>([])
|
|
const [mintBalanceToTransferFrom, setMintBalanceToTransferFrom] = useState<MintBalance | undefined>()
|
|
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus | undefined>()
|
|
const [transaction, setTransaction] = useState<Transaction | undefined>()
|
|
const [info, setInfo] = useState('')
|
|
const [error, setError] = useState<AppError | undefined>()
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [isPasteInvoiceModalVisible, setIsPasteInvoiceModalVisible] = useState(false)
|
|
const [isInvoiceDonation, setIsInvoiceDonation] = useState(false)
|
|
const [isTransferTaskSentToQueue, setIsTransferTaskSentToQueue] = useState(false)
|
|
const [isResultModalVisible, setIsResultModalVisible] = useState(false)
|
|
const [resultModalInfo, setResultModalInfo] = useState<{status: TransactionStatus; title?: string, message: string} | undefined>()
|
|
|
|
|
|
useEffect(() => {
|
|
const focus = () => {
|
|
if (route.params?.paymentOption === SendOption.LNURL_PAY) {
|
|
amountInputRef && amountInputRef.current
|
|
? amountInputRef.current.focus()
|
|
: false
|
|
}
|
|
}
|
|
|
|
const timer = setTimeout(() => focus(), 100)
|
|
|
|
return () => {
|
|
clearTimeout(timer)
|
|
}
|
|
}, [])
|
|
|
|
// TODO: fix indentation in this component
|
|
|
|
useEffect(() => {
|
|
const setUnitAndMint = () => {
|
|
try {
|
|
const {unit, mintUrl} = route.params
|
|
if(!unit) {
|
|
throw new AppError(Err.VALIDATION_ERROR, translate('missingMintUnitRouteParamsError'))
|
|
}
|
|
|
|
setUnit(unit)
|
|
|
|
if(mintUrl) {
|
|
const mintBalance = proofsStore.getMintBalance(mintUrl)
|
|
setMintBalanceToTransferFrom(mintBalance)
|
|
}
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
setUnitAndMint()
|
|
return () => {}
|
|
}, [])
|
|
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
const {paymentOption} = route.params
|
|
log.trace('[useFocusEffect]', {paymentOption})
|
|
|
|
const handleInvoice = () => {
|
|
try {
|
|
const {encodedInvoice} = route.params
|
|
|
|
if (!encodedInvoice) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing invoice.')
|
|
}
|
|
|
|
log.trace('[handleInvoice] Invoice', {encodedInvoice})
|
|
|
|
onEncodedInvoice(encodedInvoice)
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
const handlePaymentRequest = () => {
|
|
try {
|
|
const {paymentRequest} = route.params
|
|
|
|
if (!paymentRequest) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing paymentRequest.')
|
|
}
|
|
|
|
log.trace('[handlePaymentRequest] Payment request', {paymentRequest})
|
|
|
|
const {encodedInvoice, description, paymentHash} = paymentRequest
|
|
|
|
setPaymentHash(paymentHash)
|
|
onEncodedInvoice(encodedInvoice, description)
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
const handleLnurlPay = () => {
|
|
try {
|
|
const {lnurlParams} = route.params
|
|
|
|
if (!lnurlParams) {
|
|
throw new AppError(Err.VALIDATION_ERROR, translate('missingLNURLParamsError'))
|
|
}
|
|
|
|
const metadata = lnurlParams.decodedMetadata
|
|
|
|
if(metadata) {
|
|
let desc: string = ''
|
|
let address: string = ''
|
|
|
|
for (const entry of metadata) {
|
|
if (entry[0] === "text/plain") {
|
|
desc = entry[1];
|
|
break // Exit the loop once we find the "text/plain" entry
|
|
}
|
|
}
|
|
|
|
for (const entry of metadata) {
|
|
if (entry[0] === "text/identifier" || entry[0] === "text/email") {
|
|
address = entry[1];
|
|
break
|
|
}
|
|
}
|
|
if ('commentAllowed' in lnurlParams && lnurlParams.commentAllowed > 0) {
|
|
setLnurlPayCommentAllowed(lnurlParams.commentAllowed)
|
|
}
|
|
|
|
if(desc) {
|
|
setLnurlDescription(desc)
|
|
}
|
|
|
|
if(address) {
|
|
// overwrite sender address set by wallet with the address from the lnurl response
|
|
lnurlParams.address = address
|
|
}
|
|
}
|
|
|
|
const amountSats = roundUp(lnurlParams.minSendable / 1000, 0)
|
|
|
|
log.trace('[handleLnurlPay]', {lnurlParams})
|
|
|
|
setAmountToTransfer(`${numbro(amountSats).format({thousandSeparated: true, mantissa: 0})}`)
|
|
setLnurlPayParams(lnurlParams)
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
const handleDonation = () => {
|
|
try {
|
|
const {encodedInvoice} = route.params
|
|
|
|
if (!encodedInvoice) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing donation invoice.')
|
|
}
|
|
|
|
if(unit !== 'sat') {
|
|
throw new AppError(Err.VALIDATION_ERROR, `Donations can currently be paid only with ${CurrencyCode.SATS} balances.`)
|
|
}
|
|
|
|
log.trace('[handleDonation]', {encodedInvoice})
|
|
|
|
setIsInvoiceDonation(true)
|
|
onEncodedInvoice(encodedInvoice)
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
if(paymentOption && paymentOption === SendOption.PASTE_OR_SCAN_INVOICE) {
|
|
handleInvoice()
|
|
}
|
|
|
|
if(paymentOption && paymentOption === SendOption.PAY_PAYMENT_REQUEST) {
|
|
handlePaymentRequest()
|
|
}
|
|
|
|
if(paymentOption && paymentOption === SendOption.LNURL_PAY) {
|
|
handleLnurlPay()
|
|
}
|
|
|
|
if(paymentOption && paymentOption === SendOption.DONATION) {
|
|
handleDonation()
|
|
}
|
|
|
|
|
|
}, [route.params?.paymentOption]),
|
|
)
|
|
|
|
|
|
useEffect(() => {
|
|
const getMeltQuote = async function () {
|
|
try {
|
|
log.trace('[getEstimatedFee]', {mintBalanceToTransferFrom})
|
|
if (!mintBalanceToTransferFrom || !mintBalanceToTransferFrom.balances[unit] || !encodedInvoice) {
|
|
log.trace(
|
|
'[getEstimatedFee]',
|
|
'Not ready... exiting',
|
|
mintBalanceToTransferFrom,
|
|
mintBalanceToTransferFrom?.balances[unit],
|
|
encodedInvoice,
|
|
)
|
|
return
|
|
}
|
|
|
|
setIsLoading(true)
|
|
const meltQuote = await MintClient.getLightningMeltQuote(
|
|
mintBalanceToTransferFrom.mintUrl,
|
|
unit,
|
|
encodedInvoice,
|
|
)
|
|
|
|
setIsLoading(false)
|
|
setMeltQuote(meltQuote)
|
|
setAmountToTransfer(`${numbro(meltQuote.amount / getCurrency(unit).precision).format({thousandSeparated: true, mantissa: getCurrency(unit).mantissa})}`)
|
|
|
|
const totalAmount = meltQuote.amount + meltQuote.fee_reserve
|
|
|
|
let availableBalances = proofsStore.getMintBalancesWithEnoughBalance(totalAmount, unit)
|
|
|
|
if (availableBalances.length === 0) {
|
|
infoMessage(translate("transferScreen.insufficientFunds", {
|
|
currency: getCurrency(unit).code,
|
|
amount: amountToTransfer
|
|
}))
|
|
return
|
|
}
|
|
|
|
setAvailableMintBalances(availableBalances)
|
|
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
getMeltQuote()
|
|
|
|
}, [mintBalanceToTransferFrom])
|
|
|
|
|
|
useEffect(() => {
|
|
const handleTransferTaskResult = async (result: TransactionTaskResult) => {
|
|
log.trace('handleTransferTaskResult event handler triggered', {isInvoiceDonation})
|
|
|
|
setIsLoading(false)
|
|
const {transaction, message, error, finalFee} = result
|
|
|
|
// handle errors before transaction is created
|
|
if (!transaction && error) {
|
|
setTransactionStatus(TransactionStatus.ERROR)
|
|
setResultModalInfo({
|
|
status: TransactionStatus.ERROR,
|
|
message: error.message,
|
|
})
|
|
|
|
setIsLoading(false)
|
|
toggleResultModal()
|
|
return
|
|
}
|
|
|
|
const { status } = transaction as Transaction
|
|
setTransactionStatus(status)
|
|
setTransaction(transaction)
|
|
|
|
if(transaction && lnurlPayParams && lnurlPayParams.address) {
|
|
await transactionsStore.updateSentTo( // set ln address to send to to the tx, could be elsewhere //
|
|
transaction.id as number,
|
|
lnurlPayParams.address as string
|
|
)
|
|
}
|
|
|
|
if (error) { // This handles timed out pending payments
|
|
if(status === TransactionStatus.PENDING) {
|
|
setResultModalInfo({
|
|
status,
|
|
message,
|
|
})
|
|
} else {
|
|
setResultModalInfo({
|
|
status,
|
|
title: error.params?.message ? error.message : translate('payCommon.failed'),
|
|
message: error.params?.message || error.message,
|
|
})
|
|
}
|
|
|
|
} else {
|
|
if(!isInvoiceDonation) { // Donation has own polling to avoid paying with test ecash and triggers own ResultModal on paid invoice
|
|
setResultModalInfo({
|
|
status,
|
|
message,
|
|
})
|
|
}
|
|
|
|
// update related paymentRequest status if exists
|
|
if(paymentHash) {
|
|
const pr = paymentRequestsStore.findByPaymentHash(paymentHash)
|
|
|
|
if(pr) {
|
|
pr.setStatus(PaymentRequestStatus.PAID)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (finalFee) {
|
|
setFinalFee(finalFee)
|
|
}
|
|
|
|
if(!isInvoiceDonation || error) {
|
|
toggleResultModal()
|
|
}
|
|
}
|
|
|
|
// Subscribe to the task result event
|
|
EventEmitter.on('ev_transferTask_result', handleTransferTaskResult)
|
|
|
|
// Unsubscribe from the task result event on component unmount
|
|
return () => {
|
|
EventEmitter.off('ev_transferTask_result', handleTransferTaskResult)
|
|
}
|
|
}, [isTransferTaskSentToQueue])
|
|
|
|
|
|
|
|
const resetState = function () {
|
|
setEncodedInvoice('')
|
|
setInvoice(undefined)
|
|
setAmountToTransfer('')
|
|
setInvoiceExpiry(undefined)
|
|
setMeltQuote(undefined)
|
|
setMemo('')
|
|
setAvailableMintBalances([])
|
|
setMintBalanceToTransferFrom(undefined)
|
|
setTransactionStatus(undefined)
|
|
setInfo('')
|
|
setError(undefined)
|
|
setIsLoading(false)
|
|
setIsPasteInvoiceModalVisible(false)
|
|
setIsInvoiceDonation(false)
|
|
setIsTransferTaskSentToQueue(false)
|
|
setIsResultModalVisible(false)
|
|
setResultModalInfo(undefined)
|
|
setLnurlPayCommentAllowed(0)
|
|
setLnurlPayComment('')
|
|
}
|
|
|
|
const toggleResultModal = () => setIsResultModalVisible(previousState => !previousState)
|
|
|
|
const onMintBalanceSelect = function (balance: MintBalance) {
|
|
setMintBalanceToTransferFrom(balance) // this triggers effect to get estimated fees
|
|
}
|
|
|
|
// Amount is editable only in case of LNURL Pay, while invoice is not yet retrieved
|
|
const onRequestLnurlInvoice = async function () { // onAmountEndEditing
|
|
try {
|
|
const precision = getCurrency(unit).precision
|
|
const amount = round(toNumber(amountToTransfer) * precision, 0)
|
|
|
|
if (!amount || amount === 0) {
|
|
infoMessage(translate('payCommon.amountZeroOrNegative'))
|
|
return;
|
|
}
|
|
|
|
if(!lnurlPayParams) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing LNURL pay parameters', {caller: 'onAmountEndEditing'})
|
|
}
|
|
|
|
if (lnurlPayParams.minSendable && amount < lnurlPayParams.minSendable / 1000) {
|
|
infoMessage(translate('payCommon.minimumWithdraw', {
|
|
amount: roundUp(lnurlPayParams.minSendable / 1000, 0),
|
|
currency: CurrencyCode.SATS
|
|
}))
|
|
return;
|
|
}
|
|
|
|
if (lnurlPayParams.maxSendable && amount > lnurlPayParams.maxSendable / 1000) {
|
|
infoMessage(translate("payCommon.maximumPay", {
|
|
amount: roundDown(lnurlPayParams.maxSendable / 1000, 0),
|
|
currency: CurrencyCode.SATS
|
|
}))
|
|
return;
|
|
}
|
|
|
|
if (lnurlPayParams.payerData) {
|
|
infoMessage(translate("transferScreen.LUD18unsupported"))
|
|
}
|
|
|
|
setAmountToTransfer(`${numbro(amountToTransfer).format({thousandSeparated: true, mantissa: getCurrency(unit).mantissa})}`)
|
|
|
|
setIsLoading(true)
|
|
const encoded = await LnurlClient.getInvoice(lnurlPayParams, amount * 1000, lnurlPayCommentAllowed > 0 ? lnurlPayComment : void 0)
|
|
setIsLoading(false)
|
|
|
|
if (encoded) return onEncodedInvoice(encoded)
|
|
|
|
throw new AppError(Err.NOTFOUND_ERROR, `Could not get lightning invoice from ${lnurlPayParams.domain}`)
|
|
} catch (e: any) { handleError(e) }
|
|
}
|
|
|
|
const ensureCommentNotTooLong = async function () {
|
|
Keyboard.dismiss()
|
|
if (!lnurlPayCommentAllowed || lnurlPayComment.trim().length === 0) return;
|
|
if (lnurlPayComment.trim().length > lnurlPayCommentAllowed) {
|
|
setLnurlPayComment(lnurlPayComment.slice(0, lnurlPayCommentAllowed));
|
|
}
|
|
}
|
|
|
|
|
|
const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: string = '') {
|
|
log.trace("onEncodedInvoice")
|
|
try {
|
|
navigation.setParams({encodedInvoice: undefined})
|
|
navigation.setParams({paymentRequest: undefined})
|
|
navigation.setParams({lnurlParams: undefined})
|
|
navigation.setParams({paymentOption: undefined})
|
|
|
|
const invoice = LightningUtils.decodeInvoice(encoded)
|
|
const {amount, expiry, description, timestamp} = LightningUtils.getInvoiceData(invoice)
|
|
const expiresAt = addSeconds(new Date(timestamp as number * 1000), expiry as number)
|
|
|
|
if (!amount || amount === 0) {
|
|
infoMessage(translate('payCommon.amountZeroOrNegative'))
|
|
return;
|
|
}
|
|
|
|
if(!isInternetReachable) setInfo(translate('common.offlinePretty'));
|
|
|
|
setEncodedInvoice(encoded)
|
|
setInvoice(invoice)
|
|
setInvoiceExpiry(expiresAt)
|
|
|
|
if(description) {
|
|
setMemo(description)
|
|
}
|
|
|
|
if (paymentRequestDesc) {
|
|
setMemo(paymentRequestDesc)
|
|
}
|
|
|
|
if (lnurlPayComment) {
|
|
setMemo(lnurlPayComment)
|
|
}
|
|
|
|
// We need to retrieve the quote first to know how much is needed to settle invoice in selected currency unit
|
|
const { mintUrl } = route.params
|
|
const balanceToTransferFrom = mintUrl ?
|
|
proofsStore.getMintBalance(mintUrl) :
|
|
proofsStore.getMintBalancesWithUnit(unit)[0]
|
|
|
|
if (!balanceToTransferFrom) {
|
|
infoMessage(translate("transferScreen.noMintWithBalance", { unit }))
|
|
return
|
|
}
|
|
|
|
setMintBalanceToTransferFrom(balanceToTransferFrom)
|
|
// continues in hook that handles other mint selection by user
|
|
|
|
} catch (e: any) {
|
|
resetState()
|
|
handleError(e)
|
|
navigation.popToTop()
|
|
}
|
|
}
|
|
|
|
const transfer = async function () {
|
|
try {
|
|
if(!meltQuote) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing quote to initiate transfer transaction')
|
|
}
|
|
|
|
if (!mintBalanceToTransferFrom) {
|
|
setInfo(translate("transferScreen.selectMintFrom"))
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true)
|
|
setIsTransferTaskSentToQueue(true)
|
|
|
|
|
|
log.trace('[transfer]', {isInvoiceDonation})
|
|
|
|
const amountToTransferInt = round(toNumber(amountToTransfer) * getCurrency(unit).precision, 0)
|
|
|
|
WalletTask.transfer(
|
|
mintBalanceToTransferFrom,
|
|
amountToTransferInt,
|
|
unit,
|
|
meltQuote,
|
|
memo,
|
|
invoiceExpiry as Date,
|
|
encodedInvoice,
|
|
)
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
|
|
const onClose = function () {
|
|
resetState()
|
|
navigation.popToTop()
|
|
}
|
|
|
|
|
|
const handleError = function(e: AppError): void {
|
|
setIsLoading(false)
|
|
setError(e)
|
|
}
|
|
|
|
const headerBg = useThemeColor('header')
|
|
const iconColor = useThemeColor('textDim')
|
|
|
|
|
|
return (
|
|
<Screen preset="fixed" contentContainerStyle={$screen}>
|
|
<MintHeader
|
|
mint={
|
|
mintBalanceToTransferFrom
|
|
? mintsStore.findByUrl(mintBalanceToTransferFrom?.mintUrl)
|
|
: undefined
|
|
}
|
|
unit={unit}
|
|
navigation={navigation}
|
|
/>
|
|
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
|
<View style={$amountContainer}>
|
|
<TextInput
|
|
ref={amountInputRef}
|
|
onChangeText={amount => setAmountToTransfer(amount)}
|
|
// onEndEditing={onAmountEndEditing}
|
|
value={amountToTransfer}
|
|
style={$amountInput}
|
|
maxLength={9}
|
|
keyboardType="numeric"
|
|
selectTextOnFocus={true}
|
|
editable={encodedInvoice ? false : true}
|
|
/>
|
|
|
|
{encodedInvoice && (meltQuote?.fee_reserve || finalFee) ? (
|
|
<FeeBadge
|
|
currencyCode={getCurrency(unit).code}
|
|
estimatedFee={meltQuote?.fee_reserve || 0}
|
|
finalFee={finalFee}
|
|
/>
|
|
) : (
|
|
<Text
|
|
size="sm"
|
|
tx="payCommon.amountToPayLabel"
|
|
style={{color: 'white', textAlign: 'center'}}
|
|
/>
|
|
)}
|
|
</View>
|
|
</View>
|
|
<View style={$contentContainer}>
|
|
{transactionStatus !== TransactionStatus.COMPLETED && (
|
|
<Card
|
|
style={[$card, {minHeight: 50}]}
|
|
ContentComponent={
|
|
<>
|
|
<ListItem
|
|
text={
|
|
lnurlPayParams?.address ||
|
|
memo ||
|
|
lnurlPayParams?.domain ||
|
|
translate('common.noDescPlaceholder')
|
|
}
|
|
subText={lnurlDescription}
|
|
LeftComponent={
|
|
<Icon
|
|
containerStyle={$iconContainer}
|
|
icon="faInfoCircle"
|
|
size={spacing.medium}
|
|
color={iconColor}
|
|
/>
|
|
}
|
|
style={$item}
|
|
/>
|
|
{lnurlPayComment && encodedInvoice && (
|
|
<ListItem
|
|
text={lnurlPayComment}
|
|
topSeparator={true}
|
|
LeftComponent={
|
|
<Icon
|
|
containerStyle={$iconContainer}
|
|
icon="faPencil"
|
|
size={spacing.medium}
|
|
color={iconColor}
|
|
/>
|
|
}
|
|
style={$item}
|
|
/>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
{!encodedInvoice && transactionStatus !== TransactionStatus.COMPLETED && lnurlPayCommentAllowed > 0 && (
|
|
<MemoInputCard
|
|
memo={lnurlPayComment}
|
|
setMemo={setLnurlPayComment}
|
|
ref={lnurlCommentInputRef}
|
|
onMemoDone={ensureCommentNotTooLong}
|
|
disabled={encodedInvoice ? true : false}
|
|
maxLength={lnurlPayCommentAllowed}
|
|
/>
|
|
)}
|
|
{!encodedInvoice && transactionStatus !== TransactionStatus.COMPLETED && (
|
|
<View style={$bottomContainer}>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
tx="transferScreen.requestInvoice"
|
|
onPress={onRequestLnurlInvoice}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
{availableMintBalances.length > 0 &&
|
|
transactionStatus !== TransactionStatus.COMPLETED && (
|
|
<MintBalanceSelector
|
|
mintBalances={availableMintBalances}
|
|
selectedMintBalance={mintBalanceToTransferFrom}
|
|
unit={unit}
|
|
title={translate('payCommon.payFrom')}
|
|
confirmTitle={translate('payCommon.payNow')}
|
|
onMintBalanceSelect={onMintBalanceSelect}
|
|
onCancel={onClose}
|
|
onMintBalanceConfirm={transfer}
|
|
/>
|
|
)}
|
|
{transaction && transactionStatus === TransactionStatus.COMPLETED && (
|
|
<Card
|
|
style={{padding: spacing.medium}}
|
|
ContentComponent={
|
|
<>
|
|
<TranItem
|
|
label="tranDetailScreen.trasferredTo"
|
|
isFirst={true}
|
|
value={
|
|
mintsStore.findByUrl(transaction.mint)
|
|
?.shortname as string
|
|
}
|
|
/>
|
|
{transaction?.memo && (
|
|
<TranItem
|
|
label="tranDetailScreen.memoFromInvoice"
|
|
value={transaction.memo as string}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="transactionCommon.feePaid"
|
|
value={transaction.fee || 0}
|
|
unit={unit}
|
|
isCurrency={true}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.status"
|
|
value={transaction.status as string}
|
|
/>
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
{transactionStatus === TransactionStatus.COMPLETED && (
|
|
<View style={$bottomContainer}>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx={'common.close'}
|
|
onPress={onClose}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
{isLoading && <Loading />}
|
|
{error && <ErrorModal error={error} />}
|
|
{info && <InfoModal message={info} />}
|
|
</View>
|
|
<BottomModal
|
|
isVisible={isResultModalVisible}
|
|
ContentComponent={
|
|
<>
|
|
{resultModalInfo &&
|
|
transactionStatus === TransactionStatus.COMPLETED && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faCheckCircle"
|
|
iconColor={colors.palette.success200}
|
|
title={translate('payCommon.completed')}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx={'common.close'}
|
|
onPress={() => {
|
|
if (isInvoiceDonation) {
|
|
navigation.navigate('ContactsNavigator', {
|
|
screen: 'Contacts',
|
|
params: {},
|
|
})
|
|
} else {
|
|
navigation.navigate('Wallet', {})
|
|
}
|
|
}}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
{resultModalInfo &&
|
|
transactionStatus === TransactionStatus.REVERTED && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faRotate"
|
|
iconColor={colors.palette.accent300}
|
|
title={translate('transactionCommon.reverted')}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx={'common.close'}
|
|
onPress={toggleResultModal}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
{resultModalInfo &&
|
|
transactionStatus === TransactionStatus.ERROR && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faTriangleExclamation"
|
|
iconColor={colors.palette.angry500}
|
|
title={
|
|
resultModalInfo?.title || translate('payCommon.failed')
|
|
}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx={'common.close'}
|
|
onPress={toggleResultModal}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
{resultModalInfo &&
|
|
transactionStatus === TransactionStatus.PENDING && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faTriangleExclamation"
|
|
iconColor={colors.palette.iconYellow300}
|
|
title={translate('payCommon.isPending')}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx={'common.close'}
|
|
onPress={() => {
|
|
navigation.navigate('Wallet', {})
|
|
}}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
</>
|
|
}
|
|
onBackButtonPress={toggleResultModal}
|
|
onBackdropPress={toggleResultModal}
|
|
/>
|
|
</Screen>
|
|
)
|
|
}
|
|
)
|
|
|
|
|
|
const $screen: ViewStyle = {
|
|
flex: 1,
|
|
}
|
|
|
|
const $headerContainer: TextStyle = {
|
|
alignItems: 'center',
|
|
padding: spacing.extraSmall,
|
|
paddingTop: 0,
|
|
height: spacing.screenHeight * 0.20,
|
|
}
|
|
|
|
const $amountContainer: ViewStyle = {
|
|
}
|
|
|
|
const $amountInput: TextStyle = {
|
|
borderRadius: spacing.small,
|
|
margin: 0,
|
|
padding: 0,
|
|
fontSize: moderateVerticalScale(48),
|
|
fontFamily: typography.primary?.medium,
|
|
textAlign: 'center',
|
|
color: 'white',
|
|
}
|
|
|
|
const $commentInput: TextStyle = {
|
|
textAlignVertical: 'top' ,
|
|
borderRadius: spacing.extraSmall,
|
|
padding: spacing.extraSmall,
|
|
alignSelf: 'stretch',
|
|
height: 120,
|
|
}
|
|
|
|
const $contentContainer: TextStyle = {
|
|
flex: 1,
|
|
padding: spacing.extraSmall,
|
|
marginTop: -spacing.extraLarge * 2
|
|
}
|
|
|
|
const $iconContainer: ViewStyle = {
|
|
padding: spacing.extraSmall,
|
|
alignSelf: 'center',
|
|
marginRight: spacing.medium,
|
|
}
|
|
|
|
const $card: ViewStyle = {
|
|
marginBottom: spacing.small,
|
|
paddingTop: 0,
|
|
}
|
|
|
|
const $item: ViewStyle = {
|
|
paddingHorizontal: spacing.small,
|
|
paddingLeft: 0,
|
|
}
|
|
|
|
const $bottomModal: ViewStyle = {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
paddingVertical: spacing.large,
|
|
paddingHorizontal: spacing.small,
|
|
}
|
|
|
|
const $buttonContainer: ViewStyle = {
|
|
flexDirection: 'row',
|
|
alignSelf: 'center',
|
|
}
|
|
|
|
const $receiveMsg: ViewStyle = {
|
|
flexDirection: 'row',
|
|
borderRadius: spacing.large,
|
|
justifyContent: 'flex-start',
|
|
padding: spacing.small,
|
|
}
|
|
|
|
const $bottomContainer: ViewStyle = {
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
flex: 1,
|
|
justifyContent: 'flex-end',
|
|
marginBottom: spacing.medium,
|
|
alignSelf: 'stretch',
|
|
// opacity: 0,
|
|
}
|