Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c3c0a80
feature: key value for address disable banner
vsevolod-zhuravlov Jan 13, 2026
59cb88e
fix: show wrap only for deposit & mint
vsevolod-zhuravlov Jan 13, 2026
aa24f44
fix: hide wrap preview on warning or erorr
vsevolod-zhuravlov Jan 13, 2026
1b19549
fix: window between input and preview
vsevolod-zhuravlov Jan 15, 2026
c6bbf72
fix: build error
vsevolod-zhuravlov Jan 15, 2026
9428014
fix: button blick issue
vsevolod-zhuravlov Jan 15, 2026
de4d62c
feature: custom rpc setting
vsevolod-zhuravlov Jan 16, 2026
877c6f9
fix: insufficient balance blick
vsevolod-zhuravlov Jan 16, 2026
e1cfb2a
fix: remove unnecessary bool var
vsevolod-zhuravlov Jan 30, 2026
9bba083
fix: simplify reset logic
vsevolod-zhuravlov Jan 30, 2026
61a4bf0
fix: rewrite boolean logic
vsevolod-zhuravlov Jan 30, 2026
3719baf
fix: alternative fix
Cycxyz Feb 2, 2026
1e32866
Revert "fix: insufficient balance blick"
vsevolod-zhuravlov Feb 3, 2026
4e0c072
Merge branch 'alternative_insufficient_balance_blick' into fix/insuff…
vsevolod-zhuravlov Feb 3, 2026
4e613ae
fix: insufficient balance blick issue
vsevolod-zhuravlov Feb 3, 2026
cbef28f
fix: move necessary to configs
vsevolod-zhuravlov Feb 3, 2026
d09f9bd
fix: getDefaultUrl
vsevolod-zhuravlov Feb 3, 2026
84322b2
fix: remove unused part
vsevolod-zhuravlov Feb 3, 2026
729aeb5
change default to ethereum
vsevolod-zhuravlov Feb 3, 2026
9f67e15
fix: rewrite dublication
vsevolod-zhuravlov Feb 3, 2026
a93a239
update missing dependencies
vsevolod-zhuravlov Feb 4, 2026
536f657
fix: remove window reloading
vsevolod-zhuravlov Feb 4, 2026
f632940
fix: some nice fixes
vsevolod-zhuravlov Feb 10, 2026
42dfb6e
fix: rpc scrollbar color
vsevolod-zhuravlov Feb 10, 2026
97e19f3
Merge branch 'fix/hide-wrap-preview-on-error' into integration/order-…
vsevolod-zhuravlov Feb 11, 2026
dfed75e
Merge branch 'fix/window-between-input-and-preview' into integration/…
vsevolod-zhuravlov Feb 11, 2026
c7a9c20
Merge branch 'fix/insufficient-balance-blick' into integration/order-…
vsevolod-zhuravlov Feb 11, 2026
c013f26
Merge branch 'fix/button-blick-issue' into integration/order-test
vsevolod-zhuravlov Feb 11, 2026
d052675
Merge branch 'feature/key-value-disable-for-banner' into integration/…
vsevolod-zhuravlov Feb 11, 2026
31876b5
feature: disallow switching tabs while processing
vsevolod-zhuravlov Jan 13, 2026
59a1574
fix: refreshBalances try catch
vsevolod-zhuravlov Jan 30, 2026
263492f
fix: rewrite tabs component to more simple props
vsevolod-zhuravlov Feb 10, 2026
cf23e58
fix: is input zero or nan
vsevolod-zhuravlov Jan 15, 2026
11f0c63
feature: lens is lens
vsevolod-zhuravlov Jan 14, 2026
8f3dc1c
Merge branch 'fix/lens-is-lens' into feature/custom-rpc-setting
vsevolod-zhuravlov Feb 11, 2026
2ff0f52
fix: build error
vsevolod-zhuravlov Feb 11, 2026
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
34 changes: 33 additions & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
import { useState, useEffect, useRef } from "react";
import ConnectWallet from "./connect/ConnectWallet";
import Container from "./ui/Container";
import SettingsPopup from "./SettingsPopup";

