diff --git a/app/components/UI/AddCustomCollectible/__snapshots__/index.test.tsx.snap b/app/components/UI/AddCustomCollectible/__snapshots__/index.test.tsx.snap index 70800439d57..0b5024730d6 100644 --- a/app/components/UI/AddCustomCollectible/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/AddCustomCollectible/__snapshots__/index.test.tsx.snap @@ -1,8 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AddCustomCollectible should render correctly 1`] = ` - + + + `; diff --git a/app/components/UI/AddCustomCollectible/index.js b/app/components/UI/AddCustomCollectible/index.js deleted file mode 100644 index 92d6de74261..00000000000 --- a/app/components/UI/AddCustomCollectible/index.js +++ /dev/null @@ -1,245 +0,0 @@ -import React, { PureComponent } from 'react'; -import { Alert, Text, TextInput, View, StyleSheet } from 'react-native'; -import { colors, fontStyles } from '../../../styles/common'; -import Engine from '../../../core/Engine'; -import PropTypes from 'prop-types'; -import { strings } from '../../../../locales/i18n'; -import { isValidAddress } from 'ethereumjs-util'; -import ActionView from '../ActionView'; -import { isSmartContractAddress } from '../../../util/transactions'; -import Device from '../../../util/device'; -import { connect } from 'react-redux'; -import AnalyticsV2 from '../../../util/analyticsV2'; -import { toLowerCaseEquals } from '../../../util/general'; - -const styles = StyleSheet.create({ - wrapper: { - backgroundColor: colors.white, - flex: 1, - }, - rowWrapper: { - padding: 20, - }, - textInput: { - borderWidth: 1, - borderRadius: 4, - borderColor: colors.grey100, - padding: 16, - ...fontStyles.normal, - }, - warningText: { - marginTop: 15, - color: colors.red, - ...fontStyles.normal, - }, -}); - -/** - * PureComponent that provides ability to add custom collectibles. - */ -class AddCustomCollectible extends PureComponent { - state = { - address: '', - tokenId: '', - inputWidth: Device.isAndroid() ? '99%' : undefined, - }; - - static propTypes = { - /** - /* navigation object required to push new views - */ - navigation: PropTypes.object, - /** - * A string that represents the selected address - */ - selectedAddress: PropTypes.string, - /** - * Collectible contract object of collectible to add - */ - collectibleContract: PropTypes.object, - }; - - componentDidMount = () => { - this.mounted = true; - // Workaround https://github.com/facebook/react-native/issues/9958 - this.state.inputWidth && - setTimeout(() => { - this.mounted && this.setState({ inputWidth: '100%' }); - }, 100); - const { collectibleContract } = this.props; - collectibleContract && this.setState({ address: collectibleContract.address }); - }; - - componentWillUnmount = () => { - this.mounted = false; - }; - - getAnalyticsParams = () => { - try { - const { NetworkController } = Engine.context; - const { chainId, type } = NetworkController?.state?.provider || {}; - return { - network_name: type, - chain_id: chainId, - }; - } catch (error) { - return {}; - } - }; - - addCollectible = async () => { - if (!(await this.validateCustomCollectible())) return; - const isOwner = await this.validateCollectibleOwnership(); - if (!isOwner) { - this.handleNotCollectibleOwner(); - return; - } - const { CollectiblesController } = Engine.context; - const { address, tokenId } = this.state; - CollectiblesController.addCollectible(address, tokenId); - - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.COLLECTIBLE_ADDED, this.getAnalyticsParams()); - - this.props.navigation.goBack(); - }; - - cancelAddCollectible = () => { - this.props.navigation.goBack(); - }; - - onAddressChange = (address) => { - this.setState({ address }); - }; - - onTokenIdChange = (tokenId) => { - this.setState({ tokenId }); - }; - - validateCustomCollectibleAddress = async () => { - let validated = true; - const address = this.state.address; - const isValidEthAddress = isValidAddress(address); - if (address.length === 0) { - this.setState({ warningAddress: strings('collectible.address_cant_be_empty') }); - validated = false; - } else if (!isValidEthAddress) { - this.setState({ warningAddress: strings('collectible.address_must_be_valid') }); - validated = false; - } else if (!(await isSmartContractAddress(address))) { - this.setState({ warningAddress: strings('collectible.address_must_be_smart_contract') }); - validated = false; - } else { - this.setState({ warningAddress: `` }); - } - return validated; - }; - - validateCustomCollectibleTokenId = () => { - let validated = true; - const tokenId = this.state.tokenId; - if (tokenId.length === 0) { - this.setState({ warningTokenId: strings('collectible.token_id_cant_be_empty') }); - validated = false; - } else { - this.setState({ warningTokenId: `` }); - } - return validated; - }; - - validateCustomCollectible = async () => { - const validatedAddress = await this.validateCustomCollectibleAddress(); - const validatedTokenId = this.validateCustomCollectibleTokenId(); - return validatedAddress && validatedTokenId; - }; - - assetTokenIdInput = React.createRef(); - - jumpToAssetTokenId = () => { - const { current } = this.assetTokenIdInput; - current && current.focus(); - }; - - handleNotCollectibleOwner = () => { - Alert.alert(strings('collectible.ownership_error_title'), strings('collectible.ownership_error')); - }; - - validateCollectibleOwnership = async () => { - const { AssetsContractController } = Engine.context; - const { address, tokenId } = this.state; - const { selectedAddress } = this.props; - try { - const owner = await AssetsContractController.getOwnerOf(address, tokenId); - return toLowerCaseEquals(owner, selectedAddress); - } catch (e) { - return false; - } - }; - - render = () => { - const { address, tokenId } = this.state; - - return ( - - - - - {strings('collectible.collectible_address')} - - - {this.state.warningAddress} - - - - {strings('collectible.collectible_token_id')} - - - {this.state.warningTokenId} - - - - - - ); - }; -} - -const mapStateToProps = (state) => ({ - selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, -}); - -export default connect(mapStateToProps)(AddCustomCollectible); diff --git a/app/components/UI/AddCustomCollectible/index.test.tsx b/app/components/UI/AddCustomCollectible/index.test.tsx index 4abbe7cdc48..c5ddbd5adc6 100644 --- a/app/components/UI/AddCustomCollectible/index.test.tsx +++ b/app/components/UI/AddCustomCollectible/index.test.tsx @@ -16,6 +16,11 @@ const initialState = { }; const store = mockStore(initialState); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn().mockImplementation(() => ''), +})); + describe('AddCustomCollectible', () => { it('should render correctly', () => { const wrapper = shallow( @@ -23,6 +28,6 @@ describe('AddCustomCollectible', () => { ); - expect(wrapper.dive()).toMatchSnapshot(); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/app/components/UI/AddCustomCollectible/index.tsx b/app/components/UI/AddCustomCollectible/index.tsx new file mode 100644 index 00000000000..3ac1e668a54 --- /dev/null +++ b/app/components/UI/AddCustomCollectible/index.tsx @@ -0,0 +1,216 @@ +import React, { useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { Alert, Text, TextInput, View, StyleSheet } from 'react-native'; +import { colors, fontStyles } from '../../../styles/common'; +import Engine from '../../../core/Engine'; +import { strings } from '../../../../locales/i18n'; +import { isValidAddress } from 'ethereumjs-util'; +import ActionView from '../ActionView'; +import { isSmartContractAddress } from '../../../util/transactions'; +import Device from '../../../util/device'; +import AnalyticsV2 from '../../../util/analyticsV2'; +import { toLowerCaseEquals } from '../../../util/general'; + +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: colors.white, + flex: 1, + }, + rowWrapper: { + padding: 20, + }, + textInput: { + borderWidth: 1, + borderRadius: 4, + borderColor: colors.grey100, + padding: 16, + ...(fontStyles.normal as any), + }, + warningText: { + marginTop: 15, + color: colors.red, + ...(fontStyles.normal as any), + }, +}); + +interface AddCustomCollectibleProps { + navigation?: any; + collectibleContract?: { + address: string; + }; +} + +const AddCustomCollectible = ({ navigation, collectibleContract }: AddCustomCollectibleProps) => { + const [mounted, setMounted] = useState(true); + const [address, setAddress] = useState(''); + const [tokenId, setTokenId] = useState(''); + const [warningAddress, setWarningAddress] = useState(''); + const [warningTokenId, setWarningTokenId] = useState(''); + const [inputWidth, setInputWidth] = useState(Device.isAndroid() ? '99%' : undefined); + const assetTokenIdInput = React.createRef() as any; + + const selectedAddress = useSelector( + (state: any) => state.engine.backgroundState.PreferencesController.selectedAddress + ); + const chainId = useSelector((state: any) => state.engine.backgroundState.NetworkController.provider.chainId); + + useEffect(() => { + setMounted(true); + // Workaround https://github.com/facebook/react-native/issues/9958 + inputWidth && + setTimeout(() => { + mounted && setInputWidth('100%'); + }, 100); + collectibleContract && setAddress(collectibleContract.address); + return () => { + setMounted(false); + }; + }, [mounted, collectibleContract, inputWidth]); + + const getAnalyticsParams = () => { + try { + const { NetworkController } = Engine.context as any; + const { type } = NetworkController?.state?.provider || {}; + return { + network_name: type, + chain_id: chainId, + }; + } catch (error) { + return {}; + } + }; + + const handleNotCollectibleOwner = (): void => { + Alert.alert(strings('collectible.ownership_error_title'), strings('collectible.ownership_error')); + }; + + const validateCustomCollectibleTokenId = (): boolean => { + let validated = true; + if (tokenId.length === 0) { + setWarningTokenId(strings('collectible.token_id_cant_be_empty')); + validated = false; + } else { + setWarningTokenId(``); + } + return validated; + }; + + const validateCustomCollectibleAddress = async (): Promise => { + let validated = true; + const isValidEthAddress = isValidAddress(address); + if (address.length === 0) { + setWarningAddress(strings('collectible.address_cant_be_empty')); + validated = false; + } else if (!isValidEthAddress) { + setWarningAddress(strings('collectible.address_must_be_valid')); + validated = false; + } else if (!(await isSmartContractAddress(address, chainId))) { + setWarningAddress(strings('collectible.address_must_be_smart_contract')); + validated = false; + } else { + setWarningAddress(``); + } + return validated; + }; + + const validateCollectibleOwnership = async (): Promise => { + try { + const { AssetsContractController } = Engine.context as any; + const owner = await AssetsContractController.getOwnerOf(address, tokenId); + return toLowerCaseEquals(owner, selectedAddress); + } catch (e) { + return false; + } + }; + + const validateCustomCollectible = async (): Promise => { + const validatedAddress = await validateCustomCollectibleAddress(); + const validatedTokenId = validateCustomCollectibleTokenId(); + return validatedAddress && validatedTokenId; + }; + + const addCollectible = async (): Promise => { + if (!(await validateCustomCollectible())) return; + const isOwner = await validateCollectibleOwnership(); + if (!isOwner) { + handleNotCollectibleOwner(); + return; + } + const { CollectiblesController } = Engine.context as any; + CollectiblesController.addCollectible(address, tokenId); + + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.COLLECTIBLE_ADDED, getAnalyticsParams()); + + navigation.goBack(); + }; + + const cancelAddCollectible = (): void => { + navigation.goBack(); + }; + + const onAddressChange = (newAddress: string): void => { + setAddress(newAddress); + }; + + const onTokenIdChange = (newTokenId: string): void => { + setTokenId(newTokenId); + }; + + const jumpToAssetTokenId = (): void => { + assetTokenIdInput.current?.focus(); + }; + + return ( + + + + + {strings('collectible.collectible_address')} + + + {warningAddress} + + + + {strings('collectible.collectible_token_id')} + + + {warningTokenId} + + + + + + ); +}; + +export default AddCustomCollectible; diff --git a/app/util/analyticsV2.js b/app/util/analyticsV2.js index 850f7554499..08594a2d746 100644 --- a/app/util/analyticsV2.js +++ b/app/util/analyticsV2.js @@ -104,7 +104,7 @@ export const ANALYTICS_EVENTS_V2 = { /** * This takes params with the following structure: * { foo : 'this is not anonymous', bar: {value: 'this is anonymous', anonymous: true} } - * @param {String} eventName + * @param {Object} eventName * @param {Object} params */ export const trackEventV2 = (eventName, params) => {