Files
minibits_wallet/src/screens/RemoteRecoveryScreen.tsx
T

1099 lines
45 KiB
TypeScript

import {observer} from 'mobx-react-lite'
import React, {FC, useEffect, useRef, useState} from 'react'
import {FlatList, LayoutAnimation, Platform, Pressable, Switch, TextInput, TextStyle, UIManager, View, ViewStyle} from 'react-native'
import {validateMnemonic} from '@scure/bip39'
import QuickCrypto from 'react-native-quick-crypto'
import { wordlist } from '@scure/bip39/wordlists/english'
import {colors, spacing, useThemeColor} from '../theme'
import {AppStackScreenProps} from '../navigation' // @demo remove-current-line
import {
Icon,
ListItem,
Screen,
Text,
Card,
Loading,
ErrorModal,
InfoModal,
BottomModal,
Button,
$sizeStyles,
} from '../components'
import {useHeader} from '../utils/useHeader'
import AppError, { Err } from '../utils/AppError'
import { KeyChain, log, MinibitsClient, NostrClient } from '../services'
import Clipboard from '@react-native-clipboard/clipboard'
import { useStores } from '../models'
import { MintListItem } from './Mints/MintListItem'
import { Mint } from '../models/Mint'
import { MintKeyset } from '@cashu/cashu-ts'
import { CashuUtils } from '../services/cashu/cashuUtils'
import { Proof } from '../models/Proof'
import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../models/Transaction'
import { ResultModalInfo } from './Wallet/ResultModalInfo'
import { deriveSeedFromMnemonic } from '@cashu/cashu-ts'
import { MINIBITS_NIP05_DOMAIN } from '@env'
import { delay } from '../utils/utils'
import { getSnapshot, isStateTreeNode } from 'mobx-state-tree'
import { scale } from '@gocodingnow/rn-size-matters'
import { WalletUtils } from '../services/wallet/utils'
import { WalletScreen } from './WalletScreen'
import { MintUnit, formatCurrency, getCurrency } from '../services/wallet/currency'
import { isObj } from '@cashu/cashu-ts/src/utils'
import { WalletProfileRecord } from '../models/WalletProfileStore'
import { translate } from '../i18n'
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true)
}
const RESTORE_INDEX_INTERVAL = 50
export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = observer(function RemoteRecoveryScreen(_props) {
const {navigation, route} = _props
useHeader({
leftIcon: 'faArrowLeft',
onLeftPress: () => {
navigation.goBack()
},
})
const {
mintsStore,
proofsStore,
userSettingsStore,
transactionsStore,
walletProfileStore,
walletStore
} = useStores()
const mnemonicInputRef = useRef<TextInput>(null)
const indexInputRef = useRef<TextInput>(null)
const [info, setInfo] = useState('')
const [isAddressOnlyRecovery, setIsAddressOnlyRecovery] = useState(route.params.isAddressOnlyRecovery ? true : false)
const [mnemonic, setMnemonic] = useState<string>('')
const [mnemonicExists, setMnemonicExists] = useState(false)
const [isValidMnemonic, setIsValidMnemonic] = useState(false)
const [seed, setSeed] = useState<Uint8Array>()
const [profileToRecover, setProfileToRecover] = useState<WalletProfileRecord | undefined>(undefined)
const [selectedMintUrl, setSelectedMintUrl] = useState<string | undefined>()
const [selectedKeyset, setSelectedKeyset] = useState<MintKeyset | undefined>()
const [selectedMintKeysets, setSelectedMintKeysets] = useState<MintKeyset[]>([])
const [startIndexString, setStartIndexString] = useState<string>('0')
const [startIndex, setStartIndex] = useState<number>(0) // start of interval of indexes of proofs to recover
const [endIndex, setEndIndex] = useState<number>(RESTORE_INDEX_INTERVAL) // end of interval
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<AppError | undefined>()
const [isErrorsModalVisible, setIsErrorsModalVisible] = useState(false)
const [isIndexModalVisible, setIsIndexModalVisible] = useState(false)
const [isKeysetModalVisible, setIsKeysetModalVisible] = useState(false)
const [resultModalInfo, setResultModalInfo] = useState<{status: TransactionStatus, message: string} | undefined>()
const [isResultModalVisible, setIsResultModalVisible] = useState(false)
const [lastRecoveredAmount, setLastRecoveredAmount] = useState<number>(0)
const [totalRecoveredAmount, setTotalRecoveredAmount] = useState<number>(0)
const [recoveryErrors, setRecoveryErrors] = useState<AppError[]>([])
const [statusMessage, setStatusMessage] = useState<string>()
useEffect(() => {
const getMnemonic = async () => {
try {
setIsLoading(true)
const existing = await walletStore.getMnemonic()
if(existing) {
setMnemonicExists(true)
}
setIsLoading(false)
} catch (e: any) {
handleError(e)
}
}
getMnemonic()
}, [])
const toggleResultModal = () => {
if(isResultModalVisible === true) {
setResultModalInfo(undefined)
}
setIsResultModalVisible(previousState => !previousState)
}
const toggleErrorsModal = () => {
setIsErrorsModalVisible(previousState => !previousState)
}
const onPaste = async function () {
try {
const maybeMnemonic = await Clipboard.getString()
if(!maybeMnemonic) {
throw new AppError(Err.VALIDATION_ERROR, translate('backupScreen.missingMnemonicError'))
}
const cleanedMnemonic = maybeMnemonic.replace(/\s+/g, ' ').trim()
setMnemonic(cleanedMnemonic)
} catch (e: any) {
handleError(e)
}
}
const onConfirm = async function () {
try {
setStatusMessage(translate("derivingSeedStatus"))
if(!mnemonic) {
throw new AppError(Err.VALIDATION_ERROR, translate('backupScreen.missingMnemonicError'))
}
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setIsLoading(true)
if (!validateMnemonic(mnemonic, wordlist)) {
throw new AppError(Err.VALIDATION_ERROR, translate("recoveryInvalidMnemonicError"))
}
setTimeout(() => {
const start = performance.now()
const binarySeed = deriveSeedFromMnemonic(mnemonic) // expensive
const end = performance.now()
console.log(`[onConfirm] deriveSeedFromMnemonic took ${end - start} ms.`)
setSeed(binarySeed)
setIsValidMnemonic(true)
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setIsLoading(false)
}, 200)
} catch (e: any) {
handleError(e)
}
}
const onBack = function (): void {
return navigation.goBack()
}
const onAddMints = function (): void {
return navigation.navigate('Mints', {})
}
const onMintSelect = async function (mint: Mint) {
try {
setSelectedMintUrl(mint.mintUrl)
const allKeysets = getSnapshot(mint.keysets!)
const defaultKeyset = walletStore.getOptimalKeyset(mint, 'sat')
setSelectedKeyset(defaultKeyset)
setSelectedMintKeysets(allKeysets)
setStartIndex(0)
setEndIndex(RESTORE_INDEX_INTERVAL)
} catch (e: any) {
handleError(e)
}
}
const toggleIndexModal = () => {
setIsIndexModalVisible(previousState => !previousState)
}
const toggleKeysetModal = () => {
setIsKeysetModalVisible(previousState => !previousState)
}
const onResetStartIndex = function () {
setStartIndex(parseInt(startIndexString))
setEndIndex(parseInt(startIndexString) + RESTORE_INDEX_INTERVAL)
toggleIndexModal()
}
const startRecovery = async function () {
if(!selectedMintUrl) {
setInfo(translate("recovery.selectMintFrom"))
return
}
setStatusMessage(translate("recovery.starting"))
setIsLoading(true)
setTimeout(() => doRecovery(), 100)
}
const doRecovery = async function () {
let errors: AppError[] = []
let recoveredAmount: number = 0
let alreadySpentAmount: number = 0
const transactionData: TransactionData[] = []
let transaction: Transaction | undefined = undefined
const pendingTransactionData: TransactionData[] = []
let pendingTransaction: Transaction | undefined = undefined
let recoveredMint = mintsStore.findByUrl(selectedMintUrl as string)
try {
if(!recoveredMint) {
setInfo(translate("recovery.noMintSelected"))
return
}
if(!selectedKeyset) {
setInfo(translate("recovery.noKeysetSelected"))
return
}
setStatusMessage(translate("recovery.restoringFromParam", { hostname: recoveredMint.hostname }))
log.info('[restore]', `Restoring from ${recoveredMint.hostname}...`)
const { proofs } = await walletStore.restore(
recoveredMint.mintUrl,
seed as Uint8Array,
{
indexFrom: startIndex,
indexTo: endIndex,
keysetId: selectedKeyset.id as string
}
)
log.debug('[restore]', `Restored proofs`, proofs.length)
setStatusMessage(translate("recovery.foundProofsAmount", { amount: proofs.length }))
if (proofs.length > 0) {
// need to move counter by whole interval to avoid duplicate _B!!!
const proofsCounter = recoveredMint.getProofsCounterByKeysetId(selectedKeyset.id)
proofsCounter?.increaseProofsCounter(Math.abs(endIndex - startIndex))
const {spent, pending} = await walletStore.getSpentOrPendingProofsFromMint(
recoveredMint.mintUrl,
selectedKeyset.unit as MintUnit,
proofs as Proof[],
)
log.debug('[restore]', `Spent and pending proofs`, {spent: spent.length, pending: pending.length})
setStatusMessage(translate("recovery.spentProofsAmount", { amount: spent.length }))
const spentAmount = CashuUtils.getProofsAmount(spent as Proof[])
alreadySpentAmount += spentAmount
const unspent = proofs.filter((proof: Proof) => !spent.includes(proof))
if(unspent && unspent.length > 0) {
setStatusMessage(translate("recovery.completing"))
const amount = CashuUtils.getProofsAmount(unspent as Proof[])
recoveredAmount = amount
// Let's create new draft receive transaction in database
transactionData.push({
status: TransactionStatus.PREPARED,
amount,
createdAt: new Date(),
})
const newTransaction = {
type: TransactionType.RECEIVE,
amount,
fee: 0,
unit: selectedKeyset.unit as MintUnit,
data: JSON.stringify(transactionData),
memo: 'Wallet recovery from seed',
mint: recoveredMint.mintUrl,
status: TransactionStatus.PREPARED,
}
transaction = await transactionsStore.addTransaction(newTransaction)
const { amountToAdd, addedAmount } = WalletUtils.addCashuProofs(
recoveredMint.mintUrl,
unspent,
{
unit: selectedKeyset.unit as MintUnit,
transactionId: transaction.id,
isPending: false
}
)
if (amountToAdd !== addedAmount) {
transaction.setReceivedAmount(addedAmount)
recoveredAmount = addedAmount
}
// Finally, update completed transaction
transactionData.push({
status: TransactionStatus.COMPLETED,
recoveredAmount,
createdAt: new Date(),
})
transaction.setStatus(
TransactionStatus.COMPLETED,
JSON.stringify(transactionData),
)
const balanceAfter = proofsStore.getUnitBalance(selectedKeyset.unit as MintUnit)?.unitBalance
transaction.setBalanceAfter(balanceAfter || 0)
}
if(pending && pending.length > 0) {
// setStatusMessage(`Found ${pending.length} pending proofs...`)
setStatusMessage(translate("recovery.foundPendingProofsAmount", { amount: pending.length }))
log.debug(`Found pending ecash with ${recoveredMint.hostname}...`)
const amount = CashuUtils.getProofsAmount(pending as Proof[])
// Let's create new draft receive transaction in database
pendingTransactionData.push({
status: TransactionStatus.PREPARED,
amount,
createdAt: new Date(),
})
const newTransaction = {
type: TransactionType.RECEIVE,
amount,
fee: 0,
unit: selectedKeyset?.unit as MintUnit,
data: JSON.stringify(pendingTransactionData),
memo: 'Wallet recovery - pending',
mint: recoveredMint.mintUrl,
status: TransactionStatus.PREPARED,
}
pendingTransaction = await transactionsStore.addTransaction(newTransaction)
const { amountToAdd, addedAmount } = WalletUtils.addCashuProofs(
recoveredMint.mintUrl,
pending,
{
unit: selectedKeyset?.unit as MintUnit,
transactionId: pendingTransaction.id,
isPending: true
}
)
if (amountToAdd !== addedAmount) {
pendingTransaction.setReceivedAmount(addedAmount)
}
// Finally, update pending transaction
pendingTransactionData.push({
status: TransactionStatus.PENDING,
createdAt: new Date(),
})
pendingTransaction.setStatus(
TransactionStatus.PENDING,
JSON.stringify(pendingTransactionData),
)
}
}
} catch(e: any) {
if (selectedMintUrl) {
e.params = {mintUrl: selectedMintUrl}
}
log.error('[doRecovery]', {name: e.name, message: isObj(e.message) ? JSON.stringify(e.message) : e.message, params: e.params})
errors.push({name: e.name, message: e.message}) // TODO this could now be single error as we do not loop anymore
if (transaction) {
transactionData.push({
status: TransactionStatus.ERROR,
error: WalletUtils.formatError(e),
createdAt: new Date(),
})
transaction.setStatus(
TransactionStatus.ERROR,
JSON.stringify(transactionData),
)
}
if (pendingTransaction) {
pendingTransactionData.push({
status: TransactionStatus.ERROR,
error: WalletUtils.formatError(e),
createdAt: new Date(),
})
pendingTransaction.setStatus(
TransactionStatus.ERROR,
JSON.stringify(pendingTransactionData),
)
}
setStatusMessage(undefined)
}
setLastRecoveredAmount(recoveredAmount)
setTotalRecoveredAmount(prevTotalRecoveredAmount => prevTotalRecoveredAmount + recoveredAmount)
setStartIndex(startIndex + RESTORE_INDEX_INTERVAL)
setEndIndex(endIndex + RESTORE_INDEX_INTERVAL)
setStatusMessage(undefined)
setIsLoading(false)
if(recoveredAmount > 0) {
const currency = getCurrency(selectedKeyset?.unit as MintUnit)
setResultModalInfo({
status: TransactionStatus.COMPLETED,
message: translate("recovery.recoveredResult", {
formattedCurrency: formatCurrency(recoveredAmount, currency.code),
code: currency.code
})
})
} else {
if(errors.length > 0) {
setResultModalInfo({
status: TransactionStatus.ERROR,
message: translate("recovery.resultErrors")
})
setRecoveryErrors(errors)
} else {
if(alreadySpentAmount > 0) {
setResultModalInfo({
status: TransactionStatus.EXPIRED,
message: translate("recovery.resultSpent")
})
} else {
setResultModalInfo({
status: TransactionStatus.EXPIRED,
message: translate("recovery.resultExpired")
})
}
}
}
toggleResultModal() // open
}
const onFindWalletAddress = async () => {
try {
if(!seed || !mnemonic) {
throw new AppError(Err.VALIDATION_ERROR, translate("backupScreen.missingMnemonicOrSeedError"))
}
const seedHash = QuickCrypto.createHash('sha256')
.update(seed)
.digest('hex')
const currentSeedHash = await KeyChain.loadSeedHash()
/* if(currentSeedHash === seedHash) {
throw new AppError(Err.VALIDATION_ERROR, translate("recovery.seedFromCurrentDeviceError"))
}*/ // allow on-device profile refresh based on server data
const profile = await MinibitsClient.getWalletProfileBySeedHash(seedHash as string)
// Skip external profiles beacause we do not control keys
if(profile) {
log.info('[onCheckWalletAddress] profileToRecover', {profile})
if(profile.nip05.includes(MINIBITS_NIP05_DOMAIN)) {
setProfileToRecover(profile)
} else {
setInfo(translate("recovery.ownKeysImportAgain", { addr: profile.nip05 }))
await delay(4000)
}
} else {
setInfo(translate("recovery.noWalletForSeedError"))
}
} catch (e: any) {
handleError(e)
}
}
const onCompleteAddress = async () => {
try {
if(!seed || !mnemonic || !profileToRecover) {
throw new AppError(Err.VALIDATION_ERROR, translate("recovery.missingMnemonicSeedProfileError"))
}
setStatusMessage(translate("recovery.recoveringAddress"))
setIsLoading(true)
const seedHash = QuickCrypto.createHash('sha256')
.update(seed)
.digest('hex')
// get nostr key from current on device profile
const {publicKey: newPublicKey} = await NostrClient.getOrCreateKeyPair()
// delete orphaned server profile then update server profile with seedHash with pubkey + update on device wallet name and address
await walletProfileStore.recover(seedHash as string, newPublicKey)
// align walletId in userSettings with recovered profile
userSettingsStore.setWalletId(walletProfileStore.walletId)
await KeyChain.saveMnemonic(mnemonic)
await KeyChain.saveSeed(seed as Uint8Array)
await delay(1000)
setStatusMessage(translate("recovery.completed"))
await delay(2000)
userSettingsStore.setIsOnboarded(true)
setStatusMessage('')
setIsLoading(false)
navigation.goBack()
} catch (e: any) {
handleError(e)
}
}
const onComplete = async () => {
try {
if(!seed || !mnemonic) {
throw new AppError(Err.VALIDATION_ERROR, translate('backupScreen.missingMnemonicOrSeedError'))
}
setStatusMessage(translate('recovery.recoveringAddress'))
setIsLoading(true)
await KeyChain.saveMnemonic(mnemonic)
await KeyChain.saveSeed(seed as Uint8Array)
// Wallet address recovery
const seedHash = await KeyChain.loadSeedHash()
log.trace('[onComplete]', 'getWalletProfileBySeedHash')
const profileToRecover = await MinibitsClient.getWalletProfileBySeedHash(seedHash as string)
// Skip external profiles beacause we do not control keys
if(profileToRecover) {
log.info('[onComplete] recovery', {profileToRecover})
setStatusMessage(translate("recovery.foundAddrParam", { addr: profileToRecover.nip05 }))
if(profileToRecover.nip05.includes(MINIBITS_NIP05_DOMAIN)) {
const {publicKey: newPublicKey} = await NostrClient.getOrCreateKeyPair()
// Updates pubkey and imports wallet profile
await walletProfileStore.recover(seedHash as string, newPublicKey)
// Align walletId in userSettings with recovered profile
userSettingsStore.setWalletId(walletProfileStore.walletId)
await delay(1000)
setStatusMessage(translate('recovery.completed'))
await delay(2000)
} else {
setInfo(translate("recovery.ownKeysImportAgain", { addr: profileToRecover.nip05 }))
await delay(5000)
}
}
userSettingsStore.setIsOnboarded(true)
setStatusMessage('')
setIsLoading(false)
navigation.navigate('Tabs')
} catch (e: any) {
handleError(e)
}
}
const handleError = function (e: AppError): void {
setIsLoading(false)
setError(e)
}
const headerBg = useThemeColor('header')
const numIconColor = useThemeColor('textDim')
const textHint = useThemeColor('textDim')
const inputBg = useThemeColor('background')
const headerTitle = useThemeColor('headerTitle')
return (
<Screen contentContainerStyle={$screen} preset="auto">
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<Text preset="heading" text="Wallet recovery" style={{color: headerTitle, zIndex: 10}} />
</View>
<View style={$contentContainer}>
{mnemonicExists && !isAddressOnlyRecovery ? (
<Card
style={$card}
ContentComponent={
<ListItem
tx="recovery.mnemonicCollision"
subTx="recovery.mnemonicCollisionDesc"
leftIcon='faTriangleExclamation'
// leftIconColor='red'
style={$item}
bottomSeparator={true}
/>
}
FooterComponent={
<View style={$buttonContainer}>
<Button
onPress={onBack}
tx='common.back'
preset='secondary'
/>
</View>
}
/>
) : (
<>
{isValidMnemonic ? (
<>
<Card
style={$card}
ContentComponent={
<ListItem
tx='backupScreen.mnemonicTitle'
subText={mnemonic}
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='1'/></View>}
style={$item}
/>
}
/>
{isAddressOnlyRecovery ? (
<>
{profileToRecover ? (
<>
<Card
style={$card}
ContentComponent={
<ListItem
text={profileToRecover.nip05}
subTx="profileToRecoverDesc"
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='2'/></View>}
style={$item}
/>
}
/>
<View style={$buttonContainer}>
<Button
onPress={onCompleteAddress}
tx="recovery.completeCTA"
style={{marginRight: spacing.small}}
/>
</View>
</>
) : (
<View style={$buttonContainer}>
<Button
onPress={onFindWalletAddress}
tx="recovery.findAddressCTA"
style={{marginRight: spacing.small}}
/>
</View>
)}
</>
) : (
<>
<Card
style={$card}
HeadingComponent={
<>
<ListItem
tx="recoveryFromMints"
subTx="recoveryFromMintsDesc"
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='2'/></View>}
RightComponent={mintsStore.mintCount > 0 ? (
<View style={$rightContainer}>
<Button
onPress={onAddMints}
text='Mints'
preset='secondary'
/>
</View>
) : (undefined)
}
style={$item}
/>
{mintsStore.mintCount === 0 && (
<View style={$buttonContainer}>
<Button
onPress={onAddMints}
tx="addMints"
/>
</View>
)}
</>
}
ContentComponent={
<>
{mintsStore.mints.map((mint: Mint, index: number) => (
<MintListItem
key={mint.mintUrl}
mint={mint}
mintBalance={proofsStore.getMintBalance(mint.mintUrl)}
onMintSelect={() => onMintSelect(mint)}
isSelectable={true}
isSelected={selectedMintUrl === mint.mintUrl}
separator={index === 0 ? 'both' : 'bottom'}
/>
))}
</>
}
FooterComponent={
<>
{mintsStore.mintCount > 0 && selectedMintUrl && (
<>
<View style={$buttonContainer}>
{(startIndex > 0 || totalRecoveredAmount > 0) && (
<Button
onPress={onComplete}
tx="common.complete"
style={{marginRight: spacing.small}}
/>
)}
<Button
onPress={startRecovery}
tx={startIndex === 0 ? 'startRecovery' : 'nextInterval'}
preset={(startIndex === 0 || totalRecoveredAmount > 0) ? 'default' : 'secondary'}
disabled={selectedMintUrl ? false : true}
/>
</View>
<View style={$buttonContainer}>
<Text
text={translate("recovery.intervalParam", {
startIndex: startIndex,
endIndex: endIndex
})}
size='xxs'
style={{color: textHint, alignSelf: 'center', marginTop: spacing.small}}
/>
<Pressable onPress={toggleIndexModal}>
<Text
tx="recovery.setManually"
size='xxs'
style={{color: textHint, alignSelf: 'center', marginTop: spacing.small}}
/>
</Pressable>
</View>
<View style={[$buttonContainer,{marginTop: 0}]}>
<Text
text={translate("recovery.keysetID", {
id: selectedKeyset?.id,
unit: selectedKeyset?.unit
})}
size='xxs'
style={{color: textHint, alignSelf: 'center', marginTop: spacing.extraSmall}}
/>
{selectedMintKeysets.length > 1 && (
<Pressable onPress={toggleKeysetModal}>
<Text
tx="recovery.selectAnotherKeyset"
size='xxs'
style={{color: textHint, alignSelf: 'center', marginTop: spacing.extraSmall}}
/>
</Pressable>
)}
</View>
</>
)}
{mintsStore.mintCount > 0 && !selectedMintUrl && (
<Text
tx='recovery.selectMintFrom'
size='xxs'
style={{color: textHint, alignSelf: 'center', margin: spacing.large}}
/>
)}
</>
}
/>
</>
)}
</>
) : (
<Card
style={$card}
ContentComponent={
<ListItem
tx="recoveryInsertMnemonic"
subTx={isAddressOnlyRecovery
? 'recoveryInsertMnemonicDescAddrOnly'
: 'recoveryInsertMnemonicDesc'
}
LeftComponent={<View style={[$numIcon, {backgroundColor: numIconColor}]}><Text text='1'/></View>}
style={$item}
/>
}
FooterComponent={
<>
<TextInput
ref={mnemonicInputRef}
onChangeText={(mnemonic: string) => setMnemonic(mnemonic)}
value={mnemonic}
numberOfLines={3}
multiline={true}
autoCapitalize='none'
keyboardType='default'
maxLength={150}
placeholder={translate("mnemonicPhrasePlaceholder")}
selectTextOnFocus={true}
style={[$mnemonicInput, {backgroundColor: inputBg, flexWrap: 'wrap'}]}
/>
<View style={$buttonContainer}>
{mnemonic ? (
<Button
onPress={onConfirm}
tx='common.confirm'
/>
) : (
<Button
onPress={onPaste}
tx='common.paste'
/>
)
}
</View>
</>
}
/>
)}
</>
)}
</View>
<BottomModal
isVisible={isIndexModalVisible}
ContentComponent={
<View style={$indexContainer}>
<Text tx="setStartIndex" preset="subheading" />
<View style={{flexDirection: 'row', alignItems: 'center'}}>
<TextInput
ref={indexInputRef}
onChangeText={index => setStartIndexString(index)}
value={startIndexString}
style={[$noteInput, {backgroundColor: inputBg}]}
maxLength={8}
selectTextOnFocus={true}
keyboardType='numeric'
textAlign='right'
/>
<Button
tx='common.save'
onPress={onResetStartIndex}
/>
</View>
<Text
tx="recovery.startIndexDesc"
size='xxs'
style={{color: textHint, margin: spacing.small}}
/>
</View>
}
onBackButtonPress={toggleIndexModal}
onBackdropPress={toggleIndexModal}
/>
<BottomModal
isVisible={isKeysetModalVisible}
// style={{alignItems: 'stretch'}}
HeadingComponent={<Text tx="recovery.selectKeyset" style={{textAlign: 'center', margin: spacing.small}}/>}
ContentComponent={
<FlatList
data={selectedMintKeysets}
numColumns={2}
renderItem={({ item, index }) => {
return(
<Button
key={index}
preset={selectedKeyset?.id === item.id ? 'default' : 'secondary'}
onPress={() => {
setSelectedKeyset(item)
setStartIndex(0)
setEndIndex(RESTORE_INDEX_INTERVAL)
}}
text={`${item.id} (${item.unit})`}
style={{minWidth: scale(80), margin: spacing.extraSmall}}
textStyle={$sizeStyles.xxs}
/>
)
}}
keyExtractor={(item) => item.id}
style={{ flexGrow: 0 }}
/>
}
FooterComponent={
<Button
preset={'secondary'}
onPress={toggleKeysetModal}
tx='common.close'
style={{marginTop: spacing.small}}
/>}
onBackButtonPress={toggleKeysetModal}
onBackdropPress={toggleKeysetModal}
/>
<BottomModal
isVisible={isErrorsModalVisible}
style={{alignItems: 'stretch'}}
ContentComponent={
<>
{recoveryErrors?.map((err, index) => (
<ListItem
key={index}
leftIcon='faTriangleExclamation'
leftIconColor={colors.palette.angry500}
text={err.message}
subText={err.params ? err.params.mintUrl : ''}
bottomSeparator={true}
style={{paddingHorizontal: spacing.small}}
/>
)
)}
</>
}
onBackButtonPress={toggleErrorsModal}
onBackdropPress={toggleErrorsModal}
/>
<BottomModal
isVisible={isResultModalVisible ? true : false}
ContentComponent={
<>
{resultModalInfo &&
resultModalInfo.status === TransactionStatus.COMPLETED && (
<>
<ResultModalInfo
icon="faCheckCircle"
iconColor={colors.palette.success200}
title={translate("recovery.success")}
message={resultModalInfo?.message}
/>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx='common.close'
onPress={toggleResultModal}
/>
</View>
</>
)}
{resultModalInfo &&
resultModalInfo.status === TransactionStatus.ERROR && (
<>
<ResultModalInfo
icon="faTriangleExclamation"
iconColor={colors.palette.angry500}
title={translate("recovery.failed")}
message={resultModalInfo?.message}
/>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx="showErrors"
onPress={toggleErrorsModal}
/>
</View>
</>
)}
{resultModalInfo &&
resultModalInfo.status === TransactionStatus.EXPIRED && (
<>
<ResultModalInfo
icon='faInfoCircle'
iconColor={colors.palette.neutral400}
title={translate("noEcashRecovered")}
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} />}
{isLoading && <Loading statusMessage={statusMessage} textStyle={{color: 'white'}} style={{backgroundColor: headerBg, opacity: 1}}/>}
</Screen>
)
})
const $screen: ViewStyle = {flex: 1}
const $headerContainer: TextStyle = {
alignItems: 'center',
padding: spacing.medium,
height: spacing.screenHeight * 0.20,
}
const $contentContainer: TextStyle = {
flex: 1,
marginTop: -spacing.extraLarge * 2,
padding: spacing.extraSmall,
}
const $indexContainer: TextStyle = {
padding: spacing.small,
alignItems: 'center',
}
const $card: ViewStyle = {
marginBottom: spacing.small,
}
const $numIcon: ViewStyle = {
width: 30,
height: 30,
borderRadius: 15,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.medium
}
const $mnemonicInput: TextStyle = {
// flex: 1,
borderRadius: spacing.small,
fontSize: 16,
padding: spacing.small,
alignSelf: 'stretch',
textAlignVertical: 'top',
}
const $buttonContainer: ViewStyle = {
flexDirection: 'row',
alignSelf: 'center',
marginTop: spacing.small,
}
const $noteInput: TextStyle = {
flex: 1,
margin: spacing.small,
borderRadius: spacing.extraSmall,
fontSize: 16,
padding: spacing.small,
alignSelf: 'stretch',
textAlignVertical: 'top',
}
const $bottomModal: ViewStyle = {
flex: 1,
alignItems: 'center',
paddingVertical: spacing.large,
paddingHorizontal: spacing.small,
}
const $item: ViewStyle = {
paddingHorizontal: spacing.small,
paddingLeft: 0,
}
const $rightContainer: ViewStyle = {
// padding: spacing.extraSmall,
alignSelf: 'center',
//marginLeft: spacing.small,
}