JimmyMow
6 years ago
committed by
GitHub
34 changed files with 2324 additions and 17 deletions
@ -0,0 +1,702 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { Box, Flex } from 'rebass' |
||||
|
import { animated, Keyframes, Transition } from 'react-spring' |
||||
|
import { FormattedMessage, injectIntl } from 'react-intl' |
||||
|
import lightningPayReq from 'bolt11' |
||||
|
import { getMinFee, getMaxFee, getFeeRange, isOnchain, isLn } from 'lib/utils/crypto' |
||||
|
import { convert } from 'lib/utils/btc' |
||||
|
import { |
||||
|
Bar, |
||||
|
CryptoAmountInput, |
||||
|
Dropdown, |
||||
|
FiatAmountInput, |
||||
|
Form, |
||||
|
FormFieldMessage, |
||||
|
Label, |
||||
|
LightningInvoiceInput, |
||||
|
Text |
||||
|
} from 'components/UI' |
||||
|
|
||||
|
import PayButtons from './PayButtons' |
||||
|
import PayHeader from './PayHeader' |
||||
|
import { PaySummaryLightning, PaySummaryOnChain } from '.' |
||||
|
import messages from './messages' |
||||
|
|
||||
|
/** |
||||
|
* Animation to handle showing/hiding the payReq field. |
||||
|
*/ |
||||
|
const ShowHidePayReq = Keyframes.Spring({ |
||||
|
small: { height: 48 }, |
||||
|
big: async (next, cancel, ownProps) => { |
||||
|
ownProps.context.focusPayReqInput() |
||||
|
await next({ height: 130 }) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
/** |
||||
|
* Animation to handle showing/hiding the form buttons. |
||||
|
*/ |
||||
|
const ShowHideButtons = Keyframes.Spring({ |
||||
|
show: { opacity: 1 }, |
||||
|
hide: { opacity: 0 } |
||||
|
}) |
||||
|
|
||||
|
/** |
||||
|
* Animation to handle showing/hiding the amount fields. |
||||
|
*/ |
||||
|
const ShowHideAmount = Keyframes.Spring({ |
||||
|
show: async (next, cancel, ownProps) => { |
||||
|
await next({ display: 'block' }) |
||||
|
ownProps.context.focusAmountInput() |
||||
|
await next({ opacity: 1, height: 'auto' }) |
||||
|
}, |
||||
|
hide: { opacity: 0, height: 0, display: 'none' }, |
||||
|
remove: { opacity: 0, height: 0, display: 'none', immediate: true } |
||||
|
}) |
||||
|
|
||||
|
/** |
||||
|
* Payment form (onchain & offchain) |
||||
|
*/ |
||||
|
class Pay extends React.Component { |
||||
|
static propTypes = { |
||||
|
/** The currently active chain (bitcoin, litecoin etc) */ |
||||
|
chain: PropTypes.string.isRequired, |
||||
|
/** The currently active chain (mainnet, testnet) */ |
||||
|
network: PropTypes.string.isRequired, |
||||
|
/** Human readable chain name */ |
||||
|
cryptoName: PropTypes.string.isRequired, |
||||
|
/** Current channel balance (in satoshis). */ |
||||
|
channelBalance: PropTypes.number.isRequired, |
||||
|
/** Current ticker data as provided by blockchain.info */ |
||||
|
currentTicker: PropTypes.object.isRequired, |
||||
|
/** Currently selected cryptocurrency (key). */ |
||||
|
cryptoCurrency: PropTypes.string.isRequired, |
||||
|
/** Ticker symbol of the currently selected cryptocurrency. */ |
||||
|
cryptoCurrencyTicker: PropTypes.string.isRequired, |
||||
|
/** List of supported cryptocurrencies. */ |
||||
|
cryptoCurrencies: PropTypes.arrayOf( |
||||
|
PropTypes.shape({ |
||||
|
key: PropTypes.string.isRequired, |
||||
|
name: PropTypes.string.isRequired |
||||
|
}) |
||||
|
).isRequired, |
||||
|
/** List of supported fiat currencies. */ |
||||
|
fiatCurrencies: PropTypes.array.isRequired, |
||||
|
/** Currently selected fiat currency (key). */ |
||||
|
fiatCurrency: PropTypes.string.isRequired, |
||||
|
/** Payment address or invoice to populate the payReq field with when the form first loads. */ |
||||
|
initialPayReq: PropTypes.string, |
||||
|
/** Amount value to populate the amountCrypto field with when the form first loads. */ |
||||
|
initialAmountCrypto: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
|
/** Amount value to populate the amountFiat field with when the form first loads. */ |
||||
|
initialAmountFiat: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
|
/** Boolean indicating wether the form is being processed. If true, form buttons are disabled. */ |
||||
|
isProcessing: PropTypes.bool, |
||||
|
/** Boolean indicating wether fee information is currently being fetched. */ |
||||
|
isQueryingFees: PropTypes.bool, |
||||
|
/** Boolean indicating wether routing information is currently being fetched. */ |
||||
|
isQueryingRoutes: PropTypes.bool, |
||||
|
/** List of known nodes */ |
||||
|
nodes: PropTypes.array, |
||||
|
/** Current fee information as provided by bitcoinfees.earn.com */ |
||||
|
onchainFees: PropTypes.shape({ |
||||
|
fastestFee: PropTypes.number, |
||||
|
halfHourFee: PropTypes.number, |
||||
|
hourFee: PropTypes.number |
||||
|
}), |
||||
|
/** Routing information */ |
||||
|
routes: PropTypes.array, |
||||
|
/** Current wallet balance (in satoshis). */ |
||||
|
walletBalance: PropTypes.number.isRequired, |
||||
|
/** Method to process offChain invoice payments. Called when the form is submitted. */ |
||||
|
payInvoice: PropTypes.func.isRequired, |
||||
|
/** Set the current cryptocurrency. */ |
||||
|
setCryptoCurrency: PropTypes.func.isRequired, |
||||
|
/** Set the current fiat currency */ |
||||
|
setFiatCurrency: PropTypes.func.isRequired, |
||||
|
/** Method to process onChain transactions. Called when the form is submitted. */ |
||||
|
sendCoins: PropTypes.func.isRequired, |
||||
|
/** Method to fetch fee information for onchain transactions. */ |
||||
|
queryFees: PropTypes.func.isRequired, |
||||
|
/** Method to collect route information for lightning invoices. */ |
||||
|
queryRoutes: PropTypes.func.isRequired |
||||
|
} |
||||
|
|
||||
|
static defaultProps = { |
||||
|
initialAmountCrypto: null, |
||||
|
initialAmountFiat: null, |
||||
|
isProcessing: false, |
||||
|
isQueryingFees: false, |
||||
|
isQueryingRoutes: false, |
||||
|
nodes: [], |
||||
|
routes: [] |
||||
|
} |
||||
|
|
||||
|
state = { |
||||
|
currentStep: 'address', |
||||
|
isLn: null, |
||||
|
isOnchain: null |
||||
|
} |
||||
|
|
||||
|
amountInput = React.createRef() |
||||
|
payReqInput = React.createRef() |
||||
|
|
||||
|
/** |
||||
|
* If we have an address when the component mounts, run the payReq change handler to compure isLn / isOnchain. |
||||
|
*/ |
||||
|
componentDidMount() { |
||||
|
const { formApi } = this |
||||
|
if (formApi.getValue('payReq')) { |
||||
|
this.handlePayReqOnChange() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* If we have gone back to the address step, focus the address input and unmark all fields from being touched. |
||||
|
*/ |
||||
|
componentDidUpdate(prevProps, prevState) { |
||||
|
const { currentStep } = this.state |
||||
|
if (currentStep !== prevState.currentStep) { |
||||
|
if (currentStep === 'address') { |
||||
|
Object.keys(this.formApi.getState().touched).forEach(field => { |
||||
|
this.formApi.setTouched(field, false) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Form submit handler. |
||||
|
* @param {Object} values submitted form values. |
||||
|
*/ |
||||
|
onSubmit = values => { |
||||
|
const { currentStep, isOnchain } = this.state |
||||
|
const { cryptoCurrency, payInvoice, sendCoins } = this.props |
||||
|
if (currentStep === 'summary') { |
||||
|
return isOnchain |
||||
|
? sendCoins({ |
||||
|
value: values.amountCrypto, |
||||
|
addr: values.payReq, |
||||
|
currency: cryptoCurrency |
||||
|
}) |
||||
|
: payInvoice(values.payReq) |
||||
|
} else { |
||||
|
this.nextStep() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Store the formApi on the component context to make it available at this.formApi. |
||||
|
*/ |
||||
|
setFormApi = formApi => { |
||||
|
this.formApi = formApi |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* set the amountFiat field. |
||||
|
*/ |
||||
|
setAmountFiat = () => { |
||||
|
if (this.amountInput.current) { |
||||
|
this.amountInput.current.focus() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Focus the payReq input. |
||||
|
*/ |
||||
|
focusPayReqInput = () => { |
||||
|
if (this.payReqInput.current) { |
||||
|
this.payReqInput.current.focus() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Focus the amount input. |
||||
|
*/ |
||||
|
focusAmountInput = () => { |
||||
|
if (this.amountInput.current) { |
||||
|
this.amountInput.current.focus() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Liost of enabled form steps. |
||||
|
*/ |
||||
|
steps = () => { |
||||
|
const { isLn, isOnchain } = this.state |
||||
|
let steps = ['address'] |
||||
|
if (isLn) { |
||||
|
steps = ['address', 'summary'] |
||||
|
} else if (isOnchain) { |
||||
|
steps = ['address', 'amount', 'summary'] |
||||
|
} |
||||
|
return steps |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Go back to previous form step. |
||||
|
*/ |
||||
|
previousStep = () => { |
||||
|
const { currentStep } = this.state |
||||
|
const nextStep = Math.max(this.steps().indexOf(currentStep) - 1, 0) |
||||
|
if (currentStep !== nextStep) { |
||||
|
this.setState({ currentStep: this.steps()[nextStep] }) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Progress to next form step. |
||||
|
*/ |
||||
|
nextStep = () => { |
||||
|
const { currentStep } = this.state |
||||
|
const nextStep = Math.min(this.steps().indexOf(currentStep) + 1, this.steps().length - 1) |
||||
|
if (currentStep !== nextStep) { |
||||
|
this.setState({ currentStep: this.steps()[nextStep] }) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set isLn/isOnchain state based on payReq value. |
||||
|
*/ |
||||
|
handlePayReqOnChange = () => { |
||||
|
const { chain, network, queryRoutes } = this.props |
||||
|
const payReq = this.formApi.getValue('payReq') |
||||
|
const state = { |
||||
|
isLn: null, |
||||
|
isOnchain: null, |
||||
|
invoice: null |
||||
|
} |
||||
|
|
||||
|
// See if the user has entered a valid lightning payment request.
|
||||
|
if (isLn(payReq, chain, network)) { |
||||
|
let invoice |
||||
|
try { |
||||
|
invoice = lightningPayReq.decode(payReq) |
||||
|
state.invoice = invoice |
||||
|
} catch (e) { |
||||
|
return |
||||
|
} |
||||
|
state.isLn = true |
||||
|
const { satoshis, payeeNodeKey } = invoice |
||||
|
queryRoutes(payeeNodeKey, satoshis) |
||||
|
} |
||||
|
|
||||
|
// Otherwise, see if we have a valid onchain address.
|
||||
|
else if (isOnchain(payReq, chain, network)) { |
||||
|
state.isOnchain = true |
||||
|
} |
||||
|
|
||||
|
// Update the state with our findings.
|
||||
|
this.setState(state) |
||||
|
|
||||
|
// As soon as we have detected a valid address, submit the form.
|
||||
|
if (state.isLn || state.isOnchain) { |
||||
|
this.formApi.submitForm() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* set the amountFiat field whenever the crypto amount changes. |
||||
|
*/ |
||||
|
handleAmountCryptoChange = e => { |
||||
|
const { cryptoCurrency, currentTicker, fiatCurrency } = this.props |
||||
|
const lastPrice = currentTicker[fiatCurrency].last |
||||
|
const value = convert(cryptoCurrency, 'fiat', e.target.value, lastPrice) |
||||
|
this.formApi.setValue('amountFiat', value) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* set the amountCrypto field whenever the fiat amount changes. |
||||
|
*/ |
||||
|
handleAmountFiatChange = e => { |
||||
|
const { cryptoCurrency, currentTicker, fiatCurrency } = this.props |
||||
|
const lastPrice = currentTicker[fiatCurrency].last |
||||
|
const value = convert('fiat', cryptoCurrency, e.target.value, lastPrice) |
||||
|
this.formApi.setValue('amountCrypto', value) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle changes from the crypto currency dropdown. |
||||
|
*/ |
||||
|
handleCryptoCurrencyChange = value => { |
||||
|
const { setCryptoCurrency } = this.props |
||||
|
setCryptoCurrency(value) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle changes from the fiat currency dropdown. |
||||
|
*/ |
||||
|
handleFiatCurrencyChange = value => { |
||||
|
const { setFiatCurrency } = this.props |
||||
|
setFiatCurrency(value) |
||||
|
} |
||||
|
|
||||
|
renderHelpText = () => { |
||||
|
const { currentStep } = this.state |
||||
|
return ( |
||||
|
<Transition |
||||
|
native |
||||
|
items={currentStep === 'address'} |
||||
|
from={{ opacity: 0, height: 0 }} |
||||
|
enter={{ opacity: 1, height: 'auto' }} |
||||
|
leave={{ opacity: 0, height: 0 }} |
||||
|
initial={{ opacity: 1, height: 'auto' }} |
||||
|
> |
||||
|
{show => |
||||
|
show && |
||||
|
(styles => ( |
||||
|
<animated.div style={styles}> |
||||
|
<Box mb={5}> |
||||
|
<Text textAlign="justify"> |
||||
|
<FormattedMessage {...messages.description} /> |
||||
|
</Text> |
||||
|
</Box> |
||||
|
</animated.div> |
||||
|
)) |
||||
|
} |
||||
|
</Transition> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
renderAddressField = () => { |
||||
|
const { currentStep, isLn } = this.state |
||||
|
const { chain, initialPayReq, network } = this.props |
||||
|
return ( |
||||
|
<Box className={currentStep !== 'summary' ? 'element-show' : 'element-hide'}> |
||||
|
<Box pb={2}> |
||||
|
<Label htmlFor="payReq" readOnly={currentStep !== 'address'}> |
||||
|
{currentStep === 'address' ? ( |
||||
|
<FormattedMessage {...messages.request_label_combined} /> |
||||
|
) : isLn ? ( |
||||
|
<FormattedMessage {...messages.request_label_offchain} /> |
||||
|
) : ( |
||||
|
<FormattedMessage {...messages.request_label_onchain} /> |
||||
|
)} |
||||
|
</Label> |
||||
|
</Box> |
||||
|
|
||||
|
<ShowHidePayReq state={currentStep === 'address' ? 'big' : 'small'} context={this}> |
||||
|
{styles => ( |
||||
|
<React.Fragment> |
||||
|
<LightningInvoiceInput |
||||
|
style={styles} |
||||
|
initialValue={initialPayReq} |
||||
|
required |
||||
|
chain={chain} |
||||
|
network={network} |
||||
|
field="payReq" |
||||
|
validateOnBlur |
||||
|
validateOnChange |
||||
|
onChange={this.handlePayReqOnChange} |
||||
|
width={1} |
||||
|
readOnly={currentStep !== 'address'} |
||||
|
forwardedRef={this.payReqInput} |
||||
|
css={{ |
||||
|
resize: 'vertical', |
||||
|
'min-height': '48px' |
||||
|
}} |
||||
|
/> |
||||
|
</React.Fragment> |
||||
|
)} |
||||
|
</ShowHidePayReq> |
||||
|
</Box> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
renderAmountFields = () => { |
||||
|
const { currentStep } = this.state |
||||
|
const { |
||||
|
cryptoCurrency, |
||||
|
cryptoCurrencies, |
||||
|
currentTicker, |
||||
|
fiatCurrency, |
||||
|
fiatCurrencies, |
||||
|
initialAmountCrypto, |
||||
|
initialAmountFiat |
||||
|
} = this.props |
||||
|
return ( |
||||
|
<ShowHideAmount |
||||
|
state={currentStep === 'amount' ? 'show' : currentStep === 'address' ? 'hide' : 'remove'} |
||||
|
context={this} |
||||
|
> |
||||
|
{styles => ( |
||||
|
<Box style={styles}> |
||||
|
<Bar my={3} /> |
||||
|
<Label htmlFor="amountCrypto" pb={2}> |
||||
|
<FormattedMessage {...messages.amount} /> |
||||
|
</Label> |
||||
|
|
||||
|
<Flex justifyContent="space-between" alignItems="flex-start" mb={3}> |
||||
|
<Flex width={6 / 13}> |
||||
|
<Box width={145}> |
||||
|
<CryptoAmountInput |
||||
|
initialValue={initialAmountCrypto} |
||||
|
currency={cryptoCurrency} |
||||
|
required |
||||
|
field="amountCrypto" |
||||
|
width={145} |
||||
|
validateOnChange |
||||
|
validateOnBlur |
||||
|
onChange={this.handleAmountCryptoChange} |
||||
|
forwardedRef={this.amountInput} |
||||
|
disabled={currentStep === 'address'} |
||||
|
/> |
||||
|
</Box> |
||||
|
<Dropdown |
||||
|
activeKey={cryptoCurrency} |
||||
|
items={cryptoCurrencies} |
||||
|
onChange={this.handleCryptoCurrencyChange} |
||||
|
mt={2} |
||||
|
ml={2} |
||||
|
/> |
||||
|
</Flex> |
||||
|
<Text textAlign="center" mt={3} width={1 / 11}> |
||||
|
= |
||||
|
</Text> |
||||
|
<Flex width={6 / 13}> |
||||
|
<Box width={145} ml="auto"> |
||||
|
<FiatAmountInput |
||||
|
initialValue={initialAmountFiat} |
||||
|
currency={fiatCurrency} |
||||
|
currentTicker={currentTicker} |
||||
|
field="amountFiat" |
||||
|
width={145} |
||||
|
onChange={this.handleAmountFiatChange} |
||||
|
disabled={currentStep === 'address'} |
||||
|
/> |
||||
|
</Box> |
||||
|
|
||||
|
<Dropdown |
||||
|
activeKey={fiatCurrency} |
||||
|
items={fiatCurrencies} |
||||
|
onChange={this.handleFiatCurrencyChange} |
||||
|
mt={2} |
||||
|
ml={2} |
||||
|
/> |
||||
|
</Flex> |
||||
|
</Flex> |
||||
|
</Box> |
||||
|
)} |
||||
|
</ShowHideAmount> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
renderSummary = () => { |
||||
|
const { isOnchain } = this.state |
||||
|
const { |
||||
|
cryptoCurrency, |
||||
|
cryptoCurrencyTicker, |
||||
|
cryptoCurrencies, |
||||
|
currentTicker, |
||||
|
fiatCurrency, |
||||
|
isQueryingFees, |
||||
|
isQueryingRoutes, |
||||
|
nodes, |
||||
|
onchainFees, |
||||
|
queryFees, |
||||
|
routes, |
||||
|
setCryptoCurrency |
||||
|
} = this.props |
||||
|
const formState = this.formApi.getState() |
||||
|
let minFee, maxFee |
||||
|
if (routes.length) { |
||||
|
minFee = getMinFee(routes) |
||||
|
maxFee = getMaxFee(routes) |
||||
|
} |
||||
|
|
||||
|
// convert entered amount to satoshis
|
||||
|
if (isOnchain) { |
||||
|
const amountInSatoshis = convert(cryptoCurrency, 'sats', formState.values.amountCrypto) |
||||
|
return ( |
||||
|
<PaySummaryOnChain |
||||
|
amount={amountInSatoshis} |
||||
|
address={formState.values.payReq} |
||||
|
cryptoCurrency={cryptoCurrency} |
||||
|
cryptoCurrencyTicker={cryptoCurrencyTicker} |
||||
|
cryptoCurrencies={cryptoCurrencies} |
||||
|
currentTicker={currentTicker} |
||||
|
setCryptoCurrency={setCryptoCurrency} |
||||
|
fiatCurrency={fiatCurrency} |
||||
|
isQueryingFees={isQueryingFees} |
||||
|
onchainFees={onchainFees} |
||||
|
queryFees={queryFees} |
||||
|
/> |
||||
|
) |
||||
|
} else if (isLn) { |
||||
|
return ( |
||||
|
<PaySummaryLightning |
||||
|
currentTicker={currentTicker} |
||||
|
cryptoCurrency={cryptoCurrency} |
||||
|
cryptoCurrencyTicker={cryptoCurrencyTicker} |
||||
|
cryptoCurrencies={cryptoCurrencies} |
||||
|
fiatCurrency={fiatCurrency} |
||||
|
isQueryingRoutes={isQueryingRoutes} |
||||
|
minFee={minFee} |
||||
|
maxFee={maxFee} |
||||
|
nodes={nodes} |
||||
|
payReq={formState.values.payReq} |
||||
|
setCryptoCurrency={setCryptoCurrency} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Form renderer. |
||||
|
*/ |
||||
|
render() { |
||||
|
const { currentStep, invoice, isLn, isOnchain } = this.state |
||||
|
const { |
||||
|
chain, |
||||
|
network, |
||||
|
channelBalance, |
||||
|
cryptoCurrency, |
||||
|
cryptoCurrencyTicker, |
||||
|
cryptoCurrencies, |
||||
|
currentTicker, |
||||
|
cryptoName, |
||||
|
fiatCurrencies, |
||||
|
fiatCurrency, |
||||
|
initialPayReq, |
||||
|
initialAmountCrypto, |
||||
|
initialAmountFiat, |
||||
|
intl, |
||||
|
isProcessing, |
||||
|
isQueryingFees, |
||||
|
isQueryingRoutes, |
||||
|
onchainFees, |
||||
|
payInvoice, |
||||
|
sendCoins, |
||||
|
setCryptoCurrency, |
||||
|
setFiatCurrency, |
||||
|
queryFees, |
||||
|
queryRoutes, |
||||
|
routes, |
||||
|
walletBalance, |
||||
|
...rest |
||||
|
} = this.props |
||||
|
return ( |
||||
|
<Form |
||||
|
width={1} |
||||
|
css={{ height: '100%' }} |
||||
|
{...rest} |
||||
|
getApi={this.setFormApi} |
||||
|
onSubmit={this.onSubmit} |
||||
|
> |
||||
|
{({ formState }) => { |
||||
|
// Deterine which buttons should be visible.
|
||||
|
const showBack = currentStep !== 'address' |
||||
|
const showSubmit = currentStep !== 'address' || (isOnchain || isLn) |
||||
|
|
||||
|
// Determine wether we have a route to the sender.
|
||||
|
let hasRoute = true |
||||
|
if (isLn && currentStep === 'summary') { |
||||
|
const { min, max } = getFeeRange(routes || []) |
||||
|
if (min === null || max === null) { |
||||
|
hasRoute = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Determine wether we have enough funds available.
|
||||
|
let hasEnoughFunds = true |
||||
|
if (isLn && invoice) { |
||||
|
hasEnoughFunds = invoice.satoshis <= channelBalance |
||||
|
} else if (isOnchain) { |
||||
|
const valueInSats = convert(cryptoCurrency, 'sats', formState.values.amountCrypto) |
||||
|
hasEnoughFunds = valueInSats <= walletBalance |
||||
|
} |
||||
|
|
||||
|
// Determine what the text should be for the next button.
|
||||
|
let nextButtonText = intl.formatMessage({ ...messages.next }) |
||||
|
if (currentStep === 'summary') { |
||||
|
const value = |
||||
|
isLn && invoice |
||||
|
? convert('sats', cryptoCurrency, invoice.satoshis) |
||||
|
: formState.values.amountCrypto |
||||
|
nextButtonText = `${intl.formatMessage({ |
||||
|
...messages.send |
||||
|
})} ${value} ${cryptoCurrencyTicker}` |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Flex as="article" flexDirection="column" css={{ height: '100%' }}> |
||||
|
<Flex as="header" flexDirection="column" mb={2}> |
||||
|
<PayHeader |
||||
|
title={`${intl.formatMessage({ |
||||
|
...messages.send |
||||
|
})} ${cryptoName} (${cryptoCurrencyTicker})`}
|
||||
|
type={isLn ? 'offchain' : isOnchain ? 'onchain' : null} |
||||
|
/> |
||||
|
<Bar /> |
||||
|
</Flex> |
||||
|
|
||||
|
<Box as="section" css={{ flex: 1 }} mb={3}> |
||||
|
{this.renderHelpText()} |
||||
|
{this.renderAddressField()} |
||||
|
{isOnchain && this.renderAmountFields()} |
||||
|
{currentStep === 'summary' && this.renderSummary()} |
||||
|
</Box> |
||||
|
|
||||
|
<Box as="footer" mt="auto"> |
||||
|
<ShowHideButtons state={showBack || showSubmit ? 'show' : 'show'}> |
||||
|
{styles => ( |
||||
|
<Box style={styles}> |
||||
|
{currentStep === 'summary' && |
||||
|
!isQueryingRoutes && |
||||
|
!hasRoute && ( |
||||
|
<FormFieldMessage variant="error" justifyContent="center" mb={2}> |
||||
|
<FormattedMessage {...messages.error_no_route} /> |
||||
|
</FormFieldMessage> |
||||
|
)} |
||||
|
|
||||
|
{currentStep === 'summary' && |
||||
|
!hasEnoughFunds && ( |
||||
|
<FormFieldMessage variant="error" justifyContent="center" mb={2}> |
||||
|
<FormattedMessage {...messages.error_not_enough_funds} /> |
||||
|
</FormFieldMessage> |
||||
|
)} |
||||
|
|
||||
|
<PayButtons |
||||
|
disabled={ |
||||
|
formState.pristine || |
||||
|
formState.invalid || |
||||
|
(currentStep === 'summary' && (!hasRoute || !hasEnoughFunds)) |
||||
|
} |
||||
|
nextButtonText={nextButtonText} |
||||
|
processing={isProcessing} |
||||
|
showBack={showBack} |
||||
|
showSubmit={showSubmit} |
||||
|
previousStep={this.previousStep} |
||||
|
/> |
||||
|
|
||||
|
{walletBalance !== null && ( |
||||
|
<React.Fragment> |
||||
|
<Text textAlign="center" mt={3} fontWeight="normal"> |
||||
|
<FormattedMessage {...messages.current_balance} />: |
||||
|
</Text> |
||||
|
<Text textAlign="center" fontSize="xs"> |
||||
|
{convert('sats', cryptoCurrency, walletBalance)} |
||||
|
{` `} |
||||
|
{cryptoCurrencyTicker} (onchain), |
||||
|
</Text> |
||||
|
<Text textAlign="center" fontSize="xs"> |
||||
|
{convert('sats', cryptoCurrency, channelBalance)} |
||||
|
{` `} |
||||
|
{cryptoCurrencyTicker} (in channels) |
||||
|
</Text> |
||||
|
</React.Fragment> |
||||
|
)} |
||||
|
</Box> |
||||
|
)} |
||||
|
</ShowHideButtons> |
||||
|
</Box> |
||||
|
</Flex> |
||||
|
) |
||||
|
}} |
||||
|
</Form> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default injectIntl(Pay) |
@ -0,0 +1,74 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { FormattedMessage } from 'react-intl' |
||||
|
import { Box, Flex, Text } from 'rebass' |
||||
|
import BigArrowLeft from 'components/Icon/BigArrowLeft' |
||||
|
import { Button } from 'components/UI' |
||||
|
import messages from './messages' |
||||
|
|
||||
|
/** |
||||
|
* Buttons for Pay. |
||||
|
*/ |
||||
|
class PayButtons extends React.PureComponent { |
||||
|
static propTypes = { |
||||
|
disabled: PropTypes.bool, |
||||
|
nextButtonText: PropTypes.node, |
||||
|
previousStep: PropTypes.func, |
||||
|
processing: PropTypes.bool, |
||||
|
showBack: PropTypes.bool, |
||||
|
showSubmit: PropTypes.bool |
||||
|
} |
||||
|
|
||||
|
static defaultProps = { |
||||
|
disabled: false, |
||||
|
nextButtonText: <FormattedMessage {...messages.next} />, |
||||
|
previousStep: () => ({}), |
||||
|
processing: false, |
||||
|
showBack: true, |
||||
|
showSubmit: true |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { |
||||
|
disabled, |
||||
|
nextButtonText, |
||||
|
previousStep, |
||||
|
processing, |
||||
|
showBack, |
||||
|
showSubmit, |
||||
|
...rest |
||||
|
} = this.props |
||||
|
return ( |
||||
|
<Flex {...rest} justifyContent="space-between" alignItems="center"> |
||||
|
<Box width={1 / 5}> |
||||
|
{showBack && ( |
||||
|
<Button |
||||
|
type="button" |
||||
|
variant="secondary" |
||||
|
onClick={previousStep} |
||||
|
px={0} |
||||
|
disabled={processing} |
||||
|
> |
||||
|
<Flex> |
||||
|
<Text> |
||||
|
<BigArrowLeft /> |
||||
|
</Text> |
||||
|
<Text ml={1}> |
||||
|
<FormattedMessage {...messages.back} /> |
||||
|
</Text> |
||||
|
</Flex> |
||||
|
</Button> |
||||
|
)} |
||||
|
</Box> |
||||
|
{showSubmit && ( |
||||
|
<Button type="submit" mx="auto" disabled={disabled || processing} processing={processing}> |
||||
|
{nextButtonText} |
||||
|
</Button> |
||||
|
)} |
||||
|
<Box width={1 / 5} /> |
||||
|
</Flex> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default PayButtons |
@ -0,0 +1,37 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { Box } from 'rebass' |
||||
|
import { Heading, Text } from 'components/UI' |
||||
|
import Lightning from 'components/Icon/Lightning' |
||||
|
import Onchain from 'components/Icon/Onchain' |
||||
|
import PaperPlane from 'components/Icon/PaperPlane' |
||||
|
|
||||
|
/** |
||||
|
* Header for opayment form. |
||||
|
*/ |
||||
|
class PayHeader extends React.PureComponent { |
||||
|
static propTypes = { |
||||
|
title: PropTypes.string.isRequired, |
||||
|
type: PropTypes.oneOf(['onchain', 'offchain']) |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { title, type } = this.props |
||||
|
return ( |
||||
|
<Text textAlign="center"> |
||||
|
<Box mx="auto" css={{ height: '55px' }}> |
||||
|
{type === 'offchain' && <Lightning height="45px" width="45px" />} |
||||
|
{type === 'onchain' && <Onchain height="45px" width="45px" />} |
||||
|
{!type && <PaperPlane height="35px" width="35px" />} |
||||
|
</Box> |
||||
|
<Heading.h1 mx="auto">{title}</Heading.h1> |
||||
|
<Heading.h4 mx="auto"> |
||||
|
|
||||
|
{type === 'onchain' && 'On-Chain Payment'} {type === 'offchain' && 'Lightning Payment'} |
||||
|
</Heading.h4> |
||||
|
</Text> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default PayHeader |
@ -0,0 +1,163 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { Box, Flex } from 'rebass' |
||||
|
import { FormattedNumber, FormattedMessage } from 'react-intl' |
||||
|
import lightningPayReq from 'bolt11' |
||||
|
import { satoshisToFiat } from 'lib/utils/btc' |
||||
|
import { getNodeAlias } from 'lib/utils/crypto' |
||||
|
import BigArrowRight from 'components/Icon/BigArrowRight' |
||||
|
import { Bar, Dropdown, Spinner, Text, Truncate } from 'components/UI' |
||||
|
import Value from 'components/Value' |
||||
|
import { PaySummaryRow } from '.' |
||||
|
import messages from './messages' |
||||
|
|
||||
|
class PaySummaryLightning extends React.PureComponent { |
||||
|
static propTypes = { |
||||
|
/** Current ticker data as provided by blockchain.info */ |
||||
|
currentTicker: PropTypes.object.isRequired, |
||||
|
/** Currently selected cryptocurrency (key). */ |
||||
|
cryptoCurrency: PropTypes.string.isRequired, |
||||
|
/** Ticker symbol of the currently selected cryptocurrency. */ |
||||
|
cryptoCurrencyTicker: PropTypes.string.isRequired, |
||||
|
/** List of supported cryptocurrencies. */ |
||||
|
cryptoCurrencies: PropTypes.arrayOf( |
||||
|
PropTypes.shape({ |
||||
|
key: PropTypes.string.isRequired, |
||||
|
name: PropTypes.string.isRequired |
||||
|
}) |
||||
|
).isRequired, |
||||
|
/** Currently selected fiat currency (key). */ |
||||
|
fiatCurrency: PropTypes.string.isRequired, |
||||
|
/** Boolean indicating wether routing information is currently being fetched. */ |
||||
|
isQueryingRoutes: PropTypes.bool, |
||||
|
/** Maximum fee for the payment */ |
||||
|
maxFee: PropTypes.number, |
||||
|
/** Minimumfee for the payment */ |
||||
|
minFee: PropTypes.number, |
||||
|
/** List of nodes as returned by lnd */ |
||||
|
nodes: PropTypes.array, |
||||
|
/** Lightning Payment request */ |
||||
|
payReq: PropTypes.string.isRequired, |
||||
|
|
||||
|
/** Set the current cryptocurrency. */ |
||||
|
setCryptoCurrency: PropTypes.func.isRequired |
||||
|
} |
||||
|
|
||||
|
static defaultProps = { |
||||
|
isQueryingRoutes: false, |
||||
|
minFee: null, |
||||
|
maxFee: null, |
||||
|
nodes: [] |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { |
||||
|
cryptoCurrency, |
||||
|
cryptoCurrencyTicker, |
||||
|
cryptoCurrencies, |
||||
|
currentTicker, |
||||
|
fiatCurrency, |
||||
|
isQueryingRoutes, |
||||
|
maxFee, |
||||
|
minFee, |
||||
|
nodes, |
||||
|
payReq, |
||||
|
setCryptoCurrency |
||||
|
} = this.props |
||||
|
|
||||
|
let invoice |
||||
|
try { |
||||
|
invoice = lightningPayReq.decode(payReq) |
||||
|
} catch (e) { |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
const { satoshis, payeeNodeKey } = invoice |
||||
|
const descriptionTag = invoice.tags.find(tag => tag.tagName === 'description') || {} |
||||
|
const memo = descriptionTag.data |
||||
|
const fiatAmount = satoshisToFiat(satoshis, currentTicker[fiatCurrency].last) |
||||
|
const nodeAlias = getNodeAlias(payeeNodeKey, nodes) |
||||
|
|
||||
|
return ( |
||||
|
<React.Fragment> |
||||
|
<Box pb={2}> |
||||
|
<Flex alignItems="center"> |
||||
|
<Box width={5 / 11}> |
||||
|
<Flex flexWrap="wrap" alignItems="baseline"> |
||||
|
<Box> |
||||
|
<Text textAlign="left" fontSize={6}> |
||||
|
<Value value={satoshis} currency={cryptoCurrency} /> |
||||
|
</Text> |
||||
|
</Box> |
||||
|
<Dropdown |
||||
|
activeKey={cryptoCurrency} |
||||
|
items={cryptoCurrencies} |
||||
|
onChange={setCryptoCurrency} |
||||
|
ml={2} |
||||
|
/> |
||||
|
</Flex> |
||||
|
<Text color="gray"> |
||||
|
{'≈ '} |
||||
|
<FormattedNumber currency={fiatCurrency} style="currency" value={fiatAmount} /> |
||||
|
</Text> |
||||
|
</Box> |
||||
|
<Box width={1 / 11}> |
||||
|
<Text textAlign="center" color="lightningOrange"> |
||||
|
<BigArrowRight width="40px" height="28px" /> |
||||
|
</Text> |
||||
|
</Box> |
||||
|
<Box width={5 / 11}> |
||||
|
<Text textAlign="right" className="hint--bottom-left" data-hint={payeeNodeKey}> |
||||
|
{<Truncate text={nodeAlias || payeeNodeKey} />} |
||||
|
</Text> |
||||
|
</Box> |
||||
|
</Flex> |
||||
|
</Box> |
||||
|
|
||||
|
<Bar /> |
||||
|
|
||||
|
<PaySummaryRow |
||||
|
left={<FormattedMessage {...messages.fee} />} |
||||
|
right={ |
||||
|
isQueryingRoutes ? ( |
||||
|
<Flex ml="auto" alignItems="center" justifyContent="flex-end"> |
||||
|
<Text mr={2}> |
||||
|
<FormattedMessage {...messages.searching_routes} /> |
||||
|
… |
||||
|
</Text> |
||||
|
<Spinner color="lightningOrange" /> |
||||
|
</Flex> |
||||
|
) : minFee === null || maxFee === null ? ( |
||||
|
<FormattedMessage {...messages.unknown} /> |
||||
|
) : ( |
||||
|
<FormattedMessage {...messages.fee_range} values={{ minFee, maxFee }} /> |
||||
|
) |
||||
|
} |
||||
|
/> |
||||
|
|
||||
|
<Bar /> |
||||
|
|
||||
|
<PaySummaryRow |
||||
|
left={<FormattedMessage {...messages.total} />} |
||||
|
right={ |
||||
|
<React.Fragment> |
||||
|
<Value value={satoshis} currency={cryptoCurrency} /> {cryptoCurrencyTicker} |
||||
|
{!isQueryingRoutes && |
||||
|
maxFee && ( |
||||
|
<Text fontSize="s"> |
||||
|
(+ <FormattedMessage {...messages.upto} /> {maxFee} msats) |
||||
|
</Text> |
||||
|
)} |
||||
|
</React.Fragment> |
||||
|
} |
||||
|
/> |
||||
|
|
||||
|
<Bar /> |
||||
|
|
||||
|
{memo && <PaySummaryRow left={<FormattedMessage {...messages.memo} />} right={memo} />} |
||||
|
</React.Fragment> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default PaySummaryLightning |
@ -0,0 +1,155 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { Box, Flex } from 'rebass' |
||||
|
import { FormattedNumber, FormattedMessage } from 'react-intl' |
||||
|
import get from 'lodash.get' |
||||
|
import { satoshisToFiat } from 'lib/utils/btc' |
||||
|
import BigArrowRight from 'components/Icon/BigArrowRight' |
||||
|
import { Bar, Dropdown, Spinner, Text, Truncate } from 'components/UI' |
||||
|
import Value from 'components/Value' |
||||
|
import { PaySummaryRow } from '.' |
||||
|
import messages from './messages' |
||||
|
|
||||
|
class PaySummaryOnChain extends React.Component { |
||||
|
static propTypes = { |
||||
|
/** Amount to send (in satoshis). */ |
||||
|
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, |
||||
|
/** Onchain address of recipient. */ |
||||
|
address: PropTypes.string.isRequired, |
||||
|
/** Currently selected cryptocurrency (key). */ |
||||
|
cryptoCurrency: PropTypes.string.isRequired, |
||||
|
/** Ticker symbol of the currently selected cryptocurrency. */ |
||||
|
cryptoCurrencyTicker: PropTypes.string.isRequired, |
||||
|
/** List of supported cryptocurrencies. */ |
||||
|
cryptoCurrencies: PropTypes.arrayOf( |
||||
|
PropTypes.shape({ |
||||
|
key: PropTypes.string.isRequired, |
||||
|
name: PropTypes.string.isRequired |
||||
|
}) |
||||
|
).isRequired, |
||||
|
/** Current ticker data as provided by blockchain.info */ |
||||
|
currentTicker: PropTypes.object.isRequired, |
||||
|
/** Current fee information as provided by bitcoinfees.earn.com */ |
||||
|
onchainFees: PropTypes.shape({ |
||||
|
fastestFee: PropTypes.number, |
||||
|
halfHourFee: PropTypes.number, |
||||
|
hourFee: PropTypes.number |
||||
|
}), |
||||
|
/** Currently selected fiat currency (key). */ |
||||
|
fiatCurrency: PropTypes.string.isRequired, |
||||
|
/** Boolean indicating wether routing information is currently being fetched. */ |
||||
|
isQueryingFees: PropTypes.bool, |
||||
|
|
||||
|
/** Method to fetch fee information for onchain transactions. */ |
||||
|
queryFees: PropTypes.func.isRequired, |
||||
|
/** Set the current cryptocurrency. */ |
||||
|
setCryptoCurrency: PropTypes.func.isRequired |
||||
|
} |
||||
|
|
||||
|
static defaultProps = { |
||||
|
isQueryingFees: false, |
||||
|
onchainFees: {} |
||||
|
} |
||||
|
|
||||
|
componenDidMount() { |
||||
|
const { queryFees } = this.props |
||||
|
queryFees() |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { |
||||
|
amount, |
||||
|
address, |
||||
|
cryptoCurrency, |
||||
|
cryptoCurrencyTicker, |
||||
|
cryptoCurrencies, |
||||
|
currentTicker, |
||||
|
fiatCurrency, |
||||
|
onchainFees, |
||||
|
isQueryingFees, |
||||
|
setCryptoCurrency |
||||
|
} = this.props |
||||
|
|
||||
|
const fiatAmount = satoshisToFiat(amount, currentTicker[fiatCurrency].last) |
||||
|
const fee = get(onchainFees, 'fastestFee', null) |
||||
|
return ( |
||||
|
<React.Fragment> |
||||
|
<Box pb={2}> |
||||
|
<Flex alignItems="center"> |
||||
|
<Box width={5 / 11}> |
||||
|
<Flex flexWrap="wrap" alignItems="baseline"> |
||||
|
<Box> |
||||
|
<Text textAlign="left" fontSize={6}> |
||||
|
<Value value={amount} currency={cryptoCurrency} /> |
||||
|
</Text> |
||||
|
</Box> |
||||
|
<Dropdown |
||||
|
activeKey={cryptoCurrency} |
||||
|
items={cryptoCurrencies} |
||||
|
onChange={setCryptoCurrency} |
||||
|
ml={2} |
||||
|
/> |
||||
|
</Flex> |
||||
|
<Text color="gray"> |
||||
|
{' ≈ '} |
||||
|
<FormattedNumber currency={fiatCurrency} style="currency" value={fiatAmount} /> |
||||
|
</Text> |
||||
|
</Box> |
||||
|
<Box width={1 / 11}> |
||||
|
<Text textAlign="center" color="lightningOrange"> |
||||
|
<BigArrowRight width="40px" height="28px" /> |
||||
|
</Text> |
||||
|
</Box> |
||||
|
<Box width={5 / 11}> |
||||
|
<Text textAlign="right" className="hint--bottom-left" data-hint={address}> |
||||
|
<Truncate text={address} /> |
||||
|
</Text> |
||||
|
</Box> |
||||
|
</Flex> |
||||
|
</Box> |
||||
|
|
||||
|
<Bar /> |
||||
|
|
||||
|
<PaySummaryRow |
||||
|
left={<FormattedMessage {...messages.fee} />} |
||||
|
right={ |
||||
|
isQueryingFees ? ( |
||||
|
<Flex ml="auto" alignItems="center" justifyContent="flex-end"> |
||||
|
<Text mr={2}> |
||||
|
<FormattedMessage {...messages.calculating} /> |
||||
|
… |
||||
|
</Text> |
||||
|
<Spinner color="lightningOrange" /> |
||||
|
</Flex> |
||||
|
) : !fee ? ( |
||||
|
<FormattedMessage {...messages.unknown} /> |
||||
|
) : ( |
||||
|
<React.Fragment> |
||||
|
<Text> |
||||
|
{fee} satoshis <FormattedMessage {...messages.per_byte} /> |
||||
|
</Text> |
||||
|
<Text fontSize="s"> |
||||
|
(<FormattedMessage {...messages.next_block_confirmation} />) |
||||
|
</Text> |
||||
|
</React.Fragment> |
||||
|
) |
||||
|
} |
||||
|
/> |
||||
|
|
||||
|
<Bar /> |
||||
|
|
||||
|
<PaySummaryRow |
||||
|
left={<FormattedMessage {...messages.total} />} |
||||
|
right={ |
||||
|
<React.Fragment> |
||||
|
<Value value={amount} currency={cryptoCurrency} /> {cryptoCurrencyTicker} |
||||
|
{!isQueryingFees && fee && <Text fontSize="s">(+ {fee} satoshis per byte</Text>} |
||||
|
</React.Fragment> |
||||
|
} |
||||
|
/> |
||||
|
</React.Fragment> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default PaySummaryOnChain |
@ -0,0 +1,24 @@ |
|||||
|
import React from 'react' |
||||
|
import PropTypes from 'prop-types' |
||||
|
import { Box, Flex } from 'rebass' |
||||
|
import { Text } from 'components/UI' |
||||
|
|
||||
|
const PaySummaryRow = ({ left, right }) => ( |
||||
|
<Box py={2}> |
||||
|
<Flex alignItems="center"> |
||||
|
<Box width={1 / 2}> |
||||
|
<Text fontWeight="normal">{left}</Text> |
||||
|
</Box> |
||||
|
<Box width={1 / 2}> |
||||
|
<Text textAlign="right">{right}</Text> |
||||
|
</Box> |
||||
|
</Flex> |
||||
|
</Box> |
||||
|
) |
||||
|
|
||||
|
PaySummaryRow.propTypes = { |
||||
|
left: PropTypes.any, |
||||
|
right: PropTypes.any |
||||
|
} |
||||
|
|
||||
|
export default PaySummaryRow |
@ -0,0 +1,6 @@ |
|||||
|
export Pay from './Pay' |
||||
|
export PayButtons from './PayButtons' |
||||
|
export PayHeader from './PayHeader' |
||||
|
export PaySummaryLightning from './PaySummaryLightning' |
||||
|
export PaySummaryOnChain from './PaySummaryOnChain' |
||||
|
export PaySummaryRow from './PaySummaryRow' |
@ -0,0 +1,27 @@ |
|||||
|
import { defineMessages } from 'react-intl' |
||||
|
|
||||
|
/* eslint-disable max-len */ |
||||
|
export default defineMessages({ |
||||
|
calculating: 'calculating', |
||||
|
current_balance: 'Your current balance', |
||||
|
error_no_route: 'We were unable to find a route to your destination. Please try again later.', |
||||
|
error_not_enough_funds: 'You do not have enough funds available to make this payment.', |
||||
|
request_label_combined: 'Payment Request or Address', |
||||
|
request_label_offchain: 'Payment Request', |
||||
|
request_label_onchain: 'Address', |
||||
|
searching_routes: 'searching for routes', |
||||
|
next_block_confirmation: 'next block confirmation', |
||||
|
next: 'Next', |
||||
|
back: 'Back', |
||||
|
send: 'Send', |
||||
|
fee: 'Fee', |
||||
|
fee_range: 'between {minFee} and {maxFee} msat', |
||||
|
unknown: 'unknown', |
||||
|
amount: 'Amount', |
||||
|
per_byte: 'per byte', |
||||
|
upto: 'up to', |
||||
|
total: 'Total', |
||||
|
memo: 'Memo', |
||||
|
description: |
||||
|
'You can send Bitcoin (BTC) through the Lightning Network or make a On-Chain Transaction. Just paste your Lightning Payment Request or the Bitcoin Address in the field below. Zap will guide you to the process.' |
||||
|
}) |
@ -0,0 +1,12 @@ |
|||||
|
import system from '@rebass/components' |
||||
|
import { styles } from 'styled-system' |
||||
|
import { Form as InformedForm } from 'informed' |
||||
|
// Create an html input element that accepts all style props from styled-system.
|
||||
|
const Form = system( |
||||
|
{ |
||||
|
extend: InformedForm |
||||
|
}, |
||||
|
...Object.keys(styles) |
||||
|
) |
||||
|
|
||||
|
export default Form |
@ -0,0 +1,21 @@ |
|||||
|
import PropTypes from 'prop-types' |
||||
|
|
||||
|
const Truncate = ({ text, maxlen = 12 }) => { |
||||
|
if (!text) { |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
const truncatedText = |
||||
|
text.length < maxlen |
||||
|
? text |
||||
|
: text.substr(0, maxlen / 2) + '...' + text.substr(text.length - maxlen / 2) |
||||
|
|
||||
|
return truncatedText |
||||
|
} |
||||
|
|
||||
|
Truncate.propTypes = { |
||||
|
text: PropTypes.string.isRequired, |
||||
|
maxlen: PropTypes.number |
||||
|
} |
||||
|
|
||||
|
export default Truncate |
@ -0,0 +1,9 @@ |
|||||
|
import { defineMessages } from 'react-intl' |
||||
|
|
||||
|
/* eslint-disable max-len */ |
||||
|
export default defineMessages({ |
||||
|
required_field: 'This is a required field', |
||||
|
invalid_request: 'Not a valid {chain} request.', |
||||
|
valid_request: 'Valid {chain} request', |
||||
|
payreq_placeholder: 'Paste a Lightning Payment Request or Bitcoin Address here' |
||||
|
}) |
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
"rules": { |
||||
|
"react/display-name": 0 |
||||
|
} |
||||
|
} |
@ -0,0 +1,296 @@ |
|||||
|
/* eslint-disable max-len */ |
||||
|
|
||||
|
import React from 'react' |
||||
|
import { storiesOf } from '@storybook/react' |
||||
|
import { action } from '@storybook/addon-actions' |
||||
|
import { number, select, text } from '@storybook/addon-knobs' |
||||
|
import { State, Store } from '@sambego/storybook-state' |
||||
|
import { Modal, Page } from 'components/UI' |
||||
|
import { Pay, PayButtons, PayHeader, PaySummaryLightning, PaySummaryOnChain } from 'components/Pay' |
||||
|
|
||||
|
const delay = time => new Promise(resolve => setTimeout(() => resolve(), time)) |
||||
|
|
||||
|
const store = new Store({ |
||||
|
chain: 'bitcoin', |
||||
|
network: 'testnet', |
||||
|
cryptoName: 'Bitcoin', |
||||
|
walletBalance: 10000000, |
||||
|
channelBalance: 25000, |
||||
|
cryptoCurrency: 'btc', |
||||
|
cryptoCurrencyTicker: 'BTC', |
||||
|
cryptoCurrencies: [ |
||||
|
{ |
||||
|
key: 'btc', |
||||
|
name: 'BTC' |
||||
|
}, |
||||
|
{ |
||||
|
key: 'bits', |
||||
|
name: 'bits' |
||||
|
}, |
||||
|
{ |
||||
|
key: 'sats', |
||||
|
name: 'satoshis' |
||||
|
} |
||||
|
], |
||||
|
|
||||
|
fiatCurrency: 'USD', |
||||
|
fiatCurrencies: ['USD', 'EUR', 'GBP'], |
||||
|
|
||||
|
nodes: [ |
||||
|
{ |
||||
|
pub_key: '03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463', |
||||
|
alias: 'htlc.me' |
||||
|
} |
||||
|
], |
||||
|
routes: [], |
||||
|
|
||||
|
currentTicker: { |
||||
|
USD: { |
||||
|
last: 6477.78 |
||||
|
}, |
||||
|
EUR: { |
||||
|
last: 5656.01 |
||||
|
}, |
||||
|
GBP: { |
||||
|
last: 5052.73 |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
isProcessing: false |
||||
|
}) |
||||
|
|
||||
|
const mockPayInvoice = async () => { |
||||
|
store.set({ isProcessing: true }) |
||||
|
await delay(2000) |
||||
|
store.set({ isProcessing: false }) |
||||
|
} |
||||
|
|
||||
|
const mockSendCoins = async () => { |
||||
|
store.set({ isProcessing: true }) |
||||
|
await delay(2000) |
||||
|
store.set({ isProcessing: false }) |
||||
|
} |
||||
|
|
||||
|
const mockQueryFees = async () => { |
||||
|
store.set({ isQueryingFees: true }) |
||||
|
await delay(2000) |
||||
|
store.set({ |
||||
|
onchainFees: { |
||||
|
fastestFee: 8, |
||||
|
halfHourFee: 8, |
||||
|
hourFee: 4 |
||||
|
} |
||||
|
}) |
||||
|
store.set({ isQueryingFees: false }) |
||||
|
} |
||||
|
|
||||
|
const mockQueryRoutes = async pubKey => { |
||||
|
store.set({ isQueryingRoutes: true }) |
||||
|
await delay(2000) |
||||
|
const nodes = store.get('nodes') |
||||
|
if (nodes.find(n => n.pub_key === pubKey)) { |
||||
|
store.set({ |
||||
|
routes: [ |
||||
|
{ |
||||
|
total_time_lock: 547118, |
||||
|
total_fees: '0', |
||||
|
total_amt: '10000', |
||||
|
hops: [ |
||||
|
{ |
||||
|
chan_id: '565542601916153857', |
||||
|
chan_capacity: '15698', |
||||
|
amt_to_forward: '10000', |
||||
|
fee: '0', |
||||
|
expiry: 546974, |
||||
|
amt_to_forward_msat: '10000010', |
||||
|
fee_msat: '21' |
||||
|
} |
||||
|
], |
||||
|
total_fees_msat: '21', |
||||
|
total_amt_msat: '10000021' |
||||
|
}, |
||||
|
{ |
||||
|
total_time_lock: 547118, |
||||
|
total_fees: '0', |
||||
|
total_amt: '10000', |
||||
|
hops: [ |
||||
|
{ |
||||
|
chan_id: '565542601916153857', |
||||
|
chan_capacity: '15698', |
||||
|
amt_to_forward: '10000', |
||||
|
fee: '0', |
||||
|
expiry: 546974, |
||||
|
amt_to_forward_msat: '10000010', |
||||
|
fee_msat: '3' |
||||
|
} |
||||
|
], |
||||
|
total_fees_msat: '3', |
||||
|
total_amt_msat: '10000021' |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
} else { |
||||
|
store.set({ routes: [] }) |
||||
|
} |
||||
|
store.set({ isQueryingRoutes: false }) |
||||
|
} |
||||
|
|
||||
|
const setCryptoCurrency = key => { |
||||
|
const items = store.get('cryptoCurrencies') |
||||
|
const item = items.find(i => i.key === key) |
||||
|
store.set({ cryptoCurrency: item.key }) |
||||
|
store.set({ cryptoCurrencyTicker: item.name }) |
||||
|
} |
||||
|
|
||||
|
const setFiatCurrency = key => { |
||||
|
store.set({ fiatCurrency: key }) |
||||
|
} |
||||
|
|
||||
|
storiesOf('Containers.Pay', module) |
||||
|
.add('Pay', () => { |
||||
|
const network = select( |
||||
|
'Network', |
||||
|
{ |
||||
|
Testnet: 'testnet', |
||||
|
Mainnet: 'mainnet' |
||||
|
}, |
||||
|
'testnet' |
||||
|
) |
||||
|
|
||||
|
return ( |
||||
|
<Page css={{ height: 'calc(100vh - 40px)' }}> |
||||
|
<Modal onClose={action('clicked')}> |
||||
|
<State store={store}> |
||||
|
<Pay |
||||
|
width={1 / 2} |
||||
|
mx="auto" |
||||
|
// State
|
||||
|
isProcessing={store.get('isProcessing')} |
||||
|
chain={store.get('chain')} |
||||
|
channelBalance={store.get('channelBalance')} |
||||
|
network={network} |
||||
|
cryptoCurrency={store.get('cryptoCurrency')} |
||||
|
cryptoCurrencyTicker={store.get('cryptoCurrencyTicker')} |
||||
|
cryptoCurrencies={store.get('cryptoCurrencies')} |
||||
|
currentTicker={store.get('currentTicker')} |
||||
|
cryptoName={store.get('cryptoName')} |
||||
|
fiatCurrency={store.get('fiatCurrency')} |
||||
|
fiatCurrencies={store.get('fiatCurrencies')} |
||||
|
isQueryingFees={store.get('isQueryingFees')} |
||||
|
isQueryingRoutes={store.get('isQueryingRoutes')} |
||||
|
nodes={store.get('nodes')} |
||||
|
walletBalance={store.get('walletBalance')} |
||||
|
// Dispatch
|
||||
|
payInvoice={mockPayInvoice} |
||||
|
setCryptoCurrency={setCryptoCurrency} |
||||
|
setFiatCurrency={setFiatCurrency} |
||||
|
sendCoins={mockSendCoins} |
||||
|
queryFees={mockQueryFees} |
||||
|
queryRoutes={mockQueryRoutes} |
||||
|
/> |
||||
|
</State> |
||||
|
</Modal> |
||||
|
</Page> |
||||
|
) |
||||
|
}) |
||||
|
.addWithChapters('PayHeader', { |
||||
|
chapters: [ |
||||
|
{ |
||||
|
sections: [ |
||||
|
{ |
||||
|
title: 'On-chain', |
||||
|
sectionFn: () => <PayHeader title="Send Bitcoin" type="onchain" /> |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Off-chain', |
||||
|
sectionFn: () => <PayHeader title="Send Bitcoin" type="offchain" /> |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Generic', |
||||
|
sectionFn: () => <PayHeader title="Send Bitcoin" /> |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
.addWithChapters('PaySummary', { |
||||
|
chapters: [ |
||||
|
{ |
||||
|
sections: [ |
||||
|
{ |
||||
|
title: 'PaySummaryLightning', |
||||
|
sectionFn: () => ( |
||||
|
<PaySummaryLightning |
||||
|
// State
|
||||
|
cryptoCurrency={store.get('cryptoCurrency')} |
||||
|
cryptoCurrencyTicker={store.get('cryptoCurrencyTicker')} |
||||
|
cryptoCurrencies={store.get('cryptoCurrencies')} |
||||
|
currentTicker={store.get('currentTicker')} |
||||
|
fiatCurrency={store.get('fiatCurrency')} |
||||
|
fiatCurrencies={store.get('fiatCurrencies')} |
||||
|
minFee={12} |
||||
|
maxFee={18} |
||||
|
nodes={store.get('nodes')} |
||||
|
payReq={text( |
||||
|
'Lightning Invoice', |
||||
|
'lntb100u1pdaxza7pp5x73t3j7xgvkzgcdvzgpdg74k4pn0uhwuxlxu9qssytjn77x7zs4qdqqcqzysxqyz5vqd20eaq5uferzgzwasu5te3pla7gv8tzk8gcdxlj7lpkygvfdwndhwtl3ezn9ltjejl3hsp36ps3z3e5pp4rzp2hgqjqql80ec3hyzucq4d9axl' |
||||
|
)} |
||||
|
// Dispatch
|
||||
|
setCryptoCurrency={setCryptoCurrency} |
||||
|
setFiatCurrency={setFiatCurrency} |
||||
|
/> |
||||
|
), |
||||
|
options: { |
||||
|
decorator: story => <State store={store}>{story()}</State> |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
title: 'PaySummaryOnChain', |
||||
|
sectionFn: () => { |
||||
|
mockQueryFees() |
||||
|
return ( |
||||
|
<PaySummaryOnChain |
||||
|
// State
|
||||
|
address={text('Address', 'mmxyr3LNKbnbrf6jdGXZpCE4EDpMSZRf4c')} |
||||
|
amount={number('Amount (satoshis)', 10000)} |
||||
|
cryptoCurrency={store.get('cryptoCurrency')} |
||||
|
cryptoCurrencyTicker={store.get('cryptoCurrencyTicker')} |
||||
|
cryptoCurrencies={store.get('cryptoCurrencies')} |
||||
|
currentTicker={store.get('currentTicker')} |
||||
|
fiatCurrency={store.get('fiatCurrency')} |
||||
|
fiatCurrencies={store.get('fiatCurrencies')} |
||||
|
onchainFees={store.get('onchainFees')} |
||||
|
// Dispatch
|
||||
|
setCryptoCurrency={setCryptoCurrency} |
||||
|
setFiatCurrency={setFiatCurrency} |
||||
|
/> |
||||
|
) |
||||
|
}, |
||||
|
options: { |
||||
|
decorator: story => <State store={store}>{story()}</State> |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
.addWithChapters('PayButtons', { |
||||
|
chapters: [ |
||||
|
{ |
||||
|
sections: [ |
||||
|
{ |
||||
|
title: 'Default', |
||||
|
sectionFn: () => <PayButtons previousStep={action('clicked')} /> |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Disabled', |
||||
|
sectionFn: () => <PayButtons previousStep={action('clicked')} disabled /> |
||||
|
}, |
||||
|
{ |
||||
|
title: 'Processing', |
||||
|
sectionFn: () => <PayButtons previousStep={action('clicked')} processing /> |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}) |
@ -1 +1,6 @@ |
|||||
import 'jest-styled-components' |
import 'jest-styled-components' |
||||
|
|
||||
|
import { configure } from 'enzyme' |
||||
|
import Adapter from 'enzyme-adapter-react-16' |
||||
|
|
||||
|
configure({ adapter: new Adapter() }) |
||||
|
@ -0,0 +1,11 @@ |
|||||
|
import React from 'react' |
||||
|
import { shallow } from 'enzyme' |
||||
|
import toJSON from 'enzyme-to-json' |
||||
|
import { PayButtons } from 'components/Pay' |
||||
|
|
||||
|
describe('component.Form.PayButtons', () => { |
||||
|
it('should render correctly with default props', () => { |
||||
|
const wrapper = shallow(<PayButtons />) |
||||
|
expect(toJSON(wrapper)).toMatchSnapshot() |
||||
|
}) |
||||
|
}) |
@ -0,0 +1,25 @@ |
|||||
|
import React from 'react' |
||||
|
import { shallow } from 'enzyme' |
||||
|
import toJSON from 'enzyme-to-json' |
||||
|
import { PayHeader } from 'components/Pay' |
||||
|
|
||||
|
describe('component.Pay.PayHeader', () => { |
||||
|
describe('onchain', () => { |
||||
|
it('should render correctly', () => { |
||||
|
const wrapper = shallow(<PayHeader title="Send Bitcoin" type="onchain" />) |
||||
|
expect(toJSON(wrapper)).toMatchSnapshot() |
||||
|
}) |
||||
|
}) |
||||
|
describe('offchain', () => { |
||||
|
it('should render correctly', () => { |
||||
|
const wrapper = shallow(<PayHeader title="Send Bitcoin" type="offchain" />) |
||||
|
expect(toJSON(wrapper)).toMatchSnapshot() |
||||
|
}) |
||||
|
}) |
||||
|
describe('generic', () => { |
||||
|
it('should render correctly', () => { |
||||
|
const wrapper = shallow(<PayHeader title="Send Bitcoin" />) |
||||
|
expect(toJSON(wrapper)).toMatchSnapshot() |
||||
|
}) |
||||
|
}) |
||||
|
}) |
@ -0,0 +1,45 @@ |
|||||
|
import React from 'react' |
||||
|
import { shallow } from 'enzyme' |
||||
|
import toJSON from 'enzyme-to-json' |
||||
|
import { PaySummaryLightning } from 'components/Pay' |
||||
|
|
||||
|
const props = { |
||||
|
currentTicker: { |
||||
|
USD: { |
||||
|
last: 6477.78 |
||||
|
}, |
||||
|
EUR: { |
||||
|
last: 5656.01 |
||||
|
}, |
||||
|
GBP: { |
||||
|
last: 5052.73 |
||||
|
} |
||||
|
}, |
||||
|
cryptoCurrency: 'btc', |
||||
|
cryptoCurrencyTicker: 'BTC', |
||||
|
cryptoCurrencies: [ |
||||
|
{ |
||||
|
key: 'btc', |
||||
|
name: 'BTC' |
||||
|
}, |
||||
|
{ |
||||
|
key: 'bits', |
||||
|
name: 'bits' |
||||
|
}, |
||||
|
{ |
||||
|
key: 'sats', |
||||
|
name: 'satoshis' |
||||
|
} |
||||
|
], |
||||
|
fiatCurrency: 'USD', |
||||
|
/* eslint-disable max-len */ |
||||
|
payReq: |
||||
|
'lntb100u1pdaxza7pp5x73t3j7xgvkzgcdvzgpdg74k4pn0uhwuxlxu9qssytjn77x7zs4qdqqcqzysxqyz5vqd20eaq5uferzgzwasu5te3pla7gv8tzk8gcdxlj7lpkygvfdwndhwtl3ezn9ltjejl3hsp36ps3z3e5pp4rzp2hgqjqql80ec3hyzucq4d9axl', |
||||
|
setCryptoCurrency: jest.fn() |
||||
|
} |
||||
|
describe('component.Form.PaySummaryLightning', () => { |
||||
|
it('should render correctly', () => { |
||||
|
const wrapper = shallow(<PaySummaryLightning {...props} />) |
||||
|
expect(toJSON(wrapper)).toMatchSnapshot() |
||||
|
}) |
||||
|
}) |
@ -0,0 +1,45 @@ |
|||||
|
import React from 'react' |
||||
|
import { shallow } from 'enzyme' |
||||
|
import toJSON from 'enzyme-to-json' |
||||
|
import { PaySummaryOnChain } from 'components/Pay' |
||||
|
|
||||
|
const props = { |
||||
|
amount: 1000, |
||||
|
address: 'mmxyr3LNKbnbrf6jdGXZpCE4EDpMSZRf4c', |
||||
|
currentTicker: { |
||||
|
USD: { |
||||
|
last: 6477.78 |
||||
|
}, |
||||
|
EUR: { |
||||
|
last: 5656.01 |
||||
|
}, |
||||
|
GBP: { |
||||
|
last: 5052.73 |
||||
|
} |
||||
|
}, |
||||
|
cryptoCurrency: 'btc', |
||||
|
cryptoCurrencyTicker: 'BTC', |
||||
|
cryptoCurrencies: [ |
||||
|
{ |
||||
|
key: 'btc', |
||||
|
name: 'BTC' |
||||
|
}, |
||||
|
{ |
||||
|
key: 'bits', |
||||
|
name: 'bits' |
||||
|
}, |
||||
|
{ |
||||
|
key: 'sats', |
||||
|
name: 'satoshis' |
||||
|
} |
||||
|
], |
||||
|
fiatCurrency: 'USD', |
||||
|
setCryptoCurrency: jest.fn() |
||||
|
} |
||||
|
|
||||
|
describe('component.Form.PaySummaryOnchain', () => { |
||||
|
it('should render correctly', () => { |
||||
|
const wrapper = shallow(<PaySummaryOnChain {...props} />) |
||||
|
expect(toJSON(wrapper)).toMatchSnapshot() |
||||
|
}) |
||||
|
}) |
@ -0,0 +1,11 @@ |
|||||
|
import React from 'react' |
||||
|
import { shallow } from 'enzyme' |
||||
|
import toJSON from 'enzyme-to-json' |
||||
|
import { PaySummaryRow } from 'components/Pay' |
||||
|
|
||||
|
describe('component.Form.PaySummaryRow', () => { |
||||
|
it('should render correctly', () => { |
||||
|
const wrapper = shallow(<PaySummaryRow left="left contnet" right="right content" />) |
||||
|
expect(toJSON(wrapper)).toMatchSnapshot() |
||||
|
}) |
||||
|
}) |
@ -0,0 +1,54 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`component.Form.PayButtons should render correctly with default props 1`] = ` |
||||
|
<Styled(styled.div) |
||||
|
alignItems="center" |
||||
|
justifyContent="space-between" |
||||
|
> |
||||
|
<styled.div |
||||
|
width={0.2} |
||||
|
> |
||||
|
<Button |
||||
|
disabled={false} |
||||
|
onClick={[Function]} |
||||
|
processing={false} |
||||
|
px={0} |
||||
|
size="medium" |
||||
|
type="button" |
||||
|
variant="secondary" |
||||
|
> |
||||
|
<Styled(styled.div)> |
||||
|
<Styled(styled.div)> |
||||
|
<SvgBigArrowLeft /> |
||||
|
</Styled(styled.div)> |
||||
|
<Styled(styled.div) |
||||
|
ml={1} |
||||
|
> |
||||
|
<FormattedMessage |
||||
|
defaultMessage="Back" |
||||
|
id="components.Pay.back" |
||||
|
values={Object {}} |
||||
|
/> |
||||
|
</Styled(styled.div)> |
||||
|
</Styled(styled.div)> |
||||
|
</Button> |
||||
|
</styled.div> |
||||
|
<Button |
||||
|
disabled={false} |
||||
|
mx="auto" |
||||
|
processing={false} |
||||
|
size="medium" |
||||
|
type="submit" |
||||
|
variant="normal" |
||||
|
> |
||||
|
<FormattedMessage |
||||
|
defaultMessage="Next" |
||||
|
id="components.Pay.next" |
||||
|
values={Object {}} |
||||
|
/> |
||||
|
</Button> |
||||
|
<styled.div |
||||
|
width={0.2} |
||||
|
/> |
||||
|
</Styled(styled.div)> |
||||
|
`; |
@ -0,0 +1,96 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`component.Pay.PayHeader generic should render correctly 1`] = ` |
||||
|
<Text |
||||
|
textAlign="center" |
||||
|
> |
||||
|
<styled.div |
||||
|
css={ |
||||
|
Object { |
||||
|
"height": "55px", |
||||
|
} |
||||
|
} |
||||
|
mx="auto" |
||||
|
> |
||||
|
<SvgPaperPlane |
||||
|
height="35px" |
||||
|
width="35px" |
||||
|
/> |
||||
|
</styled.div> |
||||
|
<Heading1 |
||||
|
mx="auto" |
||||
|
> |
||||
|
Send Bitcoin |
||||
|
</Heading1> |
||||
|
<Heading4 |
||||
|
mx="auto" |
||||
|
> |
||||
|
|
||||
|
|
||||
|
</Heading4> |
||||
|
</Text> |
||||
|
`; |
||||
|
|
||||
|
exports[`component.Pay.PayHeader offchain should render correctly 1`] = ` |
||||
|
<Text |
||||
|
textAlign="center" |
||||
|
> |
||||
|
<styled.div |
||||
|
css={ |
||||
|
Object { |
||||
|
"height": "55px", |
||||
|
} |
||||
|
} |
||||
|
mx="auto" |
||||
|
> |
||||
|
<SvgLightning |
||||
|
height="45px" |
||||
|
width="45px" |
||||
|
/> |
||||
|
</styled.div> |
||||
|
<Heading1 |
||||
|
mx="auto" |
||||
|
> |
||||
|
Send Bitcoin |
||||
|
</Heading1> |
||||
|
<Heading4 |
||||
|
mx="auto" |
||||
|
> |
||||
|
|
||||
|
|
||||
|
Lightning Payment |
||||
|
</Heading4> |
||||
|
</Text> |
||||
|
`; |
||||
|
|
||||
|
exports[`component.Pay.PayHeader onchain should render correctly 1`] = ` |
||||
|
<Text |
||||
|
textAlign="center" |
||||
|
> |
||||
|
<styled.div |
||||
|
css={ |
||||
|
Object { |
||||
|
"height": "55px", |
||||
|
} |
||||
|
} |
||||
|
mx="auto" |
||||
|
> |
||||
|
<SvgOnchain |
||||
|
height="45px" |
||||
|
width="45px" |
||||
|
/> |
||||
|
</styled.div> |
||||
|
<Heading1 |
||||
|
mx="auto" |
||||
|
> |
||||
|
Send Bitcoin |
||||
|
</Heading1> |
||||
|
<Heading4 |
||||
|
mx="auto" |
||||
|
> |
||||
|
|
||||
|
On-Chain Payment |
||||
|
|
||||
|
</Heading4> |
||||
|
</Text> |
||||
|
`; |
@ -0,0 +1,129 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`component.Form.PaySummaryLightning should render correctly 1`] = ` |
||||
|
<Fragment> |
||||
|
<styled.div |
||||
|
pb={2} |
||||
|
> |
||||
|
<Styled(styled.div) |
||||
|
alignItems="center" |
||||
|
> |
||||
|
<styled.div |
||||
|
width={0.45454545454545453} |
||||
|
> |
||||
|
<Styled(styled.div) |
||||
|
alignItems="baseline" |
||||
|
flexWrap="wrap" |
||||
|
> |
||||
|
<styled.div> |
||||
|
<Text |
||||
|
fontSize={6} |
||||
|
textAlign="left" |
||||
|
> |
||||
|
<Value |
||||
|
currency="btc" |
||||
|
value={10000} |
||||
|
/> |
||||
|
</Text> |
||||
|
</styled.div> |
||||
|
<WithTheme(Dropdown) |
||||
|
activeKey="btc" |
||||
|
items={ |
||||
|
Array [ |
||||
|
Object { |
||||
|
"key": "btc", |
||||
|
"name": "BTC", |
||||
|
}, |
||||
|
Object { |
||||
|
"key": "bits", |
||||
|
"name": "bits", |
||||
|
}, |
||||
|
Object { |
||||
|
"key": "sats", |
||||
|
"name": "satoshis", |
||||
|
}, |
||||
|
] |
||||
|
} |
||||
|
ml={2} |
||||
|
onChange={[MockFunction]} |
||||
|
/> |
||||
|
</Styled(styled.div)> |
||||
|
<Text |
||||
|
color="gray" |
||||
|
> |
||||
|
≈ |
||||
|
<FormattedNumber |
||||
|
currency="USD" |
||||
|
style="currency" |
||||
|
value={0.647778} |
||||
|
/> |
||||
|
</Text> |
||||
|
</styled.div> |
||||
|
<styled.div |
||||
|
width={0.09090909090909091} |
||||
|
> |
||||
|
<Text |
||||
|
color="lightningOrange" |
||||
|
textAlign="center" |
||||
|
> |
||||
|
<SvgBigArrowRight |
||||
|
height="28px" |
||||
|
width="40px" |
||||
|
/> |
||||
|
</Text> |
||||
|
</styled.div> |
||||
|
<styled.div |
||||
|
width={0.45454545454545453} |
||||
|
> |
||||
|
<Text |
||||
|
className="hint--bottom-left" |
||||
|
data-hint="03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463" |
||||
|
textAlign="right" |
||||
|
> |
||||
|
<Truncate |
||||
|
text="03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463" |
||||
|
/> |
||||
|
</Text> |
||||
|
</styled.div> |
||||
|
</Styled(styled.div)> |
||||
|
</styled.div> |
||||
|
<Bar /> |
||||
|
<PaySummaryRow |
||||
|
left={ |
||||
|
<FormattedMessage |
||||
|
defaultMessage="Fee" |
||||
|
id="components.Pay.fee" |
||||
|
values={Object {}} |
||||
|
/> |
||||
|
} |
||||
|
right={ |
||||
|
<FormattedMessage |
||||
|
defaultMessage="unknown" |
||||
|
id="components.Pay.unknown" |
||||
|
values={Object {}} |
||||
|
/> |
||||
|
} |
||||
|
/> |
||||
|
<Bar /> |
||||
|
<PaySummaryRow |
||||
|
left={ |
||||
|
<FormattedMessage |
||||
|
defaultMessage="Total" |
||||
|
id="components.Pay.total" |
||||
|
values={Object {}} |
||||
|
/> |
||||
|
} |
||||
|
right={ |
||||
|
<React.Fragment> |
||||
|
<Value |
||||
|
currency="btc" |
||||
|
value={10000} |
||||
|
/> |
||||
|
|
||||
|
BTC |
||||
|
</React.Fragment> |
||||
|
} |
||||
|
/> |
||||
|
<Bar /> |
||||
|
</Fragment> |
||||
|
`; |
@ -0,0 +1,128 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`component.Form.PaySummaryOnchain should render correctly 1`] = ` |
||||
|
<Fragment> |
||||
|
<styled.div |
||||
|
pb={2} |
||||
|
> |
||||
|
<Styled(styled.div) |
||||
|
alignItems="center" |
||||
|
> |
||||
|
<styled.div |
||||
|
width={0.45454545454545453} |
||||
|
> |
||||
|
<Styled(styled.div) |
||||
|
alignItems="baseline" |
||||
|
flexWrap="wrap" |
||||
|
> |
||||
|
<styled.div> |
||||
|
<Text |
||||
|
fontSize={6} |
||||
|
textAlign="left" |
||||
|
> |
||||
|
<Value |
||||
|
currency="btc" |
||||
|
value={1000} |
||||
|
/> |
||||
|
</Text> |
||||
|
</styled.div> |
||||
|
<WithTheme(Dropdown) |
||||
|
activeKey="btc" |
||||
|
items={ |
||||
|
Array [ |
||||
|
Object { |
||||
|
"key": "btc", |
||||
|
"name": "BTC", |
||||
|
}, |
||||
|
Object { |
||||
|
"key": "bits", |
||||
|
"name": "bits", |
||||
|
}, |
||||
|
Object { |
||||
|
"key": "sats", |
||||
|
"name": "satoshis", |
||||
|
}, |
||||
|
] |
||||
|
} |
||||
|
ml={2} |
||||
|
onChange={[MockFunction]} |
||||
|
/> |
||||
|
</Styled(styled.div)> |
||||
|
<Text |
||||
|
color="gray" |
||||
|
> |
||||
|
≈ |
||||
|
<FormattedNumber |
||||
|
currency="USD" |
||||
|
style="currency" |
||||
|
value={0.0647778} |
||||
|
/> |
||||
|
</Text> |
||||
|
</styled.div> |
||||
|
<styled.div |
||||
|
width={0.09090909090909091} |
||||
|
> |
||||
|
<Text |
||||
|
color="lightningOrange" |
||||
|
textAlign="center" |
||||
|
> |
||||
|
<SvgBigArrowRight |
||||
|
height="28px" |
||||
|
width="40px" |
||||
|
/> |
||||
|
</Text> |
||||
|
</styled.div> |
||||
|
<styled.div |
||||
|
width={0.45454545454545453} |
||||
|
> |
||||
|
<Text |
||||
|
className="hint--bottom-left" |
||||
|
data-hint="mmxyr3LNKbnbrf6jdGXZpCE4EDpMSZRf4c" |
||||
|
textAlign="right" |
||||
|
> |
||||
|
<Truncate |
||||
|
text="mmxyr3LNKbnbrf6jdGXZpCE4EDpMSZRf4c" |
||||
|
/> |
||||
|
</Text> |
||||
|
</styled.div> |
||||
|
</Styled(styled.div)> |
||||
|
</styled.div> |
||||
|
<Bar /> |
||||
|
<PaySummaryRow |
||||
|
left={ |
||||
|
<FormattedMessage |
||||
|
defaultMessage="Fee" |
||||
|
id="components.Pay.fee" |
||||
|
values={Object {}} |
||||
|
/> |
||||
|
} |
||||
|
right={ |
||||
|
<FormattedMessage |
||||
|
defaultMessage="unknown" |
||||
|
id="components.Pay.unknown" |
||||
|
values={Object {}} |
||||
|
/> |
||||
|
} |
||||
|
/> |
||||
|
<Bar /> |
||||
|
<PaySummaryRow |
||||
|
left={ |
||||
|
<FormattedMessage |
||||
|
defaultMessage="Total" |
||||
|
id="components.Pay.total" |
||||
|
values={Object {}} |
||||
|
/> |
||||
|
} |
||||
|
right={ |
||||
|
<React.Fragment> |
||||
|
<Value |
||||
|
currency="btc" |
||||
|
value={1000} |
||||
|
/> |
||||
|
|
||||
|
BTC |
||||
|
</React.Fragment> |
||||
|
} |
||||
|
/> |
||||
|
</Fragment> |
||||
|
`; |
@ -0,0 +1,30 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`component.Form.PaySummaryRow should render correctly 1`] = ` |
||||
|
<styled.div |
||||
|
py={2} |
||||
|
> |
||||
|
<Styled(styled.div) |
||||
|
alignItems="center" |
||||
|
> |
||||
|
<styled.div |
||||
|
width={0.5} |
||||
|
> |
||||
|
<Text |
||||
|
fontWeight="normal" |
||||
|
> |
||||
|
left contnet |
||||
|
</Text> |
||||
|
</styled.div> |
||||
|
<styled.div |
||||
|
width={0.5} |
||||
|
> |
||||
|
<Text |
||||
|
textAlign="right" |
||||
|
> |
||||
|
right content |
||||
|
</Text> |
||||
|
</styled.div> |
||||
|
</Styled(styled.div)> |
||||
|
</styled.div> |
||||
|
`; |
@ -0,0 +1,10 @@ |
|||||
|
import React from 'react' |
||||
|
import renderer from 'react-test-renderer' |
||||
|
import { Form } from 'components/UI' |
||||
|
|
||||
|
describe('component.UI.Form', () => { |
||||
|
it('should render correctly', () => { |
||||
|
const tree = renderer.create(<Form />).toJSON() |
||||
|
expect(tree).toMatchSnapshot() |
||||
|
}) |
||||
|
}) |
@ -0,0 +1,21 @@ |
|||||
|
import React from 'react' |
||||
|
import renderer from 'react-test-renderer' |
||||
|
import { Truncate } from 'components/UI' |
||||
|
|
||||
|
describe('component.UI.Truncate', () => { |
||||
|
it('should truncate text to 12 chars by default', () => { |
||||
|
const tree = renderer |
||||
|
.create(<Truncate text="Lorem ipsum dolor sit amet, consectetur adipiscing elit" />) |
||||
|
.toJSON() |
||||
|
expect(tree).toMatchSnapshot() |
||||
|
}) |
||||
|
|
||||
|
it('should truncate test to a specific length when the maxlen parm is provided', () => { |
||||
|
const tree = renderer |
||||
|
.create( |
||||
|
<Truncate text="Lorem ipsum dolor sit amet, consectetur adipiscing elit" maxlen={30} /> |
||||
|
) |
||||
|
.toJSON() |
||||
|
expect(tree).toMatchSnapshot() |
||||
|
}) |
||||
|
}) |
@ -0,0 +1,9 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`component.UI.Form should render correctly 1`] = ` |
||||
|
<form |
||||
|
className="" |
||||
|
onReset={[Function]} |
||||
|
onSubmit={[Function]} |
||||
|
/> |
||||
|
`; |
@ -0,0 +1,5 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`component.UI.Truncate should truncate test to a specific length when the maxlen parm is provided 1`] = `"Lorem ipsum dol...adipiscing elit"`; |
||||
|
|
||||
|
exports[`component.UI.Truncate should truncate text to 12 chars by default 1`] = `"Lorem ...g elit"`; |
Loading…
Reference in new issue