0xtrails 0.2.4 → 0.2.5

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 (161) hide show
  1. package/dist/aave.d.ts +8 -0
  2. package/dist/aave.d.ts.map +1 -1
  3. package/dist/{ccip-BlV1Mry3.js → ccip-CXlshvBY.js} +1 -1
  4. package/dist/config.d.ts +1 -1
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/error.d.ts +1 -0
  9. package/dist/error.d.ts.map +1 -1
  10. package/dist/estimate.d.ts +52 -0
  11. package/dist/estimate.d.ts.map +1 -1
  12. package/dist/{index-BNWCIGfQ.js → index-_QuyGrjU.js} +72332 -72246
  13. package/dist/index.js +2 -2
  14. package/dist/intents.d.ts +40 -0
  15. package/dist/intents.d.ts.map +1 -1
  16. package/dist/metaTxnMonitor.d.ts +3 -3
  17. package/dist/metaTxnMonitor.d.ts.map +1 -1
  18. package/dist/metaTxns.d.ts +3 -3
  19. package/dist/metaTxns.d.ts.map +1 -1
  20. package/dist/morpho.d.ts +8 -0
  21. package/dist/morpho.d.ts.map +1 -1
  22. package/dist/prepareSend.d.ts +16 -6
  23. package/dist/prepareSend.d.ts.map +1 -1
  24. package/dist/queryParams.d.ts.map +1 -1
  25. package/dist/relayer.d.ts +6 -6
  26. package/dist/relayer.d.ts.map +1 -1
  27. package/dist/sequenceWallet.d.ts +2 -2
  28. package/dist/sequenceWallet.d.ts.map +1 -1
  29. package/dist/tokens.d.ts.map +1 -1
  30. package/dist/wallets.d.ts.map +1 -1
  31. package/dist/widget/components/AccountActionsDropdown.d.ts.map +1 -1
  32. package/dist/widget/components/AccountSettings.d.ts.map +1 -1
  33. package/dist/widget/components/ClassicSwap.d.ts +2 -0
  34. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  35. package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
  36. package/dist/widget/components/ConnectedWallets.d.ts +4 -0
  37. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -1
  38. package/dist/widget/components/Earn.d.ts.map +1 -1
  39. package/dist/widget/components/Fund.d.ts.map +1 -1
  40. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  41. package/dist/widget/components/{FundSendForm.d.ts → FundSwap.d.ts} +11 -5
  42. package/dist/widget/components/FundSwap.d.ts.map +1 -0
  43. package/dist/widget/components/FundingMethodSelectorButton.d.ts +4 -0
  44. package/dist/widget/components/FundingMethodSelectorButton.d.ts.map +1 -0
  45. package/dist/widget/components/Modal.d.ts.map +1 -1
  46. package/dist/widget/components/Pay.d.ts.map +1 -1
  47. package/dist/widget/components/PercentageMaxButtons.d.ts +12 -0
  48. package/dist/widget/components/PercentageMaxButtons.d.ts.map +1 -0
  49. package/dist/widget/components/{PaySendForm.d.ts → PoolDeposit.d.ts} +11 -34
  50. package/dist/widget/components/PoolDeposit.d.ts.map +1 -0
  51. package/dist/widget/components/{SimpleSwap.d.ts → PoolWithdraw.d.ts} +16 -8
  52. package/dist/widget/components/PoolWithdraw.d.ts.map +1 -0
  53. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  54. package/dist/widget/components/Receive.d.ts.map +1 -1
  55. package/dist/widget/components/RecipientSelectorButton.d.ts +4 -0
  56. package/dist/widget/components/RecipientSelectorButton.d.ts.map +1 -0
  57. package/dist/widget/components/Recipients.d.ts.map +1 -1
  58. package/dist/widget/components/RequiredPropsError.d.ts +8 -0
  59. package/dist/widget/components/RequiredPropsError.d.ts.map +1 -0
  60. package/dist/widget/components/ScreenHeader.d.ts.map +1 -1
  61. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  62. package/dist/widget/components/Swap.d.ts +1 -0
  63. package/dist/widget/components/Swap.d.ts.map +1 -1
  64. package/dist/widget/components/SwapSettings.d.ts.map +1 -1
  65. package/dist/widget/components/TokenImage.d.ts +1 -0
  66. package/dist/widget/components/TokenImage.d.ts.map +1 -1
  67. package/dist/widget/components/TokenList.d.ts.map +1 -1
  68. package/dist/widget/components/TokenSelector.d.ts.map +1 -1
  69. package/dist/widget/components/TokenSelectorButton.d.ts +16 -0
  70. package/dist/widget/components/TokenSelectorButton.d.ts.map +1 -0
  71. package/dist/widget/components/UserPreferences.d.ts.map +1 -1
  72. package/dist/widget/components/WaasFeeOptions.d.ts +8 -0
  73. package/dist/widget/components/WaasFeeOptions.d.ts.map +1 -0
  74. package/dist/widget/components/WalletConfirmation.d.ts.map +1 -1
  75. package/dist/widget/components/WalletList.d.ts.map +1 -1
  76. package/dist/widget/css/compiled.css +2 -0
  77. package/dist/widget/css/index.css +554 -0
  78. package/dist/widget/hooks/useBack.d.ts +1 -0
  79. package/dist/widget/hooks/useBack.d.ts.map +1 -1
  80. package/dist/widget/hooks/useCheckout.d.ts +1 -1
  81. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  82. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  83. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  84. package/dist/widget/hooks/useDefaultTokenSelection.d.ts +3 -3
  85. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -1
  86. package/dist/widget/hooks/usePayMessage.d.ts.map +1 -1
  87. package/dist/widget/hooks/useSelectedFundMethod.d.ts +12 -0
  88. package/dist/widget/hooks/useSelectedFundMethod.d.ts.map +1 -0
  89. package/dist/widget/hooks/useSelectedRecipient.d.ts.map +1 -1
  90. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  91. package/dist/widget/index.js +1 -1
  92. package/dist/widget/widget.d.ts +4 -4
  93. package/dist/widget/widget.d.ts.map +1 -1
  94. package/package.json +18 -12
  95. package/src/aave.ts +32 -0
  96. package/src/config.ts +12 -4
  97. package/src/constants.ts +2 -0
  98. package/src/error.ts +19 -1
  99. package/src/estimate.ts +416 -5
  100. package/src/intents.ts +161 -11
  101. package/src/metaTxnMonitor.ts +3 -3
  102. package/src/metaTxns.ts +3 -5
  103. package/src/morpho.ts +32 -0
  104. package/src/prepareSend.ts +503 -166
  105. package/src/queryParams.ts +2 -1
  106. package/src/relayer.ts +11 -11
  107. package/src/sequenceWallet.ts +2 -2
  108. package/src/tokens.ts +7 -1
  109. package/src/wallets.ts +8 -0
  110. package/src/widget/compiled.css +2 -2
  111. package/src/widget/components/AccountActionsDropdown.tsx +3 -13
  112. package/src/widget/components/AccountSettings.tsx +6 -24
  113. package/src/widget/components/ClassicSwap.tsx +111 -155
  114. package/src/widget/components/ConnectWallet.tsx +4 -37
  115. package/src/widget/components/ConnectedWallets.tsx +113 -58
  116. package/src/widget/components/Earn.tsx +73 -589
  117. package/src/widget/components/Fund.tsx +31 -82
  118. package/src/widget/components/FundMethods.tsx +82 -159
  119. package/src/widget/components/FundSwap.tsx +52 -0
  120. package/src/widget/components/FundingMethodSelectorButton.tsx +60 -0
  121. package/src/widget/components/Modal.tsx +6 -2
  122. package/src/widget/components/Pay.tsx +183 -208
  123. package/src/widget/components/PercentageMaxButtons.tsx +77 -0
  124. package/src/widget/components/PoolDeposit.tsx +593 -0
  125. package/src/widget/components/PoolWithdraw.tsx +903 -0
  126. package/src/widget/components/QuoteDetails.tsx +22 -8
  127. package/src/widget/components/Receive.tsx +0 -2
  128. package/src/widget/components/RecipientSelectorButton.tsx +42 -0
  129. package/src/widget/components/Recipients.tsx +62 -156
  130. package/src/widget/components/RequiredPropsError.tsx +33 -0
  131. package/src/widget/components/ScreenHeader.tsx +5 -1
  132. package/src/widget/components/SlippageToleranceSettings.tsx +2 -1
  133. package/src/widget/components/Swap.tsx +2 -43
  134. package/src/widget/components/SwapSettings.tsx +2 -14
  135. package/src/widget/components/TokenImage.tsx +21 -4
  136. package/src/widget/components/TokenList.tsx +0 -1
  137. package/src/widget/components/TokenSelector.tsx +1 -0
  138. package/src/widget/components/TokenSelectorButton.tsx +75 -0
  139. package/src/widget/components/UserPreferences.tsx +6 -24
  140. package/src/widget/components/WaasFeeOptions.tsx +331 -0
  141. package/src/widget/components/WalletConfirmation.tsx +55 -3
  142. package/src/widget/components/WalletList.tsx +4 -2
  143. package/src/widget/hooks/useBack.tsx +2 -0
  144. package/src/widget/hooks/useCheckout.ts +36 -20
  145. package/src/widget/hooks/useCurrentScreen.tsx +1 -0
  146. package/src/widget/hooks/useDefaultTokenSelection.tsx +104 -28
  147. package/src/widget/hooks/usePayMessage.tsx +86 -11
  148. package/src/widget/hooks/useSelectedFundMethod.tsx +41 -0
  149. package/src/widget/hooks/useSelectedRecipient.tsx +10 -0
  150. package/src/widget/hooks/useSendForm.ts +24 -2
  151. package/src/widget/index.css +27 -0
  152. package/src/widget/widget.tsx +169 -111
  153. package/dist/widget/components/FundSendForm.d.ts.map +0 -1
  154. package/dist/widget/components/PaySendForm.d.ts.map +0 -1
  155. package/dist/widget/components/SimpleSwap.d.ts.map +0 -1
  156. package/dist/widget/hooks/useSwapSettings.d.ts +0 -16
  157. package/dist/widget/hooks/useSwapSettings.d.ts.map +0 -1
  158. package/src/widget/components/FundSendForm.tsx +0 -903
  159. package/src/widget/components/PaySendForm.tsx +0 -869
  160. package/src/widget/components/SimpleSwap.tsx +0 -983
  161. package/src/widget/hooks/useSwapSettings.tsx +0 -100
