Merge pull request #2055 from MyCryptoHQ/enhancement/address-book-to-view-address
Enhance `View Address` screen with a dropdown that draws from address book and recent addresses
This commit is contained in:
commit
283a97c145
|
@ -13,6 +13,12 @@ interface OwnProps {
|
|||
isSelfAddress?: boolean;
|
||||
isCheckSummed?: boolean;
|
||||
showLabelMatch?: boolean;
|
||||
showIdenticon?: boolean;
|
||||
showInputLabel?: boolean;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
dropdownThreshold?: number;
|
||||
onChangeOverride?(ev: React.FormEvent<HTMLInputElement>): void;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
|
@ -26,26 +32,42 @@ const AddressField: React.SFC<Props> = ({
|
|||
isSelfAddress,
|
||||
isCheckSummed,
|
||||
showLabelMatch,
|
||||
toChecksumAddress
|
||||
toChecksumAddress,
|
||||
showIdenticon,
|
||||
placeholder = donationAddressMap.ETH,
|
||||
showInputLabel = true,
|
||||
onChangeOverride,
|
||||
value,
|
||||
dropdownThreshold
|
||||
}) => (
|
||||
<AddressFieldFactory
|
||||
isSelfAddress={isSelfAddress}
|
||||
showLabelMatch={showLabelMatch}
|
||||
showIdenticon={showIdenticon}
|
||||
onChangeOverride={onChangeOverride}
|
||||
value={value}
|
||||
dropdownThreshold={dropdownThreshold}
|
||||
withProps={({ currentTo, isValid, isLabelEntry, onChange, onFocus, onBlur, readOnly }) => (
|
||||
<div className="input-group-wrapper">
|
||||
<label className="input-group">
|
||||
<div className="input-group-header">
|
||||
{translate(isSelfAddress ? 'X_ADDRESS' : 'SEND_ADDR')}
|
||||
</div>
|
||||
{showInputLabel && (
|
||||
<div className="input-group-header">
|
||||
{translate(isSelfAddress ? 'X_ADDRESS' : 'SEND_ADDR')}
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
className={`input-group-input ${!isValid && !isLabelEntry ? 'invalid' : ''}`}
|
||||
isValid={isValid}
|
||||
type="text"
|
||||
value={isCheckSummed ? toChecksumAddress(currentTo.raw) : currentTo.raw}
|
||||
placeholder={donationAddressMap.ETH}
|
||||
value={
|
||||
value != null
|
||||
? value
|
||||
: isCheckSummed ? toChecksumAddress(currentTo.raw) : currentTo.raw
|
||||
}
|
||||
placeholder={placeholder}
|
||||
readOnly={!!(isReadOnly || readOnly)}
|
||||
spellCheck={false}
|
||||
onChange={onChange}
|
||||
onChange={onChangeOverride || onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
|
|
|
@ -50,6 +50,8 @@
|
|||
|
||||
&-label {
|
||||
flex: 1 0 0;
|
||||
padding-left: 0.2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&-address {
|
||||
|
|
|
@ -5,89 +5,138 @@ import translate, { translateRaw } from 'translations';
|
|||
import { AppState } from 'features/reducers';
|
||||
import { transactionActions, transactionSelectors } from 'features/transaction';
|
||||
import { addressBookSelectors } from 'features/addressBook';
|
||||
import { walletSelectors } from 'features/wallet';
|
||||
import { Address, Identicon } from 'components/ui';
|
||||
import './AddressFieldDropdown.scss';
|
||||
|
||||
interface StateProps {
|
||||
interface Props {
|
||||
addressInput: string;
|
||||
dropdownThreshold?: number;
|
||||
labelAddresses: ReturnType<typeof addressBookSelectors.getLabelAddresses>;
|
||||
currentTo: ReturnType<typeof transactionSelectors.getToRaw>;
|
||||
recentAddresses: ReturnType<typeof walletSelectors.getRecentAddresses>;
|
||||
onEntryClick(address: string): void;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
setCurrentTo: transactionActions.TSetCurrentTo;
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps;
|
||||
|
||||
interface State {
|
||||
activeIndex: number | null;
|
||||
}
|
||||
|
||||
class AddressFieldDropdown extends React.Component<Props> {
|
||||
public state: State = {
|
||||
class AddressFieldDropdownClass extends React.Component<Props, State> {
|
||||
public state = {
|
||||
activeIndex: null
|
||||
};
|
||||
|
||||
private exists: boolean = true;
|
||||
|
||||
public componentDidMount() {
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
this.exists = false;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { currentTo } = this.props;
|
||||
const noMatchContent = currentTo.startsWith('0x') ? null : (
|
||||
<li className="AddressFieldDropdown-dropdown-item AddressFieldDropdown-dropdown-item-no-match">
|
||||
<i className="fa fa-warning" /> {translate('NO_LABEL_FOUND_CONTAINING')} "{currentTo}".
|
||||
</li>
|
||||
);
|
||||
const { addressInput } = this.props;
|
||||
|
||||
return this.props.currentTo.length > 1 ? (
|
||||
<ul className="AddressFieldDropdown" role="listbox">
|
||||
{this.getFilteredLabels().length > 0 ? this.renderDropdownItems() : noMatchContent}
|
||||
</ul>
|
||||
) : null;
|
||||
return (
|
||||
this.getIsVisible() && (
|
||||
<ul className="AddressFieldDropdown" role="listbox">
|
||||
{this.getFilteredLabels().length > 0 ? (
|
||||
this.renderDropdownItems()
|
||||
) : (
|
||||
<li className="AddressFieldDropdown-dropdown-item AddressFieldDropdown-dropdown-item-no-match">
|
||||
<i className="fa fa-warning" /> {translate('NO_LABEL_FOUND_CONTAINING')} "{
|
||||
addressInput
|
||||
}".
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private renderDropdownItems = () =>
|
||||
this.getFilteredLabels().map((filteredLabel, index: number) => {
|
||||
const { activeIndex } = this.state;
|
||||
const { address, label } = filteredLabel;
|
||||
const isActive = activeIndex === index;
|
||||
const className = `AddressFieldDropdown-dropdown-item ${
|
||||
isActive ? 'AddressFieldDropdown-dropdown-item--active' : ''
|
||||
}`;
|
||||
private renderDropdownItems = () => {
|
||||
const { onEntryClick } = this.props;
|
||||
const { activeIndex } = this.state;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={address}
|
||||
className={className}
|
||||
onClick={() => this.props.setCurrentTo(address)}
|
||||
role="option"
|
||||
title={`${translateRaw('SEND_TO')}${label}`}
|
||||
>
|
||||
<div className="AddressFieldDropdown-dropdown-item-identicon">
|
||||
<Identicon address={address} />
|
||||
</div>
|
||||
<strong className="AddressFieldDropdown-dropdown-item-label">{label}</strong>
|
||||
<em className="AddressFieldDropdown-dropdown-item-address">
|
||||
<Address address={address} />
|
||||
</em>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
return this.getFilteredLabels().map(
|
||||
({ address, label }: { address: string; label: string }, index: number) => {
|
||||
const isActive = activeIndex === index;
|
||||
const className = `AddressFieldDropdown-dropdown-item ${
|
||||
isActive ? 'AddressFieldDropdown-dropdown-item--active' : ''
|
||||
}`;
|
||||
|
||||
private getFilteredLabels = () =>
|
||||
Object.keys(this.props.labelAddresses)
|
||||
.filter(label => label.toLowerCase().includes(this.props.currentTo.toLowerCase()))
|
||||
.map(label => ({ address: this.props.labelAddresses[label], label }))
|
||||
return (
|
||||
<li
|
||||
key={address}
|
||||
role="option"
|
||||
className={className}
|
||||
onClick={() => onEntryClick(address)}
|
||||
title={`${translateRaw('SEND_TO')}${label}`}
|
||||
>
|
||||
<div className="AddressFieldDropdown-dropdown-item-identicon">
|
||||
<Identicon address={address} />
|
||||
</div>
|
||||
<strong className="AddressFieldDropdown-dropdown-item-label">{label}</strong>
|
||||
<em className="AddressFieldDropdown-dropdown-item-address">
|
||||
<Address address={address} />
|
||||
</em>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
private getFormattedRecentAddresses = (): { [label: string]: string } => {
|
||||
const { labelAddresses, recentAddresses } = this.props;
|
||||
// Hash existing entries by address for performance.
|
||||
const addressesInBook: { [address: string]: boolean } = Object.values(labelAddresses).reduce(
|
||||
(prev: { [address: string]: boolean }, next: string) => {
|
||||
prev[next] = true;
|
||||
return prev;
|
||||
},
|
||||
{}
|
||||
);
|
||||
// Make recent addresses sequential.
|
||||
let recentAddressCount: number = 0;
|
||||
const addresses = recentAddresses.reduce((prev: { [label: string]: string }, next: string) => {
|
||||
// Prevent duplication.
|
||||
if (addressesInBook[next]) {
|
||||
return prev;
|
||||
}
|
||||
prev[
|
||||
translateRaw('RECENT_ADDRESS_NUMBER', { $number: (++recentAddressCount).toString() })
|
||||
] = next;
|
||||
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
return addresses;
|
||||
};
|
||||
|
||||
private getFilteredLabels = () => {
|
||||
const { addressInput, labelAddresses } = this.props;
|
||||
const formattedRecentAddresses = this.getFormattedRecentAddresses();
|
||||
|
||||
return Object.keys({ ...labelAddresses, ...formattedRecentAddresses })
|
||||
.filter(label => label.toLowerCase().includes(addressInput))
|
||||
.map(label => ({ address: labelAddresses[label] || formattedRecentAddresses[label], label }))
|
||||
.slice(0, 5);
|
||||
};
|
||||
|
||||
private getIsVisible = () =>
|
||||
this.props.currentTo.length > 1 && this.getFilteredLabels().length > 0;
|
||||
private getIsVisible = () => {
|
||||
const { addressInput, dropdownThreshold = 3 } = this.props;
|
||||
|
||||
return addressInput.length >= dropdownThreshold && this.getFilteredLabels().length > 0;
|
||||
};
|
||||
|
||||
private setActiveIndex = (activeIndex: number | null) => this.setState({ activeIndex });
|
||||
|
||||
private clearActiveIndex = () => this.setActiveIndex(null);
|
||||
|
||||
//#region Keyboard Controls
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (this.getIsVisible()) {
|
||||
switch (e.key) {
|
||||
|
@ -107,6 +156,7 @@ class AddressFieldDropdown extends React.Component<Props> {
|
|||
};
|
||||
|
||||
private handleEnterKeyDown = () => {
|
||||
const { onEntryClick } = this.props;
|
||||
const { activeIndex } = this.state;
|
||||
|
||||
if (activeIndex !== null) {
|
||||
|
@ -114,11 +164,13 @@ class AddressFieldDropdown extends React.Component<Props> {
|
|||
|
||||
filteredLabels.forEach(({ address }, index) => {
|
||||
if (activeIndex === index) {
|
||||
this.props.setCurrentTo(address);
|
||||
onEntryClick(address);
|
||||
}
|
||||
});
|
||||
|
||||
this.clearActiveIndex();
|
||||
if (this.exists) {
|
||||
this.clearActiveIndex();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -150,16 +202,116 @@ class AddressFieldDropdown extends React.Component<Props> {
|
|||
|
||||
this.setState({ activeIndex });
|
||||
};
|
||||
|
||||
private setActiveIndex = (activeIndex: number | null) => this.setState({ activeIndex });
|
||||
|
||||
private clearActiveIndex = () => this.setActiveIndex(null);
|
||||
//#endregion Keyboard Controls
|
||||
}
|
||||
|
||||
export default connect(
|
||||
//#region Uncontrolled
|
||||
/**
|
||||
* @desc The `onChangeOverride` prop needs to work
|
||||
* with actual events, but also needs a value to be directly passed in
|
||||
* occasionally. This interface allows us to skip all of the other FormEvent
|
||||
* properties and methods.
|
||||
*/
|
||||
interface FakeFormEvent {
|
||||
currentTarget: {
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UncontrolledAddressFieldDropdownProps {
|
||||
value: string;
|
||||
labelAddresses: ReturnType<typeof addressBookSelectors.getLabelAddresses>;
|
||||
recentAddresses: ReturnType<typeof walletSelectors.getRecentAddresses>;
|
||||
dropdownThreshold?: number;
|
||||
onChangeOverride(ev: React.FormEvent<HTMLInputElement> | FakeFormEvent): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc The uncontrolled dropdown changes the address input onClick,
|
||||
* as well as calls the onChange override, but does not update the `currentTo`
|
||||
* property in the Redux store.
|
||||
*/
|
||||
function RawUncontrolledAddressFieldDropdown({
|
||||
value,
|
||||
onChangeOverride,
|
||||
labelAddresses,
|
||||
recentAddresses,
|
||||
dropdownThreshold
|
||||
}: UncontrolledAddressFieldDropdownProps) {
|
||||
const onEntryClick = (address: string) => onChangeOverride({ currentTarget: { value: address } });
|
||||
|
||||
return (
|
||||
<AddressFieldDropdownClass
|
||||
addressInput={value}
|
||||
onEntryClick={onEntryClick}
|
||||
labelAddresses={labelAddresses}
|
||||
recentAddresses={recentAddresses}
|
||||
dropdownThreshold={dropdownThreshold}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const UncontrolledAddressFieldDropdown = connect((state: AppState) => ({
|
||||
labelAddresses: addressBookSelectors.getLabelAddresses(state),
|
||||
recentAddresses: walletSelectors.getRecentAddresses(state)
|
||||
}))(RawUncontrolledAddressFieldDropdown);
|
||||
//#endregion Uncontrolled
|
||||
|
||||
//#region Controlled
|
||||
interface ControlledAddressFieldDropdownProps {
|
||||
currentTo: ReturnType<typeof transactionSelectors.getToRaw>;
|
||||
labelAddresses: ReturnType<typeof addressBookSelectors.getLabelAddresses>;
|
||||
recentAddresses: ReturnType<typeof walletSelectors.getRecentAddresses>;
|
||||
setCurrentTo: transactionActions.TSetCurrentTo;
|
||||
dropdownThreshold?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc The controlled dropdown connects directly to the Redux store,
|
||||
* modifying the `currentTo` property onChange.
|
||||
*/
|
||||
function RawControlledAddressFieldDropdown({
|
||||
currentTo,
|
||||
labelAddresses,
|
||||
recentAddresses,
|
||||
setCurrentTo,
|
||||
dropdownThreshold
|
||||
}: ControlledAddressFieldDropdownProps) {
|
||||
return (
|
||||
<AddressFieldDropdownClass
|
||||
addressInput={currentTo}
|
||||
onEntryClick={setCurrentTo}
|
||||
labelAddresses={labelAddresses}
|
||||
recentAddresses={recentAddresses}
|
||||
dropdownThreshold={dropdownThreshold}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const ControlledAddressFieldDropdown = connect(
|
||||
(state: AppState) => ({
|
||||
currentTo: transactionSelectors.getToRaw(state),
|
||||
labelAddresses: addressBookSelectors.getLabelAddresses(state),
|
||||
currentTo: transactionSelectors.getToRaw(state)
|
||||
recentAddresses: walletSelectors.getRecentAddresses(state)
|
||||
}),
|
||||
{ setCurrentTo: transactionActions.setCurrentTo }
|
||||
)(AddressFieldDropdown);
|
||||
{
|
||||
setCurrentTo: transactionActions.setCurrentTo
|
||||
}
|
||||
)(RawControlledAddressFieldDropdown);
|
||||
//#endregion Controlled
|
||||
|
||||
interface AddressFieldDropdownProps {
|
||||
controlled: boolean;
|
||||
value?: string;
|
||||
dropdownThreshold?: number;
|
||||
onChangeOverride?(ev: React.FormEvent<HTMLInputElement> | FakeFormEvent): void;
|
||||
}
|
||||
|
||||
export default function AddressFieldDropdown({
|
||||
controlled = true,
|
||||
...props
|
||||
}: AddressFieldDropdownProps) {
|
||||
const Dropdown = controlled ? ControlledAddressFieldDropdown : UncontrolledAddressFieldDropdown;
|
||||
|
||||
return <Dropdown {...props} />;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,11 @@ interface OwnProps {
|
|||
to: string | null;
|
||||
isSelfAddress?: boolean;
|
||||
showLabelMatch?: boolean;
|
||||
showIdenticon?: boolean;
|
||||
value?: string;
|
||||
dropdownThreshold?: number;
|
||||
withProps(props: CallbackProps): React.ReactElement<any> | null;
|
||||
onChangeOverride?(ev: React.FormEvent<HTMLInputElement>): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -56,16 +60,30 @@ class AddressFieldFactoryClass extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
isSelfAddress,
|
||||
showLabelMatch,
|
||||
withProps,
|
||||
showIdenticon,
|
||||
onChangeOverride,
|
||||
value,
|
||||
dropdownThreshold
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="AddressField">
|
||||
<AddressInputFactory
|
||||
isSelfAddress={this.props.isSelfAddress}
|
||||
showLabelMatch={this.props.showLabelMatch}
|
||||
isSelfAddress={isSelfAddress}
|
||||
showLabelMatch={showLabelMatch}
|
||||
withProps={withProps}
|
||||
showIdenticon={showIdenticon}
|
||||
onChangeOverride={onChangeOverride}
|
||||
value={value}
|
||||
dropdownThreshold={dropdownThreshold}
|
||||
isFocused={this.state.isFocused}
|
||||
onChange={this.setAddress}
|
||||
onFocus={this.focus}
|
||||
onBlur={this.setBlurTimeout}
|
||||
withProps={this.props.withProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -76,8 +94,10 @@ class AddressFieldFactoryClass extends React.Component<Props> {
|
|||
private blur = () => this.setState({ isFocused: false });
|
||||
|
||||
private setAddress = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const { onChangeOverride, setCurrentTo } = this.props;
|
||||
const { value } = ev.currentTarget;
|
||||
this.props.setCurrentTo(value);
|
||||
|
||||
onChangeOverride ? onChangeOverride(ev) : setCurrentTo(value);
|
||||
};
|
||||
|
||||
private setBlurTimeout = () => (this.goingToBlur = window.setTimeout(this.blur, 150));
|
||||
|
@ -90,13 +110,21 @@ const AddressFieldFactory = connect(null, { setCurrentTo: transactionActions.set
|
|||
interface DefaultAddressFieldProps {
|
||||
isSelfAddress?: boolean;
|
||||
showLabelMatch?: boolean;
|
||||
showIdenticon?: boolean;
|
||||
value?: string;
|
||||
dropdownThreshold?: number;
|
||||
withProps(props: CallbackProps): React.ReactElement<any> | null;
|
||||
onChangeOverride?(ev: React.FormEvent<HTMLInputElement>): void;
|
||||
}
|
||||
|
||||
const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({
|
||||
isSelfAddress,
|
||||
showLabelMatch,
|
||||
withProps
|
||||
showIdenticon,
|
||||
value,
|
||||
withProps,
|
||||
onChangeOverride,
|
||||
dropdownThreshold
|
||||
}) => (
|
||||
<Query
|
||||
params={['to']}
|
||||
|
@ -106,6 +134,10 @@ const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({
|
|||
isSelfAddress={isSelfAddress}
|
||||
showLabelMatch={showLabelMatch}
|
||||
withProps={withProps}
|
||||
showIdenticon={showIdenticon}
|
||||
onChangeOverride={onChangeOverride}
|
||||
value={value}
|
||||
dropdownThreshold={dropdownThreshold}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -27,8 +27,13 @@ interface StateProps {
|
|||
interface OwnProps {
|
||||
isSelfAddress?: boolean;
|
||||
showLabelMatch?: boolean;
|
||||
showIdenticon?: boolean;
|
||||
isFocused?: boolean;
|
||||
className?: string;
|
||||
value?: string;
|
||||
dropdownThreshold?: number;
|
||||
onChange(ev: React.FormEvent<HTMLInputElement>): void;
|
||||
onChangeOverride?(ev: React.FormEvent<HTMLInputElement>): void;
|
||||
onFocus(ev: React.FormEvent<HTMLInputElement>): void;
|
||||
onBlur(ev: React.FormEvent<HTMLInputElement>): void;
|
||||
withProps(props: CallbackProps): React.ReactElement<any> | null;
|
||||
|
@ -69,14 +74,32 @@ class AddressInputFactoryClass extends Component<Props> {
|
|||
showLabelMatch,
|
||||
isSelfAddress,
|
||||
isResolving,
|
||||
isFocused
|
||||
isFocused,
|
||||
showIdenticon = true,
|
||||
onChangeOverride,
|
||||
value,
|
||||
dropdownThreshold
|
||||
} = this.props;
|
||||
const { value } = currentTo;
|
||||
const addr = addHexPrefix(value ? value.toString('hex') : '0');
|
||||
const inputClassName = `AddressInput-input ${label ? 'AddressInput-input-with-label' : ''}`;
|
||||
const sendingTo = `${translateRaw('SENDING_TO')} ${label}`;
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc If passed a value and an onChangeOverride function,
|
||||
* infer that the dropdown should be uncontrolled.
|
||||
*/
|
||||
const controlled = value == null && !onChangeOverride;
|
||||
|
||||
return (
|
||||
<div className="AddressInput form-group">
|
||||
<div className={inputClassName}>
|
||||
|
@ -95,7 +118,15 @@ class AddressInputFactoryClass extends Component<Props> {
|
|||
}
|
||||
/>
|
||||
<ENSStatus ensAddress={currentTo.raw} isLoading={isResolving} rawAddress={addr} />
|
||||
{isFocused && !isENSAddress && <AddressFieldDropdown />}
|
||||
{isFocused &&
|
||||
!isENSAddress && (
|
||||
<AddressFieldDropdown
|
||||
controlled={controlled}
|
||||
value={value}
|
||||
onChangeOverride={onChangeOverride}
|
||||
dropdownThreshold={dropdownThreshold}
|
||||
/>
|
||||
)}
|
||||
{showLabelMatch &&
|
||||
label && (
|
||||
<div title={sendingTo} className="AddressInput-input-label">
|
||||
|
@ -103,9 +134,11 @@ class AddressInputFactoryClass extends Component<Props> {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="AddressInput-identicon">
|
||||
<Identicon address={addr} />
|
||||
</div>
|
||||
{showIdenticon && (
|
||||
<div className="AddressInput-identicon">
|
||||
<Identicon address={addr} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ $speed: 500ms;
|
|||
}
|
||||
|
||||
&-form {
|
||||
max-width: 360px;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
&-label {
|
||||
|
|
|
@ -2,24 +2,24 @@
|
|||
@import 'common/sass/mixins';
|
||||
|
||||
.ViewOnly {
|
||||
&-recent {
|
||||
text-align: left;
|
||||
&-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-separator {
|
||||
display: block;
|
||||
margin: $space-sm 0;
|
||||
text-align: center;
|
||||
color: shade-dark(0.3);
|
||||
}
|
||||
&-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
|
||||
.Select {
|
||||
&-option {
|
||||
@include ellipsis;
|
||||
&:nth-of-type(2) {
|
||||
margin-bottom: calc(1rem + 15px);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.Identicon {
|
||||
display: inline-block;
|
||||
margin-right: $space-sm;
|
||||
& .AddressField {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Select, { Option } from 'react-select';
|
||||
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import { AddressOnlyWallet } from 'libs/wallet';
|
||||
import { AppState } from 'features/reducers';
|
||||
import { getIsValidAddressFn } from 'features/config';
|
||||
import { walletSelectors } from 'features/wallet';
|
||||
import { Input, Identicon } from 'components/ui';
|
||||
import { Input } from 'components/ui';
|
||||
import { AddressField } from 'components';
|
||||
import './ViewOnly.scss';
|
||||
|
||||
interface OwnProps {
|
||||
|
@ -15,7 +14,6 @@ interface OwnProps {
|
|||
}
|
||||
|
||||
interface StateProps {
|
||||
recentAddresses: ReturnType<typeof walletSelectors.getRecentAddresses>;
|
||||
isValidAddress: ReturnType<typeof getIsValidAddressFn>;
|
||||
}
|
||||
|
||||
|
@ -23,55 +21,50 @@ type Props = OwnProps & StateProps;
|
|||
|
||||
interface State {
|
||||
address: string;
|
||||
addressFromBook: string;
|
||||
}
|
||||
|
||||
class ViewOnlyDecryptClass extends PureComponent<Props, State> {
|
||||
public state = {
|
||||
address: ''
|
||||
address: '',
|
||||
addressFromBook: ''
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { recentAddresses, isValidAddress } = this.props;
|
||||
const { address } = this.state;
|
||||
const { isValidAddress } = this.props;
|
||||
const { address, addressFromBook } = this.state;
|
||||
const isValid = isValidAddress(address);
|
||||
|
||||
const recentOptions = (recentAddresses.map(addr => ({
|
||||
label: (
|
||||
<React.Fragment>
|
||||
<Identicon address={addr} />
|
||||
{addr}
|
||||
</React.Fragment>
|
||||
),
|
||||
value: addr
|
||||
// I hate this assertion, but React elements definitely work as labels
|
||||
})) as any) as Option[];
|
||||
|
||||
return (
|
||||
<div className="ViewOnly">
|
||||
<form className="form-group" onSubmit={this.openWallet}>
|
||||
{!!recentOptions.length && (
|
||||
<div className="ViewOnly-recent">
|
||||
<Select
|
||||
value={address}
|
||||
onChange={this.handleSelectAddress}
|
||||
options={recentOptions}
|
||||
placeholder={translateRaw('VIEW_ONLY_RECENT')}
|
||||
<form className="form-group" onSubmit={this.openWalletWithAddress}>
|
||||
<section className="ViewOnly-fields">
|
||||
<section className="ViewOnly-fields-field">
|
||||
<AddressField
|
||||
value={addressFromBook}
|
||||
showInputLabel={false}
|
||||
showIdenticon={false}
|
||||
placeholder={translateRaw('SELECT_FROM_ADDRESS_BOOK')}
|
||||
onChangeOverride={this.handleSelectAddressFromBook}
|
||||
dropdownThreshold={0}
|
||||
/>
|
||||
<em className="ViewOnly-recent-separator">{translate('OR')}</em>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Input
|
||||
isValid={isValid}
|
||||
className="ViewOnly-input"
|
||||
value={address}
|
||||
onChange={this.changeAddress}
|
||||
placeholder={translateRaw('VIEW_ONLY_ENTER')}
|
||||
/>
|
||||
|
||||
<button className="ViewOnly-submit btn btn-primary btn-block" disabled={!isValid}>
|
||||
{translate('VIEW_ADDR')}
|
||||
</button>
|
||||
</section>
|
||||
<section className="ViewOnly-fields-field">
|
||||
<em>{translate('OR')}</em>
|
||||
</section>
|
||||
<section className="ViewOnly-fields-field">
|
||||
<Input
|
||||
isValid={isValid}
|
||||
className="ViewOnly-input"
|
||||
value={address}
|
||||
onChange={this.changeAddress}
|
||||
placeholder={translateRaw('VIEW_ONLY_ENTER')}
|
||||
/>
|
||||
<button className="ViewOnly-submit btn btn-primary btn-block" disabled={!isValid}>
|
||||
{translate('VIEW_ADDR')}
|
||||
</button>
|
||||
</section>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
@ -81,18 +74,30 @@ class ViewOnlyDecryptClass extends PureComponent<Props, State> {
|
|||
this.setState({ address: ev.currentTarget.value });
|
||||
};
|
||||
|
||||
private handleSelectAddress = (option: Option) => {
|
||||
const address = option && option.value ? option.value.toString() : '';
|
||||
this.setState({ address }, () => this.openWallet());
|
||||
private handleSelectAddressFromBook = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
const { currentTarget: { value: addressFromBook } } = ev;
|
||||
this.setState({ addressFromBook }, this.openWalletWithAddressBook);
|
||||
};
|
||||
|
||||
private openWallet = (ev?: React.FormEvent<HTMLElement>) => {
|
||||
private openWalletWithAddress = (ev?: React.FormEvent<HTMLElement>) => {
|
||||
const { address } = this.state;
|
||||
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
this.openWallet(address);
|
||||
};
|
||||
|
||||
private openWalletWithAddressBook = () => {
|
||||
const { addressFromBook } = this.state;
|
||||
|
||||
this.openWallet(addressFromBook);
|
||||
};
|
||||
|
||||
private openWallet = (address: string) => {
|
||||
const { isValidAddress } = this.props;
|
||||
const { address } = this.state;
|
||||
|
||||
if (isValidAddress(address)) {
|
||||
const wallet = new AddressOnlyWallet(address);
|
||||
this.props.onUnlock(wallet);
|
||||
|
@ -101,6 +106,5 @@ class ViewOnlyDecryptClass extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
export const ViewOnlyDecrypt = connect((state: AppState): StateProps => ({
|
||||
recentAddresses: walletSelectors.getRecentAddresses(state),
|
||||
isValidAddress: getIsValidAddressFn(state)
|
||||
}))(ViewOnlyDecryptClass);
|
||||
|
|
|
@ -669,6 +669,8 @@
|
|||
"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).",
|
||||
"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",
|
||||
"RECENT_ADDRESS_NUMBER": "Recent Address #$number"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue