@wheelx-widget/widget 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,7 @@ An open-source, framework-agnostic React widget for cross-chain token swaps and
10
10
  - ⚛️ **Framework Agnostic** — Works with any React framework (Next.js, Vite, Create React App, etc.)
11
11
  - 📦 **Tree-shakeable** — ESM + CJS builds with TypeScript declarations
12
12
  - 🔌 **Configurable Default Tokens** — Set default from/to chains and tokens
13
+ - 📱 **Deep Link Support** — Wallet app launch support in WebView (React Native) — see [Deep Link Guide](./DEEP_LINK_GUIDE.md)
13
14
 
14
15
  ## Installation
15
16
 
@@ -105,6 +106,45 @@ Customize 14 visual tokens:
105
106
  </WheelXProvider>
106
107
  ```
107
108
 
109
+ ### API Base URL
110
+
111
+ Override the default WheelX API endpoint. Useful for testing against a staging or development environment.
112
+
113
+ ```tsx
114
+ <WheelXProvider config={{
115
+ apiBaseUrl: 'https://dev-api.wheelx.fi' // Default: 'https://api.wheelx.fi'
116
+ }}>
117
+ <WheelXWidget />
118
+ </WheelXProvider>
119
+ ```
120
+
121
+ ### Fixed Recipient Address (Different Address)
122
+
123
+ Configure fixed recipient addresses for EVM or Solana chains. When set, the widget will:
124
+
125
+ - **Display the configured address** in the To field instead of showing "Connect Wallet"
126
+ - **Query the token balance** of the configured address
127
+ - **Skip wallet connection** for the target chain (only the source chain wallet is required for signing)
128
+ - **Show GetGas** when the configured address has insufficient native token balance
129
+
130
+ 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.
131
+
132
+ **Priority order for recipient address:**
133
+ 1. Configured fixed address (`evmDifferentAddress` / `solanaDifferentAddress`)
134
+ 2. User manually entered different address (via Different Address dialog)
135
+ 3. Connected wallet address
136
+
137
+ ```tsx
138
+ <WheelXProvider config={{
139
+ evmDifferentAddress: '0xAbCdEf1234567890...', // Fixed recipient for EVM chains (0x format)
140
+ solanaDifferentAddress: '9zQX...' // Fixed recipient for Solana chains (base58 format)
141
+ }}>
142
+ <WheelXWidget />
143
+ </WheelXProvider>
144
+ ```
145
+
146
+ > **Note:** You can configure one, both, or neither. The widget will only use the relevant one based on the selected To chain.
147
+
108
148
  ## API Reference
109
149
 
110
150
  ### `<WheelXProvider>`
@@ -129,6 +169,10 @@ The main trading widget component.
129
169
  ```typescript
