commit
48f4e9845b
After Width: | Height: | Size: 1022 KiB |
@ -0,0 +1,37 @@ |
||||
import { cloneDeep } from 'lodash'; |
||||
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets'; |
||||
|
||||
const version = 66; |
||||
|
||||
/** |
||||
* Changes the useLedgerLive boolean property to the ledgerTransportType enum |
||||
*/ |
||||
export default { |
||||
version, |
||||
async migrate(originalVersionedData) { |
||||
const versionedData = cloneDeep(originalVersionedData); |
||||
versionedData.meta.version = version; |
||||
const state = versionedData.data; |
||||
const newState = transformState(state); |
||||
versionedData.data = newState; |
||||
return versionedData; |
||||
}, |
||||
}; |
||||
|
||||
function transformState(state) { |
||||
const defaultTransportType = window.navigator.hid |
||||
? LEDGER_TRANSPORT_TYPES.WEBHID |
||||
: LEDGER_TRANSPORT_TYPES.U2F; |
||||
const useLedgerLive = Boolean(state.PreferencesController?.useLedgerLive); |
||||
const newState = { |
||||
...state, |
||||
PreferencesController: { |
||||
...state?.PreferencesController, |
||||
ledgerTransportType: useLedgerLive |
||||
? LEDGER_TRANSPORT_TYPES.LIVE |
||||
: defaultTransportType, |
||||
}, |
||||
}; |
||||
delete newState.PreferencesController.useLedgerLive; |
||||
return newState; |
||||
} |
@ -0,0 +1,116 @@ |
||||
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets'; |
||||
import migration66 from './066'; |
||||
|
||||
describe('migration #66', () => { |
||||
beforeEach(() => { |
||||
jest.resetAllMocks(); |
||||
}); |
||||
|
||||
it('should update the version metadata', async () => { |
||||
const oldStorage = { |
||||
meta: { |
||||
version: 65, |
||||
}, |
||||
data: {}, |
||||
}; |
||||
|
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect(newStorage.meta).toStrictEqual({ |
||||
version: 66, |
||||
}); |
||||
}); |
||||
|
||||
it('should set ledgerTransportType to `u2f` if no preferences controller exists and webhid is not available', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: {}, |
||||
}; |
||||
|
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F); |
||||
}); |
||||
|
||||
it('should set ledgerTransportType to `u2f` if no useLedgerLive property exists and webhid is not available', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
PreferencesController: {}, |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F); |
||||
}); |
||||
|
||||
it('should set ledgerTransportType to `u2f` if useLedgerLive is false and webhid is not available', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
PreferencesController: { |
||||
useLedgerLive: false, |
||||
}, |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F); |
||||
}); |
||||
|
||||
it('should set ledgerTransportType to `webhid` if useLedgerLive is false and webhid is available', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
PreferencesController: { |
||||
useLedgerLive: false, |
||||
}, |
||||
}, |
||||
}; |
||||
jest |
||||
.spyOn(window, 'navigator', 'get') |
||||
.mockImplementation(() => ({ hid: true })); |
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual(LEDGER_TRANSPORT_TYPES.WEBHID); |
||||
}); |
||||
|
||||
it('should set ledgerTransportType to `ledgerLive` if useLedgerLive is true', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
PreferencesController: { |
||||
useLedgerLive: true, |
||||
}, |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual('ledgerLive'); |
||||
}); |
||||
|
||||
it('should not change ledgerTransportType if useLedgerLive is true and webhid is available', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
PreferencesController: { |
||||
useLedgerLive: true, |
||||
}, |
||||
}, |
||||
}; |
||||
jest |
||||
.spyOn(window, 'navigator', 'get') |
||||
.mockImplementation(() => ({ hid: true })); |
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual(LEDGER_TRANSPORT_TYPES.LIVE); |
||||
}); |
||||
}); |
@ -0,0 +1,12 @@ |
||||
export const readAddressAsContract = async (ethQuery, address) => { |
||||
let contractCode; |
||||
try { |
||||
contractCode = await ethQuery.getCode(address); |
||||
} catch (e) { |
||||
contractCode = null; |
||||
} |
||||
|
||||
const isContractAddress = |
||||
contractCode && contractCode !== '0x' && contractCode !== '0x0'; |
||||
return { contractCode, isContractAddress }; |
||||
}; |
@ -0,0 +1,30 @@ |
||||
const { readAddressAsContract } = require('./contract-utils'); |
||||
|
||||
describe('Contract Utils', () => { |
||||
it('checks is an address is a contract address or not', async () => { |
||||
let mockEthQuery = { |
||||
getCode: () => { |
||||
return '0xa'; |
||||
}, |
||||
}; |
||||
const { isContractAddress } = await readAddressAsContract( |
||||
mockEthQuery, |
||||
'0x76B4aa9Fc4d351a0062c6af8d186DF959D564A84', |
||||
); |
||||
expect(isContractAddress).toStrictEqual(true); |
||||
|
||||
mockEthQuery = { |
||||
getCode: () => { |
||||
return '0x'; |
||||
}, |
||||
}; |
||||
|
||||
const { |
||||
isContractAddress: isNotContractAddress, |
||||
} = await readAddressAsContract( |
||||
mockEthQuery, |
||||
'0x76B4aa9Fc4d351a0062c6af8d186DF959D564A84', |
||||
); |
||||
expect(isNotContractAddress).toStrictEqual(false); |
||||
}); |
||||
}); |
@ -0,0 +1 @@ |
||||
export { default } from './ledger-instruction-field'; |
@ -0,0 +1,153 @@ |
||||
import React, { useEffect } from 'react'; |
||||
import { useDispatch, useSelector } from 'react-redux'; |
||||
import PropTypes from 'prop-types'; |
||||
import { |
||||
LEDGER_TRANSPORT_TYPES, |
||||
LEDGER_USB_VENDOR_ID, |
||||
WEBHID_CONNECTED_STATUSES, |
||||
} from '../../../../shared/constants/hardware-wallets'; |
||||
import { |
||||
PLATFORM_FIREFOX, |
||||
ENVIRONMENT_TYPE_FULLSCREEN, |
||||
} from '../../../../shared/constants/app'; |
||||
|
||||
import { |
||||
setLedgerWebHidConnectedStatus, |
||||
getLedgerWebHidConnectedStatus, |
||||
} from '../../../ducks/app/app'; |
||||
|
||||
import Typography from '../../ui/typography/typography'; |
||||
import Button from '../../ui/button'; |
||||
import { useI18nContext } from '../../../hooks/useI18nContext'; |
||||
import { |
||||
COLORS, |
||||
FONT_WEIGHT, |
||||
TYPOGRAPHY, |
||||
} from '../../../helpers/constants/design-system'; |
||||
import Dialog from '../../ui/dialog'; |
||||
import { |
||||
getPlatform, |
||||
getEnvironmentType, |
||||
} from '../../../../app/scripts/lib/util'; |
||||
import { getLedgerTransportType } from '../../../ducks/metamask/metamask'; |
||||
|
||||
const renderInstructionStep = (text, show = true, color = COLORS.PRIMARY3) => { |
||||
return ( |
||||
show && ( |
||||
<Typography |
||||
boxProps={{ margin: 0 }} |
||||
color={color} |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
variant={TYPOGRAPHY.H7} |
||||
> |
||||
{text} |
||||
</Typography> |
||||
) |
||||
); |
||||
}; |
||||
|
||||
export default function LedgerInstructionField({ showDataInstruction }) { |
||||
const t = useI18nContext(); |
||||
const dispatch = useDispatch(); |
||||
|
||||
const webHidConnectedStatus = useSelector(getLedgerWebHidConnectedStatus); |
||||
const ledgerTransportType = useSelector(getLedgerTransportType); |
||||
const environmentType = getEnvironmentType(); |
||||
const environmentTypeIsFullScreen = |
||||
environmentType === ENVIRONMENT_TYPE_FULLSCREEN; |
||||
|
||||
useEffect(() => { |
||||
const initialConnectedDeviceCheck = async () => { |
||||
if ( |
||||
ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID && |
||||
webHidConnectedStatus !== WEBHID_CONNECTED_STATUSES.CONNECTED |
||||
) { |
||||
const devices = await window.navigator.hid.getDevices(); |
||||
const webHidIsConnected = devices.some( |
||||
(device) => device.vendorId === Number(LEDGER_USB_VENDOR_ID), |
||||
); |
||||
dispatch( |
||||
setLedgerWebHidConnectedStatus( |
||||
webHidIsConnected |
||||
? WEBHID_CONNECTED_STATUSES.CONNECTED |
||||
: WEBHID_CONNECTED_STATUSES.NOT_CONNECTED, |
||||
), |
||||
); |
||||
} |
||||
}; |
||||
initialConnectedDeviceCheck(); |
||||
}, [dispatch, ledgerTransportType, webHidConnectedStatus]); |
||||
|
||||
const usingLedgerLive = ledgerTransportType === LEDGER_TRANSPORT_TYPES.LIVE; |
||||
const usingWebHID = ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID; |
||||
|
||||
const isFirefox = getPlatform() === PLATFORM_FIREFOX; |
||||
|
||||
return ( |
||||
<div> |
||||
<div className="confirm-detail-row"> |
||||
<Dialog type="message"> |
||||
<div className="ledger-live-dialog"> |
||||
{renderInstructionStep(t('ledgerConnectionInstructionHeader'))} |
||||
{renderInstructionStep( |
||||
`- ${t('ledgerConnectionInstructionStepOne')}`, |
||||
!isFirefox && usingLedgerLive, |
||||
)} |
||||
{renderInstructionStep( |
||||
`- ${t('ledgerConnectionInstructionStepTwo')}`, |
||||
!isFirefox && usingLedgerLive, |
||||
)} |
||||
{renderInstructionStep( |
||||
`- ${t('ledgerConnectionInstructionStepThree')}`, |
||||
)} |
||||
{renderInstructionStep( |
||||
`- ${t('ledgerConnectionInstructionStepFour')}`, |
||||
showDataInstruction, |
||||
)} |
||||
{renderInstructionStep( |
||||
<span> |
||||
<Button |
||||
type="link" |
||||
onClick={async () => { |
||||
if (environmentTypeIsFullScreen) { |
||||
const connectedDevices = await window.navigator.hid.requestDevice( |
||||
{ |
||||
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }], |
||||
}, |
||||
); |
||||
const webHidIsConnected = connectedDevices.some( |
||||
(device) => |
||||
device.vendorId === Number(LEDGER_USB_VENDOR_ID), |
||||
); |
||||
dispatch( |
||||
setLedgerWebHidConnectedStatus({ |
||||
webHidConnectedStatus: webHidIsConnected |
||||
? WEBHID_CONNECTED_STATUSES.CONNECTED |
||||
: WEBHID_CONNECTED_STATUSES.NOT_CONNECTED, |
||||
}), |
||||
); |
||||
} else { |
||||
global.platform.openExtensionInBrowser(null, null, true); |
||||
} |
||||
}} |
||||
> |
||||
{environmentTypeIsFullScreen |
||||
? t('clickToConnectLedgerViaWebHID') |
||||
: t('openFullScreenForLedgerWebHid')} |
||||
</Button> |
||||
</span>, |
||||
usingWebHID && |
||||
webHidConnectedStatus === |
||||
WEBHID_CONNECTED_STATUSES.NOT_CONNECTED, |
||||
COLORS.SECONDARY1, |
||||
)} |
||||
</div> |
||||
</Dialog> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
LedgerInstructionField.propTypes = { |
||||
showDataInstruction: PropTypes.bool, |
||||
}; |
@ -0,0 +1,41 @@ |
||||
.onboarding-metametrics { |
||||
width: 600px; |
||||
|
||||
ul { |
||||
margin: 24px 0 0 0; |
||||
|
||||
li { |
||||
padding-bottom: 20px; |
||||
display: flex; |
||||
} |
||||
} |
||||
|
||||
.fa { |
||||
width: 16px; |
||||
} |
||||
|
||||
.fa-check { |
||||
margin-inline-end: 12px; |
||||
color: #1acc56; |
||||
} |
||||
|
||||
.fa-times { |
||||
margin-inline-end: 12px; |
||||
color: #d0021b; |
||||
} |
||||
|
||||
&__terms a { |
||||
color: $Blue-500; |
||||
} |
||||
|
||||
&__buttons { |
||||
margin: 24px auto 0 auto; |
||||
justify-content: space-between; |
||||
display: flex; |
||||
|
||||
button { |
||||
margin-bottom: 24px; |
||||
width: 200px; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,152 @@ |
||||
import React, { useContext } from 'react'; |
||||
import { useDispatch, useSelector } from 'react-redux'; |
||||
import { useHistory } from 'react-router-dom'; |
||||
import Typography from '../../../components/ui/typography/typography'; |
||||
import { |
||||
TYPOGRAPHY, |
||||
FONT_WEIGHT, |
||||
TEXT_ALIGN, |
||||
COLORS, |
||||
} from '../../../helpers/constants/design-system'; |
||||
import Button from '../../../components/ui/button'; |
||||
import { useI18nContext } from '../../../hooks/useI18nContext'; |
||||
import { setParticipateInMetaMetrics } from '../../../store/actions'; |
||||
import { |
||||
getFirstTimeFlowTypeRoute, |
||||
getFirstTimeFlowType, |
||||
getParticipateInMetaMetrics, |
||||
} from '../../../selectors'; |
||||
|
||||
import { MetaMetricsContext } from '../../../contexts/metametrics'; |
||||
|
||||
const firstTimeFlowTypeNameMap = { |
||||
create: 'Selected Create New Wallet', |
||||
import: 'Selected Import Wallet', |
||||
}; |
||||
|
||||
export default function OnboardingMetametrics() { |
||||
const t = useI18nContext(); |
||||
const dispatch = useDispatch(); |
||||
const history = useHistory(); |
||||
|
||||
const nextRoute = useSelector(getFirstTimeFlowTypeRoute); |
||||
const firstTimeFlowType = useSelector(getFirstTimeFlowType); |
||||
|
||||
const participateInMetaMetrics = useSelector(getParticipateInMetaMetrics); |
||||
const firstTimeSelectionMetaMetricsName = |
||||
firstTimeFlowTypeNameMap[firstTimeFlowType]; |
||||
|
||||
const metricsEvent = useContext(MetaMetricsContext); |
||||
|
||||
const onConfirm = async () => { |
||||
const [, metaMetricsId] = await dispatch(setParticipateInMetaMetrics(true)); |
||||
|
||||
try { |
||||
if (!participateInMetaMetrics) { |
||||
metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Onboarding', |
||||
action: 'Metrics Option', |
||||
name: 'Metrics Opt In', |
||||
}, |
||||
isOptIn: true, |
||||
flushImmediately: true, |
||||
}); |
||||
} |
||||
metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Onboarding', |
||||
action: 'Import or Create', |
||||
name: firstTimeSelectionMetaMetricsName, |
||||
}, |
||||
isOptIn: true, |
||||
metaMetricsId, |
||||
flushImmediately: true, |
||||
}); |
||||
} finally { |
||||
history.push(nextRoute); |
||||
} |
||||
}; |
||||
|
||||
const onCancel = async () => { |
||||
await dispatch(setParticipateInMetaMetrics(false)); |
||||
|
||||
try { |
||||
if (!participateInMetaMetrics) { |
||||
metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Onboarding', |
||||
action: 'Metrics Option', |
||||
name: 'Metrics Opt Out', |
||||
}, |
||||
isOptIn: true, |
||||
flushImmediately: true, |
||||
}); |
||||
} |
||||
} finally { |
||||
history.push(nextRoute); |
||||
} |
||||
}; |
||||
|
||||
return ( |
||||
<div className="onboarding-metametrics"> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H2} |
||||
align={TEXT_ALIGN.CENTER} |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
> |
||||
{t('metametricsTitle')} |
||||
</Typography> |
||||
<Typography align={TEXT_ALIGN.CENTER}> |
||||
{t('metametricsOptInDescription2')} |
||||
</Typography> |
||||
<ul> |
||||
<li> |
||||
<i className="fa fa-check" /> |
||||
{t('metametricsCommitmentsAllowOptOut2')} |
||||
</li> |
||||
<li> |
||||
<i className="fa fa-check" /> |
||||
{t('metametricsCommitmentsSendAnonymizedEvents')} |
||||
</li> |
||||
<li> |
||||
<i className="fa fa-times" /> |
||||
{t('metametricsCommitmentsNeverCollect')} |
||||
</li> |
||||
<li> |
||||
<i className="fa fa-times" /> |
||||
{t('metametricsCommitmentsNeverIP')} |
||||
</li> |
||||
<li> |
||||
<i className="fa fa-times" /> |
||||
{t('metametricsCommitmentsNeverSell')} |
||||
</li> |
||||
</ul> |
||||
<Typography |
||||
color={COLORS.UI4} |
||||
align={TEXT_ALIGN.CENTER} |
||||
variant={TYPOGRAPHY.H6} |
||||
className="onboarding-metametrics__terms" |
||||
> |
||||
{t('gdprMessage', [ |
||||
<a |
||||
key="metametrics-bottom-text-wrapper" |
||||
href="https://metamask.io/privacy.html" |
||||
target="_blank" |
||||
rel="noopener noreferrer" |
||||
> |
||||
{t('gdprMessagePrivacyPolicy')} |
||||
</a>, |
||||
])} |
||||
</Typography> |
||||
<div className="onboarding-metametrics__buttons"> |
||||
<Button type="secondary" onClick={onCancel}> |
||||
{t('noThanks')} |
||||
</Button> |
||||
<Button type="primary" onClick={onConfirm}> |
||||
{t('affirmAgree')} |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,9 @@ |
||||
import React from 'react'; |
||||
import OnboardingMetametrics from './metametrics'; |
||||
|
||||
export default { |
||||
title: 'Onboarding', |
||||
id: __filename, |
||||
}; |
||||
|
||||
export const OnboardingComponent = () => <OnboardingMetametrics />; |
@ -0,0 +1,23 @@ |
||||
.onboarding-pin-extension { |
||||
max-width: 800px; |
||||
|
||||
.control-dots .dot { |
||||
background: $ui-2; |
||||
box-shadow: none; |
||||
|
||||
&.selected { |
||||
background: $ui-4; |
||||
} |
||||
} |
||||
|
||||
&__diagram { |
||||
margin: 24px auto; |
||||
width: 799px; |
||||
height: 320px; |
||||
} |
||||
|
||||
&__buttons { |
||||
max-width: 50%; |
||||
margin: 40px auto 0 auto; |
||||
} |
||||
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,75 @@ |
||||
import React, { useState } from 'react'; |
||||
import { useHistory } from 'react-router-dom'; |
||||
|
||||
import { Carousel } from 'react-responsive-carousel'; |
||||
import Typography from '../../../components/ui/typography/typography'; |
||||
import { useI18nContext } from '../../../hooks/useI18nContext'; |
||||
import Button from '../../../components/ui/button'; |
||||
import { |
||||
TYPOGRAPHY, |
||||
FONT_WEIGHT, |
||||
TEXT_ALIGN, |
||||
} from '../../../helpers/constants/design-system'; |
||||
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; |
||||
import OnboardingPinBillboard from './pin-billboard'; |
||||
|
||||
export default function OnboardingPinExtension() { |
||||
const t = useI18nContext(); |
||||
const [selectedIndex, setSelectedIndex] = useState(0); |
||||
const history = useHistory(); |
||||
|
||||
return ( |
||||
<div className="onboarding-pin-extension"> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H2} |
||||
align={TEXT_ALIGN.CENTER} |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
> |
||||
{t('onboardingPinExtensionTitle')} |
||||
</Typography> |
||||
<Carousel |
||||
selectedItem={selectedIndex} |
||||
showThumbs={false} |
||||
showStatus={false} |
||||
showArrows={false} |
||||
> |
||||
<div> |
||||
<Typography align={TEXT_ALIGN.CENTER}> |
||||
{t('onboardingPinExtensionDescription')} |
||||
</Typography> |
||||
<div className="onboarding-pin-extension__diagram"> |
||||
<OnboardingPinBillboard /> |
||||
</div> |
||||
</div> |
||||
<div> |
||||
<Typography align={TEXT_ALIGN.CENTER}> |
||||
{t('onboardingPinExtensionDescription2')} |
||||
</Typography> |
||||
<Typography align={TEXT_ALIGN.CENTER}> |
||||
{t('onboardingPinExtensionDescription3')} |
||||
</Typography> |
||||
<img |
||||
src="/images/onboarding-pin-browser.svg" |
||||
width="799" |
||||
height="320" |
||||
alt="" |
||||
/> |
||||
</div> |
||||
</Carousel> |
||||
<div className="onboarding-pin-extension__buttons"> |
||||
<Button |
||||
type="primary" |
||||
onClick={() => { |
||||
if (selectedIndex === 0) { |
||||
setSelectedIndex(1); |
||||
} else { |
||||
history.push(DEFAULT_ROUTE); |
||||
} |
||||
}} |
||||
> |
||||
{selectedIndex === 0 ? t('next') : t('done')} |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,9 @@ |
||||
import React from 'react'; |
||||
import OnboardingPinExtension from './pin-extension'; |
||||
|
||||
export default { |
||||
title: 'Onboarding', |
||||
id: __filename, |
||||
}; |
||||
|
||||
export const OnboardingComponent = () => <OnboardingPinExtension />; |
Loading…
Reference in new issue