Implement initial functionality

This commit is contained in:
Connor Bryan 2018-07-12 12:49:21 -05:00
parent 97bfbba51f
commit 6b2a8eedbc
7 changed files with 167 additions and 38 deletions

View File

@ -13,6 +13,12 @@ interface OwnProps {
isSelfAddress?: boolean; isSelfAddress?: boolean;
isCheckSummed?: boolean; isCheckSummed?: boolean;
showLabelMatch?: boolean; showLabelMatch?: boolean;
showIdenticon?: boolean;
showInputLabel?: boolean;
placeholder?: string;
value?: string;
dropdownThreshold?: number;
onChangeOverride?: (ev: React.FormEvent<HTMLInputElement>) => void;
} }
interface StateProps { interface StateProps {
@ -26,26 +32,38 @@ const AddressField: React.SFC<Props> = ({
isSelfAddress, isSelfAddress,
isCheckSummed, isCheckSummed,
showLabelMatch, showLabelMatch,
toChecksumAddress toChecksumAddress,
showIdenticon,
placeholder = donationAddressMap.ETH,
showInputLabel = true,
onChangeOverride,
value,
dropdownThreshold
}) => ( }) => (
<AddressFieldFactory <AddressFieldFactory
isSelfAddress={isSelfAddress} isSelfAddress={isSelfAddress}
showLabelMatch={showLabelMatch} showLabelMatch={showLabelMatch}
showIdenticon={showIdenticon}
onChangeOverride={onChangeOverride}
value={value}
dropdownThreshold={dropdownThreshold}
withProps={({ currentTo, isValid, isLabelEntry, onChange, onFocus, onBlur, readOnly }) => ( withProps={({ currentTo, isValid, isLabelEntry, onChange, onFocus, onBlur, readOnly }) => (
<div className="input-group-wrapper"> <div className="input-group-wrapper">
<label className="input-group"> <label className="input-group">
{showInputLabel && (
<div className="input-group-header"> <div className="input-group-header">
{translate(isSelfAddress ? 'X_ADDRESS' : 'SEND_ADDR')} {translate(isSelfAddress ? 'X_ADDRESS' : 'SEND_ADDR')}
</div> </div>
)}
<Input <Input
className={`input-group-input ${!isValid && !isLabelEntry ? 'invalid' : ''}`} className={`input-group-input ${!isValid && !isLabelEntry ? 'invalid' : ''}`}
isValid={isValid} isValid={isValid}
type="text" type="text"
value={isCheckSummed ? toChecksumAddress(currentTo.raw) : currentTo.raw} value={value ? value : isCheckSummed ? toChecksumAddress(currentTo.raw) : currentTo.raw}
placeholder={donationAddressMap.ETH} placeholder={placeholder}
readOnly={!!(isReadOnly || readOnly)} readOnly={!!(isReadOnly || readOnly)}
spellCheck={false} spellCheck={false}
onChange={onChange} onChange={onChangeOverride || onChange}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
/> />

View File

@ -9,6 +9,9 @@ import { Address, Identicon } from 'components/ui';
import './AddressFieldDropdown.scss'; import './AddressFieldDropdown.scss';
interface StateProps { interface StateProps {
value?: string;
dropdownThreshold?: number;
onChangeOverride?: (ev: React.FormEvent<HTMLInputElement>) => void;
labelAddresses: ReturnType<typeof addressBookSelectors.getLabelAddresses>; labelAddresses: ReturnType<typeof addressBookSelectors.getLabelAddresses>;
currentTo: ReturnType<typeof transactionSelectors.getToRaw>; currentTo: ReturnType<typeof transactionSelectors.getToRaw>;
} }
@ -37,14 +40,15 @@ class AddressFieldDropdown extends React.Component<Props> {
} }
public render() { public render() {
const { currentTo } = this.props; const { value, currentTo, dropdownThreshold = 3 } = this.props;
const noMatchContent = currentTo.startsWith('0x') ? null : ( const stringInQuestion = value != null ? value : currentTo;
const noMatchContent = stringInQuestion.startsWith('0x') ? null : (
<li className="AddressFieldDropdown-dropdown-item AddressFieldDropdown-dropdown-item-no-match"> <li className="AddressFieldDropdown-dropdown-item AddressFieldDropdown-dropdown-item-no-match">
<i className="fa fa-warning" /> {translate('NO_LABEL_FOUND_CONTAINING')} "{currentTo}". <i className="fa fa-warning" /> {translate('NO_LABEL_FOUND_CONTAINING')} "{stringInQuestion}".
</li> </li>
); );
return this.props.currentTo.length > 1 ? ( return stringInQuestion.length >= dropdownThreshold ? (
<ul className="AddressFieldDropdown" role="listbox"> <ul className="AddressFieldDropdown" role="listbox">
{this.getFilteredLabels().length > 0 ? this.renderDropdownItems() : noMatchContent} {this.getFilteredLabels().length > 0 ? this.renderDropdownItems() : noMatchContent}
</ul> </ul>
@ -53,6 +57,7 @@ class AddressFieldDropdown extends React.Component<Props> {
private renderDropdownItems = () => private renderDropdownItems = () =>
this.getFilteredLabels().map((filteredLabel, index: number) => { this.getFilteredLabels().map((filteredLabel, index: number) => {
const { onChangeOverride, setCurrentTo } = this.props;
const { activeIndex } = this.state; const { activeIndex } = this.state;
const { address, label } = filteredLabel; const { address, label } = filteredLabel;
const isActive = activeIndex === index; const isActive = activeIndex === index;
@ -64,7 +69,11 @@ class AddressFieldDropdown extends React.Component<Props> {
<li <li
key={address} key={address}
className={className} className={className}
onClick={() => this.props.setCurrentTo(address)} onClick={() =>
onChangeOverride
? onChangeOverride({ currentTarget: { value: address } })
: setCurrentTo(address)
}
role="option" role="option"
title={`${translateRaw('SEND_TO')}${label}`} title={`${translateRaw('SEND_TO')}${label}`}
> >
@ -79,11 +88,15 @@ class AddressFieldDropdown extends React.Component<Props> {
); );
}); });
private getFilteredLabels = () => private getFilteredLabels = () => {
Object.keys(this.props.labelAddresses) const { value, currentTo } = this.props;
.filter(label => label.toLowerCase().includes(this.props.currentTo.toLowerCase())) const includedString = value != null ? value : currentTo.toLowerCase();
return Object.keys(this.props.labelAddresses)
.filter(label => label.toLowerCase().includes(includedString))
.map(label => ({ address: this.props.labelAddresses[label], label })) .map(label => ({ address: this.props.labelAddresses[label], label }))
.slice(0, 5); .slice(0, 5);
};
private getIsVisible = () => private getIsVisible = () =>
this.props.currentTo.length > 1 && this.getFilteredLabels().length > 0; this.props.currentTo.length > 1 && this.getFilteredLabels().length > 0;

View File

@ -15,6 +15,10 @@ interface OwnProps {
to: string | null; to: string | null;
isSelfAddress?: boolean; isSelfAddress?: boolean;
showLabelMatch?: boolean; showLabelMatch?: boolean;
showIdenticon?: boolean;
value?: string;
dropdownThreshold?: number;
onChangeOverride?: (ev: React.FormEvent<HTMLInputElement>) => void;
withProps(props: CallbackProps): React.ReactElement<any> | null; withProps(props: CallbackProps): React.ReactElement<any> | null;
} }
@ -56,16 +60,30 @@ class AddressFieldFactoryClass extends React.Component<Props> {
} }
public render() { public render() {
const {
isSelfAddress,
showLabelMatch,
withProps,
showIdenticon,
onChangeOverride,
value,
dropdownThreshold
} = this.props;
return ( return (
<div className="AddressField"> <div className="AddressField">
<AddressInputFactory <AddressInputFactory
isSelfAddress={this.props.isSelfAddress} isSelfAddress={isSelfAddress}
showLabelMatch={this.props.showLabelMatch} showLabelMatch={showLabelMatch}
withProps={withProps}
showIdenticon={showIdenticon}
onChangeOverride={onChangeOverride}
value={value}
dropdownThreshold={dropdownThreshold}
isFocused={this.state.isFocused} isFocused={this.state.isFocused}
onChange={this.setAddress} onChange={this.setAddress}
onFocus={this.focus} onFocus={this.focus}
onBlur={this.setBlurTimeout} onBlur={this.setBlurTimeout}
withProps={this.props.withProps}
/> />
</div> </div>
); );
@ -76,8 +94,10 @@ class AddressFieldFactoryClass extends React.Component<Props> {
private blur = () => this.setState({ isFocused: false }); private blur = () => this.setState({ isFocused: false });
private setAddress = (ev: React.FormEvent<HTMLInputElement>) => { private setAddress = (ev: React.FormEvent<HTMLInputElement>) => {
const { onChangeOverride, setCurrentTo } = this.props;
const { value } = ev.currentTarget; const { value } = ev.currentTarget;
this.props.setCurrentTo(value);
onChangeOverride ? onChangeOverride(ev) : setCurrentTo(value);
}; };
private setBlurTimeout = () => (this.goingToBlur = window.setTimeout(this.blur, 150)); private setBlurTimeout = () => (this.goingToBlur = window.setTimeout(this.blur, 150));
@ -90,13 +110,21 @@ const AddressFieldFactory = connect(null, { setCurrentTo: transactionActions.set
interface DefaultAddressFieldProps { interface DefaultAddressFieldProps {
isSelfAddress?: boolean; isSelfAddress?: boolean;
showLabelMatch?: boolean; showLabelMatch?: boolean;
showIdenticon?: boolean;
value?: string;
dropdownThreshold?: number;
onChangeOverride?: (ev: React.FormEvent<HTMLInputElement>) => void;
withProps(props: CallbackProps): React.ReactElement<any> | null; withProps(props: CallbackProps): React.ReactElement<any> | null;
} }
const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({ const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({
isSelfAddress, isSelfAddress,
showLabelMatch, showLabelMatch,
withProps showIdenticon,
value,
withProps,
onChangeOverride,
dropdownThreshold
}) => ( }) => (
<Query <Query
params={['to']} params={['to']}
@ -106,6 +134,10 @@ const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({
isSelfAddress={isSelfAddress} isSelfAddress={isSelfAddress}
showLabelMatch={showLabelMatch} showLabelMatch={showLabelMatch}
withProps={withProps} withProps={withProps}
showIdenticon={showIdenticon}
onChangeOverride={onChangeOverride}
value={value}
dropdownThreshold={dropdownThreshold}
/> />
)} )}
/> />

View File

@ -27,7 +27,12 @@ interface StateProps {
interface OwnProps { interface OwnProps {
isSelfAddress?: boolean; isSelfAddress?: boolean;
showLabelMatch?: boolean; showLabelMatch?: boolean;
showIdenticon?: boolean;
isFocused?: boolean; isFocused?: boolean;
className?: string;
value?: string;
dropdownThreshold?: number;
onChangeOverride?: (ev: React.FormEvent<HTMLInputElement>) => void;
onChange(ev: React.FormEvent<HTMLInputElement>): void; onChange(ev: React.FormEvent<HTMLInputElement>): void;
onFocus(ev: React.FormEvent<HTMLInputElement>): void; onFocus(ev: React.FormEvent<HTMLInputElement>): void;
onBlur(ev: React.FormEvent<HTMLInputElement>): void; onBlur(ev: React.FormEvent<HTMLInputElement>): void;
@ -58,6 +63,7 @@ type Props = OwnProps & StateProps;
class AddressInputFactoryClass extends Component<Props> { class AddressInputFactoryClass extends Component<Props> {
public render() { public render() {
const { const {
className,
label, label,
currentTo, currentTo,
onChange, onChange,
@ -69,16 +75,28 @@ class AddressInputFactoryClass extends Component<Props> {
showLabelMatch, showLabelMatch,
isSelfAddress, isSelfAddress,
isResolving, isResolving,
isFocused isFocused,
showIdenticon = true,
onChangeOverride,
value,
dropdownThreshold
} = this.props; } = this.props;
const { value } = currentTo;
const addr = addHexPrefix(value ? value.toString('hex') : '0');
const inputClassName = `AddressInput-input ${label ? 'AddressInput-input-with-label' : ''}`; const inputClassName = `AddressInput-input ${label ? 'AddressInput-input-with-label' : ''}`;
const sendingTo = `${translateRaw('SENDING_TO')} ${label}`; const sendingTo = `${translateRaw('SENDING_TO')} ${label}`;
const isENSAddress = currentTo.raw.includes('.eth'); const isENSAddress = currentTo.raw.includes('.eth');
/**
* @desc Initially set the address to the passed value.
* If there wasn't a value passed, use the value from the redux store.
*/
let addr = value;
if (addr == null) {
addr = addHexPrefix(currentTo.value ? currentTo.value.toString('hex') : '0');
}
return ( return (
<div className="AddressInput form-group"> <div className={`AddressInput form-group`}>
<div className={inputClassName}> <div className={inputClassName}>
<Query <Query
params={['readOnly']} params={['readOnly']}
@ -95,7 +113,14 @@ class AddressInputFactoryClass extends Component<Props> {
} }
/> />
<ENSStatus ensAddress={currentTo.raw} isLoading={isResolving} rawAddress={addr} /> <ENSStatus ensAddress={currentTo.raw} isLoading={isResolving} rawAddress={addr} />
{isFocused && !isENSAddress && <AddressFieldDropdown />} {isFocused &&
!isENSAddress && (
<AddressFieldDropdown
onChangeOverride={onChangeOverride}
value={value}
dropdownThreshold={dropdownThreshold}
/>
)}
{showLabelMatch && {showLabelMatch &&
label && ( label && (
<div title={sendingTo} className="AddressInput-input-label"> <div title={sendingTo} className="AddressInput-input-label">
@ -103,9 +128,11 @@ class AddressInputFactoryClass extends Component<Props> {
</div> </div>
)} )}
</div> </div>
{showIdenticon && (
<div className="AddressInput-identicon"> <div className="AddressInput-identicon">
<Identicon address={addr} /> <Identicon address={addr} />
</div> </div>
)}
</div> </div>
); );
} }

View File

@ -2,6 +2,14 @@
@import 'common/sass/mixins'; @import 'common/sass/mixins';
.ViewOnly { .ViewOnly {
& .AddressInput {
margin-top: 15px;
& .input-group-input {
margin-top: 1rem;
}
}
&-recent { &-recent {
text-align: left; text-align: left;

View File

@ -8,6 +8,7 @@ import { AppState } from 'features/reducers';
import { getIsValidAddressFn } from 'features/config'; import { getIsValidAddressFn } from 'features/config';
import { walletSelectors } from 'features/wallet'; import { walletSelectors } from 'features/wallet';
import { Input, Identicon } from 'components/ui'; import { Input, Identicon } from 'components/ui';
import { AddressField } from 'components';
import './ViewOnly.scss'; import './ViewOnly.scss';
interface OwnProps { interface OwnProps {
@ -23,17 +24,20 @@ type Props = OwnProps & StateProps;
interface State { interface State {
address: string; address: string;
addressFromBook: string;
} }
class ViewOnlyDecryptClass extends PureComponent<Props, State> { class ViewOnlyDecryptClass extends PureComponent<Props, State> {
public state = { public state = {
address: '' address: '',
addressFromBook: ''
}; };
public render() { public render() {
const { recentAddresses, isValidAddress } = this.props; const { recentAddresses, isValidAddress } = this.props;
const { address } = this.state; const { address, addressFromBook } = this.state;
const isValid = isValidAddress(address); const isValid = isValidAddress(address);
const or = <em className="ViewOnly-recent-separator">{translate('OR')}</em>;
const recentOptions = (recentAddresses.map(addr => ({ const recentOptions = (recentAddresses.map(addr => ({
label: ( label: (
@ -48,7 +52,7 @@ class ViewOnlyDecryptClass extends PureComponent<Props, State> {
return ( return (
<div className="ViewOnly"> <div className="ViewOnly">
<form className="form-group" onSubmit={this.openWallet}> <form className="form-group" onSubmit={this.openWalletWithAddress}>
{!!recentOptions.length && ( {!!recentOptions.length && (
<div className="ViewOnly-recent"> <div className="ViewOnly-recent">
<Select <Select
@ -57,10 +61,20 @@ class ViewOnlyDecryptClass extends PureComponent<Props, State> {
options={recentOptions} options={recentOptions}
placeholder={translateRaw('VIEW_ONLY_RECENT')} placeholder={translateRaw('VIEW_ONLY_RECENT')}
/> />
<em className="ViewOnly-recent-separator">{translate('OR')}</em> {or}
</div> </div>
)} )}
<div className="ViewOnly-book">
<AddressField
value={addressFromBook}
showInputLabel={false}
showIdenticon={false}
placeholder={translateRaw('SELECT_FROM_ADDRESS_BOOK')}
onChangeOverride={this.handleSelectAddressFromBook}
dropdownThreshold={0}
/>
{or}
</div>
<Input <Input
isValid={isValid} isValid={isValid}
className="ViewOnly-input" className="ViewOnly-input"
@ -68,7 +82,6 @@ class ViewOnlyDecryptClass extends PureComponent<Props, State> {
onChange={this.changeAddress} onChange={this.changeAddress}
placeholder={translateRaw('VIEW_ONLY_ENTER')} placeholder={translateRaw('VIEW_ONLY_ENTER')}
/> />
<button className="ViewOnly-submit btn btn-primary btn-block" disabled={!isValid}> <button className="ViewOnly-submit btn btn-primary btn-block" disabled={!isValid}>
{translate('VIEW_ADDR')} {translate('VIEW_ADDR')}
</button> </button>
@ -83,16 +96,33 @@ class ViewOnlyDecryptClass extends PureComponent<Props, State> {
private handleSelectAddress = (option: Option) => { private handleSelectAddress = (option: Option) => {
const address = option && option.value ? option.value.toString() : ''; const address = option && option.value ? option.value.toString() : '';
this.setState({ address }, () => this.openWallet()); this.setState({ address }, this.openWalletWithAddress);
}; };
private openWallet = (ev?: React.FormEvent<HTMLElement>) => { private handleSelectAddressFromBook = (ev: React.FormEvent<HTMLInputElement>) => {
const { currentTarget: { value: addressFromBook } } = ev;
this.setState({ addressFromBook }, this.openWalletWithAddressBook);
};
private openWalletWithAddress = (ev?: React.FormEvent<HTMLElement>) => {
const { address } = this.state;
if (ev) { if (ev) {
ev.preventDefault(); ev.preventDefault();
} }
this.openWallet(address);
};
private openWalletWithAddressBook = () => {
const { addressFromBook } = this.state;
this.openWallet(addressFromBook);
};
private openWallet = (address: string) => {
const { isValidAddress } = this.props; const { isValidAddress } = this.props;
const { address } = this.state;
if (isValidAddress(address)) { if (isValidAddress(address)) {
const wallet = new AddressOnlyWallet(address); const wallet = new AddressOnlyWallet(address);
this.props.onUnlock(wallet); this.props.onUnlock(wallet);

View File

@ -665,6 +665,7 @@
"WHAT_IS_PAYMENT_ID": "what's a payment ID?", "WHAT_IS_PAYMENT_ID": "what's a payment ID?",
"ANNOUNCEMENT_MESSAGE": "MyCrypto.com no longer allows the use of private keys, mnemonics, or keystore files in the browser. To continue using them, please download the [MyCrypto Desktop App](https://download.mycrypto.com).", "ANNOUNCEMENT_MESSAGE": "MyCrypto.com no longer allows the use of private keys, mnemonics, or keystore files in the browser. To continue using them, please download the [MyCrypto Desktop App](https://download.mycrypto.com).",
"U2F_NOT_SUPPORTED": "The U2F standard that hardware wallets use does not seem to be supported by your browser. Please try again using Google Chrome.", "U2F_NOT_SUPPORTED": "The U2F standard that hardware wallets use does not seem to be supported by your browser. Please try again using Google Chrome.",
"SIMILAR_TRANSACTION_ERROR": "This transaction is very similar to a recent transaction. Please wait a few moments and try again, or click 'Advanced' and manually set the nonce to a new value." "SIMILAR_TRANSACTION_ERROR": "This transaction is very similar to a recent transaction. Please wait a few moments and try again, or click 'Advanced' and manually set the nonce to a new value.",
"SELECT_FROM_ADDRESS_BOOK": "Select from your address book"
} }
} }