Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 75 additions & 12 deletions src/components/value-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,61 @@ function scaleFactor(width: number) {
return 1.1875 - 0.00307 * width + 3.7e-6 * width ** 2 - 1.62e-9 * width ** 3
}

// Maximum number of digits allowed after the decimal point while typing
const MAX_DECIMALS = 10

// Sanitizes arbitrary user input into a valid decimal string.
// - Strict dot-only (commas removed)
// - Keeps at most one dot
// - Normalizes leading zeros
// - Allows a trailing dot as an intermediate state ("1.")
function sanitizeDecimalInput(raw: string): string {
// Remove commas (strict dot-only) and any non-digit/non-dot characters
let s = raw.replace(/,/g, "").replace(/[^0-9.]/g, "")

if (s === "") return ""

const hadTrailingDot = s.endsWith(".")

// Keep only the first dot
const firstDotIndex = s.indexOf(".")
if (firstDotIndex !== -1) {
s =
s.slice(0, firstDotIndex + 1) +
s.slice(firstDotIndex + 1).replace(/\./g, "")
}

const hasDot = s.includes(".")

const normalizeInteger = (intPart: string) => {
const withoutLeading = intPart.replace(/^0+(?=\d)/, "")
return withoutLeading === "" ? "0" : withoutLeading
}

if (!hasDot) {
const intPart = normalizeInteger(s)
return intPart
}

let [intPart, fracPart = ""] = s.split(".")

// If started with dot, ensure integer becomes 0
if (intPart === "") intPart = "0"
intPart = normalizeInteger(intPart)

// Enforce max decimals
if (fracPart.length > MAX_DECIMALS) {
fracPart = fracPart.slice(0, MAX_DECIMALS)
}

// Allow a trailing dot during typing
if (hadTrailingDot && fracPart.length === 0) {
return `${intPart}.`
}

return fracPart.length > 0 ? `${intPart}.${fracPart}` : intPart
}

export const ValueInput: React.FC<ValueInputProps> = ({
value,
onValueChange,
Expand All @@ -27,17 +82,20 @@ export const ValueInput: React.FC<ValueInputProps> = ({
const [fontSize, setFontSize] = useState(1)
const spanRef = useRef<HTMLSpanElement>(null)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
e.stopPropagation()
const newValue = e.target.value.replace(/[^0-9.]/g, "")

// Allow '0' after the first character
let finalValue = newValue
if (newValue.length > 1 && newValue[0] === "0" && newValue[1] !== ".") {
finalValue = newValue.slice(1)
}
const sanitized = sanitizeDecimalInput(e.target.value)
onValueChange?.(sanitized)
}

onValueChange?.(finalValue)
const handleBlur = () => {
if (!onValueChange) return
if (!value) return
if (value === ".") {
onValueChange("0")
return
}
if (value.endsWith(".")) {
onValueChange(value.slice(0, -1))
}
}

useEffect(() => {
Expand All @@ -63,14 +121,19 @@ export const ValueInput: React.FC<ValueInputProps> = ({
type="text"
value={value}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
className="bg-transparent font-semibold placeholder-muted focus:outline-hidden"
className="placeholder-muted bg-transparent font-semibold focus:outline-hidden"
style={{ width: `${inputWidth}px` }}
inputMode="decimal"
autoComplete="off"
spellCheck={false}
aria-label={label || "Value"}
/>
<span className="ml-1 text-gray-400">{label}</span>
<span
ref={spanRef}
className="invisible absolute left-0 whitespace-pre font-semibold"
className="invisible absolute left-0 font-semibold whitespace-pre"
aria-hidden="true"
>
{value || placeholder}
Expand Down
16 changes: 1 addition & 15 deletions src/components/wallet-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
import { useEffect, useState } from "react"
import { useWallets } from "@/providers/wallet-provider"
import { ExportType, useTurnkey } from "@turnkey/react-wallet-kit"
import { CopyIcon, Download, HandCoins, Upload } from "lucide-react"
import { CopyIcon, Download, Upload } from "lucide-react"
import { toast } from "sonner"
import { formatEther } from "viem"

import { truncateAddress } from "@/lib/utils"
import { fundWallet } from "@/lib/web3"
import { useTokenPrice } from "@/hooks/use-token-price"
import { Button } from "@/components/ui/button"
import {
Expand All @@ -29,11 +28,6 @@ export default function WalletCard() {
const { selectedWallet, selectedAccount } = state
const [usdAmount, setUsdAmount] = useState<number | undefined>(undefined)

const handleFundWallet = async () => {
if (!selectedAccount?.address) return
await fundWallet(selectedAccount?.address)
}

const handleCopyAddress = () => {
if (selectedAccount?.address) {
navigator.clipboard.writeText(selectedAccount.address)
Expand All @@ -58,10 +52,6 @@ export default function WalletCard() {
</CardTitle>

<div className="hidden items-center gap-2 sm:flex">
<Button onClick={handleFundWallet} className="h-min cursor-pointer">
<HandCoins className="mr-2 h-4 w-4" />
Add funds
</Button>
<TransferDialog />

<Button variant="outline" onClick={() => handleImport()}>
Expand Down Expand Up @@ -111,10 +101,6 @@ export default function WalletCard() {
</CardContent>
<CardFooter className="sm:hidden">
<div className="mx-auto flex w-full flex-col items-center gap-2">
<Button className="w-full">
<HandCoins className="mr-2 h-4 w-4" />
Add funds
</Button>
<TransferDialog />
<div className="flex w-full items-center gap-2">
<Button
Expand Down
1 change: 0 additions & 1 deletion src/config/turnkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export const customWallet = {
export const turnkeyConfig: TurnkeyProviderConfig = {
organizationId: NEXT_PUBLIC_ORGANIZATION_ID,
authProxyConfigId: NEXT_PUBLIC_AUTH_PROXY_ID,
authProxyUrl: NEXT_PUBLIC_AUTH_PROXY_URL,
apiBaseUrl: NEXT_PUBLIC_BASE_URL,
auth: {
autoRefreshSession: true,
Expand Down