Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TW-1626: Add token / network from dApp confirmations #1256

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion public/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,9 @@
"mustBeValidURL": {
"message": "Must be a valid URL"
},
"rpcURL": {
"message": "RPC URL"
},
"rpcBaseURL": {
"message": "RPC base URL",
"description": "RPC = Remote Procedure Call (https://en.wikipedia.org/wiki/Remote_procedure_call)"
Expand All @@ -1775,7 +1778,7 @@
"message": "Must be unique"
},
"addNetwork": {
"message": "Add network"
"message": "Add Network"
},
"currentNetworks": {
"message": "Current networks"
Expand Down Expand Up @@ -2014,6 +2017,9 @@
"contract": {
"message": "Contract"
},
"contractAddress": {
"message": "Contract Address"
},
"contractAddressInputDescription": {
"message": "Address of the contract to connect."
},
Expand Down
2 changes: 1 addition & 1 deletion public/_locales/en_GB/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,7 @@
"message": "Must be unique"
},
"addNetwork": {
"message": "Add network"
"message": "Add Network"
},
"currentNetworks": {
"message": "Current networks"
Expand Down
115 changes: 115 additions & 0 deletions src/app/ConfirmPage/add-asset/add-asset-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { memo, useCallback, useEffect, useMemo } from 'react';

import { CaptionAlert } from 'app/atoms';
import { HashChip } from 'app/atoms/HashChip';
import { EvmNetworkLogo } from 'app/atoms/NetworkLogo';
import Spinner from 'app/atoms/Spinner/Spinner';
import { EvmAssetIconWithNetwork } from 'app/templates/AssetIcon';
import { fetchEvmTokenMetadataFromChain } from 'lib/evm/on-chain/metadata';
import { EvmAssetStandard } from 'lib/evm/types';
import { T } from 'lib/i18n';
import { EvmTokenMetadata } from 'lib/metadata/types';
import { useTypedSWR } from 'lib/swr';
import { EvmAssetToAddMetadata } from 'lib/temple/types';
import { useEvmChainByChainId } from 'temple/front/chains';

import { useAddAsset } from './context';

interface Props {
metadata: EvmAssetToAddMetadata;
}

export const AddAssetView = memo<Props>(({ metadata }) => {
const { errorMessage, setErrorMessage, setAssetMetadata } = useAddAsset();

const network = useEvmChainByChainId(metadata.chainId);

const loadAssetMetadata = useCallback(() => {
if (!network) {
setErrorMessage('Network is not supported.');

return;
}

return fetchEvmTokenMetadataFromChain(network, metadata.address);
}, [metadata.address, network, setErrorMessage]);

const { data: metadataResponse, isValidating: isChainMetadataLoading } = useTypedSWR(
['add-dApp-asset', metadata.chainId, metadata.address],
loadAssetMetadata,
{
shouldRetryOnError: false,
focusThrottleInterval: 10_000,
dedupingInterval: 10_000
}
);

const chainMetadata = useMemo<EvmTokenMetadata>(
() => ({ ...metadataResponse, address: metadata.address, standard: EvmAssetStandard.ERC20 }),
[metadataResponse, metadata.address]
);

useEffect(() => {
if (!isChainMetadataLoading && !metadataResponse) setErrorMessage('Failed to load asset metadata.');
if (metadataResponse) {
setErrorMessage(null);
setAssetMetadata(chainMetadata);
}
}, [metadataResponse, isChainMetadataLoading, setAssetMetadata, chainMetadata, setErrorMessage]);

if (!metadataResponse && isChainMetadataLoading) {
return (
<div className="flex justify-center my-20">
<Spinner theme="gray" className="w-20" />
</div>
);
}

const name = chainMetadata?.name || metadata.name || 'Unknown Asset';
const symbol = chainMetadata?.symbol || metadata.symbol || '???';
const decimals = chainMetadata?.decimals || metadata.decimals || 0;

return (
<div className="flex flex-col gap-6 mb-6">
<div className="flex flex-col justify-center items-center text-center">
<EvmAssetIconWithNetwork metadata={chainMetadata} evmChainId={metadata.chainId} assetSlug={metadata.address} />

<span className="text-font-medium-bold mt-2">{symbol}</span>
<span className="text-font-description text-grey-1">{name}</span>
</div>

<div className="flex flex-col gap-4">
{errorMessage && <CaptionAlert type="error" message={errorMessage} className="items-center" />}

<div className="flex flex-col px-4 py-2 rounded-8 shadow-bottom border-0.5 border-transparent">
{network && (
<div className="py-2 flex flex-row justify-between items-center border-b-0.5 border-lines">
<p className="p-1 text-font-description text-grey-1">
<T id="network" />
</p>

<div className="flex flex-row items-center">
<span className="p-1 text-font-num-bold-12">{network.name}</span>
<EvmNetworkLogo chainId={network.chainId} chainName={network.name} size={16} />
</div>
</div>
)}

<div className="py-2 flex flex-row justify-between items-center border-b-0.5 border-lines">
<p className="p-1 text-font-description text-grey-1">
<T id="contractAddress" />
</p>
<HashChip hash={metadata.address} />
</div>

<div className="py-2 flex flex-row justify-between items-center">
<p className="p-1 text-font-description text-grey-1">
<T id="decimals" />
</p>
<p className="p-1 text-font-num-bold-12">{decimals}</p>
</div>
</div>
</div>
</div>
);
});
57 changes: 57 additions & 0 deletions src/app/ConfirmPage/add-asset/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useCallback, useState } from 'react';

