Introduction
Every Web3 developer has faced the same problem: you start a new dApp, and the first thing you rebuild from scratch is wallet connection. Connect button, network switching, wrong-network warnings, account display — the same boilerplate, project after project.
Conflux Connect UI Kit solves this by packaging wallet connection for Conflux eSpace into a drop-in React component library. Write it once, import it anywhere. The kit supports Conflux eSpace Mainnet (chain ID 1030) and Conflux eSpace Testnet (chain ID 71), with built-in detection for MetaMask and Fluent Wallet.
This post walks through the architecture, core code, and how to integrate the kit into your own project.
What We Built
| Package | Description |
|---|---|
@conflux-connect/ui-kit |
React component library for wallet connect |
@conflux-connect/demo |
Vite demo application |
@conflux-connect/api |
Optional Express backend for SIWE auth |
Key Features
- ConnectKit — all-in-one connect button, wallet modal, and account menu
- Network switching — auto-add and switch Conflux chains in MetaMask / Fluent
- NetworkBanner — wrong-network warning with one-click fix
- Wallet detection — MetaMask, Fluent Wallet, and generic injected providers
-
Headless hook —
useConfluxConnect()for fully custom UIs - Light / dark theme — CSS variable-based theming
- Optional SIWE — Sign-In with Ethereum for backend authentication
Project Structure
Conflux-Connect-UI-Kit/
├── packages/ui/ # @conflux-connect/ui-kit
│ └── src/
│ ├── provider.tsx # Core wallet context & logic
│ ├── chains.ts # Conflux network definitions
│ ├── utils.ts # Wallet helpers
│ ├── components/ # UI components
│ └── styles.css # Component styles
├── apps/demo/ # Vite demo app
├── apps/api/ # Express SIWE API (optional)
└── package.json # npm workspaces root
Quick Start
Clone the repository and run the demo:
git clone https://github.com/your-username/Conflux-Connect-UI-Kit.git
cd Conflux-Connect-UI-Kit
npm install
npm run dev
This starts both the demo app (http://localhost:5173) and the API server (http://localhost:3001) together.
To integrate into your own React project, install the UI kit and wrap your app:
import {
ConfluxConnectProvider,
ConnectKit,
CONFLUX_ESPACE_TESTNET,
SUPPORTED_CHAINS,
} from "@conflux-connect/ui-kit";
import "@conflux-connect/ui-kit/styles.css";
function App() {
return (
<ConfluxConnectProvider
chainId={CONFLUX_ESPACE_TESTNET.chainId}
chains={[...SUPPORTED_CHAINS]}
appName="My Conflux DApp"
>
<ConnectKit theme="dark" />
</ConfluxConnectProvider>
);
}
Three steps: import, wrap with provider, add ConnectKit. Done.
Defining Conflux Networks
All supported chains are defined in packages/ui/src/chains.ts. Each chain object follows the ConfluxChain interface:
export interface ConfluxChain {
chainId: number;
chainIdHex: string;
name: string;
nativeCurrency: {
name: string;
symbol: string;
decimals: number;
};
rpcUrls: readonly string[];
blockExplorerUrls: readonly string[];
}
Conflux eSpace Mainnet and Testnet are pre-configured:
/** Conflux eSpace Mainnet — chain ID 1030 (0x406). */
export const CONFLUX_ESPACE_MAINNET: ConfluxChain = {
chainId: 1030,
chainIdHex: "0x406",
name: "Conflux eSpace",
nativeCurrency: {
name: "Conflux",
symbol: "CFX",
decimals: 18,
},
rpcUrls: ["https://evm.confluxrpc.com"],
blockExplorerUrls: ["https://evm.confluxscan.org"],
};
/** Conflux eSpace Testnet — chain ID 71 (0x47). */
export const CONFLUX_ESPACE_TESTNET: ConfluxChain = {
chainId: 71,
chainIdHex: "0x47",
name: "Conflux eSpace Testnet",
nativeCurrency: {
name: "Conflux",
symbol: "CFX",
decimals: 18,
},
rpcUrls: ["https://evmtestnet.confluxrpc.com"],
blockExplorerUrls: ["https://evmtestnet.confluxscan.org"],
};
export const SUPPORTED_CHAINS = [
CONFLUX_ESPACE_MAINNET,
CONFLUX_ESPACE_TESTNET,
] as const;
You can add a custom network by defining a new ConfluxChain object and passing it to the provider’s chains prop.
Core Provider: Wallet State Management
ConfluxConnectProvider is the brain of the library. It manages wallet state via React Context and exposes a useConfluxConnect() hook to any child component.
Provider Configuration
<ConfluxConnectProvider
chainId={71} // Default target network
chains={[...SUPPORTED_CHAINS]} // Supported networks
appName="My Conflux DApp" // Shown in wallet prompts
authApiUrl="" // Optional SIWE backend URL
>
{children}
</ConfluxConnectProvider>
| Prop | Type | Description |
|---|---|---|
chainId |
number |
Default target chain ID (defaults to Testnet 71) |
chains |
ConfluxChain[] |
List of supported networks |
appName |
string |
Application name for wallet prompts |
authApiUrl |
string |
SIWE API base URL (empty = same-origin /api) |
The Connect Flow
When a user clicks Connect Wallet, three things happen in sequence:
const connect = useCallback(async (connectorId?: string) => {
const ethereum = getEthereum();
if (!ethereum) {
setError("No wallet detected. Install MetaMask or Fluent Wallet.");
return;
}
setIsConnecting(true);
try {
await requestAccounts(ethereum); // 1. Request wallet permission
await switchToChain(ethereum, targetChain); // 2. Switch to Conflux network
await bindWallet(ethereum); // 3. Save address + chainId
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to connect wallet");
} finally {
setIsConnecting(false);
}
}, [bindWallet, getEthereum, targetChain]);
-
eth_requestAccounts— asks the wallet for permission -
wallet_switchEthereumChain— switches to the target Conflux network (or adds it if missing) - State update — stores the address and chain ID in React state
Network Switching
The switchToChain utility handles both switching and adding chains:
export async function switchToChain(
ethereum: EthereumProvider,
chain: ConfluxChain
): Promise<void> {
try {
await ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: chain.chainIdHex }],
});
} catch (err: unknown) {
const code =
err && typeof err === "object" && "code" in err
? (err as { code: number }).code
: null;
// Error 4902 = chain not added to wallet yet
if (code !== 4902) throw err;
await ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: chain.chainIdHex,
chainName: chain.name,
nativeCurrency: chain.nativeCurrency,
rpcUrls: [...chain.rpcUrls],
blockExplorerUrls: [...chain.blockExplorerUrls],
}],
});
}
}
If the Conflux network is not yet in the user’s wallet, error code 4902 is caught and wallet_addEthereumChain is called automatically.
Wallet Event Listeners
The provider listens for wallet events so the UI stays in sync even when the user changes account or network directly inside MetaMask:
useEffect(() => {
const ethereum = getEthereum();
if (!ethereum) return;
const handleAccountsChanged = (accounts: unknown) => {
const list = accounts as string[];
if (list.length === 0) {
disconnect();
return;
}
setAddress(list[0]);
};
const handleChainChanged = (hexChainId: unknown) => {
setCurrentChainId(parseInt(hexChainId as string, 16));
};
ethereum.on("accountsChanged", handleAccountsChanged);
ethereum.on("chainChanged", handleChainChanged);
void bindWallet(ethereum);
return () => {
ethereum.removeListener("accountsChanged", handleAccountsChanged);
ethereum.removeListener("chainChanged", handleChainChanged);
};
}, [bindWallet, disconnect, getEthereum]);
ConnectKit: The All-in-One Component
ConnectKit is the simplest way to add wallet connect to your app. It conditionally renders based on connection state:
export function ConnectKit({ className, theme = "dark" }: ConnectKitProps) {
const [modalOpen, setModalOpen] = useState(false);
const { isConnected, isCorrectChain } = useConfluxConnect();
return (
<div className={cn("cc-root", className)} data-theme={theme}>
{isConnected && isCorrectChain ? (
<AccountMenu theme={theme} />
) : (
<ConnectButton showModal onOpenModal={() => setModalOpen(true)} />
)}
<WalletModal open={modalOpen} onClose={() => setModalOpen(false)} />
</div>
);
}
| State | What renders |
|---|---|
| Not connected | Connect Wallet button → opens wallet modal |
| Connected, wrong network | Switch Network button |
| Connected, correct network | Account menu with address + explorer link |
Component Reference
ConnectKit
All-in-one widget. Drop it anywhere inside the provider:
<ConnectKit theme="dark" />
ConnectButton
Standalone connect / disconnect button:
import { ConnectButton } from "@conflux-connect/ui-kit";
<ConnectButton />
WalletModal
Wallet picker modal (MetaMask, Fluent):
import { WalletModal } from "@conflux-connect/ui-kit";
const [open, setOpen] = useState(false);
<WalletModal open={open} onClose={() => setOpen(false)} />
NetworkBanner
Wrong-network warning banner. Appears automatically when the user is on the wrong chain:
import { NetworkBanner } from "@conflux-connect/ui-kit";
<NetworkBanner theme="dark" />
NetworkSwitcher
Chain selector for toggling between Mainnet and Testnet:
import { NetworkSwitcher } from "@conflux-connect/ui-kit";
<div className="cc-root" data-theme="dark">
<NetworkSwitcher />
</div>
AccountMenu
Address dropdown with explorer link and disconnect:
import { AccountMenu } from "@conflux-connect/ui-kit";
<AccountMenu theme="dark" />
Headless Usage with useConfluxConnect
For fully custom UIs, use the hook directly without the pre-built components:
import { useConfluxConnect } from "@conflux-connect/ui-kit";
function CustomWalletButton() {
const {
address,
chainId,
isConnected,
isCorrectChain,
isConnecting,
connect,
disconnect,
switchNetwork,
error,
} = useConfluxConnect();
if (!isConnected) {
return (
<button onClick={() => connect()} disabled={isConnecting}>
{isConnecting ? "Connecting…" : "Connect Wallet"}
</button>
);
}
if (!isCorrectChain) {
return <button onClick={() => switchNetwork()}>Switch Network</button>;
}
return (
<div>
<span>{address}</span>
<button onClick={disconnect}>Disconnect</button>
</div>
);
}
Hook API
| Property / Method | Type | Description |
|---|---|---|
address |
string \| null |
Connected wallet address |
chainId |
number \| null |
Current chain ID |
isConnected |
boolean |
Whether a wallet is connected |
isCorrectChain |
boolean |
Whether user is on the target network |
isConnecting |
boolean |
Connect in progress |
isSwitchingChain |
boolean |
Network switch in progress |
error |
string \| null |
Last error message |
connect() |
() => Promise<void> |
Connect wallet |
disconnect() |
() => void |
Disconnect wallet |
switchNetwork(chainId?) |
(chainId?) => Promise<void> |
Switch network |
Wallet Detection
The kit auto-detects installed wallets via window.ethereum:
export function detectConnectors() {
const connectors = [];
if (typeof window !== "undefined" && window.ethereum) {
const eth = window.ethereum;
if (eth.isFluent) {
connectors.push({ id: "fluent", name: "Fluent Wallet", installed: true });
}
if (eth.isMetaMask) {
connectors.push({ id: "metamask", name: "MetaMask", installed: true });
}
if (connectors.length === 0) {
connectors.push({ id: "injected", name: "Browser Wallet", installed: true });
}
}
// Show install links for wallets not detected
connectors.push({
id: "metamask",
name: "MetaMask",
installed: false,
downloadUrl: "https://metamask.io/download/",
});
return connectors;
}
If a wallet is not installed, clicking it opens the download page in a new tab.
Optional: SIWE Authentication
Wallet connect gives you the user’s address. SIWE (Sign-In with Ethereum) proves they own that address by signing a message with their private key. This is only needed if you have a backend with user-specific data.
When do you need SIWE?
| Use case | Wallet connect only | SIWE required |
|---|---|---|
| On-chain transactions | Yes | No |
| Read public blockchain data | Yes | No |
| User profiles / saved data | No | Yes |
| Protected API routes | No | Yes |
Enabling SIWE
Set authApiUrl on the provider:
<ConfluxConnectProvider
chainId={CONFLUX_ESPACE_TESTNET.chainId}
authApiUrl="" // empty = uses Vite proxy at /api
>
Then call signIn() from the hook:
const { signIn, signOut, isAuthenticated } = useConfluxConnect();
<button onClick={() => isAuthenticated ? signOut() : signIn()}>
{isAuthenticated ? "Sign Out" : "Sign In"}
</button>
SIWE Flow
1. Client → POST /api/auth/nonce → Server generates SIWE message
2. Client → personal_sign(message) → Wallet signs the message
3. Client → POST /api/auth/verify → Server verifies signature, returns JWT
The API normalizes wallet addresses to EIP-55 checksum format before creating SIWE messages, since MetaMask often returns lowercase addresses.
Theming
Components support light and dark themes via the data-theme attribute and CSS variables:
<ConnectKit theme="dark" />
<NetworkBanner theme="light" />
Override CSS variables for custom branding:
.cc-root {
--cc-primary: #1b70e2;
--cc-primary-hover: #1558b8;
--cc-surface: #ffffff;
--cc-text: #0f172a;
--cc-border: #e2e8f0;
--cc-radius: 14px;
}
.cc-root[data-theme="dark"] {
--cc-primary: #3b9eff;
--cc-surface: #111827;
--cc-text: #f1f5f9;
--cc-border: #334155;
}
Building the Library
The UI kit is built with tsup and published from packages/ui:
# Build the library
npm run build:ui
# Output
packages/ui/dist/
├── index.js # ESM bundle
├── index.cjs # CommonJS bundle
├── index.d.ts # TypeScript declarations
└── ui-kit.css # Component styles
package.json exports
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./styles.css": "./dist/ui-kit.css"
}
}
Full Demo App Example
Here is how the demo app wires everything together in apps/demo/src/App.tsx:
import {
ConfluxConnectProvider,
ConnectKit,
NetworkBanner,
NetworkSwitcher,
useConfluxConnect,
CONFLUX_ESPACE_TESTNET,
SUPPORTED_CHAINS,
} from "@conflux-connect/ui-kit";
import "@conflux-connect/ui-kit/styles.css";
export default function App() {
return (
<ConfluxConnectProvider
chainId={CONFLUX_ESPACE_TESTNET.chainId}
chains={[...SUPPORTED_CHAINS]}
appName="Conflux Connect Demo"
authApiUrl=""
>
<header>
<ConnectKit theme="dark" />
</header>
<NetworkBanner theme="dark" />
<main>
<WalletStatus />
<div className="cc-root" data-theme="dark">
<NetworkSwitcher />
</div>
</main>
</ConfluxConnectProvider>
);
}
function WalletStatus() {
const { address, chainId, isConnected, isCorrectChain, error } =
useConfluxConnect();
return (
<div>
<p>Connected: {isConnected ? "Yes" : "No"}</p>
<p>Address: {address ?? "—"}</p>
<p>Chain ID: {chainId ?? "—"}</p>
<p>Correct Network: {isCorrectChain ? "Yes" : "No"}</p>
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}
Supported Networks
| Network | Chain ID | Hex | RPC URL |
|---|---|---|---|
| Conflux eSpace Mainnet | 1030 | 0x406 |
https://evm.confluxrpc.com |
| Conflux eSpace Testnet | 71 | 0x47 |
https://evmtestnet.confluxrpc.com |
Conclusion
Conflux Connect UI Kit eliminates the repetitive work of building wallet connection for every Conflux dApp. Wrap your app with ConfluxConnectProvider, drop in ConnectKit, and you get wallet detection, network switching, wrong-network handling, and account management out of the box.
The library is designed to be extended — use individual components, build custom UIs with the headless hook, or add SIWE authentication when your backend needs it.