import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ToggleButton from '../../../components/ui/toggle-button';
import TextField from '../../../components/ui/text-field';
import Button from '../../../components/ui/button';
import { MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes';
import Dropdown from '../../../components/ui/dropdown';
import Dialog from '../../../components/ui/dialog';
import { getPlatform } from '../../../../app/scripts/lib/util';
import { PLATFORM_FIREFOX } from '../../../../shared/constants/app';
import {
getNumberOfSettingsInSection,
handleSettingsRefs,
} from '../../../helpers/utils/settings-search';
import {
LEDGER_TRANSPORT_TYPES,
LEDGER_USB_VENDOR_ID,
} from '../../../../shared/constants/hardware-wallets';
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
import { exportAsFile } from '../../../../shared/modules/export-utils';
import ActionableMessage from '../../../components/ui/actionable-message';
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
export default class AdvancedTab extends PureComponent {
static contextTypes = {
t: PropTypes.func,
trackEvent: PropTypes.func,
};
static propTypes = {
setUseNonceField: PropTypes.func,
useNonceField: PropTypes.bool,
setHexDataFeatureFlag: PropTypes.func,
displayWarning: PropTypes.func,
showResetAccountConfirmationModal: PropTypes.func,
warning: PropTypes.string,
history: PropTypes.object,
sendHexData: PropTypes.bool,
setAdvancedInlineGasFeatureFlag: PropTypes.func,
advancedInlineGas: PropTypes.bool,
showFiatInTestnets: PropTypes.bool,
showTestNetworks: PropTypes.bool,
autoLockTimeLimit: PropTypes.number,
setAutoLockTimeLimit: PropTypes.func.isRequired,
setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired,
setShowTestNetworks: PropTypes.func.isRequired,
threeBoxSyncingAllowed: PropTypes.bool.isRequired,
setThreeBoxSyncingPermission: PropTypes.func.isRequired,
threeBoxDisabled: PropTypes.bool.isRequired,
setIpfsGateway: PropTypes.func.isRequired,
ipfsGateway: PropTypes.string.isRequired,
ledgerTransportType: PropTypes.oneOf(Object.values(LEDGER_TRANSPORT_TYPES)),
setLedgerTransportPreference: PropTypes.func.isRequired,
setDismissSeedBackUpReminder: PropTypes.func.isRequired,
dismissSeedBackUpReminder: PropTypes.bool.isRequired,
userHasALedgerAccount: PropTypes.bool.isRequired,
useTokenDetection: PropTypes.bool.isRequired,
setUseTokenDetection: PropTypes.func.isRequired,
backupUserData: PropTypes.func.isRequired,
restoreUserData: PropTypes.func.isRequired,
};
state = {
autoLockTimeLimit: this.props.autoLockTimeLimit,
lockTimeError: '',
ipfsGateway: this.props.ipfsGateway,
ipfsGatewayError: '',
showLedgerTransportWarning: false,
showResultMessage: false,
restoreSuccessful: true,
};
settingsRefs = Array(
getNumberOfSettingsInSection(this.context.t, this.context.t('advanced')),
)
.fill(undefined)
.map(() => {
return React.createRef();
});
componentDidUpdate() {
const { t } = this.context;
handleSettingsRefs(t, t('advanced'), this.settingsRefs);
}
componentDidMount() {
const { t } = this.context;
handleSettingsRefs(t, t('advanced'), this.settingsRefs);
}
renderMobileSync() {
const { t } = this.context;
const { history } = this.props;
return (
{t('syncWithMobile')}
{
event.preventDefault();
history.push(MOBILE_SYNC_ROUTE);
}}
>
{t('syncWithMobile')}
);
}
async getTextFromFile(file) {
return new Promise((resolve, reject) => {
const reader = new window.FileReader();
reader.onload = (e) => {
const text = e.target.result;
resolve(text);
};
reader.onerror = (e) => {
reject(e);
};
reader.readAsText(file);
});
}
async handleFileUpload(event) {
/**
* we need this to be able to access event.target after
* the event handler has been called. [Synthetic Event Pooling, pre React 17]
*
* @see https://fb.me/react-event-pooling
*/
event.persist();
const file = event.target.files[0];
const jsonString = await this.getTextFromFile(file);
/**
* so that we can restore same file again if we want to.
* chrome blocks uploading same file twice.
*
*/
event.target.value = '';
const result = await this.props.restoreUserData(jsonString);
this.setState({
showResultMessage: true,
restoreSuccessful: result,
});
}
renderRestoreUserData() {
const { t } = this.context;
const { showResultMessage, restoreSuccessful } = this.state;
return (
{t('restoreUserData')}
{t('restoreUserDataDescription')}
{t('restore')}
this.handleFileUpload(e)}
/>
{showResultMessage && (
)}
);
}
renderUserDataBackup() {
const { t } = this.context;
return (
{t('backupUserData')}
{t('backupUserDataDescription')}
{
this.props.backupUserData();
}}
>
{t('backup')}
);
}
renderStateLogs() {
const { t } = this.context;
const { displayWarning } = this.props;
return (
{t('stateLogs')}
{t('stateLogsDescription')}
{
window.logStateString((err, result) => {
if (err) {
displayWarning(t('stateLogError'));
} else {
exportAsFile(`${t('stateLogFileName')}.json`, result);
}
});
}}
>
{t('downloadStateLogs')}
);
}
renderResetAccount() {
const { t } = this.context;
const { showResetAccountConfirmationModal } = this.props;
return (
{t('resetAccount')}
{t('resetAccountDescription')}
{
event.preventDefault();
this.context.trackEvent({
category: EVENT.CATEGORIES.SETTINGS,
event: EVENT_NAMES.ACCOUNT_RESET,
properties: {},
});
showResetAccountConfirmationModal();
}}
>
{t('resetAccount')}
);
}
renderHexDataOptIn() {
const { t } = this.context;
const { sendHexData, setHexDataFeatureFlag } = this.props;
return (
{t('showHexData')}
{t('showHexDataDescription')}
setHexDataFeatureFlag(!value)}
offLabel={t('off')}
onLabel={t('on')}
/>
);
}
renderAdvancedGasInputInline() {
const { t } = this.context;
const { advancedInlineGas, setAdvancedInlineGasFeatureFlag } = this.props;
return (
{t('showAdvancedGasInline')}
{t('showAdvancedGasInlineDescription')}
setAdvancedInlineGasFeatureFlag(!value)}
offLabel={t('off')}
onLabel={t('on')}
/>
);
}
renderToggleTestNetworks() {
const { t } = this.context;
const { showTestNetworks, setShowTestNetworks } = this.props;
return (
{t('showTestnetNetworks')}
{t('showTestnetNetworksDescription')}
setShowTestNetworks(!value)}
offLabel={t('off')}
onLabel={t('on')}
/>
);
}
renderShowConversionInTestnets() {
const { t } = this.context;
const { showFiatInTestnets, setShowFiatConversionOnTestnetsPreference } =
this.props;
return (
{t('showFiatConversionInTestnets')}
{t('showFiatConversionInTestnetsDescription')}
setShowFiatConversionOnTestnetsPreference(!value)
}
offLabel={t('off')}
onLabel={t('on')}
/>
);
}
renderUseNonceOptIn() {
const { t } = this.context;
const { useNonceField, setUseNonceField } = this.props;
return (
{t('nonceField')}
{t('nonceFieldDescription')}
setUseNonceField(!value)}
offLabel={t('off')}
onLabel={t('on')}
/>
);
}
handleLockChange(time) {
const { t } = this.context;
const autoLockTimeLimit = Math.max(Number(time), 0);
this.setState(() => {
let lockTimeError = '';
if (autoLockTimeLimit > 10080) {
lockTimeError = t('lockTimeTooGreat');
}
return {
autoLockTimeLimit,
lockTimeError,
};
});
}
renderAutoLockTimeLimit() {
const { t } = this.context;
const { lockTimeError } = this.state;
const { setAutoLockTimeLimit } = this.props;
return (
{t('autoLockTimeLimit')}
{t('autoLockTimeLimitDescription')}
this.handleLockChange(e.target.value)}
error={lockTimeError}
fullWidth
margin="dense"
min={0}
/>
{
setAutoLockTimeLimit(this.state.autoLockTimeLimit);
}}
>
{t('save')}
);
}
renderThreeBoxControl() {
const { t } = this.context;
const {
threeBoxSyncingAllowed,
setThreeBoxSyncingPermission,
threeBoxDisabled,
} = this.props;
let allowed = threeBoxSyncingAllowed;
let description = t('syncWithThreeBoxDescription');
if (threeBoxDisabled) {
allowed = false;
description = t('syncWithThreeBoxDisabled');
}
return (
{t('syncWithThreeBox')}
{description}
{
if (!threeBoxDisabled) {
setThreeBoxSyncingPermission(!value);
}
}}
offLabel={t('off')}
onLabel={t('on')}
/>
);
}
renderLedgerLiveControl() {
const { t } = this.context;
const {
ledgerTransportType,
setLedgerTransportPreference,
userHasALedgerAccount,
} = this.props;
const LEDGER_TRANSPORT_NAMES = {
LIVE: t('ledgerLive'),
WEBHID: t('webhid'),
U2F: t('u2f'),
};
const transportTypeOptions = [
{
name: LEDGER_TRANSPORT_NAMES.LIVE,
value: LEDGER_TRANSPORT_TYPES.LIVE,
},
{
name: LEDGER_TRANSPORT_NAMES.U2F,
value: LEDGER_TRANSPORT_TYPES.U2F,
},
];
if (window.navigator.hid) {
transportTypeOptions.push({
name: LEDGER_TRANSPORT_NAMES.WEBHID,
value: LEDGER_TRANSPORT_TYPES.WEBHID,
});
}
const recommendedLedgerOption = window.navigator.hid
? LEDGER_TRANSPORT_NAMES.WEBHID
: LEDGER_TRANSPORT_NAMES.U2F;
return (
{t('preferredLedgerConnectionType')}
{t('ledgerConnectionPreferenceDescription', [
recommendedLedgerOption,
{t('learnMore')}
,
])}
{
if (
ledgerTransportType === LEDGER_TRANSPORT_TYPES.LIVE &&
transportType === LEDGER_TRANSPORT_TYPES.WEBHID
) {
this.setState({ showLedgerTransportWarning: true });
}
setLedgerTransportPreference(transportType);
if (
transportType === LEDGER_TRANSPORT_TYPES.WEBHID &&
userHasALedgerAccount
) {
await window.navigator.hid.requestDevice({
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }],
});
}
}}
/>
{this.state.showLedgerTransportWarning ? (
{t('ledgerTransportChangeWarning')}
) : null}
);
}
handleIpfsGatewayChange(url) {
const { t } = this.context;
this.setState(() => {
let ipfsGatewayError = '';
try {
const urlObj = new URL(addUrlProtocolPrefix(url));
if (!urlObj.host) {
throw new Error();
}
// don't allow the use of this gateway
if (urlObj.host === 'gateway.ipfs.io') {
throw new Error('Forbidden gateway');
}
} catch (error) {
ipfsGatewayError =
error.message === 'Forbidden gateway'
? t('forbiddenIpfsGateway')
: t('invalidIpfsGateway');
}
return {
ipfsGateway: url,
ipfsGatewayError,
};
});
}
handleIpfsGatewaySave() {
const url = new URL(addUrlProtocolPrefix(this.state.ipfsGateway));
const { host } = url;
this.props.setIpfsGateway(host);
}
renderIpfsGatewayControl() {
const { t } = this.context;
const { ipfsGatewayError } = this.state;
return (
{t('ipfsGateway')}
{t('ipfsGatewayDescription')}
this.handleIpfsGatewayChange(e.target.value)}
error={ipfsGatewayError}
fullWidth
margin="dense"
/>
{
this.handleIpfsGatewaySave();
}}
>
{t('save')}
);
}
renderDismissSeedBackupReminderControl() {
const { t } = this.context;
const { dismissSeedBackUpReminder, setDismissSeedBackUpReminder } =
this.props;
return (
{t('dismissReminderField')}
{t('dismissReminderDescriptionField')}
setDismissSeedBackUpReminder(!value)}
offLabel={t('off')}
onLabel={t('on')}
/>
);
}
renderTokenDetectionToggle() {
const { t } = this.context;
const { useTokenDetection, setUseTokenDetection } = this.props;
return (
{t('tokenDetection')}
{t('tokenDetectionDescription')}
{
this.context.trackEvent({
category: EVENT.CATEGORIES.SETTINGS,
event: 'Token Detection',
properties: {
action: 'Token Detection',
legacy_event: true,
},
});
setUseTokenDetection(!value);
}}
offLabel={t('off')}
onLabel={t('on')}
/>
);
}
render() {
const { warning } = this.props;
const notUsingFirefox = getPlatform() !== PLATFORM_FIREFOX;
return (
{warning ?
{warning}
: null}
{this.renderStateLogs()}
{this.renderMobileSync()}
{this.renderResetAccount()}
{this.renderAdvancedGasInputInline()}
{this.renderTokenDetectionToggle()}
{this.renderHexDataOptIn()}
{this.renderShowConversionInTestnets()}
{this.renderToggleTestNetworks()}
{this.renderUseNonceOptIn()}
{this.renderAutoLockTimeLimit()}
{this.renderUserDataBackup()}
{this.renderRestoreUserData()}
{this.renderThreeBoxControl()}
{this.renderIpfsGatewayControl()}
{notUsingFirefox ? this.renderLedgerLiveControl() : null}
{this.renderDismissSeedBackupReminderControl()}
);
}
}
function addUrlProtocolPrefix(urlString) {
if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) {
return `https://${urlString}`;
}
return urlString;
}