Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions frontend/components/chain-selector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { render, screen } from '@testing-library/react';
import { ChainSelector } from './chain-selector';

describe('ChainSelector', () => {
it('renders Stellar as supported active network', () => {
render(<ChainSelector selectedChainId="stellar" />);
const option = screen.getByRole('option', { name: /Stellar \(XLM\)/i });
expect(option).toBeInTheDocument();
expect(option).not.toBeDisabled();
});

it('renders future EVM and Soroban chains as disabled stubs with Coming Soon badges', () => {
render(<ChainSelector selectedChainId="stellar" />);
const ethOption = screen.getByRole('option', { name: /Ethereum \(ETH\) — \[Coming Soon\]/i });
expect(ethOption).toBeInTheDocument();
expect(ethOption).toBeDisabled();

const polygonOption = screen.getByRole('option', { name: /Polygon \(MATIC\) — \[Coming Soon\]/i });
expect(polygonOption).toBeInTheDocument();
expect(polygonOption).toBeDisabled();
});
});
76 changes: 49 additions & 27 deletions frontend/components/chain-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,55 @@
export function ChainSelector() {
'use client';

export interface ChainOption {
id: string;
name: string;
symbol: string;
isSupported: boolean;
badge?: string;
}

export const SUPPORTED_CHAINS: ChainOption[] = [
{ id: 'stellar', name: 'Stellar', symbol: 'XLM', isSupported: true },
{ id: 'ethereum', name: 'Ethereum', symbol: 'ETH', isSupported: false, badge: 'Coming Soon' },
{ id: 'polygon', name: 'Polygon', symbol: 'MATIC', isSupported: false, badge: 'Coming Soon' },
{ id: 'soroban', name: 'Soroban Smart Contracts', symbol: 'XLM', isSupported: false, badge: 'Coming Soon' },
];

type ChainSelectorProps = {
selectedChainId: string;
onSelectChain?: (chainId: string) => void;
label?: string;
};

export function ChainSelector({
selectedChainId = 'stellar',
onSelectChain,
label = 'Select Network / Chain',
}: ChainSelectorProps) {
return (
<div className="space-y-1">
<label htmlFor="chain-select" className="block text-sm font-medium text-slate-700">
Network
<div className="space-y-1.5">
<label htmlFor="chain-selector" className="block text-sm font-medium text-slate-900">
{label}
</label>
<div className="relative group">
<select
id="chain-select"
disabled
className="block w-full appearance-none rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-500 cursor-not-allowed focus:outline-none"
>
<option value="stellar">Stellar Network</option>
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-slate-400">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
{/* Tooltip for the disabled state */}
<div className="absolute hidden group-hover:block bottom-full mb-2 left-1/2 -translate-x-1/2 w-max rounded bg-slate-800 px-2 py-1 text-xs text-white">
More chains coming soon
<svg
className="absolute top-full left-1/2 -translate-x-1/2 text-slate-800 h-2 w-2"
viewBox="0 0 255 255"
<select
id="chain-selector"
value={selectedChainId}
onChange={(e) => onSelectChain?.(e.target.value)}
className="block w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-slate-500 focus:outline-none focus:ring-1 focus:ring-slate-500"
>
{SUPPORTED_CHAINS.map((chain) => (
<option
key={chain.id}
value={chain.id}
disabled={!chain.isSupported}
>
<polygon className="fill-current" points="0,0 127.5,127.5 255,0" />
</svg>
</div>
</div>
{chain.name} ({chain.symbol}) {!chain.isSupported ? `— [${chain.badge}]` : ''}
</option>
))}
</select>
<p className="text-xs text-slate-500">
Bridgelet currently operates on the <strong>Stellar</strong> network. Multi-chain EVM & Soroban support is under development.
</p>
</div>
);
}
12 changes: 7 additions & 5 deletions frontend/components/send-form/steps/details-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import { useEffect, useState } from 'react';
import type { SendFormState } from '../index';
import { ChainSelector } from '../../chain-selector';
import { getXlmUsdRate, formatFiat } from '@/lib/xlm-price';

const SUPPORTED_ASSETS = ['XLM', 'USDC'] as const;

// Simple format check — the backend performs authoritative validation.
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

type FieldErrors = {
Expand Down Expand Up @@ -44,11 +44,11 @@
};

export function DetailsStep({ state, onChange, onBack, onNext }: DetailsStepProps) {
const [selectedChain, setSelectedChain] = useState('stellar');
const [errors, setErrors] = useState<FieldErrors>({});
const [touched, setTouched] = useState(false);
const [usdRate, setUsdRate] = useState<number | null>(null);

// Fetch the XLM/USD rate once on mount; the helper caches for 60s.
useEffect(() => {
let cancelled = false;
getXlmUsdRate().then((rate) => {
Expand All @@ -59,12 +59,9 @@
};
}, []);

// Re-validate on every change once the user has attempted to submit,
// so error messages clear as soon as the input becomes valid.
useEffect(() => {
if (touched) setErrors(validateDetails(state));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.recipientEmail, state.amountXlm, state.assetCode, touched]);

Check warning on line 64 in frontend/components/send-form/steps/details-step.tsx

View workflow job for this annotation

GitHub Actions / Lint, type-check, test, and build

React Hook useEffect has a missing dependency: 'state'. Either include it or remove the dependency array. If 'setErrors' needs the current value of 'state', you can also switch to useReducer instead of useState and read 'state' in the reducer

function handleSubmit(e: React.FormEvent) {
e.preventDefault();
Expand All @@ -80,6 +77,11 @@

return (
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
<ChainSelector
selectedChainId={selectedChain}
onSelectChain={setSelectedChain}
/>

<div>
<label htmlFor="recipient-name" className="block text-sm font-medium text-slate-900">
Recipient name <span className="font-normal text-slate-500">(optional)</span>
Expand Down
Loading