@wheelx-widget/widget-native
v0.1.4
Published
WheelX Widget Native - React Native WebView version of cross-chain swap and bridge trading component, wallet connection via RN bridge
Maintainers
Readme
@wheelx-widget/widget-native
The React Native WebView version of the WheelX Widget. Designed to be loaded inside a react-native-webview, it communicates with the host RN app via postMessage and CustomEvent for wallet connections, EIP-1193 bridging, and transaction signing.
Wallet Framework: widget-native relies exclusively on Reown (reown.com) on the RN side. The host app must integrate
@reown/appkit-react-nativeto provide wallet functionality.
Features
- 🔄 Cross-chain Swap & Bridge — Full trading flow with quote polling, gas estimation, approval handling
- 👛 Reown Wallet Integration — All wallet operations forwarded to RN host's Reown AppKit via WebView bridge
- 🌐 Multi-chain Support — EVM (50+ chains) and Solana with unified bridge protocol
- 🎨 Configurable Theme — 14 color tokens for custom skinning
- 📦 Framework Agnostic — Works with any React framework (Next.js, Vite, Create React App, etc.)
- 🔌 Configurable Default Tokens — Set default from/to chains and tokens
- 📱 React Native Ready — Built specifically for RN WebView embedding with bridge protocol
Architecture
┌──────────────────────────────────────────────────────────┐
│ WebView (widget-native) │
│ │
│ WheelXProvider │
│ ├── ExternalWalletBridge ← CustomEvent('wheelx:walletState') │
│ ├── Eip1193BridgeProvider ← window.__wheelx_ethereum │
│ ├── Eip1193AutoConnector → wagmi auto-connect │
│ └── WheelXWidget (MainForm) │
│ ├── FormButton → useWalletConnectModal │
│ ├── TxState → transaction flow │
│ └── Solana Tx → useSolanaSendTransaction │
│ │
└──────────────────────┬───────────────────────────────────┘
│ postMessage (Widget → RN)
│ injectJavaScript (RN → Widget)
┌──────────────────────▼───────────────────────────────────┐
│ React Native Host (e.g. desafa-master) │
│ │
│ AppContent.tsx │
│ ├── WebView component │
│ ├── onMessage handler → routes bridge messages │
│ └── injectLatestState() → pushes wallet state to WebView│
│ │
│ BridgeHandler.ts │
│ ├── buildWalletState() — constructs wallet state │
│ ├── handleEip1193Request() — JSON-RPC via Reown │
│ └── handleSolanaSignRequest() — sign + broadcast │
│ │
│ Reown AppKit (@reown/appkit-react-native) │
│ ├── EVM: EthersAdapter + WagmiAdapter │
│ ├── Solana: SolanaAdapter + Phantom/Solflare │
│ └── Wallet Connect / Disconnect / Chain Switch │
│ │
└──────────────────────────────────────────────────────────┘Installation
npm install @wheelx-widget/widget-native @chakra-ui/react @emotion/reactNote: @chakra-ui/react and @emotion/react are required peer dependencies.
Quick Start
// main.tsx — wrap with ChakraProvider
import { ChakraProvider, defaultSystem } from '@chakra-ui/react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(
<ChakraProvider value={defaultSystem}>
<App />
</ChakraProvider>
)// App.tsx
// Import the CSS for animations (required!)
import '@wheelx-widget/widget-native/styles.css'
import { WheelXProvider, WheelXWidget } from '@wheelx-widget/widget-native'
function App() {
return (
<WheelXProvider>
<WheelXWidget />
</WheelXProvider>
)
}RN ↔ WebView Communication Protocol
The widget communicates with the RN host through a bidirectional bridge.
RN → Widget (via injectJavaScript + CustomEvent)
The RN host injects JavaScript into the WebView to dispatch CustomEvents that the widget listens for.
| CustomEvent | Payload | Description |
| ---------------------- | ---------------------------------------------------------------------- | ---------------------------- |
| wheelx:walletState | { evmAddress, solanaAddress, chainId, walletType, isConnected, ... } | Full wallet connection state |
| wheelx:eip1193Result | { requestId, result, error } | EIP-1193 JSON-RPC response |
| wheelx:solanaResult | { requestId, signature, error } | Solana signing result |
RN injection example:
// Inside RN host AppContent.tsx
webViewRef.current?.injectJavaScript(`
window.dispatchEvent(new CustomEvent('wheelx:walletState', {
detail: {
evmAddress: '0x...',
solanaAddress: null,
chainId: 8453,
chainIdStr: '8453',
walletType: 'evm',
isConnected: true
}
}));
true;
`)Widget → RN (via postMessage)
The widget sends JSON messages to RN through window.ReactNativeWebView.postMessage().
| Message Type | Payload | Description |
| --------------------------- | ---------------------------------------- | ----------------------------------------------------- |
| WHEELX_REQUEST_CONNECT | { type, timestamp, chainType } | Request RN to open Reown wallet connect modal |
| WHEELX_REQUEST_DISCONNECT | { type, timestamp } | Request RN to disconnect wallet |
| WHEELX_EIP1193_REQUEST | { type, requestId, method, params } | EIP-1193 JSON-RPC request (eth_sendTransaction, etc.) |
| WHEELX_SOLANA_SIGN | { type, requestId, txMessage, sender } | Solana transaction signing request |
RN handler example:
// Inside RN host
const handleMessage = (event: WebViewMessageEvent) => {
const data = JSON.parse(event.nativeEvent.data);
switch (data.type) {
case 'WHEELX_REQUEST_CONNECT':
appKit.open({ view: 'Connect' });
break;
case 'WHEELX_EIP1193_REQUEST':
handleEip1193Request(data);
break;
case 'WHEELX_SOLANA_SIGN':
handleSolanaSignRequest(data);
break;
}
};
<WebView onMessage={handleMessage} ... />EIP-1193 Bridge (EVM Signing)
- wagmi calls
window.ethereum.request({ method: 'eth_sendTransaction', params: [...] }) - This is intercepted by
window.__wheelx_ethereum(injected by RN) - The request is forwarded to RN via
WHEELX_EIP1193_REQUESTpostMessage - RN's Reown AppKit processes the request via
walletProvider.request() - Result is injected back as
wheelx:eip1193ResultCustomEvent - The original Promise resolves/rejects accordingly
Key detail: Error codes (e.g., 4902 for chain not added) are preserved, enabling wagmi's automatic chain-switch/add fallback logic.
Solana Signing Flow
- Widget calls
requestSolanaSignFromRN(txMessage, sender) - Sends
WHEELX_SOLANA_SIGNvia postMessage with requestId - Awaits
wheelx:solanaResultCustomEvent matching the requestId - RN's Reown AppKit signs via
solanaProvider.request({ method: 'solana_signTransaction' }) - RN broadcasts the signed transaction to the network
- Signature is injected back to WebView, resolving the Promise
- Timeout: 5 minutes
State Injection Strategy
To ensure the WebView always has the correct wallet state, RN should inject at multiple timepoints:
- Before page load —
injectedJavaScriptBeforeContentLoadedsetswindow.__wheelx_initial_wallet_state__ - On load (0ms) — Immediate state injection after page completes
- On load (500ms) — EIP-1193 provider + wallet state injection (for Reown async session restore)
- On load (2000ms) — Fallback injection (belated session restore)
- Event-driven — Inject state on
CONNECT_SUCCESS/DISCONNECT_SUCCESS/accountsChanged
Reown Integration on RN Side
The widget-native expects the RN host to integrate Reown AppKit. Here's a minimal setup based on the reference implementation:
1. Install Dependencies
npm install @reown/appkit-react-native @reown/appkit-ethers-react-native @reown/appkit-solana-react-native react-native-webview2. Configure AppKit
// AppKitConfig.ts
import { AppKit } from '@reown/appkit-react-native'
import { EthersAdapter } from '@reown/appkit-ethers-react-native'
import { SolanaAdapter } from '@reown/appkit-solana-react-native'
const evmAdapter = new EthersAdapter()
const solanaAdapter = new SolanaAdapter()
const appKit = new AppKit({
adapters: [evmAdapter, solanaAdapter],
projectId: 'your-reown-project-id',
networks: [
/* your chains */
]
})3. Wrap with Providers
// App.tsx
import { AppKitProvider } from '@reown/appkit-react-native'
export default function App() {
return (
<SafeAreaProvider>
<AppKitProvider>
<AppContent />
</AppKitProvider>
</SafeAreaProvider>
)
}4. Bridge Handler
Implement a bridge handler that:
- Listens for
postMessageevents from the WebView - Routes
WHEELX_REQUEST_CONNECT→appKit.open() - Routes
WHEELX_EIP1193_REQUEST→walletProvider.request() - Routes
WHEELX_SOLANA_SIGN→ Solana provider sign + broadcast - Injects wallet state on connect/disconnect/account changes
Refer to the reference implementation at F:\react-native-app\desafa-master for a complete working example.
Configuration
Theme Customization
Customize 14 visual tokens:
<WheelXProvider
config={{
theme: {
defaultTextColor: '#1A1D26',
highlightTextColor: '#007B9D',
weakTextColor: '#5D6270',
defaultBgColor: '#FFFFFF',
highlightBgColor: '#E6F7FB',
assistBgColor: '#F5F6F8',
defaultBorderColor: '#E2E4E8',
highlightBorderColor: '#007B9D',
highlightButtonBg: '#007B9D',
highlightButtonText: '#FFFFFF',
normalButtonBg: '#F5F6F8',
normalButtonText: '#1A1D26',
disabledButtonBg: '#E2E4E8',
disabledButtonText: '#A0A4AE'
}
}}
>
<WheelXWidget />
</WheelXProvider>Default Chain/Token Configuration
<WheelXProvider
config={{
from: {
chains: [8453, 1], // Only show Base and Ethereum as from chains
defaultChainId: 8453 // Default to Base
},
to: {
chains: [1868], // Only show Soneium as to chain
defaultChainId: 1868 // Default to Soneium
}
}}
>
<WheelXWidget />
</WheelXProvider>Fixed Recipient Address (Different Address)
Configure fixed recipient addresses for EVM or Solana chains. When set, the widget will:
- Display the configured address in the To field instead of showing "Connect Wallet"
- Query the token balance of the configured address
- Skip wallet connection for the target chain (only the source chain wallet is required for signing)
- Show GetGas when the configured address has insufficient native token balance
This is useful for exchange or merchant scenarios where funds should always be sent to a specific address regardless of which wallet the user connects.
Priority order for recipient address:
- Configured fixed address (
evmDifferentAddress/solanaDifferentAddress) - User manually entered different address (via Different Address dialog)
- Connected wallet address
<WheelXProvider
config={{
evmDifferentAddress: '0xAbCdEf1234567890...', // Fixed recipient for EVM chains (0x format)
solanaDifferentAddress: '9zQX...' // Fixed recipient for Solana chains (base58 format)
}}
>
<WheelXWidget />
</WheelXProvider>Note: You can configure one, both, or neither. The widget will only use the relevant one based on the selected To chain.
API Reference
<WheelXProvider>
The root provider component that sets up wallet connections, React Query, and theme.
| Prop | Type | Default | Description |
| ---------- | ----------------- | ------- | -------------------- |
| config | WidgetConfig | {} | Widget configuration |
| children | React.ReactNode | — | Child components |
Key components inside WheelXProvider:
ExternalWalletBridge— Listens for RN-injected wallet state viawheelx:walletStateCustomEventEip1193BridgeProvider— Detectswindow.__wheelx_ethereumEIP-1193 bridge provider from RNEip1193AutoConnector— Auto-connects wagmi to the EIP-1193 bridge when availableChainsAndTokensLoader— Fetches and caches chain/token metadata from API
<WheelXWidget>
The main trading widget component.
| Prop | Type | Default | Description |
| -------- | -------------- | ------- | -------------------- |
| config | WidgetConfig | {} | Widget configuration |
WidgetConfig
interface WidgetConfig {
theme?: Partial<WidgetTheme>
from?: {
chains?: number[]
defaultChainId?: number
chainTokens?: Record<number, TokenConfig[]>
defaultToken?: { chainId: number; address: string }
}
to?: {
chains?: number[]
defaultChainId?: number
chainTokens?: Record<number, TokenConfig[]>
defaultToken?: { chainId: number; address: string }
}
routerCallback?: {
onPathnameChange?: (pathname: string) => void
onRouteLeave?: () => void
getCurrentPathname?: () => string
}
/**
* Deep link handler. When the widget runs inside a WebView, this passes wallet deep link URLs
* to the host app for handling (e.g., opening the wallet app via Linking.openURL).
*
* If not provided, the widget will automatically attempt to communicate via
* ReactNativeWebView.postMessage or custom DOM events.
*/
onDeepLink?: (url: string) => void
/** Fixed receive address for EVM chains (0x format). When set, this address is used as the recipient on EVM destination chains. */
evmDifferentAddress?: string
/** Fixed receive address for Solana chains (base58 format). When set, this address is used as the recipient on Solana destination chains. */
solanaDifferentAddress?: string
}WidgetTheme
interface WidgetTheme {
defaultTextColor: string
highlightTextColor: string
weakTextColor: string
defaultBgColor: string
highlightBgColor: string
assistBgColor: string
defaultBorderColor: string
highlightBorderColor: string
highlightButtonBg: string
highlightButtonText: string
normalButtonBg: string
normalButtonText: string
disabledButtonBg: string
disabledButtonText: string
/** Color palette for the GetGas switch component. e.g. 'purple', 'blue', 'green'. Default: 'purple' */
getGasSwitchColorPalette?: string
}Exported Bridge Utilities
// External Wallet State (injected by RN)
export type { ExternalWalletState } from '@wheelx-widget/widget-native'
export { ExternalWalletBridge } from '@wheelx-widget/widget-native'
// EIP-1193 Bridge Provider
export {
Eip1193BridgeProvider,
useEip1193Bridge,
isEip1193BridgeAvailable
} from '@wheelx-widget/widget-native'
export type { BridgeProviderStatus } from '@wheelx-widget/widget-native'
// Wallet Connect via RN Bridge
export {
useWalletConnectModal,
WHEELX_MESSAGES
} from '@wheelx-widget/widget-native'
// Platform detection
export {
isReactNativeWebView,
isWebView,
isMobileDevice
} from '@wheelx-widget/widget-native'Communication Protocol Reference
ExternalWalletState
The wallet state object injected by RN via wheelx:walletState CustomEvent:
interface ExternalWalletState {
// EVM
evmAddress: string | null
evmWalletName?: string | null // Per-chain wallet name (e.g. "MetaMask")
evmWalletIcon?: string | null // Per-chain wallet icon URL
// Solana
solanaAddress: string | null
solanaWalletName?: string | null // Per-chain wallet name (e.g. "Phantom")
solanaWalletIcon?: string | null // Per-chain wallet icon URL
// Chain info
chainId: number | null
chainIdStr: string | null
walletType: 'evm' | 'solana' | null
// Legacy (single wallet — kept for backward compat)
walletName: string | null
walletIcon: string | null
// Connection status
isConnected: boolean
}Pre-injected State
RN can pre-inject wallet state before the widget's JavaScript runs:
// In RN: injectedJavaScriptBeforeContentLoaded
window.__wheelx_initial_wallet_state__ = {
evmAddress: '0x...',
chainId: 8453
// ...full ExternalWalletState
}The ExternalWalletBridge component will pick this up on mount and remove it from window.
Demo
The demo page is available at https://widget.wheelx.fi/widget-native.
Troubleshooting
| Problem | Symptom | Solution |
|----------|---------|----------|
| Forgot to import CSS | Component works but has no styling/animations | Add import '@wheelx-widget/widget-native/styles.css' in your entry file |
| Wallet not connecting | Widget shows "Connect Wallet" but never connects | Ensure the RN host properly implements the bridge — inject wheelx:walletState CustomEvent on connect/disconnect, and handle WHEELX_REQUEST_CONNECT postMessage |
| EIP-1193 transactions fail | Transaction signing hangs or errors out | Verify window.__wheelx_ethereum provider is injected and routes WHEELX_EIP1193_REQUEST to Reown's walletProvider.request(). Check that error codes (e.g., 4902) are preserved |
| React hooks errors | "Invalid hook call" or context mismatch | You may have duplicate React instances. Ensure react and react-dom are peer dependencies in your project, and use a single version (18 or 19) |
License
MIT
