fix(frontend): more wallet popover fixes (#11285)

## Changes 🏗️

<img width="800" height="547" alt="Screenshot 2025-10-29 at 22 11 35"
src="https://github.com/user-attachments/assets/5c700ddc-d770-48ef-9847-7e652c5dedcb"
/>
<br /><br />

- Use
[`react-currency-input-field`](https://www.npmjs.com/package/react-currency-input-field)
for `<Input type="amount" />` under the hood
  - so it formats numbers nicely with `,` and `.`
- Simplify form logic
- Make the popover cover the trigger button when open
- Re-organize imports
- Show a `$` prefix in front of the amount inputs

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Login
  - [x] Open the wallet with credits enabled
  - [x] Play with the inputs

---------

Co-authored-by: Swifty <craigswift13@gmail.com>
This commit is contained in:
Ubbe
2025-10-30 14:44:29 +04:00
committed by GitHub
parent 4140331731
commit 04493598e2
6 changed files with 128 additions and 110 deletions

View File

@@ -84,6 +84,7 @@
"nuqs": "2.4.3",
"party-js": "2.2.0",
"react": "18.3.1",
"react-currency-input-field": "4.0.3",
"react-day-picker": "9.8.1",
"react-dom": "18.3.1",
"react-drag-drop-files": "2.4.0",

View File

@@ -185,6 +185,9 @@ importers:
react:
specifier: 18.3.1
version: 18.3.1
react-currency-input-field:
specifier: 4.0.3
version: 4.0.3(react@18.3.1)
react-day-picker:
specifier: 9.8.1
version: 9.8.1(react@18.3.1)
@@ -6247,6 +6250,11 @@ packages:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
react-currency-input-field@4.0.3:
resolution: {integrity: sha512-alimHDX5tplPsNB3jEAW7qjlJ76RfBAc/p8yru3cTiAYslj3oJ+KNnk788IZYe6ja3cAuH26v047lMzadh47ow==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-day-picker@9.8.1:
resolution: {integrity: sha512-kMcLrp3PfN/asVJayVv82IjF3iLOOxuH5TNFWezX6lS/T8iVRFPTETpHl3TUSTH99IDMZLubdNPJr++rQctkEw==}
engines: {node: '>=18'}
@@ -14308,6 +14316,10 @@ snapshots:
range-parser@1.2.1: {}
react-currency-input-field@4.0.3(react@18.3.1):
dependencies:
react: 18.3.1
react-day-picker@9.8.1(react@18.3.1):
dependencies:
'@date-fns/tz': 1.2.0

View File

@@ -5,6 +5,7 @@ import {
import { cn } from "@/lib/utils";
import { Eye, EyeSlash } from "@phosphor-icons/react";
import { ReactNode, useState } from "react";
import CurrencyInput from "react-currency-input-field";
import { Text } from "../Text/Text";
import { useInput } from "./useInput";
@@ -30,6 +31,8 @@ export interface TextFieldProps extends Omit<InputProps, "size"> {
| "datetime-local";
// Textarea-specific props
rows?: number;
amountPrefix?: string;
amountSuffix?: string;
}
export function Input({
@@ -42,13 +45,16 @@ export function Input({
error,
size = "medium",
wrapperClassName,
amountPrefix,
amountSuffix,
...props
}: TextFieldProps) {
const { handleInputChange, handleTextareaChange } = useInput({
type: props.type,
onChange: props.onChange,
decimalCount,
});
const { handleInputChange, handleTextareaChange, handleAmountValueChange } =
useInput({
type: props.type,
onChange: props.onChange,
decimalCount,
});
const [showPassword, setShowPassword] = useState(false);
const isPasswordType = props.type === "password";
@@ -110,6 +116,44 @@ export function Input({
);
}
if (props.type === "amount") {
return (
<CurrencyInput
className={cn(
baseStyles,
errorStyles,
// Size variants
size === "small" && [
"h-[2.25rem]",
"py-2",
"text-sm leading-[22px]",
"placeholder:text-sm placeholder:leading-[22px]",
],
size === "medium" && ["h-[2.875rem]", "py-2.5"],
)}
placeholder={placeholder || label}
// CurrencyInput gives unformatted numeric string in value param
onValueChange={handleAmountValueChange}
value={props.value as string | number | undefined}
id={props.id}
name={props.name}
disabled={props.disabled}
inputMode="decimal"
decimalsLimit={decimalCount ?? 4}
allowDecimals={decimalCount !== 0}
groupSeparator=","
decimalSeparator="."
allowNegativeValue
{...(hideLabel ? { "aria-label": label } : {})}
// Pass through common handlers
onBlur={props.onBlur as any}
onFocus={props.onFocus as any}
prefix={amountPrefix}
suffix={amountSuffix}
/>
);
}
return (
<BaseInput
className={cn(

View File

@@ -65,6 +65,20 @@ export function useInput(args: ExtendedInputProps) {
}
}
function handleAmountValueChange(value?: string) {
if (!args.onChange) return;
const processedValue = value ?? "";
const syntheticEvent = {
// We only need target.value for our consumers
target: {
value: processedValue,
},
} as unknown as React.ChangeEvent<HTMLInputElement>;
args.onChange(syntheticEvent);
}
function handleTextareaChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
if (args.onChange) {
// Create synthetic event with HTMLInputElement-like target for compatibility
@@ -80,5 +94,5 @@ export function useInput(args: ExtendedInputProps) {
}
}
return { handleInputChange, handleTextareaChange };
return { handleInputChange, handleTextareaChange, handleAmountValueChange };
}

View File

@@ -5,6 +5,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/__legacy__/ui/popover";
import { ScrollArea } from "@/components/__legacy__/ui/scroll-area";
import { Text } from "@/components/atoms/Text/Text";
import useCredits from "@/hooks/useCredits";
import { OnboardingStep } from "@/lib/autogpt-server-api";
@@ -17,8 +18,7 @@ import { PopoverClose } from "@radix-ui/react-popover";
import { X } from "lucide-react";
import * as party from "party-js";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ScrollArea } from "../../../../../components/__legacy__/ui/scroll-area";
import WalletRefill from "./components/WalletRefill";
import { WalletRefill } from "./components/WalletRefill";
import { TaskGroups } from "./components/WalletTaskGroups";
export interface Task {
@@ -360,9 +360,8 @@ export function Wallet() {
<PopoverContent
side="bottom"
align="end"
sideOffset={12}
collisionPadding={16}
className={cn("z-50 w-[28.5rem] px-[0.625rem] py-2")}
className={cn("relative -top-12 z-50 w-[28.5rem] px-[0.625rem] py-2")}
>
{/* Header */}
<div className="mx-1 flex items-center justify-between border-b border-zinc-200 pb-3">

View File

@@ -1,24 +1,17 @@
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/__legacy__/ui/form";
import { Form, FormField } from "@/components/__legacy__/ui/form";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/__legacy__/ui/tabs";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import {
useToast,
useToastOnFail,
} from "@/components/molecules/Toast/use-toast";
import useCredits from "@/hooks/useCredits";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
@@ -49,12 +42,14 @@ const autoRefillSchema = z
path: ["refillAmount"],
});
export default function WalletRefill() {
export function WalletRefill() {
const { toast } = useToast();
const toastOnFail = useToastOnFail();
const { requestTopUp, autoTopUpConfig, updateAutoTopUpConfig } = useCredits({
fetchInitialAutoTopUpConfig: true,
});
const [isLoading, setIsLoading] = useState(false);
const topUpForm = useForm<z.infer<typeof topUpSchema>>({
@@ -132,48 +127,29 @@ export default function WalletRefill() {
Enter an amount (min. $5) and add credits instantly.
</div>
<Form {...topUpForm}>
<form onSubmit={topUpForm.handleSubmit(submitTopUp)}>
<form
onSubmit={topUpForm.handleSubmit(submitTopUp)}
className="my-4"
>
<FormField
control={topUpForm.control}
name="amount"
render={({ field }) => (
<FormItem className="mb-6 mt-4">
<FormLabel className="font-sans text-sm font-medium leading-snug text-zinc-800">
Amount
</FormLabel>
<FormControl>
<>
<Input
className={cn(
"mt-2 rounded-3xl border-0 bg-white py-2 pl-6 pr-4 font-sans outline outline-1 outline-zinc-300",
"focus:outline-2 focus:outline-offset-0 focus:outline-violet-700",
)}
label="Amount"
id="amount"
type="number"
step="1"
{...field}
/>
<span className="absolute left-10 -translate-y-9 text-sm text-zinc-500">
$
</span>
</>
</FormControl>
<FormMessage className="mt-2 font-sans text-xs font-normal leading-tight" />
</FormItem>
<Input
label="Amount"
type="amount"
size="small"
decimalCount={0}
id={field.name}
error={topUpForm.formState.errors.amount?.message}
amountPrefix="$"
{...field}
/>
)}
/>
<button
className={cn(
"mb-2 inline-flex h-10 w-24 items-center justify-center rounded-3xl bg-zinc-800 px-4 py-2",
"font-sans text-sm font-medium leading-snug text-white",
"transition-colors duration-200 hover:bg-zinc-700 disabled:bg-zinc-500",
)}
type="submit"
disabled={isLoading}
>
<Button type="submit" disabled={isLoading} size="small">
Top up
</button>
</Button>
</form>
</Form>
</TabsContent>
@@ -188,78 +164,50 @@ export default function WalletRefill() {
<Form {...autoRefillForm}>
<form
onSubmit={autoRefillForm.handleSubmit(submitAutoTopUpConfig)}
className="my-6"
>
<FormField
control={autoRefillForm.control}
name="threshold"
render={({ field }) => (
<FormItem className="mb-6 mt-4">
<FormLabel className="font-sans text-sm font-medium leading-snug text-zinc-800">
Refill when balance drops below:
</FormLabel>
<FormControl>
<>
<Input
className={cn(
"mt-2 rounded-3xl border-0 bg-white py-2 pl-6 pr-4 font-sans outline outline-1 outline-zinc-300",
"focus:outline-2 focus:outline-offset-0 focus:outline-violet-700",
)}
type="number"
step="1"
label="Refill when balance drops below:"
id="threshold"
{...field}
/>
<span className="absolute left-10 -translate-y-9 text-sm text-zinc-500">
$
</span>
</>
</FormControl>
<FormMessage className="mt-2 font-sans text-xs font-normal leading-tight" />
</FormItem>
<Input
type="amount"
label="Refill when balance drops below:"
id={field.name}
size="small"
decimalCount={0}
error={autoRefillForm.formState.errors.threshold?.message}
amountPrefix="$"
{...field}
/>
)}
/>
<FormField
control={autoRefillForm.control}
name="refillAmount"
render={({ field }) => (
<FormItem className="mb-6">
<FormLabel className="font-sans text-sm font-medium leading-snug text-zinc-800">
Add this amount:
</FormLabel>
<FormControl>
<>
<Input
className={cn(
"mt-2 rounded-3xl border-0 bg-white py-2 pl-6 pr-4 font-sans outline outline-1 outline-zinc-300",
"focus:outline-2 focus:outline-offset-0 focus:outline-violet-700",
)}
type="number"
step="1"
label="Add this amount:"
id="refillAmount"
{...field}
/>
<span className="absolute left-10 -translate-y-9 text-sm text-zinc-500">
$
</span>
</>
</FormControl>
<FormMessage className="mt-2 font-sans text-xs font-normal leading-tight" />
</FormItem>
<Input
type="amount"
label="Add this amount:"
size="small"
decimalCount={0}
id={field.name}
error={
autoRefillForm.formState.errors.refillAmount?.message
}
amountPrefix="$"
{...field}
/>
)}
/>
<button
className={cn(
"mb-4 inline-flex h-10 w-40 items-center justify-center rounded-3xl bg-zinc-800 px-4 py-2",
"font-sans text-sm font-medium leading-snug text-white",
"transition-colors duration-200 hover:bg-zinc-700 disabled:bg-zinc-500",
)}
<Button
type="submit"
disabled={isLoading}
size="small"
className="mt-5"
>
Enable Auto-refill
</button>
</Button>
</form>
</Form>
</TabsContent>