export default function Header() {
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const settingsRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (settingsRef.current && !settingsRef.current.contains(event.target as Node)) {
setIsSettingsOpen(false);
}
};

if (isSettingsOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [isSettingsOpen]);

return (
<header className="relative z-50 bg-white/80 h-16 flex-shrink-0">
<Container>
<div className="w-full h-full flex justify-between items-center text-gray-700">
<div className="text-black font-semibold text-lg [@media(min-width:640px)]:text-xl">LTV Protocol</div>
<ConnectWallet />

<div className="flex items-center gap-2 relative" ref={settingsRef}>
<ConnectWallet />
<button
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
className="p-2 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-colors focus:outline-none bg-white border border-gray-300"
aria-label="Settings"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<SettingsPopup isOpen={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
</div>
</div>
</Container>
</header>
Expand Down
63 changes: 63 additions & 0 deletions src/components/SettingsPopup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useState, useEffect } from 'react';
import { MainView } from './settings/MainView';
import { NetworkSelectView } from './settings/NetworkSelectView';
import { RpcConfigView } from './settings/RpcConfigView';

interface SettingsPopupProps {
isOpen: boolean;
onClose: () => void;
}

type View = 'main' | 'network-select' | 'rpc-config';

export default function SettingsPopup({ isOpen, onClose }: SettingsPopupProps) {
const [currentView, setCurrentView] = useState<View>('main');
const [selectedNetwork, setSelectedNetwork] = useState<string | null>(null);

useEffect(() => {
if (!isOpen) {
// Reset navigation state when closed
setCurrentView('main');
setSelectedNetwork(null);
}
}, [isOpen]);

if (!isOpen) return null;

const handleNetworkSelect = (chainId: string) => {
setSelectedNetwork(chainId);
setCurrentView('rpc-config');
};

return (
<div className="
absolute top-[65px] right-0 bg-white rounded-lg
min-w-[320px] z-50 overflow-hidden shadow-lg border border-gray-100
">
<div className="p-4">
{currentView === 'main' && (
<MainView
onNavigateToNetworkSelect={() => setCurrentView('network-select')}
onClose={onClose}
/>
)}

{currentView === 'network-select' && (
<NetworkSelectView
onSelectNetwork={handleNetworkSelect}
onBack={() => setCurrentView('main')}
onClose={onClose}
/>
)}

{currentView === 'rpc-config' && selectedNetwork && (
<RpcConfigView
selectedNetwork={selectedNetwork}
onBack={() => setCurrentView('network-select')}
onClose={onClose}
/>
)}
</div>
</div>
);
}
3 changes: 1 addition & 2 deletions src/components/actions/ActionHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default function ActionHandler({ actionType, tokenType }: ActionHandlerPr
const displayTokenSymbol = config.usesShares ? sharesSymbol : formatTokenSymbol(tokenSymbol);
const displayDecimals = config.usesShares ? sharesDecimals : tokenDecimals;

const { isLoadingPreview, previewData, receive, provide } = useActionPreview({
const { previewData, receive, provide } = useActionPreview({
amount,
actionType,
tokenType,
Expand Down Expand Up @@ -314,7 +314,6 @@ export default function ActionHandler({ actionType, tokenType }: ActionHandlerPr
<PreviewBox
receive={receive}
provide={provide}
isLoading={isLoadingPreview}
title="Transaction Preview"
/>
) : undefined
Expand Down
3 changes: 1 addition & 2 deletions src/components/actions/SafeActionHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export default function SafeActionHandler({ actionType, tokenType }: SafeActionH
const displayTokenSymbol = config.usesShares ? sharesSymbol : formatTokenSymbol(tokenSymbol);
const displayDecimals = config.usesShares ? sharesDecimals : tokenDecimals;

const { isLoadingPreview, previewData, receive, provide } = useActionPreview({
const { previewData, receive, provide } = useActionPreview({
amount,
actionType,
tokenType,
Expand Down Expand Up @@ -458,7 +458,6 @@ export default function SafeActionHandler({ actionType, tokenType }: SafeActionH
<PreviewBox
receive={receive}
provide={provide}
isLoading={isLoadingPreview}
title="Transaction Preview"
/>
) : undefined
Expand Down
23 changes: 23 additions & 0 deletions src/components/settings/MainView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SettingsHeader } from './SettingsHeader';

interface MainViewProps {
onNavigateToNetworkSelect: () => void;
onClose: () => void;
}

export function MainView({ onNavigateToNetworkSelect, onClose }: MainViewProps) {
return (
<div className="space-y-2">
<SettingsHeader title="Settings" onClose={onClose} />
<button
onClick={onNavigateToNetworkSelect}
className="w-full text-left px-4 py-3 rounded-lg bg-white hover:bg-gray-50 flex justify-between items-center transition-colors border border-gray-100"
>
<span className="font-medium text-gray-700">Set Custom RPC</span>
<svg className="w-5 h-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
</div>
);
};
27 changes: 27 additions & 0 deletions src/components/settings/NetworkSelectView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SettingsHeader } from './SettingsHeader';
import { NETWORKS_LIST } from '@/constants';

interface NetworkSelectViewProps {
onSelectNetwork: (chainId: string) => void;
onBack: () => void;
onClose: () => void;
}

export function NetworkSelectView({ onSelectNetwork, onBack, onClose }: NetworkSelectViewProps) {
return (
<div className="space-y-2">
<SettingsHeader title="Select Network" onClose={onClose} onBack={onBack} />

{NETWORKS_LIST.map((network) => (
<button
key={network.chainIdString}
onClick={() => onSelectNetwork(network.chainIdString)}
className="w-full text-left px-4 py-3 rounded-lg bg-white hover:bg-gray-50 flex items-center gap-3 transition-colors border border-gray-100"
>
<div className={`w-2 h-2 rounded-full ${network.color}`}></div>
<span className="text-gray-700">{network.name}</span>
</button>
))}
</div>
);
}
171 changes: 171 additions & 0 deletions src/components/settings/RpcConfigView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { useState, useEffect } from 'react';
import { SettingsHeader } from './SettingsHeader';
import { NETWORK_CONFIGS } from '@/constants';
import { useAppContext } from '@/contexts';
import { JsonRpcProvider } from 'ethers';
import { WarningMessage, SuccessMessage } from '@/components/ui';

interface RpcConfigViewProps {
selectedNetwork: string;
onBack: () => void;
onClose: () => void;
}

export function RpcConfigView({ selectedNetwork, onBack, onClose }: RpcConfigViewProps) {
const { refreshPublicProvider } = useAppContext();

const [rpcUrl, setRpcUrl] = useState<string>('');
const [activeRpcDisplay, setActiveRpcDisplay] = useState<string>('');

const [isCustomRpcSet, setIsCustomRpcSet] = useState(false);

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);

const getDefaultUrl = () => {
return NETWORK_CONFIGS[selectedNetwork]?.rpcUrls[0] ?? '';
};

useEffect(() => {
// Load current RPC
const custom = localStorage.getItem(`custom_rpc_${selectedNetwork}`);
const defaultUrl = getDefaultUrl();

// If custom is set, show it in input. If not, input is empty.
setRpcUrl(custom || '');
// Active is custom if exists, else it's default
setActiveRpcDisplay(custom || defaultUrl);


setIsCustomRpcSet(!!custom);
setError(null);
setSuccess(null);
}, [selectedNetwork]);

const validateAndSave = async () => {
setError(null);
setSuccess(null);
setIsLoading(true);

try {
if (!rpcUrl.startsWith('http://') && !rpcUrl.startsWith('https://')) {
throw new Error('URL must start with http:// or https://');
}

const tempProvider = new JsonRpcProvider(rpcUrl);
const network = await tempProvider.getNetwork();

const returnedChainId = network.chainId.toString();

const networkConfig = NETWORK_CONFIGS[selectedNetwork];
const expectedChainIdBigInt = networkConfig?.chainIdBigInt;

if (expectedChainIdBigInt && network.chainId !== expectedChainIdBigInt) {
throw new Error(`Chain ID mismatch. Expected ${expectedChainIdBigInt}, got ${returnedChainId}`);
}

const currentStored = localStorage.getItem(`custom_rpc_${selectedNetwork}`);
if (currentStored !== rpcUrl) {
localStorage.setItem(`custom_rpc_${selectedNetwork}`, rpcUrl);
await refreshPublicProvider();
}

setIsCustomRpcSet(true);
setActiveRpcDisplay(rpcUrl);
setSuccess('RPC updated successfully!');
} catch (err: any) {
console.error("RPC Validation failed:", err);
const msg = err.message || '';

if (
msg.includes('NetworkError') ||
msg.includes('Failed to fetch') ||
msg.includes('connection refused') ||
err.code === "NETWORK_ERROR"
) {
setError('This RPC URL is not responding.');
} else {
setError('Failed to connect to RPC');
}
} finally {
setIsLoading(false);
}
};

const handleReset = async () => {
setIsLoading(true);
setError(null);

try {
const defaultUrl = getDefaultUrl();
setRpcUrl('');
setActiveRpcDisplay(defaultUrl);

localStorage.removeItem(`custom_rpc_${selectedNetwork}`);
await refreshPublicProvider();
setIsCustomRpcSet(false);
setSuccess('Reset to default successfully.');
} catch (err: any) {
setError('Failed to reset RPC');
} finally {
setIsLoading(false);
}
};

// Determine if save should be disabled
const isInputEmpty = !rpcUrl || rpcUrl.trim() === '';
const isSaveDisabled = isLoading || isInputEmpty;

return (
<div className="space-y-4">
<SettingsHeader title="Configure RPC" onClose={onClose} onBack={onBack} />

<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Active RPC URL for {NETWORK_CONFIGS[selectedNetwork]?.name ?? 'Network'}
</label>
<div
className="mb-2 p-2 bg-gray-50 rounded border border-gray-200 text-xs text-gray-600 font-mono max-w-[286.4px] overflow-x-auto whitespace-nowrap select-text cursor-text"
style={{ colorScheme: 'light' }}
>
{activeRpcDisplay}
</div>

<label className="block text-sm font-medium text-gray-700 mb-1 mt-3">
New Custom RPC URL
</label>
<input
type="text"
value={rpcUrl}
onChange={(e) => setRpcUrl(e.target.value)}
disabled={isLoading}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="https://..."
/>
</div>

{error && <WarningMessage text={error} className="max-w-[286.4px]" />}
{success && <SuccessMessage text={success} className="max-w-[286.4px]" />}

<div className="flex flex-col gap-2">
<button
onClick={validateAndSave}
disabled={isSaveDisabled}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{isLoading ? 'Saving...' : 'Save'}
</button>
{isCustomRpcSet && (
<button
onClick={handleReset}
disabled={isLoading}
className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
Reset to Default
</button>
)}
</div>
</div>
);
};
27 changes: 27 additions & 0 deletions src/components/settings/SettingsHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
interface SettingsHeaderProps {
title: string;
onClose: () => void;
onBack?: () => void;
}

export function SettingsHeader({ title, onClose, onBack }: SettingsHeaderProps) {
return (
<div className="flex justify-between items-center mb-3">
<div className="flex items-center">
{onBack && (
<button onClick={onBack} className="mr-2 text-gray-500 hover:text-gray-700 bg-white rounded-md p-1 hover:bg-gray-50 border border-transparent hover:border-gray-200">
<svg className="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
)}
<span className="font-medium text-gray-900">{title}</span>
</div>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 bg-white rounded-md p-1 hover:bg-gray-50">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
);
};
Loading