130
170
  interface WidgetConfig {
131
171
  theme?: Partial<WidgetTheme>
172
+
173
+ /** API base URL. Default 'https://api.wheelx.fi' */
174
+ apiBaseUrl?: string
175
+
132
176
  from?: {
133
177
  chains?: number[]
134
178
  defaultChainId?: number
@@ -147,6 +191,12 @@ interface WidgetConfig {
147
191
  getCurrentPathname?: () => string
148
192
  }
149
193
  dynamicEnvironmentId?: string
194
+ onDeepLink?: (url: string) => void // See Deep Link Guide for WebView environments
195
+
196
+ /** Fixed recipient address for EVM chains (0x format). Takes priority over wallet address and manual different address */
197
+ evmDifferentAddress?: string
198
+ /** Fixed recipient address for Solana chains (base58 format). Takes priority over wallet address and manual different address */
199
+ solanaDifferentAddress?: string
150
200
  }
151
201
  ```
152
202
 
@@ -168,6 +218,8 @@ interface WidgetTheme {
168
218
  normalButtonText: string
169
219
  disabledButtonBg: string
170
220
  disabledButtonText: string
221
+ /** Color palette for the GetGas switch component. e.g. 'purple', 'blue', 'green'. Default: 'purple' */
222
+ getGasSwitchColorPalette?: string
171
223
  }
172
224
  ```
173
225
 
@@ -179,6 +231,17 @@ A demo `https://widget.wheelx.fi`
179
231
 
180
232
  This widget connects to the WheelX API at `https://api.wheelx.fi`. The API domain is not configurable.
181
233
 
234
+ ## React Native / WebView Integration
235
+
236
+ If you embed the Widget into a React Native WebView, additional configuration is required to properly launch wallet apps.
237
+
238
+ Please refer to: **[Mobile Wallet Launch Guide (DEEP_LINK_GUIDE.md)](./DEEP_LINK_GUIDE.md)**
239
+
240
+ In short, three things are required:
241
+ 1. Add `onMessage` listener in the WebView
242
+ 2. Configure `<queries>` declaration in Android's `AndroidManifest.xml`
243
+ 3. Configure `LSApplicationQueriesSchemes` in iOS's `Info.plist`
244
+
182
245
  ## License
183
246
 
184
247
  MIT
@@ -0,0 +1,426 @@
1
+ import React from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ import { Fraction } from 'bi-fraction';
4
+ import * as _tanstack_react_query from '@tanstack/react-query';
5
+ import * as zustand from 'zustand';
6
+
7
+ interface TokenConfig {
8
+ chainId: number;
9
+ address: string;
10
+ decimals?: number;
11
+ symbol?: string;
12
+ name?: string;
13
+ logo?: string;
14
+ }
15
+ interface WidgetTheme {
16
+ defaultTextColor: string;
17
+ highlightTextColor: string;
18
+ weakTextColor: string;
19
+ defaultBgColor: string;
20
+ highlightBgColor: string;
21
+ assistBgColor: string;
22
+ defaultBorderColor: string;
23
+ highlightBorderColor: string;
24
+ highlightButtonBg: string;
25
+ highlightButtonText: string;
26
+ normalButtonBg: string;
27
+ normalButtonText: string;
28
+ disabledButtonBg: string;
29
+ disabledButtonText: string;
30
+ /**
31
+ * GetGas Switch colorPalette — accepts Chakra UI color palette names
32
+ * e.g. 'gray', 'red', 'orange', 'yellow', 'green', 'teal', 'cyan', 'blue', 'purple', 'pink'
33
+ * @default 'purple'
34
+ */
35
+ getGasSwitchColorPalette?: string;
36
+ }
37
+ interface WidgetConfig {
38
+ theme?: Partial<WidgetTheme>;
39
+ /** API base URL. Default 'https://api.wheelx.fi' */
40
+ apiBaseUrl?: string;
41
+ from?: {
42
+ chains?: number[];
43
+ defaultChainId?: number;
44
+ chainTokens?: Record<number, TokenConfig[]>;
45
+ defaultToken?: {
46
+ chainId: number;
47
+ address: string;
48
+ };
49
+ };
50
+ to?: {
51
+ chains?: number[];
52
+ defaultChainId?: number;
53
+ chainTokens?: Record<number, TokenConfig[]>;
54
+ defaultToken?: {
55
+ chainId: number;
56
+ address: string;
57
+ };
58
+ };
59
+ routerCallback?: {
60
+ onPathnameChange?: (pathname: string) => void;
61
+ onRouteLeave?: () => void;
62
+ getCurrentPathname?: () => string;
63
+ };
64
+ dynamicEnvironmentId?: string;
65
+ /**
66
+ * 深度链接处理器。当 widget 在 WebView 中运行时,用于将钱包的 deep link URL
67
+ * 传递给宿主 App 处理(如通过 Linking.openURL 打开钱包 App)。
68
+ *
69
+ * 如果不提供此回调,widget 会自动尝试通过 ReactNativeWebView.postMessage
70
+ * 或自定义 DOM 事件进行通信。
71
+ */
72
+ onDeepLink?: (url: string) => void;
73
+ /** EVM链固定收款异地址(0x格式)。配置后,to网络为EVM链时优先使用此地址 */
74
+ evmDifferentAddress?: string;
75
+ /** Solana链固定收款异地址(base58格式)。配置后,to网络为Solana链时优先使用此地址 */
76
+ solanaDifferentAddress?: string;
77
+ }
78
+
79
+ interface WheelXProviderProps {
80
+ config?: WidgetConfig;
81
+ children: React.ReactNode;
82
+ }
83
+ declare const WheelXProvider: React.FC<WheelXProviderProps>;
84
+
85
+ interface routesItem {
86
+ name: string;
87
+ logo: string;
88
+ }
89
+ interface TxInfoProps {
90
+ priceImpact: {
91
+ bridgeFee: string;
92
+ dstGasFee: string;
93
+ swapFee: string;
94
+ before_discount_fee: string;
95
+ discount_percentage: string | undefined;
96
+ };
97
+ slippage: number;
98
+ minReceive: string;
99
+ estimatedTime: number;
100
+ recipient: string;
101
+ router: string;
102
+ points: number | string;
103
+ amountOut: string;
104
+ fromTokenInUsd?: Fraction;
105
+ router_type: string | null | undefined;
106
+ routes?: routesItem[] | undefined;
107
+ }
108
+
109
+ interface RefetchQuoteParams {
110
+ slippageValue?: string;
111
+ isAutoSlippage?: boolean;
112
+ }
113
+
114
+ interface QuoteRequest {
115
+ from_chain: number;
116
+ to_chain: number;
117
+ from_token: string;
118
+ to_token: string;
119
+ from_address: string;
120
+ to_address: string;
121
+ amount: string;
122
+ slippage?: number;
123
+ to_platform_id: number;
124
+ sponsor_gas?: boolean;
125
+ }
126
+ type QuoteErrorType = 'business_error' | 'http_error' | 'network_error' | 'timeout' | 'unknown_error';
127
+ interface QuoteMutationError extends Error {
128
+ httpStatus?: number;
129
+ hasResponseBody?: boolean;
130
+ errorType?: QuoteErrorType;
131
+ detail?: string;
132
+ code?: string;
133
+ }
134
+
135
+ type Tag = 'pin' | 'top' | 'cert' | 'native';
136
+ type FeeLevel = 'Group1' | 'Group2' | null;
137
+ interface TokenInfo {
138
+ name: string;
139
+ address: `0x${string}`;
140
+ chain_id: number;
141
+ decimals: number;
142
+ symbol: string;
143
+ logo: string;
144
+ tags: Tag[];
145
+ support_kline?: boolean;
146
+ token_type?: string;
147
+ categories?: string[];
148
+ chain_icon?: string;
149
+ platform_id?: number;
150
+ platform_type?: string[];
151
+ isSolana?: boolean;
152
+ fee_level?: FeeLevel;
153
+ }
154
+ interface TokenInfoWithBalance extends TokenInfo {
155
+ balance?: string;
156
+ usdPrice?: string;
157
+ }
158
+ type ChainInfo = {
159
+ name: string;
160
+ rpc_url: string;
161
+ rpc_fallback: string[];
162
+ chain_icon: string;
163
+ chain_id: number;
164
+ eth_token: `0x${string}`;
165
+ support_kline: boolean;
166
+ is_popular: boolean;
167
+ outbound: boolean;
168
+ inbound: boolean;
169
+ is_testnet?: boolean;
170
+ platform_id?: number;
171
+ support_sponsor_gas?: boolean;
172
+ };
173
+ interface Prediction {
174
+ chains: ChainInfo[];
175
+ tokens: TokenInfo[];
176
+ }
177
+ interface SlippagePolicy {
178
+ bridge: boolean;
179
+ from_fee_level: FeeLevel;
180
+ to_fee_level: FeeLevel;
181
+ default_slippage: number;
182
+ min_slippage: number;
183
+ max_slippage: number;
184
+ refresh_interval: number;
185
+ /** Number of quote rounds after which to show a transaction-failure warning. Defaults to 2. */
186
+ notice_rounds?: number;
187
+ }
188
+ interface ChainsAndTokens {
189
+ chains: ChainInfo[];
190
+ tokens: TokenInfo[];
191
+ slippage_policies?: SlippagePolicy[];
192
+ deposit_platforms: {
193
+ prediction: Prediction;
194
+ };
195
+ }
196
+ declare function getChainsAndTokens(): Promise<ChainsAndTokens>;
197
+ declare const useChainsAndTokens: (enabled?: boolean) => _tanstack_react_query.UseQueryResult<ChainsAndTokens, Error>;
198
+ declare function mergeTokensWithCompositeKey(arr1: TokenInfoWithBalance[], arr2: TokenInfoWithBalance[]): TokenInfoWithBalance[];
199
+
200
+ type WheelXWidgetProps = {
201
+ config?: WidgetConfig;
202
+ };
203
+
204
+ interface QuoteInfo {
205
+ isFetching: boolean;
206
+ error: string | null;
207
+ data: `0x${string}` | null;
208
+ contractAddress: `0x${string}` | null;
209
+ info: TxInfoProps | null;
210
+ request_id?: string | null;
211
+ gas?: bigint | null;
212
+ maxFeePerGas?: bigint | undefined;
213
+ maxPriorityFeePerGas?: bigint | undefined;
214
+ backApprove?: boolean;
215
+ router_type?: string | null;
216
+ tx_value?: string | null;
217
+ points?: number | 0 | string;
218
+ quote_message?: string | null;
219
+ routes?: routesItem[];
220
+ solanaTxMessage?: string;
221
+ solanaTxSender?: string;
222
+ }
223
+ interface QuoteRequestWithToTokenDecimals extends QuoteRequest {
224
+ toTokenDecimals: number;
225
+ isAutoRefetch?: boolean;
226
+ affiliation?: string | null;
227
+ }
228
+ declare const initialQuoteInfo: QuoteInfo;
229
+ declare const WheelXWidget: ({ config }: WheelXWidgetProps) => react_jsx_runtime.JSX.Element;
230
+
231
+ declare const defaultTheme: WidgetTheme;
232
+
233
+ interface WidgetThemeContextValue {
234
+ theme: WidgetTheme;
235
+ }
236
+ declare const useWidgetTheme: () => WidgetThemeContextValue;
237
+ interface WidgetThemeProviderProps {
238
+ theme?: Partial<WidgetTheme>;
239
+ children: React.ReactNode;
240
+ }
241
+ declare const WidgetThemeProvider: React.FC<WidgetThemeProviderProps>;
242
+
243
+ interface SwapState {
244
+ fromTokenInfo: TokenInfo;
245
+ toTokenInfo: TokenInfo;
246
+ fromAmount: string;
247
+ toAmount: string;
248
+ slippage?: string;
249
+ fromAmountToUsd?: string | null;
250
+ toAmountToUsd?: string | null;
251
+ isShortLink?: boolean;
252
+ shortLink?: string;
253
+ transactionType?: string;
254
+ autoSlippage?: number | undefined;
255
+ tradeType?: string | null;
256
+ }
257
+ interface SwapAndBridgeContextStore extends SwapState {
258
+ setFromTokenInfo: (tokenInfo: TokenInfo) => void;
259
+ setToTokenInfo: (tokenInfo: TokenInfo) => void;
260
+ setFromAmount: (amount: string) => void;
261
+ setToAmount: (amount: string) => void;
262
+ switchTokens: () => void;
263
+ resetState: () => void;
264
+ setAmounts: (fromAmount: string, toAmount: string) => void;
265
+ resetAmounts: () => void;
266
+ setSlippage: (slippage?: string) => void;
267
+ setFromAmountToUsd: (value: string | null) => void;
268
+ setToAmountToUsd: (value: string | null) => void;
269
+ setIsShortLink: (value: boolean) => void;
270
+ setShortLink: (value: string) => void;
271
+ setTransactionType: (value: string) => void;
272
+ setAutoSlippage: (value: number | undefined) => void;
273
+ }
274
+ declare const useSwapAndBridgeContextStore: zustand.UseBoundStore<zustand.StoreApi<SwapAndBridgeContextStore>>;
275
+ declare const useInitSwapAndBridgeState: (config?: WidgetConfig) => void;
276
+ declare const useResetSwapAndBridgeState: () => void;
277
+
278
+ interface TxStateContextStore {
279
+ fromTxHash: `0x${string}` | null;
280
+ toTxHash: `0x${string}` | null;
281
+ setFromTxHash: (fromTxHash: `0x${string}` | null) => void;
282
+ setToTxHash: (toTxHash: `0x${string}` | null) => void;
283
+ resetTxState: () => void;
284
+ startTrading: boolean;
285
+ setStartTrading: (startTrading: boolean) => void;
286
+ userCancelsTransaction: boolean;
287
+ setUserCancelsTransaction: (userCancelsTransaction: boolean) => void;
288
+ fromValue?: string | null;
289
+ setFromValue?: (fromValue: string | null) => void;
290
+ tryAgain?: boolean;
291
+ setTryAgain: (tryAgain: boolean) => void;
292
+ solanaFromTxHash: string | null;
293
+ setSolanaFromTxHash: (hash: string | null) => void;
294
+ isSolanaTransaction: boolean;
295
+ setIsSolanaTransaction: (isSolana: boolean) => void;
296
+ }
297
+ declare const useTxStateContextStore: zustand.UseBoundStore<zustand.StoreApi<TxStateContextStore>>;
298
+
299
+ declare const useGuardChain: (callback: () => void, requiredChainId: number) => {
300
+ isSwitching: boolean;
301
+ sendTx: () => Promise<void>;
302
+ };
303
+
304
+ declare const useRefreshLoading: () => {
305
+ isLoading: boolean;
306
+ startLoading: () => void;
307
+ };
308
+
309
+ interface Props {
310
+ chainId: number;
311
+ tokenAddress?: `0x${string}`;
312
+ connectAddress?: `0x${string}`;
313
+ }
314
+ interface BalanceResult {
315
+ decimals: number;
316
+ formatted: string;
317
+ symbol: string;
318
+ value: bigint;
319
+ }
320
+ declare const useTokenBalance: ({ chainId, tokenAddress, connectAddress }: Props) => BalanceResult | undefined;
321
+
322
+ interface ChainsStore {
323
+ chains: ChainInfo[] | null;
324
+ setChains: (chains: ChainInfo[]) => void;
325
+ isSelectPredictionChain: boolean;
326
+ setIsSelectPredictionChain: (isSelectPredictionChain: boolean) => void;
327
+ slippagePolicies: SlippagePolicy[] | null;
328
+ setSlippagePolicies: (policies: SlippagePolicy[]) => void;
329
+ }
330
+ interface TokenStore {
331
+ tokens: TokenInfo[] | null;
332
+ setTokens: (tokens: TokenInfo[]) => void;
333
+ }
334
+ declare const useChainsStore: zustand.UseBoundStore<zustand.StoreApi<ChainsStore>>;
335
+ declare const useTokensStore: zustand.UseBoundStore<zustand.StoreApi<TokenStore>>;
336
+
337
+ /**
338
+ * Hook to get EVM wallet address with fallback to saved address
339
+ * This ensures that when a user first connects an EVM wallet (e.g., OKX),
340
+ * then connects a Solana wallet (e.g., MetaMask), the EVM address remains
341
+ * consistent with the first connected wallet.
342
+ *
343
+ * @returns The EVM wallet address (saved address or current wagmi address)
344
+ */
345
+ declare const useEvmAddress: () => `0x${string}` | undefined;
346
+
347
+ /**
348
+ * Platform detection utilities for WebView environments.
349
+ * Used to detect and intercept deep links when the widget runs inside
350
+ * a WebView (e.g., React Native, Android WebView, WKWebView).
351
+ */
352
+ /** Check if running inside a React Native WebView */
353
+ declare const isReactNativeWebView: () => boolean;
354
+ /** Check if running in any WebView (Android/iOS) */
355
+ declare const isWebView: () => boolean;
356
+ /** Check if running on mobile device */
357
+ declare const isMobileDevice: () => boolean;
358
+ /**
359
+ * Check if we should intercept deep links.
360
+ *
361
+ * We ONLY intercept in React Native WebView because that's the only WebView
362
+ * environment where we have a reliable forwarding mechanism (postMessage).
363
+ *
364
+ * Regular browsers (Chrome, Safari) handle deep links natively — we must not
365
+ * interfere there. Generic Android/iOS WebViews may or may not handle deep
366
+ * links depending on host app configuration, so we leave them alone.
367
+ */
368
+ declare const shouldInterceptDeepLinks: () => boolean;
369
+ /** Check if URL is a deep link (non-http/https scheme) */
370
+ declare const isDeepLinkUrl: (url: string) => boolean;
371
+ /**
372
+ * Known wallet universal link (https) domains.
373
+ * Wallet libraries (e.g. Dynamic SDK) may prefer universal links over
374
+ * custom URL schemes. These https:// URLs look like normal web pages but
375
+ * are designed to open wallet apps via OS-level app link resolution.
376
+ * In WebView environments, we must intercept them too.
377
+ */
378
+ declare const KNOWN_WALLET_UNIVERSAL_LINKS: Record<string, string>;
379
+ /**
380
+ * Convert a wallet universal link to its corresponding deep-link scheme
381
+ * so that `Linking.openURL()` on React Native opens the wallet app
382
+ * directly instead of routing through the system browser.
383
+ *
384
+ * For example:
385
+ * https://phantom.app/ul/v1/connect?... → phantom://ul/v1/connect?...
386
+ */
387
+ declare const convertToDeepLink: (url: string) => string;
388
+ /**
389
+ * Check if URL is a deep link (non-http/https scheme) OR a known wallet
390
+ * universal link that should be intercepted and forwarded to native.
391
+ */
392
+ declare const isWalletRelatedUrl: (url: string) => boolean;
393
+
394
+ interface WalletStore {
395
+ isEthWalletConnected: boolean;
396
+ setIsEthWalletConnected: (isEthWalletConnected: boolean) => void;
397
+ isSolanaWalletConnected: boolean;
398
+ setIsSolanaWalletConnected: (isSolanaWalletConnected: boolean) => void;
399
+ totalConnectedWallets: number;
400
+ setTotalConnectedWallets: () => void;
401
+ totalWallets: number;
402
+ solanaWalletAddress: string | null;
403
+ setSolanaWalletAddress: (address: string | null) => void;
404
+ evmWalletAddress: string | null;
405
+ evmWalletConnectorId: string | null;
406
+ setEvmWalletInfo: (address: string | null, connectorId: string | null) => void;
407
+ isEvmWalletMetamask: boolean;
408
+ setIsEvmWalletMetamask: (isEvmWalletMetamask: boolean) => void;
409
+ isLoggingOut: boolean;
410
+ setIsLoggingOut: (isLoggingOut: boolean) => void;
411
+ lastDisconnectTimestamp: number;
412
+ setLastDisconnectTimestamp: (timestamp: number) => void;
413
+ }
414
+ declare const useWalletConnectedStore: zustand.UseBoundStore<zustand.StoreApi<WalletStore>>;
415
+
416
+ interface DifferentAddressProp {
417
+ differentAddress: `0x${string}` | undefined | string;
418
+ setDifferentAddress: (address: `0x${string}` | undefined | string) => void;
419
+ isDepositPlatformToken: boolean;
420
+ setIsDepositPlatformToken: (isDepositPlatformToken: boolean) => void;
421
+ }
422
+ declare const useDifferentAddressStore: zustand.UseBoundStore<zustand.StoreApi<DifferentAddressProp>>;
423
+
424
+ declare const useSyncWalletConnectedStore: () => void;
425
+
426
+ export { type ChainInfo, type ChainsAndTokens, type FeeLevel, KNOWN_WALLET_UNIVERSAL_LINKS, type QuoteErrorType, type QuoteInfo, type QuoteMutationError, type QuoteRequest, type QuoteRequestWithToTokenDecimals, type RefetchQuoteParams, type SlippagePolicy, type TokenConfig, type TokenInfo, type TxInfoProps, WheelXProvider, type WheelXProviderProps, WheelXWidget, type WheelXWidgetProps, type WidgetConfig, type WidgetTheme, WidgetThemeProvider, convertToDeepLink, defaultTheme, getChainsAndTokens, initialQuoteInfo, isDeepLinkUrl, isMobileDevice, isReactNativeWebView, isWalletRelatedUrl, isWebView, mergeTokensWithCompositeKey, shouldInterceptDeepLinks, useChainsAndTokens, useChainsStore, useDifferentAddressStore, useEvmAddress, useGuardChain, useInitSwapAndBridgeState, useRefreshLoading, useResetSwapAndBridgeState, useSwapAndBridgeContextStore, useSyncWalletConnectedStore, useTokenBalance, useTokensStore, useTxStateContextStore, useWalletConnectedStore, useWidgetTheme };