diff --git a/apps/extension-wallet/eslint.config.cjs b/apps/extension-wallet/eslint.config.cjs index 0d7e2f9..5b0a5a1 100644 --- a/apps/extension-wallet/eslint.config.cjs +++ b/apps/extension-wallet/eslint.config.cjs @@ -11,20 +11,12 @@ module.exports = [ files: ['**/*.{ts,tsx}'], languageOptions: { parser: tsparser, - globals: { - ...globals.browser, - ...globals.vitest, - }, parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true, }, - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module', - ecmaFeatures: { jsx: true }, }, globals: { ...globals.browser, @@ -33,34 +25,34 @@ module.exports = [ }, plugins: { '@typescript-eslint': tseslint, - 'react': react, + react, 'react-hooks': reactHooks, }, rules: { ...tseslint.configs.recommended.rules, ...react.configs.recommended.rules, ...reactHooks.configs.recommended.rules, - 'react/react-in-jsx-scope': 'off', // Not needed in React 18+ - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + 'no-undef': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], }, settings: { react: { version: 'detect', }, }, - }, - rules: { - ...tseslint.configs.recommended.rules, - 'no-undef': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], - }, }, { files: ['**/__tests__/**/*.{ts,tsx}', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'], languageOptions: { globals: { ...globals.jest, + ...globals.vitest, vi: 'readonly', }, }, diff --git a/apps/extension-wallet/package.json b/apps/extension-wallet/package.json index a04cf80..46abbff 100644 --- a/apps/extension-wallet/package.json +++ b/apps/extension-wallet/package.json @@ -19,9 +19,10 @@ "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-error-boundary": "^6.1.1" + "react-error-boundary": "^6.1.1", + "react-router-dom": "^6.20.0" }, - "devDependencies": { + "devDependencies": { "@eslint/js": "^9.0.0", "@testing-library/jest-dom": "^6.1.0", "@testing-library/react": "^14.0.0", diff --git a/apps/extension-wallet/src/App.tsx b/apps/extension-wallet/src/App.tsx index e7bdc93..c80987a 100644 --- a/apps/extension-wallet/src/App.tsx +++ b/apps/extension-wallet/src/App.tsx @@ -1,293 +1 @@ -/** - * App Component Example - * - * Demonstrates how to use ErrorBoundary and error-handler in a React application. - * This example shows: - * 1. Wrapping the app with ErrorBoundary - * 2. Using error-handler in async functions - * 3. Implementing retry functionality - * 4. Handling different error categories - */ - -import { useState, useEffect, useCallback } from 'react'; -import { - ErrorBoundary, - useErrorHandler, - handleError, - withErrorHandling, - createRetryable, -} from './errors'; - -/** - * Sample data type - */ -interface UserData { - id: string; - name: string; - balance: string; -} - -/** - * Example component that fetches data - demonstrates async error handling - * Uses the error-handler to classify and log errors - */ -function DataFetcher(): JSX.Element { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - // Use the error handler hook for manual error dispatching - const { reset } = useErrorHandler(); - - const fetchData = useCallback(async () => { - setLoading(true); - setError(null); - - try { - // Simulate a network request that might fail - const response = await fetch('/api/user'); - - if (!response.ok) { - // Use the global error handler to classify the error - const errorInfo = handleError( - new Error(`HTTP ${response.status}: ${response.statusText}`), - 'fetchUserData' - ); - throw new Error(errorInfo.message); - } - - const userData = await response.json(); - setData(userData); - } catch (err) { - const handledError = handleError(err, 'fetchUserData'); - - // Log the error (handled by error-handler internally) - console.log('Error category:', handledError.category); - console.log('Recoverable:', handledError.recoverable); - - setError(handledError.originalError as Error); - } finally { - setLoading(false); - } - }, []); - - // Initial fetch on mount - useEffect(() => { - fetchData(); - }, [fetchData]); - - if (error) { - return ( -
-

Error: {error.message}

-
- - -
-
- ); - } - - if (loading) { - return
Loading...
; - } - - return ( -
-

User Data

- {data && ( -
    -
  • ID: {data.id}
  • -
  • Name: {data.name}
  • -
  • Balance: {data.balance}
  • -
- )} - -
- ); -} - -/** - * Example component using withErrorHandling HOC - * Wraps an async function with automatic error handling - */ -async function fetchUserBalance(userId: string): Promise { - // Simulate network call - const response = await fetch(`/api/balance/${userId}`); - - if (!response.ok) { - throw new Error('Failed to fetch balance'); - } - - const data = await response.json(); - return data.balance; -} - -// Wrap function with error handling (example utility export usage) -void withErrorHandling(fetchUserBalance as any, 'fetchUserBalance'); - -/** - * Example component using createRetryable - * Creates a function that automatically retries on failure - */ -async function submitTransaction(txData: object): Promise<{ txHash: string }> { - // Simulate transaction submission - const response = await fetch('/api/submit', { - method: 'POST', - body: JSON.stringify(txData), - }); - - if (!response.ok) { - throw new Error('Transaction failed'); - } - - return response.json(); -} - -// Create a retryable version that retries up to 3 times -const submitTransactionWithRetry = createRetryable(submitTransaction as any, 3, 1000); - -/** - * Transaction component - demonstrates retry functionality - */ -function TransactionComponent(): JSX.Element { - const [status, setStatus] = useState('idle'); - const [txHash, setTxHash] = useState(null); - - const handleSubmit = async () => { - setStatus('submitting'); - - const result = (await submitTransactionWithRetry({ amount: 100 })) as { txHash: string }; - - if ('txHash' in result) { - setTxHash(result.txHash); - setStatus('success'); - } else { - setStatus('failed'); - } - }; - - return ( -
-

Transaction

-

Status: {status}

- {txHash &&

Tx Hash: {txHash}

} - -
- ); -} - -/** - * Main App component - wrapped with ErrorBoundary - * The ErrorBoundary will catch any rendering errors in children - */ -export function App(): JSX.Element { - // Callback for handling errors that escape component boundaries - const handleAppError = (error: Error, errorInfo: React.ErrorInfo) => { - console.error('App-level error:', error, errorInfo.componentStack); - }; - - // Callback for resetting app state - const handleReset = () => { - console.log('App reset requested'); - }; - - return ( -
-

Extension Wallet

- - {/* Wrap the entire app with ErrorBoundary */} - -
- {/* Example 1: Data fetching with manual error handling */} -
-

Data Fetcher Example

- -
- - {/* Example 2: Transaction with retry */} -
-

Transaction Example (with retry)

- -
- - {/* Example 3: Direct error handler usage */} -
-

Direct Error Handler Example

- -
-
-
-
- ); -} - -/** - * Component demonstrating direct use of error-handler - */ -function DirectErrorExample(): JSX.Element { - const [result, setResult] = useState(null); - - const testNetworkError = () => { - const errorInfo = handleError(new Error('ECONNREFUSED: Connection refused'), 'networkTest'); - setResult(`Category: ${errorInfo.category}, Recoverable: ${errorInfo.recoverable}`); - }; - - const testValidationError = () => { - const errorInfo = handleError( - new Error('validation failed: invalid address'), - 'validationTest' - ); - setResult(`Category: ${errorInfo.category}, Recoverable: ${errorInfo.recoverable}`); - }; - - const testContractError = () => { - const errorInfo = handleError(new Error('Contract: execution reverted'), 'contractTest'); - setResult(`Category: ${errorInfo.category}, Recoverable: ${errorInfo.recoverable}`); - }; - - const testUnknownError = () => { - const errorInfo = handleError(new Error('Something unexpected'), 'unknownTest'); - setResult(`Category: ${errorInfo.category}, Recoverable: ${errorInfo.recoverable}`); - }; - - return ( -
-
- - - - -
- {result &&

{result}

} -
- ); -} - -export default App; +export { ExtensionRouter as App } from './router'; diff --git a/apps/extension-wallet/src/components/Navigation/NavBar.tsx b/apps/extension-wallet/src/components/Navigation/NavBar.tsx new file mode 100644 index 0000000..b072f53 --- /dev/null +++ b/apps/extension-wallet/src/components/Navigation/NavBar.tsx @@ -0,0 +1,41 @@ +import { ArrowDownLeft, ArrowUpRight, History, Home, Settings } from 'lucide-react'; +import { NavLink } from 'react-router-dom'; + +const items = [ + { to: '/home', label: 'Home', icon: Home }, + { to: '/send', label: 'Send', icon: ArrowUpRight }, + { to: '/receive', label: 'Receive', icon: ArrowDownLeft }, + { to: '/history', label: 'History', icon: History }, + { to: '/settings', label: 'Settings', icon: Settings }, +]; + +export function NavBar() { + return ( + + ); +} diff --git a/apps/extension-wallet/src/components/PaymentQRCode.tsx b/apps/extension-wallet/src/components/PaymentQRCode.tsx index ee323ce..b608685 100644 --- a/apps/extension-wallet/src/components/PaymentQRCode.tsx +++ b/apps/extension-wallet/src/components/PaymentQRCode.tsx @@ -1,4 +1,3 @@ -import * as React from 'react'; import { QRCodeSVG } from 'qrcode.react'; import { cn } from '@ancore/ui-kit'; @@ -22,11 +21,7 @@ export interface PaymentQRCodeProps { * Uses qrcode.react under the hood with a styled card border so it fits * the extension-wallet design system. */ -export function PaymentQRCode({ - value, - size = 220, - className, -}: PaymentQRCodeProps) { +export function PaymentQRCode({ value, size = 220, className }: PaymentQRCodeProps) { return (
* ``` */ -export function ErrorBoundary({ children, fallback, onError }: ErrorBoundaryProps): JSX.Element { +export function ErrorBoundary({ children, fallback, onError }: ErrorBoundaryProps): ReactElement { // If there's a custom fallback component provided, use it if (fallback) { return ( @@ -88,7 +88,7 @@ function ErrorFallback({ resetErrorBoundary, onError, customFallback, -}: ErrorFallbackProps): JSX.Element { +}: ErrorFallbackProps): ReactElement { // Convert unknown error to Error object const err = error && typeof error === 'object' && 'message' in error @@ -166,10 +166,10 @@ export function useErrorHandler() { * ``` */ export function withErrorBoundary

( - Component: React.ComponentType

, + Component: ComponentType

, errorBoundaryProps?: Partial -): React.ComponentType

{ - return function WrappedComponent(props: P): JSX.Element { +): ComponentType

{ + return function WrappedComponent(props: P): ReactElement { return ( @@ -192,7 +192,7 @@ interface ErrorBoundaryResetProps { * A button component that resets the ErrorBoundary when clicked * Useful for triggering error recovery */ -export function ErrorBoundaryReset({ children, className }: ErrorBoundaryResetProps): JSX.Element { +export function ErrorBoundaryReset({ children, className }: ErrorBoundaryResetProps): ReactElement { const { reset } = useErrorHandler(); return ( diff --git a/apps/extension-wallet/src/errors/ErrorScreen.tsx b/apps/extension-wallet/src/errors/ErrorScreen.tsx index 1adf2d3..4b6847c 100644 --- a/apps/extension-wallet/src/errors/ErrorScreen.tsx +++ b/apps/extension-wallet/src/errors/ErrorScreen.tsx @@ -7,7 +7,7 @@ import { Button } from '@ancore/ui-kit'; import { AlertTriangle, RotateCcw, RefreshCw, Info } from 'lucide-react'; -import { ReactNode } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import { getErrorMessage, ErrorCategory } from './error-messages'; import { ErrorInfo, handleError } from './error-handler'; @@ -54,7 +54,7 @@ export function ErrorScreen({ children, showDetails = false, className, -}: ErrorScreenProps): JSX.Element { +}: ErrorScreenProps): ReactElement { // Get user-friendly message from error info or fall back to defaults const userMessage = errorInfo ? getErrorMessage(errorInfo.category, errorInfo.code) @@ -187,7 +187,7 @@ export function ErrorCard({ onRetry, variant = 'error', className, -}: ErrorCardProps): JSX.Element { +}: ErrorCardProps): ReactElement { const variantStyles = { error: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800', warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800', @@ -261,7 +261,7 @@ export function AsyncErrorHandler({ onRetry, onReset, compact = false, -}: AsyncErrorHandlerProps): JSX.Element { +}: AsyncErrorHandlerProps): ReactElement { // Handle the error through our error handler const errorInfo = handleError(error, 'Async operation'); const userMessage = getErrorMessage(errorInfo.category, errorInfo.code); diff --git a/apps/extension-wallet/src/errors/__tests__/errors.test.ts b/apps/extension-wallet/src/errors/__tests__/errors.test.ts index 35f644e..6bd02a7 100644 --- a/apps/extension-wallet/src/errors/__tests__/errors.test.ts +++ b/apps/extension-wallet/src/errors/__tests__/errors.test.ts @@ -99,7 +99,7 @@ describe('ErrorHandler', () => { it('should extract node error code property', () => { const error = new Error('Test error'); - (error as any).code = 'ENOENT'; + (error as Error & { code?: string }).code = 'ENOENT'; const code = errorHandler.extractErrorCode(error); expect(code).toBe('ENOENT'); }); @@ -293,7 +293,7 @@ describe('Recovery functionality', () => { const result = await retryableFn(); expect(result).toHaveProperty('category'); - expect((result as any).category).toBe(ErrorCategory.VALIDATION); + expect((result as { category: ErrorCategory }).category).toBe(ErrorCategory.VALIDATION); }); }); }); diff --git a/apps/extension-wallet/src/index.ts b/apps/extension-wallet/src/index.ts index 6bc3fe4..e01902e 100644 --- a/apps/extension-wallet/src/index.ts +++ b/apps/extension-wallet/src/index.ts @@ -1,5 +1,8 @@ export * from './components/TransactionStatus'; +export * from './components/Navigation/NavBar'; export * from './components/PaymentQRCode'; +export * from './router'; +export * from './router/AuthGuard'; export * from './screens/TransactionDetail'; export * from './screens/ReceiveScreen'; export * from './utils/explorer-links'; diff --git a/apps/extension-wallet/src/main.tsx b/apps/extension-wallet/src/main.tsx index a3da5de..129b48e 100644 --- a/apps/extension-wallet/src/main.tsx +++ b/apps/extension-wallet/src/main.tsx @@ -1,46 +1,13 @@ -import React, { useState } from 'react'; +import React from 'react'; import ReactDOM from 'react-dom/client'; import { NotificationProvider } from '@ancore/ui-kit'; -import { ReceiveScreen } from './screens/ReceiveScreen'; -import { SettingsScreen } from './screens/Settings/SettingsScreen'; +import { ExtensionRouter } from './router'; import './index.css'; -function App() { - const [view, setView] = useState<'receive' | 'settings'>('receive'); - const [network, setNetwork] = useState<'mainnet' | 'testnet' | 'futurenet'>('testnet'); - - return ( -

- {/* Simple Navigation for development */} -
- - -
- - {view === 'receive' ? ( - setView('settings')} - /> - ) : ( - - )} -
- ); -} - ReactDOM.createRoot(document.getElementById('root')!).render( - + -); \ No newline at end of file +); diff --git a/apps/extension-wallet/src/router/AuthGuard.tsx b/apps/extension-wallet/src/router/AuthGuard.tsx new file mode 100644 index 0000000..2b69d6b --- /dev/null +++ b/apps/extension-wallet/src/router/AuthGuard.tsx @@ -0,0 +1,153 @@ +import * as React from 'react'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; + +export const AUTH_STORAGE_KEY = 'ancore_extension_auth'; + +export interface AuthState { + hasOnboarded: boolean; + isUnlocked: boolean; + walletName: string; + accountAddress: string; +} + +export const DEFAULT_AUTH_STATE: AuthState = { + hasOnboarded: false, + isUnlocked: false, + walletName: 'Ancore Wallet', + accountAddress: 'GCFX...WALLET', +}; + +interface AuthContextValue { + authState: AuthState; + completeOnboarding: (walletName: string) => void; + unlockWallet: () => void; + lockWallet: () => void; + resetWallet: () => void; +} + +const AuthContext = React.createContext(null); + +export function readAuthState(): AuthState { + if (typeof window === 'undefined') { + return DEFAULT_AUTH_STATE; + } + + try { + const raw = window.localStorage.getItem(AUTH_STORAGE_KEY); + if (!raw) { + return DEFAULT_AUTH_STATE; + } + + return { + ...DEFAULT_AUTH_STATE, + ...JSON.parse(raw), + }; + } catch { + return DEFAULT_AUTH_STATE; + } +} + +function writeAuthState(authState: AuthState): void { + window.localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(authState)); +} + +export function ExtensionAuthProvider({ children }: { children: React.ReactNode }) { + const [authState, setAuthState] = React.useState(readAuthState); + + React.useEffect(() => { + writeAuthState(authState); + }, [authState]); + + React.useEffect(() => { + function handleStorage(event: StorageEvent) { + if (event.key === AUTH_STORAGE_KEY) { + setAuthState(readAuthState()); + } + } + + window.addEventListener('storage', handleStorage); + return () => window.removeEventListener('storage', handleStorage); + }, []); + + const value = React.useMemo( + () => ({ + authState, + completeOnboarding: (walletName: string) => { + setAuthState({ + hasOnboarded: true, + isUnlocked: true, + walletName: walletName.trim() || DEFAULT_AUTH_STATE.walletName, + accountAddress: DEFAULT_AUTH_STATE.accountAddress, + }); + }, + unlockWallet: () => { + setAuthState((current) => ({ + ...current, + hasOnboarded: true, + isUnlocked: true, + })); + }, + lockWallet: () => { + setAuthState((current) => ({ + ...current, + isUnlocked: false, + })); + }, + resetWallet: () => { + setAuthState(DEFAULT_AUTH_STATE); + }, + }), + [authState] + ); + + return {children}; +} + +export function useExtensionAuth(): AuthContextValue { + const context = React.useContext(AuthContext); + + if (!context) { + throw new Error('useExtensionAuth must be used within ExtensionAuthProvider'); + } + + return context; +} + +export function AuthGuard() { + const { authState } = useExtensionAuth(); + const location = useLocation(); + + if (!authState.hasOnboarded) { + return ; + } + + if (!authState.isUnlocked) { + return ; + } + + return ; +} + +export function PublicOnlyGuard({ + children, + mode, +}: { + children: React.ReactElement; + mode: 'welcome' | 'create-account' | 'unlock'; +}) { + const { authState } = useExtensionAuth(); + + if (mode === 'unlock') { + if (authState.isUnlocked) { + return ; + } + + return children; + } + + if (authState.hasOnboarded) { + return ; + } + + return children; +} diff --git a/apps/extension-wallet/src/router/__tests__/router.test.tsx b/apps/extension-wallet/src/router/__tests__/router.test.tsx new file mode 100644 index 0000000..f4fa3a3 --- /dev/null +++ b/apps/extension-wallet/src/router/__tests__/router.test.tsx @@ -0,0 +1,93 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { AUTH_STORAGE_KEY, DEFAULT_AUTH_STATE } from '../AuthGuard'; +import { ExtensionRouterTestHarness } from '..'; + +function renderRouter(pathname: string, authState = DEFAULT_AUTH_STATE) { + window.localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(authState)); + return render(); +} + +describe('extension router', () => { + beforeEach(() => { + window.localStorage.clear(); + document.title = 'Ancore Extension'; + }); + + it('redirects first-time users to welcome when they hit a protected route', () => { + renderRouter('/home'); + + expect(screen.getByRole('heading', { name: /meet your ancore wallet/i })).toBeInTheDocument(); + expect(document.title).toBe('Welcome | Ancore Extension'); + }); + + it('redirects onboarded locked users to unlock', () => { + renderRouter('/send', { + ...DEFAULT_AUTH_STATE, + hasOnboarded: true, + walletName: 'Locked Wallet', + }); + + expect(screen.getByRole('heading', { name: /unlock wallet/i })).toBeInTheDocument(); + expect(document.title).toBe('Unlock Wallet | Ancore Extension'); + }); + + it('creates an account and lands on the protected home route', async () => { + const user = userEvent.setup(); + renderRouter('/create-account'); + + await user.clear(screen.getByLabelText(/wallet name/i)); + await user.type(screen.getByLabelText(/wallet name/i), 'Router Test Wallet'); + await user.click(screen.getByRole('button', { name: /create wallet/i })); + + expect(await screen.findByRole('heading', { name: /home/i })).toBeInTheDocument(); + expect(screen.getByText(/router test wallet/i)).toBeInTheDocument(); + expect(screen.getByTestId('nav-bar')).toBeInTheDocument(); + }); + + it('navigates between protected routes and updates titles', async () => { + const user = userEvent.setup(); + renderRouter('/home', { + ...DEFAULT_AUTH_STATE, + hasOnboarded: true, + isUnlocked: true, + }); + + await user.click(screen.getByRole('link', { name: /settings/i })); + + expect(await screen.findByRole('heading', { name: /settings/i })).toBeInTheDocument(); + expect(document.title).toBe('Settings | Ancore Extension'); + }); + + it('shows a 404 screen for unknown routes and recovers to the right fallback', async () => { + const user = userEvent.setup(); + renderRouter('/not-a-real-route', { + ...DEFAULT_AUTH_STATE, + hasOnboarded: true, + isUnlocked: true, + }); + + expect(screen.getByRole('heading', { name: '404' })).toBeInTheDocument(); + expect(document.title).toBe('Page Not Found | Ancore Extension'); + + await user.click(screen.getByRole('link', { name: /go back to safety/i })); + + expect(await screen.findByRole('heading', { name: /home/i })).toBeInTheDocument(); + }); + + it('supports back-style navigation for nested routes', async () => { + const user = userEvent.setup(); + renderRouter('/session-keys', { + ...DEFAULT_AUTH_STATE, + hasOnboarded: true, + isUnlocked: true, + }); + + expect(screen.getByRole('heading', { name: /session keys/i })).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: /go back/i })); + + expect(await screen.findByRole('heading', { name: /settings/i })).toBeInTheDocument(); + }); +}); diff --git a/apps/extension-wallet/src/router/index.tsx b/apps/extension-wallet/src/router/index.tsx new file mode 100644 index 0000000..11df158 --- /dev/null +++ b/apps/extension-wallet/src/router/index.tsx @@ -0,0 +1,547 @@ +import * as React from 'react'; +import { + BrowserRouter, + Link, + MemoryRouter, + Navigate, + Outlet, + Route, + Routes, + useLocation, + useNavigate, +} from 'react-router-dom'; +import { + ArrowLeft, + ArrowRight, + Copy, + Lock, + PlusCircle, + ShieldCheck, + Sparkles, + Wallet, +} from 'lucide-react'; +import { NotificationProvider } from '@ancore/ui-kit'; +import { AuthGuard, ExtensionAuthProvider, PublicOnlyGuard, useExtensionAuth } from './AuthGuard'; +import { NavBar } from '../components/Navigation/NavBar'; +import { SettingsScreen } from '../screens/Settings/SettingsScreen'; + +const APP_TITLE = 'Ancore Extension'; + +const pageTitles: Record = { + '/unlock': 'Unlock Wallet', + '/welcome': 'Welcome', + '/create-account': 'Create Account', + '/home': 'Home', + '/send': 'Send', + '/receive': 'Receive', + '/history': 'History', + '/settings': 'Settings', + '/session-keys': 'Session Keys', +}; + +function getPageTitle(pathname: string): string { + return pageTitles[pathname] ?? 'Page Not Found'; +} + +function TitleSync() { + const location = useLocation(); + + React.useEffect(() => { + document.title = `${getPageTitle(location.pathname)} | ${APP_TITLE}`; + }, [location.pathname]); + + return null; +} + +function PopupFrame({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function RootRedirect() { + const { authState } = useExtensionAuth(); + + if (!authState.hasOnboarded) { + return ; + } + + return ; +} + +function ProtectedLayout() { + return ( +
+
+ +
+ +
+ ); +} + +function PageScaffold({ + eyebrow, + title, + description, + children, + backTo, + rightAction, +}: { + eyebrow?: string; + title: string; + description: string; + children: React.ReactNode; + backTo?: string; + rightAction?: React.ReactNode; +}) { + const navigate = useNavigate(); + + return ( +
+
+
+ {backTo ? ( + + ) : ( + + )} + {rightAction ?? } +
+ {eyebrow ? ( +

+ {eyebrow} +

+ ) : null} +

{title}

+

{description}

+
+
{children}
+
+ ); +} + +function Card({ + title, + description, + children, +}: { + title: string; + description?: string; + children?: React.ReactNode; +}) { + return ( +
+

{title}

+ {description ?

{description}

: null} + {children ?
{children}
: null} +
+ ); +} + +function PrimaryButton({ + className, + type = 'button', + ...props +}: React.ButtonHTMLAttributes) { + return ( + + + ); +} + +function HomeScreen() { + const { authState, lockWallet } = useExtensionAuth(); + + return ( + + + + } + > + +
+

Available balance

+

1,245.80 XLM

+

{authState.accountAddress}

+
+
+
+ Send funds + Receive funds + View history + Session keys +
+
+ ); +} + +function SendScreen() { + return ( + + +
+ + + Review transaction +
+
+
+ ); +} + +function ReceiveScreen() { + return ( + + +
+ GCFX...WALLET +
+
+ + +
+
+
+ ); +} + +function HistoryScreen() { + const entries = [ + { id: '1', label: 'Received from Treasury', amount: '+320 XLM', date: 'Today' }, + { id: '2', label: 'Sent to Merchant', amount: '-48 XLM', date: 'Yesterday' }, + { id: '3', label: 'Session key refresh', amount: 'Security event', date: 'Mar 23' }, + ]; + + return ( + + +
+ {entries.map((entry) => ( +
+
+

{entry.label}

+

{entry.date}

+
+ {entry.amount} +
+ ))} +
+
+
+ ); +} + +function SessionKeysScreen() { + return ( + + +
+
+

Trading bot

+

Valid for 12 more hours

+
+
+

Automation script

+

Read-only access, expires tomorrow

+
+
+
+ + + Add session key + +
+ ); +} + +function NotFoundScreen() { + const { authState } = useExtensionAuth(); + const fallbackPath = !authState.hasOnboarded + ? '/welcome' + : authState.isUnlocked + ? '/home' + : '/unlock'; + + return ( + + + Go back to safety + + + ); +} + +export function ExtensionRouterContent() { + return ( + + + + } path="/" /> + + + + } + path="/welcome" + /> + + + + } + path="/create-account" + /> + + + + } + path="/unlock" + /> + }> + }> + } path="/home" /> + } path="/send" /> + } path="/receive" /> + } path="/history" /> + } path="/settings" /> + } path="/session-keys" /> + + + } path="*" /> + + + ); +} + +export function ExtensionRouter() { + return ( + + + + + + + + ); +} + +export function ExtensionRouterTestHarness({ initialEntries }: { initialEntries: string[] }) { + return ( + + + + + + + + ); +} diff --git a/apps/extension-wallet/src/screens/ReceiveScreen.tsx b/apps/extension-wallet/src/screens/ReceiveScreen.tsx index f0a443b..170e6d0 100644 --- a/apps/extension-wallet/src/screens/ReceiveScreen.tsx +++ b/apps/extension-wallet/src/screens/ReceiveScreen.tsx @@ -68,19 +68,11 @@ export function ReceiveScreen({ }, []); return ( - + {/* ── Header ─────────────────────────────────────────────────── */}
- Receive @@ -116,12 +108,7 @@ export function ReceiveScreen({ {account.name}

)} - +
{/* Print button */} diff --git a/apps/extension-wallet/src/screens/Settings/SecuritySettings.tsx b/apps/extension-wallet/src/screens/Settings/SecuritySettings.tsx index c2361dd..580d7b2 100644 --- a/apps/extension-wallet/src/screens/Settings/SecuritySettings.tsx +++ b/apps/extension-wallet/src/screens/Settings/SecuritySettings.tsx @@ -70,8 +70,8 @@ function ChangePasswordView({ onDone }: { onDone: () => void }) { type="password" placeholder="Enter current password" value={form.current} - onChange={(e: React.ChangeEvent) => - setForm((f) => ({ ...f, current: e.target.value })) + onChange={(event: React.ChangeEvent) => + setForm((formState) => ({ ...formState, current: event.target.value })) } />
@@ -84,8 +84,8 @@ function ChangePasswordView({ onDone }: { onDone: () => void }) { type={showNext ? 'text' : 'password'} placeholder="Min. 8 characters" value={form.next} - onChange={(e: React.ChangeEvent) => - setForm((f) => ({ ...f, next: e.target.value })) + onChange={(event: React.ChangeEvent) => + setForm((formState) => ({ ...formState, next: event.target.value })) } className="pr-10" /> @@ -106,8 +106,8 @@ function ChangePasswordView({ onDone }: { onDone: () => void }) { type="password" placeholder="Repeat new password" value={form.confirm} - onChange={(e: React.ChangeEvent) => - setForm((f) => ({ ...f, confirm: e.target.value })) + onChange={(event: React.ChangeEvent) => + setForm((formState) => ({ ...formState, confirm: event.target.value })) } /> @@ -259,7 +259,7 @@ function ExportWarningView({ type="password" placeholder="Enter password to continue" value={password} - onChange={(e: React.ChangeEvent) => setPassword(e.target.value)} + onChange={(event: React.ChangeEvent) => setPassword(event.target.value)} /> {error &&

{error}

} diff --git a/apps/extension-wallet/src/screens/__tests__/ReceiveScreen.test.tsx b/apps/extension-wallet/src/screens/__tests__/ReceiveScreen.test.tsx index 838e528..f4d285f 100644 --- a/apps/extension-wallet/src/screens/__tests__/ReceiveScreen.test.tsx +++ b/apps/extension-wallet/src/screens/__tests__/ReceiveScreen.test.tsx @@ -1,9 +1,7 @@ -import * as React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { vi } from 'vitest'; - vi.mock('qrcode.react', () => ({ QRCodeSVG: ({ value, 'aria-label': ariaLabel }: { value: string; 'aria-label'?: string }) => ( @@ -12,7 +10,6 @@ vi.mock('qrcode.react', () => ({ import { ReceiveScreen } from '@/screens/ReceiveScreen'; - const MAINNET_ACCOUNT = { publicKey: 'GABC1234567890DEFGHIJKLMNOPQRSTUVWXYZ', name: 'Primary Wallet', @@ -27,9 +24,7 @@ describe('ReceiveScreen', () => { describe('rendering', () => { it('renders the screen title', () => { render(); - expect( - screen.getByRole('heading', { name: /receive/i }) - ).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: /receive/i })).toBeInTheDocument(); }); it('renders the QR code with the correct address value', () => { @@ -73,9 +68,7 @@ describe('ReceiveScreen', () => { }); it('shows "Futurenet" badge when network is futurenet', () => { - render( - - ); + render(); expect(screen.getByText('Futurenet')).toBeInTheDocument(); }); }); @@ -117,9 +110,7 @@ describe('ReceiveScreen', () => { await waitFor(() => { // After copying the button aria-label changes inside AddressDisplay // The icon swaps to a check – verify the button is still in the DOM - expect( - screen.getByRole('button', { name: /copy address/i }) - ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /copy address/i })).toBeInTheDocument(); }); }); }); @@ -127,14 +118,12 @@ describe('ReceiveScreen', () => { describe('print', () => { it('renders a print button', () => { render(); - expect( - screen.getByRole('button', { name: /print qr code/i }) - ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /print qr code/i })).toBeInTheDocument(); }); it('calls window.print when the print button is clicked', async () => { const user = userEvent.setup(); - const printSpy = vi.spyOn(window, 'print').mockImplementation(() => { }); + const printSpy = vi.spyOn(window, 'print').mockImplementation(() => {}); render(); await user.click(screen.getByRole('button', { name: /print qr code/i })); @@ -146,13 +135,9 @@ describe('ReceiveScreen', () => { describe('QR generation', () => { it('encodes exactly the publicKey in the QR code', () => { - const publicKey = - 'GD6SZQJNKL3ZYXPWLUVFXZNXUVXJTQPWMQHZMDMQHLS5VNLQBQNPFLM'; + const publicKey = 'GD6SZQJNKL3ZYXPWLUVFXZNXUVXJTQPWMQHZMDMQHLS5VNLQBQNPFLM'; render(); - expect(screen.getByTestId('qr-code-svg')).toHaveAttribute( - 'data-value', - publicKey - ); + expect(screen.getByTestId('qr-code-svg')).toHaveAttribute('data-value', publicKey); }); it('renders a different QR value when account changes', () => { diff --git a/apps/extension-wallet/tailwind.config.js b/apps/extension-wallet/tailwind.config.js index d0de3d5..f2ff762 100644 --- a/apps/extension-wallet/tailwind.config.js +++ b/apps/extension-wallet/tailwind.config.js @@ -1,5 +1,7 @@ +import tailwindcssAnimate from 'tailwindcss-animate'; + /** @type {import('tailwindcss').Config} */ -export default { +const config = { darkMode: ['class'], content: ['./index.html', './src/**/*.{ts,tsx}', '../../packages/ui-kit/src/**/*.{ts,tsx}'], theme: { @@ -42,5 +44,7 @@ export default { }, }, }, - plugins: [require('tailwindcss-animate')], -}; \ No newline at end of file + plugins: [tailwindcssAnimate], +}; + +export default config; diff --git a/apps/extension-wallet/vite.config.ts b/apps/extension-wallet/vite.config.ts index 14729eb..b38d17f 100644 --- a/apps/extension-wallet/vite.config.ts +++ b/apps/extension-wallet/vite.config.ts @@ -1,15 +1,16 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; -import path from 'path'; +import { resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const rootDir = fileURLToPath(new URL('.', import.meta.url)); export default defineConfig({ plugins: [react()], resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, + alias: { '@': resolve(rootDir, 'src') }, }, css: { postcss: './postcss.config.js', }, -}); \ No newline at end of file +}); diff --git a/apps/extension-wallet/vitest.config.ts b/apps/extension-wallet/vitest.config.ts index 00957ab..4106231 100644 --- a/apps/extension-wallet/vitest.config.ts +++ b/apps/extension-wallet/vitest.config.ts @@ -1,6 +1,9 @@ import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; import path from 'path'; +import { fileURLToPath } from 'url'; + +const rootDir = fileURLToPath(new URL('.', import.meta.url)); export default defineConfig({ plugins: [react()], @@ -12,7 +15,7 @@ export default defineConfig({ }, resolve: { alias: { - '@': path.resolve(__dirname, './src'), + '@': path.resolve(rootDir, './src'), }, }, }); diff --git a/packages/account-abstraction/eslint.config.cjs b/packages/account-abstraction/eslint.config.cjs index e62e4f9..dc37024 100644 --- a/packages/account-abstraction/eslint.config.cjs +++ b/packages/account-abstraction/eslint.config.cjs @@ -6,6 +6,7 @@ const nodeGlobals = { Buffer: 'readonly', console: 'readonly', process: 'readonly', + TextEncoder: 'readonly', setTimeout: 'readonly', clearTimeout: 'readonly', setInterval: 'readonly', diff --git a/packages/core-sdk/eslint.config.cjs b/packages/core-sdk/eslint.config.cjs index bd157db..4c86b26 100644 --- a/packages/core-sdk/eslint.config.cjs +++ b/packages/core-sdk/eslint.config.cjs @@ -26,10 +26,13 @@ module.exports = [ }, globals: { Buffer: 'readonly', + BufferSource: 'readonly', TextEncoder: 'readonly', TextDecoder: 'readonly', CryptoKey: 'readonly', crypto: 'readonly', + console: 'readonly', + process: 'readonly', }, }, plugins: { diff --git a/packages/core-sdk/src/storage/__tests__/manager.test.ts b/packages/core-sdk/src/storage/__tests__/manager.test.ts index df00aef..766e153 100644 --- a/packages/core-sdk/src/storage/__tests__/manager.test.ts +++ b/packages/core-sdk/src/storage/__tests__/manager.test.ts @@ -1,7 +1,8 @@ import { webcrypto } from 'crypto'; +import type { EncryptedPayload, StorageAdapter } from '../types'; if (!globalThis.crypto) { - // @ts-ignore + // @ts-expect-error Node test environment does not expose writable crypto typing. globalThis.crypto = webcrypto; } if (!globalThis.btoa) { @@ -12,16 +13,16 @@ if (!globalThis.atob) { } import { SecureStorageManager } from '../secure-storage-manager'; -import { StorageAdapter, AccountData, SessionKeysData } from '../types'; +import type { AccountData, SessionKeysData } from '../types'; class MockStorageAdapter implements StorageAdapter { - private store: Map = new Map(); + private store = new Map(); - async get(key: string): Promise { - return this.store.get(key) || null; + async get(key: string): Promise { + return (this.store.get(key) as T | undefined) ?? null; } - async set(key: string, value: any): Promise { + async set(key: string, value: T): Promise { this.store.set(key, value); } @@ -29,7 +30,7 @@ class MockStorageAdapter implements StorageAdapter { this.store.delete(key); } - public inspectStore(): Map { + public inspectStore(): Map { return this.store; } } @@ -51,7 +52,7 @@ describe('SecureStorageManager', () => { await manager.unlock(password); await manager.saveAccount(accountData); - const storedData = await storage.get('account'); + const storedData = await storage.get('account'); expect(storedData).toBeDefined(); // Ensure it's not plaintext diff --git a/packages/core-sdk/src/storage/secure-storage-manager.ts b/packages/core-sdk/src/storage/secure-storage-manager.ts index ad2b210..d93bf92 100644 --- a/packages/core-sdk/src/storage/secure-storage-manager.ts +++ b/packages/core-sdk/src/storage/secure-storage-manager.ts @@ -1,7 +1,10 @@ import { AccountData, EncryptedPayload, SessionKeysData, StorageAdapter } from './types'; -function bufferToBase64(buffer: ArrayBuffer): string { - const bytes = new Uint8Array(buffer); +function bufferToBase64(buffer: BufferSource): string { + const bytes = + buffer instanceof ArrayBuffer + ? new Uint8Array(buffer) + : new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); @@ -53,12 +56,12 @@ export class SecureStorageManager { return this.baseKey !== null; } - private async deriveAesKey(salt: Uint8Array | any): Promise { + private async deriveAesKey(salt: BufferSource): Promise { if (!this.baseKey) throw new Error('Storage manager is locked'); return globalThis.crypto.subtle.deriveKey( { name: 'PBKDF2', - salt: salt as any, + salt, iterations: 100000, hash: 'SHA-256', }, @@ -70,8 +73,8 @@ export class SecureStorageManager { } private async encryptData(plaintext: string): Promise { - const salt = globalThis.crypto.getRandomValues(new Uint8Array(16) as any); - const iv = globalThis.crypto.getRandomValues(new Uint8Array(12) as any); + const salt = globalThis.crypto.getRandomValues(new Uint8Array(16)); + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); const aesKey = await this.deriveAesKey(salt); const encoder = new TextEncoder(); @@ -94,16 +97,16 @@ export class SecureStorageManager { const iv = base64ToBuffer(payload.iv); const ciphertext = base64ToBuffer(payload.data); - const aesKey = await this.deriveAesKey(new Uint8Array(salt) as any); + const aesKey = await this.deriveAesKey(new Uint8Array(salt)); try { const decryptedBuffer = await globalThis.crypto.subtle.decrypt( - { name: 'AES-GCM', iv: new Uint8Array(iv) as any }, + { name: 'AES-GCM', iv: new Uint8Array(iv) }, aesKey, - ciphertext as any + ciphertext ); return new TextDecoder().decode(decryptedBuffer); - } catch (error: any) { + } catch { throw new Error('Invalid password or corrupted data'); } } @@ -114,7 +117,7 @@ export class SecureStorageManager { } public async getAccount(): Promise { - const payload = (await this.storage.get('account')) as EncryptedPayload | null; + const payload = await this.storage.get('account'); if (!payload) return null; const json = await this.decryptData(payload); return JSON.parse(json); @@ -126,7 +129,7 @@ export class SecureStorageManager { } public async getSessionKeys(): Promise { - const payload = (await this.storage.get('sessionKeys')) as EncryptedPayload | null; + const payload = await this.storage.get('sessionKeys'); if (!payload) return null; const json = await this.decryptData(payload); return JSON.parse(json); diff --git a/packages/core-sdk/src/storage/types.ts b/packages/core-sdk/src/storage/types.ts index d34bdc2..cd55af7 100644 --- a/packages/core-sdk/src/storage/types.ts +++ b/packages/core-sdk/src/storage/types.ts @@ -8,17 +8,17 @@ export interface EncryptedPayload { } export interface StorageAdapter { - get(key: string): Promise; - set(key: string, value: any): Promise; + get(key: string): Promise; + set(key: string, value: T): Promise; remove(key: string): Promise; } export interface AccountData { privateKey: string; - [key: string]: any; + [key: string]: unknown; } export interface SessionKeysData { keys: Record; - [key: string]: any; + [key: string]: unknown; } diff --git a/packages/crypto/eslint.config.cjs b/packages/crypto/eslint.config.cjs index 8f1b1a7..18e4431 100644 --- a/packages/crypto/eslint.config.cjs +++ b/packages/crypto/eslint.config.cjs @@ -24,6 +24,14 @@ module.exports = [ ecmaVersion: 2020, sourceType: 'module', }, + globals: { + Buffer: 'readonly', + Crypto: 'readonly', + CryptoKey: 'readonly', + TextEncoder: 'readonly', + TextDecoder: 'readonly', + crypto: 'readonly', + }, }, plugins: { '@typescript-eslint': tseslint, diff --git a/packages/crypto/src/__tests__/encryption-roundtrip.test.ts b/packages/crypto/src/__tests__/encryption-roundtrip.test.ts index 19c1708..5b764d8 100644 --- a/packages/crypto/src/__tests__/encryption-roundtrip.test.ts +++ b/packages/crypto/src/__tests__/encryption-roundtrip.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from '@jest/globals'; -import { - decryptSecretKey, - encryptSecretKey, -} from '../encryption'; +import { decryptSecretKey, encryptSecretKey } from '../encryption'; describe('encryptSecretKey()/decryptSecretKey() round-trip', () => { it('decrypts encrypted payload back to original secret key', async () => { diff --git a/packages/crypto/src/encryption.ts b/packages/crypto/src/encryption.ts index b9f4cb7..1c86086 100644 --- a/packages/crypto/src/encryption.ts +++ b/packages/crypto/src/encryption.ts @@ -1,4 +1,6 @@ import { Buffer } from 'node:buffer'; +import { webcrypto } from 'node:crypto'; +import { TextDecoder, TextEncoder } from 'node:util'; const PBKDF2_ITERATIONS = 100000; const MAX_PBKDF2_ITERATIONS = 600000; @@ -16,7 +18,7 @@ export interface EncryptedSecretKeyPayload { ciphertext: string; } -function getCrypto(): Crypto { +function getCrypto(): webcrypto.Crypto { if (!globalThis.crypto?.subtle) { throw new Error('WebCrypto API is not available in this environment.'); } @@ -37,7 +39,7 @@ async function deriveEncryptionKey( password: string, salt: Uint8Array, iterations: number -): Promise { +): Promise { const cryptoApi = getCrypto(); const passwordKey = await cryptoApi.subtle.importKey( 'raw', @@ -162,11 +164,7 @@ export async function decryptSecretKey( const salt = fromBase64(validatedPayload.salt); const iv = fromBase64(validatedPayload.iv); const ciphertext = fromBase64(validatedPayload.ciphertext); - const encryptionKey = await deriveEncryptionKey( - password, - salt, - validatedPayload.iterations - ); + const encryptionKey = await deriveEncryptionKey(password, salt, validatedPayload.iterations); const plaintext = await cryptoApi.subtle.decrypt( { diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 7370d23..31ab6c6 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -8,8 +8,5 @@ export const CRYPTO_VERSION = '0.1.0'; export { verifySignature } from './signing'; export { validatePasswordStrength } from './password'; -export { - encryptSecretKey, - decryptSecretKey, -} from './encryption'; +export { encryptSecretKey, decryptSecretKey } from './encryption'; export type { EncryptedSecretKeyPayload } from './encryption'; diff --git a/packages/ui-kit/eslint.config.cjs b/packages/ui-kit/eslint.config.cjs index 74b6aad..e9a2960 100644 --- a/packages/ui-kit/eslint.config.cjs +++ b/packages/ui-kit/eslint.config.cjs @@ -32,6 +32,7 @@ module.exports = [ ...react.configs.recommended.rules, ...reactHooks.configs.recommended.rules, 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], '@typescript-eslint/no-empty-object-type': 'off', }, @@ -60,6 +61,7 @@ module.exports = [ afterAll: 'readonly', afterEach: 'readonly', jest: 'readonly', + vi: 'readonly', }, }, }, diff --git a/packages/ui-kit/src/__tests__/Form/validation.test.ts b/packages/ui-kit/src/__tests__/Form/validation.test.ts index e8641f2..2a050b5 100644 --- a/packages/ui-kit/src/__tests__/Form/validation.test.ts +++ b/packages/ui-kit/src/__tests__/Form/validation.test.ts @@ -22,7 +22,9 @@ describe('isStellarAddress', () => { }); it('returns false for an address that does not start with G', () => { - expect(isStellarAddress('SDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37')).toBe(false); + expect(isStellarAddress('SDQP2KPQGKIHYJGXNUIYOMHARUARCA7DJT5FO2FFOOKY3B2WSQHG4W37')).toBe( + false + ); }); it('returns false for an address that is too short', () => { diff --git a/packages/ui-kit/src/components/Form/AddressInput.tsx b/packages/ui-kit/src/components/Form/AddressInput.tsx index a2060a5..a2e096b 100644 --- a/packages/ui-kit/src/components/Form/AddressInput.tsx +++ b/packages/ui-kit/src/components/Form/AddressInput.tsx @@ -10,7 +10,6 @@ import { cn } from '@/lib/utils'; /** Safely attempt to read the react-hook-form context without throwing. */ function useOptionalFormContext() { try { - // eslint-disable-next-line react-hooks/rules-of-hooks return useFormContext(); } catch { return null; @@ -21,8 +20,10 @@ function useOptionalFormContext() { // AddressInputBase – the pure presentational layer // --------------------------------------------------------------------------- -export interface AddressInputBaseProps - extends Omit, 'type'> { +export interface AddressInputBaseProps extends Omit< + React.InputHTMLAttributes, + 'type' +> { /** Field label */ label?: string; /** Validation error message */ @@ -58,11 +59,7 @@ const AddressInputBase = React.forwardRef {error && ( - )} diff --git a/packages/ui-kit/src/components/Form/AmountInput.tsx b/packages/ui-kit/src/components/Form/AmountInput.tsx index f4ff0ae..3131803 100644 --- a/packages/ui-kit/src/components/Form/AmountInput.tsx +++ b/packages/ui-kit/src/components/Form/AmountInput.tsx @@ -10,7 +10,6 @@ import { cn } from '@/lib/utils'; function useOptionalFormContext() { try { - // eslint-disable-next-line react-hooks/rules-of-hooks return useFormContext(); } catch { return null; @@ -21,8 +20,10 @@ function useOptionalFormContext() { // AmountInputBase – pure presentational layer // --------------------------------------------------------------------------- -export interface AmountInputBaseProps - extends Omit, 'type'> { +export interface AmountInputBaseProps extends Omit< + React.InputHTMLAttributes, + 'type' +> { /** Field label */ label?: string; /** Validation or runtime error message */ @@ -37,17 +38,7 @@ export interface AmountInputBaseProps const AmountInputBase = React.forwardRef( ( - { - label = 'Amount', - error, - balance, - asset = 'XLM', - onMax, - className, - id, - onChange, - ...props - }, + { label = 'Amount', error, balance, asset = 'XLM', onMax, className, id, onChange, ...props }, ref ) => { const inputId = id ?? label.toLowerCase().replace(/\s+/g, '-'); diff --git a/packages/ui-kit/src/components/Form/Form.stories.tsx b/packages/ui-kit/src/components/Form/Form.stories.tsx index e1e749e..077fb8a 100644 --- a/packages/ui-kit/src/components/Form/Form.stories.tsx +++ b/packages/ui-kit/src/components/Form/Form.stories.tsx @@ -50,22 +50,9 @@ export const SendTransaction: Story = { return (
- - - + + + Send Transaction diff --git a/packages/ui-kit/src/components/Form/Form.tsx b/packages/ui-kit/src/components/Form/Form.tsx index f8229d7..b3e5eb1 100644 --- a/packages/ui-kit/src/components/Form/Form.tsx +++ b/packages/ui-kit/src/components/Form/Form.tsx @@ -42,8 +42,7 @@ FormError.displayName = 'FormError'; // FormSubmit – submit button that reads loading state from form context // --------------------------------------------------------------------------- -export interface FormSubmitProps - extends React.ButtonHTMLAttributes { +export interface FormSubmitProps extends React.ButtonHTMLAttributes { /** Override the auto-derived isSubmitting state */ loading?: boolean; children: React.ReactNode; diff --git a/packages/ui-kit/src/components/Form/PasswordInput.tsx b/packages/ui-kit/src/components/Form/PasswordInput.tsx index d081aca..2cb5842 100644 --- a/packages/ui-kit/src/components/Form/PasswordInput.tsx +++ b/packages/ui-kit/src/components/Form/PasswordInput.tsx @@ -11,7 +11,6 @@ import { getPasswordStrength } from './validation'; function useOptionalFormContext() { try { - // eslint-disable-next-line react-hooks/rules-of-hooks return useFormContext(); } catch { return null; @@ -50,9 +49,7 @@ function StrengthMeter({ password, id }: StrengthMeterProps) {
{/* Label */} -

- {strength.label} -

+

{strength.label}

); } @@ -61,8 +58,10 @@ function StrengthMeter({ password, id }: StrengthMeterProps) { // PasswordInputBase – pure presentational layer // --------------------------------------------------------------------------- -export interface PasswordInputBaseProps - extends Omit, 'type'> { +export interface PasswordInputBaseProps extends Omit< + React.InputHTMLAttributes, + 'type' +> { /** Field label */ label?: string; /** Validation error message */ @@ -101,10 +100,7 @@ const PasswordInputBase = React.forwardRef @@ -114,11 +110,7 @@ const PasswordInputBase = React.forwardRef - {visible ? ( - - ) : ( - - )} + {visible ? : } @@ -127,11 +119,7 @@ const PasswordInputBase = React.forwardRef + )} diff --git a/packages/ui-kit/src/components/Form/validation.ts b/packages/ui-kit/src/components/Form/validation.ts index 6deb6fe..1deb51d 100644 --- a/packages/ui-kit/src/components/Form/validation.ts +++ b/packages/ui-kit/src/components/Form/validation.ts @@ -97,7 +97,13 @@ const STRENGTH_BG_CLASSES: string[] = [ */ export function getPasswordStrength(password: string): PasswordStrength & { bgClass: string } { if (!password) { - return { score: 0, label: 'Very Weak', colorClass: STRENGTH_COLOR_CLASSES[0], bgClass: STRENGTH_BG_CLASSES[0], percent: 0 }; + return { + score: 0, + label: 'Very Weak', + colorClass: STRENGTH_COLOR_CLASSES[0], + bgClass: STRENGTH_BG_CLASSES[0], + percent: 0, + }; } let score = 0; @@ -110,7 +116,11 @@ export function getPasswordStrength(password: string): PasswordStrength & { bgCl // Penalise trivial patterns if (/^(.)\1+$/.test(password)) score = Math.max(0, score - 2); - if (/^(012|123|234|345|456|567|678|789|890|abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)/i.test(password)) { + if ( + /^(012|123|234|345|456|567|678|789|890|abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz)/i.test( + password + ) + ) { score = Math.max(0, score - 1); } diff --git a/packages/ui-kit/src/components/ui/input.tsx b/packages/ui-kit/src/components/ui/input.tsx index e5acb7c..7f269d4 100644 --- a/packages/ui-kit/src/components/ui/input.tsx +++ b/packages/ui-kit/src/components/ui/input.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; -export interface InputProps extends React.InputHTMLAttributes {} +export type InputProps = React.InputHTMLAttributes; const Input = React.forwardRef( ({ className, type, ...props }, ref) => { diff --git a/packages/ui-kit/src/index.ts b/packages/ui-kit/src/index.ts index 277fc94..e45f9d5 100644 --- a/packages/ui-kit/src/index.ts +++ b/packages/ui-kit/src/index.ts @@ -40,10 +40,7 @@ export type { FormProps, FormSubmitProps, FormErrorProps } from './components/Fo export { AddressInput, AddressInputBase } from './components/Form/AddressInput'; export type { AddressInputProps, AddressInputBaseProps } from './components/Form/AddressInput'; -export { - AmountInput as FormAmountInput, - AmountInputBase, -} from './components/Form/AmountInput'; +export { AmountInput as FormAmountInput, AmountInputBase } from './components/Form/AmountInput'; export type { AmountInputProps as FormAmountInputProps, AmountInputBaseProps, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 034338c..c489c4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,9 @@ importers: react-error-boundary: specifier: ^6.1.1 version: 6.1.1(react@18.3.1) + react-router-dom: + specifier: ^6.20.0 + version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@eslint/js': specifier: ^9.0.0 @@ -82,10 +85,10 @@ importers: version: 18.3.7(@types/react@18.3.28) '@typescript-eslint/eslint-plugin': specifier: ^8.0.0 - version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3))(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^8.0.0 - version: 8.56.1(eslint@10.1.0(jiti@1.21.7))(typescript@5.9.3) + version: 8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) '@vitejs/plugin-react': specifier: ^4.2.0 version: 4.7.0(vite@5.4.21(@types/node@25.3.1)) @@ -93,14 +96,14 @@ importers: specifier: ^10.4.0 version: 10.4.27(postcss@8.5.6) eslint: - specifier: ^10.1.0 - version: 10.1.0(jiti@1.21.7) + specifier: ^9.0.0 + version: 9.39.3(jiti@1.21.7) eslint-plugin-react: specifier: ^7.37.0 - version: 7.37.5(eslint@10.1.0(jiti@1.21.7)) + version: 7.37.5(eslint@9.39.3(jiti@1.21.7)) eslint-plugin-react-hooks: specifier: ^5.0.0 - version: 5.2.0(eslint@10.1.0(jiti@1.21.7)) + version: 5.2.0(eslint@9.39.3(jiti@1.21.7)) globals: specifier: ^17.4.0 version: 17.4.0 @@ -119,36 +122,6 @@ importers: typescript: specifier: ^5.6.0 version: 5.9.3 - vite: - specifier: ^5.0.0 - version: 5.4.21(@types/node@25.3.1) - '@vitejs/plugin-react': - specifier: ^4.2.0 - version: 4.7.0(vite@5.4.21(@types/node@25.3.1)) - autoprefixer: - specifier: ^10.4.0 - version: 10.4.27(postcss@8.5.6) - eslint: - specifier: ^9.0.0 - version: 9.39.3(jiti@1.21.7) - eslint-plugin-react: - specifier: ^7.34.0 - version: 7.37.5(eslint@9.39.3(jiti@1.21.7)) - eslint-plugin-react-hooks: - specifier: ^4.6.0 - version: 4.6.2(eslint@9.39.3(jiti@1.21.7)) - globals: - specifier: ^15.0.0 - version: 15.15.0 - jsdom: - specifier: ^23.0.0 - version: 23.2.0 - tailwindcss: - specifier: ^3.4.0 - version: 3.4.19(yaml@2.8.2) - typescript: - specifier: ^5.6.0 - version: 5.9.3 vite: specifier: ^5.4.0 version: 5.4.21(@types/node@25.3.1) @@ -367,6 +340,9 @@ importers: packages/ui-kit: dependencies: + '@hookform/resolvers': + specifier: ^3.3.0 + version: 3.10.0(react-hook-form@7.72.0(react@18.3.1)) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.2.4(@types/react@18.3.28)(react@18.3.1) @@ -379,9 +355,15 @@ importers: lucide-react: specifier: ^0.344.0 version: 0.344.0(react@18.3.1) + react-hook-form: + specifier: ^7.49.0 + version: 7.72.0(react@18.3.1) tailwind-merge: specifier: ^2.2.0 version: 2.6.1 + zod: + specifier: ^3.22.0 + version: 3.25.76 devDependencies: '@eslint/js': specifier: ^9.0.0 @@ -1342,26 +1324,14 @@ packages: resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-array@0.23.3': - resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.4.2': resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.5.3': - resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@0.17.0': resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@1.1.1': - resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.4': resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1374,18 +1344,10 @@ packages: resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@3.0.3': - resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.4.1': resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.6.1': - resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@fal-works/esbuild-plugin-global-externals@2.1.2': resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} @@ -1404,6 +1366,11 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@hookform/resolvers@3.10.0': + resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} + peerDependencies: + react-hook-form: ^7.0.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2237,6 +2204,10 @@ packages: '@radix-ui/rect@1.0.1': resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} + '@remix-run/router@1.23.2': + resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} + engines: {node: '>=14.0.0'} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -2686,9 +2657,6 @@ packages: '@types/escodegen@0.0.6': resolution: {integrity: sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==} - '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - '@types/estree@0.0.51': resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} @@ -2818,17 +2786,6 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@7.18.0': - resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/eslint-plugin@8.56.1': resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2837,16 +2794,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@7.18.0': - resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/parser@8.56.1': resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2860,10 +2807,6 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@7.18.0': - resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} - engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/scope-manager@8.56.1': resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2874,16 +2817,6 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@7.18.0': - resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/type-utils@8.56.1': resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2891,35 +2824,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@7.18.0': - resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} - engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/types@8.56.1': resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@7.18.0': - resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/typescript-estree@8.56.1': resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@7.18.0': - resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - '@typescript-eslint/utils@8.56.1': resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2927,10 +2841,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@7.18.0': - resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} - engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/visitor-keys@8.56.1': resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4001,12 +3911,6 @@ packages: engines: {node: '>=6.0'} hasBin: true - eslint-plugin-react-hooks@4.6.2: - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - eslint-plugin-react-hooks@5.2.0: resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} engines: {node: '>=10'} @@ -4023,10 +3927,6 @@ packages: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4039,16 +3939,6 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.1.0: - resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - eslint@9.39.3: resolution: {integrity: sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4063,10 +3953,6 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -4394,8 +4280,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@15.15.0: - resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} engines: {node: '>=18'} globalthis@1.0.4: @@ -4417,9 +4303,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - gunzip-maybe@1.4.2: resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} hasBin: true @@ -5930,6 +5813,12 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + react-hook-form@7.72.0: + resolution: {integrity: sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -5970,6 +5859,19 @@ packages: '@types/react': optional: true + react-router-dom@6.30.3: + resolution: {integrity: sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.3: + resolution: {integrity: sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -6571,12 +6473,6 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -8066,11 +7962,6 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@1.21.7))': - dependencies: - eslint: 10.1.0(jiti@1.21.7) - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@1.21.7))': dependencies: eslint: 9.39.3(jiti@1.21.7) @@ -8086,30 +7977,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-array@0.23.3': - dependencies: - '@eslint/object-schema': 3.0.3 - debug: 4.4.3 - minimatch: 10.2.4 - transitivePeerDependencies: - - supports-color - '@eslint/config-helpers@0.4.2': dependencies: '@eslint/core': 0.17.0 - '@eslint/config-helpers@0.5.3': - dependencies: - '@eslint/core': 1.1.1 - '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/core@1.1.1': - dependencies: - '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.4': dependencies: ajv: 6.14.0 @@ -8128,18 +8003,11 @@ snapshots: '@eslint/object-schema@2.1.7': {} - '@eslint/object-schema@3.0.3': {} - '@eslint/plugin-kit@0.4.1': dependencies: '@eslint/core': 0.17.0 levn: 0.4.1 - '@eslint/plugin-kit@0.6.1': - dependencies: - '@eslint/core': 1.1.1 - levn: 0.4.1 - '@fal-works/esbuild-plugin-global-externals@2.1.2': {} '@floating-ui/core@1.7.4': @@ -8159,6 +8027,10 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@hookform/resolvers@3.10.0(react-hook-form@7.72.0(react@18.3.1))': + dependencies: + react-hook-form: 7.72.0(react@18.3.1) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -9145,6 +9017,8 @@ snapshots: dependencies: '@babel/runtime': 7.28.6 + '@remix-run/router@1.23.2': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/pluginutils@5.3.0(rollup@4.59.0)': @@ -9984,8 +9858,6 @@ snapshots: '@types/escodegen@0.0.6': {} - '@types/esrecurse@4.3.1': {} - '@types/estree@0.0.51': {} '@types/estree@1.0.8': {} @@ -10126,24 +9998,6 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.39.3(jiti@1.21.7) - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -10160,19 +10014,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.3 - eslint: 9.39.3(jiti@1.21.7) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.1 @@ -10194,11 +10035,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@7.18.0': - dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - '@typescript-eslint/scope-manager@8.56.1': dependencies: '@typescript-eslint/types': 8.56.1 @@ -10208,18 +10044,6 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@7.18.0(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3) - debug: 4.4.3 - eslint: 9.39.3(jiti@1.21.7) - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.1 @@ -10232,25 +10056,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@7.18.0': {} - '@typescript-eslint/types@8.56.1': {} - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.3 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.8 - semver: 7.7.4 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) @@ -10266,17 +10073,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3) - eslint: 9.39.3(jiti@1.21.7) - transitivePeerDependencies: - - supports-color - - typescript - '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@1.21.7)) @@ -10288,11 +10084,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@7.18.0': - dependencies: - '@typescript-eslint/types': 7.18.0 - eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.56.1': dependencies: '@typescript-eslint/types': 8.56.1 @@ -11528,36 +11319,10 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-plugin-react-hooks@5.2.0(eslint@10.1.0(jiti@1.21.7)): - dependencies: - eslint: 10.1.0(jiti@1.21.7) - eslint-plugin-react-hooks@5.2.0(eslint@9.39.3(jiti@1.21.7)): dependencies: eslint: 9.39.3(jiti@1.21.7) - eslint-plugin-react@7.37.5(eslint@10.1.0(jiti@1.21.7)): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.2 - eslint: 10.1.0(jiti@1.21.7) - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.5 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.6 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - eslint-plugin-react@7.37.5(eslint@9.39.3(jiti@1.21.7)): dependencies: array-includes: 3.1.9 @@ -11585,56 +11350,12 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-scope@9.1.2: - dependencies: - '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 - esrecurse: 4.3.0 - estraverse: 5.3.0 - eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@4.2.1: {} eslint-visitor-keys@5.0.1: {} - eslint@10.1.0(jiti@1.21.7): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@1.21.7)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.3 - '@eslint/config-helpers': 0.5.3 - '@eslint/core': 1.1.1 - '@eslint/plugin-kit': 0.6.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.14.0 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 9.1.2 - eslint-visitor-keys: 5.0.1 - espree: 11.2.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.4 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 1.21.7 - transitivePeerDependencies: - - supports-color - eslint@9.39.3(jiti@1.21.7): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@1.21.7)) @@ -11682,12 +11403,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 - espree@11.2.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 5.0.1 - esprima@4.0.1: {} esquery@1.7.0: @@ -12107,8 +11822,6 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - gunzip-maybe@1.4.2: dependencies: browserify-zlib: 0.1.4 @@ -14080,6 +13793,10 @@ snapshots: dependencies: react: 18.3.1 + react-hook-form@7.72.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -14111,6 +13828,18 @@ snapshots: optionalDependencies: '@types/react': 18.3.28 + react-router-dom@6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.3(react@18.3.1) + + react-router@6.30.3(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.2 + react: 18.3.1 + react-style-singleton@2.2.3(@types/react@18.3.28)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -14861,10 +14590,6 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@1.4.3(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3