@@ -1,7 +1,8 @@
1
- import { ChevronRight, Search, Loader2, ChevronDown } from "lucide-react"
1
+ import { Search, Loader2, ChevronRight } from "lucide-react"
2
2
  import { useEffect, useState, useMemo, useRef, useCallback } from "react"
3
3
  import type React from "react"
4
4
  import type { Account, WalletClient } from "viem"
5
+ import { zeroAddress } from "viem"
5
6
  import type { TransactionState } from "../../transactions.js"
6
7
  import type { OnCompleteProps } from "../hooks/useSendForm.js"
7
8
  import type { CheckoutOnHandlers } from "../hooks/useCheckout.js"
@@ -31,6 +32,10 @@ import type { PrepareSendQuote } from "../../prepareSend.js"
31
32
  import type { SupportedToken } from "../../tokens.js"
32
33
  import { logger } from "../../logger.js"
33
34
  import { RefundWarning } from "./RefundWarning.js"
35
+ import { TokenSelectorButton } from "./TokenSelectorButton.js"
36
+ import { RequiredPropsError } from "./RequiredPropsError.js"
37
+ import { FundingMethodSelectorButton } from "./FundingMethodSelectorButton.js"
38
+ import { PercentageMaxButtons } from "./PercentageMaxButtons.js"
34
39
 
