mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
1776 lines
61 KiB
TypeScript
1776 lines
61 KiB
TypeScript
import {observer} from 'mobx-react-lite'
|
|
import React, {FC, useCallback, useEffect, useRef, useState} from 'react'
|
|
import {
|
|
Alert,
|
|
TextInput,
|
|
TextStyle,
|
|
View,
|
|
ViewStyle,
|
|
} from 'react-native'
|
|
import Clipboard from '@react-native-clipboard/clipboard'
|
|
import JSONTree from 'react-native-json-tree'
|
|
import {colors, spacing, useThemeColor} from '../theme'
|
|
import {TransactionsStackScreenProps, WalletStackScreenProps} from '../navigation'
|
|
import EventEmitter from '../utils/eventEmitter'
|
|
import {
|
|
Button,
|
|
Icon,
|
|
ListItem,
|
|
Screen,
|
|
Text,
|
|
Card,
|
|
ErrorModal,
|
|
BottomModal,
|
|
InfoModal,
|
|
Loading,
|
|
Header,
|
|
} from '../components'
|
|
import {useHeader} from '../utils/useHeader'
|
|
import {useStores} from '../models'
|
|
import {translate, TxKeyPath} from '../i18n'
|
|
import {
|
|
Transaction,
|
|
TransactionStatus,
|
|
TransactionType,
|
|
} from '../models/Transaction'
|
|
import AppError, {Err} from '../utils/AppError'
|
|
import {log} from '../services/logService'
|
|
import {isArray} from 'lodash'
|
|
import {Database, TransactionTaskResult, WalletTask} from '../services'
|
|
import {BackupProof, Proof} from '../models/Proof'
|
|
import useColorScheme from '../theme/useThemeColor'
|
|
import useIsInternetReachable from '../utils/useIsInternetReachable'
|
|
import { ResultModalInfo } from './Wallet/ResultModalInfo'
|
|
import QRCode from 'react-native-qrcode-svg'
|
|
import { getDecodedToken, Token as CashuToken } from '@cashu/cashu-ts'
|
|
import { CashuUtils } from '../services/cashu/cashuUtils'
|
|
import { MintStatus } from '../models/Mint'
|
|
import { moderateVerticalScale } from '@gocodingnow/rn-size-matters'
|
|
import { CurrencySign } from './Wallet/CurrencySign'
|
|
import { MintUnit, formatCurrency, getCurrency } from "../services/wallet/currency"
|
|
import { Token } from '../models/Token'
|
|
import { PaymentRequest } from '../models/PaymentRequest'
|
|
import { pollerExists } from '../utils/poller'
|
|
import { StackActions, useFocusEffect } from '@react-navigation/native'
|
|
import { CurrencyAmount } from './Wallet/CurrencyAmount'
|
|
import { toNumber } from '../utils/number'
|
|
import { QRCodeBlock } from './Wallet/QRCode'
|
|
|
|
type ProofsByStatus = {
|
|
isSpent: Proof[]
|
|
isPending: Proof[]
|
|
isReceived: Proof[]
|
|
}
|
|
|
|
export const TranDetailScreen: FC<TransactionsStackScreenProps<'TranDetail'>> =
|
|
observer(function TranDetailScreen(_props) {
|
|
const {navigation, route} = _props
|
|
const {transactionsStore, userSettingsStore} = useStores()
|
|
|
|
const noteInputRef = useRef<TextInput>(null)
|
|
|
|
const [transaction, setTransaction] = useState<Transaction>()
|
|
const [proofsByStatus, setProofsByStatus] = useState<
|
|
ProofsByStatus | undefined
|
|
>(undefined)
|
|
const [error, setError] = useState<AppError | undefined>()
|
|
const [isNoteModalVisible, setIsNoteModalVisible] = useState<boolean>(false)
|
|
const [isDataParsable, setIsDataParsable] = useState<boolean>(true)
|
|
const [info, setInfo] = useState('')
|
|
const [note, setNote] = useState<string>('')
|
|
const [savedNote, setSavedNote] = useState<string>('')
|
|
|
|
useFocusEffect(useCallback(() => {
|
|
try {
|
|
const {id} = route.params
|
|
const tx = transactionsStore.findById(id)
|
|
log.trace('Transaction loaded', {id: tx?.id, unit: tx?.unit})
|
|
|
|
if (!tx) {
|
|
throw new AppError(
|
|
Err.VALIDATION_ERROR,
|
|
'Could not retrieve transaction from transactionsStore',
|
|
)
|
|
}
|
|
|
|
try {
|
|
JSON.parse(tx.data)
|
|
} catch (e: any) {
|
|
setIsDataParsable(false)
|
|
}
|
|
|
|
|
|
setTransaction(tx as Transaction)
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}, [route]))
|
|
|
|
useEffect(() => {
|
|
try {
|
|
const {id} = route.params
|
|
|
|
if (userSettingsStore.isLocalBackupOn === false) {
|
|
return
|
|
}
|
|
|
|
const proofs = Database.getProofsByTransaction(id)
|
|
|
|
if (proofs.length === 0) {
|
|
return
|
|
}
|
|
|
|
const proofsByStatus = proofs.reduce(
|
|
(result: ProofsByStatus, proof: BackupProof) => {
|
|
if (proof.isSpent) {
|
|
result.isSpent.push(proof)
|
|
} else if (proof.isPending) {
|
|
result.isPending.push(proof)
|
|
} else {
|
|
result.isReceived.push(proof)
|
|
}
|
|
return result
|
|
},
|
|
{isReceived: [], isPending: [], isSpent: []},
|
|
)
|
|
setProofsByStatus(proofsByStatus)
|
|
|
|
} catch (e: any) {
|
|
log.error(e.name, e.message)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const focus = () => {
|
|
noteInputRef && noteInputRef.current
|
|
? noteInputRef.current.focus()
|
|
: false
|
|
}
|
|
|
|
if (isNoteModalVisible) {
|
|
setTimeout(() => focus(), 100)
|
|
}
|
|
}, [isNoteModalVisible])
|
|
|
|
|
|
|
|
const toggleNoteModal = function () {
|
|
if (isNoteModalVisible) {
|
|
setIsNoteModalVisible(false)
|
|
} else {
|
|
setIsNoteModalVisible(true)
|
|
}
|
|
}
|
|
|
|
const saveNote = async function () {
|
|
try {
|
|
setIsNoteModalVisible(false)
|
|
await transactionsStore.saveNote(transaction?.id as number, note)
|
|
setSavedNote(note)
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
const copyAuditTrail = function (transaction: Transaction) {
|
|
try {
|
|
Clipboard.setString(JSON.stringify(getAuditTrail(transaction)))
|
|
} catch (e: any) {
|
|
setInfo(translate("common.copyFailParam", { param: e.message }))
|
|
}
|
|
}
|
|
|
|
const copyToken = function (transaction: Transaction) {
|
|
try {
|
|
const encoded = getEncodedTokenToSend(transaction)
|
|
|
|
if (!encoded) {
|
|
throw new AppError(
|
|
Err.VALIDATION_ERROR,
|
|
'Could not get encoded ecash token from transaction',
|
|
)
|
|
}
|
|
|
|
Clipboard.setString(encoded)
|
|
|
|
} catch (e: any) {
|
|
setInfo(translate("common.copyFailParam", { param: e.message }))
|
|
}
|
|
}
|
|
|
|
|
|
const copyBackupProofs = function (proofsByStatus: ProofsByStatus) {
|
|
try {
|
|
Clipboard.setString(JSON.stringify(proofsByStatus))
|
|
} catch (e: any) {
|
|
setInfo(translate("common.copyFailParam", { param: e.message }))
|
|
}
|
|
}
|
|
|
|
|
|
const handleError = function (e: AppError): void {
|
|
setIsNoteModalVisible(false)
|
|
setError(e)
|
|
}
|
|
|
|
|
|
const headerBg = useThemeColor('header')
|
|
const iconColor = useThemeColor('textDim')
|
|
const labelColor = useThemeColor('textDim')
|
|
const inputBg = useThemeColor('background')
|
|
const tokenTextColor = useThemeColor('textDim')
|
|
|
|
|
|
|
|
const getFormattedAmount = function(): string {
|
|
if (!transaction) { return '' }
|
|
|
|
switch (transaction?.type) {
|
|
case TransactionType.RECEIVE || TransactionType.RECEIVE_OFFLINE:
|
|
return `+${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
|
case TransactionType.SEND:
|
|
return `-${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
|
case TransactionType.TOPUP:
|
|
return `+${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
|
case TransactionType.TRANSFER:
|
|
return `-${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
|
default:
|
|
return `${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
|
}
|
|
}
|
|
|
|
const colorScheme = useColorScheme()
|
|
|
|
return (
|
|
<Screen contentContainerStyle={$screen} preset="auto">
|
|
{transaction && (
|
|
<>
|
|
<Header
|
|
leftIcon='faArrowLeft'
|
|
onLeftPress={() => navigation.goBack()}
|
|
TitleActionComponent={
|
|
<CurrencySign
|
|
mintUnit={transaction.unit}
|
|
textStyle={{color: 'white'}}
|
|
/>
|
|
}
|
|
/>
|
|
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
|
<Text
|
|
preset="heading"
|
|
text={getFormattedAmount()}
|
|
style={$tranAmount}
|
|
/>
|
|
</View>
|
|
<View style={$contentContainer}>
|
|
<Card
|
|
style={$actionCard}
|
|
ContentComponent={
|
|
<>
|
|
<ListItem
|
|
text={
|
|
transaction.noteToSelf
|
|
? transaction.noteToSelf
|
|
: savedNote
|
|
? savedNote
|
|
: translate("tranDetailScreen.addYourNote")
|
|
}
|
|
LeftComponent={
|
|
<Icon
|
|
containerStyle={$iconContainer}
|
|
icon="faPencil"
|
|
size={spacing.medium}
|
|
color={iconColor}
|
|
/>
|
|
}
|
|
style={$item}
|
|
// bottomSeparator={true}
|
|
onPress={toggleNoteModal}
|
|
/>
|
|
<ListItem
|
|
tx="tranDetailScreen.addTags"
|
|
LeftComponent={
|
|
<Icon
|
|
containerStyle={$iconContainer}
|
|
icon="faTags"
|
|
size={spacing.medium}
|
|
color={iconColor}
|
|
/>
|
|
}
|
|
style={$item}
|
|
bottomSeparator={false}
|
|
onPress={() => Alert.alert('Not yet implemented')}
|
|
/>
|
|
</>
|
|
}
|
|
/>
|
|
|
|
{transaction.type === TransactionType.RECEIVE && (
|
|
<ReceiveInfoBlock
|
|
transaction={transaction}
|
|
isDataParsable={isDataParsable}
|
|
copyAuditTrail={copyAuditTrail}
|
|
colorScheme={colorScheme}
|
|
navigation={navigation}
|
|
/>
|
|
)}
|
|
{transaction.type === TransactionType.RECEIVE_OFFLINE && (
|
|
<ReceiveOfflineInfoBlock
|
|
transaction={transaction}
|
|
isDataParsable={isDataParsable}
|
|
copyAuditTrail={copyAuditTrail}
|
|
colorScheme={colorScheme}
|
|
navigation={navigation}
|
|
/>
|
|
)}
|
|
{transaction.type === TransactionType.SEND && (
|
|
<SendInfoBlock
|
|
transaction={transaction}
|
|
isDataParsable={isDataParsable}
|
|
copyToken={copyToken}
|
|
copyAuditTrail={copyAuditTrail}
|
|
colorScheme={colorScheme}
|
|
/>
|
|
)}
|
|
{transaction.type === TransactionType.TOPUP && (
|
|
<TopupInfoBlock
|
|
transaction={transaction}
|
|
isDataParsable={isDataParsable}
|
|
copyAuditTrail={copyAuditTrail}
|
|
colorScheme={colorScheme}
|
|
navigation={navigation}
|
|
/>
|
|
)}
|
|
{transaction.type === TransactionType.TRANSFER && (
|
|
<TransferInfoBlock
|
|
transaction={transaction}
|
|
isDataParsable={isDataParsable}
|
|
copyAuditTrail={copyAuditTrail}
|
|
colorScheme={colorScheme}
|
|
/>
|
|
)}
|
|
{proofsByStatus && (
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
<Text
|
|
style={{color: labelColor, fontSize: 14}}
|
|
tx="tranDetailScreen.backedUpEcash"
|
|
/>
|
|
<JSONTree
|
|
hideRoot
|
|
data={proofsByStatus}
|
|
theme={{
|
|
scheme: 'default',
|
|
base00: '#eee',
|
|
}}
|
|
invertTheme={colorScheme === 'light' ? false : true}
|
|
/>
|
|
</>
|
|
}
|
|
FooterComponent={
|
|
<Button
|
|
preset="tertiary"
|
|
onPress={() => copyBackupProofs(proofsByStatus)}
|
|
tx="common.copy"
|
|
style={{
|
|
minHeight: 25,
|
|
paddingVertical: spacing.extraSmall,
|
|
marginTop: spacing.small,
|
|
alignSelf: 'center',
|
|
}}
|
|
textStyle={{fontSize: 14}}
|
|
/>
|
|
}
|
|
/>
|
|
)}
|
|
</View>
|
|
</>
|
|
)}
|
|
<BottomModal
|
|
isVisible={isNoteModalVisible}
|
|
ContentComponent={
|
|
<View style={$noteContainer}>
|
|
<Text tx="tranDetailScreen.addYourNote" preset="subheading" />
|
|
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
|
<TextInput
|
|
ref={noteInputRef}
|
|
onChangeText={note => setNote(note)}
|
|
value={note}
|
|
style={[$noteInput, {backgroundColor: inputBg}]}
|
|
maxLength={200}
|
|
/>
|
|
<Button
|
|
tx="common.save"
|
|
onPress={saveNote}
|
|
/>
|
|
</View>
|
|
</View>
|
|
}
|
|
onBackButtonPress={toggleNoteModal}
|
|
onBackdropPress={toggleNoteModal}
|
|
/>
|
|
{error && <ErrorModal error={error} />}
|
|
{info && <InfoModal message={info} />}
|
|
</Screen>
|
|
)
|
|
})
|
|
|
|
const ReceiveInfoBlock = function (props: {
|
|
transaction: Transaction
|
|
isDataParsable: boolean
|
|
copyAuditTrail: any
|
|
colorScheme: 'light' | 'dark'
|
|
navigation: any
|
|
}) {
|
|
const {
|
|
transaction,
|
|
isDataParsable,
|
|
copyAuditTrail,
|
|
colorScheme,
|
|
navigation
|
|
} = props
|
|
|
|
const isInternetReachable = useIsInternetReachable()
|
|
const encodedTokenToRetry = getEncodedTokenToRetry(transaction)
|
|
const {transactionsStore} = useStores()
|
|
const [isReceiveTaskSentToQueue, setIsReceiveTaskSentToQueue] = useState<boolean>(false)
|
|
const [isResultModalVisible, setIsResultModalVisible] = useState<boolean>(false)
|
|
const [resultModalInfo, setResultModalInfo] = useState<
|
|
{status: TransactionStatus; message: string} | undefined
|
|
>()
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
|
|
|
|
useEffect(() => {
|
|
const handleReceiveTaskResult = async (result: TransactionTaskResult) => {
|
|
log.trace('[handleReceiveTaskResult] event handler triggered')
|
|
|
|
setIsLoading(false)
|
|
|
|
if (result.error) {
|
|
setResultModalInfo({
|
|
status: result.transaction?.status as TransactionStatus,
|
|
message: result.error.params?.message || result.error.message,
|
|
})
|
|
} else {
|
|
|
|
const transactionDataUpdate = {
|
|
status: TransactionStatus.EXPIRED,
|
|
message: translate("tranDetailScreen.successRetrieveEcashAfterRetry"),
|
|
createdAt: new Date(),
|
|
}
|
|
|
|
transactionsStore.updateStatuses(
|
|
[transaction.id as number],
|
|
TransactionStatus.EXPIRED,
|
|
JSON.stringify(transactionDataUpdate),
|
|
)
|
|
|
|
const modalInfo = {
|
|
status: result.transaction?.status as TransactionStatus,
|
|
message: result.message,
|
|
}
|
|
|
|
setResultModalInfo(modalInfo)
|
|
}
|
|
|
|
}
|
|
|
|
// Subscribe to the 'sendCompleted' event
|
|
EventEmitter.on('ev_receiveTask_result', handleReceiveTaskResult)
|
|
|
|
// Unsubscribe from the 'sendCompleted' event on component unmount
|
|
return () => {
|
|
EventEmitter.off('ev_receiveTask_result', handleReceiveTaskResult)
|
|
}
|
|
}, [isReceiveTaskSentToQueue])
|
|
|
|
|
|
const toggleResultModal = () =>
|
|
setIsResultModalVisible(previousState => !previousState)
|
|
|
|
const onRetryToReceive = async function () {
|
|
if(!encodedTokenToRetry || !isInternetReachable) {
|
|
return
|
|
}
|
|
|
|
setIsLoading(true)
|
|
|
|
try {
|
|
const tokenToRetry: CashuToken = getDecodedToken(encodedTokenToRetry)
|
|
const amountToReceive = CashuUtils.getTokenAmounts(tokenToRetry as Token).totalAmount
|
|
const memo = tokenToRetry.memo || ''
|
|
|
|
setIsReceiveTaskSentToQueue(true)
|
|
WalletTask.receive(
|
|
tokenToRetry as Token,
|
|
amountToReceive,
|
|
memo,
|
|
encodedTokenToRetry
|
|
)
|
|
|
|
|
|
} catch (e: any) {
|
|
setResultModalInfo({
|
|
status: TransactionStatus.ERROR,
|
|
message: e.message,
|
|
})
|
|
} finally {
|
|
setIsLoading(false)
|
|
toggleResultModal()
|
|
}
|
|
}
|
|
|
|
const onGoBack = () => {
|
|
navigation.goBack()
|
|
}
|
|
|
|
const labelColor = useThemeColor('textDim')
|
|
|
|
return (
|
|
<>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
<TranItem
|
|
label="tranDetailScreen.amount"
|
|
value={transaction.amount}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
isFirst={true}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.memoFromSender"
|
|
value={transaction.memo as string}
|
|
/>
|
|
{transaction.sentFrom && (
|
|
<TranItem
|
|
label="tranDetailScreen.sentFrom"
|
|
value={transaction.sentFrom as string}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="tranDetailScreen.type"
|
|
value={transaction.type as string}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.fee"
|
|
value={transaction.fee}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
/>
|
|
{encodedTokenToRetry ? (
|
|
<View
|
|
style={{flexDirection: 'row', justifyContent: 'space-between'}}>
|
|
<TranItem
|
|
label="tranDetailScreen.status"
|
|
value={transaction.status as string}
|
|
/>
|
|
<Button
|
|
style={{marginTop: spacing.medium}}
|
|
// preset="secondary"
|
|
tx="tranDetailScreen.retryToReceive"
|
|
onPress={onRetryToReceive}
|
|
/>
|
|
</View>
|
|
) : (
|
|
<TranItem
|
|
label="tranDetailScreen.status"
|
|
value={transaction.status as string}
|
|
/>
|
|
)}
|
|
{transaction.status === TransactionStatus.COMPLETED && (
|
|
<TranItem
|
|
label="tranDetailScreen.balanceAfter"
|
|
value={transaction.balanceAfter || 0}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="tranDetailScreen.createdAt"
|
|
value={(transaction.createdAt as Date).toLocaleString()}
|
|
/>
|
|
<TranItem label="tranDetailScreen.id" value={`${transaction.id}`} />
|
|
</>
|
|
}
|
|
/>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<TranItem
|
|
label="transactionCommon.receivedTo"
|
|
value={transaction.mint as string}
|
|
/>
|
|
}
|
|
/>
|
|
{isDataParsable && (
|
|
<>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
<Text
|
|
style={{color: labelColor, fontSize: 14}}
|
|
tx="tranDetailScreen.auditTrail"
|
|
/>
|
|
<JSONTree
|
|
hideRoot
|
|
data={getAuditTrail(transaction)}
|
|
theme={{
|
|
scheme: 'default',
|
|
base00: '#eee',
|
|
}}
|
|
invertTheme={colorScheme === 'light' ? false : true}
|
|
/>
|
|
</>
|
|
}
|
|
FooterComponent={
|
|
<Button
|
|
preset="tertiary"
|
|
onPress={() => copyAuditTrail(transaction)}
|
|
tx="common.copy"
|
|
style={{
|
|
minHeight: 25,
|
|
paddingVertical: spacing.extraSmall,
|
|
marginTop: spacing.small,
|
|
alignSelf: 'center',
|
|
}}
|
|
textStyle={{fontSize: 14}}
|
|
/>
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
<BottomModal
|
|
isVisible={isResultModalVisible ? true : false}
|
|
ContentComponent={
|
|
<>
|
|
{resultModalInfo?.status === TransactionStatus.COMPLETED && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faCheckCircle"
|
|
iconColor={colors.palette.success200}
|
|
title={translate("tranDetailScreen.modalSuccess")}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx={'common.close'}
|
|
onPress={onGoBack}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
{(resultModalInfo?.status === TransactionStatus.ERROR ||
|
|
resultModalInfo?.status === TransactionStatus.BLOCKED) && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faTriangleExclamation"
|
|
iconColor={colors.palette.angry500}
|
|
title={translate('transactionCommon.receiveFailed')}
|
|
message={resultModalInfo?.message as string}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx='common.close'
|
|
onPress={toggleResultModal}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
</>
|
|
}
|
|
onBackButtonPress={toggleResultModal}
|
|
onBackdropPress={toggleResultModal}
|
|
/>
|
|
{isLoading && <Loading />}
|
|
</>
|
|
)
|
|
}
|
|
|
|
|
|
const ReceiveOfflineInfoBlock = function (props: {
|
|
transaction: Transaction
|
|
isDataParsable: boolean
|
|
copyAuditTrail: any
|
|
colorScheme: 'light' | 'dark'
|
|
navigation: any
|
|
}) {
|
|
const {
|
|
transaction,
|
|
isDataParsable,
|
|
copyAuditTrail,
|
|
colorScheme,
|
|
navigation
|
|
} = props
|
|
|
|
const labelColor = useThemeColor('textDim')
|
|
const isInternetReachable = useIsInternetReachable()
|
|
const [isReceiveOfflineCompleteTaskSentToQueue, setIsReceiveOfflineCompleteTaskSentToQueue] = useState<boolean>(false)
|
|
const [isResultModalVisible, setIsResultModalVisible] = useState<boolean>(false)
|
|
const [resultModalInfo, setResultModalInfo] = useState<
|
|
{status: TransactionStatus; message: string} | undefined
|
|
>()
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
|
|
|
|
useEffect(() => {
|
|
const handleReceiveOfflineCompleteTaskResult = async (result: TransactionTaskResult) => {
|
|
log.trace('handleReceiveOfflineCompleteTaskResult event handler triggered')
|
|
|
|
setIsLoading(false)
|
|
|
|
if (result.error) {
|
|
setResultModalInfo({
|
|
status: result.transaction?.status as TransactionStatus,
|
|
message: result.error.message,
|
|
})
|
|
} else {
|
|
setResultModalInfo({
|
|
status: result.transaction?.status as TransactionStatus,
|
|
message: result.message,
|
|
})
|
|
}
|
|
setIsLoading(false)
|
|
toggleResultModal()
|
|
}
|
|
|
|
// Subscribe to the 'sendCompleted' event
|
|
EventEmitter.on('ev_receiveOfflineCompleteTask', handleReceiveOfflineCompleteTaskResult)
|
|
|
|
// Unsubscribe from the 'sendCompleted' event on component unmount
|
|
return () => {
|
|
EventEmitter.off('ev_receiveOfflineCompleteTask', handleReceiveOfflineCompleteTaskResult)
|
|
}
|
|
}, [isReceiveOfflineCompleteTaskSentToQueue])
|
|
|
|
// MVP implementaition
|
|
const toggleResultModal = () =>
|
|
setIsResultModalVisible(previousState => !previousState)
|
|
|
|
const receiveOfflineComplete = async function () {
|
|
setIsLoading(true)
|
|
setIsReceiveOfflineCompleteTaskSentToQueue(true)
|
|
WalletTask.receiveOfflineComplete(transaction)
|
|
}
|
|
|
|
const onGoBack = () => {
|
|
navigation.goBack()
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
{transaction.status === TransactionStatus.PREPARED_OFFLINE ? (
|
|
<View
|
|
style={{flexDirection: 'row', justifyContent: 'space-between'}}>
|
|
<TranItem
|
|
label="tranDetailScreen.amount"
|
|
value={transaction.amount}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
isFirst={true}
|
|
/>
|
|
{isInternetReachable ? (
|
|
<Button
|
|
style={{maxHeight: 10, marginTop: spacing.medium}}
|
|
preset="default"
|
|
tx="tranDetailScreen.receiveOfflineComplete"
|
|
onPress={receiveOfflineComplete}
|
|
/>
|
|
) : (
|
|
<Button
|
|
style={{maxHeight: 10, marginTop: spacing.medium}}
|
|
preset="secondary"
|
|
tx="tranDetailScreen.isOffline"
|
|
/>
|
|
)}
|
|
</View>
|
|
) : (
|
|
<TranItem
|
|
label="tranDetailScreen.amount"
|
|
value={transaction.amount}
|
|
isFirst={true}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="tranDetailScreen.memoFromSender"
|
|
value={transaction.memo as string}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.sentFrom"
|
|
value={transaction.sentFrom as string}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.type"
|
|
value={transaction.type as string}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.fee"
|
|
value={transaction.fee}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.status"
|
|
value={transaction.status as string}
|
|
/>
|
|
{transaction.status === TransactionStatus.COMPLETED && (
|
|
<TranItem
|
|
label="tranDetailScreen.balanceAfter"
|
|
value={transaction.balanceAfter || 0}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="tranDetailScreen.createdAt"
|
|
value={(transaction.createdAt as Date).toLocaleString()}
|
|
/>
|
|
<TranItem label="tranDetailScreen.id" value={`${transaction.id}`} />
|
|
</>
|
|
}
|
|
/>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<TranItem
|
|
label="transactionCommon.receivedTo"
|
|
value={transaction.mint as string}
|
|
/>
|
|
}
|
|
/>
|
|
{isDataParsable && (
|
|
<>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
<Text
|
|
style={{color: labelColor, fontSize: 14}}
|
|
tx='tranDetailScreen.auditTrail'
|
|
/>
|
|
<JSONTree
|
|
hideRoot
|
|
data={getAuditTrail(transaction)}
|
|
theme={{
|
|
scheme: 'default',
|
|
base00: '#eee',
|
|
}}
|
|
invertTheme={colorScheme === 'light' ? false : true}
|
|
/>
|
|
</>
|
|
}
|
|
FooterComponent={
|
|
<Button
|
|
preset="tertiary"
|
|
onPress={() => copyAuditTrail(transaction)}
|
|
tx="common.copy"
|
|
style={{
|
|
minHeight: 25,
|
|
paddingVertical: spacing.extraSmall,
|
|
marginTop: spacing.small,
|
|
alignSelf: 'center',
|
|
}}
|
|
textStyle={{fontSize: 14}}
|
|
/>
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
<BottomModal
|
|
isVisible={isResultModalVisible ? true : false}
|
|
ContentComponent={
|
|
<>
|
|
{(resultModalInfo?.status === TransactionStatus.COMPLETED ||
|
|
resultModalInfo?.status === TransactionStatus.PREPARED_OFFLINE ) && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faCheckCircle"
|
|
iconColor={colors.palette.success200}
|
|
title={translate('common.success')}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx='common.close'
|
|
onPress={onGoBack}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
{(resultModalInfo?.status === TransactionStatus.ERROR ||
|
|
resultModalInfo?.status === TransactionStatus.BLOCKED) && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faTriangleExclamation"
|
|
iconColor={colors.palette.angry500}
|
|
title={translate('transactionCommon.receiveFailed')}
|
|
message={resultModalInfo?.message as string}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx='common.close'
|
|
onPress={onGoBack}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
</>
|
|
}
|
|
onBackButtonPress={toggleResultModal}
|
|
onBackdropPress={toggleResultModal}
|
|
/>
|
|
{isLoading && <Loading />}
|
|
</>
|
|
)
|
|
}
|
|
|
|
const SendInfoBlock = function (props: {
|
|
transaction: Transaction
|
|
isDataParsable: boolean
|
|
copyToken: any
|
|
copyAuditTrail: any
|
|
colorScheme: 'light' | 'dark'
|
|
}) {
|
|
const {transaction, isDataParsable, copyToken, copyAuditTrail, colorScheme} = props
|
|
const labelColor = useThemeColor('textDim')
|
|
|
|
return (
|
|
<>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
<TranItem
|
|
label="tranDetailScreen.amount"
|
|
value={transaction.amount}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
isFirst={true}
|
|
/>
|
|
<TranItem
|
|
label="receiverMemo"
|
|
value={transaction.memo as string}
|
|
/>
|
|
{transaction.sentTo && (
|
|
<TranItem
|
|
label="tranDetailScreen.sentTo"
|
|
value={transaction.sentTo as string}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="tranDetailScreen.type"
|
|
value={transaction.type as string}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.fee"
|
|
value={transaction.fee}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
/>
|
|
{transaction.status === TransactionStatus.PENDING ? (
|
|
<View
|
|
style={{flexDirection: 'row', justifyContent: 'space-between'}}>
|
|
<TranItem
|
|
label="tranDetailScreen.status"
|
|
value={transaction.status as string}
|
|
/>
|
|
<Button
|
|
style={{marginVertical: spacing.small}}
|
|
preset="secondary"
|
|
tx="tranDetailScreen.revert"
|
|
onPress={() =>
|
|
Alert.alert('Not yet implemented. Copy the token instead.')
|
|
}
|
|
/>
|
|
</View>
|
|
) : (
|
|
<TranItem
|
|
label="tranDetailScreen.status"
|
|
value={transaction.status as string}
|
|
/>
|
|
)}
|
|
{transaction.status !== TransactionStatus.ERROR && (
|
|
<TranItem
|
|
label="tranDetailScreen.balanceAfter"
|
|
value={transaction.balanceAfter || 0}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="tranDetailScreen.createdAt"
|
|
value={(transaction.createdAt as Date).toLocaleString()}
|
|
/>
|
|
<TranItem label="tranDetailScreen.id" value={`${transaction.id}`} />
|
|
</>
|
|
}
|
|
/>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<TranItem
|
|
label="tranDetailScreen.sentFrom"
|
|
value={transaction.mint as string}
|
|
/>
|
|
}
|
|
/>
|
|
{isDataParsable && (
|
|
<>
|
|
{transaction.status === TransactionStatus.PENDING && (
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
|
|
<>
|
|
<Text
|
|
style={{
|
|
color: labelColor,
|
|
fontSize: 14,
|
|
marginTop: spacing.small,
|
|
}}
|
|
tx="tranDetailScreen.pendingToken"
|
|
/>
|
|
<Text
|
|
text={getEncodedTokenToSend(transaction) as string}
|
|
numberOfLines={1}
|
|
ellipsizeMode="middle"
|
|
/>
|
|
</>
|
|
|
|
</>
|
|
}
|
|
FooterComponent={
|
|
<>
|
|
<Button
|
|
preset="tertiary"
|
|
onPress={() => copyToken(transaction)}
|
|
tx='common.copy'
|
|
style={{
|
|
minHeight: 25,
|
|
paddingVertical: spacing.extraSmall,
|
|
marginTop: spacing.small,
|
|
alignSelf: 'center',
|
|
}}
|
|
textStyle={{fontSize: 14}}
|
|
/>
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
<Text
|
|
style={{color: labelColor, fontSize: 14}}
|
|
tx='tranDetailScreen.auditTrail'
|
|
/>
|
|
<JSONTree
|
|
hideRoot
|
|
data={getAuditTrail(transaction)}
|
|
theme={{
|
|
scheme: 'default',
|
|
base00: '#eee',
|
|
}}
|
|
invertTheme={colorScheme === 'light' ? false : true}
|
|
/>
|
|
</>
|
|
}
|
|
// footerStyle={{borderWidth: 10, borderColor: 'red'}}
|
|
FooterComponent={
|
|
<Button
|
|
preset="tertiary"
|
|
onPress={() => copyAuditTrail(transaction)}
|
|
tx='common.copy'
|
|
style={{
|
|
minHeight: 25,
|
|
paddingVertical: spacing.extraSmall,
|
|
marginTop: spacing.small,
|
|
alignSelf: 'center',
|
|
}}
|
|
textStyle={{fontSize: 14}}
|
|
/>
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
const TopupInfoBlock = function (props: {
|
|
transaction: Transaction
|
|
isDataParsable: boolean
|
|
copyAuditTrail: any
|
|
colorScheme: 'dark' | 'light'
|
|
navigation: any
|
|
}) {
|
|
const {transaction, isDataParsable, copyAuditTrail, colorScheme, navigation} = props
|
|
|
|
// retrieve pr from transaction as it might have been expired and removed from storage
|
|
const paymentRequest = getPaymentRequestToRetry(transaction)
|
|
const isInternetReachable = useIsInternetReachable()
|
|
|
|
const [isPendingTopupTaskSentToQueue, setIsPendingTopupTaskSentToQueue] = useState<boolean>(false)
|
|
const [isResultModalVisible, setIsResultModalVisible] = useState<boolean>(false)
|
|
const [resultModalInfo, setResultModalInfo] = useState<
|
|
{status: TransactionStatus; message: string} | undefined
|
|
>()
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
|
|
|
|
useFocusEffect(useCallback(() => {
|
|
const handlePendingTopupTaskResult = async (result: TransactionTaskResult) => {
|
|
log.trace('[handlePendingTopupTaskResult] event handler triggered')
|
|
setIsLoading(false)
|
|
|
|
// do not react to an active poller :)
|
|
if(pollerExists(`handlePendingTopupTaskPoller-${result.paymentHash}`)) {
|
|
return false
|
|
}
|
|
|
|
if (result.error) {
|
|
setResultModalInfo({
|
|
status: result.transaction?.status as TransactionStatus,
|
|
message: result.error.params?.message || result.error.message,
|
|
})
|
|
} else {
|
|
setResultModalInfo({
|
|
status: result.transaction?.status as TransactionStatus,
|
|
message: result.message,
|
|
})
|
|
}
|
|
|
|
toggleResultModal()
|
|
}
|
|
|
|
// Subscribe to the 'sendCompleted' event
|
|
EventEmitter.on('ev__handlePendingTopupTask_result', handlePendingTopupTaskResult)
|
|
|
|
// Unsubscribe from the 'sendCompleted' event on component unmount
|
|
return () => {
|
|
EventEmitter.off('ev__handlePendingTopupTask_result', handlePendingTopupTaskResult)
|
|
}
|
|
}, [isPendingTopupTaskSentToQueue]))
|
|
|
|
|
|
const toggleResultModal = () =>
|
|
setIsResultModalVisible(previousState => !previousState)
|
|
|
|
|
|
const onRetryToHandlePendingTopup = async function () {
|
|
if(!isInternetReachable || !paymentRequest) {
|
|
return
|
|
}
|
|
setIsLoading(true)
|
|
|
|
setIsPendingTopupTaskSentToQueue(true)
|
|
WalletTask.handlePendingTopup(
|
|
{paymentRequest}
|
|
)
|
|
|
|
}
|
|
|
|
const onGoBack = () => {
|
|
navigation.goBack()
|
|
}
|
|
|
|
const labelColor = useThemeColor('textDim')
|
|
|
|
return (
|
|
<>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
<TranItem
|
|
label="tranDetailScreen.amount"
|
|
value={transaction.amount}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
isFirst={true}
|
|
/>
|
|
{transaction.memo && transaction.memo.length > 0 && (
|
|
<TranItem
|
|
label="receiverMemo"
|
|
value={transaction.memo as string}
|
|
/>
|
|
)}
|
|
{transaction.sentFrom && (
|
|
<TranItem
|
|
label="tranDetailScreen.sentFrom"
|
|
value={transaction.sentFrom as string}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="tranDetailScreen.type"
|
|
value={transaction.type as string}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.fee"
|
|
value={transaction.fee}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
/>
|
|
{paymentRequest && isInternetReachable ? (
|
|
<View
|
|
style={{flexDirection: 'row', justifyContent: 'space-between'}}>
|
|
<TranItem
|
|
label="tranDetailScreen.status"
|
|
value={transaction.status as string}
|
|
/>
|
|
<Button
|
|
style={{marginTop: spacing.medium}}
|
|
preset="secondary"
|
|
tx="tranDetailScreen.retryToComplete"
|
|
onPress={onRetryToHandlePendingTopup}
|
|
/>
|
|
</View>
|
|
) : (
|
|
<TranItem
|
|
label="tranDetailScreen.status"
|
|
value={transaction.status as string}
|
|
/>
|
|
)}
|
|
{transaction.status === TransactionStatus.COMPLETED && (
|
|
<TranItem
|
|
label="tranDetailScreen.balanceAfter"
|
|
value={transaction.balanceAfter || 0}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="tranDetailScreen.createdAt"
|
|
value={(transaction.createdAt as Date).toLocaleString()}
|
|
/>
|
|
{paymentRequest && (
|
|
<TranItem
|
|
label="tranDetailScreen.expiresAt"
|
|
value={(new Date(paymentRequest.expiresAt!)).toLocaleString()}
|
|
/>
|
|
)}
|
|
<TranItem label="tranDetailScreen.id" value={`${transaction.id}`} />
|
|
</>
|
|
}
|
|
/>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
<TranItem
|
|
label="tranDetailScreen.topupTo"
|
|
value={transaction.mint as string}
|
|
/>
|
|
</>
|
|
}
|
|
/>
|
|
{transaction.status === TransactionStatus.PENDING && paymentRequest && (
|
|
<View style={{marginBottom: spacing.small}}>
|
|
<QRCodeBlock
|
|
qrCodeData={paymentRequest.encodedInvoice}
|
|
title={translate("tranDetailScreen.invoice")}
|
|
size={spacing.screenWidth * 0.8}
|
|
/>
|
|
</View>
|
|
)}
|
|
{isDataParsable && (
|
|
<>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
<Text
|
|
style={{color: labelColor, fontSize: 14}}
|
|
tx="tranDetailScreen.auditTrail"
|
|
/>
|
|
<JSONTree
|
|
hideRoot
|
|
data={getAuditTrail(transaction)}
|
|
theme={{
|
|
scheme: 'default',
|
|
base00: '#eee',
|
|
}}
|
|
invertTheme={colorScheme === 'light' ? false : true}
|
|
/>
|
|
</>
|
|
}
|
|
FooterComponent={
|
|
<Button
|
|
preset="tertiary"
|
|
onPress={() => copyAuditTrail(transaction)}
|
|
tx="common.copy"
|
|
style={{
|
|
minHeight: 25,
|
|
paddingVertical: spacing.extraSmall,
|
|
marginTop: spacing.small,
|
|
alignSelf: 'center',
|
|
}}
|
|
textStyle={{fontSize: 14}}
|
|
/>
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
<BottomModal
|
|
isVisible={isResultModalVisible ? true : false}
|
|
ContentComponent={
|
|
<>
|
|
{(resultModalInfo?.status === TransactionStatus.COMPLETED) && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faCheckCircle"
|
|
iconColor={colors.palette.success200}
|
|
title={translate("tranDetailScreen.modalSuccess")}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx='common.close'
|
|
onPress={onGoBack}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
{(resultModalInfo?.status === TransactionStatus.PENDING) && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faTriangleExclamation"
|
|
iconColor={colors.palette.accent400}
|
|
title={translate("tranDetailScreen.invoiceNotPaid")}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx={'common.close'}
|
|
onPress={toggleResultModal}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
{(resultModalInfo?.status === TransactionStatus.EXPIRED) && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faTriangleExclamation"
|
|
iconColor={colors.palette.accent400}
|
|
title={translate("tranDetailScreen.invoiceExpired")}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx='common.close'
|
|
onPress={toggleResultModal}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
{(resultModalInfo?.status === TransactionStatus.ERROR) && (
|
|
<>
|
|
<ResultModalInfo
|
|
icon="faTriangleExclamation"
|
|
iconColor={colors.palette.angry500}
|
|
title={translate("tranDetailScreen.topupError")}
|
|
message={resultModalInfo?.message}
|
|
/>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
preset="secondary"
|
|
tx='common.close'
|
|
onPress={toggleResultModal}
|
|
/>
|
|
</View>
|
|
</>
|
|
)}
|
|
</>
|
|
}
|
|
onBackButtonPress={toggleResultModal}
|
|
onBackdropPress={toggleResultModal}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
const TransferInfoBlock = function (props: {
|
|
transaction: Transaction
|
|
isDataParsable: boolean
|
|
copyAuditTrail: any
|
|
colorScheme: 'dark' | 'light'
|
|
}) {
|
|
const {transaction, isDataParsable, copyAuditTrail, colorScheme} = props
|
|
|
|
const labelColor = useThemeColor('textDim')
|
|
|
|
// log.trace(JSON.parse(transaction.data)[0])
|
|
|
|
return (
|
|
<>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<>
|
|
<TranItem
|
|
label="tranDetailScreen.amount"
|
|
value={transaction.amount}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
isFirst={true}
|
|
/>
|
|
{transaction.memo && (
|
|
<TranItem
|
|
label="tranDetailScreen.memoFromInvoice"
|
|
value={transaction.memo as string}
|
|
/>
|
|
)}
|
|
{transaction.sentTo && (
|
|
<TranItem
|
|
label="tranDetailScreen.trasferredTo"
|
|
value={transaction.sentTo as string}
|
|
/>
|
|
)}
|
|
<TranItem
|
|
label="tranDetailScreen.type"
|
|
value={transaction.type as string}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.fee"
|
|
value={transaction.fee}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
/>
|
|
<TranItem
|
|
label="tranDetailScreen.status"
|
|
value={transaction.status as string}
|
|
/>
|
|
|
|
{transaction.status !== TransactionStatus.ERROR && (
|
|
<TranItem
|
|
label="tranDetailScreen.balanceAfter"
|
|
value={transaction.balanceAfter || 0}
|
|
unit={transaction.unit}
|
|
isCurrency={true}
|
|
/>
|
|
)}
|
|
|
|
<TranItem
|
|
label="tranDetailScreen.createdAt"
|
|
value={(transaction.createdAt as Date).toLocaleString()}
|
|
/>
|
|
<TranItem label="tranDetailScreen.id" value={`${transaction.id}`} />
|
|
</>
|
|
}
|
|
/>
|
|
<Card
|
|
style={$dataCard}
|
|
ContentComponent={
|
|
<TranItem
|
|
label="transactionCommon.paidFrom"
|
|
value={transaction.mint as string}
|
|
/>
|
|
}
|
|
/>
|
|
{isDataParsable && (
|
|
<>
|
|
<Card
|
|
style={[$dataCard]}
|
|
ContentComponent={
|
|
<>
|
|
<Text
|
|
style={{color: labelColor, fontSize: 14}}
|
|
tx="tranDetailScreen.auditTrail"
|
|
/>
|
|
<JSONTree
|
|
hideRoot
|
|
data={getAuditTrail(transaction)}
|
|
theme={{
|
|
scheme: 'default',
|
|
base00: '#eee',
|
|
}}
|
|
invertTheme={colorScheme === 'light' ? false : true}
|
|
/>
|
|
</>
|
|
}
|
|
// footerStyle={{borderWidth: 10, borderColor: 'red'}}
|
|
FooterComponent={
|
|
<Button
|
|
preset="tertiary"
|
|
onPress={() => copyAuditTrail(transaction)}
|
|
tx="common.copy"
|
|
style={{
|
|
minHeight: 25,
|
|
paddingVertical: spacing.extraSmall,
|
|
marginTop: spacing.small,
|
|
alignSelf: 'center',
|
|
}}
|
|
textStyle={{fontSize: 14}}
|
|
/>
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export const TranItem = function (props: {
|
|
label: TxKeyPath
|
|
value: string | number
|
|
unit?: MintUnit
|
|
labelStyle?: TextStyle
|
|
valueStyle?: TextStyle
|
|
isFirst?: boolean
|
|
isLast?: boolean
|
|
isCurrency?: boolean
|
|
}) {
|
|
|
|
const labelColor = useThemeColor('textDim')
|
|
const margin = !props.isFirst ? {marginTop: spacing.small} : null
|
|
|
|
return (
|
|
<View>
|
|
<Text
|
|
style={[props.labelStyle, {color: labelColor, fontSize: 14}, margin]}
|
|
tx={props.label}
|
|
/>
|
|
{props.isCurrency && props.unit ? (
|
|
<Text style={props.valueStyle || {}} text={`${formatCurrency(props.value as number, getCurrency(props.unit).code)} ${getCurrency(props.unit).code}`} />
|
|
) : (
|
|
<Text style={props.valueStyle || {}} text={props.value as string} />
|
|
)}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
|
|
const getAuditTrail = function (transaction: Transaction) {
|
|
try {
|
|
const data = JSON.parse(transaction.data)
|
|
|
|
if (data && isArray(data)) {
|
|
return data
|
|
/* for (const item of data) {
|
|
if(item.status === TransactionStatus.ERROR) {
|
|
return item
|
|
}
|
|
}*/
|
|
}
|
|
return false
|
|
} catch (e) {
|
|
// silent
|
|
return false
|
|
}
|
|
}
|
|
|
|
const getEncodedTokenToSend = (
|
|
transaction: Transaction,
|
|
): string | undefined => {
|
|
try {
|
|
const data = JSON.parse(transaction.data)
|
|
const pendingRecord = data.find(
|
|
(record: any) => record.status === 'PENDING',
|
|
)
|
|
|
|
if (pendingRecord) {
|
|
return pendingRecord.encodedTokenToSend
|
|
}
|
|
|
|
return undefined // No pending record found
|
|
} catch (e) {
|
|
// silent
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
|
|
const getEncodedTokenToRetry = (
|
|
transaction: Transaction,
|
|
): string | undefined => {
|
|
try {
|
|
if(transaction.type !== (TransactionType.RECEIVE || TransactionType.RECEIVE_OFFLINE)) {
|
|
return undefined
|
|
}
|
|
|
|
if (transaction.status !== TransactionStatus.ERROR &&
|
|
transaction.status !== TransactionStatus.DRAFT &&
|
|
transaction.status !== TransactionStatus.BLOCKED) {
|
|
return undefined
|
|
}
|
|
|
|
const {mintsStore} = useStores()
|
|
|
|
// skip if mint is still offline
|
|
const {mint} = transaction
|
|
const mintInstance = mintsStore.findByUrl(mint)
|
|
if(!mintInstance || mintInstance.status === MintStatus.OFFLINE) {
|
|
return undefined
|
|
}
|
|
|
|
const data = JSON.parse(transaction.data)
|
|
const draftRecord = data.find(
|
|
(record: any) => record.status === 'DRAFT',
|
|
)
|
|
|
|
const encodedTokenToRetry: string = draftRecord.encodedToken
|
|
|
|
if(!encodedTokenToRetry) {return undefined}
|
|
|
|
// return token to retry if transaction somehow got stuck in DRAFT status or user blocked a mint and than changed his mind
|
|
// solves #57
|
|
if (transaction.status === TransactionStatus.DRAFT || transaction.status === TransactionStatus.BLOCKED){
|
|
return encodedTokenToRetry
|
|
}
|
|
|
|
const errorRecord = data.find(
|
|
(record: any) => record.status === 'ERROR',
|
|
)
|
|
|
|
const {error} = errorRecord
|
|
|
|
if(error && error.message) {
|
|
if(error.message.toLowerCase().includes('network request failed') ||
|
|
error.message.toLowerCase().includes('bad gateway') ||
|
|
error.message.toLowerCase().includes('outputs')) {
|
|
return encodedTokenToRetry
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
} catch (e) {
|
|
// silent
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
|
|
const getPaymentRequestToRetry = (
|
|
transaction: Transaction,
|
|
): PaymentRequest | undefined => {
|
|
try {
|
|
if(transaction.type !== (TransactionType.TOPUP)) {
|
|
return undefined
|
|
}
|
|
|
|
if (transaction.status !== TransactionStatus.ERROR &&
|
|
transaction.status !== TransactionStatus.PENDING &&
|
|
transaction.status !== TransactionStatus.EXPIRED) {
|
|
return undefined
|
|
}
|
|
|
|
const {mintsStore} = useStores()
|
|
|
|
// skip if mint is still offline
|
|
const {mint} = transaction
|
|
const mintInstance = mintsStore.findByUrl(mint)
|
|
if(!mintInstance || mintInstance.status === MintStatus.OFFLINE) {
|
|
return undefined
|
|
}
|
|
|
|
const data = JSON.parse(transaction.data)
|
|
const pendingRecord = data.find(
|
|
(record: any) => record.status === 'PENDING',
|
|
)
|
|
|
|
const paymentRequest: PaymentRequest = pendingRecord.paymentRequest
|
|
|
|
if(!paymentRequest) {return undefined}
|
|
if(pollerExists(`handlePendingTopupTaskPoller-${paymentRequest.paymentHash}`)) {return undefined}
|
|
|
|
// return paymentRequest to retry if wallet somehow failed to retreive proofs for paid invoice
|
|
return paymentRequest
|
|
|
|
} catch (e) {
|
|
// silent
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
|
|
const $screen: ViewStyle = {}
|
|
|
|
const $headerContainer: TextStyle = {
|
|
alignItems: 'center',
|
|
paddingBottom: spacing.medium,
|
|
height: spacing.screenHeight * 0.20,
|
|
}
|
|
|
|
const $contentContainer: TextStyle = {
|
|
// flex: 1,
|
|
padding: spacing.extraSmall,
|
|
}
|
|
|
|
const $tranAmount: TextStyle = {
|
|
fontSize: moderateVerticalScale(48),
|
|
lineHeight: moderateVerticalScale(48),
|
|
marginTop: spacing.extraSmall,
|
|
marginLeft: -30,
|
|
color: 'white',
|
|
}
|
|
|
|
const $actionCard: ViewStyle = {
|
|
marginBottom: spacing.extraSmall,
|
|
marginTop: -spacing.extraLarge * 2,
|
|
paddingVertical: 0,
|
|
}
|
|
|
|
const $dataCard: ViewStyle = {
|
|
padding: spacing.medium,
|
|
marginBottom: spacing.extraSmall,
|
|
paddingTop: spacing.extraSmall,
|
|
}
|
|
|
|
const $item: ViewStyle = {
|
|
paddingHorizontal: spacing.small,
|
|
paddingLeft: 0,
|
|
}
|
|
|
|
|
|
const $iconContainer: ViewStyle = {
|
|
padding: spacing.extraSmall,
|
|
alignSelf: 'center',
|
|
marginRight: spacing.medium,
|
|
}
|
|
|
|
const $noteContainer: TextStyle = {
|
|
padding: spacing.small,
|
|
alignItems: 'center',
|
|
}
|
|
|
|
const $noteInput: TextStyle = {
|
|
flex: 1,
|
|
margin: spacing.small,
|
|
borderRadius: spacing.extraSmall,
|
|
fontSize: 16,
|
|
padding: spacing.small,
|
|
alignSelf: 'stretch',
|
|
textAlignVertical: 'top',
|
|
}
|
|
|
|
const $buttonContainer: ViewStyle = {
|
|
flexDirection: 'row',
|
|
alignSelf: 'center',
|
|
}
|
|
|
|
const $qrCodeContainer: ViewStyle = {
|
|
backgroundColor: 'white',
|
|
padding: spacing.small,
|
|
margin: spacing.small,
|
|
borderRadius: spacing.small,
|
|
alignSelf: 'center'
|
|
}
|