0xtrails 0.2.5 → 0.2.6

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 (116) hide show
  1. package/dist/abortController.d.ts +8 -0
  2. package/dist/abortController.d.ts.map +1 -0
  3. package/dist/{ccip-CXlshvBY.js → ccip-Xjh9d1gb.js} +7 -7
  4. package/dist/constants.d.ts +2 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/fees.d.ts +19 -0
  7. package/dist/fees.d.ts.map +1 -0
  8. package/dist/{index-_QuyGrjU.js → index-BnhdZ8Ho.js} +34769 -34247
  9. package/dist/index.js +726 -520
  10. package/dist/prepareSend.d.ts +11 -77
  11. package/dist/prepareSend.d.ts.map +1 -1
  12. package/dist/transactions.d.ts +4 -2
  13. package/dist/transactions.d.ts.map +1 -1
  14. package/dist/widget/components/AccountIntentTransactionHistoryButton.d.ts +4 -0
  15. package/dist/widget/components/AccountIntentTransactionHistoryButton.d.ts.map +1 -0
  16. package/dist/widget/components/AccountSettings.d.ts.map +1 -1
  17. package/dist/widget/components/ChainFilterDropdown.d.ts.map +1 -1
  18. package/dist/widget/components/ClassicSwap.d.ts +2 -2
  19. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  20. package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
  21. package/dist/widget/components/DynamicInputStyles.d.ts +18 -0
  22. package/dist/widget/components/DynamicInputStyles.d.ts.map +1 -0
  23. package/dist/widget/components/Earn.d.ts +2 -2
  24. package/dist/widget/components/Earn.d.ts.map +1 -1
  25. package/dist/widget/components/ErrorAnimationIcon.d.ts +2 -0
  26. package/dist/widget/components/ErrorAnimationIcon.d.ts.map +1 -0
  27. package/dist/widget/components/FeeBreakdown.d.ts +9 -0
  28. package/dist/widget/components/FeeBreakdown.d.ts.map +1 -0
  29. package/dist/widget/components/Fund.d.ts +2 -2
  30. package/dist/widget/components/Fund.d.ts.map +1 -1
  31. package/dist/widget/components/FundSwap.d.ts +2 -2
  32. package/dist/widget/components/FundSwap.d.ts.map +1 -1
  33. package/dist/widget/components/FundingMethodSelectorButton.d.ts.map +1 -1
  34. package/dist/widget/components/Identicon.d.ts.map +1 -1
  35. package/dist/widget/components/MeshConnectExchanges.d.ts +0 -3
  36. package/dist/widget/components/MeshConnectExchanges.d.ts.map +1 -1
  37. package/dist/widget/components/Modal.d.ts.map +1 -1
  38. package/dist/widget/components/Pay.d.ts +2 -2
  39. package/dist/widget/components/Pay.d.ts.map +1 -1
  40. package/dist/widget/components/PoolDeposit.d.ts +3 -2
  41. package/dist/widget/components/PoolDeposit.d.ts.map +1 -1
  42. package/dist/widget/components/PoolWithdraw.d.ts +3 -2
  43. package/dist/widget/components/PoolWithdraw.d.ts.map +1 -1
  44. package/dist/widget/components/QuoteDetails.d.ts +1 -0
  45. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  46. package/dist/widget/components/Receipt.d.ts.map +1 -1
  47. package/dist/widget/components/RecipientSelectorButton.d.ts.map +1 -1
  48. package/dist/widget/components/ScreenHeader.d.ts.map +1 -1
  49. package/dist/widget/components/Swap.d.ts +2 -2
  50. package/dist/widget/components/Swap.d.ts.map +1 -1
  51. package/dist/widget/components/ThemeProvider.d.ts.map +1 -1
  52. package/dist/widget/components/TokenDisplayNonSelectable.d.ts +11 -0
  53. package/dist/widget/components/TokenDisplayNonSelectable.d.ts.map +1 -0
  54. package/dist/widget/components/TokenSelector.d.ts.map +1 -1
  55. package/dist/widget/components/TokenSelectorButton.d.ts.map +1 -1
  56. package/dist/widget/components/Tooltip.d.ts +9 -0
  57. package/dist/widget/components/Tooltip.d.ts.map +1 -0
  58. package/dist/widget/components/WaasFeeOptions.d.ts +1 -0
  59. package/dist/widget/components/WaasFeeOptions.d.ts.map +1 -1
  60. package/dist/widget/components/WalletConfirmation.d.ts.map +1 -1
  61. package/dist/widget/components/WalletConnect.d.ts.map +1 -1
  62. package/dist/widget/css/compiled.css +1 -1
  63. package/dist/widget/hooks/useQuote.d.ts +83 -0
  64. package/dist/widget/hooks/useQuote.d.ts.map +1 -0
  65. package/dist/widget/hooks/useSendForm.d.ts +2 -2
  66. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  67. package/dist/widget/index.js +2 -2
  68. package/dist/widget/widget.d.ts +5 -0
  69. package/dist/widget/widget.d.ts.map +1 -1
  70. package/package.json +2 -2
  71. package/src/abortController.ts +35 -0
  72. package/src/constants.ts +3 -0
  73. package/src/fees.ts +199 -0
  74. package/src/prepareSend.ts +225 -398
  75. package/src/trails.ts +3 -3
  76. package/src/transactions.ts +62 -18
  77. package/src/widget/compiled.css +1 -1
  78. package/src/widget/components/AccountIntentTransactionHistoryButton.tsx +22 -0
  79. package/src/widget/components/AccountSettings.tsx +48 -36
  80. package/src/widget/components/ChainFilterDropdown.tsx +24 -3
  81. package/src/widget/components/ClassicSwap.tsx +24 -62
  82. package/src/widget/components/ConnectWallet.tsx +4 -1
  83. package/src/widget/components/ConnectedWallets.tsx +21 -21
  84. package/src/widget/components/DynamicInputStyles.tsx +76 -0
  85. package/src/widget/components/Earn.tsx +34 -29
  86. package/src/widget/components/ErrorAnimationIcon.tsx +130 -0
  87. package/src/widget/components/FeeBreakdown.tsx +155 -0
  88. package/src/widget/components/Fund.tsx +10 -26
  89. package/src/widget/components/FundSwap.tsx +2 -2
  90. package/src/widget/components/FundingMethodSelectorButton.tsx +24 -14
  91. package/src/widget/components/Identicon.tsx +164 -95
  92. package/src/widget/components/MeshConnectExchanges.tsx +2 -15
  93. package/src/widget/components/Modal.tsx +0 -12
  94. package/src/widget/components/Pay.tsx +65 -63
  95. package/src/widget/components/PoolDeposit.tsx +206 -230
  96. package/src/widget/components/PoolWithdraw.tsx +219 -238
  97. package/src/widget/components/PriceImpactWarning.tsx +1 -1
  98. package/src/widget/components/QuoteDetails.tsx +25 -8
  99. package/src/widget/components/Receipt.tsx +16 -2
  100. package/src/widget/components/RecipientSelectorButton.tsx +7 -5
  101. package/src/widget/components/Recipients.tsx +1 -1
  102. package/src/widget/components/ScreenHeader.tsx +60 -36
  103. package/src/widget/components/Swap.tsx +2 -2
  104. package/src/widget/components/ThemeProvider.tsx +2 -1
  105. package/src/widget/components/TokenDisplayNonSelectable.tsx +40 -0
  106. package/src/widget/components/TokenImage.tsx +1 -1
  107. package/src/widget/components/TokenSelector.tsx +62 -53
  108. package/src/widget/components/TokenSelectorButton.tsx +38 -15
  109. package/src/widget/components/Tooltip.tsx +51 -0
  110. package/src/widget/components/TransferPendingVertical.tsx +1 -1
  111. package/src/widget/components/WaasFeeOptions.tsx +124 -5
  112. package/src/widget/components/WalletConfirmation.tsx +23 -13
  113. package/src/widget/components/WalletConnect.tsx +93 -29
  114. package/src/widget/hooks/useQuote.ts +413 -0
  115. package/src/widget/hooks/useSendForm.ts +8 -4
  116. package/src/widget/widget.tsx +175 -190
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from "react"
2
2
  import { useWaasFeeOptions } from "@0xsequence/connect"
