Files
minibits_wallet/src/screens/TopupScreen.tsx
T
2024-01-25 22:17:09 +01:00

1306 lines
44 KiB
TypeScript

import {observer} from 'mobx-react-lite'
import React, {FC, useEffect, useState, useRef, useCallback} from 'react'
import {
UIManager,
Platform,
Alert,
TextInput,
TextStyle,
View,
ViewStyle,
LayoutAnimation,
ScrollView,
Share,
Image,
ImageStyle,
FlatList,
} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
import QRCode from 'react-native-qrcode-svg'
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 {TransactionStatus, Transaction} from '../models/Transaction'
import {useStores} from '../models'
import {useHeader} from '../utils/useHeader'
import {NostrClient, NostrProfile, Wallet} from '../services'
import { UnsignedEvent as NostrUnsignedEvent } from 'nostr-tools/core'
import {log} from '../services/logService'
import AppError, { Err } from '../utils/AppError'
import {Mint, MintBalance} from '../models/Mint'
import {MintListItem} from './Mints/MintListItem'
import EventEmitter from '../utils/eventEmitter'
import {ResultModalInfo} from './Wallet/ResultModalInfo'
import {PaymentRequest} from '../models/PaymentRequest'
import { useFocusEffect } from '@react-navigation/native'
import { Contact } from '../models/Contact'
import { getImageSource, infoMessage } from '../utils/utils'
import { ReceiveOption } from './ReceiveOptionsScreen'
import { LNURLWithdrawParams } from 'js-lnurl'
import { roundDown } from '../utils/number'
import { LnurlClient, LnurlWithdrawResult } from '../services/lnurlService'
import { update } from 'lodash'
import { verticalScale } from '@gocodingnow/rn-size-matters'
import { CurrencyCode, CurrencySign } from './Wallet/CurrencySign'
if (
Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true)
}
export const TopupScreen: FC<WalletStackScreenProps<'Topup'>> = observer(
function TopupScreen({navigation, route}) {
useHeader({
leftIcon: 'faArrowLeft',
onLeftPress: () => navigation.goBack(),
})
const {proofsStore, mintsStore, walletProfileStore, transactionsStore} = useStores()
const amountInputRef = useRef<TextInput>(null)
const memoInputRef = useRef<TextInput>(null)
// const tokenInputRef = useRef<TextInput>(null)
const [paymentOption, setPaymentOption] = useState<ReceiveOption>(ReceiveOption.SHOW_INVOICE)
const [amountToTopup, setAmountToTopup] = useState<string>('')
const [contactToSendFrom, setContactToSendFrom] = useState<Contact| undefined>()
const [contactToSendTo, setContactToSendTo] = useState<Contact| undefined>()
const [relaysToShareTo, setRelaysToShareTo] = useState<string[]>([])
const [lnurlWithdrawParams, setLnurlWithdrawParams] = useState<LNURLWithdrawParams | undefined>()
const [memo, setMemo] = useState('')
const [availableMintBalances, setAvailableMintBalances] = useState<MintBalance[]>([])
const [mintBalanceToTopup, setMintBalanceToTopup] = useState<MintBalance | undefined>(undefined)
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus | undefined>()
const [transactionId, setTransactionId] = useState<number | undefined>()
const [invoiceToPay, setInvoiceToPay] = useState<string>('')
const [lnurlWithdrawResult, setLnurlWithdrawResult] = useState<LnurlWithdrawResult | undefined>()
const [info, setInfo] = useState('')
const [error, setError] = useState<AppError | undefined>()
const [isAmountEndEditing, setIsAmountEndEditing] = useState<boolean>(false)
const [isMemoEndEditing, setIsMemoEndEditing] = useState<boolean>(false)
const [resultModalInfo, setResultModalInfo] = useState<{status: TransactionStatus; title?: string, message: string} | undefined>()
const [isLoading, setIsLoading] = useState(false)
const [isMintSelectorVisible, setIsMintSelectorVisible] = useState(false)
const [isQRModalVisible, setIsQRModalVisible] = useState(false)
const [isNostrDMModalVisible, setIsNostrDMModalVisible] = useState(false)
const [isWithdrawModalVisible, setIsWithdrawModalVisible] = useState(false)
const [isResultModalVisible, setIsResultModalVisible] = useState(false)
const [isNostrDMSending, setIsNostrDMSending] = useState(false)
const [isNostrDMSuccess, setIsNostrDMSuccess] = useState(false)
const [isWithdrawRequestSending, setIsWithdrawRequestSending] = useState(false)
const [isWithdrawRequestSuccess, setIsWithdrawRequestSuccess] = useState(false)
useEffect(() => {
const focus = () => {
amountInputRef && amountInputRef.current
? amountInputRef.current.focus()
: false
}
const timer = setTimeout(() => focus(), 100)
return () => {
clearTimeout(timer)
}
}, [])
// Send to contact
useFocusEffect(
useCallback(() => {
const { paymentOption } = route.params
const prepareSendPaymentRequest = () => {
try {
const {contact, relays} = route.params
if (!contact || !relays) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing contact or relay')
}
const {
pubkey,
npub,
name,
picture,
} = walletProfileStore
const contactFrom: Contact = {
pubkey,
npub,
name,
picture
}
setPaymentOption(ReceiveOption.SEND_PAYMENT_REQUEST)
setContactToSendFrom(contactFrom)
setContactToSendTo(contact)
setRelaysToShareTo(relays)
navigation.setParams({contact: undefined})
navigation.setParams({relays: undefined})
} catch(e: any) {
handleError(e)
}
}
const prepareLnurlWithdraw = () => {
try {
const { lnurlParams } = route.params
if (!lnurlParams) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing LNURL params.')
}
const amountSats = roundDown(lnurlParams.maxWithdrawable / 1000, 0)
setAmountToTopup(`${amountSats}`)
setLnurlWithdrawParams(lnurlParams)
setMemo(lnurlParams.defaultDescription)
setPaymentOption(ReceiveOption.LNURL_WITHDRAW)
// onAmountEndEditing(`${amountSats}`)
} catch(e: any) {
handleError(e)
}
}
if(paymentOption && paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) {
prepareSendPaymentRequest()
}
if(paymentOption && paymentOption === ReceiveOption.LNURL_WITHDRAW) {
prepareLnurlWithdraw()
}
}, [route.params?.paymentOption])
)
useEffect(() => {
const handleCompleted = (paymentRequest: PaymentRequest) => {
log.trace('handleCompleted event handler trigerred')
if (!transactionId) {
return
}
// Filter and handle events only for this topup transactionId
if (paymentRequest.transactionId === transactionId) {
log.trace('[handleCompleted]', 'Invoice has been paid and new proofs received')
setResultModalInfo({
status: TransactionStatus.COMPLETED,
message: `Your invoice has been paid and your wallet balance credited with ${paymentRequest.amount} sats.`,
})
setTransactionStatus(TransactionStatus.COMPLETED)
setIsQRModalVisible(false)
setIsNostrDMModalVisible(false)
setIsWithdrawModalVisible(false)
setIsResultModalVisible(true)
}
}
EventEmitter.on('topupCompleted', handleCompleted)
return () => {
EventEmitter.off('topupCompleted', handleCompleted)
}
}, [transactionId])
const toggleQRModal = () => setIsQRModalVisible(previousState => !previousState)
const toggleNostrDMModal = () => setIsNostrDMModalVisible(previousState => !previousState)
const toggleWithdrawModal = () => setIsWithdrawModalVisible(previousState => !previousState)
const toggleResultModal = () => setIsResultModalVisible(previousState => !previousState)
const onAmountEndEditing = function () {
try {
const amount = parseInt(amountToTopup)
if (!amount || amount === 0) {
infoMessage('Amount should be positive number.')
return
}
if (lnurlWithdrawParams && amount < lnurlWithdrawParams?.minWithdrawable / 1000 ) {
infoMessage(`Minimal withdraw amount is ${lnurlWithdrawParams?.minWithdrawable / 1000} sats.`)
return
}
const availableBalances = proofsStore.getBalances().mintBalances
if (availableBalances.length === 0) {
infoMessage('Add the mint first.', 'There is no mint connected to your wallet that you would receive your ecash from.')
return
}
setAvailableMintBalances(availableBalances)
// Default mint with highest balance to topup
setMintBalanceToTopup(availableBalances[0])
setIsAmountEndEditing(true)
// We do not make memo focus mandatory
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
// 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(amountToTopup) > 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) {
setMintBalanceToTopup(balance)
}
const onMintBalanceConfirm = async function () {
if (!mintBalanceToTopup) {
return
}
const result = await requestTopup()
if (result.error) {
setResultModalInfo({
status: result.transaction?.status as TransactionStatus,
title: result.error.params?.message ? result.error.message : 'Topup failed',
message: result.error.params?.message || result.error.message,
})
setIsResultModalVisible(true)
return
}
if (paymentOption === ReceiveOption.SHOW_INVOICE) {
toggleQRModal()
}
if (paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST) {
toggleNostrDMModal()
}
if (paymentOption === ReceiveOption.LNURL_WITHDRAW) {
toggleWithdrawModal()
}
setIsMintSelectorVisible(false)
}
const onMintBalanceCancel = async function () {
setIsMintSelectorVisible(false)
}
const requestTopup = async function () {
setIsLoading(true)
const result = await Wallet.topup(
mintBalanceToTopup as MintBalance,
parseInt(amountToTopup),
memo,
contactToSendTo
)
const {status, id} = result.transaction as Transaction
setTransactionStatus(status)
setTransactionId(id)
if (result.encodedInvoice) {
setInvoiceToPay(result.encodedInvoice)
}
setIsLoading(false)
return result
}
const sendAsNostrDM = async function () {
try {
setIsNostrDMSending(true)
const senderPubkey = walletProfileStore.pubkey
const receiverPubkey = contactToSendTo?.pubkey
// log.trace('', {senderPrivkey, senderPubkey, receiverPubkey}, 'sendAsNostrDM')
let message = `nostr:${walletProfileStore.npub} sent you Lightning invoice for ${amountToTopup} sats from Minibits wallet!`
if (memo) {
message += ` Memo: ${memo}`
}
const content = message + ' \n' + invoiceToPay
const encryptedContent = await NostrClient.encryptNip04(
receiverPubkey as string,
content as string
)
// log.trace('Relays', relaysToShareTo)
const dmEvent: NostrUnsignedEvent = {
kind: 4,
pubkey: senderPubkey,
tags: [['p', receiverPubkey as string], ['from', walletProfileStore.nip05]],
content: encryptedContent,
created_at: Math.floor(Date.now() / 1000)
}
const sentEvent: Event | undefined = await NostrClient.publish(
dmEvent,
relaysToShareTo,
)
setIsNostrDMSending(false)
if(sentEvent) {
setIsNostrDMSuccess(true)
const transaction = transactionsStore.findById(transactionId as number)
if(!transaction || !transaction.data) {
return
}
const updated = JSON.parse(transaction.data)
if(updated.length > 1) {
updated[1].sentToRelays = relaysToShareTo
updated[1].sentEvent = sentEvent
await transactionsStore.updateStatus( // status does not change, just add event and relay info to tx.data
transactionId as number,
TransactionStatus.PENDING,
JSON.stringify(updated)
)
}
const txupdate = await transactionsStore.updateSentTo( // set contact to send to to the tx, could be elsewhere //
transactionId as number,
contactToSendTo?.nip05handle as string
)
log.trace('sentTo tx', txupdate, 'sendAsNostrDM')
} else {
setInfo('Relay could not confirm that the message has been published.')
}
} catch (e: any) {
handleError(e)
}
}
const onShareToApp = async () => {
try {
const result = await Share.share({
message: invoiceToPay as string,
})
if (result.action === Share.sharedAction) {
setTimeout(
() => infoMessage('Lightning invoice has been shared, waiting to be paid by receiver.'),
500,
)
} else if (result.action === Share.dismissedAction) {
infoMessage('Sharing cancelled')
}
} catch (e: any) {
handleError(e)
}
}
const onCopy = function () {
try {
Clipboard.setString(invoiceToPay as string)
} catch (e: any) {
setInfo(`Could not copy: ${e.message}`)
}
}
const onLnurlWithdraw = async function () {
try {
setIsWithdrawRequestSending(true) // replace, not working
const result = await LnurlClient.withdraw(lnurlWithdrawParams as LNURLWithdrawParams, invoiceToPay)
log.trace('Withdraw result', result, 'onLnurlWithdraw')
if(result.status === 'OK') {
setIsWithdrawRequestSuccess(true)
setLnurlWithdrawResult(result)
setIsWithdrawRequestSending(false)
return
}
const transaction = transactionsStore.findById(transactionId as number)
if(!transaction) {
throw new AppError(Err.NOTFOUND_ERROR, 'Could not find transaction in the app state.', {transactionId})
}
const updated = JSON.parse(transaction.data)
updated.push({
status: TransactionStatus.ERROR,
error: result,
})
await transactionsStore.updateStatus(
transactionId as number,
TransactionStatus.ERROR,
JSON.stringify(updated),
)
setResultModalInfo({
status: TransactionStatus.ERROR,
message: JSON.stringify(result),
})
toggleWithdrawModal()
setIsResultModalVisible(true)
return
} catch (e: any) {
handleError(e)
}
}
const resetState = function () {
// reset state so it does not interfere next payment
setAmountToTopup('')
setMemo('')
setIsAmountEndEditing(false)
setIsMemoEndEditing(false)
setIsMintSelectorVisible(false)
setIsNostrDMModalVisible(false)
setIsWithdrawModalVisible(false)
setIsWithdrawRequestSending(false)
setPaymentOption(ReceiveOption.SHOW_INVOICE)
navigation.popToTop()
}
const handleError = function (e: AppError): void {
setIsLoading(false)
setError(e)
}
const headerBg = useThemeColor('header')
const getAmountTitle = function () {
switch (paymentOption) {
case ReceiveOption.SEND_PAYMENT_REQUEST:
return 'Requested amount'
case ReceiveOption.LNURL_WITHDRAW:
return 'Withdraw amount'
default:
return 'Topup amount'
}
}
// const inputBg = useThemeColor('background')
const satsColor = colors.palette.primary200
return (
<Screen preset="fixed" contentContainerStyle={$screen}>
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<Text
preset="subheading"
text={getAmountTitle()}
style={{color: 'white'}}
/>
<View style={$amountContainer}>
<TextInput
ref={amountInputRef}
onChangeText={amount => setAmountToTopup(amount)}
onEndEditing={onAmountEndEditing}
value={amountToTopup}
style={$amountInput}
maxLength={9}
keyboardType="numeric"
selectTextOnFocus={true}
editable={
transactionStatus === TransactionStatus.PENDING ? false : true
}
/>
<CurrencySign
currencyCode={CurrencyCode.SATS}
/>
</View>
</View>
<View style={$contentContainer}>
<Card
style={$memoCard}
ContentComponent={
<View style={$memoContainer}>
<TextInput
ref={memoInputRef}
onChangeText={memo => setMemo(memo)}
onEndEditing={onMemoEndEditing}
value={`${memo}`}
style={$memoInput}
maxLength={200}
keyboardType="default"
selectTextOnFocus={true}
placeholder="Memo for the payer"
editable={
transactionStatus === TransactionStatus.PENDING
? false
: true
}
/>
<Button
preset="secondary"
style={$memoButton}
text="Done"
onPress={onMemoDone}
disabled={
transactionStatus === TransactionStatus.PENDING
? true
: false
}
/>
</View>
}
/>
{isMintSelectorVisible && (
<MintBalanceSelector
availableMintBalances={availableMintBalances}
mintBalanceToTopup={mintBalanceToTopup as MintBalance}
onMintBalanceSelect={onMintBalanceSelect}
onCancel={onMintBalanceCancel}
findByUrl={mintsStore.findByUrl}
onMintBalanceConfirm={onMintBalanceConfirm}
/>
)}
{transactionStatus === TransactionStatus.PENDING && invoiceToPay && paymentOption && (
<SelectedMintBlock
toggleNostrDMModal={toggleNostrDMModal}
toggleQRModal={toggleQRModal}
toggleWithdrawModal={toggleWithdrawModal}
paymentOption={paymentOption}
invoiceToPay={invoiceToPay}
mintBalanceToTopup={mintBalanceToTopup as MintBalance}
gotoWallet={resetState}
/>
)}
{(transactionStatus === TransactionStatus.COMPLETED) && (
<View style={$bottomContainer}>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx={'common.close'}
onPress={resetState}
/>
</View>
</View>
)}
{isLoading && <Loading />}
</View>
<BottomModal
isVisible={isQRModalVisible}
ContentComponent={
<ShareAsQRCodeBlock
toggleQRModal={toggleQRModal}
invoiceToPay={invoiceToPay as string}
onShareToApp={onShareToApp}
onCopy={onCopy}
onError={handleError}
/>
}
onBackButtonPress={toggleQRModal}
onBackdropPress={toggleQRModal}
/>
<BottomModal
isVisible={isNostrDMModalVisible ? true : false}
ContentComponent={
(isNostrDMSuccess ? (
<NostrDMSuccessBlock
toggleNostrDMModal={toggleNostrDMModal}
contactToSendFrom={contactToSendFrom as Contact}
contactToSendTo={contactToSendTo as Contact}
amountToTopup={amountToTopup}
onClose={resetState}
/>
) : (
<SendAsNostrDMBlock
toggleNostrDMModal={toggleNostrDMModal}
encodedInvoiceToSend={invoiceToPay as string}
contactToSendFrom={contactToSendFrom as Contact}
contactToSendTo={contactToSendTo as Contact}
relaysToShareTo={relaysToShareTo}
amountToTopup={amountToTopup}
sendAsNostrDM={sendAsNostrDM}
isNostrDMSending={isNostrDMSending}
/>
))
}
onBackButtonPress={toggleNostrDMModal}
onBackdropPress={toggleNostrDMModal}
/>
<BottomModal
isVisible={isWithdrawModalVisible ? true : false}
style={{alignItems: 'stretch'}}
ContentComponent={
(isWithdrawRequestSuccess ? (
<LnurlWithdrawSuccessBlock
toggleWithdrawModal={toggleWithdrawModal}
amountToTopup={amountToTopup}
lnurlWithdrawParams={lnurlWithdrawParams as LNURLWithdrawParams}
lnurlWithdrawResult={lnurlWithdrawResult as LnurlWithdrawResult}
onClose={resetState}
/>
) : (
<LnurlWithdrawBlock
toggleWithdrawModal={toggleWithdrawModal}
amountToTopup={amountToTopup}
mintBalanceToTopup={mintBalanceToTopup as MintBalance}
lnurlWithdrawParams={lnurlWithdrawParams as LNURLWithdrawParams}
memo={memo}
onLnurlWithdraw={onLnurlWithdraw}
isWithdrawRequestSending={isWithdrawRequestSending}
/>
)
)}
onBackButtonPress={toggleWithdrawModal}
onBackdropPress={toggleWithdrawModal}
/>
<BottomModal
isVisible={isResultModalVisible ? true : false}
ContentComponent={
<>
{resultModalInfo &&
transactionStatus === TransactionStatus.COMPLETED && (
<>
<ResultModalInfo
icon="faCheckCircle"
iconColor={colors.palette.success200}
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 || "Topup failed"}
message={resultModalInfo?.message}
/>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx={'common.close'}
onPress={toggleResultModal}
/>
</View>
</>
)}
</>
}
onBackButtonPress={toggleResultModal}
onBackdropPress={toggleResultModal}
/>
{error && <ErrorModal error={error} />}
{info && <InfoModal message={info} />}
</Screen>
)
},
)
const MintBalanceSelector = observer(function (props: {
availableMintBalances: MintBalance[]
mintBalanceToTopup: MintBalance
onMintBalanceSelect: any
onCancel: any
findByUrl: any
onMintBalanceConfirm: any
}) {
const onMintSelect = function (balance: MintBalance) {
log.trace('onMintBalanceSelect', balance.mint)
return props.onMintBalanceSelect(balance)
}
return (
<>
<Card
style={$card}
heading={'Select mint to top-up'}
headingStyle={{textAlign: 'center', padding: spacing.small}}
ContentComponent={
<>
<FlatList<MintBalance>
data={props.availableMintBalances}
renderItem={({ item, index }) => {
return(
<MintListItem
key={item.mint}
mint={props.findByUrl(item.mint)}
mintBalance={item}
onMintSelect={() => onMintSelect(item)}
isSelectable={true}
isSelected={props.mintBalanceToTopup.mint === item.mint}
separator={'top'}
/>
)
}}
keyExtractor={(item) => item.mint}
style={{ flexGrow: 0, maxHeight: spacing.screenHeight * 0.35 }}
/>
</>
}
/>
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
<Button
text="Create invoice"
onPress={props.onMintBalanceConfirm}
style={{marginRight: spacing.medium}}
/>
<Button
preset="secondary"
tx={'common.cancel'}
onPress={props.onCancel}
/>
</View>
</>
)
})
const SelectedMintBlock = observer(function (props: {
toggleNostrDMModal: any
toggleQRModal: any
toggleWithdrawModal: any
invoiceToPay: string
paymentOption: ReceiveOption
mintBalanceToTopup: MintBalance
gotoWallet: any
}) {
const {mintsStore} = useStores()
const sendBg = useThemeColor('card')
const tokenTextColor = useThemeColor('textDim')
return (
<View style={{flex: 1}}>
<Card
style={$card}
heading={'Mint to topup'}
headingStyle={{textAlign: 'center', padding: spacing.small}}
ContentComponent={
<MintListItem
mint={
mintsStore.findByUrl(
props.mintBalanceToTopup?.mint as string,
) as Mint
}
isSelectable={false}
separator={'top'}
/>
}
/>
<View style={$bottomContainer}>
<View style={$buttonContainer}>
<Button
text='QR code'
preset='secondary'
onPress={props.toggleQRModal}
LeftAccessory={() => (
<Icon
icon='faQrcode'
// color="white"
size={spacing.medium}
/>
)}
/>
{props.paymentOption === ReceiveOption.SEND_PAYMENT_REQUEST && (
<Button
text='Send to contact'
preset='secondary'
onPress={props.toggleNostrDMModal}
style={{marginLeft: spacing.medium}}
LeftAccessory={() => (
<Icon
icon='faPaperPlane'
// color="white"
size={spacing.medium}
/>
)}
/>
)}
{props.paymentOption === ReceiveOption.LNURL_WITHDRAW && (
<Button
text='Withdraw'
preset='secondary'
onPress={props.toggleWithdrawModal}
style={{marginLeft: spacing.medium}}
LeftAccessory={() => (
<Icon
icon='faArrowTurnDown'
// color="white"
size={spacing.medium}
/>
)}
/>
)}
<Button
preset="secondary"
tx={'common.close'}
onPress={props.gotoWallet}
style={{marginLeft: spacing.small}}
/>
</View>
</View>
</View>
)
})
const ShareAsQRCodeBlock = observer(function (props: {
toggleQRModal: any
invoiceToPay: string
onShareToApp: any
onCopy: any
onError: any
}) {
return (
<View style={[$bottomModal, {marginHorizontal: spacing.small}]}>
<Text text={'Scan and pay to top-up'} />
<View style={$qrCodeContainer}>
<QRCode
size={270}
value={props.invoiceToPay}
onError={props.onError}
/>
</View>
<View style={$buttonContainer}>
<Button
text="Share"
onPress={props.onShareToApp}
style={{marginRight: spacing.medium}}
LeftAccessory={() => (
<Icon
icon="faShareFromSquare"
color="white"
size={spacing.medium}
// containerStyle={{marginRight: spacing.small}}
/>
)}
/>
<Button preset="secondary" text="Copy" onPress={props.onCopy} />
<Button
preset="tertiary"
text="Close"
onPress={props.toggleQRModal}
/>
</View>
</View>
)
})
const SendAsNostrDMBlock = observer(function (props: {
toggleNostrDMModal: any
encodedInvoiceToSend: string
contactToSendFrom: Contact
contactToSendTo: Contact
relaysToShareTo: string[]
amountToTopup: string
sendAsNostrDM: any
isNostrDMSending: boolean
}) {
const sendBg = useThemeColor('background')
const tokenTextColor = useThemeColor('textDim')
return (
<View style={$bottomModal}>
<NostrDMInfoBlock
contactToSendFrom={props.contactToSendFrom}
amountToTopup={props.amountToTopup}
contactToSendTo={props.contactToSendTo}
/>
<ScrollView
style={[
$tokenContainer,
{backgroundColor: sendBg, marginHorizontal: spacing.small},
]}>
<Text
selectable
text={props.encodedInvoiceToSend}
style={{color: tokenTextColor, paddingBottom: spacing.medium}}
size="xxs"
/>
</ScrollView>
{props.isNostrDMSending ? (
<View style={[$buttonContainer, {minHeight: verticalScale(55)}]}>
<Loading />
</View>
) : (
<View style={$buttonContainer}>
<Button
text="Send request"
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
amountToTopup: string
onClose: any
}) {
return (
<View style={$bottomModal}>
{/* <NostrDMInfoBlock
contactToSendFrom={props.contactToSendFrom}
amountToTopup={props.amountToTopup}
contactToSendTo={props.contactToSendTo}
/> */}
<ResultModalInfo
icon="faCheckCircle"
iconColor={colors.palette.success200}
title="Success!"
message="Payment reuest has been succesfully sent."
/>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx={'common.close'}
onPress={props.onClose}
/>
</View>
</View>
)
})
const NostrDMInfoBlock = observer(function (props: {
contactToSendFrom: NostrProfile
amountToTopup: string
contactToSendTo: NostrProfile
}) {
const {walletProfileStore} = useStores()
const tokenTextColor = useThemeColor('textDim')
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'}}>
<Text size='xxs' style={{color: tokenTextColor, marginTop: -20}} text={`requests`} />
<Icon
icon='faPaperPlane'
size={spacing.medium}
color={tokenTextColor}
/>
<Text size='xxs' style={{color: tokenTextColor, marginBottom: -10}} text={`${props.amountToTopup} sats`} />
</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: 40
}]}
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}/>
</View>
</View>
)
})
const LnurlWithdrawBlock = observer(function (props: {
toggleWithdrawModal: any
amountToTopup: string
mintBalanceToTopup: MintBalance
lnurlWithdrawParams: any
memo: string
onLnurlWithdraw: any
isWithdrawRequestSending: boolean
}) {
return (
<View style={[$bottomModal, {alignItems: 'stretch'}]}>
<Text style={{textAlign: 'center', marginBottom: spacing.small}} text={props.lnurlWithdrawParams.domain} preset={'subheading'} />
<ListItem
leftIcon='faCheckCircle'
leftIconColor={colors.palette.success200}
text={`Withdrawal is available`}
subText={`Up to ${props.lnurlWithdrawParams.maxWithdrawable / 1000} sats are available to withdraw`}
topSeparator={true}
/>
<ListItem
leftIcon='faCheckCircle'
leftIconColor={colors.palette.success200}
text={`Invoice for ${props.amountToTopup} sats created`}
subText={`Your selected mint balance to top up is ${props.mintBalanceToTopup.mint}`}
bottomSeparator={true}
/>
{props.isWithdrawRequestSending ? (
<View style={[$buttonContainer, {marginTop: spacing.medium}]}>
<Loading />
</View>
) : (
<View style={[$buttonContainer, {marginTop: spacing.medium}]}>
<Button
text="Withdraw"
onPress={props.onLnurlWithdraw}
style={{marginRight: spacing.medium}}
LeftAccessory={() => (
<Icon
icon='faArrowTurnDown'
color="white"
size={spacing.medium}
// containerStyle={{marginRight: spacing.small}}
/>
)}
/>
<Button
preset="tertiary"
text="Cancel"
onPress={props.toggleWithdrawModal}
/>
</View>
)}
</View>
)
})
const LnurlWithdrawSuccessBlock = observer(function (props: {
toggleWithdrawModal: any,
amountToTopup: string,
lnurlWithdrawParams: LNURLWithdrawParams,
lnurlWithdrawResult: LnurlWithdrawResult,
onClose: any
}) {
return (
<View style={$bottomModal}>
<ResultModalInfo
icon='faCheckCircle'
iconColor={colors.palette.success200}
title='Success!'
message={`Withdrawal request has been received by ${props.lnurlWithdrawParams.domain}.`}
/>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx={'common.close'}
onPress={props.onClose}
/>
</View>
</View>
)
})
const $screen: ViewStyle = {
flex: 1,
}
const $headerContainer: TextStyle = {
alignItems: 'center',
padding: spacing.extraSmall,
paddingTop: 0,
height: spacing.screenHeight * 0.18,
}
const $amountContainer: ViewStyle = {
}
const $amountInput: TextStyle = {
borderRadius: spacing.small,
margin: 0,
padding: 0,
fontSize: 52,
fontWeight: '400',
textAlign: 'center',
color: 'white',
}
const $contentContainer: TextStyle = {
flex:1,
padding: spacing.extraSmall,
}
const $memoCard: ViewStyle = {
marginBottom: spacing.small,
}
const $memoContainer: ViewStyle = {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
}
const $memoInput: TextStyle = {
flex: 1,
borderRadius: spacing.small,
fontSize: 16,
textAlignVertical: 'center',
marginRight: spacing.small,
}
const $tokenContainer: ViewStyle = {
borderRadius: spacing.small,
alignSelf: 'stretch',
padding: spacing.small,
maxHeight: 150,
marginTop: spacing.small,
marginBottom: spacing.large,
// marginHorizontal: spacing.small
}
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',
padding: spacing.small,
margin: spacing.small,
borderRadius: spacing.small
}
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.medium,
alignSelf: 'stretch',
// opacity: 0,
}