Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7,117 changes: 5,133 additions & 1,984 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.5",
"@fontsource/mulish": "^5.0.18",
"@iexec/web3mail": "^1.2.0",
"@iexec/web3mail": "^1.6.0",
"@mui/material": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0"
Expand Down
136 changes: 133 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,26 @@ import {
Box,
Button,
CircularProgress,
FormControl,
MenuItem,
Select,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Typography,
} from '@mui/material';
import { Fragment, useState } from 'react';
import { Fragment, useState, useEffect } from 'react';
import { type Contact } from '@iexec/web3mail';
import { fetchMyContacts, sendMail } from './web3mail/web3mail';
import { SUPPORTED_CHAINS } from './web3mail/utils';
import { useWalletConnection } from './hooks/useWalletConnection';
import './styles.css';

export default function App() {
const { isConnected, address } = useWalletConnection();
const [selectedChain, setSelectedChain] = useState(SUPPORTED_CHAINS[1].id);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [displayTable, setdisplayTable] = useState(false);
Expand Down Expand Up @@ -64,10 +72,65 @@ export default function App() {
return pageNumbers;
};

const handleChainChange = async (event: { target: { value: number } }) => {
const newChainId = Number(event.target.value);
setSelectedChain(newChainId);

// Reset state when switching chains
setContacts([]);
setdisplayTable(false);
setErrorMessage('');
setemailSentSuccess(false);

// Switch MetaMask to the selected chain
try {
const chain = SUPPORTED_CHAINS.find((c) => c.id === newChainId);
if (!chain) return;

const chainIdHex = `0x${newChainId.toString(16)}`;

try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chainIdHex }],
});
} catch (switchError: unknown) {
if ((switchError as { code?: number }).code === 4902) {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: chainIdHex,
chainName: chain.name,
nativeCurrency: {
name: chain.tokenSymbol,
symbol: chain.tokenSymbol,
decimals: 18,
},
rpcUrls: chain.rpcUrls,
blockExplorerUrls: [chain.blockExplorerUrl],
},
],
});
}
}
} catch (error) {
console.error('Failed to switch chain:', error);
}
};

// Force chain switch on component mount if wallet is connected
useEffect(() => {
if (isConnected && window.ethereum) {
handleChainChange({ target: { value: selectedChain } });
}
}, [isConnected, selectedChain]);

const handleLoadAddresses = async () => {
try {
setLoading(true);
const { contacts: myContacts, error } = await fetchMyContacts();
const { contacts: myContacts, error } =
await fetchMyContacts(selectedChain);
setLoading(false);
if (error) {
setErrorMessage(error);
Expand All @@ -78,6 +141,9 @@ export default function App() {
} catch (err) {
console.log('[fetchMyContacts] ERROR', err);
setLoading(false);
setErrorMessage(
'Failed to load contacts. Please check your wallet connection and chain selection.'
);
}
};

Expand All @@ -89,18 +155,82 @@ export default function App() {
'Sandbox mail content',
protectedData,
'text/plain',
'iExec-sandbox'
'iExec-sandbox',
selectedChain
);
setLoading(false);
setemailSentSuccess(true);
} catch (err) {
console.log('[sendEmail] ERROR', err);
setLoading(false);
setErrorMessage(
'Failed to send email. Please check your wallet connection and chain selection.'
);
}
};

return (
<Box className="my-box">
{/* Chain Selection */}
<Box
sx={{
marginBottom: '30px',
padding: '20px',
border: '1px solid #ddd',
borderRadius: '8px',
}}
>
<Typography
variant="h6"
sx={{ marginBottom: '20px', textAlign: 'center' }}
>
Chain Selection
</Typography>

<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
marginBottom: '20px',
flexWrap: 'wrap',
justifyContent: 'center',
}}
>
<FormControl size="small" sx={{ minWidth: 150 }}>
<Select
value={selectedChain}
onChange={handleChainChange}
displayEmpty
>
{SUPPORTED_CHAINS.map((chain) => (
<MenuItem key={chain.id} value={chain.id}>
{chain.name}
</MenuItem>
))}
</Select>
</FormControl>
</Box>

<Typography
variant="body2"
sx={{ marginBottom: '10px', color: '#666' }}
>
Selected: {SUPPORTED_CHAINS.find((c) => c.id === selectedChain)?.name}{' '}
(ID: {selectedChain})
</Typography>
<Typography variant="body2" sx={{ color: '#666' }}>
<strong>Wallet:</strong> {isConnected ? 'Connected' : 'Disconnected'}
{address && (
<>
<br />
<strong>Address:</strong> {address.slice(0, 6)}...
{address.slice(-4)}
</>
)}
</Typography>
</Box>

<Button
sx={{ display: 'block', margin: '30px auto' }}
onClick={handleLoadAddresses}
Expand Down
70 changes: 70 additions & 0 deletions src/hooks/useWalletConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useEffect, useState } from 'react';

