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) => {