3
3
  import { TokenImage } from "./TokenImage.js"
4
- import { ChevronDown, ChevronUp } from "lucide-react"
4
+ import { ChevronDown, ChevronUp, Copy } from "lucide-react"
5
5
  import { formatUnits } from "viem"
6
6
 
7
7
  // Define types based on the @0xsequence/connect documentation
@@ -29,12 +29,14 @@ interface WaasFeeOptionsProps {
29
29
  chainId?: number
30
30
  setIsFeeOptionConfirmed: (isFeeOptionConfirmed: boolean) => void
31
31
  onFeeOptionsLoaded?: () => void
32
+ setIsError?: (isError: boolean) => void
32
33
  }
33
34
 
34
35
  export const WaasFeeOptions: React.FC<WaasFeeOptionsProps> = ({
35
36
  chainId,
36
37
  setIsFeeOptionConfirmed,
37
38
  onFeeOptionsLoaded,
39
+ setIsError,
38
40
  }) => {
39
41
  const [
40
42
  pendingFeeOptionConfirmation,
@@ -47,6 +49,7 @@ export const WaasFeeOptions: React.FC<WaasFeeOptionsProps> = ({
47
49
  const [isExpanded, setIsExpanded] = useState(false)
48
50
  const [isProcessing, setIsProcessing] = useState(false)
49
51
  const [isLoading, setIsLoading] = useState(false)
52
+ const [copiedAddress, setCopiedAddress] = useState<string | null>(null)
50
53
 
51
54
  // Debug logging
52
55
  useEffect(() => {
@@ -88,6 +91,7 @@ export const WaasFeeOptions: React.FC<WaasFeeOptionsProps> = ({
88
91
 
89
92
  // Initialize with first option when fee options become available
90
93
  useEffect(() => {
94
+ setIsError?.(false)
91
95
  if (pendingFeeOptionConfirmation) {
92
96
  console.log(
93
97
  "[trails-sdk] Pending fee options: ",
@@ -97,7 +101,10 @@ export const WaasFeeOptions: React.FC<WaasFeeOptionsProps> = ({
97
101
  // Notify parent that fee options are loaded
98
102
  if (
99
103
  onFeeOptionsLoaded &&
100
- pendingFeeOptionConfirmation.options?.length > 0
104
+ pendingFeeOptionConfirmation.options?.length > 0 &&
105
+ pendingFeeOptionConfirmation.options.find(
106
+ (option: any) => option.hasEnoughBalanceForFee,
107
+ )
101
108
  ) {
102
109
  onFeeOptionsLoaded()
103
110
  }
@@ -112,7 +119,7 @@ export const WaasFeeOptions: React.FC<WaasFeeOptionsProps> = ({
112
119
  }
113
120
  }
114
121
  }
115
- }, [pendingFeeOptionConfirmation, onFeeOptionsLoaded])
122
+ }, [pendingFeeOptionConfirmation, onFeeOptionsLoaded, setIsError])
116
123
 
117
124
  // Handle fee option selection and confirmation
118
125
  const handleConfirmFee = async (tokenAddress: string | null) => {
@@ -139,6 +146,30 @@ export const WaasFeeOptions: React.FC<WaasFeeOptionsProps> = ({
139
146
  }
140
147
  }
141
148
 
149
+ // Handle copy address to clipboard
150
+ const handleCopyAddress = async (address: string, e: React.MouseEvent) => {
151
+ e.preventDefault()
152
+ e.stopPropagation()
153
+ try {
154
+ await navigator.clipboard.writeText(address)
155
+ setCopiedAddress(address)
156
+ setTimeout(() => setCopiedAddress(null), 2000)
157
+ } catch (error) {
158
+ try {
159
+ const textArea = document.createElement("textarea")
160
+ textArea.value = address
161
+ document.body.appendChild(textArea)
162
+ textArea.select()
163
+ document.execCommand("copy")
164
+ document.body.removeChild(textArea)
165
+ setCopiedAddress(address)
166
+ setTimeout(() => setCopiedAddress(null), 2000)
167
+ } catch (fallbackError) {
168
+ console.error("Fallback copy failed:", fallbackError)
169
+ }
170
+ }
171
+ }
172
+
142
173
  // Don't render if no pending fee confirmation
143
174
  if (!pendingFeeOptionConfirmation) {
144
175
  return null
@@ -162,6 +193,94 @@ export const WaasFeeOptions: React.FC<WaasFeeOptionsProps> = ({
162
193
  | FeeOptionExtended
163
194
  | undefined
164
195
 
196
+ const hasTokenForFee = pendingFeeOptionConfirmation.options.find(
197
+ (option: any) => option.hasEnoughBalanceForFee,
198
+ )
199
+
200
+ if (!hasTokenForFee) {
201
+ setIsError?.(true)
202
+ }
203
+
204
+ if (!hasTokenForFee) {
205
+ return (
206
+ <div className="space-y-4 py-6">
207
+ <div className="text-center space-y-1">
208
+ <p className="text-xs trails-text-muted">
209
+ Fund your wallet with one of the following tokens
210
+ </p>
211
+ </div>
212
+
213
+ <div className="flex flex-col space-y-2">
214
+ {pendingFeeOptionConfirmation.options.map((option: any) => {
215
+ const contractAddress = option.token.contractAddress
216
+
217
+ return (
218
+ <div
219
+ key={option.token.symbol || contractAddress}
220
+ className="flex items-center space-x-3 p-3 trails-bg-secondary trails-border-radius-container border border-transparent"
221
+ >
222
+ <div className="flex items-center shrink-0">
223
+ <TokenImage
224
+ symbol={option.token.symbol}
225
+ imageUrl={(option.token as any).logoURL}
226
+ chainId={chainId}
227
+ size={32}
228
+ />
229
+ </div>
230
+ <div className="flex-1 min-w-0 flex flex-col items-start">
231
+ <span className="text-sm font-bold trails-text-primary leading-tight">
232
+ {option.token.symbol}
233
+ </span>
234
+ {contractAddress ? (
235
+ <div className="flex items-center space-x-1.5 mt-0.5">
236
+ <span className="text-xs trails-text-muted leading-tight">
237
+ {contractAddress.slice(0, 4)}...
238
+ {contractAddress.slice(-4)}
239
+ </span>
240
+ <button
241
+ type="button"
242
+ onClick={(e) => handleCopyAddress(contractAddress, e)}
243
+ className="shrink-0 p-0.5 rounded transition-colors cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center"
244
+ title={
245
+ copiedAddress === contractAddress
246
+ ? "Copied!"
247
+ : "Copy address"
248
+ }
249
+ >
250
+ {copiedAddress === contractAddress ? (
251
+ <svg
252
+ className="w-3 h-3 text-green-600 dark:text-green-400"
253
+ fill="none"
254
+ viewBox="0 0 24 24"
255
+ stroke="currentColor"
256
+ aria-label="Copied"
257
+ >
258
+ <path
259
+ strokeLinecap="round"
260
+ strokeLinejoin="round"
261
+ strokeWidth={2}
262
+ d="M5 13l4 4L19 7"
263
+ />
264
+ </svg>
265
+ ) : (
266
+ <Copy className="w-3 h-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" />
267
+ )}
268
+ </button>
269
+ </div>
270
+ ) : (
271
+ <span className="text-xs trails-text-muted mt-0.5 leading-tight">
272
+ Native Token
273
+ </span>
274
+ )}
275
+ </div>
276
+ </div>
277
+ )
278
+ })}
279
+ </div>
280
+ </div>
281
+ )
282
+ }
283
+
165
284
  return (
166
285
  <div className="space-y-2">
167
286
  {/* Header */}
@@ -208,7 +327,7 @@ export const WaasFeeOptions: React.FC<WaasFeeOptionsProps> = ({
208
327
  {/* Display balance info if available */}
209
328
  {selectedOption && "balanceFormatted" in selectedOption && (
210
329
  <div className="text-right">
211
- <div className="text-xs trails-text-muted">
330
+ <div className="text-xs text-gray-500 dark:text-gray-400">
212
331
  Balance: {String(selectedOption.balanceFormatted)}
213
332
  </div>
214
333
  {!selectedOption.hasEnoughBalanceForFee && (
@@ -266,7 +385,7 @@ export const WaasFeeOptions: React.FC<WaasFeeOptionsProps> = ({
266
385
  {/* Display balance info if available */}
267
386
  {"balanceFormatted" in option && (
268
387
  <div className="text-right">
269
- <div className="text-xs trails-text-muted">
388
+ <div className="text-xs text-gray-500 dark:text-gray-400">
270
389
  Balance: {String(option.balanceFormatted)}
271
390
  </div>
272
391
  <div className="text-xs trails-text-muted">
@@ -1,4 +1,5 @@
1
1
  import { TokenImage } from "./TokenImage.js"
2
+ import { ErrorAnimationIcon } from "./ErrorAnimationIcon.js"
2
3
  import { ChevronLeft } from "lucide-react"
3
4
  import type React from "react"
4
5
  import { useEffect, useMemo, useState } from "react"
@@ -25,9 +26,11 @@ export const WalletConfirmation: React.FC<WalletConfirmationProps> = ({
25
26
  const [showTimeoutWarning, setShowTimeoutWarning] = useState(false)
26
27
  const [isFeeOptionConfirmed, setIsFeeOptionConfirmed] = useState(false)
27
28
  const [areFeeOptionsLoaded, setAreFeeOptionsLoaded] = useState(false)
29
+ const [feeOptionsError, setFeeOptionsError] = useState<boolean>(false)
28
30
  const { connector } = useAccount()
29
31
 
30
32
  useEffect(() => {
33
+ setAreFeeOptionsLoaded(false)
31
34
  setShowContent(true)
32
35
  }, [])
33
36
 
@@ -80,21 +83,25 @@ export const WalletConfirmation: React.FC<WalletConfirmationProps> = ({
80
83
  <div
81
84
  className={`mx-auto flex items-center justify-center transition-all duration-500 ease-out relative mb-4 ${showContent ? "transform -translate-y-8" : ""}`}
82
85
  >
83
- {retryEnabled ? (
86
+ {feeOptionsError === true ? (
87
+ <ErrorAnimationIcon />
88
+ ) : retryEnabled ? (
84
89
  <div className={`h-24 w-24`} />
85
90
  ) : (
86
91
  <div className="animate-spin rounded-full h-24 w-24 border-solid border-b-2 border-blue-500 dark:border-blue-400" />
87
92
  )}
88
93
 
89
- <div className="absolute">
90
- <TokenImage
91
- imageUrl={quote?.originToken.imageUrl}
92
- symbol={quote?.originToken.symbol}
93
- chainId={quote?.originChain.id}
94
- contractAddress={quote?.originToken.contractAddress}
95
- size={64}
96
- />
97
- </div>
94
+ {!feeOptionsError && (
95
+ <div className="absolute">
96
+ <TokenImage
97
+ imageUrl={quote?.originToken.imageUrl}
98
+ symbol={quote?.originToken.symbol}
99
+ chainId={quote?.originChain.id}
100
+ contractAddress={quote?.originToken.contractAddress}
101
+ size={64}
102
+ />
103
+ </div>
104
+ )}
98
105
  </div>
99
106
 
100
107
  <div
@@ -109,7 +116,9 @@ export const WalletConfirmation: React.FC<WalletConfirmationProps> = ({
109
116
  ? "Waiting for wallet…"
110
117
  : areFeeOptionsLoaded
111
118
  ? "Waiting for fee selection…"
112
- : "Waiting for fee options…"}
119
+ : feeOptionsError
120
+ ? "No fee token found"
121
+ : "Waiting for fee options…"}
113
122
  </h2>
114
123
  <p className="mt-2 text-sm text-gray-600 dark:text-gray-300">
115
124
  {!isSequenceWaas
@@ -127,6 +136,7 @@ export const WalletConfirmation: React.FC<WalletConfirmationProps> = ({
127
136
  <WaasFeeOptions
128
137
  chainId={quote?.originChain.id}
129
138
  setIsFeeOptionConfirmed={setIsFeeOptionConfirmed}
139
+ setIsError={setFeeOptionsError}
130
140
  onFeeOptionsLoaded={() => {
131
141
  console.log("[trails-sdk] Fee options loaded callback called")
132
142
  setAreFeeOptionsLoaded(true)
@@ -144,7 +154,7 @@ export const WalletConfirmation: React.FC<WalletConfirmationProps> = ({
144
154
  <div className="p-4 trails-border-radius-container text-sm bg-yellow-50 border border-solid border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-700/50">
145
155
  <div className="flex items-start space-x-3">
146
156
  <svg
147
- className="w-5 h-5 mt-0.5 flex-shrink-0 text-yellow-600 dark:text-yellow-400"
157
+ className="w-5 h-5 mt-0.5 shrink-0 text-yellow-600 dark:text-yellow-400"
148
158
  fill="none"
149
159
  viewBox="0 0 24 24"
150
160
  stroke="currentColor"
@@ -187,7 +197,7 @@ export const WalletConfirmation: React.FC<WalletConfirmationProps> = ({
187
197
  )}
188
198
 
189
199
  {/* Transaction Details */}
190
- <QuoteDetails quote={quote} showContent={true} />
200
+ <QuoteDetails quote={quote} showContent={true} compact={true} />
191
201
  </div>
192
202
  </div>
193
203
  )
@@ -44,6 +44,7 @@ export const WalletConnectScreen: React.FC<WalletConnectProps> = ({
44
44
  const { isConnected, address, connector } = useAccount()
45
45
  const [wcUri, setWcUri] = React.useState<string | null>(null)
46
46
  const [showUri, setShowUri] = React.useState(false)
47
+ const [copied, setCopied] = React.useState(false)
47
48
  const listenerRef = React.useRef<(() => void) | null>(null)
48
49
  const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null)
49
50
 
@@ -57,6 +58,21 @@ export const WalletConnectScreen: React.FC<WalletConnectProps> = ({
57
58
  setWcUri(uri)
58
59
  }, 500) // 300ms debounce delay
59
60
  }, [])
61
+
62
+ // Copy URI to clipboard
63
+ const handleCopyUri = useCallback(async () => {
64
+ if (!wcUri) return
65
+ try {
66
+ await navigator.clipboard.writeText(wcUri)
67
+ setCopied(true)
68
+ setTimeout(() => setCopied(false), 2000)
69
+ } catch (err) {
70
+ logger.console.error(
71
+ "[trails-sdk] [WalletConnect] Failed to copy URI",
72
+ err,
73
+ )
74
+ }
75
+ }, [wcUri])
60
76
  const isWalletConnectConnector = React.useMemo(() => {
61
77
  return connector?.name === "WalletConnect" && isConnected
62
78
  }, [connector, isConnected])
@@ -68,18 +84,13 @@ export const WalletConnectScreen: React.FC<WalletConnectProps> = ({
68
84
  (c.id || "").toLowerCase().includes("walletconnect"),
69
85
  )
70
86
 
71
- // Only auto-navigate back if we successfully connect a new wallet
87
+ // Only auto-navigate to home if we successfully connect a new wallet
72
88
  // Don't auto-navigate if user was already connected when they entered this screen
73
89
  useEffect(() => {
74
- if (
75
- isConnected &&
76
- status === "success" &&
77
- !wasConnectedOnMount.current &&
78
- onBack
79
- ) {
80
- onBack()
90
+ if (isConnected && status === "success" && !wasConnectedOnMount.current) {
91
+ onContinue()
81
92
  }
82
- }, [isConnected, status, onBack])
93
+ }, [isConnected, status, onContinue])
83
94
 
84
95
  const handleConnect = useCallback(
85
96
  async (force: boolean = false) => {
@@ -306,29 +317,82 @@ export const WalletConnectScreen: React.FC<WalletConnectProps> = ({
306
317
  getWalletIcon("walletconnect")
307
318
  }
308
319
  />
309
- <button
310
- onClick={() => setShowUri(!showUri)}
311
- className="mt-2 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:underline cursor-pointer flex items-center gap-1"
312
- >
313
- {showUri ? "Hide URI" : "Show URI"}
314
- <svg
315
- className={`w-3 h-3 transition-transform ${showUri ? "rotate-180" : ""}`}
316
- fill="none"
317
- stroke="currentColor"
318
- viewBox="0 0 24 24"
320
+ <div className="flex items-center justify-center gap-2 mt-2">
321
+ <button
322
+ type="button"
323
+ onClick={handleCopyUri}
324
+ className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 cursor-pointer flex items-center gap-1 hover:underline"
325
+ title="Copy URI to clipboard"
319
326
  >
320
- <path
321
- strokeLinecap="round"
322
- strokeLinejoin="round"
323
- strokeWidth={2}
324
- d="M19 9l-7 7-7-7"
325
- />
326
- </svg>
327
- </button>
327
+ {copied ? (
328
+ <>
329
+ <svg
330
+ className="w-3 h-3"
331
+ fill="none"
332
+ stroke="currentColor"
333
+ viewBox="0 0 24 24"
334
+ aria-hidden="true"
335
+ >
336
+ <path
337
+ strokeLinecap="round"
338
+ strokeLinejoin="round"
339
+ strokeWidth={2}
340
+ d="M5 13l4 4L19 7"
341
+ />
342
+ </svg>
343
+ Copied!
344
+ </>
345
+ ) : (
346
+ <>
347
+ <svg
348
+ className="w-3 h-3"
349
+ fill="none"
350
+ stroke="currentColor"
351
+ viewBox="0 0 24 24"
352
+ aria-hidden="true"
353
+ >
354
+ <path
355
+ strokeLinecap="round"
356
+ strokeLinejoin="round"
357
+ strokeWidth={2}
358
+ d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
359
+ />
360
+ </svg>
361
+ Copy URI
362
+ </>
363
+ )}
364
+ </button>
365
+ <button
366
+ type="button"
367
+ onClick={() => setShowUri(!showUri)}
368
+ className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:underline cursor-pointer flex items-center gap-1"
369
+ >
370
+ {showUri ? "Hide URI" : "Show URI"}
371
+ <svg
372
+ className={`w-3 h-3 transition-transform ${showUri ? "rotate-180" : ""}`}
373
+ fill="none"
374
+ stroke="currentColor"
375
+ viewBox="0 0 24 24"
376
+ aria-hidden="true"
377
+ >
378
+ <path
379
+ strokeLinecap="round"
380
+ strokeLinejoin="round"
381
+ strokeWidth={2}
382
+ d="M19 9l-7 7-7-7"
383
+ />
384
+ </svg>
385
+ </button>
386
+ </div>
328
387
  {showUri && (
329
- <p className="mt-2 text-xs text-gray-600 dark:text-gray-400 break-all">
388
+ <button
389
+ type="button"
390
+ className="mt-2 text-xs text-gray-600 dark:text-gray-400 break-all cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 p-2 rounded transition-colors text-left w-full"
391
+ onClick={handleCopyUri}
392
+ title="Click to copy URI"
393
+ >
330
394
  {wcUri}
331
- </p>
395
+ </button>
332
396
  )}
333
397
  </div>
334
398
  ) : (