@@ -8,6 +8,7 @@ import CopyButton from "./CopyButton";
88import { toast } from "sonner" ;
99import IntegrationCodeSnippets from "./IntegrationCodeSnippets" ;
1010import Link from "next/link" ;
11+ import { InfoTooltip } from "./InfoTooltip" ;
1112import {
1213 useHydrateMerchantStore ,
1314 useMerchantApiKey ,
@@ -354,6 +355,7 @@ export default function CreatePaymentForm() {
354355 const [ error , setError ] = useState < string | null > ( null ) ;
355356 const [ amountError , setAmountError ] = useState < string | null > ( null ) ;
356357 const [ recipientError , setRecipientError ] = useState < string | null > ( null ) ;
358+ const [ webhookUrlError , setWebhookUrlError ] = useState < string | null > ( null ) ;
357359 const [ created , setCreated ] = useState < CreatedPayment | null > ( null ) ;
358360 const apiKey = useMerchantApiKey ( ) ;
359361 const hydrated = useMerchantHydrated ( ) ;
@@ -384,7 +386,41 @@ export default function CreatePaymentForm() {
384386 label : selectedTrustedAddressLabel ,
385387 } )
386388 : t ( "recipientPlaceholder" , { asset } ) ;
387- const descriptionPlaceholder = t ( "descriptionPlaceholder" , { asset } ) ;
389+ const validateAmount = ( value : string ) => {
390+ const numAmount = parseFloat ( value ) ;
391+ if ( isNaN ( numAmount ) || numAmount <= 0 ) {
392+ return "Amount must be greater than 0." ;
393+ }
394+ return null ;
395+ } ;
396+
397+ const validateRecipient = ( value : string ) => {
398+ if ( ! STELLAR_ADDRESS_RE . test ( value . trim ( ) ) ) {
399+ return "Must be a valid Stellar public key (56 characters, starts with G)." ;
400+ }
401+ return null ;
402+ } ;
403+
404+ const validateWebhookUrl = ( value : string ) => {
405+ const trimmed = value . trim ( ) ;
406+ if ( ! trimmed ) return null ;
407+ try {
408+ const parsed = new URL ( trimmed ) ;
409+ if ( parsed . protocol !== "http:" && parsed . protocol !== "https:" ) {
410+ return "Webhook URL must start with http:// or https://" ;
411+ }
412+ } catch {
413+ return "Enter a valid webhook URL." ;
414+ }
415+ return null ;
416+ } ;
417+
418+ const isFormValid =
419+ ! validateAmount ( amount ) &&
420+ ! validateRecipient ( recipient ) &&
421+ ! validateWebhookUrl ( description ) &&
422+ amount . trim ( ) . length > 0 &&
423+ recipient . trim ( ) . length > 0 ;
388424
389425 // ── Rate-limit countdown ──────────────────────────────────
390426 const [ retryAfter , setRetryAfter ] = useState ( 0 ) ;
@@ -419,19 +455,15 @@ export default function CreatePaymentForm() {
419455 setError ( null ) ;
420456
421457 // Client-side validation
422- let hasError = false ;
458+ const nextAmountError = validateAmount ( amount ) ;
459+ const nextRecipientError = validateRecipient ( recipient ) ;
460+ const nextWebhookUrlError = validateWebhookUrl ( description ) ;
461+ setAmountError ( nextAmountError ) ;
462+ setRecipientError ( nextRecipientError ) ;
463+ setWebhookUrlError ( nextWebhookUrlError ) ;
464+ if ( nextAmountError || nextRecipientError || nextWebhookUrlError ) return ;
465+
423466 const numAmount = parseFloat ( amount ) ;
424- if ( isNaN ( numAmount ) || numAmount <= 0 ) {
425- setAmountError ( "Amount must be greater than 0." ) ;
426- hasError = true ;
427- }
428- if ( ! STELLAR_ADDRESS_RE . test ( recipient . trim ( ) ) ) {
429- setRecipientError (
430- "Must be a valid Stellar public key (56 characters, starts with G)." ,
431- ) ;
432- hasError = true ;
433- }
434- if ( hasError ) return ;
435467
436468 setLoading ( true ) ;
437469 try {
@@ -495,6 +527,7 @@ export default function CreatePaymentForm() {
495527 setError ( null ) ;
496528 setAmountError ( null ) ;
497529 setRecipientError ( null ) ;
530+ setWebhookUrlError ( null ) ;
498531 setRetryAfter ( 0 ) ;
499532 } ;
500533
@@ -637,7 +670,7 @@ export default function CreatePaymentForm() {
637670 value = { amount }
638671 onChange = { ( e ) => {
639672 setAmount ( e . target . value ) ;
640- setAmountError ( null ) ;
673+ setAmountError ( validateAmount ( e . target . value ) ) ;
641674 } }
642675 aria-invalid = { ! ! amountError }
643676 aria-describedby = { amountError ? "amount-error" : undefined }
@@ -724,6 +757,21 @@ export default function CreatePaymentForm() {
724757 className = "text-xs font-medium uppercase tracking-wider text-slate-400"
725758 >
726759 { t ( "recipientAddress" ) }
760+ < InfoTooltip
761+ className = "ml-2"
762+ content = {
763+ < span >
764+ Use a valid Stellar public key that starts with G and is 56
765+ characters long. Example:
766+ < br />
767+ < code className = "text-[11px] text-mint" >
768+ GDQP2KPQGKIH...MBCQ4MMR
769+ </ code >
770+ </ span >
771+ }
772+ >
773+ < span tabIndex = { 0 } > What is this?</ span >
774+ </ InfoTooltip >
727775 </ label >
728776 < input
729777 id = "recipient"
@@ -732,7 +780,7 @@ export default function CreatePaymentForm() {
732780 value = { recipient }
733781 onChange = { ( e ) => {
734782 setRecipient ( e . target . value ) ;
735- setRecipientError ( null ) ;
783+ setRecipientError ( validateRecipient ( e . target . value ) ) ;
736784 } }
737785 aria-invalid = { ! ! recipientError }
738786 aria-describedby = {
@@ -764,15 +812,39 @@ export default function CreatePaymentForm() {
764812 < span className = "normal-case text-slate-600" >
765813 ({ t ( "optional" ) } )
766814 </ span >
815+ < InfoTooltip
816+ className = "ml-2"
817+ content = {
818+ < span >
819+ If you add a webhook URL here, use a full URL like
820+ < br />
821+ < code className = "text-[11px] text-mint" >
822+ https://example.com/api/webhooks/stellar
823+ </ code >
824+ </ span >
825+ }
826+ >
827+ < span tabIndex = { 0 } > Webhook URL help</ span >
828+ </ InfoTooltip >
767829 </ label >
768830 < input
769831 id = "description"
770832 type = "text"
771833 value = { description }
772- onChange = { ( e ) => setDescription ( e . target . value ) }
773- 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"
774- placeholder = { descriptionPlaceholder }
834+ onChange = { ( e ) => {
835+ setDescription ( e . target . value ) ;
836+ setWebhookUrlError ( validateWebhookUrl ( e . target . value ) ) ;
837+ } }
838+ aria-invalid = { Boolean ( webhookUrlError ) }
839+ aria-describedby = { webhookUrlError ? "webhook-url-error" : undefined }
840+ className = { `rounded-xl border bg-white/5 p-3 text-white placeholder:text-slate-600 focus:outline-none focus:ring-1 ${ webhookUrlError ? "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" } ` }
841+ placeholder = "Optional memo or webhook URL (https://...)"
775842 />
843+ { webhookUrlError && (
844+ < p id = "webhook-url-error" className = "text-xs text-red-400" role = "alert" >
845+ { webhookUrlError }
846+ </ p >
847+ ) }
776848 </ div >
777849
778850 { /* Branding panel */ }
@@ -860,7 +932,7 @@ export default function CreatePaymentForm() {
860932 { /* Submit */ }
861933 < button
862934 type = "submit"
863- disabled = { loading }
935+ disabled = { loading || ! isFormValid }
864936 className = "group relative flex h-12 items-center justify-center rounded-xl bg-mint px-6 font-bold text-black transition-all hover:bg-glow disabled:cursor-not-allowed disabled:opacity-50"
865937 >
866938 { loading ? (
0 commit comments