@hongming-wang/usdc-bridge-widget 0.1.1 → 0.2.0

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.
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback, useRef, useId, useMemo } from "react";
1
+ import { useState, useEffect, useCallback, useRef, useId, useMemo } from "react";
2
2
  import {
3
3
  useAccount,
4
4
  useChainId,
@@ -11,7 +11,7 @@ import type {
11
11
  BridgeWidgetTheme,
12
12
  BridgeChainConfig,
13
13
  } from "./types";
14
- import { useUSDCBalance, useUSDCAllowance, useAllUSDCBalances } from "./hooks";
14
+ import { useUSDCAllowance, useAllUSDCBalances } from "./hooks";
15
15
  import { useBridge } from "./useBridge";
16
16
  import { DEFAULT_CHAIN_CONFIGS } from "./chains";
17
17
  import { USDC_BRAND_COLOR } from "./constants";
@@ -19,6 +19,46 @@ import { formatNumber, getErrorMessage, validateAmountInput, validateChainConfig
19
19
  import { mergeTheme } from "./theme";
20
20
  import { ChevronDownIcon, SwapIcon } from "./icons";
21
21
 
22
+ // Constants
23
+ const TYPE_AHEAD_RESET_MS = 1000;
24
+ const DROPDOWN_MAX_HEIGHT = 300;
25
+ const BOX_SHADOW_COLOR = "rgba(0,0,0,0.3)";
26
+ const DISABLED_BUTTON_BACKGROUND = "rgba(255,255,255,0.1)";
27
+
28
+ // Helper function for borderless styles
29
+ function getBorderlessStyles(
30
+ borderless: boolean | undefined,
31
+ theme: Required<BridgeWidgetTheme>,
32
+ options?: { includeBoxShadow?: boolean; useBackgroundColor?: boolean }
33
+ ) {
34
+ const bgColor = options?.useBackgroundColor
35
+ ? theme.backgroundColor
36
+ : theme.cardBackgroundColor;
37
+
38
+ return {
39
+ borderRadius: borderless ? 0 : `${theme.borderRadius}px`,
40
+ background: borderless ? "transparent" : bgColor,
41
+ border: borderless ? "none" : `1px solid ${theme.borderColor}`,
42
+ ...(options?.includeBoxShadow && {
43
+ boxShadow: borderless ? "none" : `0 4px 24px ${BOX_SHADOW_COLOR}`,
44
+ }),
45
+ };
46
+ }
47
+
48
+ // Shared keyframes style - injected once per document
49
+ const SPINNER_KEYFRAMES = `@keyframes cc-balance-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`;
50
+ const KEYFRAMES_ATTR = "data-cc-spinner-keyframes";
51
+
52
+ function injectSpinnerKeyframes() {
53
+ if (typeof document === "undefined") return;
54
+ // Check if already injected using a data attribute on the style element
55
+ if (document.querySelector(`style[${KEYFRAMES_ATTR}]`)) return;
56
+ const style = document.createElement("style");
57
+ style.setAttribute(KEYFRAMES_ATTR, "true");
58
+ style.textContent = SPINNER_KEYFRAMES;
59
+ document.head.appendChild(style);
60
+ }
61
+
22
62
  // Chain icon with fallback
23
63
  function ChainIcon({
24
64
  chainConfig,
@@ -31,6 +71,11 @@ function ChainIcon({
31
71
  }) {
32
72
  const [hasError, setHasError] = useState(false);
33
73
 
74
+ // Reset error state when chainConfig changes
75
+ useEffect(() => {
76
+ setHasError(false);
77
+ }, [chainConfig.iconUrl]);
78
+
34
79
  if (!chainConfig.iconUrl || hasError) {
35
80
  return (
36
81
  <div
@@ -66,6 +111,11 @@ function ChainIcon({
66
111
 
67
112
  // Small loading spinner for balance display
68
113
  function BalanceSpinner({ size = 12 }: { size?: number }) {
114
+ // Inject keyframes once on first render
115
+ useEffect(() => {
116
+ injectSpinnerKeyframes();
117
+ }, []);
118
+
69
119
  return (
70
120
  <svg
71
121
  width={size}
@@ -80,7 +130,6 @@ function BalanceSpinner({ size = 12 }: { size?: number }) {
80
130
  }}
81
131
  aria-hidden="true"
82
132
  >
83
- <style>{`@keyframes cc-balance-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`}</style>
84
133
  <circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
85
134
  <path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
86
135
  </svg>
@@ -99,6 +148,7 @@ function ChainSelector({
99
148
  balances,
100
149
  isLoadingBalances,
101
150
  disabled,
151
+ borderless,
102
152
  }: {
103
153
  label: string;
104
154
  chains: BridgeChainConfig[];
@@ -110,6 +160,7 @@ function ChainSelector({
110
160
  balances?: Record<number, { balance: bigint; formatted: string }>;
111
161
  isLoadingBalances?: boolean;
112
162
  disabled?: boolean;
163
+ borderless?: boolean;
113
164
  }) {
114
165
  const [isOpen, setIsOpen] = useState(false);
115
166
  const [focusedIndex, setFocusedIndex] = useState(-1);
@@ -117,8 +168,11 @@ function ChainSelector({
117
168
  const typeAheadTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
118
169
  const buttonRef = useRef<HTMLButtonElement>(null);
119
170
  const listRef = useRef<HTMLUListElement>(null);
120
- const availableChains = chains.filter(
121
- (c) => c.chain.id !== excludeChainId
171
+
172
+ // Memoize filtered chains to avoid recalculation on every render
173
+ const availableChains = useMemo(
174
+ () => chains.filter((c) => c.chain.id !== excludeChainId),
175
+ [chains, excludeChainId]
122
176
  );
123
177
 
124
178
  // Clear type-ahead timer on unmount
@@ -189,10 +243,10 @@ function ChainSelector({
189
243
  clearTimeout(typeAheadTimeoutRef.current);
190
244
  }
191
245
 
192
- // Reset type-ahead after 1 second of inactivity
246
+ // Reset type-ahead after timeout
193
247
  typeAheadTimeoutRef.current = setTimeout(() => {
194
248
  setTypeAhead("");
195
- }, 1000);
249
+ }, TYPE_AHEAD_RESET_MS);
196
250
 
197
251
  // Find matching chain
198
252
  const matchIndex = availableChains.findIndex((chain) =>
@@ -280,9 +334,7 @@ function ChainSelector({
280
334
  alignItems: "center",
281
335
  justifyContent: "space-between",
282
336
  padding: "10px 12px",
283
- borderRadius: `${theme.borderRadius}px`,
284
- background: theme.cardBackgroundColor,
285
- border: `1px solid ${theme.borderColor}`,
337
+ ...getBorderlessStyles(borderless, theme),
286
338
  cursor: disabled ? "not-allowed" : "pointer",
287
339
  opacity: disabled ? 0.6 : 1,
288
340
  transition: "all 0.2s",
@@ -312,7 +364,7 @@ function ChainSelector({
312
364
  >
313
365
  <BalanceSpinner size={10} /> Loading...
314
366
  </span>
315
- ) : selectedBalance ? (
367
+ ) : balances && selectedBalance ? (
316
368
  <span
317
369
  style={{
318
370
  fontSize: "10px",
@@ -363,11 +415,11 @@ function ChainSelector({
363
415
  width: "100%",
364
416
  marginTop: "8px",
365
417
  borderRadius: `${theme.borderRadius}px`,
366
- boxShadow: "0 10px 40px rgba(0,0,0,0.3)",
418
+ boxShadow: `0 10px 40px ${BOX_SHADOW_COLOR}`,
367
419
  background: theme.cardBackgroundColor,
368
420
  backdropFilter: "blur(10px)",
369
421
  border: `1px solid ${theme.borderColor}`,
370
- maxHeight: "300px",
422
+ maxHeight: `${DROPDOWN_MAX_HEIGHT}px`,
371
423
  overflowY: "auto",
372
424
  overflowX: "hidden",
373
425
  padding: 0,
@@ -380,6 +432,8 @@ function ChainSelector({
380
432
  const chainBalance = balances?.[chainConfig.chain.id];
381
433
  const isFocused = index === focusedIndex;
382
434
  const isSelected = chainConfig.chain.id === selectedChain.chain.id;
435
+ // Pre-compute parsed balance to avoid parseFloat in render
436
+ const hasPositiveBalance = chainBalance ? parseFloat(chainBalance.formatted) > 0 : false;
383
437
 
384
438
  return (
385
439
  <li
@@ -428,25 +482,16 @@ function ChainSelector({
428
482
  >
429
483
  <BalanceSpinner size={10} />
430
484
  </span>
431
- ) : chainBalance ? (
485
+ ) : balances && chainBalance ? (
432
486
  <span
433
487
  style={{
434
488
  fontSize: "10px",
435
- color: parseFloat(chainBalance.formatted) > 0 ? theme.successColor : theme.mutedTextColor,
489
+ color: hasPositiveBalance ? theme.successColor : theme.mutedTextColor,
436
490
  }}
437
491
  >
438
492
  {formatNumber(chainBalance.formatted, 2)} USDC
439
493
  </span>
440
- ) : (
441
- <span
442
- style={{
443
- fontSize: "10px",
444
- color: theme.mutedTextColor,
445
- }}
446
- >
447
- 0.00 USDC
448
- </span>
449
- )}
494
+ ) : null}
450
495
  </div>
451
496
  </li>
452
497
  );
@@ -507,6 +552,8 @@ function AmountInput({
507
552
  theme,
508
553
  id,
509
554
  disabled,
555
+ showBalance = true,
556
+ borderless,
510
557
  }: {
511
558
  value: string;
512
559
  onChange: (value: string) => void;
@@ -515,6 +562,8 @@ function AmountInput({
515
562
  theme: Required<BridgeWidgetTheme>;
516
563
  id: string;
517
564
  disabled?: boolean;
565
+ showBalance?: boolean;
566
+ borderless?: boolean;
518
567
  }) {
519
568
  const inputId = `${id}-input`;
520
569
  const labelId = `${id}-label`;
@@ -564,24 +613,24 @@ function AmountInput({
564
613
  >
565
614
  Amount
566
615
  </label>
567
- <span
568
- style={{ fontSize: "10px", color: theme.mutedTextColor }}
569
- aria-live="polite"
570
- >
571
- Balance:{" "}
572
- <span style={{ color: theme.textColor }}>
573
- {formatNumber(balance)} USDC
616
+ {showBalance && (
617
+ <span
618
+ style={{ fontSize: "10px", color: theme.mutedTextColor }}
619
+ aria-live="polite"
620
+ >
621
+ Balance:{" "}
622
+ <span style={{ color: theme.textColor }}>
623
+ {formatNumber(balance)} USDC
624
+ </span>
574
625
  </span>
575
- </span>
626
+ )}
576
627
  </div>
577
628
  <div
578
629
  style={{
579
630
  display: "flex",
580
631
  alignItems: "center",
581
- borderRadius: `${theme.borderRadius}px`,
582
632
  overflow: "hidden",
583
- background: theme.cardBackgroundColor,
584
- border: `1px solid ${theme.borderColor}`,
633
+ ...getBorderlessStyles(borderless, theme),
585
634
  opacity: disabled ? 0.6 : 1,
586
635
  }}
587
636
  >
@@ -690,6 +739,7 @@ export function BridgeWidget({
690
739
  onBridgeError,
691
740
  onConnectWallet,
692
741
  theme: themeOverrides,
742
+ borderless = false,
693
743
  className,
694
744
  style,
695
745
  }: BridgeWidgetProps) {
@@ -705,7 +755,6 @@ export function BridgeWidget({
705
755
  const validation = validateChainConfigs(chains);
706
756
  if (!validation.isValid) {
707
757
  const errorMsg = validation.errors.join("; ");
708
- console.error("[BridgeWidget] Invalid chain configuration:", errorMsg);
709
758
  setConfigError(errorMsg);
710
759
  } else {
711
760
  setConfigError(null);
@@ -741,28 +790,28 @@ export function BridgeWidget({
741
790
  const [txHash, setTxHash] = useState<`0x${string}` | undefined>();
742
791
  const [error, setError] = useState<string | null>(null);
743
792
 
744
- // Hooks - fetch balances for all chains
793
+ // Hooks - fetch balances for all chains (single batch request via multicall)
745
794
  const { balances: allBalances, isLoading: isLoadingAllBalances, refetch: refetchAllBalances } = useAllUSDCBalances(chains);
746
- const { balanceFormatted, refetch: refetchBalance } = useUSDCBalance(
747
- sourceChainConfig
748
- );
795
+
796
+ // Derive source chain balance from the batch-fetched balances (no extra network request)
797
+ const balanceFormatted = useMemo(() => {
798
+ return allBalances[sourceChainConfig.chain.id]?.formatted ?? "0";
799
+ }, [allBalances, sourceChainConfig.chain.id]);
800
+
801
+ // Memoize parsed values to avoid repeated parsing
802
+ const parsedBalance = useMemo(() => parseFloat(balanceFormatted), [balanceFormatted]);
803
+ const parsedAmount = useMemo(() => parseFloat(amount) || 0, [amount]);
804
+
749
805
  const { needsApproval, approve, isApproving } = useUSDCAllowance(
750
806
  sourceChainConfig
751
807
  );
752
808
 
753
- // Combined refetch for all balances
754
- const refetchBalances = useCallback(() => {
755
- refetchBalance();
756
- refetchAllBalances();
757
- }, [refetchBalance, refetchAllBalances]);
758
-
759
809
  // Refetch balances when wallet connects or address changes
760
810
  useEffect(() => {
761
811
  if (address) {
762
812
  refetchAllBalances();
763
- refetchBalance();
764
813
  }
765
- }, [address, refetchAllBalances, refetchBalance]);
814
+ }, [address, refetchAllBalances]);
766
815
 
767
816
  // Bridge hook
768
817
  const { bridge: executeBridge, state: bridgeState, reset: resetBridge } = useBridge();
@@ -793,11 +842,11 @@ export function BridgeWidget({
793
842
 
794
843
  // Swap chains
795
844
  const handleSwapChains = useCallback(() => {
796
- setSourceChainConfig((prev) => {
797
- setDestChainConfig(prev);
798
- return destChainConfig;
799
- });
800
- }, [destChainConfig]);
845
+ const newSource = destChainConfig;
846
+ const newDest = sourceChainConfig;
847
+ setSourceChainConfig(newSource);
848
+ setDestChainConfig(newDest);
849
+ }, [destChainConfig, sourceChainConfig]);
801
850
 
802
851
  // Handle max click
803
852
  const handleMaxClick = useCallback(() => {
@@ -815,7 +864,7 @@ export function BridgeWidget({
815
864
 
816
865
  // Handle bridge
817
866
  const handleBridge = useCallback(async () => {
818
- if (!address || !amount || parseFloat(amount) <= 0) return;
867
+ if (!address || !amount || parsedAmount <= 0) return;
819
868
 
820
869
  setError(null);
821
870
  resetBridge();
@@ -856,6 +905,7 @@ export function BridgeWidget({
856
905
  }, [
857
906
  address,
858
907
  amount,
908
+ parsedAmount,
859
909
  needsApproval,
860
910
  approve,
861
911
  executeBridge,
@@ -899,7 +949,7 @@ export function BridgeWidget({
899
949
  const currentTxHash = bridgeState.txHash;
900
950
 
901
951
  setAmount("");
902
- refetchBalances();
952
+ refetchAllBalances();
903
953
 
904
954
  if (currentTxHash) {
905
955
  onBridgeSuccessRef.current?.({
@@ -916,19 +966,19 @@ export function BridgeWidget({
916
966
  bridgeState.status,
917
967
  bridgeState.txHash,
918
968
  bridgeState.error,
919
- refetchBalances,
969
+ refetchAllBalances,
920
970
  amount,
921
971
  sourceChainConfig.chain.id,
922
972
  destChainConfig.chain.id,
923
973
  ]);
924
974
 
925
- // Computed disabled state
975
+ // Computed disabled state using memoized values
926
976
  const isButtonDisabled =
927
977
  !isConnected ||
928
978
  needsChainSwitch ||
929
979
  !amount ||
930
- parseFloat(amount) <= 0 ||
931
- parseFloat(amount) > parseFloat(balanceFormatted) ||
980
+ parsedAmount <= 0 ||
981
+ parsedAmount > parsedBalance ||
932
982
  isConfirming ||
933
983
  isApproving ||
934
984
  isBridging;
@@ -952,8 +1002,8 @@ export function BridgeWidget({
952
1002
  return "Approving...";
953
1003
  }
954
1004
 
955
- if (!amount || parseFloat(amount) <= 0) return "Enter Amount";
956
- if (parseFloat(amount) > parseFloat(balanceFormatted)) {
1005
+ if (!amount || parsedAmount <= 0) return "Enter Amount";
1006
+ if (parsedAmount > parsedBalance) {
957
1007
  return "Insufficient Balance";
958
1008
  }
959
1009
  if (needsApproval(amount)) return "Approve & Bridge USDC";
@@ -966,7 +1016,8 @@ export function BridgeWidget({
966
1016
  isConfirming,
967
1017
  isApproving,
968
1018
  amount,
969
- balanceFormatted,
1019
+ parsedAmount,
1020
+ parsedBalance,
970
1021
  needsApproval,
971
1022
  ]);
972
1023
 
@@ -1011,7 +1062,7 @@ export function BridgeWidget({
1011
1062
  transition: "all 0.2s",
1012
1063
  color: isButtonActuallyDisabled ? theme.mutedTextColor : theme.textColor,
1013
1064
  background: isButtonActuallyDisabled
1014
- ? "rgba(255,255,255,0.1)"
1065
+ ? DISABLED_BUTTON_BACKGROUND
1015
1066
  : `linear-gradient(135deg, ${theme.primaryColor} 0%, ${theme.secondaryColor} 100%)`,
1016
1067
  boxShadow: isButtonActuallyDisabled
1017
1068
  ? "none"
@@ -1036,11 +1087,11 @@ export function BridgeWidget({
1036
1087
  fontFamily: theme.fontFamily,
1037
1088
  maxWidth: "480px",
1038
1089
  width: "100%",
1039
- borderRadius: `${theme.borderRadius}px`,
1040
1090
  padding: "16px",
1041
- background: theme.backgroundColor,
1042
- border: `1px solid ${theme.borderColor}`,
1043
- boxShadow: "0 4px 24px rgba(0,0,0,0.3)",
1091
+ ...getBorderlessStyles(borderless, theme, {
1092
+ includeBoxShadow: true,
1093
+ useBackgroundColor: true,
1094
+ }),
1044
1095
  ...style,
1045
1096
  }}
1046
1097
  >
@@ -1061,9 +1112,10 @@ export function BridgeWidget({
1061
1112
  onSelect={setSourceChainConfig}
1062
1113
  excludeChainId={destChainConfig.chain.id}
1063
1114
  theme={theme}
1064
- balances={allBalances}
1065
- isLoadingBalances={isLoadingAllBalances}
1115
+ balances={isConnected ? allBalances : undefined}
1116
+ isLoadingBalances={isConnected && isLoadingAllBalances}
1066
1117
  disabled={isOperationPending}
1118
+ borderless={borderless}
1067
1119
  />
1068
1120
  <SwapButton onClick={handleSwapChains} theme={theme} disabled={isOperationPending} />
1069
1121
  <ChainSelector
@@ -1074,9 +1126,10 @@ export function BridgeWidget({
1074
1126
  onSelect={setDestChainConfig}
1075
1127
  excludeChainId={sourceChainConfig.chain.id}
1076
1128
  theme={theme}
1077
- balances={allBalances}
1078
- isLoadingBalances={isLoadingAllBalances}
1129
+ balances={isConnected ? allBalances : undefined}
1130
+ isLoadingBalances={isConnected && isLoadingAllBalances}
1079
1131
  disabled={isOperationPending}
1132
+ borderless={borderless}
1080
1133
  />
1081
1134
  </div>
1082
1135
 
@@ -1090,6 +1143,8 @@ export function BridgeWidget({
1090
1143
  onMaxClick={handleMaxClick}
1091
1144
  theme={theme}
1092
1145
  disabled={isOperationPending}
1146
+ showBalance={isConnected}
1147
+ borderless={borderless}
1093
1148
  />
1094
1149
  </div>
1095
1150
 
@@ -217,6 +217,22 @@ describe("BridgeWidget", () => {
217
217
  expect(widget.style.backgroundColor).toBe("red");
218
218
  });
219
219
 
220
+ it("applies borderless style when borderless prop is true", () => {
221
+ render(<BridgeWidget borderless />);
222
+ const widget = screen.getByRole("region", { name: "USDC Bridge Widget" });
223
+ // Check that borderless styles are applied
224
+ expect(widget.style.background).toBe("transparent");
225
+ // JSDOM normalizes "0px" to "0"
226
+ expect(widget.style.borderRadius).toBe("0");
227
+ });
228
+
229
+ it("applies default borders when borderless prop is false", () => {
230
+ render(<BridgeWidget borderless={false} />);
231
+ const widget = screen.getByRole("region", { name: "USDC Bridge Widget" });
232
+ // Check that default styles have border radius (not 0)
233
+ expect(widget.style.borderRadius).not.toBe("0");
234
+ });
235
+
220
236
  it("calls onBridgeStart when bridge is initiated", async () => {
221
237
  const onBridgeStart = vi.fn();
222
238
  render(<BridgeWidget onBridgeStart={onBridgeStart} />);
@@ -264,6 +280,19 @@ describe("BridgeWidget - Disconnected State", () => {
264
280
 
265
281
  expect(onConnectWallet).toHaveBeenCalled();
266
282
  });
283
+
284
+ it("does not show balance in amount input when wallet is disconnected", () => {
285
+ render(<BridgeWidget />);
286
+ // Balance label should not be present when disconnected
287
+ expect(screen.queryByText(/Balance:/)).toBeNull();
288
+ });
289
+
290
+ it("does not show balance in chain selectors when wallet is disconnected", () => {
291
+ render(<BridgeWidget />);
292
+ // USDC balance text should not appear in chain selectors
293
+ expect(screen.queryByText(/1,000.00 USDC/)).toBeNull();
294
+ expect(screen.queryByText(/500.00 USDC/)).toBeNull();
295
+ });
267
296
  });
268
297
 
269
298
  describe("BridgeWidget - Chain Switch Required", () => {
@@ -5,10 +5,11 @@ import type { BridgeChainConfig } from "../types";
5
5
 
6
6
  // Create mock functions
7
7
  const mockUseReadContracts = vi.fn();
8
+ const mockUseAccount = vi.fn();
8
9
 
9
10
  // Mock wagmi hooks
10
11
  vi.mock("wagmi", () => ({
11
- useAccount: vi.fn(() => ({ address: "0x1234567890123456789012345678901234567890" })),
12
+ useAccount: () => mockUseAccount(),
12
13
  useReadContract: vi.fn(() => ({
13
14
  data: 1000000000n,
14
15
  isLoading: false,
@@ -58,6 +59,8 @@ describe("useAllUSDCBalances", () => {
58
59
 
59
60
  beforeEach(() => {
60
61
  vi.clearAllMocks();
62
+ // Default to connected wallet
63
+ mockUseAccount.mockReturnValue({ address: "0x1234567890123456789012345678901234567890" });
61
64
  mockUseReadContracts.mockReturnValue({
62
65
  data: [
63
66
  { status: "success", result: 1000000000n },
@@ -124,4 +127,20 @@ describe("useAllUSDCBalances", () => {
124
127
  // Successful call should work
125
128
  expect(result.current.balances[8453].balance).toBe(500000000n);
126
129
  });
130
+
131
+ it("returns isLoading false when wallet not connected", () => {
132
+ // Set wallet as disconnected
133
+ mockUseAccount.mockReturnValue({ address: undefined });
134
+ mockUseReadContracts.mockReturnValue({
135
+ data: undefined,
136
+ isLoading: true, // Query reports loading but...
137
+ refetch: vi.fn(),
138
+ });
139
+
140
+ const { result } = renderHook(() => useAllUSDCBalances(mockChainConfigs));
141
+
142
+ // ...isLoading should be false since wallet is not connected
143
+ expect(result.current.isLoading).toBe(false);
144
+ expect(result.current.balances).toEqual({});
145
+ });
127
146
  });
package/src/hooks.ts CHANGED
@@ -20,7 +20,7 @@ export function useUSDCBalance(chainConfig: BridgeChainConfig | undefined) {
20
20
 
21
21
  const {
22
22
  data: balance,
23
- isLoading,
23
+ isLoading: queryLoading,
24
24
  refetch,
25
25
  } = useReadContract({
26
26
  address: chainConfig?.usdcAddress,
@@ -32,6 +32,9 @@ export function useUSDCBalance(chainConfig: BridgeChainConfig | undefined) {
32
32
  },
33
33
  });
34
34
 
35
+ // Only show loading when wallet is connected and query is actually running
36
+ const isLoading = !!address && queryLoading;
37
+
35
38
  return {
36
39
  balance: balance ?? 0n,
37
40
  balanceFormatted: balance ? formatUnits(balance, USDC_DECIMALS) : "0",
@@ -74,7 +77,7 @@ export function useAllUSDCBalances(chainConfigs: BridgeChainConfig[]): {
74
77
 
75
78
  const {
76
79
  data: results,
77
- isLoading,
80
+ isLoading: queryLoading,
78
81
  refetch,
79
82
  } = useReadContracts({
80
83
  contracts,
@@ -83,6 +86,9 @@ export function useAllUSDCBalances(chainConfigs: BridgeChainConfig[]): {
83
86
  },
84
87
  });
85
88
 
89
+ // Only show loading when wallet is connected and query is actually running
90
+ const isLoading = !!address && queryLoading;
91
+
86
92
  // Map results to chain IDs
87
93
  const balances = useMemo(() => {
88
94
  const balanceMap: Record<
@@ -130,7 +136,7 @@ export function useUSDCAllowance(
130
136
 
131
137
  const {
132
138
  data: allowance,
133
- isLoading,
139
+ isLoading: queryLoading,
134
140
  refetch,
135
141
  } = useReadContract({
136
142
  address: chainConfig?.usdcAddress,
@@ -143,6 +149,9 @@ export function useUSDCAllowance(
143
149
  },
144
150
  });
145
151
 
152
+ // Only show loading when wallet is connected and query is actually running
153
+ const isLoading = !!address && queryLoading;
154
+
146
155
  const { writeContractAsync, isPending: isApproving } = useWriteContract();
147
156
  const [approvalTxHash, setApprovalTxHash] = useState<
148
157
  `0x${string}` | undefined
package/src/types.ts CHANGED
@@ -123,6 +123,8 @@ export interface BridgeWidgetProps {
123
123
  onConnectWallet?: () => void;
124
124
  /** Custom theme overrides to customize the widget appearance */
125
125
  theme?: BridgeWidgetTheme;
126
+ /** Remove all borders from the widget for seamless integration */
127
+ borderless?: boolean;
126
128
  /** Custom CSS class name to apply to the widget container */
127
129
  className?: string;
128
130
  /** Custom inline styles to apply to the widget container */