diff --git a/frontend/src/__tests__/components.test.tsx b/frontend/src/__tests__/components.test.tsx index d8a4a57c..870ee22d 100644 --- a/frontend/src/__tests__/components.test.tsx +++ b/frontend/src/__tests__/components.test.tsx @@ -146,6 +146,19 @@ describe('RecipientStep', () => { fireEvent.blur(screen.getByRole('textbox')); expect(onChange).toHaveBeenCalledWith(VALID_KEY); }); + + it('shows immediate inline error when recipient equals connected sender wallet address', () => { + render( + , + ); + expect(screen.getByRole('alert')).toHaveTextContent( + 'Recipient address cannot be the same as your connected sender wallet', + ); + }); }); // ─── AmountStep ────────────────────────────────────────────────────────────── diff --git a/frontend/src/components/stream-creation/RecipientStep.tsx b/frontend/src/components/stream-creation/RecipientStep.tsx index 8e91074d..ebe71d77 100644 --- a/frontend/src/components/stream-creation/RecipientStep.tsx +++ b/frontend/src/components/stream-creation/RecipientStep.tsx @@ -6,17 +6,32 @@ interface RecipientStepProps { value: string; onChange: (value: string) => void; error?: string; + senderAddress?: string | null; } const STELLAR_PUBLIC_KEY_LENGTH = 56; +function getPersistedWalletPublicKey(): string | undefined { + if (typeof window === "undefined") return undefined; + try { + const raw = window.localStorage.getItem("flowfi.wallet.session.v1"); + if (!raw) return undefined; + const parsed = JSON.parse(raw) as { publicKey?: unknown }; + return typeof parsed?.publicKey === "string" ? parsed.publicKey : undefined; + } catch { + return undefined; + } +} + export const RecipientStep: React.FC = ({ value, onChange, error, + senderAddress, }) => { const inputRef = useRef(null); const [touched, setTouched] = useState(false); + const effectiveSenderAddress = senderAddress ?? getPersistedWalletPublicKey(); // Auto-focus on mount useEffect(() => { @@ -24,13 +39,19 @@ export const RecipientStep: React.FC = ({ }, []); const trimmed = value.trim(); + const normalizedSender = effectiveSenderAddress?.trim(); + const isSameAsSender = + Boolean(normalizedSender) && + trimmed.length > 0 && + trimmed.toLowerCase() === normalizedSender!.toLowerCase(); // Real-time feedback: validate once the user has committed the field - // (blur / paste) or entered a full-length key, so partial typing does not - // flash errors while still catching bad checksums immediately. + // (blur / paste), entered a full-length key, or matched the connected sender, + // so partial typing does not flash errors while still catching bad checksums + // and self-streams immediately. const liveError = - touched || trimmed.length >= STELLAR_PUBLIC_KEY_LENGTH - ? validateRecipient(value) + touched || trimmed.length >= STELLAR_PUBLIC_KEY_LENGTH || isSameAsSender + ? validateRecipient(value, effectiveSenderAddress) : null; // The wizard-provided error (set on step validation) takes precedence. diff --git a/frontend/src/components/stream-creation/StreamCreationWizard.tsx b/frontend/src/components/stream-creation/StreamCreationWizard.tsx index f6b259cf..1e74c72d 100644 --- a/frontend/src/components/stream-creation/StreamCreationWizard.tsx +++ b/frontend/src/components/stream-creation/StreamCreationWizard.tsx @@ -19,7 +19,10 @@ import { useStreamForm, type StreamFormData, } from "@/hooks/useStreamForm"; -import { validateAmount } from "@/lib/stream-validation"; +import { + SAME_SENDER_RECIPIENT_ERROR, + validateAmount, +} from "@/lib/stream-validation"; // Re-export StreamFormData so existing imports keep working export type { StreamFormData }; @@ -97,6 +100,15 @@ export const StreamCreationWizard: React.FC = ({ const amountValid = amountError === null; const canSubmit = !isSubmitting && amountValid; + const isSelfRecipient = + Boolean(walletPublicKey?.trim()) && + formData.recipient.trim().length > 0 && + formData.recipient.trim().toLowerCase() === + walletPublicKey!.trim().toLowerCase(); + const recipientError = + errors.recipient ?? + (isSelfRecipient ? SAME_SENDER_RECIPIENT_ERROR : undefined); + const handleApplyTemplate = (templateId: string) => { const msg = applyTemplateHook(templateId); if (msg) setTemplateSaveMessage(msg); @@ -260,7 +272,8 @@ export const StreamCreationWizard: React.FC = ({ updateFormData({ recipient: value })} - error={errors.recipient} + error={recipientError} + senderAddress={walletPublicKey} /> ); case 3: @@ -469,7 +482,10 @@ export const StreamCreationWizard: React.FC = ({ Cancel {currentStep < STEPS.length ? ( -