import constate from 'constate';

import { dispatch, persistor } from 'app/store';
import { putNewEvmTokenAction } from 'app/store/evm/assets/actions';
import { putEvmTokensMetadataAction } from 'app/store/evm/tokens-metadata/actions';
import { toTokenSlug } from 'lib/assets';
import { EvmTokenMetadata } from 'lib/metadata/types';
import { useTempleClient } from 'lib/temple/front';
import { EvmAssetToAddMetadata } from 'lib/temple/types';
import { useAccountAddressForEvm } from 'temple/front';

export const [AddAssetProvider, useAddAsset] = constate(() => {
const { confirmDAppEvmAssetAdding } = useTempleClient();
const accountPkh = useAccountAddressForEvm();

const [errorMessage, setErrorMessage] = useState<string | nullish>(null);
const [assetMetadata, setAssetMetadata] = useState<EvmTokenMetadata | nullish>(null);

const handleConfirm = useCallback(
async (id: string, confirmed: boolean, dAppAssetMetadata: EvmAssetToAddMetadata) => {
if (confirmed) {
if (assetMetadata && accountPkh) {
const assetSlug = toTokenSlug(assetMetadata.address);

dispatch(
putNewEvmTokenAction({
publicKeyHash: accountPkh,
chainId: dAppAssetMetadata.chainId,
assetSlug
})
);

dispatch(
putEvmTokensMetadataAction({
chainId: dAppAssetMetadata.chainId,
records: { [assetSlug]: assetMetadata }
})
);

// ensuring the last changes to the store will be persisted before window closes
await persistor.flush();

confirmDAppEvmAssetAdding(id, confirmed);
} else {
setErrorMessage('Something’s not right. Please try again later.');
}
} else {
confirmDAppEvmAssetAdding(id, confirmed);
}
},
[accountPkh, assetMetadata, confirmDAppEvmAssetAdding]
);

return { errorMessage, setErrorMessage, assetMetadata, setAssetMetadata, handleConfirm };
});
98 changes: 98 additions & 0 deletions src/app/ConfirmPage/add-chain/add-chain-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { memo, useCallback, useMemo } from 'react';

import clsx from 'clsx';

