432 lines
15 KiB
TypeScript
432 lines
15 KiB
TypeScript
import * as React from 'react';
|
|
import { StyleSheet, View, ScrollView, TouchableOpacity } from 'react-native';
|
|
import { inject, observer } from 'mobx-react';
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
import { Route } from '@react-navigation/native';
|
|
|
|
import { AdditionalOutput } from '../models/TransactionRequest';
|
|
|
|
import Screen from '../components/Screen';
|
|
import Header from '../components/Header';
|
|
import KeyValue from '../components/KeyValue';
|
|
import Amount from '../components/Amount';
|
|
import SwipeButton from '../components/SwipeButton';
|
|
import Button from '../components/Button';
|
|
import Text from '../components/Text';
|
|
|
|
import { localeString } from '../utils/LocaleUtils';
|
|
import { themeColor } from '../utils/ThemeUtils';
|
|
|
|
import TransactionsStore from '../stores/TransactionsStore';
|
|
import SettingsStore from '../stores/SettingsStore';
|
|
import UnitsStore from '../stores/UnitsStore';
|
|
|
|
import QR from '../assets/images/SVG/QR.svg';
|
|
|
|
interface VerifyOnChainParams {
|
|
destination: string;
|
|
fee: string;
|
|
satAmount: string | number;
|
|
amount: string;
|
|
utxos: string[];
|
|
account: string;
|
|
additionalOutputs: AdditionalOutput[];
|
|
fundMax: boolean;
|
|
}
|
|
|
|
interface VerifyOnChainProps {
|
|
navigation: NativeStackNavigationProp<any, any>;
|
|
route: Route<'VerifyOnChain', VerifyOnChainParams>;
|
|
TransactionsStore: TransactionsStore;
|
|
SettingsStore: SettingsStore;
|
|
UnitsStore: UnitsStore;
|
|
}
|
|
|
|
interface VerifyOnChainState {
|
|
bitcoinUnits: 'sats' | 'BTC';
|
|
slideToPayThreshold: number;
|
|
}
|
|
|
|
@inject('TransactionsStore', 'SettingsStore', 'UnitsStore')
|
|
@observer
|
|
export default class VerifyOnChain extends React.Component<VerifyOnChainProps> {
|
|
state: VerifyOnChainState = {
|
|
bitcoinUnits: 'sats',
|
|
slideToPayThreshold: 10000
|
|
};
|
|
|
|
async componentDidMount() {
|
|
const { SettingsStore } = this.props;
|
|
const settings = await SettingsStore.getSettings();
|
|
this.setState({
|
|
slideToPayThreshold: settings?.payments?.slideToPayThreshold
|
|
});
|
|
}
|
|
|
|
toggleBitcoinUnits = () => {
|
|
const { UnitsStore } = this.props;
|
|
if (UnitsStore.units === 'fiat') {
|
|
UnitsStore.changeUnits();
|
|
return;
|
|
}
|
|
this.setState((prevState: VerifyOnChainState) => ({
|
|
bitcoinUnits: prevState.bitcoinUnits === 'sats' ? 'BTC' : 'sats'
|
|
}));
|
|
};
|
|
|
|
getTotalAmount = () => {
|
|
const { route } = this.props;
|
|
const { satAmount, amount, additionalOutputs } = route?.params || {};
|
|
|
|
const primary = satAmount || amount || '0';
|
|
let total = Number(primary || 0);
|
|
if (Array.isArray(additionalOutputs)) {
|
|
additionalOutputs.forEach((o) => {
|
|
total += Number(o?.satAmount || o?.amount || 0);
|
|
});
|
|
}
|
|
return total;
|
|
};
|
|
|
|
renderProminentAmount = () => {
|
|
const total = this.getTotalAmount();
|
|
|
|
return (
|
|
<View style={styles.center}>
|
|
<Amount
|
|
sats={total.toString()}
|
|
sensitive
|
|
jumboText
|
|
toggleable
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
renderTransactionSummary = () => {
|
|
const { route } = this.props;
|
|
const { additionalOutputs, utxos } = route?.params || {};
|
|
const addresses = this.getDestinationAddresses();
|
|
const utxoCount = Array.isArray(utxos) ? utxos.length : 0;
|
|
const hasMultipleOutputs =
|
|
Array.isArray(additionalOutputs) && additionalOutputs.length > 0;
|
|
|
|
return (
|
|
<View style={styles.summary}>
|
|
<Text
|
|
style={{
|
|
fontSize: 14,
|
|
color: themeColor('secondaryText'),
|
|
textAlign: 'center'
|
|
}}
|
|
>
|
|
{`${addresses.length} ${localeString(
|
|
hasMultipleOutputs
|
|
? 'general.destinations'
|
|
: 'general.destination'
|
|
)}`}
|
|
</Text>
|
|
|
|
{utxoCount > 0 && (
|
|
<Text
|
|
style={{
|
|
fontSize: 14,
|
|
color: themeColor('secondaryText'),
|
|
textAlign: 'center',
|
|
marginTop: 5
|
|
}}
|
|
>
|
|
{`${utxoCount} ${
|
|
utxoCount === 1
|
|
? localeString('general.utxo')
|
|
: localeString('general.utxos')
|
|
}`}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
onConfirm = () => {
|
|
const { navigation, TransactionsStore, SettingsStore, route } =
|
|
this.props;
|
|
const {
|
|
destination,
|
|
fee,
|
|
satAmount,
|
|
amount,
|
|
utxos,
|
|
account,
|
|
additionalOutputs,
|
|
fundMax
|
|
} = route?.params || {};
|
|
|
|
const request: any = {
|
|
addr: destination,
|
|
sat_per_vbyte: fee,
|
|
amount: (satAmount || amount)?.toString(),
|
|
spend_unconfirmed: true,
|
|
additional_outputs: additionalOutputs,
|
|
account
|
|
};
|
|
if (utxos && utxos.length > 0) request.utxos = utxos;
|
|
|
|
if (fundMax) {
|
|
if (SettingsStore.implementation === 'cln-rest') {
|
|
request.amount = 'all';
|
|
} else {
|
|
if (request.amount) delete request.amount;
|
|
request.send_all = true;
|
|
}
|
|
}
|
|
|
|
TransactionsStore.sendCoins(request);
|
|
navigation.navigate('SendingOnChain');
|
|
};
|
|
|
|
renderOutputs = (destination: string, primaryAmount: string | number) => {
|
|
const { route } = this.props;
|
|
const { bitcoinUnits } = this.state;
|
|
const { additionalOutputs } = route?.params || {};
|
|
const hasAdditional =
|
|
Array.isArray(additionalOutputs) && additionalOutputs.length > 0;
|
|
return (
|
|
<View style={{ marginTop: 10 }}>
|
|
<View style={{ marginTop: 10 }}>
|
|
<KeyValue
|
|
keyValue={
|
|
hasAdditional
|
|
? `${localeString('general.destination')} #1`
|
|
: localeString('general.destination')
|
|
}
|
|
value={destination}
|
|
disableCopy={false}
|
|
/>
|
|
{hasAdditional && (
|
|
<TouchableOpacity onPress={this.toggleBitcoinUnits}>
|
|
<KeyValue
|
|
keyValue={localeString('views.Send.amount')}
|
|
value={
|
|
bitcoinUnits === 'sats' ? (
|
|
<Amount
|
|
fixedUnits="sats"
|
|
sats={primaryAmount}
|
|
/>
|
|
) : (
|
|
<Amount
|
|
fixedUnits="BTC"
|
|
sats={primaryAmount}
|
|
/>
|
|
)
|
|
}
|
|
disableCopy
|
|
/>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
{Array.isArray(additionalOutputs) &&
|
|
additionalOutputs.map((o: any, idx: number) => (
|
|
<View key={`output-${idx}`} style={{ marginTop: 10 }}>
|
|
<KeyValue
|
|
keyValue={`${localeString(
|
|
'general.destination'
|
|
)} #${idx + 2}`}
|
|
value={o?.address}
|
|
disableCopy={false}
|
|
/>
|
|
<TouchableOpacity onPress={this.toggleBitcoinUnits}>
|
|
<KeyValue
|
|
keyValue={localeString('views.Send.amount')}
|
|
value={
|
|
bitcoinUnits === 'sats' ? (
|
|
<Amount
|
|
fixedUnits="sats"
|
|
sats={
|
|
o?.satAmount ||
|
|
o?.amount ||
|
|
'0'
|
|
}
|
|
/>
|
|
) : (
|
|
<Amount
|
|
fixedUnits="BTC"
|
|
sats={
|
|
o?.satAmount ||
|
|
o?.amount ||
|
|
'0'
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
disableCopy
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
renderInputs = () => {
|
|
const { route } = this.props;
|
|
const { utxos } = route?.params || {};
|
|
if (!Array.isArray(utxos) || utxos.length === 0) return null;
|
|
return (
|
|
<View style={{ marginTop: 10 }}>
|
|
<KeyValue
|
|
keyValue={localeString('general.utxos')}
|
|
value={utxos.length}
|
|
/>
|
|
{utxos.map((u: string, idx: number) => (
|
|
<KeyValue
|
|
key={`utxo-${idx}`}
|
|
keyValue={`${localeString('views.PSBT.input')} #${
|
|
idx + 1
|
|
}`}
|
|
value={u}
|
|
disableCopy={false}
|
|
/>
|
|
))}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
getDestinationAddresses = () => {
|
|
const { route } = this.props;
|
|
const { destination, additionalOutputs } = route?.params || {};
|
|
|
|
const addresses: string[] = [];
|
|
if (destination) {
|
|
addresses.push(destination);
|
|
}
|
|
if (Array.isArray(additionalOutputs)) {
|
|
additionalOutputs.forEach((output) => {
|
|
if (output?.address) {
|
|
addresses.push(output.address);
|
|
}
|
|
});
|
|
}
|
|
return addresses;
|
|
};
|
|
|
|
renderQRButton = () => {
|
|
const { navigation } = this.props;
|
|
const addresses = this.getDestinationAddresses();
|
|
|
|
if (!addresses || addresses.length === 0) {
|
|
return <View />;
|
|
}
|
|
|
|
const addPrefixToAddresses = (addresses: string[], prefix: string) =>
|
|
addresses.filter(Boolean).map((address) => `${prefix}${address}`);
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
navigation.navigate('MultiQR', {
|
|
addressData: addPrefixToAddresses(addresses, 'bitcoin:')
|
|
});
|
|
}}
|
|
>
|
|
<QR fill={themeColor('text')} style={{ alignSelf: 'center' }} />
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
render() {
|
|
const { navigation, route } = this.props;
|
|
const { destination, fee, satAmount, amount, account, fundMax } =
|
|
route?.params;
|
|
const { slideToPayThreshold } = this.state;
|
|
|
|
const totalAmount = this.getTotalAmount();
|
|
const shouldUseSwipeButton = totalAmount >= slideToPayThreshold;
|
|
|
|
return (
|
|
<Screen>
|
|
<Header
|
|
leftComponent="Back"
|
|
centerComponent={{
|
|
text: localeString('views.VerifyOnChain.title'),
|
|
style: {
|
|
color: themeColor('text'),
|
|
fontFamily: 'PPNeueMontreal-Book'
|
|
}
|
|
}}
|
|
rightComponent={this.renderQRButton()}
|
|
navigation={navigation}
|
|
/>
|
|
|
|
{this.renderProminentAmount()}
|
|
{this.renderTransactionSummary()}
|
|
|
|
<ScrollView contentContainerStyle={styles.content}>
|
|
{this.renderOutputs(
|
|
destination,
|
|
fundMax ? '0' : satAmount || amount || '0'
|
|
)}
|
|
|
|
<KeyValue
|
|
keyValue={localeString('views.Send.feeSatsVbyte')}
|
|
value={fee}
|
|
/>
|
|
|
|
{this.renderInputs()}
|
|
|
|
{account && account !== 'default' && (
|
|
<KeyValue
|
|
keyValue={localeString('general.account')}
|
|
value={account}
|
|
/>
|
|
)}
|
|
</ScrollView>
|
|
<View style={{ bottom: 10 }}>
|
|
{shouldUseSwipeButton ? (
|
|
<SwipeButton
|
|
onSwipeSuccess={this.onConfirm}
|
|
instructionText={localeString(
|
|
'views.PaymentRequest.slideToPay'
|
|
)}
|
|
containerStyle={{
|
|
backgroundColor: themeColor('secondaryText')
|
|
}}
|
|
swipeButtonStyle={{
|
|
backgroundColor: themeColor('text')
|
|
}}
|
|
/>
|
|
) : (
|
|
<View style={styles.button}>
|
|
<Button
|
|
title={localeString('views.Send.sendCoins')}
|
|
icon={{
|
|
name: 'send',
|
|
size: 25
|
|
}}
|
|
onPress={this.onConfirm}
|
|
/>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</Screen>
|
|
);
|
|
}
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
content: {
|
|
paddingHorizontal: 15
|
|
},
|
|
button: {
|
|
paddingHorizontal: 20
|
|
},
|
|
center: {
|
|
alignItems: 'center',
|
|
paddingVertical: 15
|
|
},
|
|
summary: {
|
|
alignItems: 'center',
|
|
paddingHorizontal: 20,
|
|
paddingBottom: 15
|
|
}
|
|
});
|