35
40
  interface PayProps {
36
41
  selectedToken?: any // Origin token (optional - user can select)
@@ -328,10 +333,12 @@ export const Pay: React.FC<PayProps> = ({
328
333
  const destTokenToUse = globalDestinationToken || defaultDestinationToken
329
334
 
330
335
  if (destTokenToUse && !isLoadingDefaults) {
331
- logger.console.log(
332
- "[trails-sdk] Initializing destination token:",
333
- destTokenToUse,
334
- )
336
+ logger.console.log("[trails-sdk] Initializing destination token:", {
337
+ symbol: destTokenToUse.symbol,
338
+ chainId: destTokenToUse.chainId,
339
+ name: destTokenToUse.name,
340
+ contractAddress: destTokenToUse.contractAddress,
341
+ })
335
342
 
336
343
  // Set destination token if not already set by global state
337
344
  if (!globalDestinationToken && defaultDestinationToken) {
@@ -417,21 +424,21 @@ export const Pay: React.FC<PayProps> = ({
417
424
  return inputDisplayValue
418
425
  }, [inputDisplayValue])
419
426
 
420
- // Dynamic font size based on input length
427
+ // Dynamic font size based on input length for destination amount
421
428
  const inputStyles = useMemo(() => {
422
429
  const inputLength = displayAmount.length
423
430
  let fontSize: string
424
431
 
425
432
  if (inputLength > 12) {
426
- fontSize = "0.875rem"
427
- } else if (inputLength > 9) {
428
433
  fontSize = "1rem"
434
+ } else if (inputLength > 9) {
435
+ fontSize = "1.25rem"
429
436
  } else if (inputLength > 6) {
430
- fontSize = "1.125rem"
437
+ fontSize = "1.5rem"
431
438
  } else if (inputLength > 3) {
432
- fontSize = "1.25rem"
439
+ fontSize = "1.75rem"
433
440
  } else {
434
- fontSize = "1.5rem"
441
+ fontSize = "2rem"
435
442
  }
436
443
 
437
444
  return {
@@ -440,6 +447,29 @@ export const Pay: React.FC<PayProps> = ({
440
447
  }
441
448
  }, [displayAmount.length])
442
449
 
450
+ // Dynamic font size based on input length for origin amount (Pay with section)
451
+ const originInputStyles = useMemo(() => {
452
+ const inputLength = (prepareSendQuote?.originAmountFormatted || "").length
453
+ let fontSize: string
454
+
455
+ if (inputLength > 12) {
456
+ fontSize = "1rem"
457
+ } else if (inputLength > 9) {
458
+ fontSize = "1.25rem"
459
+ } else if (inputLength > 6) {
460
+ fontSize = "1.5rem"
461
+ } else if (inputLength > 3) {
462
+ fontSize = "1.75rem"
463
+ } else {
464
+ fontSize = "2rem"
465
+ }
466
+
467
+ return {
468
+ fontSize,
469
+ transition: "all 0.1s ease-in-out",
470
+ }
471
+ }, [prepareSendQuote?.originAmountFormatted])
472
+
443
473
  const handleOriginTokenSelect = useCallback(
444
474
  (token: any) => {
445
475
  const formattedToken = {
@@ -533,7 +563,6 @@ export const Pay: React.FC<PayProps> = ({
533
563
  onBack={() => setShowOriginTokenSelector(false)}
534
564
  headerContent="Select Token"
535
565
  headerContentAlign="left"
536
- showAccountActions={true}
537
566
  />
538
567
  <TokenSelector
539
568
  onTokenSelect={handleOriginTokenSelect}
@@ -580,7 +609,6 @@ export const Pay: React.FC<PayProps> = ({
580
609
  onBack={() => setShowDestinationTokenSelector(false)}
581
610
  headerContent="Select Token"
582
611
  headerContentAlign="left"
583
- showAccountActions={true}
584
612
  />
585
613
  <TokenSelector
586
614
  onTokenSelect={handleDestinationTokenSelect}
@@ -617,11 +645,35 @@ export const Pay: React.FC<PayProps> = ({
617
645
  )
618
646
  }
619
647
 
648
+ // Check for required props
649
+ const missingRequiredProps = []
650
+ if (!toAmount) missingRequiredProps.push("toAmount")
651
+ if (!toToken) missingRequiredProps.push("toToken")
652
+ if (!toRecipient) missingRequiredProps.push("toRecipient")
653
+
620
654
  // Check if this is a payment request (all required props are set)
621
655
  const isPaymentRequest = !!(toToken && toAmount && toChainId && toRecipient)
622
656
 
657
+ // If required props are missing, only show the error
658
+ if (missingRequiredProps.length > 0) {
659
+ return (
660
+ <div className="space-y-4">
661
+ <ScreenHeader
662
+ onBack={onBack}
663
+ headerContent="Pay"
664
+ headerContentAlign="left"
665
+ showAccountActions={true}
666
+ />
667
+ <RequiredPropsError
668
+ missingProps={missingRequiredProps}
669
+ componentName="Pay"
670
+ />
671
+ </div>
672
+ )
673
+ }
674
+
623
675
  return (
624
- <div className="space-y-4">
676
+ <div className="space-y-2">
625
677
  <ScreenHeader
626
678
  onBack={onBack}
627
679
  headerContent="Pay"
@@ -635,7 +687,7 @@ export const Pay: React.FC<PayProps> = ({
635
687
  {/* Payment Request Header */}
636
688
  <div className="space-y-1 trails-bg-secondary trails-border-radius-container p-3">
637
689
  <div className="flex justify-start">
638
- <div className="flex items-center font-medium trails-text-primary text-sm whitespace-nowrap overflow-hidden">
690
+ <div className="flex items-center font-medium trails-text-primary text-sm whitespace-nowrap overflow-hidden min-w-full">
639
691
  {payMessage}
640
692
  </div>
641
693
  </div>
@@ -643,10 +695,10 @@ export const Pay: React.FC<PayProps> = ({
643
695
 
644
696
  {/* Origin Token Selection for Payment Request */}
645
697
  <div className="space-y-1">
646
- <div className="trails-bg-secondary trails-bg-secondary-hover trails-border-radius-container p-3 group transition-all duration-200 border border-transparent focus-within:!bg-white dark:focus-within:!bg-gray-800 trails-focus-border-secondary">
698
+ <div className="trails-bg-secondary trails-bg-secondary-hover trails-border-radius-container p-3 group transition-all duration-200 border border-transparent focus-within:!bg-white dark:focus-within:!bg-gray-800 trails-focus-border-secondary min-h-[120px] flex flex-col">
647
699
  {/* Amount to Pay Label */}
648
700
  <div className="flex justify-between items-center mb-2">
649
- <div className="text-sm font-bold trails-text-secondary text-left">
701
+ <div className="text-sm font-medium trails-text-secondary text-left">
650
702
  Pay with
651
703
  {fundMethod === "qr-code"
652
704
  ? " QR Code"
@@ -654,82 +706,38 @@ export const Pay: React.FC<PayProps> = ({
654
706
  ? " Exchange"
655
707
  : ""}
656
708
  </div>
709
+ <FundingMethodSelectorButton />
657
710
  </div>
658
711
 
659
- <div className="flex items-center space-x-2">
712
+ <div className="flex items-center space-x-2 flex-1">
660
713
  {/* Amount Display - Non-editable */}
661
714
  <div className="flex-1">
662
- <div
663
- className="flex items-center justify-start cursor-text"
664
- onClick={() => paymentRequestInputRef.current?.focus()}
665
- >
666
- <div className="flex items-center">
667
- <input
668
- ref={paymentRequestInputRef}
669
- type="text"
670
- value={prepareSendQuote?.originAmountFormatted || ""}
671
- readOnly={true}
672
- className={`bg-transparent border-none outline-none font-bold text-left trails-text-primary ${
673
- isLoadingQuote ? "animate-pulse" : ""
674
- }`}
675
- style={{
676
- fontSize: inputStyles.fontSize,
677
- width: prepareSendQuote?.originAmountFormatted
678
- ? `${Math.max(prepareSendQuote.originAmountFormatted.length - 1 + 0.5, 1)}ch`
679
- : "1ch",
680
- minWidth: "1ch",
681
- maxWidth: "200px",
682
- padding: "0",
683
- margin: "0",
684
- transition: "all 0.1s ease-in-out",
685
- }}
686
- />
687
- <span
688
- className="font-bold text-gray-400 dark:text-gray-500"
689
- style={{
690
- fontSize: inputStyles.fontSize,
691
- marginLeft: "0.1em",
692
- padding: "0",
693
- transition: "all 0.2s ease-in-out",
694
- }}
695
- >
696
- {originToken?.symbol || "TOKEN"}
697
- </span>
698
- {isLoadingQuote && (
699
- <div className="ml-2 animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
700
- )}
701
- </div>
715
+ <div className="flex items-center space-x-2">
716
+ <input
717
+ ref={paymentRequestInputRef}
718
+ type="text"
719
+ value={prepareSendQuote?.originAmountFormatted || ""}
720
+ placeholder={`0 ${originToken?.symbol || ""}`.trim()}
721
+ readOnly={true}
722
+ className={`w-full bg-transparent font-bold trails-text-primary border-none outline-none ${
723
+ isLoadingQuote ? "animate-pulse" : ""
724
+ }`}
725
+ style={originInputStyles}
726
+ />
727
+ {isLoadingQuote && (
728
+ <div className="animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
729
+ )}
702
730
  </div>
703
731
  </div>
704
732
 
705
- {/* Origin Token Selection Button */}
706
- <button
707
- type="button"
708
- onClick={() => setShowOriginTokenSelector(true)}
709
- className="flex items-center space-x-2 trails-bg-card hover:trails-hover-bg trails-border-radius-input px-2.5 py-1.5 border trails-border-primary transition-colors cursor-pointer"
710
- >
711
- {originToken ? (
712
- <>
713
- <TokenImage
714
- symbol={originToken.symbol}
715
- imageUrl={originToken.imageUrl}
716
- chainId={originToken.chainId}
717
- size={20}
718
- />
719
- <span className="font-medium trails-text-primary text-sm">
720
- {originToken.symbol}
721
- </span>
722
- <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
723
- </>
724
- ) : (
725
- <>
726
- <span className="font-medium trails-text-muted text-sm">
727
- Select Token
728
- </span>
729
- <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
730
- </>
731
- )}
732
- </button>
733
+ {/* Origin Token Selection */}
734
+ <div className="relative">
735
+ <TokenSelectorButton
736
+ token={originToken}
737
+ chainId={originToken?.chainId}
738
+ onSelect={() => setShowOriginTokenSelector(true)}
739
+ />
740
+ </div>
733
741
  </div>
734
742
 
735
743
  {/* Bottom Info Row */}
@@ -743,17 +751,59 @@ export const Pay: React.FC<PayProps> = ({
743
751
  <span>&nbsp;</span>
744
752
  )}
745
753
  </div>
746
- {/* Token Balance */}
747
- <div className="text-xs trails-text-muted">
748
- {originTokenBalance?.balanceFormatted ? (
749
- <>
750
- Balance: {originTokenBalance.balanceFormatted}{" "}
751
- {originToken?.symbol}
752
- </>
753
- ) : (
754
- <span>&nbsp;</span>
755
- )}
756
- </div>
754
+
755
+ {/* Origin Token Balance and Percentage Buttons */}
756
+ {originToken && originTokenBalance?.balanceFormatted && (
757
+ <div className="flex items-center space-x-2">
758
+ <button
759
+ type="button"
760
+ className="text-xs trails-text-muted cursor-pointer hover:trails-hover-text transition-colors bg-transparent border-none p-0"
761
+ onClick={() => {
762
+ if (originTokenBalance.balanceFormatted) {
763
+ const balance = parseFloat(
764
+ originTokenBalance.balanceFormatted,
765
+ )
766
+ if (!Number.isNaN(balance)) {
767
+ setTokenAmountForBackend(balance.toFixed(6))
768
+ }
769
+ }
770
+ }}
771
+ onKeyDown={(e) => {
772
+ if (e.key === "Enter" || e.key === " ") {
773
+ e.preventDefault()
774
+ if (originTokenBalance.balanceFormatted) {
775
+ const balance = parseFloat(
776
+ originTokenBalance.balanceFormatted,
777
+ )
778
+ if (!Number.isNaN(balance)) {
779
+ setTokenAmountForBackend(balance.toFixed(6))
780
+ }
781
+ }
782
+ }
783
+ }}
784
+ title="Click to use full balance"
785
+ >
786
+ Balance: {originTokenBalance.balanceFormatted}
787
+ </button>
788
+
789
+ {/* Percentage Buttons - Only show if toAmount is not set */}
790
+ {!toAmount && (
791
+ <PercentageMaxButtons
792
+ userBalance={originTokenBalance.balanceFormatted}
793
+ isNativeToken={
794
+ originToken.contractAddress === zeroAddress ||
795
+ originToken.contractAddress === undefined
796
+ }
797
+ gasCostFormatted={prepareSendQuote?.gasCostFormatted}
798
+ chainId={originToken.chainId}
799
+ onAmountSelect={(amount) => {
800
+ setTokenAmountForBackend(amount)
801
+ }}
802
+ className="opacity-100"
803
+ />
804
+ )}
805
+ </div>
806
+ )}
757
807
  </div>
758
808
  </div>
759
809
  </div>
@@ -839,12 +889,7 @@ export const Pay: React.FC<PayProps> = ({
839
889
  >
840
890
  <div className="text-left">
841
891
  <div className="font-medium trails-text-primary text-sm">
842
- Pay with
843
- {fundMethod === "qr-code"
844
- ? " QR Code"
845
- : fundMethod === "exchange"
846
- ? " Exchange"
847
- : ""}
892
+ Payment method
848
893
  </div>
849
894
  </div>
850
895
 
@@ -888,113 +933,46 @@ export const Pay: React.FC<PayProps> = ({
888
933
 
889
934
  <div className="space-y-1">
890
935
  {/* Destination Amount Input Section - Like Fund.tsx but for destination token */}
891
- <div className="trails-bg-secondary trails-bg-secondary-hover trails-border-radius-container p-3 group transition-all duration-200 border border-transparent focus-within:!bg-white dark:focus-within:!bg-gray-800 trails-focus-border-secondary">
936
+ <div className="trails-bg-secondary trails-bg-secondary-hover trails-border-radius-container p-3 group transition-all duration-200 border border-transparent focus-within:!bg-white dark:focus-within:!bg-gray-800 trails-focus-border-secondary min-h-[120px] flex flex-col">
892
937
  {/* Amount to Pay Label */}
893
938
  <div className="flex justify-between items-center mb-2">
894
939
  <div className="text-sm font-medium trails-text-secondary text-left">
895
940
  Recipient receives
896
941
  </div>
942
+ <FundingMethodSelectorButton />
897
943
  </div>
898
944
 
899
- <div className="flex items-center space-x-2">
945
+ <div className="flex items-center space-x-2 flex-1">
900
946
  {/* Amount Input */}
901
947
  <div className="flex-1">
902
- <div
903
- className="flex items-center justify-start cursor-text"
904
- onClick={() => inputRef.current?.focus()}
905
- >
906
- <div className="flex items-center">
907
- <input
908
- ref={inputRef}
909
- type="text"
910
- value={displayAmount}
911
- onChange={(e) => handleAmountChange(e.target.value)}
912
- placeholder={`0 ${selectedDestToken?.symbol || ""}`}
913
- readOnly={!!toAmount}
914
- className={`bg-transparent border-none outline-none font-bold text-left trails-text-primary placeholder-trails-text-primary ${
915
- isLoadingQuote ? "animate-pulse" : ""
916
- }`}
917
- style={{
918
- fontSize: inputStyles.fontSize,
919
- width: `${Math.max((displayAmount || "0").length - 1 + 0.5, 1)}ch`,
920
- minWidth: "1ch",
921
- maxWidth: "270px",
922
- padding: "0",
923
- margin: "0",
924
- transition: "all 0.1s ease-in-out",
925
- }}
926
- inputMode="decimal"
927
- />
928
- <span
929
- className="font-bold text-gray-400 dark:text-gray-500"
930
- style={{
931
- fontSize: inputStyles.fontSize,
932
- marginLeft: "0.1em",
933
- padding: "0",
934
- transition: "all 0.2s ease-in-out",
935
- }}
936
- >
937
- {selectedDestToken?.symbol?.slice(0, 4) || "TOKEN"}
938
- </span>
939
- {isLoadingQuote && (
940
- <div className="ml-2 animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
941
- )}
942
- </div>
948
+ <div className="flex items-center space-x-2">
949
+ <input
950
+ ref={inputRef}
951
+ type="text"
952
+ value={displayAmount}
953
+ onChange={(e) => handleAmountChange(e.target.value)}
954
+ placeholder={`0 ${selectedDestToken?.symbol || ""}`}
955
+ readOnly={!!toAmount}
956
+ className={`w-full bg-transparent font-bold trails-text-primary placeholder:trails-text-muted border-none outline-none ${
957
+ isLoadingQuote ? "animate-pulse" : ""
958
+ }`}
959
+ style={inputStyles}
960
+ />
961
+ {isLoadingQuote && (
962
+ <div className="animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
963
+ )}
943
964
  </div>
944
965
  </div>
945
966
 
946
- {/* Destination Token Selection Button */}
947
- {toToken ? (
948
- /* Non-clickable display when toToken is provided */
949
- <div className="flex items-center space-x-2 trails-bg-card trails-border-radius-input px-2.5 py-1.5 border trails-border-primary">
950
- {selectedDestToken ? (
951
- <>
952
- <TokenImage
953
- symbol={selectedDestToken.symbol}
954
- imageUrl={selectedDestToken.imageUrl}
955
- chainId={globalDestinationToken?.chainId || toChainId}
956
- size={20}
957
- />
958
- <span className="font-medium trails-text-primary text-sm">
959
- {selectedDestToken.symbol}
960
- </span>
961
- </>
962
- ) : (
963
- <span className="font-medium trails-text-muted text-sm">
964
- Select Token
965
- </span>
966
- )}
967
- </div>
968
- ) : (
969
- /* Clickable button when toToken is not provided */
970
- <button
971
- type="button"
972
- onClick={() => setShowDestinationTokenSelector(true)}
973
- className="flex items-center space-x-2 trails-bg-card hover:trails-hover-bg trails-border-radius-input px-2.5 py-1.5 border trails-border-primary transition-colors cursor-pointer"
974
- >
975
- {selectedDestToken ? (
976
- <>
977
- <TokenImage
978
- symbol={selectedDestToken.symbol}
979
- imageUrl={selectedDestToken.imageUrl}
980
- chainId={globalDestinationToken?.chainId || toChainId}
981
- size={20}
982
- />
983
- <span className="font-medium trails-text-primary text-sm">
984
- {selectedDestToken.symbol}
985
- </span>
986
- <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
987
- </>
988
- ) : (
989
- <>
990
- <span className="font-medium trails-text-muted text-sm">
991
- Select Token
992
- </span>
993
- <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
994
- </>
995
- )}
996
- </button>
997
- )}
967
+ {/* Destination Token Selection */}
968
+ <div className="relative">
969
+ <TokenSelectorButton
970
+ token={selectedDestToken}
971
+ chainId={globalDestinationToken?.chainId || toChainId}
972
+ onSelect={() => setShowDestinationTokenSelector(true)}
973
+ unselectable={!!toToken}
974
+ />
975
+ </div>
998
976
  </div>
999
977
 
1000
978
  {/* Bottom Info Row */}
@@ -1007,9 +985,6 @@ export const Pay: React.FC<PayProps> = ({
1007
985
  <span>&nbsp;</span>
1008
986
  )}
1009
987
  </div>
1010
- <div className="text-xs trails-text-muted">
1011
- <span>&nbsp;</span>
1012
- </div>
1013
988
  </div>
1014
989
  </div>
1015
990
  </div>
@@ -1063,6 +1038,13 @@ export const Pay: React.FC<PayProps> = ({
1063
1038
  chainId={originToken?.chainId}
1064
1039
  />
1065
1040
 
1041
+ {/* Quote Details */}
1042
+ {prepareSendQuote && (
1043
+ <div className="space-y-2">
1044
+ <QuoteDetails quote={prepareSendQuote} showContent={true} />
1045
+ </div>
1046
+ )}
1047
+
1066
1048
  {/* Pay Button */}
1067
1049
  <form onSubmit={handleSubmit}>
1068
1050
  <button
@@ -1101,13 +1083,6 @@ export const Pay: React.FC<PayProps> = ({
1101
1083
  )}
1102
1084
  </button>
1103
1085
  </form>
1104
-
1105
- {/* Quote Details */}
1106
- {prepareSendQuote && (
1107
- <div className="space-y-2">
1108
- <QuoteDetails quote={prepareSendQuote} showContent={true} />
1109
- </div>
1110
- )}
1111
1086
  </div>
1112
1087
  )
1113
1088
  }
@@ -0,0 +1,77 @@
1
+ import type React from "react"
2
+ import {
3
+ calculateMaxNativeAmount,
4
+ getDefaultGasCostEstimate,
5
+ } from "../../estimate.js"
6
+
7
+ interface PercentageMaxButtonsProps {
8
+ userBalance: string | undefined
9
+ isNativeToken: boolean
10
+ gasCostFormatted?: string
11
+ chainId?: number
12
+ onAmountSelect: (amount: string) => void
13
+ className?: string
14
+ }
15
+
16
+ export const PercentageMaxButtons: React.FC<PercentageMaxButtonsProps> = ({
17
+ userBalance,
18
+ isNativeToken,
19
+ gasCostFormatted,
20
+ chainId,
21
+ onAmountSelect,
22
+ className = "",
23
+ }) => {
24
+ // Don't render if no balance
25
+ if (!userBalance) return null
26
+
27
+ const handlePercentageClick = (percentage: number) => {
28
+ const balance = parseFloat(userBalance)
29
+ if (Number.isNaN(balance)) return
30
+
31
+ const amount = (balance * percentage) / 100
32
+ onAmountSelect(amount.toFixed(6))
33
+ }
34
+
35
+ const handleMaxClick = async () => {
36
+ if (isNativeToken) {
37
+ // For native tokens, subtract gas cost
38
+ // Priority: 1) Use actual gas cost from quote, 2) Fetch real gas price, 3) Fallback to 1% of balance
39
+ const effectiveGasCost =
40
+ gasCostFormatted ||
41
+ (await getDefaultGasCostEstimate(userBalance, chainId))
42
+ const maxAmount = calculateMaxNativeAmount(userBalance, effectiveGasCost)
43
+ onAmountSelect(maxAmount)
44
+ } else {
45
+ // For ERC20 tokens, use full balance
46
+ onAmountSelect(userBalance)
47
+ }
48
+ }
49
+
50
+ return (
51
+ <div
52
+ className={`flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 ${className}`}
53
+ >
54
+ <button
55
+ type="button"
56
+ onClick={() => handlePercentageClick(20)}
57
+ className="py-0.5 px-1.5 text-xs font-medium rounded-full transition-colors cursor-pointer trails-bg-percentage-button trails-text-percentage-button trails-hover-percentage-button"
58
+ >
59
+ 20%
60
+ </button>
61
+ <button
62
+ type="button"
63
+ onClick={() => handlePercentageClick(50)}
64
+ className="py-0.5 px-1.5 text-xs font-medium rounded-full transition-colors cursor-pointer trails-bg-percentage-button trails-text-percentage-button trails-hover-percentage-button"
65
+ >
66
+ 50%
67
+ </button>
68
+ <button
69
+ type="button"
70
+ onClick={handleMaxClick}
71
+ className="py-0.5 px-1.5 text-xs font-medium rounded-full transition-colors cursor-pointer trails-bg-percentage-button trails-text-percentage-button trails-hover-percentage-button"
72
+ >
73
+ Max
74
+ </button>
75
+ </div>
76
+ )
77
+ }