import { Anchor, IconBase } from 'app/atoms';
import { EvmNetworkLogo } from 'app/atoms/NetworkLogo';
import { SettingsCheckbox } from 'app/atoms/SettingsCheckbox';
import { ReactComponent as CopyIcon } from 'app/icons/base/copy.svg';
import { ReactComponent as OutLinkIcon } from 'app/icons/base/outLink.svg';
import { toastSuccess } from 'app/toaster';
import { t, T } from 'lib/i18n';
import { EvmChainToAddMetadata } from 'lib/temple/types';

import { useAddChainDataState } from './context';

interface Props {
metadata: EvmChainToAddMetadata;
}

export const AddChainView = memo<Props>(({ metadata }) => {
const chainId = Number(metadata.chainId);
const { testnet, setTestnet } = useAddChainDataState();

const displayRpcUrl = useMemo(() => new URL(metadata.rpcUrl).hostname, [metadata.rpcUrl]);
const displayBlockExplorerUrl = useMemo(
() => metadata.blockExplorerUrl && new URL(metadata.blockExplorerUrl).hostname,
[metadata.blockExplorerUrl]
);

const handleCopyRpcUrl = useCallback(() => {
window.navigator.clipboard.writeText(metadata.rpcUrl);
toastSuccess(t('copiedAddress'));
}, [metadata.rpcUrl]);

const handleTestnetChange = useCallback((checked: boolean) => setTestnet(checked), [setTestnet]);

return (
<div className="flex flex-col gap-4 mb-6">
<div className="flex flex-col px-4 py-2 rounded-8 shadow-bottom border-0.5 border-transparent">
<div className="py-2 flex flex-row justify-between items-center border-b-0.5 border-lines">
<p className="p-1 text-font-description text-grey-1">
<T id="network" />
</p>
<div className="flex flex-row items-center">
<span className="p-1 text-font-num-bold-12">{metadata.name}</span>
<EvmNetworkLogo chainId={chainId} chainName={metadata.name} size={16} />
</div>
</div>

<div className="py-2 flex flex-row justify-between items-center border-b-0.5 border-lines">
<p className="p-1 text-font-description text-grey-1">
<T id="rpcURL" />
</p>
<div className="flex flex-row px-1 py-0.5 gap-x-0.5 text-secondary cursor-pointer" onClick={handleCopyRpcUrl}>
<p className="text-font-description max-w-52 truncate">{displayRpcUrl}</p>
<IconBase Icon={CopyIcon} size={12} />
</div>
</div>

<div className="py-2 flex flex-row justify-between items-center border-b-0.5 border-lines">
<p className="p-1 text-font-description text-grey-1">
<T id="chainId" />
</p>
<p className="p-1 text-font-description-bold">{chainId}</p>
</div>

<div
className={clsx(
'py-2 flex flex-row justify-between items-center',
metadata.blockExplorerUrl && 'border-b-0.5 border-lines'
)}
>
<p className="p-1 text-font-description text-grey-1">
<T id="symbol" />
</p>
<p className="p-1 text-font-description-bold">{metadata.nativeCurrency.symbol}</p>
</div>

{metadata.blockExplorerUrl && (
<div className="py-2 flex flex-row justify-between items-center">
<p className="p-1 text-font-description text-grey-1">
<T id="blockExplorer" />
</p>
<Anchor
href={metadata.blockExplorerUrl}
className="flex flex-row px-1 py-0.5 gap-x-0.5 text-secondary cursor-pointer"
>
<p className="text-font-description max-w-48 truncate">{displayBlockExplorerUrl}</p>
<IconBase Icon={OutLinkIcon} size={12} />
</Anchor>
</div>
)}
</div>

<SettingsCheckbox checked={testnet} onChange={handleTestnetChange} label={<T id="testnet" />} />
</div>
);
});
9 changes: 9 additions & 0 deletions src/app/ConfirmPage/add-chain/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useState } from 'react';

import constate from 'constate';

export const [AddChainDataProvider, useAddChainDataState] = constate(() => {
const [testnet, setTestnet] = useState(false);

return { testnet, setTestnet };
});
Loading
Loading