Wallet implementation (#92)
* Created walletAddress and walletBalance components * Added walletSend and transaction list components. * added transaction history page
This commit is contained in:
parent
351e39826b
commit
3706bc5936
31 changed files with 845 additions and 15 deletions
BIN
app/src/assets/stripe@2x.png
Normal file
BIN
app/src/assets/stripe@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -1,10 +1,12 @@
|
|||
import React from 'react';
|
||||
import AboutPage from '../page/about';
|
||||
import DiscoverPage from '../page/discover';
|
||||
import FilePage from '../page/file';
|
||||
import SearchPage from '../page/search';
|
||||
import SettingsPage from '../page/settings';
|
||||
import AboutPage from '../page/about';
|
||||
import SplashScreen from '../page/splash';
|
||||
import TransactionHistoryPage from '../page/transactionHistory';
|
||||
import WalletPage from '../page/wallet';
|
||||
import SearchInput from '../component/searchInput';
|
||||
import {
|
||||
addNavigationHelpers,
|
||||
|
@ -19,9 +21,10 @@ import {
|
|||
AsyncStorage,
|
||||
BackHandler,
|
||||
NativeModules,
|
||||
TextInput
|
||||
TextInput,
|
||||
ToastAndroid
|
||||
} from 'react-native';
|
||||
import { SETTINGS } from 'lbry-redux';
|
||||
import { SETTINGS, doHideNotification, selectNotification } from 'lbry-redux';
|
||||
import { makeSelectClientSetting } from '../redux/selectors/settings';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import discoverStyle from '../styles/discover';
|
||||
|
@ -55,8 +58,29 @@ const discoverStack = StackNavigator({
|
|||
headerMode: 'screen',
|
||||
});
|
||||
|
||||
const walletStack = StackNavigator({
|
||||
Wallet: {
|
||||
screen: WalletPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Wallet',
|
||||
headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
|
||||
headerRight: <Feather name="search" size={24} style={discoverStyle.rightHeaderIcon} onPress={() => navigation.navigate('Search')} />
|
||||
})
|
||||
},
|
||||
TransactionHistory: {
|
||||
screen: TransactionHistoryPage,
|
||||
navigationOptions: {
|
||||
title: 'Transaction History',
|
||||
drawerLockMode: 'locked-closed'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
headerMode: 'screen'
|
||||
});
|
||||
|
||||
const drawer = DrawerNavigator({
|
||||
Discover: { screen: discoverStack },
|
||||
Wallet: { screen: walletStack },
|
||||
Settings: { screen: SettingsPage, navigationOptions: { drawerLockMode: 'locked-closed' } },
|
||||
About: { screen: AboutPage, navigationOptions: { drawerLockMode: 'locked-closed' } }
|
||||
}, {
|
||||
|
@ -79,6 +103,8 @@ export const AppNavigator = new StackNavigator({
|
|||
});
|
||||
|
||||
class AppWithNavigationState extends React.Component {
|
||||
static supportedDisplayTypes = ['toast'];
|
||||
|
||||
componentWillMount() {
|
||||
AppState.addEventListener('change', this._handleAppStateChange);
|
||||
BackHandler.addEventListener('hardwareBackPress', function() {
|
||||
|
@ -106,7 +132,33 @@ class AppWithNavigationState extends React.Component {
|
|||
AppState.removeEventListener('change', this._handleAppStateChange);
|
||||
BackHandler.removeEventListener('hardwareBackPress');
|
||||
}
|
||||
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
const { notification } = nextProps;
|
||||
if (notification) {
|
||||
const { displayType, message } = notification;
|
||||
let currentDisplayType;
|
||||
if (displayType.length) {
|
||||
for (let i = 0; i < displayType.length; i++) {
|
||||
const type = displayType[i];
|
||||
if (AppWithNavigationState.supportedDisplayTypes.indexOf(type) > -1) {
|
||||
currentDisplayType = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (AppWithNavigationState.supportedDisplayTypes.indexOf(displayType) > -1) {
|
||||
currentDisplayType = displayType;
|
||||
}
|
||||
|
||||
if ('toast' === currentDisplayType) {
|
||||
ToastAndroid.show(message, ToastAndroid.SHORT);
|
||||
}
|
||||
|
||||
dispatch(doHideNotification());
|
||||
}
|
||||
}
|
||||
|
||||
_handleAppStateChange = (nextAppState) => {
|
||||
// Check if the app was suspended
|
||||
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
|
||||
|
@ -136,8 +188,9 @@ class AppWithNavigationState extends React.Component {
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
nav: state.nav,
|
||||
notification: selectNotification(state),
|
||||
keepDaemonRunning: makeSelectClientSetting(SETTINGS.KEEP_DAEMON_RUNNING)(state),
|
||||
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(AppWithNavigationState);
|
||||
export default connect(mapStateToProps)(AppWithNavigationState);
|
||||
|
|
7
app/src/component/address/index.js
Normal file
7
app/src/component/address/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doNotify } from 'lbry-redux';
|
||||
import Address from './view';
|
||||
|
||||
export default connect(null, {
|
||||
doNotify,
|
||||
})(Address);
|
29
app/src/component/address/view.js
Normal file
29
app/src/component/address/view.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Clipboard, Text, View } from 'react-native';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
address: string,
|
||||
doNotify: ({ message: string, displayType: Array<string> }) => void,
|
||||
};
|
||||
|
||||
export default class Address extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { address, doNotify, style } = this.props;
|
||||
|
||||
return (
|
||||
<View style={[walletStyle.row, style]}>
|
||||
<Text selectable={true} numberOfLines={1} style={walletStyle.address}>{address || ''}</Text>
|
||||
<Button icon={'clipboard'} style={walletStyle.button} onPress={() => {
|
||||
Clipboard.setString(address);
|
||||
doNotify({
|
||||
message: 'Address copied',
|
||||
displayType: ['toast'],
|
||||
});
|
||||
}} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
4
app/src/component/button/index.js
Normal file
4
app/src/component/button/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Button from './view';
|
||||
|
||||
export default connect(null, null)(Button);
|
39
app/src/component/button/view.js
Normal file
39
app/src/component/button/view.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import buttonStyle from '../../styles/button';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
export default class Button extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const {
|
||||
disabled,
|
||||
style,
|
||||
text,
|
||||
icon,
|
||||
onPress
|
||||
} = this.props;
|
||||
|
||||
let styles = [buttonStyle.button, buttonStyle.row];
|
||||
if (style.length) {
|
||||
styles = styles.concat(style);
|
||||
} else {
|
||||
styles.push(style);
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
styles.push(buttonStyle.disabled);
|
||||
}
|
||||
|
||||
const textStyles = [buttonStyle.text];
|
||||
if (icon && icon.trim().length > 0) {
|
||||
textStyles.push(buttonStyle.textWithIcon);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity disabled={disabled} style={styles} onPress={onPress}>
|
||||
{icon && <Icon name={icon} size={18} color='#ffffff' class={buttonStyle.icon} /> }
|
||||
{text && (text.trim().length > 0) && <Text style={textStyles}>{text}</Text>}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
};
|
13
app/src/component/transactionList/index.js
Normal file
13
app/src/component/transactionList/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
//import { selectClaimedRewardsByTransactionId } from 'redux/selectors/rewards';
|
||||
import { selectAllMyClaimsByOutpoint } from 'lbry-redux';
|
||||
import TransactionList from './view';
|
||||
|
||||
const select = state => ({
|
||||
//rewards: selectClaimedRewardsByTransactionId(state),
|
||||
myClaims: selectAllMyClaimsByOutpoint(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(select, perform)(TransactionList);
|
|
@ -0,0 +1,45 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { formatCredits } from 'lbry-redux';
|
||||
import moment from 'moment';
|
||||
import transactionListStyle from '../../../styles/transactionList';
|
||||
|
||||
class TransactionListItem extends React.PureComponent {
|
||||
capitalize(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { transaction } = this.props;
|
||||
const { amount, claim_id: claimId, claim_name: name, date, fee, txid, type } = transaction;
|
||||
|
||||
return (
|
||||
<View style={transactionListStyle.listItem}>
|
||||
<View style={[transactionListStyle.row, transactionListStyle.topRow]}>
|
||||
<View style={transactionListStyle.col}>
|
||||
<Text style={transactionListStyle.text}>{this.capitalize(type)}</Text>
|
||||
</View>
|
||||
<View style={transactionListStyle.col}>
|
||||
<Text style={[transactionListStyle.amount, transactionListStyle.text]}>{formatCredits(amount, 8)}</Text>
|
||||
{ fee !== 0 && (<Text style={[transactionListStyle.amount, transactionListStyle.text]}>fee {formatCredits(fee, 8)}</Text>) }
|
||||
</View>
|
||||
</View>
|
||||
<View style={transactionListStyle.row}>
|
||||
<View style={transactionListStyle.col}>
|
||||
<Text style={[transactionListStyle.smallText, transactionListStyle.txid]}>{txid.substring(0, 8)}</Text>
|
||||
</View>
|
||||
<View style={transactionListStyle.col}>
|
||||
{date ? (
|
||||
<Text style={transactionListStyle.smallText}>{moment(date).format('MMM D')}</Text>
|
||||
) : (
|
||||
<Text style={transactionListStyle.smallText}>Pending</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionListItem;
|
69
app/src/component/transactionList/view.js
Normal file
69
app/src/component/transactionList/view.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import TransactionListItem from './internal/transaction-list-item';
|
||||
import transactionListStyle from '../../styles/transactionList';
|
||||
|
||||
export type Transaction = {
|
||||
amount: number,
|
||||
claim_id: string,
|
||||
claim_name: string,
|
||||
fee: number,
|
||||
nout: number,
|
||||
txid: string,
|
||||
type: string,
|
||||
date: Date,
|
||||
};
|
||||
|
||||
class TransactionList extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filter: 'all',
|
||||
};
|
||||
|
||||
(this: any).handleFilterChanged = this.handleFilterChanged.bind(this);
|
||||
(this: any).filterTransaction = this.filterTransaction.bind(this);
|
||||
}
|
||||
|
||||
handleFilterChanged(event: React.SyntheticInputEvent<*>) {
|
||||
this.setState({
|
||||
filter: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
filterTransaction(transaction: Transaction) {
|
||||
const { filter } = this.state;
|
||||
|
||||
return filter === 'all' || filter === transaction.type;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { emptyMessage, rewards, transactions } = this.props;
|
||||
const { filter } = this.state;
|
||||
const transactionList = transactions.filter(this.filterTransaction);
|
||||
|
||||
return (
|
||||
<View>
|
||||
{!transactionList.length && (
|
||||
<Text style={transactionListStyle.noTransactions}>{emptyMessage || 'No transactions to list.'}</Text>
|
||||
)}
|
||||
|
||||
{!!transactionList.length && (
|
||||
<View>
|
||||
{transactionList.map(t => (
|
||||
<TransactionListItem
|
||||
key={`${t.txid}:${t.nout}`}
|
||||
transaction={t}
|
||||
reward={rewards && rewards[t.txid]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionList;
|
20
app/src/component/transactionListRecent/index.js
Normal file
20
app/src/component/transactionListRecent/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchTransactions,
|
||||
selectRecentTransactions,
|
||||
selectHasTransactions,
|
||||
selectIsFetchingTransactions,
|
||||
} from 'lbry-redux';
|
||||
import TransactionListRecent from './view';
|
||||
|
||||
const select = state => ({
|
||||
fetchingTransactions: selectIsFetchingTransactions(state),
|
||||
transactions: selectRecentTransactions(state),
|
||||
hasTransactions: selectHasTransactions(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchTransactions: () => dispatch(doFetchTransactions()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(TransactionListRecent);
|
46
app/src/component/transactionListRecent/view.js
Normal file
46
app/src/component/transactionListRecent/view.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
//import BusyIndicator from 'component/common/busy-indicator';
|
||||
import { Text, View } from 'react-native';
|
||||
import Button from '../button';
|
||||
import TransactionList from '../transactionList';
|
||||
import type { Transaction } from '../transactionList/view';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
fetchTransactions: () => void,
|
||||
fetchingTransactions: boolean,
|
||||
hasTransactions: boolean,
|
||||
transactions: Array<Transaction>,
|
||||
};
|
||||
|
||||
class TransactionListRecent extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchTransactions();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fetchingTransactions, hasTransactions, transactions, navigation } = this.props;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.transactionsCard}>
|
||||
<View style={[walletStyle.row, walletStyle.transactionsHeader]}>
|
||||
<Text style={walletStyle.transactionsTitle}>Recent Transactions</Text>
|
||||
<Button style={walletStyle.button} text={'View All'}
|
||||
onPress={() => navigation.navigate('TransactionHistory')} />
|
||||
</View>
|
||||
{fetchingTransactions && (
|
||||
<Text style={walletStyle.infoText}>Fetching transactions...</Text>
|
||||
)}
|
||||
{!fetchingTransactions && (
|
||||
<TransactionList
|
||||
transactions={transactions}
|
||||
emptyMessage={"Looks like you don't have any recent transactions."}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionListRecent;
|
20
app/src/component/walletAddress/index.js
Normal file
20
app/src/component/walletAddress/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doCheckAddressIsMine,
|
||||
doGetNewAddress,
|
||||
selectReceiveAddress,
|
||||
selectGettingNewAddress,
|
||||
} from 'lbry-redux';
|
||||
import WalletAddress from './view';
|
||||
|
||||
const select = state => ({
|
||||
receiveAddress: selectReceiveAddress(state),
|
||||
gettingNewAddress: selectGettingNewAddress(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
checkAddressIsMine: address => dispatch(doCheckAddressIsMine(address)),
|
||||
getNewAddress: () => dispatch(doGetNewAddress()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(WalletAddress);
|
47
app/src/component/walletAddress/view.js
Normal file
47
app/src/component/walletAddress/view.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import Address from '../address';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
checkAddressIsMine: string => void,
|
||||
receiveAddress: string,
|
||||
getNewAddress: () => void,
|
||||
gettingNewAddress: boolean,
|
||||
};
|
||||
|
||||
class WalletAddress extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
const { checkAddressIsMine, receiveAddress, getNewAddress } = this.props;
|
||||
if (!receiveAddress) {
|
||||
getNewAddress();
|
||||
} else {
|
||||
checkAddressIsMine(receiveAddress);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { receiveAddress, getNewAddress, gettingNewAddress } = this.props;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.card}>
|
||||
<Text style={walletStyle.title}>Receive Credits</Text>
|
||||
<Text style={[walletStyle.text, walletStyle.bottomMarginMedium]}>Use this wallet address to receive credits sent by another user (or yourself).</Text>
|
||||
<Address address={receiveAddress} style={walletStyle.bottomMarginSmall} />
|
||||
<Button style={[walletStyle.button, walletStyle.bottomMarginLarge]}
|
||||
icon={'refresh'}
|
||||
text={'Get New Address'}
|
||||
onPress={getNewAddress}
|
||||
disabled={gettingNewAddress}
|
||||
/>
|
||||
<Text style={walletStyle.smallText}>
|
||||
You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletAddress;
|
9
app/src/component/walletBalance/index.js
Normal file
9
app/src/component/walletBalance/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectBalance } from 'lbry-redux';
|
||||
import WalletBalance from './view';
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
});
|
||||
|
||||
export default connect(select, null)(WalletBalance);
|
29
app/src/component/walletBalance/view.js
Normal file
29
app/src/component/walletBalance/view.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Image, Text, View } from 'react-native';
|
||||
import { formatCredits } from 'lbry-redux'
|
||||
import Address from '../address';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
};
|
||||
|
||||
class WalletBalance extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { balance } = this.props;
|
||||
return (
|
||||
<View style={walletStyle.balanceCard}>
|
||||
<Image style={walletStyle.balanceBackground} resizeMode={'cover'} source={require('../../assets/stripe.png')} />
|
||||
<Text style={walletStyle.balanceTitle}>Balance</Text>
|
||||
<Text style={walletStyle.balanceCaption}>You currently have</Text>
|
||||
<Text style={walletStyle.balance}>
|
||||
{(balance || balance === 0) && (formatCredits(balance, 2) + ' LBC')}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletBalance;
|
22
app/src/component/walletSend/index.js
Normal file
22
app/src/component/walletSend/index.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doNotify,
|
||||
doSendDraftTransaction,
|
||||
selectDraftTransaction,
|
||||
selectDraftTransactionError,
|
||||
selectBalance
|
||||
} from 'lbry-redux';
|
||||
import WalletSend from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
sendToAddress: (address, amount) => dispatch(doSendDraftTransaction(address, amount)),
|
||||
notify: (data) => dispatch(doNotify(data))
|
||||
});
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
draftTransaction: selectDraftTransaction(state),
|
||||
transactionError: selectDraftTransactionError(state),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(WalletSend);
|
89
app/src/component/walletSend/view.js
Normal file
89
app/src/component/walletSend/view.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { regexAddress } from 'lbry-redux';
|
||||
import { TextInput, Text, View } from 'react-native';
|
||||
import Button from '../button';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
type DraftTransaction = {
|
||||
address: string,
|
||||
amount: ?number, // So we can use a placeholder in the input
|
||||
};
|
||||
|
||||
type Props = {
|
||||
sendToAddress: (string, number) => void,
|
||||
balance: number,
|
||||
};
|
||||
|
||||
class WalletSend extends React.PureComponent<Props> {
|
||||
state = {
|
||||
amount: null,
|
||||
address: null
|
||||
};
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
const { draftTransaction, transactionError } = nextProps;
|
||||
if (transactionError && transactionError.trim().length > 0) {
|
||||
this.setState({ address: draftTransaction.address, amount: draftTransaction.amount });
|
||||
}
|
||||
}
|
||||
|
||||
handleSend = () => {
|
||||
const { balance, sendToAddress, notify } = this.props;
|
||||
const { address, amount } = this.state;
|
||||
if (address && !regexAddress.test(address)) {
|
||||
notify({
|
||||
message: 'The recipient address is not a valid LBRY address.',
|
||||
displayType: ['toast']
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > balance) {
|
||||
notify({
|
||||
message: 'Insufficient credits',
|
||||
displayType: ['toast']
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount && address) {
|
||||
sendToAddress(address, parseFloat(amount));
|
||||
this.setState({ address: null, amount: null });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { balance } = this.props;
|
||||
const canSend = this.state.address &&
|
||||
this.state.amount > 0 &&
|
||||
this.state.address.trim().length > 0;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.card}>
|
||||
<Text style={walletStyle.title}>Send Credits</Text>
|
||||
<Text style={walletStyle.text}>Amount</Text>
|
||||
<View style={[walletStyle.amountRow, walletStyle.bottomMarginMedium]}>
|
||||
<TextInput onChangeText={value => this.setState({amount: value})}
|
||||
keyboardType={'numeric'}
|
||||
value={this.state.amount}
|
||||
style={[walletStyle.input, walletStyle.amountInput]} />
|
||||
<Text style={[walletStyle.text, walletStyle.currency]}>LBC</Text>
|
||||
</View>
|
||||
<Text style={walletStyle.text}>Recipient address</Text>
|
||||
<View style={walletStyle.row}>
|
||||
<TextInput onChangeText={value => this.setState({address: value})}
|
||||
placeholder={'bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs'}
|
||||
value={this.state.address}
|
||||
style={[walletStyle.input, walletStyle.addressInput, walletStyle.bottomMarginMedium]} />
|
||||
<Button text={'Send'}
|
||||
style={[walletStyle.button, walletStyle.sendButton]}
|
||||
disabled={!canSend}
|
||||
onPress={this.handleSend} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletSend;
|
|
@ -24,6 +24,7 @@ import {
|
|||
claimsReducer,
|
||||
costInfoReducer,
|
||||
fileInfoReducer,
|
||||
notificationsReducer,
|
||||
searchReducer,
|
||||
walletReducer
|
||||
} from 'lbry-redux';
|
||||
|
@ -71,6 +72,7 @@ const reducers = combineReducers({
|
|||
claims: claimsReducer,
|
||||
costInfo: costInfoReducer,
|
||||
fileInfo: fileInfoReducer,
|
||||
notifications: notificationsReducer,
|
||||
search: searchReducer,
|
||||
wallet: walletReducer,
|
||||
nav: navigatorReducer,
|
||||
|
@ -96,12 +98,13 @@ const compressor = createCompressor();
|
|||
const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
|
||||
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
|
||||
const settingsFilter = createFilter('settings', ['clientSettings']);
|
||||
const walletFilter = createFilter('wallet', ['receiveAddress']);
|
||||
|
||||
const persistOptions = {
|
||||
whitelist: ['claims', 'subscriptions', 'settings'],
|
||||
whitelist: ['claims', 'subscriptions', 'settings', 'wallet'],
|
||||
// Order is important. Needs to be compressed last or other transforms can't
|
||||
// read the data
|
||||
transforms: [saveClaimsFilter, subscriptionsFilter, settingsFilter, compressor],
|
||||
transforms: [saveClaimsFilter, subscriptionsFilter, settingsFilter, walletFilter, compressor],
|
||||
debounce: 10000,
|
||||
storage: AsyncStorage
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@ import discoverStyle from '../../styles/discover';
|
|||
import Feather from 'react-native-vector-icons/Feather';
|
||||
|
||||
class DiscoverPage extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
// Track the total time taken if this is the first launch
|
||||
AsyncStorage.getItem('firstLaunchTime').then(startTime => {
|
||||
if (startTime !== null && !isNaN(parseInt(startTime, 10))) {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doBalanceSubscribe } from 'lbry-redux';
|
||||
import SplashScreen from './view';
|
||||
|
||||
const select = state => ({});
|
||||
const perform = dispatch => ({});
|
||||
const perform = dispatch => ({
|
||||
balanceSubscribe: () => dispatch(doBalanceSubscribe())
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SplashScreen);
|
||||
export default connect(null, perform)(SplashScreen);
|
|
@ -40,7 +40,8 @@ class SplashScreen extends React.PureComponent {
|
|||
|
||||
Lbry.resolve({ uri: 'lbry://one' }).then(() => {
|
||||
// Leave the splash screen
|
||||
const { navigation } = this.props;
|
||||
const { balanceSubscribe, navigation } = this.props;
|
||||
balanceSubscribe();
|
||||
navigation.navigate('Main');
|
||||
});
|
||||
return;
|
||||
|
|
18
app/src/page/transactionHistory/index.js
Normal file
18
app/src/page/transactionHistory/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchTransactions,
|
||||
selectTransactionItems,
|
||||
selectIsFetchingTransactions,
|
||||
} from 'lbry-redux';
|
||||
import TransactionHistoryPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
fetchingTransactions: selectIsFetchingTransactions(state),
|
||||
transactions: selectTransactionItems(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchTransactions: () => dispatch(doFetchTransactions()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(TransactionHistoryPage);
|
29
app/src/page/transactionHistory/view.js
Normal file
29
app/src/page/transactionHistory/view.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import { View, ScrollView, Text } from 'react-native';
|
||||
import TransactionList from '../../component/transactionList';
|
||||
import walletStyle from '../../styles/wallet';
|
||||
|
||||
class TransactionHistoryPage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.fetchTransactions();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fetchingTransactions, transactions } = this.props;
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={walletStyle.historyList}>
|
||||
{fetchingTransactions && !transactions.length && (
|
||||
<Text style={walletStyle.infoText}>Loading transactions...</Text>
|
||||
)}
|
||||
{transactions && transactions.length && (
|
||||
<TransactionList transactions={transactions} />
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionHistoryPage;
|
4
app/src/page/wallet/index.js
Normal file
4
app/src/page/wallet/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import WalletPage from './view';
|
||||
|
||||
export default connect(null, null)(WalletPage);
|
21
app/src/page/wallet/view.js
Normal file
21
app/src/page/wallet/view.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
import TransactionListRecent from '../../component/transactionListRecent';
|
||||
import WalletAddress from '../../component/walletAddress';
|
||||
import WalletBalance from '../../component/walletBalance';
|
||||
import WalletSend from '../../component/walletSend';
|
||||
|
||||
class WalletPage extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<ScrollView>
|
||||
<WalletBalance />
|
||||
<WalletAddress />
|
||||
<WalletSend />
|
||||
<TransactionListRecent navigation={this.props.navigation} />
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletPage;
|
|
@ -1,9 +1,9 @@
|
|||
import {
|
||||
ACTIONS,
|
||||
Lbry,
|
||||
selectBalance,
|
||||
makeSelectCostInfoForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
selectTotalDownloadProgress,
|
||||
selectDownloadingByOutpoint,
|
||||
} from 'lbry-redux';
|
||||
import { NativeModules } from 'react-native';
|
||||
|
@ -187,7 +187,7 @@ export function doLoadVideo(uri) {
|
|||
export function doPurchaseUri(uri, specificCostInfo) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const balance = 0;//selectBalance(state);
|
||||
const balance = selectBalance(state);
|
||||
const fileInfo = makeSelectFileInfoForUri(uri)(state);
|
||||
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
|
||||
const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
|
||||
|
|
30
app/src/styles/button.js
Normal file
30
app/src/styles/button.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
|
||||
const buttonStyle = StyleSheet.create({
|
||||
button: {
|
||||
borderRadius: 24,
|
||||
padding: 8,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12
|
||||
},
|
||||
disabled: {
|
||||
backgroundColor: '#999999'
|
||||
},
|
||||
row: {
|
||||
alignSelf: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
icon: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
text: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14
|
||||
},
|
||||
textWithIcon: {
|
||||
marginLeft: 8
|
||||
}
|
||||
});
|
||||
|
||||
export default buttonStyle;
|
46
app/src/styles/transactionList.js
Normal file
46
app/src/styles/transactionList.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const transactionListStyle = StyleSheet.create({
|
||||
listItem: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eeeeee',
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
topRow: {
|
||||
marginBottom: 4
|
||||
},
|
||||
col: {
|
||||
alignSelf: 'stretch'
|
||||
},
|
||||
text: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14
|
||||
},
|
||||
amount: {
|
||||
textAlign: 'right'
|
||||
},
|
||||
txid: {
|
||||
color: Colors.LbryGreen
|
||||
},
|
||||
smallText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 12,
|
||||
color: '#aaaaaa'
|
||||
},
|
||||
noTransactions: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
textAlign: 'center',
|
||||
padding: 16,
|
||||
color: '#aaaaaa'
|
||||
}
|
||||
});
|
||||
|
||||
export default transactionListStyle;
|
133
app/src/styles/wallet.js
Normal file
133
app/src/styles/wallet.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from './colors';
|
||||
|
||||
const walletStyle = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
},
|
||||
amountRow: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
address: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'dashed',
|
||||
borderColor: '#cccccc',
|
||||
backgroundColor: '#f9f9f9',
|
||||
padding: 8,
|
||||
width: '85%'
|
||||
},
|
||||
button: {
|
||||
backgroundColor: Colors.LbryGreen
|
||||
},
|
||||
historyList: {
|
||||
backgroundColor: '#ffffff'
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#ffffff',
|
||||
marginTop: 16,
|
||||
marginLeft: 16,
|
||||
marginRight: 16,
|
||||
padding: 16
|
||||
},
|
||||
transactionsCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
margin: 16
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 20,
|
||||
marginBottom: 24
|
||||
},
|
||||
transactionsTitle: {
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 20
|
||||
},
|
||||
transactionsHeader: {
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#eeeeee'
|
||||
},
|
||||
text: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14
|
||||
},
|
||||
smallText: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 12
|
||||
},
|
||||
balanceCard: {
|
||||
marginTop: 16,
|
||||
marginLeft: 16,
|
||||
marginRight: 16
|
||||
},
|
||||
balanceBackground: {
|
||||
position: 'absolute',
|
||||
alignSelf: 'stretch',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
balanceTitle: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 18,
|
||||
marginLeft: 16,
|
||||
marginTop: 16
|
||||
},
|
||||
balanceCaption: {
|
||||
color: '#caedB9',
|
||||
fontFamily: 'Metropolis-Medium',
|
||||
fontSize: 14,
|
||||
marginLeft: 16,
|
||||
marginTop: 8,
|
||||
marginBottom: 96
|
||||
},
|
||||
balance: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Metropolis-Bold',
|
||||
fontSize: 36,
|
||||
marginLeft: 16,
|
||||
marginBottom: 16
|
||||
},
|
||||
infoText: {
|
||||
color: '#aaaaaa',
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14,
|
||||
padding: 16,
|
||||
textAlign: 'center'
|
||||
},
|
||||
input: {
|
||||
fontFamily: 'Metropolis-Regular',
|
||||
fontSize: 14
|
||||
},
|
||||
amountInput: {
|
||||
alignSelf: 'flex-start',
|
||||
width: 150
|
||||
},
|
||||
addressInput: {
|
||||
width: '80%'
|
||||
},
|
||||
currency: {
|
||||
alignSelf: 'flex-start',
|
||||
marginTop: 17
|
||||
},
|
||||
sendButton: {
|
||||
marginTop: 8
|
||||
},
|
||||
bottomMarginSmall: {
|
||||
marginBottom: 8
|
||||
},
|
||||
bottomMarginMedium: {
|
||||
marginBottom: 16
|
||||
},
|
||||
bottomMarginLarge: {
|
||||
marginBottom: 24
|
||||
}
|
||||
});
|
||||
|
||||
export default walletStyle;
|
|
@ -16,7 +16,9 @@ import org.json.JSONException;
|
|||
|
||||
public class MixpanelModule extends ReactContextBaseJavaModule {
|
||||
|
||||
private static final String MIXPANEL_TOKEN = "93b81fb957cb0ddcd3198c10853a6a95";
|
||||
// TODO: Detect dev / debug and release mode and update value accordingly
|
||||
//private static final String MIXPANEL_TOKEN = "93b81fb957cb0ddcd3198c10853a6a95"; // Production
|
||||
private static final String MIXPANEL_TOKEN = "bc1630b8be64c5dfaa4700b3a62969f3"; // Dev Testing
|
||||
|
||||
private Context context;
|
||||
|
||||
|
|
BIN
src/main/res/drawable-xhdpi/src_assets_stripe.png
Normal file
BIN
src/main/res/drawable-xhdpi/src_assets_stripe.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Loading…
Reference in a new issue