@jaw.id/ui 0.0.1

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.
Files changed (170) hide show
  1. package/.babelrc +12 -0
  2. package/README.md +55 -0
  3. package/components.json +22 -0
  4. package/dist/ccip-Cdkx99iZ.js +147 -0
  5. package/dist/components/ConnectDialog/index.d.ts +4 -0
  6. package/dist/components/ConnectDialog/index.d.ts.map +1 -0
  7. package/dist/components/ConnectDialog/types.d.ts +20 -0
  8. package/dist/components/ConnectDialog/types.d.ts.map +1 -0
  9. package/dist/components/DefaultDialog/index.d.ts +14 -0
  10. package/dist/components/DefaultDialog/index.d.ts.map +1 -0
  11. package/dist/components/Eip712Dialog/index.d.ts +4 -0
  12. package/dist/components/Eip712Dialog/index.d.ts.map +1 -0
  13. package/dist/components/Eip712Dialog/types.d.ts +19 -0
  14. package/dist/components/Eip712Dialog/types.d.ts.map +1 -0
  15. package/dist/components/FeeTokenSelector/index.d.ts +26 -0
  16. package/dist/components/FeeTokenSelector/index.d.ts.map +1 -0
  17. package/dist/components/OnboardingDialog/index.d.ts +4 -0
  18. package/dist/components/OnboardingDialog/index.d.ts.map +1 -0
  19. package/dist/components/OnboardingDialog/types.d.ts +36 -0
  20. package/dist/components/OnboardingDialog/types.d.ts.map +1 -0
  21. package/dist/components/OrSeparator/index.d.ts +2 -0
  22. package/dist/components/OrSeparator/index.d.ts.map +1 -0
  23. package/dist/components/PermissionDialog/index.d.ts +4 -0
  24. package/dist/components/PermissionDialog/index.d.ts.map +1 -0
  25. package/dist/components/PermissionDialog/types.d.ts +49 -0
  26. package/dist/components/PermissionDialog/types.d.ts.map +1 -0
  27. package/dist/components/SignatureDialog/index.d.ts +4 -0
  28. package/dist/components/SignatureDialog/index.d.ts.map +1 -0
  29. package/dist/components/SignatureDialog/types.d.ts +19 -0
  30. package/dist/components/SignatureDialog/types.d.ts.map +1 -0
  31. package/dist/components/SiweDialog/index.d.ts +4 -0
  32. package/dist/components/SiweDialog/index.d.ts.map +1 -0
  33. package/dist/components/SiweDialog/types.d.ts +22 -0
  34. package/dist/components/SiweDialog/types.d.ts.map +1 -0
  35. package/dist/components/TransactionDialog/DecodedCalldata.d.ts +9 -0
  36. package/dist/components/TransactionDialog/DecodedCalldata.d.ts.map +1 -0
  37. package/dist/components/TransactionDialog/index.d.ts +4 -0
  38. package/dist/components/TransactionDialog/index.d.ts.map +1 -0
  39. package/dist/components/TransactionDialog/types.d.ts +34 -0
  40. package/dist/components/TransactionDialog/types.d.ts.map +1 -0
  41. package/dist/components/ui/accordion.d.ts +8 -0
  42. package/dist/components/ui/accordion.d.ts.map +1 -0
  43. package/dist/components/ui/avatar.d.ts +7 -0
  44. package/dist/components/ui/avatar.d.ts.map +1 -0
  45. package/dist/components/ui/button.d.ts +11 -0
  46. package/dist/components/ui/button.d.ts.map +1 -0
  47. package/dist/components/ui/card.d.ts +10 -0
  48. package/dist/components/ui/card.d.ts.map +1 -0
  49. package/dist/components/ui/checkbox.d.ts +5 -0
  50. package/dist/components/ui/checkbox.d.ts.map +1 -0
  51. package/dist/components/ui/dialog.d.ts +18 -0
  52. package/dist/components/ui/dialog.d.ts.map +1 -0
  53. package/dist/components/ui/dropdown-menu.d.ts +26 -0
  54. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  55. package/dist/components/ui/form.d.ts +25 -0
  56. package/dist/components/ui/form.d.ts.map +1 -0
  57. package/dist/components/ui/input.d.ts +8 -0
  58. package/dist/components/ui/input.d.ts.map +1 -0
  59. package/dist/components/ui/label.d.ts +5 -0
  60. package/dist/components/ui/label.d.ts.map +1 -0
  61. package/dist/components/ui/popover.d.ts +8 -0
  62. package/dist/components/ui/popover.d.ts.map +1 -0
  63. package/dist/components/ui/scroll-area.d.ts +6 -0
  64. package/dist/components/ui/scroll-area.d.ts.map +1 -0
  65. package/dist/components/ui/select.d.ts +14 -0
  66. package/dist/components/ui/select.d.ts.map +1 -0
  67. package/dist/components/ui/separator.d.ts +5 -0
  68. package/dist/components/ui/separator.d.ts.map +1 -0
  69. package/dist/components/ui/sheet.d.ts +14 -0
  70. package/dist/components/ui/sheet.d.ts.map +1 -0
  71. package/dist/components/ui/spinner.d.ts +3 -0
  72. package/dist/components/ui/spinner.d.ts.map +1 -0
  73. package/dist/components/ui/tabs.d.ts +8 -0
  74. package/dist/components/ui/tabs.d.ts.map +1 -0
  75. package/dist/components/ui/tooltip.d.ts +8 -0
  76. package/dist/components/ui/tooltip.d.ts.map +1 -0
  77. package/dist/hooks/index.d.ts +6 -0
  78. package/dist/hooks/index.d.ts.map +1 -0
  79. package/dist/hooks/useChainIconURI.d.ts +12 -0
  80. package/dist/hooks/useChainIconURI.d.ts.map +1 -0
  81. package/dist/hooks/useDecodedCalldata.d.ts +15 -0
  82. package/dist/hooks/useDecodedCalldata.d.ts.map +1 -0
  83. package/dist/hooks/useFeeTokenPrice.d.ts +8 -0
  84. package/dist/hooks/useFeeTokenPrice.d.ts.map +1 -0
  85. package/dist/hooks/useGasEstimation.d.ts +77 -0
  86. package/dist/hooks/useGasEstimation.d.ts.map +1 -0
  87. package/dist/hooks/useIsMobile.d.ts +2 -0
  88. package/dist/hooks/useIsMobile.d.ts.map +1 -0
  89. package/dist/icons/index.d.ts +12 -0
  90. package/dist/icons/index.d.ts.map +1 -0
  91. package/dist/index-DlfjdYW4.js +83119 -0
  92. package/dist/index.css +1 -0
  93. package/dist/index.d.ts +16 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +38 -0
  96. package/dist/lib/utils.d.ts +3 -0
  97. package/dist/lib/utils.d.ts.map +1 -0
  98. package/dist/react/ReactUIHandler.d.ts +35 -0
  99. package/dist/react/ReactUIHandler.d.ts.map +1 -0
  100. package/dist/react/index.d.ts +2 -0
  101. package/dist/react/index.d.ts.map +1 -0
  102. package/dist/utils/formatAddress.d.ts +18 -0
  103. package/dist/utils/formatAddress.d.ts.map +1 -0
  104. package/dist/utils/index.d.ts +5 -0
  105. package/dist/utils/index.d.ts.map +1 -0
  106. package/dist/utils/justaNameInstance.d.ts +7 -0
  107. package/dist/utils/justaNameInstance.d.ts.map +1 -0
  108. package/dist/utils/tokenBalance.d.ts +10 -0
  109. package/dist/utils/tokenBalance.d.ts.map +1 -0
  110. package/dist/utils/tokenPrice.d.ts +12 -0
  111. package/dist/utils/tokenPrice.d.ts.map +1 -0
  112. package/package.json +67 -0
  113. package/postcss.config.cjs +6 -0
  114. package/src/components/ConnectDialog/index.tsx +270 -0
  115. package/src/components/ConnectDialog/types.ts +34 -0
  116. package/src/components/DefaultDialog/index.tsx +99 -0
  117. package/src/components/Eip712Dialog/index.tsx +525 -0
  118. package/src/components/Eip712Dialog/types.ts +27 -0
  119. package/src/components/FeeTokenSelector/index.tsx +308 -0
  120. package/src/components/OnboardingDialog/index.tsx +317 -0
  121. package/src/components/OnboardingDialog/types.ts +43 -0
  122. package/src/components/OrSeparator/index.tsx +13 -0
  123. package/src/components/PermissionDialog/index.tsx +598 -0
  124. package/src/components/PermissionDialog/types.ts +77 -0
  125. package/src/components/SignatureDialog/index.tsx +180 -0
  126. package/src/components/SignatureDialog/types.ts +27 -0
  127. package/src/components/SiweDialog/index.tsx +231 -0
  128. package/src/components/SiweDialog/types.ts +34 -0
  129. package/src/components/TransactionDialog/DecodedCalldata.tsx +79 -0
  130. package/src/components/TransactionDialog/index.tsx +663 -0
  131. package/src/components/TransactionDialog/types.ts +54 -0
  132. package/src/components/ui/accordion.tsx +66 -0
  133. package/src/components/ui/avatar.tsx +53 -0
  134. package/src/components/ui/button.tsx +59 -0
  135. package/src/components/ui/card.tsx +92 -0
  136. package/src/components/ui/checkbox.tsx +32 -0
  137. package/src/components/ui/dialog.tsx +183 -0
  138. package/src/components/ui/dropdown-menu.tsx +258 -0
  139. package/src/components/ui/form.tsx +167 -0
  140. package/src/components/ui/input.tsx +60 -0
  141. package/src/components/ui/label.tsx +24 -0
  142. package/src/components/ui/popover.tsx +48 -0
  143. package/src/components/ui/scroll-area.tsx +58 -0
  144. package/src/components/ui/select.tsx +160 -0
  145. package/src/components/ui/separator.tsx +28 -0
  146. package/src/components/ui/sheet.tsx +169 -0
  147. package/src/components/ui/spinner.tsx +18 -0
  148. package/src/components/ui/tabs.tsx +69 -0
  149. package/src/components/ui/tooltip.tsx +61 -0
  150. package/src/hooks/index.ts +5 -0
  151. package/src/hooks/useChainIconURI.tsx +117 -0
  152. package/src/hooks/useDecodedCalldata.ts +128 -0
  153. package/src/hooks/useFeeTokenPrice.tsx +36 -0
  154. package/src/hooks/useGasEstimation.ts +474 -0
  155. package/src/hooks/useIsMobile.ts +36 -0
  156. package/src/icons/index.tsx +114 -0
  157. package/src/index.ts +19 -0
  158. package/src/lib/utils.ts +6 -0
  159. package/src/react/ReactUIHandler.tsx +3004 -0
  160. package/src/react/index.ts +2 -0
  161. package/src/styles.css +210 -0
  162. package/src/utils/formatAddress.ts +24 -0
  163. package/src/utils/index.ts +4 -0
  164. package/src/utils/justaNameInstance.ts +25 -0
  165. package/src/utils/tokenBalance.ts +41 -0
  166. package/src/utils/tokenPrice.ts +58 -0
  167. package/tailwind.config.js +130 -0
  168. package/tsconfig.json +19 -0
  169. package/tsconfig.lib.json +43 -0
  170. package/vite.config.ts +45 -0