export function useWalletConnection() {
const [isConnected, setIsConnected] = useState(false);
const [address, setAddress] = useState<string | null>(null);
const [chainId, setChainId] = useState<number | null>(null);

useEffect(() => {
if (!window.ethereum) {
return;
}

const checkConnection = async () => {
try {
const accounts = await window.ethereum.request({
method: 'eth_accounts',
});

if (accounts.length > 0) {
setIsConnected(true);
setAddress(accounts[0]);

const newChainId = await window.ethereum.request({
method: 'eth_chainId',
});
const parsedChainId = parseInt(newChainId, 16);
setChainId(parsedChainId);
}
} catch (error) {
console.error('Error checking connection:', error);
}
};

checkConnection();

const handleAccountsChanged = (accounts: string[]) => {
if (accounts.length > 0) {
setIsConnected(true);
setAddress(accounts[0]);
} else {
setIsConnected(false);
setAddress(null);
}
};

const handleChainChanged = (newChainHexId: string) => {
const newChainId = parseInt(newChainHexId, 16);
setChainId(newChainId);
};

window.ethereum.on('accountsChanged', handleAccountsChanged);
window.ethereum.on('chainChanged', handleChainChanged);

return () => {
if (window.ethereum.removeListener) {
window.ethereum.removeListener(
'accountsChanged',
handleAccountsChanged
);
window.ethereum.removeListener('chainChanged', handleChainChanged);
}
};
}, []);

return {
isConnected,
address,
chainId,
};
}
11 changes: 9 additions & 2 deletions src/styles.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
html, body {
font-family: "Mulish", ui-sans-serif, system-ui, sans-serif;
html,
body {
font-family: 'Mulish', ui-sans-serif, system-ui, sans-serif;
text-align: center;
}

#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
}

.ButtonSendM {
display: flex;
flex-direction: row;
Expand Down
64 changes: 53 additions & 11 deletions src/web3mail/utils.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,81 @@
export type Address = `0x${string}`;

export type AddressOrEnsName = Address | string;

export const IEXEC_EXPLORER_URL = 'https://explorer.iex.ec/bellecour/dataset/';

export const WEB3MAIL_APP_ENS = 'web3mail.apps.iexec.eth';

const IEXEC_CHAIN_ID = '0x86'; // 134

export const SUPPORTED_CHAINS = [
{
id: 134,
name: 'Bellecour',
blockExplorerUrl: 'https://blockscout-bellecour.iex.ec',
tokenSymbol: 'xRLC',
rpcUrls: ['https://bellecour.iex.ec'],
},
{
id: 42161,
name: 'Arbitrum',
blockExplorerUrl: 'https://arbiscan.io/',
tokenSymbol: 'RLC',
rpcUrls: ['https://arb1.arbitrum.io/rpc'],
},
{
id: 421614,
name: 'Arbitrum Sepolia',
blockExplorerUrl: 'https://sepolia.arbiscan.io/',
tokenSymbol: 'RLC',
rpcUrls: ['https://sepolia-rollup.arbitrum.io/rpc'],
},
];

export function checkIsConnected() {
if (!window.ethereum) {
console.log('Please install MetaMask');
throw new Error('No Ethereum provider found');
}
}

export async function checkCurrentChain() {
export async function checkCurrentChain(selectedChainId?: number) {
const currentChainId = await window.ethereum.request({
method: 'eth_chainId',
params: [],
});
if (currentChainId !== IEXEC_CHAIN_ID) {
console.log('Please switch to iExec chain');

const targetChainId = selectedChainId
? `0x${selectedChainId.toString(16)}`
: IEXEC_CHAIN_ID;

if (currentChainId !== targetChainId) {
const chain = SUPPORTED_CHAINS.find((c) => c.id === selectedChainId);
if (!chain) {
throw new Error(`Chain with ID ${selectedChainId} not supported`);
}

console.log(`Please switch to ${chain.name} chain`);
try {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: '0x86',
chainName: 'iExec Sidechain',
chainId: targetChainId,
chainName: chain.name,
nativeCurrency: {
name: 'xRLC',
symbol: 'xRLC',
name: chain.tokenSymbol,
symbol: chain.tokenSymbol,
decimals: 18,
},
rpcUrls: ['https://bellecour.iex.ec'],
blockExplorerUrls: ['https://blockscout-bellecour.iex.ec'],
rpcUrls: chain.rpcUrls || ['https://bellecour.iex.ec'],
blockExplorerUrls: [chain.blockExplorerUrl],
},
],
});
console.log('Switched to iExec chain');
console.log(`Switched to ${chain.name} chain`);
} catch (err) {
console.error('Failed to switch to iExec chain:', err);
console.error(`Failed to switch to ${chain.name} chain:`, err);
throw err;
}
}
Expand Down
Loading