@@ -15,7 +15,6 @@ import {
1515} from "@/lib/merchant-store" ;
1616import { useLocalStorage } from "@/hooks/useLocalStorage" ;
1717
18-
1918const API_URL = process . env . NEXT_PUBLIC_API_URL ?? "http://localhost:4000" ;
2019
2120const USDC_ISSUER =
@@ -179,8 +178,7 @@ function SuccessCard({ created, onReset, t }: SuccessCardProps) {
179178 useEffect ( ( ) => {
180179 fireConfetti ( ) ;
181180 setCanShare (
182- typeof navigator !== "undefined" &&
183- typeof navigator . share === "function" ,
181+ typeof navigator !== "undefined" && typeof navigator . share === "function" ,
184182 ) ;
185183 } , [ ] ) ;
186184
@@ -284,7 +282,10 @@ function SuccessCard({ created, onReset, t }: SuccessCardProps) {
284282 </ div >
285283 </ motion . div >
286284
287- < motion . div variants = { childVariants } className = "mt-4 flex flex-wrap gap-2" >
285+ < motion . div
286+ variants = { childVariants }
287+ className = "mt-4 flex flex-wrap gap-2"
288+ >
288289 { canShare && (
289290 < button
290291 type = "button"
@@ -349,6 +350,8 @@ export default function CreatePaymentForm() {
349350 ) ;
350351 const [ loading , setLoading ] = useState ( false ) ;
351352 const [ error , setError ] = useState < string | null > ( null ) ;
353+ const [ amountError , setAmountError ] = useState < string | null > ( null ) ;
354+ const [ recipientError , setRecipientError ] = useState < string | null > ( null ) ;
352355 const [ created , setCreated ] = useState < CreatedPayment | null > ( null ) ;
353356 const apiKey = useMerchantApiKey ( ) ;
354357 const hydrated = useMerchantHydrated ( ) ;
@@ -413,15 +416,20 @@ export default function CreatePaymentForm() {
413416 e . preventDefault ( ) ;
414417 setError ( null ) ;
415418
419+ // Client-side validation
420+ let hasError = false ;
416421 const numAmount = parseFloat ( amount ) ;
417422 if ( isNaN ( numAmount ) || numAmount <= 0 ) {
418- setError ( t ( "invalidAmount" ) ) ;
419- return ;
423+ setAmountError ( "Amount must be greater than 0." ) ;
424+ hasError = true ;
420425 }
421426 if ( ! STELLAR_ADDRESS_RE . test ( recipient . trim ( ) ) ) {
422- setError ( t ( "invalidRecipient" ) ) ;
423- return ;
427+ setRecipientError (
428+ "Must be a valid Stellar public key (56 characters, starts with G)." ,
429+ ) ;
430+ hasError = true ;
424431 }
432+ if ( hasError ) return ;
425433
426434 setLoading ( true ) ;
427435 try {
@@ -483,6 +491,8 @@ export default function CreatePaymentForm() {
483491 localStorage . removeItem ( "payment_branding" ) ;
484492 localStorage . removeItem ( "payment_trusted_address" ) ;
485493 setError ( null ) ;
494+ setAmountError ( null ) ;
495+ setRecipientError ( null ) ;
486496 setRetryAfter ( 0 ) ;
487497 } ;
488498
@@ -496,7 +506,7 @@ export default function CreatePaymentForm() {
496506
497507 const updateBrandingField = (
498508 key : keyof typeof DEFAULT_BRANDING ,
499- value : string
509+ value : string ,
500510 ) => {
501511 setBranding ( ( current ) => ( { ...current , [ key ] : normalizeHexInput ( value ) } ) ) ;
502512 } ;
@@ -571,10 +581,24 @@ export default function CreatePaymentForm() {
571581 step = "any"
572582 required
573583 value = { amount }
574- onChange = { ( e ) => setAmount ( e . target . value ) }
575- className = "rounded-xl border border-white/10 bg-white/5 p-3 text-white placeholder:text-slate-600 focus:border-mint/50 focus:outline-none focus:ring-1 focus:ring-mint/50 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
584+ onChange = { ( e ) => {
585+ setAmount ( e . target . value ) ;
586+ setAmountError ( null ) ;
587+ } }
588+ aria-invalid = { ! ! amountError }
589+ aria-describedby = { amountError ? "amount-error" : undefined }
590+ className = { `rounded-xl border bg-white/5 p-3 text-white placeholder:text-slate-600 focus:outline-none focus:ring-1 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none ${ amountError ? "border-red-500/50 focus:border-red-500/50 focus:ring-red-500/50" : "border-white/10 focus:border-mint/50 focus:ring-mint/50" } ` }
576591 placeholder = { amountPlaceholder }
577592 />
593+ { amountError && (
594+ < p
595+ id = "amount-error"
596+ role = "alert"
597+ className = "text-xs text-red-400"
598+ >
599+ { amountError }
600+ </ p >
601+ ) }
578602 </ div >
579603
580604 { /* Asset */ }
@@ -593,10 +617,11 @@ export default function CreatePaymentForm() {
593617 type = "button"
594618 onClick = { ( ) => setAsset ( a ) }
595619 aria-pressed = { asset === a }
596- className = { `flex-1 rounded-xl border py-2.5 text-sm font-medium transition-all ${ asset === a
597- ? "border-mint/50 bg-mint/10 text-mint"
598- : "border-white/10 bg-white/5 text-slate-400 hover:border-white/20 hover:text-white"
599- } `}
620+ className = { `flex-1 rounded-xl border py-2.5 text-sm font-medium transition-all ${
621+ asset === a
622+ ? "border-mint/50 bg-mint/10 text-mint"
623+ : "border-white/10 bg-white/5 text-slate-400 hover:border-white/20 hover:text-white"
624+ } `}
600625 >
601626 { a }
602627 </ button >
@@ -651,12 +676,28 @@ export default function CreatePaymentForm() {
651676 type = "text"
652677 required
653678 value = { recipient }
654- onChange = { ( e ) => setRecipient ( e . target . value ) }
655- className = "rounded-xl border border-white/10 bg-white/5 p-3 font-mono text-sm text-white placeholder:font-sans placeholder:text-slate-600 focus:border-mint/50 focus:outline-none focus:ring-1 focus:ring-mint/50"
679+ onChange = { ( e ) => {
680+ setRecipient ( e . target . value ) ;
681+ setRecipientError ( null ) ;
682+ } }
683+ aria-invalid = { ! ! recipientError }
684+ aria-describedby = {
685+ recipientError ? "recipient-error" : undefined
686+ }
687+ className = { `rounded-xl border bg-white/5 p-3 font-mono text-sm text-white placeholder:font-sans placeholder:text-slate-600 focus:outline-none focus:ring-1 ${ recipientError ? "border-red-500/50 focus:border-red-500/50 focus:ring-red-500/50" : "border-white/10 focus:border-mint/50 focus:ring-mint/50" } ` }
656688 placeholder = { recipientPlaceholder }
657689 autoComplete = "off"
658690 spellCheck = { false }
659691 />
692+ { recipientError && (
693+ < p
694+ id = "recipient-error"
695+ role = "alert"
696+ className = "text-xs text-red-400"
697+ >
698+ { recipientError }
699+ </ p >
700+ ) }
660701 </ div >
661702
662703 { /* Description */ }
@@ -694,10 +735,11 @@ export default function CreatePaymentForm() {
694735 < button
695736 type = "button"
696737 onClick = { ( ) => setUseSessionBranding ( ( v ) => ! v ) }
697- className = { `rounded-lg px-3 py-1.5 text-xs font-semibold transition-colors ${ useSessionBranding
698- ? "bg-mint text-black"
699- : "border border-white/20 text-slate-300"
700- } `}
738+ className = { `rounded-lg px-3 py-1.5 text-xs font-semibold transition-colors ${
739+ useSessionBranding
740+ ? "bg-mint text-black"
741+ : "border border-white/20 text-slate-300"
742+ } `}
701743 >
702744 { useSessionBranding ? t ( "enabled" ) : t ( "disabled" ) }
703745 </ button >
0 commit comments