@@ -0,0 +1,663 @@
1
+ 'use client'
2
+
3
+ import { Button } from "../ui/button";
4
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../ui/accordion";
5
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
6
+ import { DefaultDialog } from "../DefaultDialog";
7
+ import { FeeTokenSelector } from "../FeeTokenSelector";
8
+ import { CopiedIcon, CopyIcon, WalletIcon } from "../../icons";
9
+ import { useState, useEffect } from "react";
10
+ import { formatEther } from "viem";
11
+ import { Info } from "lucide-react";
12
+ import { TransactionDialogProps } from "./types";
13
+ import { useIsMobile, useChainIconURI, useFeeTokenPrice } from "../../hooks";
14
+ import { getJustaNameInstance, getDisplayAddress } from "../../utils";
15
+ import { DecodedCalldata } from "./DecodedCalldata";
16
+
17
+ export const TransactionDialog = ({
18
+ // open,
19
+ // onOpenChange,
20
+ transactions,
21
+ walletAddress,
22
+ gasFee,
23
+ gasFeeLoading,
24
+ gasEstimationError,
25
+ sponsored,
26
+ onConfirm,
27
+ onCancel,
28
+ isProcessing,
29
+ transactionStatus,
30
+ networkName,
31
+ apiKey,
32
+ // Fee token props
33
+ feeTokens,
34
+ feeTokensLoading,
35
+ selectedFeeToken,
36
+ onFeeTokenSelect,
37
+ showFeeTokenSelector,
38
+ isPayingWithErc20,
39
+ // RPC configuration
40
+ mainnetRpcUrl,
41
+ }: TransactionDialogProps) => {
42
+ const isMobile = useIsMobile();
43
+ const [isDataCopied, setIsDataCopied] = useState<{ [key: number]: boolean }>({});
44
+ const [resolvedAddresses, setResolvedAddresses] = useState<Record<string, string>>({});
45
+
46
+ const totalTransactions = transactions.length;
47
+ const isSingleTransaction = totalTransactions === 1;
48
+ const currentTransaction = transactions[0];
49
+
50
+ // Get chain icon using the hook - fetch from capabilities chainMetadata
51
+ const chainIcon = useChainIconURI(currentTransaction?.chainId || 1, apiKey, 24);
52
+
53
+ // Get native token symbol from feeTokens (defaults to ETH if not found)
54
+ const nativeToken = feeTokens?.find(t => t.isNative);
55
+ const nativeSymbol = nativeToken?.symbol || 'ETH';
56
+
57
+ // Fetch native token price dynamically based on the chain's native token symbol
58
+ const nativeTokenPrice = useFeeTokenPrice(nativeSymbol);
59
+
60
+ // Check if there are any selectable payment options
61
+ // If feeTokens is not loaded yet (null/undefined/empty), assume there are selectable options
62
+ const hasSelectablePaymentOption = !feeTokens || feeTokens.length === 0
63
+ ? true
64
+ : feeTokens.some(t => t.isSelectable);
65
+
66
+ // Initialize JustaName and resolve addresses
67
+ useEffect(() => {
68
+ const justaName = getJustaNameInstance(mainnetRpcUrl);
69
+
70
+ // Resolve wallet address
71
+ if (walletAddress && currentTransaction?.chainId) {
72
+ justaName.subnames.reverseResolve({
73
+ address: walletAddress as `0x${string}`,
74
+ chainId: currentTransaction.chainId,
75
+ }).then((result) => {
76
+ console.log('result', result);
77
+ if (result) {
78
+ setResolvedAddresses(prev => ({ ...prev, [walletAddress]: result }));
79
+ }
80
+ }).catch(() => {
81
+ // Silently fail if resolution fails
82
+ });
83
+ }
84
+
85
+ // Resolve transaction 'to' addresses
86
+ transactions.forEach((transaction) => {
87
+ if (transaction.to && transaction.chainId) {
88
+ justaName.subnames.reverseResolve({
89
+ address: transaction.to as `0x${string}`,
90
+ chainId: transaction.chainId,
91
+ }).then((result) => {
92
+ if (result) {
93
+ setResolvedAddresses(prev => ({ ...prev, [transaction.to]: result }));
94
+ }
95
+ }).catch(() => {
96
+ // Silently fail if resolution fails
97
+ });
98
+ }
99
+ });
100
+ }, [walletAddress, transactions, currentTransaction?.chainId]);
101
+
102
+ // Get display addresses - use resolved name or formatted address
103
+ const displayWalletAddress = getDisplayAddress(resolvedAddresses[walletAddress], walletAddress);
104
+ const displayToAddress = currentTransaction?.to
105
+ ? getDisplayAddress(resolvedAddresses[currentTransaction.to], currentTransaction.to)
106
+ : '';
107
+
108
+ // Helper function to format value for display
109
+ const formatTransactionValue = (value?: string) => {
110
+ if (!value || value === '0' || value === '0x0') return null;
111
+
112
+ try {
113
+ // Handle hex strings (0x...)
114
+ if (value.startsWith('0x')) {
115
+ const bigIntValue = BigInt(value);
116
+ return formatEther(bigIntValue);
117
+ }
118
+
119
+ // If value looks like wei (long number, no decimals), format it from wei to ETH
120
+ if (/^\d+$/.test(value) && value.length > 10) {
121
+ return formatEther(BigInt(value));
122
+ }
123
+
124
+ // If it's already a decimal string (like "0.001"), return as is
125
+ if (/^\d+\.?\d*$/.test(value) && value.length <= 20) {
126
+ return value;
127
+ }
128
+
129
+ // Try to parse as BigInt and format
130
+ const bigIntValue = BigInt(value);
131
+ return formatEther(bigIntValue);
132
+ } catch (error) {
133
+ // If parsing fails, return null to hide the value
134
+ console.warn('Failed to format transaction value:', value, error);
135
+ return null;
136
+ }
137
+ };
138
+
139
+ // Determine if confirmation is allowed:
140
+ // - Not processing
141
+ // - Gas estimation not loading
142
+ // - No gas estimation error (unless sponsored)
143
+ // - Must have at least one selectable payment option
144
+ const hasInsufficientFunds = !hasSelectablePaymentOption || (gasEstimationError && !sponsored && !isPayingWithErc20);
145
+ const canConfirm = !isProcessing && !gasFeeLoading && !hasInsufficientFunds;
146
+
147
+ return (
148
+ <DefaultDialog
149
+ // open={open}
150
+ // onOpenChange={!isProcessing ? onOpenChange : undefined}
151
+ open={true}
152
+ onOpenChange={isProcessing ? undefined : () => {
153
+ // Empty handler to prevent dialog close
154
+ }}
155
+ handleClose={isProcessing ? undefined : onCancel}
156
+ header={
157
+ <div className="flex flex-col gap-2.5 p-3.5">
158
+ <p className="text-xs font-bold text-muted-foreground leading-[100%]">
159
+ {new Date().toLocaleDateString('en-US', {
160
+ weekday: 'long',
161
+ day: 'numeric',
162
+ month: 'long'
163
+ })} at {new Date().toLocaleTimeString('en-US', {
164
+ hour: '2-digit',
165
+ minute: '2-digit',
166
+ second: '2-digit',
167
+ timeZoneName: 'short'
168
+ })}
169
+ </p>
170
+ <p className="text-[30px] font-normal leading-[100%] text-foreground">
171
+ {'Review Transaction'}
172
+ </p>
173
+ {totalTransactions > 1 && currentTransaction?.description && (
174
+ <p className="text-sm text-muted-foreground">
175
+ {currentTransaction.action}: {currentTransaction.description}
176
+ </p>
177
+ )}
178
+ </div>
179
+ }
180
+ contentStyle={isMobile ? {
181
+ width: '100%',
182
+ height: '100%',
183
+ maxWidth: 'none',
184
+ maxHeight: 'none',
185
+ overflowY: 'auto',
186
+ } : {
187
+ width: '500px',
188
+ minWidth: '500px',
189
+ maxHeight: !isSingleTransaction ? '85vh' : undefined,
190
+ }}
191
+ >
192
+ <div className={`flex flex-col gap-6 justify-between max-md:h-full ${!isSingleTransaction ? 'overflow-hidden h-full' : ''}`}>
193
+ {isSingleTransaction ? (
194
+ // Single Transaction Layout
195
+ <>
196
+ <div className="flex flex-col gap-3 flex-1 overflow-y-auto min-h-0 max-h-[60vh]">
197
+ {/* From - To */}
198
+ <div className="flex flex-row justify-between items-center gap-2.5 p-3.5 border border-border rounded-[6px]">
199
+ <div className="flex flex-col text-foreground gap-0.5 min-w-0 flex-1">
200
+ <p className="text-xs font-bold leading-[133%]">From</p>
201
+ <div className="flex flex-row items-center gap-1 min-w-0">
202
+ <WalletIcon className="w-3 h-3 flex-shrink-0" stroke="black" />
203
+ <p className="text-base font-normal leading-[150%]">{displayWalletAddress}</p>
204
+ </div>
205
+ </div>
206
+ <div className="w-[1px] rounded-full bg-border h-full flex-shrink-0 min-h-[70px]" />
207
+ <div className="flex flex-col text-foreground gap-0.5 min-w-0 flex-1">
208
+ <p className="text-xs font-bold leading-[133%]">To</p>
209
+ <div className="flex flex-row items-center gap-1 min-w-0">
210
+ <WalletIcon className="w-3 h-3 flex-shrink-0" stroke="black" />
211
+ <p className="text-base font-normal leading-[150%]">
212
+ {displayToAddress}
213
+ </p>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ {/* Value */}
219
+ {formatTransactionValue(currentTransaction?.value) && (
220
+ <div className="flex flex-row justify-between items-center gap-2.5 p-3.5 border border-border rounded-[6px]">
221
+ <div className="flex flex-col text-foreground gap-0.5">
222
+ <p className="text-xs font-bold leading-[133%]">Value</p>
223
+ <p className="text-base font-normal leading-[150%]">{formatTransactionValue(currentTransaction?.value)} {nativeSymbol}</p>
224
+ </div>
225
+ </div>
226
+ )}
227
+
228
+ {/* Network - Fees */}
229
+ <div className="flex flex-row justify-between items-center gap-2.5 p-3.5 border border-border rounded-[6px]">
230
+ <div className="flex flex-col text-foreground flex-1 gap-0.5">
231
+ <p className="text-xs font-bold leading-[133%]">Network</p>
232
+ <div className="flex flex-row items-center gap-1">
233
+ {chainIcon}
234
+ <p className="text-base font-normal text-ellipsis leading-[150%] truncate">{networkName || 'Ethereum'}</p>
235
+ </div>
236
+ </div>
237
+ <div className="w-[1px] rounded-full bg-border h-full min-h-[50px]" />
238
+ <div className="flex flex-col text-foreground flex-1 gap-0.5">
239
+ <div className="flex items-center gap-1">
240
+ <p className="text-xs font-bold leading-[133%]">Network Fees</p>
241
+ <TooltipProvider delayDuration={0}>
242
+ <Tooltip>
243
+ <TooltipTrigger asChild>
244
+ <Info className="size-3 text-muted-foreground cursor-help" />
245
+ </TooltipTrigger>
246
+ <TooltipContent side="top" className="max-w-[200px] text-xs">
247
+ <p>Gas fees paid to network validators to process your transaction. You can pay with {nativeSymbol} or supported tokens.</p>
248
+ </TooltipContent>
249
+ </Tooltip>
250
+ </TooltipProvider>
251
+ </div>
252
+ <div className="flex flex-row items-center w-full justify-between gap-1">
253
+ {gasFeeLoading && !isPayingWithErc20 ? (
254
+ <p className="text-base font-normal text-muted-foreground">Estimating...</p>
255
+ ) : gasEstimationError && !sponsored ? (
256
+ <div className="flex flex-col">
257
+ <p className="text-sm text-red-600 font-medium">{gasEstimationError}</p>
258
+ </div>
259
+ ) : sponsored ? (
260
+ <div className="flex flex-col">
261
+ <div className="flex items-center gap-2">
262
+ {gasFee && gasFee !== 'sponsored' && nativeTokenPrice > 0 && (
263
+ <div className="flex flex-col line-through text-muted-foreground">
264
+ <p className="text-base font-normal">
265
+ ${(nativeTokenPrice * Number(gasFee)).toFixed(4)}
266
+ </p>
267
+ </div>
268
+ )}
269
+ <span className="text-xs font-semibold text-green-600 bg-green-100 px-2 py-0.5 rounded">
270
+ Sponsored
271
+ </span>
272
+ </div>
273
+ <p className="text-xs font-normal text-muted-foreground">
274
+ {gasFee && gasFee !== 'sponsored' ? (() => {
275
+ const gasValue = Number(gasFee);
276
+ if (gasValue > 0 && gasValue < 0.0001) {
277
+ return `< 0.0001 ${nativeSymbol}`;
278
+ }
279
+ return `${gasValue.toFixed(4)} ${nativeSymbol}`;
280
+ })() : 'Gas fees covered'}
281
+ </p>
282
+ </div>
283
+ ) : isPayingWithErc20 && selectedFeeToken ? (
284
+ <div className="flex flex-col gap-0.5 w-full">
285
+ <div className="flex items-center justify-between w-full">
286
+ <p className="text-base font-normal text-foreground">
287
+ {/* Show estimated cost from paymaster quote - don't fallback to ETH calculation */}
288
+ {selectedFeeToken.gasCostFormatted ? (
289
+ // For stablecoins like USDC/USDT, the value is approximately USD
290
+ `$${selectedFeeToken.gasCostFormatted}`
291
+ ) : (
292
+ <span className="text-muted-foreground">Estimating...</span>
293
+ )}
294
+ </p>
295
+ {/* Inline Fee Token Selector */}
296
+ {showFeeTokenSelector && feeTokens && onFeeTokenSelect && (
297
+ <FeeTokenSelector
298
+ tokens={feeTokens}
299
+ selectedToken={selectedFeeToken}
300
+ onSelect={onFeeTokenSelect}
301
+ isLoading={feeTokensLoading ?? false}
302
+ disabled={isProcessing}
303
+ nativeTokenPrice={nativeTokenPrice}
304
+ estimatedGasEth={gasFee || '0'}
305
+ />
306
+ )}
307
+ </div>
308
+ {selectedFeeToken.gasCostFormatted && (
309
+ <p className="text-xs font-normal text-muted-foreground">
310
+ Up to {selectedFeeToken.gasCostFormatted} {selectedFeeToken.symbol}
311
+ </p>
312
+ )}
313
+ </div>
314
+ ) : gasFee && gasFee !== 'sponsored' ? (
315
+ <div className="flex flex-col gap-0.5 w-full">
316
+ <div className="flex items-center justify-between w-full">
317
+ <p className="text-base font-normal text-foreground">
318
+ {nativeTokenPrice > 0 ? `$${(nativeTokenPrice * Number(gasFee)).toFixed(4)}` : ''}
319
+ </p>
320
+ {/* Inline Fee Token Selector */}
321
+ {showFeeTokenSelector && !sponsored && feeTokens && onFeeTokenSelect && (
322
+ <FeeTokenSelector
323
+ tokens={feeTokens}
324
+ selectedToken={selectedFeeToken ?? null}
325
+ onSelect={onFeeTokenSelect}
326
+ isLoading={feeTokensLoading ?? false}
327
+ disabled={isProcessing}
328
+ nativeTokenPrice={nativeTokenPrice}
329
+ estimatedGasEth={gasFee}
330
+ />
331
+ )}
332
+ </div>
333
+ <p className="text-xs font-normal text-muted-foreground">
334
+ {(() => {
335
+ const gasValue = Number(gasFee);
336
+ if (gasValue > 0 && gasValue < 0.0001) {
337
+ return `< 0.0001 ${nativeSymbol}`;
338
+ }
339
+ return `${gasValue.toFixed(4)} ${nativeSymbol}`;
340
+ })()}
341
+ </p>
342
+ </div>
343
+ ) : (
344
+ <p className="text-base font-normal text-muted-foreground">Unable to estimate</p>
345
+ )}
346
+ </div>
347
+ </div>
348
+ </div>
349
+
350
+ {/* Show Data section if data is provided */}
351
+ {currentTransaction?.data && (
352
+ <div className="flex flex-col p-3.5 gap-2.5 border border-border rounded-[6px]">
353
+ <div className="flex flex-row items-center justify-between w-full">
354
+ <p className="text-xs font-bold leading-[133%] text-foreground">Data</p>
355
+ {isDataCopied[0] ? (
356
+ <CopiedIcon width={16} height={16} />
357
+ ) : (
358
+ <CopyIcon width={16} height={16} onClick={() => {
359
+ if (typeof window !== 'undefined' && navigator?.clipboard) {
360
+ navigator.clipboard.writeText(currentTransaction?.data ?? '');
361
+ setIsDataCopied({ ...isDataCopied, 0: true });
362
+ setTimeout(() => setIsDataCopied(prev => ({ ...prev, 0: false })), 3000);
363
+ }
364
+ }} className="cursor-pointer" />
365
+ )}
366
+ </div>
367
+ <DecodedCalldata
368
+ to={currentTransaction.to}
369
+ data={currentTransaction.data!}
370
+ chainId={currentTransaction.chainId}
371
+ apiKey={apiKey}
372
+ />
373
+ </div>
374
+ )}
375
+
376
+ {/* Transaction Status */}
377
+ {transactionStatus && (
378
+ <div className={`text-sm p-3 rounded-lg ${transactionStatus.includes('Error') ? 'bg-red-50 text-red-600' :
379
+ transactionStatus.includes('successfully') ? 'bg-green-50 text-green-600' :
380
+ 'bg-blue-50 text-blue-600'
381
+ }`}>
382
+ {transactionStatus}
383
+ </div>
384
+ )}
385
+ </div>
386
+
387
+ {/* Action Buttons */}
388
+ <div className="flex gap-3 p-3.5 max-md:mt-auto flex-shrink-0">
389
+ <Button
390
+ variant="outline"
391
+ onClick={onCancel}
392
+ disabled={isProcessing}
393
+ className="flex-1"
394
+ >
395
+ Cancel
396
+ </Button>
397
+ <Button
398
+ onClick={onConfirm}
399
+ disabled={!canConfirm}
400
+ className="flex-1"
401
+ >
402
+ {hasInsufficientFunds ? 'Insufficient Funds' :
403
+ isProcessing ? 'Processing...' : 'Transact'}
404
+ </Button>
405
+ </div>
406
+ </>
407
+ ) : (
408
+ // Multiple Transactions Layout with Accordion
409
+ <>
410
+ <div className="flex flex-col gap-3 flex-1 min-h-0 overflow-y-auto max-h-[60vh]">
411
+ {/* From Address */}
412
+ <div className="p-3.5 border border-border rounded-[6px] flex-shrink-0">
413
+ <p className="text-xs font-bold leading-[133%] text-foreground mb-1">From</p>
414
+ <div className="flex flex-row items-center gap-1">
415
+ <WalletIcon className="w-3 h-3 flex-shrink-0" stroke="black" />
416
+ <p className="text-base font-normal leading-[150%]">{displayWalletAddress}</p>
417
+ </div>
418
+ </div>
419
+
420
+ {/* Accordion for Transactions */}
421
+ <div className="flex-1 overflow-y-auto min-h-0">
422
+ <Accordion type="multiple" className="w-full space-y-3" defaultValue={transactions.map((_, index) => `transaction-${index}`)}>
423
+ {transactions.map((transaction, index) => (
424
+ <AccordionItem key={index} value={`transaction-${index}`} className="border border-border rounded-[6px] overflow-hidden">
425
+ <AccordionTrigger className="px-3.5 py-2.5 hover:no-underline">
426
+ <span className="text-base font-medium">Call {index + 1}</span>
427
+ </AccordionTrigger>
428
+ <AccordionContent className="px-3.5 pb-3.5">
429
+ <div className="flex flex-col gap-3">
430
+ {/* Interacting with (To) */}
431
+ <div className="flex flex-col gap-1 border border-border rounded-[6px] p-2">
432
+ <p className="text-xs font-bold leading-[133%] text-black">Interacting with (to)</p>
433
+ <div className="flex flex-row items-center gap-1">
434
+ <WalletIcon className="w-3 h-3 flex-shrink-0" stroke="black" />
435
+ <p className="text-sm font-normal leading-[150%]">
436
+ {getDisplayAddress(resolvedAddresses[transaction.to], transaction.to)}
437
+ </p>
438
+ </div>
439
+ </div>
440
+
441
+ {/* Value */}
442
+ {formatTransactionValue(transaction.value) && (
443
+ <div className="flex items-center gap-2 border border-border rounded-[6px] p-2">
444
+ <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
445
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
446
+ <path d="M10 2L3 10L10 14L17 10L10 2Z" fill="currentColor" className="text-primary" />
447
+ </svg>
448
+ </div>
449
+ <div className="flex-1">
450
+ <p className="text-xs font-bold leading-[133%] text-muted-foreground">Value</p>
451
+ <div className="flex items-baseline gap-2">
452
+ <p className="text-base font-normal">{formatTransactionValue(transaction.value)} {nativeSymbol}</p>
453
+ {nativeTokenPrice > 0 && (
454
+ <p className="text-sm text-muted-foreground">
455
+ ${(Number(formatTransactionValue(transaction.value)) * nativeTokenPrice).toFixed(2)}
456
+ </p>
457
+ )}
458
+ </div>
459
+ </div>
460
+ </div>
461
+ )}
462
+
463
+ {/* Data */}
464
+ {transaction.data && (
465
+ <div className="flex flex-col gap-1 border border-border rounded-[6px] p-2">
466
+ <div className="flex items-center justify-between mb-2">
467
+ <p className="text-xs font-bold leading-[133%] text-muted-foreground">Data</p>
468
+ {isDataCopied[index] ? (
469
+ <CopiedIcon width={16} height={16} />
470
+ ) : (
471
+ <CopyIcon
472
+ width={16}
473
+ height={16}
474
+ onClick={(e) => {
475
+ e.stopPropagation();
476
+ if (typeof window !== 'undefined' && navigator?.clipboard) {
477
+ navigator.clipboard.writeText(transaction.data ?? '');
478
+ setIsDataCopied({ ...isDataCopied, [index]: true });
479
+ setTimeout(() => setIsDataCopied(prev => ({ ...prev, [index]: false })), 3000);
480
+ }
481
+ }}
482
+ className="cursor-pointer"
483
+ />
484
+ )}
485
+ </div>
486
+ <DecodedCalldata
487
+ to={transaction.to}
488
+ data={transaction.data!}
489
+ chainId={transaction.chainId}
490
+ apiKey={apiKey}
491
+ />
492
+ </div>
493
+ )}
494
+ </div>
495
+ </AccordionContent>
496
+ </AccordionItem>
497
+ ))}
498
+ </Accordion>
499
+ </div>
500
+
501
+ {/* Transaction Status */}
502
+ {transactionStatus && (
503
+ <div className={`text-sm p-3 rounded-lg ${transactionStatus.includes('Error') ? 'bg-red-50 text-red-600' :
504
+ transactionStatus.includes('successfully') ? 'bg-green-50 text-green-600' :
505
+ 'bg-blue-50 text-blue-600'
506
+ }`}>
507
+ {transactionStatus}
508
+ </div>
509
+ )}
510
+ </div>
511
+
512
+ {/* Fixed Bottom Section */}
513
+ <div className="border-t pt-3 space-y-3 flex-shrink-0">
514
+ {/* Network and Fees */}
515
+ <div className="flex flex-row justify-between items-center gap-2.5 p-3.5 border border-border rounded-[6px]">
516
+ <div className="flex flex-col text-foreground flex-1 gap-0.5">
517
+ <p className="text-xs font-bold leading-[133%]">Network</p>
518
+ <div className="flex flex-row items-center gap-1">
519
+ {chainIcon}
520
+ <p className="text-base font-normal text-ellipsis leading-[150%] truncate">{networkName || 'Ethereum'}</p>
521
+ </div>
522
+ </div>
523
+ <div className="w-[1px] rounded-full bg-border h-full min-h-[50px]" />
524
+ <div className="flex flex-col text-foreground flex-1 gap-0.5">
525
+ <div className="flex items-center gap-1">
526
+ <p className="text-xs font-bold leading-[133%]">Network Fees</p>
527
+ <TooltipProvider delayDuration={0}>
528
+ <Tooltip>
529
+ <TooltipTrigger asChild>
530
+ <Info className="size-3 text-muted-foreground cursor-help" />
531
+ </TooltipTrigger>
532
+ <TooltipContent side="top" className="max-w-[200px] text-xs">
533
+ <p>Gas fees paid to network validators to process your transaction. You can pay with {nativeSymbol} or supported tokens.</p>
534
+ </TooltipContent>
535
+ </Tooltip>
536
+ </TooltipProvider>
537
+ </div>
538
+ <div className="flex flex-row items-center w-full justify-between gap-1">
539
+ {gasFeeLoading && !isPayingWithErc20 ? (
540
+ <p className="text-base font-normal text-muted-foreground">Estimating...</p>
541
+ ) : gasEstimationError && !sponsored ? (
542
+ <div className="flex flex-col">
543
+ <p className="text-sm text-red-600 font-medium">{gasEstimationError}</p>
544
+ </div>
545
+ ) : sponsored ? (
546
+ <div className="flex flex-col">
547
+ <div className="flex items-center gap-2">
548
+ {gasFee && gasFee !== 'sponsored' && nativeTokenPrice > 0 && (
549
+ <div className="flex flex-col line-through text-muted-foreground">
550
+ <p className="text-base font-normal">
551
+ ${(nativeTokenPrice * Number(gasFee)).toFixed(4)}
552
+ </p>
553
+ </div>
554
+ )}
555
+ <span className="text-xs font-semibold text-green-600 bg-green-100 px-2 py-0.5 rounded">
556
+ Sponsored
557
+ </span>
558
+ </div>
559
+ <p className="text-xs font-normal text-muted-foreground">
560
+ {gasFee && gasFee !== 'sponsored' ? (() => {
561
+ const gasValue = Number(gasFee);
562
+ if (gasValue > 0 && gasValue < 0.0001) {
563
+ return `< 0.0001 ${nativeSymbol}`;
564
+ }
565
+ return `${gasValue.toFixed(4)} ${nativeSymbol}`;
566
+ })() : 'Gas fees covered'}
567
+ </p>
568
+ </div>
569
+ ) : isPayingWithErc20 && selectedFeeToken ? (
570
+ <div className="flex flex-col gap-0.5 w-full">
571
+ <div className="flex items-center justify-between w-full">
572
+ <p className="text-base font-normal text-foreground">
573
+ {/* Show estimated cost from paymaster quote - don't fallback to ETH calculation */}
574
+ {selectedFeeToken.gasCostFormatted ? (
575
+ // For stablecoins like USDC/USDT, the value is approximately USD
576
+ `$${selectedFeeToken.gasCostFormatted}`
577
+ ) : (
578
+ <span className="text-muted-foreground">Estimating...</span>
579
+ )}
580
+ </p>
581
+ {/* Inline Fee Token Selector */}
582
+ {showFeeTokenSelector && feeTokens && onFeeTokenSelect && (
583
+ <FeeTokenSelector
584
+ tokens={feeTokens}
585
+ selectedToken={selectedFeeToken}
586
+ onSelect={onFeeTokenSelect}
587
+ isLoading={feeTokensLoading ?? false}
588
+ disabled={isProcessing}
589
+ nativeTokenPrice={nativeTokenPrice}
590
+ estimatedGasEth={gasFee || '0'}
591
+ />
592
+ )}
593
+ </div>
594
+ {selectedFeeToken.gasCostFormatted && (
595
+ <p className="text-xs font-normal text-muted-foreground">
596
+ Up to {selectedFeeToken.gasCostFormatted} {selectedFeeToken.symbol}
597
+ </p>
598
+ )}
599
+ </div>
600
+ ) : gasFee && gasFee !== 'sponsored' ? (
601
+ <div className="flex flex-col gap-0.5 w-full">
602
+ <div className="flex items-center justify-between w-full">
603
+ <p className="text-base font-normal text-foreground">
604
+ {nativeTokenPrice > 0 ? `$${(nativeTokenPrice * Number(gasFee)).toFixed(4)}` : ''}
605
+ </p>
606
+ {/* Inline Fee Token Selector */}
607
+ {showFeeTokenSelector && !sponsored && feeTokens && onFeeTokenSelect && (
608
+ <FeeTokenSelector
609
+ tokens={feeTokens}
610
+ selectedToken={selectedFeeToken ?? null}
611
+ onSelect={onFeeTokenSelect}
612
+ isLoading={feeTokensLoading ?? false}
613
+ disabled={isProcessing}
614
+ nativeTokenPrice={nativeTokenPrice}
615
+ estimatedGasEth={gasFee}
616
+ />
617
+ )}
618
+ </div>
619
+ <p className="text-xs font-normal text-muted-foreground">
620
+ {(() => {
621
+ const gasValue = Number(gasFee);
622
+ if (gasValue > 0 && gasValue < 0.0001) {
623
+ return `< 0.0001 ${nativeSymbol}`;
624
+ }
625
+ return `${gasValue.toFixed(4)} ${nativeSymbol}`;
626
+ })()}
627
+ </p>
628
+ </div>
629
+ ) : (
630
+ <p className="text-base font-normal text-muted-foreground">Unable to estimate</p>
631
+ )}
632
+ </div>
633
+ </div>
634
+ </div>
635
+
636
+ {/* Action Buttons */}
637
+ <div className="flex gap-3 px-3.5 flex-shrink-0">
638
+ <Button
639
+ variant="outline"
640
+ onClick={onCancel}
641
+ disabled={isProcessing}
642
+ className="flex-1"
643
+ >
644
+ Cancel
645
+ </Button>
646
+ <Button
647
+ onClick={onConfirm}
648
+ disabled={!canConfirm}
649
+ className="flex-1"
650
+ >
651
+ {gasEstimationError && !sponsored ? 'Insufficient Funds' :
652
+ isProcessing ? 'Processing...' : 'Transact'}
653
+ </Button>
654
+ </div>
655
+ </div>
656
+ </>
657
+ )}
658
+ </div>
659
+ </DefaultDialog>
660
+ )
661
+ }
662
+
663
+ export * from './types';