diff --git a/src/components/ComboInputField.tsx b/src/components/ComboInputField.tsx index a3613517..dfdc9f27 100644 --- a/src/components/ComboInputField.tsx +++ b/src/components/ComboInputField.tsx @@ -10,7 +10,7 @@ import { useFarmerSilo } from "@/state/useFarmerSilo"; import { usePriceData } from "@/state/usePriceData"; import useTokenData from "@/state/useTokenData"; import { formatter, truncateHex } from "@/utils/format"; -import { sanitizeNumericInputValue, stringEq, stringToNumber, toValidStringNumInput } from "@/utils/string"; +import { MAX_INPUT_VALUE, sanitizeNumericInputValue, stringEq, stringToNumber } from "@/utils/string"; import { FarmFromMode, Plot, Token } from "@/utils/types"; import { useDebouncedEffect } from "@/utils/useDebounce"; import { cn } from "@/utils/utils"; @@ -102,6 +102,9 @@ export interface ComboInputProps extends InputHTMLAttributes { enableSlider?: boolean; sliderMarkers?: number[]; customTokenSelector?: React.ReactNode; + + // Additional info display + showAdditionalInfo?: boolean; } function ComboInputField({ @@ -142,6 +145,7 @@ function ComboInputField({ placeholder, enableSlider, sliderMarkers, + showAdditionalInfo = true, }: ComboInputProps) { const tokenData = useTokenData(); const { balances } = useFarmerBalances(); @@ -453,7 +457,7 @@ function ComboInputField({ setIsUserInput(true); // Sanitize the input value - const cleaned = sanitizeNumericInputValue(value, getDecimals()); + const cleaned = sanitizeNumericInputValue(value, getDecimals(), false, MAX_INPUT_VALUE); const clamped = getClamped(cleaned.tv, maxAmount); // Set the display value to the sanitized string value @@ -631,7 +635,7 @@ function ComboInputField({ {formatter.usd(inputValue)} ) : null} - {mode === "deposits" && shouldShowAdditionalInfo() && ( + {mode === "deposits" && shouldShowAdditionalInfo() && showAdditionalInfo && ( <> Stalk diff --git a/src/components/SmartSubmitButton.tsx b/src/components/SmartSubmitButton.tsx index 0f827599..9d65336b 100644 --- a/src/components/SmartSubmitButton.tsx +++ b/src/components/SmartSubmitButton.tsx @@ -168,7 +168,7 @@ export default function SmartSubmitButton({ } const btnDisabled = disabled || allowanceFetching || submittingApproval || isConfirmingApproval; - const btnApproval = needsApproval && !allowanceFetching; + const btnApproval = needsApproval && !allowanceFetching && !disabled; return ( )} - +
Referral Code
diff --git a/src/components/Tractor/form/SowOrderV0Schema.ts b/src/components/Tractor/form/SowOrderV0Schema.ts index 633b521f..b8a4c746 100644 --- a/src/components/Tractor/form/SowOrderV0Schema.ts +++ b/src/components/Tractor/form/SowOrderV0Schema.ts @@ -235,6 +235,7 @@ export type SowV0FormOrderData = { morningAuction: boolean; tokenStrategy: TractorTokenStrategy["type"]; token: Token | undefined; + referralCode?: string; }; export type SowOrderV0State = { @@ -322,6 +323,7 @@ export const useSowOrderV0State = () => { tokenStrategy: formData.selectedTokenStrategy.type, token: tokenInstance, operatorTip: formData.operatorTip || "", + referralCode: formData.referralCode || "", }); setState({ diff --git a/src/components/ui/Popover.tsx b/src/components/ui/Popover.tsx index 83702342..89e5004a 100644 --- a/src/components/ui/Popover.tsx +++ b/src/components/ui/Popover.tsx @@ -11,9 +11,9 @@ const PopoverAnchor = PopoverPrimitive.Anchor; const PopoverContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( - + React.ComponentPropsWithoutRef & { container?: HTMLElement | null } +>(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => ( + { * Uses global state for cross-component synchronization. */ export function useReferralCode() { + const { address } = useAccount(); const referralCode = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); // Get valid referral code from localStorage (for conditional rendering) const validReferralCodeFromStorage = useSyncExternalStore(subscribeStorage, getStorageSnapshot, getStorageSnapshot); - const setReferralCode = useCallback((value: string) => { - const trimmed = value.trim(); + const setReferralCode = useCallback( + (value: string) => { + const trimmed = value.trim(); - // Always update global state for immediate UI feedback (so user can type) - globalReferralCode = trimmed; - notifyListeners(); + // Always update global state for immediate UI feedback (so user can type) + globalReferralCode = trimmed; + notifyListeners(); - // Save to localStorage only if valid, otherwise clear it - if (!trimmed) { - // Empty value: clear localStorage - saveToLocalStorage(""); - } else if (isValidReferralCode(trimmed)) { - // Valid code: save to localStorage - saveToLocalStorage(trimmed); - } else { - // Invalid code: clear localStorage - saveToLocalStorage(""); - } - // Invalid codes are kept in global state for typing feedback, but localStorage is cleared - }, []); + // Save to localStorage only if valid, otherwise clear it + if (!trimmed) { + // Empty value: clear localStorage + saveToLocalStorage(""); + } else { + // Check if referral code is valid and doesn't belong to connected wallet + const decodedAddress = decodeReferralAddress(trimmed); + const isValid = isValidReferralCode(trimmed); + const isOwnCode = address && decodedAddress && stringEq(decodedAddress, address); + + if (isValid && !isOwnCode) { + // Valid code and not own code: save to localStorage + saveToLocalStorage(trimmed); + } else { + // Invalid code or own code: clear localStorage + saveToLocalStorage(""); + } + } + // Invalid codes are kept in global state for typing feedback, but localStorage is cleared + }, + [address], + ); // Validate referral code for real-time validation const isReferralCodeValid = useMemo(() => { if (!referralCode) return false; - return isValidReferralCode(referralCode); - }, [referralCode]); + + // Check if referral code is valid + if (!isValidReferralCode(referralCode)) return false; + + // Check if referral code belongs to connected wallet + // If wallet is not connected (address is undefined), we can't check, so allow it + if (!address) return true; + + const decodedAddress = decodeReferralAddress(referralCode); + if (decodedAddress && stringEq(decodedAddress, address)) { + return false; // Invalid if it's the user's own code + } + + return true; + }, [referralCode, address]); + + // Clean up localStorage if wallet connects and stored code is user's own code + useEffect(() => { + if (!address || !validReferralCodeFromStorage) return; + + const decodedAddress = decodeReferralAddress(validReferralCodeFromStorage); + if (decodedAddress && stringEq(decodedAddress, address)) { + // Clear localStorage if stored code belongs to connected wallet + saveToLocalStorage(""); + } + }, [address, validReferralCodeFromStorage]); return { referralCode, // For input field (can be invalid during typing) diff --git a/src/pages/field/actions/Sow.tsx b/src/pages/field/actions/Sow.tsx index ad797141..464fd2b5 100644 --- a/src/pages/field/actions/Sow.tsx +++ b/src/pages/field/actions/Sow.tsx @@ -16,7 +16,7 @@ import { useFarmerBalances } from "@/state/useFarmerBalances"; import { useFarmerField } from "@/state/useFarmerField"; import { useInvalidateField, usePodLine, useTotalSoil } from "@/state/useFieldData"; import useTokenData from "@/state/useTokenData"; -import { formatter } from "@/utils/format"; +import { NUMBER_ABBR_THRESHOLDS, formatter } from "@/utils/format"; import { decodeReferralAddress } from "@/utils/referral"; import { stringToNumber, stringToStringNum } from "@/utils/string"; import { AdvancedFarmCall, FarmFromMode, FarmToMode, Token } from "@/utils/types"; @@ -214,6 +214,11 @@ function Sow({ isMorning, onShowOrder }: SowProps) { if (inputError) { throw new Error("Invalid input"); } + // Check soil availability before submitting + // This prevents transaction submission even if UI state is bypassed + if (totalSoilLoading || !totalSoil || totalSoil.lte(0)) { + throw new Error("No Soil available"); + } // Track sow submission trackSimpleEvent(ANALYTICS_EVENTS.FIELD.SOW_SUBMIT, { @@ -346,6 +351,8 @@ function Sow({ isMorning, onShowOrder }: SowProps) { currentTemperature, inputError, referralAddress, + totalSoil, + totalSoilLoading, ]); // Callbacks @@ -435,7 +442,9 @@ function Sow({ isMorning, onShowOrder }: SowProps) { const initializing = !didSetPreferred || (hasSoil ? maxBuyQuery.isLoading : false); const isLoading = (numIn > 0 && loading) || (pods?.lte(0) && numIn > 0); - const ready = pods?.gt(0) && podLine.gte(0) && (hasSoil ? maxBuy?.gt(0) && amountInTV.gt(0) : true); + // If there's no soil, ready should be false (can't sow without soil) + // If there's soil, check that maxBuy is available and amount is greater than 0 + const ready = pods?.gt(0) && podLine.gte(0) && (hasSoil ? maxBuy?.gt(0) && amountInTV.gt(0) : false); const tokenBalance = fromSilo ? depositedByWhitelistedToken.get(tokenIn) @@ -447,7 +456,17 @@ function Sow({ isMorning, onShowOrder }: SowProps) { const ctaDisabled = isLoading || isConfirming || submitting || !ready || inputError || !canProceed; - const buttonText = inputError ? "Amount too large" : "Sow"; + // Determine button text based on error conditions + // If no soil available, show "No Soil available" (consistent with warning message) + const noSoilAvailable = !hasSoil && !totalSoilLoading; + const buttonText = inputError ? "Amount too large" : noSoilAvailable ? "No Soil available" : "Sow"; + + // Helper for compact number formatting + const formatCompact = (value: TV | undefined, decimals = 2) => + formatter.number(value, { + maxDecimals: decimals, + compact: (value?.toNumber() ?? 0) >= NUMBER_ABBR_THRESHOLDS.BILLION, + }); return ( @@ -484,6 +503,7 @@ function Sow({ isMorning, onShowOrder }: SowProps) { altText={balanceExceedsSoil ? "Usable balance:" : undefined} filterTokens={filterTokens} disableClamping={true} + showAdditionalInfo={false} /> {!hasReferralCode && ( @@ -525,19 +545,19 @@ function Sow({ isMorning, onShowOrder }: SowProps) { Sow{" "} - {`${formatter.twoDec(soilSown)}/${formatter.twoDec(totalSoil)}`}{" "} + {`${formatCompact(soilSown)}/${formatCompact(totalSoil)}`}{" "} {`available Soil and receive`} } > - + {hasReferralCode && bonusPods.gt(0) && (
- +
{fromSilo ? ( <> @@ -624,9 +644,9 @@ function Sow({ isMorning, onShowOrder }: SowProps) {