diff --git a/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap b/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap index facbd5f73..0439328f6 100644 --- a/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap +++ b/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap @@ -1,701 +1,701 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

edit.svg
0.06 XEC
,
,
XEC Airdrop Calculator
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; exports[`Without wallet defined 1`] = ` Array [
edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; diff --git a/cashtab/src/components/Alias/Alias.js b/cashtab/src/components/Alias/Alias.js index ed427cfe5..1d56288fb 100644 --- a/cashtab/src/components/Alias/Alias.js +++ b/cashtab/src/components/Alias/Alias.js @@ -1,445 +1,574 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { WalletContext } from 'utils/context'; import PropTypes from 'prop-types'; import WalletLabel from 'components/Common/WalletLabel.js'; import { ZeroBalanceHeader, SidePaddingCtn, WalletInfoCtn, } from 'components/Common/Atoms'; -import { DestinationAddressSingle } from 'components/Common/EnhancedInputs'; -import { AntdFormWrapper } from 'components/Common/EnhancedInputs'; +import { + AntdFormWrapper, + AliasInput, + AliasAddressInput, + CashtabCheckbox, +} from 'components/Common/EnhancedInputs'; import { Form, Modal } from 'antd'; import { SmartButton } from 'components/Common/PrimaryButton'; import BalanceHeader from 'components/Common/BalanceHeader'; import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat'; import { Row, Col } from 'antd'; import { UserOutlined } from '@ant-design/icons'; import { getWalletState, fromSatoshisToXec, getAliasRegistrationFee, convertToEcashPrefix, getAliasByteSize, } from 'utils/cashMethods'; import { isAliasAvailable, isAddressRegistered } from 'utils/chronik'; import { currency } from 'components/Common/Ticker.js'; import { registerNewAlias } from 'utils/transactions'; import { errorNotification, registerAliasNotification, } from 'components/Common/Notifications'; import { isAliasFormat, isValidAliasString } from 'utils/validation'; import { getPendingAliases } from 'utils/aliasUtils'; +import cashaddr from 'ecashaddrjs'; + +export const CheckboxContainer = styled.div` + text-align: left; + margin-bottom: 12px; +`; export const NamespaceCtn = styled.div` width: 100%; margin-top: 50px; margin-bottom: 20px; overflow-wrap: break-word; h2 { color: ${props => props.theme.contrast}; margin: 0 0 20px; } h3 { color: ${props => props.theme.contrast}; margin: 0 0 10px; } white-space: pre-wrap; `; const StyledSpacer = styled.div` height: 1px; width: 100%; background-color: ${props => props.theme.lightWhite}; margin: 60px 0 50px; `; const Alias = ({ passLoadingStatus }) => { const ContextValue = React.useContext(WalletContext); const { wallet, fiatPrice, cashtabSettings, chronik, changeCashtabSettings, cashtabCache, isAliasServerOnline, } = ContextValue; const walletState = getWalletState(wallet); const { balances } = walletState; const [formData, setFormData] = useState({ aliasName: '', aliasAddress: '', }); + const [useThisAddressChecked, setUseThisAddressChecked] = useState(false); const [isValidAliasInput, setIsValidAliasInput] = useState(false); // tracks whether to activate the registration button + const [isValidAliasAddressInput, setIsValidAliasAddressInput] = + useState(false); // tracks whether to activate the registration button const [aliasValidationError, setAliasValidationError] = useState(false); + const [aliasAddressValidationError, setAliasAddressValidationError] = + useState(false); const [activeWalletAliases, setActiveWalletAliases] = useState([]); // stores the list of aliases registered to this active wallet const [aliasLength, setAliasLength] = useState(false); // real time tracking of alias char length const [aliasFee, setAliasFee] = useState(false); // real time tracking of alias registration fee // Show a confirmation modal on alias registrations const [isModalVisible, setIsModalVisible] = useState(false); useEffect(() => { passLoadingStatus(false); }, [balances.totalBalance]); useEffect(async () => { // only run this useEffect block if wallet or cashtabCache is defined if ( !wallet || typeof wallet === 'undefined' || !cashtabCache || typeof cashtabCache === 'undefined' ) { return; } passLoadingStatus(true); - // Set address of active wallet to default alias registration address - // Use formdata approach as we will later add a form field for aliasAddress - setFormData(formData => ({ - ...formData, - aliasAddress: wallet.Path1899.cashAddress, - })); + // Default to registering the user's active wallet + // Must be called in this useEffect to ensure that wallet is loaded + // Call with this function to ensure that checkbox state and checkbox are updated + handleDefaultAddressCheckboxChange({ target: { checked: true } }); // check whether the address is attached to an onchain alias on page load const walletHasAlias = isAddressRegistered( wallet, cashtabCache.aliasCache, ); // retrieve aliases for this active wallet from cache for rendering on the frontend if ( walletHasAlias && cashtabCache.aliasCache && cashtabCache.aliasCache.cachedAliasCount > 0 ) { const thisAddress = convertToEcashPrefix( wallet.Path1899.cashAddress, ); // filter for aliases that matches this wallet's address const registeredAliasesToWallet = cashtabCache.aliasCache.aliases.filter( alias => alias.address === thisAddress, ); const appendedWithPendingAliases = await appendWithPendingAliases( registeredAliasesToWallet, ); setActiveWalletAliases([...new Set(appendedWithPendingAliases)]); // new Set() removes duplicate entries } passLoadingStatus(false); }, [wallet.name, cashtabCache.aliasCache.aliases]); const handleOk = () => { setIsModalVisible(false); registerAlias(); }; const handleCancel = () => { setIsModalVisible(false); }; const registerAlias = async () => { passLoadingStatus(true); if (!isAliasServerOnline) { // error notification on alias-server being unavailable errorNotification( null, 'Unable to connect to alias server, please try again later', 'Alias-server status check', ); passLoadingStatus(false); return; } // note: input already validated via handleAliasNameInput() const aliasInput = formData.aliasName; const aliasAddress = formData.aliasAddress; // check if the user is trying to essentially register chicken.xec.xec const doubleExtensionInput = isAliasFormat(aliasInput); if (doubleExtensionInput) { errorNotification( null, 'Please input an alias without the ".xec"', 'Alias extension check', ); passLoadingStatus(false); return; } const aliasAvailable = await isAliasAvailable( aliasInput, cashtabCache.aliasCache, ); if (aliasAvailable) { // calculate registration fee based on chars const registrationFee = getAliasRegistrationFee(aliasInput); console.log( 'Registration fee for ' + aliasInput + ' is ' + registrationFee + ' sats.', ); console.log( `Alias ${aliasInput} is available. Broadcasting registration transaction.`, ); try { const result = await registerNewAlias( chronik, wallet, currency.defaultFee, aliasInput, aliasAddress, registrationFee, ); registerAliasNotification(result.explorerLink, aliasInput); // allow alias server to process the pending alias const delay = ms => new Promise(res => setTimeout(res, ms)); await delay(1000); // 1 second const appendedWithPendingAliases = await appendWithPendingAliases(activeWalletAliases); setActiveWalletAliases([ ...new Set(appendedWithPendingAliases), ]); // new Set() removes duplicate entries } catch (err) { handleAliasRegistrationError(err); } setIsValidAliasInput(true); } else { // error notification on alias being unavailable errorNotification( null, 'This alias [' + aliasInput + '] has already been taken, please try another alias', 'Alias availability check', ); } passLoadingStatus(false); }; const appendWithPendingAliases = async currentActiveWalletAliases => { // retrieve the pending aliases and add to the registered aliases list for this wallet let pendingAliasesArray = await getPendingAliases(); if (!pendingAliasesArray) { return currentActiveWalletAliases; } // append the pending indicator for (let i = 0; i < pendingAliasesArray.length; i += 1) { pendingAliasesArray[i].alias = pendingAliasesArray[i].alias + ' (pending)'; } // filter to pending aliases matching this active wallet const thisAddress = convertToEcashPrefix(wallet.Path1899.cashAddress); const pendingAndConfirmedAliases = pendingAliasesArray.filter( element => element.address === thisAddress, ); // merge pending with confirmed list let tempActiveWalletAliases = currentActiveWalletAliases; tempActiveWalletAliases = tempActiveWalletAliases.concat( pendingAndConfirmedAliases, ); return tempActiveWalletAliases; }; const handleAliasNameInput = e => { const { name, value } = e.target; const validAliasInput = isValidAliasString(value); const aliasInputByteSize = getAliasByteSize(value); if ( value && value.trim() !== '' && aliasInputByteSize <= currency.aliasSettings.aliasMaxLength && validAliasInput ) { setIsValidAliasInput(true); const registrationFee = getAliasRegistrationFee(value); setAliasFee(registrationFee); setAliasLength(aliasInputByteSize); setAliasValidationError(false); } else { setAliasValidationError( 'Please enter an alias (lowercase a-z, 0-9) between 1 and 21 bytes', ); setIsValidAliasInput(false); setAliasFee(false); setAliasLength(false); } setFormData(p => ({ ...p, [name]: value, })); }; + const handleDefaultAddressCheckboxChange = e => { + /* handleDefaultAddressCheckboxChange + * + * Function to handle user action of checking or unchecking the + * checkbox on this page labeled 'Register active wallet address' + * + * May be called programmatically by mocking the usual js event + * of a user checking the box + * + * If the box is checked, set formData for aliasAddress to the active wallet's address + * If the box is unchecked, clear formData for aliasAddress + */ + const checked = e.target.checked; + setUseThisAddressChecked(checked); + if (checked) { + // Set address of active wallet to default alias registration address + handleAliasAddressInput({ + target: { + name: 'aliasAddress', + value: wallet.Path1899.cashAddress, + }, + }); + } else { + // Clear the form if the user unchecks + handleAliasAddressInput({ + target: { + name: 'aliasAddress', + value: '', + }, + }); + } + }; + + const handleAliasAddressInput = e => { + /* handleAliasAddressInput + * + * Function called to handle any changes to the aliasAddress input form + * + * May be called programmatically by mocking the usual js event + * of a user updating the addressName input field + */ + let { name, value } = e.target; + + // remove any whitespaces + value = value.trim(); + + // Validate + let decoded; + let isValidAddress = false; + try { + decoded = cashaddr.decode(value, true); + const { hash } = decoded; + // We only support 20-byte payloads + isValidAddress = hash.length === 40; + } catch (err) { + // Invalid cashaddress + // Log to console for user support + console.log(`Invalid address`, err); + } + + if (isValidAddress) { + setIsValidAliasAddressInput(true); + setAliasAddressValidationError(false); + } else { + setAliasAddressValidationError( + 'Invalid alias registration address.', + ); + setIsValidAliasAddressInput(false); + } + + setFormData(p => ({ + ...p, + [name]: value, + })); + }; + function handleAliasRegistrationError(errorObj) { // Set loading to false here as well, as balance may not change depending on where error occured in try loop passLoadingStatus(false); let message; if ( errorObj.error && errorObj.error.includes( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)', ) ) { message = `The address you are trying to register has too many unconfirmed ancestors (limit 50). Registration will be possible after a block confirmation. Try again in about 10 minutes.`; } else { message = errorObj.message || errorObj.error || JSON.stringify(errorObj); } errorNotification(errorObj, message, 'Registering Alias'); } return ( <>

{`Are you sure you want to register the alias '${ formData.aliasName }' for ${fromSatoshisToXec(aliasFee)} XECs?`}

{!balances.totalBalance ? ( You currently have 0 {currency.ticker}
Deposit some funds to use this feature
) : ( <> {fiatPrice !== null && ( )} )}

eCash Namespace Alias

- handleAliasNameInput(e), required: true, }} /> + + + Register active wallet address + + + {!useThisAddressChecked && ( + + handleAliasAddressInput( + e, + ), + required: true, + }} + /> + )} {aliasLength && aliasFee && `Registration fee for this ${aliasLength} byte Alias is ${fromSatoshisToXec( aliasFee, )} XEC`} setIsModalVisible(true) } > Register Alias

 Registered aliases

{activeWalletAliases && activeWalletAliases.length > 0 ? activeWalletAliases .map( alias => alias.alias + '.xec', ) .join('\n') : 'N/A'}

); }; /* passLoadingStatus must receive a default prop that is a function in order to pass the rendering unit test in Alias.test.js status => {console.log(status)} is an arbitrary stub function */ Alias.defaultProps = { passLoadingStatus: status => { console.log(status); }, }; Alias.propTypes = { passLoadingStatus: PropTypes.func, }; export default Alias; diff --git a/cashtab/src/components/Alias/__tests__/__snapshots__/Alias.test.js.snap b/cashtab/src/components/Alias/__tests__/__snapshots__/Alias.test.js.snap index 4bfb50f6b..369790893 100644 --- a/cashtab/src/components/Alias/__tests__/__snapshots__/Alias.test.js.snap +++ b/cashtab/src/components/Alias/__tests__/__snapshots__/Alias.test.js.snap @@ -1,1351 +1,1643 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with XEC balances 1`] = ` Array [

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,

eCash Namespace Alias

. xec
+
+ +

 Registered aliases

N/A

, ] `; exports[`Wallet with XEC balances and tokens 1`] = ` Array [

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,

eCash Namespace Alias

. xec
+
+ +
+
+
+
+
+
+
+ + + + + + + + +
+
+
+
+
+

 Registered aliases

N/A

, ] `; exports[`Wallet with XEC balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

edit.svg
0.06 XEC
,

eCash Namespace Alias

. xec
+
+ +

 Registered aliases

N/A

, ] `; exports[`Wallet without XEC balance 1`] = ` Array [

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,

eCash Namespace Alias

. xec
+
+ +
+
+
+
+
+
+
+ + + + + + + + +
+
+
+
+
+

 Registered aliases

N/A

, ] `; exports[`Without wallet defined 1`] = ` Array [
edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,

eCash Namespace Alias

. xec
+
+ +
+
+
+
+
+
+
+ + + + + + + + +
+
+
+
+
+

 Registered aliases

N/A

, ] `; diff --git a/cashtab/src/components/Common/CustomIcons.js b/cashtab/src/components/Common/CustomIcons.js index c4f36a588..f2855021a 100644 --- a/cashtab/src/components/Common/CustomIcons.js +++ b/cashtab/src/components/Common/CustomIcons.js @@ -1,201 +1,206 @@ import * as React from 'react'; import styled from 'styled-components'; import { CopyOutlined, DollarOutlined, LoadingOutlined, WalletOutlined, QrcodeOutlined, SettingOutlined, LockOutlined, ContactsOutlined, FireOutlined, } from '@ant-design/icons'; import { Image } from 'antd'; import { currency } from 'components/Common/Ticker'; import { ReactComponent as Send } from 'assets/send.svg'; import { ReactComponent as Receive } from 'assets/receive.svg'; import { ReactComponent as Genesis } from 'assets/flask.svg'; import { ReactComponent as Unparsed } from 'assets/alert-circle.svg'; import { ReactComponent as Home } from 'assets/home.svg'; import { ReactComponent as Settings } from 'assets/cog.svg'; import { ReactComponent as CopySolid } from 'assets/copy.svg'; import { ReactComponent as LinkSolid } from 'assets/external-link-square-alt.svg'; import { ReactComponent as Airdrop } from 'assets/airdrop-icon.svg'; import { ReactComponent as Pdf } from 'assets/file-pdf.svg'; import { ReactComponent as Plus } from 'assets/plus.svg'; import { ReactComponent as Download } from 'assets/download.svg'; import { ReactComponent as Edit } from 'assets/edit.svg'; import { ReactComponent as Trashcan } from 'assets/trashcan.svg'; import { ReactComponent as EyeSVG } from 'assets/eye.svg'; import { ReactComponent as EyeInvisibleSVG } from 'assets/eye-invisible.svg'; import { ReactComponent as Audit } from 'assets/audit.svg'; import { ReactComponent as Mail } from 'assets/mail.svg'; export const CashLoadingIcon = ; import { ReactComponent as User } from 'assets/user.svg'; export const CashReceivedNotificationIcon = () => ( ); export const TokenReceivedNotificationIcon = () => ( ); export const MessageSignedNotificationIcon = () => ( ); export const ThemedBurnOutlined = styled(FireOutlined)` color: ${props => props.theme.eCashPurple} !important; `; export const ThemedCopyOutlined = styled(CopyOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedDollarOutlined = styled(DollarOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedWalletOutlined = styled(WalletOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedQrcodeOutlined = styled(QrcodeOutlined)` color: ${props => props.theme.walletBackground} !important; `; export const ThemedSettingOutlined = styled(SettingOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedLockOutlined = styled(LockOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedContactsOutlined = styled(ContactsOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedContactSendOutlined = styled(Send)` color: ${props => props.theme.icons.outlined} !important; transform: rotate(-35deg); padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedCopySolid = styled(CopySolid)` fill: ${props => props.theme.contrast}; padding: 0rem 0rem 0.27rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedLinkSolid = styled(LinkSolid)` fill: ${props => props.theme.contrast}; padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedPdfSolid = styled(Pdf)` fill: ${props => props.theme.contrast}; padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedPlusOutlined = styled(Plus)` fill: ${props => props.theme.contrast}; padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedDownloadOutlined = styled(Download)` fill: ${props => props.theme.contrast}; padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedEditOutlined = styled(Edit)` stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; width: 20px; height: 20px; cursor: pointer; `; export const ThemedTrashcanOutlined = styled(Trashcan)` stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; width: 20px; height: 20px; cursor: pointer; `; export const ThemedMailOutlined = styled(Mail)` stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; width: 20px; height: 20px; cursor: pointer; `; export const ThemedEyeSVG = styled(EyeSVG)` fill: ${props => props.theme.buttons.primary.color}; width: 15px; height: 15px; cursor: pointer; `; export const ThemedInvisibleEyeSVG = styled(EyeInvisibleSVG)` fill: ${props => props.theme.buttons.primary.color}; width: 15px; height: 15px; cursor: pointer; `; export const ThemedSignAndVerifyMsg = styled(Audit)` min-width: 24px; `; export const ThemedUserProfileIcon = styled(User)` height: 33px; width: 30px; `; +export const ThemedAliasOutlined = styled(User)` + fill: ${props => props.theme.icons.outlined} !important; + height: 12px; + width: 12px; +`; export const LoadingBlock = styled.div` width: 100%; display: flex; align-items: center; justify-content: center; padding: 24px; flex-direction: column; svg { width: 50px; height: 50px; fill: ${props => props.theme.eCashBlue}; } `; export const CashLoader = () => ( ); export const ReceiveIcon = () => ; export const GenesisIcon = () => ; export const UnparsedIcon = () => ; export const HomeIcon = () => ; export const SettingsIcon = () => ; export const AirdropIcon = () => ; export const SendIcon = styled(Send)` transform: rotate(-35deg); `; export const AliasIcon = styled(User)``; export const CustomSpinner = ; diff --git a/cashtab/src/components/Common/EnhancedInputs.js b/cashtab/src/components/Common/EnhancedInputs.js index 9a86c94cf..d801bcb73 100644 --- a/cashtab/src/components/Common/EnhancedInputs.js +++ b/cashtab/src/components/Common/EnhancedInputs.js @@ -1,409 +1,456 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { Form, Input, Select } from 'antd'; +import { Form, Input, Select, Checkbox } from 'antd'; import { ThemedDollarOutlined, ThemedWalletOutlined, + ThemedAliasOutlined, } from 'components/Common/CustomIcons'; import styled, { css } from 'styled-components'; import ScanQRCode from './ScanQRCode'; import { currency } from 'components/Common/Ticker.js'; const { TextArea } = Input; export const AntdFormCss = css` input[type='number'] { -moz-appearance: textfield; } .ant-input-group-addon { background-color: ${props => props.theme.forms.addonBackground} !important; border: 1px solid ${props => props.theme.forms.border}; color: ${props => props.theme.forms.addonForeground} !important; } input.ant-input, .ant-select-selection { background-color: ${props => props.theme.forms.selectionBackground} !important; box-shadow: none !important; border-radius: 4px; font-weight: bold; color: ${props => props.theme.forms.text}; opacity: 1; height: 45px; } textarea.ant-input, .ant-select-selection { background-color: ${props => props.theme.forms.selectionBackground} !important; box-shadow: none !important; border-radius: 4px; font-weight: bold; color: ${props => props.theme.forms.text}; opacity: 1; height: 50px; min-height: 100px; } .ant-input-affix-wrapper { background-color: ${props => props.theme.forms.selectionBackground} !important; border: 1px solid ${props => props.theme.forms.border} !important; } .ant-input-wrapper .anticon-qrcode { color: ${props => props.theme.forms.addonForeground} !important; } input.ant-input::placeholder, .ant-select-selection::placeholder { color: ${props => props.theme.forms.placeholder} !important; } .ant-select-selector { height: 55px !important; border: 1px solid ${props => props.theme.forms.border} !important; background-color: ${props => props.theme.forms.selectionBackground}!important; } .ant-form-item-has-error > div > div.ant-form-item-control-input > div > span > span > span.ant-input-affix-wrapper { background-color: ${props => props.theme.forms.selectionBackground}; border-color: ${props => props.theme.forms.error} !important; } .ant-form-item-control-input-content { color: ${props => props.theme.forms.text} !important; } .ant-input:hover { border-color: ${props => props.theme.forms.highlightBox}; } .ant-form-item-has-error .ant-input, .ant-form-item-has-error .ant-input-affix-wrapper, .ant-form-item-has-error .ant-input:hover, .ant-form-item-has-error .ant-input-affix-wrapper:hover { background-color: ${props => props.theme.forms.selectionBackground}; border-color: ${props => props.theme.forms.error} !important; } .ant-form-item-has-error .ant-select:not(.ant-select-disabled):not(.ant-select-customize-input) .ant-select-selector { background-color: ${props => props.theme.forms.selectionBackground}; border-color: ${props => props.theme.forms.error} !important; } .ant-select-single .ant-select-selector .ant-select-selection-item, .ant-select-single .ant-select-selector .ant-select-selection-placeholder { line-height: 55px; text-align: left; color: ${props => props.theme.forms.text}; font-weight: bold; } .ant-form-item-has-error .ant-input-group-addon { color: ${props => props.theme.forms.error} !important; border-color: ${props => props.theme.forms.error} !important; } .ant-form-item-explain .ant-form-item-explain-error { color: ${props => props.theme.forms.error} !important; overflow: hidden; text-overflow: ellipsis; width: 100%; min-width: 1px; } .ant-input-suffix { color: ${props => props.theme.lightWhite}; } `; export const AntdFormWrapper = styled.div` ${AntdFormCss} `; export const InputAddonText = styled.span` width: 100%; height: 100%; display: block; ${props => props.disabled ? ` cursor: not-allowed; ` : `cursor: pointer;`} `; export const InputNumberAddonText = styled.span` background-color: ${props => props.theme.forms.addonBackground} !important; border: 1px solid ${props => props.theme.forms.border}; color: ${props => props.theme.forms.addonForeground} !important; height: 50px; line-height: 47px; * { color: ${props => props.theme.forms.addonForeground} !important; } ${props => props.disabled ? ` cursor: not-allowed; ` : `cursor: pointer;`} `; +export const CashtabCheckbox = styled(Checkbox)` + .ant-checkbox-checked .ant-checkbox-inner { + background-color: ${props => props.theme.eCashBlue} !important; + border-color: ${props => props.theme.eCashBlue} !important; + } + .ant-checkbox + span { + color: ${props => props.theme.forms.text} !important; + } +`; + export const SendBchInput = ({ onMax, inputProps, selectProps, activeFiatCode, ...otherProps }) => { const { Option } = Select; const currencies = [ { value: currency.ticker, label: currency.ticker, }, { value: activeFiatCode ? activeFiatCode : 'USD', label: activeFiatCode ? activeFiatCode : 'USD', }, ]; const currencyOptions = currencies.map(currency => { return ( ); }); const CurrencySelect = ( ); return ( event.target.blur()} step={ inputProps.dollar === 1 ? 0.01 : 1 / 10 ** currency.cashDecimals } prefix={ inputProps.dollar === 1 ? ( ) : ( ) } {...inputProps} /> {CurrencySelect} max ); }; SendBchInput.propTypes = { onMax: PropTypes.func, inputProps: PropTypes.object, selectProps: PropTypes.object, activeFiatCode: PropTypes.string, }; export const DestinationAmount = ({ onMax, inputProps, ...otherProps }) => { return ( event.target.blur()} prefix={ } addonAfter={ max } {...inputProps} /> ); }; DestinationAmount.propTypes = { onMax: PropTypes.func, inputProps: PropTypes.object, }; export const InputAmountSingle = ({ inputProps, ...otherProps }) => { return ( ); }; InputAmountSingle.propTypes = { inputProps: PropTypes.object, }; // loadWithCameraOpen prop: if true, load page with camera scanning open export const DestinationAddressSingle = ({ onScan, loadWithCameraOpen, inputProps, ...otherProps }) => { return ( } autoComplete="off" addonAfter={ } {...inputProps} /> ); }; DestinationAddressSingle.propTypes = { onScan: PropTypes.func, loadWithCameraOpen: PropTypes.bool, inputProps: PropTypes.object, }; export const DestinationAddressSingleWithoutQRScan = ({ inputProps, ...otherProps }) => { return ( } autoComplete="off" {...inputProps} /> ); }; DestinationAddressSingleWithoutQRScan.propTypes = { inputProps: PropTypes.object, }; +export const AliasInput = ({ inputProps, ...otherProps }) => { + return ( + + + } + autoComplete="off" + {...inputProps} + /> + + + ); +}; + +AliasInput.propTypes = { + inputProps: PropTypes.object, +}; + +export const AliasAddressInput = ({ inputProps, ...otherProps }) => { + return ( + + + } + autoComplete="off" + {...inputProps} + /> + + + ); +}; + +AliasAddressInput.propTypes = { + inputProps: PropTypes.object, +}; + export const DestinationAddressMulti = ({ inputProps, ...otherProps }) => { return (