mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
1184 lines
43 KiB
TypeScript
1184 lines
43 KiB
TypeScript
import {observer} from 'mobx-react-lite'
|
|
import React, {FC, useState, useEffect, useCallback, useRef} from 'react'
|
|
import {useFocusEffect} from '@react-navigation/native'
|
|
import {
|
|
TextStyle,
|
|
ViewStyle,
|
|
View,
|
|
Text as RNText,
|
|
AppState,
|
|
Image,
|
|
Animated,
|
|
FlatList,
|
|
Pressable,
|
|
Linking,
|
|
LayoutAnimation,
|
|
AppStateStatus,
|
|
} from 'react-native'
|
|
import codePush, { RemotePackage } from 'react-native-code-push'
|
|
import {moderateScale, verticalScale} from '@gocodingnow/rn-size-matters'
|
|
import { SvgXml } from 'react-native-svg'
|
|
import {useThemeColor, spacing, colors, typography} from '../theme'
|
|
import {
|
|
Button,
|
|
Icon,
|
|
Screen,
|
|
Text,
|
|
Card,
|
|
ListItem,
|
|
InfoModal,
|
|
Loading,
|
|
BottomModal,
|
|
ErrorModal,
|
|
Header,
|
|
ScanIcon,
|
|
MintIcon
|
|
} from '../components'
|
|
import {useStores} from '../models'
|
|
import {WalletStackScreenProps} from '../navigation'
|
|
import {Mint, UnitBalance} from '../models/Mint'
|
|
import {MintsByUnit} from '../models/MintsStore'
|
|
import {log, NostrClient} from '../services'
|
|
import {Env} from '../utils/envtypes'
|
|
import {Transaction} from '../models/Transaction'
|
|
import {TransactionListItem} from './Transactions/TransactionListItem'
|
|
import {WalletTask} from '../services'
|
|
import {translate} from '../i18n'
|
|
import AppError, { Err } from '../utils/AppError'
|
|
import {
|
|
APP_ENV,
|
|
CODEPUSH_STAGING_DEPLOYMENT_KEY,
|
|
CODEPUSH_PRODUCTION_DEPLOYMENT_KEY,
|
|
MINIBITS_MINT_URL,
|
|
NATIVE_VERSION_ANDROID
|
|
} from '@env'
|
|
import { round } from '../utils/number'
|
|
import { IncomingParser } from '../services/incomingParser'
|
|
import useIsInternetReachable from '../utils/useIsInternetReachable'
|
|
import { CurrencySign } from './Wallet/CurrencySign'
|
|
import { CurrencyCode, MintUnit, MintUnitCurrencyPairs, convertToFromSats, getCurrency } from "../services/wallet/currency"
|
|
import { CurrencyAmount } from './Wallet/CurrencyAmount'
|
|
import { LeftProfileHeader } from './ContactsScreen'
|
|
import { NavigationState, Route, TabBar, TabView } from 'react-native-tab-view'
|
|
import { getUnixTime } from 'date-fns/getUnixTime'
|
|
|
|
const deploymentKey = APP_ENV === Env.PROD ? CODEPUSH_PRODUCTION_DEPLOYMENT_KEY : CODEPUSH_STAGING_DEPLOYMENT_KEY
|
|
|
|
interface WalletScreenProps extends WalletStackScreenProps<'Wallet'> {}
|
|
|
|
export const WalletScreen: FC<WalletScreenProps> = observer(
|
|
function WalletScreen({route, navigation}) {
|
|
const {
|
|
relaysStore,
|
|
mintsStore,
|
|
proofsStore,
|
|
transactionsStore,
|
|
paymentRequestsStore,
|
|
userSettingsStore,
|
|
nwcStore,
|
|
walletProfileStore,
|
|
walletStore
|
|
} = useStores()
|
|
|
|
const appState = useRef(AppState.currentState)
|
|
const isInternetReachable = useIsInternetReachable()
|
|
const groupedMints: MintsByUnit[] = mintsStore.groupedByUnit
|
|
|
|
// Tab view by unit
|
|
const [routes] = useState(
|
|
groupedMints.map((mintUnit) => ({
|
|
key: mintUnit.unit,
|
|
title: getCurrency(mintUnit.unit).code,
|
|
}))
|
|
)
|
|
const [tabIndex, setTabIndex] = useState(0)
|
|
const [currentUnit, setCurrentUnit] = useState<MintUnit>(groupedMints.length > 0 ? groupedMints[0].unit : 'sat')
|
|
|
|
const [info, setInfo] = useState<string>('')
|
|
const [defaultMintUrl, setDefaultMintUrl] = useState<string>(MINIBITS_MINT_URL)
|
|
const [error, setError] = useState<AppError | undefined>()
|
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
|
const [lastClaimCheck, setLastClaimCheck] = useState<number>(getUnixTime(new Date()))
|
|
const [lastPendingCheck, setLastPendingCheck] = useState<number>(getUnixTime(new Date()))
|
|
const [isMintsModalVisible, setIsMintsModalVisible] = useState<boolean>(false)
|
|
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false)
|
|
const [isUpdateModalVisible, setIsUpdateModalVisible] = useState<boolean>(false)
|
|
const [isSendModalVisible, setIsSendModalVisible] = useState<boolean>(false)
|
|
const [isReceiveModalVisible, setIsReceiveModalVisible] = useState<boolean>(false)
|
|
const [updateDescription, setUpdateDescription] = useState<string>('')
|
|
const [updateSize, setUpdateSize] = useState<string>('')
|
|
const [isNativeUpdateAvailable, setIsNativeUpdateAvailable] = useState<boolean>(false)
|
|
|
|
// On app start
|
|
useEffect(() => {
|
|
const checkForUpdate = async () => {
|
|
try {
|
|
const update = await codePush.checkForUpdate(deploymentKey, handleBinaryVersionMismatchCallback)
|
|
|
|
if (update && update.failedInstall !== true) { // do not announce update that failed to install before
|
|
setUpdateDescription(update.description)
|
|
setUpdateSize(`${round(update.packageSize * 0.000001, 2)}MB`)
|
|
setIsUpdateAvailable(true)
|
|
toggleUpdateModal()
|
|
log.info('OTA Update available', update, 'checkForUpdate')
|
|
}
|
|
} catch (e: any) {
|
|
return false // silent
|
|
}
|
|
|
|
}
|
|
|
|
setTimeout(() => {
|
|
if(!isInternetReachable) {
|
|
return
|
|
}
|
|
checkForUpdate()
|
|
}, 100)
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
const handleBinaryVersionMismatchCallback = function(update: RemotePackage) {
|
|
log.info('[handleBinaryVersionMismatchCallback] triggered', NATIVE_VERSION_ANDROID, update)
|
|
// setIsNativeUpdateAvailable(true)
|
|
// toggleUpdateModal()
|
|
}
|
|
|
|
// On app start
|
|
useEffect(() => {
|
|
const getInitialData = async () => {
|
|
// get deeplink data if any
|
|
const url = await Linking.getInitialURL()
|
|
|
|
if (url) {
|
|
handleDeeplink({url})
|
|
return // skip further processing so that it does not slow down or clash deep link
|
|
}
|
|
|
|
if (nwcStore.all.length > 0) {
|
|
nwcStore.resetDailyLimits()
|
|
}
|
|
|
|
if(!isInternetReachable) { return }
|
|
|
|
if(groupedMints.length === 0) {
|
|
await addMint()
|
|
}
|
|
|
|
// check lnaddress claims on app start and set timestamp to trigger focus updates
|
|
WalletTask.handleClaim().catch(e => setInfo(e.message))
|
|
// Auto-recover inflight proofs - do only on startup and before checkPendingReceived to prevent conflicts
|
|
WalletTask.handleInFlight().catch(e => false)
|
|
// Create websocket subscriptions to receive tokens or payment requests by NOSTR DMs
|
|
WalletTask.receiveEventsFromRelays().catch(e => false)
|
|
// Get exchange rate
|
|
if(userSettingsStore.exchangeCurrency) {
|
|
walletStore.refreshExchangeRate(userSettingsStore.exchangeCurrency!)
|
|
}
|
|
// Set wallet tab to preferred unit
|
|
const preferredUnit: MintUnit = userSettingsStore.preferredUnit
|
|
const preferredTabIndex = routes.findIndex(route => route.key === preferredUnit)
|
|
if(tabIndex !== preferredTabIndex) {
|
|
onTabChange(preferredTabIndex)
|
|
}
|
|
|
|
// Create websocket subscriptions to receive NWC requests from remote wallets (if any)
|
|
// go through websockets only if remote notifications not working as push data messages are
|
|
// delivered even if notifications are disabled on device
|
|
const isRemoteDataPushEnabled = walletProfileStore.device ? true : false
|
|
if(!isRemoteDataPushEnabled) {nwcStore.receiveNwcEvents()}
|
|
}
|
|
|
|
Linking.addEventListener('url', handleDeeplink)
|
|
getInitialData()
|
|
|
|
return () => {}
|
|
}, [])
|
|
|
|
|
|
const onTabChange = (tabIndex: number) => {
|
|
if(groupedMints.length === 0) {
|
|
return
|
|
}
|
|
|
|
const currentUnit = routes[tabIndex].key
|
|
log.trace('[onTabChange] currentUnit', currentUnit)
|
|
|
|
setCurrentUnit(currentUnit)
|
|
setTabIndex(tabIndex)
|
|
|
|
const preferredUnit = userSettingsStore.preferredUnit
|
|
if(currentUnit !== preferredUnit) { // prevents db write on first load
|
|
userSettingsStore.setPreferredUnit(currentUnit)
|
|
}
|
|
}
|
|
|
|
|
|
const handleDeeplink = async function ({url}: {url: string}) {
|
|
try {
|
|
|
|
const incomingData = IncomingParser.findAndExtract(url)
|
|
await IncomingParser.navigateWithIncomingData(
|
|
incomingData,
|
|
navigation,
|
|
currentUnit
|
|
)
|
|
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
}
|
|
}
|
|
|
|
|
|
/* const handleClipboard = function (clipboard: string) {
|
|
log.trace('clipboard', clipboard, 'handleClipboard')
|
|
}*/
|
|
|
|
|
|
const gotoUpdate = function() {
|
|
navigation.navigate('SettingsNavigator', {screen: 'Update', params: {
|
|
isNativeUpdateAvailable,
|
|
isUpdateAvailable,
|
|
updateDescription,
|
|
updateSize
|
|
}, initial: false})
|
|
}
|
|
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
if(!isInternetReachable) {
|
|
return
|
|
}
|
|
|
|
const nowInSec = getUnixTime(new Date())
|
|
// log.trace('[useFocusEffect]', {nowInSec, lastClaimCheck, delay: lastClaimCheck ? nowInSec - lastClaimCheck : undefined})
|
|
|
|
if(lastPendingCheck && nowInSec - lastPendingCheck > 10) {
|
|
WalletTask.syncPendingStateWithMints().catch(e => false)
|
|
WalletTask.handlePendingTopups().catch(e => false)
|
|
} else {
|
|
log.trace('[useFocusEffect] Skipping pending checks...')
|
|
}
|
|
|
|
if(lastClaimCheck && nowInSec - lastClaimCheck > 60) {
|
|
setLastClaimCheck(nowInSec)
|
|
WalletTask.handleClaim().catch(e => false)
|
|
|
|
if(userSettingsStore.exchangeCurrency) {
|
|
walletStore.refreshExchangeRate(
|
|
userSettingsStore.exchangeCurrency!
|
|
)
|
|
}
|
|
} else {
|
|
log.trace('[useFocusEffect] Skipping claim and rate checks...')
|
|
}
|
|
|
|
|
|
}, [lastClaimCheck, isInternetReachable, userSettingsStore.exchangeCurrency])
|
|
)
|
|
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
if (!route.params?.scannedMintUrl) {
|
|
return
|
|
}
|
|
|
|
const scannedMintUrl = route.params?.scannedMintUrl
|
|
addMint({scannedMintUrl})
|
|
|
|
}, [route.params?.scannedMintUrl])
|
|
)
|
|
|
|
|
|
useEffect(() => {
|
|
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
|
|
if (!isInternetReachable) {
|
|
relaysStore.resetStatuses()
|
|
return
|
|
}
|
|
|
|
const nowInSec = getUnixTime(new Date())
|
|
|
|
// log.trace('[appState]', { nowInSec, lastClaimCheck, delay: lastClaimCheck ? nowInSec - lastClaimCheck : undefined });
|
|
|
|
if (lastPendingCheck && nowInSec - lastPendingCheck > 10) {
|
|
WalletTask.syncPendingStateWithMints().catch(e => false)
|
|
WalletTask.handlePendingTopups().catch(e => false)
|
|
} else {
|
|
log.trace('[handleAppStateChange] Skipping pending checks...')
|
|
}
|
|
|
|
if(lastClaimCheck && nowInSec - lastClaimCheck > 60) {
|
|
setLastClaimCheck(nowInSec)
|
|
WalletTask.handleClaim().catch(e => false)
|
|
|
|
if(userSettingsStore.exchangeCurrency) {
|
|
walletStore.refreshExchangeRate(
|
|
userSettingsStore.exchangeCurrency!
|
|
)
|
|
}
|
|
} else {
|
|
log.trace('[handleAppStateChange] Skipping claim and rate checks...')
|
|
}
|
|
|
|
// calls receiveEventsFromRelays if re-connects
|
|
NostrClient.reconnectToRelays().catch(e => false);
|
|
}
|
|
appState.current = nextAppState;
|
|
};
|
|
|
|
const subscription = AppState.addEventListener('change', handleAppStateChange);
|
|
|
|
return () => {
|
|
subscription.remove(); // Ensure cleanup to avoid multiple listeners
|
|
};
|
|
}, [lastClaimCheck, lastPendingCheck, isInternetReachable, userSettingsStore.exchangeCurrency])
|
|
|
|
|
|
const toggleMintsModal = () => {
|
|
setIsMintsModalVisible(previousState => !previousState)
|
|
}
|
|
|
|
|
|
const toggleUpdateModal = () => {
|
|
setIsUpdateModalVisible(previousState => !previousState)
|
|
}
|
|
|
|
|
|
const toggleSendModal = () => {
|
|
setIsSendModalVisible(previousState => !previousState)
|
|
}
|
|
|
|
|
|
const toggleReceiveModal = () => {
|
|
setIsReceiveModalVisible(previousState => !previousState)
|
|
}
|
|
|
|
const addMint = async function ({scannedMintUrl = ''} = {}) {
|
|
// necessary
|
|
navigation.setParams({scannedMintUrl: undefined})
|
|
|
|
const newMintUrl = scannedMintUrl || defaultMintUrl
|
|
|
|
log.trace('newMintUrl', newMintUrl)
|
|
|
|
if (mintsStore.alreadyExists(newMintUrl)) {
|
|
const msg = translate('walletScreen.mintExists')
|
|
log.info(msg)
|
|
setInfo(msg)
|
|
return
|
|
}
|
|
|
|
try {
|
|
setIsLoading(true)
|
|
await mintsStore.addMint(newMintUrl)
|
|
} catch (e: any) {
|
|
handleError(e)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
const gotoScan = function () {
|
|
navigation.navigate('Scan', {unit: currentUnit})
|
|
}
|
|
|
|
const gotoTokenReceive = async function () {
|
|
toggleReceiveModal()
|
|
navigation.navigate('TokenReceive', {unit: currentUnit})
|
|
}
|
|
|
|
const gotoTranDetail = function (id: number) {
|
|
// navigation.navigate('TranDetail', {id})
|
|
navigation.navigate('TransactionsNavigator', {screen: 'TranDetail', params: {id}, initial: false})
|
|
}
|
|
|
|
const gotoPaymentRequests = function () {
|
|
navigation.navigate('PaymentRequests')
|
|
}
|
|
|
|
const gotoProfile = function () {
|
|
navigation.navigate('ContactsNavigator', {screen: 'Profile', params: {}, initial: false})
|
|
}
|
|
|
|
const gotoMintInfo = function (mintUrl: string) {
|
|
setIsMintsModalVisible(false)
|
|
navigation.navigate('SettingsNavigator', {
|
|
screen: 'MintInfo',
|
|
params: {mintUrl},
|
|
initial: false
|
|
})
|
|
}
|
|
|
|
const gotoTopup = function (mintUrl?: string) {
|
|
if(mintUrl) {
|
|
setIsMintsModalVisible(false)
|
|
navigation.navigate('Topup', {
|
|
mintUrl,
|
|
unit: currentUnit
|
|
})
|
|
} else {
|
|
setIsReceiveModalVisible(false)
|
|
navigation.navigate('Topup', {
|
|
unit: currentUnit
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
const gotoLightningPay = function (mintUrl?: string) {
|
|
log.trace({mintUrl})
|
|
if(mintUrl) {
|
|
setIsMintsModalVisible(false)
|
|
navigation.navigate('LightningPay', {
|
|
mintUrl,
|
|
unit: currentUnit
|
|
})
|
|
} else {
|
|
setIsSendModalVisible(false)
|
|
navigation.navigate('LightningPay', {
|
|
unit: currentUnit
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
const gotoSend = function () {
|
|
// log.trace('[gotoSend]', {currentUnit})
|
|
setIsSendModalVisible(false)
|
|
navigation.navigate('Send', {
|
|
unit: currentUnit
|
|
})
|
|
}
|
|
|
|
const handleError = function (e: AppError) {
|
|
setIsLoading(false)
|
|
setError(e)
|
|
}
|
|
|
|
|
|
const renderUnitTabs = function ({ route }: { route: { key: string } }) {
|
|
const unitMints = groupedMints.find((mintUnit) => mintUnit.unit === route.key)
|
|
|
|
// log.trace('[renderUnitTabs]', {recentByUnit: transactionsStore.getRecentByUnit(unitMints!.unit), unit: unitMints!.unit})
|
|
|
|
if (unitMints) {
|
|
return (
|
|
<>
|
|
<View style={[
|
|
$headerContainer, {
|
|
backgroundColor: headerBg,
|
|
paddingTop: spacing.small,
|
|
}
|
|
]}>
|
|
<UnitBalanceBlock
|
|
unitBalance={balances.unitBalances.find((balance) => balance.unit === unitMints.unit)!}
|
|
/>
|
|
<Pressable
|
|
onPress={toggleMintsModal}
|
|
>
|
|
<MintsByUnitSummary
|
|
mintsByUnit={unitMints}
|
|
navigation={navigation}
|
|
/>
|
|
</Pressable>
|
|
</View>
|
|
<View style={$tabContainer}>
|
|
{transactionsStore.getRecentByUnit(unitMints.unit).length > 0 ? (
|
|
<Card
|
|
ContentComponent={
|
|
<FlatList
|
|
data={transactionsStore.getRecentByUnit(unitMints.unit) as Transaction[]}
|
|
renderItem={({item, index}) => {
|
|
return (
|
|
<TransactionListItem
|
|
key={item.id}
|
|
transaction={item}
|
|
isFirst={index === 0}
|
|
isTimeAgoVisible={true}
|
|
gotoTranDetail={() => gotoTranDetail(item.id!)}
|
|
/>
|
|
)}
|
|
}
|
|
/>
|
|
}
|
|
style={[$card, {paddingVertical: spacing.extraSmall}]}
|
|
/>
|
|
) : (
|
|
<Card
|
|
ContentComponent={
|
|
<ListItem
|
|
leftIcon='faArrowTurnDown'
|
|
leftIconColor={colors.palette.green400}
|
|
text='Start by funding your wallet'
|
|
textStyle={{fontSize: moderateScale(14)}}
|
|
RightComponent={
|
|
<View style={$rightContainer}>
|
|
<Button
|
|
preset='secondary'
|
|
text={`Topup`}
|
|
onPress={() => gotoTopup()}
|
|
/>
|
|
</View>
|
|
}
|
|
/>
|
|
}
|
|
style={[$card, {paddingTop: spacing.extraSmall, minHeight: 80}]}
|
|
/>
|
|
)}
|
|
</View>
|
|
</>
|
|
)
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const tabWidth = moderateScale(75)
|
|
|
|
const getActiveTabColor = (state: NavigationState<Route>) => {
|
|
/* if(state && state.routes.length > 0) {
|
|
switch (state.routes[tabIndex].key) {
|
|
case 'usd':
|
|
return useThemeColor('usd')
|
|
case 'eur':
|
|
return useThemeColor('eur')
|
|
default:
|
|
return useThemeColor('btc')
|
|
}
|
|
}*/
|
|
|
|
return useThemeColor('headerTitle')
|
|
}
|
|
|
|
const renderTabBar = (props: any) => {
|
|
return(
|
|
<View style={{backgroundColor: headerBg, marginTop: -spacing.small}}>
|
|
<View style={{width: routes.length * tabWidth, alignSelf: 'center', backgroundColor: headerBg}}>
|
|
<TabBar
|
|
{...props}
|
|
tabStyle={{width: tabWidth}}
|
|
renderLabel={({ route, focused, color }) => (
|
|
<CurrencySign
|
|
mintUnit={route.key as MintUnit}
|
|
textStyle={{color: 'white'}}
|
|
containerStyle={focused ? {} : {opacity: 0.5}}
|
|
/>
|
|
)}
|
|
indicatorStyle={{backgroundColor: getActiveTabColor(props.navigationState)}}
|
|
style={{backgroundColor: headerBg, shadowColor: 'transparent'}}
|
|
/>
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const headerBg = useThemeColor('header')
|
|
const balances = proofsStore.getBalances()
|
|
const screenBg = useThemeColor('background')
|
|
const mainButtonIcon = useThemeColor('mainButtonIcon')
|
|
const mainButtonColor = useThemeColor('card')
|
|
const label = useThemeColor('textDim')
|
|
const headerTitle = useThemeColor('headerTitle')
|
|
|
|
const isNwcVisible = nwcStore.all.some(c => c.remainingDailyLimit !== c.dailyLimit)
|
|
const nwcCardsData = nwcStore.all.filter(c => c.remainingDailyLimit !== c.dailyLimit)
|
|
|
|
return (
|
|
<Screen contentContainerStyle={$screen}>
|
|
<Header
|
|
LeftActionComponent={<LeftProfileHeader
|
|
gotoProfile={gotoProfile}
|
|
isAvatarVisible={false}
|
|
/>}
|
|
TitleActionComponent={!isInternetReachable ? (
|
|
<Text
|
|
tx={'common.offline'}
|
|
style={$offline}
|
|
size='xxs'
|
|
/>
|
|
) : ( undefined )
|
|
}
|
|
RightActionComponent={
|
|
<>
|
|
{paymentRequestsStore.countNotExpired > 0 && (
|
|
<Pressable
|
|
style={{flexDirection: 'row', alignItems:'center', marginRight: spacing.medium}}
|
|
onPress={() => gotoPaymentRequests()}
|
|
>
|
|
<Icon icon='faPaperPlane' color={headerTitle}/>
|
|
<Text text={`${paymentRequestsStore.countNotExpired}`} style={{color: headerTitle}} />
|
|
</Pressable>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
{groupedMints.length > 0 && (
|
|
<TabView
|
|
renderTabBar={renderTabBar}
|
|
navigationState={{ index: tabIndex, routes }}
|
|
renderScene={renderUnitTabs}
|
|
onIndexChange={onTabChange}
|
|
initialLayout={{ width: spacing.screenWidth }}
|
|
// style={{borderWidth: 1, borderColor: 'red'}}
|
|
/>
|
|
)}
|
|
{isNwcVisible && (
|
|
<View style={$nwcContainer}>
|
|
<FlatList
|
|
data={nwcCardsData}
|
|
horizontal={true}
|
|
renderItem={({item, index}) => {
|
|
return (
|
|
<Card
|
|
HeadingComponent={<Text text={item.name} size='xs'/>}
|
|
ContentComponent={
|
|
<>
|
|
<Text text='Spent today' size='xxs' preset='formHelper' style={{color: label, overflow: 'hidden'}}/>
|
|
<CurrencyAmount
|
|
amount={item.dailyLimit - item.remainingDailyLimit}
|
|
currencyCode={CurrencyCode.SAT}
|
|
containerStyle={{marginLeft: -spacing.tiny, marginTop: spacing.small}}
|
|
size='medium'
|
|
/>
|
|
</>
|
|
}
|
|
style={{
|
|
width: spacing.screenWidth * 0.28,
|
|
marginRight: spacing.small,
|
|
}}
|
|
/>
|
|
)}
|
|
}
|
|
style={{
|
|
|
|
|
|
}}
|
|
/>
|
|
</View>
|
|
)}
|
|
<View style={[$bottomContainer]}>
|
|
<View style={$buttonContainer}>
|
|
<Button
|
|
LeftAccessory={() => (
|
|
<Icon
|
|
icon='faArrowUp'
|
|
size={spacing.medium}
|
|
color={mainButtonIcon}
|
|
/>
|
|
)}
|
|
onPress={toggleSendModal}
|
|
style={[{backgroundColor: mainButtonColor, borderWidth: 1, borderColor: screenBg}, $buttonSend]}
|
|
textStyle={{marginRight: spacing.tiny}}
|
|
preset='secondary'
|
|
tx='payCommon.send'
|
|
/>
|
|
<Button
|
|
RightAccessory={() => (
|
|
<SvgXml
|
|
width={spacing.large}
|
|
height={spacing.large}
|
|
xml={ScanIcon}
|
|
fill={mainButtonIcon}
|
|
/>
|
|
)}
|
|
onPress={gotoScan}
|
|
style={[{backgroundColor: mainButtonColor, borderWidth: 1, borderColor: screenBg}, $buttonScan]}
|
|
preset='secondary'
|
|
/>
|
|
<Button
|
|
RightAccessory={() => (
|
|
<Icon
|
|
icon='faArrowDown'
|
|
size={spacing.medium}
|
|
color={mainButtonIcon}
|
|
containerStyle={{paddingLeft: spacing.tiny}}
|
|
/>
|
|
)}
|
|
onPress={toggleReceiveModal}
|
|
tx='payCommon.receive'
|
|
style={[{backgroundColor: mainButtonColor, borderWidth: 1, borderColor: screenBg}, $buttonReceive]}
|
|
textStyle={{marginLeft: spacing.tiny}}
|
|
preset='secondary'
|
|
/>
|
|
</View>
|
|
</View>
|
|
{info && <InfoModal message={info} />}
|
|
{error && <ErrorModal error={error} />}
|
|
{isLoading && <Loading />}
|
|
<BottomModal
|
|
isVisible={isMintsModalVisible ? true : false}
|
|
style={{alignItems: 'stretch', padding: spacing.medium}}
|
|
ContentComponent={
|
|
<MintsByUnitList
|
|
mintsByUnit={groupedMints}
|
|
currentUnit={currentUnit}
|
|
onTopup={gotoTopup}
|
|
onLightningPay={gotoLightningPay}
|
|
onMintInfo={gotoMintInfo}
|
|
/>
|
|
}
|
|
onBackButtonPress={toggleMintsModal}
|
|
onBackdropPress={toggleMintsModal}
|
|
/>
|
|
<BottomModal
|
|
isVisible={isUpdateModalVisible ? true : false}
|
|
style={{alignItems: 'stretch'}}
|
|
ContentComponent={
|
|
<ListItem
|
|
LeftComponent={
|
|
<View style={{marginRight: spacing.medium}}>
|
|
<Image
|
|
source={{uri: 'https://www.minibits.cash/img/minibits_icon-192.png'}}
|
|
style={{width: 40, height: 40}}
|
|
/>
|
|
</View>
|
|
}
|
|
tx="updateModal.title"
|
|
subTx="updateModal.desc"
|
|
onPress={gotoUpdate}
|
|
/>
|
|
}
|
|
onBackButtonPress={toggleUpdateModal}
|
|
onBackdropPress={toggleUpdateModal}
|
|
/>
|
|
<BottomModal
|
|
isVisible={isSendModalVisible ? true : false}
|
|
style={{alignItems: 'stretch'}}
|
|
ContentComponent={
|
|
<>
|
|
<ListItem
|
|
leftIcon='faMoneyBill1'
|
|
tx="walletScreen.sendEcash"
|
|
subTx="walletScreen.sendEcashDesc"
|
|
onPress={gotoSend}
|
|
bottomSeparator={true}
|
|
/>
|
|
<ListItem
|
|
leftIcon='faBolt'
|
|
tx="walletScreen.payWithLightning"
|
|
subTx="walletScreen.payWithLightningDesc"
|
|
onPress={() => gotoLightningPay()}
|
|
/>
|
|
</>
|
|
}
|
|
onBackButtonPress={toggleSendModal}
|
|
onBackdropPress={toggleSendModal}
|
|
/>
|
|
<BottomModal
|
|
isVisible={isReceiveModalVisible ? true : false}
|
|
style={{alignItems: 'stretch'}}
|
|
ContentComponent={
|
|
<>
|
|
<ListItem
|
|
leftIcon='faMoneyBill1'
|
|
tx="walletScreen.receiveEcash"
|
|
subTx="walletScreen.receiveEcashDesc"
|
|
onPress={gotoTokenReceive}
|
|
bottomSeparator={true}
|
|
/>
|
|
<ListItem
|
|
leftIcon='faBolt'
|
|
tx='walletScreen.topupWithLightning'
|
|
subTx="walletScreen.topupWithLightningDesc"
|
|
onPress={() => gotoTopup()}
|
|
/>
|
|
</>
|
|
}
|
|
onBackButtonPress={toggleReceiveModal}
|
|
onBackdropPress={toggleReceiveModal}
|
|
/>
|
|
|
|
</Screen>
|
|
)
|
|
},
|
|
)
|
|
|
|
|
|
const UnitBalanceBlock = observer(function (props: {
|
|
unitBalance: UnitBalance
|
|
}) {
|
|
const {walletStore, userSettingsStore} = useStores()
|
|
const convertedBalanceColor = useThemeColor('headerSubTitle')
|
|
const {unitBalance} = props
|
|
const headerTitle = useThemeColor('headerTitle')
|
|
const balanceColor = headerTitle
|
|
|
|
const getConvertedBalance = function () {
|
|
return convertToFromSats(
|
|
unitBalance.unitBalance,
|
|
MintUnitCurrencyPairs[unitBalance.unit],
|
|
walletStore.exchangeRate!
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<CurrencyAmount
|
|
amount={unitBalance.unitBalance || 0}
|
|
mintUnit={unitBalance.unit}
|
|
symbolStyle={{display: 'none'}}
|
|
amountStyle={[$unitBalance, {color: balanceColor}]}
|
|
containerStyle={{marginTop: spacing.medium}}
|
|
/>
|
|
<View style={{height: verticalScale(40)}}>
|
|
{walletStore.exchangeRate
|
|
&& (userSettingsStore.exchangeCurrency === getCurrency(unitBalance.unit).code || unitBalance.unit === 'sat')
|
|
&& (
|
|
<CurrencyAmount
|
|
amount={getConvertedBalance() ?? 0}
|
|
currencyCode={unitBalance.unit === 'sat' ? userSettingsStore.exchangeCurrency : CurrencyCode.SAT}
|
|
symbolStyle={{color: convertedBalanceColor, marginTop: spacing.tiny, fontSize: verticalScale(10)}}
|
|
amountStyle={{color: convertedBalanceColor}}
|
|
size='small'
|
|
/>
|
|
)}
|
|
</View>
|
|
</>
|
|
)
|
|
})
|
|
|
|
|
|
const ZeroBalanceBlock = function () {
|
|
const headerBg = useThemeColor('header')
|
|
const balanceColor = 'white'
|
|
const currencyColor = colors.palette.primary200
|
|
|
|
|
|
return (
|
|
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
|
<Text
|
|
preset='heading'
|
|
style={[$unitBalance, {color: balanceColor}]}
|
|
text={'0'}
|
|
/>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
|
|
|
|
const MintsByUnitSummary = observer(function (props: {
|
|
mintsByUnit: MintsByUnit
|
|
navigation: any
|
|
}) {
|
|
const {
|
|
mintsStore,
|
|
proofsStore,
|
|
} = useStores()
|
|
|
|
const [selectedMint, setSelectedMint] = useState<Mint | undefined>(undefined)
|
|
|
|
useEffect(() => {
|
|
const setSelected = async () => {
|
|
const balance = proofsStore.getMintBalanceWithMaxBalance(props.mintsByUnit.unit)
|
|
if(!balance) return
|
|
|
|
const mint = mintsStore.findByUrl(balance?.mintUrl)
|
|
setSelectedMint(mint)
|
|
}
|
|
|
|
setSelected()
|
|
return () => {}
|
|
}, [])
|
|
|
|
const {mintsByUnit} = props
|
|
const mintsCountText = `and ${mintsByUnit.mints.length - 1} other${mintsByUnit.mints.length - 1 > 1 ? 's' : ''}`
|
|
|
|
return (
|
|
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
|
<View
|
|
style={{
|
|
marginEnd: spacing.small,
|
|
flex: 0,
|
|
borderRadius: spacing.small,
|
|
padding: spacing.extraSmall,
|
|
backgroundColor: getMintColor(mintsByUnit.unit)
|
|
}}
|
|
>
|
|
<SvgXml
|
|
width={spacing.medium}
|
|
height={spacing.medium}
|
|
xml={MintIcon}
|
|
fill='white'
|
|
/>
|
|
</View>
|
|
<Text size='xs' style={{color: 'white'}} text={`${selectedMint?.shortname} ${mintsByUnit.mints.length > 1 ? mintsCountText : ''}`}/>
|
|
</View>
|
|
)
|
|
})
|
|
|
|
|
|
const MintsByUnitList = observer(function (props: {
|
|
mintsByUnit: MintsByUnit[]
|
|
currentUnit: MintUnit
|
|
onTopup: Function
|
|
onLightningPay: Function
|
|
onMintInfo: Function
|
|
}) {
|
|
|
|
const [selectedMintUrl, setSelectedMintUrl] = useState<string | undefined>(undefined)
|
|
|
|
const onSelectedMint = function (mintUrl: string) {
|
|
if (selectedMintUrl && selectedMintUrl === mintUrl) {
|
|
LayoutAnimation.easeInEaseOut()
|
|
setSelectedMintUrl(undefined)
|
|
return
|
|
}
|
|
|
|
LayoutAnimation.easeInEaseOut()
|
|
setSelectedMintUrl(mintUrl)
|
|
}
|
|
|
|
const color = useThemeColor('textDim')
|
|
const balanceColor = useThemeColor('amount')
|
|
const {mintsByUnit} = props
|
|
const mintsByUnitCurrent = mintsByUnit.find((mintUnit) => mintUnit.unit === props.currentUnit)
|
|
|
|
return (
|
|
<>
|
|
{mintsByUnitCurrent!.mints.map((mint: Mint, index: number) => (
|
|
<View key={mint.mintUrl}>
|
|
<ListItem
|
|
text={mint.shortname}
|
|
subText={mint.hostname}
|
|
LeftComponent={
|
|
<View
|
|
style={{
|
|
marginEnd: spacing.small,
|
|
flex: 0,
|
|
borderRadius: spacing.small,
|
|
padding: spacing.extraSmall,
|
|
backgroundColor: getMintColor(mintsByUnitCurrent!.unit)
|
|
}}
|
|
>
|
|
<SvgXml
|
|
width={spacing.medium}
|
|
height={spacing.medium}
|
|
xml={MintIcon}
|
|
fill='white'
|
|
/>
|
|
</View>
|
|
}
|
|
RightComponent={
|
|
<CurrencyAmount
|
|
amount={mint.balances?.balances[mintsByUnitCurrent!.unit] || 0}
|
|
mintUnit={mintsByUnitCurrent!.unit}
|
|
size='medium'
|
|
/>
|
|
}
|
|
//topSeparator={true}
|
|
style={$item}
|
|
onPress={() => onSelectedMint(mint.mintUrl)}
|
|
topSeparator={index !== 0 ?? true}
|
|
/>
|
|
{selectedMintUrl === mint.mintUrl && (
|
|
<View style={{flexDirection: 'row', marginBottom: spacing.small, justifyContent: 'flex-start'}}>
|
|
<Button
|
|
tx="walletScreen.topup"
|
|
LeftAccessory={() => (
|
|
<Icon
|
|
icon='faPlus'
|
|
color={color}
|
|
size={spacing.small}
|
|
/>
|
|
)}
|
|
preset='secondary'
|
|
textStyle={{fontSize: 14, color}}
|
|
onPress={() => props.onTopup(mint.mintUrl)}
|
|
style={{
|
|
minHeight: verticalScale(40),
|
|
paddingVertical: verticalScale(spacing.tiny),
|
|
marginRight: spacing.small
|
|
}}
|
|
/>
|
|
<Button
|
|
tx="walletScreen.pay"
|
|
LeftAccessory={() => (
|
|
<Icon
|
|
icon='faBolt'
|
|
color={color}
|
|
size={spacing.small}
|
|
/>
|
|
)}
|
|
textStyle={{fontSize: 14, color}}
|
|
preset='secondary'
|
|
onPress={() => props.onLightningPay(mint.mintUrl)}
|
|
style={{
|
|
minHeight: verticalScale(40),
|
|
paddingVertical: verticalScale(spacing.tiny),
|
|
marginRight: spacing.small
|
|
}}
|
|
/>
|
|
<Button
|
|
tx="wallerScreen.mintButton"
|
|
LeftAccessory={() => (
|
|
<View style={{marginHorizontal: spacing.extraSmall}}>
|
|
<SvgXml
|
|
width={spacing.small}
|
|
height={spacing.small}
|
|
xml={MintIcon}
|
|
fill={color}
|
|
/>
|
|
</View>
|
|
)}
|
|
textStyle={{fontSize: 14, color}}
|
|
preset='secondary'
|
|
onPress={() => props.onMintInfo(mint.mintUrl)}
|
|
style={{
|
|
minHeight: verticalScale(40),
|
|
paddingVertical: verticalScale(spacing.tiny),
|
|
marginRight: spacing.small
|
|
}}
|
|
/>
|
|
</View>
|
|
)}
|
|
</View>
|
|
))}
|
|
</>
|
|
)
|
|
})
|
|
|
|
|
|
export const getMintColor = function (unit: MintUnit) {
|
|
if (unit === 'sat' || unit === 'msat' || unit === 'btc') {
|
|
return colors.palette.orange600
|
|
}
|
|
|
|
if (unit === 'eur') {
|
|
return colors.palette.blue600
|
|
}
|
|
|
|
if (unit === 'usd') {
|
|
return colors.palette.green400
|
|
}
|
|
}
|
|
|
|
|
|
const $screen: ViewStyle = {
|
|
flex: 1,
|
|
}
|
|
|
|
const $headerContainer: TextStyle = {
|
|
alignItems: 'center',
|
|
// padding: spacing.tiny,
|
|
height: spacing.screenHeight * 0.28,
|
|
}
|
|
|
|
const $tabContainer: TextStyle = {
|
|
marginTop: -spacing.extraLarge * 1.5,
|
|
// alignSelf: 'stretch',
|
|
// padding: spacing.extraSmall,
|
|
// flex: 1,
|
|
// paddingTop: spacing.extraSmall - 3,
|
|
// borderWidth: 1,
|
|
// borderColor: 'green',
|
|
}
|
|
|
|
const $promoContainer: TextStyle = {
|
|
marginTop: -spacing.extraLarge * 1.5,
|
|
}
|
|
|
|
const $rightContainer: ViewStyle = {
|
|
padding: spacing.extraSmall,
|
|
alignSelf: 'center',
|
|
marginLeft: spacing.small,
|
|
}
|
|
|
|
const $card: ViewStyle = {
|
|
marginBottom: spacing.small,
|
|
//paddingTop: 0,
|
|
marginHorizontal: spacing.extraSmall,
|
|
}
|
|
|
|
const $cardHeading: TextStyle = {
|
|
fontFamily: typography.primary?.medium,
|
|
fontSize: verticalScale(18),
|
|
}
|
|
|
|
const $unitBalance: TextStyle = {
|
|
fontSize: verticalScale(48),
|
|
lineHeight: verticalScale(48)
|
|
}
|
|
|
|
const $promoIconContainer: ViewStyle = {
|
|
marginTop: -spacing.large,
|
|
alignItems: 'center',
|
|
}
|
|
|
|
const $promoText: TextStyle = {
|
|
padding: spacing.small,
|
|
textAlign: 'center',
|
|
fontSize: 18,
|
|
}
|
|
|
|
const $item: ViewStyle = {
|
|
marginHorizontal: spacing.micro,
|
|
}
|
|
|
|
|
|
const $nwcContainer: ViewStyle = {
|
|
justifyContent: 'flex-start',
|
|
paddingHorizontal: spacing.extraSmall,
|
|
}
|
|
|
|
const $bottomContainer: ViewStyle = {
|
|
justifyContent: 'flex-end',
|
|
paddingHorizontal: spacing.extraSmall,
|
|
marginTop: spacing.large,
|
|
marginBottom: 0
|
|
}
|
|
|
|
const $buttonContainer: ViewStyle = {
|
|
flexDirection: 'row',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
}
|
|
|
|
const $buttonSend: ViewStyle = {
|
|
borderTopLeftRadius: verticalScale(60 / 2),
|
|
borderBottomLeftRadius: verticalScale(60 / 2),
|
|
borderTopRightRadius: 0,
|
|
borderBottomRightRadius: 0,
|
|
width: verticalScale(130),
|
|
height: verticalScale(55),
|
|
marginRight: verticalScale(-25),
|
|
}
|
|
|
|
const $buttonScan: ViewStyle = {
|
|
borderRadius: verticalScale(70 / 2),
|
|
width: verticalScale(70),
|
|
height: verticalScale(70),
|
|
zIndex: 99,
|
|
}
|
|
|
|
const $buttonReceive: ViewStyle = {
|
|
borderTopLeftRadius: 0,
|
|
borderBottomLeftRadius: 0,
|
|
borderTopRightRadius: verticalScale(30),
|
|
borderBottomRightRadius: verticalScale(30),
|
|
width: verticalScale(130),
|
|
height: verticalScale(55),
|
|
marginLeft: verticalScale(-15),
|
|
}
|
|
|
|
const $bottomModal: ViewStyle = {
|
|
alignItems: 'center',
|
|
paddingVertical: spacing.large,
|
|
paddingHorizontal: spacing.small,
|
|
}
|
|
|
|
|
|
const $offline: TextStyle = {
|
|
paddingHorizontal: spacing.small,
|
|
borderRadius: spacing.tiny,
|
|
alignSelf: 'center',
|
|
marginVertical: spacing.small,
|
|
lineHeight: spacing.medium,
|
|
backgroundColor: colors.palette.orange400,
|
|
color: 'white'
|
|
}
|