mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-08-10 16:43:19 +00:00
1430 lines
52 KiB
TypeScript
1430 lines
52 KiB
TypeScript
import {observer} from 'mobx-react-lite'
|
|
import {getSnapshot} from 'mobx-state-tree'
|
|
import React, {
|
|
FC,
|
|
useEffect,
|
|
useState,
|
|
useCallback,
|
|
useRef,
|
|
useMemo,
|
|
} from 'react'
|
|
import {StackActions, StaticScreenProps, useFocusEffect, useNavigation} from '@react-navigation/native'
|
|
import {
|
|
UIManager,
|
|
Platform,
|
|
TextInput,
|
|
TextStyle,
|
|
View,
|
|
ViewStyle,
|
|
LayoutAnimation,
|
|
ScrollView,
|
|
FlatList,
|
|
ImageStyle,
|
|
Image,
|
|
} from 'react-native'
|
|
import {spacing, typography, useThemeColor, colors} from '../theme'
|
|
import {
|
|
Button,
|
|
Icon,
|
|
Card,
|
|
Screen,
|
|
Loading,
|
|
InfoModal,
|
|
ErrorModal,
|
|
BottomModal,
|
|
Text,
|
|
} from '../components'
|
|
import {TransactionStatus, Transaction} from '../models/Transaction'
|
|
import {useStores} from '../models'
|
|
import {NostrClient, SYNC_STATE_WITH_MINT_TASK, SyncStateTaskResult, TransactionTaskResult, WalletTask} from '../services'
|
|
import {log} from '../services/logService'
|
|
import AppError, {Err} from '../utils/AppError'
|
|
import {translate} from '../i18n'
|
|
import {MintBalance} from '../models/Mint'
|
|
import EventEmitter from '../utils/eventEmitter'
|
|
import {ResultModalInfo} from './Wallet/ResultModalInfo'
|
|
import useIsInternetReachable from '../utils/useIsInternetReachable'
|
|
import { Proof } from '../models/Proof'
|
|
import { Contact, ContactType } from '../models/Contact'
|
|
import { getImageSource, infoMessage } from '../utils/utils'
|
|
import { verticalScale } from '@gocodingnow/rn-size-matters'
|
|
import { MintUnit, MintUnits, formatCurrency, getCurrency } from "../services/wallet/currency"
|
|
import { MintHeader } from './Mints/MintHeader'
|
|
import { MintBalanceSelector } from './Mints/MintBalanceSelector'
|
|
import { round, toNumber } from '../utils/number'
|
|
import { QRCodeBlock } from './Wallet/QRCode'
|
|
import numbro from 'numbro'
|
|
import { TranItem } from './TranDetailScreen'
|
|
import { MemoInputCard } from '../components/MemoInputCard'
|
|
import { PaymentRequest as CashuPaymentRequest, PaymentRequestTransport, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken } from '@cashu/cashu-ts'
|
|
import { ProfilePointer } from 'nostr-tools/nip19'
|
|
import { MINIBITS_NIP05_DOMAIN } from '@env'
|
|
import { SEND_TASK } from '../services/wallet/sendTask'
|
|
|
|
export enum SendOption {
|
|
SEND_TOKEN = 'SEND_TOKEN',
|
|
PASTE_OR_SCAN_INVOICE = 'PASTE_OR_SCAN_INVOICE',
|
|
SHOW_TOKEN = 'SHOW_TOKEN',
|
|
PAY_PAYMENT_REQUEST = 'PAY_PAYMENT_REQUEST',
|
|
LNURL_PAY = 'LNURL_PAY',
|
|
LNURL_ADDRESS = 'LNURL_ADDRESS',
|
|
// DONATION = 'DONATION',
|
|
}
|
|
|
|
if (Platform.OS === 'android' &&
|
|
UIManager.setLayoutAnimationEnabledExperimental) {
|
|
UIManager.setLayoutAnimationEnabledExperimental(true)
|
|
}
|
|
|
|
type Props = StaticScreenProps<{
|
|
unit: MintUnit,
|
|
paymentOption?: SendOption,
|
|
encodedCashuPaymentRequest?: string,
|
|
contact?: Contact,
|
|
mintUrl?: string,
|
|
}>
|
|
|
|
export const SendScreen = observer(function SendScreen({ route }: Props) {
|
|
const navigation = useNavigation()
|
|
const isInternetReachable = useIsInternetReachable()
|
|
|
|
const {
|
|
proofsStore,
|
|
walletProfileStore,
|
|
transactionsStore,
|
|
mintsStore,
|
|
relaysStore,
|
|
walletStore,
|
|
contactsStore
|
|
} = useStores()
|
|
|
|
const amountInputRef = useRef<TextInput>(null)
|
|
const memoInputRef = useRef<TextInput>(null)
|
|
const unitRef = useRef<MintUnit>('sat')
|
|
|
|
const [paymentOption, setPaymentOption] = useState<SendOption>(SendOption.SHOW_TOKEN)
|
|
const [encodedTokenToSend, setEncodedTokenToSend] = useState<string | undefined>()
|
|
const [decodedCashuPaymentRequest, setDecodedCashuPaymentRequest] = useState<CashuPaymentRequest | undefined>()
|
|
const [amountToSend, setAmountToSend] = useState<string>('0')
|
|
//const [unit, setUnit] = useState<MintUnit>('sat')
|
|
const [contactToSendFrom, setContactToSendFrom] = useState<Contact| undefined>()
|
|
const [contactToSendTo, setContactToSendTo] = useState<Contact| undefined>()
|
|
const [relaysToShareTo, setRelaysToShareTo] = useState<string[]>([])
|
|
const [memo, setMemo] = useState('')
|
|
const [availableMintBalances, setAvailableMintBalances] = useState<MintBalance[]>([])
|
|
const [mintBalanceToSendFrom, setMintBalanceToSendFrom] = useState<MintBalance | undefined>()
|
|
const [selectedProofs, setSelectedProofs] = useState<Proof[]>([])
|
|
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus | undefined>()
|
|
const [transaction, setTransaction] = useState<Transaction | undefined>()
|
|
const [transactionId, setTransactionId] = useState<number | undefined>()
|
|
const [info, setInfo] = useState('')
|
|
const [error, setError] = useState<AppError | undefined>()
|
|
const [isAmountEndEditing, setIsAmountEndEditing] = useState<boolean>(false)
|
|
const [isSharedAsNostrDirectMessage, setIsSharedAsNostrDirectMessage] = useState<boolean>(false)
|
|
const [resultModalInfo, setResultModalInfo] = useState<{status: TransactionStatus, title?: string, message: string} | undefined>()
|
|
const [isMemoEndEditing, setIsMemoEndEditing] = useState<boolean>(false)
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
|
|
const [isMintSelectorVisible, setIsMintSelectorVisible] = useState(false)
|
|
const [isOfflineSend, setIsOfflineSend] = useState(false)
|
|
const [isCashuPrWithAmount, setIsCashuPrWithAmount] = useState(false)
|
|
const [isCashuPrWithDesc, setIsCashuPrWithDesc] = useState(false)
|
|
const [isNostrDMModalVisible, setIsNostrDMModalVisible] = useState(false)
|
|
const [isProofSelectorModalVisible, setIsProofSelectorModalVisible] = useState(false) // offline mode
|
|
const [isSendTaskSentToQueue, setIsSendTaskSentToQueue] = useState(false)
|
|
const [isResultModalVisible, setIsResultModalVisible] = useState(false)
|
|
const [isNostrDMSending, setIsNostrDMSending] = useState(false)
|
|
const [isNostrDMSuccess, setIsNostrDMSuccess] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const focus = () => {
|
|
amountInputRef && amountInputRef.current
|
|
? amountInputRef.current.focus()
|
|
: false
|
|
}
|
|
const timer = setTimeout(() => focus(), 400)
|
|
return () => {
|
|
clearTimeout(timer)
|
|
}
|
|
}, [])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
const setUnitAndMint = () => {
|
|
try {
|
|
const {unit, mintUrl} = route.params
|
|
if(!unit) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing mint unit in route params')
|
|
}
|
|
|
|
unitRef.current = unit
|
|
|
|
if(mintUrl) {
|
|
const mintBalance = proofsStore.getMintBalance(mintUrl)
|
|
setMintBalanceToSendFrom(mintBalance)
|
|
}
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
setUnitAndMint()
|
|
return () => {}
|
|
}, [])
|
|
|
|
|
|
// Send to contact
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
|
|
const {paymentOption, contact} = route.params
|
|
|
|
const getContactFrom = () => {
|
|
const {
|
|
pubkey,
|
|
npub,
|
|
name,
|
|
picture,
|
|
} = walletProfileStore
|
|
|
|
return {
|
|
pubkey,
|
|
npub,
|
|
name,
|
|
picture
|
|
} as Contact
|
|
}
|
|
|
|
const prepareSendToContact = () => {
|
|
try {
|
|
let relays: string[] = []
|
|
log.trace('[prepareSendToContact] selected contact', contact, paymentOption)
|
|
|
|
if(contact?.type === ContactType.PUBLIC) {
|
|
relays = relaysStore.allPublicUrls
|
|
} else {
|
|
relays = relaysStore.allUrls
|
|
}
|
|
|
|
if (!relays) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing NOSTR relays')
|
|
}
|
|
|
|
setPaymentOption(SendOption.SEND_TOKEN)
|
|
setContactToSendFrom(getContactFrom())
|
|
setContactToSendTo(contact)
|
|
setRelaysToShareTo(relays)
|
|
|
|
if(encodedTokenToSend) {
|
|
toggleNostrDMModal() // open if we already have a token
|
|
}
|
|
|
|
//reset
|
|
navigation.setParams({
|
|
paymentOption: undefined,
|
|
contact: undefined
|
|
})
|
|
|
|
} catch(e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
|
|
const handlePaymentRequest = async () => {
|
|
try {
|
|
setPaymentOption(SendOption.PAY_PAYMENT_REQUEST)
|
|
setContactToSendFrom(getContactFrom())
|
|
|
|
const {encodedCashuPaymentRequest} = route.params
|
|
|
|
if (!encodedCashuPaymentRequest) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing encodedCashuPaymentRequest.')
|
|
}
|
|
|
|
const pr: CashuPaymentRequest = decodePaymentRequest(encodedCashuPaymentRequest)
|
|
setDecodedCashuPaymentRequest(pr)
|
|
const transports: PaymentRequestTransport[] = pr.transport
|
|
|
|
for (const transport of transports) {
|
|
|
|
if (transport.type == PaymentRequestTransportType.NOSTR) {
|
|
|
|
const decoded = NostrClient.decodeNprofile(transport.target)
|
|
const pubkey = (decoded.data as ProfilePointer).pubkey
|
|
const npub = NostrClient.getNpubkey(pubkey)
|
|
let relays = (decoded.data as ProfilePointer).relays?.slice(0, 5)
|
|
|
|
if(!relays || relays.length === 0) {
|
|
relays = NostrClient.getAllRelays()
|
|
}
|
|
|
|
let contactTo = {
|
|
pubkey,
|
|
npub,
|
|
} as Contact
|
|
|
|
const existing = contactsStore.findByPubkey(pubkey)
|
|
|
|
if(!existing) {
|
|
try {
|
|
const profile = await NostrClient.getProfileFromRelays(pubkey, relays)
|
|
|
|
if(profile) {
|
|
contactTo.nip05 = profile.nip05
|
|
contactTo.picture = profile.picture
|
|
contactTo.lud16 = profile.lud16
|
|
contactTo.name = profile.name
|
|
contactTo.isExternalDomain = profile.nip05.includes(MINIBITS_NIP05_DOMAIN) ? false : true
|
|
|
|
contactsStore.addContact(contactTo)
|
|
}
|
|
|
|
} catch (e:any) {
|
|
log.warn('[handlePaymentRequest] Could not get the payee profile from relays.')
|
|
}
|
|
} else {
|
|
contactTo = existing
|
|
}
|
|
|
|
setContactToSendTo(contactTo)
|
|
setRelaysToShareTo(relays)
|
|
}
|
|
|
|
if (transport.type == PaymentRequestTransportType.POST) {
|
|
// TODO
|
|
}
|
|
}
|
|
|
|
log.trace('[handlePaymentRequest]', {pr})
|
|
|
|
if(pr.unit && !MintUnits.includes(pr.unit as MintUnit)) {
|
|
throw new AppError(Err.NOTFOUND_ERROR, `Wallet does not support ${pr.unit} unit.`)
|
|
}
|
|
|
|
if (pr.unit) {
|
|
unitRef.current = pr.unit as MintUnit
|
|
}
|
|
|
|
if (pr.description && pr.description.length > 0) {
|
|
setMemo(pr.description)
|
|
setIsCashuPrWithDesc(true)
|
|
}
|
|
|
|
if (pr.amount) {
|
|
setAmountToSend(`${numbro(pr.amount / getCurrency(unitRef.current).precision)
|
|
.format({
|
|
thousandSeparated: true,
|
|
mantissa: getCurrency(unitRef.current).mantissa
|
|
})}`)
|
|
|
|
setIsCashuPrWithAmount(true)
|
|
}
|
|
|
|
const availableBalances: MintBalance[] = []
|
|
|
|
if (pr.mints && pr.mints.length > 0) {
|
|
|
|
for (const mint of pr.mints) {
|
|
if (mintsStore.mintExists(mint)) {
|
|
const mintBalance = proofsStore.getMintBalance(mint)
|
|
availableBalances.push(mintBalance!)
|
|
}
|
|
}
|
|
|
|
if (availableBalances.length === 0) {
|
|
throw new AppError(Err.NOTFOUND_ERROR, 'Wallet has not any of requested mint.', {mints: pr.mints})
|
|
}
|
|
|
|
const withEnoughBalance = availableBalances.filter(balance => {
|
|
const unitBalance = balance.balances[unitRef.current]
|
|
|
|
if(!pr.amount) {
|
|
return balance
|
|
}
|
|
|
|
if(pr.amount && pr.amount > 0 && unitBalance && unitBalance >= pr.amount) {
|
|
return balance
|
|
}
|
|
|
|
return null
|
|
})
|
|
|
|
setAvailableMintBalances(withEnoughBalance)
|
|
setMintBalanceToSendFrom(withEnoughBalance[0])
|
|
|
|
} else {
|
|
let withEnoughBalance: MintBalance[] = []
|
|
|
|
if(pr.amount && pr.amount > 0) {
|
|
withEnoughBalance = proofsStore.getMintBalancesWithEnoughBalance(pr.amount, unitRef.current)
|
|
setAvailableMintBalances(withEnoughBalance)
|
|
} else {
|
|
withEnoughBalance = proofsStore.getMintBalancesWithUnit(unitRef.current)
|
|
setAvailableMintBalances(withEnoughBalance)
|
|
}
|
|
|
|
setMintBalanceToSendFrom(withEnoughBalance[0])
|
|
}
|
|
|
|
setIsMintSelectorVisible(true)
|
|
|
|
//reset
|
|
navigation.setParams({
|
|
paymentOption: undefined,
|
|
encodedCashuPaymentRequest: undefined
|
|
})
|
|
|
|
} catch(e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
if(paymentOption && contact && paymentOption === SendOption.SEND_TOKEN) {
|
|
prepareSendToContact()
|
|
}
|
|
|
|
if(paymentOption && paymentOption === SendOption.PAY_PAYMENT_REQUEST) {
|
|
handlePaymentRequest()
|
|
}
|
|
|
|
}, [route.params?.paymentOption])
|
|
)
|
|
|
|
|
|
// Offline send
|
|
useEffect(() => {
|
|
if(isInternetReachable) return
|
|
log.trace('[Offline send]')
|
|
|
|
// if offline we set all non-zero mint balances as available to allow ecash selection
|
|
const availableBalances = proofsStore.getMintBalancesWithEnoughBalance(1, unitRef.current)
|
|
|
|
if (availableBalances.length === 0) {
|
|
setInfo('There are not enough funds to send')
|
|
return
|
|
}
|
|
|
|
log.trace('Setting availableBalances')
|
|
|
|
setIsOfflineSend(true)
|
|
setAvailableMintBalances(availableBalances)
|
|
setMintBalanceToSendFrom(availableBalances[0])
|
|
setIsMintSelectorVisible(true)
|
|
}, [isInternetReachable])
|
|
|
|
|
|
useEffect(() => {
|
|
const handleSendTaskResult = async (result: TransactionTaskResult) => {
|
|
log.trace('handleSendTaskResult event handler triggered')
|
|
|
|
setIsLoading(false)
|
|
|
|
const {transaction} = result
|
|
|
|
setTransactionStatus(transaction?.status)
|
|
setTransaction(transaction)
|
|
setTransactionId(transaction?.id)
|
|
|
|
if (result.encodedTokenToSend) {
|
|
setEncodedTokenToSend(result.encodedTokenToSend)
|
|
}
|
|
|
|
if (result.error) {
|
|
setResultModalInfo({
|
|
status: result.transaction?.status as TransactionStatus,
|
|
title: result.error.params?.message ? result.error.message : 'Send failed',
|
|
message: result.error.params?.message || result.error.message,
|
|
})
|
|
setIsResultModalVisible(true)
|
|
return
|
|
}
|
|
|
|
setIsMintSelectorVisible(false)
|
|
|
|
if (paymentOption === SendOption.SEND_TOKEN) {
|
|
toggleNostrDMModal()
|
|
}
|
|
|
|
if (paymentOption === SendOption.PAY_PAYMENT_REQUEST) {
|
|
toggleNostrDMModal()
|
|
}
|
|
}
|
|
|
|
// Subscribe to the 'sendCompleted' event
|
|
if(isSendTaskSentToQueue) {
|
|
EventEmitter.on(`ev_${SEND_TASK}_result`, handleSendTaskResult)
|
|
}
|
|
|
|
// Unsubscribe from the 'sendCompleted' event on component unmount
|
|
return () => {
|
|
EventEmitter.off(`ev_${SEND_TASK}_result`, handleSendTaskResult)
|
|
}
|
|
}, [isSendTaskSentToQueue])
|
|
|
|
|
|
useEffect(() => {
|
|
const handleSendCompleted = async (result: SyncStateTaskResult) => {
|
|
log.trace('handleSendCompleted event handler triggered')
|
|
|
|
if (!transactionId) return
|
|
// Filter and handle event related only to this transactionId
|
|
if (result.completedTransactionIds && result.completedTransactionIds.includes(transactionId)) {
|
|
log.trace(
|
|
'Sent ecash has been claimed by the receiver for tx',
|
|
transactionId,
|
|
)
|
|
|
|
const amountSentInt = round(toNumber(amountToSend) * getCurrency(unitRef.current).precision, 0)
|
|
|
|
setIsNostrDMModalVisible(false)
|
|
setIsProofSelectorModalVisible(false)
|
|
setResultModalInfo({
|
|
status: TransactionStatus.COMPLETED,
|
|
title: '🚀 That was fast!',
|
|
message: `${formatCurrency(amountSentInt, getCurrency(unitRef.current).code)} ${getCurrency(unitRef.current).code} were received by the payee.`,
|
|
})
|
|
setTransactionStatus(TransactionStatus.COMPLETED)
|
|
setIsResultModalVisible(true)
|
|
}
|
|
|
|
// sync check might end with error in case tx proofs spentAmount !== tx amount
|
|
if (result.errorTransactionIds &&
|
|
result.errorTransactionIds.includes(transactionId)) {
|
|
|
|
log.trace(
|
|
'Error when completing the send tx',
|
|
{transactionId},
|
|
)
|
|
|
|
const statusUpdate = result.transactionStateUpdates.find(update => update.tId === transactionId)
|
|
const message = statusUpdate?.message || 'Error when completing the transaction.'
|
|
|
|
setIsNostrDMModalVisible(false)
|
|
setIsProofSelectorModalVisible(false)
|
|
setResultModalInfo({
|
|
status: TransactionStatus.ERROR,
|
|
title: 'Send failed',
|
|
message,
|
|
})
|
|
setTransactionStatus(TransactionStatus.ERROR)
|
|
setIsResultModalVisible(true)
|
|
}
|
|
}
|
|
|
|
// Subscribe to the '_syncStateWithMintTask' event
|
|
if(transactionId) {
|
|
EventEmitter.on(`ev_${SYNC_STATE_WITH_MINT_TASK}_result`, handleSendCompleted)
|
|
}
|
|
|
|
// Unsubscribe from the '_syncStateWithMintTask' event on component unmount
|
|
return () => {
|
|
EventEmitter.off(`ev_${SYNC_STATE_WITH_MINT_TASK}_result`, handleSendCompleted)
|
|
}
|
|
}, [transactionId])
|
|
|
|
const toggleNostrDMModal = () => setIsNostrDMModalVisible(previousState => !previousState)
|
|
const toggleProofSelectorModal = () => setIsProofSelectorModalVisible(previousState => !previousState)
|
|
const toggleResultModal = () => setIsResultModalVisible(previousState => !previousState)
|
|
|
|
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)
|
|
|
|
log.trace('[onAmountEndEditing]', amount)
|
|
|
|
if (!amount || amount === 0) {
|
|
infoMessage(translate('payCommon.amountZeroOrNegative'))
|
|
return
|
|
}
|
|
|
|
const availableBalances = proofsStore.getMintBalancesWithEnoughBalance(amount, unitRef.current)
|
|
|
|
if (availableBalances.length === 0) {
|
|
infoMessage(translate('payCommon.insufficientFunds'))
|
|
return
|
|
}
|
|
|
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.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])}
|
|
setIsAmountEndEditing(true)
|
|
// We do not make memo focus mandatory
|
|
// Show mint selector
|
|
setIsMintSelectorVisible(true)
|
|
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
|
|
const onMemoEndEditing = function () {
|
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
|
|
|
// Show mint selector
|
|
if (availableMintBalances.length > 0) {
|
|
setIsMintSelectorVisible(true)
|
|
}
|
|
setIsMemoEndEditing(true)
|
|
}
|
|
|
|
|
|
const onMemoDone = function () {
|
|
if (parseInt(amountToSend) > 0) {
|
|
memoInputRef && memoInputRef.current
|
|
? memoInputRef.current.blur()
|
|
: false
|
|
amountInputRef && amountInputRef.current
|
|
? amountInputRef.current.blur()
|
|
: false
|
|
onMemoEndEditing()
|
|
} else {
|
|
amountInputRef && amountInputRef.current
|
|
? amountInputRef.current.focus()
|
|
: false
|
|
}
|
|
}
|
|
|
|
|
|
const onMintBalanceSelect = function (balance: MintBalance) {
|
|
setMintBalanceToSendFrom(balance)
|
|
}
|
|
|
|
|
|
const onMintBalanceConfirm = async function () {
|
|
if (!mintBalanceToSendFrom) {
|
|
return
|
|
}
|
|
|
|
setIsLoading(true)
|
|
setIsSendTaskSentToQueue(true)
|
|
|
|
const amountToSendInt = round(toNumber(amountToSend) * getCurrency(unitRef.current).precision, 0)
|
|
|
|
WalletTask.sendQueue(
|
|
mintBalanceToSendFrom as MintBalance,
|
|
amountToSendInt,
|
|
unitRef.current,
|
|
memo,
|
|
selectedProofs
|
|
)
|
|
}
|
|
|
|
|
|
const increaseProofsCounterAndRetry = async function () {
|
|
try {
|
|
const walletInstance = await walletStore.getWallet(
|
|
mintBalanceToSendFrom?.mintUrl as string,
|
|
unitRef.current,
|
|
{withSeed: true}
|
|
)
|
|
const mintInstance = mintsStore.findByUrl(mintBalanceToSendFrom?.mintUrl as string)
|
|
const counter = mintInstance!.getProofsCounterByKeysetId!(walletInstance.keysetId)
|
|
counter!.increaseProofsCounter(20)
|
|
|
|
// retry send
|
|
onMintBalanceConfirm()
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
} finally {
|
|
toggleResultModal() //close
|
|
}
|
|
}
|
|
|
|
|
|
const retryAfterSpentCleaned = async function () {
|
|
try {
|
|
// retry send
|
|
onMintBalanceConfirm()
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
} finally {
|
|
toggleResultModal() //close
|
|
}
|
|
}
|
|
|
|
|
|
const onSelectProofsOffline = async function () {
|
|
if (!mintBalanceToSendFrom) {
|
|
return
|
|
}
|
|
|
|
setIsProofSelectorModalVisible(true)
|
|
}
|
|
|
|
|
|
|
|
const onMintBalanceCancel = async function () {
|
|
setIsMintSelectorVisible(false)
|
|
|
|
amountInputRef && amountInputRef.current
|
|
? amountInputRef.current.focus()
|
|
: false
|
|
}
|
|
|
|
|
|
|
|
const sendAsNostrDM = async function () {
|
|
try {
|
|
if(!contactToSendFrom || !contactToSendTo) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing sender or receiver information.')
|
|
}
|
|
|
|
if(!encodedTokenToSend) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing token to send.')
|
|
}
|
|
|
|
setIsNostrDMSending(true)
|
|
let messageContent: string | undefined = undefined
|
|
|
|
if(paymentOption === SendOption.SEND_TOKEN) {
|
|
const message = `nostr:${contactToSendFrom.npub} sent you ${amountToSend} ${getCurrency(unitRef.current).code} from Minibits wallet!`
|
|
messageContent = message + ' \n' + encodedTokenToSend
|
|
}
|
|
|
|
if(paymentOption === SendOption.PAY_PAYMENT_REQUEST) {
|
|
if(!decodedCashuPaymentRequest) {
|
|
throw new AppError(Err.VALIDATION_ERROR, 'Missing payment request to pay.')
|
|
}
|
|
|
|
const decodedTokenToSend = getDecodedToken(encodedTokenToSend)
|
|
|
|
messageContent = JSON.stringify({
|
|
id: decodedCashuPaymentRequest.id,
|
|
mint: decodedTokenToSend.mint,
|
|
unit: decodedTokenToSend.unit,
|
|
proofs: decodedTokenToSend.proofs,
|
|
})
|
|
}
|
|
|
|
const sentEvent = await NostrClient.encryptAndSendDirectMessageNip17(
|
|
contactToSendTo.pubkey,
|
|
messageContent!,
|
|
relaysToShareTo
|
|
)
|
|
|
|
setIsNostrDMSending(false)
|
|
|
|
if(sentEvent) {
|
|
setIsNostrDMSuccess(true)
|
|
|
|
if(!transactionId) {
|
|
return
|
|
}
|
|
|
|
const transaction = transactionsStore.findById(transactionId)
|
|
|
|
if(!transaction || !transaction.data) {
|
|
return
|
|
}
|
|
|
|
const updated = JSON.parse(transaction.data)
|
|
|
|
if(updated.length > 2) {
|
|
updated[2].sentToRelays = relaysToShareTo
|
|
updated[2].sentEvent = sentEvent
|
|
|
|
transaction.setStatus( // status does not change, just add event and relay info to tx.data
|
|
TransactionStatus.PENDING,
|
|
JSON.stringify(updated)
|
|
)
|
|
}
|
|
|
|
if(contactToSendTo) {
|
|
transaction.setProfile(
|
|
JSON.stringify(contactToSendTo)
|
|
)
|
|
|
|
transaction.setSentTo(
|
|
contactToSendTo.nip05handle ?? contactToSendTo.name!
|
|
)
|
|
}
|
|
|
|
} else {
|
|
setInfo('Nostr relays could not confirm that the message has been sent')
|
|
}
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
|
|
const toggleSelectedProof = function (proof: Proof) {
|
|
setSelectedProofs(prevSelectedProofs => {
|
|
const isSelected = prevSelectedProofs.some(
|
|
p => p.secret === proof.secret
|
|
)
|
|
|
|
if (isSelected) {
|
|
// If the proof is already selected, remove it from the array
|
|
setAmountToSend(`${parseInt(amountToSend) - proof.amount}`)
|
|
return prevSelectedProofs.filter(p => p.secret !== proof.secret)
|
|
} else {
|
|
// If the proof is not selected, add it to the array
|
|
setAmountToSend(`${(parseInt(amountToSend) || 0) + proof.amount}`)
|
|
return [...prevSelectedProofs, proof]
|
|
}
|
|
})
|
|
}
|
|
|
|
const resetSelectedProofs = function () {
|
|
setSelectedProofs([])
|
|
setAmountToSend('0')
|
|
}
|
|
|
|
|
|
const onOfflineSendConfirm = function () {
|
|
toggleProofSelectorModal() // close
|
|
onMintBalanceConfirm()
|
|
}
|
|
|
|
|
|
const gotoContacts = function () {
|
|
//@ts-ignore
|
|
navigation.navigate('ContactsNavigator', {
|
|
screen: 'Contacts',
|
|
params: {paymentOption: SendOption.SEND_TOKEN}
|
|
})
|
|
}
|
|
|
|
|
|
const resetState = function () {
|
|
// reset state so it does not interfere next payment
|
|
setAmountToSend('')
|
|
setMemo('')
|
|
setIsAmountEndEditing(false)
|
|
setIsMemoEndEditing(false)
|
|
setIsMintSelectorVisible(false)
|
|
setIsNostrDMModalVisible(false)
|
|
setIsSharedAsNostrDirectMessage(false)
|
|
setIsNostrDMSending(false)
|
|
setIsNostrDMModalVisible(false)
|
|
setIsProofSelectorModalVisible(false)
|
|
setIsLoading(false)
|
|
setResultModalInfo(undefined)
|
|
setIsResultModalVisible(false)
|
|
|
|
navigation.dispatch(
|
|
StackActions.popToTop()
|
|
)
|
|
}
|
|
|
|
|
|
const handleError = function(e: AppError): void {
|
|
// TODO resetState() on all tx data on error? Or save txId to state and allow retry / recovery?
|
|
setIsNostrDMSending(false)
|
|
setIsProofSelectorModalVisible(false)
|
|
setIsNostrDMModalVisible(false)
|
|
setIsLoading(false)
|
|
setError(e)
|
|
}
|
|
|
|
const headerBg = useThemeColor('header')
|
|
const amountInputColor = useThemeColor('amountInput')
|
|
|
|
return (
|
|
<Screen preset="fixed" contentContainerStyle={$screen}>
|
|
<MintHeader
|
|
mint={mintBalanceToSendFrom ? mintsStore.findByUrl(mintBalanceToSendFrom?.mintUrl) : undefined}
|
|
unit={unitRef.current}
|
|
/>
|
|
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
|
<View style={$amountContainer}>
|
|
<TextInput
|
|
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
|
|
}
|
|
/>
|
|
<Text
|
|
size='sm'
|
|
text="Amount to send"
|
|
style={{color: amountInputColor, textAlign: 'center'}}
|
|
/>
|
|
</View>
|
|
</View>
|
|
<View style={$contentContainer}>
|
|
{!encodedTokenToSend && (
|
|
<MemoInputCard
|
|
memo={memo}
|
|
ref={memoInputRef}
|
|
setMemo={setMemo}
|
|
disabled={transactionStatus === TransactionStatus.PENDING || isCashuPrWithDesc}
|
|
onMemoDone={onMemoDone}
|
|
onMemoEndEditing={onMemoEndEditing}
|
|
/>
|
|
)}
|
|
{isMintSelectorVisible && (
|
|
<MintBalanceSelector
|
|
mintBalances={availableMintBalances}
|
|
selectedMintBalance={mintBalanceToSendFrom as MintBalance}
|
|
unit={unitRef.current}
|
|
title='Send from mint'
|
|
confirmTitle={isOfflineSend ? 'Send offline' : 'Create token'}
|
|
onMintBalanceSelect={onMintBalanceSelect}
|
|
onCancel={onMintBalanceCancel}
|
|
onMintBalanceConfirm={isOfflineSend ? onSelectProofsOffline : onMintBalanceConfirm}
|
|
/>
|
|
)}
|
|
{transactionStatus === TransactionStatus.PENDING && encodedTokenToSend && paymentOption && (
|
|
<>
|
|
<QRCodeBlock
|
|
qrCodeData={encodedTokenToSend as string}
|
|
title='Ecash token to send'
|
|
type='EncodedV4Token'
|
|
/>
|
|
<TokenOptionsBlock
|
|
toggleNostrDMModal={toggleNostrDMModal}
|
|
contactToSendTo={contactToSendTo}
|
|
gotoContacts={gotoContacts}
|
|
/>
|
|
</>
|
|
)}
|
|
{transaction && transactionStatus === TransactionStatus.COMPLETED && (
|
|
<Card
|
|
style={{padding: spacing.medium}}
|
|
ContentComponent={
|
|
<>
|
|
<TranItem
|
|
label="tranDetailScreen.sentTo"
|
|
isFirst={true}
|
|
value={mintsStore.findByUrl(transaction.mint)?.shortname as string}
|
|
/>
|
|
{transaction?.memo && (
|
|
<TranItem
|
|
label="receiverMemo"
|
|
value={transaction.memo as string}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="transactionCommon.feePaid"
|
|
value={transaction.fee || 0}
|
|
unit={unitRef.current}
|
|
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={resetState}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
<BottomModal
|
|
isVisible={isProofSelectorModalVisible}
|
|
ContentComponent={
|
|
<SelectProofsBlock
|
|
mintBalanceToSendFrom={mintBalanceToSendFrom as MintBalance}
|
|
unit={unitRef.current}
|
|
selectedProofs={selectedProofs}
|
|
toggleProofSelectorModal={toggleProofSelectorModal}
|
|
toggleSelectedProof={toggleSelectedProof}
|
|
resetSelectedProofs={resetSelectedProofs}
|
|
onOfflineSendConfirm={onOfflineSendConfirm}
|
|
/>
|
|
}
|
|
onBackButtonPress={toggleProofSelectorModal}
|
|
onBackdropPress={toggleProofSelectorModal}
|
|
/>
|
|
<BottomModal
|
|
isVisible={isNostrDMModalVisible ? true : false}
|
|
ContentComponent={
|
|
(isNostrDMSuccess ? (
|
|
<NostrDMSuccessBlock
|
|
toggleNostrDMModal={toggleNostrDMModal}
|
|
contactToSendFrom={contactToSendFrom as Contact}
|
|
contactToSendTo={contactToSendTo as Contact}
|
|
amountToSend={amountToSend}
|
|
onClose={resetState}
|
|
/>
|
|
) : (
|
|
<SendAsNostrDMBlock
|
|
toggleNostrDMModal={toggleNostrDMModal}
|
|
encodedTokenToSend={encodedTokenToSend as string}
|
|
contactToSendFrom={contactToSendFrom as Contact}
|
|
contactToSendTo={contactToSendTo as Contact}
|
|
relaysToShareTo={relaysToShareTo}
|
|
amountToSend={amountToSend}
|
|
unit={unitRef.current}
|
|
sendAsNostrDM={sendAsNostrDM}
|
|
isNostrDMSending={isNostrDMSending}
|
|
/>
|
|
))
|
|
|
|
}
|
|
onBackButtonPress={toggleNostrDMModal}
|
|
onBackdropPress={toggleNostrDMModal}
|
|
/>
|
|
<BottomModal
|
|
isVisible={isResultModalVisible ? true : false}
|
|
ContentComponent={
|
|
<>
|
|
{resultModalInfo &&
|
|
transactionStatus === TransactionStatus.COMPLETED && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faCheckCircle"
|
|
iconColor={colors.palette.success200}
|
|
title={resultModalInfo?.title || "Success!"}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx={'common.close'}
|
|
onPress={() => navigation.navigate('Wallet', {})}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
{resultModalInfo &&
|
|
transactionStatus === TransactionStatus.ERROR && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faTriangleExclamation"
|
|
iconColor={colors.palette.angry500}
|
|
title={resultModalInfo?.title || "Send failed"}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
{resultModalInfo.message.includes('outputs have already been signed before') ? (
|
|
<Button
|
|
preset="secondary"
|
|
text={"Retry again"}
|
|
onPress={increaseProofsCounterAndRetry}
|
|
/>
|
|
) : resultModalInfo.message.includes('Token already spent') || resultModalInfo.message.includes('Some spent ecash') ? (
|
|
<Button
|
|
preset="secondary"
|
|
text={"Retry again"}
|
|
onPress={retryAfterSpentCleaned}
|
|
/>
|
|
) : (
|
|
<Button
|
|
preset="secondary"
|
|
tx={'common.close'}
|
|
onPress={toggleResultModal}
|
|
/>
|
|
)}
|
|
</View>
|
|
</>
|
|
)}
|
|
</>
|
|
}
|
|
onBackButtonPress={toggleResultModal}
|
|
onBackdropPress={toggleResultModal}
|
|
/>
|
|
{isLoading && <Loading />}
|
|
{error && <ErrorModal error={error} />}
|
|
{info && <InfoModal message={info} />}
|
|
</Screen>
|
|
)
|
|
}
|
|
)
|
|
|
|
|
|
const SelectProofsBlock = observer(function (props: {
|
|
mintBalanceToSendFrom: MintBalance
|
|
unit: MintUnit
|
|
selectedProofs: Proof[]
|
|
toggleProofSelectorModal: any
|
|
toggleSelectedProof: any
|
|
resetSelectedProofs: any
|
|
onOfflineSendConfirm: any
|
|
}) {
|
|
|
|
const {proofsStore} = useStores()
|
|
const hintColor = useThemeColor('textDim')
|
|
|
|
|
|
const onCancel = function () {
|
|
props.resetSelectedProofs()
|
|
props.toggleProofSelectorModal()
|
|
}
|
|
|
|
return (
|
|
<View style={$bottomModal}>
|
|
<Text text='Select ecash to send' />
|
|
<Text
|
|
text='You can send only exact ecash denominations while you are offline.'
|
|
style={{color: hintColor, paddingHorizontal: spacing.small, textAlign: 'center', marginBottom: spacing.small}}
|
|
size='xs'
|
|
/>
|
|
<View style={{maxHeight: spacing.screenHeight * 0.45}}>
|
|
<FlatList<Proof>
|
|
data={proofsStore.getByMint(props.mintBalanceToSendFrom.mintUrl, {isPending: false, unit: props.unit})}
|
|
renderItem={({ item }) => {
|
|
const isSelected = props.selectedProofs.some(
|
|
p => p.secret === item.secret
|
|
)
|
|
|
|
return (
|
|
<Button
|
|
preset={isSelected ? 'default' : 'secondary'}
|
|
onPress={() => props.toggleSelectedProof(item)}
|
|
text={`${item.amount}`}
|
|
style={{minWidth: 80, margin: spacing.small}}
|
|
/>
|
|
)
|
|
}}
|
|
numColumns={3}
|
|
keyExtractor={(item) => item.secret}
|
|
/>
|
|
</View>
|
|
<View style={[$buttonContainer, {marginTop: spacing.extraLarge}]}>
|
|
<Button
|
|
text="Create token"
|
|
onPress={props.onOfflineSendConfirm}
|
|
style={{marginRight: spacing.medium}}
|
|
/>
|
|
<Button
|
|
preset="secondary"
|
|
text="Cancel"
|
|
onPress={onCancel}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)
|
|
|
|
})
|
|
|
|
|
|
const TokenOptionsBlock = observer(function (props: {
|
|
toggleNostrDMModal: any
|
|
contactToSendTo?: Contact
|
|
gotoContacts: any
|
|
}) {
|
|
|
|
return (
|
|
<View style={{flex: 1}}>
|
|
<View style={$bottomContainer}>
|
|
<View style={$buttonContainer}>
|
|
{props.contactToSendTo ? (
|
|
<Button
|
|
text={`Send to ${props.contactToSendTo.nip05}`}
|
|
preset='secondary'
|
|
onPress={props.toggleNostrDMModal}
|
|
style={{
|
|
minHeight: verticalScale(40),
|
|
paddingVertical: verticalScale(spacing.tiny),
|
|
}}
|
|
LeftAccessory={() => (
|
|
<Icon
|
|
icon='faPaperPlane'
|
|
// color="white"
|
|
size={spacing.medium}
|
|
/>
|
|
)}
|
|
/>
|
|
) : (
|
|
<Button
|
|
text='Send to contact'
|
|
preset='secondary'
|
|
onPress={props.gotoContacts}
|
|
style={{
|
|
minHeight: verticalScale(40),
|
|
paddingVertical: verticalScale(spacing.tiny),
|
|
}}
|
|
LeftAccessory={() => (
|
|
<Icon
|
|
icon='faPaperPlane'
|
|
// color="white"
|
|
size={spacing.medium}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)
|
|
})
|
|
|
|
|
|
const SendAsNostrDMBlock = observer(function (props: {
|
|
toggleNostrDMModal: any
|
|
encodedTokenToSend: string
|
|
contactToSendFrom: Contact
|
|
contactToSendTo: Contact
|
|
relaysToShareTo: string[]
|
|
amountToSend: string
|
|
unit: MintUnit
|
|
sendAsNostrDM: any
|
|
isNostrDMSending: boolean
|
|
}) {
|
|
const sendBg = useThemeColor('background')
|
|
const tokenTextColor = useThemeColor('textDim')
|
|
|
|
return (
|
|
<View style={$bottomModal}>
|
|
<Text text={'Send to contact'} />
|
|
<NostDMInfoBlock
|
|
contactToSendFrom={props.contactToSendFrom}
|
|
amountToSend={props.amountToSend}
|
|
unit={props.unit}
|
|
contactToSendTo={props.contactToSendTo}
|
|
/>
|
|
<ScrollView
|
|
style={[
|
|
$tokenContainer,
|
|
{backgroundColor: sendBg, marginHorizontal: spacing.small},
|
|
]}>
|
|
<Text
|
|
selectable
|
|
text={props.encodedTokenToSend}
|
|
style={{color: tokenTextColor, paddingBottom: spacing.medium, fontFamily: typography.code?.normal}}
|
|
size="xxs"
|
|
/>
|
|
</ScrollView>
|
|
{props.isNostrDMSending ? (
|
|
<View style={[$buttonContainer, {minHeight: verticalScale(55)}]}>
|
|
<Loading />
|
|
</View>
|
|
) : (
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
text="Send"
|
|
onPress={props.sendAsNostrDM}
|
|
style={{marginRight: spacing.medium}}
|
|
LeftAccessory={() => (
|
|
<Icon
|
|
icon="faPaperPlane"
|
|
color="white"
|
|
size={spacing.medium}
|
|
//containerStyle={{marginRight: spacing.small}}
|
|
/>
|
|
)}
|
|
/>
|
|
<Button
|
|
preset="tertiary"
|
|
text="Close"
|
|
onPress={props.toggleNostrDMModal}
|
|
/>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)
|
|
})
|
|
|
|
|
|
const NostrDMSuccessBlock = observer(function (props: {
|
|
toggleNostrDMModal: any
|
|
contactToSendFrom: Contact
|
|
contactToSendTo: Contact
|
|
amountToSend: string
|
|
onClose: any
|
|
}) {
|
|
|
|
return (
|
|
<View style={$bottomModal}>
|
|
{/*<NostDMInfoBlock
|
|
contactToSendFrom={props.contactToSendFrom}
|
|
amountToSend={props.amountToSend}
|
|
contactToSendTo={props.contactToSendTo}
|
|
/>*/}
|
|
<ResultModalInfo
|
|
icon="faCheckCircle"
|
|
iconColor={colors.palette.success200}
|
|
title="Success!"
|
|
message="Ecash has been successfully sent."
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx={'common.close'}
|
|
onPress={props.onClose}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)
|
|
})
|
|
|
|
const NostDMInfoBlock = observer(function (props: {
|
|
contactToSendFrom: Contact
|
|
amountToSend: string
|
|
unit: MintUnit
|
|
contactToSendTo: Contact
|
|
}) {
|
|
|
|
const {walletProfileStore} = useStores()
|
|
const tokenTextColor = useThemeColor('textDim')
|
|
const amountToSendInt = round(toNumber(props.amountToSend) * getCurrency(props.unit).precision, 0)
|
|
const amountToSendDisplay = formatCurrency(amountToSendInt, getCurrency(props.unit).code)
|
|
|
|
|
|
return(
|
|
<View style={{flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', marginBottom: spacing.medium}}>
|
|
<View style={{flexDirection: 'column', alignItems: 'center', width: 100}}>
|
|
<Image style={[
|
|
$profileIcon, {
|
|
width: 40,
|
|
height: walletProfileStore.isOwnProfile ? 40 : 43,
|
|
borderRadius: walletProfileStore.isOwnProfile ? 20 : 0,
|
|
}]}
|
|
source={{
|
|
uri: getImageSource(props.contactToSendFrom.picture as string)
|
|
}}
|
|
/>
|
|
<Text size='xxs' style={{color: tokenTextColor}} text={props.contactToSendFrom.name}/>
|
|
</View>
|
|
<Text size='xxs' style={{color: tokenTextColor, textAlign: 'center', marginLeft: 30, marginBottom: 20}} text='...........' />
|
|
<View style={{flexDirection: 'column', alignItems: 'center'}}>
|
|
<Icon
|
|
icon='faPaperPlane'
|
|
size={spacing.medium}
|
|
color={tokenTextColor}
|
|
/>
|
|
<Text size='xxs' style={{color: tokenTextColor, marginBottom: -10}} text={`${amountToSendDisplay} ${getCurrency(props.unit).code}`} />
|
|
</View>
|
|
<Text size='xxs' style={{color: tokenTextColor, textAlign: 'center', marginRight: 30, marginBottom: 20}} text='...........' />
|
|
<View style={{flexDirection: 'column', alignItems: 'center', width: 100}}>
|
|
{props.contactToSendTo.picture ? (
|
|
<View style={{borderRadius: 20, overflow: 'hidden'}}>
|
|
<Image style={[
|
|
$profileIcon, {
|
|
width: 40,
|
|
height: props.contactToSendTo.isExternalDomain ? 40 : 43,
|
|
borderRadius: props.contactToSendTo.isExternalDomain ? 20 : 0,
|
|
}]}
|
|
source={{
|
|
uri: getImageSource(props.contactToSendTo.picture as string)
|
|
}}
|
|
/>
|
|
</View>
|
|
) : (
|
|
<Icon
|
|
icon='faCircleUser'
|
|
size={38}
|
|
color={tokenTextColor}
|
|
/>
|
|
)}
|
|
<Text size='xxs' style={{color: tokenTextColor}} text={props.contactToSendTo.name || props.contactToSendTo.npub.slice(0, 10)}/>
|
|
</View>
|
|
</View>
|
|
)
|
|
|
|
})
|
|
|
|
|
|
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: verticalScale(48),
|
|
fontFamily: typography.primary?.medium,
|
|
textAlign: 'center',
|
|
color: 'white',
|
|
}
|
|
|
|
const $contentContainer: TextStyle = {
|
|
flex: 1,
|
|
padding: spacing.extraSmall,
|
|
marginTop: -spacing.extraLarge * 1.5
|
|
}
|
|
|
|
|
|
const $tokenContainer: ViewStyle = {
|
|
borderRadius: spacing.small,
|
|
alignSelf: 'stretch',
|
|
padding: spacing.small,
|
|
maxHeight: 114,
|
|
marginTop: spacing.small,
|
|
marginBottom: spacing.large,
|
|
}
|
|
|
|
const $memoButton: ViewStyle = {
|
|
maxHeight: 50,
|
|
}
|
|
|
|
const $card: ViewStyle = {
|
|
marginBottom: spacing.small,
|
|
paddingTop: 0,
|
|
}
|
|
|
|
const $item: ViewStyle = {
|
|
paddingHorizontal: spacing.small,
|
|
paddingLeft: 0,
|
|
}
|
|
|
|
const $bottomModal: ViewStyle = {
|
|
alignItems: 'center',
|
|
paddingVertical: spacing.large,
|
|
}
|
|
|
|
const $qrCodeContainer: ViewStyle = {
|
|
backgroundColor: 'white',
|
|
paddingHorizontal: spacing.small,
|
|
marginHorizontal: spacing.small,
|
|
marginBottom: spacing.small,
|
|
borderRadius: spacing.small,
|
|
alignItems: 'center',
|
|
}
|
|
|
|
const $buttonContainer: ViewStyle = {
|
|
flexDirection: 'row',
|
|
alignSelf: 'center',
|
|
}
|
|
|
|
const $profileIcon: ImageStyle = {
|
|
padding: spacing.medium,
|
|
}
|
|
|
|
const $bottomContainer: ViewStyle = {
|
|
// position: 'absolute',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
flex: 1,
|
|
justifyContent: 'flex-end',
|
|
marginBottom: spacing.tiny,
|
|
alignSelf: 'stretch',
|
|
// opacity: 0,
|
|
}
|
|
|
|
|